diff --git a/.gitignore b/.gitignore
index e95f42a..ae4cf7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
# generated types
-source/.astro
-source/node_modules
+web/.astro
+web/node_modules
.minpluto/generated
# dependencies
@@ -13,16 +13,16 @@ yarn-error.log*
pnpm-debug.log*
# environment variables
-source/.env
-source/.env.production
+web/.env
+web/.env.production
# macOS-specific files
.DS_Store
# i18n
-/source/src/pages/en/
-/source/src/pages/jp/
-/source/src/pages/ru/
+/web/src/pages/en/
+/web/src/pages/jp/
+/web/src/pages/ru/
# other
.minpluto/docker/supabase/.env
diff --git a/.minpluto/docker/supabase/.env b/.minpluto/docker/supabase/.env
deleted file mode 100755
index b675925..0000000
--- a/.minpluto/docker/supabase/.env
+++ /dev/null
@@ -1,107 +0,0 @@
-###########
-# Docker Volumes
-# All folders provided in `./supabase-volume/` must be included
-###########
-SUPABASE_ENTIRE_VOLUME="./supabase-volume/"
-
-############
-# Secrets
-# YOU MUST CHANGE THESE BEFORE GOING INTO PRODUCTION
-############
-
-POSTGRES_PASSWORD=5lgHamV44d8D1GN9LRS6b44VxREi4692
-JWT_SECRET=paxDX2xE00qFa4I1r6PKe15nIkB089I4
-ANON_KEY=9X43H6LKq3115zmZhZj95f2IJ104a603
-SERVICE_ROLE_KEY=K2G792rYBZR0kZvw9Zp6182zAwzxsdas
-DASHBOARD_USERNAME=mp_admin
-DASHBOARD_PASSWORD=ez116oqVWd4wHZUQNbgW3fA0m958FN09
-
-############
-# Database - You can change these to any PostgreSQL database that has logical replication enabled.
-############
-
-# default user is postgres
-POSTGRES_HOST=db
-POSTGRES_DB=postgres
-POSTGRES_PORT=1945
-
-############
-# API Proxy - Configuration for the Kong Reverse proxy.
-############
-
-KONG_HTTP_PORT=1942
-KONG_HTTPS_PORT=1943
-
-############
-# API - Configuration for PostgREST.
-############
-
-PGRST_DB_SCHEMAS=public,storage,graphql_public
-
-############
-# Auth - Configuration for the GoTrue authentication server.
-############
-
-## General
-SITE_URL=http://localhost:1930
-ADDITIONAL_REDIRECT_URLS=
-JWT_EXPIRY=3600
-DISABLE_SIGNUP=false
-API_EXTERNAL_URL=https://db.minpluto.org
-
-## Mailer Config
-MAILER_URLPATHS_CONFIRMATION="/auth/v1/verify"
-MAILER_URLPATHS_INVITE="/auth/v1/verify"
-MAILER_URLPATHS_RECOVERY="/auth/v1/verify"
-MAILER_URLPATHS_EMAIL_CHANGE="/auth/v1/verify"
-
-## Email auth
-ENABLE_EMAIL_SIGNUP=true
-ENABLE_EMAIL_AUTOCONFIRM=false
-SMTP_ADMIN_EMAIL=no-reply@sudovanilla.org
-SMTP_HOST=smtp.resend.com
-SMTP_PORT=587
-SMTP_USER=resend
-SMTP_PASS=re_XLbiDxHd_9Yucx4y9EwiacKgHrRowfJVU
-SMTP_SENDER_NAME=MinPluto
-ENABLE_ANONYMOUS_USERS=true
-
-## Phone auth
-ENABLE_PHONE_SIGNUP=false
-ENABLE_PHONE_AUTOCONFIRM=false
-
-############
-# Studio - Configuration for the Dashboard
-############
-
-STUDIO_DEFAULT_ORGANIZATION=Default Organization
-STUDIO_DEFAULT_PROJECT=Default Project
-
-STUDIO_PORT=1944
-SUPABASE_PUBLIC_URL=http://localhost:8000
-
-# Enable webp support
-IMGPROXY_ENABLE_WEBP_DETECTION=true
-
-############
-# Functions - Configuration for Functions
-############
-# NOTE: VERIFY_JWT applies to all functions. Per-function VERIFY_JWT is not supported yet.
-FUNCTIONS_VERIFY_JWT=false
-
-############
-# Logs - Configuration for Logflare
-# Please refer to https://supabase.com/docs/reference/self-hosting-analytics/introduction
-############
-
-LOGFLARE_LOGGER_BACKEND_API_KEY=your-super-secret-and-long-logflare-key
-
-# Change vector.toml sinks to reflect this change
-LOGFLARE_API_KEY=your-super-secret-and-long-logflare-key
-
-# Docker socket location - this value will differ depending on your OS
-DOCKER_SOCKET_LOCATION=/var/run/docker.sock
-
-# Google Cloud Project details
-GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID
-GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER
\ No newline at end of file
diff --git a/.minpluto/docs/Compatibility.md b/.minpluto/docs/Compatibility.md
index daeb7e1..34217a4 100644
--- a/.minpluto/docs/Compatibility.md
+++ b/.minpluto/docs/Compatibility.md
@@ -50,6 +50,7 @@ Cloudflare Pages | 🔘 | 🔘 |
| Mullvad | ✅ | ❌ | ✅ | ✅ | 🔘 | 🔘 |
| Tor | 🔘 | 🔘 | 🔘 | 🔘 | 🔘 | 🔘 |
| Waterfox | ✅ | ✅ | ✅ | ✅ | 🔘 | 🔘 |
+| Zen | ✅ | ✅ | ✅ | ✅ | 🔘 | 🔘 |
| **Outdated Browsers**|
| Internet Explorer | ❌ | ✅ | ❌ | ✅ | 🔘 | 🔘 |
diff --git a/.minpluto/docs/TODO.md b/.minpluto/docs/TODO.md
index e6d01b9..cbc2a24 100644
--- a/.minpluto/docs/TODO.md
+++ b/.minpluto/docs/TODO.md
@@ -1,41 +1,35 @@
## To Do
- [ ] i18n
- - [x] API
+ - [ ] API
- [ ] Languages
- - [x] English
+ - [ ] English
- [ ] Japanese
- [ ] French
- [ ] Spanish
- [ ] Russian
- - [x] Data
- - [x] Track Events (Users should be opted-out by default, OpenPanel will be used)
- - [x] Make privacy policy adaptive
- - [x] Mobile Support
- - [ ] Server Configuration (.env)
- - [ ] Quality
- - [ ] Allow 1080p
- - [ ] Allow 4K
- - [ ] Allow 8K
- - [ ] Account System (Based on [Account System Demo](https://ark.sudovanilla.org/MinPluto/Account-System-Demo))
- - [x] Use Supabase Library
+ - [ ] Data
+ - [ ] Track Events
+ - [ ] Make privacy policy adaptive
+ - [ ] Account System
+ - [ ] Use Supabase Library
- [ ] Create Pages:
- [ ] Subscription Feed
- - [ ] History (Maybe, maybe not)
- - [x] Login
- - [x] Register
- - [x] Account
+ - [ ] History
+ - [ ] Login
+ - [ ] Register
+ - [ ] Account
- [ ] Preferences
- [ ] Delete
- [ ] Anomymous Account Creation
- - [x] Email Confirmation Code
+ - [ ] Email Confirmation Code
- [ ] Ability to:
- [ ] Update Data
- - [x] Username
+ - [ ] Username
- [ ] Email
- [ ] Pasword
- [ ] Delete Account
- [ ] API
- - [x] `/api/update/name`
+ - [ ] `/api/update/name`
- [ ] `/api/update/email`
- [ ] `/api/update/password`
- [ ] `/api/update/preference/ui/theme`
@@ -48,17 +42,17 @@
- [ ] `/api/update/preference/instance/invidious/data`
- [ ] `/api/update/preference/instance/safetwitch/media`
- [ ] `/api/update/preference/instance/safetwitch/data`
- - [x] `/api/auth/login`
- - [x] `/api/auth/register`
+ - [ ] `/api/auth/login`
+ - [ ] `/api/auth/register`
- [ ] `/api/auth/delete`
- - [x] `/api/auth/confirm`
- - [x] `/api/auth/logout`
+ - [ ] `/api/auth/confirm`
+ - [ ] `/api/auth/logout`
- [ ] `/api/anon/create`
- [ ] `/api/anon/delete`
- [ ] `/api/anon/signout`
- [ ] `/api/subscription/add`
- [ ] `/api/subscription/remove`
- - [ ] Revamp Design and Layout ([UI Library Repo](https://ark.sudovanilla.org/MinPluto/UI-Library/))
+ - [ ] Revamp Design and Layout
- [ ] Use Header over Sidebar
- [ ] Generic
- [ ] Dropdown
@@ -68,7 +62,7 @@
- [ ] Radio Buttons
- [ ] Toast
- [ ] Tooltip
- - [ ] Hovercard (For Creators) [Example](https://www.radix-vue.com/components/hover-card)
+ - [ ] Hovercard (For Creators)
- [ ] Scrollable Areas
- [ ] KBD
- [ ] Empty State
@@ -91,24 +85,24 @@
- [ ] Discovery Pages
- [ ] Animation
- [ ] Automotive
- - [x] Comedy
+ - [ ] Comedy
- [ ] Courses
- [ ] Educational
- [ ] Family Friendly
- [ ] Fashion
- [ ] Fitness
- [ ] Food
- - [x] Games
+ - [ ] Games
- [ ] Music
- [ ] News
- [ ] Podcasts
- [ ] Science
- [ ] Sports
- - [x] Tech
+ - [ ] Tech
- [ ] Web Series
- [ ] Twitch Support
- - [x] API
- - [x] Video Player HLS Support (Required to play streams)
+ - [ ] API
+ - [ ] Video Player HLS Support
- [ ] Polycentric Chat
- [ ] Categories
- [ ] Games
@@ -128,12 +122,12 @@
- [ ] Search
- [ ] Revamp Experience
- [ ] Filters
- - [x] Auto Complete
+ - [ ] Auto Complete
- [ ] Video Player
- - [x] Dash Format (1080p/4K/8K)
+ - [ ] Dash Format
- [ ] 360° Support
- [ ] Mobile Gestures
- - [x] Embed Page
+ - [ ] Embed Page
- [ ] Download
- [ ] Share
- [ ] Report
@@ -146,8 +140,6 @@
- [ ] Theater Mode
- [ ] Cast
- [ ] Video Page
- - [ ] ~~Important Infomation Card ([Example](https://img.sudovanilla.org/pXqzT10.png))~~ Controversial, do not proceed
- - [ ] Viewers Note (Like Community Notes, in [experimental phase at YouTube](https://blog.youtube/news-and-events/new-ways-to-offer-viewers-more-context/))
- [ ] Toggle:
- [ ] Audio Only
- [ ] Autoplay
@@ -168,4 +160,14 @@
- [ ] Import/Export MinPluto User Settings
- [ ] Feed Page
- [ ] Universal Feed (YouTube and Twitch)
- - [ ] Subscription Management
\ No newline at end of file
+ - [ ] Subscription Management
+ - [ ] Frontend Support (Replace official links to frontend alternatives)
+ - [ ] YouTube (Just use current instance)
+ - [ ] Twitch (Just use current instance)
+ - [ ] X
+ - [ ] Reddit
+ - [ ] Medium
+ - [ ] Quora
+ - [ ] StackOverflow
+ - [ ] Wikipedia
+ - [ ] Imgur
\ No newline at end of file
diff --git a/README.md b/README.md
index 4e54851..2d6c255 100755
--- a/README.md
+++ b/README.md
@@ -6,14 +6,14 @@ MinPluto is a modern privacy frontend for YouTube and Twitch giving your persona
___
## Docs
- - [FAQ](/.minpluto/docs/FAQ.md)
- - [API](/.minpluto/docs/API.md)
- - [Requirements](/.minpluto/docs/Requirements.md)
- - [Compatibility](/.minpluto/docs/Compatibility.md)
+ - [FAQ](./.minpluto/docs/FAQ.md)
+ - [API](./.minpluto/docs/API.md)
+ - [Requirements](./.minpluto/docs/Requirements.md)
+ - [Compatibility](./.minpluto/docs/Compatibility.md)
- Develop, Build, Run
- Selfhosting
- Player
___
-MinPluto is inspired by [Poke](https://poketube.fun/), Poke is a project by [Ashley](https://codeberg.org/ashley).
\ No newline at end of file
+MinPluto is inspired by [Poke](https://poketube.fun/), a project by [Ashley](https://codeberg.org/ashley).
\ No newline at end of file
diff --git a/bun.lockb b/bun.lockb
index 5f642a1..ccfba6c 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 0000000..66ec787
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,3 @@
+# Flurry and Zorn are on SudoVanilla Packages
+[install]
+registry = "https://npm.sudovanilla.org"
\ No newline at end of file
diff --git a/package.json b/package.json
index 4f02cfc..9ef9a23 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "minpluto",
- "version": "24.10.30",
+ "version": "24.09.07",
"description": "An open source frontend alternative to YouTube and Twitch.",
"repository": "https://ark.sudovanilla.org/MinPluto/MinPluto",
"author": "Korbs ",
@@ -18,38 +18,32 @@
"keywords": [
"privacy",
"frontend",
- "proxy",
+ "downloader",
"ytdl",
"invidious",
"safetwitch",
+ "youtube",
"twitch",
- "live",
- "stream"
+ "astro",
+ "proxy",
+ "anonymous"
],
"scripts": {
- "start": "astro dev --config ./source/astro.mjs --host",
- "translate": "astro-i18next --config ./source/astro-i18next.config.mjs generate",
- "build": "astro build --config ./source/astro.js"
+ "start": "astro dev --config ./web/astro.mjs --host",
+ "translate": "astro-i18next --config ./web/astro-i18next.config.mjs generate",
+ "build": "astro build --config ./web/astro.mjs"
},
"dependencies": {
- "@astrojs/mdx": "^3.1.8",
- "@astrojs/node": "^8.3.4",
- "@astrojs/vue": "^4.5.2",
- "@iconoir/vue": "^7.9.0",
+ "@astrojs/mdx": "^3.1.5",
+ "@astrojs/node": "^8.3.3",
+ "@astrojs/vue": "^4.5.0",
+ "@iconoir/vue": "^7.8.0",
+ "@minpluto/polestar": "^0.0.54-1",
"@minpluto/zorn": "^0.4.51",
"@nurodev/astro-bun": "^1.1.5",
- "@openpanel/sdk": "^1.0.0",
- "@supabase/supabase-js": "^2.45.6",
- "@xexiu/astro-modal": "^0.5.9",
- "astro": "^4.16.7",
- "astro-analytics": "^2.7.0",
- "astro-i18next": "^1.0.0-beta.21",
+ "@supabase/supabase-js": "^2.45.3",
+ "astro": "^4.15.4",
"astro-tooltips": "^0.6.2",
- "astro-useragent": "^4.0.2",
- "rss-to-json": "^2.1.1",
- "undici": "^6.20.1"
- },
- "devDependencies": {
- "sass": "^1.80.4"
+ "sass": "^1.78.0"
}
-}
\ No newline at end of file
+}
diff --git a/source/astro-i18next.config.mjs b/source/astro-i18next.config.mjs
deleted file mode 100755
index 90f4155..0000000
--- a/source/astro-i18next.config.mjs
+++ /dev/null
@@ -1,4 +0,0 @@
-/** @type {import('astro-i18next').AstroI18nextConfig} */
-export default {
- locales: ["en"]
-}
\ No newline at end of file
diff --git a/source/astro.mjs b/source/astro.mjs
deleted file mode 100755
index 27cc78c..0000000
--- a/source/astro.mjs
+++ /dev/null
@@ -1,49 +0,0 @@
-import { defineConfig } from 'astro/config'
-import vue from '@astrojs/vue'
-import astroI18next from "astro-i18next"
-import mdx from '@astrojs/mdx'
-import bun from "@nurodev/astro-bun";
-
-// https://astro.build/config
-export default defineConfig({
- // Project Structure
- cacheDir: './.minpluto/generated/astro/cache/',
- outDir: './.minpluto/generated/astro/dist/',
- publicDir: './source/src/public',
- root: './source',
- srcDir: './source/src',
- // Integrations and Plugins
- integrations: [mdx(), vue(), astroI18next()],
- // Security
- security: {
- checkOrigin: true
- },
- // Server Options
- server: {
- port: 1930,
- host: true
- },
- // Use Server-Side Rendering
- output: 'server',
- adapter: bun(),
- // Vite
- vite: {
- server: {
- hmr: true // Auto Reload
- }
- },
- // Experimental
- experimental: {
- directRenderScript: true,
- clientPrerender: true,
- serverIslands: true
- },
- prefetch: {
- prefetchAll: true,
- defaultStrategy: "viewport"
- },
- // Others
- devToolbar: {
- enabled: false
- }
-})
\ No newline at end of file
diff --git a/source/env.d.ts b/source/env.d.ts
deleted file mode 100755
index 8187490..0000000
--- a/source/env.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-declare namespace App {
- interface Locals {
- email: string
- }
-}
\ No newline at end of file
diff --git a/source/src/components/Chip.astro b/source/src/components/Chip.astro
deleted file mode 100755
index 004f040..0000000
--- a/source/src/components/Chip.astro
+++ /dev/null
@@ -1,17 +0,0 @@
----
-const {
- Text,
- Color
-} = Astro.props
----
-
-{Text}
-
-
\ No newline at end of file
diff --git a/source/src/components/CreatorInSidebar.astro b/source/src/components/CreatorInSidebar.astro
deleted file mode 100755
index d30b535..0000000
--- a/source/src/components/CreatorInSidebar.astro
+++ /dev/null
@@ -1,44 +0,0 @@
----
-// Properties
-const {
- ChannelId
-} = Astro.props
-
-// Configuration
-import { DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY} from '@utils/GetConfig'
-
-// Fetch
-const channel = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/channels/" + ChannelId).then((response) => response.json());
----
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/Dialog.astro b/source/src/components/Dialog.astro
deleted file mode 100755
index f0e7e55..0000000
--- a/source/src/components/Dialog.astro
+++ /dev/null
@@ -1,83 +0,0 @@
----
-import { Xmark } from "@iconoir/vue"
-
-const {
- Title,
- Description,
- Closable,
- CloseOnclick
-} = Astro.props
----
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/DiscoverChannel.astro b/source/src/components/DiscoverChannel.astro
deleted file mode 100755
index 882e795..0000000
--- a/source/src/components/DiscoverChannel.astro
+++ /dev/null
@@ -1,50 +0,0 @@
----
-// Properties
-const {
- ChannelId
-} = Astro.props
-
-// Configuration
-import { DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY} from '@utils/GetConfig'
-
-// Fetch
-const channel = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/channels/" + ChannelId).then((response) => response.json());
----
-
-
-
-
-
{channel.author}
-
-
-
\ No newline at end of file
diff --git a/source/src/components/Dropdown.astro b/source/src/components/Dropdown.astro
deleted file mode 100755
index e9052af..0000000
--- a/source/src/components/Dropdown.astro
+++ /dev/null
@@ -1,23 +0,0 @@
----
-const { Name, OnClick } = Astro.props
----
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/MusicItem.astro b/source/src/components/MusicItem.astro
deleted file mode 100755
index 8694052..0000000
--- a/source/src/components/MusicItem.astro
+++ /dev/null
@@ -1,67 +0,0 @@
----
-// Properties
-const {
- ID,
- Title,
- Creator,
- Views,
- UploadDate,
- Length
-} = Astro.props
-
-// Configuration
-import {
- DEFAULT_MEDIA_DATA_PROXY,
- DEFAULT_IMAGE_PROXY
-} from '@utils/GetConfig'
-
-// i18n
-import i18next, { t } from "i18next";
-
-// Format Published Date
-const DateFormat = new Date(UploadDate * 1000).toLocaleDateString()
-
-// Format Video Length
-// Thanks to "mingjunlu" for helping out with the time format
-const LengthFormat = new Date(Length * 1000).toISOString().slice(14, 19).split(':').map(Number).join(':')
-
-// Format Views
-const ViewsConversion = Intl.NumberFormat('en', { notation: 'compact'})
-const ViewsFormat = ViewsConversion.format(Views)
----
-
-
-
-
- {Title}
- {Creator}
- {DateFormat}
- {LengthFormat}
-
-
-
\ No newline at end of file
diff --git a/source/src/components/Search.astro b/source/src/components/Search.astro
deleted file mode 100755
index 2178c60..0000000
--- a/source/src/components/Search.astro
+++ /dev/null
@@ -1,94 +0,0 @@
----
-import { Search } from "@iconoir/vue";
-import { t } from "i18next";
----
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/VideoItem.astro b/source/src/components/VideoItem.astro
deleted file mode 100755
index 67d1f6e..0000000
--- a/source/src/components/VideoItem.astro
+++ /dev/null
@@ -1,84 +0,0 @@
----
-// Properties
-const {
- ID,
- Title,
- Creator,
- Views,
- UploadDate,
- Length
-} = Astro.props
-
-// Configuration
-import {
- DEFAULT_MEDIA_DATA_PROXY,
- DEFAULT_IMAGE_PROXY
-} from '@utils/GetConfig'
-
-// i18n
-import i18next, { t } from "i18next";
-
-// Format Published Date
-const DateFormat = new Date(UploadDate * 1000).toLocaleDateString()
-
-// Format Video Length
-// Thanks to "mingjunlu" for helping out with the time format
-const LengthFormat = null
-
-// Format Views
-const ViewsConversion = Intl.NumberFormat('en', { notation: 'compact'})
-const ViewsFormat = ViewsConversion.format(Views)
----
-
-
-
-
-
{LengthFormat}
-
-
-
{Title}
-
{t("WATCH.BY")} {Creator}
-
{ViewsFormat} {t("WATCH.VIEWS")} - {DateFormat}
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/buttons/Telemtry.astro b/source/src/components/buttons/Telemtry.astro
deleted file mode 100755
index 4bbc220..0000000
--- a/source/src/components/buttons/Telemtry.astro
+++ /dev/null
@@ -1,17 +0,0 @@
----
-import i18next,{ t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-if (Astro.cookies.get("Telemtry").value === "Enabled") {
- var UserIsOptedIn = true
-}
-else if (Astro.cookies.get("Telemtry").value === "Disabled") {
- var UserIsOptedIn = false
-}
----
-
-{UserIsOptedIn ?
- {t("TELEMTRY.OPTOUT")}
- :
- {t("TELEMTRY.OPTIN")}
-}
diff --git a/source/src/components/category/CategoryCard.astro b/source/src/components/category/CategoryCard.astro
deleted file mode 100644
index 71578c8..0000000
--- a/source/src/components/category/CategoryCard.astro
+++ /dev/null
@@ -1,80 +0,0 @@
----
-const {
- Link,
- Name,
- Platform,
- Thumbnail,
- Ratio = "16:9" // "16:9" or "9:16"
-} = Astro.props
----
-
-
-
- {
- ()=> {
- if (Platform === "YouTube") {
- return
- } else if (Platform === "Twitch") {
- return
- }
- }
- }
-
-
-
-
-
-{
- ()=> {
- if (Ratio === "16:9") {
- return
- } else if (Ratio === "9:16") {
- return
- }
- }
-}
\ No newline at end of file
diff --git a/source/src/components/category/trending.astro b/source/src/components/category/trending.astro
deleted file mode 100755
index 9f80a50..0000000
--- a/source/src/components/category/trending.astro
+++ /dev/null
@@ -1,28 +0,0 @@
----
-// Properties
-const {
- FetchData,
- CategoryName,
- CategoryDescription,
- GradientHero
-} = Astro.props
-
-// Configuration
-import {
- DEFAULT_MEDIA_DATA_PROXY,
- DEFAULT_IMAGE_PROXY
-} from '@utils/GetConfig'
-
-
-// Fetch
-const fetchFrom = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending' + FetchData
-const response = await fetch(fetchFrom)
-const data = await response.json()
----
-
-
-
- {data.map((data) =>
-
- )}
-
\ No newline at end of file
diff --git a/source/src/components/global/Analytics.astro b/source/src/components/global/Analytics.astro
deleted file mode 100755
index 20b96a0..0000000
--- a/source/src/components/global/Analytics.astro
+++ /dev/null
@@ -1,66 +0,0 @@
----
-// Environment Variables
-import {
- ANALYTICS,
- MATOMO_ID,
- MATOMO_SRC,
- PLAUSIBLE_DOMAIN,
- PLAUSIBLE_SRC,
- UMAMI_SRC,
- AMPLITUDE_APIKEY,
- METRICAL_APP,
- FATHOM_SITE,
- FATHOM_SRC,
- MINIAML_ID,
- SWETRIX_SRC,
- SWETRIX_API,
- SWETRIX_PROJECT_ID,
- SIMPLEANALYTICS_DOMAIN
-} from '@utils/GetConfig'
-
-// Get Astro Analytics
-import {
- Fathom,
- Metrical,
- Plausible,
- Umami,
- Amplitude,
- Matomo,
- MinimalAnalytics
-} from 'astro-analytics'
----
-
-
-{
- ()=> {
- if (ANALYTICS === "None") {
- return null
- } else if (ANALYTICS === "Plausible") {
-
- } else if (ANALYTICS === "Umami") {
-
- } else if (ANALYTICS === "Amplitude") {
-
- } else if (ANALYTICS === "Matomo") {
-
- } else if (ANALYTICS === "Metrical") {
-
- } else if (ANALYTICS === "Fathom") {
-
- } else if (ANALYTICS === "MinimalAnalytics") {
-
- } else if (ANALYTICS === "Swetrix") {
-
-
-
- } else if (ANALYTICS === "Simple Analytics") {
-
-
- }
- }
-}
\ No newline at end of file
diff --git a/source/src/components/global/Footer.astro b/source/src/components/global/Footer.astro
deleted file mode 100755
index c49a6e1..0000000
--- a/source/src/components/global/Footer.astro
+++ /dev/null
@@ -1,19 +0,0 @@
----
-// Configuration
-import {
- DEFAULT_PLAYER
-} from '@utils/GetConfig'
----
-
-{
- () => {
- if (DEFAULT_PLAYER === "Browser") {
- return (null)
- }
- if (DEFAULT_PLAYER === "Zorn") {
- return (
-
- )
- }
- }
-}
\ No newline at end of file
diff --git a/source/src/components/global/Head.astro b/source/src/components/global/Head.astro
deleted file mode 100755
index a51bcc9..0000000
--- a/source/src/components/global/Head.astro
+++ /dev/null
@@ -1,103 +0,0 @@
----
-// i18n
-import i18next, { t } from "i18next";
-
-// Cookies
-/// Set Default
-if (Astro.cookies.get("Language") === undefined) {
- Astro.cookies.set("Language", "EN", {path: "/",sameSite: 'strict'})
-} else {
- var UserLanguage = Astro.cookies.get("Language").value
- if (UserLanguage === "JP") {i18next.changeLanguage("jp")}
- else if (UserLanguage === "EN") {i18next.changeLanguage("en")}
-}
-if (Astro.cookies.get("Milieu") === undefined) {
- Astro.cookies.set("Milieu", "Enabled", {path: "/",sameSite: 'strict'})
-} else {
- var UserLanguage = Astro.cookies.get("Language").value
- if (UserLanguage === "JP") {i18next.changeLanguage("jp")}
- else if (UserLanguage === "EN") {i18next.changeLanguage("en")}
-}
-
-/// Telemtry
-//// Users should be opted-out by default
-if (Astro.cookies.get("Telemtry") === undefined) {
- Astro.cookies.set("Telemtry", "Disabled", {path: "/",sameSite: 'strict'})
-}
-
-// Properties
-const {
- Title,
- Description,
- EmbedId,
- EmbedVideo,
- EmbedImage,
- EmbedTitle,
- } = Astro.props
-
-// Components
-import Analytics from "@components/global/Analytics.astro";
-import { Tooltips } from 'astro-tooltips';
-
- // Configuration
-import {SERVER_DOMAIN} from '@utils/GetConfig'
-
-// Embed
-const SWV = Astro.url.href.split("embed/").pop();
-
-if (Astro.url.href.match('watch')) {
- var IsVideo = true
-} else if (Astro.url.href.match('embed')) {
- var IsVideo = false
-} else {
- var IsVideo = false
-}
-
-// Track Events
-// TODO
-// Before re-enabling, create an "opt-in" solution
-// first for the end-user, as asking for consent
-// would be ideal. Astro Cookies can be used to
-// toggle the option, make sure end-users are
-// opted-out by default for privacy purposes.
-
-// import { OpenpanelSdk } from '@openpanel/sdk';
-// const op = new OpenpanelSdk({
-// clientId: 'b4c27f56-18f5-4d66-bb62-cbf7f7161812',
-// clientSecret: 'sec_107558407af59a591b50',
-// });
----
-
-
-
- {Title}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {IsVideo ?
-
-
-
-
-
-
-
-
- :
- null
- }
-
-
\ No newline at end of file
diff --git a/source/src/components/global/MobileNav.astro b/source/src/components/global/MobileNav.astro
deleted file mode 100755
index a776b45..0000000
--- a/source/src/components/global/MobileNav.astro
+++ /dev/null
@@ -1,13 +0,0 @@
----
-import { HomeSimple, InputSearch, MediaVideoList, MoreHoriz, ProfileCircle, Safari, Settings } from "@iconoir/vue";
-
----
-
-
\ No newline at end of file
diff --git a/source/src/components/global/Sidebar.astro b/source/src/components/global/Sidebar.astro
deleted file mode 100755
index ec1071b..0000000
--- a/source/src/components/global/Sidebar.astro
+++ /dev/null
@@ -1,253 +0,0 @@
----
-// Configuration
-import {
- SIDEBAR_DISCOVER,
- SIDEBAR_CATEGORIES
-} from '@utils/GetConfig'
-
-import {
- version
-} from '@root/package.json'
-
-// Components
-import CreatorInSidebar from '@components/CreatorInSidebar.astro'
-import { ViewTransitions } from 'astro:transitions';
-import { slide } from "astro/virtual-modules/transitions.js";
-
-// Icons
-import {
- GraphUp,
- Movie,
- MusicDoubleNote,
- Gamepad,
- AppleImac2021Side,
- EmojiTalkingHappy,
- PeaceHand,
- PlanetAlt,
- InputSearch,
- Settings,
- LogIn,
-LogOut
-} from '@iconoir/vue'
-
-// i18n
-import i18next, { t } from "i18next"
-
-// Supabase Data
-import { supabase } from "@library/supabase"
-const { data: { user } } = await supabase.auth.getUser()
-const id = user?.id
-
-// Is the user logged in?
-if (Astro.cookies.get('sb-access-token') === undefined) {
- var Guest = true
-} else {
- var Guest = false
-}
-
-// Get Channels
-const { data: channels, error } = await supabase
- .from('channels')
- .select('*')
-let { data: subs } = await supabase
- .from('subs')
- .select("*")
- .eq('UserSubscribed', id)
----
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/information/InstanceList.astro b/source/src/components/information/InstanceList.astro
deleted file mode 100755
index d7f4cf5..0000000
--- a/source/src/components/information/InstanceList.astro
+++ /dev/null
@@ -1,50 +0,0 @@
----
-const InstanceList = await fetch('https://codeberg.org/MinPluto/MinPluto/raw/branch/master/instances.json').then((response) => response.json());
----
-
-
-
-
- URL
- Official
- Region
- Uses Cloudflare
-
-
-
- {InstanceList.map((server) =>
-
- {server[1].uri}
- {server[1].official ? Yes
: No
}
- {server[1].region}
- {server[1].CLOUDFLARE ? Yes
: No
}
-
- )}
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/information/Invidious.astro b/source/src/components/information/Invidious.astro
deleted file mode 100755
index 9411a37..0000000
--- a/source/src/components/information/Invidious.astro
+++ /dev/null
@@ -1,13 +0,0 @@
----
-import {
- DEFAULT_MEDIA_PROXY,
- DEFAULT_MEDIA_DATA_PROXY,
-} from '@utils/GetConfig'
-
-const InvidiousDataInfomation = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/api/v1/stats').then((response) => response.json());
----
-
-
-{InvidiousDataInfomation.map((server) =>
- Version:
-)}
diff --git a/source/src/components/search/Creator.astro b/source/src/components/search/Creator.astro
deleted file mode 100755
index 9afa72e..0000000
--- a/source/src/components/search/Creator.astro
+++ /dev/null
@@ -1,78 +0,0 @@
----
-const {
- Name,
- Avatar,
- Link,
- Platform,
- Banner,
- Followers
-} = Astro.props
----
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/search/Stream.astro b/source/src/components/search/Stream.astro
deleted file mode 100755
index b6fdd8e..0000000
--- a/source/src/components/search/Stream.astro
+++ /dev/null
@@ -1,73 +0,0 @@
----
-const {
- Title,
- Creator,
- Avatar,
- Link,
- Thumbnail,
- Views
-} = Astro.props
----
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/text/PrivacyPolicyAnalytics.astro b/source/src/components/text/PrivacyPolicyAnalytics.astro
deleted file mode 100755
index 9daba0f..0000000
--- a/source/src/components/text/PrivacyPolicyAnalytics.astro
+++ /dev/null
@@ -1,19 +0,0 @@
----
-import {
- ANALYTICS
-} from '@utils/GetConfig'
----
-
-
-{
- ()=> {
- if (ANALYTICS === "false") {
- return null
- } else {
- return
- Information Collection and Use
- This instance of MinPluto uses {ANALYTICS} for analytics, no personal informatin is collected about you. Data that is collected can not be used to identify you, along with devices or any other factors.
- All data collected with {ANALYTICS} is anonymous.
- }
- }
-}
\ No newline at end of file
diff --git a/source/src/components/text/PrivacyPolicyCloudflare.astro b/source/src/components/text/PrivacyPolicyCloudflare.astro
deleted file mode 100755
index cd914ec..0000000
--- a/source/src/components/text/PrivacyPolicyCloudflare.astro
+++ /dev/null
@@ -1,28 +0,0 @@
----
-import {
- CLOUDFLARE
-} from '@utils/GetConfig'
----
-
-
-{CLOUDFLARE ?
- Cloudflare Usage
- This instance uses Cloudflare which means the following information is collected:
-
- Refers
- Paths
- Hosts
- Browser
- Operating System
- Device Type
- ASN
- User Agent
- Data Center
- Status Code
- IP Address
- HTTP Version
-
- This is shown in the "Analytics & Logs" portion in Cloudflare's Dashboard, this can't be turned off by the instance operator.
- :
- null
-}
\ No newline at end of file
diff --git a/source/src/components/video-player/Controls.astro b/source/src/components/video-player/Controls.astro
deleted file mode 100755
index e737c8f..0000000
--- a/source/src/components/video-player/Controls.astro
+++ /dev/null
@@ -1,148 +0,0 @@
----
-// Cookies
-if (Astro.cookies.get("Milieu").value === "Enabled") {
- var Milieu = true
-} else {
- var Milieu = false
-}
-
-// Icons
-import {
-ArrowUpRight,
- Backward15Seconds,
- Enlarge,
- Forward15Seconds,
- NavArrowRight,
- PlaySolid,
- Settings,
-SwitchOff,
-SwitchOn,
-} from "@iconoir/vue";
----
-
-
-
-
-
-
-
-
-
-
- 00:00
- 00:00
- 00:00
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/components/video-player/Player.astro b/source/src/components/video-player/Player.astro
deleted file mode 100755
index a9d79d5..0000000
--- a/source/src/components/video-player/Player.astro
+++ /dev/null
@@ -1,101 +0,0 @@
----
-// Properties
-const { Poster, Video, Audio } = Astro.props
-
-// Cookies
-if (Astro.cookies.get("Milieu").value === "Enabled") {
- var Milieu = true
-} else {
- var Milieu = false
-}
-
-// Components
-import Controls from '@components/video-player/Controls.astro'
-
-// Styles
-import '@styles/player.scss'
----
-
-
-
-
-
-
-
-
-{Milieu ?
-
- :
- null
-}
\ No newline at end of file
diff --git a/source/src/env.d.ts b/source/src/env.d.ts
deleted file mode 100644
index c13bd73..0000000
--- a/source/src/env.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-///
-///
\ No newline at end of file
diff --git a/source/src/layouts/API.astro b/source/src/layouts/API.astro
deleted file mode 100755
index 02994df..0000000
--- a/source/src/layouts/API.astro
+++ /dev/null
@@ -1,58 +0,0 @@
----
-// Properties
-const { Title, Description } = Astro.props
-
-// Components
-import Head from '@components/global/Head.astro'
-
-// Styles
-import '@styles/index.scss'
-import '@styles/mobile.scss'
----
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/layouts/Default.astro b/source/src/layouts/Default.astro
deleted file mode 100755
index 884662d..0000000
--- a/source/src/layouts/Default.astro
+++ /dev/null
@@ -1,22 +0,0 @@
----
-// Properties
-const { Title, Description } = Astro.props;
-
-// Components
-import Head from "@components/global/Head.astro";
-import MobileNav from "@components/global/MobileNav.astro";
-import Sidebar from "@components/global/Sidebar.astro";
-import Footer from "@components/global/Footer.astro";
-
-// Styles
-import "@styles/index.scss";
-import "@styles/mobile.scss";
----
-
-
-
-
-
-
-
-
diff --git a/source/src/layouts/Embed.astro b/source/src/layouts/Embed.astro
deleted file mode 100755
index c67b415..0000000
--- a/source/src/layouts/Embed.astro
+++ /dev/null
@@ -1,29 +0,0 @@
----
-// Properties
-const {
- Title,
- Description,
- EmbedId,
- EmbedVideo,
- EmbedImage,
- EmbedTitle
-} = Astro.props
-
-// Components
-import Head from '@components/global/Head.astro'
-import Footer from '@components/global/Footer.astro'
-
-// Styles
-import '@styles/embed.scss'
----
-
-
-
-
\ No newline at end of file
diff --git a/source/src/layouts/Error.astro b/source/src/layouts/Error.astro
deleted file mode 100755
index 2851092..0000000
--- a/source/src/layouts/Error.astro
+++ /dev/null
@@ -1,34 +0,0 @@
----
-// Properties
-const {
- Title,
- Message
-} = Astro.props
----
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/layouts/Markdown.astro b/source/src/layouts/Markdown.astro
deleted file mode 100755
index 6f9864f..0000000
--- a/source/src/layouts/Markdown.astro
+++ /dev/null
@@ -1,34 +0,0 @@
----
-// Properties
-const { Title, Description } = Astro.props
-
-// Components
-import Head from '@components/global/Head.astro'
-import MobileNav from '@components/global/MobileNav.astro'
-import Sidebar from '@components/global/Sidebar.astro'
-import Footer from '@components/global/Footer.astro'
-
-// Styles
-import '@styles/index.scss'
-import '@styles/mobile.scss'
----
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/layouts/Settings.astro b/source/src/layouts/Settings.astro
deleted file mode 100755
index 8934063..0000000
--- a/source/src/layouts/Settings.astro
+++ /dev/null
@@ -1,115 +0,0 @@
----
-// Properties
-const { Title, Description } = Astro.props
-
-// Components
-import Head from '@components/global/Head.astro'
-import MobileNav from '@components/global/MobileNav.astro'
-import Sidebar from '@components/global/Sidebar.astro'
-import Footer from '@components/global/Footer.astro'
-
-// Styles
-import '@styles/index.scss'
-import '@styles/mobile.scss'
-import { CodeBrackets, EditPencil, EvPlugXmark, FloppyDisk, HelpCircle, InfoCircle, LogOut, Play, PrivacyPolicy, ProfileCircle, Server, UnionAlt, XmarkCircle } from '@iconoir/vue'
----
-
-
-
-
-
-
-
-
-
diff --git a/source/src/pages/404.astro b/source/src/pages/404.astro
deleted file mode 100755
index c118219..0000000
--- a/source/src/pages/404.astro
+++ /dev/null
@@ -1,20 +0,0 @@
----
-import { changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-
----
-
-
-
-
Error 404: Page Not Found
-
The page you've requested has either been moved or never existed. Double check the URL you're viewing.
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/500.astro b/source/src/pages/500.astro
deleted file mode 100755
index 2e19713..0000000
--- a/source/src/pages/500.astro
+++ /dev/null
@@ -1,13 +0,0 @@
----
-import ErrorLayout from '@layouts/Error.astro'
-
-// Fetch Error Output
-// https://github.com/withastro/astro/blob/main/packages/astro/src/core/errors/errors.ts#L5-L11
-interface Props {
- error: unknown
-}
-
-const { error } = Astro.props
----
-
-
\ No newline at end of file
diff --git a/source/src/pages/account/confirm-your-email.astro b/source/src/pages/account/confirm-your-email.astro
deleted file mode 100755
index 472b800..0000000
--- a/source/src/pages/account/confirm-your-email.astro
+++ /dev/null
@@ -1,53 +0,0 @@
----
-import i18next,{ t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-import '@styles/form.scss'
----
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/account/index.astro b/source/src/pages/account/index.astro
deleted file mode 100755
index 58d6f5d..0000000
--- a/source/src/pages/account/index.astro
+++ /dev/null
@@ -1,59 +0,0 @@
----
-// i18n
-import i18next,{ t, changeLanguage } from "i18next";
-import Settings from "@layouts/Settings.astro";
-
-// Supabase Data
-import { supabase } from "@library/supabase"
-const { data: { user } } = await supabase.auth.getUser()
-const ID = user?.id
-const Name = user?.user_metadata.name
-const Email = user?.email
-
-// Styles
-import '@styles/form.scss'
----
-
-
- Account ID: {ID}
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/api/auth/confirm.ts b/source/src/pages/api/auth/confirm.ts
deleted file mode 100755
index aca6d15..0000000
--- a/source/src/pages/api/auth/confirm.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { APIRoute } from "astro"
-import { supabase } from "@library/supabase"
-
-export const POST: APIRoute = async ({ request, redirect }) => {
- const formData = await request.formData()
- const EMAIL = formData.get("email")?.toString()
- const OTP = formData.get("code")?.toString()
-
- if (!OTP) {
- return new Response("Code required", { status: 400 })
- }
-
- const { error } = await supabase.auth.verifyOtp({type: 'email', token: OTP, email: EMAIL})
-
- if (error) {
- return new Response(error.message, { status: 500 })
- }
-
- return redirect("/login?=confirmed")
-}
\ No newline at end of file
diff --git a/source/src/pages/api/auth/login.ts b/source/src/pages/api/auth/login.ts
deleted file mode 100755
index 3bce90a..0000000
--- a/source/src/pages/api/auth/login.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { APIRoute } from "astro"
-import { supabase } from "@library/supabase"
-
-export const POST: APIRoute = async ({ request, cookies, redirect }) => {
- const formData = await request.formData()
- const email = formData.get("email")?.toString()
- const password = formData.get("password")?.toString()
-
- if (!email || !password) {
- return new Response("Email and password are required", { status: 400 })
- }
-
- const { data, error } = await supabase.auth.signInWithPassword({
- email,
- password,
- })
-
- if (error) {
- return new Response(error.message, { status: 500 })
- }
-
- const { access_token, refresh_token } = data.session
- cookies.set("sb-access-token", access_token, {
- sameSite: "strict",
- path: "/",
- secure: true,
- })
- cookies.set("sb-refresh-token", refresh_token, {
- sameSite: "strict",
- path: "/",
- secure: true,
- })
-
- return redirect("/")
-}
diff --git a/source/src/pages/api/auth/logout.ts b/source/src/pages/api/auth/logout.ts
deleted file mode 100755
index ea87725..0000000
--- a/source/src/pages/api/auth/logout.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { APIRoute } from "astro"
-import { supabase } from "@library/supabase"
-import type { Provider } from "@supabase/supabase-js"
-
-export const GET: APIRoute = async ({ cookies, redirect }) => {
-
- if(cookies.get('anonymous-session')) {
- return redirect('/account/anon/end')
- } else {
- cookies.delete("sb-access-token", { path: "/" })
- cookies.delete("sb-refresh-token", { path: "/" })
- const { error } = await supabase.auth.signOut()
- }
- return redirect("/")
-}
\ No newline at end of file
diff --git a/source/src/pages/api/auth/register.ts b/source/src/pages/api/auth/register.ts
deleted file mode 100755
index d40fe3f..0000000
--- a/source/src/pages/api/auth/register.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { APIRoute } from "astro"
-import { supabase } from "@library/supabase"
-
-export const POST: APIRoute = async ({ request, redirect }) => {
- const formData = await request.formData()
- const name = formData.get("username")?.toString()
- const email = formData.get("email")?.toString()
- const password = formData.get("password")?.toString()
-
- if (!email || !password || !name) {
- return new Response("Email and password are required", { status: 400 })
- }
-
- const { error } = await supabase.auth.signUp({
- options: {
- emailRedirectTo: "http://localhost:1930/?=welcome",
- data: {
- name: name,
- ui_theme: "Default",
- ui_color: "Default",
- ui_zen: "false",
- ui_sidebar_size: "Normal",
- invidous_data: "https://yt.sudovanilla.org",
- invidous_media: "https://yt.sudovanilla.org",
- safetwitch_data: "https://twitch.sudovanilla.org",
- safetwitch_media: "https://twitch.sudovanilla.org",
- image_proxy: "https://ipx.sudovanilla.org",
- player_type: "Zorn"
- }
- },
- email,
- password,
- })
-
- if (error) {
- return new Response(error.message, { status: 500 })
- }
-
- return redirect("/account/confirm-your-email")
-}
\ No newline at end of file
diff --git a/source/src/pages/api/language/en.astro b/source/src/pages/api/language/en.astro
deleted file mode 100755
index af6dd52..0000000
--- a/source/src/pages/api/language/en.astro
+++ /dev/null
@@ -1,27 +0,0 @@
----
-// Layout
-import API from '@layouts/API.astro'
-
-// API Action
-Astro.cookies.set("Language", "EN", {
- path: "/",
- sameSite: "strict"
-});
-
-// Track Event
-import { OpenpanelSdk } from '@openpanel/sdk';
-const op = new OpenpanelSdk({
- clientId: 'b4c27f56-18f5-4d66-bb62-cbf7f7161812',
- clientSecret: 'sec_107558407af59a591b50',
-});
-if (Astro.cookies.get("Telemtry").value === "Enabled") {
- op.event('Language', { Language: 'English' });
-}
-else if (Astro.cookies.get("Telemtry").value === "Disabled") {
- null
-}
-
-// Return
-return Astro.redirect("/");
----
-
\ No newline at end of file
diff --git a/source/src/pages/api/language/jp.astro b/source/src/pages/api/language/jp.astro
deleted file mode 100755
index 2d2210b..0000000
--- a/source/src/pages/api/language/jp.astro
+++ /dev/null
@@ -1,27 +0,0 @@
----
-// Layout
-import API from '@layouts/API.astro'
-
-// API Action
-Astro.cookies.set("Language", "JP", {
- path: "/",
- sameSite: "strict"
-});
-
-// Track Event
-import { OpenpanelSdk } from '@openpanel/sdk';
-const op = new OpenpanelSdk({
- clientId: 'b4c27f56-18f5-4d66-bb62-cbf7f7161812',
- clientSecret: 'sec_107558407af59a591b50',
-});
-if (Astro.cookies.get("Telemtry").value === "Enabled") {
- op.event('Language', { Language: 'Japanese' });
-}
-else if (Astro.cookies.get("Telemtry").value === "Disabled") {
- null
-}
-
-// Return
-return Astro.redirect("/");
----
-
\ No newline at end of file
diff --git a/source/src/pages/api/player/milieu/disable.astro b/source/src/pages/api/player/milieu/disable.astro
deleted file mode 100755
index cb64b5b..0000000
--- a/source/src/pages/api/player/milieu/disable.astro
+++ /dev/null
@@ -1,9 +0,0 @@
----
-import API from '@layouts/API.astro'
-Astro.cookies.set("Milieu", "Disabled", {
- path: "/",
- sameSite: "strict"
-});
-return Astro.redirect("/");
----
-
\ No newline at end of file
diff --git a/source/src/pages/api/player/milieu/enable.astro b/source/src/pages/api/player/milieu/enable.astro
deleted file mode 100755
index 36a480e..0000000
--- a/source/src/pages/api/player/milieu/enable.astro
+++ /dev/null
@@ -1,14 +0,0 @@
----
-// Layout
-import API from '@layouts/API.astro'
-
-// API Action
-Astro.cookies.set("Milieu", "Enabled", {
- path: "/",
- sameSite: "strict"
-});
-
-// Return
-return Astro.redirect("/telemtry");
----
-
\ No newline at end of file
diff --git a/source/src/pages/api/subscription/add.astro b/source/src/pages/api/subscription/add.astro
deleted file mode 100644
index 93c14f8..0000000
--- a/source/src/pages/api/subscription/add.astro
+++ /dev/null
@@ -1,24 +0,0 @@
----
-import Base from "@layouts/Default.astro"
-const CreatorId = Astro.url.href.split("add?=").pop()
-import { supabase } from "@library/supabase"
-const { data: { user } } = await supabase.auth.getUser()
-const { data, error } = await supabase
- .from('subs')
- .insert([
- {
- UserSubscribed: user?.id,
- Id: CreatorId,
- Platform: "YouTube"
- },
- ])
- .select()
-
-return Astro.redirect("/channel/" + CreatorId)
----
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/api/subscription/remove.astro b/source/src/pages/api/subscription/remove.astro
deleted file mode 100644
index 25f467a..0000000
--- a/source/src/pages/api/subscription/remove.astro
+++ /dev/null
@@ -1,21 +0,0 @@
----
-import Base from "@layouts/Default.astro"
-const CreatorId = Astro.url.href.split("remove?=").pop()
-import { supabase } from "@library/supabase"
-const { data: { user } } = await supabase.auth.getUser()
-const id = user?.id
-const { data, error } = await supabase
- .from('subs')
- .delete()
- .eq('UserSubscribed', id)
- .eq('Id', CreatorId)
- .eq('Platform', 'YouTube')
-
-return Astro.redirect("/channel/" + CreatorId)
----
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/api/telemtry/disable.astro b/source/src/pages/api/telemtry/disable.astro
deleted file mode 100755
index d680855..0000000
--- a/source/src/pages/api/telemtry/disable.astro
+++ /dev/null
@@ -1,24 +0,0 @@
----
-import API from '@layouts/API.astro'
-Astro.cookies.set("Telemtry", "Disabled", {
- path: "/",
- sameSite: "strict"
-});
-return Astro.redirect("/telemtry");
-
-// SDK
-import { OpenpanelSdk } from '@openpanel/sdk';
-const op = new OpenpanelSdk({
- clientId: 'b4c27f56-18f5-4d66-bb62-cbf7f7161812',
- clientSecret: 'sec_107558407af59a591b50',
-});
-
-// Track Event
-if (Astro.cookies.get("Telemtry").value === "Enabled") {
- op.event('Language', { Language: 'English' });
-}
-else if (Astro.cookies.get("Telemtry").value === "Disabled") {
- null
-}
----
-
\ No newline at end of file
diff --git a/source/src/pages/api/telemtry/enable.astro b/source/src/pages/api/telemtry/enable.astro
deleted file mode 100755
index 7759fba..0000000
--- a/source/src/pages/api/telemtry/enable.astro
+++ /dev/null
@@ -1,36 +0,0 @@
----
-// Layout
-import API from '@layouts/API.astro'
-
-// API Action
-Astro.cookies.set("Telemtry", "Enabled", {
- path: "/",
- sameSite: "strict"
-});
-
-// SDK
-import { OpenpanelSdk } from '@openpanel/sdk';
-const op = new OpenpanelSdk({
- clientId: 'b4c27f56-18f5-4d66-bb62-cbf7f7161812',
- clientSecret: 'sec_107558407af59a591b50',
-});
-
-// Track Event
-import { useUserAgent } from "astro-useragent";
-const uaString = Astro.request.headers.get("user-agent");
-const {
- os,
- browser,
- browserVersion,
- isDesktop,
- isMobile
-} = useUserAgent(uaString);
-op.event('Operating System', {OS: os})
-op.event('Browser', {Browser: browser + ' v' + browserVersion})
-op.event('Desktop', {Desktop: isDesktop})
-op.event('Mobile', {Mobile: isMobile})
-
-// Return
-return Astro.redirect("/telemtry");
----
-
\ No newline at end of file
diff --git a/source/src/pages/api/update/email.ts b/source/src/pages/api/update/email.ts
deleted file mode 100755
index 1877b6f..0000000
--- a/source/src/pages/api/update/email.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { APIRoute } from "astro"
-import { supabase } from "@library/supabase"
-
-export const POST: APIRoute = async ({ request, redirect }) => {
- const formData = await request.formData()
- const NewEmail = formData.get("email")?.toString()
-
- if (!NewEmail) {
- return new Response("Error.", { status: 400 })
- }
-
- const { error } = await supabase.auth.resend({
- type: 'email_change',
- email: NewEmail
- })
-
- if (error) {
- return new Response(error.message, { status: 500 })
- }
-
- return redirect("/account?=CheckEmail")
-}
\ No newline at end of file
diff --git a/source/src/pages/api/update/name.ts b/source/src/pages/api/update/name.ts
deleted file mode 100755
index 4dfb337..0000000
--- a/source/src/pages/api/update/name.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { APIRoute } from "astro"
-import { supabase } from "@library/supabase"
-
-export const POST: APIRoute = async ({ request, redirect }) => {
- const formData = await request.formData()
- const name = formData.get("name")?.toString()
-
- if (!name) {
- return new Response("Error.", { status: 400 })
- }
-
- const { error } = await supabase.auth.updateUser({
- data: {name: name}
- })
-
- if (error) {
- return new Response(error.message, { status: 500 })
- }
-
- return redirect("/account?=NameUpdated")
-}
diff --git a/source/src/pages/begin-search.astro b/source/src/pages/begin-search.astro
deleted file mode 100644
index d135920..0000000
--- a/source/src/pages/begin-search.astro
+++ /dev/null
@@ -1,57 +0,0 @@
----
-import Base from "@layouts/Default.astro";
-import Search from "@components/Search.astro";
----
-
-
-
-
-
-
-
diff --git a/source/src/pages/category/gaming.astro b/source/src/pages/category/gaming.astro
deleted file mode 100755
index 873c82c..0000000
--- a/source/src/pages/category/gaming.astro
+++ /dev/null
@@ -1,9 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Category from "@layouts/Category.astro";
-
-
----
-
-
-
diff --git a/source/src/pages/category/movies.astro b/source/src/pages/category/movies.astro
deleted file mode 100755
index 62577c9..0000000
--- a/source/src/pages/category/movies.astro
+++ /dev/null
@@ -1,10 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Category from "@layouts/Category.astro";
-
-
----
-
-
-
-
diff --git a/source/src/pages/category/music.astro b/source/src/pages/category/music.astro
deleted file mode 100755
index f60f3fc..0000000
--- a/source/src/pages/category/music.astro
+++ /dev/null
@@ -1,111 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Default from "@layouts/Default.astro";
-
-
-
-// Configuration
-import {
- DEFAULT_MEDIA_DATA_PROXY
-} from '@utils/GetConfig'
-
-// Components
-import MusicItem from '@components/MusicItem.astro'
-
-// Fetch
-const fetchFrom = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending?type=music'
-const response = await fetch(fetchFrom)
-const data = await response.json()
-
-const heroItem = data.slice(0,1)
----
-
-
-
-
-
-
Music
-
Listen to the latest hits
-
-
- {heroItem.map((data) =>
-
- )}
-
-
-
-
-
-
-
{t("MUSIC.TITLE")}
-
{t("MUSIC.ARTIST")}
-
{t("MUSIC.UPLOADED")}
-
{t("MUSIC.DURATION")}
-
-
- {data.map((data) =>
-
- )}
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/category/trending.astro b/source/src/pages/category/trending.astro
deleted file mode 100755
index d268046..0000000
--- a/source/src/pages/category/trending.astro
+++ /dev/null
@@ -1,9 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Category from "@layouts/Category.astro";
-
-
----
-
-
-
diff --git a/source/src/pages/channel/[...slug].astro b/source/src/pages/channel/[...slug].astro
deleted file mode 100755
index cb9d54c..0000000
--- a/source/src/pages/channel/[...slug].astro
+++ /dev/null
@@ -1,183 +0,0 @@
----
-import Base from "@layouts/Default.astro";
-
-// i18n
-import i18next, { t, changeLanguage } from "i18next";
-
-
-// Configuration
-import { DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY, SERVER_DOMAIN } from '@utils/GetConfig'
-import { BrightStar, Donate, Download, ShareIos, ThumbsUp } from "@iconoir/vue";
-
-// Components
-import Video from '@components/VideoItem.astro'
-
-// Fetch
-const CreatorId = Astro.url.href.split("channel/").pop();
-const channel = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/channels/" + CreatorId).then((response) => response.json());
-const DescriptionFormat = channel.descriptionHtml.replaceAll("\n", " ");
-
-// Is the user logged in?
-if (Astro.cookies.get('sb-access-token') === undefined) {
- var Guest = true
-} else {
- var Guest = false
-}
-
-// User Subscription
-import { supabase } from "@library/supabase"
-const { data: { user } } = await supabase.auth.getUser()
-const id = user?.id
-let { data: subs } = await supabase
- .from('subs')
- .select("*")
- .eq('UserSubscribed', id)
- .eq('Id', CreatorId)
-
-
-if (Guest === false) {
- if (subs[0] === undefined) {
- var Subbed = false
- } else {
- var Subbed = true
- }
-}
-else {
- var Subbed = "NotLoggedIn"
-}
----
-
-
-
-
-
- {Subbed ?
- Unfollow
- :
- Follow
- }
- {
- ()=> {
- if (Subbed === true) {
- return Unfollow
- } else if (Subbed === false) {
- return Follow
- } else if(Subbed === "NotLoggedIn") {
- return null
- }
- }
- }
-
-
-
-
-
-
-
-
Latest Videos
-
- {channel.latestVideos.map((data) =>
-
- )}
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/discover.astro b/source/src/pages/discover.astro
deleted file mode 100755
index ea814a4..0000000
--- a/source/src/pages/discover.astro
+++ /dev/null
@@ -1,112 +0,0 @@
----
-// Layout
-import Base from "@layouts/Default.astro"
-
-// i18n
-import { t, changeLanguage } from "i18next"
-
-// Configuration
-import { DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY, DEFAULT_STREAM_DATA_PROXY } from '@utils/GetConfig'
-
-// Components
-import Video from '@components/VideoItem.astro'
-import Stream from "@components/search/Stream.astro"
-
-// Get URL Strings
-const SearchQueryWithParameters = Astro.url.href.split("&?").shift()
-const SearchQuery = SearchQueryWithParameters.split("discover?category=").pop()
-if (Astro.url.href.includes('?platform=youtube')) {var SelectedPlatform = "YouTube"}
-else if (Astro.url.href.includes('?platform=twitch')) {var SelectedPlatform = "Twitch"}
-var FullCategoryName = SearchQuery.charAt(0).toUpperCase() + SearchQuery.slice(1)
-
-// Fetch
-if (Astro.url.href.includes('?platform=youtube')) {
- var PlatformYouTube = true
- var YouTubeCategory = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending?type=' + SearchQuery
- var YouTubeFetch = await fetch(YouTubeCategory)
- var YouTubeData = await YouTubeFetch.json()
-}
-else if (Astro.url.href.includes('?platform=twitch')) {
- var PlatformTwitch = true
- var TwitchFetch = await fetch('https://twitch-backend.sudovanilla.org/api/discover/' + SearchQuery)
- var TwitchData = await TwitchFetch.json()
-}
-
----
-
-
-
- {PlatformTwitch ?
-
-
{TwitchData.data.displayName}
-
{TwitchData.data.viewers} Viewers
-
-
{TwitchData.data.description}
-
- Tags:
- {TwitchData.data.tags.map((tag) =>
- {tag}
- )}
-
- :
- null
- }
-
-
-
- {PlatformYouTube ?
- YouTubeData.map((video) =>
-
- )
- :
- null
- }
- {PlatformTwitch ?
- TwitchData.data.streams.map((channel) =>
-
- )
- :
- null
- }
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/embed/[...slug].astro b/source/src/pages/embed/[...slug].astro
deleted file mode 100755
index 3e2b8db..0000000
--- a/source/src/pages/embed/[...slug].astro
+++ /dev/null
@@ -1,129 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Embed from "@layouts/Embed.astro";
-import "@styles/video.scss";
-
-// Configuration
-import { DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY, SERVER_DOMAIN } from '@utils/GetConfig'
-
-// Components
-import { Zorn } from "@minpluto/zorn"
-
-// Fetch
-const SWV = Astro.url.href.split("embed/").pop();
-const video = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/videos/" + SWV).then((response) => response.json());
-
-// Quality Check
-const EightKCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=571')
-if (EightKCheck.status == 200) {
- var EightK = true
-} else {
- var EightK = false
-}
-
-const FourKCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=313')
-if (FourKCheck.status == 200) {
- var FourK = true
-} else {
- var FourK = false
-}
-
-const TenEightyCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=303')
-if (TenEightyCheck.status == 200) {
- var TenEighty = true
-} else {
- var TenEighty = false
-}
-
-const ThreeSixtyCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=134')
-if (ThreeSixtyCheck.status == 200) {var ThreeSixty = true}
-
-if (EightK === true) { // 571
- var Quality = '571'
-} else if (FourK === true) { // 313
- var Quality = '313'
-} else if (TenEighty === true) { // 137
- var Quality = '303'
-} else if (ThreeSixty === true) { // 134
- var Quality = '134'
-}
----
-
-
-
-
-
-
-
-
-
-
diff --git a/source/src/pages/iframe/category/trending.astro b/source/src/pages/iframe/category/trending.astro
deleted file mode 100755
index 4968fe3..0000000
--- a/source/src/pages/iframe/category/trending.astro
+++ /dev/null
@@ -1,5 +0,0 @@
----
-// Components
-import Trending from "@components/category/trending.astro";
----
-
\ No newline at end of file
diff --git a/source/src/pages/index.astro b/source/src/pages/index.astro
deleted file mode 100755
index c6fccd6..0000000
--- a/source/src/pages/index.astro
+++ /dev/null
@@ -1,158 +0,0 @@
-P---
-import i18next,{ t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-// Configuration
-import {
- DEFAULT_MEDIA_DATA_PROXY,
- DEFAULT_IMAGE_PROXY,
-DEFAULT_STREAM_DATA_PROXY
-} from '@utils/GetConfig'
-import { FireFlame, Frame, Gamepad, GraphUp, Movie, MusicDoubleNote } from "@iconoir/vue";
-
-// Components
-import Trending from "@components/category/trending.astro";
-import Chip from "@components/Chip.astro";
-import CategoryCard from "@components/category/CategoryCard.astro";
-
-// Fetch
-const TrendingFetch = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending'
-const TrendingResponse = await fetch(TrendingFetch)
-const TrendingData = await TrendingResponse.json()
-const TrendingSplit = TrendingData.slice(0, 1)
-
-const MoviesFetch = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending?type=movies'
-const MoviesResponse = await fetch(MoviesFetch)
-const MoviesData = await MoviesResponse.json()
-const MoviesSplit = MoviesData.slice(0, 1)
-
-
-const MusicFetch = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending?type=music'
-const MusicResponse = await fetch(MusicFetch)
-const MusicData = await MusicResponse.json()
-const MusicSplit = MusicData.slice(0, 1)
-
-
-const GamingFetch = DEFAULT_MEDIA_DATA_PROXY + '/api/v1/trending?type=gaming'
-const GamingResponse = await fetch(GamingFetch)
-const GamingData = await GamingResponse.json()
-const GamingSplit = GamingData.slice(0, 1)
-
-/// Twitch (/api/discover/)
-const TwitchDiscoverFetch = await fetch(DEFAULT_STREAM_DATA_PROXY + '/api/discover')
-const TwitchDiscoverData = await TwitchDiscoverFetch.json()
----
-
-
-
-
- {TrendingSplit.map((data) =>
-
-
-
- )}
- {MoviesSplit.map((data) =>
-
-
-
- )}
- {MusicSplit.map((data) =>
-
-
-
- )}
- {GamingSplit.map((data) =>
-
-
-
- )}
-
-
-
- {TwitchDiscoverData.data.map((data) =>
-
-
-
- )}
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/instance.mdx b/source/src/pages/instance.mdx
deleted file mode 100755
index bb28d25..0000000
--- a/source/src/pages/instance.mdx
+++ /dev/null
@@ -1,49 +0,0 @@
----
-layout: '@layouts/Settings.astro'
----
-
-import {
- SERVER_ADMIN,
- SERVER_LOCATION,
- DEFAULT_MEDIA_PROXY,
- DEFAULT_MEDIA_DATA_PROXY,
- DEFAULT_STREAM_PROXY,
- DEFAULT_STREAM_DATA_PROXY,
- DEFAULT_IMAGE_PROXY,
- MODIFIED,
- CUSTOM_SOURCE_CODE,
- ANALYTICS,
-} from '@utils/GetConfig'
-
-import Invidous from '@components/information/Invidious.astro'
-import InstanceList from '@components/information/InstanceList.astro'
-
-MinPluto Instance Information
------------------------------
-
-Server Operator: {SERVER_ADMIN}
-
-Location: {SERVER_LOCATION}
-
-Invidious Media Proxy: {DEFAULT_MEDIA_PROXY}
-
-Invidious Data Proxy: {DEFAULT_MEDIA_DATA_PROXY}
-
-SafeTwitch Data Proxy: {DEFAULT_STREAM_PROXY}
-
-SafeTwitch Media Proxy: {DEFAULT_STREAM_DATA_PROXY}
-
-Image Proxy: {DEFAULT_IMAGE_PROXY}
-
-Modified: {MODIFIED}
-
-{MODIFIED ?
Modified Source Code: {CUSTOM_SOURCE_CODE}
: null}
-
-Analytics Software: {ANALYTICS}
-
-___
-
-MinPluto Instances
----------------------------
-
-
\ No newline at end of file
diff --git a/source/src/pages/live.astro b/source/src/pages/live.astro
deleted file mode 100755
index c6018c0..0000000
--- a/source/src/pages/live.astro
+++ /dev/null
@@ -1,191 +0,0 @@
----
-// Layout
-import Base from "@layouts/Default.astro";
-import "@styles/video.scss";
-
-// Environment Variables
-// const DEFAULT_IMAGE_PROXY = import.meta.env.DEFAULT_IMAGE_PROXY
-// const DEFAULT_STREAM_PROXY = import.meta.env.DEFAULT_STREAM_PROXY
-// const DEFAULT_STREAM_DATA_PROXY = import.meta.env.DEFAULT_STREAM_DATA_PROXY
-
-// Components
-import { Zorn } from "@minpluto/zorn";
-import { ArrowDown } from "@iconoir/vue";
-
-// Fetch
-const CreatorName = Astro.url.href.split("live?=").pop();
-const Creator = await fetch("https://twitch-backend.sudovanilla.org" + "/api/users/" + CreatorName,).then((response) => response.json());
-
-// Check if the Creator is live
-if(Creator.data.isLive == true) {
- var IsLive = true
-} else if(Creator.data.isLive == false) {
- var IsLive = false
-}
----
-
-
-
-
- In order to watch a Twitch live stream on MinPluto, your browser is
- required to have JavaScript enabled.
-
-
- If your browser does not support JavaScript, try a modern web browser such
- as Firefox.
-
-
-
-
-
-
-
-
- {IsLive ?
Online :
Offline }
-
{Creator.data.username}
-
{Creator.data.followers} Followers
-
-
{Creator.data.about}
-
-
-
-
-
- {IsLive ?
-
- :
-
- }
-
-
-
-
-{IsLive ?
-
-:
-
-}
-
-
\ No newline at end of file
diff --git a/source/src/pages/login.astro b/source/src/pages/login.astro
deleted file mode 100755
index aa007fb..0000000
--- a/source/src/pages/login.astro
+++ /dev/null
@@ -1,58 +0,0 @@
----
-import i18next,{ t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-import '@styles/form.scss'
----
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/m/about.astro b/source/src/pages/m/about.astro
deleted file mode 100755
index 358f7f5..0000000
--- a/source/src/pages/m/about.astro
+++ /dev/null
@@ -1,66 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-import { version } from "@root/package.json";
-
-
----
-
-
-
-
-
MinPluto
-
{t("HOME.P1")}
-
-
{t("HOME.P2")}
-
-
-
-
v{version}
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/m/categories.astro b/source/src/pages/m/categories.astro
deleted file mode 100755
index 6219dfe..0000000
--- a/source/src/pages/m/categories.astro
+++ /dev/null
@@ -1,40 +0,0 @@
----
-import { t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-import { HomeSimple, GraphUp, Movie, MusicDoubleNote, Gamepad, AppleImac2021Side, EmojiTalkingHappy, PizzaSlice, Treadmill, PeaceHand } from "@iconoir/vue";
-
-
----
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/offline.astro b/source/src/pages/offline.astro
deleted file mode 100755
index a1a38f2..0000000
--- a/source/src/pages/offline.astro
+++ /dev/null
@@ -1,20 +0,0 @@
----
-import { changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-
----
-
-
-
-
No Internet Connection
-
It appears that you are offline, try connecting your device to the internet, and try again.
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/playlist.mdx b/source/src/pages/playlist.mdx
deleted file mode 100755
index 75685a0..0000000
--- a/source/src/pages/playlist.mdx
+++ /dev/null
@@ -1,7 +0,0 @@
----
-layout: '@layouts/Markdown.astro'
----
-
-Error
------------------------------
-Playlists are not yet supported in MinPluto.
\ No newline at end of file
diff --git a/source/src/pages/privacy.mdx b/source/src/pages/privacy.mdx
deleted file mode 100755
index 9dc469c..0000000
--- a/source/src/pages/privacy.mdx
+++ /dev/null
@@ -1,97 +0,0 @@
----
-layout: "@layouts/Settings.astro"
----
-
-import {
- ANALYTICS
-} from '@utils/GetConfig'
-
-import PrivacyPolicyAnalytics from '@components/text/PrivacyPolicyAnalytics.astro'
-import PrivacyPolicyCloudflare from '@components/text/PrivacyPolicyCloudflare.astro'
-import OptButtons from '@components/buttons/Telemtry.astro'
-
-## Privacy Policy
-
-### 3rd Party Services are Proxied
-
-Any service in MinPluto that is 3rd party is proxied by this instance or another instance such as images, videos, fonts, scripts, and more. No personal information information is collected and no information is sent to any 3rd party.
-
-
-
-
-
-### We Don't Know What You're Watching
-When you watch a video on a MinPluto instance with analytics enabled, the analytics software doesn't show what video are you watching. The query in the URL, which is the video ID, is not saved to analytics. What we see is only "/watch", not "/watch?=dQw4w9WgXcQ". This goes for anything else that may have a query in it.
-
-### Telemtry
-Telemtry data is used in MinPluto to see metrics of usage on different features and other aspects. This is help SudoVanilla, the developer of MinPluto, make better and informed decisions on what needs priority.
-
-By default, all users are opted-out for privacy purposes.
-
-If you decide to opt-in, the following is tracked:
-
-* Language
-* Browser Useragent
-* Operating System
-* Errors
-* Location
-* All settings, except for some
-* Platforms you've imported from
-
-The following will not be tracked:
-
-* Searches
-* Videos/Streams you watched
-* Subscribed Channels
-* Downloads
-* Shares
-* Embeds
-* Personal Information in your account, such as name and email
-* CSS/JS Customization
-* Themes
-
-Note that this can change at anytime.
-
-It is expected by the MinPluto developer that all telemtry data is sent to SudoVanilla's OpenPanel ananlytics. Note that the instance operator can easily change the destination of this, it is not expected.
-
-
-
-### Liability
-MinPluto and SudoVanilla take no responsibility for the use of our tool, or external instances provided by third parties. It is strongly recommended that you abide by the valid official regulations in your country. Furthermore, we refuse liability for any inappropriate use of MinPluto, such as illegal downloading.
-
-MinPluto is licenced under AGPL v3, this software is included with a copy.
-
-
-```
-Copyright (C) 2024 SudoVanilla
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see https://www.gnu.org/licenses/.
-```
-
-
-### Links to Other Sites
-
-Channels and videos on MinPluto may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by SudoVanilla, MinPluto, or the MinPluto instance. Therefore, I strongly advise you to review the privacy policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
-
-### Changes to This Privacy Policy
-
-The developers of MinPluto, SudoVanilla, may update our privacy policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new privacy policy on this page.
-
-This also goes for the instance itself.
-
-This policy is effective as of March 7th, 2024.
-
-### Contact Us
-
-If you have any questions or suggestions about MinPluto's privacy policy, do not hesitate to contact me at hello@minpluto.org or to [submit an issue](https://ark.sudovanilla.org/MinPluto/MinPluto/issues).
\ No newline at end of file
diff --git a/source/src/pages/register.astro b/source/src/pages/register.astro
deleted file mode 100755
index b2acfcd..0000000
--- a/source/src/pages/register.astro
+++ /dev/null
@@ -1,66 +0,0 @@
----
-import i18next,{ t, changeLanguage } from "i18next";
-import Base from "@layouts/Default.astro";
-
-import '@styles/form.scss'
----
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/search.astro b/source/src/pages/search.astro
deleted file mode 100755
index 80d4644..0000000
--- a/source/src/pages/search.astro
+++ /dev/null
@@ -1,142 +0,0 @@
----
-// Layout
-import Base from "@layouts/Default.astro"
-
-// i18n
-import { t, changeLanguage } from "i18next"
-
-// Configuration
-import { DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY, DEFAULT_STREAM_DATA_PROXY } from '@utils/GetConfig'
-
-// Components
-import Video from '@components/VideoItem.astro'
-import DiscoverChannel from "@components/DiscoverChannel.astro"
-import Stream from "@components/search/Stream.astro"
-import { Group, Movie, Play, PlaylistPlay, ReportColumns, VideoCamera } from "@iconoir/vue"
-
-// Fetch
-const SearchQueryWithParameters = Astro.url.href.split("&?").shift()
-const SearchQuery = SearchQueryWithParameters.split("search?query=").pop()
-
-// Check Filters
-if (Astro.url.href.includes('?type=channel')) {
- var ShowChannels = true
-}
-else if (Astro.url.href.includes('?type=playlist')) {
- var ShowPlaylists = true
-}
-else if (Astro.url.href.includes('?type=stream')) {
- var ShowStreams = true
-}
-else if (Astro.url.href.includes('?type=all')) {
- var ShowAll = true
-}
-else {
- var ShowVideos = true
-}
-
-/// Videos (YouTube)
-const VideoSearchResults = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/search?q=" + SearchQuery + '&page=1&date=none&type=video&duration=none&sort=relevance')
-const VideoSearch = await VideoSearchResults.json()
-
-/// Playlists (YouTube)
-const PlaylistsSearchResults = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/search?q=" + SearchQuery + '&page=1&date=none&type=playlist&duration=none&sort=relevance')
-const PlaylistsSearch = await PlaylistsSearchResults.json()
-
-// Streams (Twitch)
-const StreamSearchResults = await fetch(DEFAULT_STREAM_DATA_PROXY + "/api/search/?query=" + SearchQuery)
-const StreamSearch = await StreamSearchResults.json()
-
-/// Channels (YouTube)
-const ChannelSearchResults = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/search?q=" + SearchQuery + '&page=1&date=none&type=channel&duration=none&sort=relevance')
-const ChannelSearch = await ChannelSearchResults.json()
----
-
-
-
-
- {ShowVideos ? : null}
- {ShowPlaylists ? : null}
- {ShowStreams ? : null}
- {ShowChannels ? : null}
-
- {ShowVideos ?
- VideoSearch.map((video) =>
-
- )
- :
- null
- }
- {ShowPlaylists ?
- PlaylistsSearch.map((playlist) =>
-
{playlist.title}
- )
- :
- null
- }
- {ShowChannels ?
- ChannelSearch.map((channel) =>
-
- )
- :
- null
- }
- {ShowStreams ?
- StreamSearch.data.relatedChannels.map((channel) =>
-
- )
- :
- null
- }
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/telemtry.mdx b/source/src/pages/telemtry.mdx
deleted file mode 100755
index d190776..0000000
--- a/source/src/pages/telemtry.mdx
+++ /dev/null
@@ -1,38 +0,0 @@
----
-layout: '@layouts/Settings.astro'
----
-
-import OptButtons from '@components/buttons/Telemtry.astro'
-
-MinPluto Telemtry
------------------
-
-Telemtry data is used in MinPluto to see metrics of usage on different features and other aspects. This is to help SudoVanilla, the developer of MinPluto, make better and informed decisions on what needs priority.
-
-By default, you're opted-out for privacy purposes.
-
-If you decide to opt-in, the following is tracked:
-
-* Language
-* Browser Useragent
-* Operating System
-* Errors
-* Location
-* All settings, except for some
-* Platforms you've imported from
-
-The following will not be tracked:
-
-* Searches
-* Videos/Streams you watched
-* Subscribed Channels
-* Downloads
-* Shares
-* Embeds
-* Personal Information in your account, such as name and email
-* CSS/JS Customization
-* Themes
-
-Note that this can change at anytime.
-
-
\ No newline at end of file
diff --git a/source/src/pages/vp.astro b/source/src/pages/vp.astro
deleted file mode 100755
index ac1c9a1..0000000
--- a/source/src/pages/vp.astro
+++ /dev/null
@@ -1,19 +0,0 @@
----
-import Base from "@layouts/Default.astro";
-import Player from "@components/video-player/Player.astro"
----
-
-
-
-
-
-
\ No newline at end of file
diff --git a/source/src/pages/watch.astro b/source/src/pages/watch.astro
deleted file mode 100755
index b06a6f8..0000000
--- a/source/src/pages/watch.astro
+++ /dev/null
@@ -1,272 +0,0 @@
----
-import { t, changeLanguage } from "i18next"
-import Base from "@layouts/Default.astro"
-import "@styles/video.scss"
-
-// Configuration
-import { DEFAULT_MEDIA_PROXY, DEFAULT_MEDIA_DATA_PROXY, DEFAULT_IMAGE_PROXY, SERVER_DOMAIN } from '@utils/GetConfig'
-import { Donate, Download, ShareIos, ThumbsUp, MediaVideo } from "@iconoir/vue"
-
-// Components
-import Dialog from '@components/Dialog.astro'
-import Video from '@components/VideoItem.astro'
-import { Zorn } from "@minpluto/zorn"
-
-// Fetch
-const SWV = Astro.url.href.split("watch?v=").pop();
-const video = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/videos/" + SWV).then((response) => response.json());
-const comments = await fetch(DEFAULT_MEDIA_DATA_PROXY + "/api/v1/comments/" + SWV).then((response) => response.json());
-const Description = video.description;
-const UploadDate = video.published;
-const Views = video.viewCount;
-const VideoSeconds = video.lengthSeconds;
-let DescriptionFormat = Description.replaceAll("\n", " ");
-
-var CheckComments = console.log(comments) // If not found, disable comment section
-
-if (CheckComments = "{ error: 'Comments not found.' }") {
- var EnableComments = false
-} else {
- var EnableComments = true
-}
-
-
-// Format Published Date
-const DateFormat = new Date(UploadDate * 1000).toLocaleDateString();
-
-// Format Video Length
-// Thanks to "mingjunlu" for helping out with the time format
-new Date(VideoSeconds * 1000)
- .toISOString()
- .slice(14, 19)
- .split(":")
- .map(Number)
- .join(":");
-
-// Format Views
-const ViewsConversion = Intl.NumberFormat("en", { notation: "compact" });
-let ViewsFormat = ViewsConversion.format(Views);
-
-// Quality Check
-const EightKCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=571')
-if (EightKCheck.status == 200) {
- var EightK = true
-} else {
- var EightK = false
-}
-
-const FourKCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=313')
-if (FourKCheck.status == 200) {
- var FourK = true
-} else {
- var FourK = false
-}
-
-const TenEightyCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=303')
-if (TenEightyCheck.status == 200) {
- var TenEighty = true
-} else {
- var TenEighty = false
-}
-
-const ThreeSixtyCheck = await fetch(DEFAULT_MEDIA_DATA_PROXY + '/latest_version?id=' + video.videoId + '&itag=134')
-if (ThreeSixtyCheck.status == 200) {var ThreeSixty = true}
-
-if (EightK === true) { // 571
- var Quality = '571'
-} else if (FourK === true) { // 313
- var Quality = '313'
-} else if (TenEighty === true) { // 137
- var Quality = '303'
-} else if (ThreeSixty === true) { // 134
- var Quality = '134'
-}
----
-
-
-
-
-
-
{video.title}
-
-
-
{ViewsFormat} Views - {DateFormat}
-
-
-
-
-
-
-
{t("WATCH.RELATED")}
- {
- video.recommendedVideos.map((data) => (
-
- ))
- }
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/source/src/public/demo-media/480.mp4 b/source/src/public/demo-media/480.mp4
deleted file mode 100755
index 8971f9f..0000000
Binary files a/source/src/public/demo-media/480.mp4 and /dev/null differ
diff --git a/source/src/public/demo-media/720.mp4 b/source/src/public/demo-media/720.mp4
deleted file mode 100755
index f9bb949..0000000
Binary files a/source/src/public/demo-media/720.mp4 and /dev/null differ
diff --git a/source/src/public/locales/en/translation.json b/source/src/public/locales/en/translation.json
deleted file mode 100755
index 47d68a2..0000000
--- a/source/src/public/locales/en/translation.json
+++ /dev/null
@@ -1,86 +0,0 @@
-{
- "GLOBAL": {
- "MINPLUTO": "MinPluto"
- },
- "SIDEBAR": {
- "HOME": "Home",
- "TRENDING": "Trending",
- "CATEGORY_LIST": {
- "POPULAR": "Popular",
- "TRAILERS": "Trailers",
- "MUSIC": "Music",
- "GAMES": "Games"
- },
- "DISCOVER": "Discover",
- "DISCOVER_LIST": {
- "TECH": "Tech",
- "COMEDY": "Comedy",
- "FOOD": "Food",
- "GAMES": "Games",
- "FITNESS": "Fitness"
- },
- "FOOTER": {
- "ALPHA": "You're in Alpha",
- "STATUS": "Status",
- "FORUM": "Forum",
- "SOURCE_CODE": "Source Code"
- },
- "ACCOUNT": {
- "CUSTOMIZATION": "",
- "VIDEO_PLAYER": "",
- "CSS/JS": "",
- "ACCOUNT": "",
- "RESET": "",
- "BACKUP_RESTORE": "",
- "DELETE_ACCOUNT": "",
- "HEADERS": {
- "SETTINGS": "Settings",
- "YOU": "You",
- "ADVANCE": "Advance"
- }
- }
- },
- "HEADER": {
- "SEARCH": "Search",
- "FEEDBACK": "Feedback"
- },
- "FORM": {
- "EMAIL": "Email",
- "PASSWORD": "Password",
- "USERNAME": "Username",
- "CREATE_ACCOUNT": "Create Account",
- "FORGOT_PASSWORD": "Forgot Password",
- "LOGIN": "Login",
- "REGISTER": "Register",
- "UPDATE": "Update",
- "SUBMIT": "Submit"
- },
- "TELEMTRY": {
- "OPTIN": "Opt-In",
- "OPTOUT": "Opt-Out"
- },
- "HOME": {
- "P1": "Select a category or search something to get started.",
- "P2": "Currently, MinPluto is in alpha. Features are currently barebones or may not work as expected."
- },
- "WATCH": {
- "BY": "By",
- "VIEWS": "Views",
- "RELATED": "Related Videos",
- "COMMENTS": "Comments"
- },
- "CHANNELS": {
- "FAMILY_FRIENDLY": "Family Friendly",
- "HOME": "Home",
- "VIDEOS": "Videos",
- "COMMUNITY": "Community",
- "LATEST": "Latest Videos",
- "ABOUT": "About"
- },
- "MUSIC": {
- "TITLE": "Title",
- "ARTIST": "Artist",
- "UPLOADED": "Uploaded",
- "DURATION": "Duration"
- }
-}
\ No newline at end of file
diff --git a/source/src/public/manifest.json b/source/src/public/manifest.json
deleted file mode 100755
index f3a7ecc..0000000
--- a/source/src/public/manifest.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "name": "MinPluto",
- "short_name": "MinPluto",
- "description": "An open source frontend alternative to YouTube.",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#000000",
- "theme_color": "#000000",
- "categories": [
- "entertainment",
- "music"
- ],
- "dir": "ltr",
- "orientation": "any",
- "display_override": [
- "window-controls-overlay"
- ],
- "icons": [
- {
- "src": "/images/pwa/MinPluto - PWA Logo.png",
- "sizes": "512x512",
- "type": "image/png"
- }
- ],
- "screenshots": [
- {
- "src": "/images/pwa/Mobile.png",
- "type": "image/png",
- "sizes": "527x1004",
- "form_factor": "narrow"
- }
- ]
-}
\ No newline at end of file
diff --git a/source/src/public/player/controller.js b/source/src/public/player/controller.js
deleted file mode 100755
index f5fac46..0000000
--- a/source/src/public/player/controller.js
+++ /dev/null
@@ -1,193 +0,0 @@
-var VideoContainer = document.querySelector('.video-container')
-var VideoControls = document.querySelector('.video-controls')
-var Player = document.querySelector('video')
-
-// Icons
-var play_solid_default = ' ';
-var pause_solid_default = ' ';
-var PlayIcon = play_solid_default;
-var PauseIcon = pause_solid_default;
-
-// Fullscreen
-function Fullscreen() {
- const Button_Fullscreen = document.getElementById("vc-fullscreen");
- function Toggle_Fullscreen() {
- if (document.fullscreenElement) {
- document.querySelector('.vc-top').style.opacity = '0'
- document.exitFullscreen();
- } else if (document.webkitFullscreenElement) {
- document.querySelector('.vc-top').style.opacity = '0'
- document.webkitExitFullscreen();
- } else if (VideoContainer.webkitRequestFullscreen) {
- document.querySelector('.vc-top').style.opacity = '1'
- VideoContainer.webkitRequestFullscreen();
- } else {
- document.querySelector('.vc-top').style.opacity = '1'
- VideoContainer.requestFullscreen();
- }
- }
- Button_Fullscreen.onclick = Toggle_Fullscreen;
- function Update_FullscreenButton() {
- if (document.fullscreenElement) {
- Button_Fullscreen.setAttribute("data-title", "Exit full screen (f)");
- } else {
- Button_Fullscreen.setAttribute("data-title", "Full screen (f)");
- }
- }
- Player.addEventListener("dblclick", () => {
- Toggle_Fullscreen()
- Update_FullscreenButton()
- });
-}
-
-// Play/Pause
-function PlayPause() {
- const Button_PlayPause = document.querySelector(".video-controls #vc-playpause");
- Button_PlayPause.addEventListener("click", Toggle_PlayPause);
- Player.addEventListener("click", Toggle_PlayPause);
- Player.addEventListener("play", Update_PlayPauseButton);
- Player.addEventListener("pause", Update_PlayPauseButton);
- function Toggle_PlayPause() {
- if (Player.paused || Player.ended) {
- Player.play();
- } else {
- Player.pause();
- }
- }
- function Update_PlayPauseButton() {
- if (Player.paused) {
- Button_PlayPause.setAttribute("data-title", "Play (K)");
- Button_PlayPause.innerHTML = `${PlayIcon}`;
- } else {
- Button_PlayPause.setAttribute("data-title", "Pause (K)");
- Button_PlayPause.innerHTML = `${PauseIcon}`;
- }
- }
-}
-
-// Skip Around
-function SkipAround() {
- const Button_SkipBack = document.querySelector(".video-controls #vc-backwards");
- const Button_SkipForth = document.querySelector(".video-controls #vc-forwards");
- Button_SkipBack.addEventListener("click", Toggle_SkipBack);
- Button_SkipForth.addEventListener("click", Toggle_SkipForth);
- function Toggle_SkipBack() {
- Skip(-10);
- }
- function Toggle_SkipForth() {
- Skip(10);
- }
- function Skip(value) {
- Player.currentTime += value;
- }
-}
-
-// Hide Controls
-function AutoToggleControls() {
- function Hide_Controls2() {
- if (Player.paused) {
- return;
- } else {
- document.querySelector(".video-controls").classList.add("hide");
- }
- }
- function Show_Controls2() {
- document.querySelector(".video-controls").classList.remove("hide");
- }
- VideoControls.addEventListener("mouseenter", Show_Controls2);
- VideoControls.addEventListener("mouseleave", Hide_Controls2);
- var mouseTimer = null, cursorVisible = true;
- function Hide_Cursor() {
- mouseTimer = null;
- VideoContainer.style.cursor = "none";
- cursorVisible = false;
- Hide_Controls2();
- }
- document.onmousemove = function () {
- if (mouseTimer) {
- window.clearTimeout(mouseTimer);
- Show_Controls2();
- }
- if (!cursorVisible) {
- VideoContainer.style.cursor = "default";
- cursorVisible = true;
- }
- mouseTimer = window.setTimeout(Hide_Cursor, 3200);
- };
-}
-
-// Keyboard Shortcuts
-function KeyboardShortcuts(events) {
- if (Player.hasAttribute("keyboard-shortcut-fullscreen")) {
- var Fullscreen_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-fullscreen");
- } else {
- var Fullscreen_KeyboardShortcut = "f";
- }
- if (Player.hasAttribute("keyboard-shortcut-mute")) {
- var Mute_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-mute");
- } else {
- var Mute_KeyboardShortcut = "m";
- }
- if (Player.hasAttribute("keyboard-shortcut-playpause")) {
- var PlayPause_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-playpause");
- } else {
- var PlayPause_KeyboardShortcut = "k";
- }
- if (Player.hasAttribute("keyboard-shortcut-skipback")) {
- var SkipBack_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-skipback");
- } else {
- var SkipBack_KeyboardShortcut = "j";
- }
- if (Player.hasAttribute("keyboard-shortcut-skipforth")) {
- var SkipForth_KeyboardShortcut = Player.getAttribute("keyboard-shortcut-skipforth");
- } else {
- var SkipForth_KeyboardShortcut = "l";
- }
- function keyboardShortcuts(event) {
- const { key } = event;
- if (key === PlayPause_KeyboardShortcut) {
- if (Player.paused || Player.ended) {
- Player.play();
- } else {
- Player.pause();
- }
- if (Player.paused) {
- Show_Controls();
- } else {
- setTimeout(() => {
- Hide_Controls();
- }, 1200);
- }
- } else if (key === Mute_KeyboardShortcut) {
- Player.muted = !Player.muted;
- if (Player.muted) {
- volume.setAttribute("data-volume", volume.value);
- volume.value = 0;
- } else {
- volume.value = volume.dataset.volume;
- }
- } else if (key === Fullscreen_KeyboardShortcut) {
- if (document.fullscreenElement) {
- document.exitFullscreen();
- } else if (document.webkitFullscreenElement) {
- document.webkitExitFullscreen();
- } else if (VideoContainer.webkitRequestFullscreen) {
- VideoContainer.webkitRequestFullscreen();
- } else {
- VideoContainer.requestFullscreen();
- }
- } else if (key === SkipBack_KeyboardShortcut) {
- Player.currentTime += -10;
- } else if (key === SkipForth_KeyboardShortcut) {
- Player.currentTime += 10;
- }
- }
- document.addEventListener("keyup", keyboardShortcuts);
-}
-
-// Init All Functions
-AutoToggleControls()
-Fullscreen()
-KeyboardShortcuts()
-PlayPause()
-SkipAround()
\ No newline at end of file
diff --git a/source/src/public/player/seek.js b/source/src/public/player/seek.js
deleted file mode 100755
index a879732..0000000
--- a/source/src/public/player/seek.js
+++ /dev/null
@@ -1,81 +0,0 @@
-function Seek() {
- var Player = document.querySelector('video')
- const timeElapsed = document.getElementById("current");
- const duration = document.getElementById("duration");
- function formatTime(timeInSeconds) {
- const result = new Date(timeInSeconds * 1e3)
- .toISOString()
- .substr(11, 8);
- return {
- minutes: result.substr(3, 2),
- seconds: result.substr(6, 2),
- };
- }
- function initializeVideo() {
- const videoDuration = Math.round(Player.duration);
- const time = formatTime(videoDuration);
- duration.innerText = `${time.minutes}:${time.seconds}`;
- duration.setAttribute(
- "datetime",
- `${time.minutes}m ${time.seconds}s`,
- );
- }
- Player.addEventListener("loadedmetadata", initializeVideo);
- function updateTimeElapsed() {
- const time = formatTime(Math.round(Player.currentTime));
- timeElapsed.innerText = `${time.minutes}:${time.seconds}`;
- timeElapsed.setAttribute(
- "datetime",
- `${time.minutes}m ${time.seconds}s`,
- );
- }
- Player.addEventListener("timeupdate", updateTimeElapsed);
- const progressBar = document.querySelector(".vc-progress-bar");
- const seek = document.getElementById("seek");
- function initializeVideo() {
- const videoDuration = Math.round(Player.duration);
- seek.setAttribute("max", videoDuration);
- progressBar.setAttribute("max", videoDuration);
- const time = formatTime(videoDuration);
- duration.innerText = `${time.minutes}:${time.seconds}`;
- duration.setAttribute(
- "datetime",
- `${time.minutes}m ${time.seconds}s`,
- );
- }
- function updateProgress() {
- seek.value = Math.floor(Player.currentTime);
- document.querySelector('.vc-progress-bar').style.width = Player.currentTime / Player.duration * 100 + '%'
- }
- Player.addEventListener("timeupdate", updateProgress);
- const seekTooltip = document.getElementById("seek-tooltip");
- function updateSeekTooltip(event) {
- const skipTo = Math.round(
- (event.offsetX / event.target.clientWidth) *
- parseInt(event.target.getAttribute("max"), 10),
- );
- seek.setAttribute("data-seek", skipTo);
- const t = formatTime(skipTo);
- seekTooltip.textContent = `${t.minutes}:${t.seconds}`;
- const rect = Player.getBoundingClientRect();
- seekTooltip.style.left = `${event.pageX - rect.left}px`;
- seekTooltip.style.opacity = '1'
- document.querySelector('.vc-progress-bar').style.width = Player.currentTime / Player.duration * 100 + '%'
- seek.addEventListener('mouseleave', () => {
- seekTooltip.style.opacity = '0'
- })
- }
- seek.addEventListener("mousemove", updateSeekTooltip);
- function skipAhead(event) {
- const skipTo = event.target.dataset.seek
- ? event.target.dataset.seek
- : event.target.value;
- Player.currentTime = skipTo;
- progressBar.value = skipTo;
- seek.value = skipTo;
- }
- seek.addEventListener("input", skipAhead);
-
- initializeVideo();
-}
-Seek();
\ No newline at end of file
diff --git a/source/src/public/player/sync.js b/source/src/public/player/sync.js
deleted file mode 100755
index abaf72d..0000000
--- a/source/src/public/player/sync.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// https://gist.github.com/michancio/59b9f3dc54b3ff4f6a84
-// Find elements
-var SyncVideo = document.querySelector(".main-video");
-var SyncAudio = document.querySelector(".main-audio");
-
-// Object for synchronization of multiple media/sources
-if (typeof window.MediaController === "function") {
- var controller = new MediaController();
- SyncVideo.controller = controller;
- SyncAudio.controller = controller;
-} else {
- controller = null;
-}
-
-// Run SyncAudio and SyncVideo simultaneously
-SyncVideo.addEventListener(
- "play",
- function () {
- if (!controller && SyncAudio.paused) {
- SyncAudio.play();
- }
- },
- false,
-);
-
-// Pause/Play and Buffering
-SyncVideo.addEventListener("waiting", () => {
- // If SyncVideo is buffering
- SyncAudio.pause();
-});
-SyncVideo.addEventListener("playing", () => {
- // If SyncVideo is done buffering
- SyncAudio.play();
- SyncTimestamp();
-});
-
-SyncVideo.addEventListener(
- "pause",
- function () {
- if (!controller && !SyncAudio.paused) {
- SyncAudio.pause();
- }
- },
- false,
-);
-
-// When Media Ends
-SyncVideo.addEventListener(
- "ended",
- function () {
- if (controller) {
- controller.pause();
- } else {
- SyncVideo.pause();
- SyncAudio.pause();
- }
- },
- false,
-);
-
-// Seekbar
-function SyncTimestamp() {
- SyncAudio.currentTime = SyncVideo.currentTime;
-}
\ No newline at end of file
diff --git a/source/src/public/robots.txt b/source/src/public/robots.txt
deleted file mode 100755
index 67428a8..0000000
--- a/source/src/public/robots.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-User-agent: Applebot
-User-agent: Googlebot
-User-agent: bingbot
-User-agent: Yandex
-User-agent: Yeti
-User-agent: Baiduspider
-User-agent: 360Spider
-User-agent: *
-Disallow: /
\ No newline at end of file
diff --git a/source/src/public/scripts/aptabase/web/dist/index.cjs b/source/src/public/scripts/aptabase/web/dist/index.cjs
deleted file mode 100755
index ba69c9c..0000000
--- a/source/src/public/scripts/aptabase/web/dist/index.cjs
+++ /dev/null
@@ -1,2 +0,0 @@
-"use strict";var r=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var w=Object.prototype.hasOwnProperty;var y=(e,n)=>{for(var t in n)r(e,t,{get:n[t],enumerable:!0})},S=(e,n,t,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let s of h(n))!w.call(e,s)&&s!==t&&r(e,s,{get:()=>n[s],enumerable:!(i=v(n,s))||i.enumerable});return e};var I=e=>S(r({},"__esModule",{value:!0}),e);var k={};y(k,{init:()=>T,trackEvent:()=>$});module.exports=I(k);var A=V(),E=N(),D=typeof window<"u"&&typeof window.fetch<"u",O=typeof chrome<"u"&&!!chrome.runtime?.id,p=u(),d=new Date,l={US:"https://us.aptabase.com",EU:"https://eu.aptabase.com",DEV:"http://localhost:3000",SH:""};function c(e){let n=new Date,t=n.getTime()-d.getTime();return Math.floor(t/1e3)>e&&(p=u()),d=n,p}function u(){let e=Math.floor(Date.now()/1e3).toString(),n=Math.floor(Math.random()*1e8).toString().padStart(8,"0");return e+n}function g(e){let n=e.split("-");return n.length!==3||l[n[1]]===void 0?(console.warn(`The Aptabase App Key "${e}" is invalid. Tracking will be disabled.`),!1):!0}function f(e,n){let t=e.split("-")[1];if(t==="SH"){if(!n?.host){console.warn("Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.");return}return`${n.host}/api/v0/event`}return`${n?.host??l[t]}/api/v0/event`}async function b(e){if(!D&&!O){console.warn(`Aptabase: trackEvent requires a browser environment. Event "${e.eventName}" will be discarded.`);return}if(!e.appKey){console.warn(`Aptabase: init must be called before trackEvent. Event "${e.eventName}" will be discarded.`);return}try{let n=await fetch(e.apiUrl,{method:"POST",headers:{"Content-Type":"application/json","App-Key":e.appKey},credentials:"omit",body:JSON.stringify({timestamp:new Date().toISOString(),sessionId:e.sessionId,eventName:e.eventName,systemProps:{locale:e.locale??A,isDebug:e.isDebug??E,appVersion:e.appVersion??"",sdkVersion:e.sdkVersion},props:e.props})});if(n.status>=300){let t=await n.text();console.warn(`Failed to send event "${e.eventName}": ${n.status} ${t}`)}}catch(n){console.warn(`Failed to send event "${e.eventName}"`),console.warn(n)}}function V(){if(!(typeof navigator>"u"))return navigator.languages.length>0?navigator.languages[0]:navigator.language}function N(){return process.env.NODE_ENV==="development"?!0:typeof location>"u"?!1:location.hostname==="localhost"}var x=1*60*60,U="aptabase-web@0.4.2",m="",o,a;function T(e,n){g(e)&&(o=n?.apiUrl??f(e,n),m=e,a=n)}async function $(e,n){if(!o)return;let t=c(x);await b({apiUrl:o,sessionId:t,appKey:m,isDebug:a?.isDebug,appVersion:a?.appVersion,sdkVersion:U,eventName:e,props:n})}0&&(module.exports={init,trackEvent});
-//# sourceMappingURL=index.cjs.map
\ No newline at end of file
diff --git a/source/src/public/scripts/aptabase/web/dist/index.cjs.map b/source/src/public/scripts/aptabase/web/dist/index.cjs.map
deleted file mode 100755
index a8a363f..0000000
--- a/source/src/public/scripts/aptabase/web/dist/index.cjs.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../src/index.ts","../../shared.ts"],"sourcesContent":["import { getApiUrl, inMemorySessionId, sendEvent, validateAppKey, type AptabaseOptions } from '../../shared';\n\n// Session expires after 1 hour of inactivity\nconst SESSION_TIMEOUT = 1 * 60 * 60;\nconst sdkVersion = `aptabase-web@${process.env.PKG_VERSION}`;\n\nlet _appKey = '';\nlet _apiUrl: string | undefined;\nlet _options: AptabaseOptions | undefined;\n\nexport { type AptabaseOptions };\n\nexport function init(appKey: string, options?: AptabaseOptions) {\n if (!validateAppKey(appKey)) return;\n\n _apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);\n _appKey = appKey;\n _options = options;\n}\n\nexport async function trackEvent(eventName: string, props?: Record): Promise {\n if (!_apiUrl) return;\n\n const sessionId = inMemorySessionId(SESSION_TIMEOUT);\n\n await sendEvent({\n apiUrl: _apiUrl,\n sessionId,\n appKey: _appKey,\n isDebug: _options?.isDebug,\n appVersion: _options?.appVersion,\n sdkVersion,\n eventName,\n props,\n });\n}\n","const defaultLocale = getBrowserLocale();\nconst defaultIsDebug = getIsDebug();\nconst isInBrowser = typeof window !== 'undefined' && typeof window.fetch !== 'undefined';\nconst isInBrowserExtension = typeof chrome !== 'undefined' && !!chrome.runtime?.id;\n\nlet _sessionId = newSessionId();\nlet _lastTouched = new Date();\n\nconst _hosts: { [region: string]: string } = {\n US: 'https://us.aptabase.com',\n EU: 'https://eu.aptabase.com',\n DEV: 'http://localhost:3000',\n SH: '',\n};\n\nexport type AptabaseOptions = {\n // Custom host for self-hosted Aptabase.\n host?: string;\n // Custom path for API endpoint. Useful when using reverse proxy.\n apiUrl?: string;\n // Defines the app version.\n appVersion?: string;\n // Defines whether the app is running on debug mode.\n isDebug?: boolean;\n};\n\nexport function inMemorySessionId(timeout: number): string {\n let now = new Date();\n const diffInMs = now.getTime() - _lastTouched.getTime();\n const diffInSec = Math.floor(diffInMs / 1000);\n if (diffInSec > timeout) {\n _sessionId = newSessionId();\n }\n _lastTouched = now;\n\n return _sessionId;\n}\n\nexport function newSessionId(): string {\n const epochInSeconds = Math.floor(Date.now() / 1000).toString();\n const random = Math.floor(Math.random() * 100000000)\n .toString()\n .padStart(8, '0');\n\n return epochInSeconds + random;\n}\n\nexport function validateAppKey(appKey: string): boolean {\n const parts = appKey.split('-');\n if (parts.length !== 3 || _hosts[parts[1]] === undefined) {\n console.warn(`The Aptabase App Key \"${appKey}\" is invalid. Tracking will be disabled.`);\n return false;\n }\n return true;\n}\n\nexport function getApiUrl(appKey: string, options?: AptabaseOptions): string | undefined {\n const region = appKey.split('-')[1];\n if (region === 'SH') {\n if (!options?.host) {\n console.warn(`Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`);\n return;\n }\n\n return `${options.host}/api/v0/event`;\n }\n\n const host = options?.host ?? _hosts[region];\n return `${host}/api/v0/event`;\n}\n\nexport async function sendEvent(opts: {\n apiUrl: string;\n appKey?: string;\n sessionId: string;\n locale?: string;\n isDebug?: boolean;\n appVersion?: string;\n sdkVersion: string;\n eventName: string;\n props?: Record;\n}): Promise {\n if (!isInBrowser && !isInBrowserExtension) {\n console.warn(`Aptabase: trackEvent requires a browser environment. Event \"${opts.eventName}\" will be discarded.`);\n return;\n }\n\n if (!opts.appKey) {\n console.warn(`Aptabase: init must be called before trackEvent. Event \"${opts.eventName}\" will be discarded.`);\n return;\n }\n\n try {\n const response = await fetch(opts.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'App-Key': opts.appKey,\n },\n credentials: 'omit',\n body: JSON.stringify({\n timestamp: new Date().toISOString(),\n sessionId: opts.sessionId,\n eventName: opts.eventName,\n systemProps: {\n locale: opts.locale ?? defaultLocale,\n isDebug: opts.isDebug ?? defaultIsDebug,\n appVersion: opts.appVersion ?? '',\n sdkVersion: opts.sdkVersion,\n },\n props: opts.props,\n }),\n });\n\n if (response.status >= 300) {\n const responseBody = await response.text();\n console.warn(`Failed to send event \"${opts.eventName}\": ${response.status} ${responseBody}`);\n }\n } catch (e) {\n console.warn(`Failed to send event \"${opts.eventName}\"`);\n console.warn(e);\n }\n}\n\nfunction getBrowserLocale(): string | undefined {\n if (typeof navigator === 'undefined') {\n return undefined;\n }\n\n if (navigator.languages.length > 0) {\n return navigator.languages[0];\n }\n\n return navigator.language;\n}\n\nfunction getIsDebug(): boolean {\n if (process.env.NODE_ENV === 'development') {\n return true;\n }\n\n if (typeof location === 'undefined') {\n return false;\n }\n\n return location.hostname === 'localhost';\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,UAAAE,EAAA,eAAAC,IAAA,eAAAC,EAAAJ,GCAA,IAAMK,EAAgBC,EAAiB,EACjCC,EAAiBC,EAAW,EAC5BC,EAAc,OAAO,OAAW,KAAe,OAAO,OAAO,MAAU,IACvEC,EAAuB,OAAO,OAAW,KAAe,CAAC,CAAC,OAAO,SAAS,GAE5EC,EAAaC,EAAa,EAC1BC,EAAe,IAAI,KAEjBC,EAAuC,CAC3C,GAAI,0BACJ,GAAI,0BACJ,IAAK,wBACL,GAAI,EACN,EAaO,SAASC,EAAkBC,EAAyB,CACzD,IAAIC,EAAM,IAAI,KACRC,EAAWD,EAAI,QAAQ,EAAIJ,EAAa,QAAQ,EAEtD,OADkB,KAAK,MAAMK,EAAW,GAAI,EAC5BF,IACdL,EAAaC,EAAa,GAE5BC,EAAeI,EAERN,CACT,CAEO,SAASC,GAAuB,CACrC,IAAMO,EAAiB,KAAK,MAAM,KAAK,IAAI,EAAI,GAAI,EAAE,SAAS,EACxDC,EAAS,KAAK,MAAM,KAAK,OAAO,EAAI,GAAS,EAChD,SAAS,EACT,SAAS,EAAG,GAAG,EAElB,OAAOD,EAAiBC,CAC1B,CAEO,SAASC,EAAeC,EAAyB,CACtD,IAAMC,EAAQD,EAAO,MAAM,GAAG,EAC9B,OAAIC,EAAM,SAAW,GAAKT,EAAOS,EAAM,CAAC,CAAC,IAAM,QAC7C,QAAQ,KAAK,yBAAyBD,CAAM,0CAA0C,EAC/E,IAEF,EACT,CAEO,SAASE,EAAUF,EAAgBG,EAA+C,CACvF,IAAMC,EAASJ,EAAO,MAAM,GAAG,EAAE,CAAC,EAClC,GAAII,IAAW,KAAM,CACnB,GAAI,CAACD,GAAS,KAAM,CAClB,QAAQ,KAAK,2FAA2F,EACxG,MACF,CAEA,MAAO,GAAGA,EAAQ,IAAI,eACxB,CAGA,MAAO,GADMA,GAAS,MAAQX,EAAOY,CAAM,CAC7B,eAChB,CAEA,eAAsBC,EAAUC,EAUd,CAChB,GAAI,CAACnB,GAAe,CAACC,EAAsB,CACzC,QAAQ,KAAK,+DAA+DkB,EAAK,SAAS,sBAAsB,EAChH,MACF,CAEA,GAAI,CAACA,EAAK,OAAQ,CAChB,QAAQ,KAAK,2DAA2DA,EAAK,SAAS,sBAAsB,EAC5G,MACF,CAEA,GAAI,CACF,IAAMC,EAAW,MAAM,MAAMD,EAAK,OAAQ,CACxC,OAAQ,OACR,QAAS,CACP,eAAgB,mBAChB,UAAWA,EAAK,MAClB,EACA,YAAa,OACb,KAAM,KAAK,UAAU,CACnB,UAAW,IAAI,KAAK,EAAE,YAAY,EAClC,UAAWA,EAAK,UAChB,UAAWA,EAAK,UAChB,YAAa,CACX,OAAQA,EAAK,QAAUvB,EACvB,QAASuB,EAAK,SAAWrB,EACzB,WAAYqB,EAAK,YAAc,GAC/B,WAAYA,EAAK,UACnB,EACA,MAAOA,EAAK,KACd,CAAC,CACH,CAAC,EAED,GAAIC,EAAS,QAAU,IAAK,CAC1B,IAAMC,EAAe,MAAMD,EAAS,KAAK,EACzC,QAAQ,KAAK,yBAAyBD,EAAK,SAAS,MAAMC,EAAS,MAAM,IAAIC,CAAY,EAAE,CAC7F,CACF,OAASC,EAAG,CACV,QAAQ,KAAK,yBAAyBH,EAAK,SAAS,GAAG,EACvD,QAAQ,KAAKG,CAAC,CAChB,CACF,CAEA,SAASzB,GAAuC,CAC9C,GAAI,SAAO,UAAc,KAIzB,OAAI,UAAU,UAAU,OAAS,EACxB,UAAU,UAAU,CAAC,EAGvB,UAAU,QACnB,CAEA,SAASE,GAAsB,CAC7B,OAAI,QAAQ,IAAI,WAAa,cACpB,GAGL,OAAO,SAAa,IACf,GAGF,SAAS,WAAa,WAC/B,CD/IA,IAAMwB,EAAkB,EAAI,GAAK,GAC3BC,EAAa,qBAEfC,EAAU,GACVC,EACAC,EAIG,SAASC,EAAKC,EAAgBC,EAA2B,CACzDC,EAAeF,CAAM,IAE1BG,EAAUF,GAAS,QAAUG,EAAUJ,EAAQC,CAAO,EACtDI,EAAUL,EACVM,EAAWL,EACb,CAEA,eAAsBM,EAAWC,EAAmBC,EAAkE,CACpH,GAAI,CAACN,EAAS,OAEd,IAAMO,EAAYC,EAAkBC,CAAe,EAEnD,MAAMC,EAAU,CACd,OAAQV,EACR,UAAAO,EACA,OAAQL,EACR,QAASC,GAAU,QACnB,WAAYA,GAAU,WACtB,WAAAQ,EACA,UAAAN,EACA,MAAAC,CACF,CAAC,CACH","names":["src_exports","__export","init","trackEvent","__toCommonJS","defaultLocale","getBrowserLocale","defaultIsDebug","getIsDebug","isInBrowser","isInBrowserExtension","_sessionId","newSessionId","_lastTouched","_hosts","inMemorySessionId","timeout","now","diffInMs","epochInSeconds","random","validateAppKey","appKey","parts","getApiUrl","options","region","sendEvent","opts","response","responseBody","e","SESSION_TIMEOUT","sdkVersion","_appKey","_apiUrl","_options","init","appKey","options","validateAppKey","_apiUrl","getApiUrl","_appKey","_options","trackEvent","eventName","props","sessionId","inMemorySessionId","SESSION_TIMEOUT","sendEvent","sdkVersion"]}
\ No newline at end of file
diff --git a/source/src/public/scripts/aptabase/web/dist/index.d.cts b/source/src/public/scripts/aptabase/web/dist/index.d.cts
deleted file mode 100755
index becd530..0000000
--- a/source/src/public/scripts/aptabase/web/dist/index.d.cts
+++ /dev/null
@@ -1,11 +0,0 @@
-type AptabaseOptions = {
- host?: string;
- apiUrl?: string;
- appVersion?: string;
- isDebug?: boolean;
-};
-
-declare function init(appKey: string, options?: AptabaseOptions): void;
-declare function trackEvent(eventName: string, props?: Record): Promise;
-
-export { AptabaseOptions, init, trackEvent };
diff --git a/source/src/public/scripts/aptabase/web/dist/index.d.ts b/source/src/public/scripts/aptabase/web/dist/index.d.ts
deleted file mode 100755
index becd530..0000000
--- a/source/src/public/scripts/aptabase/web/dist/index.d.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-type AptabaseOptions = {
- host?: string;
- apiUrl?: string;
- appVersion?: string;
- isDebug?: boolean;
-};
-
-declare function init(appKey: string, options?: AptabaseOptions): void;
-declare function trackEvent(eventName: string, props?: Record): Promise;
-
-export { AptabaseOptions, init, trackEvent };
diff --git a/source/src/public/scripts/aptabase/web/dist/index.js b/source/src/public/scripts/aptabase/web/dist/index.js
deleted file mode 100755
index 91e60b6..0000000
--- a/source/src/public/scripts/aptabase/web/dist/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-var b=w(),m=y(),v=typeof window<"u"&&typeof window.fetch<"u",h=typeof chrome<"u"&&!!chrome.runtime?.id,r=d(),o=new Date,a={US:"https://us.aptabase.com",EU:"https://eu.aptabase.com",DEV:"http://localhost:3000",SH:""};function p(e){let n=new Date,t=n.getTime()-o.getTime();return Math.floor(t/1e3)>e&&(r=d()),o=n,r}function d(){let e=Math.floor(Date.now()/1e3).toString(),n=Math.floor(Math.random()*1e8).toString().padStart(8,"0");return e+n}function l(e){let n=e.split("-");return n.length!==3||a[n[1]]===void 0?(console.warn(`The Aptabase App Key "${e}" is invalid. Tracking will be disabled.`),!1):!0}function c(e,n){let t=e.split("-")[1];if(t==="SH"){if(!n?.host){console.warn("Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.");return}return`${n.host}/api/v0/event`}return`${n?.host??a[t]}/api/v0/event`}async function u(e){if(!v&&!h){console.warn(`Aptabase: trackEvent requires a browser environment. Event "${e.eventName}" will be discarded.`);return}if(!e.appKey){console.warn(`Aptabase: init must be called before trackEvent. Event "${e.eventName}" will be discarded.`);return}try{let n=await fetch(e.apiUrl,{method:"POST",headers:{"Content-Type":"application/json","App-Key":e.appKey},credentials:"omit",body:JSON.stringify({timestamp:new Date().toISOString(),sessionId:e.sessionId,eventName:e.eventName,systemProps:{locale:e.locale??b,isDebug:e.isDebug??m,appVersion:e.appVersion??"",sdkVersion:e.sdkVersion},props:e.props})});if(n.status>=300){let t=await n.text();console.warn(`Failed to send event "${e.eventName}": ${n.status} ${t}`)}}catch(n){console.warn(`Failed to send event "${e.eventName}"`),console.warn(n)}}function w(){if(!(typeof navigator>"u"))return navigator.languages.length>0?navigator.languages[0]:navigator.language}function y(){return process.env.NODE_ENV==="development"?!0:typeof location>"u"?!1:location.hostname==="localhost"}var S=1*60*60,I="aptabase-web@0.4.2",g="",s,i;function D(e,n){l(e)&&(s=n?.apiUrl??c(e,n),g=e,i=n)}async function O(e,n){if(!s)return;let t=p(S);await u({apiUrl:s,sessionId:t,appKey:g,isDebug:i?.isDebug,appVersion:i?.appVersion,sdkVersion:I,eventName:e,props:n})}export{D as init,O as trackEvent};
-//# sourceMappingURL=index.js.map
\ No newline at end of file
diff --git a/source/src/public/scripts/aptabase/web/dist/index.js.map b/source/src/public/scripts/aptabase/web/dist/index.js.map
deleted file mode 100755
index 0918d51..0000000
--- a/source/src/public/scripts/aptabase/web/dist/index.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"sources":["../../shared.ts","../src/index.ts"],"sourcesContent":["const defaultLocale = getBrowserLocale();\nconst defaultIsDebug = getIsDebug();\nconst isInBrowser = typeof window !== 'undefined' && typeof window.fetch !== 'undefined';\nconst isInBrowserExtension = typeof chrome !== 'undefined' && !!chrome.runtime?.id;\n\nlet _sessionId = newSessionId();\nlet _lastTouched = new Date();\n\nconst _hosts: { [region: string]: string } = {\n US: 'https://us.aptabase.com',\n EU: 'https://eu.aptabase.com',\n DEV: 'http://localhost:3000',\n SH: '',\n};\n\nexport type AptabaseOptions = {\n // Custom host for self-hosted Aptabase.\n host?: string;\n // Custom path for API endpoint. Useful when using reverse proxy.\n apiUrl?: string;\n // Defines the app version.\n appVersion?: string;\n // Defines whether the app is running on debug mode.\n isDebug?: boolean;\n};\n\nexport function inMemorySessionId(timeout: number): string {\n let now = new Date();\n const diffInMs = now.getTime() - _lastTouched.getTime();\n const diffInSec = Math.floor(diffInMs / 1000);\n if (diffInSec > timeout) {\n _sessionId = newSessionId();\n }\n _lastTouched = now;\n\n return _sessionId;\n}\n\nexport function newSessionId(): string {\n const epochInSeconds = Math.floor(Date.now() / 1000).toString();\n const random = Math.floor(Math.random() * 100000000)\n .toString()\n .padStart(8, '0');\n\n return epochInSeconds + random;\n}\n\nexport function validateAppKey(appKey: string): boolean {\n const parts = appKey.split('-');\n if (parts.length !== 3 || _hosts[parts[1]] === undefined) {\n console.warn(`The Aptabase App Key \"${appKey}\" is invalid. Tracking will be disabled.`);\n return false;\n }\n return true;\n}\n\nexport function getApiUrl(appKey: string, options?: AptabaseOptions): string | undefined {\n const region = appKey.split('-')[1];\n if (region === 'SH') {\n if (!options?.host) {\n console.warn(`Host parameter must be defined when using Self-Hosted App Key. Tracking will be disabled.`);\n return;\n }\n\n return `${options.host}/api/v0/event`;\n }\n\n const host = options?.host ?? _hosts[region];\n return `${host}/api/v0/event`;\n}\n\nexport async function sendEvent(opts: {\n apiUrl: string;\n appKey?: string;\n sessionId: string;\n locale?: string;\n isDebug?: boolean;\n appVersion?: string;\n sdkVersion: string;\n eventName: string;\n props?: Record;\n}): Promise {\n if (!isInBrowser && !isInBrowserExtension) {\n console.warn(`Aptabase: trackEvent requires a browser environment. Event \"${opts.eventName}\" will be discarded.`);\n return;\n }\n\n if (!opts.appKey) {\n console.warn(`Aptabase: init must be called before trackEvent. Event \"${opts.eventName}\" will be discarded.`);\n return;\n }\n\n try {\n const response = await fetch(opts.apiUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'App-Key': opts.appKey,\n },\n credentials: 'omit',\n body: JSON.stringify({\n timestamp: new Date().toISOString(),\n sessionId: opts.sessionId,\n eventName: opts.eventName,\n systemProps: {\n locale: opts.locale ?? defaultLocale,\n isDebug: opts.isDebug ?? defaultIsDebug,\n appVersion: opts.appVersion ?? '',\n sdkVersion: opts.sdkVersion,\n },\n props: opts.props,\n }),\n });\n\n if (response.status >= 300) {\n const responseBody = await response.text();\n console.warn(`Failed to send event \"${opts.eventName}\": ${response.status} ${responseBody}`);\n }\n } catch (e) {\n console.warn(`Failed to send event \"${opts.eventName}\"`);\n console.warn(e);\n }\n}\n\nfunction getBrowserLocale(): string | undefined {\n if (typeof navigator === 'undefined') {\n return undefined;\n }\n\n if (navigator.languages.length > 0) {\n return navigator.languages[0];\n }\n\n return navigator.language;\n}\n\nfunction getIsDebug(): boolean {\n if (process.env.NODE_ENV === 'development') {\n return true;\n }\n\n if (typeof location === 'undefined') {\n return false;\n }\n\n return location.hostname === 'localhost';\n}\n","import { getApiUrl, inMemorySessionId, sendEvent, validateAppKey, type AptabaseOptions } from '../../shared';\n\n// Session expires after 1 hour of inactivity\nconst SESSION_TIMEOUT = 1 * 60 * 60;\nconst sdkVersion = `aptabase-web@${process.env.PKG_VERSION}`;\n\nlet _appKey = '';\nlet _apiUrl: string | undefined;\nlet _options: AptabaseOptions | undefined;\n\nexport { type AptabaseOptions };\n\nexport function init(appKey: string, options?: AptabaseOptions) {\n if (!validateAppKey(appKey)) return;\n\n _apiUrl = options?.apiUrl ?? getApiUrl(appKey, options);\n _appKey = appKey;\n _options = options;\n}\n\nexport async function trackEvent(eventName: string, props?: Record): Promise {\n if (!_apiUrl) return;\n\n const sessionId = inMemorySessionId(SESSION_TIMEOUT);\n\n await sendEvent({\n apiUrl: _apiUrl,\n sessionId,\n appKey: _appKey,\n isDebug: _options?.isDebug,\n appVersion: _options?.appVersion,\n sdkVersion,\n eventName,\n props,\n });\n}\n"],"mappings":"AAAA,IAAMA,EAAgBC,EAAiB,EACjCC,EAAiBC,EAAW,EAC5BC,EAAc,OAAO,OAAW,KAAe,OAAO,OAAO,MAAU,IACvEC,EAAuB,OAAO,OAAW,KAAe,CAAC,CAAC,OAAO,SAAS,GAE5EC,EAAaC,EAAa,EAC1BC,EAAe,IAAI,KAEjBC,EAAuC,CAC3C,GAAI,0BACJ,GAAI,0BACJ,IAAK,wBACL,GAAI,EACN,EAaO,SAASC,EAAkBC,EAAyB,CACzD,IAAIC,EAAM,IAAI,KACRC,EAAWD,EAAI,QAAQ,EAAIJ,EAAa,QAAQ,EAEtD,OADkB,KAAK,MAAMK,EAAW,GAAI,EAC5BF,IACdL,EAAaC,EAAa,GAE5BC,EAAeI,EAERN,CACT,CAEO,SAASC,GAAuB,CACrC,IAAMO,EAAiB,KAAK,MAAM,KAAK,IAAI,EAAI,GAAI,EAAE,SAAS,EACxDC,EAAS,KAAK,MAAM,KAAK,OAAO,EAAI,GAAS,EAChD,SAAS,EACT,SAAS,EAAG,GAAG,EAElB,OAAOD,EAAiBC,CAC1B,CAEO,SAASC,EAAeC,EAAyB,CACtD,IAAMC,EAAQD,EAAO,MAAM,GAAG,EAC9B,OAAIC,EAAM,SAAW,GAAKT,EAAOS,EAAM,CAAC,CAAC,IAAM,QAC7C,QAAQ,KAAK,yBAAyBD,CAAM,0CAA0C,EAC/E,IAEF,EACT,CAEO,SAASE,EAAUF,EAAgBG,EAA+C,CACvF,IAAMC,EAASJ,EAAO,MAAM,GAAG,EAAE,CAAC,EAClC,GAAII,IAAW,KAAM,CACnB,GAAI,CAACD,GAAS,KAAM,CAClB,QAAQ,KAAK,2FAA2F,EACxG,MACF,CAEA,MAAO,GAAGA,EAAQ,IAAI,eACxB,CAGA,MAAO,GADMA,GAAS,MAAQX,EAAOY,CAAM,CAC7B,eAChB,CAEA,eAAsBC,EAAUC,EAUd,CAChB,GAAI,CAACnB,GAAe,CAACC,EAAsB,CACzC,QAAQ,KAAK,+DAA+DkB,EAAK,SAAS,sBAAsB,EAChH,MACF,CAEA,GAAI,CAACA,EAAK,OAAQ,CAChB,QAAQ,KAAK,2DAA2DA,EAAK,SAAS,sBAAsB,EAC5G,MACF,CAEA,GAAI,CACF,IAAMC,EAAW,MAAM,MAAMD,EAAK,OAAQ,CACxC,OAAQ,OACR,QAAS,CACP,eAAgB,mBAChB,UAAWA,EAAK,MAClB,EACA,YAAa,OACb,KAAM,KAAK,UAAU,CACnB,UAAW,IAAI,KAAK,EAAE,YAAY,EAClC,UAAWA,EAAK,UAChB,UAAWA,EAAK,UAChB,YAAa,CACX,OAAQA,EAAK,QAAUvB,EACvB,QAASuB,EAAK,SAAWrB,EACzB,WAAYqB,EAAK,YAAc,GAC/B,WAAYA,EAAK,UACnB,EACA,MAAOA,EAAK,KACd,CAAC,CACH,CAAC,EAED,GAAIC,EAAS,QAAU,IAAK,CAC1B,IAAMC,EAAe,MAAMD,EAAS,KAAK,EACzC,QAAQ,KAAK,yBAAyBD,EAAK,SAAS,MAAMC,EAAS,MAAM,IAAIC,CAAY,EAAE,CAC7F,CACF,OAASC,EAAG,CACV,QAAQ,KAAK,yBAAyBH,EAAK,SAAS,GAAG,EACvD,QAAQ,KAAKG,CAAC,CAChB,CACF,CAEA,SAASzB,GAAuC,CAC9C,GAAI,SAAO,UAAc,KAIzB,OAAI,UAAU,UAAU,OAAS,EACxB,UAAU,UAAU,CAAC,EAGvB,UAAU,QACnB,CAEA,SAASE,GAAsB,CAC7B,OAAI,QAAQ,IAAI,WAAa,cACpB,GAGL,OAAO,SAAa,IACf,GAGF,SAAS,WAAa,WAC/B,CC/IA,IAAMwB,EAAkB,EAAI,GAAK,GAC3BC,EAAa,qBAEfC,EAAU,GACVC,EACAC,EAIG,SAASC,EAAKC,EAAgBC,EAA2B,CACzDC,EAAeF,CAAM,IAE1BG,EAAUF,GAAS,QAAUG,EAAUJ,EAAQC,CAAO,EACtDI,EAAUL,EACVM,EAAWL,EACb,CAEA,eAAsBM,EAAWC,EAAmBC,EAAkE,CACpH,GAAI,CAACN,EAAS,OAEd,IAAMO,EAAYC,EAAkBC,CAAe,EAEnD,MAAMC,EAAU,CACd,OAAQV,EACR,UAAAO,EACA,OAAQL,EACR,QAASC,GAAU,QACnB,WAAYA,GAAU,WACtB,WAAAQ,EACA,UAAAN,EACA,MAAAC,CACF,CAAC,CACH","names":["defaultLocale","getBrowserLocale","defaultIsDebug","getIsDebug","isInBrowser","isInBrowserExtension","_sessionId","newSessionId","_lastTouched","_hosts","inMemorySessionId","timeout","now","diffInMs","epochInSeconds","random","validateAppKey","appKey","parts","getApiUrl","options","region","sendEvent","opts","response","responseBody","e","SESSION_TIMEOUT","sdkVersion","_appKey","_apiUrl","_options","init","appKey","options","validateAppKey","_apiUrl","getApiUrl","_appKey","_options","trackEvent","eventName","props","sessionId","inMemorySessionId","SESSION_TIMEOUT","sendEvent","sdkVersion"]}
\ No newline at end of file
diff --git a/source/src/public/scripts/zorn.js b/source/src/public/scripts/zorn.js
deleted file mode 100755
index d8fe639..0000000
--- a/source/src/public/scripts/zorn.js
+++ /dev/null
@@ -1,778 +0,0 @@
-(() => {
- // src/assets/icons/play-solid.svg
- var play_solid_default = ' ';
-
- // src/assets/icons/pause-solid.svg
- var pause_solid_default = ' ';
-
- // src/assets/icons/maximize.svg
- var maximize_default = ' ';
-
- // src/assets/icons/closed-captions-tag.svg
- var closed_captions_tag_default = ' ';
-
- // src/assets/icons/backward15-seconds.svg
- var backward15_seconds_default = ' ';
-
- // src/assets/icons/forward15-seconds.svg
- var forward15_seconds_default = ' ';
-
- // src/assets/icons/sound-high.svg
- var sound_high_default = ' ';
-
- // src/assets/icons/sound-min.svg
- var sound_min_default = ' ';
-
- // src/assets/icons/sound-off.svg
- var sound_off_default = ' ';
-
- // src/assets/icons/refresh-double.svg
- var refresh_double_default = ' ';
-
- // src/get.js
- var ZornVideoPlayer = document.querySelector(".zorn-player");
- var VideoContainer = document.querySelector(".video-container");
- var VideoControls = document.querySelector(".zorn-player-controls");
- var PlayIcon = play_solid_default;
- var PauseIcon = pause_solid_default;
- var FullcreenIcon = maximize_default;
- var CaptionsIcon = closed_captions_tag_default;
- var Backward15Icon = backward15_seconds_default;
- var Forward15Icon = forward15_seconds_default;
- var VolumeHighIcon = sound_high_default;
- var VolumeMinIcon = sound_min_default;
- var VolumeOffIcon = sound_off_default;
- var RefreshIcon = refresh_double_default;
-
- // src/themes/default.js
- function Title() {
- let VideoTitle = ZornVideoPlayer.getAttribute("video-title");
- document.querySelector(".zorn-title").innerHTML = VideoTitle;
- if (ZornVideoPlayer.hasAttribute("video-title")) {
- document.querySelector(".zorn-title").style.display = "inherit";
- } else {
- document.querySelector(".zorn-title").style.display = "none";
- }
- }
- var Controls = `
-
-
-
-
-
${PlayIcon}
-
${Backward15Icon}
-
${Forward15Icon}
-
- ${VolumeHighIcon}
-
-
-
- 00:00
- /
- 00:00
-
-
-
-
-
- ${CaptionsIcon}
- ${FullcreenIcon}
-
-
-
-
- `;
- ZornVideoPlayer.insertAdjacentHTML("afterend", Controls);
- if (ZornVideoPlayer.getAttribute("layout") === "default") {
- Title();
- }
-
- // src/events.js
- function Events() {
- ZornVideoPlayer.addEventListener("error", function(event) {
- document.querySelector("#invalid-src").style.display = "inherit";
- document.querySelector(".zorn-player-controls").style.display = "none";
- videoContainer.style.backgroundColor = "#101010";
- setTimeout(() => {
- ZornVideoPlayer.style.opacity = "0.10";
- document.querySelector("#buffering").style.display = "none";
- }, 168);
- }, true);
- ZornVideoPlayer.onwaiting = (event) => {
- document.querySelector("#buffering").style.display = "inherit";
- ZornVideoPlayer.style.transition = "5s opacity";
- ZornVideoPlayer.style.opacity = "0.25";
- };
- ZornVideoPlayer.oncanplaythrough = (event) => {
- document.querySelector("#buffering").style.display = "none";
- ZornVideoPlayer.style.transition = "0.3s opacity";
- ZornVideoPlayer.style.opacity = "1";
- };
- }
-
- // src/functions/PlayPause.js
- function PlayPause() {
- const Button_PlayPause = document.querySelector(".zorn-player-controls #play-pause");
- Button_PlayPause.addEventListener("click", Toggle_PlayPause);
- ZornVideoPlayer.addEventListener("click", Toggle_PlayPause);
- ZornVideoPlayer.addEventListener("play", Update_PlayPauseButton);
- ZornVideoPlayer.addEventListener("pause", Update_PlayPauseButton);
- function Toggle_PlayPause() {
- if (ZornVideoPlayer.paused || ZornVideoPlayer.ended) {
- ZornVideoPlayer.play();
- } else {
- ZornVideoPlayer.pause();
- }
- }
- function Update_PlayPauseButton() {
- if (ZornVideoPlayer.paused) {
- Button_PlayPause.setAttribute("data-title", "Play (K)");
- Button_PlayPause.innerHTML = `${PlayIcon}`;
- } else {
- Button_PlayPause.setAttribute("data-title", "Pause (K)");
- Button_PlayPause.innerHTML = `${PauseIcon}`;
- }
- }
- }
-
- // src/functions/SkipAround.js
- function SkipAround() {
- const Button_SkipBack = document.querySelector(".zorn-player-controls #skip-back");
- const Button_SkipForth = document.querySelector(".zorn-player-controls #skip-forth");
- Button_SkipBack.addEventListener("click", Toggle_SkipBack);
- Button_SkipForth.addEventListener("click", Toggle_SkipForth);
- function Toggle_SkipBack() {
- Skip(-10);
- }
- function Toggle_SkipForth() {
- Skip(10);
- }
- function Skip(value) {
- ZornVideoPlayer.currentTime += value;
- }
- }
-
- // src/functions/Fullscreen.js
- function Fullscreen() {
- const Button_Fullscreen = document.getElementById("fullscreen");
- function Toggle_Fullscreen() {
- if (document.fullscreenElement) {
- document.exitFullscreen();
- } else if (document.webkitFullscreenElement) {
- document.webkitExitFullscreen();
- } else if (VideoContainer.webkitRequestFullscreen) {
- VideoContainer.webkitRequestFullscreen();
- } else {
- VideoContainer.requestFullscreen();
- }
- }
- Button_Fullscreen.onclick = Toggle_Fullscreen;
- function Update_FullscreenButton() {
- if (document.fullscreenElement) {
- Button_Fullscreen.setAttribute("data-title", "Exit full screen (f)");
- } else {
- Button_Fullscreen.setAttribute("data-title", "Full screen (f)");
- }
- }
- ZornVideoPlayer.addEventListener("dblclick", () => {
- Toggle_Fullscreen();
- });
- }
-
- // src/functions/Subtitles.js
- function Subtitles() {
- var subtitles = document.querySelector('.zorn-player-controls #subtitles')
- var subtitleMenuButtons = []
- var createMenuItem = function(id, lang, label) {
- var listItem = document.createElement('li')
- var button = listItem.appendChild(document.createElement('button'))
- button.setAttribute('id', id)
- button.className = 'subtitles-button'
- if (lang.length > 0) button.setAttribute('lang', lang)
- button.value = label
- button.setAttribute('data-state', 'inactive')
- button.appendChild(document.createTextNode(label))
- button.addEventListener('click', function(e) {
- subtitleMenuButtons.map(function(v, i, a) {
- subtitleMenuButtons[i].setAttribute('data-state', 'inactive')
- })
- var lang = this.getAttribute('lang')
- for (var i = 0; i < ZornVideoPlayer.textTracks.length; i++) {
- if (ZornVideoPlayer.textTracks[i].language == lang) {
- ZornVideoPlayer.textTracks[i].mode = 'showing'
- this.setAttribute('data-state', 'active')
- }
- else {
- ZornVideoPlayer.textTracks[i].mode = 'hidden'
- }
- }
- subtitlesMenu.style.display = 'none'
- })
- subtitleMenuButtons.push(button)
- return listItem
- }
- var subtitlesMenu
- if (ZornVideoPlayer.textTracks) {
- var df = document.createDocumentFragment()
- var subtitlesMenu = df.appendChild(document.createElement('ul'))
- subtitlesMenu.className = 'subtitles-menu'
- subtitlesMenu.appendChild(createMenuItem('subtitles-off', '', 'Off'))
- for (var i = 0; i < ZornVideoPlayer.textTracks.length; i++) {
- subtitlesMenu.appendChild(createMenuItem('subtitles-' + ZornVideoPlayer.textTracks[i].language, ZornVideoPlayer.textTracks[i].language, ZornVideoPlayer.textTracks[i].label))
- }
- VideoContainer.appendChild(subtitlesMenu)
- }
- subtitles.addEventListener('click', function(e) {
- if (subtitlesMenu) {
- subtitlesMenu.style.display = (subtitlesMenu.style.display == 'block' ? 'none' : 'block')
- }
- })
- }
-
- // src/functions/Volume.js
- function Volume() {
- const Button_Volume = document.getElementById("volume-button");
- const volume2 = document.getElementById("volume");
- function Update_Volme() {
- if (ZornVideoPlayer.muted) {
- ZornVideoPlayer.muted = false;
- }
- ZornVideoPlayer.volume = volume2.value;
- }
- volume2.addEventListener("input", Update_Volme);
- function Update_Volume_Icon() {
- Button_Volume.setAttribute("data-title", "Mute (M)");
- if (ZornVideoPlayer.muted || ZornVideoPlayer.volume === 0) {
- Button_Volume.innerHTML = `${VolumeOffIcon}`;
- Button_Volume.setAttribute("data-title", "Unmute (M)");
- } else if (ZornVideoPlayer.volume > 0 && ZornVideoPlayer.volume <= 0.5) {
- Button_Volume.innerHTML = `${VolumeMinIcon}`;
- } else {
- Button_Volume.innerHTML = `${VolumeHighIcon}`;
- }
- }
- ZornVideoPlayer.addEventListener("volumechange", Update_Volume_Icon);
- function Toggle_Mute() {
- ZornVideoPlayer.muted = !ZornVideoPlayer.muted;
- if (ZornVideoPlayer.muted) {
- volume2.setAttribute("data-volume", volume2.value);
- volume2.value = 0;
- } else {
- volume2.value = volume2.dataset.volume;
- }
- }
- Button_Volume.addEventListener("click", Toggle_Mute);
- }
-
- // src/functions/Seek.js
- function Seek() {
- const timeElapsed = document.getElementById("time-elapsed");
- const duration = document.getElementById("duration");
- function formatTime(timeInSeconds) {
- const result = new Date(timeInSeconds * 1e3).toISOString().substr(11, 8);
- return {
- minutes: result.substr(3, 2),
- seconds: result.substr(6, 2)
- };
- }
- ;
- function initializeVideo() {
- const videoDuration = Math.round(ZornVideoPlayer.duration);
- const time = formatTime(videoDuration);
- duration.innerText = `${time.minutes}:${time.seconds}`;
- duration.setAttribute("datetime", `${time.minutes}m ${time.seconds}s`);
- }
- ZornVideoPlayer.addEventListener("loadedmetadata", initializeVideo);
- function updateTimeElapsed() {
- const time = formatTime(Math.round(ZornVideoPlayer.currentTime));
- timeElapsed.innerText = `${time.minutes}:${time.seconds}`;
- timeElapsed.setAttribute("datetime", `${time.minutes}m ${time.seconds}s`);
- }
- ZornVideoPlayer.addEventListener("timeupdate", updateTimeElapsed);
- const progressBar = document.getElementById("progress-bar");
- const seek = document.getElementById("seek");
- function initializeVideo() {
- const videoDuration = Math.round(ZornVideoPlayer.duration);
- seek.setAttribute("max", videoDuration);
- progressBar.setAttribute("max", videoDuration);
- const time = formatTime(videoDuration);
- duration.innerText = `${time.minutes}:${time.seconds}`;
- duration.setAttribute("datetime", `${time.minutes}m ${time.seconds}s`);
- }
- function updateProgress() {
- seek.value = Math.floor(ZornVideoPlayer.currentTime);
- progressBar.value = Math.floor(ZornVideoPlayer.currentTime);
- }
- ZornVideoPlayer.addEventListener("timeupdate", updateProgress);
- const seekTooltip = document.getElementById("seek-tooltip");
- function updateSeekTooltip(event) {
- const skipTo = Math.round(event.offsetX / event.target.clientWidth * parseInt(event.target.getAttribute("max"), 10));
- seek.setAttribute("data-seek", skipTo);
- const t = formatTime(skipTo);
- seekTooltip.textContent = `${t.minutes}:${t.seconds}`;
- const rect = ZornVideoPlayer.getBoundingClientRect();
- seekTooltip.style.left = `${event.pageX - rect.left}px`;
- }
- seek.addEventListener("mousemove", updateSeekTooltip);
- function skipAhead(event) {
- const skipTo = event.target.dataset.seek ? event.target.dataset.seek : event.target.value;
- ZornVideoPlayer.currentTime = skipTo;
- progressBar.value = skipTo;
- seek.value = skipTo;
- }
- seek.addEventListener("input", skipAhead);
- initializeVideo();
- }
-
- // src/dialogs/Buffering.js
- var BufferDialog = `
-
- ${RefreshIcon}
-
- `;
- ZornVideoPlayer.insertAdjacentHTML("afterend", BufferDialog);
-
- // src/functions/AutoToggleControls.js
- function AutoToggleControls() {
- function Hide_Controls2() {
- if (ZornVideoPlayer.paused) {
- return;
- } else {
- document.querySelector(".zorn-player-controls").classList.add("hide");
- }
- }
- function Show_Controls2() {
- document.querySelector(".zorn-player-controls").classList.remove("hide");
- }
- ZornVideoPlayer.addEventListener("mouseenter", Show_Controls2);
- ZornVideoPlayer.addEventListener("mouseleave", Hide_Controls2);
- document.querySelector(".zorn-player-controls").addEventListener("mouseenter", Show_Controls2);
- document.querySelector(".zorn-player-controls").addEventListener("mouseleave", Hide_Controls2);
- var mouseTimer = null, cursorVisible = true;
- function Hide_Cursor() {
- mouseTimer = null;
- VideoContainer.style.cursor = "none";
- cursorVisible = false;
- Hide_Controls2();
- }
- document.onmousemove = function() {
- if (mouseTimer) {
- window.clearTimeout(mouseTimer);
- Show_Controls2();
- }
- if (!cursorVisible) {
- VideoContainer.style.cursor = "default";
- cursorVisible = true;
- }
- mouseTimer = window.setTimeout(Hide_Cursor, 3200);
- };
- }
-
- // src/functions/KeyboardShortcuts.js
- function KeyboardShortcuts(events) {
- if (ZornVideoPlayer.hasAttribute("keyboard-shortcut-fullscreen")) {
- var Fullscreen_KeyboardShortcut = ZornVideoPlayer.getAttribute("keyboard-shortcut-fullscreen");
- } else {
- var Fullscreen_KeyboardShortcut = "f";
- }
- if (ZornVideoPlayer.hasAttribute("keyboard-shortcut-mute")) {
- var Mute_KeyboardShortcut = ZornVideoPlayer.getAttribute("keyboard-shortcut-mute");
- } else {
- var Mute_KeyboardShortcut = "m";
- }
- if (ZornVideoPlayer.hasAttribute("keyboard-shortcut-playpause")) {
- var PlayPause_KeyboardShortcut = ZornVideoPlayer.getAttribute("keyboard-shortcut-playpause");
- } else {
- var PlayPause_KeyboardShortcut = "k";
- }
- if (ZornVideoPlayer.hasAttribute("keyboard-shortcut-skipback")) {
- var SkipBack_KeyboardShortcut = ZornVideoPlayer.getAttribute("keyboard-shortcut-skipback");
- } else {
- var SkipBack_KeyboardShortcut = "j";
- }
- if (ZornVideoPlayer.hasAttribute("keyboard-shortcut-skipforth")) {
- var SkipForth_KeyboardShortcut = ZornVideoPlayer.getAttribute("keyboard-shortcut-skipforth");
- } else {
- var SkipForth_KeyboardShortcut = "l";
- }
- function keyboardShortcuts(event) {
- const { key } = event;
- if (key === PlayPause_KeyboardShortcut) {
- if (ZornVideoPlayer.paused || ZornVideoPlayer.ended) {
- ZornVideoPlayer.play();
- } else {
- ZornVideoPlayer.pause();
- }
- if (ZornVideoPlayer.paused) {
- Show_Controls();
- } else {
- setTimeout(() => {
- Hide_Controls();
- }, 1200);
- }
- } else if (key === Mute_KeyboardShortcut) {
- ZornVideoPlayer.muted = !ZornVideoPlayer.muted;
- if (ZornVideoPlayer.muted) {
- volume.setAttribute("data-volume", volume.value);
- volume.value = 0;
- } else {
- volume.value = volume.dataset.volume;
- }
- } else if (key === Fullscreen_KeyboardShortcut) {
- if (document.fullscreenElement) {
- document.exitFullscreen();
- } else if (document.webkitFullscreenElement) {
- document.webkitExitFullscreen();
- } else if (VideoContainer.webkitRequestFullscreen) {
- VideoContainer.webkitRequestFullscreen();
- } else {
- VideoContainer.requestFullscreen();
- }
- } else if (key === SkipBack_KeyboardShortcut) {
- ZornVideoPlayer.currentTime += -10;
- } else if (key === SkipForth_KeyboardShortcut) {
- ZornVideoPlayer.currentTime += 10;
- }
- }
- document.addEventListener("keyup", keyboardShortcuts);
- }
-
- // src/index.js
- Events();
- KeyboardShortcuts();
- PlayPause();
- AutoToggleControls();
- SkipAround();
- Fullscreen();
- Subtitles();
- Volume();
- Seek();
- Buffering();
- })();
-
\ No newline at end of file
diff --git a/source/src/public/service-worker.js b/source/src/public/service-worker.js
deleted file mode 100755
index ecbcb65..0000000
--- a/source/src/public/service-worker.js
+++ /dev/null
@@ -1,43 +0,0 @@
-// Service Worker is used to cache the offline page
-// if the end-user were to ever go offline. An offline page
-// should show up if their network condition is offline.
-// This is best practice for the Progress Web App and
-// provides a better user experience.
-
-// Read more on this topic:
-// https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Offline_and_background_operation#offline_operation
-
-'use strict'
-
-var cacheVersion = 1
-var currentCache = {
- offline: 'offline-cache' + cacheVersion
-}
-const offlineUrl = 'offline'
-
-this.addEventListener('install', event => {
- event.waitUntil(
- caches.open(currentCache.offline).then(function (cache) {
- return cache.addAll([
- offlineUrl
- ])
- })
- )
-})
-
-this.addEventListener('fetch', event => {
- if (event.request.mode === 'navigate' || (event.request.method === 'GET' && event.request.headers.get('accept').includes('text/html'))) {
- event.respondWith(
- fetch(event.request.url).catch(error => {
- return caches.match(offlineUrl)
- })
- )
- }
- else {
- event.respondWith(caches.match(event.request)
- .then(function (response) {
- return response || fetch(event.request)
- })
- )
- }
-})
\ No newline at end of file
diff --git a/source/src/public/twitch/video.js/alt/video-js-cdn.css b/source/src/public/twitch/video.js/alt/video-js-cdn.css
deleted file mode 100755
index 2f602f6..0000000
--- a/source/src/public/twitch/video.js/alt/video-js-cdn.css
+++ /dev/null
@@ -1,1935 +0,0 @@
-.vjs-svg-icon {
- display: inline-block;
- background-repeat: no-repeat;
- background-position: center;
- fill: currentColor;
- height: 1.8em;
- width: 1.8em;
-}
-.vjs-svg-icon:before {
- content: none !important;
-}
-
-.vjs-svg-icon:hover,
-.vjs-control:focus .vjs-svg-icon {
- filter: drop-shadow(0 0 0.25em #fff);
-}
-
-.vjs-modal-dialog .vjs-modal-dialog-content, .video-js .vjs-modal-dialog, .vjs-button > .vjs-icon-placeholder:before, .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-
-.vjs-button > .vjs-icon-placeholder:before, .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
- text-align: center;
-}
-
-@font-face {
- font-family: VideoJS;
- src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABUgAAsAAAAAItAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPgAAAFZRiV33Y21hcAAAAYQAAAEJAAAD5p42+VxnbHlmAAACkAAADwwAABdk9R/WHmhlYWQAABGcAAAAKwAAADYn8kSnaGhlYQAAEcgAAAAdAAAAJA+RCL1obXR4AAAR6AAAABMAAAC8Q44AAGxvY2EAABH8AAAAYAAAAGB7SIHGbWF4cAAAElwAAAAfAAAAIAFAAI9uYW1lAAASfAAAASUAAAIK1cf1oHBvc3QAABOkAAABfAAAAnXdFqh1eJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGR7xDiBgZWBgaWQ5RkDA8MvCM0cwxDOeI6BgYmBlZkBKwhIc01hcPjI+FGPHcRdyA4RZgQRADbZCycAAHic7dPXbcMwAEXRK1vuvffem749XAbKV3bjBA6fXsaIgMMLEWoQJaAEFKNnlELyQ4K27zib5PNF6vl8yld+TKr5kH0+cUw0xv00Hwvx2DResUyFKrV4XoMmLdp06NKjz4AhI8ZMmDJjzoIlK9Zs2LJjz4EjJ85cuHLjziPe/0UWL17mf2tqKLz/9jK9f8tXpGCoRdPKhtS0RqFkWvVQNtSKoVYNtWaoddPXEBqG2jQ9XWgZattQO4baNdSeofYNdWCoQ0MdGerYUCeGOjXUmaHODXVhqEtDXRnq2lA3hro11J2h7g31YKhHQz0Z6tlQL4Z6NdSbod4N9WGoT9MfHF6GmhnZLxyDcRMAAAB4nJ1YC1gUV5auc6urCmxEGrq6VRD6ATQP5dHPKK8GRIyoKApoEBUDAiGzGmdUfKNRM4qLZrUZdGKcGN/GZJKd0SyOWTbfbmZ2NxqzM5IxRtNZd78vwYlJdtREoO7sudVNq6PmmxmKqrqPU+eee173P80Bh39Cu9DOEY4DHZBK3i20D/QRLcfxbE5sEVtwLpZzclw4ibFIkSCJUcZ4MBpMnnzwuKNsGWBL5i3qy6kO2dVpvUpKbkAP9fq62rdeGJ+TM/7C1nbIutfuWrWk5ci4zMxxR1qW/N+9JsmCGXj9VKWhFx/6tr/nz78INDm2C9yPF/fDcxLuyKxLBZ1ZBz2QTi+RSkiH5RrDQJ/GgGQadX9m0YSURs7GpSG905Zsk41uj14yul1OtieZ7QUk5GRG/YiS7PYYPSAZNRed9sq3+bOpz00rKb7pe/ZEZvbALxZAHT3AFoH8GXP3rt67QFn40kt8W13FjLTDb48c+fSi5/7h0P4dL5yz7DPtbmgmYxfQA9RL2+EOfTcvdp+1vmuBpvOll1As1S6ak0IvJzC7sKWJFtJgBd2uWcg+0Zyg7dzQfhcjXRgXGZRf5/a4A58IDU777Nl252AUk4m2ByRRjqTNqIDCEJeAnU3iCFwrkrNwXEzg4yFevBwypzxkcX+AIfk3VEKl3XmWbT8788SzvpvFJaiOezL6QyuSr9VNf97csNu0z3LuhR0wATUxZAfVBwVOy+nQFhxYdWaXlXe4HC4zWGWzzsrLDtmhI9pOWOHv7PTT7XybH1Z0+v2d5Abd3kmG+TsH23CS/KwTxx/JkzEwx6jcQOUc42LLwHJ/J93uZ9ygh3HuZGwqsY9dWDHQ58dxNqyqKRQTYdxwTubiOSs3FiMDkq0WSZQgCT0GBDOg2lxOAd1FlPVGs4AKBAcYHHaP2wPkHaivmLF5zYqnIZrvcHx5gN4k/6tchNW1DtdgNL2KrxEkS/kfnIHoVnp1VjmjpTf5r0lTzLj0mdS28tX+XGorU364eMPmnWVl8J36nlKGw3CZhjEiuMw8h8mKvhGD+4/lElBWjAhLJMg6fTw4zPZ8cOmcGQBm2Qxml1nAm13CpYGq1JKUlJJUzQn1PTAO0mgv6VMMpA/DuRfSWEu4lDIxdbAtdWIKvnn2Vk766CWfz9fpY0sH/UpdP50rfszaVpdVRmvIejEdLMk45s4Bu0EWHjeOySmFyZSiMahvZdNSn29peoI/YexYfKQTLeurTXXwEVLeSfInTWHkkMaeUx7sBvOCSTSj3AlcKjfueyS36tCrXDlgRtF0etFq9jhc1kfKuBT/OwMr0F4UUTTh1AN0g20+H/ScPcsIEsYu9d/zN5PmjprPtNwI1ZZcDK6iC97Mcjp2y2aX36f+QbpGHrgRuHlXJ+Zf6PFRL2uQSp8vxHeF2IoRb8Rd2rhMzsNxSRmEuKK4JFnkojhMcx6jzqHzGMGFcW+MhBj0bhf6cowN+45I4LHvwT6fteu7M42wGRI/pxcg6/MZdEvt1U1XaulHFXuLmqov/MukvRVL35/b3ODM1+4aPjtzeK7zmUkV2h3DN54HaQ9GzJvxHRb6Ks2gB81fwqraT+A7GvZJrRLRofU6G0urNL+zFw3v0FaVDFxsKEZW56F31r6ip6vOL+FCObBPuIMRiXld9RaMdLzRIOGhPey2T9vA/35DmZPK9IWaT9d/WgOGMieYqJ/dzjLIhZU118gbysxrNUGefxD6UO/hyNNllpFTOIbx32kSFQctnweV5PxTMHLjRqiAN+fQE9gL+Xy5WB6MOS4GJJuYbDUHhcKDhHGRbLzOpjsjdM1+iwAZLGeieehACX2hhI7SjK/ZUTNrvVje31TxJiFBGYViWFkCn9PMeX9fS6qVbzfCj4fOCTzDnuWy2c4xA7mdNkA3RS9FH2VeqzdCBlixxbzXjvkHU1I8BOYFb1pZvPIHSSIj4svT8xpzcxtXN+ZKyjdDvbz08niiF3PqV9Tn5NST8vg48MTaY8E5xqSSIsWoWHo+LtAzxdH/GDUyp37CBEYfso04F/NlMTcDJUTpECLY0HFGQHImE8xsEUdgnrQlixIvGhJA1BvxpDHGxEMBYFeNOHcBJlSjwe2JcSfbBEsGOPPBHg/6SBBOCsLLw0SpUxod0Z1bFMfLkbQ3UiZxEyd0Dx8t+SRBu18Q9msFbI4e3p1THEfkSEh7kEJ5orR10qTWDvbgPWn5aWvCYyOAjwgXyjJi34uMjo58L25cmRAeQZWI2PA1QQLsPESAH8WGFwZZ4SPoR73BHPzIPMJj9AreBzKUmrH4todT18ANvi1oc3YGjUT/0j+ExUwq8PI9BLaCQIpvewwYu2evAG/Vo/5avPdY7o+BemLLXw3y+AdkzP9bpIxB1wm5EYq8fesHbPEPtm6HrHvtx4jcGPR8fDDpkZBefIjB46QnlUNRltv4Z/pO/J6dxEjhYAtmoMeq+GozvUVvNYOW3m6GCIhoprcfr97B8AcIQYsfD8ljUvGNjvkrpj0ETA48ZMIxCeqsRIsQALE0gi2GB+glSOfbOjW3GSBM9yPq8/rpJXrJDz0BPxV6xdN4uiCGDQed3WhgFkBUZEFsmeyyBpzXrm7UGTBZG8Lh5aubFufk5eUsbrrFGr7McYdbltxa0nKYqRKbQjvikXYkTGM0f2xuyM3Ly21oXnWfvf6I1BmZwfh7EWWIYsg2nHhsDhOnczhJcmI6eBAmy3jZ3RiJmKQR/JA99FcwsfaVbNDDyi1rL9NPj9hfo61wjM6BjzOLijLpeTgk/pL+ip6tfYWupzeOgPny2tcUu9J/9mhxJlgyi985NFRbvCVewXUNXLJaW0RxZqtRYtnfYdcYomXQWdnJHQA3jiEEkeTQWcWxdDP9IvvVWvo2TK553XEMEq+s69/QDU1Q7p0zxwsm9qS379whr8NI2PJqLUyGyfNeX3eFfnJU2U+uHR9cVV1IqgurqwuV44XVp0h2qN55X5XJwtk59yP0IZuHrqBOBIuIYhkcoT6Kx79Pu2HS/IPZIMOqLWs/pteOOk4NPgEb6QAIdAPsyZk5Mwd+wVaHMexJv719W7xCu2l37UG6lvYdBcvHa08p89741zd63phTRGqL5ggo6SlvdbWXzCqsPq78NnSu7wnKy2HNZbVoRCI7UJEOyRj+sPE002tOOY7Qa5fXboFWkLNeqYUSZRocp9XwSUZxcQZ9Hw6LV2pOoVmvHQEDbGIENEG5i6bLgMSM4n8+FNLTtAds99DaWEvgcf4o5SyYe9x+kF6/tGoTPAdRmS/XQIEy//QxKC2oqioAI3tS5auvxCtzT6y6RK8fhChYcwCJaMJhxc0vqSxQ/qmgsrKAlBZUHlauheTpvd9uj5DnLzJct6qfq5fXbYHVIGcfrIVJihbaVLu1wW7Vbs8zK0A8e9Jvb91S9cVMjPrazD6gpfeZTXzYbCFMcppVRsGMpp55OWgx1/3JeAxW1Y7AORgM/m3rWrsdLkQVmEVSU16cX/e7uvkvpqRiQsG06XJ0t64Tf+l0nG1dt025gyOIZlvq5u9KSU1N2TW/rsWnnMRPyTDkctbhvIcNvYIXWyLzdwYLoYesUbaQG4iK2cWO2gdpeUYLqDD0MUTOPhDIGnZEs58yArR86FznuWEsU4YDi2x26dA4klkn8Qa6vhk2QUfX4Jxm/ngX9r7ogn1dmlmwqZmuhxtdg9XN/DEcUgqb+9hMyNansfaQET2mcROCmGEMVqxm5u+h6kN2MOwgqykV2wH9yQG9DvVFU38Pogaf4FVuE62KI/oJ02RDdWW2w5dqQwU/8+N1q1DlvsL863u61KLE7x/o8w0VJQM/Y/SQ3unIrqxueEa1BqT5VFNsO7p39/UC771a77RowpaKe9nvJQIT1Pog5LGx8XblBKmCNGTf3xMogAQvPnz9PYKX/08sVDTG1OKUlOLUgS/UaZtm1NAaYTsl7i9ZQ+L6O4Rl0OGa577LuWvc+C+x96/vYh0lLBuM+7XwI/dTLtdT7v4d6rRTWDnku0IBrqFnZ5bVIqKP8lasJlithWnaLhTsr8qFJBulF/70p4undou36HeTJ5+jv1fCybeQ8nH3+Xv6aENczmOFlab+hqMDg1rLOt12A+tiUFrYDwQ6c3RUJp601nzegTNX6WlYAI2zSUV945F6zU56ZmZVQaWspWcIADxJ9GmljQUnL2p2Dpr5T8H+5KJFu+vqBq8qvyHRzStLHPEO5SPYCV9nZe0yZT2RcH0oHvegSzNEJ0oGWU8iQWM12dgPEugngVceGIwZgPFp0BiT1a0a3R5Rcot7ihfA1J/20v96jX7zmTX9s583H0kwx6WnLd09cXrR9LGroOa9sHNbdyz8wcKk5lqhaVFJZNwmqtw884MXNdvJujpBa3xzuSaZH9sxa06Z7x+HJSduPbdYHv/DgmEhfbehvlmGN7JUkcG78GDM12CeyFFTPNqVeNxC1gzjz+c2nVo63Xxs8rKJWXoBJM0tmEbfGm4qzpoOH3xpzQfyxLzW1gnE9NHo6tol1eMEic4ZVPrjnVi0kqAe2sQ2bgqupScaq8WGlUWgWHI51SKJl/UYT6zccNsCSkBtiVZLsiefuFSDYT3Fi8Zk7EUnmjTRYtsFeuDDJS05MW79M3mr3mla+d8dzac31KTPmBYfFiYSUef48PhPjm9ryZsSGZZkdNvzq0Y9rdNcwDq5Dg5C3QW+7UN64IKptvS3tvHbvu5c9pv1Exau21rc9LIpwpQwUjTq8576yeVDz5+4WZ1nXT43wV60rPLJbDp/UksNrP3iQ2SA63Pst058gOYDbhRnRUw8l/sRt4HbxPzO4WYpInCpuVgSbVh6JXuwnnJngKTTCwaPWmG5Xbhpm1U0Yt3FyBGpGYemPM77p2TD904JjgJ2QFpFLeYpGx8X15Qx1Zk31p5ki9ZLUuXE0lmuJlcakJMVLeFS1iIvrB8drY0aloilakqCZwzwRORtxlgwxS4IThggJd4TDxoiaAIT80fFPGrCPPru+puFn504P/ybr4ihA/6dKASLshEJic7xE8tmzu3KzA7TABBe8y5fNbWo3ilQn/SuFKM16b2l5bOeayqfGhYmhIulU+fVNDdWVv4NMzX10MBHyPR5uhWUu8D9P1VnIMt4nGNgZGBgAOJ/1bf64vltvjJwszOAwAOlmqvINEc/WJyDgQlEAQA+dgnjAHicY2BkYGBnAAGOPgaG//85+hkYGVCBPgBGJwNkAAAAeJxjYGBgYB/EmKMPtxwAhg4B0gAAAAAAAA4AaAB+AMwA4AECAUIBbAGYAe4CLgKKAtAC/ANiA4wDqAPgBDAEsATaBQgFWgXABggGLgZwBqwG9gdOB4oH0ggqCHAIhgicCMgJJAlWCYgJrAnyCkAKdgrkC7J4nGNgZGBg0GdoZmBnAAEmIOYCQgaG/2A+AwAaqwHQAHicXZBNaoNAGIZfE5PQCKFQ2lUps2oXBfOzzAESyDKBQJdGR2NQR3QSSE/QE/QEPUUPUHqsvsrXjTMw83zPvPMNCuAWP3DQDAejdm1GjzwS7pMmwi75XngAD4/CQ/oX4TFe4Qt7uMMbOzjuDc0EmXCP/C7cJ38Iu+RP4QEe8CU8pP8WHmOPX2EPz87TPo202ey2OjlnQSXV/6arOjWFmvszMWtd6CqwOlKHq6ovycLaWMWVydXKFFZnmVFlZU46tP7R2nI5ncbi/dDkfDtFBA2DDXbYkhKc+V0Bqs5Zt9JM1HQGBRTm/EezTmZNKtpcAMs9Yu6AK9caF76zoLWIWcfMGOSkVduvSWechqZsz040Ib2PY3urxBJTzriT95lipz+TN1fmAAAAeJxtkXlT2zAQxf1C4thJAwRajt4HRy8VMwwfSJHXsQZZcnUQ+PYoTtwpM+wf2t9brWZ2n5JBsol58nJcYYAdDDFCijEy5JhgileYYRd72MccBzjEa7zBEY5xglO8xTu8xwd8xCd8xhd8xTec4RwXuMR3/MBP/MJvMPzBFYpk2Cr+OF0fTEgrFI1aHhxN740KDbEmeJpsWZlVj40s+45aLuv9KijlhCXSjLQnu/d/4UH6sWul1mRzFxZeekUuE7z10mg3qMtM1FGQddPSrLQyvJR6OaukItYXDp6pCJrmz0umqkau5pZ2hFmm7m+ImG5W2t0kZoJXUtPhVnYTbbdOBdeCVGqpJe7XKTqSbRK7zbdwXfR0U+SVsStuS3Y76em6+Ic3xYiHUppc04Nn0lMzay3dSxNcp8auDlWlaCi48yetFD7Y9USsx87G45cuop1ZxQUtjLnL4j53FO0a+5X08UXqQ7NQNo92R0XOz7sxWEnxN2TneJI8Acttu4Q=) format("woff");
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-play, .video-js .vjs-play-control .vjs-icon-placeholder, .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-play:before, .video-js .vjs-play-control .vjs-icon-placeholder:before, .video-js .vjs-big-play-button .vjs-icon-placeholder:before {
- content: "\f101";
-}
-
-.vjs-icon-play-circle {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-play-circle:before {
- content: "\f102";
-}
-
-.vjs-icon-pause, .video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-pause:before, .video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before {
- content: "\f103";
-}
-
-.vjs-icon-volume-mute, .video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-volume-mute:before, .video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before {
- content: "\f104";
-}
-
-.vjs-icon-volume-low, .video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-volume-low:before, .video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before {
- content: "\f105";
-}
-
-.vjs-icon-volume-mid, .video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-volume-mid:before, .video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before {
- content: "\f106";
-}
-
-.vjs-icon-volume-high, .video-js .vjs-mute-control .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-volume-high:before, .video-js .vjs-mute-control .vjs-icon-placeholder:before {
- content: "\f107";
-}
-
-.vjs-icon-fullscreen-enter, .video-js .vjs-fullscreen-control .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-fullscreen-enter:before, .video-js .vjs-fullscreen-control .vjs-icon-placeholder:before {
- content: "\f108";
-}
-
-.vjs-icon-fullscreen-exit, .video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-fullscreen-exit:before, .video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before {
- content: "\f109";
-}
-
-.vjs-icon-spinner {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-spinner:before {
- content: "\f10a";
-}
-
-.vjs-icon-subtitles, .video-js .vjs-subs-caps-button .vjs-icon-placeholder,
-.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,
-.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,
-.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,
-.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder, .video-js .vjs-subtitles-button .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-subtitles:before, .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,
-.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,
-.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,
-.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,
-.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before, .video-js .vjs-subtitles-button .vjs-icon-placeholder:before {
- content: "\f10b";
-}
-
-.vjs-icon-captions, .video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,
-.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder, .video-js .vjs-captions-button .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-captions:before, .video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,
-.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before, .video-js .vjs-captions-button .vjs-icon-placeholder:before {
- content: "\f10c";
-}
-
-.vjs-icon-hd {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-hd:before {
- content: "\f10d";
-}
-
-.vjs-icon-chapters, .video-js .vjs-chapters-button .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-chapters:before, .video-js .vjs-chapters-button .vjs-icon-placeholder:before {
- content: "\f10e";
-}
-
-.vjs-icon-downloading {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-downloading:before {
- content: "\f10f";
-}
-
-.vjs-icon-file-download {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-file-download:before {
- content: "\f110";
-}
-
-.vjs-icon-file-download-done {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-file-download-done:before {
- content: "\f111";
-}
-
-.vjs-icon-file-download-off {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-file-download-off:before {
- content: "\f112";
-}
-
-.vjs-icon-share {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-share:before {
- content: "\f113";
-}
-
-.vjs-icon-cog {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-cog:before {
- content: "\f114";
-}
-
-.vjs-icon-square {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-square:before {
- content: "\f115";
-}
-
-.vjs-icon-circle, .vjs-seek-to-live-control .vjs-icon-placeholder, .video-js .vjs-volume-level, .video-js .vjs-play-progress {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-circle:before, .vjs-seek-to-live-control .vjs-icon-placeholder:before, .video-js .vjs-volume-level:before, .video-js .vjs-play-progress:before {
- content: "\f116";
-}
-
-.vjs-icon-circle-outline {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-circle-outline:before {
- content: "\f117";
-}
-
-.vjs-icon-circle-inner-circle {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-circle-inner-circle:before {
- content: "\f118";
-}
-
-.vjs-icon-cancel, .video-js .vjs-control.vjs-close-button .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-cancel:before, .video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before {
- content: "\f119";
-}
-
-.vjs-icon-repeat {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-repeat:before {
- content: "\f11a";
-}
-
-.vjs-icon-replay, .video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-replay:before, .video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before {
- content: "\f11b";
-}
-
-.vjs-icon-replay-5, .video-js .vjs-skip-backward-5 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-replay-5:before, .video-js .vjs-skip-backward-5 .vjs-icon-placeholder:before {
- content: "\f11c";
-}
-
-.vjs-icon-replay-10, .video-js .vjs-skip-backward-10 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-replay-10:before, .video-js .vjs-skip-backward-10 .vjs-icon-placeholder:before {
- content: "\f11d";
-}
-
-.vjs-icon-replay-30, .video-js .vjs-skip-backward-30 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-replay-30:before, .video-js .vjs-skip-backward-30 .vjs-icon-placeholder:before {
- content: "\f11e";
-}
-
-.vjs-icon-forward-5, .video-js .vjs-skip-forward-5 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-forward-5:before, .video-js .vjs-skip-forward-5 .vjs-icon-placeholder:before {
- content: "\f11f";
-}
-
-.vjs-icon-forward-10, .video-js .vjs-skip-forward-10 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-forward-10:before, .video-js .vjs-skip-forward-10 .vjs-icon-placeholder:before {
- content: "\f120";
-}
-
-.vjs-icon-forward-30, .video-js .vjs-skip-forward-30 .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-forward-30:before, .video-js .vjs-skip-forward-30 .vjs-icon-placeholder:before {
- content: "\f121";
-}
-
-.vjs-icon-audio, .video-js .vjs-audio-button .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-audio:before, .video-js .vjs-audio-button .vjs-icon-placeholder:before {
- content: "\f122";
-}
-
-.vjs-icon-next-item {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-next-item:before {
- content: "\f123";
-}
-
-.vjs-icon-previous-item {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-previous-item:before {
- content: "\f124";
-}
-
-.vjs-icon-shuffle {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-shuffle:before {
- content: "\f125";
-}
-
-.vjs-icon-cast {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-cast:before {
- content: "\f126";
-}
-
-.vjs-icon-picture-in-picture-enter, .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-picture-in-picture-enter:before, .video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
- content: "\f127";
-}
-
-.vjs-icon-picture-in-picture-exit, .video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-picture-in-picture-exit:before, .video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder:before {
- content: "\f128";
-}
-
-.vjs-icon-facebook {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-facebook:before {
- content: "\f129";
-}
-
-.vjs-icon-linkedin {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-linkedin:before {
- content: "\f12a";
-}
-
-.vjs-icon-twitter {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-twitter:before {
- content: "\f12b";
-}
-
-.vjs-icon-tumblr {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-tumblr:before {
- content: "\f12c";
-}
-
-.vjs-icon-pinterest {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-pinterest:before {
- content: "\f12d";
-}
-
-.vjs-icon-audio-description, .video-js .vjs-descriptions-button .vjs-icon-placeholder {
- font-family: VideoJS;
- font-weight: normal;
- font-style: normal;
-}
-.vjs-icon-audio-description:before, .video-js .vjs-descriptions-button .vjs-icon-placeholder:before {
- content: "\f12e";
-}
-
-.video-js {
- display: inline-block;
- vertical-align: top;
- box-sizing: border-box;
- color: #fff;
- background-color: #000;
- position: relative;
- padding: 0;
- font-size: 10px;
- line-height: 1;
- font-weight: normal;
- font-style: normal;
- font-family: Arial, Helvetica, sans-serif;
- word-break: initial;
-}
-.video-js:-moz-full-screen {
- position: absolute;
-}
-.video-js:-webkit-full-screen {
- width: 100% !important;
- height: 100% !important;
-}
-
-.video-js[tabindex="-1"] {
- outline: none;
-}
-
-.video-js *,
-.video-js *:before,
-.video-js *:after {
- box-sizing: inherit;
-}
-
-.video-js ul {
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
- list-style-position: outside;
- margin-left: 0;
- margin-right: 0;
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.video-js.vjs-fluid,
-.video-js.vjs-16-9,
-.video-js.vjs-4-3,
-.video-js.vjs-9-16,
-.video-js.vjs-1-1 {
- width: 100%;
- max-width: 100%;
-}
-
-.video-js.vjs-fluid:not(.vjs-audio-only-mode),
-.video-js.vjs-16-9:not(.vjs-audio-only-mode),
-.video-js.vjs-4-3:not(.vjs-audio-only-mode),
-.video-js.vjs-9-16:not(.vjs-audio-only-mode),
-.video-js.vjs-1-1:not(.vjs-audio-only-mode) {
- height: 0;
-}
-
-.video-js.vjs-16-9:not(.vjs-audio-only-mode) {
- padding-top: 56.25%;
-}
-
-.video-js.vjs-4-3:not(.vjs-audio-only-mode) {
- padding-top: 75%;
-}
-
-.video-js.vjs-9-16:not(.vjs-audio-only-mode) {
- padding-top: 177.7777777778%;
-}
-
-.video-js.vjs-1-1:not(.vjs-audio-only-mode) {
- padding-top: 100%;
-}
-
-.video-js.vjs-fill:not(.vjs-audio-only-mode) {
- width: 100%;
- height: 100%;
-}
-
-.video-js .vjs-tech {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-
-.video-js.vjs-audio-only-mode .vjs-tech {
- display: none;
-}
-
-body.vjs-full-window,
-body.vjs-pip-window {
- padding: 0;
- margin: 0;
- height: 100%;
-}
-
-.vjs-full-window .video-js.vjs-fullscreen,
-body.vjs-pip-window .video-js {
- position: fixed;
- overflow: hidden;
- z-index: 1000;
- left: 0;
- top: 0;
- bottom: 0;
- right: 0;
-}
-
-.video-js.vjs-fullscreen:not(.vjs-ios-native-fs),
-body.vjs-pip-window .video-js {
- width: 100% !important;
- height: 100% !important;
- padding-top: 0 !important;
- display: block;
-}
-
-.video-js.vjs-fullscreen.vjs-user-inactive {
- cursor: none;
-}
-
-.vjs-pip-container .vjs-pip-text {
- position: absolute;
- bottom: 10%;
- font-size: 2em;
- background-color: rgba(0, 0, 0, 0.7);
- padding: 0.5em;
- text-align: center;
- width: 100%;
-}
-
-.vjs-layout-tiny.vjs-pip-container .vjs-pip-text,
-.vjs-layout-x-small.vjs-pip-container .vjs-pip-text,
-.vjs-layout-small.vjs-pip-container .vjs-pip-text {
- bottom: 0;
- font-size: 1.4em;
-}
-
-.vjs-hidden {
- display: none !important;
-}
-
-.vjs-disabled {
- opacity: 0.5;
- cursor: default;
-}
-
-.video-js .vjs-offscreen {
- height: 1px;
- left: -9999px;
- position: absolute;
- top: 0;
- width: 1px;
-}
-
-.vjs-lock-showing {
- display: block !important;
- opacity: 1 !important;
- visibility: visible !important;
-}
-
-.vjs-no-js {
- padding: 20px;
- color: #fff;
- background-color: #000;
- font-size: 18px;
- font-family: Arial, Helvetica, sans-serif;
- text-align: center;
- width: 300px;
- height: 150px;
- margin: 0px auto;
-}
-
-.vjs-no-js a,
-.vjs-no-js a:visited {
- color: #66A8CC;
-}
-
-.video-js .vjs-big-play-button {
- font-size: 3em;
- line-height: 1.5em;
- height: 1.63332em;
- width: 3em;
- display: block;
- position: absolute;
- top: 50%;
- left: 50%;
- padding: 0;
- margin-top: -0.81666em;
- margin-left: -1.5em;
- cursor: pointer;
- opacity: 1;
- border: 0.06666em solid #fff;
- background-color: #2B333F;
- background-color: rgba(43, 51, 63, 0.7);
- border-radius: 0.3em;
- transition: all 0.4s;
-}
-.vjs-big-play-button .vjs-svg-icon {
- width: 1em;
- height: 1em;
- position: absolute;
- top: 50%;
- left: 50%;
- line-height: 1;
- transform: translate(-50%, -50%);
-}
-
-.video-js:hover .vjs-big-play-button,
-.video-js .vjs-big-play-button:focus {
- border-color: #fff;
- background-color: #73859f;
- background-color: rgba(115, 133, 159, 0.5);
- transition: all 0s;
-}
-
-.vjs-controls-disabled .vjs-big-play-button,
-.vjs-has-started .vjs-big-play-button,
-.vjs-using-native-controls .vjs-big-play-button,
-.vjs-error .vjs-big-play-button {
- display: none;
-}
-
-.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause:not(.vjs-seeking, .vjs-scrubbing, .vjs-error) .vjs-big-play-button {
- display: block;
-}
-
-.video-js button {
- background: none;
- border: none;
- color: inherit;
- display: inline-block;
- font-size: inherit;
- line-height: inherit;
- text-transform: none;
- text-decoration: none;
- transition: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
-}
-
-.vjs-control .vjs-button {
- width: 100%;
- height: 100%;
-}
-
-.video-js .vjs-control.vjs-close-button {
- cursor: pointer;
- height: 3em;
- position: absolute;
- right: 0;
- top: 0.5em;
- z-index: 2;
-}
-.video-js .vjs-modal-dialog {
- background: rgba(0, 0, 0, 0.8);
- background: linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0));
- overflow: auto;
-}
-
-.video-js .vjs-modal-dialog > * {
- box-sizing: border-box;
-}
-
-.vjs-modal-dialog .vjs-modal-dialog-content {
- font-size: 1.2em;
- line-height: 1.5;
- padding: 20px 24px;
- z-index: 1;
-}
-
-.vjs-menu-button {
- cursor: pointer;
-}
-
-.vjs-menu-button.vjs-disabled {
- cursor: default;
-}
-
-.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu {
- display: none;
-}
-
-.vjs-menu .vjs-menu-content {
- display: block;
- padding: 0;
- margin: 0;
- font-family: Arial, Helvetica, sans-serif;
- overflow: auto;
-}
-
-.vjs-menu .vjs-menu-content > * {
- box-sizing: border-box;
-}
-
-.vjs-scrubbing .vjs-control.vjs-menu-button:hover .vjs-menu {
- display: none;
-}
-
-.vjs-menu li {
- display: flex;
- justify-content: center;
- list-style: none;
- margin: 0;
- padding: 0.2em 0;
- line-height: 1.4em;
- font-size: 1.2em;
- text-align: center;
- text-transform: lowercase;
-}
-
-.vjs-menu li.vjs-menu-item:focus,
-.vjs-menu li.vjs-menu-item:hover,
-.js-focus-visible .vjs-menu li.vjs-menu-item:hover {
- background-color: #73859f;
- background-color: rgba(115, 133, 159, 0.5);
-}
-
-.vjs-menu li.vjs-selected,
-.vjs-menu li.vjs-selected:focus,
-.vjs-menu li.vjs-selected:hover,
-.js-focus-visible .vjs-menu li.vjs-selected:hover {
- background-color: #fff;
- color: #2B333F;
-}
-.vjs-menu li.vjs-selected .vjs-svg-icon,
-.vjs-menu li.vjs-selected:focus .vjs-svg-icon,
-.vjs-menu li.vjs-selected:hover .vjs-svg-icon,
-.js-focus-visible .vjs-menu li.vjs-selected:hover .vjs-svg-icon {
- fill: #000000;
-}
-
-.video-js .vjs-menu *:not(.vjs-selected):focus:not(:focus-visible),
-.js-focus-visible .vjs-menu *:not(.vjs-selected):focus:not(.focus-visible) {
- background: none;
-}
-
-.vjs-menu li.vjs-menu-title {
- text-align: center;
- text-transform: uppercase;
- font-size: 1em;
- line-height: 2em;
- padding: 0;
- margin: 0 0 0.3em 0;
- font-weight: bold;
- cursor: default;
-}
-
-.vjs-menu-button-popup .vjs-menu {
- display: none;
- position: absolute;
- bottom: 0;
- width: 10em;
- left: -3em;
- height: 0em;
- margin-bottom: 1.5em;
- border-top-color: rgba(43, 51, 63, 0.7);
-}
-
-.vjs-pip-window .vjs-menu-button-popup .vjs-menu {
- left: unset;
- right: 1em;
-}
-
-.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
- background-color: #2B333F;
- background-color: rgba(43, 51, 63, 0.7);
- position: absolute;
- width: 100%;
- bottom: 1.5em;
- max-height: 15em;
-}
-
-.vjs-layout-tiny .vjs-menu-button-popup .vjs-menu .vjs-menu-content,
-.vjs-layout-x-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
- max-height: 5em;
-}
-
-.vjs-layout-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
- max-height: 10em;
-}
-
-.vjs-layout-medium .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
- max-height: 14em;
-}
-
-.vjs-layout-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,
-.vjs-layout-x-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,
-.vjs-layout-huge .vjs-menu-button-popup .vjs-menu .vjs-menu-content {
- max-height: 25em;
-}
-
-.vjs-workinghover .vjs-menu-button-popup.vjs-hover .vjs-menu,
-.vjs-menu-button-popup .vjs-menu.vjs-lock-showing {
- display: block;
-}
-
-.video-js .vjs-menu-button-inline {
- transition: all 0.4s;
- overflow: hidden;
-}
-
-.video-js .vjs-menu-button-inline:before {
- width: 2.222222222em;
-}
-
-.video-js .vjs-menu-button-inline:hover,
-.video-js .vjs-menu-button-inline:focus,
-.video-js .vjs-menu-button-inline.vjs-slider-active {
- width: 12em;
-}
-
-.vjs-menu-button-inline .vjs-menu {
- opacity: 0;
- height: 100%;
- width: auto;
- position: absolute;
- left: 4em;
- top: 0;
- padding: 0;
- margin: 0;
- transition: all 0.4s;
-}
-
-.vjs-menu-button-inline:hover .vjs-menu,
-.vjs-menu-button-inline:focus .vjs-menu,
-.vjs-menu-button-inline.vjs-slider-active .vjs-menu {
- display: block;
- opacity: 1;
-}
-
-.vjs-menu-button-inline .vjs-menu-content {
- width: auto;
- height: 100%;
- margin: 0;
- overflow: hidden;
-}
-
-.video-js .vjs-control-bar {
- display: none;
- width: 100%;
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- height: 3em;
- background-color: #2B333F;
- background-color: rgba(43, 51, 63, 0.7);
-}
-
-.video-js:not(.vjs-controls-disabled, .vjs-using-native-controls, .vjs-error) .vjs-control-bar.vjs-lock-showing {
- display: flex !important;
-}
-
-.vjs-has-started .vjs-control-bar,
-.vjs-audio-only-mode .vjs-control-bar {
- display: flex;
- visibility: visible;
- opacity: 1;
- transition: visibility 0.1s, opacity 0.1s;
-}
-
-.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
- visibility: visible;
- opacity: 0;
- pointer-events: none;
- transition: visibility 1s, opacity 1s;
-}
-
-.vjs-controls-disabled .vjs-control-bar,
-.vjs-using-native-controls .vjs-control-bar,
-.vjs-error .vjs-control-bar {
- display: none !important;
-}
-
-.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar,
-.vjs-audio-only-mode.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar {
- opacity: 1;
- visibility: visible;
- pointer-events: auto;
-}
-
-.video-js .vjs-control {
- position: relative;
- text-align: center;
- margin: 0;
- padding: 0;
- height: 100%;
- width: 4em;
- flex: none;
-}
-
-.video-js .vjs-control.vjs-visible-text {
- width: auto;
- padding-left: 1em;
- padding-right: 1em;
-}
-
-.vjs-button > .vjs-icon-placeholder:before {
- font-size: 1.8em;
- line-height: 1.67;
-}
-
-.vjs-button > .vjs-icon-placeholder {
- display: block;
-}
-
-.vjs-button > .vjs-svg-icon {
- display: inline-block;
-}
-
-.video-js .vjs-control:focus:before,
-.video-js .vjs-control:hover:before,
-.video-js .vjs-control:focus {
- text-shadow: 0em 0em 1em white;
-}
-
-.video-js *:not(.vjs-visible-text) > .vjs-control-text {
- border: 0;
- clip: rect(0 0 0 0);
- height: 1px;
- overflow: hidden;
- padding: 0;
- position: absolute;
- width: 1px;
-}
-
-.video-js .vjs-custom-control-spacer {
- display: none;
-}
-
-.video-js .vjs-progress-control {
- cursor: pointer;
- flex: auto;
- display: flex;
- align-items: center;
- min-width: 4em;
- touch-action: none;
-}
-
-.video-js .vjs-progress-control.disabled {
- cursor: default;
-}
-
-.vjs-live .vjs-progress-control {
- display: none;
-}
-
-.vjs-liveui .vjs-progress-control {
- display: flex;
- align-items: center;
-}
-
-.video-js .vjs-progress-holder {
- flex: auto;
- transition: all 0.2s;
- height: 0.3em;
-}
-
-.video-js .vjs-progress-control .vjs-progress-holder {
- margin: 0 10px;
-}
-
-.video-js .vjs-progress-control:hover .vjs-progress-holder {
- font-size: 1.6666666667em;
-}
-
-.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled {
- font-size: 1em;
-}
-
-.video-js .vjs-progress-holder .vjs-play-progress,
-.video-js .vjs-progress-holder .vjs-load-progress,
-.video-js .vjs-progress-holder .vjs-load-progress div {
- position: absolute;
- display: block;
- height: 100%;
- margin: 0;
- padding: 0;
- width: 0;
-}
-
-.video-js .vjs-play-progress {
- background-color: #fff;
-}
-.video-js .vjs-play-progress:before {
- font-size: 0.9em;
- position: absolute;
- right: -0.5em;
- line-height: 0.35em;
- z-index: 1;
-}
-
-.vjs-svg-icons-enabled .vjs-play-progress:before {
- content: none !important;
-}
-
-.vjs-play-progress .vjs-svg-icon {
- position: absolute;
- top: -0.35em;
- right: -0.4em;
- width: 0.9em;
- height: 0.9em;
- pointer-events: none;
- line-height: 0.15em;
- z-index: 1;
-}
-
-.video-js .vjs-load-progress {
- background: rgba(115, 133, 159, 0.5);
-}
-
-.video-js .vjs-load-progress div {
- background: rgba(115, 133, 159, 0.75);
-}
-
-.video-js .vjs-time-tooltip {
- background-color: #fff;
- background-color: rgba(255, 255, 255, 0.8);
- border-radius: 0.3em;
- color: #000;
- float: right;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 1em;
- padding: 6px 8px 8px 8px;
- pointer-events: none;
- position: absolute;
- top: -3.4em;
- visibility: hidden;
- z-index: 1;
-}
-
-.video-js .vjs-progress-holder:focus .vjs-time-tooltip {
- display: none;
-}
-
-.video-js .vjs-progress-control:hover .vjs-time-tooltip,
-.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip {
- display: block;
- font-size: 0.6em;
- visibility: visible;
-}
-
-.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip {
- font-size: 1em;
-}
-
-.video-js .vjs-progress-control .vjs-mouse-display {
- display: none;
- position: absolute;
- width: 1px;
- height: 100%;
- background-color: #000;
- z-index: 1;
-}
-
-.video-js .vjs-progress-control:hover .vjs-mouse-display {
- display: block;
-}
-
-.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display {
- visibility: hidden;
- opacity: 0;
- transition: visibility 1s, opacity 1s;
-}
-
-.vjs-mouse-display .vjs-time-tooltip {
- color: #fff;
- background-color: #000;
- background-color: rgba(0, 0, 0, 0.8);
-}
-
-.video-js .vjs-slider {
- position: relative;
- cursor: pointer;
- padding: 0;
- margin: 0 0.45em 0 0.45em;
- /* iOS Safari */
- -webkit-touch-callout: none;
- /* Safari, and Chrome 53 */
- -webkit-user-select: none;
- /* Non-prefixed version, currently supported by Chrome and Opera */
- -moz-user-select: none;
- user-select: none;
- background-color: #73859f;
- background-color: rgba(115, 133, 159, 0.5);
-}
-
-.video-js .vjs-slider.disabled {
- cursor: default;
-}
-
-.video-js .vjs-slider:focus {
- text-shadow: 0em 0em 1em white;
- box-shadow: 0 0 1em #fff;
-}
-
-.video-js .vjs-mute-control {
- cursor: pointer;
- flex: none;
-}
-.video-js .vjs-volume-control {
- cursor: pointer;
- margin-right: 1em;
- display: flex;
-}
-
-.video-js .vjs-volume-control.vjs-volume-horizontal {
- width: 5em;
-}
-
-.video-js .vjs-volume-panel .vjs-volume-control {
- visibility: visible;
- opacity: 0;
- width: 1px;
- height: 1px;
- margin-left: -1px;
-}
-
-.video-js .vjs-volume-panel {
- transition: width 1s;
-}
-.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control, .video-js .vjs-volume-panel:active .vjs-volume-control, .video-js .vjs-volume-panel:focus .vjs-volume-control, .video-js .vjs-volume-panel .vjs-volume-control:active, .video-js .vjs-volume-panel.vjs-hover .vjs-mute-control ~ .vjs-volume-control, .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active {
- visibility: visible;
- opacity: 1;
- position: relative;
- transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s;
-}
-.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-horizontal, .video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal, .video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal, .video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal, .video-js .vjs-volume-panel.vjs-hover .vjs-mute-control ~ .vjs-volume-control.vjs-volume-horizontal, .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal {
- width: 5em;
- height: 3em;
- margin-right: 0;
-}
-.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-vertical, .video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical, .video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical, .video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical, .video-js .vjs-volume-panel.vjs-hover .vjs-mute-control ~ .vjs-volume-control.vjs-volume-vertical, .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical {
- left: -3.5em;
- transition: left 0s;
-}
-.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active {
- width: 10em;
- transition: width 0.1s;
-}
-.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only {
- width: 4em;
-}
-
-.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical {
- height: 8em;
- width: 3em;
- left: -3000em;
- transition: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s;
-}
-
-.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
- transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s;
-}
-
-.video-js .vjs-volume-panel {
- display: flex;
-}
-
-.video-js .vjs-volume-bar {
- margin: 1.35em 0.45em;
-}
-
-.vjs-volume-bar.vjs-slider-horizontal {
- width: 5em;
- height: 0.3em;
-}
-
-.vjs-volume-bar.vjs-slider-vertical {
- width: 0.3em;
- height: 5em;
- margin: 1.35em auto;
-}
-
-.video-js .vjs-volume-level {
- position: absolute;
- bottom: 0;
- left: 0;
- background-color: #fff;
-}
-.video-js .vjs-volume-level:before {
- position: absolute;
- font-size: 0.9em;
- z-index: 1;
-}
-
-.vjs-slider-vertical .vjs-volume-level {
- width: 0.3em;
-}
-.vjs-slider-vertical .vjs-volume-level:before {
- top: -0.5em;
- left: -0.3em;
- z-index: 1;
-}
-
-.vjs-svg-icons-enabled .vjs-volume-level:before {
- content: none;
-}
-
-.vjs-volume-level .vjs-svg-icon {
- position: absolute;
- width: 0.9em;
- height: 0.9em;
- pointer-events: none;
- z-index: 1;
-}
-
-.vjs-slider-horizontal .vjs-volume-level {
- height: 0.3em;
-}
-.vjs-slider-horizontal .vjs-volume-level:before {
- line-height: 0.35em;
- right: -0.5em;
-}
-
-.vjs-slider-horizontal .vjs-volume-level .vjs-svg-icon {
- right: -0.3em;
- transform: translateY(-50%);
-}
-
-.vjs-slider-vertical .vjs-volume-level .vjs-svg-icon {
- top: -0.55em;
- transform: translateX(-50%);
-}
-
-.video-js .vjs-volume-panel.vjs-volume-panel-vertical {
- width: 4em;
-}
-
-.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level {
- height: 100%;
-}
-
-.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level {
- width: 100%;
-}
-
-.video-js .vjs-volume-vertical {
- width: 3em;
- height: 8em;
- bottom: 8em;
- background-color: #2B333F;
- background-color: rgba(43, 51, 63, 0.7);
-}
-
-.video-js .vjs-volume-horizontal .vjs-menu {
- left: -2em;
-}
-
-.video-js .vjs-volume-tooltip {
- background-color: #fff;
- background-color: rgba(255, 255, 255, 0.8);
- border-radius: 0.3em;
- color: #000;
- float: right;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 1em;
- padding: 6px 8px 8px 8px;
- pointer-events: none;
- position: absolute;
- top: -3.4em;
- visibility: hidden;
- z-index: 1;
-}
-
-.video-js .vjs-volume-control:hover .vjs-volume-tooltip,
-.video-js .vjs-volume-control:hover .vjs-progress-holder:focus .vjs-volume-tooltip {
- display: block;
- font-size: 1em;
- visibility: visible;
-}
-
-.video-js .vjs-volume-vertical:hover .vjs-volume-tooltip,
-.video-js .vjs-volume-vertical:hover .vjs-progress-holder:focus .vjs-volume-tooltip {
- left: 1em;
- top: -12px;
-}
-
-.video-js .vjs-volume-control.disabled:hover .vjs-volume-tooltip {
- font-size: 1em;
-}
-
-.video-js .vjs-volume-control .vjs-mouse-display {
- display: none;
- position: absolute;
- width: 100%;
- height: 1px;
- background-color: #000;
- z-index: 1;
-}
-
-.video-js .vjs-volume-horizontal .vjs-mouse-display {
- width: 1px;
- height: 100%;
-}
-
-.video-js .vjs-volume-control:hover .vjs-mouse-display {
- display: block;
-}
-
-.video-js.vjs-user-inactive .vjs-volume-control .vjs-mouse-display {
- visibility: hidden;
- opacity: 0;
- transition: visibility 1s, opacity 1s;
-}
-
-.vjs-mouse-display .vjs-volume-tooltip {
- color: #fff;
- background-color: #000;
- background-color: rgba(0, 0, 0, 0.8);
-}
-
-.vjs-poster {
- display: inline-block;
- vertical-align: middle;
- cursor: pointer;
- margin: 0;
- padding: 0;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- height: 100%;
-}
-
-.vjs-has-started .vjs-poster,
-.vjs-using-native-controls .vjs-poster {
- display: none;
-}
-
-.vjs-audio.vjs-has-started .vjs-poster,
-.vjs-has-started.vjs-audio-poster-mode .vjs-poster,
-.vjs-pip-container.vjs-has-started .vjs-poster {
- display: block;
-}
-
-.vjs-poster img {
- width: 100%;
- height: 100%;
- -o-object-fit: contain;
- object-fit: contain;
-}
-
-.video-js .vjs-live-control {
- display: flex;
- align-items: flex-start;
- flex: auto;
- font-size: 1em;
- line-height: 3em;
-}
-
-.video-js:not(.vjs-live) .vjs-live-control,
-.video-js.vjs-liveui .vjs-live-control {
- display: none;
-}
-
-.video-js .vjs-seek-to-live-control {
- align-items: center;
- cursor: pointer;
- flex: none;
- display: inline-flex;
- height: 100%;
- padding-left: 0.5em;
- padding-right: 0.5em;
- font-size: 1em;
- line-height: 3em;
- width: auto;
- min-width: 4em;
-}
-
-.video-js.vjs-live:not(.vjs-liveui) .vjs-seek-to-live-control,
-.video-js:not(.vjs-live) .vjs-seek-to-live-control {
- display: none;
-}
-
-.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge {
- cursor: auto;
-}
-
-.vjs-seek-to-live-control .vjs-icon-placeholder {
- margin-right: 0.5em;
- color: #888;
-}
-
-.vjs-svg-icons-enabled .vjs-seek-to-live-control {
- line-height: 0;
-}
-
-.vjs-seek-to-live-control .vjs-svg-icon {
- width: 1em;
- height: 1em;
- pointer-events: none;
- fill: #888888;
-}
-
-.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder {
- color: red;
-}
-
-.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-svg-icon {
- fill: red;
-}
-
-.video-js .vjs-time-control {
- flex: none;
- font-size: 1em;
- line-height: 3em;
- min-width: 2em;
- width: auto;
- padding-left: 1em;
- padding-right: 1em;
-}
-
-.vjs-live .vjs-time-control,
-.vjs-live .vjs-time-divider,
-.video-js .vjs-current-time,
-.video-js .vjs-duration {
- display: none;
-}
-
-.vjs-time-divider {
- display: none;
- line-height: 3em;
-}
-
-.video-js .vjs-play-control {
- cursor: pointer;
-}
-
-.video-js .vjs-play-control .vjs-icon-placeholder {
- flex: none;
-}
-
-.vjs-text-track-display {
- position: absolute;
- bottom: 3em;
- left: 0;
- right: 0;
- top: 0;
- pointer-events: none;
-}
-
-.vjs-error .vjs-text-track-display {
- display: none;
-}
-
-.video-js.vjs-controls-disabled .vjs-text-track-display,
-.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display {
- bottom: 1em;
-}
-
-.video-js .vjs-text-track {
- font-size: 1.4em;
- text-align: center;
- margin-bottom: 0.1em;
-}
-
-.vjs-subtitles {
- color: #fff;
-}
-
-.vjs-captions {
- color: #fc6;
-}
-
-.vjs-tt-cue {
- display: block;
-}
-
-video::-webkit-media-text-track-display {
- transform: translateY(-3em);
-}
-
-.video-js.vjs-controls-disabled video::-webkit-media-text-track-display,
-.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display {
- transform: translateY(-1.5em);
-}
-
-.video-js.vjs-force-center-align-cues .vjs-text-track-cue {
- text-align: center !important;
- width: 80% !important;
-}
-
-.video-js .vjs-picture-in-picture-control {
- cursor: pointer;
- flex: none;
-}
-.video-js.vjs-audio-only-mode .vjs-picture-in-picture-control,
-.vjs-pip-window .vjs-picture-in-picture-control {
- display: none;
-}
-
-.video-js .vjs-fullscreen-control {
- cursor: pointer;
- flex: none;
-}
-.video-js.vjs-audio-only-mode .vjs-fullscreen-control,
-.vjs-pip-window .vjs-fullscreen-control {
- display: none;
-}
-
-.vjs-playback-rate > .vjs-menu-button,
-.vjs-playback-rate .vjs-playback-rate-value {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
-}
-
-.vjs-playback-rate .vjs-playback-rate-value {
- pointer-events: none;
- font-size: 1.5em;
- line-height: 2;
- text-align: center;
-}
-
-.vjs-playback-rate .vjs-menu {
- width: 4em;
- left: 0em;
-}
-
-.vjs-error .vjs-error-display .vjs-modal-dialog-content {
- font-size: 1.4em;
- text-align: center;
-}
-
-.vjs-loading-spinner {
- display: none;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- opacity: 0.85;
- text-align: left;
- border: 0.6em solid rgba(43, 51, 63, 0.7);
- box-sizing: border-box;
- background-clip: padding-box;
- width: 5em;
- height: 5em;
- border-radius: 50%;
- visibility: hidden;
-}
-
-.vjs-seeking .vjs-loading-spinner,
-.vjs-waiting .vjs-loading-spinner {
- display: flex;
- justify-content: center;
- align-items: center;
- animation: vjs-spinner-show 0s linear 0.3s forwards;
-}
-
-.vjs-error .vjs-loading-spinner {
- display: none;
-}
-
-.vjs-loading-spinner:before,
-.vjs-loading-spinner:after {
- content: "";
- position: absolute;
- box-sizing: inherit;
- width: inherit;
- height: inherit;
- border-radius: inherit;
- opacity: 1;
- border: inherit;
- border-color: transparent;
- border-top-color: white;
-}
-
-.vjs-seeking .vjs-loading-spinner:before,
-.vjs-seeking .vjs-loading-spinner:after,
-.vjs-waiting .vjs-loading-spinner:before,
-.vjs-waiting .vjs-loading-spinner:after {
- animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite;
-}
-
-.vjs-seeking .vjs-loading-spinner:before,
-.vjs-waiting .vjs-loading-spinner:before {
- border-top-color: rgb(255, 255, 255);
-}
-
-.vjs-seeking .vjs-loading-spinner:after,
-.vjs-waiting .vjs-loading-spinner:after {
- border-top-color: rgb(255, 255, 255);
- animation-delay: 0.44s;
-}
-
-@keyframes vjs-spinner-show {
- to {
- visibility: visible;
- }
-}
-@keyframes vjs-spinner-spin {
- 100% {
- transform: rotate(360deg);
- }
-}
-@keyframes vjs-spinner-fade {
- 0% {
- border-top-color: #73859f;
- }
- 20% {
- border-top-color: #73859f;
- }
- 35% {
- border-top-color: white;
- }
- 60% {
- border-top-color: #73859f;
- }
- 100% {
- border-top-color: #73859f;
- }
-}
-.video-js.vjs-audio-only-mode .vjs-captions-button {
- display: none;
-}
-
-.vjs-chapters-button .vjs-menu ul {
- width: 24em;
-}
-
-.video-js.vjs-audio-only-mode .vjs-descriptions-button {
- display: none;
-}
-
-.vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-svg-icon {
- width: 1.5em;
- height: 1.5em;
-}
-
-.video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder {
- vertical-align: middle;
- display: inline-block;
- margin-bottom: -0.1em;
-}
-
-.video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before {
- font-family: VideoJS;
- content: "\f10c";
- font-size: 1.5em;
- line-height: inherit;
-}
-
-.video-js.vjs-audio-only-mode .vjs-subs-caps-button {
- display: none;
-}
-
-.video-js .vjs-audio-button + .vjs-menu .vjs-description-menu-item .vjs-menu-item-text .vjs-icon-placeholder,
-.video-js .vjs-audio-button + .vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder {
- vertical-align: middle;
- display: inline-block;
- margin-bottom: -0.1em;
-}
-
-.video-js .vjs-audio-button + .vjs-menu .vjs-description-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before,
-.video-js .vjs-audio-button + .vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before {
- font-family: VideoJS;
- content: " \f12e";
- font-size: 1.5em;
- line-height: inherit;
-}
-
-.video-js.vjs-layout-small .vjs-current-time,
-.video-js.vjs-layout-small .vjs-time-divider,
-.video-js.vjs-layout-small .vjs-duration,
-.video-js.vjs-layout-small .vjs-remaining-time,
-.video-js.vjs-layout-small .vjs-playback-rate,
-.video-js.vjs-layout-small .vjs-volume-control, .video-js.vjs-layout-x-small .vjs-current-time,
-.video-js.vjs-layout-x-small .vjs-time-divider,
-.video-js.vjs-layout-x-small .vjs-duration,
-.video-js.vjs-layout-x-small .vjs-remaining-time,
-.video-js.vjs-layout-x-small .vjs-playback-rate,
-.video-js.vjs-layout-x-small .vjs-volume-control, .video-js.vjs-layout-tiny .vjs-current-time,
-.video-js.vjs-layout-tiny .vjs-time-divider,
-.video-js.vjs-layout-tiny .vjs-duration,
-.video-js.vjs-layout-tiny .vjs-remaining-time,
-.video-js.vjs-layout-tiny .vjs-playback-rate,
-.video-js.vjs-layout-tiny .vjs-volume-control {
- display: none;
-}
-.video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active, .video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover {
- width: auto;
- width: initial;
-}
-.video-js.vjs-layout-x-small .vjs-progress-control, .video-js.vjs-layout-tiny .vjs-progress-control {
- display: none;
-}
-.video-js.vjs-layout-x-small .vjs-custom-control-spacer {
- flex: auto;
- display: block;
-}
-
-.vjs-modal-dialog.vjs-text-track-settings {
- background-color: #2B333F;
- background-color: rgba(43, 51, 63, 0.75);
- color: #fff;
- height: 70%;
-}
-
-.vjs-error .vjs-text-track-settings {
- display: none;
-}
-
-.vjs-text-track-settings .vjs-modal-dialog-content {
- display: table;
-}
-
-.vjs-text-track-settings .vjs-track-settings-colors,
-.vjs-text-track-settings .vjs-track-settings-font,
-.vjs-text-track-settings .vjs-track-settings-controls {
- display: table-cell;
-}
-
-.vjs-text-track-settings .vjs-track-settings-controls {
- text-align: right;
- vertical-align: bottom;
-}
-
-@supports (display: grid) {
- .vjs-text-track-settings .vjs-modal-dialog-content {
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: 1fr;
- padding: 20px 24px 0px 24px;
- }
- .vjs-track-settings-controls .vjs-default-button {
- margin-bottom: 20px;
- }
- .vjs-text-track-settings .vjs-track-settings-controls {
- grid-column: 1/-1;
- }
- .vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content,
- .vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content,
- .vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content {
- grid-template-columns: 1fr;
- }
-}
-.vjs-text-track-settings select {
- font-size: inherit;
-}
-
-.vjs-track-setting > select {
- margin-right: 1em;
- margin-bottom: 0.5em;
-}
-
-.vjs-text-track-settings fieldset {
- margin: 10px;
- border: none;
-}
-
-.vjs-text-track-settings fieldset span {
- display: inline-block;
- padding: 0 0.6em 0.8em;
-}
-
-.vjs-text-track-settings fieldset span > select {
- max-width: 7.3em;
-}
-
-.vjs-text-track-settings legend {
- color: #fff;
- font-weight: bold;
- font-size: 1.2em;
-}
-
-.vjs-text-track-settings .vjs-label {
- margin: 0 0.5em 0.5em 0;
-}
-
-.vjs-track-settings-controls button:focus,
-.vjs-track-settings-controls button:active {
- outline-style: solid;
- outline-width: medium;
- background-image: linear-gradient(0deg, #fff 88%, #73859f 100%);
-}
-
-.vjs-track-settings-controls button:hover {
- color: rgba(43, 51, 63, 0.75);
-}
-
-.vjs-track-settings-controls button {
- background-color: #fff;
- background-image: linear-gradient(-180deg, #fff 88%, #73859f 100%);
- color: #2B333F;
- cursor: pointer;
- border-radius: 2px;
-}
-
-.vjs-track-settings-controls .vjs-default-button {
- margin-right: 1em;
-}
-
-.vjs-title-bar {
- background: rgba(0, 0, 0, 0.9);
- background: linear-gradient(180deg, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.7) 60%, rgba(0, 0, 0, 0) 100%);
- font-size: 1.2em;
- line-height: 1.5;
- transition: opacity 0.1s;
- padding: 0.666em 1.333em 4em;
- pointer-events: none;
- position: absolute;
- top: 0;
- width: 100%;
-}
-
-.vjs-error .vjs-title-bar {
- display: none;
-}
-
-.vjs-title-bar-title,
-.vjs-title-bar-description {
- margin: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.vjs-title-bar-title {
- font-weight: bold;
- margin-bottom: 0.333em;
-}
-
-.vjs-playing.vjs-user-inactive .vjs-title-bar {
- opacity: 0;
- transition: opacity 1s;
-}
-
-.video-js .vjs-skip-forward-5 {
- cursor: pointer;
-}
-.video-js .vjs-skip-forward-10 {
- cursor: pointer;
-}
-.video-js .vjs-skip-forward-30 {
- cursor: pointer;
-}
-.video-js .vjs-skip-backward-5 {
- cursor: pointer;
-}
-.video-js .vjs-skip-backward-10 {
- cursor: pointer;
-}
-.video-js .vjs-skip-backward-30 {
- cursor: pointer;
-}
-@media print {
- .video-js > *:not(.vjs-tech):not(.vjs-poster) {
- visibility: hidden;
- }
-}
-.vjs-resize-manager {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- border: none;
- z-index: -1000;
-}
-
-.js-focus-visible .video-js *:focus:not(.focus-visible) {
- outline: none;
-}
-
-.video-js *:focus:not(:focus-visible) {
- outline: none;
-}
diff --git a/source/src/public/twitch/video.js/alt/video-js-cdn.min.css b/source/src/public/twitch/video.js/alt/video-js-cdn.min.css
deleted file mode 100755
index 36b8b2d..0000000
--- a/source/src/public/twitch/video.js/alt/video-js-cdn.min.css
+++ /dev/null
@@ -1 +0,0 @@
-.vjs-svg-icon{display:inline-block;background-repeat:no-repeat;background-position:center;fill:currentColor;height:1.8em;width:1.8em}.vjs-svg-icon:before{content:none!important}.vjs-control:focus .vjs-svg-icon,.vjs-svg-icon:hover{filter:drop-shadow(0 0 .25em #fff)}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-modal-dialog,.vjs-button>.vjs-icon-placeholder:before,.vjs-modal-dialog .vjs-modal-dialog-content{position:absolute;top:0;left:0;width:100%;height:100%}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.vjs-button>.vjs-icon-placeholder:before{text-align:center}@font-face{font-family:VideoJS;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABUgAAsAAAAAItAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPgAAAFZRiV33Y21hcAAAAYQAAAEJAAAD5p42+VxnbHlmAAACkAAADwwAABdk9R/WHmhlYWQAABGcAAAAKwAAADYn8kSnaGhlYQAAEcgAAAAdAAAAJA+RCL1obXR4AAAR6AAAABMAAAC8Q44AAGxvY2EAABH8AAAAYAAAAGB7SIHGbWF4cAAAElwAAAAfAAAAIAFAAI9uYW1lAAASfAAAASUAAAIK1cf1oHBvc3QAABOkAAABfAAAAnXdFqh1eJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGR7xDiBgZWBgaWQ5RkDA8MvCM0cwxDOeI6BgYmBlZkBKwhIc01hcPjI+FGPHcRdyA4RZgQRADbZCycAAHic7dPXbcMwAEXRK1vuvffem749XAbKV3bjBA6fXsaIgMMLEWoQJaAEFKNnlELyQ4K27zib5PNF6vl8yld+TKr5kH0+cUw0xv00Hwvx2DResUyFKrV4XoMmLdp06NKjz4AhI8ZMmDJjzoIlK9Zs2LJjz4EjJ85cuHLjziPe/0UWL17mf2tqKLz/9jK9f8tXpGCoRdPKhtS0RqFkWvVQNtSKoVYNtWaoddPXEBqG2jQ9XWgZattQO4baNdSeofYNdWCoQ0MdGerYUCeGOjXUmaHODXVhqEtDXRnq2lA3hro11J2h7g31YKhHQz0Z6tlQL4Z6NdSbod4N9WGoT9MfHF6GmhnZLxyDcRMAAAB4nJ1YC1gUV5auc6urCmxEGrq6VRD6ATQP5dHPKK8GRIyoKApoEBUDAiGzGmdUfKNRM4qLZrUZdGKcGN/GZJKd0SyOWTbfbmZ2NxqzM5IxRtNZd78vwYlJdtREoO7sudVNq6PmmxmKqrqPU+eee173P80Bh39Cu9DOEY4DHZBK3i20D/QRLcfxbE5sEVtwLpZzclw4ibFIkSCJUcZ4MBpMnnzwuKNsGWBL5i3qy6kO2dVpvUpKbkAP9fq62rdeGJ+TM/7C1nbIutfuWrWk5ci4zMxxR1qW/N+9JsmCGXj9VKWhFx/6tr/nz78INDm2C9yPF/fDcxLuyKxLBZ1ZBz2QTi+RSkiH5RrDQJ/GgGQadX9m0YSURs7GpSG905Zsk41uj14yul1OtieZ7QUk5GRG/YiS7PYYPSAZNRed9sq3+bOpz00rKb7pe/ZEZvbALxZAHT3AFoH8GXP3rt67QFn40kt8W13FjLTDb48c+fSi5/7h0P4dL5yz7DPtbmgmYxfQA9RL2+EOfTcvdp+1vmuBpvOll1As1S6ak0IvJzC7sKWJFtJgBd2uWcg+0Zyg7dzQfhcjXRgXGZRf5/a4A58IDU777Nl252AUk4m2ByRRjqTNqIDCEJeAnU3iCFwrkrNwXEzg4yFevBwypzxkcX+AIfk3VEKl3XmWbT8788SzvpvFJaiOezL6QyuSr9VNf97csNu0z3LuhR0wATUxZAfVBwVOy+nQFhxYdWaXlXe4HC4zWGWzzsrLDtmhI9pOWOHv7PTT7XybH1Z0+v2d5Abd3kmG+TsH23CS/KwTxx/JkzEwx6jcQOUc42LLwHJ/J93uZ9ygh3HuZGwqsY9dWDHQ58dxNqyqKRQTYdxwTubiOSs3FiMDkq0WSZQgCT0GBDOg2lxOAd1FlPVGs4AKBAcYHHaP2wPkHaivmLF5zYqnIZrvcHx5gN4k/6tchNW1DtdgNL2KrxEkS/kfnIHoVnp1VjmjpTf5r0lTzLj0mdS28tX+XGorU364eMPmnWVl8J36nlKGw3CZhjEiuMw8h8mKvhGD+4/lElBWjAhLJMg6fTw4zPZ8cOmcGQBm2Qxml1nAm13CpYGq1JKUlJJUzQn1PTAO0mgv6VMMpA/DuRfSWEu4lDIxdbAtdWIKvnn2Vk766CWfz9fpY0sH/UpdP50rfszaVpdVRmvIejEdLMk45s4Bu0EWHjeOySmFyZSiMahvZdNSn29peoI/YexYfKQTLeurTXXwEVLeSfInTWHkkMaeUx7sBvOCSTSj3AlcKjfueyS36tCrXDlgRtF0etFq9jhc1kfKuBT/OwMr0F4UUTTh1AN0g20+H/ScPcsIEsYu9d/zN5PmjprPtNwI1ZZcDK6iC97Mcjp2y2aX36f+QbpGHrgRuHlXJ+Zf6PFRL2uQSp8vxHeF2IoRb8Rd2rhMzsNxSRmEuKK4JFnkojhMcx6jzqHzGMGFcW+MhBj0bhf6cowN+45I4LHvwT6fteu7M42wGRI/pxcg6/MZdEvt1U1XaulHFXuLmqov/MukvRVL35/b3ODM1+4aPjtzeK7zmUkV2h3DN54HaQ9GzJvxHRb6Ks2gB81fwqraT+A7GvZJrRLRofU6G0urNL+zFw3v0FaVDFxsKEZW56F31r6ip6vOL+FCObBPuIMRiXld9RaMdLzRIOGhPey2T9vA/35DmZPK9IWaT9d/WgOGMieYqJ/dzjLIhZU118gbysxrNUGefxD6UO/hyNNllpFTOIbx32kSFQctnweV5PxTMHLjRqiAN+fQE9gL+Xy5WB6MOS4GJJuYbDUHhcKDhHGRbLzOpjsjdM1+iwAZLGeieehACX2hhI7SjK/ZUTNrvVje31TxJiFBGYViWFkCn9PMeX9fS6qVbzfCj4fOCTzDnuWy2c4xA7mdNkA3RS9FH2VeqzdCBlixxbzXjvkHU1I8BOYFb1pZvPIHSSIj4svT8xpzcxtXN+ZKyjdDvbz08niiF3PqV9Tn5NST8vg48MTaY8E5xqSSIsWoWHo+LtAzxdH/GDUyp37CBEYfso04F/NlMTcDJUTpECLY0HFGQHImE8xsEUdgnrQlixIvGhJA1BvxpDHGxEMBYFeNOHcBJlSjwe2JcSfbBEsGOPPBHg/6SBBOCsLLw0SpUxod0Z1bFMfLkbQ3UiZxEyd0Dx8t+SRBu18Q9msFbI4e3p1THEfkSEh7kEJ5orR10qTWDvbgPWn5aWvCYyOAjwgXyjJi34uMjo58L25cmRAeQZWI2PA1QQLsPESAH8WGFwZZ4SPoR73BHPzIPMJj9AreBzKUmrH4todT18ANvi1oc3YGjUT/0j+ExUwq8PI9BLaCQIpvewwYu2evAG/Vo/5avPdY7o+BemLLXw3y+AdkzP9bpIxB1wm5EYq8fesHbPEPtm6HrHvtx4jcGPR8fDDpkZBefIjB46QnlUNRltv4Z/pO/J6dxEjhYAtmoMeq+GozvUVvNYOW3m6GCIhoprcfr97B8AcIQYsfD8ljUvGNjvkrpj0ETA48ZMIxCeqsRIsQALE0gi2GB+glSOfbOjW3GSBM9yPq8/rpJXrJDz0BPxV6xdN4uiCGDQed3WhgFkBUZEFsmeyyBpzXrm7UGTBZG8Lh5aubFufk5eUsbrrFGr7McYdbltxa0nKYqRKbQjvikXYkTGM0f2xuyM3Ly21oXnWfvf6I1BmZwfh7EWWIYsg2nHhsDhOnczhJcmI6eBAmy3jZ3RiJmKQR/JA99FcwsfaVbNDDyi1rL9NPj9hfo61wjM6BjzOLijLpeTgk/pL+ip6tfYWupzeOgPny2tcUu9J/9mhxJlgyi985NFRbvCVewXUNXLJaW0RxZqtRYtnfYdcYomXQWdnJHQA3jiEEkeTQWcWxdDP9IvvVWvo2TK553XEMEq+s69/QDU1Q7p0zxwsm9qS379whr8NI2PJqLUyGyfNeX3eFfnJU2U+uHR9cVV1IqgurqwuV44XVp0h2qN55X5XJwtk59yP0IZuHrqBOBIuIYhkcoT6Kx79Pu2HS/IPZIMOqLWs/pteOOk4NPgEb6QAIdAPsyZk5Mwd+wVaHMexJv719W7xCu2l37UG6lvYdBcvHa08p89741zd63phTRGqL5ggo6SlvdbWXzCqsPq78NnSu7wnKy2HNZbVoRCI7UJEOyRj+sPE002tOOY7Qa5fXboFWkLNeqYUSZRocp9XwSUZxcQZ9Hw6LV2pOoVmvHQEDbGIENEG5i6bLgMSM4n8+FNLTtAds99DaWEvgcf4o5SyYe9x+kF6/tGoTPAdRmS/XQIEy//QxKC2oqioAI3tS5auvxCtzT6y6RK8fhChYcwCJaMJhxc0vqSxQ/qmgsrKAlBZUHlauheTpvd9uj5DnLzJct6qfq5fXbYHVIGcfrIVJihbaVLu1wW7Vbs8zK0A8e9Jvb91S9cVMjPrazD6gpfeZTXzYbCFMcppVRsGMpp55OWgx1/3JeAxW1Y7AORgM/m3rWrsdLkQVmEVSU16cX/e7uvkvpqRiQsG06XJ0t64Tf+l0nG1dt025gyOIZlvq5u9KSU1N2TW/rsWnnMRPyTDkctbhvIcNvYIXWyLzdwYLoYesUbaQG4iK2cWO2gdpeUYLqDD0MUTOPhDIGnZEs58yArR86FznuWEsU4YDi2x26dA4klkn8Qa6vhk2QUfX4Jxm/ngX9r7ogn1dmlmwqZmuhxtdg9XN/DEcUgqb+9hMyNansfaQET2mcROCmGEMVqxm5u+h6kN2MOwgqykV2wH9yQG9DvVFU38Pogaf4FVuE62KI/oJ02RDdWW2w5dqQwU/8+N1q1DlvsL863u61KLE7x/o8w0VJQM/Y/SQ3unIrqxueEa1BqT5VFNsO7p39/UC771a77RowpaKe9nvJQIT1Pog5LGx8XblBKmCNGTf3xMogAQvPnz9PYKX/08sVDTG1OKUlOLUgS/UaZtm1NAaYTsl7i9ZQ+L6O4Rl0OGa577LuWvc+C+x96/vYh0lLBuM+7XwI/dTLtdT7v4d6rRTWDnku0IBrqFnZ5bVIqKP8lasJlithWnaLhTsr8qFJBulF/70p4undou36HeTJ5+jv1fCybeQ8nH3+Xv6aENczmOFlab+hqMDg1rLOt12A+tiUFrYDwQ6c3RUJp601nzegTNX6WlYAI2zSUV945F6zU56ZmZVQaWspWcIADxJ9GmljQUnL2p2Dpr5T8H+5KJFu+vqBq8qvyHRzStLHPEO5SPYCV9nZe0yZT2RcH0oHvegSzNEJ0oGWU8iQWM12dgPEugngVceGIwZgPFp0BiT1a0a3R5Rcot7ihfA1J/20v96jX7zmTX9s583H0kwx6WnLd09cXrR9LGroOa9sHNbdyz8wcKk5lqhaVFJZNwmqtw884MXNdvJujpBa3xzuSaZH9sxa06Z7x+HJSduPbdYHv/DgmEhfbehvlmGN7JUkcG78GDM12CeyFFTPNqVeNxC1gzjz+c2nVo63Xxs8rKJWXoBJM0tmEbfGm4qzpoOH3xpzQfyxLzW1gnE9NHo6tol1eMEic4ZVPrjnVi0kqAe2sQ2bgqupScaq8WGlUWgWHI51SKJl/UYT6zccNsCSkBtiVZLsiefuFSDYT3Fi8Zk7EUnmjTRYtsFeuDDJS05MW79M3mr3mla+d8dzac31KTPmBYfFiYSUef48PhPjm9ryZsSGZZkdNvzq0Y9rdNcwDq5Dg5C3QW+7UN64IKptvS3tvHbvu5c9pv1Exau21rc9LIpwpQwUjTq8576yeVDz5+4WZ1nXT43wV60rPLJbDp/UksNrP3iQ2SA63Pst058gOYDbhRnRUw8l/sRt4HbxPzO4WYpInCpuVgSbVh6JXuwnnJngKTTCwaPWmG5Xbhpm1U0Yt3FyBGpGYemPM77p2TD904JjgJ2QFpFLeYpGx8X15Qx1Zk31p5ki9ZLUuXE0lmuJlcakJMVLeFS1iIvrB8drY0aloilakqCZwzwRORtxlgwxS4IThggJd4TDxoiaAIT80fFPGrCPPru+puFn504P/ybr4ihA/6dKASLshEJic7xE8tmzu3KzA7TABBe8y5fNbWo3ilQn/SuFKM16b2l5bOeayqfGhYmhIulU+fVNDdWVv4NMzX10MBHyPR5uhWUu8D9P1VnIMt4nGNgZGBgAOJ/1bf64vltvjJwszOAwAOlmqvINEc/WJyDgQlEAQA+dgnjAHicY2BkYGBnAAGOPgaG//85+hkYGVCBPgBGJwNkAAAAeJxjYGBgYB/EmKMPtxwAhg4B0gAAAAAAAA4AaAB+AMwA4AECAUIBbAGYAe4CLgKKAtAC/ANiA4wDqAPgBDAEsATaBQgFWgXABggGLgZwBqwG9gdOB4oH0ggqCHAIhgicCMgJJAlWCYgJrAnyCkAKdgrkC7J4nGNgZGBg0GdoZmBnAAEmIOYCQgaG/2A+AwAaqwHQAHicXZBNaoNAGIZfE5PQCKFQ2lUps2oXBfOzzAESyDKBQJdGR2NQR3QSSE/QE/QEPUUPUHqsvsrXjTMw83zPvPMNCuAWP3DQDAejdm1GjzwS7pMmwi75XngAD4/CQ/oX4TFe4Qt7uMMbOzjuDc0EmXCP/C7cJ38Iu+RP4QEe8CU8pP8WHmOPX2EPz87TPo202ey2OjlnQSXV/6arOjWFmvszMWtd6CqwOlKHq6ovycLaWMWVydXKFFZnmVFlZU46tP7R2nI5ncbi/dDkfDtFBA2DDXbYkhKc+V0Bqs5Zt9JM1HQGBRTm/EezTmZNKtpcAMs9Yu6AK9caF76zoLWIWcfMGOSkVduvSWechqZsz040Ib2PY3urxBJTzriT95lipz+TN1fmAAAAeJxtkXlT2zAQxf1C4thJAwRajt4HRy8VMwwfSJHXsQZZcnUQ+PYoTtwpM+wf2t9brWZ2n5JBsol58nJcYYAdDDFCijEy5JhgileYYRd72MccBzjEa7zBEY5xglO8xTu8xwd8xCd8xhd8xTec4RwXuMR3/MBP/MJvMPzBFYpk2Cr+OF0fTEgrFI1aHhxN740KDbEmeJpsWZlVj40s+45aLuv9KijlhCXSjLQnu/d/4UH6sWul1mRzFxZeekUuE7z10mg3qMtM1FGQddPSrLQyvJR6OaukItYXDp6pCJrmz0umqkau5pZ2hFmm7m+ImG5W2t0kZoJXUtPhVnYTbbdOBdeCVGqpJe7XKTqSbRK7zbdwXfR0U+SVsStuS3Y76em6+Ic3xYiHUppc04Nn0lMzay3dSxNcp8auDlWlaCi48yetFD7Y9USsx87G45cuop1ZxQUtjLnL4j53FO0a+5X08UXqQ7NQNo92R0XOz7sxWEnxN2TneJI8Acttu4Q=) format("woff");font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder,.vjs-icon-play{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder:before,.vjs-icon-play:before{content:"\f101"}.vjs-icon-play-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-play-circle:before{content:"\f102"}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder,.vjs-icon-pause{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before,.vjs-icon-pause:before{content:"\f103"}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder,.vjs-icon-volume-mute{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before,.vjs-icon-volume-mute:before{content:"\f104"}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder,.vjs-icon-volume-low{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before,.vjs-icon-volume-low:before{content:"\f105"}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder,.vjs-icon-volume-mid{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before,.vjs-icon-volume-mid:before{content:"\f106"}.video-js .vjs-mute-control .vjs-icon-placeholder,.vjs-icon-volume-high{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control .vjs-icon-placeholder:before,.vjs-icon-volume-high:before{content:"\f107"}.video-js .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-enter:before{content:"\f108"}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-exit:before{content:"\f109"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10a"}.video-js .vjs-subs-caps-button .vjs-icon-placeholder,.video-js .vjs-subtitles-button .vjs-icon-placeholder,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-subtitles{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js .vjs-subtitles-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-subtitles:before{content:"\f10b"}.video-js .vjs-captions-button .vjs-icon-placeholder,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-captions{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-captions-button .vjs-icon-placeholder:before,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-captions:before{content:"\f10c"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f10d"}.video-js .vjs-chapters-button .vjs-icon-placeholder,.vjs-icon-chapters{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-chapters-button .vjs-icon-placeholder:before,.vjs-icon-chapters:before{content:"\f10e"}.vjs-icon-downloading{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-downloading:before{content:"\f10f"}.vjs-icon-file-download{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-file-download:before{content:"\f110"}.vjs-icon-file-download-done{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-file-download-done:before{content:"\f111"}.vjs-icon-file-download-off{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-file-download-off:before{content:"\f112"}.vjs-icon-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f113"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f114"}.vjs-icon-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f115"}.video-js .vjs-play-progress,.video-js .vjs-volume-level,.vjs-icon-circle,.vjs-seek-to-live-control .vjs-icon-placeholder{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-progress:before,.video-js .vjs-volume-level:before,.vjs-icon-circle:before,.vjs-seek-to-live-control .vjs-icon-placeholder:before{content:"\f116"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f117"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f118"}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder,.vjs-icon-cancel{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before,.vjs-icon-cancel:before{content:"\f119"}.vjs-icon-repeat{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-repeat:before{content:"\f11a"}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder,.vjs-icon-replay{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before,.vjs-icon-replay:before{content:"\f11b"}.video-js .vjs-skip-backward-5 .vjs-icon-placeholder,.vjs-icon-replay-5{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-skip-backward-5 .vjs-icon-placeholder:before,.vjs-icon-replay-5:before{content:"\f11c"}.video-js .vjs-skip-backward-10 .vjs-icon-placeholder,.vjs-icon-replay-10{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-skip-backward-10 .vjs-icon-placeholder:before,.vjs-icon-replay-10:before{content:"\f11d"}.video-js .vjs-skip-backward-30 .vjs-icon-placeholder,.vjs-icon-replay-30{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-skip-backward-30 .vjs-icon-placeholder:before,.vjs-icon-replay-30:before{content:"\f11e"}.video-js .vjs-skip-forward-5 .vjs-icon-placeholder,.vjs-icon-forward-5{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-skip-forward-5 .vjs-icon-placeholder:before,.vjs-icon-forward-5:before{content:"\f11f"}.video-js .vjs-skip-forward-10 .vjs-icon-placeholder,.vjs-icon-forward-10{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-skip-forward-10 .vjs-icon-placeholder:before,.vjs-icon-forward-10:before{content:"\f120"}.video-js .vjs-skip-forward-30 .vjs-icon-placeholder,.vjs-icon-forward-30{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-skip-forward-30 .vjs-icon-placeholder:before,.vjs-icon-forward-30:before{content:"\f121"}.video-js .vjs-audio-button .vjs-icon-placeholder,.vjs-icon-audio{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-audio-button .vjs-icon-placeholder:before,.vjs-icon-audio:before{content:"\f122"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f123"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f124"}.vjs-icon-shuffle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-shuffle:before{content:"\f125"}.vjs-icon-cast{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cast:before{content:"\f126"}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-enter:before{content:"\f127"}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-exit:before{content:"\f128"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f129"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f12a"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f12b"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f12c"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f12d"}.video-js .vjs-descriptions-button .vjs-icon-placeholder,.vjs-icon-audio-description{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-descriptions-button .vjs-icon-placeholder:before,.vjs-icon-audio-description:before{content:"\f12e"}.video-js{display:inline-block;vertical-align:top;box-sizing:border-box;color:#fff;background-color:#000;position:relative;padding:0;font-size:10px;line-height:1;font-weight:400;font-style:normal;font-family:Arial,Helvetica,sans-serif;word-break:initial}.video-js:-moz-full-screen{position:absolute}.video-js:-webkit-full-screen{width:100%!important;height:100%!important}.video-js[tabindex="-1"]{outline:0}.video-js *,.video-js :after,.video-js :before{box-sizing:inherit}.video-js ul{font-family:inherit;font-size:inherit;line-height:inherit;list-style-position:outside;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}.video-js.vjs-1-1,.video-js.vjs-16-9,.video-js.vjs-4-3,.video-js.vjs-9-16,.video-js.vjs-fluid{width:100%;max-width:100%}.video-js.vjs-1-1:not(.vjs-audio-only-mode),.video-js.vjs-16-9:not(.vjs-audio-only-mode),.video-js.vjs-4-3:not(.vjs-audio-only-mode),.video-js.vjs-9-16:not(.vjs-audio-only-mode),.video-js.vjs-fluid:not(.vjs-audio-only-mode){height:0}.video-js.vjs-16-9:not(.vjs-audio-only-mode){padding-top:56.25%}.video-js.vjs-4-3:not(.vjs-audio-only-mode){padding-top:75%}.video-js.vjs-9-16:not(.vjs-audio-only-mode){padding-top:177.7777777778%}.video-js.vjs-1-1:not(.vjs-audio-only-mode){padding-top:100%}.video-js.vjs-fill:not(.vjs-audio-only-mode){width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}.video-js.vjs-audio-only-mode .vjs-tech{display:none}body.vjs-full-window,body.vjs-pip-window{padding:0;margin:0;height:100%}.vjs-full-window .video-js.vjs-fullscreen,body.vjs-pip-window .video-js{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen:not(.vjs-ios-native-fs),body.vjs-pip-window .video-js{width:100%!important;height:100%!important;padding-top:0!important;display:block}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.vjs-pip-container .vjs-pip-text{position:absolute;bottom:10%;font-size:2em;background-color:rgba(0,0,0,.7);padding:.5em;text-align:center;width:100%}.vjs-layout-small.vjs-pip-container .vjs-pip-text,.vjs-layout-tiny.vjs-pip-container .vjs-pip-text,.vjs-layout-x-small.vjs-pip-container .vjs-pip-text{bottom:0;font-size:1.4em}.vjs-hidden{display:none!important}.vjs-disabled{opacity:.5;cursor:default}.video-js .vjs-offscreen{height:1px;left:-9999px;position:absolute;top:0;width:1px}.vjs-lock-showing{display:block!important;opacity:1!important;visibility:visible!important}.vjs-no-js{padding:20px;color:#fff;background-color:#000;font-size:18px;font-family:Arial,Helvetica,sans-serif;text-align:center;width:300px;height:150px;margin:0 auto}.vjs-no-js a,.vjs-no-js a:visited{color:#66a8cc}.video-js .vjs-big-play-button{font-size:3em;line-height:1.5em;height:1.63332em;width:3em;display:block;position:absolute;top:50%;left:50%;padding:0;margin-top:-.81666em;margin-left:-1.5em;cursor:pointer;opacity:1;border:.06666em solid #fff;background-color:#2b333f;background-color:rgba(43,51,63,.7);border-radius:.3em;transition:all .4s}.vjs-big-play-button .vjs-svg-icon{width:1em;height:1em;position:absolute;top:50%;left:50%;line-height:1;transform:translate(-50%,-50%)}.video-js .vjs-big-play-button:focus,.video-js:hover .vjs-big-play-button{border-color:#fff;background-color:#73859f;background-color:rgba(115,133,159,.5);transition:all 0s}.vjs-controls-disabled .vjs-big-play-button,.vjs-error .vjs-big-play-button,.vjs-has-started .vjs-big-play-button,.vjs-using-native-controls .vjs-big-play-button{display:none}.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause:not(.vjs-seeking,.vjs-scrubbing,.vjs-error) .vjs-big-play-button{display:block}.video-js button{background:0 0;border:none;color:inherit;display:inline-block;font-size:inherit;line-height:inherit;text-transform:none;text-decoration:none;transition:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.vjs-control .vjs-button{width:100%;height:100%}.video-js .vjs-control.vjs-close-button{cursor:pointer;height:3em;position:absolute;right:0;top:.5em;z-index:2}.video-js .vjs-modal-dialog{background:rgba(0,0,0,.8);background:linear-gradient(180deg,rgba(0,0,0,.8),rgba(255,255,255,0));overflow:auto}.video-js .vjs-modal-dialog>*{box-sizing:border-box}.vjs-modal-dialog .vjs-modal-dialog-content{font-size:1.2em;line-height:1.5;padding:20px 24px;z-index:1}.vjs-menu-button{cursor:pointer}.vjs-menu-button.vjs-disabled{cursor:default}.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu{display:none}.vjs-menu .vjs-menu-content{display:block;padding:0;margin:0;font-family:Arial,Helvetica,sans-serif;overflow:auto}.vjs-menu .vjs-menu-content>*{box-sizing:border-box}.vjs-scrubbing .vjs-control.vjs-menu-button:hover .vjs-menu{display:none}.vjs-menu li{display:flex;justify-content:center;list-style:none;margin:0;padding:.2em 0;line-height:1.4em;font-size:1.2em;text-align:center;text-transform:lowercase}.js-focus-visible .vjs-menu li.vjs-menu-item:hover,.vjs-menu li.vjs-menu-item:focus,.vjs-menu li.vjs-menu-item:hover{background-color:#73859f;background-color:rgba(115,133,159,.5)}.js-focus-visible .vjs-menu li.vjs-selected:hover,.vjs-menu li.vjs-selected,.vjs-menu li.vjs-selected:focus,.vjs-menu li.vjs-selected:hover{background-color:#fff;color:#2b333f}.js-focus-visible .vjs-menu li.vjs-selected:hover .vjs-svg-icon,.vjs-menu li.vjs-selected .vjs-svg-icon,.vjs-menu li.vjs-selected:focus .vjs-svg-icon,.vjs-menu li.vjs-selected:hover .vjs-svg-icon{fill:#000}.js-focus-visible .vjs-menu :not(.vjs-selected):focus:not(.focus-visible),.video-js .vjs-menu :not(.vjs-selected):focus:not(:focus-visible){background:0 0}.vjs-menu li.vjs-menu-title{text-align:center;text-transform:uppercase;font-size:1em;line-height:2em;padding:0;margin:0 0 .3em 0;font-weight:700;cursor:default}.vjs-menu-button-popup .vjs-menu{display:none;position:absolute;bottom:0;width:10em;left:-3em;height:0;margin-bottom:1.5em;border-top-color:rgba(43,51,63,.7)}.vjs-pip-window .vjs-menu-button-popup .vjs-menu{left:unset;right:1em}.vjs-menu-button-popup .vjs-menu .vjs-menu-content{background-color:#2b333f;background-color:rgba(43,51,63,.7);position:absolute;width:100%;bottom:1.5em;max-height:15em}.vjs-layout-tiny .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:5em}.vjs-layout-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:10em}.vjs-layout-medium .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:14em}.vjs-layout-huge .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:25em}.vjs-menu-button-popup .vjs-menu.vjs-lock-showing,.vjs-workinghover .vjs-menu-button-popup.vjs-hover .vjs-menu{display:block}.video-js .vjs-menu-button-inline{transition:all .4s;overflow:hidden}.video-js .vjs-menu-button-inline:before{width:2.222222222em}.video-js .vjs-menu-button-inline.vjs-slider-active,.video-js .vjs-menu-button-inline:focus,.video-js .vjs-menu-button-inline:hover{width:12em}.vjs-menu-button-inline .vjs-menu{opacity:0;height:100%;width:auto;position:absolute;left:4em;top:0;padding:0;margin:0;transition:all .4s}.vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-menu-button-inline:focus .vjs-menu,.vjs-menu-button-inline:hover .vjs-menu{display:block;opacity:1}.vjs-menu-button-inline .vjs-menu-content{width:auto;height:100%;margin:0;overflow:hidden}.video-js .vjs-control-bar{display:none;width:100%;position:absolute;bottom:0;left:0;right:0;height:3em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js:not(.vjs-controls-disabled,.vjs-using-native-controls,.vjs-error) .vjs-control-bar.vjs-lock-showing{display:flex!important}.vjs-audio-only-mode .vjs-control-bar,.vjs-has-started .vjs-control-bar{display:flex;visibility:visible;opacity:1;transition:visibility .1s,opacity .1s}.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{visibility:visible;opacity:0;pointer-events:none;transition:visibility 1s,opacity 1s}.vjs-controls-disabled .vjs-control-bar,.vjs-error .vjs-control-bar,.vjs-using-native-controls .vjs-control-bar{display:none!important}.vjs-audio-only-mode.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar,.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible;pointer-events:auto}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;flex:none}.video-js .vjs-control.vjs-visible-text{width:auto;padding-left:1em;padding-right:1em}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.vjs-button>.vjs-icon-placeholder{display:block}.vjs-button>.vjs-svg-icon{display:inline-block}.video-js .vjs-control:focus,.video-js .vjs-control:focus:before,.video-js .vjs-control:hover:before{text-shadow:0 0 1em #fff}.video-js :not(.vjs-visible-text)>.vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.video-js .vjs-custom-control-spacer{display:none}.video-js .vjs-progress-control{cursor:pointer;flex:auto;display:flex;align-items:center;min-width:4em;touch-action:none}.video-js .vjs-progress-control.disabled{cursor:default}.vjs-live .vjs-progress-control{display:none}.vjs-liveui .vjs-progress-control{display:flex;align-items:center}.video-js .vjs-progress-holder{flex:auto;transition:all .2s;height:.3em}.video-js .vjs-progress-control .vjs-progress-holder{margin:0 10px}.video-js .vjs-progress-control:hover .vjs-progress-holder{font-size:1.6666666667em}.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled{font-size:1em}.video-js .vjs-progress-holder .vjs-load-progress,.video-js .vjs-progress-holder .vjs-load-progress div,.video-js .vjs-progress-holder .vjs-play-progress{position:absolute;display:block;height:100%;margin:0;padding:0;width:0}.video-js .vjs-play-progress{background-color:#fff}.video-js .vjs-play-progress:before{font-size:.9em;position:absolute;right:-.5em;line-height:.35em;z-index:1}.vjs-svg-icons-enabled .vjs-play-progress:before{content:none!important}.vjs-play-progress .vjs-svg-icon{position:absolute;top:-.35em;right:-.4em;width:.9em;height:.9em;pointer-events:none;line-height:.15em;z-index:1}.video-js .vjs-load-progress{background:rgba(115,133,159,.5)}.video-js .vjs-load-progress div{background:rgba(115,133,159,.75)}.video-js .vjs-time-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:absolute;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-progress-holder:focus .vjs-time-tooltip{display:none}.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip,.video-js .vjs-progress-control:hover .vjs-time-tooltip{display:block;font-size:.6em;visibility:visible}.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip{font-size:1em}.video-js .vjs-progress-control .vjs-mouse-display{display:none;position:absolute;width:1px;height:100%;background-color:#000;z-index:1}.video-js .vjs-progress-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display{visibility:hidden;opacity:0;transition:visibility 1s,opacity 1s}.vjs-mouse-display .vjs-time-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.video-js .vjs-slider{position:relative;cursor:pointer;padding:0;margin:0 .45em 0 .45em;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:#73859f;background-color:rgba(115,133,159,.5)}.video-js .vjs-slider.disabled{cursor:default}.video-js .vjs-slider:focus{text-shadow:0 0 1em #fff;box-shadow:0 0 1em #fff}.video-js .vjs-mute-control{cursor:pointer;flex:none}.video-js .vjs-volume-control{cursor:pointer;margin-right:1em;display:flex}.video-js .vjs-volume-control.vjs-volume-horizontal{width:5em}.video-js .vjs-volume-panel .vjs-volume-control{visibility:visible;opacity:0;width:1px;height:1px;margin-left:-1px}.video-js .vjs-volume-panel{transition:width 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.video-js .vjs-volume-panel .vjs-volume-control:active,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control,.video-js .vjs-volume-panel:active .vjs-volume-control,.video-js .vjs-volume-panel:focus .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;margin-right:0}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical{left:-3.5em;transition:left 0s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active{width:10em;transition:width .1s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only{width:4em}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{height:8em;width:3em;left:-3000em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel{display:flex}.video-js .vjs-volume-bar{margin:1.35em .45em}.vjs-volume-bar.vjs-slider-horizontal{width:5em;height:.3em}.vjs-volume-bar.vjs-slider-vertical{width:.3em;height:5em;margin:1.35em auto}.video-js .vjs-volume-level{position:absolute;bottom:0;left:0;background-color:#fff}.video-js .vjs-volume-level:before{position:absolute;font-size:.9em;z-index:1}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em;z-index:1}.vjs-svg-icons-enabled .vjs-volume-level:before{content:none}.vjs-volume-level .vjs-svg-icon{position:absolute;width:.9em;height:.9em;pointer-events:none;z-index:1}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{line-height:.35em;right:-.5em}.vjs-slider-horizontal .vjs-volume-level .vjs-svg-icon{right:-.3em;transform:translateY(-50%)}.vjs-slider-vertical .vjs-volume-level .vjs-svg-icon{top:-.55em;transform:translateX(-50%)}.video-js .vjs-volume-panel.vjs-volume-panel-vertical{width:4em}.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level{height:100%}.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level{width:100%}.video-js .vjs-volume-vertical{width:3em;height:8em;bottom:8em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js .vjs-volume-horizontal .vjs-menu{left:-2em}.video-js .vjs-volume-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:absolute;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-volume-control:hover .vjs-progress-holder:focus .vjs-volume-tooltip,.video-js .vjs-volume-control:hover .vjs-volume-tooltip{display:block;font-size:1em;visibility:visible}.video-js .vjs-volume-vertical:hover .vjs-progress-holder:focus .vjs-volume-tooltip,.video-js .vjs-volume-vertical:hover .vjs-volume-tooltip{left:1em;top:-12px}.video-js .vjs-volume-control.disabled:hover .vjs-volume-tooltip{font-size:1em}.video-js .vjs-volume-control .vjs-mouse-display{display:none;position:absolute;width:100%;height:1px;background-color:#000;z-index:1}.video-js .vjs-volume-horizontal .vjs-mouse-display{width:1px;height:100%}.video-js .vjs-volume-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-volume-control .vjs-mouse-display{visibility:hidden;opacity:0;transition:visibility 1s,opacity 1s}.vjs-mouse-display .vjs-volume-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.vjs-poster{display:inline-block;vertical-align:middle;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-has-started .vjs-poster,.vjs-using-native-controls .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster,.vjs-has-started.vjs-audio-poster-mode .vjs-poster,.vjs-pip-container.vjs-has-started .vjs-poster{display:block}.vjs-poster img{width:100%;height:100%;-o-object-fit:contain;object-fit:contain}.video-js .vjs-live-control{display:flex;align-items:flex-start;flex:auto;font-size:1em;line-height:3em}.video-js.vjs-liveui .vjs-live-control,.video-js:not(.vjs-live) .vjs-live-control{display:none}.video-js .vjs-seek-to-live-control{align-items:center;cursor:pointer;flex:none;display:inline-flex;height:100%;padding-left:.5em;padding-right:.5em;font-size:1em;line-height:3em;width:auto;min-width:4em}.video-js.vjs-live:not(.vjs-liveui) .vjs-seek-to-live-control,.video-js:not(.vjs-live) .vjs-seek-to-live-control{display:none}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge{cursor:auto}.vjs-seek-to-live-control .vjs-icon-placeholder{margin-right:.5em;color:#888}.vjs-svg-icons-enabled .vjs-seek-to-live-control{line-height:0}.vjs-seek-to-live-control .vjs-svg-icon{width:1em;height:1em;pointer-events:none;fill:#888}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder{color:red}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-svg-icon{fill:red}.video-js .vjs-time-control{flex:none;font-size:1em;line-height:3em;min-width:2em;width:auto;padding-left:1em;padding-right:1em}.video-js .vjs-current-time,.video-js .vjs-duration,.vjs-live .vjs-time-control,.vjs-live .vjs-time-divider{display:none}.vjs-time-divider{display:none;line-height:3em}.video-js .vjs-play-control{cursor:pointer}.video-js .vjs-play-control .vjs-icon-placeholder{flex:none}.vjs-text-track-display{position:absolute;bottom:3em;left:0;right:0;top:0;pointer-events:none}.vjs-error .vjs-text-track-display{display:none}.video-js.vjs-controls-disabled .vjs-text-track-display,.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display{bottom:1em}.video-js .vjs-text-track{font-size:1.4em;text-align:center;margin-bottom:.1em}.vjs-subtitles{color:#fff}.vjs-captions{color:#fc6}.vjs-tt-cue{display:block}video::-webkit-media-text-track-display{transform:translateY(-3em)}.video-js.vjs-controls-disabled video::-webkit-media-text-track-display,.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display{transform:translateY(-1.5em)}.video-js.vjs-force-center-align-cues .vjs-text-track-cue{text-align:center!important;width:80%!important}.video-js .vjs-picture-in-picture-control{cursor:pointer;flex:none}.video-js.vjs-audio-only-mode .vjs-picture-in-picture-control,.vjs-pip-window .vjs-picture-in-picture-control{display:none}.video-js .vjs-fullscreen-control{cursor:pointer;flex:none}.video-js.vjs-audio-only-mode .vjs-fullscreen-control,.vjs-pip-window .vjs-fullscreen-control{display:none}.vjs-playback-rate .vjs-playback-rate-value,.vjs-playback-rate>.vjs-menu-button{position:absolute;top:0;left:0;width:100%;height:100%}.vjs-playback-rate .vjs-playback-rate-value{pointer-events:none;font-size:1.5em;line-height:2;text-align:center}.vjs-playback-rate .vjs-menu{width:4em;left:0}.vjs-error .vjs-error-display .vjs-modal-dialog-content{font-size:1.4em;text-align:center}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);opacity:.85;text-align:left;border:.6em solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:5em;height:5em;border-radius:50%;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:flex;justify-content:center;align-items:center;animation:vjs-spinner-show 0s linear .3s forwards}.vjs-error .vjs-loading-spinner{display:none}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;box-sizing:inherit;width:inherit;height:inherit;border-radius:inherit;opacity:1;border:inherit;border-color:transparent;border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:before{animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite}.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:before{border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:after{border-top-color:#fff;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}.video-js.vjs-audio-only-mode .vjs-captions-button{display:none}.vjs-chapters-button .vjs-menu ul{width:24em}.video-js.vjs-audio-only-mode .vjs-descriptions-button{display:none}.vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-svg-icon{width:1.5em;height:1.5em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:"\f10c";font-size:1.5em;line-height:inherit}.video-js.vjs-audio-only-mode .vjs-subs-caps-button{display:none}.video-js .vjs-audio-button+.vjs-menu .vjs-description-menu-item .vjs-menu-item-text .vjs-icon-placeholder,.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-audio-button+.vjs-menu .vjs-description-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before,.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:" \f12e";font-size:1.5em;line-height:inherit}.video-js.vjs-layout-small .vjs-current-time,.video-js.vjs-layout-small .vjs-duration,.video-js.vjs-layout-small .vjs-playback-rate,.video-js.vjs-layout-small .vjs-remaining-time,.video-js.vjs-layout-small .vjs-time-divider,.video-js.vjs-layout-small .vjs-volume-control,.video-js.vjs-layout-tiny .vjs-current-time,.video-js.vjs-layout-tiny .vjs-duration,.video-js.vjs-layout-tiny .vjs-playback-rate,.video-js.vjs-layout-tiny .vjs-remaining-time,.video-js.vjs-layout-tiny .vjs-time-divider,.video-js.vjs-layout-tiny .vjs-volume-control,.video-js.vjs-layout-x-small .vjs-current-time,.video-js.vjs-layout-x-small .vjs-duration,.video-js.vjs-layout-x-small .vjs-playback-rate,.video-js.vjs-layout-x-small .vjs-remaining-time,.video-js.vjs-layout-x-small .vjs-time-divider,.video-js.vjs-layout-x-small .vjs-volume-control{display:none}.video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js.vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js.vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js.vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:auto;width:initial}.video-js.vjs-layout-tiny .vjs-progress-control,.video-js.vjs-layout-x-small .vjs-progress-control{display:none}.video-js.vjs-layout-x-small .vjs-custom-control-spacer{flex:auto;display:block}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.vjs-error .vjs-text-track-settings{display:none}.vjs-text-track-settings .vjs-modal-dialog-content{display:table}.vjs-text-track-settings .vjs-track-settings-colors,.vjs-text-track-settings .vjs-track-settings-controls,.vjs-text-track-settings .vjs-track-settings-font{display:table-cell}.vjs-text-track-settings .vjs-track-settings-controls{text-align:right;vertical-align:bottom}@supports (display:grid){.vjs-text-track-settings .vjs-modal-dialog-content{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr;padding:20px 24px 0 24px}.vjs-track-settings-controls .vjs-default-button{margin-bottom:20px}.vjs-text-track-settings .vjs-track-settings-controls{grid-column:1/-1}.vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content{grid-template-columns:1fr}}.vjs-text-track-settings select{font-size:inherit}.vjs-track-setting>select{margin-right:1em;margin-bottom:.5em}.vjs-text-track-settings fieldset{margin:10px;border:none}.vjs-text-track-settings fieldset span{display:inline-block;padding:0 .6em .8em}.vjs-text-track-settings fieldset span>select{max-width:7.3em}.vjs-text-track-settings legend{color:#fff;font-weight:700;font-size:1.2em}.vjs-text-track-settings .vjs-label{margin:0 .5em .5em 0}.vjs-track-settings-controls button:active,.vjs-track-settings-controls button:focus{outline-style:solid;outline-width:medium;background-image:linear-gradient(0deg,#fff 88%,#73859f 100%)}.vjs-track-settings-controls button:hover{color:rgba(43,51,63,.75)}.vjs-track-settings-controls button{background-color:#fff;background-image:linear-gradient(-180deg,#fff 88%,#73859f 100%);color:#2b333f;cursor:pointer;border-radius:2px}.vjs-track-settings-controls .vjs-default-button{margin-right:1em}.vjs-title-bar{background:rgba(0,0,0,.9);background:linear-gradient(180deg,rgba(0,0,0,.9) 0,rgba(0,0,0,.7) 60%,rgba(0,0,0,0) 100%);font-size:1.2em;line-height:1.5;transition:opacity .1s;padding:.666em 1.333em 4em;pointer-events:none;position:absolute;top:0;width:100%}.vjs-error .vjs-title-bar{display:none}.vjs-title-bar-description,.vjs-title-bar-title{margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vjs-title-bar-title{font-weight:700;margin-bottom:.333em}.vjs-playing.vjs-user-inactive .vjs-title-bar{opacity:0;transition:opacity 1s}.video-js .vjs-skip-forward-5{cursor:pointer}.video-js .vjs-skip-forward-10{cursor:pointer}.video-js .vjs-skip-forward-30{cursor:pointer}.video-js .vjs-skip-backward-5{cursor:pointer}.video-js .vjs-skip-backward-10{cursor:pointer}.video-js .vjs-skip-backward-30{cursor:pointer}@media print{.video-js>:not(.vjs-tech):not(.vjs-poster){visibility:hidden}}.vjs-resize-manager{position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:-1000}.js-focus-visible .video-js :focus:not(.focus-visible){outline:0}.video-js :focus:not(:focus-visible){outline:0}
\ No newline at end of file
diff --git a/source/src/public/twitch/video.js/alt/video.core.js b/source/src/public/twitch/video.js/alt/video.core.js
deleted file mode 100755
index 28b38e7..0000000
--- a/source/src/public/twitch/video.js/alt/video.core.js
+++ /dev/null
@@ -1,28595 +0,0 @@
-/**
- * @license
- * Video.js 8.12.0
- * Copyright Brightcove, Inc.
- * Available under Apache License Version 2.0
- *
- *
- * Includes vtt.js
- * Available under Apache License Version 2.0
- *
- */
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
-})(this, (function () { 'use strict';
-
- var version = "8.12.0";
-
- /**
- * An Object that contains lifecycle hooks as keys which point to an array
- * of functions that are run when a lifecycle is triggered
- *
- * @private
- */
- const hooks_ = {};
-
- /**
- * Get a list of hooks for a specific lifecycle
- *
- * @param {string} type
- * the lifecycle to get hooks from
- *
- * @param {Function|Function[]} [fn]
- * Optionally add a hook (or hooks) to the lifecycle that your are getting.
- *
- * @return {Array}
- * an array of hooks, or an empty array if there are none.
- */
- const hooks = function (type, fn) {
- hooks_[type] = hooks_[type] || [];
- if (fn) {
- hooks_[type] = hooks_[type].concat(fn);
- }
- return hooks_[type];
- };
-
- /**
- * Add a function hook to a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle to hook the function to.
- *
- * @param {Function|Function[]}
- * The function or array of functions to attach.
- */
- const hook = function (type, fn) {
- hooks(type, fn);
- };
-
- /**
- * Remove a hook from a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle that the function hooked to
- *
- * @param {Function} fn
- * The hooked function to remove
- *
- * @return {boolean}
- * The function that was removed or undef
- */
- const removeHook = function (type, fn) {
- const index = hooks(type).indexOf(fn);
- if (index <= -1) {
- return false;
- }
- hooks_[type] = hooks_[type].slice();
- hooks_[type].splice(index, 1);
- return true;
- };
-
- /**
- * Add a function hook that will only run once to a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle to hook the function to.
- *
- * @param {Function|Function[]}
- * The function or array of functions to attach.
- */
- const hookOnce = function (type, fn) {
- hooks(type, [].concat(fn).map(original => {
- const wrapper = (...args) => {
- removeHook(type, wrapper);
- return original(...args);
- };
- return wrapper;
- }));
- };
-
- /**
- * @file fullscreen-api.js
- * @module fullscreen-api
- */
-
- /**
- * Store the browser-specific methods for the fullscreen API.
- *
- * @type {Object}
- * @see [Specification]{@link https://fullscreen.spec.whatwg.org}
- * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
- */
- const FullscreenApi = {
- prefixed: true
- };
-
- // browser API methods
- const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
- // WebKit
- ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
- const specApi = apiMap[0];
- let browserApi;
-
- // determine the supported set of functions
- for (let i = 0; i < apiMap.length; i++) {
- // check for exitFullscreen function
- if (apiMap[i][1] in document) {
- browserApi = apiMap[i];
- break;
- }
- }
-
- // map the browser API names to the spec API names
- if (browserApi) {
- for (let i = 0; i < browserApi.length; i++) {
- FullscreenApi[specApi[i]] = browserApi[i];
- }
- FullscreenApi.prefixed = browserApi[0] !== specApi[0];
- }
-
- /**
- * @file create-logger.js
- * @module create-logger
- */
-
- // This is the private tracking variable for the logging history.
- let history = [];
-
- /**
- * Log messages to the console and history based on the type of message
- *
- * @private
- * @param {string} name
- * The name of the console method to use.
- *
- * @param {Object} log
- * The arguments to be passed to the matching console method.
- *
- * @param {string} [styles]
- * styles for name
- */
- const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
- const lvl = log.levels[level];
- const lvlRegExp = new RegExp(`^(${lvl})$`);
- let resultName = name;
- if (type !== 'log') {
- // Add the type to the front of the message when it's not "log".
- args.unshift(type.toUpperCase() + ':');
- }
- if (styles) {
- resultName = `%c${name}`;
- args.unshift(styles);
- }
-
- // Add console prefix after adding to history.
- args.unshift(resultName + ':');
-
- // Add a clone of the args at this point to history.
- if (history) {
- history.push([].concat(args));
-
- // only store 1000 history entries
- const splice = history.length - 1000;
- history.splice(0, splice > 0 ? splice : 0);
- }
-
- // If there's no console then don't try to output messages, but they will
- // still be stored in history.
- if (!window.console) {
- return;
- }
-
- // Was setting these once outside of this function, but containing them
- // in the function makes it easier to test cases where console doesn't exist
- // when the module is executed.
- let fn = window.console[type];
- if (!fn && type === 'debug') {
- // Certain browsers don't have support for console.debug. For those, we
- // should default to the closest comparable log.
- fn = window.console.info || window.console.log;
- }
-
- // Bail out if there's no console or if this type is not allowed by the
- // current logging level.
- if (!fn || !lvl || !lvlRegExp.test(type)) {
- return;
- }
- fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
- };
- function createLogger$1(name, delimiter = ':', styles = '') {
- // This is the private tracking variable for logging level.
- let level = 'info';
-
- // the curried logByType bound to the specific log and history
- let logByType;
-
- /**
- * Logs plain debug messages. Similar to `console.log`.
- *
- * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
- * of our JSDoc template, we cannot properly document this as both a function
- * and a namespace, so its function signature is documented here.
- *
- * #### Arguments
- * ##### *args
- * *[]
- *
- * Any combination of values that could be passed to `console.log()`.
- *
- * #### Return Value
- *
- * `undefined`
- *
- * @namespace
- * @param {...*} args
- * One or more messages or objects that should be logged.
- */
- const log = function (...args) {
- logByType('log', level, args);
- };
-
- // This is the logByType helper that the logging methods below use
- logByType = LogByTypeFactory(name, log, styles);
-
- /**
- * Create a new subLogger which chains the old name to the new name.
- *
- * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
- * ```js
- * mylogger('foo');
- * // > VIDEOJS: player: foo
- * ```
- *
- * @param {string} subName
- * The name to add call the new logger
- * @param {string} [subDelimiter]
- * Optional delimiter
- * @param {string} [subStyles]
- * Optional styles
- * @return {Object}
- */
- log.createLogger = (subName, subDelimiter, subStyles) => {
- const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
- const resultStyles = subStyles !== undefined ? subStyles : styles;
- const resultName = `${name} ${resultDelimiter} ${subName}`;
- return createLogger$1(resultName, resultDelimiter, resultStyles);
- };
-
- /**
- * Create a new logger.
- *
- * @param {string} newName
- * The name for the new logger
- * @param {string} [newDelimiter]
- * Optional delimiter
- * @param {string} [newStyles]
- * Optional styles
- * @return {Object}
- */
- log.createNewLogger = (newName, newDelimiter, newStyles) => {
- return createLogger$1(newName, newDelimiter, newStyles);
- };
-
- /**
- * Enumeration of available logging levels, where the keys are the level names
- * and the values are `|`-separated strings containing logging methods allowed
- * in that logging level. These strings are used to create a regular expression
- * matching the function name being called.
- *
- * Levels provided by Video.js are:
- *
- * - `off`: Matches no calls. Any value that can be cast to `false` will have
- * this effect. The most restrictive.
- * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
- * `log.warn`, and `log.error`).
- * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
- * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
- * - `warn`: Matches `log.warn` and `log.error` calls.
- * - `error`: Matches only `log.error` calls.
- *
- * @type {Object}
- */
- log.levels = {
- all: 'debug|log|warn|error',
- off: '',
- debug: 'debug|log|warn|error',
- info: 'log|warn|error',
- warn: 'warn|error',
- error: 'error',
- DEFAULT: level
- };
-
- /**
- * Get or set the current logging level.
- *
- * If a string matching a key from {@link module:log.levels} is provided, acts
- * as a setter.
- *
- * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
- * Pass a valid level to set a new logging level.
- *
- * @return {string}
- * The current logging level.
- */
- log.level = lvl => {
- if (typeof lvl === 'string') {
- if (!log.levels.hasOwnProperty(lvl)) {
- throw new Error(`"${lvl}" in not a valid log level`);
- }
- level = lvl;
- }
- return level;
- };
-
- /**
- * Returns an array containing everything that has been logged to the history.
- *
- * This array is a shallow clone of the internal history record. However, its
- * contents are _not_ cloned; so, mutating objects inside this array will
- * mutate them in history.
- *
- * @return {Array}
- */
- log.history = () => history ? [].concat(history) : [];
-
- /**
- * Allows you to filter the history by the given logger name
- *
- * @param {string} fname
- * The name to filter by
- *
- * @return {Array}
- * The filtered list to return
- */
- log.history.filter = fname => {
- return (history || []).filter(historyItem => {
- // if the first item in each historyItem includes `fname`, then it's a match
- return new RegExp(`.*${fname}.*`).test(historyItem[0]);
- });
- };
-
- /**
- * Clears the internal history tracking, but does not prevent further history
- * tracking.
- */
- log.history.clear = () => {
- if (history) {
- history.length = 0;
- }
- };
-
- /**
- * Disable history tracking if it is currently enabled.
- */
- log.history.disable = () => {
- if (history !== null) {
- history.length = 0;
- history = null;
- }
- };
-
- /**
- * Enable history tracking if it is currently disabled.
- */
- log.history.enable = () => {
- if (history === null) {
- history = [];
- }
- };
-
- /**
- * Logs error messages. Similar to `console.error`.
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as an error
- */
- log.error = (...args) => logByType('error', level, args);
-
- /**
- * Logs warning messages. Similar to `console.warn`.
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as a warning.
- */
- log.warn = (...args) => logByType('warn', level, args);
-
- /**
- * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
- * log if `console.debug` is not available
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as debug.
- */
- log.debug = (...args) => logByType('debug', level, args);
- return log;
- }
-
- /**
- * @file log.js
- * @module log
- */
- const log = createLogger$1('VIDEOJS');
- const createLogger = log.createLogger;
-
- /**
- * @file obj.js
- * @module obj
- */
-
- /**
- * @callback obj:EachCallback
- *
- * @param {*} value
- * The current key for the object that is being iterated over.
- *
- * @param {string} key
- * The current key-value for object that is being iterated over
- */
-
- /**
- * @callback obj:ReduceCallback
- *
- * @param {*} accum
- * The value that is accumulating over the reduce loop.
- *
- * @param {*} value
- * The current key for the object that is being iterated over.
- *
- * @param {string} key
- * The current key-value for object that is being iterated over
- *
- * @return {*}
- * The new accumulated value.
- */
- const toString$1 = Object.prototype.toString;
-
- /**
- * Get the keys of an Object
- *
- * @param {Object}
- * The Object to get the keys from
- *
- * @return {string[]}
- * An array of the keys from the object. Returns an empty array if the
- * object passed in was invalid or had no keys.
- *
- * @private
- */
- const keys = function (object) {
- return isObject(object) ? Object.keys(object) : [];
- };
-
- /**
- * Array-like iteration for objects.
- *
- * @param {Object} object
- * The object to iterate over
- *
- * @param {obj:EachCallback} fn
- * The callback function which is called for each key in the object.
- */
- function each(object, fn) {
- keys(object).forEach(key => fn(object[key], key));
- }
-
- /**
- * Array-like reduce for objects.
- *
- * @param {Object} object
- * The Object that you want to reduce.
- *
- * @param {Function} fn
- * A callback function which is called for each key in the object. It
- * receives the accumulated value and the per-iteration value and key
- * as arguments.
- *
- * @param {*} [initial = 0]
- * Starting value
- *
- * @return {*}
- * The final accumulated value.
- */
- function reduce(object, fn, initial = 0) {
- return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
- }
-
- /**
- * Returns whether a value is an object of any kind - including DOM nodes,
- * arrays, regular expressions, etc. Not functions, though.
- *
- * This avoids the gotcha where using `typeof` on a `null` value
- * results in `'object'`.
- *
- * @param {Object} value
- * @return {boolean}
- */
- function isObject(value) {
- return !!value && typeof value === 'object';
- }
-
- /**
- * Returns whether an object appears to be a "plain" object - that is, a
- * direct instance of `Object`.
- *
- * @param {Object} value
- * @return {boolean}
- */
- function isPlain(value) {
- return isObject(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object;
- }
-
- /**
- * Merge two objects recursively.
- *
- * Performs a deep merge like
- * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
- * plain objects (not arrays, elements, or anything else).
- *
- * Non-plain object values will be copied directly from the right-most
- * argument.
- *
- * @param {Object[]} sources
- * One or more objects to merge into a new object.
- *
- * @return {Object}
- * A new object that is the merged result of all sources.
- */
- function merge(...sources) {
- const result = {};
- sources.forEach(source => {
- if (!source) {
- return;
- }
- each(source, (value, key) => {
- if (!isPlain(value)) {
- result[key] = value;
- return;
- }
- if (!isPlain(result[key])) {
- result[key] = {};
- }
- result[key] = merge(result[key], value);
- });
- });
- return result;
- }
-
- /**
- * Returns an array of values for a given object
- *
- * @param {Object} source - target object
- * @return {Array} - object values
- */
- function values(source = {}) {
- const result = [];
- for (const key in source) {
- if (source.hasOwnProperty(key)) {
- const value = source[key];
- result.push(value);
- }
- }
- return result;
- }
-
- /**
- * Object.defineProperty but "lazy", which means that the value is only set after
- * it is retrieved the first time, rather than being set right away.
- *
- * @param {Object} obj the object to set the property on
- * @param {string} key the key for the property to set
- * @param {Function} getValue the function used to get the value when it is needed.
- * @param {boolean} setter whether a setter should be allowed or not
- */
- function defineLazyProperty(obj, key, getValue, setter = true) {
- const set = value => Object.defineProperty(obj, key, {
- value,
- enumerable: true,
- writable: true
- });
- const options = {
- configurable: true,
- enumerable: true,
- get() {
- const value = getValue();
- set(value);
- return value;
- }
- };
- if (setter) {
- options.set = set;
- }
- return Object.defineProperty(obj, key, options);
- }
-
- var Obj = /*#__PURE__*/Object.freeze({
- __proto__: null,
- each: each,
- reduce: reduce,
- isObject: isObject,
- isPlain: isPlain,
- merge: merge,
- values: values,
- defineLazyProperty: defineLazyProperty
- });
-
- /**
- * @file browser.js
- * @module browser
- */
-
- /**
- * Whether or not this device is an iPod.
- *
- * @static
- * @type {Boolean}
- */
- let IS_IPOD = false;
-
- /**
- * The detected iOS version - or `null`.
- *
- * @static
- * @type {string|null}
- */
- let IOS_VERSION = null;
-
- /**
- * Whether or not this is an Android device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_ANDROID = false;
-
- /**
- * The detected Android version - or `null` if not Android or indeterminable.
- *
- * @static
- * @type {number|string|null}
- */
- let ANDROID_VERSION;
-
- /**
- * Whether or not this is Mozilla Firefox.
- *
- * @static
- * @type {Boolean}
- */
- let IS_FIREFOX = false;
-
- /**
- * Whether or not this is Microsoft Edge.
- *
- * @static
- * @type {Boolean}
- */
- let IS_EDGE = false;
-
- /**
- * Whether or not this is any Chromium Browser
- *
- * @static
- * @type {Boolean}
- */
- let IS_CHROMIUM = false;
-
- /**
- * Whether or not this is any Chromium browser that is not Edge.
- *
- * This will also be `true` for Chrome on iOS, which will have different support
- * as it is actually Safari under the hood.
- *
- * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
- * IS_CHROMIUM should be used instead.
- * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
- *
- * @static
- * @deprecated
- * @type {Boolean}
- */
- let IS_CHROME = false;
-
- /**
- * The detected Chromium version - or `null`.
- *
- * @static
- * @type {number|null}
- */
- let CHROMIUM_VERSION = null;
-
- /**
- * The detected Google Chrome version - or `null`.
- * This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
- * Deprecated, use CHROMIUM_VERSION instead.
- *
- * @static
- * @deprecated
- * @type {number|null}
- */
- let CHROME_VERSION = null;
-
- /**
- * The detected Internet Explorer version - or `null`.
- *
- * @static
- * @deprecated
- * @type {number|null}
- */
- let IE_VERSION = null;
-
- /**
- * Whether or not this is desktop Safari.
- *
- * @static
- * @type {Boolean}
- */
- let IS_SAFARI = false;
-
- /**
- * Whether or not this is a Windows machine.
- *
- * @static
- * @type {Boolean}
- */
- let IS_WINDOWS = false;
-
- /**
- * Whether or not this device is an iPad.
- *
- * @static
- * @type {Boolean}
- */
- let IS_IPAD = false;
-
- /**
- * Whether or not this device is an iPhone.
- *
- * @static
- * @type {Boolean}
- */
- // The Facebook app's UIWebView identifies as both an iPhone and iPad, so
- // to identify iPhones, we need to exclude iPads.
- // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
- let IS_IPHONE = false;
-
- /**
- * Whether or not this is a Tizen device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_TIZEN = false;
-
- /**
- * Whether or not this is a WebOS device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_WEBOS = false;
-
- /**
- * Whether or not this is a Smart TV (Tizen or WebOS) device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_SMART_TV = false;
-
- /**
- * Whether or not this device is touch-enabled.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch));
- const UAD = window.navigator && window.navigator.userAgentData;
- if (UAD && UAD.platform && UAD.brands) {
- // If userAgentData is present, use it instead of userAgent to avoid warnings
- // Currently only implemented on Chromium
- // userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
-
- IS_ANDROID = UAD.platform === 'Android';
- IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
- IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
- IS_CHROME = !IS_EDGE && IS_CHROMIUM;
- CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
- IS_WINDOWS = UAD.platform === 'Windows';
- }
-
- // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
- // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
- // the checks need to be made agiainst the regular userAgent string.
- if (!IS_CHROMIUM) {
- const USER_AGENT = window.navigator && window.navigator.userAgent || '';
- IS_IPOD = /iPod/i.test(USER_AGENT);
- IOS_VERSION = function () {
- const match = USER_AGENT.match(/OS (\d+)_/i);
- if (match && match[1]) {
- return match[1];
- }
- return null;
- }();
- IS_ANDROID = /Android/i.test(USER_AGENT);
- ANDROID_VERSION = function () {
- // This matches Android Major.Minor.Patch versions
- // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
- const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
- if (!match) {
- return null;
- }
- const major = match[1] && parseFloat(match[1]);
- const minor = match[2] && parseFloat(match[2]);
- if (major && minor) {
- return parseFloat(match[1] + '.' + match[2]);
- } else if (major) {
- return major;
- }
- return null;
- }();
- IS_FIREFOX = /Firefox/i.test(USER_AGENT);
- IS_EDGE = /Edg/i.test(USER_AGENT);
- IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
- IS_CHROME = !IS_EDGE && IS_CHROMIUM;
- CHROMIUM_VERSION = CHROME_VERSION = function () {
- const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
- if (match && match[2]) {
- return parseFloat(match[2]);
- }
- return null;
- }();
- IE_VERSION = function () {
- const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
- let version = result && parseFloat(result[1]);
- if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
- // IE 11 has a different user agent string than other IE versions
- version = 11.0;
- }
- return version;
- }();
- IS_TIZEN = /Tizen/i.test(USER_AGENT);
- IS_WEBOS = /Web0S/i.test(USER_AGENT);
- IS_SMART_TV = IS_TIZEN || IS_WEBOS;
- IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV;
- IS_WINDOWS = /Windows/i.test(USER_AGENT);
- IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
- IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
- }
-
- /**
- * Whether or not this is an iOS device.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
-
- /**
- * Whether or not this is any flavor of Safari - including iOS.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
-
- var browser = /*#__PURE__*/Object.freeze({
- __proto__: null,
- get IS_IPOD () { return IS_IPOD; },
- get IOS_VERSION () { return IOS_VERSION; },
- get IS_ANDROID () { return IS_ANDROID; },
- get ANDROID_VERSION () { return ANDROID_VERSION; },
- get IS_FIREFOX () { return IS_FIREFOX; },
- get IS_EDGE () { return IS_EDGE; },
- get IS_CHROMIUM () { return IS_CHROMIUM; },
- get IS_CHROME () { return IS_CHROME; },
- get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
- get CHROME_VERSION () { return CHROME_VERSION; },
- get IE_VERSION () { return IE_VERSION; },
- get IS_SAFARI () { return IS_SAFARI; },
- get IS_WINDOWS () { return IS_WINDOWS; },
- get IS_IPAD () { return IS_IPAD; },
- get IS_IPHONE () { return IS_IPHONE; },
- get IS_TIZEN () { return IS_TIZEN; },
- get IS_WEBOS () { return IS_WEBOS; },
- get IS_SMART_TV () { return IS_SMART_TV; },
- TOUCH_ENABLED: TOUCH_ENABLED,
- IS_IOS: IS_IOS,
- IS_ANY_SAFARI: IS_ANY_SAFARI
- });
-
- /**
- * @file dom.js
- * @module dom
- */
-
- /**
- * Detect if a value is a string with any non-whitespace characters.
- *
- * @private
- * @param {string} str
- * The string to check
- *
- * @return {boolean}
- * Will be `true` if the string is non-blank, `false` otherwise.
- *
- */
- function isNonBlankString(str) {
- // we use str.trim as it will trim any whitespace characters
- // from the front or back of non-whitespace characters. aka
- // Any string that contains non-whitespace characters will
- // still contain them after `trim` but whitespace only strings
- // will have a length of 0, failing this check.
- return typeof str === 'string' && Boolean(str.trim());
- }
-
- /**
- * Throws an error if the passed string has whitespace. This is used by
- * class methods to be relatively consistent with the classList API.
- *
- * @private
- * @param {string} str
- * The string to check for whitespace.
- *
- * @throws {Error}
- * Throws an error if there is whitespace in the string.
- */
- function throwIfWhitespace(str) {
- // str.indexOf instead of regex because str.indexOf is faster performance wise.
- if (str.indexOf(' ') >= 0) {
- throw new Error('class has illegal whitespace characters');
- }
- }
-
- /**
- * Whether the current DOM interface appears to be real (i.e. not simulated).
- *
- * @return {boolean}
- * Will be `true` if the DOM appears to be real, `false` otherwise.
- */
- function isReal() {
- // Both document and window will never be undefined thanks to `global`.
- return document === window.document;
- }
-
- /**
- * Determines, via duck typing, whether or not a value is a DOM element.
- *
- * @param {*} value
- * The value to check.
- *
- * @return {boolean}
- * Will be `true` if the value is a DOM element, `false` otherwise.
- */
- function isEl(value) {
- return isObject(value) && value.nodeType === 1;
- }
-
- /**
- * Determines if the current DOM is embedded in an iframe.
- *
- * @return {boolean}
- * Will be `true` if the DOM is embedded in an iframe, `false`
- * otherwise.
- */
- function isInFrame() {
- // We need a try/catch here because Safari will throw errors when attempting
- // to get either `parent` or `self`
- try {
- return window.parent !== window.self;
- } catch (x) {
- return true;
- }
- }
-
- /**
- * Creates functions to query the DOM using a given method.
- *
- * @private
- * @param {string} method
- * The method to create the query with.
- *
- * @return {Function}
- * The query method
- */
- function createQuerier(method) {
- return function (selector, context) {
- if (!isNonBlankString(selector)) {
- return document[method](null);
- }
- if (isNonBlankString(context)) {
- context = document.querySelector(context);
- }
- const ctx = isEl(context) ? context : document;
- return ctx[method] && ctx[method](selector);
- };
- }
-
- /**
- * Creates an element and applies properties, attributes, and inserts content.
- *
- * @param {string} [tagName='div']
- * Name of tag to be created.
- *
- * @param {Object} [properties={}]
- * Element properties to be applied.
- *
- * @param {Object} [attributes={}]
- * Element attributes to be applied.
- *
- * @param {ContentDescriptor} [content]
- * A content descriptor object.
- *
- * @return {Element}
- * The element that was created.
- */
- function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
- const el = document.createElement(tagName);
- Object.getOwnPropertyNames(properties).forEach(function (propName) {
- const val = properties[propName];
-
- // Handle textContent since it's not supported everywhere and we have a
- // method for it.
- if (propName === 'textContent') {
- textContent(el, val);
- } else if (el[propName] !== val || propName === 'tabIndex') {
- el[propName] = val;
- }
- });
- Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
- el.setAttribute(attrName, attributes[attrName]);
- });
- if (content) {
- appendContent(el, content);
- }
- return el;
- }
-
- /**
- * Injects text into an element, replacing any existing contents entirely.
- *
- * @param {HTMLElement} el
- * The element to add text content into
- *
- * @param {string} text
- * The text content to add.
- *
- * @return {Element}
- * The element with added text content.
- */
- function textContent(el, text) {
- if (typeof el.textContent === 'undefined') {
- el.innerText = text;
- } else {
- el.textContent = text;
- }
- return el;
- }
-
- /**
- * Insert an element as the first child node of another
- *
- * @param {Element} child
- * Element to insert
- *
- * @param {Element} parent
- * Element to insert child into
- */
- function prependTo(child, parent) {
- if (parent.firstChild) {
- parent.insertBefore(child, parent.firstChild);
- } else {
- parent.appendChild(child);
- }
- }
-
- /**
- * Check if an element has a class name.
- *
- * @param {Element} element
- * Element to check
- *
- * @param {string} classToCheck
- * Class name to check for
- *
- * @return {boolean}
- * Will be `true` if the element has a class, `false` otherwise.
- *
- * @throws {Error}
- * Throws an error if `classToCheck` has white space.
- */
- function hasClass(element, classToCheck) {
- throwIfWhitespace(classToCheck);
- return element.classList.contains(classToCheck);
- }
-
- /**
- * Add a class name to an element.
- *
- * @param {Element} element
- * Element to add class name to.
- *
- * @param {...string} classesToAdd
- * One or more class name to add.
- *
- * @return {Element}
- * The DOM element with the added class name.
- */
- function addClass(element, ...classesToAdd) {
- element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
- return element;
- }
-
- /**
- * Remove a class name from an element.
- *
- * @param {Element} element
- * Element to remove a class name from.
- *
- * @param {...string} classesToRemove
- * One or more class name to remove.
- *
- * @return {Element}
- * The DOM element with class name removed.
- */
- function removeClass(element, ...classesToRemove) {
- // Protect in case the player gets disposed
- if (!element) {
- log.warn("removeClass was called with an element that doesn't exist");
- return null;
- }
- element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
- return element;
- }
-
- /**
- * The callback definition for toggleClass.
- *
- * @callback module:dom~PredicateCallback
- * @param {Element} element
- * The DOM element of the Component.
- *
- * @param {string} classToToggle
- * The `className` that wants to be toggled
- *
- * @return {boolean|undefined}
- * If `true` is returned, the `classToToggle` will be added to the
- * `element`. If `false`, the `classToToggle` will be removed from
- * the `element`. If `undefined`, the callback will be ignored.
- */
-
- /**
- * Adds or removes a class name to/from an element depending on an optional
- * condition or the presence/absence of the class name.
- *
- * @param {Element} element
- * The element to toggle a class name on.
- *
- * @param {string} classToToggle
- * The class that should be toggled.
- *
- * @param {boolean|module:dom~PredicateCallback} [predicate]
- * See the return value for {@link module:dom~PredicateCallback}
- *
- * @return {Element}
- * The element with a class that has been toggled.
- */
- function toggleClass(element, classToToggle, predicate) {
- if (typeof predicate === 'function') {
- predicate = predicate(element, classToToggle);
- }
- if (typeof predicate !== 'boolean') {
- predicate = undefined;
- }
- classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
- return element;
- }
-
- /**
- * Apply attributes to an HTML element.
- *
- * @param {Element} el
- * Element to add attributes to.
- *
- * @param {Object} [attributes]
- * Attributes to be applied.
- */
- function setAttributes(el, attributes) {
- Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
- const attrValue = attributes[attrName];
- if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
- el.removeAttribute(attrName);
- } else {
- el.setAttribute(attrName, attrValue === true ? '' : attrValue);
- }
- });
- }
-
- /**
- * Get an element's attribute values, as defined on the HTML tag.
- *
- * Attributes are not the same as properties. They're defined on the tag
- * or with setAttribute.
- *
- * @param {Element} tag
- * Element from which to get tag attributes.
- *
- * @return {Object}
- * All attributes of the element. Boolean attributes will be `true` or
- * `false`, others will be strings.
- */
- function getAttributes(tag) {
- const obj = {};
-
- // known boolean attributes
- // we can check for matching boolean properties, but not all browsers
- // and not all tags know about these attributes, so, we still want to check them manually
- const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
- if (tag && tag.attributes && tag.attributes.length > 0) {
- const attrs = tag.attributes;
- for (let i = attrs.length - 1; i >= 0; i--) {
- const attrName = attrs[i].name;
- /** @type {boolean|string} */
- let attrVal = attrs[i].value;
-
- // check for known booleans
- // the matching element property will return a value for typeof
- if (knownBooleans.includes(attrName)) {
- // the value of an included boolean attribute is typically an empty
- // string ('') which would equal false if we just check for a false value.
- // we also don't want support bad code like autoplay='false'
- attrVal = attrVal !== null ? true : false;
- }
- obj[attrName] = attrVal;
- }
- }
- return obj;
- }
-
- /**
- * Get the value of an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to get the value of.
- *
- * @return {string}
- * The value of the attribute.
- */
- function getAttribute(el, attribute) {
- return el.getAttribute(attribute);
- }
-
- /**
- * Set the value of an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to set.
- *
- * @param {string} value
- * Value to set the attribute to.
- */
- function setAttribute(el, attribute, value) {
- el.setAttribute(attribute, value);
- }
-
- /**
- * Remove an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to remove.
- */
- function removeAttribute(el, attribute) {
- el.removeAttribute(attribute);
- }
-
- /**
- * Attempt to block the ability to select text.
- */
- function blockTextSelection() {
- document.body.focus();
- document.onselectstart = function () {
- return false;
- };
- }
-
- /**
- * Turn off text selection blocking.
- */
- function unblockTextSelection() {
- document.onselectstart = function () {
- return true;
- };
- }
-
- /**
- * Identical to the native `getBoundingClientRect` function, but ensures that
- * the method is supported at all (it is in all browsers we claim to support)
- * and that the element is in the DOM before continuing.
- *
- * This wrapper function also shims properties which are not provided by some
- * older browsers (namely, IE8).
- *
- * Additionally, some browsers do not support adding properties to a
- * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
- * properties (except `x` and `y` which are not widely supported). This helps
- * avoid implementations where keys are non-enumerable.
- *
- * @param {Element} el
- * Element whose `ClientRect` we want to calculate.
- *
- * @return {Object|undefined}
- * Always returns a plain object - or `undefined` if it cannot.
- */
- function getBoundingClientRect(el) {
- if (el && el.getBoundingClientRect && el.parentNode) {
- const rect = el.getBoundingClientRect();
- const result = {};
- ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
- if (rect[k] !== undefined) {
- result[k] = rect[k];
- }
- });
- if (!result.height) {
- result.height = parseFloat(computedStyle(el, 'height'));
- }
- if (!result.width) {
- result.width = parseFloat(computedStyle(el, 'width'));
- }
- return result;
- }
- }
-
- /**
- * Represents the position of a DOM element on the page.
- *
- * @typedef {Object} module:dom~Position
- *
- * @property {number} left
- * Pixels to the left.
- *
- * @property {number} top
- * Pixels from the top.
- */
-
- /**
- * Get the position of an element in the DOM.
- *
- * Uses `getBoundingClientRect` technique from John Resig.
- *
- * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
- *
- * @param {Element} el
- * Element from which to get offset.
- *
- * @return {module:dom~Position}
- * The position of the element that was passed in.
- */
- function findPosition(el) {
- if (!el || el && !el.offsetParent) {
- return {
- left: 0,
- top: 0,
- width: 0,
- height: 0
- };
- }
- const width = el.offsetWidth;
- const height = el.offsetHeight;
- let left = 0;
- let top = 0;
- while (el.offsetParent && el !== document[FullscreenApi.fullscreenElement]) {
- left += el.offsetLeft;
- top += el.offsetTop;
- el = el.offsetParent;
- }
- return {
- left,
- top,
- width,
- height
- };
- }
-
- /**
- * Represents x and y coordinates for a DOM element or mouse pointer.
- *
- * @typedef {Object} module:dom~Coordinates
- *
- * @property {number} x
- * x coordinate in pixels
- *
- * @property {number} y
- * y coordinate in pixels
- */
-
- /**
- * Get the pointer position within an element.
- *
- * The base on the coordinates are the bottom left of the element.
- *
- * @param {Element} el
- * Element on which to get the pointer position on.
- *
- * @param {Event} event
- * Event object.
- *
- * @return {module:dom~Coordinates}
- * A coordinates object corresponding to the mouse position.
- *
- */
- function getPointerPosition(el, event) {
- const translated = {
- x: 0,
- y: 0
- };
- if (IS_IOS) {
- let item = el;
- while (item && item.nodeName.toLowerCase() !== 'html') {
- const transform = computedStyle(item, 'transform');
- if (/^matrix/.test(transform)) {
- const values = transform.slice(7, -1).split(/,\s/).map(Number);
- translated.x += values[4];
- translated.y += values[5];
- } else if (/^matrix3d/.test(transform)) {
- const values = transform.slice(9, -1).split(/,\s/).map(Number);
- translated.x += values[12];
- translated.y += values[13];
- }
- item = item.parentNode;
- }
- }
- const position = {};
- const boxTarget = findPosition(event.target);
- const box = findPosition(el);
- const boxW = box.width;
- const boxH = box.height;
- let offsetY = event.offsetY - (box.top - boxTarget.top);
- let offsetX = event.offsetX - (box.left - boxTarget.left);
- if (event.changedTouches) {
- offsetX = event.changedTouches[0].pageX - box.left;
- offsetY = event.changedTouches[0].pageY + box.top;
- if (IS_IOS) {
- offsetX -= translated.x;
- offsetY -= translated.y;
- }
- }
- position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
- position.x = Math.max(0, Math.min(1, offsetX / boxW));
- return position;
- }
-
- /**
- * Determines, via duck typing, whether or not a value is a text node.
- *
- * @param {*} value
- * Check if this value is a text node.
- *
- * @return {boolean}
- * Will be `true` if the value is a text node, `false` otherwise.
- */
- function isTextNode(value) {
- return isObject(value) && value.nodeType === 3;
- }
-
- /**
- * Empties the contents of an element.
- *
- * @param {Element} el
- * The element to empty children from
- *
- * @return {Element}
- * The element with no children
- */
- function emptyEl(el) {
- while (el.firstChild) {
- el.removeChild(el.firstChild);
- }
- return el;
- }
-
- /**
- * This is a mixed value that describes content to be injected into the DOM
- * via some method. It can be of the following types:
- *
- * Type | Description
- * -----------|-------------
- * `string` | The value will be normalized into a text node.
- * `Element` | The value will be accepted as-is.
- * `Text` | A TextNode. The value will be accepted as-is.
- * `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
- * `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
- *
- * @typedef {string|Element|Text|Array|Function} ContentDescriptor
- */
-
- /**
- * Normalizes content for eventual insertion into the DOM.
- *
- * This allows a wide range of content definition methods, but helps protect
- * from falling into the trap of simply writing to `innerHTML`, which could
- * be an XSS concern.
- *
- * The content for an element can be passed in multiple types and
- * combinations, whose behavior is as follows:
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Array}
- * All of the content that was passed in, normalized to an array of
- * elements or text nodes.
- */
- function normalizeContent(content) {
- // First, invoke content if it is a function. If it produces an array,
- // that needs to happen before normalization.
- if (typeof content === 'function') {
- content = content();
- }
-
- // Next up, normalize to an array, so one or many items can be normalized,
- // filtered, and returned.
- return (Array.isArray(content) ? content : [content]).map(value => {
- // First, invoke value if it is a function to produce a new value,
- // which will be subsequently normalized to a Node of some kind.
- if (typeof value === 'function') {
- value = value();
- }
- if (isEl(value) || isTextNode(value)) {
- return value;
- }
- if (typeof value === 'string' && /\S/.test(value)) {
- return document.createTextNode(value);
- }
- }).filter(value => value);
- }
-
- /**
- * Normalizes and appends content to an element.
- *
- * @param {Element} el
- * Element to append normalized content to.
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Element}
- * The element with appended normalized content.
- */
- function appendContent(el, content) {
- normalizeContent(content).forEach(node => el.appendChild(node));
- return el;
- }
-
- /**
- * Normalizes and inserts content into an element; this is identical to
- * `appendContent()`, except it empties the element first.
- *
- * @param {Element} el
- * Element to insert normalized content into.
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Element}
- * The element with inserted normalized content.
- */
- function insertContent(el, content) {
- return appendContent(emptyEl(el), content);
- }
-
- /**
- * Check if an event was a single left click.
- *
- * @param {MouseEvent} event
- * Event object.
- *
- * @return {boolean}
- * Will be `true` if a single left click, `false` otherwise.
- */
- function isSingleLeftClick(event) {
- // Note: if you create something draggable, be sure to
- // call it on both `mousedown` and `mousemove` event,
- // otherwise `mousedown` should be enough for a button
-
- if (event.button === undefined && event.buttons === undefined) {
- // Why do we need `buttons` ?
- // Because, middle mouse sometimes have this:
- // e.button === 0 and e.buttons === 4
- // Furthermore, we want to prevent combination click, something like
- // HOLD middlemouse then left click, that would be
- // e.button === 0, e.buttons === 5
- // just `button` is not gonna work
-
- // Alright, then what this block does ?
- // this is for chrome `simulate mobile devices`
- // I want to support this as well
-
- return true;
- }
- if (event.button === 0 && event.buttons === undefined) {
- // Touch screen, sometimes on some specific device, `buttons`
- // doesn't have anything (safari on ios, blackberry...)
-
- return true;
- }
-
- // `mouseup` event on a single left click has
- // `button` and `buttons` equal to 0
- if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
- return true;
- }
- if (event.button !== 0 || event.buttons !== 1) {
- // This is the reason we have those if else block above
- // if any special case we can catch and let it slide
- // we do it above, when get to here, this definitely
- // is-not-left-click
-
- return false;
- }
- return true;
- }
-
- /**
- * Finds a single DOM element matching `selector` within the optional
- * `context` of another DOM element (defaulting to `document`).
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelector`.
- *
- * @param {Element|String} [context=document]
- * A DOM element within which to query. Can also be a selector
- * string in which case the first matching element will be used
- * as context. If missing (or no element matches selector), falls
- * back to `document`.
- *
- * @return {Element|null}
- * The element that was found or null.
- */
- const $ = createQuerier('querySelector');
-
- /**
- * Finds a all DOM elements matching `selector` within the optional
- * `context` of another DOM element (defaulting to `document`).
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelectorAll`.
- *
- * @param {Element|String} [context=document]
- * A DOM element within which to query. Can also be a selector
- * string in which case the first matching element will be used
- * as context. If missing (or no element matches selector), falls
- * back to `document`.
- *
- * @return {NodeList}
- * A element list of elements that were found. Will be empty if none
- * were found.
- *
- */
- const $$ = createQuerier('querySelectorAll');
-
- /**
- * A safe getComputedStyle.
- *
- * This is needed because in Firefox, if the player is loaded in an iframe with
- * `display:none`, then `getComputedStyle` returns `null`, so, we do a
- * null-check to make sure that the player doesn't break in these cases.
- *
- * @param {Element} el
- * The element you want the computed style of
- *
- * @param {string} prop
- * The property name you want
- *
- * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
- */
- function computedStyle(el, prop) {
- if (!el || !prop) {
- return '';
- }
- if (typeof window.getComputedStyle === 'function') {
- let computedStyleValue;
- try {
- computedStyleValue = window.getComputedStyle(el);
- } catch (e) {
- return '';
- }
- return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
- }
- return '';
- }
-
- /**
- * Copy document style sheets to another window.
- *
- * @param {Window} win
- * The window element you want to copy the document style sheets to.
- *
- */
- function copyStyleSheetsToWindow(win) {
- [...document.styleSheets].forEach(styleSheet => {
- try {
- const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
- const style = document.createElement('style');
- style.textContent = cssRules;
- win.document.head.appendChild(style);
- } catch (e) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.type = styleSheet.type;
- // For older Safari this has to be the string; on other browsers setting the MediaList works
- link.media = styleSheet.media.mediaText;
- link.href = styleSheet.href;
- win.document.head.appendChild(link);
- }
- });
- }
-
- var Dom = /*#__PURE__*/Object.freeze({
- __proto__: null,
- isReal: isReal,
- isEl: isEl,
- isInFrame: isInFrame,
- createEl: createEl,
- textContent: textContent,
- prependTo: prependTo,
- hasClass: hasClass,
- addClass: addClass,
- removeClass: removeClass,
- toggleClass: toggleClass,
- setAttributes: setAttributes,
- getAttributes: getAttributes,
- getAttribute: getAttribute,
- setAttribute: setAttribute,
- removeAttribute: removeAttribute,
- blockTextSelection: blockTextSelection,
- unblockTextSelection: unblockTextSelection,
- getBoundingClientRect: getBoundingClientRect,
- findPosition: findPosition,
- getPointerPosition: getPointerPosition,
- isTextNode: isTextNode,
- emptyEl: emptyEl,
- normalizeContent: normalizeContent,
- appendContent: appendContent,
- insertContent: insertContent,
- isSingleLeftClick: isSingleLeftClick,
- $: $,
- $$: $$,
- computedStyle: computedStyle,
- copyStyleSheetsToWindow: copyStyleSheetsToWindow
- });
-
- /**
- * @file setup.js - Functions for setting up a player without
- * user interaction based on the data-setup `attribute` of the video tag.
- *
- * @module setup
- */
- let _windowLoaded = false;
- let videojs$1;
-
- /**
- * Set up any tags that have a data-setup `attribute` when the player is started.
- */
- const autoSetup = function () {
- if (videojs$1.options.autoSetup === false) {
- return;
- }
- const vids = Array.prototype.slice.call(document.getElementsByTagName('video'));
- const audios = Array.prototype.slice.call(document.getElementsByTagName('audio'));
- const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js'));
- const mediaEls = vids.concat(audios, divs);
-
- // Check if any media elements exist
- if (mediaEls && mediaEls.length > 0) {
- for (let i = 0, e = mediaEls.length; i < e; i++) {
- const mediaEl = mediaEls[i];
-
- // Check if element exists, has getAttribute func.
- if (mediaEl && mediaEl.getAttribute) {
- // Make sure this player hasn't already been set up.
- if (mediaEl.player === undefined) {
- const options = mediaEl.getAttribute('data-setup');
-
- // Check if data-setup attr exists.
- // We only auto-setup if they've added the data-setup attr.
- if (options !== null) {
- // Create new video.js instance.
- videojs$1(mediaEl);
- }
- }
-
- // If getAttribute isn't defined, we need to wait for the DOM.
- } else {
- autoSetupTimeout(1);
- break;
- }
- }
-
- // No videos were found, so keep looping unless page is finished loading.
- } else if (!_windowLoaded) {
- autoSetupTimeout(1);
- }
- };
-
- /**
- * Wait until the page is loaded before running autoSetup. This will be called in
- * autoSetup if `hasLoaded` returns false.
- *
- * @param {number} wait
- * How long to wait in ms
- *
- * @param {module:videojs} [vjs]
- * The videojs library function
- */
- function autoSetupTimeout(wait, vjs) {
- // Protect against breakage in non-browser environments
- if (!isReal()) {
- return;
- }
- if (vjs) {
- videojs$1 = vjs;
- }
- window.setTimeout(autoSetup, wait);
- }
-
- /**
- * Used to set the internal tracking of window loaded state to true.
- *
- * @private
- */
- function setWindowLoaded() {
- _windowLoaded = true;
- window.removeEventListener('load', setWindowLoaded);
- }
- if (isReal()) {
- if (document.readyState === 'complete') {
- setWindowLoaded();
- } else {
- /**
- * Listen for the load event on window, and set _windowLoaded to true.
- *
- * We use a standard event listener here to avoid incrementing the GUID
- * before any players are created.
- *
- * @listens load
- */
- window.addEventListener('load', setWindowLoaded);
- }
- }
-
- /**
- * @file stylesheet.js
- * @module stylesheet
- */
-
- /**
- * Create a DOM style element given a className for it.
- *
- * @param {string} className
- * The className to add to the created style element.
- *
- * @return {Element}
- * The element that was created.
- */
- const createStyleElement = function (className) {
- const style = document.createElement('style');
- style.className = className;
- return style;
- };
-
- /**
- * Add text to a DOM element.
- *
- * @param {Element} el
- * The Element to add text content to.
- *
- * @param {string} content
- * The text to add to the element.
- */
- const setTextContent = function (el, content) {
- if (el.styleSheet) {
- el.styleSheet.cssText = content;
- } else {
- el.textContent = content;
- }
- };
-
- /**
- * @file dom-data.js
- * @module dom-data
- */
-
- /**
- * Element Data Store.
- *
- * Allows for binding data to an element without putting it directly on the
- * element. Ex. Event listeners are stored here.
- * (also from jsninja.com, slightly modified and updated for closure compiler)
- *
- * @type {Object}
- * @private
- */
- var DomData = new WeakMap();
-
- /**
- * @file guid.js
- * @module guid
- */
-
- // Default value for GUIDs. This allows us to reset the GUID counter in tests.
- //
- // The initial GUID is 3 because some users have come to rely on the first
- // default player ID ending up as `vjs_video_3`.
- //
- // See: https://github.com/videojs/video.js/pull/6216
- const _initialGuid = 3;
-
- /**
- * Unique ID for an element or function
- *
- * @type {Number}
- */
- let _guid = _initialGuid;
-
- /**
- * Get a unique auto-incrementing ID by number that has not been returned before.
- *
- * @return {number}
- * A new unique ID.
- */
- function newGUID() {
- return _guid++;
- }
-
- /**
- * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
- * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
- * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
- * robust as jquery's, so there's probably some differences.
- *
- * @file events.js
- * @module events
- */
-
- /**
- * Clean up the listener cache and dispatchers
- *
- * @param {Element|Object} elem
- * Element to clean up
- *
- * @param {string} type
- * Type of event to clean up
- */
- function _cleanUpEvents(elem, type) {
- if (!DomData.has(elem)) {
- return;
- }
- const data = DomData.get(elem);
-
- // Remove the events of a particular type if there are none left
- if (data.handlers[type].length === 0) {
- delete data.handlers[type];
- // data.handlers[type] = null;
- // Setting to null was causing an error with data.handlers
-
- // Remove the meta-handler from the element
- if (elem.removeEventListener) {
- elem.removeEventListener(type, data.dispatcher, false);
- } else if (elem.detachEvent) {
- elem.detachEvent('on' + type, data.dispatcher);
- }
- }
-
- // Remove the events object if there are no types left
- if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
- delete data.handlers;
- delete data.dispatcher;
- delete data.disabled;
- }
-
- // Finally remove the element data if there is no data left
- if (Object.getOwnPropertyNames(data).length === 0) {
- DomData.delete(elem);
- }
- }
-
- /**
- * Loops through an array of event types and calls the requested method for each type.
- *
- * @param {Function} fn
- * The event method we want to use.
- *
- * @param {Element|Object} elem
- * Element or object to bind listeners to
- *
- * @param {string[]} types
- * Type of event to bind to.
- *
- * @param {Function} callback
- * Event listener.
- */
- function _handleMultipleEvents(fn, elem, types, callback) {
- types.forEach(function (type) {
- // Call the event method for each one of the types
- fn(elem, type, callback);
- });
- }
-
- /**
- * Fix a native event to have standard property values
- *
- * @param {Object} event
- * Event object to fix.
- *
- * @return {Object}
- * Fixed event object.
- */
- function fixEvent(event) {
- if (event.fixed_) {
- return event;
- }
- function returnTrue() {
- return true;
- }
- function returnFalse() {
- return false;
- }
-
- // Test if fixing up is needed
- // Used to check if !event.stopPropagation instead of isPropagationStopped
- // But native events return true for stopPropagation, but don't have
- // other expected methods like isPropagationStopped. Seems to be a problem
- // with the Javascript Ninja code. So we're just overriding all events now.
- if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
- const old = event || window.event;
- event = {};
- // Clone the old object so that we can modify the values event = {};
- // IE8 Doesn't like when you mess with native event properties
- // Firefox returns false for event.hasOwnProperty('type') and other props
- // which makes copying more difficult.
- // TODO: Probably best to create a whitelist of event props
- for (const key in old) {
- // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
- // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
- // and webkitMovementX/Y
- // Lighthouse complains if Event.path is copied
- if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
- // Chrome 32+ warns if you try to copy deprecated returnValue, but
- // we still want to if preventDefault isn't supported (IE8).
- if (!(key === 'returnValue' && old.preventDefault)) {
- event[key] = old[key];
- }
- }
- }
-
- // The event occurred on this element
- if (!event.target) {
- event.target = event.srcElement || document;
- }
-
- // Handle which other element the event is related to
- if (!event.relatedTarget) {
- event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
- }
-
- // Stop the default browser action
- event.preventDefault = function () {
- if (old.preventDefault) {
- old.preventDefault();
- }
- event.returnValue = false;
- old.returnValue = false;
- event.defaultPrevented = true;
- };
- event.defaultPrevented = false;
-
- // Stop the event from bubbling
- event.stopPropagation = function () {
- if (old.stopPropagation) {
- old.stopPropagation();
- }
- event.cancelBubble = true;
- old.cancelBubble = true;
- event.isPropagationStopped = returnTrue;
- };
- event.isPropagationStopped = returnFalse;
-
- // Stop the event from bubbling and executing other handlers
- event.stopImmediatePropagation = function () {
- if (old.stopImmediatePropagation) {
- old.stopImmediatePropagation();
- }
- event.isImmediatePropagationStopped = returnTrue;
- event.stopPropagation();
- };
- event.isImmediatePropagationStopped = returnFalse;
-
- // Handle mouse position
- if (event.clientX !== null && event.clientX !== undefined) {
- const doc = document.documentElement;
- const body = document.body;
- event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
- event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
- }
-
- // Handle key presses
- event.which = event.charCode || event.keyCode;
-
- // Fix button for mouse clicks:
- // 0 == left; 1 == middle; 2 == right
- if (event.button !== null && event.button !== undefined) {
- // The following is disabled because it does not pass videojs-standard
- // and... yikes.
- /* eslint-disable */
- event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
- /* eslint-enable */
- }
- }
-
- event.fixed_ = true;
- // Returns fixed-up instance
- return event;
- }
-
- /**
- * Whether passive event listeners are supported
- */
- let _supportsPassive;
- const supportsPassive = function () {
- if (typeof _supportsPassive !== 'boolean') {
- _supportsPassive = false;
- try {
- const opts = Object.defineProperty({}, 'passive', {
- get() {
- _supportsPassive = true;
- }
- });
- window.addEventListener('test', null, opts);
- window.removeEventListener('test', null, opts);
- } catch (e) {
- // disregard
- }
- }
- return _supportsPassive;
- };
-
- /**
- * Touch events Chrome expects to be passive
- */
- const passiveEvents = ['touchstart', 'touchmove'];
-
- /**
- * Add an event listener to element
- * It stores the handler function in a separate cache object
- * and adds a generic handler to the element's event,
- * along with a unique id (guid) to the element.
- *
- * @param {Element|Object} elem
- * Element or object to bind listeners to
- *
- * @param {string|string[]} type
- * Type of event to bind to.
- *
- * @param {Function} fn
- * Event listener.
- */
- function on(elem, type, fn) {
- if (Array.isArray(type)) {
- return _handleMultipleEvents(on, elem, type, fn);
- }
- if (!DomData.has(elem)) {
- DomData.set(elem, {});
- }
- const data = DomData.get(elem);
-
- // We need a place to store all our handler data
- if (!data.handlers) {
- data.handlers = {};
- }
- if (!data.handlers[type]) {
- data.handlers[type] = [];
- }
- if (!fn.guid) {
- fn.guid = newGUID();
- }
- data.handlers[type].push(fn);
- if (!data.dispatcher) {
- data.disabled = false;
- data.dispatcher = function (event, hash) {
- if (data.disabled) {
- return;
- }
- event = fixEvent(event);
- const handlers = data.handlers[event.type];
- if (handlers) {
- // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
- const handlersCopy = handlers.slice(0);
- for (let m = 0, n = handlersCopy.length; m < n; m++) {
- if (event.isImmediatePropagationStopped()) {
- break;
- } else {
- try {
- handlersCopy[m].call(elem, event, hash);
- } catch (e) {
- log.error(e);
- }
- }
- }
- }
- };
- }
- if (data.handlers[type].length === 1) {
- if (elem.addEventListener) {
- let options = false;
- if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
- options = {
- passive: true
- };
- }
- elem.addEventListener(type, data.dispatcher, options);
- } else if (elem.attachEvent) {
- elem.attachEvent('on' + type, data.dispatcher);
- }
- }
- }
-
- /**
- * Removes event listeners from an element
- *
- * @param {Element|Object} elem
- * Object to remove listeners from.
- *
- * @param {string|string[]} [type]
- * Type of listener to remove. Don't include to remove all events from element.
- *
- * @param {Function} [fn]
- * Specific listener to remove. Don't include to remove listeners for an event
- * type.
- */
- function off(elem, type, fn) {
- // Don't want to add a cache object through getElData if not needed
- if (!DomData.has(elem)) {
- return;
- }
- const data = DomData.get(elem);
-
- // If no events exist, nothing to unbind
- if (!data.handlers) {
- return;
- }
- if (Array.isArray(type)) {
- return _handleMultipleEvents(off, elem, type, fn);
- }
-
- // Utility function
- const removeType = function (el, t) {
- data.handlers[t] = [];
- _cleanUpEvents(el, t);
- };
-
- // Are we removing all bound events?
- if (type === undefined) {
- for (const t in data.handlers) {
- if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
- removeType(elem, t);
- }
- }
- return;
- }
- const handlers = data.handlers[type];
-
- // If no handlers exist, nothing to unbind
- if (!handlers) {
- return;
- }
-
- // If no listener was provided, remove all listeners for type
- if (!fn) {
- removeType(elem, type);
- return;
- }
-
- // We're only removing a single handler
- if (fn.guid) {
- for (let n = 0; n < handlers.length; n++) {
- if (handlers[n].guid === fn.guid) {
- handlers.splice(n--, 1);
- }
- }
- }
- _cleanUpEvents(elem, type);
- }
-
- /**
- * Trigger an event for an element
- *
- * @param {Element|Object} elem
- * Element to trigger an event on
- *
- * @param {EventTarget~Event|string} event
- * A string (the type) or an event object with a type attribute
- *
- * @param {Object} [hash]
- * data hash to pass along with the event
- *
- * @return {boolean|undefined}
- * Returns the opposite of `defaultPrevented` if default was
- * prevented. Otherwise, returns `undefined`
- */
- function trigger(elem, event, hash) {
- // Fetches element data and a reference to the parent (for bubbling).
- // Don't want to add a data object to cache for every parent,
- // so checking hasElData first.
- const elemData = DomData.has(elem) ? DomData.get(elem) : {};
- const parent = elem.parentNode || elem.ownerDocument;
- // type = event.type || event,
- // handler;
-
- // If an event name was passed as a string, creates an event out of it
- if (typeof event === 'string') {
- event = {
- type: event,
- target: elem
- };
- } else if (!event.target) {
- event.target = elem;
- }
-
- // Normalizes the event properties.
- event = fixEvent(event);
-
- // If the passed element has a dispatcher, executes the established handlers.
- if (elemData.dispatcher) {
- elemData.dispatcher.call(elem, event, hash);
- }
-
- // Unless explicitly stopped or the event does not bubble (e.g. media events)
- // recursively calls this function to bubble the event up the DOM.
- if (parent && !event.isPropagationStopped() && event.bubbles === true) {
- trigger.call(null, parent, event, hash);
-
- // If at the top of the DOM, triggers the default action unless disabled.
- } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
- if (!DomData.has(event.target)) {
- DomData.set(event.target, {});
- }
- const targetData = DomData.get(event.target);
-
- // Checks if the target has a default action for this event.
- if (event.target[event.type]) {
- // Temporarily disables event dispatching on the target as we have already executed the handler.
- targetData.disabled = true;
- // Executes the default action.
- if (typeof event.target[event.type] === 'function') {
- event.target[event.type]();
- }
- // Re-enables event dispatching.
- targetData.disabled = false;
- }
- }
-
- // Inform the triggerer if the default was prevented by returning false
- return !event.defaultPrevented;
- }
-
- /**
- * Trigger a listener only once for an event.
- *
- * @param {Element|Object} elem
- * Element or object to bind to.
- *
- * @param {string|string[]} type
- * Name/type of event
- *
- * @param {Event~EventListener} fn
- * Event listener function
- */
- function one(elem, type, fn) {
- if (Array.isArray(type)) {
- return _handleMultipleEvents(one, elem, type, fn);
- }
- const func = function () {
- off(elem, type, func);
- fn.apply(this, arguments);
- };
-
- // copy the guid to the new function so it can removed using the original function's ID
- func.guid = fn.guid = fn.guid || newGUID();
- on(elem, type, func);
- }
-
- /**
- * Trigger a listener only once and then turn if off for all
- * configured events
- *
- * @param {Element|Object} elem
- * Element or object to bind to.
- *
- * @param {string|string[]} type
- * Name/type of event
- *
- * @param {Event~EventListener} fn
- * Event listener function
- */
- function any(elem, type, fn) {
- const func = function () {
- off(elem, type, func);
- fn.apply(this, arguments);
- };
-
- // copy the guid to the new function so it can removed using the original function's ID
- func.guid = fn.guid = fn.guid || newGUID();
-
- // multiple ons, but one off for everything
- on(elem, type, func);
- }
-
- var Events = /*#__PURE__*/Object.freeze({
- __proto__: null,
- fixEvent: fixEvent,
- on: on,
- off: off,
- trigger: trigger,
- one: one,
- any: any
- });
-
- /**
- * @file fn.js
- * @module fn
- */
- const UPDATE_REFRESH_INTERVAL = 30;
-
- /**
- * A private, internal-only function for changing the context of a function.
- *
- * It also stores a unique id on the function so it can be easily removed from
- * events.
- *
- * @private
- * @function
- * @param {*} context
- * The object to bind as scope.
- *
- * @param {Function} fn
- * The function to be bound to a scope.
- *
- * @param {number} [uid]
- * An optional unique ID for the function to be set
- *
- * @return {Function}
- * The new function that will be bound into the context given
- */
- const bind_ = function (context, fn, uid) {
- // Make sure the function has a unique ID
- if (!fn.guid) {
- fn.guid = newGUID();
- }
-
- // Create the new function that changes the context
- const bound = fn.bind(context);
-
- // Allow for the ability to individualize this function
- // Needed in the case where multiple objects might share the same prototype
- // IF both items add an event listener with the same function, then you try to remove just one
- // it will remove both because they both have the same guid.
- // when using this, you need to use the bind method when you remove the listener as well.
- // currently used in text tracks
- bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
- return bound;
- };
-
- /**
- * Wraps the given function, `fn`, with a new function that only invokes `fn`
- * at most once per every `wait` milliseconds.
- *
- * @function
- * @param {Function} fn
- * The function to be throttled.
- *
- * @param {number} wait
- * The number of milliseconds by which to throttle.
- *
- * @return {Function}
- */
- const throttle = function (fn, wait) {
- let last = window.performance.now();
- const throttled = function (...args) {
- const now = window.performance.now();
- if (now - last >= wait) {
- fn(...args);
- last = now;
- }
- };
- return throttled;
- };
-
- /**
- * Creates a debounced function that delays invoking `func` until after `wait`
- * milliseconds have elapsed since the last time the debounced function was
- * invoked.
- *
- * Inspired by lodash and underscore implementations.
- *
- * @function
- * @param {Function} func
- * The function to wrap with debounce behavior.
- *
- * @param {number} wait
- * The number of milliseconds to wait after the last invocation.
- *
- * @param {boolean} [immediate]
- * Whether or not to invoke the function immediately upon creation.
- *
- * @param {Object} [context=window]
- * The "context" in which the debounced function should debounce. For
- * example, if this function should be tied to a Video.js player,
- * the player can be passed here. Alternatively, defaults to the
- * global `window` object.
- *
- * @return {Function}
- * A debounced function.
- */
- const debounce = function (func, wait, immediate, context = window) {
- let timeout;
- const cancel = () => {
- context.clearTimeout(timeout);
- timeout = null;
- };
-
- /* eslint-disable consistent-this */
- const debounced = function () {
- const self = this;
- const args = arguments;
- let later = function () {
- timeout = null;
- later = null;
- if (!immediate) {
- func.apply(self, args);
- }
- };
- if (!timeout && immediate) {
- func.apply(self, args);
- }
- context.clearTimeout(timeout);
- timeout = context.setTimeout(later, wait);
- };
- /* eslint-enable consistent-this */
-
- debounced.cancel = cancel;
- return debounced;
- };
-
- var Fn = /*#__PURE__*/Object.freeze({
- __proto__: null,
- UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
- bind_: bind_,
- throttle: throttle,
- debounce: debounce
- });
-
- /**
- * @file src/js/event-target.js
- */
- let EVENT_MAP;
-
- /**
- * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
- * adds shorthand functions that wrap around lengthy functions. For example:
- * the `on` function is a wrapper around `addEventListener`.
- *
- * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
- * @class EventTarget
- */
- class EventTarget {
- /**
- * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
- * function that will get called when an event with a certain name gets triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to call with `EventTarget`s
- */
- on(type, fn) {
- // Remove the addEventListener alias before calling Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- on(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * Removes an `event listener` for a specific event from an instance of `EventTarget`.
- * This makes it so that the `event listener` will no longer get called when the
- * named event happens.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to remove.
- */
- off(type, fn) {
- off(this, type, fn);
- }
- /**
- * This function will add an `event listener` that gets triggered only once. After the
- * first trigger it will get removed. This is like adding an `event listener`
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- one(type, fn) {
- // Remove the addEventListener aliasing Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- one(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * This function will add an `event listener` that gets triggered only once and is
- * removed from all events. This is like adding an array of `event listener`s
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
- * first time it is triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- any(type, fn) {
- // Remove the addEventListener aliasing Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- any(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * This function causes an event to happen. This will then cause any `event listeners`
- * that are waiting for that event, to get called. If there are no `event listeners`
- * for an event then nothing will happen.
- *
- * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
- * Trigger will also call the `on` + `uppercaseEventName` function.
- *
- * Example:
- * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
- * `onClick` if it exists.
- *
- * @param {string|EventTarget~Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- */
- trigger(event) {
- const type = event.type || event;
-
- // deprecation
- // In a future version we should default target to `this`
- // similar to how we default the target to `elem` in
- // `Events.trigger`. Right now the default `target` will be
- // `document` due to the `Event.fixEvent` call.
- if (typeof event === 'string') {
- event = {
- type
- };
- }
- event = fixEvent(event);
- if (this.allowedEvents_[type] && this['on' + type]) {
- this['on' + type](event);
- }
- trigger(this, event);
- }
- queueTrigger(event) {
- // only set up EVENT_MAP if it'll be used
- if (!EVENT_MAP) {
- EVENT_MAP = new Map();
- }
- const type = event.type || event;
- let map = EVENT_MAP.get(this);
- if (!map) {
- map = new Map();
- EVENT_MAP.set(this, map);
- }
- const oldTimeout = map.get(type);
- map.delete(type);
- window.clearTimeout(oldTimeout);
- const timeout = window.setTimeout(() => {
- map.delete(type);
- // if we cleared out all timeouts for the current target, delete its map
- if (map.size === 0) {
- map = null;
- EVENT_MAP.delete(this);
- }
- this.trigger(event);
- }, 0);
- map.set(type, timeout);
- }
- }
-
- /**
- * A Custom DOM event.
- *
- * @typedef {CustomEvent} Event
- * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
- */
-
- /**
- * All event listeners should follow the following format.
- *
- * @callback EventListener
- * @this {EventTarget}
- *
- * @param {Event} event
- * the event that triggered this function
- *
- * @param {Object} [hash]
- * hash of data sent during the event
- */
-
- /**
- * An object containing event names as keys and booleans as values.
- *
- * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
- * will have extra functionality. See that function for more information.
- *
- * @property EventTarget.prototype.allowedEvents_
- * @protected
- */
- EventTarget.prototype.allowedEvents_ = {};
-
- /**
- * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#on}
- */
- EventTarget.prototype.addEventListener = EventTarget.prototype.on;
-
- /**
- * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#off}
- */
- EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
-
- /**
- * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#trigger}
- */
- EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
-
- /**
- * @file mixins/evented.js
- * @module evented
- */
- const objName = obj => {
- if (typeof obj.name === 'function') {
- return obj.name();
- }
- if (typeof obj.name === 'string') {
- return obj.name;
- }
- if (obj.name_) {
- return obj.name_;
- }
- if (obj.constructor && obj.constructor.name) {
- return obj.constructor.name;
- }
- return typeof obj;
- };
-
- /**
- * Returns whether or not an object has had the evented mixin applied.
- *
- * @param {Object} object
- * An object to test.
- *
- * @return {boolean}
- * Whether or not the object appears to be evented.
- */
- const isEvented = object => object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
-
- /**
- * Adds a callback to run after the evented mixin applied.
- *
- * @param {Object} target
- * An object to Add
- * @param {Function} callback
- * The callback to run.
- */
- const addEventedCallback = (target, callback) => {
- if (isEvented(target)) {
- callback();
- } else {
- if (!target.eventedCallbacks) {
- target.eventedCallbacks = [];
- }
- target.eventedCallbacks.push(callback);
- }
- };
-
- /**
- * Whether a value is a valid event type - non-empty string or array.
- *
- * @private
- * @param {string|Array} type
- * The type value to test.
- *
- * @return {boolean}
- * Whether or not the type is a valid event type.
- */
- const isValidEventType = type =>
- // The regex here verifies that the `type` contains at least one non-
- // whitespace character.
- typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
-
- /**
- * Validates a value to determine if it is a valid event target. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the target does not appear to be a valid event target.
- *
- * @param {Object} target
- * The object to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateTarget = (target, obj, fnName) => {
- if (!target || !target.nodeName && !isEvented(target)) {
- throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
- }
- };
-
- /**
- * Validates a value to determine if it is a valid event target. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the type does not appear to be a valid event type.
- *
- * @param {string|Array} type
- * The type to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateEventType = (type, obj, fnName) => {
- if (!isValidEventType(type)) {
- throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
- }
- };
-
- /**
- * Validates a value to determine if it is a valid listener. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the listener is not a function.
- *
- * @param {Function} listener
- * The listener to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateListener = (listener, obj, fnName) => {
- if (typeof listener !== 'function') {
- throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
- }
- };
-
- /**
- * Takes an array of arguments given to `on()` or `one()`, validates them, and
- * normalizes them into an object.
- *
- * @private
- * @param {Object} self
- * The evented object on which `on()` or `one()` was called. This
- * object will be bound as the `this` value for the listener.
- *
- * @param {Array} args
- * An array of arguments passed to `on()` or `one()`.
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- *
- * @return {Object}
- * An object containing useful values for `on()` or `one()` calls.
- */
- const normalizeListenArgs = (self, args, fnName) => {
- // If the number of arguments is less than 3, the target is always the
- // evented object itself.
- const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
- let target;
- let type;
- let listener;
- if (isTargetingSelf) {
- target = self.eventBusEl_;
-
- // Deal with cases where we got 3 arguments, but we are still listening to
- // the evented object itself.
- if (args.length >= 3) {
- args.shift();
- }
- [type, listener] = args;
- } else {
- [target, type, listener] = args;
- }
- validateTarget(target, self, fnName);
- validateEventType(type, self, fnName);
- validateListener(listener, self, fnName);
- listener = bind_(self, listener);
- return {
- isTargetingSelf,
- target,
- type,
- listener
- };
- };
-
- /**
- * Adds the listener to the event type(s) on the target, normalizing for
- * the type of target.
- *
- * @private
- * @param {Element|Object} target
- * A DOM node or evented object.
- *
- * @param {string} method
- * The event binding method to use ("on" or "one").
- *
- * @param {string|Array} type
- * One or more event type(s).
- *
- * @param {Function} listener
- * A listener function.
- */
- const listen = (target, method, type, listener) => {
- validateTarget(target, target, method);
- if (target.nodeName) {
- Events[method](target, type, listener);
- } else {
- target[method](type, listener);
- }
- };
-
- /**
- * Contains methods that provide event capabilities to an object which is passed
- * to {@link module:evented|evented}.
- *
- * @mixin EventedMixin
- */
- const EventedMixin = {
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- on(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'on');
- listen(target, 'on', type, listener);
-
- // If this object is listening to another evented object.
- if (!isTargetingSelf) {
- // If this object is disposed, remove the listener.
- const removeListenerOnDispose = () => this.off(target, type, listener);
-
- // Use the same function ID as the listener so we can remove it later it
- // using the ID of the original listener.
- removeListenerOnDispose.guid = listener.guid;
-
- // Add a listener to the target's dispose event as well. This ensures
- // that if the target is disposed BEFORE this object, we remove the
- // removal listener that was just added. Otherwise, we create a memory leak.
- const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- removeRemoverOnTargetDispose.guid = listener.guid;
- listen(this, 'on', 'dispose', removeListenerOnDispose);
- listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
- }
- },
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object. The listener will be called once per event and then removed.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- one(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'one');
-
- // Targeting this evented object.
- if (isTargetingSelf) {
- listen(target, 'one', type, listener);
-
- // Targeting another evented object.
- } else {
- // TODO: This wrapper is incorrect! It should only
- // remove the wrapper for the event type that called it.
- // Instead all listeners are removed on the first trigger!
- // see https://github.com/videojs/video.js/issues/5962
- const wrapper = (...largs) => {
- this.off(target, type, wrapper);
- listener.apply(null, largs);
- };
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- wrapper.guid = listener.guid;
- listen(target, 'one', type, wrapper);
- }
- },
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object. The listener will only be called once for the first event that is triggered
- * then removed.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- any(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'any');
-
- // Targeting this evented object.
- if (isTargetingSelf) {
- listen(target, 'any', type, listener);
-
- // Targeting another evented object.
- } else {
- const wrapper = (...largs) => {
- this.off(target, type, wrapper);
- listener.apply(null, largs);
- };
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- wrapper.guid = listener.guid;
- listen(target, 'any', type, wrapper);
- }
- },
- /**
- * Removes listener(s) from event(s) on an evented object.
- *
- * @param {string|Array|Element|Object} [targetOrType]
- * If this is a string or array, it represents the event type(s).
- *
- * Another evented object can be passed here instead, in which case
- * ALL 3 arguments are _required_.
- *
- * @param {string|Array|Function} [typeOrListener]
- * If the first argument was a string or array, this may be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function; otherwise, _all_ listeners bound to the
- * event type(s) will be removed.
- */
- off(targetOrType, typeOrListener, listener) {
- // Targeting this evented object.
- if (!targetOrType || isValidEventType(targetOrType)) {
- off(this.eventBusEl_, targetOrType, typeOrListener);
-
- // Targeting another evented object.
- } else {
- const target = targetOrType;
- const type = typeOrListener;
-
- // Fail fast and in a meaningful way!
- validateTarget(target, this, 'off');
- validateEventType(type, this, 'off');
- validateListener(listener, this, 'off');
-
- // Ensure there's at least a guid, even if the function hasn't been used
- listener = bind_(this, listener);
-
- // Remove the dispose listener on this evented object, which was given
- // the same guid as the event listener in on().
- this.off('dispose', listener);
- if (target.nodeName) {
- off(target, type, listener);
- off(target, 'dispose', listener);
- } else if (isEvented(target)) {
- target.off(type, listener);
- target.off('dispose', listener);
- }
- }
- },
- /**
- * Fire an event on this evented object, causing its listeners to be called.
- *
- * @param {string|Object} event
- * An event type or an object with a type property.
- *
- * @param {Object} [hash]
- * An additional object to pass along to listeners.
- *
- * @return {boolean}
- * Whether or not the default behavior was prevented.
- */
- trigger(event, hash) {
- validateTarget(this.eventBusEl_, this, 'trigger');
- const type = event && typeof event !== 'string' ? event.type : event;
- if (!isValidEventType(type)) {
- throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
- }
- return trigger(this.eventBusEl_, event, hash);
- }
- };
-
- /**
- * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
- *
- * @param {Object} target
- * The object to which to add event methods.
- *
- * @param {Object} [options={}]
- * Options for customizing the mixin behavior.
- *
- * @param {string} [options.eventBusKey]
- * By default, adds a `eventBusEl_` DOM element to the target object,
- * which is used as an event bus. If the target object already has a
- * DOM element that should be used, pass its key here.
- *
- * @return {Object}
- * The target object.
- */
- function evented(target, options = {}) {
- const {
- eventBusKey
- } = options;
-
- // Set or create the eventBusEl_.
- if (eventBusKey) {
- if (!target[eventBusKey].nodeName) {
- throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
- }
- target.eventBusEl_ = target[eventBusKey];
- } else {
- target.eventBusEl_ = createEl('span', {
- className: 'vjs-event-bus'
- });
- }
- Object.assign(target, EventedMixin);
- if (target.eventedCallbacks) {
- target.eventedCallbacks.forEach(callback => {
- callback();
- });
- }
-
- // When any evented object is disposed, it removes all its listeners.
- target.on('dispose', () => {
- target.off();
- [target, target.el_, target.eventBusEl_].forEach(function (val) {
- if (val && DomData.has(val)) {
- DomData.delete(val);
- }
- });
- window.setTimeout(() => {
- target.eventBusEl_ = null;
- }, 0);
- });
- return target;
- }
-
- /**
- * @file mixins/stateful.js
- * @module stateful
- */
-
- /**
- * Contains methods that provide statefulness to an object which is passed
- * to {@link module:stateful}.
- *
- * @mixin StatefulMixin
- */
- const StatefulMixin = {
- /**
- * A hash containing arbitrary keys and values representing the state of
- * the object.
- *
- * @type {Object}
- */
- state: {},
- /**
- * Set the state of an object by mutating its
- * {@link module:stateful~StatefulMixin.state|state} object in place.
- *
- * @fires module:stateful~StatefulMixin#statechanged
- * @param {Object|Function} stateUpdates
- * A new set of properties to shallow-merge into the plugin state.
- * Can be a plain object or a function returning a plain object.
- *
- * @return {Object|undefined}
- * An object containing changes that occurred. If no changes
- * occurred, returns `undefined`.
- */
- setState(stateUpdates) {
- // Support providing the `stateUpdates` state as a function.
- if (typeof stateUpdates === 'function') {
- stateUpdates = stateUpdates();
- }
- let changes;
- each(stateUpdates, (value, key) => {
- // Record the change if the value is different from what's in the
- // current state.
- if (this.state[key] !== value) {
- changes = changes || {};
- changes[key] = {
- from: this.state[key],
- to: value
- };
- }
- this.state[key] = value;
- });
-
- // Only trigger "statechange" if there were changes AND we have a trigger
- // function. This allows us to not require that the target object be an
- // evented object.
- if (changes && isEvented(this)) {
- /**
- * An event triggered on an object that is both
- * {@link module:stateful|stateful} and {@link module:evented|evented}
- * indicating that its state has changed.
- *
- * @event module:stateful~StatefulMixin#statechanged
- * @type {Object}
- * @property {Object} changes
- * A hash containing the properties that were changed and
- * the values they were changed `from` and `to`.
- */
- this.trigger({
- changes,
- type: 'statechanged'
- });
- }
- return changes;
- }
- };
-
- /**
- * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
- * object.
- *
- * If the target object is {@link module:evented|evented} and has a
- * `handleStateChanged` method, that method will be automatically bound to the
- * `statechanged` event on itself.
- *
- * @param {Object} target
- * The object to be made stateful.
- *
- * @param {Object} [defaultState]
- * A default set of properties to populate the newly-stateful object's
- * `state` property.
- *
- * @return {Object}
- * Returns the `target`.
- */
- function stateful(target, defaultState) {
- Object.assign(target, StatefulMixin);
-
- // This happens after the mixing-in because we need to replace the `state`
- // added in that step.
- target.state = Object.assign({}, target.state, defaultState);
-
- // Auto-bind the `handleStateChanged` method of the target object if it exists.
- if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
- target.on('statechanged', target.handleStateChanged);
- }
- return target;
- }
-
- /**
- * @file str.js
- * @module to-lower-case
- */
-
- /**
- * Lowercase the first letter of a string.
- *
- * @param {string} string
- * String to be lowercased
- *
- * @return {string}
- * The string with a lowercased first letter
- */
- const toLowerCase = function (string) {
- if (typeof string !== 'string') {
- return string;
- }
- return string.replace(/./, w => w.toLowerCase());
- };
-
- /**
- * Uppercase the first letter of a string.
- *
- * @param {string} string
- * String to be uppercased
- *
- * @return {string}
- * The string with an uppercased first letter
- */
- const toTitleCase = function (string) {
- if (typeof string !== 'string') {
- return string;
- }
- return string.replace(/./, w => w.toUpperCase());
- };
-
- /**
- * Compares the TitleCase versions of the two strings for equality.
- *
- * @param {string} str1
- * The first string to compare
- *
- * @param {string} str2
- * The second string to compare
- *
- * @return {boolean}
- * Whether the TitleCase versions of the strings are equal
- */
- const titleCaseEquals = function (str1, str2) {
- return toTitleCase(str1) === toTitleCase(str2);
- };
-
- var Str = /*#__PURE__*/Object.freeze({
- __proto__: null,
- toLowerCase: toLowerCase,
- toTitleCase: toTitleCase,
- titleCaseEquals: titleCaseEquals
- });
-
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
-
- function unwrapExports (x) {
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
- }
-
- function createCommonjsModule(fn, module) {
- return module = { exports: {} }, fn(module, module.exports), module.exports;
- }
-
- var keycode = createCommonjsModule(function (module, exports) {
- // Source: http://jsfiddle.net/vWx8V/
- // http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes
-
- /**
- * Conenience method returns corresponding value for given keyName or keyCode.
- *
- * @param {Mixed} keyCode {Number} or keyName {String}
- * @return {Mixed}
- * @api public
- */
-
- function keyCode(searchInput) {
- // Keyboard Events
- if (searchInput && 'object' === typeof searchInput) {
- var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
- if (hasKeyCode) searchInput = hasKeyCode;
- }
-
- // Numbers
- if ('number' === typeof searchInput) return names[searchInput];
-
- // Everything else (cast to string)
- var search = String(searchInput);
-
- // check codes
- var foundNamedKey = codes[search.toLowerCase()];
- if (foundNamedKey) return foundNamedKey;
-
- // check aliases
- var foundNamedKey = aliases[search.toLowerCase()];
- if (foundNamedKey) return foundNamedKey;
-
- // weird character?
- if (search.length === 1) return search.charCodeAt(0);
- return undefined;
- }
-
- /**
- * Compares a keyboard event with a given keyCode or keyName.
- *
- * @param {Event} event Keyboard event that should be tested
- * @param {Mixed} keyCode {Number} or keyName {String}
- * @return {Boolean}
- * @api public
- */
- keyCode.isEventKey = function isEventKey(event, nameOrCode) {
- if (event && 'object' === typeof event) {
- var keyCode = event.which || event.keyCode || event.charCode;
- if (keyCode === null || keyCode === undefined) {
- return false;
- }
- if (typeof nameOrCode === 'string') {
- // check codes
- var foundNamedKey = codes[nameOrCode.toLowerCase()];
- if (foundNamedKey) {
- return foundNamedKey === keyCode;
- }
-
- // check aliases
- var foundNamedKey = aliases[nameOrCode.toLowerCase()];
- if (foundNamedKey) {
- return foundNamedKey === keyCode;
- }
- } else if (typeof nameOrCode === 'number') {
- return nameOrCode === keyCode;
- }
- return false;
- }
- };
- exports = module.exports = keyCode;
-
- /**
- * Get by name
- *
- * exports.code['enter'] // => 13
- */
-
- var codes = exports.code = exports.codes = {
- 'backspace': 8,
- 'tab': 9,
- 'enter': 13,
- 'shift': 16,
- 'ctrl': 17,
- 'alt': 18,
- 'pause/break': 19,
- 'caps lock': 20,
- 'esc': 27,
- 'space': 32,
- 'page up': 33,
- 'page down': 34,
- 'end': 35,
- 'home': 36,
- 'left': 37,
- 'up': 38,
- 'right': 39,
- 'down': 40,
- 'insert': 45,
- 'delete': 46,
- 'command': 91,
- 'left command': 91,
- 'right command': 93,
- 'numpad *': 106,
- 'numpad +': 107,
- 'numpad -': 109,
- 'numpad .': 110,
- 'numpad /': 111,
- 'num lock': 144,
- 'scroll lock': 145,
- 'my computer': 182,
- 'my calculator': 183,
- ';': 186,
- '=': 187,
- ',': 188,
- '-': 189,
- '.': 190,
- '/': 191,
- '`': 192,
- '[': 219,
- '\\': 220,
- ']': 221,
- "'": 222
- };
-
- // Helper aliases
-
- var aliases = exports.aliases = {
- 'windows': 91,
- '⇧': 16,
- '⌥': 18,
- '⌃': 17,
- '⌘': 91,
- 'ctl': 17,
- 'control': 17,
- 'option': 18,
- 'pause': 19,
- 'break': 19,
- 'caps': 20,
- 'return': 13,
- 'escape': 27,
- 'spc': 32,
- 'spacebar': 32,
- 'pgup': 33,
- 'pgdn': 34,
- 'ins': 45,
- 'del': 46,
- 'cmd': 91
- };
-
- /*!
- * Programatically add the following
- */
-
- // lower case chars
- for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32;
-
- // numbers
- for (var i = 48; i < 58; i++) codes[i - 48] = i;
-
- // function keys
- for (i = 1; i < 13; i++) codes['f' + i] = i + 111;
-
- // numpad keys
- for (i = 0; i < 10; i++) codes['numpad ' + i] = i + 96;
-
- /**
- * Get by code
- *
- * exports.name[13] // => 'Enter'
- */
-
- var names = exports.names = exports.title = {}; // title for backward compat
-
- // Create reverse mapping
- for (i in codes) names[codes[i]] = i;
-
- // Add aliases
- for (var alias in aliases) {
- codes[alias] = aliases[alias];
- }
- });
- keycode.code;
- keycode.codes;
- keycode.aliases;
- keycode.names;
- keycode.title;
-
- /**
- * Player Component - Base class for all UI objects
- *
- * @file component.js
- */
-
- /**
- * Base class for all UI Components.
- * Components are UI objects which represent both a javascript object and an element
- * in the DOM. They can be children of other components, and can have
- * children themselves.
- *
- * Components can also use methods from {@link EventTarget}
- */
- class Component {
- /**
- * A callback that is called when a component is ready. Does not have any
- * parameters and any callback value will be ignored.
- *
- * @callback ReadyCallback
- * @this Component
- */
-
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of component options.
- *
- * @param {Object[]} [options.children]
- * An array of children objects to initialize this component with. Children objects have
- * a name property that will be used if more than one component of the same type needs to be
- * added.
- *
- * @param {string} [options.className]
- * A class or space separated list of classes to add the component
- *
- * @param {ReadyCallback} [ready]
- * Function that gets called when the `Component` is ready.
- */
- constructor(player, options, ready) {
- // The component might be the player itself and we can't pass `this` to super
- if (!player && this.play) {
- this.player_ = player = this; // eslint-disable-line
- } else {
- this.player_ = player;
- }
- this.isDisposed_ = false;
-
- // Hold the reference to the parent component via `addChild` method
- this.parentComponent_ = null;
-
- // Make a copy of prototype.options_ to protect against overriding defaults
- this.options_ = merge({}, this.options_);
-
- // Updated options with supplied options
- options = this.options_ = merge(this.options_, options);
-
- // Get ID from options or options element if one is supplied
- this.id_ = options.id || options.el && options.el.id;
-
- // If there was no ID from the options, generate one
- if (!this.id_) {
- // Don't require the player ID function in the case of mock players
- const id = player && player.id && player.id() || 'no_player';
- this.id_ = `${id}_component_${newGUID()}`;
- }
- this.name_ = options.name || null;
-
- // Create element if one wasn't provided in options
- if (options.el) {
- this.el_ = options.el;
- } else if (options.createEl !== false) {
- this.el_ = this.createEl();
- }
- if (options.className && this.el_) {
- options.className.split(' ').forEach(c => this.addClass(c));
- }
-
- // Remove the placeholder event methods. If the component is evented, the
- // real methods are added next
- ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
- this[fn] = undefined;
- });
-
- // if evented is anything except false, we want to mixin in evented
- if (options.evented !== false) {
- // Make this an evented object and use `el_`, if available, as its event bus
- evented(this, {
- eventBusKey: this.el_ ? 'el_' : null
- });
- this.handleLanguagechange = this.handleLanguagechange.bind(this);
- this.on(this.player_, 'languagechange', this.handleLanguagechange);
- }
- stateful(this, this.constructor.defaultState);
- this.children_ = [];
- this.childIndex_ = {};
- this.childNameIndex_ = {};
- this.setTimeoutIds_ = new Set();
- this.setIntervalIds_ = new Set();
- this.rafIds_ = new Set();
- this.namedRafs_ = new Map();
- this.clearingTimersOnDispose_ = false;
-
- // Add any child components in options
- if (options.initChildren !== false) {
- this.initChildren();
- }
-
- // Don't want to trigger ready here or it will go before init is actually
- // finished for all children that run this constructor
- this.ready(ready);
- if (options.reportTouchActivity !== false) {
- this.enableTouchActivity();
- }
- }
-
- // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
- // They are replaced or removed in the constructor
-
- /**
- * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
- * function that will get called when an event with a certain name gets triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to call with `EventTarget`s
- */
- on(type, fn) {}
-
- /**
- * Removes an `event listener` for a specific event from an instance of `EventTarget`.
- * This makes it so that the `event listener` will no longer get called when the
- * named event happens.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} [fn]
- * The function to remove. If not specified, all listeners managed by Video.js will be removed.
- */
- off(type, fn) {}
-
- /**
- * This function will add an `event listener` that gets triggered only once. After the
- * first trigger it will get removed. This is like adding an `event listener`
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- one(type, fn) {}
-
- /**
- * This function will add an `event listener` that gets triggered only once and is
- * removed from all events. This is like adding an array of `event listener`s
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
- * first time it is triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- any(type, fn) {}
-
- /**
- * This function causes an event to happen. This will then cause any `event listeners`
- * that are waiting for that event, to get called. If there are no `event listeners`
- * for an event then nothing will happen.
- *
- * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
- * Trigger will also call the `on` + `uppercaseEventName` function.
- *
- * Example:
- * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
- * `onClick` if it exists.
- *
- * @param {string|Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- *
- * @param {Object} [hash]
- * Optionally extra argument to pass through to an event listener
- */
- trigger(event, hash) {}
-
- /**
- * Dispose of the `Component` and all child components.
- *
- * @fires Component#dispose
- *
- * @param {Object} options
- * @param {Element} options.originalEl element with which to replace player element
- */
- dispose(options = {}) {
- // Bail out if the component has already been disposed.
- if (this.isDisposed_) {
- return;
- }
- if (this.readyQueue_) {
- this.readyQueue_.length = 0;
- }
-
- /**
- * Triggered when a `Component` is disposed.
- *
- * @event Component#dispose
- * @type {Event}
- *
- * @property {boolean} [bubbles=false]
- * set to false so that the dispose event does not
- * bubble up
- */
- this.trigger({
- type: 'dispose',
- bubbles: false
- });
- this.isDisposed_ = true;
-
- // Dispose all children.
- if (this.children_) {
- for (let i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i].dispose) {
- this.children_[i].dispose();
- }
- }
- }
-
- // Delete child references
- this.children_ = null;
- this.childIndex_ = null;
- this.childNameIndex_ = null;
- this.parentComponent_ = null;
- if (this.el_) {
- // Remove element from DOM
- if (this.el_.parentNode) {
- if (options.restoreEl) {
- this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
- } else {
- this.el_.parentNode.removeChild(this.el_);
- }
- }
- this.el_ = null;
- }
-
- // remove reference to the player after disposing of the element
- this.player_ = null;
- }
-
- /**
- * Determine whether or not this component has been disposed.
- *
- * @return {boolean}
- * If the component has been disposed, will be `true`. Otherwise, `false`.
- */
- isDisposed() {
- return Boolean(this.isDisposed_);
- }
-
- /**
- * Return the {@link Player} that the `Component` has attached to.
- *
- * @return { import('./player').default }
- * The player that this `Component` has attached to.
- */
- player() {
- return this.player_;
- }
-
- /**
- * Deep merge of options objects with new options.
- * > Note: When both `obj` and `options` contain properties whose values are objects.
- * The two properties get merged using {@link module:obj.merge}
- *
- * @param {Object} obj
- * The object that contains new options.
- *
- * @return {Object}
- * A new object of `this.options_` and `obj` merged together.
- */
- options(obj) {
- if (!obj) {
- return this.options_;
- }
- this.options_ = merge(this.options_, obj);
- return this.options_;
- }
-
- /**
- * Get the `Component`s DOM element
- *
- * @return {Element}
- * The DOM element for this `Component`.
- */
- el() {
- return this.el_;
- }
-
- /**
- * Create the `Component`s DOM element.
- *
- * @param {string} [tagName]
- * Element's DOM node type. e.g. 'div'
- *
- * @param {Object} [properties]
- * An object of properties that should be set.
- *
- * @param {Object} [attributes]
- * An object of attributes that should be set.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tagName, properties, attributes) {
- return createEl(tagName, properties, attributes);
- }
-
- /**
- * Localize a string given the string in english.
- *
- * If tokens are provided, it'll try and run a simple token replacement on the provided string.
- * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
- *
- * If a `defaultValue` is provided, it'll use that over `string`,
- * if a value isn't found in provided language files.
- * This is useful if you want to have a descriptive key for token replacement
- * but have a succinct localized string and not require `en.json` to be included.
- *
- * Currently, it is used for the progress bar timing.
- * ```js
- * {
- * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
- * }
- * ```
- * It is then used like so:
- * ```js
- * this.localize('progress bar timing: currentTime={1} duration{2}',
- * [this.player_.currentTime(), this.player_.duration()],
- * '{1} of {2}');
- * ```
- *
- * Which outputs something like: `01:23 of 24:56`.
- *
- *
- * @param {string} string
- * The string to localize and the key to lookup in the language files.
- * @param {string[]} [tokens]
- * If the current item has token replacements, provide the tokens here.
- * @param {string} [defaultValue]
- * Defaults to `string`. Can be a default value to use for token replacement
- * if the lookup key is needed to be separate.
- *
- * @return {string}
- * The localized string or if no localization exists the english string.
- */
- localize(string, tokens, defaultValue = string) {
- const code = this.player_.language && this.player_.language();
- const languages = this.player_.languages && this.player_.languages();
- const language = languages && languages[code];
- const primaryCode = code && code.split('-')[0];
- const primaryLang = languages && languages[primaryCode];
- let localizedString = defaultValue;
- if (language && language[string]) {
- localizedString = language[string];
- } else if (primaryLang && primaryLang[string]) {
- localizedString = primaryLang[string];
- }
- if (tokens) {
- localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
- const value = tokens[index - 1];
- let ret = value;
- if (typeof value === 'undefined') {
- ret = match;
- }
- return ret;
- });
- }
- return localizedString;
- }
-
- /**
- * Handles language change for the player in components. Should be overridden by sub-components.
- *
- * @abstract
- */
- handleLanguagechange() {}
-
- /**
- * Return the `Component`s DOM element. This is where children get inserted.
- * This will usually be the the same as the element returned in {@link Component#el}.
- *
- * @return {Element}
- * The content element for this `Component`.
- */
- contentEl() {
- return this.contentEl_ || this.el_;
- }
-
- /**
- * Get this `Component`s ID
- *
- * @return {string}
- * The id of this `Component`
- */
- id() {
- return this.id_;
- }
-
- /**
- * Get the `Component`s name. The name gets used to reference the `Component`
- * and is set during registration.
- *
- * @return {string}
- * The name of this `Component`.
- */
- name() {
- return this.name_;
- }
-
- /**
- * Get an array of all child components
- *
- * @return {Array}
- * The children
- */
- children() {
- return this.children_;
- }
-
- /**
- * Returns the child `Component` with the given `id`.
- *
- * @param {string} id
- * The id of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The child `Component` with the given `id` or undefined.
- */
- getChildById(id) {
- return this.childIndex_[id];
- }
-
- /**
- * Returns the child `Component` with the given `name`.
- *
- * @param {string} name
- * The name of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The child `Component` with the given `name` or undefined.
- */
- getChild(name) {
- if (!name) {
- return;
- }
- return this.childNameIndex_[name];
- }
-
- /**
- * Returns the descendant `Component` following the givent
- * descendant `names`. For instance ['foo', 'bar', 'baz'] would
- * try to get 'foo' on the current component, 'bar' on the 'foo'
- * component and 'baz' on the 'bar' component and return undefined
- * if any of those don't exist.
- *
- * @param {...string[]|...string} names
- * The name of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The descendant `Component` following the given descendant
- * `names` or undefined.
- */
- getDescendant(...names) {
- // flatten array argument into the main array
- names = names.reduce((acc, n) => acc.concat(n), []);
- let currentChild = this;
- for (let i = 0; i < names.length; i++) {
- currentChild = currentChild.getChild(names[i]);
- if (!currentChild || !currentChild.getChild) {
- return;
- }
- }
- return currentChild;
- }
-
- /**
- * Adds an SVG icon element to another element or component.
- *
- * @param {string} iconName
- * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
- *
- * @param {Element} [el=this.el()]
- * Element to set the title on. Defaults to the current Component's element.
- *
- * @return {Element}
- * The newly created icon element.
- */
- setIcon(iconName, el = this.el()) {
- // TODO: In v9 of video.js, we will want to remove font icons entirely.
- // This means this check, as well as the others throughout the code, and
- // the unecessary CSS for font icons, will need to be removed.
- // See https://github.com/videojs/video.js/pull/8260 as to which components
- // need updating.
- if (!this.player_.options_.experimentalSvgIcons) {
- return;
- }
- const xmlnsURL = 'http://www.w3.org/2000/svg';
-
- // The below creates an element in the format of:
- // ....
- const iconContainer = createEl('span', {
- className: 'vjs-icon-placeholder vjs-svg-icon'
- }, {
- 'aria-hidden': 'true'
- });
- const svgEl = document.createElementNS(xmlnsURL, 'svg');
- svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
- const useEl = document.createElementNS(xmlnsURL, 'use');
- svgEl.appendChild(useEl);
- useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
- iconContainer.appendChild(svgEl);
-
- // Replace a pre-existing icon if one exists.
- if (this.iconIsSet_) {
- el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
- } else {
- el.appendChild(iconContainer);
- }
- this.iconIsSet_ = true;
- return iconContainer;
- }
-
- /**
- * Add a child `Component` inside the current `Component`.
- *
- * @param {string|Component} child
- * The name or instance of a child to add.
- *
- * @param {Object} [options={}]
- * The key/value store of options that will get passed to children of
- * the child.
- *
- * @param {number} [index=this.children_.length]
- * The index to attempt to add a child into.
- *
- *
- * @return {Component}
- * The `Component` that gets added as a child. When using a string the
- * `Component` will get created by this process.
- */
- addChild(child, options = {}, index = this.children_.length) {
- let component;
- let componentName;
-
- // If child is a string, create component with options
- if (typeof child === 'string') {
- componentName = toTitleCase(child);
- const componentClassName = options.componentClass || componentName;
-
- // Set name through options
- options.name = componentName;
-
- // Create a new object & element for this controls set
- // If there's no .player_, this is a player
- const ComponentClass = Component.getComponent(componentClassName);
- if (!ComponentClass) {
- throw new Error(`Component ${componentClassName} does not exist`);
- }
-
- // data stored directly on the videojs object may be
- // misidentified as a component to retain
- // backwards-compatibility with 4.x. check to make sure the
- // component class can be instantiated.
- if (typeof ComponentClass !== 'function') {
- return null;
- }
- component = new ComponentClass(this.player_ || this, options);
-
- // child is a component instance
- } else {
- component = child;
- }
- if (component.parentComponent_) {
- component.parentComponent_.removeChild(component);
- }
- this.children_.splice(index, 0, component);
- component.parentComponent_ = this;
- if (typeof component.id === 'function') {
- this.childIndex_[component.id()] = component;
- }
-
- // If a name wasn't used to create the component, check if we can use the
- // name function of the component
- componentName = componentName || component.name && toTitleCase(component.name());
- if (componentName) {
- this.childNameIndex_[componentName] = component;
- this.childNameIndex_[toLowerCase(componentName)] = component;
- }
-
- // Add the UI object's element to the container div (box)
- // Having an element is not required
- if (typeof component.el === 'function' && component.el()) {
- // If inserting before a component, insert before that component's element
- let refNode = null;
- if (this.children_[index + 1]) {
- // Most children are components, but the video tech is an HTML element
- if (this.children_[index + 1].el_) {
- refNode = this.children_[index + 1].el_;
- } else if (isEl(this.children_[index + 1])) {
- refNode = this.children_[index + 1];
- }
- }
- this.contentEl().insertBefore(component.el(), refNode);
- }
-
- // Return so it can stored on parent object if desired.
- return component;
- }
-
- /**
- * Remove a child `Component` from this `Component`s list of children. Also removes
- * the child `Component`s element from this `Component`s element.
- *
- * @param {Component} component
- * The child `Component` to remove.
- */
- removeChild(component) {
- if (typeof component === 'string') {
- component = this.getChild(component);
- }
- if (!component || !this.children_) {
- return;
- }
- let childFound = false;
- for (let i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i] === component) {
- childFound = true;
- this.children_.splice(i, 1);
- break;
- }
- }
- if (!childFound) {
- return;
- }
- component.parentComponent_ = null;
- this.childIndex_[component.id()] = null;
- this.childNameIndex_[toTitleCase(component.name())] = null;
- this.childNameIndex_[toLowerCase(component.name())] = null;
- const compEl = component.el();
- if (compEl && compEl.parentNode === this.contentEl()) {
- this.contentEl().removeChild(component.el());
- }
- }
-
- /**
- * Add and initialize default child `Component`s based upon options.
- */
- initChildren() {
- const children = this.options_.children;
- if (children) {
- // `this` is `parent`
- const parentOptions = this.options_;
- const handleAdd = child => {
- const name = child.name;
- let opts = child.opts;
-
- // Allow options for children to be set at the parent options
- // e.g. videojs(id, { controlBar: false });
- // instead of videojs(id, { children: { controlBar: false });
- if (parentOptions[name] !== undefined) {
- opts = parentOptions[name];
- }
-
- // Allow for disabling default components
- // e.g. options['children']['posterImage'] = false
- if (opts === false) {
- return;
- }
-
- // Allow options to be passed as a simple boolean if no configuration
- // is necessary.
- if (opts === true) {
- opts = {};
- }
-
- // We also want to pass the original player options
- // to each component as well so they don't need to
- // reach back into the player for options later.
- opts.playerOptions = this.options_.playerOptions;
-
- // Create and add the child component.
- // Add a direct reference to the child by name on the parent instance.
- // If two of the same component are used, different names should be supplied
- // for each
- const newChild = this.addChild(name, opts);
- if (newChild) {
- this[name] = newChild;
- }
- };
-
- // Allow for an array of children details to passed in the options
- let workingChildren;
- const Tech = Component.getComponent('Tech');
- if (Array.isArray(children)) {
- workingChildren = children;
- } else {
- workingChildren = Object.keys(children);
- }
- workingChildren
- // children that are in this.options_ but also in workingChildren would
- // give us extra children we do not want. So, we want to filter them out.
- .concat(Object.keys(this.options_).filter(function (child) {
- return !workingChildren.some(function (wchild) {
- if (typeof wchild === 'string') {
- return child === wchild;
- }
- return child === wchild.name;
- });
- })).map(child => {
- let name;
- let opts;
- if (typeof child === 'string') {
- name = child;
- opts = children[name] || this.options_[name] || {};
- } else {
- name = child.name;
- opts = child;
- }
- return {
- name,
- opts
- };
- }).filter(child => {
- // we have to make sure that child.name isn't in the techOrder since
- // techs are registered as Components but can't aren't compatible
- // See https://github.com/videojs/video.js/issues/2772
- const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name));
- return c && !Tech.isTech(c);
- }).forEach(handleAdd);
- }
- }
-
- /**
- * Builds the default DOM class name. Should be overridden by sub-components.
- *
- * @return {string}
- * The DOM class name for this object.
- *
- * @abstract
- */
- buildCSSClass() {
- // Child classes can include a function that does:
- // return 'CLASS NAME' + this._super();
- return '';
- }
-
- /**
- * Bind a listener to the component's ready state.
- * Different from event listeners in that if the ready event has already happened
- * it will trigger the function immediately.
- *
- * @param {ReadyCallback} fn
- * Function that gets called when the `Component` is ready.
- *
- * @return {Component}
- * Returns itself; method can be chained.
- */
- ready(fn, sync = false) {
- if (!fn) {
- return;
- }
- if (!this.isReady_) {
- this.readyQueue_ = this.readyQueue_ || [];
- this.readyQueue_.push(fn);
- return;
- }
- if (sync) {
- fn.call(this);
- } else {
- // Call the function asynchronously by default for consistency
- this.setTimeout(fn, 1);
- }
- }
-
- /**
- * Trigger all the ready listeners for this `Component`.
- *
- * @fires Component#ready
- */
- triggerReady() {
- this.isReady_ = true;
-
- // Ensure ready is triggered asynchronously
- this.setTimeout(function () {
- const readyQueue = this.readyQueue_;
-
- // Reset Ready Queue
- this.readyQueue_ = [];
- if (readyQueue && readyQueue.length > 0) {
- readyQueue.forEach(function (fn) {
- fn.call(this);
- }, this);
- }
-
- // Allow for using event listeners also
- /**
- * Triggered when a `Component` is ready.
- *
- * @event Component#ready
- * @type {Event}
- */
- this.trigger('ready');
- }, 1);
- }
-
- /**
- * Find a single DOM element matching a `selector`. This can be within the `Component`s
- * `contentEl()` or another custom context.
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelector`.
- *
- * @param {Element|string} [context=this.contentEl()]
- * A DOM element within which to query. Can also be a selector string in
- * which case the first matching element will get used as context. If
- * missing `this.contentEl()` gets used. If `this.contentEl()` returns
- * nothing it falls back to `document`.
- *
- * @return {Element|null}
- * the dom element that was found, or null
- *
- * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
- */
- $(selector, context) {
- return $(selector, context || this.contentEl());
- }
-
- /**
- * Finds all DOM element matching a `selector`. This can be within the `Component`s
- * `contentEl()` or another custom context.
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelectorAll`.
- *
- * @param {Element|string} [context=this.contentEl()]
- * A DOM element within which to query. Can also be a selector string in
- * which case the first matching element will get used as context. If
- * missing `this.contentEl()` gets used. If `this.contentEl()` returns
- * nothing it falls back to `document`.
- *
- * @return {NodeList}
- * a list of dom elements that were found
- *
- * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
- */
- $$(selector, context) {
- return $$(selector, context || this.contentEl());
- }
-
- /**
- * Check if a component's element has a CSS class name.
- *
- * @param {string} classToCheck
- * CSS class name to check.
- *
- * @return {boolean}
- * - True if the `Component` has the class.
- * - False if the `Component` does not have the class`
- */
- hasClass(classToCheck) {
- return hasClass(this.el_, classToCheck);
- }
-
- /**
- * Add a CSS class name to the `Component`s element.
- *
- * @param {...string} classesToAdd
- * One or more CSS class name to add.
- */
- addClass(...classesToAdd) {
- addClass(this.el_, ...classesToAdd);
- }
-
- /**
- * Remove a CSS class name from the `Component`s element.
- *
- * @param {...string} classesToRemove
- * One or more CSS class name to remove.
- */
- removeClass(...classesToRemove) {
- removeClass(this.el_, ...classesToRemove);
- }
-
- /**
- * Add or remove a CSS class name from the component's element.
- * - `classToToggle` gets added when {@link Component#hasClass} would return false.
- * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
- *
- * @param {string} classToToggle
- * The class to add or remove based on (@link Component#hasClass}
- *
- * @param {boolean|Dom~predicate} [predicate]
- * An {@link Dom~predicate} function or a boolean
- */
- toggleClass(classToToggle, predicate) {
- toggleClass(this.el_, classToToggle, predicate);
- }
-
- /**
- * Show the `Component`s element if it is hidden by removing the
- * 'vjs-hidden' class name from it.
- */
- show() {
- this.removeClass('vjs-hidden');
- }
-
- /**
- * Hide the `Component`s element if it is currently showing by adding the
- * 'vjs-hidden` class name to it.
- */
- hide() {
- this.addClass('vjs-hidden');
- }
-
- /**
- * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
- * class name to it. Used during fadeIn/fadeOut.
- *
- * @private
- */
- lockShowing() {
- this.addClass('vjs-lock-showing');
- }
-
- /**
- * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
- * class name from it. Used during fadeIn/fadeOut.
- *
- * @private
- */
- unlockShowing() {
- this.removeClass('vjs-lock-showing');
- }
-
- /**
- * Get the value of an attribute on the `Component`s element.
- *
- * @param {string} attribute
- * Name of the attribute to get the value from.
- *
- * @return {string|null}
- * - The value of the attribute that was asked for.
- * - Can be an empty string on some browsers if the attribute does not exist
- * or has no value
- * - Most browsers will return null if the attribute does not exist or has
- * no value.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
- */
- getAttribute(attribute) {
- return getAttribute(this.el_, attribute);
- }
-
- /**
- * Set the value of an attribute on the `Component`'s element
- *
- * @param {string} attribute
- * Name of the attribute to set.
- *
- * @param {string} value
- * Value to set the attribute to.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
- */
- setAttribute(attribute, value) {
- setAttribute(this.el_, attribute, value);
- }
-
- /**
- * Remove an attribute from the `Component`s element.
- *
- * @param {string} attribute
- * Name of the attribute to remove.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
- */
- removeAttribute(attribute) {
- removeAttribute(this.el_, attribute);
- }
-
- /**
- * Get or set the width of the component based upon the CSS styles.
- * See {@link Component#dimension} for more detailed information.
- *
- * @param {number|string} [num]
- * The width that you want to set postfixed with '%', 'px' or nothing.
- *
- * @param {boolean} [skipListeners]
- * Skip the componentresize event trigger
- *
- * @return {number|undefined}
- * The width when getting, zero if there is no width
- */
- width(num, skipListeners) {
- return this.dimension('width', num, skipListeners);
- }
-
- /**
- * Get or set the height of the component based upon the CSS styles.
- * See {@link Component#dimension} for more detailed information.
- *
- * @param {number|string} [num]
- * The height that you want to set postfixed with '%', 'px' or nothing.
- *
- * @param {boolean} [skipListeners]
- * Skip the componentresize event trigger
- *
- * @return {number|undefined}
- * The height when getting, zero if there is no height
- */
- height(num, skipListeners) {
- return this.dimension('height', num, skipListeners);
- }
-
- /**
- * Set both the width and height of the `Component` element at the same time.
- *
- * @param {number|string} width
- * Width to set the `Component`s element to.
- *
- * @param {number|string} height
- * Height to set the `Component`s element to.
- */
- dimensions(width, height) {
- // Skip componentresize listeners on width for optimization
- this.width(width, true);
- this.height(height);
- }
-
- /**
- * Get or set width or height of the `Component` element. This is the shared code
- * for the {@link Component#width} and {@link Component#height}.
- *
- * Things to know:
- * - If the width or height in an number this will return the number postfixed with 'px'.
- * - If the width/height is a percent this will return the percent postfixed with '%'
- * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
- * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
- * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
- * for more information
- * - If you want the computed style of the component, use {@link Component#currentWidth}
- * and {@link {Component#currentHeight}
- *
- * @fires Component#componentresize
- *
- * @param {string} widthOrHeight
- 8 'width' or 'height'
- *
- * @param {number|string} [num]
- 8 New dimension
- *
- * @param {boolean} [skipListeners]
- * Skip componentresize event trigger
- *
- * @return {number|undefined}
- * The dimension when getting or 0 if unset
- */
- dimension(widthOrHeight, num, skipListeners) {
- if (num !== undefined) {
- // Set to zero if null or literally NaN (NaN !== NaN)
- if (num === null || num !== num) {
- num = 0;
- }
-
- // Check if using css width/height (% or px) and adjust
- if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
- this.el_.style[widthOrHeight] = num;
- } else if (num === 'auto') {
- this.el_.style[widthOrHeight] = '';
- } else {
- this.el_.style[widthOrHeight] = num + 'px';
- }
-
- // skipListeners allows us to avoid triggering the resize event when setting both width and height
- if (!skipListeners) {
- /**
- * Triggered when a component is resized.
- *
- * @event Component#componentresize
- * @type {Event}
- */
- this.trigger('componentresize');
- }
- return;
- }
-
- // Not setting a value, so getting it
- // Make sure element exists
- if (!this.el_) {
- return 0;
- }
-
- // Get dimension value from style
- const val = this.el_.style[widthOrHeight];
- const pxIndex = val.indexOf('px');
- if (pxIndex !== -1) {
- // Return the pixel value with no 'px'
- return parseInt(val.slice(0, pxIndex), 10);
- }
-
- // No px so using % or no style was set, so falling back to offsetWidth/height
- // If component has display:none, offset will return 0
- // TODO: handle display:none and no dimension style using px
- return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
- }
-
- /**
- * Get the computed width or the height of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @param {string} widthOrHeight
- * A string containing 'width' or 'height'. Whichever one you want to get.
- *
- * @return {number}
- * The dimension that gets asked for or 0 if nothing was set
- * for that dimension.
- */
- currentDimension(widthOrHeight) {
- let computedWidthOrHeight = 0;
- if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
- throw new Error('currentDimension only accepts width or height value');
- }
- computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
-
- // remove 'px' from variable and parse as integer
- computedWidthOrHeight = parseFloat(computedWidthOrHeight);
-
- // if the computed value is still 0, it's possible that the browser is lying
- // and we want to check the offset values.
- // This code also runs wherever getComputedStyle doesn't exist.
- if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
- const rule = `offset${toTitleCase(widthOrHeight)}`;
- computedWidthOrHeight = this.el_[rule];
- }
- return computedWidthOrHeight;
- }
-
- /**
- * An object that contains width and height values of the `Component`s
- * computed style. Uses `window.getComputedStyle`.
- *
- * @typedef {Object} Component~DimensionObject
- *
- * @property {number} width
- * The width of the `Component`s computed style.
- *
- * @property {number} height
- * The height of the `Component`s computed style.
- */
-
- /**
- * Get an object that contains computed width and height values of the
- * component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {Component~DimensionObject}
- * The computed dimensions of the component's element.
- */
- currentDimensions() {
- return {
- width: this.currentDimension('width'),
- height: this.currentDimension('height')
- };
- }
-
- /**
- * Get the computed width of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {number}
- * The computed width of the component's element.
- */
- currentWidth() {
- return this.currentDimension('width');
- }
-
- /**
- * Get the computed height of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {number}
- * The computed height of the component's element.
- */
- currentHeight() {
- return this.currentDimension('height');
- }
-
- /**
- * Set the focus to this component
- */
- focus() {
- this.el_.focus();
- }
-
- /**
- * Remove the focus from this component
- */
- blur() {
- this.el_.blur();
- }
-
- /**
- * When this Component receives a `keydown` event which it does not process,
- * it passes the event to the Player for handling.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- */
- handleKeyDown(event) {
- if (this.player_) {
- // We only stop propagation here because we want unhandled events to fall
- // back to the browser. Exclude Tab for focus trapping.
- if (!keycode.isEventKey(event, 'Tab')) {
- event.stopPropagation();
- }
- this.player_.handleKeyDown(event);
- }
- }
-
- /**
- * Many components used to have a `handleKeyPress` method, which was poorly
- * named because it listened to a `keydown` event. This method name now
- * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
- * will not see their method calls stop working.
- *
- * @param {KeyboardEvent} event
- * The event that caused this function to be called.
- */
- handleKeyPress(event) {
- this.handleKeyDown(event);
- }
-
- /**
- * Emit a 'tap' events when touch event support gets detected. This gets used to
- * support toggling the controls through a tap on the video. They get enabled
- * because every sub-component would have extra overhead otherwise.
- *
- * @protected
- * @fires Component#tap
- * @listens Component#touchstart
- * @listens Component#touchmove
- * @listens Component#touchleave
- * @listens Component#touchcancel
- * @listens Component#touchend
- */
- emitTapEvents() {
- // Track the start time so we can determine how long the touch lasted
- let touchStart = 0;
- let firstTouch = null;
-
- // Maximum movement allowed during a touch event to still be considered a tap
- // Other popular libs use anywhere from 2 (hammer.js) to 15,
- // so 10 seems like a nice, round number.
- const tapMovementThreshold = 10;
-
- // The maximum length a touch can be while still being considered a tap
- const touchTimeThreshold = 200;
- let couldBeTap;
- this.on('touchstart', function (event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length === 1) {
- // Copy pageX/pageY from the object
- firstTouch = {
- pageX: event.touches[0].pageX,
- pageY: event.touches[0].pageY
- };
- // Record start time so we can detect a tap vs. "touch and hold"
- touchStart = window.performance.now();
- // Reset couldBeTap tracking
- couldBeTap = true;
- }
- });
- this.on('touchmove', function (event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length > 1) {
- couldBeTap = false;
- } else if (firstTouch) {
- // Some devices will throw touchmoves for all but the slightest of taps.
- // So, if we moved only a small distance, this could still be a tap
- const xdiff = event.touches[0].pageX - firstTouch.pageX;
- const ydiff = event.touches[0].pageY - firstTouch.pageY;
- const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
- if (touchDistance > tapMovementThreshold) {
- couldBeTap = false;
- }
- }
- });
- const noTap = function () {
- couldBeTap = false;
- };
-
- // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
- this.on('touchleave', noTap);
- this.on('touchcancel', noTap);
-
- // When the touch ends, measure how long it took and trigger the appropriate
- // event
- this.on('touchend', function (event) {
- firstTouch = null;
- // Proceed only if the touchmove/leave/cancel event didn't happen
- if (couldBeTap === true) {
- // Measure how long the touch lasted
- const touchTime = window.performance.now() - touchStart;
-
- // Make sure the touch was less than the threshold to be considered a tap
- if (touchTime < touchTimeThreshold) {
- // Don't let browser turn this into a click
- event.preventDefault();
- /**
- * Triggered when a `Component` is tapped.
- *
- * @event Component#tap
- * @type {MouseEvent}
- */
- this.trigger('tap');
- // It may be good to copy the touchend event object and change the
- // type to tap, if the other event properties aren't exact after
- // Events.fixEvent runs (e.g. event.target)
- }
- }
- });
- }
-
- /**
- * This function reports user activity whenever touch events happen. This can get
- * turned off by any sub-components that wants touch events to act another way.
- *
- * Report user touch activity when touch events occur. User activity gets used to
- * determine when controls should show/hide. It is simple when it comes to mouse
- * events, because any mouse event should show the controls. So we capture mouse
- * events that bubble up to the player and report activity when that happens.
- * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
- * controls. So touch events can't help us at the player level either.
- *
- * User activity gets checked asynchronously. So what could happen is a tap event
- * on the video turns the controls off. Then the `touchend` event bubbles up to
- * the player. Which, if it reported user activity, would turn the controls right
- * back on. We also don't want to completely block touch events from bubbling up.
- * Furthermore a `touchmove` event and anything other than a tap, should not turn
- * controls back on.
- *
- * @listens Component#touchstart
- * @listens Component#touchmove
- * @listens Component#touchend
- * @listens Component#touchcancel
- */
- enableTouchActivity() {
- // Don't continue if the root player doesn't support reporting user activity
- if (!this.player() || !this.player().reportUserActivity) {
- return;
- }
-
- // listener for reporting that the user is active
- const report = bind_(this.player(), this.player().reportUserActivity);
- let touchHolding;
- this.on('touchstart', function () {
- report();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(touchHolding);
- // report at the same interval as activityCheck
- touchHolding = this.setInterval(report, 250);
- });
- const touchEnd = function (event) {
- report();
- // stop the interval that maintains activity if the touch is holding
- this.clearInterval(touchHolding);
- };
- this.on('touchmove', report);
- this.on('touchend', touchEnd);
- this.on('touchcancel', touchEnd);
- }
-
- /**
- * A callback that has no parameters and is bound into `Component`s context.
- *
- * @callback Component~GenericCallback
- * @this Component
- */
-
- /**
- * Creates a function that runs after an `x` millisecond timeout. This function is a
- * wrapper around `window.setTimeout`. There are a few reasons to use this one
- * instead though:
- * 1. It gets cleared via {@link Component#clearTimeout} when
- * {@link Component#dispose} gets called.
- * 2. The function callback will gets turned into a {@link Component~GenericCallback}
- *
- * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
- * will cause its dispose listener not to get cleaned up! Please use
- * {@link Component#clearTimeout} or {@link Component#dispose} instead.
- *
- * @param {Component~GenericCallback} fn
- * The function that will be run after `timeout`.
- *
- * @param {number} timeout
- * Timeout in milliseconds to delay before executing the specified function.
- *
- * @return {number}
- * Returns a timeout ID that gets used to identify the timeout. It can also
- * get used in {@link Component#clearTimeout} to clear the timeout that
- * was set.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
- */
- setTimeout(fn, timeout) {
- // declare as variables so they are properly available in timeout function
- // eslint-disable-next-line
- var timeoutId;
- fn = bind_(this, fn);
- this.clearTimersOnDispose_();
- timeoutId = window.setTimeout(() => {
- if (this.setTimeoutIds_.has(timeoutId)) {
- this.setTimeoutIds_.delete(timeoutId);
- }
- fn();
- }, timeout);
- this.setTimeoutIds_.add(timeoutId);
- return timeoutId;
- }
-
- /**
- * Clears a timeout that gets created via `window.setTimeout` or
- * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
- * use this function instead of `window.clearTimout`. If you don't your dispose
- * listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} timeoutId
- * The id of the timeout to clear. The return value of
- * {@link Component#setTimeout} or `window.setTimeout`.
- *
- * @return {number}
- * Returns the timeout id that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
- */
- clearTimeout(timeoutId) {
- if (this.setTimeoutIds_.has(timeoutId)) {
- this.setTimeoutIds_.delete(timeoutId);
- window.clearTimeout(timeoutId);
- }
- return timeoutId;
- }
-
- /**
- * Creates a function that gets run every `x` milliseconds. This function is a wrapper
- * around `window.setInterval`. There are a few reasons to use this one instead though.
- * 1. It gets cleared via {@link Component#clearInterval} when
- * {@link Component#dispose} gets called.
- * 2. The function callback will be a {@link Component~GenericCallback}
- *
- * @param {Component~GenericCallback} fn
- * The function to run every `x` seconds.
- *
- * @param {number} interval
- * Execute the specified function every `x` milliseconds.
- *
- * @return {number}
- * Returns an id that can be used to identify the interval. It can also be be used in
- * {@link Component#clearInterval} to clear the interval.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
- */
- setInterval(fn, interval) {
- fn = bind_(this, fn);
- this.clearTimersOnDispose_();
- const intervalId = window.setInterval(fn, interval);
- this.setIntervalIds_.add(intervalId);
- return intervalId;
- }
-
- /**
- * Clears an interval that gets created via `window.setInterval` or
- * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
- * use this function instead of `window.clearInterval`. If you don't your dispose
- * listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} intervalId
- * The id of the interval to clear. The return value of
- * {@link Component#setInterval} or `window.setInterval`.
- *
- * @return {number}
- * Returns the interval id that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
- */
- clearInterval(intervalId) {
- if (this.setIntervalIds_.has(intervalId)) {
- this.setIntervalIds_.delete(intervalId);
- window.clearInterval(intervalId);
- }
- return intervalId;
- }
-
- /**
- * Queues up a callback to be passed to requestAnimationFrame (rAF), but
- * with a few extra bonuses:
- *
- * - Supports browsers that do not support rAF by falling back to
- * {@link Component#setTimeout}.
- *
- * - The callback is turned into a {@link Component~GenericCallback} (i.e.
- * bound to the component).
- *
- * - Automatic cancellation of the rAF callback is handled if the component
- * is disposed before it is called.
- *
- * @param {Component~GenericCallback} fn
- * A function that will be bound to this component and executed just
- * before the browser's next repaint.
- *
- * @return {number}
- * Returns an rAF ID that gets used to identify the timeout. It can
- * also be used in {@link Component#cancelAnimationFrame} to cancel
- * the animation frame callback.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
- */
- requestAnimationFrame(fn) {
- this.clearTimersOnDispose_();
-
- // declare as variables so they are properly available in rAF function
- // eslint-disable-next-line
- var id;
- fn = bind_(this, fn);
- id = window.requestAnimationFrame(() => {
- if (this.rafIds_.has(id)) {
- this.rafIds_.delete(id);
- }
- fn();
- });
- this.rafIds_.add(id);
- return id;
- }
-
- /**
- * Request an animation frame, but only one named animation
- * frame will be queued. Another will never be added until
- * the previous one finishes.
- *
- * @param {string} name
- * The name to give this requestAnimationFrame
- *
- * @param {Component~GenericCallback} fn
- * A function that will be bound to this component and executed just
- * before the browser's next repaint.
- */
- requestNamedAnimationFrame(name, fn) {
- if (this.namedRafs_.has(name)) {
- return;
- }
- this.clearTimersOnDispose_();
- fn = bind_(this, fn);
- const id = this.requestAnimationFrame(() => {
- fn();
- if (this.namedRafs_.has(name)) {
- this.namedRafs_.delete(name);
- }
- });
- this.namedRafs_.set(name, id);
- return name;
- }
-
- /**
- * Cancels a current named animation frame if it exists.
- *
- * @param {string} name
- * The name of the requestAnimationFrame to cancel.
- */
- cancelNamedAnimationFrame(name) {
- if (!this.namedRafs_.has(name)) {
- return;
- }
- this.cancelAnimationFrame(this.namedRafs_.get(name));
- this.namedRafs_.delete(name);
- }
-
- /**
- * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
- * (rAF).
- *
- * If you queue an rAF callback via {@link Component#requestAnimationFrame},
- * use this function instead of `window.cancelAnimationFrame`. If you don't,
- * your dispose listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} id
- * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
- *
- * @return {number}
- * Returns the rAF ID that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
- */
- cancelAnimationFrame(id) {
- if (this.rafIds_.has(id)) {
- this.rafIds_.delete(id);
- window.cancelAnimationFrame(id);
- }
- return id;
- }
-
- /**
- * A function to setup `requestAnimationFrame`, `setTimeout`,
- * and `setInterval`, clearing on dispose.
- *
- * > Previously each timer added and removed dispose listeners on it's own.
- * For better performance it was decided to batch them all, and use `Set`s
- * to track outstanding timer ids.
- *
- * @private
- */
- clearTimersOnDispose_() {
- if (this.clearingTimersOnDispose_) {
- return;
- }
- this.clearingTimersOnDispose_ = true;
- this.one('dispose', () => {
- [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
- // for a `Set` key will actually be the value again
- // so forEach((val, val) =>` but for maps we want to use
- // the key.
- this[idName].forEach((val, key) => this[cancelName](key));
- });
- this.clearingTimersOnDispose_ = false;
- });
- }
-
- /**
- * Register a `Component` with `videojs` given the name and the component.
- *
- * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
- * should be registered using {@link Tech.registerTech} or
- * {@link videojs:videojs.registerTech}.
- *
- * > NOTE: This function can also be seen on videojs as
- * {@link videojs:videojs.registerComponent}.
- *
- * @param {string} name
- * The name of the `Component` to register.
- *
- * @param {Component} ComponentToRegister
- * The `Component` class to register.
- *
- * @return {Component}
- * The `Component` that was registered.
- */
- static registerComponent(name, ComponentToRegister) {
- if (typeof name !== 'string' || !name) {
- throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
- }
- const Tech = Component.getComponent('Tech');
-
- // We need to make sure this check is only done if Tech has been registered.
- const isTech = Tech && Tech.isTech(ComponentToRegister);
- const isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
- if (isTech || !isComp) {
- let reason;
- if (isTech) {
- reason = 'techs must be registered using Tech.registerTech()';
- } else {
- reason = 'must be a Component subclass';
- }
- throw new Error(`Illegal component, "${name}"; ${reason}.`);
- }
- name = toTitleCase(name);
- if (!Component.components_) {
- Component.components_ = {};
- }
- const Player = Component.getComponent('Player');
- if (name === 'Player' && Player && Player.players) {
- const players = Player.players;
- const playerNames = Object.keys(players);
-
- // If we have players that were disposed, then their name will still be
- // in Players.players. So, we must loop through and verify that the value
- // for each item is not null. This allows registration of the Player component
- // after all players have been disposed or before any were created.
- if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
- throw new Error('Can not register Player component after player has been created.');
- }
- }
- Component.components_[name] = ComponentToRegister;
- Component.components_[toLowerCase(name)] = ComponentToRegister;
- return ComponentToRegister;
- }
-
- /**
- * Get a `Component` based on the name it was registered with.
- *
- * @param {string} name
- * The Name of the component to get.
- *
- * @return {typeof Component}
- * The `Component` that got registered under the given name.
- */
- static getComponent(name) {
- if (!name || !Component.components_) {
- return;
- }
- return Component.components_[name];
- }
- }
- Component.registerComponent('Component', Component);
-
- /**
- * @file time.js
- * @module time
- */
-
- /**
- * Returns the time for the specified index at the start or end
- * of a TimeRange object.
- *
- * @typedef {Function} TimeRangeIndex
- *
- * @param {number} [index=0]
- * The range number to return the time for.
- *
- * @return {number}
- * The time offset at the specified index.
- *
- * @deprecated The index argument must be provided.
- * In the future, leaving it out will throw an error.
- */
-
- /**
- * An object that contains ranges of time, which mimics {@link TimeRanges}.
- *
- * @typedef {Object} TimeRange
- *
- * @property {number} length
- * The number of time ranges represented by this object.
- *
- * @property {module:time~TimeRangeIndex} start
- * Returns the time offset at which a specified time range begins.
- *
- * @property {module:time~TimeRangeIndex} end
- * Returns the time offset at which a specified time range ends.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
- */
-
- /**
- * Check if any of the time ranges are over the maximum index.
- *
- * @private
- * @param {string} fnName
- * The function name to use for logging
- *
- * @param {number} index
- * The index to check
- *
- * @param {number} maxIndex
- * The maximum possible index
- *
- * @throws {Error} if the timeRanges provided are over the maxIndex
- */
- function rangeCheck(fnName, index, maxIndex) {
- if (typeof index !== 'number' || index < 0 || index > maxIndex) {
- throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
- }
- }
-
- /**
- * Get the time for the specified index at the start or end
- * of a TimeRange object.
- *
- * @private
- * @param {string} fnName
- * The function name to use for logging
- *
- * @param {string} valueIndex
- * The property that should be used to get the time. should be
- * 'start' or 'end'
- *
- * @param {Array} ranges
- * An array of time ranges
- *
- * @param {Array} [rangeIndex=0]
- * The index to start the search at
- *
- * @return {number}
- * The time that offset at the specified index.
- *
- * @deprecated rangeIndex must be set to a value, in the future this will throw an error.
- * @throws {Error} if rangeIndex is more than the length of ranges
- */
- function getRange(fnName, valueIndex, ranges, rangeIndex) {
- rangeCheck(fnName, rangeIndex, ranges.length - 1);
- return ranges[rangeIndex][valueIndex];
- }
-
- /**
- * Create a time range object given ranges of time.
- *
- * @private
- * @param {Array} [ranges]
- * An array of time ranges.
- *
- * @return {TimeRange}
- */
- function createTimeRangesObj(ranges) {
- let timeRangesObj;
- if (ranges === undefined || ranges.length === 0) {
- timeRangesObj = {
- length: 0,
- start() {
- throw new Error('This TimeRanges object is empty');
- },
- end() {
- throw new Error('This TimeRanges object is empty');
- }
- };
- } else {
- timeRangesObj = {
- length: ranges.length,
- start: getRange.bind(null, 'start', 0, ranges),
- end: getRange.bind(null, 'end', 1, ranges)
- };
- }
- if (window.Symbol && window.Symbol.iterator) {
- timeRangesObj[window.Symbol.iterator] = () => (ranges || []).values();
- }
- return timeRangesObj;
- }
-
- /**
- * Create a `TimeRange` object which mimics an
- * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
- *
- * @param {number|Array[]} start
- * The start of a single range (a number) or an array of ranges (an
- * array of arrays of two numbers each).
- *
- * @param {number} end
- * The end of a single range. Cannot be used with the array form of
- * the `start` argument.
- *
- * @return {TimeRange}
- */
- function createTimeRanges(start, end) {
- if (Array.isArray(start)) {
- return createTimeRangesObj(start);
- } else if (start === undefined || end === undefined) {
- return createTimeRangesObj();
- }
- return createTimeRangesObj([[start, end]]);
- }
-
- /**
- * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
- * seconds) will force a number of leading zeros to cover the length of the
- * guide.
- *
- * @private
- * @param {number} seconds
- * Number of seconds to be turned into a string
- *
- * @param {number} guide
- * Number (in seconds) to model the string after
- *
- * @return {string}
- * Time formatted as H:MM:SS or M:SS
- */
- const defaultImplementation = function (seconds, guide) {
- seconds = seconds < 0 ? 0 : seconds;
- let s = Math.floor(seconds % 60);
- let m = Math.floor(seconds / 60 % 60);
- let h = Math.floor(seconds / 3600);
- const gm = Math.floor(guide / 60 % 60);
- const gh = Math.floor(guide / 3600);
-
- // handle invalid times
- if (isNaN(seconds) || seconds === Infinity) {
- // '-' is false for all relational operators (e.g. <, >=) so this setting
- // will add the minimum number of fields specified by the guide
- h = m = s = '-';
- }
-
- // Check if we need to show hours
- h = h > 0 || gh > 0 ? h + ':' : '';
-
- // If hours are showing, we may need to add a leading zero.
- // Always show at least one digit of minutes.
- m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
-
- // Check if leading zero is need for seconds
- s = s < 10 ? '0' + s : s;
- return h + m + s;
- };
-
- // Internal pointer to the current implementation.
- let implementation = defaultImplementation;
-
- /**
- * Replaces the default formatTime implementation with a custom implementation.
- *
- * @param {Function} customImplementation
- * A function which will be used in place of the default formatTime
- * implementation. Will receive the current time in seconds and the
- * guide (in seconds) as arguments.
- */
- function setFormatTime(customImplementation) {
- implementation = customImplementation;
- }
-
- /**
- * Resets formatTime to the default implementation.
- */
- function resetFormatTime() {
- implementation = defaultImplementation;
- }
-
- /**
- * Delegates to either the default time formatting function or a custom
- * function supplied via `setFormatTime`.
- *
- * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
- * guide (in seconds) will force a number of leading zeros to cover the
- * length of the guide.
- *
- * @example formatTime(125, 600) === "02:05"
- * @param {number} seconds
- * Number of seconds to be turned into a string
- *
- * @param {number} guide
- * Number (in seconds) to model the string after
- *
- * @return {string}
- * Time formatted as H:MM:SS or M:SS
- */
- function formatTime(seconds, guide = seconds) {
- return implementation(seconds, guide);
- }
-
- var Time = /*#__PURE__*/Object.freeze({
- __proto__: null,
- createTimeRanges: createTimeRanges,
- createTimeRange: createTimeRanges,
- setFormatTime: setFormatTime,
- resetFormatTime: resetFormatTime,
- formatTime: formatTime
- });
-
- /**
- * @file buffer.js
- * @module buffer
- */
-
- /**
- * Compute the percentage of the media that has been buffered.
- *
- * @param { import('./time').TimeRange } buffered
- * The current `TimeRanges` object representing buffered time ranges
- *
- * @param {number} duration
- * Total duration of the media
- *
- * @return {number}
- * Percent buffered of the total duration in decimal form.
- */
- function bufferedPercent(buffered, duration) {
- let bufferedDuration = 0;
- let start;
- let end;
- if (!duration) {
- return 0;
- }
- if (!buffered || !buffered.length) {
- buffered = createTimeRanges(0, 0);
- }
- for (let i = 0; i < buffered.length; i++) {
- start = buffered.start(i);
- end = buffered.end(i);
-
- // buffered end can be bigger than duration by a very small fraction
- if (end > duration) {
- end = duration;
- }
- bufferedDuration += end - start;
- }
- return bufferedDuration / duration;
- }
-
- /**
- * @file media-error.js
- */
-
- /**
- * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
- *
- * @param {number|string|Object|MediaError} value
- * This can be of multiple types:
- * - number: should be a standard error code
- * - string: an error message (the code will be 0)
- * - Object: arbitrary properties
- * - `MediaError` (native): used to populate a video.js `MediaError` object
- * - `MediaError` (video.js): will return itself if it's already a
- * video.js `MediaError` object.
- *
- * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
- * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
- *
- * @class MediaError
- */
- function MediaError(value) {
- // Allow redundant calls to this constructor to avoid having `instanceof`
- // checks peppered around the code.
- if (value instanceof MediaError) {
- return value;
- }
- if (typeof value === 'number') {
- this.code = value;
- } else if (typeof value === 'string') {
- // default code is zero, so this is a custom error
- this.message = value;
- } else if (isObject(value)) {
- // We assign the `code` property manually because native `MediaError` objects
- // do not expose it as an own/enumerable property of the object.
- if (typeof value.code === 'number') {
- this.code = value.code;
- }
- Object.assign(this, value);
- }
- if (!this.message) {
- this.message = MediaError.defaultMessages[this.code] || '';
- }
- }
-
- /**
- * The error code that refers two one of the defined `MediaError` types
- *
- * @type {Number}
- */
- MediaError.prototype.code = 0;
-
- /**
- * An optional message that to show with the error. Message is not part of the HTML5
- * video spec but allows for more informative custom errors.
- *
- * @type {String}
- */
- MediaError.prototype.message = '';
-
- /**
- * An optional status code that can be set by plugins to allow even more detail about
- * the error. For example a plugin might provide a specific HTTP status code and an
- * error message for that code. Then when the plugin gets that error this class will
- * know how to display an error message for it. This allows a custom message to show
- * up on the `Player` error overlay.
- *
- * @type {Array}
- */
- MediaError.prototype.status = null;
-
- /**
- * An object containing an error type, as well as other information regarding the error.
- *
- * @typedef {{errorType: string, [key: string]: any}} ErrorMetadata
- */
-
- /**
- * An optional object to give more detail about the error. This can be used to give
- * a higher level of specificity to an error versus the more generic MediaError codes.
- * `metadata` expects an `errorType` string that should align with the values from videojs.Error.
- *
- * @type {ErrorMetadata}
- */
- MediaError.prototype.metadata = null;
-
- /**
- * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
- * specification listed under {@link MediaError} for more information.
- *
- * @enum {array}
- * @readonly
- * @property {string} 0 - MEDIA_ERR_CUSTOM
- * @property {string} 1 - MEDIA_ERR_ABORTED
- * @property {string} 2 - MEDIA_ERR_NETWORK
- * @property {string} 3 - MEDIA_ERR_DECODE
- * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
- * @property {string} 5 - MEDIA_ERR_ENCRYPTED
- */
- MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
-
- /**
- * The default `MediaError` messages based on the {@link MediaError.errorTypes}.
- *
- * @type {Array}
- * @constant
- */
- MediaError.defaultMessages = {
- 1: 'You aborted the media playback',
- 2: 'A network error caused the media download to fail part-way.',
- 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
- 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
- 5: 'The media is encrypted and we do not have the keys to decrypt it.'
- };
-
- /**
- * W3C error code for any custom error.
- *
- * @member MediaError#MEDIA_ERR_CUSTOM
- * @constant {number}
- * @default 0
- */
- MediaError.MEDIA_ERR_CUSTOM = 0;
-
- /**
- * W3C error code for any custom error.
- *
- * @member MediaError.MEDIA_ERR_CUSTOM
- * @constant {number}
- * @default 0
- */
- MediaError.prototype.MEDIA_ERR_CUSTOM = 0;
-
- /**
- * W3C error code for media error aborted.
- *
- * @member MediaError#MEDIA_ERR_ABORTED
- * @constant {number}
- * @default 1
- */
- MediaError.MEDIA_ERR_ABORTED = 1;
-
- /**
- * W3C error code for media error aborted.
- *
- * @member MediaError.MEDIA_ERR_ABORTED
- * @constant {number}
- * @default 1
- */
- MediaError.prototype.MEDIA_ERR_ABORTED = 1;
-
- /**
- * W3C error code for any network error.
- *
- * @member MediaError#MEDIA_ERR_NETWORK
- * @constant {number}
- * @default 2
- */
- MediaError.MEDIA_ERR_NETWORK = 2;
-
- /**
- * W3C error code for any network error.
- *
- * @member MediaError.MEDIA_ERR_NETWORK
- * @constant {number}
- * @default 2
- */
- MediaError.prototype.MEDIA_ERR_NETWORK = 2;
-
- /**
- * W3C error code for any decoding error.
- *
- * @member MediaError#MEDIA_ERR_DECODE
- * @constant {number}
- * @default 3
- */
- MediaError.MEDIA_ERR_DECODE = 3;
-
- /**
- * W3C error code for any decoding error.
- *
- * @member MediaError.MEDIA_ERR_DECODE
- * @constant {number}
- * @default 3
- */
- MediaError.prototype.MEDIA_ERR_DECODE = 3;
-
- /**
- * W3C error code for any time that a source is not supported.
- *
- * @member MediaError#MEDIA_ERR_SRC_NOT_SUPPORTED
- * @constant {number}
- * @default 4
- */
- MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
-
- /**
- * W3C error code for any time that a source is not supported.
- *
- * @member MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
- * @constant {number}
- * @default 4
- */
- MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
-
- /**
- * W3C error code for any time that a source is encrypted.
- *
- * @member MediaError#MEDIA_ERR_ENCRYPTED
- * @constant {number}
- * @default 5
- */
- MediaError.MEDIA_ERR_ENCRYPTED = 5;
-
- /**
- * W3C error code for any time that a source is encrypted.
- *
- * @member MediaError.MEDIA_ERR_ENCRYPTED
- * @constant {number}
- * @default 5
- */
- MediaError.prototype.MEDIA_ERR_ENCRYPTED = 5;
-
- var tuple = SafeParseTuple;
- function SafeParseTuple(obj, reviver) {
- var json;
- var error = null;
- try {
- json = JSON.parse(obj, reviver);
- } catch (err) {
- error = err;
- }
- return [error, json];
- }
-
- /**
- * Returns whether an object is `Promise`-like (i.e. has a `then` method).
- *
- * @param {Object} value
- * An object that may or may not be `Promise`-like.
- *
- * @return {boolean}
- * Whether or not the object is `Promise`-like.
- */
- function isPromise(value) {
- return value !== undefined && value !== null && typeof value.then === 'function';
- }
-
- /**
- * Silence a Promise-like object.
- *
- * This is useful for avoiding non-harmful, but potentially confusing "uncaught
- * play promise" rejection error messages.
- *
- * @param {Object} value
- * An object that may or may not be `Promise`-like.
- */
- function silencePromise(value) {
- if (isPromise(value)) {
- value.then(null, e => {});
- }
- }
-
- /**
- * @file text-track-list-converter.js Utilities for capturing text track state and
- * re-creating tracks based on a capture.
- *
- * @module text-track-list-converter
- */
-
- /**
- * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
- * represents the {@link TextTrack}'s state.
- *
- * @param {TextTrack} track
- * The text track to query.
- *
- * @return {Object}
- * A serializable javascript representation of the TextTrack.
- * @private
- */
- const trackToJson_ = function (track) {
- const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
- if (track[prop]) {
- acc[prop] = track[prop];
- }
- return acc;
- }, {
- cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
- return {
- startTime: cue.startTime,
- endTime: cue.endTime,
- text: cue.text,
- id: cue.id
- };
- })
- });
- return ret;
- };
-
- /**
- * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
- * state of all {@link TextTrack}s currently configured. The return array is compatible with
- * {@link text-track-list-converter:jsonToTextTracks}.
- *
- * @param { import('../tech/tech').default } tech
- * The tech object to query
- *
- * @return {Array}
- * A serializable javascript representation of the {@link Tech}s
- * {@link TextTrackList}.
- */
- const textTracksToJson = function (tech) {
- const trackEls = tech.$$('track');
- const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
- const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
- const json = trackToJson_(trackEl.track);
- if (trackEl.src) {
- json.src = trackEl.src;
- }
- return json;
- });
- return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
- return trackObjs.indexOf(track) === -1;
- }).map(trackToJson_));
- };
-
- /**
- * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
- * object {@link TextTrack} representations.
- *
- * @param {Array} json
- * An array of `TextTrack` representation objects, like those that would be
- * produced by `textTracksToJson`.
- *
- * @param {Tech} tech
- * The `Tech` to create the `TextTrack`s on.
- */
- const jsonToTextTracks = function (json, tech) {
- json.forEach(function (track) {
- const addedTrack = tech.addRemoteTextTrack(track).track;
- if (!track.src && track.cues) {
- track.cues.forEach(cue => addedTrack.addCue(cue));
- }
- });
- return tech.textTracks();
- };
- var textTrackConverter = {
- textTracksToJson,
- jsonToTextTracks,
- trackToJson_
- };
-
- /**
- * @file modal-dialog.js
- */
- const MODAL_CLASS_NAME = 'vjs-modal-dialog';
-
- /**
- * The `ModalDialog` displays over the video and its controls, which blocks
- * interaction with the player until it is closed.
- *
- * Modal dialogs include a "Close" button and will close when that button
- * is activated - or when ESC is pressed anywhere.
- *
- * @extends Component
- */
- class ModalDialog extends Component {
- /**
- * Create an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param { import('./utils/dom').ContentDescriptor} [options.content=undefined]
- * Provide customized content for this modal.
- *
- * @param {string} [options.description]
- * A text description for the modal, primarily for accessibility.
- *
- * @param {boolean} [options.fillAlways=false]
- * Normally, modals are automatically filled only the first time
- * they open. This tells the modal to refresh its content
- * every time it opens.
- *
- * @param {string} [options.label]
- * A text label for the modal, primarily for accessibility.
- *
- * @param {boolean} [options.pauseOnOpen=true]
- * If `true`, playback will will be paused if playing when
- * the modal opens, and resumed when it closes.
- *
- * @param {boolean} [options.temporary=true]
- * If `true`, the modal can only be opened once; it will be
- * disposed as soon as it's closed.
- *
- * @param {boolean} [options.uncloseable=false]
- * If `true`, the user will not be able to close the modal
- * through the UI in the normal ways. Programmatic closing is
- * still possible.
- */
- constructor(player, options) {
- super(player, options);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.close_ = e => this.close(e);
- this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
- this.closeable(!this.options_.uncloseable);
- this.content(this.options_.content);
-
- // Make sure the contentEl is defined AFTER any children are initialized
- // because we only want the contents of the modal in the contentEl
- // (not the UI elements like the close button).
- this.contentEl_ = createEl('div', {
- className: `${MODAL_CLASS_NAME}-content`
- }, {
- role: 'document'
- });
- this.descEl_ = createEl('p', {
- className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
- id: this.el().getAttribute('aria-describedby')
- });
- textContent(this.descEl_, this.description());
- this.el_.appendChild(this.descEl_);
- this.el_.appendChild(this.contentEl_);
- }
-
- /**
- * Create the `ModalDialog`'s DOM element
- *
- * @return {Element}
- * The DOM element that gets created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildCSSClass(),
- tabIndex: -1
- }, {
- 'aria-describedby': `${this.id()}_description`,
- 'aria-hidden': 'true',
- 'aria-label': this.label(),
- 'role': 'dialog',
- 'aria-live': 'polite'
- });
- }
- dispose() {
- this.contentEl_ = null;
- this.descEl_ = null;
- this.previouslyActiveEl_ = null;
- super.dispose();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
- }
-
- /**
- * Returns the label string for this modal. Primarily used for accessibility.
- *
- * @return {string}
- * the localized or raw label of this modal.
- */
- label() {
- return this.localize(this.options_.label || 'Modal Window');
- }
-
- /**
- * Returns the description string for this modal. Primarily used for
- * accessibility.
- *
- * @return {string}
- * The localized or raw description of this modal.
- */
- description() {
- let desc = this.options_.description || this.localize('This is a modal window.');
-
- // Append a universal closeability message if the modal is closeable.
- if (this.closeable()) {
- desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
- }
- return desc;
- }
-
- /**
- * Opens the modal.
- *
- * @fires ModalDialog#beforemodalopen
- * @fires ModalDialog#modalopen
- */
- open() {
- if (this.opened_) {
- if (this.options_.fillAlways) {
- this.fill();
- }
- return;
- }
- const player = this.player();
-
- /**
- * Fired just before a `ModalDialog` is opened.
- *
- * @event ModalDialog#beforemodalopen
- * @type {Event}
- */
- this.trigger('beforemodalopen');
- this.opened_ = true;
-
- // Fill content if the modal has never opened before and
- // never been filled.
- if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
- this.fill();
- }
-
- // If the player was playing, pause it and take note of its previously
- // playing state.
- this.wasPlaying_ = !player.paused();
- if (this.options_.pauseOnOpen && this.wasPlaying_) {
- player.pause();
- }
- this.on('keydown', this.handleKeyDown_);
-
- // Hide controls and note if they were enabled.
- this.hadControls_ = player.controls();
- player.controls(false);
- this.show();
- this.conditionalFocus_();
- this.el().setAttribute('aria-hidden', 'false');
-
- /**
- * Fired just after a `ModalDialog` is opened.
- *
- * @event ModalDialog#modalopen
- * @type {Event}
- */
- this.trigger('modalopen');
- this.hasBeenOpened_ = true;
- }
-
- /**
- * If the `ModalDialog` is currently open or closed.
- *
- * @param {boolean} [value]
- * If given, it will open (`true`) or close (`false`) the modal.
- *
- * @return {boolean}
- * the current open state of the modaldialog
- */
- opened(value) {
- if (typeof value === 'boolean') {
- this[value ? 'open' : 'close']();
- }
- return this.opened_;
- }
-
- /**
- * Closes the modal, does nothing if the `ModalDialog` is
- * not open.
- *
- * @fires ModalDialog#beforemodalclose
- * @fires ModalDialog#modalclose
- */
- close() {
- if (!this.opened_) {
- return;
- }
- const player = this.player();
-
- /**
- * Fired just before a `ModalDialog` is closed.
- *
- * @event ModalDialog#beforemodalclose
- * @type {Event}
- */
- this.trigger('beforemodalclose');
- this.opened_ = false;
- if (this.wasPlaying_ && this.options_.pauseOnOpen) {
- player.play();
- }
- this.off('keydown', this.handleKeyDown_);
- if (this.hadControls_) {
- player.controls(true);
- }
- this.hide();
- this.el().setAttribute('aria-hidden', 'true');
-
- /**
- * Fired just after a `ModalDialog` is closed.
- *
- * @event ModalDialog#modalclose
- * @type {Event}
- */
- this.trigger('modalclose');
- this.conditionalBlur_();
- if (this.options_.temporary) {
- this.dispose();
- }
- }
-
- /**
- * Check to see if the `ModalDialog` is closeable via the UI.
- *
- * @param {boolean} [value]
- * If given as a boolean, it will set the `closeable` option.
- *
- * @return {boolean}
- * Returns the final value of the closable option.
- */
- closeable(value) {
- if (typeof value === 'boolean') {
- const closeable = this.closeable_ = !!value;
- let close = this.getChild('closeButton');
-
- // If this is being made closeable and has no close button, add one.
- if (closeable && !close) {
- // The close button should be a child of the modal - not its
- // content element, so temporarily change the content element.
- const temp = this.contentEl_;
- this.contentEl_ = this.el_;
- close = this.addChild('closeButton', {
- controlText: 'Close Modal Dialog'
- });
- this.contentEl_ = temp;
- this.on(close, 'close', this.close_);
- }
-
- // If this is being made uncloseable and has a close button, remove it.
- if (!closeable && close) {
- this.off(close, 'close', this.close_);
- this.removeChild(close);
- close.dispose();
- }
- }
- return this.closeable_;
- }
-
- /**
- * Fill the modal's content element with the modal's "content" option.
- * The content element will be emptied before this change takes place.
- */
- fill() {
- this.fillWith(this.content());
- }
-
- /**
- * Fill the modal's content element with arbitrary content.
- * The content element will be emptied before this change takes place.
- *
- * @fires ModalDialog#beforemodalfill
- * @fires ModalDialog#modalfill
- *
- * @param { import('./utils/dom').ContentDescriptor} [content]
- * The same rules apply to this as apply to the `content` option.
- */
- fillWith(content) {
- const contentEl = this.contentEl();
- const parentEl = contentEl.parentNode;
- const nextSiblingEl = contentEl.nextSibling;
-
- /**
- * Fired just before a `ModalDialog` is filled with content.
- *
- * @event ModalDialog#beforemodalfill
- * @type {Event}
- */
- this.trigger('beforemodalfill');
- this.hasBeenFilled_ = true;
-
- // Detach the content element from the DOM before performing
- // manipulation to avoid modifying the live DOM multiple times.
- parentEl.removeChild(contentEl);
- this.empty();
- insertContent(contentEl, content);
- /**
- * Fired just after a `ModalDialog` is filled with content.
- *
- * @event ModalDialog#modalfill
- * @type {Event}
- */
- this.trigger('modalfill');
-
- // Re-inject the re-filled content element.
- if (nextSiblingEl) {
- parentEl.insertBefore(contentEl, nextSiblingEl);
- } else {
- parentEl.appendChild(contentEl);
- }
-
- // make sure that the close button is last in the dialog DOM
- const closeButton = this.getChild('closeButton');
- if (closeButton) {
- parentEl.appendChild(closeButton.el_);
- }
- }
-
- /**
- * Empties the content element. This happens anytime the modal is filled.
- *
- * @fires ModalDialog#beforemodalempty
- * @fires ModalDialog#modalempty
- */
- empty() {
- /**
- * Fired just before a `ModalDialog` is emptied.
- *
- * @event ModalDialog#beforemodalempty
- * @type {Event}
- */
- this.trigger('beforemodalempty');
- emptyEl(this.contentEl());
-
- /**
- * Fired just after a `ModalDialog` is emptied.
- *
- * @event ModalDialog#modalempty
- * @type {Event}
- */
- this.trigger('modalempty');
- }
-
- /**
- * Gets or sets the modal content, which gets normalized before being
- * rendered into the DOM.
- *
- * This does not update the DOM or fill the modal, but it is called during
- * that process.
- *
- * @param { import('./utils/dom').ContentDescriptor} [value]
- * If defined, sets the internal content value to be used on the
- * next call(s) to `fill`. This value is normalized before being
- * inserted. To "clear" the internal content value, pass `null`.
- *
- * @return { import('./utils/dom').ContentDescriptor}
- * The current content of the modal dialog
- */
- content(value) {
- if (typeof value !== 'undefined') {
- this.content_ = value;
- }
- return this.content_;
- }
-
- /**
- * conditionally focus the modal dialog if focus was previously on the player.
- *
- * @private
- */
- conditionalFocus_() {
- const activeEl = document.activeElement;
- const playerEl = this.player_.el_;
- this.previouslyActiveEl_ = null;
- if (playerEl.contains(activeEl) || playerEl === activeEl) {
- this.previouslyActiveEl_ = activeEl;
- this.focus();
- }
- }
-
- /**
- * conditionally blur the element and refocus the last focused element
- *
- * @private
- */
- conditionalBlur_() {
- if (this.previouslyActiveEl_) {
- this.previouslyActiveEl_.focus();
- this.previouslyActiveEl_ = null;
- }
- }
-
- /**
- * Keydown handler. Attached when modal is focused.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Do not allow keydowns to reach out of the modal dialog.
- event.stopPropagation();
- if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
- event.preventDefault();
- this.close();
- return;
- }
-
- // exit early if it isn't a tab key
- if (!keycode.isEventKey(event, 'Tab')) {
- return;
- }
- const focusableEls = this.focusableEls_();
- const activeEl = this.el_.querySelector(':focus');
- let focusIndex;
- for (let i = 0; i < focusableEls.length; i++) {
- if (activeEl === focusableEls[i]) {
- focusIndex = i;
- break;
- }
- }
- if (document.activeElement === this.el_) {
- focusIndex = 0;
- }
- if (event.shiftKey && focusIndex === 0) {
- focusableEls[focusableEls.length - 1].focus();
- event.preventDefault();
- } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
- focusableEls[0].focus();
- event.preventDefault();
- }
- }
-
- /**
- * get all focusable elements
- *
- * @private
- */
- focusableEls_() {
- const allChildren = this.el_.querySelectorAll('*');
- return Array.prototype.filter.call(allChildren, child => {
- return (child instanceof window.HTMLAnchorElement || child instanceof window.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window.HTMLInputElement || child instanceof window.HTMLSelectElement || child instanceof window.HTMLTextAreaElement || child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window.HTMLIFrameElement || child instanceof window.HTMLObjectElement || child instanceof window.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
- });
- }
- }
-
- /**
- * Default options for `ModalDialog` default options.
- *
- * @type {Object}
- * @private
- */
- ModalDialog.prototype.options_ = {
- pauseOnOpen: true,
- temporary: true
- };
- Component.registerComponent('ModalDialog', ModalDialog);
-
- /**
- * @file track-list.js
- */
-
- /**
- * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
- * {@link VideoTrackList}
- *
- * @extends EventTarget
- */
- class TrackList extends EventTarget {
- /**
- * Create an instance of this class
- *
- * @param { import('./track').default[] } tracks
- * A list of tracks to initialize the list with.
- *
- * @abstract
- */
- constructor(tracks = []) {
- super();
- this.tracks_ = [];
-
- /**
- * @memberof TrackList
- * @member {number} length
- * The current number of `Track`s in the this Trackist.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.tracks_.length;
- }
- });
- for (let i = 0; i < tracks.length; i++) {
- this.addTrack(tracks[i]);
- }
- }
-
- /**
- * Add a {@link Track} to the `TrackList`
- *
- * @param { import('./track').default } track
- * The audio, video, or text track to add to the list.
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- const index = this.tracks_.length;
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.tracks_[index];
- }
- });
- }
-
- // Do not add duplicate tracks
- if (this.tracks_.indexOf(track) === -1) {
- this.tracks_.push(track);
- /**
- * Triggered when a track is added to a track list.
- *
- * @event TrackList#addtrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was added.
- */
- this.trigger({
- track,
- type: 'addtrack',
- target: this
- });
- }
-
- /**
- * Triggered when a track label is changed.
- *
- * @event TrackList#addtrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was added.
- */
- track.labelchange_ = () => {
- this.trigger({
- track,
- type: 'labelchange',
- target: this
- });
- };
- if (isEvented(track)) {
- track.addEventListener('labelchange', track.labelchange_);
- }
- }
-
- /**
- * Remove a {@link Track} from the `TrackList`
- *
- * @param { import('./track').default } rtrack
- * The audio, video, or text track to remove from the list.
- *
- * @fires TrackList#removetrack
- */
- removeTrack(rtrack) {
- let track;
- for (let i = 0, l = this.length; i < l; i++) {
- if (this[i] === rtrack) {
- track = this[i];
- if (track.off) {
- track.off();
- }
- this.tracks_.splice(i, 1);
- break;
- }
- }
- if (!track) {
- return;
- }
-
- /**
- * Triggered when a track is removed from track list.
- *
- * @event TrackList#removetrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was removed.
- */
- this.trigger({
- track,
- type: 'removetrack',
- target: this
- });
- }
-
- /**
- * Get a Track from the TrackList by a tracks id
- *
- * @param {string} id - the id of the track to get
- * @method getTrackById
- * @return { import('./track').default }
- * @private
- */
- getTrackById(id) {
- let result = null;
- for (let i = 0, l = this.length; i < l; i++) {
- const track = this[i];
- if (track.id === id) {
- result = track;
- break;
- }
- }
- return result;
- }
- }
-
- /**
- * Triggered when a different track is selected/enabled.
- *
- * @event TrackList#change
- * @type {Event}
- */
-
- /**
- * Events that can be called with on + eventName. See {@link EventHandler}.
- *
- * @property {Object} TrackList#allowedEvents_
- * @protected
- */
- TrackList.prototype.allowedEvents_ = {
- change: 'change',
- addtrack: 'addtrack',
- removetrack: 'removetrack',
- labelchange: 'labelchange'
- };
-
- // emulate attribute EventHandler support to allow for feature detection
- for (const event in TrackList.prototype.allowedEvents_) {
- TrackList.prototype['on' + event] = null;
- }
-
- /**
- * @file audio-track-list.js
- */
-
- /**
- * Anywhere we call this function we diverge from the spec
- * as we only support one enabled audiotrack at a time
- *
- * @param {AudioTrackList} list
- * list to work on
- *
- * @param { import('./audio-track').default } track
- * The track to skip
- *
- * @private
- */
- const disableOthers$1 = function (list, track) {
- for (let i = 0; i < list.length; i++) {
- if (!Object.keys(list[i]).length || track.id === list[i].id) {
- continue;
- }
- // another audio track is enabled, disable it
- list[i].enabled = false;
- }
- };
-
- /**
- * The current list of {@link AudioTrack} for a media file.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
- * @extends TrackList
- */
- class AudioTrackList extends TrackList {
- /**
- * Create an instance of this class.
- *
- * @param { import('./audio-track').default[] } [tracks=[]]
- * A list of `AudioTrack` to instantiate the list with.
- */
- constructor(tracks = []) {
- // make sure only 1 track is enabled
- // sorted from last index to first index
- for (let i = tracks.length - 1; i >= 0; i--) {
- if (tracks[i].enabled) {
- disableOthers$1(tracks, tracks[i]);
- break;
- }
- }
- super(tracks);
- this.changing_ = false;
- }
-
- /**
- * Add an {@link AudioTrack} to the `AudioTrackList`.
- *
- * @param { import('./audio-track').default } track
- * The AudioTrack to add to the list
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- if (track.enabled) {
- disableOthers$1(this, track);
- }
- super.addTrack(track);
- // native tracks don't have this
- if (!track.addEventListener) {
- return;
- }
- track.enabledChange_ = () => {
- // when we are disabling other tracks (since we don't support
- // more than one track at a time) we will set changing_
- // to true so that we don't trigger additional change events
- if (this.changing_) {
- return;
- }
- this.changing_ = true;
- disableOthers$1(this, track);
- this.changing_ = false;
- this.trigger('change');
- };
-
- /**
- * @listens AudioTrack#enabledchange
- * @fires TrackList#change
- */
- track.addEventListener('enabledchange', track.enabledChange_);
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
- if (rtrack.removeEventListener && rtrack.enabledChange_) {
- rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
- rtrack.enabledChange_ = null;
- }
- }
- }
-
- /**
- * @file video-track-list.js
- */
-
- /**
- * Un-select all other {@link VideoTrack}s that are selected.
- *
- * @param {VideoTrackList} list
- * list to work on
- *
- * @param { import('./video-track').default } track
- * The track to skip
- *
- * @private
- */
- const disableOthers = function (list, track) {
- for (let i = 0; i < list.length; i++) {
- if (!Object.keys(list[i]).length || track.id === list[i].id) {
- continue;
- }
- // another video track is enabled, disable it
- list[i].selected = false;
- }
- };
-
- /**
- * The current list of {@link VideoTrack} for a video.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
- * @extends TrackList
- */
- class VideoTrackList extends TrackList {
- /**
- * Create an instance of this class.
- *
- * @param {VideoTrack[]} [tracks=[]]
- * A list of `VideoTrack` to instantiate the list with.
- */
- constructor(tracks = []) {
- // make sure only 1 track is enabled
- // sorted from last index to first index
- for (let i = tracks.length - 1; i >= 0; i--) {
- if (tracks[i].selected) {
- disableOthers(tracks, tracks[i]);
- break;
- }
- }
- super(tracks);
- this.changing_ = false;
-
- /**
- * @member {number} VideoTrackList#selectedIndex
- * The current index of the selected {@link VideoTrack`}.
- */
- Object.defineProperty(this, 'selectedIndex', {
- get() {
- for (let i = 0; i < this.length; i++) {
- if (this[i].selected) {
- return i;
- }
- }
- return -1;
- },
- set() {}
- });
- }
-
- /**
- * Add a {@link VideoTrack} to the `VideoTrackList`.
- *
- * @param { import('./video-track').default } track
- * The VideoTrack to add to the list
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- if (track.selected) {
- disableOthers(this, track);
- }
- super.addTrack(track);
- // native tracks don't have this
- if (!track.addEventListener) {
- return;
- }
- track.selectedChange_ = () => {
- if (this.changing_) {
- return;
- }
- this.changing_ = true;
- disableOthers(this, track);
- this.changing_ = false;
- this.trigger('change');
- };
-
- /**
- * @listens VideoTrack#selectedchange
- * @fires TrackList#change
- */
- track.addEventListener('selectedchange', track.selectedChange_);
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
- if (rtrack.removeEventListener && rtrack.selectedChange_) {
- rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
- rtrack.selectedChange_ = null;
- }
- }
- }
-
- /**
- * @file text-track-list.js
- */
-
- /**
- * The current list of {@link TextTrack} for a media file.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
- * @extends TrackList
- */
- class TextTrackList extends TrackList {
- /**
- * Add a {@link TextTrack} to the `TextTrackList`
- *
- * @param { import('./text-track').default } track
- * The text track to add to the list.
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- super.addTrack(track);
- if (!this.queueChange_) {
- this.queueChange_ = () => this.queueTrigger('change');
- }
- if (!this.triggerSelectedlanguagechange) {
- this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
- }
-
- /**
- * @listens TextTrack#modechange
- * @fires TrackList#change
- */
- track.addEventListener('modechange', this.queueChange_);
- const nonLanguageTextTrackKind = ['metadata', 'chapters'];
- if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
- track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
- }
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
-
- // manually remove the event handlers we added
- if (rtrack.removeEventListener) {
- if (this.queueChange_) {
- rtrack.removeEventListener('modechange', this.queueChange_);
- }
- if (this.selectedlanguagechange_) {
- rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
- }
- }
- }
- }
-
- /**
- * @file html-track-element-list.js
- */
-
- /**
- * The current list of {@link HtmlTrackElement}s.
- */
- class HtmlTrackElementList {
- /**
- * Create an instance of this class.
- *
- * @param {HtmlTrackElement[]} [tracks=[]]
- * A list of `HtmlTrackElement` to instantiate the list with.
- */
- constructor(trackElements = []) {
- this.trackElements_ = [];
-
- /**
- * @memberof HtmlTrackElementList
- * @member {number} length
- * The current number of `Track`s in the this Trackist.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.trackElements_.length;
- }
- });
- for (let i = 0, length = trackElements.length; i < length; i++) {
- this.addTrackElement_(trackElements[i]);
- }
- }
-
- /**
- * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
- *
- * @param {HtmlTrackElement} trackElement
- * The track element to add to the list.
- *
- * @private
- */
- addTrackElement_(trackElement) {
- const index = this.trackElements_.length;
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.trackElements_[index];
- }
- });
- }
-
- // Do not add duplicate elements
- if (this.trackElements_.indexOf(trackElement) === -1) {
- this.trackElements_.push(trackElement);
- }
- }
-
- /**
- * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
- * {@link TextTrack}.
- *
- * @param {TextTrack} track
- * The track associated with a track element.
- *
- * @return {HtmlTrackElement|undefined}
- * The track element that was found or undefined.
- *
- * @private
- */
- getTrackElementByTrack_(track) {
- let trackElement_;
- for (let i = 0, length = this.trackElements_.length; i < length; i++) {
- if (track === this.trackElements_[i].track) {
- trackElement_ = this.trackElements_[i];
- break;
- }
- }
- return trackElement_;
- }
-
- /**
- * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
- *
- * @param {HtmlTrackElement} trackElement
- * The track element to remove from the list.
- *
- * @private
- */
- removeTrackElement_(trackElement) {
- for (let i = 0, length = this.trackElements_.length; i < length; i++) {
- if (trackElement === this.trackElements_[i]) {
- if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
- this.trackElements_[i].track.off();
- }
- if (typeof this.trackElements_[i].off === 'function') {
- this.trackElements_[i].off();
- }
- this.trackElements_.splice(i, 1);
- break;
- }
- }
- }
- }
-
- /**
- * @file text-track-cue-list.js
- */
-
- /**
- * @typedef {Object} TextTrackCueList~TextTrackCue
- *
- * @property {string} id
- * The unique id for this text track cue
- *
- * @property {number} startTime
- * The start time for this text track cue
- *
- * @property {number} endTime
- * The end time for this text track cue
- *
- * @property {boolean} pauseOnExit
- * Pause when the end time is reached if true.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
- */
-
- /**
- * A List of TextTrackCues.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
- */
- class TextTrackCueList {
- /**
- * Create an instance of this class..
- *
- * @param {Array} cues
- * A list of cues to be initialized with
- */
- constructor(cues) {
- TextTrackCueList.prototype.setCues_.call(this, cues);
-
- /**
- * @memberof TextTrackCueList
- * @member {number} length
- * The current number of `TextTrackCue`s in the TextTrackCueList.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.length_;
- }
- });
- }
-
- /**
- * A setter for cues in this list. Creates getters
- * an an index for the cues.
- *
- * @param {Array} cues
- * An array of cues to set
- *
- * @private
- */
- setCues_(cues) {
- const oldLength = this.length || 0;
- let i = 0;
- const l = cues.length;
- this.cues_ = cues;
- this.length_ = cues.length;
- const defineProp = function (index) {
- if (!('' + index in this)) {
- Object.defineProperty(this, '' + index, {
- get() {
- return this.cues_[index];
- }
- });
- }
- };
- if (oldLength < l) {
- i = oldLength;
- for (; i < l; i++) {
- defineProp.call(this, i);
- }
- }
- }
-
- /**
- * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
- *
- * @param {string} id
- * The id of the cue that should be searched for.
- *
- * @return {TextTrackCueList~TextTrackCue|null}
- * A single cue or null if none was found.
- */
- getCueById(id) {
- let result = null;
- for (let i = 0, l = this.length; i < l; i++) {
- const cue = this[i];
- if (cue.id === id) {
- result = cue;
- break;
- }
- }
- return result;
- }
- }
-
- /**
- * @file track-kinds.js
- */
-
- /**
- * All possible `VideoTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
- * @typedef VideoTrack~Kind
- * @enum
- */
- const VideoTrackKind = {
- alternative: 'alternative',
- captions: 'captions',
- main: 'main',
- sign: 'sign',
- subtitles: 'subtitles',
- commentary: 'commentary'
- };
-
- /**
- * All possible `AudioTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
- * @typedef AudioTrack~Kind
- * @enum
- */
- const AudioTrackKind = {
- 'alternative': 'alternative',
- 'descriptions': 'descriptions',
- 'main': 'main',
- 'main-desc': 'main-desc',
- 'translation': 'translation',
- 'commentary': 'commentary'
- };
-
- /**
- * All possible `TextTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
- * @typedef TextTrack~Kind
- * @enum
- */
- const TextTrackKind = {
- subtitles: 'subtitles',
- captions: 'captions',
- descriptions: 'descriptions',
- chapters: 'chapters',
- metadata: 'metadata'
- };
-
- /**
- * All possible `TextTrackMode`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
- * @typedef TextTrack~Mode
- * @enum
- */
- const TextTrackMode = {
- disabled: 'disabled',
- hidden: 'hidden',
- showing: 'showing'
- };
-
- /**
- * @file track.js
- */
-
- /**
- * A Track class that contains all of the common functionality for {@link AudioTrack},
- * {@link VideoTrack}, and {@link TextTrack}.
- *
- * > Note: This class should not be used directly
- *
- * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
- * @extends EventTarget
- * @abstract
- */
- class Track extends EventTarget {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {string} [options.kind='']
- * A valid kind for the track type you are creating.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @abstract
- */
- constructor(options = {}) {
- super();
- const trackProps = {
- id: options.id || 'vjs_track_' + newGUID(),
- kind: options.kind || '',
- language: options.language || ''
- };
- let label = options.label || '';
-
- /**
- * @memberof Track
- * @member {string} id
- * The id of this track. Cannot be changed after creation.
- * @instance
- *
- * @readonly
- */
-
- /**
- * @memberof Track
- * @member {string} kind
- * The kind of track that this is. Cannot be changed after creation.
- * @instance
- *
- * @readonly
- */
-
- /**
- * @memberof Track
- * @member {string} language
- * The two letter language code for this track. Cannot be changed after
- * creation.
- * @instance
- *
- * @readonly
- */
-
- for (const key in trackProps) {
- Object.defineProperty(this, key, {
- get() {
- return trackProps[key];
- },
- set() {}
- });
- }
-
- /**
- * @memberof Track
- * @member {string} label
- * The label of this track. Cannot be changed after creation.
- * @instance
- *
- * @fires Track#labelchange
- */
- Object.defineProperty(this, 'label', {
- get() {
- return label;
- },
- set(newLabel) {
- if (newLabel !== label) {
- label = newLabel;
-
- /**
- * An event that fires when label changes on this track.
- *
- * > Note: This is not part of the spec!
- *
- * @event Track#labelchange
- * @type {Event}
- */
- this.trigger('labelchange');
- }
- }
- });
- }
- }
-
- /**
- * @file url.js
- * @module url
- */
-
- /**
- * @typedef {Object} url:URLObject
- *
- * @property {string} protocol
- * The protocol of the url that was parsed.
- *
- * @property {string} hostname
- * The hostname of the url that was parsed.
- *
- * @property {string} port
- * The port of the url that was parsed.
- *
- * @property {string} pathname
- * The pathname of the url that was parsed.
- *
- * @property {string} search
- * The search query of the url that was parsed.
- *
- * @property {string} hash
- * The hash of the url that was parsed.
- *
- * @property {string} host
- * The host of the url that was parsed.
- */
-
- /**
- * Resolve and parse the elements of a URL.
- *
- * @function
- * @param {String} url
- * The url to parse
- *
- * @return {url:URLObject}
- * An object of url details
- */
- const parseUrl = function (url) {
- // This entire method can be replace with URL once we are able to drop IE11
-
- const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
-
- // add the url to an anchor and let the browser parse the URL
- const a = document.createElement('a');
- a.href = url;
-
- // Copy the specific URL properties to a new object
- // This is also needed for IE because the anchor loses its
- // properties when it's removed from the dom
- const details = {};
- for (let i = 0; i < props.length; i++) {
- details[props[i]] = a[props[i]];
- }
-
- // IE adds the port to the host property unlike everyone else. If
- // a port identifier is added for standard ports, strip it.
- if (details.protocol === 'http:') {
- details.host = details.host.replace(/:80$/, '');
- }
- if (details.protocol === 'https:') {
- details.host = details.host.replace(/:443$/, '');
- }
- if (!details.protocol) {
- details.protocol = window.location.protocol;
- }
-
- /* istanbul ignore if */
- if (!details.host) {
- details.host = window.location.host;
- }
- return details;
- };
-
- /**
- * Get absolute version of relative URL.
- *
- * @function
- * @param {string} url
- * URL to make absolute
- *
- * @return {string}
- * Absolute URL
- *
- * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
- */
- const getAbsoluteURL = function (url) {
- // Check if absolute URL
- if (!url.match(/^https?:\/\//)) {
- // Add the url to an anchor and let the browser parse it to convert to an absolute url
- const a = document.createElement('a');
- a.href = url;
- url = a.href;
- }
- return url;
- };
-
- /**
- * Returns the extension of the passed file name. It will return an empty string
- * if passed an invalid path.
- *
- * @function
- * @param {string} path
- * The fileName path like '/path/to/file.mp4'
- *
- * @return {string}
- * The extension in lower case or an empty string if no
- * extension could be found.
- */
- const getFileExtension = function (path) {
- if (typeof path === 'string') {
- const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
- const pathParts = splitPathRe.exec(path);
- if (pathParts) {
- return pathParts.pop().toLowerCase();
- }
- }
- return '';
- };
-
- /**
- * Returns whether the url passed is a cross domain request or not.
- *
- * @function
- * @param {string} url
- * The url to check.
- *
- * @param {Object} [winLoc]
- * the domain to check the url against, defaults to window.location
- *
- * @param {string} [winLoc.protocol]
- * The window location protocol defaults to window.location.protocol
- *
- * @param {string} [winLoc.host]
- * The window location host defaults to window.location.host
- *
- * @return {boolean}
- * Whether it is a cross domain request or not.
- */
- const isCrossOrigin = function (url, winLoc = window.location) {
- const urlInfo = parseUrl(url);
-
- // IE8 protocol relative urls will return ':' for protocol
- const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
-
- // Check if url is for another domain/origin
- // IE8 doesn't know location.origin, so we won't rely on it here
- const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
- return crossOrigin;
- };
-
- var Url = /*#__PURE__*/Object.freeze({
- __proto__: null,
- parseUrl: parseUrl,
- getAbsoluteURL: getAbsoluteURL,
- getFileExtension: getFileExtension,
- isCrossOrigin: isCrossOrigin
- });
-
- var win;
- if (typeof window !== "undefined") {
- win = window;
- } else if (typeof commonjsGlobal !== "undefined") {
- win = commonjsGlobal;
- } else if (typeof self !== "undefined") {
- win = self;
- } else {
- win = {};
- }
- var window_1 = win;
-
- var _extends_1 = createCommonjsModule(function (module) {
- function _extends() {
- module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
- return target;
- }, module.exports.__esModule = true, module.exports["default"] = module.exports;
- return _extends.apply(this, arguments);
- }
- module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports;
- });
- unwrapExports(_extends_1);
-
- var isFunction_1 = isFunction;
- var toString = Object.prototype.toString;
- function isFunction(fn) {
- if (!fn) {
- return false;
- }
- var string = toString.call(fn);
- return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && (
- // IE8 and below
- fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt);
- }
-
- var httpResponseHandler = function httpResponseHandler(callback, decodeResponseBody) {
- if (decodeResponseBody === void 0) {
- decodeResponseBody = false;
- }
- return function (err, response, responseBody) {
- // if the XHR failed, return that error
- if (err) {
- callback(err);
- return;
- } // if the HTTP status code is 4xx or 5xx, the request also failed
-
- if (response.statusCode >= 400 && response.statusCode <= 599) {
- var cause = responseBody;
- if (decodeResponseBody) {
- if (window_1.TextDecoder) {
- var charset = getCharset(response.headers && response.headers['content-type']);
- try {
- cause = new TextDecoder(charset).decode(responseBody);
- } catch (e) {}
- } else {
- cause = String.fromCharCode.apply(null, new Uint8Array(responseBody));
- }
- }
- callback({
- cause: cause
- });
- return;
- } // otherwise, request succeeded
-
- callback(null, responseBody);
- };
- };
- function getCharset(contentTypeHeader) {
- if (contentTypeHeader === void 0) {
- contentTypeHeader = '';
- }
- return contentTypeHeader.toLowerCase().split(';').reduce(function (charset, contentType) {
- var _contentType$split = contentType.split('='),
- type = _contentType$split[0],
- value = _contentType$split[1];
- if (type.trim() === 'charset') {
- return value.trim();
- }
- return charset;
- }, 'utf-8');
- }
- var httpHandler = httpResponseHandler;
-
- createXHR.httpHandler = httpHandler;
- /**
- * @license
- * slighly modified parse-headers 2.0.2
- * Copyright (c) 2014 David Björklund
- * Available under the MIT license
- *
- */
-
- var parseHeaders = function parseHeaders(headers) {
- var result = {};
- if (!headers) {
- return result;
- }
- headers.trim().split('\n').forEach(function (row) {
- var index = row.indexOf(':');
- var key = row.slice(0, index).trim().toLowerCase();
- var value = row.slice(index + 1).trim();
- if (typeof result[key] === 'undefined') {
- result[key] = value;
- } else if (Array.isArray(result[key])) {
- result[key].push(value);
- } else {
- result[key] = [result[key], value];
- }
- });
- return result;
- };
- var lib = createXHR; // Allow use of default import syntax in TypeScript
-
- var default_1 = createXHR;
- createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop;
- createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest;
- forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) {
- createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) {
- options = initParams(uri, options, callback);
- options.method = method.toUpperCase();
- return _createXHR(options);
- };
- });
- function forEachArray(array, iterator) {
- for (var i = 0; i < array.length; i++) {
- iterator(array[i]);
- }
- }
- function isEmpty(obj) {
- for (var i in obj) {
- if (obj.hasOwnProperty(i)) return false;
- }
- return true;
- }
- function initParams(uri, options, callback) {
- var params = uri;
- if (isFunction_1(options)) {
- callback = options;
- if (typeof uri === "string") {
- params = {
- uri: uri
- };
- }
- } else {
- params = _extends_1({}, options, {
- uri: uri
- });
- }
- params.callback = callback;
- return params;
- }
- function createXHR(uri, options, callback) {
- options = initParams(uri, options, callback);
- return _createXHR(options);
- }
- function _createXHR(options) {
- if (typeof options.callback === "undefined") {
- throw new Error("callback argument missing");
- }
- var called = false;
- var callback = function cbOnce(err, response, body) {
- if (!called) {
- called = true;
- options.callback(err, response, body);
- }
- };
- function readystatechange() {
- if (xhr.readyState === 4) {
- setTimeout(loadFunc, 0);
- }
- }
- function getBody() {
- // Chrome with requestType=blob throws errors arround when even testing access to responseText
- var body = undefined;
- if (xhr.response) {
- body = xhr.response;
- } else {
- body = xhr.responseText || getXml(xhr);
- }
- if (isJson) {
- try {
- body = JSON.parse(body);
- } catch (e) {}
- }
- return body;
- }
- function errorFunc(evt) {
- clearTimeout(timeoutTimer);
- if (!(evt instanceof Error)) {
- evt = new Error("" + (evt || "Unknown XMLHttpRequest Error"));
- }
- evt.statusCode = 0;
- return callback(evt, failureResponse);
- } // will load the data & process the response in a special response object
-
- function loadFunc() {
- if (aborted) return;
- var status;
- clearTimeout(timeoutTimer);
- if (options.useXDR && xhr.status === undefined) {
- //IE8 CORS GET successful response doesn't have a status field, but body is fine
- status = 200;
- } else {
- status = xhr.status === 1223 ? 204 : xhr.status;
- }
- var response = failureResponse;
- var err = null;
- if (status !== 0) {
- response = {
- body: getBody(),
- statusCode: status,
- method: method,
- headers: {},
- url: uri,
- rawRequest: xhr
- };
- if (xhr.getAllResponseHeaders) {
- //remember xhr can in fact be XDR for CORS in IE
- response.headers = parseHeaders(xhr.getAllResponseHeaders());
- }
- } else {
- err = new Error("Internal XMLHttpRequest Error");
- }
- return callback(err, response, response.body);
- }
- var xhr = options.xhr || null;
- if (!xhr) {
- if (options.cors || options.useXDR) {
- xhr = new createXHR.XDomainRequest();
- } else {
- xhr = new createXHR.XMLHttpRequest();
- }
- }
- var key;
- var aborted;
- var uri = xhr.url = options.uri || options.url;
- var method = xhr.method = options.method || "GET";
- var body = options.body || options.data;
- var headers = xhr.headers = options.headers || {};
- var sync = !!options.sync;
- var isJson = false;
- var timeoutTimer;
- var failureResponse = {
- body: undefined,
- headers: {},
- statusCode: 0,
- method: method,
- url: uri,
- rawRequest: xhr
- };
- if ("json" in options && options.json !== false) {
- isJson = true;
- headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
-
- if (method !== "GET" && method !== "HEAD") {
- headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
-
- body = JSON.stringify(options.json === true ? body : options.json);
- }
- }
- xhr.onreadystatechange = readystatechange;
- xhr.onload = loadFunc;
- xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function.
-
- xhr.onprogress = function () {// IE must die
- };
- xhr.onabort = function () {
- aborted = true;
- };
- xhr.ontimeout = errorFunc;
- xhr.open(method, uri, !sync, options.username, options.password); //has to be after open
-
- if (!sync) {
- xhr.withCredentials = !!options.withCredentials;
- } // Cannot set timeout with sync request
- // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
- // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
-
- if (!sync && options.timeout > 0) {
- timeoutTimer = setTimeout(function () {
- if (aborted) return;
- aborted = true; //IE9 may still call readystatechange
-
- xhr.abort("timeout");
- var e = new Error("XMLHttpRequest timeout");
- e.code = "ETIMEDOUT";
- errorFunc(e);
- }, options.timeout);
- }
- if (xhr.setRequestHeader) {
- for (key in headers) {
- if (headers.hasOwnProperty(key)) {
- xhr.setRequestHeader(key, headers[key]);
- }
- }
- } else if (options.headers && !isEmpty(options.headers)) {
- throw new Error("Headers cannot be set on an XDomainRequest object");
- }
- if ("responseType" in options) {
- xhr.responseType = options.responseType;
- }
- if ("beforeSend" in options && typeof options.beforeSend === "function") {
- options.beforeSend(xhr);
- } // Microsoft Edge browser sends "undefined" when send is called with undefined value.
- // XMLHttpRequest spec says to pass null as body to indicate no body
- // See https://github.com/naugtur/xhr/issues/100.
-
- xhr.send(body || null);
- return xhr;
- }
- function getXml(xhr) {
- // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException"
- // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML.
- try {
- if (xhr.responseType === "document") {
- return xhr.responseXML;
- }
- var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
- if (xhr.responseType === "" && !firefoxBugTakenEffect) {
- return xhr.responseXML;
- }
- } catch (e) {}
- return null;
- }
- function noop() {}
- lib.default = default_1;
-
- /**
- * @file text-track.js
- */
-
- /**
- * Takes a webvtt file contents and parses it into cues
- *
- * @param {string} srcContent
- * webVTT file contents
- *
- * @param {TextTrack} track
- * TextTrack to add cues to. Cues come from the srcContent.
- *
- * @private
- */
- const parseCues = function (srcContent, track) {
- const parser = new window.WebVTT.Parser(window, window.vttjs, window.WebVTT.StringDecoder());
- const errors = [];
- parser.oncue = function (cue) {
- track.addCue(cue);
- };
- parser.onparsingerror = function (error) {
- errors.push(error);
- };
- parser.onflush = function () {
- track.trigger({
- type: 'loadeddata',
- target: track
- });
- };
- parser.parse(srcContent);
- if (errors.length > 0) {
- if (window.console && window.console.groupCollapsed) {
- window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
- }
- errors.forEach(error => log.error(error));
- if (window.console && window.console.groupEnd) {
- window.console.groupEnd();
- }
- }
- parser.flush();
- };
-
- /**
- * Load a `TextTrack` from a specified url.
- *
- * @param {string} src
- * Url to load track from.
- *
- * @param {TextTrack} track
- * Track to add cues to. Comes from the content at the end of `url`.
- *
- * @private
- */
- const loadTrack = function (src, track) {
- const opts = {
- uri: src
- };
- const crossOrigin = isCrossOrigin(src);
- if (crossOrigin) {
- opts.cors = crossOrigin;
- }
- const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
- if (withCredentials) {
- opts.withCredentials = withCredentials;
- }
- lib(opts, bind_(this, function (err, response, responseBody) {
- if (err) {
- return log.error(err, response);
- }
- track.loaded_ = true;
-
- // Make sure that vttjs has loaded, otherwise, wait till it finished loading
- // NOTE: this is only used for the alt/video.novtt.js build
- if (typeof window.WebVTT !== 'function') {
- if (track.tech_) {
- // to prevent use before define eslint error, we define loadHandler
- // as a let here
- track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
- if (event.type === 'vttjserror') {
- log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
- return;
- }
- return parseCues(responseBody, track);
- });
- }
- } else {
- parseCues(responseBody, track);
- }
- }));
- };
-
- /**
- * A representation of a single `TextTrack`.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
- * @extends Track
- */
- class TextTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} options={}
- * Object of option names and values
- *
- * @param { import('../tech/tech').default } options.tech
- * A reference to the tech that owns this TextTrack.
- *
- * @param {TextTrack~Kind} [options.kind='subtitles']
- * A valid text track kind.
- *
- * @param {TextTrack~Mode} [options.mode='disabled']
- * A valid text track mode.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this TextTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {string} [options.srclang='']
- * A valid two character language code. An alternative, but deprioritized
- * version of `options.language`
- *
- * @param {string} [options.src]
- * A url to TextTrack cues.
- *
- * @param {boolean} [options.default]
- * If this track should default to on or off.
- */
- constructor(options = {}) {
- if (!options.tech) {
- throw new Error('A tech was not provided.');
- }
- const settings = merge(options, {
- kind: TextTrackKind[options.kind] || 'subtitles',
- language: options.language || options.srclang || ''
- });
- let mode = TextTrackMode[settings.mode] || 'disabled';
- const default_ = settings.default;
- if (settings.kind === 'metadata' || settings.kind === 'chapters') {
- mode = 'hidden';
- }
- super(settings);
- this.tech_ = settings.tech;
- this.cues_ = [];
- this.activeCues_ = [];
- this.preload_ = this.tech_.preloadTextTracks !== false;
- const cues = new TextTrackCueList(this.cues_);
- const activeCues = new TextTrackCueList(this.activeCues_);
- let changed = false;
- this.timeupdateHandler = bind_(this, function (event = {}) {
- if (this.tech_.isDisposed()) {
- return;
- }
- if (!this.tech_.isReady_) {
- if (event.type !== 'timeupdate') {
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- }
- return;
- }
-
- // Accessing this.activeCues for the side-effects of updating itself
- // due to its nature as a getter function. Do not remove or cues will
- // stop updating!
- // Use the setter to prevent deletion from uglify (pure_getters rule)
- this.activeCues = this.activeCues;
- if (changed) {
- this.trigger('cuechange');
- changed = false;
- }
- if (event.type !== 'timeupdate') {
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- }
- });
- const disposeHandler = () => {
- this.stopTracking();
- };
- this.tech_.one('dispose', disposeHandler);
- if (mode !== 'disabled') {
- this.startTracking();
- }
- Object.defineProperties(this, {
- /**
- * @memberof TextTrack
- * @member {boolean} default
- * If this track was set to be on or off by default. Cannot be changed after
- * creation.
- * @instance
- *
- * @readonly
- */
- default: {
- get() {
- return default_;
- },
- set() {}
- },
- /**
- * @memberof TextTrack
- * @member {string} mode
- * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
- * not be set if setting to an invalid mode.
- * @instance
- *
- * @fires TextTrack#modechange
- */
- mode: {
- get() {
- return mode;
- },
- set(newMode) {
- if (!TextTrackMode[newMode]) {
- return;
- }
- if (mode === newMode) {
- return;
- }
- mode = newMode;
- if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
- // On-demand load.
- loadTrack(this.src, this);
- }
- this.stopTracking();
- if (mode !== 'disabled') {
- this.startTracking();
- }
- /**
- * An event that fires when mode changes on this track. This allows
- * the TextTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec!
- *
- * @event TextTrack#modechange
- * @type {Event}
- */
- this.trigger('modechange');
- }
- },
- /**
- * @memberof TextTrack
- * @member {TextTrackCueList} cues
- * The text track cue list for this TextTrack.
- * @instance
- */
- cues: {
- get() {
- if (!this.loaded_) {
- return null;
- }
- return cues;
- },
- set() {}
- },
- /**
- * @memberof TextTrack
- * @member {TextTrackCueList} activeCues
- * The list text track cues that are currently active for this TextTrack.
- * @instance
- */
- activeCues: {
- get() {
- if (!this.loaded_) {
- return null;
- }
-
- // nothing to do
- if (this.cues.length === 0) {
- return activeCues;
- }
- const ct = this.tech_.currentTime();
- const active = [];
- for (let i = 0, l = this.cues.length; i < l; i++) {
- const cue = this.cues[i];
- if (cue.startTime <= ct && cue.endTime >= ct) {
- active.push(cue);
- }
- }
- changed = false;
- if (active.length !== this.activeCues_.length) {
- changed = true;
- } else {
- for (let i = 0; i < active.length; i++) {
- if (this.activeCues_.indexOf(active[i]) === -1) {
- changed = true;
- }
- }
- }
- this.activeCues_ = active;
- activeCues.setCues_(this.activeCues_);
- return activeCues;
- },
- // /!\ Keep this setter empty (see the timeupdate handler above)
- set() {}
- }
- });
- if (settings.src) {
- this.src = settings.src;
- if (!this.preload_) {
- // Tracks will load on-demand.
- // Act like we're loaded for other purposes.
- this.loaded_ = true;
- }
- if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
- loadTrack(this.src, this);
- }
- } else {
- this.loaded_ = true;
- }
- }
- startTracking() {
- // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
- this.tech_.on('timeupdate', this.timeupdateHandler);
- }
- stopTracking() {
- if (this.rvf_) {
- this.tech_.cancelVideoFrameCallback(this.rvf_);
- this.rvf_ = undefined;
- }
- this.tech_.off('timeupdate', this.timeupdateHandler);
- }
-
- /**
- * Add a cue to the internal list of cues.
- *
- * @param {TextTrack~Cue} cue
- * The cue to add to our internal list
- */
- addCue(originalCue) {
- let cue = originalCue;
-
- // Testing if the cue is a VTTCue in a way that survives minification
- if (!('getCueAsHTML' in cue)) {
- cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
- for (const prop in originalCue) {
- if (!(prop in cue)) {
- cue[prop] = originalCue[prop];
- }
- }
-
- // make sure that `id` is copied over
- cue.id = originalCue.id;
- cue.originalCue_ = originalCue;
- }
- const tracks = this.tech_.textTracks();
- for (let i = 0; i < tracks.length; i++) {
- if (tracks[i] !== this) {
- tracks[i].removeCue(cue);
- }
- }
- this.cues_.push(cue);
- this.cues.setCues_(this.cues_);
- }
-
- /**
- * Remove a cue from our internal list
- *
- * @param {TextTrack~Cue} removeCue
- * The cue to remove from our internal list
- */
- removeCue(removeCue) {
- let i = this.cues_.length;
- while (i--) {
- const cue = this.cues_[i];
- if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
- this.cues_.splice(i, 1);
- this.cues.setCues_(this.cues_);
- break;
- }
- }
- }
- }
-
- /**
- * cuechange - One or more cues in the track have become active or stopped being active.
- * @protected
- */
- TextTrack.prototype.allowedEvents_ = {
- cuechange: 'cuechange'
- };
-
- /**
- * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
- * only one `AudioTrack` in the list will be enabled at a time.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
- * @extends Track
- */
- class AudioTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {AudioTrack~Kind} [options.kind='']
- * A valid audio track kind
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {boolean} [options.enabled]
- * If this track is the one that is currently playing. If this track is part of
- * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
- */
- constructor(options = {}) {
- const settings = merge(options, {
- kind: AudioTrackKind[options.kind] || ''
- });
- super(settings);
- let enabled = false;
-
- /**
- * @memberof AudioTrack
- * @member {boolean} enabled
- * If this `AudioTrack` is enabled or not. When setting this will
- * fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
- * @instance
- *
- * @fires VideoTrack#selectedchange
- */
- Object.defineProperty(this, 'enabled', {
- get() {
- return enabled;
- },
- set(newEnabled) {
- // an invalid or unchanged value
- if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
- return;
- }
- enabled = newEnabled;
-
- /**
- * An event that fires when enabled changes on this track. This allows
- * the AudioTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec! Native tracks will do
- * this internally without an event.
- *
- * @event AudioTrack#enabledchange
- * @type {Event}
- */
- this.trigger('enabledchange');
- }
- });
-
- // if the user sets this track to selected then
- // set selected to that true value otherwise
- // we keep it false
- if (settings.enabled) {
- this.enabled = settings.enabled;
- }
- this.loaded_ = true;
- }
- }
-
- /**
- * A representation of a single `VideoTrack`.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
- * @extends Track
- */
- class VideoTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {string} [options.kind='']
- * A valid {@link VideoTrack~Kind}
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {boolean} [options.selected]
- * If this track is the one that is currently playing.
- */
- constructor(options = {}) {
- const settings = merge(options, {
- kind: VideoTrackKind[options.kind] || ''
- });
- super(settings);
- let selected = false;
-
- /**
- * @memberof VideoTrack
- * @member {boolean} selected
- * If this `VideoTrack` is selected or not. When setting this will
- * fire {@link VideoTrack#selectedchange} if the state of selected changed.
- * @instance
- *
- * @fires VideoTrack#selectedchange
- */
- Object.defineProperty(this, 'selected', {
- get() {
- return selected;
- },
- set(newSelected) {
- // an invalid or unchanged value
- if (typeof newSelected !== 'boolean' || newSelected === selected) {
- return;
- }
- selected = newSelected;
-
- /**
- * An event that fires when selected changes on this track. This allows
- * the VideoTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec! Native tracks will do
- * this internally without an event.
- *
- * @event VideoTrack#selectedchange
- * @type {Event}
- */
- this.trigger('selectedchange');
- }
- });
-
- // if the user sets this track to selected then
- // set selected to that true value otherwise
- // we keep it false
- if (settings.selected) {
- this.selected = settings.selected;
- }
- }
- }
-
- /**
- * @file html-track-element.js
- */
-
- /**
- * A single track represented in the DOM.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
- * @extends EventTarget
- */
- class HTMLTrackElement extends EventTarget {
- /**
- * Create an instance of this class.
- *
- * @param {Object} options={}
- * Object of option names and values
- *
- * @param { import('../tech/tech').default } options.tech
- * A reference to the tech that owns this HTMLTrackElement.
- *
- * @param {TextTrack~Kind} [options.kind='subtitles']
- * A valid text track kind.
- *
- * @param {TextTrack~Mode} [options.mode='disabled']
- * A valid text track mode.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this TextTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {string} [options.srclang='']
- * A valid two character language code. An alternative, but deprioritized
- * version of `options.language`
- *
- * @param {string} [options.src]
- * A url to TextTrack cues.
- *
- * @param {boolean} [options.default]
- * If this track should default to on or off.
- */
- constructor(options = {}) {
- super();
- let readyState;
- const track = new TextTrack(options);
- this.kind = track.kind;
- this.src = track.src;
- this.srclang = track.language;
- this.label = track.label;
- this.default = track.default;
- Object.defineProperties(this, {
- /**
- * @memberof HTMLTrackElement
- * @member {HTMLTrackElement~ReadyState} readyState
- * The current ready state of the track element.
- * @instance
- */
- readyState: {
- get() {
- return readyState;
- }
- },
- /**
- * @memberof HTMLTrackElement
- * @member {TextTrack} track
- * The underlying TextTrack object.
- * @instance
- *
- */
- track: {
- get() {
- return track;
- }
- }
- });
- readyState = HTMLTrackElement.NONE;
-
- /**
- * @listens TextTrack#loadeddata
- * @fires HTMLTrackElement#load
- */
- track.addEventListener('loadeddata', () => {
- readyState = HTMLTrackElement.LOADED;
- this.trigger({
- type: 'load',
- target: this
- });
- });
- }
- }
-
- /**
- * @protected
- */
- HTMLTrackElement.prototype.allowedEvents_ = {
- load: 'load'
- };
-
- /**
- * The text track not loaded state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.NONE = 0;
-
- /**
- * The text track loading state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.LOADING = 1;
-
- /**
- * The text track loaded state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.LOADED = 2;
-
- /**
- * The text track failed to load state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.ERROR = 3;
-
- /*
- * This file contains all track properties that are used in
- * player.js, tech.js, html5.js and possibly other techs in the future.
- */
-
- const NORMAL = {
- audio: {
- ListClass: AudioTrackList,
- TrackClass: AudioTrack,
- capitalName: 'Audio'
- },
- video: {
- ListClass: VideoTrackList,
- TrackClass: VideoTrack,
- capitalName: 'Video'
- },
- text: {
- ListClass: TextTrackList,
- TrackClass: TextTrack,
- capitalName: 'Text'
- }
- };
- Object.keys(NORMAL).forEach(function (type) {
- NORMAL[type].getterName = `${type}Tracks`;
- NORMAL[type].privateName = `${type}Tracks_`;
- });
- const REMOTE = {
- remoteText: {
- ListClass: TextTrackList,
- TrackClass: TextTrack,
- capitalName: 'RemoteText',
- getterName: 'remoteTextTracks',
- privateName: 'remoteTextTracks_'
- },
- remoteTextEl: {
- ListClass: HtmlTrackElementList,
- TrackClass: HTMLTrackElement,
- capitalName: 'RemoteTextTrackEls',
- getterName: 'remoteTextTrackEls',
- privateName: 'remoteTextTrackEls_'
- }
- };
- const ALL = Object.assign({}, NORMAL, REMOTE);
- REMOTE.names = Object.keys(REMOTE);
- NORMAL.names = Object.keys(NORMAL);
- ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
-
- var minDoc = {};
-
- var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {};
- var doccy;
- if (typeof document !== 'undefined') {
- doccy = document;
- } else {
- doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
- if (!doccy) {
- doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
- }
- }
- var document_1 = doccy;
-
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
- /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-
- var _objCreate = Object.create || function () {
- function F() {}
- return function (o) {
- if (arguments.length !== 1) {
- throw new Error('Object.create shim only accepts one parameter.');
- }
- F.prototype = o;
- return new F();
- };
- }();
-
- // Creates a new ParserError object from an errorData object. The errorData
- // object should have default code and message properties. The default message
- // property can be overriden by passing in a message parameter.
- // See ParsingError.Errors below for acceptable errors.
- function ParsingError(errorData, message) {
- this.name = "ParsingError";
- this.code = errorData.code;
- this.message = message || errorData.message;
- }
- ParsingError.prototype = _objCreate(Error.prototype);
- ParsingError.prototype.constructor = ParsingError;
-
- // ParsingError metadata for acceptable ParsingErrors.
- ParsingError.Errors = {
- BadSignature: {
- code: 0,
- message: "Malformed WebVTT signature."
- },
- BadTimeStamp: {
- code: 1,
- message: "Malformed time stamp."
- }
- };
-
- // Try to parse input as a time stamp.
- function parseTimeStamp(input) {
- function computeSeconds(h, m, s, f) {
- return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
- }
- var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/);
- if (!m) {
- return null;
- }
- if (m[3]) {
- // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
- return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]);
- } else if (m[1] > 59) {
- // Timestamp takes the form of [hours]:[minutes].[milliseconds]
- // First position is hours as it's over 59.
- return computeSeconds(m[1], m[2], 0, m[4]);
- } else {
- // Timestamp takes the form of [minutes]:[seconds].[milliseconds]
- return computeSeconds(0, m[1], m[2], m[4]);
- }
- }
-
- // A settings object holds key/value pairs and will ignore anything but the first
- // assignment to a specific key.
- function Settings() {
- this.values = _objCreate(null);
- }
- Settings.prototype = {
- // Only accept the first assignment to any key.
- set: function (k, v) {
- if (!this.get(k) && v !== "") {
- this.values[k] = v;
- }
- },
- // Return the value for a key, or a default value.
- // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
- // a number of possible default values as properties where 'defaultKey' is
- // the key of the property that will be chosen; otherwise it's assumed to be
- // a single value.
- get: function (k, dflt, defaultKey) {
- if (defaultKey) {
- return this.has(k) ? this.values[k] : dflt[defaultKey];
- }
- return this.has(k) ? this.values[k] : dflt;
- },
- // Check whether we have a value for a key.
- has: function (k) {
- return k in this.values;
- },
- // Accept a setting if its one of the given alternatives.
- alt: function (k, v, a) {
- for (var n = 0; n < a.length; ++n) {
- if (v === a[n]) {
- this.set(k, v);
- break;
- }
- }
- },
- // Accept a setting if its a valid (signed) integer.
- integer: function (k, v) {
- if (/^-?\d+$/.test(v)) {
- // integer
- this.set(k, parseInt(v, 10));
- }
- },
- // Accept a setting if its a valid percentage.
- percent: function (k, v) {
- if (v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) {
- v = parseFloat(v);
- if (v >= 0 && v <= 100) {
- this.set(k, v);
- return true;
- }
- }
- return false;
- }
- };
-
- // Helper function to parse input into groups separated by 'groupDelim', and
- // interprete each group as a key/value pair separated by 'keyValueDelim'.
- function parseOptions(input, callback, keyValueDelim, groupDelim) {
- var groups = groupDelim ? input.split(groupDelim) : [input];
- for (var i in groups) {
- if (typeof groups[i] !== "string") {
- continue;
- }
- var kv = groups[i].split(keyValueDelim);
- if (kv.length !== 2) {
- continue;
- }
- var k = kv[0].trim();
- var v = kv[1].trim();
- callback(k, v);
- }
- }
- function parseCue(input, cue, regionList) {
- // Remember the original input if we need to throw an error.
- var oInput = input;
- // 4.1 WebVTT timestamp
- function consumeTimeStamp() {
- var ts = parseTimeStamp(input);
- if (ts === null) {
- throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput);
- }
- // Remove time stamp from input.
- input = input.replace(/^[^\sa-zA-Z-]+/, "");
- return ts;
- }
-
- // 4.4.2 WebVTT cue settings
- function consumeCueSettings(input, cue) {
- var settings = new Settings();
- parseOptions(input, function (k, v) {
- switch (k) {
- case "region":
- // Find the last region we parsed with the same region id.
- for (var i = regionList.length - 1; i >= 0; i--) {
- if (regionList[i].id === v) {
- settings.set(k, regionList[i].region);
- break;
- }
- }
- break;
- case "vertical":
- settings.alt(k, v, ["rl", "lr"]);
- break;
- case "line":
- var vals = v.split(","),
- vals0 = vals[0];
- settings.integer(k, vals0);
- settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
- settings.alt(k, vals0, ["auto"]);
- if (vals.length === 2) {
- settings.alt("lineAlign", vals[1], ["start", "center", "end"]);
- }
- break;
- case "position":
- vals = v.split(",");
- settings.percent(k, vals[0]);
- if (vals.length === 2) {
- settings.alt("positionAlign", vals[1], ["start", "center", "end"]);
- }
- break;
- case "size":
- settings.percent(k, v);
- break;
- case "align":
- settings.alt(k, v, ["start", "center", "end", "left", "right"]);
- break;
- }
- }, /:/, /\s/);
-
- // Apply default values for any missing fields.
- cue.region = settings.get("region", null);
- cue.vertical = settings.get("vertical", "");
- try {
- cue.line = settings.get("line", "auto");
- } catch (e) {}
- cue.lineAlign = settings.get("lineAlign", "start");
- cue.snapToLines = settings.get("snapToLines", true);
- cue.size = settings.get("size", 100);
- // Safari still uses the old middle value and won't accept center
- try {
- cue.align = settings.get("align", "center");
- } catch (e) {
- cue.align = settings.get("align", "middle");
- }
- try {
- cue.position = settings.get("position", "auto");
- } catch (e) {
- cue.position = settings.get("position", {
- start: 0,
- left: 0,
- center: 50,
- middle: 50,
- end: 100,
- right: 100
- }, cue.align);
- }
- cue.positionAlign = settings.get("positionAlign", {
- start: "start",
- left: "start",
- center: "center",
- middle: "center",
- end: "end",
- right: "end"
- }, cue.align);
- }
- function skipWhitespace() {
- input = input.replace(/^\s+/, "");
- }
-
- // 4.1 WebVTT cue timings.
- skipWhitespace();
- cue.startTime = consumeTimeStamp(); // (1) collect cue start time
- skipWhitespace();
- if (input.substr(0, 3) !== "-->") {
- // (3) next characters must match "-->"
- throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput);
- }
- input = input.substr(3);
- skipWhitespace();
- cue.endTime = consumeTimeStamp(); // (5) collect cue end time
-
- // 4.1 WebVTT cue settings list.
- skipWhitespace();
- consumeCueSettings(input, cue);
- }
-
- // When evaluating this file as part of a Webpack bundle for server
- // side rendering, `document` is an empty object.
- var TEXTAREA_ELEMENT = document_1.createElement && document_1.createElement("textarea");
- var TAG_NAME = {
- c: "span",
- i: "i",
- b: "b",
- u: "u",
- ruby: "ruby",
- rt: "rt",
- v: "span",
- lang: "span"
- };
-
- // 5.1 default text color
- // 5.2 default text background color is equivalent to text color with bg_ prefix
- var DEFAULT_COLOR_CLASS = {
- white: 'rgba(255,255,255,1)',
- lime: 'rgba(0,255,0,1)',
- cyan: 'rgba(0,255,255,1)',
- red: 'rgba(255,0,0,1)',
- yellow: 'rgba(255,255,0,1)',
- magenta: 'rgba(255,0,255,1)',
- blue: 'rgba(0,0,255,1)',
- black: 'rgba(0,0,0,1)'
- };
- var TAG_ANNOTATION = {
- v: "title",
- lang: "lang"
- };
- var NEEDS_PARENT = {
- rt: "ruby"
- };
-
- // Parse content into a document fragment.
- function parseContent(window, input) {
- function nextToken() {
- // Check for end-of-string.
- if (!input) {
- return null;
- }
-
- // Consume 'n' characters from the input.
- function consume(result) {
- input = input.substr(result.length);
- return result;
- }
- var m = input.match(/^([^<]*)(<[^>]*>?)?/);
- // If there is some text before the next tag, return it, otherwise return
- // the tag.
- return consume(m[1] ? m[1] : m[2]);
- }
- function unescape(s) {
- TEXTAREA_ELEMENT.innerHTML = s;
- s = TEXTAREA_ELEMENT.textContent;
- TEXTAREA_ELEMENT.textContent = "";
- return s;
- }
- function shouldAdd(current, element) {
- return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName;
- }
-
- // Create an element for this tag.
- function createElement(type, annotation) {
- var tagName = TAG_NAME[type];
- if (!tagName) {
- return null;
- }
- var element = window.document.createElement(tagName);
- var name = TAG_ANNOTATION[type];
- if (name && annotation) {
- element[name] = annotation.trim();
- }
- return element;
- }
- var rootDiv = window.document.createElement("div"),
- current = rootDiv,
- t,
- tagStack = [];
- while ((t = nextToken()) !== null) {
- if (t[0] === '<') {
- if (t[1] === "/") {
- // If the closing tag matches, move back up to the parent node.
- if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
- tagStack.pop();
- current = current.parentNode;
- }
- // Otherwise just ignore the end tag.
- continue;
- }
- var ts = parseTimeStamp(t.substr(1, t.length - 2));
- var node;
- if (ts) {
- // Timestamps are lead nodes as well.
- node = window.document.createProcessingInstruction("timestamp", ts);
- current.appendChild(node);
- continue;
- }
- var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
- // If we can't parse the tag, skip to the next tag.
- if (!m) {
- continue;
- }
- // Try to construct an element, and ignore the tag if we couldn't.
- node = createElement(m[1], m[3]);
- if (!node) {
- continue;
- }
- // Determine if the tag should be added based on the context of where it
- // is placed in the cuetext.
- if (!shouldAdd(current, node)) {
- continue;
- }
- // Set the class list (as a list of classes, separated by space).
- if (m[2]) {
- var classes = m[2].split('.');
- classes.forEach(function (cl) {
- var bgColor = /^bg_/.test(cl);
- // slice out `bg_` if it's a background color
- var colorName = bgColor ? cl.slice(3) : cl;
- if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) {
- var propName = bgColor ? 'background-color' : 'color';
- var propValue = DEFAULT_COLOR_CLASS[colorName];
- node.style[propName] = propValue;
- }
- });
- node.className = classes.join(' ');
- }
- // Append the node to the current node, and enter the scope of the new
- // node.
- tagStack.push(m[1]);
- current.appendChild(node);
- current = node;
- continue;
- }
-
- // Text nodes are leaf nodes.
- current.appendChild(window.document.createTextNode(unescape(t)));
- }
- return rootDiv;
- }
-
- // This is a list of all the Unicode characters that have a strong
- // right-to-left category. What this means is that these characters are
- // written right-to-left for sure. It was generated by pulling all the strong
- // right-to-left characters out of the Unicode data table. That table can
- // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
- var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6], [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d], [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6], [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5], [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815], [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858], [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f], [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c], [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1], [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc], [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808], [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855], [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f], [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13], [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58], [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72], [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f], [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32], [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42], [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f], [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59], [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62], [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77], [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b], [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]];
- function isStrongRTLChar(charCode) {
- for (var i = 0; i < strongRTLRanges.length; i++) {
- var currentRange = strongRTLRanges[i];
- if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
- return true;
- }
- }
- return false;
- }
- function determineBidi(cueDiv) {
- var nodeStack = [],
- text = "",
- charCode;
- if (!cueDiv || !cueDiv.childNodes) {
- return "ltr";
- }
- function pushNodes(nodeStack, node) {
- for (var i = node.childNodes.length - 1; i >= 0; i--) {
- nodeStack.push(node.childNodes[i]);
- }
- }
- function nextTextNode(nodeStack) {
- if (!nodeStack || !nodeStack.length) {
- return null;
- }
- var node = nodeStack.pop(),
- text = node.textContent || node.innerText;
- if (text) {
- // TODO: This should match all unicode type B characters (paragraph
- // separator characters). See issue #115.
- var m = text.match(/^.*(\n|\r)/);
- if (m) {
- nodeStack.length = 0;
- return m[0];
- }
- return text;
- }
- if (node.tagName === "ruby") {
- return nextTextNode(nodeStack);
- }
- if (node.childNodes) {
- pushNodes(nodeStack, node);
- return nextTextNode(nodeStack);
- }
- }
- pushNodes(nodeStack, cueDiv);
- while (text = nextTextNode(nodeStack)) {
- for (var i = 0; i < text.length; i++) {
- charCode = text.charCodeAt(i);
- if (isStrongRTLChar(charCode)) {
- return "rtl";
- }
- }
- }
- return "ltr";
- }
- function computeLinePos(cue) {
- if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) {
- return cue.line;
- }
- if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) {
- return -1;
- }
- var track = cue.track,
- trackList = track.textTrackList,
- count = 0;
- for (var i = 0; i < trackList.length && trackList[i] !== track; i++) {
- if (trackList[i].mode === "showing") {
- count++;
- }
- }
- return ++count * -1;
- }
- function StyleBox() {}
-
- // Apply styles to a div. If there is no div passed then it defaults to the
- // div on 'this'.
- StyleBox.prototype.applyStyles = function (styles, div) {
- div = div || this.div;
- for (var prop in styles) {
- if (styles.hasOwnProperty(prop)) {
- div.style[prop] = styles[prop];
- }
- }
- };
- StyleBox.prototype.formatStyle = function (val, unit) {
- return val === 0 ? 0 : val + unit;
- };
-
- // Constructs the computed display state of the cue (a div). Places the div
- // into the overlay which should be a block level element (usually a div).
- function CueStyleBox(window, cue, styleOptions) {
- StyleBox.call(this);
- this.cue = cue;
-
- // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
- // have inline positioning and will function as the cue background box.
- this.cueDiv = parseContent(window, cue.text);
- var styles = {
- color: "rgba(255, 255, 255, 1)",
- backgroundColor: "rgba(0, 0, 0, 0.8)",
- position: "relative",
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
- display: "inline",
- writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
- unicodeBidi: "plaintext"
- };
- this.applyStyles(styles, this.cueDiv);
-
- // Create an absolutely positioned div that will be used to position the cue
- // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
- // mirrors of them except middle instead of center on Safari.
- this.div = window.document.createElement("div");
- styles = {
- direction: determineBidi(this.cueDiv),
- writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
- unicodeBidi: "plaintext",
- textAlign: cue.align === "middle" ? "center" : cue.align,
- font: styleOptions.font,
- whiteSpace: "pre-line",
- position: "absolute"
- };
- this.applyStyles(styles);
- this.div.appendChild(this.cueDiv);
-
- // Calculate the distance from the reference edge of the viewport to the text
- // position of the cue box. The reference edge will be resolved later when
- // the box orientation styles are applied.
- var textPos = 0;
- switch (cue.positionAlign) {
- case "start":
- case "line-left":
- textPos = cue.position;
- break;
- case "center":
- textPos = cue.position - cue.size / 2;
- break;
- case "end":
- case "line-right":
- textPos = cue.position - cue.size;
- break;
- }
-
- // Horizontal box orientation; textPos is the distance from the left edge of the
- // area to the left edge of the box and cue.size is the distance extending to
- // the right from there.
- if (cue.vertical === "") {
- this.applyStyles({
- left: this.formatStyle(textPos, "%"),
- width: this.formatStyle(cue.size, "%")
- });
- // Vertical box orientation; textPos is the distance from the top edge of the
- // area to the top edge of the box and cue.size is the height extending
- // downwards from there.
- } else {
- this.applyStyles({
- top: this.formatStyle(textPos, "%"),
- height: this.formatStyle(cue.size, "%")
- });
- }
- this.move = function (box) {
- this.applyStyles({
- top: this.formatStyle(box.top, "px"),
- bottom: this.formatStyle(box.bottom, "px"),
- left: this.formatStyle(box.left, "px"),
- right: this.formatStyle(box.right, "px"),
- height: this.formatStyle(box.height, "px"),
- width: this.formatStyle(box.width, "px")
- });
- };
- }
- CueStyleBox.prototype = _objCreate(StyleBox.prototype);
- CueStyleBox.prototype.constructor = CueStyleBox;
-
- // Represents the co-ordinates of an Element in a way that we can easily
- // compute things with such as if it overlaps or intersects with another Element.
- // Can initialize it with either a StyleBox or another BoxPosition.
- function BoxPosition(obj) {
- // Either a BoxPosition was passed in and we need to copy it, or a StyleBox
- // was passed in and we need to copy the results of 'getBoundingClientRect'
- // as the object returned is readonly. All co-ordinate values are in reference
- // to the viewport origin (top left).
- var lh, height, width, top;
- if (obj.div) {
- height = obj.div.offsetHeight;
- width = obj.div.offsetWidth;
- top = obj.div.offsetTop;
- var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects();
- obj = obj.div.getBoundingClientRect();
- // In certain cases the outter div will be slightly larger then the sum of
- // the inner div's lines. This could be due to bold text, etc, on some platforms.
- // In this case we should get the average line height and use that. This will
- // result in the desired behaviour.
- lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0;
- }
- this.left = obj.left;
- this.right = obj.right;
- this.top = obj.top || top;
- this.height = obj.height || height;
- this.bottom = obj.bottom || top + (obj.height || height);
- this.width = obj.width || width;
- this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
- }
-
- // Move the box along a particular axis. Optionally pass in an amount to move
- // the box. If no amount is passed then the default is the line height of the
- // box.
- BoxPosition.prototype.move = function (axis, toMove) {
- toMove = toMove !== undefined ? toMove : this.lineHeight;
- switch (axis) {
- case "+x":
- this.left += toMove;
- this.right += toMove;
- break;
- case "-x":
- this.left -= toMove;
- this.right -= toMove;
- break;
- case "+y":
- this.top += toMove;
- this.bottom += toMove;
- break;
- case "-y":
- this.top -= toMove;
- this.bottom -= toMove;
- break;
- }
- };
-
- // Check if this box overlaps another box, b2.
- BoxPosition.prototype.overlaps = function (b2) {
- return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top;
- };
-
- // Check if this box overlaps any other boxes in boxes.
- BoxPosition.prototype.overlapsAny = function (boxes) {
- for (var i = 0; i < boxes.length; i++) {
- if (this.overlaps(boxes[i])) {
- return true;
- }
- }
- return false;
- };
-
- // Check if this box is within another box.
- BoxPosition.prototype.within = function (container) {
- return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right;
- };
-
- // Check if this box is entirely within the container or it is overlapping
- // on the edge opposite of the axis direction passed. For example, if "+x" is
- // passed and the box is overlapping on the left edge of the container, then
- // return true.
- BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) {
- switch (axis) {
- case "+x":
- return this.left < container.left;
- case "-x":
- return this.right > container.right;
- case "+y":
- return this.top < container.top;
- case "-y":
- return this.bottom > container.bottom;
- }
- };
-
- // Find the percentage of the area that this box is overlapping with another
- // box.
- BoxPosition.prototype.intersectPercentage = function (b2) {
- var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
- y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
- intersectArea = x * y;
- return intersectArea / (this.height * this.width);
- };
-
- // Convert the positions from this box to CSS compatible positions using
- // the reference container's positions. This has to be done because this
- // box's positions are in reference to the viewport origin, whereas, CSS
- // values are in referecne to their respective edges.
- BoxPosition.prototype.toCSSCompatValues = function (reference) {
- return {
- top: this.top - reference.top,
- bottom: reference.bottom - this.bottom,
- left: this.left - reference.left,
- right: reference.right - this.right,
- height: this.height,
- width: this.width
- };
- };
-
- // Get an object that represents the box's position without anything extra.
- // Can pass a StyleBox, HTMLElement, or another BoxPositon.
- BoxPosition.getSimpleBoxPosition = function (obj) {
- var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0;
- var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0;
- var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0;
- obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj;
- var ret = {
- left: obj.left,
- right: obj.right,
- top: obj.top || top,
- height: obj.height || height,
- bottom: obj.bottom || top + (obj.height || height),
- width: obj.width || width
- };
- return ret;
- };
-
- // Move a StyleBox to its specified, or next best, position. The containerBox
- // is the box that contains the StyleBox, such as a div. boxPositions are
- // a list of other boxes that the styleBox can't overlap with.
- function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
- // Find the best position for a cue box, b, on the video. The axis parameter
- // is a list of axis, the order of which, it will move the box along. For example:
- // Passing ["+x", "-x"] will move the box first along the x axis in the positive
- // direction. If it doesn't find a good position for it there it will then move
- // it along the x axis in the negative direction.
- function findBestPosition(b, axis) {
- var bestPosition,
- specifiedPosition = new BoxPosition(b),
- percentage = 1; // Highest possible so the first thing we get is better.
-
- for (var i = 0; i < axis.length; i++) {
- while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) {
- b.move(axis[i]);
- }
- // We found a spot where we aren't overlapping anything. This is our
- // best position.
- if (b.within(containerBox)) {
- return b;
- }
- var p = b.intersectPercentage(containerBox);
- // If we're outside the container box less then we were on our last try
- // then remember this position as the best position.
- if (percentage > p) {
- bestPosition = new BoxPosition(b);
- percentage = p;
- }
- // Reset the box position to the specified position.
- b = new BoxPosition(specifiedPosition);
- }
- return bestPosition || specifiedPosition;
- }
- var boxPosition = new BoxPosition(styleBox),
- cue = styleBox.cue,
- linePos = computeLinePos(cue),
- axis = [];
-
- // If we have a line number to align the cue to.
- if (cue.snapToLines) {
- var size;
- switch (cue.vertical) {
- case "":
- axis = ["+y", "-y"];
- size = "height";
- break;
- case "rl":
- axis = ["+x", "-x"];
- size = "width";
- break;
- case "lr":
- axis = ["-x", "+x"];
- size = "width";
- break;
- }
- var step = boxPosition.lineHeight,
- position = step * Math.round(linePos),
- maxPosition = containerBox[size] + step,
- initialAxis = axis[0];
-
- // If the specified intial position is greater then the max position then
- // clamp the box to the amount of steps it would take for the box to
- // reach the max position.
- if (Math.abs(position) > maxPosition) {
- position = position < 0 ? -1 : 1;
- position *= Math.ceil(maxPosition / step) * step;
- }
-
- // If computed line position returns negative then line numbers are
- // relative to the bottom of the video instead of the top. Therefore, we
- // need to increase our initial position by the length or width of the
- // video, depending on the writing direction, and reverse our axis directions.
- if (linePos < 0) {
- position += cue.vertical === "" ? containerBox.height : containerBox.width;
- axis = axis.reverse();
- }
-
- // Move the box to the specified position. This may not be its best
- // position.
- boxPosition.move(initialAxis, position);
- } else {
- // If we have a percentage line value for the cue.
- var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100;
- switch (cue.lineAlign) {
- case "center":
- linePos -= calculatedPercentage / 2;
- break;
- case "end":
- linePos -= calculatedPercentage;
- break;
- }
-
- // Apply initial line position to the cue box.
- switch (cue.vertical) {
- case "":
- styleBox.applyStyles({
- top: styleBox.formatStyle(linePos, "%")
- });
- break;
- case "rl":
- styleBox.applyStyles({
- left: styleBox.formatStyle(linePos, "%")
- });
- break;
- case "lr":
- styleBox.applyStyles({
- right: styleBox.formatStyle(linePos, "%")
- });
- break;
- }
- axis = ["+y", "-x", "+x", "-y"];
-
- // Get the box position again after we've applied the specified positioning
- // to it.
- boxPosition = new BoxPosition(styleBox);
- }
- var bestPosition = findBestPosition(boxPosition, axis);
- styleBox.move(bestPosition.toCSSCompatValues(containerBox));
- }
- function WebVTT$1() {
- // Nothing
- }
-
- // Helper to allow strings to be decoded instead of the default binary utf8 data.
- WebVTT$1.StringDecoder = function () {
- return {
- decode: function (data) {
- if (!data) {
- return "";
- }
- if (typeof data !== "string") {
- throw new Error("Error - expected string data.");
- }
- return decodeURIComponent(encodeURIComponent(data));
- }
- };
- };
- WebVTT$1.convertCueToDOMTree = function (window, cuetext) {
- if (!window || !cuetext) {
- return null;
- }
- return parseContent(window, cuetext);
- };
- var FONT_SIZE_PERCENT = 0.05;
- var FONT_STYLE = "sans-serif";
- var CUE_BACKGROUND_PADDING = "1.5%";
-
- // Runs the processing model over the cues and regions passed to it.
- // @param overlay A block level element (usually a div) that the computed cues
- // and regions will be placed into.
- WebVTT$1.processCues = function (window, cues, overlay) {
- if (!window || !cues || !overlay) {
- return null;
- }
-
- // Remove all previous children.
- while (overlay.firstChild) {
- overlay.removeChild(overlay.firstChild);
- }
- var paddedOverlay = window.document.createElement("div");
- paddedOverlay.style.position = "absolute";
- paddedOverlay.style.left = "0";
- paddedOverlay.style.right = "0";
- paddedOverlay.style.top = "0";
- paddedOverlay.style.bottom = "0";
- paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
- overlay.appendChild(paddedOverlay);
-
- // Determine if we need to compute the display states of the cues. This could
- // be the case if a cue's state has been changed since the last computation or
- // if it has not been computed yet.
- function shouldCompute(cues) {
- for (var i = 0; i < cues.length; i++) {
- if (cues[i].hasBeenReset || !cues[i].displayState) {
- return true;
- }
- }
- return false;
- }
-
- // We don't need to recompute the cues' display states. Just reuse them.
- if (!shouldCompute(cues)) {
- for (var i = 0; i < cues.length; i++) {
- paddedOverlay.appendChild(cues[i].displayState);
- }
- return;
- }
- var boxPositions = [],
- containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
- fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
- var styleOptions = {
- font: fontSize + "px " + FONT_STYLE
- };
- (function () {
- var styleBox, cue;
- for (var i = 0; i < cues.length; i++) {
- cue = cues[i];
-
- // Compute the intial position and styles of the cue div.
- styleBox = new CueStyleBox(window, cue, styleOptions);
- paddedOverlay.appendChild(styleBox.div);
-
- // Move the cue div to it's correct line position.
- moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
-
- // Remember the computed div so that we don't have to recompute it later
- // if we don't have too.
- cue.displayState = styleBox.div;
- boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
- }
- })();
- };
- WebVTT$1.Parser = function (window, vttjs, decoder) {
- if (!decoder) {
- decoder = vttjs;
- vttjs = {};
- }
- if (!vttjs) {
- vttjs = {};
- }
- this.window = window;
- this.vttjs = vttjs;
- this.state = "INITIAL";
- this.buffer = "";
- this.decoder = decoder || new TextDecoder("utf8");
- this.regionList = [];
- };
- WebVTT$1.Parser.prototype = {
- // If the error is a ParsingError then report it to the consumer if
- // possible. If it's not a ParsingError then throw it like normal.
- reportOrThrowError: function (e) {
- if (e instanceof ParsingError) {
- this.onparsingerror && this.onparsingerror(e);
- } else {
- throw e;
- }
- },
- parse: function (data) {
- var self = this;
-
- // If there is no data then we won't decode it, but will just try to parse
- // whatever is in buffer already. This may occur in circumstances, for
- // example when flush() is called.
- if (data) {
- // Try to decode the data that we received.
- self.buffer += self.decoder.decode(data, {
- stream: true
- });
- }
- function collectNextLine() {
- var buffer = self.buffer;
- var pos = 0;
- while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
- ++pos;
- }
- var line = buffer.substr(0, pos);
- // Advance the buffer early in case we fail below.
- if (buffer[pos] === '\r') {
- ++pos;
- }
- if (buffer[pos] === '\n') {
- ++pos;
- }
- self.buffer = buffer.substr(pos);
- return line;
- }
-
- // 3.4 WebVTT region and WebVTT region settings syntax
- function parseRegion(input) {
- var settings = new Settings();
- parseOptions(input, function (k, v) {
- switch (k) {
- case "id":
- settings.set(k, v);
- break;
- case "width":
- settings.percent(k, v);
- break;
- case "lines":
- settings.integer(k, v);
- break;
- case "regionanchor":
- case "viewportanchor":
- var xy = v.split(',');
- if (xy.length !== 2) {
- break;
- }
- // We have to make sure both x and y parse, so use a temporary
- // settings object here.
- var anchor = new Settings();
- anchor.percent("x", xy[0]);
- anchor.percent("y", xy[1]);
- if (!anchor.has("x") || !anchor.has("y")) {
- break;
- }
- settings.set(k + "X", anchor.get("x"));
- settings.set(k + "Y", anchor.get("y"));
- break;
- case "scroll":
- settings.alt(k, v, ["up"]);
- break;
- }
- }, /=/, /\s/);
-
- // Create the region, using default values for any values that were not
- // specified.
- if (settings.has("id")) {
- var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)();
- region.width = settings.get("width", 100);
- region.lines = settings.get("lines", 3);
- region.regionAnchorX = settings.get("regionanchorX", 0);
- region.regionAnchorY = settings.get("regionanchorY", 100);
- region.viewportAnchorX = settings.get("viewportanchorX", 0);
- region.viewportAnchorY = settings.get("viewportanchorY", 100);
- region.scroll = settings.get("scroll", "");
- // Register the region.
- self.onregion && self.onregion(region);
- // Remember the VTTRegion for later in case we parse any VTTCues that
- // reference it.
- self.regionList.push({
- id: settings.get("id"),
- region: region
- });
- }
- }
-
- // draft-pantos-http-live-streaming-20
- // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
- // 3.5 WebVTT
- function parseTimestampMap(input) {
- var settings = new Settings();
- parseOptions(input, function (k, v) {
- switch (k) {
- case "MPEGT":
- settings.integer(k + 'S', v);
- break;
- case "LOCA":
- settings.set(k + 'L', parseTimeStamp(v));
- break;
- }
- }, /[^\d]:/, /,/);
- self.ontimestampmap && self.ontimestampmap({
- "MPEGTS": settings.get("MPEGTS"),
- "LOCAL": settings.get("LOCAL")
- });
- }
-
- // 3.2 WebVTT metadata header syntax
- function parseHeader(input) {
- if (input.match(/X-TIMESTAMP-MAP/)) {
- // This line contains HLS X-TIMESTAMP-MAP metadata
- parseOptions(input, function (k, v) {
- switch (k) {
- case "X-TIMESTAMP-MAP":
- parseTimestampMap(v);
- break;
- }
- }, /=/);
- } else {
- parseOptions(input, function (k, v) {
- switch (k) {
- case "Region":
- // 3.3 WebVTT region metadata header syntax
- parseRegion(v);
- break;
- }
- }, /:/);
- }
- }
-
- // 5.1 WebVTT file parsing.
- try {
- var line;
- if (self.state === "INITIAL") {
- // We can't start parsing until we have the first line.
- if (!/\r\n|\n/.test(self.buffer)) {
- return this;
- }
- line = collectNextLine();
- var m = line.match(/^WEBVTT([ \t].*)?$/);
- if (!m || !m[0]) {
- throw new ParsingError(ParsingError.Errors.BadSignature);
- }
- self.state = "HEADER";
- }
- var alreadyCollectedLine = false;
- while (self.buffer) {
- // We can't parse a line until we have the full line.
- if (!/\r\n|\n/.test(self.buffer)) {
- return this;
- }
- if (!alreadyCollectedLine) {
- line = collectNextLine();
- } else {
- alreadyCollectedLine = false;
- }
- switch (self.state) {
- case "HEADER":
- // 13-18 - Allow a header (metadata) under the WEBVTT line.
- if (/:/.test(line)) {
- parseHeader(line);
- } else if (!line) {
- // An empty line terminates the header and starts the body (cues).
- self.state = "ID";
- }
- continue;
- case "NOTE":
- // Ignore NOTE blocks.
- if (!line) {
- self.state = "ID";
- }
- continue;
- case "ID":
- // Check for the start of NOTE blocks.
- if (/^NOTE($|[ \t])/.test(line)) {
- self.state = "NOTE";
- break;
- }
- // 19-29 - Allow any number of line terminators, then initialize new cue values.
- if (!line) {
- continue;
- }
- self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, "");
- // Safari still uses the old middle value and won't accept center
- try {
- self.cue.align = "center";
- } catch (e) {
- self.cue.align = "middle";
- }
- self.state = "CUE";
- // 30-39 - Check if self line contains an optional identifier or timing data.
- if (line.indexOf("-->") === -1) {
- self.cue.id = line;
- continue;
- }
- // Process line as start of a cue.
- /*falls through*/
- case "CUE":
- // 40 - Collect cue timings and settings.
- try {
- parseCue(line, self.cue, self.regionList);
- } catch (e) {
- self.reportOrThrowError(e);
- // In case of an error ignore rest of the cue.
- self.cue = null;
- self.state = "BADCUE";
- continue;
- }
- self.state = "CUETEXT";
- continue;
- case "CUETEXT":
- var hasSubstring = line.indexOf("-->") !== -1;
- // 34 - If we have an empty line then report the cue.
- // 35 - If we have the special substring '-->' then report the cue,
- // but do not collect the line as we need to process the current
- // one as a new cue.
- if (!line || hasSubstring && (alreadyCollectedLine = true)) {
- // We are done parsing self cue.
- self.oncue && self.oncue(self.cue);
- self.cue = null;
- self.state = "ID";
- continue;
- }
- if (self.cue.text) {
- self.cue.text += "\n";
- }
- self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n');
- continue;
- case "BADCUE":
- // BADCUE
- // 54-62 - Collect and discard the remaining cue.
- if (!line) {
- self.state = "ID";
- }
- continue;
- }
- }
- } catch (e) {
- self.reportOrThrowError(e);
-
- // If we are currently parsing a cue, report what we have.
- if (self.state === "CUETEXT" && self.cue && self.oncue) {
- self.oncue(self.cue);
- }
- self.cue = null;
- // Enter BADWEBVTT state if header was not parsed correctly otherwise
- // another exception occurred so enter BADCUE state.
- self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
- }
- return this;
- },
- flush: function () {
- var self = this;
- try {
- // Finish decoding the stream.
- self.buffer += self.decoder.decode();
- // Synthesize the end of the current cue or region.
- if (self.cue || self.state === "HEADER") {
- self.buffer += "\n\n";
- self.parse();
- }
- // If we've flushed, parsed, and we're still on the INITIAL state then
- // that means we don't have enough of the stream to parse the first
- // line.
- if (self.state === "INITIAL") {
- throw new ParsingError(ParsingError.Errors.BadSignature);
- }
- } catch (e) {
- self.reportOrThrowError(e);
- }
- self.onflush && self.onflush();
- return this;
- }
- };
- var vtt = WebVTT$1;
-
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- var autoKeyword = "auto";
- var directionSetting = {
- "": 1,
- "lr": 1,
- "rl": 1
- };
- var alignSetting = {
- "start": 1,
- "center": 1,
- "end": 1,
- "left": 1,
- "right": 1,
- "auto": 1,
- "line-left": 1,
- "line-right": 1
- };
- function findDirectionSetting(value) {
- if (typeof value !== "string") {
- return false;
- }
- var dir = directionSetting[value.toLowerCase()];
- return dir ? value.toLowerCase() : false;
- }
- function findAlignSetting(value) {
- if (typeof value !== "string") {
- return false;
- }
- var align = alignSetting[value.toLowerCase()];
- return align ? value.toLowerCase() : false;
- }
- function VTTCue(startTime, endTime, text) {
- /**
- * Shim implementation specific properties. These properties are not in
- * the spec.
- */
-
- // Lets us know when the VTTCue's data has changed in such a way that we need
- // to recompute its display state. This lets us compute its display state
- // lazily.
- this.hasBeenReset = false;
-
- /**
- * VTTCue and TextTrackCue properties
- * http://dev.w3.org/html5/webvtt/#vttcue-interface
- */
-
- var _id = "";
- var _pauseOnExit = false;
- var _startTime = startTime;
- var _endTime = endTime;
- var _text = text;
- var _region = null;
- var _vertical = "";
- var _snapToLines = true;
- var _line = "auto";
- var _lineAlign = "start";
- var _position = "auto";
- var _positionAlign = "auto";
- var _size = 100;
- var _align = "center";
- Object.defineProperties(this, {
- "id": {
- enumerable: true,
- get: function () {
- return _id;
- },
- set: function (value) {
- _id = "" + value;
- }
- },
- "pauseOnExit": {
- enumerable: true,
- get: function () {
- return _pauseOnExit;
- },
- set: function (value) {
- _pauseOnExit = !!value;
- }
- },
- "startTime": {
- enumerable: true,
- get: function () {
- return _startTime;
- },
- set: function (value) {
- if (typeof value !== "number") {
- throw new TypeError("Start time must be set to a number.");
- }
- _startTime = value;
- this.hasBeenReset = true;
- }
- },
- "endTime": {
- enumerable: true,
- get: function () {
- return _endTime;
- },
- set: function (value) {
- if (typeof value !== "number") {
- throw new TypeError("End time must be set to a number.");
- }
- _endTime = value;
- this.hasBeenReset = true;
- }
- },
- "text": {
- enumerable: true,
- get: function () {
- return _text;
- },
- set: function (value) {
- _text = "" + value;
- this.hasBeenReset = true;
- }
- },
- "region": {
- enumerable: true,
- get: function () {
- return _region;
- },
- set: function (value) {
- _region = value;
- this.hasBeenReset = true;
- }
- },
- "vertical": {
- enumerable: true,
- get: function () {
- return _vertical;
- },
- set: function (value) {
- var setting = findDirectionSetting(value);
- // Have to check for false because the setting an be an empty string.
- if (setting === false) {
- throw new SyntaxError("Vertical: an invalid or illegal direction string was specified.");
- }
- _vertical = setting;
- this.hasBeenReset = true;
- }
- },
- "snapToLines": {
- enumerable: true,
- get: function () {
- return _snapToLines;
- },
- set: function (value) {
- _snapToLines = !!value;
- this.hasBeenReset = true;
- }
- },
- "line": {
- enumerable: true,
- get: function () {
- return _line;
- },
- set: function (value) {
- if (typeof value !== "number" && value !== autoKeyword) {
- throw new SyntaxError("Line: an invalid number or illegal string was specified.");
- }
- _line = value;
- this.hasBeenReset = true;
- }
- },
- "lineAlign": {
- enumerable: true,
- get: function () {
- return _lineAlign;
- },
- set: function (value) {
- var setting = findAlignSetting(value);
- if (!setting) {
- console.warn("lineAlign: an invalid or illegal string was specified.");
- } else {
- _lineAlign = setting;
- this.hasBeenReset = true;
- }
- }
- },
- "position": {
- enumerable: true,
- get: function () {
- return _position;
- },
- set: function (value) {
- if (value < 0 || value > 100) {
- throw new Error("Position must be between 0 and 100.");
- }
- _position = value;
- this.hasBeenReset = true;
- }
- },
- "positionAlign": {
- enumerable: true,
- get: function () {
- return _positionAlign;
- },
- set: function (value) {
- var setting = findAlignSetting(value);
- if (!setting) {
- console.warn("positionAlign: an invalid or illegal string was specified.");
- } else {
- _positionAlign = setting;
- this.hasBeenReset = true;
- }
- }
- },
- "size": {
- enumerable: true,
- get: function () {
- return _size;
- },
- set: function (value) {
- if (value < 0 || value > 100) {
- throw new Error("Size must be between 0 and 100.");
- }
- _size = value;
- this.hasBeenReset = true;
- }
- },
- "align": {
- enumerable: true,
- get: function () {
- return _align;
- },
- set: function (value) {
- var setting = findAlignSetting(value);
- if (!setting) {
- throw new SyntaxError("align: an invalid or illegal alignment string was specified.");
- }
- _align = setting;
- this.hasBeenReset = true;
- }
- }
- });
-
- /**
- * Other spec defined properties
- */
-
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state
- this.displayState = undefined;
- }
-
- /**
- * VTTCue methods
- */
-
- VTTCue.prototype.getCueAsHTML = function () {
- // Assume WebVTT.convertCueToDOMTree is on the global.
- return WebVTT.convertCueToDOMTree(window, this.text);
- };
- var vttcue = VTTCue;
-
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- var scrollSetting = {
- "": true,
- "up": true
- };
- function findScrollSetting(value) {
- if (typeof value !== "string") {
- return false;
- }
- var scroll = scrollSetting[value.toLowerCase()];
- return scroll ? value.toLowerCase() : false;
- }
- function isValidPercentValue(value) {
- return typeof value === "number" && value >= 0 && value <= 100;
- }
-
- // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface
- function VTTRegion() {
- var _width = 100;
- var _lines = 3;
- var _regionAnchorX = 0;
- var _regionAnchorY = 100;
- var _viewportAnchorX = 0;
- var _viewportAnchorY = 100;
- var _scroll = "";
- Object.defineProperties(this, {
- "width": {
- enumerable: true,
- get: function () {
- return _width;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("Width must be between 0 and 100.");
- }
- _width = value;
- }
- },
- "lines": {
- enumerable: true,
- get: function () {
- return _lines;
- },
- set: function (value) {
- if (typeof value !== "number") {
- throw new TypeError("Lines must be set to a number.");
- }
- _lines = value;
- }
- },
- "regionAnchorY": {
- enumerable: true,
- get: function () {
- return _regionAnchorY;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("RegionAnchorX must be between 0 and 100.");
- }
- _regionAnchorY = value;
- }
- },
- "regionAnchorX": {
- enumerable: true,
- get: function () {
- return _regionAnchorX;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("RegionAnchorY must be between 0 and 100.");
- }
- _regionAnchorX = value;
- }
- },
- "viewportAnchorY": {
- enumerable: true,
- get: function () {
- return _viewportAnchorY;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("ViewportAnchorY must be between 0 and 100.");
- }
- _viewportAnchorY = value;
- }
- },
- "viewportAnchorX": {
- enumerable: true,
- get: function () {
- return _viewportAnchorX;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("ViewportAnchorX must be between 0 and 100.");
- }
- _viewportAnchorX = value;
- }
- },
- "scroll": {
- enumerable: true,
- get: function () {
- return _scroll;
- },
- set: function (value) {
- var setting = findScrollSetting(value);
- // Have to check for false as an empty string is a legal value.
- if (setting === false) {
- console.warn("Scroll: an invalid or illegal string was specified.");
- } else {
- _scroll = setting;
- }
- }
- }
- });
- }
- var vttregion = VTTRegion;
-
- var browserIndex = createCommonjsModule(function (module) {
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- // Default exports for Node. Export the extended versions of VTTCue and
- // VTTRegion in Node since we likely want the capability to convert back and
- // forth between JSON. If we don't then it's not that big of a deal since we're
- // off browser.
-
- var vttjs = module.exports = {
- WebVTT: vtt,
- VTTCue: vttcue,
- VTTRegion: vttregion
- };
- window_1.vttjs = vttjs;
- window_1.WebVTT = vttjs.WebVTT;
- var cueShim = vttjs.VTTCue;
- var regionShim = vttjs.VTTRegion;
- var nativeVTTCue = window_1.VTTCue;
- var nativeVTTRegion = window_1.VTTRegion;
- vttjs.shim = function () {
- window_1.VTTCue = cueShim;
- window_1.VTTRegion = regionShim;
- };
- vttjs.restore = function () {
- window_1.VTTCue = nativeVTTCue;
- window_1.VTTRegion = nativeVTTRegion;
- };
- if (!window_1.VTTCue) {
- vttjs.shim();
- }
- });
- browserIndex.WebVTT;
- browserIndex.VTTCue;
- browserIndex.VTTRegion;
-
- /**
- * @file tech.js
- */
-
- /**
- * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
- * that just contains the src url alone.
- * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
- * `var SourceString = 'http://example.com/some-video.mp4';`
- *
- * @typedef {Object|string} SourceObject
- *
- * @property {string} src
- * The url to the source
- *
- * @property {string} type
- * The mime type of the source
- */
-
- /**
- * A function used by {@link Tech} to create a new {@link TextTrack}.
- *
- * @private
- *
- * @param {Tech} self
- * An instance of the Tech class.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @param {Object} [options={}]
- * An object with additional text track options
- *
- * @return {TextTrack}
- * The text track that was created.
- */
- function createTrackHelper(self, kind, label, language, options = {}) {
- const tracks = self.textTracks();
- options.kind = kind;
- if (label) {
- options.label = label;
- }
- if (language) {
- options.language = language;
- }
- options.tech = self;
- const track = new ALL.text.TrackClass(options);
- tracks.addTrack(track);
- return track;
- }
-
- /**
- * This is the base class for media playback technology controllers, such as
- * {@link HTML5}
- *
- * @extends Component
- */
- class Tech extends Component {
- /**
- * Create an instance of this Tech.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * Callback function to call when the `HTML5` Tech is ready.
- */
- constructor(options = {}, ready = function () {}) {
- // we don't want the tech to report user activity automatically.
- // This is done manually in addControlsListeners
- options.reportTouchActivity = false;
- super(null, options, ready);
- this.onDurationChange_ = e => this.onDurationChange(e);
- this.trackProgress_ = e => this.trackProgress(e);
- this.trackCurrentTime_ = e => this.trackCurrentTime(e);
- this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
- this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
- this.queuedHanders_ = new Set();
-
- // keep track of whether the current source has played at all to
- // implement a very limited played()
- this.hasStarted_ = false;
- this.on('playing', function () {
- this.hasStarted_ = true;
- });
- this.on('loadstart', function () {
- this.hasStarted_ = false;
- });
- ALL.names.forEach(name => {
- const props = ALL[name];
- if (options && options[props.getterName]) {
- this[props.privateName] = options[props.getterName];
- }
- });
-
- // Manually track progress in cases where the browser/tech doesn't report it.
- if (!this.featuresProgressEvents) {
- this.manualProgressOn();
- }
-
- // Manually track timeupdates in cases where the browser/tech doesn't report it.
- if (!this.featuresTimeupdateEvents) {
- this.manualTimeUpdatesOn();
- }
- ['Text', 'Audio', 'Video'].forEach(track => {
- if (options[`native${track}Tracks`] === false) {
- this[`featuresNative${track}Tracks`] = false;
- }
- });
- if (options.nativeCaptions === false || options.nativeTextTracks === false) {
- this.featuresNativeTextTracks = false;
- } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
- this.featuresNativeTextTracks = true;
- }
- if (!this.featuresNativeTextTracks) {
- this.emulateTextTracks();
- }
- this.preloadTextTracks = options.preloadTextTracks !== false;
- this.autoRemoteTextTracks_ = new ALL.text.ListClass();
- this.initTrackListeners();
-
- // Turn on component tap events only if not using native controls
- if (!options.nativeControlsForTouch) {
- this.emitTapEvents();
- }
- if (this.constructor) {
- this.name_ = this.constructor.name || 'Unknown Tech';
- }
- }
-
- /**
- * A special function to trigger source set in a way that will allow player
- * to re-trigger if the player or tech are not ready yet.
- *
- * @fires Tech#sourceset
- * @param {string} src The source string at the time of the source changing.
- */
- triggerSourceset(src) {
- if (!this.isReady_) {
- // on initial ready we have to trigger source set
- // 1ms after ready so that player can watch for it.
- this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
- }
-
- /**
- * Fired when the source is set on the tech causing the media element
- * to reload.
- *
- * @see {@link Player#event:sourceset}
- * @event Tech#sourceset
- * @type {Event}
- */
- this.trigger({
- src,
- type: 'sourceset'
- });
- }
-
- /* Fallbacks for unsupported event types
- ================================================================================ */
-
- /**
- * Polyfill the `progress` event for browsers that don't support it natively.
- *
- * @see {@link Tech#trackProgress}
- */
- manualProgressOn() {
- this.on('durationchange', this.onDurationChange_);
- this.manualProgress = true;
-
- // Trigger progress watching when a source begins loading
- this.one('ready', this.trackProgress_);
- }
-
- /**
- * Turn off the polyfill for `progress` events that was created in
- * {@link Tech#manualProgressOn}
- */
- manualProgressOff() {
- this.manualProgress = false;
- this.stopTrackingProgress();
- this.off('durationchange', this.onDurationChange_);
- }
-
- /**
- * This is used to trigger a `progress` event when the buffered percent changes. It
- * sets an interval function that will be called every 500 milliseconds to check if the
- * buffer end percent has changed.
- *
- * > This function is called by {@link Tech#manualProgressOn}
- *
- * @param {Event} event
- * The `ready` event that caused this to run.
- *
- * @listens Tech#ready
- * @fires Tech#progress
- */
- trackProgress(event) {
- this.stopTrackingProgress();
- this.progressInterval = this.setInterval(bind_(this, function () {
- // Don't trigger unless buffered amount is greater than last time
-
- const numBufferedPercent = this.bufferedPercent();
- if (this.bufferedPercent_ !== numBufferedPercent) {
- /**
- * See {@link Player#progress}
- *
- * @event Tech#progress
- * @type {Event}
- */
- this.trigger('progress');
- }
- this.bufferedPercent_ = numBufferedPercent;
- if (numBufferedPercent === 1) {
- this.stopTrackingProgress();
- }
- }), 500);
- }
-
- /**
- * Update our internal duration on a `durationchange` event by calling
- * {@link Tech#duration}.
- *
- * @param {Event} event
- * The `durationchange` event that caused this to run.
- *
- * @listens Tech#durationchange
- */
- onDurationChange(event) {
- this.duration_ = this.duration();
- }
-
- /**
- * Get and create a `TimeRange` object for buffering.
- *
- * @return { import('../utils/time').TimeRange }
- * The time range object that was created.
- */
- buffered() {
- return createTimeRanges(0, 0);
- }
-
- /**
- * Get the percentage of the current video that is currently buffered.
- *
- * @return {number}
- * A number from 0 to 1 that represents the decimal percentage of the
- * video that is buffered.
- *
- */
- bufferedPercent() {
- return bufferedPercent(this.buffered(), this.duration_);
- }
-
- /**
- * Turn off the polyfill for `progress` events that was created in
- * {@link Tech#manualProgressOn}
- * Stop manually tracking progress events by clearing the interval that was set in
- * {@link Tech#trackProgress}.
- */
- stopTrackingProgress() {
- this.clearInterval(this.progressInterval);
- }
-
- /**
- * Polyfill the `timeupdate` event for browsers that don't support it.
- *
- * @see {@link Tech#trackCurrentTime}
- */
- manualTimeUpdatesOn() {
- this.manualTimeUpdates = true;
- this.on('play', this.trackCurrentTime_);
- this.on('pause', this.stopTrackingCurrentTime_);
- }
-
- /**
- * Turn off the polyfill for `timeupdate` events that was created in
- * {@link Tech#manualTimeUpdatesOn}
- */
- manualTimeUpdatesOff() {
- this.manualTimeUpdates = false;
- this.stopTrackingCurrentTime();
- this.off('play', this.trackCurrentTime_);
- this.off('pause', this.stopTrackingCurrentTime_);
- }
-
- /**
- * Sets up an interval function to track current time and trigger `timeupdate` every
- * 250 milliseconds.
- *
- * @listens Tech#play
- * @triggers Tech#timeupdate
- */
- trackCurrentTime() {
- if (this.currentTimeInterval) {
- this.stopTrackingCurrentTime();
- }
- this.currentTimeInterval = this.setInterval(function () {
- /**
- * Triggered at an interval of 250ms to indicated that time is passing in the video.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
-
- // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
- }, 250);
- }
-
- /**
- * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
- * `timeupdate` event is no longer triggered.
- *
- * @listens {Tech#pause}
- */
- stopTrackingCurrentTime() {
- this.clearInterval(this.currentTimeInterval);
-
- // #1002 - if the video ends right before the next timeupdate would happen,
- // the progress bar won't make it all the way to the end
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- }
-
- /**
- * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
- * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
- *
- * @fires Component#dispose
- */
- dispose() {
- // clear out all tracks because we can't reuse them between techs
- this.clearTracks(NORMAL.names);
-
- // Turn off any manual progress or timeupdate tracking
- if (this.manualProgress) {
- this.manualProgressOff();
- }
- if (this.manualTimeUpdates) {
- this.manualTimeUpdatesOff();
- }
- super.dispose();
- }
-
- /**
- * Clear out a single `TrackList` or an array of `TrackLists` given their names.
- *
- * > Note: Techs without source handlers should call this between sources for `video`
- * & `audio` tracks. You don't want to use them between tracks!
- *
- * @param {string[]|string} types
- * TrackList names to clear, valid names are `video`, `audio`, and
- * `text`.
- */
- clearTracks(types) {
- types = [].concat(types);
- // clear out all tracks because we can't reuse them between techs
- types.forEach(type => {
- const list = this[`${type}Tracks`]() || [];
- let i = list.length;
- while (i--) {
- const track = list[i];
- if (type === 'text') {
- this.removeRemoteTextTrack(track);
- }
- list.removeTrack(track);
- }
- });
- }
-
- /**
- * Remove any TextTracks added via addRemoteTextTrack that are
- * flagged for automatic garbage collection
- */
- cleanupAutoTextTracks() {
- const list = this.autoRemoteTextTracks_ || [];
- let i = list.length;
- while (i--) {
- const track = list[i];
- this.removeRemoteTextTrack(track);
- }
- }
-
- /**
- * Reset the tech, which will removes all sources and reset the internal readyState.
- *
- * @abstract
- */
- reset() {}
-
- /**
- * Get the value of `crossOrigin` from the tech.
- *
- * @abstract
- *
- * @see {Html5#crossOrigin}
- */
- crossOrigin() {}
-
- /**
- * Set the value of `crossOrigin` on the tech.
- *
- * @abstract
- *
- * @param {string} crossOrigin the crossOrigin value
- * @see {Html5#setCrossOrigin}
- */
- setCrossOrigin() {}
-
- /**
- * Get or set an error on the Tech.
- *
- * @param {MediaError} [err]
- * Error to set on the Tech
- *
- * @return {MediaError|null}
- * The current error object on the tech, or null if there isn't one.
- */
- error(err) {
- if (err !== undefined) {
- this.error_ = new MediaError(err);
- this.trigger('error');
- }
- return this.error_;
- }
-
- /**
- * Returns the `TimeRange`s that have been played through for the current source.
- *
- * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
- * It only checks whether the source has played at all or not.
- *
- * @return { import('../utils/time').TimeRange }
- * - A single time range if this video has played
- * - An empty set of ranges if not.
- */
- played() {
- if (this.hasStarted_) {
- return createTimeRanges(0, 0);
- }
- return createTimeRanges();
- }
-
- /**
- * Start playback
- *
- * @abstract
- *
- * @see {Html5#play}
- */
- play() {}
-
- /**
- * Set whether we are scrubbing or not
- *
- * @abstract
- * @param {boolean} _isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- *
- * @see {Html5#setScrubbing}
- */
- setScrubbing(_isScrubbing) {}
-
- /**
- * Get whether we are scrubbing or not
- *
- * @abstract
- *
- * @see {Html5#scrubbing}
- */
- scrubbing() {}
-
- /**
- * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
- * previously called.
- *
- * @param {number} _seconds
- * Set the current time of the media to this.
- * @fires Tech#timeupdate
- */
- setCurrentTime(_seconds) {
- // improve the accuracy of manual timeupdates
- if (this.manualTimeUpdates) {
- /**
- * A manual `timeupdate` event.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- }
- }
-
- /**
- * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
- * {@link TextTrackList} events.
- *
- * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
- *
- * @fires Tech#audiotrackchange
- * @fires Tech#videotrackchange
- * @fires Tech#texttrackchange
- */
- initTrackListeners() {
- /**
- * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
- *
- * @event Tech#audiotrackchange
- * @type {Event}
- */
-
- /**
- * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
- *
- * @event Tech#videotrackchange
- * @type {Event}
- */
-
- /**
- * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
- *
- * @event Tech#texttrackchange
- * @type {Event}
- */
- NORMAL.names.forEach(name => {
- const props = NORMAL[name];
- const trackListChanges = () => {
- this.trigger(`${name}trackchange`);
- };
- const tracks = this[props.getterName]();
- tracks.addEventListener('removetrack', trackListChanges);
- tracks.addEventListener('addtrack', trackListChanges);
- this.on('dispose', () => {
- tracks.removeEventListener('removetrack', trackListChanges);
- tracks.removeEventListener('addtrack', trackListChanges);
- });
- });
- }
-
- /**
- * Emulate TextTracks using vtt.js if necessary
- *
- * @fires Tech#vttjsloaded
- * @fires Tech#vttjserror
- */
- addWebVttScript_() {
- if (window.WebVTT) {
- return;
- }
-
- // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
- // signals that the Tech is ready at which point Tech.el_ is part of the DOM
- // before inserting the WebVTT script
- if (document.body.contains(this.el())) {
- // load via require if available and vtt.js script location was not passed in
- // as an option. novtt builds will turn the above require call into an empty object
- // which will cause this if check to always fail.
- if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) {
- this.trigger('vttjsloaded');
- return;
- }
-
- // load vtt.js via the script location option or the cdn of no location was
- // passed in
- const script = document.createElement('script');
- script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
- script.onload = () => {
- /**
- * Fired when vtt.js is loaded.
- *
- * @event Tech#vttjsloaded
- * @type {Event}
- */
- this.trigger('vttjsloaded');
- };
- script.onerror = () => {
- /**
- * Fired when vtt.js was not loaded due to an error
- *
- * @event Tech#vttjsloaded
- * @type {Event}
- */
- this.trigger('vttjserror');
- };
- this.on('dispose', () => {
- script.onload = null;
- script.onerror = null;
- });
- // but have not loaded yet and we set it to true before the inject so that
- // we don't overwrite the injected window.WebVTT if it loads right away
- window.WebVTT = true;
- this.el().parentNode.appendChild(script);
- } else {
- this.ready(this.addWebVttScript_);
- }
- }
-
- /**
- * Emulate texttracks
- *
- */
- emulateTextTracks() {
- const tracks = this.textTracks();
- const remoteTracks = this.remoteTextTracks();
- const handleAddTrack = e => tracks.addTrack(e.track);
- const handleRemoveTrack = e => tracks.removeTrack(e.track);
- remoteTracks.on('addtrack', handleAddTrack);
- remoteTracks.on('removetrack', handleRemoveTrack);
- this.addWebVttScript_();
- const updateDisplay = () => this.trigger('texttrackchange');
- const textTracksChanges = () => {
- updateDisplay();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- track.removeEventListener('cuechange', updateDisplay);
- if (track.mode === 'showing') {
- track.addEventListener('cuechange', updateDisplay);
- }
- }
- };
- textTracksChanges();
- tracks.addEventListener('change', textTracksChanges);
- tracks.addEventListener('addtrack', textTracksChanges);
- tracks.addEventListener('removetrack', textTracksChanges);
- this.on('dispose', function () {
- remoteTracks.off('addtrack', handleAddTrack);
- remoteTracks.off('removetrack', handleRemoveTrack);
- tracks.removeEventListener('change', textTracksChanges);
- tracks.removeEventListener('addtrack', textTracksChanges);
- tracks.removeEventListener('removetrack', textTracksChanges);
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- track.removeEventListener('cuechange', updateDisplay);
- }
- });
- }
-
- /**
- * Create and returns a remote {@link TextTrack} object.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @return {TextTrack}
- * The TextTrack that gets created.
- */
- addTextTrack(kind, label, language) {
- if (!kind) {
- throw new Error('TextTrack kind is required but was not provided');
- }
- return createTrackHelper(this, kind, label, language);
- }
-
- /**
- * Create an emulated TextTrack for use by addRemoteTextTrack
- *
- * This is intended to be overridden by classes that inherit from
- * Tech in order to create native or custom TextTracks.
- *
- * @param {Object} options
- * The object should contain the options to initialize the TextTrack with.
- *
- * @param {string} [options.kind]
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
- *
- * @param {string} [options.label].
- * Label to identify the text track
- *
- * @param {string} [options.language]
- * Two letter language abbreviation.
- *
- * @return {HTMLTrackElement}
- * The track element that gets created.
- */
- createRemoteTextTrack(options) {
- const track = merge(options, {
- tech: this
- });
- return new REMOTE.remoteTextEl.TrackClass(track);
- }
-
- /**
- * Creates a remote text track object and returns an html track element.
- *
- * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
- *
- * @param {Object} options
- * See {@link Tech#createRemoteTextTrack} for more detailed properties.
- *
- * @param {boolean} [manualCleanup=false]
- * - When false: the TextTrack will be automatically removed from the video
- * element whenever the source changes
- * - When True: The TextTrack will have to be cleaned up manually
- *
- * @return {HTMLTrackElement}
- * An Html Track Element.
- *
- */
- addRemoteTextTrack(options = {}, manualCleanup) {
- const htmlTrackElement = this.createRemoteTextTrack(options);
- if (typeof manualCleanup !== 'boolean') {
- manualCleanup = false;
- }
-
- // store HTMLTrackElement and TextTrack to remote list
- this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
- this.remoteTextTracks().addTrack(htmlTrackElement.track);
- if (manualCleanup === false) {
- // create the TextTrackList if it doesn't exist
- this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
- }
- return htmlTrackElement;
- }
-
- /**
- * Remove a remote text track from the remote `TextTrackList`.
- *
- * @param {TextTrack} track
- * `TextTrack` to remove from the `TextTrackList`
- */
- removeRemoteTextTrack(track) {
- const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
-
- // remove HTMLTrackElement and TextTrack from remote list
- this.remoteTextTrackEls().removeTrackElement_(trackElement);
- this.remoteTextTracks().removeTrack(track);
- this.autoRemoteTextTracks_.removeTrack(track);
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object}
- * An object with supported media playback quality metrics
- *
- * @abstract
- */
- getVideoPlaybackQuality() {
- return {};
- }
-
- /**
- * Attempt to create a floating video window always on top of other windows
- * so that users may continue consuming media while they interact with other
- * content sites, or applications on their device.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @return {Promise|undefined}
- * A promise with a Picture-in-Picture window if the browser supports
- * Promises (or one was passed in as an option). It returns undefined
- * otherwise.
- *
- * @abstract
- */
- requestPictureInPicture() {
- return Promise.reject();
- }
-
- /**
- * A method to check for the value of the 'disablePictureInPicture' property.
- * Defaults to true, as it should be considered disabled if the tech does not support pip
- *
- * @abstract
- */
- disablePictureInPicture() {
- return true;
- }
-
- /**
- * A method to set or unset the 'disablePictureInPicture' property.
- *
- * @abstract
- */
- setDisablePictureInPicture() {}
-
- /**
- * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
- *
- * @param {function} cb
- * @return {number} request id
- */
- requestVideoFrameCallback(cb) {
- const id = newGUID();
- if (!this.isReady_ || this.paused()) {
- this.queuedHanders_.add(id);
- this.one('playing', () => {
- if (this.queuedHanders_.has(id)) {
- this.queuedHanders_.delete(id);
- cb();
- }
- });
- } else {
- this.requestNamedAnimationFrame(id, cb);
- }
- return id;
- }
-
- /**
- * A fallback implementation of cancelVideoFrameCallback
- *
- * @param {number} id id of callback to be cancelled
- */
- cancelVideoFrameCallback(id) {
- if (this.queuedHanders_.has(id)) {
- this.queuedHanders_.delete(id);
- } else {
- this.cancelNamedAnimationFrame(id);
- }
- }
-
- /**
- * A method to set a poster from a `Tech`.
- *
- * @abstract
- */
- setPoster() {}
-
- /**
- * A method to check for the presence of the 'playsinline' attribute.
- *
- * @abstract
- */
- playsinline() {}
-
- /**
- * A method to set or unset the 'playsinline' attribute.
- *
- * @abstract
- */
- setPlaysinline() {}
-
- /**
- * Attempt to force override of native audio tracks.
- *
- * @param {boolean} override - If set to true native audio will be overridden,
- * otherwise native audio will potentially be used.
- *
- * @abstract
- */
- overrideNativeAudioTracks(override) {}
-
- /**
- * Attempt to force override of native video tracks.
- *
- * @param {boolean} override - If set to true native video will be overridden,
- * otherwise native video will potentially be used.
- *
- * @abstract
- */
- overrideNativeVideoTracks(override) {}
-
- /**
- * Check if the tech can support the given mime-type.
- *
- * The base tech does not support any type, but source handlers might
- * overwrite this.
- *
- * @param {string} _type
- * The mimetype to check for support
- *
- * @return {string}
- * 'probably', 'maybe', or empty string
- *
- * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
- *
- * @abstract
- */
- canPlayType(_type) {
- return '';
- }
-
- /**
- * Check if the type is supported by this tech.
- *
- * The base tech does not support any type, but source handlers might
- * overwrite this.
- *
- * @param {string} _type
- * The media type to check
- * @return {string} Returns the native video element's response
- */
- static canPlayType(_type) {
- return '';
- }
-
- /**
- * Check if the tech can support the given source
- *
- * @param {Object} srcObj
- * The source object
- * @param {Object} options
- * The options passed to the tech
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- static canPlaySource(srcObj, options) {
- return Tech.canPlayType(srcObj.type);
- }
-
- /*
- * Return whether the argument is a Tech or not.
- * Can be passed either a Class like `Html5` or a instance like `player.tech_`
- *
- * @param {Object} component
- * The item to check
- *
- * @return {boolean}
- * Whether it is a tech or not
- * - True if it is a tech
- * - False if it is not
- */
- static isTech(component) {
- return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
- }
-
- /**
- * Registers a `Tech` into a shared list for videojs.
- *
- * @param {string} name
- * Name of the `Tech` to register.
- *
- * @param {Object} tech
- * The `Tech` class to register.
- */
- static registerTech(name, tech) {
- if (!Tech.techs_) {
- Tech.techs_ = {};
- }
- if (!Tech.isTech(tech)) {
- throw new Error(`Tech ${name} must be a Tech`);
- }
- if (!Tech.canPlayType) {
- throw new Error('Techs must have a static canPlayType method on them');
- }
- if (!Tech.canPlaySource) {
- throw new Error('Techs must have a static canPlaySource method on them');
- }
- name = toTitleCase(name);
- Tech.techs_[name] = tech;
- Tech.techs_[toLowerCase(name)] = tech;
- if (name !== 'Tech') {
- // camel case the techName for use in techOrder
- Tech.defaultTechOrder_.push(name);
- }
- return tech;
- }
-
- /**
- * Get a `Tech` from the shared list by name.
- *
- * @param {string} name
- * `camelCase` or `TitleCase` name of the Tech to get
- *
- * @return {Tech|undefined}
- * The `Tech` or undefined if there was no tech with the name requested.
- */
- static getTech(name) {
- if (!name) {
- return;
- }
- if (Tech.techs_ && Tech.techs_[name]) {
- return Tech.techs_[name];
- }
- name = toTitleCase(name);
- if (window && window.videojs && window.videojs[name]) {
- log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
- return window.videojs[name];
- }
- }
- }
-
- /**
- * Get the {@link VideoTrackList}
- *
- * @returns {VideoTrackList}
- * @method Tech.prototype.videoTracks
- */
-
- /**
- * Get the {@link AudioTrackList}
- *
- * @returns {AudioTrackList}
- * @method Tech.prototype.audioTracks
- */
-
- /**
- * Get the {@link TextTrackList}
- *
- * @returns {TextTrackList}
- * @method Tech.prototype.textTracks
- */
-
- /**
- * Get the remote element {@link TextTrackList}
- *
- * @returns {TextTrackList}
- * @method Tech.prototype.remoteTextTracks
- */
-
- /**
- * Get the remote element {@link HtmlTrackElementList}
- *
- * @returns {HtmlTrackElementList}
- * @method Tech.prototype.remoteTextTrackEls
- */
-
- ALL.names.forEach(function (name) {
- const props = ALL[name];
- Tech.prototype[props.getterName] = function () {
- this[props.privateName] = this[props.privateName] || new props.ListClass();
- return this[props.privateName];
- };
- });
-
- /**
- * List of associated text tracks
- *
- * @type {TextTrackList}
- * @private
- * @property Tech#textTracks_
- */
-
- /**
- * List of associated audio tracks.
- *
- * @type {AudioTrackList}
- * @private
- * @property Tech#audioTracks_
- */
-
- /**
- * List of associated video tracks.
- *
- * @type {VideoTrackList}
- * @private
- * @property Tech#videoTracks_
- */
-
- /**
- * Boolean indicating whether the `Tech` supports volume control.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresVolumeControl = true;
-
- /**
- * Boolean indicating whether the `Tech` supports muting volume.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresMuteControl = true;
-
- /**
- * Boolean indicating whether the `Tech` supports fullscreen resize control.
- * Resizing plugins using request fullscreen reloads the plugin
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresFullscreenResize = false;
-
- /**
- * Boolean indicating whether the `Tech` supports changing the speed at which the video
- * plays. Examples:
- * - Set player to play 2x (twice) as fast
- * - Set player to play 0.5x (half) as fast
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresPlaybackRate = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `progress` event.
- * This will be used to determine if {@link Tech#manualProgressOn} should be called.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresProgressEvents = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `sourceset` event.
- *
- * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
- * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
- * a new source.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresSourceset = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `timeupdate` event.
- * This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresTimeupdateEvents = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
- * This will help us integrate with native `TextTrack`s if the browser supports them.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresNativeTextTracks = false;
-
- /**
- * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresVideoFrameCallback = false;
-
- /**
- * A functional mixin for techs that want to use the Source Handler pattern.
- * Source handlers are scripts for handling specific formats.
- * The source handler pattern is used for adaptive formats (HLS, DASH) that
- * manually load video data and feed it into a Source Buffer (Media Source Extensions)
- * Example: `Tech.withSourceHandlers.call(MyTech);`
- *
- * @param {Tech} _Tech
- * The tech to add source handler functions to.
- *
- * @mixes Tech~SourceHandlerAdditions
- */
- Tech.withSourceHandlers = function (_Tech) {
- /**
- * Register a source handler
- *
- * @param {Function} handler
- * The source handler class
- *
- * @param {number} [index]
- * Register it at the following index
- */
- _Tech.registerSourceHandler = function (handler, index) {
- let handlers = _Tech.sourceHandlers;
- if (!handlers) {
- handlers = _Tech.sourceHandlers = [];
- }
- if (index === undefined) {
- // add to the end of the list
- index = handlers.length;
- }
- handlers.splice(index, 0, handler);
- };
-
- /**
- * Check if the tech can support the given type. Also checks the
- * Techs sourceHandlers.
- *
- * @param {string} type
- * The mimetype to check.
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- _Tech.canPlayType = function (type) {
- const handlers = _Tech.sourceHandlers || [];
- let can;
- for (let i = 0; i < handlers.length; i++) {
- can = handlers[i].canPlayType(type);
- if (can) {
- return can;
- }
- }
- return '';
- };
-
- /**
- * Returns the first source handler that supports the source.
- *
- * TODO: Answer question: should 'probably' be prioritized over 'maybe'
- *
- * @param {SourceObject} source
- * The source object
- *
- * @param {Object} options
- * The options passed to the tech
- *
- * @return {SourceHandler|null}
- * The first source handler that supports the source or null if
- * no SourceHandler supports the source
- */
- _Tech.selectSourceHandler = function (source, options) {
- const handlers = _Tech.sourceHandlers || [];
- let can;
- for (let i = 0; i < handlers.length; i++) {
- can = handlers[i].canHandleSource(source, options);
- if (can) {
- return handlers[i];
- }
- }
- return null;
- };
-
- /**
- * Check if the tech can support the given source.
- *
- * @param {SourceObject} srcObj
- * The source object
- *
- * @param {Object} options
- * The options passed to the tech
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- _Tech.canPlaySource = function (srcObj, options) {
- const sh = _Tech.selectSourceHandler(srcObj, options);
- if (sh) {
- return sh.canHandleSource(srcObj, options);
- }
- return '';
- };
-
- /**
- * When using a source handler, prefer its implementation of
- * any function normally provided by the tech.
- */
- const deferrable = ['seekable', 'seeking', 'duration'];
-
- /**
- * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
- * function if it exists, with a fallback to the Techs seekable function.
- *
- * @method _Tech.seekable
- */
-
- /**
- * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
- * function if it exists, otherwise it will fallback to the techs duration function.
- *
- * @method _Tech.duration
- */
-
- deferrable.forEach(function (fnName) {
- const originalFn = this[fnName];
- if (typeof originalFn !== 'function') {
- return;
- }
- this[fnName] = function () {
- if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
- return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
- }
- return originalFn.apply(this, arguments);
- };
- }, _Tech.prototype);
-
- /**
- * Create a function for setting the source using a source object
- * and source handlers.
- * Should never be called unless a source handler was found.
- *
- * @param {SourceObject} source
- * A source object with src and type keys
- */
- _Tech.prototype.setSource = function (source) {
- let sh = _Tech.selectSourceHandler(source, this.options_);
- if (!sh) {
- // Fall back to a native source handler when unsupported sources are
- // deliberately set
- if (_Tech.nativeSourceHandler) {
- sh = _Tech.nativeSourceHandler;
- } else {
- log.error('No source handler found for the current source.');
- }
- }
-
- // Dispose any existing source handler
- this.disposeSourceHandler();
- this.off('dispose', this.disposeSourceHandler_);
- if (sh !== _Tech.nativeSourceHandler) {
- this.currentSource_ = source;
- }
- this.sourceHandler_ = sh.handleSource(source, this, this.options_);
- this.one('dispose', this.disposeSourceHandler_);
- };
-
- /**
- * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
- *
- * @listens Tech#dispose
- */
- _Tech.prototype.disposeSourceHandler = function () {
- // if we have a source and get another one
- // then we are loading something new
- // than clear all of our current tracks
- if (this.currentSource_) {
- this.clearTracks(['audio', 'video']);
- this.currentSource_ = null;
- }
-
- // always clean up auto-text tracks
- this.cleanupAutoTextTracks();
- if (this.sourceHandler_) {
- if (this.sourceHandler_.dispose) {
- this.sourceHandler_.dispose();
- }
- this.sourceHandler_ = null;
- }
- };
- };
-
- // The base Tech class needs to be registered as a Component. It is the only
- // Tech that can be registered as a Component.
- Component.registerComponent('Tech', Tech);
- Tech.registerTech('Tech', Tech);
-
- /**
- * A list of techs that should be added to techOrder on Players
- *
- * @private
- */
- Tech.defaultTechOrder_ = [];
-
- /**
- * @file middleware.js
- * @module middleware
- */
- const middlewares = {};
- const middlewareInstances = {};
- const TERMINATOR = {};
-
- /**
- * A middleware object is a plain JavaScript object that has methods that
- * match the {@link Tech} methods found in the lists of allowed
- * {@link module:middleware.allowedGetters|getters},
- * {@link module:middleware.allowedSetters|setters}, and
- * {@link module:middleware.allowedMediators|mediators}.
- *
- * @typedef {Object} MiddlewareObject
- */
-
- /**
- * A middleware factory function that should return a
- * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
- *
- * This factory will be called for each player when needed, with the player
- * passed in as an argument.
- *
- * @callback MiddlewareFactory
- * @param { import('../player').default } player
- * A Video.js player.
- */
-
- /**
- * Define a middleware that the player should use by way of a factory function
- * that returns a middleware object.
- *
- * @param {string} type
- * The MIME type to match or `"*"` for all MIME types.
- *
- * @param {MiddlewareFactory} middleware
- * A middleware factory function that will be executed for
- * matching types.
- */
- function use(type, middleware) {
- middlewares[type] = middlewares[type] || [];
- middlewares[type].push(middleware);
- }
-
- /**
- * Asynchronously sets a source using middleware by recursing through any
- * matching middlewares and calling `setSource` on each, passing along the
- * previous returned value each time.
- *
- * @param { import('../player').default } player
- * A {@link Player} instance.
- *
- * @param {Tech~SourceObject} src
- * A source object.
- *
- * @param {Function}
- * The next middleware to run.
- */
- function setSource(player, src, next) {
- player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
- }
-
- /**
- * When the tech is set, passes the tech to each middleware's `setTech` method.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * A Video.js tech.
- */
- function setTech(middleware, tech) {
- middleware.forEach(mw => mw.setTech && mw.setTech(tech));
- }
-
- /**
- * Calls a getter on the tech first, through each middleware
- * from right to left to the player.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @return {*}
- * The final value from the tech after middleware has intercepted it.
- */
- function get(middleware, tech, method) {
- return middleware.reduceRight(middlewareIterator(method), tech[method]());
- }
-
- /**
- * Takes the argument given to the player and calls the setter method on each
- * middleware from left to right to the tech.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @param {*} arg
- * The value to set on the tech.
- *
- * @return {*}
- * The return value of the `method` of the `tech`.
- */
- function set(middleware, tech, method, arg) {
- return tech[method](middleware.reduce(middlewareIterator(method), arg));
- }
-
- /**
- * Takes the argument given to the player and calls the `call` version of the
- * method on each middleware from left to right.
- *
- * Then, call the passed in method on the tech and return the result unchanged
- * back to the player, through middleware, this time from right to left.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @param {*} arg
- * The value to set on the tech.
- *
- * @return {*}
- * The return value of the `method` of the `tech`, regardless of the
- * return values of middlewares.
- */
- function mediate(middleware, tech, method, arg = null) {
- const callMethod = 'call' + toTitleCase(method);
- const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
- const terminated = middlewareValue === TERMINATOR;
- // deprecated. The `null` return value should instead return TERMINATOR to
- // prevent confusion if a techs method actually returns null.
- const returnValue = terminated ? null : tech[method](middlewareValue);
- executeRight(middleware, method, returnValue, terminated);
- return returnValue;
- }
-
- /**
- * Enumeration of allowed getters where the keys are method names.
- *
- * @type {Object}
- */
- const allowedGetters = {
- buffered: 1,
- currentTime: 1,
- duration: 1,
- muted: 1,
- played: 1,
- paused: 1,
- seekable: 1,
- volume: 1,
- ended: 1
- };
-
- /**
- * Enumeration of allowed setters where the keys are method names.
- *
- * @type {Object}
- */
- const allowedSetters = {
- setCurrentTime: 1,
- setMuted: 1,
- setVolume: 1
- };
-
- /**
- * Enumeration of allowed mediators where the keys are method names.
- *
- * @type {Object}
- */
- const allowedMediators = {
- play: 1,
- pause: 1
- };
- function middlewareIterator(method) {
- return (value, mw) => {
- // if the previous middleware terminated, pass along the termination
- if (value === TERMINATOR) {
- return TERMINATOR;
- }
- if (mw[method]) {
- return mw[method](value);
- }
- return value;
- };
- }
- function executeRight(mws, method, value, terminated) {
- for (let i = mws.length - 1; i >= 0; i--) {
- const mw = mws[i];
- if (mw[method]) {
- mw[method](terminated, value);
- }
- }
- }
-
- /**
- * Clear the middleware cache for a player.
- *
- * @param { import('../player').default } player
- * A {@link Player} instance.
- */
- function clearCacheForPlayer(player) {
- middlewareInstances[player.id()] = null;
- }
-
- /**
- * {
- * [playerId]: [[mwFactory, mwInstance], ...]
- * }
- *
- * @private
- */
- function getOrCreateFactory(player, mwFactory) {
- const mws = middlewareInstances[player.id()];
- let mw = null;
- if (mws === undefined || mws === null) {
- mw = mwFactory(player);
- middlewareInstances[player.id()] = [[mwFactory, mw]];
- return mw;
- }
- for (let i = 0; i < mws.length; i++) {
- const [mwf, mwi] = mws[i];
- if (mwf !== mwFactory) {
- continue;
- }
- mw = mwi;
- }
- if (mw === null) {
- mw = mwFactory(player);
- mws.push([mwFactory, mw]);
- }
- return mw;
- }
- function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
- const [mwFactory, ...mwrest] = middleware;
-
- // if mwFactory is a string, then we're at a fork in the road
- if (typeof mwFactory === 'string') {
- setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
-
- // if we have an mwFactory, call it with the player to get the mw,
- // then call the mw's setSource method
- } else if (mwFactory) {
- const mw = getOrCreateFactory(player, mwFactory);
-
- // if setSource isn't present, implicitly select this middleware
- if (!mw.setSource) {
- acc.push(mw);
- return setSourceHelper(src, mwrest, next, player, acc, lastRun);
- }
- mw.setSource(Object.assign({}, src), function (err, _src) {
- // something happened, try the next middleware on the current level
- // make sure to use the old src
- if (err) {
- return setSourceHelper(src, mwrest, next, player, acc, lastRun);
- }
-
- // we've succeeded, now we need to go deeper
- acc.push(mw);
-
- // if it's the same type, continue down the current chain
- // otherwise, we want to go down the new chain
- setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
- });
- } else if (mwrest.length) {
- setSourceHelper(src, mwrest, next, player, acc, lastRun);
- } else if (lastRun) {
- next(src, acc);
- } else {
- setSourceHelper(src, middlewares['*'], next, player, acc, true);
- }
- }
-
- /**
- * Mimetypes
- *
- * @see https://www.iana.org/assignments/media-types/media-types.xhtml
- * @typedef Mimetypes~Kind
- * @enum
- */
- const MimetypesKind = {
- opus: 'video/ogg',
- ogv: 'video/ogg',
- mp4: 'video/mp4',
- mov: 'video/mp4',
- m4v: 'video/mp4',
- mkv: 'video/x-matroska',
- m4a: 'audio/mp4',
- mp3: 'audio/mpeg',
- aac: 'audio/aac',
- caf: 'audio/x-caf',
- flac: 'audio/flac',
- oga: 'audio/ogg',
- wav: 'audio/wav',
- m3u8: 'application/x-mpegURL',
- mpd: 'application/dash+xml',
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- gif: 'image/gif',
- png: 'image/png',
- svg: 'image/svg+xml',
- webp: 'image/webp'
- };
-
- /**
- * Get the mimetype of a given src url if possible
- *
- * @param {string} src
- * The url to the src
- *
- * @return {string}
- * return the mimetype if it was known or empty string otherwise
- */
- const getMimetype = function (src = '') {
- const ext = getFileExtension(src);
- const mimetype = MimetypesKind[ext.toLowerCase()];
- return mimetype || '';
- };
-
- /**
- * Find the mime type of a given source string if possible. Uses the player
- * source cache.
- *
- * @param { import('../player').default } player
- * The player object
- *
- * @param {string} src
- * The source string
- *
- * @return {string}
- * The type that was found
- */
- const findMimetype = (player, src) => {
- if (!src) {
- return '';
- }
-
- // 1. check for the type in the `source` cache
- if (player.cache_.source.src === src && player.cache_.source.type) {
- return player.cache_.source.type;
- }
-
- // 2. see if we have this source in our `currentSources` cache
- const matchingSources = player.cache_.sources.filter(s => s.src === src);
- if (matchingSources.length) {
- return matchingSources[0].type;
- }
-
- // 3. look for the src url in source elements and use the type there
- const sources = player.$$('source');
- for (let i = 0; i < sources.length; i++) {
- const s = sources[i];
- if (s.type && s.src && s.src === src) {
- return s.type;
- }
- }
-
- // 4. finally fallback to our list of mime types based on src url extension
- return getMimetype(src);
- };
-
- /**
- * @module filter-source
- */
-
- /**
- * Filter out single bad source objects or multiple source objects in an
- * array. Also flattens nested source object arrays into a 1 dimensional
- * array of source objects.
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]} src
- * The src object to filter
- *
- * @return {Tech~SourceObject[]}
- * An array of sourceobjects containing only valid sources
- *
- * @private
- */
- const filterSource = function (src) {
- // traverse array
- if (Array.isArray(src)) {
- let newsrc = [];
- src.forEach(function (srcobj) {
- srcobj = filterSource(srcobj);
- if (Array.isArray(srcobj)) {
- newsrc = newsrc.concat(srcobj);
- } else if (isObject(srcobj)) {
- newsrc.push(srcobj);
- }
- });
- src = newsrc;
- } else if (typeof src === 'string' && src.trim()) {
- // convert string into object
- src = [fixSource({
- src
- })];
- } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
- // src is already valid
- src = [fixSource(src)];
- } else {
- // invalid source, turn it into an empty array
- src = [];
- }
- return src;
- };
-
- /**
- * Checks src mimetype, adding it when possible
- *
- * @param {Tech~SourceObject} src
- * The src object to check
- * @return {Tech~SourceObject}
- * src Object with known type
- */
- function fixSource(src) {
- if (!src.type) {
- const mimetype = getMimetype(src.src);
- if (mimetype) {
- src.type = mimetype;
- }
- }
- return src;
- }
-
- var icons = "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ";
-
- /**
- * @file loader.js
- */
-
- /**
- * The `MediaLoader` is the `Component` that decides which playback technology to load
- * when a player is initialized.
- *
- * @extends Component
- */
- class MediaLoader extends Component {
- /**
- * Create an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should attach to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function that is run when this component is ready.
- */
- constructor(player, options, ready) {
- // MediaLoader has no element
- const options_ = merge({
- createEl: false
- }, options);
- super(player, options_, ready);
-
- // If there are no sources when the player is initialized,
- // load the first supported playback technology.
-
- if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
- for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
- const techName = toTitleCase(j[i]);
- let tech = Tech.getTech(techName);
-
- // Support old behavior of techs being registered as components.
- // Remove once that deprecated behavior is removed.
- if (!techName) {
- tech = Component.getComponent(techName);
- }
-
- // Check if the browser supports this technology
- if (tech && tech.isSupported()) {
- player.loadTech_(techName);
- break;
- }
- }
- } else {
- // Loop through playback technologies (e.g. HTML5) and check for support.
- // Then load the best source.
- // A few assumptions here:
- // All playback technologies respect preload false.
- player.src(options.playerOptions.sources);
- }
- }
- }
- Component.registerComponent('MediaLoader', MediaLoader);
-
- /**
- * @file clickable-component.js
- */
-
- /**
- * Component which is clickable or keyboard actionable, but is not a
- * native HTML button.
- *
- * @extends Component
- */
- class ClickableComponent extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of component options.
- *
- * @param {function} [options.clickHandler]
- * The function to call when the button is clicked / activated
- *
- * @param {string} [options.controlText]
- * The text to set on the button
- *
- * @param {string} [options.className]
- * A class or space separated list of classes to add the component
- *
- */
- constructor(player, options) {
- super(player, options);
- if (this.options_.controlText) {
- this.controlText(this.options_.controlText);
- }
- this.handleMouseOver_ = e => this.handleMouseOver(e);
- this.handleMouseOut_ = e => this.handleMouseOut(e);
- this.handleClick_ = e => this.handleClick(e);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.emitTapEvents();
- this.enable();
- }
-
- /**
- * Create the `ClickableComponent`s DOM element.
- *
- * @param {string} [tag=div]
- * The element's node type.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element.
- *
- * @param {Object} [attributes={}]
- * An object of attributes that should be set on the element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tag = 'div', props = {}, attributes = {}) {
- props = Object.assign({
- className: this.buildCSSClass(),
- tabIndex: 0
- }, props);
- if (tag === 'button') {
- log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
- }
-
- // Add ARIA attributes for clickable element which is not a native HTML button
- attributes = Object.assign({
- role: 'button'
- }, attributes);
- this.tabIndex_ = props.tabIndex;
- const el = createEl(tag, props, attributes);
- if (!this.player_.options_.experimentalSvgIcons) {
- el.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- this.createControlTextEl(el);
- return el;
- }
- dispose() {
- // remove controlTextEl_ on dispose
- this.controlTextEl_ = null;
- super.dispose();
- }
-
- /**
- * Create a control text element on this `ClickableComponent`
- *
- * @param {Element} [el]
- * Parent element for the control text.
- *
- * @return {Element}
- * The control text element that gets created.
- */
- createControlTextEl(el) {
- this.controlTextEl_ = createEl('span', {
- className: 'vjs-control-text'
- }, {
- // let the screen reader user know that the text of the element may change
- 'aria-live': 'polite'
- });
- if (el) {
- el.appendChild(this.controlTextEl_);
- }
- this.controlText(this.controlText_, el);
- return this.controlTextEl_;
- }
-
- /**
- * Get or set the localize text to use for the controls on the `ClickableComponent`.
- *
- * @param {string} [text]
- * Control text for element.
- *
- * @param {Element} [el=this.el()]
- * Element to set the title on.
- *
- * @return {string}
- * - The control text when getting
- */
- controlText(text, el = this.el()) {
- if (text === undefined) {
- return this.controlText_ || 'Need Text';
- }
- const localizedText = this.localize(text);
-
- /** @protected */
- this.controlText_ = text;
- textContent(this.controlTextEl_, localizedText);
- if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
- // Set title attribute if only an icon is shown
- el.setAttribute('title', localizedText);
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-control vjs-button ${super.buildCSSClass()}`;
- }
-
- /**
- * Enable this `ClickableComponent`
- */
- enable() {
- if (!this.enabled_) {
- this.enabled_ = true;
- this.removeClass('vjs-disabled');
- this.el_.setAttribute('aria-disabled', 'false');
- if (typeof this.tabIndex_ !== 'undefined') {
- this.el_.setAttribute('tabIndex', this.tabIndex_);
- }
- this.on(['tap', 'click'], this.handleClick_);
- this.on('keydown', this.handleKeyDown_);
- }
- }
-
- /**
- * Disable this `ClickableComponent`
- */
- disable() {
- this.enabled_ = false;
- this.addClass('vjs-disabled');
- this.el_.setAttribute('aria-disabled', 'true');
- if (typeof this.tabIndex_ !== 'undefined') {
- this.el_.removeAttribute('tabIndex');
- }
- this.off('mouseover', this.handleMouseOver_);
- this.off('mouseout', this.handleMouseOut_);
- this.off(['tap', 'click'], this.handleClick_);
- this.off('keydown', this.handleKeyDown_);
- }
-
- /**
- * Handles language change in ClickableComponent for the player in components
- *
- *
- */
- handleLanguagechange() {
- this.controlText(this.controlText_);
- }
-
- /**
- * Event handler that is called when a `ClickableComponent` receives a
- * `click` or `tap` event.
- *
- * @param {Event} event
- * The `tap` or `click` event that caused this function to be called.
- *
- * @listens tap
- * @listens click
- * @abstract
- */
- handleClick(event) {
- if (this.options_.clickHandler) {
- this.options_.clickHandler.call(this, arguments);
- }
- }
-
- /**
- * Event handler that is called when a `ClickableComponent` receives a
- * `keydown` event.
- *
- * By default, if the key is Space or Enter, it will trigger a `click` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Support Space or Enter key operation to fire a click event. Also,
- // prevent the event from propagating through the DOM and triggering
- // Player hotkeys.
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.preventDefault();
- event.stopPropagation();
- this.trigger('click');
- } else {
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- }
- Component.registerComponent('ClickableComponent', ClickableComponent);
-
- /**
- * @file poster-image.js
- */
-
- /**
- * A `ClickableComponent` that handles showing the poster image for the player.
- *
- * @extends ClickableComponent
- */
- class PosterImage extends ClickableComponent {
- /**
- * Create an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should attach to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update();
- this.update_ = e => this.update(e);
- player.on('posterchange', this.update_);
- }
-
- /**
- * Clean up and dispose of the `PosterImage`.
- */
- dispose() {
- this.player().off('posterchange', this.update_);
- super.dispose();
- }
-
- /**
- * Create the `PosterImage`s DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- // The el is an empty div to keep position in the DOM
- // A picture and img el will be inserted when a source is set
- return createEl('div', {
- className: 'vjs-poster'
- });
- }
-
- /**
- * Get or set the `PosterImage`'s crossOrigin option.
- *
- * @param {string|null} [value]
- * The value to set the crossOrigin to. If an argument is
- * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
- *
- * @return {string|null}
- * - The current crossOrigin value of the `Player` when getting.
- * - undefined when setting
- */
- crossOrigin(value) {
- // `null` can be set to unset a value
- if (typeof value === 'undefined') {
- if (this.$('img')) {
- // If the poster's element exists, give its value
- return this.$('img').crossOrigin;
- } else if (this.player_.tech_ && this.player_.tech_.isReady_) {
- // If not but the tech is ready, query the tech
- return this.player_.crossOrigin();
- }
- // Otherwise check options as the poster is usually set before the state of crossorigin
- // can be retrieved by the getter
- return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
- }
- if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
- this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
- return;
- }
- if (this.$('img')) {
- this.$('img').crossOrigin = value;
- }
- return;
- }
-
- /**
- * An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
- *
- * @listens Player#posterchange
- *
- * @param {Event} [event]
- * The `Player#posterchange` event that triggered this function.
- */
- update(event) {
- const url = this.player().poster();
- this.setSrc(url);
-
- // If there's no poster source we should display:none on this component
- // so it's not still clickable or right-clickable
- if (url) {
- this.show();
- } else {
- this.hide();
- }
- }
-
- /**
- * Set the source of the `PosterImage` depending on the display method. (Re)creates
- * the inner picture and img elementss when needed.
- *
- * @param {string} [url]
- * The URL to the source for the `PosterImage`. If not specified or falsy,
- * any source and ant inner picture/img are removed.
- */
- setSrc(url) {
- if (!url) {
- this.el_.textContent = '';
- return;
- }
- if (!this.$('img')) {
- this.el_.appendChild(createEl('picture', {
- className: 'vjs-poster',
- // Don't want poster to be tabbable.
- tabIndex: -1
- }, {}, createEl('img', {
- loading: 'lazy',
- crossOrigin: this.crossOrigin()
- }, {
- alt: ''
- })));
- }
- this.$('img').src = url;
- }
-
- /**
- * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
- * {@link ClickableComponent#handleClick} for instances where this will be triggered.
- *
- * @listens tap
- * @listens click
- * @listens keydown
- *
- * @param {Event} event
- + The `click`, `tap` or `keydown` event that caused this function to be called.
- */
- handleClick(event) {
- // We don't want a click to trigger playback when controls are disabled
- if (!this.player_.controls()) {
- return;
- }
- if (this.player_.tech(true)) {
- this.player_.tech(true).focus();
- }
- if (this.player_.paused()) {
- silencePromise(this.player_.play());
- } else {
- this.player_.pause();
- }
- }
- }
-
- /**
- * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the ` ` tag to control the CORS
- * behavior.
- *
- * @param {string|null} [value]
- * The value to set the `PosterImages`'s crossorigin to. If an argument is
- * given, must be one of `anonymous` or `use-credentials`.
- *
- * @return {string|null|undefined}
- * - The current crossorigin value of the `Player` when getting.
- * - undefined when setting
- */
- PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
- Component.registerComponent('PosterImage', PosterImage);
-
- /**
- * @file text-track-display.js
- */
- const darkGray = '#222';
- const lightGray = '#ccc';
- const fontMap = {
- monospace: 'monospace',
- sansSerif: 'sans-serif',
- serif: 'serif',
- monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
- monospaceSerif: '"Courier New", monospace',
- proportionalSansSerif: 'sans-serif',
- proportionalSerif: 'serif',
- casual: '"Comic Sans MS", Impact, fantasy',
- script: '"Monotype Corsiva", cursive',
- smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
- };
-
- /**
- * Construct an rgba color from a given hex color code.
- *
- * @param {number} color
- * Hex number for color, like #f0e or #f604e2.
- *
- * @param {number} opacity
- * Value for opacity, 0.0 - 1.0.
- *
- * @return {string}
- * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
- */
- function constructColor(color, opacity) {
- let hex;
- if (color.length === 4) {
- // color looks like "#f0e"
- hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
- } else if (color.length === 7) {
- // color looks like "#f604e2"
- hex = color.slice(1);
- } else {
- throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
- }
- return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
- }
-
- /**
- * Try to update the style of a DOM element. Some style changes will throw an error,
- * particularly in IE8. Those should be noops.
- *
- * @param {Element} el
- * The DOM element to be styled.
- *
- * @param {string} style
- * The CSS property on the element that should be styled.
- *
- * @param {string} rule
- * The style rule that should be applied to the property.
- *
- * @private
- */
- function tryUpdateStyle(el, style, rule) {
- try {
- el.style[style] = rule;
- } catch (e) {
- // Satisfies linter.
- return;
- }
- }
-
- /**
- * Converts the CSS top/right/bottom/left property numeric value to string in pixels.
- *
- * @param {number} position
- * The CSS top/right/bottom/left property value.
- *
- * @return {string}
- * The CSS property value that was created, like '10px'.
- *
- * @private
- */
- function getCSSPositionValue(position) {
- return position ? `${position}px` : '';
- }
-
- /**
- * The component for displaying text track cues.
- *
- * @extends Component
- */
- class TextTrackDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when `TextTrackDisplay` is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- const updateDisplayTextHandler = e => this.updateDisplay(e);
- const updateDisplayHandler = e => {
- this.updateDisplayOverlay();
- this.updateDisplay(e);
- };
- player.on('loadstart', e => this.toggleDisplay(e));
- player.on('texttrackchange', updateDisplayTextHandler);
- player.on('loadedmetadata', e => {
- this.updateDisplayOverlay();
- this.preselectTrack(e);
- });
-
- // This used to be called during player init, but was causing an error
- // if a track should show by default and the display hadn't loaded yet.
- // Should probably be moved to an external track loader when we support
- // tracks that don't need a display.
- player.ready(bind_(this, function () {
- if (player.tech_ && player.tech_.featuresNativeTextTracks) {
- this.hide();
- return;
- }
- player.on('fullscreenchange', updateDisplayHandler);
- player.on('playerresize', updateDisplayHandler);
- const screenOrientation = window.screen.orientation || window;
- const changeOrientationEvent = window.screen.orientation ? 'change' : 'orientationchange';
- screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
- player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
- const tracks = this.options_.playerOptions.tracks || [];
- for (let i = 0; i < tracks.length; i++) {
- this.player_.addRemoteTextTrack(tracks[i], true);
- }
- this.preselectTrack();
- }));
- }
-
- /**
- * Preselect a track following this precedence:
- * - matches the previously selected {@link TextTrack}'s language and kind
- * - matches the previously selected {@link TextTrack}'s language only
- * - is the first default captions track
- * - is the first default descriptions track
- *
- * @listens Player#loadstart
- */
- preselectTrack() {
- const modes = {
- captions: 1,
- subtitles: 1
- };
- const trackList = this.player_.textTracks();
- const userPref = this.player_.cache_.selectedLanguage;
- let firstDesc;
- let firstCaptions;
- let preferredTrack;
- for (let i = 0; i < trackList.length; i++) {
- const track = trackList[i];
- if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
- // Always choose the track that matches both language and kind
- if (track.kind === userPref.kind) {
- preferredTrack = track;
- // or choose the first track that matches language
- } else if (!preferredTrack) {
- preferredTrack = track;
- }
-
- // clear everything if offTextTrackMenuItem was clicked
- } else if (userPref && !userPref.enabled) {
- preferredTrack = null;
- firstDesc = null;
- firstCaptions = null;
- } else if (track.default) {
- if (track.kind === 'descriptions' && !firstDesc) {
- firstDesc = track;
- } else if (track.kind in modes && !firstCaptions) {
- firstCaptions = track;
- }
- }
- }
-
- // The preferredTrack matches the user preference and takes
- // precedence over all the other tracks.
- // So, display the preferredTrack before the first default track
- // and the subtitles/captions track before the descriptions track
- if (preferredTrack) {
- preferredTrack.mode = 'showing';
- } else if (firstCaptions) {
- firstCaptions.mode = 'showing';
- } else if (firstDesc) {
- firstDesc.mode = 'showing';
- }
- }
-
- /**
- * Turn display of {@link TextTrack}'s from the current state into the other state.
- * There are only two states:
- * - 'shown'
- * - 'hidden'
- *
- * @listens Player#loadstart
- */
- toggleDisplay() {
- if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
- this.hide();
- } else {
- this.show();
- }
- }
-
- /**
- * Create the {@link Component}'s DOM element.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-text-track-display'
- }, {
- 'translate': 'yes',
- 'aria-live': 'off',
- 'aria-atomic': 'true'
- });
- }
-
- /**
- * Clear all displayed {@link TextTrack}s.
- */
- clearDisplay() {
- if (typeof window.WebVTT === 'function') {
- window.WebVTT.processCues(window, [], this.el_);
- }
- }
-
- /**
- * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
- * a {@link Player#fullscreenchange} is fired.
- *
- * @listens Player#texttrackchange
- * @listens Player#fullscreenchange
- */
- updateDisplay() {
- const tracks = this.player_.textTracks();
- const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
- this.clearDisplay();
- if (allowMultipleShowingTracks) {
- const showingTracks = [];
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- if (track.mode !== 'showing') {
- continue;
- }
- showingTracks.push(track);
- }
- this.updateForTrack(showingTracks);
- return;
- }
-
- // Track display prioritization model: if multiple tracks are 'showing',
- // display the first 'subtitles' or 'captions' track which is 'showing',
- // otherwise display the first 'descriptions' track which is 'showing'
-
- let descriptionsTrack = null;
- let captionsSubtitlesTrack = null;
- let i = tracks.length;
- while (i--) {
- const track = tracks[i];
- if (track.mode === 'showing') {
- if (track.kind === 'descriptions') {
- descriptionsTrack = track;
- } else {
- captionsSubtitlesTrack = track;
- }
- }
- }
- if (captionsSubtitlesTrack) {
- if (this.getAttribute('aria-live') !== 'off') {
- this.setAttribute('aria-live', 'off');
- }
- this.updateForTrack(captionsSubtitlesTrack);
- } else if (descriptionsTrack) {
- if (this.getAttribute('aria-live') !== 'assertive') {
- this.setAttribute('aria-live', 'assertive');
- }
- this.updateForTrack(descriptionsTrack);
- }
- }
-
- /**
- * Updates the displayed TextTrack to be sure it overlays the video when a either
- * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired.
- */
- updateDisplayOverlay() {
- // inset-inline and inset-block are not supprted on old chrome, but these are
- // only likely to be used on TV devices
- if (!this.player_.videoHeight() || !window.CSS.supports('inset-inline: 10px')) {
- return;
- }
- const playerWidth = this.player_.currentWidth();
- const playerHeight = this.player_.currentHeight();
- const playerAspectRatio = playerWidth / playerHeight;
- const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight();
- let insetInlineMatch = 0;
- let insetBlockMatch = 0;
- if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) {
- if (playerAspectRatio > videoAspectRatio) {
- insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2);
- } else {
- insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2);
- }
- }
- tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch));
- tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch));
- }
-
- /**
- * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
- *
- * @param {TextTrack} track
- * Text track object containing active cues to style.
- */
- updateDisplayState(track) {
- const overrides = this.player_.textTrackSettings.getValues();
- const cues = track.activeCues;
- let i = cues.length;
- while (i--) {
- const cue = cues[i];
- if (!cue) {
- continue;
- }
- const cueDiv = cue.displayState;
- if (overrides.color) {
- cueDiv.firstChild.style.color = overrides.color;
- }
- if (overrides.textOpacity) {
- tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
- }
- if (overrides.backgroundColor) {
- cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
- }
- if (overrides.backgroundOpacity) {
- tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
- }
- if (overrides.windowColor) {
- if (overrides.windowOpacity) {
- tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
- } else {
- cueDiv.style.backgroundColor = overrides.windowColor;
- }
- }
- if (overrides.edgeStyle) {
- if (overrides.edgeStyle === 'dropshadow') {
- cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
- } else if (overrides.edgeStyle === 'raised') {
- cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
- } else if (overrides.edgeStyle === 'depressed') {
- cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
- } else if (overrides.edgeStyle === 'uniform') {
- cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
- }
- }
- if (overrides.fontPercent && overrides.fontPercent !== 1) {
- const fontSize = window.parseFloat(cueDiv.style.fontSize);
- cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
- cueDiv.style.height = 'auto';
- cueDiv.style.top = 'auto';
- }
- if (overrides.fontFamily && overrides.fontFamily !== 'default') {
- if (overrides.fontFamily === 'small-caps') {
- cueDiv.firstChild.style.fontVariant = 'small-caps';
- } else {
- cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
- }
- }
- }
- }
-
- /**
- * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
- *
- * @param {TextTrack|TextTrack[]} tracks
- * Text track object or text track array to be added to the list.
- */
- updateForTrack(tracks) {
- if (!Array.isArray(tracks)) {
- tracks = [tracks];
- }
- if (typeof window.WebVTT !== 'function' || tracks.every(track => {
- return !track.activeCues;
- })) {
- return;
- }
- const cues = [];
-
- // push all active track cues
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- for (let j = 0; j < track.activeCues.length; ++j) {
- cues.push(track.activeCues[j]);
- }
- }
-
- // removes all cues before it processes new ones
- window.WebVTT.processCues(window, cues, this.el_);
-
- // add unique class to each language text track & add settings styling if necessary
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- for (let j = 0; j < track.activeCues.length; ++j) {
- const cueEl = track.activeCues[j].displayState;
- addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
- if (track.language) {
- setAttribute(cueEl, 'lang', track.language);
- }
- }
- if (this.player_.textTrackSettings) {
- this.updateDisplayState(track);
- }
- }
- }
- }
- Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
-
- /**
- * @file loading-spinner.js
- */
-
- /**
- * A loading spinner for use during waiting/loading events.
- *
- * @extends Component
- */
- class LoadingSpinner extends Component {
- /**
- * Create the `LoadingSpinner`s DOM element.
- *
- * @return {Element}
- * The dom element that gets created.
- */
- createEl() {
- const isAudio = this.player_.isAudio();
- const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
- const controlText = createEl('span', {
- className: 'vjs-control-text',
- textContent: this.localize('{1} is loading.', [playerType])
- });
- const el = super.createEl('div', {
- className: 'vjs-loading-spinner',
- dir: 'ltr'
- });
- el.appendChild(controlText);
- return el;
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
- }
- }
- Component.registerComponent('LoadingSpinner', LoadingSpinner);
-
- /**
- * @file button.js
- */
-
- /**
- * Base class for all buttons.
- *
- * @extends ClickableComponent
- */
- class Button extends ClickableComponent {
- /**
- * Create the `Button`s DOM element.
- *
- * @param {string} [tag="button"]
- * The element's node type. This argument is IGNORED: no matter what
- * is passed, it will always create a `button` element.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element.
- *
- * @param {Object} [attributes={}]
- * An object of attributes that should be set on the element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tag, props = {}, attributes = {}) {
- tag = 'button';
- props = Object.assign({
- className: this.buildCSSClass()
- }, props);
-
- // Add attributes for button element
- attributes = Object.assign({
- // Necessary since the default button type is "submit"
- type: 'button'
- }, attributes);
- const el = createEl(tag, props, attributes);
- if (!this.player_.options_.experimentalSvgIcons) {
- el.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- this.createControlTextEl(el);
- return el;
- }
-
- /**
- * Add a child `Component` inside of this `Button`.
- *
- * @param {string|Component} child
- * The name or instance of a child to add.
- *
- * @param {Object} [options={}]
- * The key/value store of options that will get passed to children of
- * the child.
- *
- * @return {Component}
- * The `Component` that gets added as a child. When using a string the
- * `Component` will get created by this process.
- *
- * @deprecated since version 5
- */
- addChild(child, options = {}) {
- const className = this.constructor.name;
- log.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
-
- // Avoid the error message generated by ClickableComponent's addChild method
- return Component.prototype.addChild.call(this, child, options);
- }
-
- /**
- * Enable the `Button` element so that it can be activated or clicked. Use this with
- * {@link Button#disable}.
- */
- enable() {
- super.enable();
- this.el_.removeAttribute('disabled');
- }
-
- /**
- * Disable the `Button` element so that it cannot be activated or clicked. Use this with
- * {@link Button#enable}.
- */
- disable() {
- super.disable();
- this.el_.setAttribute('disabled', 'disabled');
- }
-
- /**
- * This gets called when a `Button` has focus and `keydown` is triggered via a key
- * press.
- *
- * @param {KeyboardEvent} event
- * The event that caused this function to get called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Ignore Space or Enter key operation, which is handled by the browser for
- // a button - though not for its super class, ClickableComponent. Also,
- // prevent the event from propagating through the DOM and triggering Player
- // hotkeys. We do not preventDefault here because we _want_ the browser to
- // handle it.
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.stopPropagation();
- return;
- }
-
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- Component.registerComponent('Button', Button);
-
- /**
- * @file big-play-button.js
- */
-
- /**
- * The initial play button that shows before the video has played. The hiding of the
- * `BigPlayButton` get done via CSS and `Player` states.
- *
- * @extends Button
- */
- class BigPlayButton extends Button {
- constructor(player, options) {
- super(player, options);
- this.mouseused_ = false;
- this.setIcon('play');
- this.on('mousedown', e => this.handleMouseDown(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object. Always returns 'vjs-big-play-button'.
- */
- buildCSSClass() {
- return 'vjs-big-play-button';
- }
-
- /**
- * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
- * for more detailed information on what a click can be.
- *
- * @param {KeyboardEvent|MouseEvent|TouchEvent} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const playPromise = this.player_.play();
-
- // exit early if clicked via the mouse
- if (this.mouseused_ && 'clientX' in event && 'clientY' in event) {
- silencePromise(playPromise);
- if (this.player_.tech(true)) {
- this.player_.tech(true).focus();
- }
- return;
- }
- const cb = this.player_.getChild('controlBar');
- const playToggle = cb && cb.getChild('playToggle');
- if (!playToggle) {
- this.player_.tech(true).focus();
- return;
- }
- const playFocus = () => playToggle.focus();
- if (isPromise(playPromise)) {
- playPromise.then(playFocus, () => {});
- } else {
- this.setTimeout(playFocus, 1);
- }
- }
-
- /**
- * Event handler that is called when a `BigPlayButton` receives a
- * `keydown` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- this.mouseused_ = false;
- super.handleKeyDown(event);
- }
-
- /**
- * Handle `mousedown` events on the `BigPlayButton`.
- *
- * @param {MouseEvent} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- this.mouseused_ = true;
- }
- }
-
- /**
- * The text that should display over the `BigPlayButton`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- */
- BigPlayButton.prototype.controlText_ = 'Play Video';
- Component.registerComponent('BigPlayButton', BigPlayButton);
-
- /**
- * @file close-button.js
- */
-
- /**
- * The `CloseButton` is a `{@link Button}` that fires a `close` event when
- * it gets clicked.
- *
- * @extends Button
- */
- class CloseButton extends Button {
- /**
- * Creates an instance of the this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('cancel');
- this.controlText(options && options.controlText || this.localize('Close'));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-close-button ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when a `CloseButton` gets clicked. See
- * {@link ClickableComponent#handleClick} for more information on when
- * this will be triggered
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- * @fires CloseButton#close
- */
- handleClick(event) {
- /**
- * Triggered when the a `CloseButton` is clicked.
- *
- * @event CloseButton#close
- * @type {Event}
- *
- * @property {boolean} [bubbles=false]
- * set to false so that the close event does not
- * bubble up to parents if there is no listener
- */
- this.trigger({
- type: 'close',
- bubbles: false
- });
- }
- /**
- * Event handler that is called when a `CloseButton` receives a
- * `keydown` event.
- *
- * By default, if the key is Esc, it will trigger a `click` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Esc button will trigger `click` event
- if (keycode.isEventKey(event, 'Esc')) {
- event.preventDefault();
- event.stopPropagation();
- this.trigger('click');
- } else {
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- }
- Component.registerComponent('CloseButton', CloseButton);
-
- /**
- * @file play-toggle.js
- */
-
- /**
- * Button to toggle between play and pause.
- *
- * @extends Button
- */
- class PlayToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- super(player, options);
-
- // show or hide replay icon
- options.replay = options.replay === undefined || options.replay;
- this.setIcon('play');
- this.on(player, 'play', e => this.handlePlay(e));
- this.on(player, 'pause', e => this.handlePause(e));
- if (options.replay) {
- this.on(player, 'ended', e => this.handleEnded(e));
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-play-control ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when an `PlayToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (this.player_.paused()) {
- silencePromise(this.player_.play());
- } else {
- this.player_.pause();
- }
- }
-
- /**
- * This gets called once after the video has ended and the user seeks so that
- * we can change the replay button back to a play button.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#seeked
- */
- handleSeeked(event) {
- this.removeClass('vjs-ended');
- if (this.player_.paused()) {
- this.handlePause(event);
- } else {
- this.handlePlay(event);
- }
- }
-
- /**
- * Add the vjs-playing class to the element so it can change appearance.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#play
- */
- handlePlay(event) {
- this.removeClass('vjs-ended', 'vjs-paused');
- this.addClass('vjs-playing');
- // change the button text to "Pause"
- this.setIcon('pause');
- this.controlText('Pause');
- }
-
- /**
- * Add the vjs-paused class to the element so it can change appearance.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#pause
- */
- handlePause(event) {
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- // change the button text to "Play"
- this.setIcon('play');
- this.controlText('Play');
- }
-
- /**
- * Add the vjs-ended class to the element so it can change appearance
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#ended
- */
- handleEnded(event) {
- this.removeClass('vjs-playing');
- this.addClass('vjs-ended');
- // change the button text to "Replay"
- this.setIcon('replay');
- this.controlText('Replay');
-
- // on the next seek remove the replay button
- this.one(this.player_, 'seeked', e => this.handleSeeked(e));
- }
- }
-
- /**
- * The text that should display over the `PlayToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- PlayToggle.prototype.controlText_ = 'Play';
- Component.registerComponent('PlayToggle', PlayToggle);
-
- /**
- * @file time-display.js
- */
-
- /**
- * Displays time information about the video
- *
- * @extends Component
- */
- class TimeDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e));
- this.updateTextNode_();
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const className = this.buildCSSClass();
- const el = super.createEl('div', {
- className: `${className} vjs-time-control vjs-control`
- });
- const span = createEl('span', {
- className: 'vjs-control-text',
- textContent: `${this.localize(this.labelText_)}\u00a0`
- }, {
- role: 'presentation'
- });
- el.appendChild(span);
- this.contentEl_ = createEl('span', {
- className: `${className}-display`
- }, {
- // span elements have no implicit role, but some screen readers (notably VoiceOver)
- // treat them as a break between items in the DOM when using arrow keys
- // (or left-to-right swipes on iOS) to read contents of a page. Using
- // role='presentation' causes VoiceOver to NOT treat this span as a break.
- role: 'presentation'
- });
- el.appendChild(this.contentEl_);
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- this.textNode_ = null;
- super.dispose();
- }
-
- /**
- * Updates the displayed time according to the `updateContent` function which is defined in the child class.
- *
- * @param {Event} [event]
- * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called.
- */
- update(event) {
- if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') {
- return;
- }
- this.updateContent(event);
- }
-
- /**
- * Updates the time display text node with a new time
- *
- * @param {number} [time=0] the time to update to
- *
- * @private
- */
- updateTextNode_(time = 0) {
- time = formatTime(time);
- if (this.formattedTime_ === time) {
- return;
- }
- this.formattedTime_ = time;
- this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
- if (!this.contentEl_) {
- return;
- }
- let oldNode = this.textNode_;
- if (oldNode && this.contentEl_.firstChild !== oldNode) {
- oldNode = null;
- log.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.');
- }
- this.textNode_ = document.createTextNode(this.formattedTime_);
- if (!this.textNode_) {
- return;
- }
- if (oldNode) {
- this.contentEl_.replaceChild(this.textNode_, oldNode);
- } else {
- this.contentEl_.appendChild(this.textNode_);
- }
- });
- }
-
- /**
- * To be filled out in the child class, should update the displayed time
- * in accordance with the fact that the current time has changed.
- *
- * @param {Event} [event]
- * The `timeupdate` event that caused this to run.
- *
- * @listens Player#timeupdate
- */
- updateContent(event) {}
- }
-
- /**
- * The text that is added to the `TimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- TimeDisplay.prototype.labelText_ = 'Time';
-
- /**
- * The text that should display over the `TimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- TimeDisplay.prototype.controlText_ = 'Time';
- Component.registerComponent('TimeDisplay', TimeDisplay);
-
- /**
- * @file current-time-display.js
- */
-
- /**
- * Displays the current time
- *
- * @extends Component
- */
- class CurrentTimeDisplay extends TimeDisplay {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-current-time';
- }
-
- /**
- * Update current time display
- *
- * @param {Event} [event]
- * The `timeupdate` event that caused this function to run.
- *
- * @listens Player#timeupdate
- */
- updateContent(event) {
- // Allows for smooth scrubbing, when player can't keep up.
- let time;
- if (this.player_.ended()) {
- time = this.player_.duration();
- } else {
- time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- }
- this.updateTextNode_(time);
- }
- }
-
- /**
- * The text that is added to the `CurrentTimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
-
- /**
- * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
- Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
-
- /**
- * @file duration-display.js
- */
-
- /**
- * Displays the duration
- *
- * @extends Component
- */
- class DurationDisplay extends TimeDisplay {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- const updateContent = e => this.updateContent(e);
-
- // we do not want to/need to throttle duration changes,
- // as they should always display the changed duration as
- // it has changed
- this.on(player, 'durationchange', updateContent);
-
- // Listen to loadstart because the player duration is reset when a new media element is loaded,
- // but the durationchange on the user agent will not fire.
- // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
- this.on(player, 'loadstart', updateContent);
-
- // Also listen for timeupdate (in the parent) and loadedmetadata because removing those
- // listeners could have broken dependent applications/libraries. These
- // can likely be removed for 7.0.
- this.on(player, 'loadedmetadata', updateContent);
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-duration';
- }
-
- /**
- * Update duration time display.
- *
- * @param {Event} [event]
- * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
- * this function to be called.
- *
- * @listens Player#durationchange
- * @listens Player#timeupdate
- * @listens Player#loadedmetadata
- */
- updateContent(event) {
- const duration = this.player_.duration();
- this.updateTextNode_(duration);
- }
- }
-
- /**
- * The text that is added to the `DurationDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- DurationDisplay.prototype.labelText_ = 'Duration';
-
- /**
- * The text that should display over the `DurationDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- DurationDisplay.prototype.controlText_ = 'Duration';
- Component.registerComponent('DurationDisplay', DurationDisplay);
-
- /**
- * @file time-divider.js
- */
-
- /**
- * The separator between the current time and duration.
- * Can be hidden if it's not needed in the design.
- *
- * @extends Component
- */
- class TimeDivider extends Component {
- /**
- * Create the component's DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-time-control vjs-time-divider'
- }, {
- // this element and its contents can be hidden from assistive techs since
- // it is made extraneous by the announcement of the control text
- // for the current time and duration displays
- 'aria-hidden': true
- });
- const div = super.createEl('div');
- const span = super.createEl('span', {
- textContent: '/'
- });
- div.appendChild(span);
- el.appendChild(div);
- return el;
- }
- }
- Component.registerComponent('TimeDivider', TimeDivider);
-
- /**
- * @file remaining-time-display.js
- */
-
- /**
- * Displays the time left in the video
- *
- * @extends Component
- */
- class RemainingTimeDisplay extends TimeDisplay {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, 'durationchange', e => this.updateContent(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-remaining-time';
- }
-
- /**
- * Create the `Component`'s DOM element with the "minus" character prepend to the time
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl();
- if (this.options_.displayNegative !== false) {
- el.insertBefore(createEl('span', {}, {
- 'aria-hidden': true
- }, '-'), this.contentEl_);
- }
- return el;
- }
-
- /**
- * Update remaining time display.
- *
- * @param {Event} [event]
- * The `timeupdate` or `durationchange` event that caused this to run.
- *
- * @listens Player#timeupdate
- * @listens Player#durationchange
- */
- updateContent(event) {
- if (typeof this.player_.duration() !== 'number') {
- return;
- }
- let time;
-
- // @deprecated We should only use remainingTimeDisplay
- // as of video.js 7
- if (this.player_.ended()) {
- time = 0;
- } else if (this.player_.remainingTimeDisplay) {
- time = this.player_.remainingTimeDisplay();
- } else {
- time = this.player_.remainingTime();
- }
- this.updateTextNode_(time);
- }
- }
-
- /**
- * The text that is added to the `RemainingTimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
-
- /**
- * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
- Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
-
- /**
- * @file live-display.js
- */
-
- // TODO - Future make it click to snap to live
-
- /**
- * Displays the live indicator when duration is Infinity.
- *
- * @extends Component
- */
- class LiveDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.updateShowing();
- this.on(this.player(), 'durationchange', e => this.updateShowing(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-live-control vjs-control'
- });
- this.contentEl_ = createEl('div', {
- className: 'vjs-live-display'
- }, {
- 'aria-live': 'off'
- });
- this.contentEl_.appendChild(createEl('span', {
- className: 'vjs-control-text',
- textContent: `${this.localize('Stream Type')}\u00a0`
- }));
- this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE')));
- el.appendChild(this.contentEl_);
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- super.dispose();
- }
-
- /**
- * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
- * it accordingly
- *
- * @param {Event} [event]
- * The {@link Player#durationchange} event that caused this function to run.
- *
- * @listens Player#durationchange
- */
- updateShowing(event) {
- if (this.player().duration() === Infinity) {
- this.show();
- } else {
- this.hide();
- }
- }
- }
- Component.registerComponent('LiveDisplay', LiveDisplay);
-
- /**
- * @file seek-to-live.js
- */
-
- /**
- * Displays the live indicator when duration is Infinity.
- *
- * @extends Component
- */
- class SeekToLive extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.updateLiveEdgeStatus();
- if (this.player_.liveTracker) {
- this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
- this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
- }
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('button', {
- className: 'vjs-seek-to-live-control vjs-control'
- });
- this.setIcon('circle', el);
- this.textEl_ = createEl('span', {
- className: 'vjs-seek-to-live-text',
- textContent: this.localize('LIVE')
- }, {
- 'aria-hidden': 'true'
- });
- el.appendChild(this.textEl_);
- return el;
- }
-
- /**
- * Update the state of this button if we are at the live edge
- * or not
- */
- updateLiveEdgeStatus() {
- // default to live edge
- if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
- this.setAttribute('aria-disabled', true);
- this.addClass('vjs-at-live-edge');
- this.controlText('Seek to live, currently playing live');
- } else {
- this.setAttribute('aria-disabled', false);
- this.removeClass('vjs-at-live-edge');
- this.controlText('Seek to live, currently behind live');
- }
- }
-
- /**
- * On click bring us as near to the live point as possible.
- * This requires that we wait for the next `live-seekable-change`
- * event which will happen every segment length seconds.
- */
- handleClick() {
- this.player_.liveTracker.seekToLiveEdge();
- }
-
- /**
- * Dispose of the element and stop tracking
- */
- dispose() {
- if (this.player_.liveTracker) {
- this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
- }
- this.textEl_ = null;
- super.dispose();
- }
- }
- /**
- * The text that should display over the `SeekToLive`s control. Added for localization.
- *
- * @type {string}
- * @protected
- */
- SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
- Component.registerComponent('SeekToLive', SeekToLive);
-
- /**
- * @file num.js
- * @module num
- */
-
- /**
- * Keep a number between a min and a max value
- *
- * @param {number} number
- * The number to clamp
- *
- * @param {number} min
- * The minimum value
- * @param {number} max
- * The maximum value
- *
- * @return {number}
- * the clamped number
- */
- function clamp(number, min, max) {
- number = Number(number);
- return Math.min(max, Math.max(min, isNaN(number) ? min : number));
- }
-
- var Num = /*#__PURE__*/Object.freeze({
- __proto__: null,
- clamp: clamp
- });
-
- /**
- * @file slider.js
- */
-
- /**
- * The base functionality for a slider. Can be vertical or horizontal.
- * For instance the volume bar or the seek bar on a video is a slider.
- *
- * @extends Component
- */
- class Slider extends Component {
- /**
- * Create an instance of this class
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.handleMouseDown_ = e => this.handleMouseDown(e);
- this.handleMouseUp_ = e => this.handleMouseUp(e);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.handleClick_ = e => this.handleClick(e);
- this.handleMouseMove_ = e => this.handleMouseMove(e);
- this.update_ = e => this.update(e);
-
- // Set property names to bar to match with the child Slider class is looking for
- this.bar = this.getChild(this.options_.barName);
-
- // Set a horizontal or vertical class on the slider depending on the slider type
- this.vertical(!!this.options_.vertical);
- this.enable();
- }
-
- /**
- * Are controls are currently enabled for this slider or not.
- *
- * @return {boolean}
- * true if controls are enabled, false otherwise
- */
- enabled() {
- return this.enabled_;
- }
-
- /**
- * Enable controls for this slider if they are disabled
- */
- enable() {
- if (this.enabled()) {
- return;
- }
- this.on('mousedown', this.handleMouseDown_);
- this.on('touchstart', this.handleMouseDown_);
- this.on('keydown', this.handleKeyDown_);
- this.on('click', this.handleClick_);
-
- // TODO: deprecated, controlsvisible does not seem to be fired
- this.on(this.player_, 'controlsvisible', this.update);
- if (this.playerEvent) {
- this.on(this.player_, this.playerEvent, this.update);
- }
- this.removeClass('disabled');
- this.setAttribute('tabindex', 0);
- this.enabled_ = true;
- }
-
- /**
- * Disable controls for this slider if they are enabled
- */
- disable() {
- if (!this.enabled()) {
- return;
- }
- const doc = this.bar.el_.ownerDocument;
- this.off('mousedown', this.handleMouseDown_);
- this.off('touchstart', this.handleMouseDown_);
- this.off('keydown', this.handleKeyDown_);
- this.off('click', this.handleClick_);
- this.off(this.player_, 'controlsvisible', this.update_);
- this.off(doc, 'mousemove', this.handleMouseMove_);
- this.off(doc, 'mouseup', this.handleMouseUp_);
- this.off(doc, 'touchmove', this.handleMouseMove_);
- this.off(doc, 'touchend', this.handleMouseUp_);
- this.removeAttribute('tabindex');
- this.addClass('disabled');
- if (this.playerEvent) {
- this.off(this.player_, this.playerEvent, this.update);
- }
- this.enabled_ = false;
- }
-
- /**
- * Create the `Slider`s DOM element.
- *
- * @param {string} type
- * Type of element to create.
- *
- * @param {Object} [props={}]
- * List of properties in Object form.
- *
- * @param {Object} [attributes={}]
- * list of attributes in Object form.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(type, props = {}, attributes = {}) {
- // Add the slider element class to all sub classes
- props.className = props.className + ' vjs-slider';
- props = Object.assign({
- tabIndex: 0
- }, props);
- attributes = Object.assign({
- 'role': 'slider',
- 'aria-valuenow': 0,
- 'aria-valuemin': 0,
- 'aria-valuemax': 100
- }, attributes);
- return super.createEl(type, props, attributes);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `Slider`.
- *
- * @param {MouseEvent} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- * @fires Slider#slideractive
- */
- handleMouseDown(event) {
- const doc = this.bar.el_.ownerDocument;
- if (event.type === 'mousedown') {
- event.preventDefault();
- }
- // Do not call preventDefault() on touchstart in Chrome
- // to avoid console warnings. Use a 'touch-action: none' style
- // instead to prevent unintended scrolling.
- // https://developers.google.com/web/updates/2017/01/scrolling-intervention
- if (event.type === 'touchstart' && !IS_CHROME) {
- event.preventDefault();
- }
- blockTextSelection();
- this.addClass('vjs-sliding');
- /**
- * Triggered when the slider is in an active state
- *
- * @event Slider#slideractive
- * @type {MouseEvent}
- */
- this.trigger('slideractive');
- this.on(doc, 'mousemove', this.handleMouseMove_);
- this.on(doc, 'mouseup', this.handleMouseUp_);
- this.on(doc, 'touchmove', this.handleMouseMove_);
- this.on(doc, 'touchend', this.handleMouseUp_);
- this.handleMouseMove(event, true);
- }
-
- /**
- * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
- * The `mousemove` and `touchmove` events will only only trigger this function during
- * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
- * {@link Slider#handleMouseUp}.
- *
- * @param {MouseEvent} event
- * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
- * this function
- * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
- *
- * @listens mousemove
- * @listens touchmove
- */
- handleMouseMove(event) {}
-
- /**
- * Handle `mouseup` or `touchend` events on the `Slider`.
- *
- * @param {MouseEvent} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- * @fires Slider#sliderinactive
- */
- handleMouseUp(event) {
- const doc = this.bar.el_.ownerDocument;
- unblockTextSelection();
- this.removeClass('vjs-sliding');
- /**
- * Triggered when the slider is no longer in an active state.
- *
- * @event Slider#sliderinactive
- * @type {Event}
- */
- this.trigger('sliderinactive');
- this.off(doc, 'mousemove', this.handleMouseMove_);
- this.off(doc, 'mouseup', this.handleMouseUp_);
- this.off(doc, 'touchmove', this.handleMouseMove_);
- this.off(doc, 'touchend', this.handleMouseUp_);
- this.update();
- }
-
- /**
- * Update the progress bar of the `Slider`.
- *
- * @return {number}
- * The percentage of progress the progress bar represents as a
- * number from 0 to 1.
- */
- update() {
- // In VolumeBar init we have a setTimeout for update that pops and update
- // to the end of the execution stack. The player is destroyed before then
- // update will cause an error
- // If there's no bar...
- if (!this.el_ || !this.bar) {
- return;
- }
-
- // clamp progress between 0 and 1
- // and only round to four decimal places, as we round to two below
- const progress = this.getProgress();
- if (progress === this.progress_) {
- return progress;
- }
- this.progress_ = progress;
- this.requestNamedAnimationFrame('Slider#update', () => {
- // Set the new bar width or height
- const sizeKey = this.vertical() ? 'height' : 'width';
-
- // Convert to a percentage for css value
- this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
- });
- return progress;
- }
-
- /**
- * Get the percentage of the bar that should be filled
- * but clamped and rounded.
- *
- * @return {number}
- * percentage filled that the slider is
- */
- getProgress() {
- return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
- }
-
- /**
- * Calculate distance for slider
- *
- * @param {Event} event
- * The event that caused this function to run.
- *
- * @return {number}
- * The current position of the Slider.
- * - position.x for vertical `Slider`s
- * - position.y for horizontal `Slider`s
- */
- calculateDistance(event) {
- const position = getPointerPosition(this.el_, event);
- if (this.vertical()) {
- return position.y;
- }
- return position.x;
- }
-
- /**
- * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
- * arrow keys. This function will only be called when the slider has focus. See
- * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
- *
- * @param {KeyboardEvent} event
- * the `keydown` event that caused this function to run.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Left and Down Arrows
- if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepBack();
-
- // Up and Right Arrows
- } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepForward();
- } else {
- // Pass keydown handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
-
- /**
- * Listener for click events on slider, used to prevent clicks
- * from bubbling up to parent elements like button menus.
- *
- * @param {Object} event
- * Event that caused this object to run
- */
- handleClick(event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- /**
- * Get/set if slider is horizontal for vertical
- *
- * @param {boolean} [bool]
- * - true if slider is vertical,
- * - false is horizontal
- *
- * @return {boolean}
- * - true if slider is vertical, and getting
- * - false if the slider is horizontal, and getting
- */
- vertical(bool) {
- if (bool === undefined) {
- return this.vertical_ || false;
- }
- this.vertical_ = !!bool;
- if (this.vertical_) {
- this.addClass('vjs-slider-vertical');
- } else {
- this.addClass('vjs-slider-horizontal');
- }
- }
- }
- Component.registerComponent('Slider', Slider);
-
- /**
- * @file load-progress-bar.js
- */
-
- // get the percent width of a time compared to the total end
- const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
-
- /**
- * Shows loading progress
- *
- * @extends Component
- */
- class LoadProgressBar extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.partEls_ = [];
- this.on(player, 'progress', e => this.update(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-load-progress'
- });
- const wrapper = createEl('span', {
- className: 'vjs-control-text'
- });
- const loadedText = createEl('span', {
- textContent: this.localize('Loaded')
- });
- const separator = document.createTextNode(': ');
- this.percentageEl_ = createEl('span', {
- className: 'vjs-control-text-loaded-percentage',
- textContent: '0%'
- });
- el.appendChild(wrapper);
- wrapper.appendChild(loadedText);
- wrapper.appendChild(separator);
- wrapper.appendChild(this.percentageEl_);
- return el;
- }
- dispose() {
- this.partEls_ = null;
- this.percentageEl_ = null;
- super.dispose();
- }
-
- /**
- * Update progress bar
- *
- * @param {Event} [event]
- * The `progress` event that caused this function to run.
- *
- * @listens Player#progress
- */
- update(event) {
- this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
- const liveTracker = this.player_.liveTracker;
- const buffered = this.player_.buffered();
- const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
- const bufferedEnd = this.player_.bufferedEnd();
- const children = this.partEls_;
- const percent = percentify(bufferedEnd, duration);
- if (this.percent_ !== percent) {
- // update the width of the progress bar
- this.el_.style.width = percent;
- // update the control-text
- textContent(this.percentageEl_, percent);
- this.percent_ = percent;
- }
-
- // add child elements to represent the individual buffered time ranges
- for (let i = 0; i < buffered.length; i++) {
- const start = buffered.start(i);
- const end = buffered.end(i);
- let part = children[i];
- if (!part) {
- part = this.el_.appendChild(createEl());
- children[i] = part;
- }
-
- // only update if changed
- if (part.dataset.start === start && part.dataset.end === end) {
- continue;
- }
- part.dataset.start = start;
- part.dataset.end = end;
-
- // set the percent based on the width of the progress bar (bufferedEnd)
- part.style.left = percentify(start, bufferedEnd);
- part.style.width = percentify(end - start, bufferedEnd);
- }
-
- // remove unused buffered range elements
- for (let i = children.length; i > buffered.length; i--) {
- this.el_.removeChild(children[i - 1]);
- }
- children.length = buffered.length;
- });
- }
- }
- Component.registerComponent('LoadProgressBar', LoadProgressBar);
-
- /**
- * @file time-tooltip.js
- */
-
- /**
- * Time tooltips display a time above the progress bar.
- *
- * @extends Component
- */
- class TimeTooltip extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the time tooltip DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-time-tooltip'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Updates the position of the time tooltip relative to the `SeekBar`.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint, content) {
- const tooltipRect = findPosition(this.el_);
- const playerRect = getBoundingClientRect(this.player_.el());
- const seekBarPointPx = seekBarRect.width * seekBarPoint;
-
- // do nothing if either rect isn't available
- // for example, if the player isn't in the DOM for testing
- if (!playerRect || !tooltipRect) {
- return;
- }
-
- // This is the space left of the `seekBarPoint` available within the bounds
- // of the player. We calculate any gap between the left edge of the player
- // and the left edge of the `SeekBar` and add the number of pixels in the
- // `SeekBar` before hitting the `seekBarPoint`
- let spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
-
- // This is the space right of the `seekBarPoint` available within the bounds
- // of the player. We calculate the number of pixels from the `seekBarPoint`
- // to the right edge of the `SeekBar` and add to that any gap between the
- // right edge of the `SeekBar` and the player.
- let spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
-
- // spaceRightOfPoint is always NaN for mouse time display
- // because the seekbarRect does not have a right property. This causes
- // the mouse tool tip to be truncated when it's close to the right edge of the player.
- // In such cases, we ignore the `playerRect.right - seekBarRect.right` value when calculating.
- // For the sake of consistency, we ignore seekBarRect.left - playerRect.left for the left edge.
- if (!spaceRightOfPoint) {
- spaceRightOfPoint = seekBarRect.width - seekBarPointPx;
- spaceLeftOfPoint = seekBarPointPx;
- }
- // This is the number of pixels by which the tooltip will need to be pulled
- // further to the right to center it over the `seekBarPoint`.
- let pullTooltipBy = tooltipRect.width / 2;
-
- // Adjust the `pullTooltipBy` distance to the left or right depending on
- // the results of the space calculations above.
- if (spaceLeftOfPoint < pullTooltipBy) {
- pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
- } else if (spaceRightOfPoint < pullTooltipBy) {
- pullTooltipBy = spaceRightOfPoint;
- }
-
- // Due to the imprecision of decimal/ratio based calculations and varying
- // rounding behaviors, there are cases where the spacing adjustment is off
- // by a pixel or two. This adds insurance to these calculations.
- if (pullTooltipBy < 0) {
- pullTooltipBy = 0;
- } else if (pullTooltipBy > tooltipRect.width) {
- pullTooltipBy = tooltipRect.width;
- }
-
- // prevent small width fluctuations within 0.4px from
- // changing the value below.
- // This really helps for live to prevent the play
- // progress time tooltip from jittering
- pullTooltipBy = Math.round(pullTooltipBy);
- this.el_.style.right = `-${pullTooltipBy}px`;
- this.write(content);
- }
-
- /**
- * Write the time to the tooltip DOM element.
- *
- * @param {string} content
- * The formatted time for the tooltip.
- */
- write(content) {
- textContent(this.el_, content);
- }
-
- /**
- * Updates the position of the time tooltip relative to the `SeekBar`.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- *
- * @param {number} time
- * The time to update the tooltip to, not used during live playback
- *
- * @param {Function} cb
- * A function that will be called during the request animation frame
- * for tooltips that need to do additional animations from the default
- */
- updateTime(seekBarRect, seekBarPoint, time, cb) {
- this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
- let content;
- const duration = this.player_.duration();
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- const liveWindow = this.player_.liveTracker.liveWindow();
- const secondsBehind = liveWindow - seekBarPoint * liveWindow;
- content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
- } else {
- content = formatTime(time, duration);
- }
- this.update(seekBarRect, seekBarPoint, content);
- if (cb) {
- cb();
- }
- });
- }
- }
- Component.registerComponent('TimeTooltip', TimeTooltip);
-
- /**
- * @file play-progress-bar.js
- */
-
- /**
- * Used by {@link SeekBar} to display media playback progress as part of the
- * {@link ProgressControl}.
- *
- * @extends Component
- */
- class PlayProgressBar extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('circle');
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-play-progress vjs-slider-bar'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Enqueues updates to its own DOM as well as the DOM of its
- * {@link TimeTooltip} child.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint) {
- const timeTooltip = this.getChild('timeTooltip');
- if (!timeTooltip) {
- return;
- }
- const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
- }
- }
-
- /**
- * Default options for {@link PlayProgressBar}.
- *
- * @type {Object}
- * @private
- */
- PlayProgressBar.prototype.options_ = {
- children: []
- };
-
- // Time tooltips should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- PlayProgressBar.prototype.options_.children.push('timeTooltip');
- }
- Component.registerComponent('PlayProgressBar', PlayProgressBar);
-
- /**
- * @file mouse-time-display.js
- */
-
- /**
- * The {@link MouseTimeDisplay} component tracks mouse movement over the
- * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
- * indicating the time which is represented by a given point in the
- * {@link ProgressControl}.
- *
- * @extends Component
- */
- class MouseTimeDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-mouse-display'
- });
- }
-
- /**
- * Enqueues updates to its own DOM as well as the DOM of its
- * {@link TimeTooltip} child.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint) {
- const time = seekBarPoint * this.player_.duration();
- this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
- this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
- });
- }
- }
-
- /**
- * Default options for `MouseTimeDisplay`
- *
- * @type {Object}
- * @private
- */
- MouseTimeDisplay.prototype.options_ = {
- children: ['timeTooltip']
- };
- Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
-
- /**
- * @file seek-bar.js
- */
-
- // The number of seconds the `step*` functions move the timeline.
- const STEP_SECONDS = 5;
-
- // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
- const PAGE_KEY_MULTIPLIER = 12;
-
- /**
- * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
- * as its `bar`.
- *
- * @extends Slider
- */
- class SeekBar extends Slider {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setEventHandlers_();
- }
-
- /**
- * Sets the event handlers
- *
- * @private
- */
- setEventHandlers_() {
- this.update_ = bind_(this, this.update);
- this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
- this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- if (this.player_.liveTracker) {
- this.on(this.player_.liveTracker, 'liveedgechange', this.update);
- }
-
- // when playing, let's ensure we smoothly update the play progress bar
- // via an interval
- this.updateInterval = null;
- this.enableIntervalHandler_ = e => this.enableInterval_(e);
- this.disableIntervalHandler_ = e => this.disableInterval_(e);
- this.on(this.player_, ['playing'], this.enableIntervalHandler_);
- this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
-
- // we don't need to update the play progress if the document is hidden,
- // also, this causes the CPU to spike and eventually crash the page on IE11.
- if ('hidden' in document && 'visibilityState' in document) {
- this.on(document, 'visibilitychange', this.toggleVisibility_);
- }
- }
- toggleVisibility_(e) {
- if (document.visibilityState === 'hidden') {
- this.cancelNamedAnimationFrame('SeekBar#update');
- this.cancelNamedAnimationFrame('Slider#update');
- this.disableInterval_(e);
- } else {
- if (!this.player_.ended() && !this.player_.paused()) {
- this.enableInterval_();
- }
-
- // we just switched back to the page and someone may be looking, so, update ASAP
- this.update();
- }
- }
- enableInterval_() {
- if (this.updateInterval) {
- return;
- }
- this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
- }
- disableInterval_(e) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
- return;
- }
- if (!this.updateInterval) {
- return;
- }
- this.clearInterval(this.updateInterval);
- this.updateInterval = null;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-progress-holder'
- }, {
- 'aria-label': this.localize('Progress Bar')
- });
- }
-
- /**
- * This function updates the play progress bar and accessibility
- * attributes to whatever is passed in.
- *
- * @param {Event} [event]
- * The `timeupdate` or `ended` event that caused this to run.
- *
- * @listens Player#timeupdate
- *
- * @return {number}
- * The current percent at a number from 0-1
- */
- update(event) {
- // ignore updates while the tab is hidden
- if (document.visibilityState === 'hidden') {
- return;
- }
- const percent = super.update();
- this.requestNamedAnimationFrame('SeekBar#update', () => {
- const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
- const liveTracker = this.player_.liveTracker;
- let duration = this.player_.duration();
- if (liveTracker && liveTracker.isLive()) {
- duration = this.player_.liveTracker.liveCurrentTime();
- }
- if (this.percent_ !== percent) {
- // machine readable value of progress bar (percentage complete)
- this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
- this.percent_ = percent;
- }
- if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
- // human readable value of progress bar (time complete)
- this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
- this.currentTime_ = currentTime;
- this.duration_ = duration;
- }
-
- // update the progress bar time tooltip with the current time
- if (this.bar) {
- this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
- }
- });
- return percent;
- }
-
- /**
- * Prevent liveThreshold from causing seeks to seem like they
- * are not happening from a user perspective.
- *
- * @param {number} ct
- * current time to seek to
- */
- userSeek_(ct) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- this.player_.liveTracker.nextSeekedFromUser();
- }
- this.player_.currentTime(ct);
- }
-
- /**
- * Get the value of current time but allows for smooth scrubbing,
- * when player can't keep up.
- *
- * @return {number}
- * The current time value to display
- *
- * @private
- */
- getCurrentTime_() {
- return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- }
-
- /**
- * Get the percentage of media played so far.
- *
- * @return {number}
- * The percentage of media played so far (0 to 1).
- */
- getPercent() {
- const currentTime = this.getCurrentTime_();
- let percent;
- const liveTracker = this.player_.liveTracker;
- if (liveTracker && liveTracker.isLive()) {
- percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
-
- // prevent the percent from changing at the live edge
- if (liveTracker.atLiveEdge()) {
- percent = 1;
- }
- } else {
- percent = currentTime / this.player_.duration();
- }
- return percent;
- }
-
- /**
- * Handle mouse down on seek bar
- *
- * @param {MouseEvent} event
- * The `mousedown` event that caused this to run.
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- if (!isSingleLeftClick(event)) {
- return;
- }
-
- // Stop event propagation to prevent double fire in progress-control.js
- event.stopPropagation();
- this.videoWasPlaying = !this.player_.paused();
- this.player_.pause();
- super.handleMouseDown(event);
- }
-
- /**
- * Handle mouse move on seek bar
- *
- * @param {MouseEvent} event
- * The `mousemove` event that caused this to run.
- * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
- *
- * @listens mousemove
- */
- handleMouseMove(event, mouseDown = false) {
- if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) {
- return;
- }
- if (!mouseDown && !this.player_.scrubbing()) {
- this.player_.scrubbing(true);
- }
- let newTime;
- const distance = this.calculateDistance(event);
- const liveTracker = this.player_.liveTracker;
- if (!liveTracker || !liveTracker.isLive()) {
- newTime = distance * this.player_.duration();
-
- // Don't let video end while scrubbing.
- if (newTime === this.player_.duration()) {
- newTime = newTime - 0.1;
- }
- } else {
- if (distance >= 0.99) {
- liveTracker.seekToLiveEdge();
- return;
- }
- const seekableStart = liveTracker.seekableStart();
- const seekableEnd = liveTracker.liveCurrentTime();
- newTime = seekableStart + distance * liveTracker.liveWindow();
-
- // Don't let video end while scrubbing.
- if (newTime >= seekableEnd) {
- newTime = seekableEnd;
- }
-
- // Compensate for precision differences so that currentTime is not less
- // than seekable start
- if (newTime <= seekableStart) {
- newTime = seekableStart + 0.1;
- }
-
- // On android seekableEnd can be Infinity sometimes,
- // this will cause newTime to be Infinity, which is
- // not a valid currentTime.
- if (newTime === Infinity) {
- return;
- }
- }
-
- // Set new time (tell player to seek to new time)
- this.userSeek_(newTime);
- if (this.player_.options_.enableSmoothSeeking) {
- this.update();
- }
- }
- enable() {
- super.enable();
- const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- if (!mouseTimeDisplay) {
- return;
- }
- mouseTimeDisplay.show();
- }
- disable() {
- super.disable();
- const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- if (!mouseTimeDisplay) {
- return;
- }
- mouseTimeDisplay.hide();
- }
-
- /**
- * Handle mouse up on seek bar
- *
- * @param {MouseEvent} event
- * The `mouseup` event that caused this to run.
- *
- * @listens mouseup
- */
- handleMouseUp(event) {
- super.handleMouseUp(event);
-
- // Stop event propagation to prevent double fire in progress-control.js
- if (event) {
- event.stopPropagation();
- }
- this.player_.scrubbing(false);
-
- /**
- * Trigger timeupdate because we're done seeking and the time has changed.
- * This is particularly useful for if the player is paused to time the time displays.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.player_.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- if (this.videoWasPlaying) {
- silencePromise(this.player_.play());
- } else {
- // We're done seeking and the time has changed.
- // If the player is paused, make sure we display the correct time on the seek bar.
- this.update_();
- }
- }
-
- /**
- * Move more quickly fast forward for keyboard-only users
- */
- stepForward() {
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
- }
-
- /**
- * Move more quickly rewind for keyboard-only users
- */
- stepBack() {
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
- }
-
- /**
- * Toggles the playback state of the player
- * This gets called when enter or space is used on the seekbar
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called
- *
- */
- handleAction(event) {
- if (this.player_.paused()) {
- this.player_.play();
- } else {
- this.player_.pause();
- }
- }
-
- /**
- * Called when this SeekBar has focus and a key gets pressed down.
- * Supports the following keys:
- *
- * Space or Enter key fire a click event
- * Home key moves to start of the timeline
- * End key moves to end of the timeline
- * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
- * PageDown key moves back a larger step than ArrowDown
- * PageUp key moves forward a large step
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- const liveTracker = this.player_.liveTracker;
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.preventDefault();
- event.stopPropagation();
- this.handleAction(event);
- } else if (keycode.isEventKey(event, 'Home')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(0);
- } else if (keycode.isEventKey(event, 'End')) {
- event.preventDefault();
- event.stopPropagation();
- if (liveTracker && liveTracker.isLive()) {
- this.userSeek_(liveTracker.liveCurrentTime());
- } else {
- this.userSeek_(this.player_.duration());
- }
- } else if (/^[0-9]$/.test(keycode(event))) {
- event.preventDefault();
- event.stopPropagation();
- const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;
- if (liveTracker && liveTracker.isLive()) {
- this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
- } else {
- this.userSeek_(this.player_.duration() * gotoFraction);
- }
- } else if (keycode.isEventKey(event, 'PgDn')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
- } else if (keycode.isEventKey(event, 'PgUp')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
- } else {
- // Pass keydown handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- dispose() {
- this.disableInterval_();
- this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- if (this.player_.liveTracker) {
- this.off(this.player_.liveTracker, 'liveedgechange', this.update);
- }
- this.off(this.player_, ['playing'], this.enableIntervalHandler_);
- this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
-
- // we don't need to update the play progress if the document is hidden,
- // also, this causes the CPU to spike and eventually crash the page on IE11.
- if ('hidden' in document && 'visibilityState' in document) {
- this.off(document, 'visibilitychange', this.toggleVisibility_);
- }
- super.dispose();
- }
- }
-
- /**
- * Default options for the `SeekBar`
- *
- * @type {Object}
- * @private
- */
- SeekBar.prototype.options_ = {
- children: ['loadProgressBar', 'playProgressBar'],
- barName: 'playProgressBar'
- };
-
- // MouseTimeDisplay tooltips should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
- }
- Component.registerComponent('SeekBar', SeekBar);
-
- /**
- * @file progress-control.js
- */
-
- /**
- * The Progress Control component contains the seek bar, load progress,
- * and play progress.
- *
- * @extends Component
- */
- class ProgressControl extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
- this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
- this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
- this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
- this.enable();
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-progress-control vjs-control'
- });
- }
-
- /**
- * When the mouse moves over the `ProgressControl`, the pointer position
- * gets passed down to the `MouseTimeDisplay` component.
- *
- * @param {Event} event
- * The `mousemove` event that caused this function to run.
- *
- * @listen mousemove
- */
- handleMouseMove(event) {
- const seekBar = this.getChild('seekBar');
- if (!seekBar) {
- return;
- }
- const playProgressBar = seekBar.getChild('playProgressBar');
- const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
- if (!playProgressBar && !mouseTimeDisplay) {
- return;
- }
- const seekBarEl = seekBar.el();
- const seekBarRect = findPosition(seekBarEl);
- let seekBarPoint = getPointerPosition(seekBarEl, event).x;
-
- // The default skin has a gap on either side of the `SeekBar`. This means
- // that it's possible to trigger this behavior outside the boundaries of
- // the `SeekBar`. This ensures we stay within it at all times.
- seekBarPoint = clamp(seekBarPoint, 0, 1);
- if (mouseTimeDisplay) {
- mouseTimeDisplay.update(seekBarRect, seekBarPoint);
- }
- if (playProgressBar) {
- playProgressBar.update(seekBarRect, seekBar.getProgress());
- }
- }
-
- /**
- * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
- *
- * @method ProgressControl#throttledHandleMouseSeek
- * @param {Event} event
- * The `mousemove` event that caused this function to run.
- *
- * @listen mousemove
- * @listen touchmove
- */
-
- /**
- * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousemove
- * @listens touchmove
- */
- handleMouseSeek(event) {
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseMove(event);
- }
- }
-
- /**
- * Are controls are currently enabled for this progress control.
- *
- * @return {boolean}
- * true if controls are enabled, false otherwise
- */
- enabled() {
- return this.enabled_;
- }
-
- /**
- * Disable all controls on the progress control and its children
- */
- disable() {
- this.children().forEach(child => child.disable && child.disable());
- if (!this.enabled()) {
- return;
- }
- this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
- this.off(this.el_, 'mousemove', this.handleMouseMove);
- this.removeListenersAddedOnMousedownAndTouchstart();
- this.addClass('disabled');
- this.enabled_ = false;
-
- // Restore normal playback state if controls are disabled while scrubbing
- if (this.player_.scrubbing()) {
- const seekBar = this.getChild('seekBar');
- this.player_.scrubbing(false);
- if (seekBar.videoWasPlaying) {
- silencePromise(this.player_.play());
- }
- }
- }
-
- /**
- * Enable all controls on the progress control and its children
- */
- enable() {
- this.children().forEach(child => child.enable && child.enable());
- if (this.enabled()) {
- return;
- }
- this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
- this.on(this.el_, 'mousemove', this.handleMouseMove);
- this.removeClass('disabled');
- this.enabled_ = true;
- }
-
- /**
- * Cleanup listeners after the user finishes interacting with the progress controls
- */
- removeListenersAddedOnMousedownAndTouchstart() {
- const doc = this.el_.ownerDocument;
- this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
- this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
- this.off(doc, 'mouseup', this.handleMouseUpHandler_);
- this.off(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseDown(event) {
- const doc = this.el_.ownerDocument;
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseDown(event);
- }
- this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
- this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
- this.on(doc, 'mouseup', this.handleMouseUpHandler_);
- this.on(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mouseup` or `touchend` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- */
- handleMouseUp(event) {
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseUp(event);
- }
- this.removeListenersAddedOnMousedownAndTouchstart();
- }
- }
-
- /**
- * Default options for `ProgressControl`
- *
- * @type {Object}
- * @private
- */
- ProgressControl.prototype.options_ = {
- children: ['seekBar']
- };
- Component.registerComponent('ProgressControl', ProgressControl);
-
- /**
- * @file picture-in-picture-toggle.js
- */
-
- /**
- * Toggle Picture-in-Picture mode
- *
- * @extends Button
- */
- class PictureInPictureToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @listens Player#enterpictureinpicture
- * @listens Player#leavepictureinpicture
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('picture-in-picture-enter');
- this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
- this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
- this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange());
-
- // TODO: Deactivate button on player emptied event.
- this.disable();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`;
- }
-
- /**
- * Displays or hides the button depending on the audio mode detection.
- * Exits picture-in-picture if it is enabled when switching to audio mode.
- */
- handlePictureInPictureAudioModeChange() {
- // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
- const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio';
- const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode();
- if (!isAudioMode) {
- this.show();
- return;
- }
- if (this.player_.isInPictureInPicture()) {
- this.player_.exitPictureInPicture();
- }
- this.hide();
- }
-
- /**
- * Enables or disables button based on availability of a Picture-In-Picture mode.
- *
- * Enabled if
- * - `player.options().enableDocumentPictureInPicture` is true and
- * window.documentPictureInPicture is available; or
- * - `player.disablePictureInPicture()` is false and
- * element.requestPictureInPicture is available
- */
- handlePictureInPictureEnabledChange() {
- if (document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window) {
- this.enable();
- } else {
- this.disable();
- }
- }
-
- /**
- * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
- *
- * @param {Event} [event]
- * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
- * called.
- *
- * @listens Player#enterpictureinpicture
- * @listens Player#leavepictureinpicture
- */
- handlePictureInPictureChange(event) {
- if (this.player_.isInPictureInPicture()) {
- this.setIcon('picture-in-picture-exit');
- this.controlText('Exit Picture-in-Picture');
- } else {
- this.setIcon('picture-in-picture-enter');
- this.controlText('Picture-in-Picture');
- }
- this.handlePictureInPictureEnabledChange();
- }
-
- /**
- * This gets called when an `PictureInPictureToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (!this.player_.isInPictureInPicture()) {
- this.player_.requestPictureInPicture();
- } else {
- this.player_.exitPictureInPicture();
- }
- }
-
- /**
- * Show the `Component`s element if it is hidden by removing the
- * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API.
- */
- show() {
- // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox.
- if (typeof document.exitPictureInPicture !== 'function') {
- return;
- }
- super.show();
- }
- }
-
- /**
- * The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
- Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
-
- /**
- * @file fullscreen-toggle.js
- */
-
- /**
- * Toggle fullscreen video
- *
- * @extends Button
- */
- class FullscreenToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('fullscreen-enter');
- this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
- if (document[player.fsApi_.fullscreenEnabled] === false) {
- this.disable();
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-fullscreen-control ${super.buildCSSClass()}`;
- }
-
- /**
- * Handles fullscreenchange on the player and change control text accordingly.
- *
- * @param {Event} [event]
- * The {@link Player#fullscreenchange} event that caused this function to be
- * called.
- *
- * @listens Player#fullscreenchange
- */
- handleFullscreenChange(event) {
- if (this.player_.isFullscreen()) {
- this.controlText('Exit Fullscreen');
- this.setIcon('fullscreen-exit');
- } else {
- this.controlText('Fullscreen');
- this.setIcon('fullscreen-enter');
- }
- }
-
- /**
- * This gets called when an `FullscreenToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (!this.player_.isFullscreen()) {
- this.player_.requestFullscreen();
- } else {
- this.player_.exitFullscreen();
- }
- }
- }
-
- /**
- * The text that should display over the `FullscreenToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- FullscreenToggle.prototype.controlText_ = 'Fullscreen';
- Component.registerComponent('FullscreenToggle', FullscreenToggle);
-
- /**
- * Check if volume control is supported and if it isn't hide the
- * `Component` that was passed using the `vjs-hidden` class.
- *
- * @param { import('../../component').default } self
- * The component that should be hidden if volume is unsupported
- *
- * @param { import('../../player').default } player
- * A reference to the player
- *
- * @private
- */
- const checkVolumeSupport = function (self, player) {
- // hide volume controls when they're not supported by the current tech
- if (player.tech_ && !player.tech_.featuresVolumeControl) {
- self.addClass('vjs-hidden');
- }
- self.on(player, 'loadstart', function () {
- if (!player.tech_.featuresVolumeControl) {
- self.addClass('vjs-hidden');
- } else {
- self.removeClass('vjs-hidden');
- }
- });
- };
-
- /**
- * @file volume-level.js
- */
-
- /**
- * Shows volume level
- *
- * @extends Component
- */
- class VolumeLevel extends Component {
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-volume-level'
- });
- this.setIcon('circle', el);
- el.appendChild(super.createEl('span', {
- className: 'vjs-control-text'
- }));
- return el;
- }
- }
- Component.registerComponent('VolumeLevel', VolumeLevel);
-
- /**
- * @file volume-level-tooltip.js
- */
-
- /**
- * Volume level tooltips display a volume above or side by side the volume bar.
- *
- * @extends Component
- */
- class VolumeLevelTooltip extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the volume tooltip DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-volume-tooltip'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Updates the position of the tooltip relative to the `VolumeBar` and
- * its content text.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- */
- update(rangeBarRect, rangeBarPoint, vertical, content) {
- if (!vertical) {
- const tooltipRect = getBoundingClientRect(this.el_);
- const playerRect = getBoundingClientRect(this.player_.el());
- const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
- if (!playerRect || !tooltipRect) {
- return;
- }
- const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
- const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
- let pullTooltipBy = tooltipRect.width / 2;
- if (spaceLeftOfPoint < pullTooltipBy) {
- pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
- } else if (spaceRightOfPoint < pullTooltipBy) {
- pullTooltipBy = spaceRightOfPoint;
- }
- if (pullTooltipBy < 0) {
- pullTooltipBy = 0;
- } else if (pullTooltipBy > tooltipRect.width) {
- pullTooltipBy = tooltipRect.width;
- }
- this.el_.style.right = `-${pullTooltipBy}px`;
- }
- this.write(`${content}%`);
- }
-
- /**
- * Write the volume to the tooltip DOM element.
- *
- * @param {string} content
- * The formatted volume for the tooltip.
- */
- write(content) {
- textContent(this.el_, content);
- }
-
- /**
- * Updates the position of the volume tooltip relative to the `VolumeBar`.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- * @param {number} volume
- * The volume level to update the tooltip to
- *
- * @param {Function} cb
- * A function that will be called during the request animation frame
- * for tooltips that need to do additional animations from the default
- */
- updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
- this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
- this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
- if (cb) {
- cb();
- }
- });
- }
- }
- Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
-
- /**
- * @file mouse-volume-level-display.js
- */
-
- /**
- * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
- * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
- * indicating the volume level which is represented by a given point in the
- * {@link VolumeBar}.
- *
- * @extends Component
- */
- class MouseVolumeLevelDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-mouse-display'
- });
- }
-
- /**
- * Enquires updates to its own DOM as well as the DOM of its
- * {@link VolumeLevelTooltip} child.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- */
- update(rangeBarRect, rangeBarPoint, vertical) {
- const volume = 100 * rangeBarPoint;
- this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
- if (vertical) {
- this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
- } else {
- this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
- }
- });
- }
- }
-
- /**
- * Default options for `MouseVolumeLevelDisplay`
- *
- * @type {Object}
- * @private
- */
- MouseVolumeLevelDisplay.prototype.options_ = {
- children: ['volumeLevelTooltip']
- };
- Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
-
- /**
- * @file volume-bar.js
- */
-
- /**
- * The bar that contains the volume level and can be clicked on to adjust the level
- *
- * @extends Slider
- */
- class VolumeBar extends Slider {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on('slideractive', e => this.updateLastVolume_(e));
- this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
- player.ready(() => this.updateARIAAttributes());
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-volume-bar vjs-slider-bar'
- }, {
- 'aria-label': this.localize('Volume Level'),
- 'aria-live': 'polite'
- });
- }
-
- /**
- * Handle mouse down on volume bar
- *
- * @param {Event} event
- * The `mousedown` event that caused this to run.
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- if (!isSingleLeftClick(event)) {
- return;
- }
- super.handleMouseDown(event);
- }
-
- /**
- * Handle movement events on the {@link VolumeMenuButton}.
- *
- * @param {Event} event
- * The event that caused this function to run.
- *
- * @listens mousemove
- */
- handleMouseMove(event) {
- const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
- if (mouseVolumeLevelDisplay) {
- const volumeBarEl = this.el();
- const volumeBarRect = getBoundingClientRect(volumeBarEl);
- const vertical = this.vertical();
- let volumeBarPoint = getPointerPosition(volumeBarEl, event);
- volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
- // The default skin has a gap on either side of the `VolumeBar`. This means
- // that it's possible to trigger this behavior outside the boundaries of
- // the `VolumeBar`. This ensures we stay within it at all times.
- volumeBarPoint = clamp(volumeBarPoint, 0, 1);
- mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
- }
- if (!isSingleLeftClick(event)) {
- return;
- }
- this.checkMuted();
- this.player_.volume(this.calculateDistance(event));
- }
-
- /**
- * If the player is muted unmute it.
- */
- checkMuted() {
- if (this.player_.muted()) {
- this.player_.muted(false);
- }
- }
-
- /**
- * Get percent of volume level
- *
- * @return {number}
- * Volume level percent as a decimal number.
- */
- getPercent() {
- if (this.player_.muted()) {
- return 0;
- }
- return this.player_.volume();
- }
-
- /**
- * Increase volume level for keyboard users
- */
- stepForward() {
- this.checkMuted();
- this.player_.volume(this.player_.volume() + 0.1);
- }
-
- /**
- * Decrease volume level for keyboard users
- */
- stepBack() {
- this.checkMuted();
- this.player_.volume(this.player_.volume() - 0.1);
- }
-
- /**
- * Update ARIA accessibility attributes
- *
- * @param {Event} [event]
- * The `volumechange` event that caused this function to run.
- *
- * @listens Player#volumechange
- */
- updateARIAAttributes(event) {
- const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
- this.el_.setAttribute('aria-valuenow', ariaValue);
- this.el_.setAttribute('aria-valuetext', ariaValue + '%');
- }
-
- /**
- * Returns the current value of the player volume as a percentage
- *
- * @private
- */
- volumeAsPercentage_() {
- return Math.round(this.player_.volume() * 100);
- }
-
- /**
- * When user starts dragging the VolumeBar, store the volume and listen for
- * the end of the drag. When the drag ends, if the volume was set to zero,
- * set lastVolume to the stored volume.
- *
- * @listens slideractive
- * @private
- */
- updateLastVolume_() {
- const volumeBeforeDrag = this.player_.volume();
- this.one('sliderinactive', () => {
- if (this.player_.volume() === 0) {
- this.player_.lastVolume_(volumeBeforeDrag);
- }
- });
- }
- }
-
- /**
- * Default options for the `VolumeBar`
- *
- * @type {Object}
- * @private
- */
- VolumeBar.prototype.options_ = {
- children: ['volumeLevel'],
- barName: 'volumeLevel'
- };
-
- // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
- }
-
- /**
- * Call the update event for this Slider when this event happens on the player.
- *
- * @type {string}
- */
- VolumeBar.prototype.playerEvent = 'volumechange';
- Component.registerComponent('VolumeBar', VolumeBar);
-
- /**
- * @file volume-control.js
- */
-
- /**
- * The component for controlling the volume level
- *
- * @extends Component
- */
- class VolumeControl extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.vertical = options.vertical || false;
-
- // Pass the vertical option down to the VolumeBar if
- // the VolumeBar is turned on.
- if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
- options.volumeBar = options.volumeBar || {};
- options.volumeBar.vertical = options.vertical;
- }
- super(player, options);
-
- // hide this control if volume support is missing
- checkVolumeSupport(this, player);
- this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
- this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
- this.on('mousedown', e => this.handleMouseDown(e));
- this.on('touchstart', e => this.handleMouseDown(e));
- this.on('mousemove', e => this.handleMouseMove(e));
-
- // while the slider is active (the mouse has been pressed down and
- // is dragging) or in focus we do not want to hide the VolumeBar
- this.on(this.volumeBar, ['focus', 'slideractive'], () => {
- this.volumeBar.addClass('vjs-slider-active');
- this.addClass('vjs-slider-active');
- this.trigger('slideractive');
- });
- this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
- this.volumeBar.removeClass('vjs-slider-active');
- this.removeClass('vjs-slider-active');
- this.trigger('sliderinactive');
- });
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- let orientationClass = 'vjs-volume-horizontal';
- if (this.options_.vertical) {
- orientationClass = 'vjs-volume-vertical';
- }
- return super.createEl('div', {
- className: `vjs-volume-control vjs-control ${orientationClass}`
- });
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseDown(event) {
- const doc = this.el_.ownerDocument;
- this.on(doc, 'mousemove', this.throttledHandleMouseMove);
- this.on(doc, 'touchmove', this.throttledHandleMouseMove);
- this.on(doc, 'mouseup', this.handleMouseUpHandler_);
- this.on(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mouseup` or `touchend` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- */
- handleMouseUp(event) {
- const doc = this.el_.ownerDocument;
- this.off(doc, 'mousemove', this.throttledHandleMouseMove);
- this.off(doc, 'touchmove', this.throttledHandleMouseMove);
- this.off(doc, 'mouseup', this.handleMouseUpHandler_);
- this.off(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseMove(event) {
- this.volumeBar.handleMouseMove(event);
- }
- }
-
- /**
- * Default options for the `VolumeControl`
- *
- * @type {Object}
- * @private
- */
- VolumeControl.prototype.options_ = {
- children: ['volumeBar']
- };
- Component.registerComponent('VolumeControl', VolumeControl);
-
- /**
- * Check if muting volume is supported and if it isn't hide the mute toggle
- * button.
- *
- * @param { import('../../component').default } self
- * A reference to the mute toggle button
- *
- * @param { import('../../player').default } player
- * A reference to the player
- *
- * @private
- */
- const checkMuteSupport = function (self, player) {
- // hide mute toggle button if it's not supported by the current tech
- if (player.tech_ && !player.tech_.featuresMuteControl) {
- self.addClass('vjs-hidden');
- }
- self.on(player, 'loadstart', function () {
- if (!player.tech_.featuresMuteControl) {
- self.addClass('vjs-hidden');
- } else {
- self.removeClass('vjs-hidden');
- }
- });
- };
-
- /**
- * @file mute-toggle.js
- */
-
- /**
- * A button component for muting the audio.
- *
- * @extends Button
- */
- class MuteToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
-
- // hide this control if volume support is missing
- checkMuteSupport(this, player);
- this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-mute-control ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when an `MuteToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const vol = this.player_.volume();
- const lastVolume = this.player_.lastVolume_();
- if (vol === 0) {
- const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
- this.player_.volume(volumeToSet);
- this.player_.muted(false);
- } else {
- this.player_.muted(this.player_.muted() ? false : true);
- }
- }
-
- /**
- * Update the `MuteToggle` button based on the state of `volume` and `muted`
- * on the player.
- *
- * @param {Event} [event]
- * The {@link Player#loadstart} event if this function was called
- * through an event.
- *
- * @listens Player#loadstart
- * @listens Player#volumechange
- */
- update(event) {
- this.updateIcon_();
- this.updateControlText_();
- }
-
- /**
- * Update the appearance of the `MuteToggle` icon.
- *
- * Possible states (given `level` variable below):
- * - 0: crossed out
- * - 1: zero bars of volume
- * - 2: one bar of volume
- * - 3: two bars of volume
- *
- * @private
- */
- updateIcon_() {
- const vol = this.player_.volume();
- let level = 3;
- this.setIcon('volume-high');
-
- // in iOS when a player is loaded with muted attribute
- // and volume is changed with a native mute button
- // we want to make sure muted state is updated
- if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
- this.player_.muted(this.player_.tech_.el_.muted);
- }
- if (vol === 0 || this.player_.muted()) {
- this.setIcon('volume-mute');
- level = 0;
- } else if (vol < 0.33) {
- this.setIcon('volume-low');
- level = 1;
- } else if (vol < 0.67) {
- this.setIcon('volume-medium');
- level = 2;
- }
- removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
- addClass(this.el_, `vjs-vol-${level}`);
- }
-
- /**
- * If `muted` has changed on the player, update the control text
- * (`title` attribute on `vjs-mute-control` element and content of
- * `vjs-control-text` element).
- *
- * @private
- */
- updateControlText_() {
- const soundOff = this.player_.muted() || this.player_.volume() === 0;
- const text = soundOff ? 'Unmute' : 'Mute';
- if (this.controlText() !== text) {
- this.controlText(text);
- }
- }
- }
-
- /**
- * The text that should display over the `MuteToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- MuteToggle.prototype.controlText_ = 'Mute';
- Component.registerComponent('MuteToggle', MuteToggle);
-
- /**
- * @file volume-control.js
- */
-
- /**
- * A Component to contain the MuteToggle and VolumeControl so that
- * they can work together.
- *
- * @extends Component
- */
- class VolumePanel extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- if (typeof options.inline !== 'undefined') {
- options.inline = options.inline;
- } else {
- options.inline = true;
- }
-
- // pass the inline option down to the VolumeControl as vertical if
- // the VolumeControl is on.
- if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
- options.volumeControl = options.volumeControl || {};
- options.volumeControl.vertical = !options.inline;
- }
- super(player, options);
-
- // this handler is used by mouse handler methods below
- this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
- this.on(player, ['loadstart'], e => this.volumePanelState_(e));
- this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
- this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
- this.on('keydown', e => this.handleKeyPress(e));
- this.on('mouseover', e => this.handleMouseOver(e));
- this.on('mouseout', e => this.handleMouseOut(e));
-
- // while the slider is active (the mouse has been pressed down and
- // is dragging) we do not want to hide the VolumeBar
- this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
- this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
- }
-
- /**
- * Add vjs-slider-active class to the VolumePanel
- *
- * @listens VolumeControl#slideractive
- * @private
- */
- sliderActive_() {
- this.addClass('vjs-slider-active');
- }
-
- /**
- * Removes vjs-slider-active class to the VolumePanel
- *
- * @listens VolumeControl#sliderinactive
- * @private
- */
- sliderInactive_() {
- this.removeClass('vjs-slider-active');
- }
-
- /**
- * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
- * depending on MuteToggle and VolumeControl state
- *
- * @listens Player#loadstart
- * @private
- */
- volumePanelState_() {
- // hide volume panel if neither volume control or mute toggle
- // are displayed
- if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
- this.addClass('vjs-hidden');
- }
-
- // if only mute toggle is visible we don't want
- // volume panel expanding when hovered or active
- if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
- this.addClass('vjs-mute-toggle-only');
- }
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- let orientationClass = 'vjs-volume-panel-horizontal';
- if (!this.options_.inline) {
- orientationClass = 'vjs-volume-panel-vertical';
- }
- return super.createEl('div', {
- className: `vjs-volume-panel vjs-control ${orientationClass}`
- });
- }
-
- /**
- * Dispose of the `volume-panel` and all child components.
- */
- dispose() {
- this.handleMouseOut();
- super.dispose();
- }
-
- /**
- * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
- * the volume panel and sets focus on `MuteToggle`.
- *
- * @param {Event} event
- * The `keyup` event that caused this function to be called.
- *
- * @listens keyup
- */
- handleVolumeControlKeyUp(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- this.muteToggle.focus();
- }
- }
-
- /**
- * This gets called when a `VolumePanel` gains hover via a `mouseover` event.
- * Turns on listening for `mouseover` event. When they happen it
- * calls `this.handleMouseOver`.
- *
- * @param {Event} event
- * The `mouseover` event that caused this function to be called.
- *
- * @listens mouseover
- */
- handleMouseOver(event) {
- this.addClass('vjs-hover');
- on(document, 'keyup', this.handleKeyPressHandler_);
- }
-
- /**
- * This gets called when a `VolumePanel` gains hover via a `mouseout` event.
- * Turns on listening for `mouseout` event. When they happen it
- * calls `this.handleMouseOut`.
- *
- * @param {Event} event
- * The `mouseout` event that caused this function to be called.
- *
- * @listens mouseout
- */
- handleMouseOut(event) {
- this.removeClass('vjs-hover');
- off(document, 'keyup', this.handleKeyPressHandler_);
- }
-
- /**
- * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
- * looking for ESC, which hides the `VolumeControl`.
- *
- * @param {Event} event
- * The keypress that triggered this event.
- *
- * @listens keydown | keyup
- */
- handleKeyPress(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- this.handleMouseOut();
- }
- }
- }
-
- /**
- * Default options for the `VolumeControl`
- *
- * @type {Object}
- * @private
- */
- VolumePanel.prototype.options_ = {
- children: ['muteToggle', 'volumeControl']
- };
- Component.registerComponent('VolumePanel', VolumePanel);
-
- /**
- * Button to skip forward a configurable amount of time
- * through a video. Renders in the control bar.
- *
- * e.g. options: {controlBar: {skipButtons: forward: 5}}
- *
- * @extends Button
- */
- class SkipForward extends Button {
- constructor(player, options) {
- super(player, options);
- this.validOptions = [5, 10, 30];
- this.skipTime = this.getSkipForwardTime();
- if (this.skipTime && this.validOptions.includes(this.skipTime)) {
- this.setIcon(`forward-${this.skipTime}`);
- this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
- this.show();
- } else {
- this.hide();
- }
- }
- getSkipForwardTime() {
- const playerOptions = this.options_.playerOptions;
- return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
- }
- buildCSSClass() {
- return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
- }
-
- /**
- * On click, skips forward in the duration/seekable range by a configurable amount of seconds.
- * If the time left in the duration/seekable range is less than the configured 'skip forward' time,
- * skips to end of duration/seekable range.
- *
- * Handle a click on a `SkipForward` button
- *
- * @param {EventTarget~Event} event
- * The `click` event that caused this function
- * to be called
- */
- handleClick(event) {
- if (isNaN(this.player_.duration())) {
- return;
- }
- const currentVideoTime = this.player_.currentTime();
- const liveTracker = this.player_.liveTracker;
- const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
- let newTime;
- if (currentVideoTime + this.skipTime <= duration) {
- newTime = currentVideoTime + this.skipTime;
- } else {
- newTime = duration;
- }
- this.player_.currentTime(newTime);
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
- }
- }
- SkipForward.prototype.controlText_ = 'Skip Forward';
- Component.registerComponent('SkipForward', SkipForward);
-
- /**
- * Button to skip backward a configurable amount of time
- * through a video. Renders in the control bar.
- *
- * * e.g. options: {controlBar: {skipButtons: backward: 5}}
- *
- * @extends Button
- */
- class SkipBackward extends Button {
- constructor(player, options) {
- super(player, options);
- this.validOptions = [5, 10, 30];
- this.skipTime = this.getSkipBackwardTime();
- if (this.skipTime && this.validOptions.includes(this.skipTime)) {
- this.setIcon(`replay-${this.skipTime}`);
- this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
- this.show();
- } else {
- this.hide();
- }
- }
- getSkipBackwardTime() {
- const playerOptions = this.options_.playerOptions;
- return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
- }
- buildCSSClass() {
- return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
- }
-
- /**
- * On click, skips backward in the video by a configurable amount of seconds.
- * If the current time in the video is less than the configured 'skip backward' time,
- * skips to beginning of video or seekable range.
- *
- * Handle a click on a `SkipBackward` button
- *
- * @param {EventTarget~Event} event
- * The `click` event that caused this function
- * to be called
- */
- handleClick(event) {
- const currentVideoTime = this.player_.currentTime();
- const liveTracker = this.player_.liveTracker;
- const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
- let newTime;
- if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
- newTime = seekableStart;
- } else if (currentVideoTime >= this.skipTime) {
- newTime = currentVideoTime - this.skipTime;
- } else {
- newTime = 0;
- }
- this.player_.currentTime(newTime);
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
- }
- }
- SkipBackward.prototype.controlText_ = 'Skip Backward';
- Component.registerComponent('SkipBackward', SkipBackward);
-
- /**
- * @file menu.js
- */
-
- /**
- * The Menu component is used to build popup menus, including subtitle and
- * captions selection menus.
- *
- * @extends Component
- */
- class Menu extends Component {
- /**
- * Create an instance of this class.
- *
- * @param { import('../player').default } player
- * the player that this component should attach to
- *
- * @param {Object} [options]
- * Object of option names and values
- *
- */
- constructor(player, options) {
- super(player, options);
- if (options) {
- this.menuButton_ = options.menuButton;
- }
- this.focusedChild_ = -1;
- this.on('keydown', e => this.handleKeyDown(e));
-
- // All the menu item instances share the same blur handler provided by the menu container.
- this.boundHandleBlur_ = e => this.handleBlur(e);
- this.boundHandleTapClick_ = e => this.handleTapClick(e);
- }
-
- /**
- * Add event listeners to the {@link MenuItem}.
- *
- * @param {Object} component
- * The instance of the `MenuItem` to add listeners to.
- *
- */
- addEventListenerForItem(component) {
- if (!(component instanceof Component)) {
- return;
- }
- this.on(component, 'blur', this.boundHandleBlur_);
- this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
- }
-
- /**
- * Remove event listeners from the {@link MenuItem}.
- *
- * @param {Object} component
- * The instance of the `MenuItem` to remove listeners.
- *
- */
- removeEventListenerForItem(component) {
- if (!(component instanceof Component)) {
- return;
- }
- this.off(component, 'blur', this.boundHandleBlur_);
- this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
- }
-
- /**
- * This method will be called indirectly when the component has been added
- * before the component adds to the new menu instance by `addItem`.
- * In this case, the original menu instance will remove the component
- * by calling `removeChild`.
- *
- * @param {Object} component
- * The instance of the `MenuItem`
- */
- removeChild(component) {
- if (typeof component === 'string') {
- component = this.getChild(component);
- }
- this.removeEventListenerForItem(component);
- super.removeChild(component);
- }
-
- /**
- * Add a {@link MenuItem} to the menu.
- *
- * @param {Object|string} component
- * The name or instance of the `MenuItem` to add.
- *
- */
- addItem(component) {
- const childComponent = this.addChild(component);
- if (childComponent) {
- this.addEventListenerForItem(childComponent);
- }
- }
-
- /**
- * Create the `Menu`s DOM element.
- *
- * @return {Element}
- * the element that was created
- */
- createEl() {
- const contentElType = this.options_.contentElType || 'ul';
- this.contentEl_ = createEl(contentElType, {
- className: 'vjs-menu-content'
- });
- this.contentEl_.setAttribute('role', 'menu');
- const el = super.createEl('div', {
- append: this.contentEl_,
- className: 'vjs-menu'
- });
- el.appendChild(this.contentEl_);
-
- // Prevent clicks from bubbling up. Needed for Menu Buttons,
- // where a click on the parent is significant
- on(el, 'click', function (event) {
- event.preventDefault();
- event.stopImmediatePropagation();
- });
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- this.boundHandleBlur_ = null;
- this.boundHandleTapClick_ = null;
- super.dispose();
- }
-
- /**
- * Called when a `MenuItem` loses focus.
- *
- * @param {Event} event
- * The `blur` event that caused this function to be called.
- *
- * @listens blur
- */
- handleBlur(event) {
- const relatedTarget = event.relatedTarget || document.activeElement;
-
- // Close menu popup when a user clicks outside the menu
- if (!this.children().some(element => {
- return element.el() === relatedTarget;
- })) {
- const btn = this.menuButton_;
- if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
- btn.unpressButton();
- }
- }
- }
-
- /**
- * Called when a `MenuItem` gets clicked or tapped.
- *
- * @param {Event} event
- * The `click` or `tap` event that caused this function to be called.
- *
- * @listens click,tap
- */
- handleTapClick(event) {
- // Unpress the associated MenuButton, and move focus back to it
- if (this.menuButton_) {
- this.menuButton_.unpressButton();
- const childComponents = this.children();
- if (!Array.isArray(childComponents)) {
- return;
- }
- const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
- if (!foundComponent) {
- return;
- }
-
- // don't focus menu button if item is a caption settings item
- // because focus will move elsewhere
- if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
- this.menuButton_.focus();
- }
- }
- }
-
- /**
- * Handle a `keydown` event on this menu. This listener is added in the constructor.
- *
- * @param {KeyboardEvent} event
- * A `keydown` event that happened on the menu.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Left and Down Arrows
- if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepForward();
-
- // Up and Right Arrows
- } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepBack();
- }
- }
-
- /**
- * Move to next (lower) menu item for keyboard users.
- */
- stepForward() {
- let stepChild = 0;
- if (this.focusedChild_ !== undefined) {
- stepChild = this.focusedChild_ + 1;
- }
- this.focus(stepChild);
- }
-
- /**
- * Move to previous (higher) menu item for keyboard users.
- */
- stepBack() {
- let stepChild = 0;
- if (this.focusedChild_ !== undefined) {
- stepChild = this.focusedChild_ - 1;
- }
- this.focus(stepChild);
- }
-
- /**
- * Set focus on a {@link MenuItem} in the `Menu`.
- *
- * @param {Object|string} [item=0]
- * Index of child item set focus on.
- */
- focus(item = 0) {
- const children = this.children().slice();
- const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
- if (haveTitle) {
- children.shift();
- }
- if (children.length > 0) {
- if (item < 0) {
- item = 0;
- } else if (item >= children.length) {
- item = children.length - 1;
- }
- this.focusedChild_ = item;
- children[item].el_.focus();
- }
- }
- }
- Component.registerComponent('Menu', Menu);
-
- /**
- * @file menu-button.js
- */
-
- /**
- * A `MenuButton` class for any popup {@link Menu}.
- *
- * @extends Component
- */
- class MenuButton extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- super(player, options);
- this.menuButton_ = new Button(player, options);
- this.menuButton_.controlText(this.controlText_);
- this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
-
- // Add buildCSSClass values to the button, not the wrapper
- const buttonClass = Button.prototype.buildCSSClass();
- this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
- this.menuButton_.removeClass('vjs-control');
- this.addChild(this.menuButton_);
- this.update();
- this.enabled_ = true;
- const handleClick = e => this.handleClick(e);
- this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
- this.on(this.menuButton_, 'tap', handleClick);
- this.on(this.menuButton_, 'click', handleClick);
- this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
- this.on(this.menuButton_, 'mouseenter', () => {
- this.addClass('vjs-hover');
- this.menu.show();
- on(document, 'keyup', this.handleMenuKeyUp_);
- });
- this.on('mouseleave', e => this.handleMouseLeave(e));
- this.on('keydown', e => this.handleSubmenuKeyDown(e));
- }
-
- /**
- * Update the menu based on the current state of its items.
- */
- update() {
- const menu = this.createMenu();
- if (this.menu) {
- this.menu.dispose();
- this.removeChild(this.menu);
- }
- this.menu = menu;
- this.addChild(menu);
-
- /**
- * Track the state of the menu button
- *
- * @type {Boolean}
- * @private
- */
- this.buttonPressed_ = false;
- this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- if (this.items && this.items.length <= this.hideThreshold_) {
- this.hide();
- this.menu.contentEl_.removeAttribute('role');
- } else {
- this.show();
- this.menu.contentEl_.setAttribute('role', 'menu');
- }
- }
-
- /**
- * Create the menu and add all items to it.
- *
- * @return {Menu}
- * The constructed menu
- */
- createMenu() {
- const menu = new Menu(this.player_, {
- menuButton: this
- });
-
- /**
- * Hide the menu if the number of items is less than or equal to this threshold. This defaults
- * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
- * it here because every time we run `createMenu` we need to reset the value.
- *
- * @protected
- * @type {Number}
- */
- this.hideThreshold_ = 0;
-
- // Add a title list item to the top
- if (this.options_.title) {
- const titleEl = createEl('li', {
- className: 'vjs-menu-title',
- textContent: toTitleCase(this.options_.title),
- tabIndex: -1
- });
- const titleComponent = new Component(this.player_, {
- el: titleEl
- });
- menu.addItem(titleComponent);
- }
- this.items = this.createItems();
- if (this.items) {
- // Add menu items to the menu
- for (let i = 0; i < this.items.length; i++) {
- menu.addItem(this.items[i]);
- }
- }
- return menu;
- }
-
- /**
- * Create the list of menu items. Specific to each subclass.
- *
- * @abstract
- */
- createItems() {}
-
- /**
- * Create the `MenuButtons`s DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildWrapperCSSClass()
- }, {});
- }
-
- /**
- * Overwrites the `setIcon` method from `Component`.
- * In this case, we want the icon to be appended to the menuButton.
- *
- * @param {string} name
- * The icon name to be added.
- */
- setIcon(name) {
- super.setIcon(name, this.menuButton_.el_);
- }
-
- /**
- * Allow sub components to stack CSS class names for the wrapper element
- *
- * @return {string}
- * The constructed wrapper DOM `className`
- */
- buildWrapperCSSClass() {
- let menuButtonClass = 'vjs-menu-button';
-
- // If the inline option is passed, we want to use different styles altogether.
- if (this.options_.inline === true) {
- menuButtonClass += '-inline';
- } else {
- menuButtonClass += '-popup';
- }
-
- // TODO: Fix the CSS so that this isn't necessary
- const buttonClass = Button.prototype.buildCSSClass();
- return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- let menuButtonClass = 'vjs-menu-button';
-
- // If the inline option is passed, we want to use different styles altogether.
- if (this.options_.inline === true) {
- menuButtonClass += '-inline';
- } else {
- menuButtonClass += '-popup';
- }
- return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
- }
-
- /**
- * Get or set the localized control text that will be used for accessibility.
- *
- * > NOTE: This will come from the internal `menuButton_` element.
- *
- * @param {string} [text]
- * Control text for element.
- *
- * @param {Element} [el=this.menuButton_.el()]
- * Element to set the title on.
- *
- * @return {string}
- * - The control text when getting
- */
- controlText(text, el = this.menuButton_.el()) {
- return this.menuButton_.controlText(text, el);
- }
-
- /**
- * Dispose of the `menu-button` and all child components.
- */
- dispose() {
- this.handleMouseLeave();
- super.dispose();
- }
-
- /**
- * Handle a click on a `MenuButton`.
- * See {@link ClickableComponent#handleClick} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (this.buttonPressed_) {
- this.unpressButton();
- } else {
- this.pressButton();
- }
- }
-
- /**
- * Handle `mouseleave` for `MenuButton`.
- *
- * @param {Event} event
- * The `mouseleave` event that caused this function to be called.
- *
- * @listens mouseleave
- */
- handleMouseLeave(event) {
- this.removeClass('vjs-hover');
- off(document, 'keyup', this.handleMenuKeyUp_);
- }
-
- /**
- * Set the focus to the actual button, not to this element
- */
- focus() {
- this.menuButton_.focus();
- }
-
- /**
- * Remove the focus from the actual button, not this element
- */
- blur() {
- this.menuButton_.blur();
- }
-
- /**
- * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
- * {@link ClickableComponent#handleKeyDown} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Escape or Tab unpress the 'button'
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- if (this.buttonPressed_) {
- this.unpressButton();
- }
-
- // Don't preventDefault for Tab key - we still want to lose focus
- if (!keycode.isEventKey(event, 'Tab')) {
- event.preventDefault();
- // Set focus back to the menu button's button
- this.menuButton_.focus();
- }
- // Up Arrow or Down Arrow also 'press' the button to open the menu
- } else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) {
- if (!this.buttonPressed_) {
- event.preventDefault();
- this.pressButton();
- }
- }
- }
-
- /**
- * Handle a `keyup` event on a `MenuButton`. The listener for this is added in
- * the constructor.
- *
- * @param {Event} event
- * Key press event
- *
- * @listens keyup
- */
- handleMenuKeyUp(event) {
- // Escape hides popup menu
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- this.removeClass('vjs-hover');
- }
- }
-
- /**
- * This method name now delegates to `handleSubmenuKeyDown`. This means
- * anyone calling `handleSubmenuKeyPress` will not see their method calls
- * stop working.
- *
- * @param {Event} event
- * The event that caused this function to be called.
- */
- handleSubmenuKeyPress(event) {
- this.handleSubmenuKeyDown(event);
- }
-
- /**
- * Handle a `keydown` event on a sub-menu. The listener for this is added in
- * the constructor.
- *
- * @param {Event} event
- * Key press event
- *
- * @listens keydown
- */
- handleSubmenuKeyDown(event) {
- // Escape or Tab unpress the 'button'
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- if (this.buttonPressed_) {
- this.unpressButton();
- }
- // Don't preventDefault for Tab key - we still want to lose focus
- if (!keycode.isEventKey(event, 'Tab')) {
- event.preventDefault();
- // Set focus back to the menu button's button
- this.menuButton_.focus();
- }
- }
- }
-
- /**
- * Put the current `MenuButton` into a pressed state.
- */
- pressButton() {
- if (this.enabled_) {
- this.buttonPressed_ = true;
- this.menu.show();
- this.menu.lockShowing();
- this.menuButton_.el_.setAttribute('aria-expanded', 'true');
-
- // set the focus into the submenu, except on iOS where it is resulting in
- // undesired scrolling behavior when the player is in an iframe
- if (IS_IOS && isInFrame()) {
- // Return early so that the menu isn't focused
- return;
- }
- this.menu.focus();
- }
- }
-
- /**
- * Take the current `MenuButton` out of a pressed state.
- */
- unpressButton() {
- if (this.enabled_) {
- this.buttonPressed_ = false;
- this.menu.unlockShowing();
- this.menu.hide();
- this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- }
- }
-
- /**
- * Disable the `MenuButton`. Don't allow it to be clicked.
- */
- disable() {
- this.unpressButton();
- this.enabled_ = false;
- this.addClass('vjs-disabled');
- this.menuButton_.disable();
- }
-
- /**
- * Enable the `MenuButton`. Allow it to be clicked.
- */
- enable() {
- this.enabled_ = true;
- this.removeClass('vjs-disabled');
- this.menuButton_.enable();
- }
- }
- Component.registerComponent('MenuButton', MenuButton);
-
- /**
- * @file track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific track types (e.g. subtitles).
- *
- * @extends MenuButton
- */
- class TrackButton extends MenuButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const tracks = options.tracks;
- super(player, options);
- if (this.items.length <= 1) {
- this.hide();
- }
- if (!tracks) {
- return;
- }
- const updateHandler = bind_(this, this.update);
- tracks.addEventListener('removetrack', updateHandler);
- tracks.addEventListener('addtrack', updateHandler);
- tracks.addEventListener('labelchange', updateHandler);
- this.player_.on('ready', updateHandler);
- this.player_.on('dispose', function () {
- tracks.removeEventListener('removetrack', updateHandler);
- tracks.removeEventListener('addtrack', updateHandler);
- tracks.removeEventListener('labelchange', updateHandler);
- });
- }
- }
- Component.registerComponent('TrackButton', TrackButton);
-
- /**
- * @file menu-keys.js
- */
-
- /**
- * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`)
- * Note that 'Enter' and 'Space' are not included here (otherwise they would
- * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable)
- *
- * @typedef MenuKeys
- * @array
- */
- const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left'];
-
- /**
- * @file menu-item.js
- */
-
- /**
- * The component for a menu item. ``
- *
- * @extends ClickableComponent
- */
- class MenuItem extends ClickableComponent {
- /**
- * Creates an instance of the this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- *
- */
- constructor(player, options) {
- super(player, options);
- this.selectable = options.selectable;
- this.isSelected_ = options.selected || false;
- this.multiSelectable = options.multiSelectable;
- this.selected(this.isSelected_);
- if (this.selectable) {
- if (this.multiSelectable) {
- this.el_.setAttribute('role', 'menuitemcheckbox');
- } else {
- this.el_.setAttribute('role', 'menuitemradio');
- }
- } else {
- this.el_.setAttribute('role', 'menuitem');
- }
- }
-
- /**
- * Create the `MenuItem's DOM element
- *
- * @param {string} [type=li]
- * Element's node type, not actually used, always set to `li`.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element
- *
- * @param {Object} [attrs={}]
- * An object of attributes that should be set on the element
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(type, props, attrs) {
- // The control is textual, not just an icon
- this.nonIconControl = true;
- const el = super.createEl('li', Object.assign({
- className: 'vjs-menu-item',
- tabIndex: -1
- }, props), attrs);
-
- // swap icon with menu item text.
- const menuItemEl = createEl('span', {
- className: 'vjs-menu-item-text',
- textContent: this.localize(this.options_.label)
- });
-
- // If using SVG icons, the element with vjs-icon-placeholder will be added separately.
- if (this.player_.options_.experimentalSvgIcons) {
- el.appendChild(menuItemEl);
- } else {
- el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder'));
- }
- return el;
- }
-
- /**
- * Ignore keys which are used by the menu, but pass any other ones up. See
- * {@link ClickableComponent#handleKeyDown} for instances where this is called.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- if (!MenuKeys.some(key => keycode.isEventKey(event, key))) {
- // Pass keydown handling up for unused keys
- super.handleKeyDown(event);
- }
- }
-
- /**
- * Any click on a `MenuItem` puts it into the selected state.
- * See {@link ClickableComponent#handleClick} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- this.selected(true);
- }
-
- /**
- * Set the state for this menu item as selected or not.
- *
- * @param {boolean} selected
- * if the menu item is selected or not
- */
- selected(selected) {
- if (this.selectable) {
- if (selected) {
- this.addClass('vjs-selected');
- this.el_.setAttribute('aria-checked', 'true');
- // aria-checked isn't fully supported by browsers/screen readers,
- // so indicate selected state to screen reader in the control text.
- this.controlText(', selected');
- this.isSelected_ = true;
- } else {
- this.removeClass('vjs-selected');
- this.el_.setAttribute('aria-checked', 'false');
- // Indicate un-selected state to screen reader
- this.controlText('');
- this.isSelected_ = false;
- }
- }
- }
- }
- Component.registerComponent('MenuItem', MenuItem);
-
- /**
- * @file text-track-menu-item.js
- */
-
- /**
- * The specific menu item type for selecting a language within a text track kind
- *
- * @extends MenuItem
- */
- class TextTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const tracks = player.textTracks();
-
- // Modify options for parent MenuItem class's init.
- options.label = track.label || track.language || 'Unknown';
- options.selected = track.mode === 'showing';
- super(player, options);
- this.track = track;
- // Determine the relevant kind(s) of tracks for this component and filter
- // out empty kinds.
- this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
- const changeHandler = (...args) => {
- this.handleTracksChange.apply(this, args);
- };
- const selectedLanguageChangeHandler = (...args) => {
- this.handleSelectedLanguageChange.apply(this, args);
- };
- player.on(['loadstart', 'texttrackchange'], changeHandler);
- tracks.addEventListener('change', changeHandler);
- tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
- this.on('dispose', function () {
- player.off(['loadstart', 'texttrackchange'], changeHandler);
- tracks.removeEventListener('change', changeHandler);
- tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
- });
-
- // iOS7 doesn't dispatch change events to TextTrackLists when an
- // associated track's mode changes. Without something like
- // Object.observe() (also not present on iOS7), it's not
- // possible to detect changes to the mode attribute and polyfill
- // the change event. As a poor substitute, we manually dispatch
- // change events whenever the controls modify the mode.
- if (tracks.onchange === undefined) {
- let event;
- this.on(['tap', 'click'], function () {
- if (typeof window.Event !== 'object') {
- // Android 2.3 throws an Illegal Constructor error for window.Event
- try {
- event = new window.Event('change');
- } catch (err) {
- // continue regardless of error
- }
- }
- if (!event) {
- event = document.createEvent('Event');
- event.initEvent('change', true, true);
- }
- tracks.dispatchEvent(event);
- });
- }
-
- // set the default state based on current tracks
- this.handleTracksChange();
- }
-
- /**
- * This gets called when an `TextTrackMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const referenceTrack = this.track;
- const tracks = this.player_.textTracks();
- super.handleClick(event);
- if (!tracks) {
- return;
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // If the track from the text tracks list is not of the right kind,
- // skip it. We do not want to affect tracks of incompatible kind(s).
- if (this.kinds.indexOf(track.kind) === -1) {
- continue;
- }
-
- // If this text track is the component's track and it is not showing,
- // set it to showing.
- if (track === referenceTrack) {
- if (track.mode !== 'showing') {
- track.mode = 'showing';
- }
-
- // If this text track is not the component's track and it is not
- // disabled, set it to disabled.
- } else if (track.mode !== 'disabled') {
- track.mode = 'disabled';
- }
- }
- }
-
- /**
- * Handle text track list change
- *
- * @param {Event} event
- * The `change` event that caused this function to be called.
- *
- * @listens TextTrackList#change
- */
- handleTracksChange(event) {
- const shouldBeSelected = this.track.mode === 'showing';
-
- // Prevent redundant selected() calls because they may cause
- // screen readers to read the appended control text unnecessarily
- if (shouldBeSelected !== this.isSelected_) {
- this.selected(shouldBeSelected);
- }
- }
- handleSelectedLanguageChange(event) {
- if (this.track.mode === 'showing') {
- const selectedLanguage = this.player_.cache_.selectedLanguage;
-
- // Don't replace the kind of track across the same language
- if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
- return;
- }
- this.player_.cache_.selectedLanguage = {
- enabled: true,
- language: this.track.language,
- kind: this.track.kind
- };
- }
- }
- dispose() {
- // remove reference to track object on dispose
- this.track = null;
- super.dispose();
- }
- }
- Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
-
- /**
- * @file off-text-track-menu-item.js
- */
-
- /**
- * A special menu item for turning off a specific type of text track
- *
- * @extends TextTrackMenuItem
- */
- class OffTextTrackMenuItem extends TextTrackMenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- // Create pseudo track info
- // Requires options['kind']
- options.track = {
- player,
- // it is no longer necessary to store `kind` or `kinds` on the track itself
- // since they are now stored in the `kinds` property of all instances of
- // TextTrackMenuItem, but this will remain for backwards compatibility
- kind: options.kind,
- kinds: options.kinds,
- default: false,
- mode: 'disabled'
- };
- if (!options.kinds) {
- options.kinds = [options.kind];
- }
- if (options.label) {
- options.track.label = options.label;
- } else {
- options.track.label = options.kinds.join(' and ') + ' off';
- }
-
- // MenuItem is selectable
- options.selectable = true;
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- options.multiSelectable = false;
- super(player, options);
- }
-
- /**
- * Handle text track change
- *
- * @param {Event} event
- * The event that caused this function to run
- */
- handleTracksChange(event) {
- const tracks = this.player().textTracks();
- let shouldBeSelected = true;
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
- shouldBeSelected = false;
- break;
- }
- }
-
- // Prevent redundant selected() calls because they may cause
- // screen readers to read the appended control text unnecessarily
- if (shouldBeSelected !== this.isSelected_) {
- this.selected(shouldBeSelected);
- }
- }
- handleSelectedLanguageChange(event) {
- const tracks = this.player().textTracks();
- let allHidden = true;
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
- allHidden = false;
- break;
- }
- }
- if (allHidden) {
- this.player_.cache_.selectedLanguage = {
- enabled: false
- };
- }
- }
-
- /**
- * Update control text and label on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
- super.handleLanguagechange();
- }
- }
- Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
-
- /**
- * @file text-track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific text track types (e.g. subtitles)
- *
- * @extends MenuButton
- */
- class TextTrackButton extends TrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.tracks = player.textTracks();
- super(player, options);
- }
-
- /**
- * Create a menu item for each text track
- *
- * @param {TextTrackMenuItem[]} [items=[]]
- * Existing array of items to use during creation
- *
- * @return {TextTrackMenuItem[]}
- * Array of menu items that were created
- */
- createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
- // Label is an override for the [track] off label
- // USed to localise captions/subtitles
- let label;
- if (this.label_) {
- label = `${this.label_} off`;
- }
- // Add an OFF menu item to turn all tracks off
- items.push(new OffTextTrackMenuItem(this.player_, {
- kinds: this.kinds_,
- kind: this.kind_,
- label
- }));
- this.hideThreshold_ += 1;
- const tracks = this.player_.textTracks();
- if (!Array.isArray(this.kinds_)) {
- this.kinds_ = [this.kind_];
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // only add tracks that are of an appropriate kind and have a label
- if (this.kinds_.indexOf(track.kind) > -1) {
- const item = new TrackMenuItem(this.player_, {
- track,
- kinds: this.kinds_,
- kind: this.kind_,
- // MenuItem is selectable
- selectable: true,
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- multiSelectable: false
- });
- item.addClass(`vjs-${track.kind}-menu-item`);
- items.push(item);
- }
- }
- return items;
- }
- }
- Component.registerComponent('TextTrackButton', TextTrackButton);
-
- /**
- * @file chapters-track-menu-item.js
- */
-
- /**
- * The chapter track menu item
- *
- * @extends MenuItem
- */
- class ChaptersTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const cue = options.cue;
- const currentTime = player.currentTime();
-
- // Modify options for parent MenuItem class's init.
- options.selectable = true;
- options.multiSelectable = false;
- options.label = cue.text;
- options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
- super(player, options);
- this.track = track;
- this.cue = cue;
- }
-
- /**
- * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick();
- this.player_.currentTime(this.cue.startTime);
- }
- }
- Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
-
- /**
- * @file chapters-button.js
- */
-
- /**
- * The button component for toggling and selecting chapters
- * Chapters act much differently than other text tracks
- * Cues are navigation vs. other tracks of alternative languages
- *
- * @extends TextTrackButton
- */
- class ChaptersButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this function is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('chapters');
- this.selectCurrentItem_ = () => {
- this.items.forEach(item => {
- item.selected(this.track_.activeCues[0] === item.cue);
- });
- };
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-chapters-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Update the menu based on the current state of its items.
- *
- * @param {Event} [event]
- * An event that triggered this function to run.
- *
- * @listens TextTrackList#addtrack
- * @listens TextTrackList#removetrack
- * @listens TextTrackList#change
- */
- update(event) {
- if (event && event.track && event.track.kind !== 'chapters') {
- return;
- }
- const track = this.findChaptersTrack();
- if (track !== this.track_) {
- this.setTrack(track);
- super.update();
- } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
- // Update the menu initially or if the number of cues has changed since set
- super.update();
- }
- }
-
- /**
- * Set the currently selected track for the chapters button.
- *
- * @param {TextTrack} track
- * The new track to select. Nothing will change if this is the currently selected
- * track.
- */
- setTrack(track) {
- if (this.track_ === track) {
- return;
- }
- if (!this.updateHandler_) {
- this.updateHandler_ = this.update.bind(this);
- }
-
- // here this.track_ refers to the old track instance
- if (this.track_) {
- const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
- if (remoteTextTrackEl) {
- remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
- }
- this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
- this.track_ = null;
- }
- this.track_ = track;
-
- // here this.track_ refers to the new track instance
- if (this.track_) {
- this.track_.mode = 'hidden';
- const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
- if (remoteTextTrackEl) {
- remoteTextTrackEl.addEventListener('load', this.updateHandler_);
- }
- this.track_.addEventListener('cuechange', this.selectCurrentItem_);
- }
- }
-
- /**
- * Find the track object that is currently in use by this ChaptersButton
- *
- * @return {TextTrack|undefined}
- * The current track or undefined if none was found.
- */
- findChaptersTrack() {
- const tracks = this.player_.textTracks() || [];
- for (let i = tracks.length - 1; i >= 0; i--) {
- // We will always choose the last track as our chaptersTrack
- const track = tracks[i];
- if (track.kind === this.kind_) {
- return track;
- }
- }
- }
-
- /**
- * Get the caption for the ChaptersButton based on the track label. This will also
- * use the current tracks localized kind as a fallback if a label does not exist.
- *
- * @return {string}
- * The tracks current label or the localized track kind.
- */
- getMenuCaption() {
- if (this.track_ && this.track_.label) {
- return this.track_.label;
- }
- return this.localize(toTitleCase(this.kind_));
- }
-
- /**
- * Create menu from chapter track
- *
- * @return { import('../../menu/menu').default }
- * New menu for the chapter buttons
- */
- createMenu() {
- this.options_.title = this.getMenuCaption();
- return super.createMenu();
- }
-
- /**
- * Create a menu item for each text track
- *
- * @return { import('./text-track-menu-item').default[] }
- * Array of menu items
- */
- createItems() {
- const items = [];
- if (!this.track_) {
- return items;
- }
- const cues = this.track_.cues;
- if (!cues) {
- return items;
- }
- for (let i = 0, l = cues.length; i < l; i++) {
- const cue = cues[i];
- const mi = new ChaptersTrackMenuItem(this.player_, {
- track: this.track_,
- cue
- });
- items.push(mi);
- }
- return items;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- ChaptersButton.prototype.kind_ = 'chapters';
-
- /**
- * The text that should display over the `ChaptersButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- ChaptersButton.prototype.controlText_ = 'Chapters';
- Component.registerComponent('ChaptersButton', ChaptersButton);
-
- /**
- * @file descriptions-button.js
- */
-
- /**
- * The button component for toggling and selecting descriptions
- *
- * @extends TextTrackButton
- */
- class DescriptionsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('audio-description');
- const tracks = player.textTracks();
- const changeHandler = bind_(this, this.handleTracksChange);
- tracks.addEventListener('change', changeHandler);
- this.on('dispose', function () {
- tracks.removeEventListener('change', changeHandler);
- });
- }
-
- /**
- * Handle text track change
- *
- * @param {Event} event
- * The event that caused this function to run
- *
- * @listens TextTrackList#change
- */
- handleTracksChange(event) {
- const tracks = this.player().textTracks();
- let disabled = false;
-
- // Check whether a track of a different kind is showing
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (track.kind !== this.kind_ && track.mode === 'showing') {
- disabled = true;
- break;
- }
- }
-
- // If another track is showing, disable this menu button
- if (disabled) {
- this.disable();
- } else {
- this.enable();
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-descriptions-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- DescriptionsButton.prototype.kind_ = 'descriptions';
-
- /**
- * The text that should display over the `DescriptionsButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- DescriptionsButton.prototype.controlText_ = 'Descriptions';
- Component.registerComponent('DescriptionsButton', DescriptionsButton);
-
- /**
- * @file subtitles-button.js
- */
-
- /**
- * The button component for toggling and selecting subtitles
- *
- * @extends TextTrackButton
- */
- class SubtitlesButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('subtitles');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-subtitles-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- SubtitlesButton.prototype.kind_ = 'subtitles';
-
- /**
- * The text that should display over the `SubtitlesButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- SubtitlesButton.prototype.controlText_ = 'Subtitles';
- Component.registerComponent('SubtitlesButton', SubtitlesButton);
-
- /**
- * @file caption-settings-menu-item.js
- */
-
- /**
- * The menu item for caption track settings menu
- *
- * @extends TextTrackMenuItem
- */
- class CaptionSettingsMenuItem extends TextTrackMenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- options.track = {
- player,
- kind: options.kind,
- label: options.kind + ' settings',
- selectable: false,
- default: false,
- mode: 'disabled'
- };
-
- // CaptionSettingsMenuItem has no concept of 'selected'
- options.selectable = false;
- options.name = 'CaptionSettingsMenuItem';
- super(player, options);
- this.addClass('vjs-texttrack-settings');
- this.controlText(', opens ' + options.kind + ' settings dialog');
- }
-
- /**
- * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- this.player().getChild('textTrackSettings').open();
- }
-
- /**
- * Update control text and label on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
- super.handleLanguagechange();
- }
- }
- Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
-
- /**
- * @file captions-button.js
- */
-
- /**
- * The button component for toggling and selecting captions
- *
- * @extends TextTrackButton
- */
- class CaptionsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('captions');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-captions-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create caption menu items
- *
- * @return {CaptionSettingsMenuItem[]}
- * The array of current menu items.
- */
- createItems() {
- const items = [];
- if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
- items.push(new CaptionSettingsMenuItem(this.player_, {
- kind: this.kind_
- }));
- this.hideThreshold_ += 1;
- }
- return super.createItems(items);
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- CaptionsButton.prototype.kind_ = 'captions';
-
- /**
- * The text that should display over the `CaptionsButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- CaptionsButton.prototype.controlText_ = 'Captions';
- Component.registerComponent('CaptionsButton', CaptionsButton);
-
- /**
- * @file subs-caps-menu-item.js
- */
-
- /**
- * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
- * in the SubsCapsMenu.
- *
- * @extends TextTrackMenuItem
- */
- class SubsCapsMenuItem extends TextTrackMenuItem {
- createEl(type, props, attrs) {
- const el = super.createEl(type, props, attrs);
- const parentSpan = el.querySelector('.vjs-menu-item-text');
- if (this.options_.track.kind === 'captions') {
- if (this.player_.options_.experimentalSvgIcons) {
- this.setIcon('captions', el);
- } else {
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-control-text',
- // space added as the text will visually flow with the
- // label
- textContent: ` ${this.localize('Captions')}`
- }));
- }
- return el;
- }
- }
- Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
-
- /**
- * @file sub-caps-button.js
- */
-
- /**
- * The button component for toggling and selecting captions and/or subtitles
- *
- * @extends TextTrackButton
- */
- class SubsCapsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options = {}) {
- super(player, options);
-
- // Although North America uses "captions" in most cases for
- // "captions and subtitles" other locales use "subtitles"
- this.label_ = 'subtitles';
- this.setIcon('subtitles');
- if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
- this.label_ = 'captions';
- this.setIcon('captions');
- }
- this.menuButton_.controlText(toTitleCase(this.label_));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-subs-caps-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create caption/subtitles menu items
- *
- * @return {CaptionSettingsMenuItem[]}
- * The array of current menu items.
- */
- createItems() {
- let items = [];
- if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
- items.push(new CaptionSettingsMenuItem(this.player_, {
- kind: this.label_
- }));
- this.hideThreshold_ += 1;
- }
- items = super.createItems(items, SubsCapsMenuItem);
- return items;
- }
- }
-
- /**
- * `kind`s of TextTrack to look for to associate it with this menu.
- *
- * @type {array}
- * @private
- */
- SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
-
- /**
- * The text that should display over the `SubsCapsButton`s controls.
- *
- *
- * @type {string}
- * @protected
- */
- SubsCapsButton.prototype.controlText_ = 'Subtitles';
- Component.registerComponent('SubsCapsButton', SubsCapsButton);
-
- /**
- * @file audio-track-menu-item.js
- */
-
- /**
- * An {@link AudioTrack} {@link MenuItem}
- *
- * @extends MenuItem
- */
- class AudioTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const tracks = player.audioTracks();
-
- // Modify options for parent MenuItem class's init.
- options.label = track.label || track.language || 'Unknown';
- options.selected = track.enabled;
- super(player, options);
- this.track = track;
- this.addClass(`vjs-${track.kind}-menu-item`);
- const changeHandler = (...args) => {
- this.handleTracksChange.apply(this, args);
- };
- tracks.addEventListener('change', changeHandler);
- this.on('dispose', () => {
- tracks.removeEventListener('change', changeHandler);
- });
- }
- createEl(type, props, attrs) {
- const el = super.createEl(type, props, attrs);
- const parentSpan = el.querySelector('.vjs-menu-item-text');
- if (['main-desc', 'description'].indexOf(this.options_.track.kind) >= 0) {
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-control-text',
- textContent: ' ' + this.localize('Descriptions')
- }));
- }
- return el;
- }
-
- /**
- * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
- * for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick(event);
-
- // the audio track list will automatically toggle other tracks
- // off for us.
- this.track.enabled = true;
-
- // when native audio tracks are used, we want to make sure that other tracks are turned off
- if (this.player_.tech_.featuresNativeAudioTracks) {
- const tracks = this.player_.audioTracks();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // skip the current track since we enabled it above
- if (track === this.track) {
- continue;
- }
- track.enabled = track === this.track;
- }
- }
- }
-
- /**
- * Handle any {@link AudioTrack} change.
- *
- * @param {Event} [event]
- * The {@link AudioTrackList#change} event that caused this to run.
- *
- * @listens AudioTrackList#change
- */
- handleTracksChange(event) {
- this.selected(this.track.enabled);
- }
- }
- Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
-
- /**
- * @file audio-track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific {@link AudioTrack} types.
- *
- * @extends TrackButton
- */
- class AudioTrackButton extends TrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param {Player} player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.tracks = player.audioTracks();
- super(player, options);
- this.setIcon('audio');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-audio-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create a menu item for each audio track
- *
- * @param {AudioTrackMenuItem[]} [items=[]]
- * An array of existing menu items to use.
- *
- * @return {AudioTrackMenuItem[]}
- * An array of menu items
- */
- createItems(items = []) {
- // if there's only one audio track, there no point in showing it
- this.hideThreshold_ = 1;
- const tracks = this.player_.audioTracks();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- items.push(new AudioTrackMenuItem(this.player_, {
- track,
- // MenuItem is selectable
- selectable: true,
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- multiSelectable: false
- }));
- }
- return items;
- }
- }
-
- /**
- * The text that should display over the `AudioTrackButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- AudioTrackButton.prototype.controlText_ = 'Audio Track';
- Component.registerComponent('AudioTrackButton', AudioTrackButton);
-
- /**
- * @file playback-rate-menu-item.js
- */
-
- /**
- * The specific menu item type for selecting a playback rate.
- *
- * @extends MenuItem
- */
- class PlaybackRateMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const label = options.rate;
- const rate = parseFloat(label, 10);
-
- // Modify options for parent MenuItem class's init.
- options.label = label;
- options.selected = rate === player.playbackRate();
- options.selectable = true;
- options.multiSelectable = false;
- super(player, options);
- this.label = label;
- this.rate = rate;
- this.on(player, 'ratechange', e => this.update(e));
- }
-
- /**
- * This gets called when an `PlaybackRateMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick();
- this.player().playbackRate(this.rate);
- }
-
- /**
- * Update the PlaybackRateMenuItem when the playbackrate changes.
- *
- * @param {Event} [event]
- * The `ratechange` event that caused this function to run.
- *
- * @listens Player#ratechange
- */
- update(event) {
- this.selected(this.player().playbackRate() === this.rate);
- }
- }
-
- /**
- * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
- *
- * @type {string}
- * @private
- */
- PlaybackRateMenuItem.prototype.contentElType = 'button';
- Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
-
- /**
- * @file playback-rate-menu-button.js
- */
-
- /**
- * The component for controlling the playback rate.
- *
- * @extends MenuButton
- */
- class PlaybackRateMenuButton extends MenuButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
- this.updateVisibility();
- this.updateLabel();
- this.on(player, 'loadstart', e => this.updateVisibility(e));
- this.on(player, 'ratechange', e => this.updateLabel(e));
- this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl();
- this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
- this.labelEl_ = createEl('div', {
- className: 'vjs-playback-rate-value',
- id: this.labelElId_,
- textContent: '1x'
- });
- el.appendChild(this.labelEl_);
- return el;
- }
- dispose() {
- this.labelEl_ = null;
- super.dispose();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-playback-rate ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create the list of menu items. Specific to each subclass.
- *
- */
- createItems() {
- const rates = this.playbackRates();
- const items = [];
- for (let i = rates.length - 1; i >= 0; i--) {
- items.push(new PlaybackRateMenuItem(this.player(), {
- rate: rates[i] + 'x'
- }));
- }
- return items;
- }
-
- /**
- * On playbackrateschange, update the menu to account for the new items.
- *
- * @listens Player#playbackrateschange
- */
- handlePlaybackRateschange(event) {
- this.update();
- }
-
- /**
- * Get possible playback rates
- *
- * @return {Array}
- * All possible playback rates
- */
- playbackRates() {
- const player = this.player();
- return player.playbackRates && player.playbackRates() || [];
- }
-
- /**
- * Get whether playback rates is supported by the tech
- * and an array of playback rates exists
- *
- * @return {boolean}
- * Whether changing playback rate is supported
- */
- playbackRateSupported() {
- return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
- }
-
- /**
- * Hide playback rate controls when they're no playback rate options to select
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#loadstart
- */
- updateVisibility(event) {
- if (this.playbackRateSupported()) {
- this.removeClass('vjs-hidden');
- } else {
- this.addClass('vjs-hidden');
- }
- }
-
- /**
- * Update button label when rate changed
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#ratechange
- */
- updateLabel(event) {
- if (this.playbackRateSupported()) {
- this.labelEl_.textContent = this.player().playbackRate() + 'x';
- }
- }
- }
-
- /**
- * The text that should display over the `PlaybackRateMenuButton`s controls.
- *
- * Added for localization.
- *
- * @type {string}
- * @protected
- */
- PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
- Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
-
- /**
- * @file spacer.js
- */
-
- /**
- * Just an empty spacer element that can be used as an append point for plugins, etc.
- * Also can be used to create space between elements when necessary.
- *
- * @extends Component
- */
- class Spacer extends Component {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-spacer ${super.buildCSSClass()}`;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl(tag = 'div', props = {}, attributes = {}) {
- if (!props.className) {
- props.className = this.buildCSSClass();
- }
- return super.createEl(tag, props, attributes);
- }
- }
- Component.registerComponent('Spacer', Spacer);
-
- /**
- * @file custom-control-spacer.js
- */
-
- /**
- * Spacer specifically meant to be used as an insertion point for new plugins, etc.
- *
- * @extends Spacer
- */
- class CustomControlSpacer extends Spacer {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildCSSClass(),
- // No-flex/table-cell mode requires there be some content
- // in the cell to fill the remaining space of the table.
- textContent: '\u00a0'
- });
- }
- }
- Component.registerComponent('CustomControlSpacer', CustomControlSpacer);
-
- /**
- * @file control-bar.js
- */
-
- /**
- * Container of main controls.
- *
- * @extends Component
- */
- class ControlBar extends Component {
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-control-bar',
- dir: 'ltr'
- });
- }
- }
-
- /**
- * Default options for `ControlBar`
- *
- * @type {Object}
- * @private
- */
- ControlBar.prototype.options_ = {
- children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
- };
- Component.registerComponent('ControlBar', ControlBar);
-
- /**
- * @file error-display.js
- */
-
- /**
- * A display that indicates an error has occurred. This means that the video
- * is unplayable.
- *
- * @extends ModalDialog
- */
- class ErrorDisplay extends ModalDialog {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, 'error', e => {
- this.open(e);
- });
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- *
- * @deprecated Since version 5.
- */
- buildCSSClass() {
- return `vjs-error-display ${super.buildCSSClass()}`;
- }
-
- /**
- * Gets the localized error message based on the `Player`s error.
- *
- * @return {string}
- * The `Player`s error message localized or an empty string.
- */
- content() {
- const error = this.player().error();
- return error ? this.localize(error.message) : '';
- }
- }
-
- /**
- * The default options for an `ErrorDisplay`.
- *
- * @private
- */
- ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
- pauseOnOpen: false,
- fillAlways: true,
- temporary: false,
- uncloseable: true
- });
- Component.registerComponent('ErrorDisplay', ErrorDisplay);
-
- /**
- * @file text-track-settings.js
- */
- const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
- const COLOR_BLACK = ['#000', 'Black'];
- const COLOR_BLUE = ['#00F', 'Blue'];
- const COLOR_CYAN = ['#0FF', 'Cyan'];
- const COLOR_GREEN = ['#0F0', 'Green'];
- const COLOR_MAGENTA = ['#F0F', 'Magenta'];
- const COLOR_RED = ['#F00', 'Red'];
- const COLOR_WHITE = ['#FFF', 'White'];
- const COLOR_YELLOW = ['#FF0', 'Yellow'];
- const OPACITY_OPAQUE = ['1', 'Opaque'];
- const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
- const OPACITY_TRANS = ['0', 'Transparent'];
-
- // Configuration for the various elements in the DOM of this component.
- //
- // Possible keys include:
- //
- // `default`:
- // The default option index. Only needs to be provided if not zero.
- // `parser`:
- // A function which is used to parse the value from the selected option in
- // a customized way.
- // `selector`:
- // The selector used to find the associated element.
- const selectConfigs = {
- backgroundColor: {
- selector: '.vjs-bg-color > select',
- id: 'captions-background-color-%s',
- label: 'Color',
- options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
- },
- backgroundOpacity: {
- selector: '.vjs-bg-opacity > select',
- id: 'captions-background-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS]
- },
- color: {
- selector: '.vjs-text-color > select',
- id: 'captions-foreground-color-%s',
- label: 'Color',
- options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
- },
- edgeStyle: {
- selector: '.vjs-edge-style > select',
- id: '%s',
- label: 'Text Edge Style',
- options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']]
- },
- fontFamily: {
- selector: '.vjs-font-family > select',
- id: 'captions-font-family-%s',
- label: 'Font Family',
- options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
- },
- fontPercent: {
- selector: '.vjs-font-percent > select',
- id: 'captions-font-size-%s',
- label: 'Font Size',
- options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']],
- default: 2,
- parser: v => v === '1.00' ? null : Number(v)
- },
- textOpacity: {
- selector: '.vjs-text-opacity > select',
- id: 'captions-foreground-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_OPAQUE, OPACITY_SEMI]
- },
- // Options for this object are defined below.
- windowColor: {
- selector: '.vjs-window-color > select',
- id: 'captions-window-color-%s',
- label: 'Color'
- },
- // Options for this object are defined below.
- windowOpacity: {
- selector: '.vjs-window-opacity > select',
- id: 'captions-window-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE]
- }
- };
- selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
-
- /**
- * Get the actual value of an option.
- *
- * @param {string} value
- * The value to get
- *
- * @param {Function} [parser]
- * Optional function to adjust the value.
- *
- * @return {*}
- * - Will be `undefined` if no value exists
- * - Will be `undefined` if the given value is "none".
- * - Will be the actual value otherwise.
- *
- * @private
- */
- function parseOptionValue(value, parser) {
- if (parser) {
- value = parser(value);
- }
- if (value && value !== 'none') {
- return value;
- }
- }
-
- /**
- * Gets the value of the selected element within a element.
- *
- * @param {Element} el
- * the element to look in
- *
- * @param {Function} [parser]
- * Optional function to adjust the value.
- *
- * @return {*}
- * - Will be `undefined` if no value exists
- * - Will be `undefined` if the given value is "none".
- * - Will be the actual value otherwise.
- *
- * @private
- */
- function getSelectedOptionValue(el, parser) {
- const value = el.options[el.options.selectedIndex].value;
- return parseOptionValue(value, parser);
- }
-
- /**
- * Sets the selected element within a element based on a
- * given value.
- *
- * @param {Element} el
- * The element to look in.
- *
- * @param {string} value
- * the property to look on.
- *
- * @param {Function} [parser]
- * Optional function to adjust the value before comparing.
- *
- * @private
- */
- function setSelectedOption(el, value, parser) {
- if (!value) {
- return;
- }
- for (let i = 0; i < el.options.length; i++) {
- if (parseOptionValue(el.options[i].value, parser) === value) {
- el.selectedIndex = i;
- break;
- }
- }
- }
-
- /**
- * Manipulate Text Tracks settings.
- *
- * @extends ModalDialog
- */
- class TextTrackSettings extends ModalDialog {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- options.temporary = false;
- super(player, options);
- this.updateDisplay = this.updateDisplay.bind(this);
-
- // fill the modal and pretend we have opened it
- this.fill();
- this.hasBeenOpened_ = this.hasBeenFilled_ = true;
- this.endDialog = createEl('p', {
- className: 'vjs-control-text',
- textContent: this.localize('End of dialog window.')
- });
- this.el().appendChild(this.endDialog);
- this.setDefaults();
-
- // Grab `persistTextTrackSettings` from the player options if not passed in child options
- if (options.persistTextTrackSettings === undefined) {
- this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
- }
- this.on(this.$('.vjs-done-button'), 'click', () => {
- this.saveSettings();
- this.close();
- });
- this.on(this.$('.vjs-default-button'), 'click', () => {
- this.setDefaults();
- this.updateDisplay();
- });
- each(selectConfigs, config => {
- this.on(this.$(config.selector), 'change', this.updateDisplay);
- });
- if (this.options_.persistTextTrackSettings) {
- this.restoreSettings();
- }
- }
- dispose() {
- this.endDialog = null;
- super.dispose();
- }
-
- /**
- * Create a element with configured options.
- *
- * @param {string} key
- * Configuration key to use during creation.
- *
- * @param {string} [legendId]
- * Id of associated .
- *
- * @param {string} [type=label]
- * Type of labelling element, `label` or `legend`
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElSelect_(key, legendId = '', type = 'label') {
- const config = selectConfigs[key];
- const id = config.id.replace('%s', this.id_);
- const selectLabelledbyIds = [legendId, id].join(' ').trim();
- const guid = `vjs_select_${newGUID()}`;
- return [`<${type} id="${id}"${type === 'label' ? ` for="${guid}" class="vjs-label"` : ''}>`, this.localize(config.label), `${type}>`, ``].concat(config.options.map(o => {
- const optionId = id + '-' + o[1].replace(/\W+/g, '');
- return [``, this.localize(o[1]), ' '].join('');
- })).concat(' ').join('');
- }
-
- /**
- * Create foreground color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElFgColor_() {
- const legendId = `captions-text-legend-${this.id_}`;
- return [' ', ``, this.localize('Text'), ' ', '', this.createElSelect_('color', legendId), ' ', '', this.createElSelect_('textOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create background color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElBgColor_() {
- const legendId = `captions-background-${this.id_}`;
- return ['', ``, this.localize('Text Background'), ' ', '', this.createElSelect_('backgroundColor', legendId), ' ', '', this.createElSelect_('backgroundOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create window color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElWinColor_() {
- const legendId = `captions-window-${this.id_}`;
- return ['', ``, this.localize('Caption Area Background'), ' ', '', this.createElSelect_('windowColor', legendId), ' ', '', this.createElSelect_('windowOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create color elements for the component
- *
- * @return {Element}
- * The element that was created
- *
- * @private
- */
- createElColors_() {
- return createEl('div', {
- className: 'vjs-track-settings-colors',
- innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('')
- });
- }
-
- /**
- * Create font elements for the component
- *
- * @return {Element}
- * The element that was created.
- *
- * @private
- */
- createElFont_() {
- return createEl('div', {
- className: 'vjs-track-settings-font',
- innerHTML: ['', this.createElSelect_('fontPercent', '', 'legend'), ' ', '', this.createElSelect_('edgeStyle', '', 'legend'), ' ', '', this.createElSelect_('fontFamily', '', 'legend'), ' '].join('')
- });
- }
-
- /**
- * Create controls for the component
- *
- * @return {Element}
- * The element that was created.
- *
- * @private
- */
- createElControls_() {
- const defaultsDescription = this.localize('restore all settings to the default values');
- return createEl('div', {
- className: 'vjs-track-settings-controls',
- innerHTML: [``, this.localize('Reset'), ` ${defaultsDescription} `, ' ', `${this.localize('Done')} `].join('')
- });
- }
- content() {
- return [this.createElColors_(), this.createElFont_(), this.createElControls_()];
- }
- label() {
- return this.localize('Caption Settings Dialog');
- }
- description() {
- return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
- }
- buildCSSClass() {
- return super.buildCSSClass() + ' vjs-text-track-settings';
- }
-
- /**
- * Gets an object of text track settings (or null).
- *
- * @return {Object}
- * An object with config values parsed from the DOM or localStorage.
- */
- getValues() {
- return reduce(selectConfigs, (accum, config, key) => {
- const value = getSelectedOptionValue(this.$(config.selector), config.parser);
- if (value !== undefined) {
- accum[key] = value;
- }
- return accum;
- }, {});
- }
-
- /**
- * Sets text track settings from an object of values.
- *
- * @param {Object} values
- * An object with config values parsed from the DOM or localStorage.
- */
- setValues(values) {
- each(selectConfigs, (config, key) => {
- setSelectedOption(this.$(config.selector), values[key], config.parser);
- });
- }
-
- /**
- * Sets all `` elements to their default values.
- */
- setDefaults() {
- each(selectConfigs, config => {
- const index = config.hasOwnProperty('default') ? config.default : 0;
- this.$(config.selector).selectedIndex = index;
- });
- }
-
- /**
- * Restore texttrack settings from localStorage
- */
- restoreSettings() {
- let values;
- try {
- values = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY));
- } catch (err) {
- log.warn(err);
- }
- if (values) {
- this.setValues(values);
- }
- }
-
- /**
- * Save text track settings to localStorage
- */
- saveSettings() {
- if (!this.options_.persistTextTrackSettings) {
- return;
- }
- const values = this.getValues();
- try {
- if (Object.keys(values).length) {
- window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values));
- } else {
- window.localStorage.removeItem(LOCAL_STORAGE_KEY);
- }
- } catch (err) {
- log.warn(err);
- }
- }
-
- /**
- * Update display of text track settings
- */
- updateDisplay() {
- const ttDisplay = this.player_.getChild('textTrackDisplay');
- if (ttDisplay) {
- ttDisplay.updateDisplay();
- }
- }
-
- /**
- * conditionally blur the element and refocus the captions button
- *
- * @private
- */
- conditionalBlur_() {
- this.previouslyActiveEl_ = null;
- const cb = this.player_.controlBar;
- const subsCapsBtn = cb && cb.subsCapsButton;
- const ccBtn = cb && cb.captionsButton;
- if (subsCapsBtn) {
- subsCapsBtn.focus();
- } else if (ccBtn) {
- ccBtn.focus();
- }
- }
-
- /**
- * Repopulate dialog with new localizations on languagechange
- */
- handleLanguagechange() {
- this.fill();
- }
- }
- Component.registerComponent('TextTrackSettings', TextTrackSettings);
-
- /**
- * @file resize-manager.js
- */
-
- /**
- * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
- *
- * It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
- *
- * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
- * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
- *
- * @example How to disable the resize manager
- * const player = videojs('#vid', {
- * resizeManager: false
- * });
- *
- * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
- *
- * @extends Component
- */
- class ResizeManager extends Component {
- /**
- * Create the ResizeManager.
- *
- * @param {Object} player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of ResizeManager options.
- *
- * @param {Object} [options.ResizeObserver]
- * A polyfill for ResizeObserver can be passed in here.
- * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
- */
- constructor(player, options) {
- let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver;
-
- // if `null` was passed, we want to disable the ResizeObserver
- if (options.ResizeObserver === null) {
- RESIZE_OBSERVER_AVAILABLE = false;
- }
-
- // Only create an element when ResizeObserver isn't available
- const options_ = merge({
- createEl: !RESIZE_OBSERVER_AVAILABLE,
- reportTouchActivity: false
- }, options);
- super(player, options_);
- this.ResizeObserver = options.ResizeObserver || window.ResizeObserver;
- this.loadListener_ = null;
- this.resizeObserver_ = null;
- this.debouncedHandler_ = debounce(() => {
- this.resizeHandler();
- }, 100, false, this);
- if (RESIZE_OBSERVER_AVAILABLE) {
- this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
- this.resizeObserver_.observe(player.el());
- } else {
- this.loadListener_ = () => {
- if (!this.el_ || !this.el_.contentWindow) {
- return;
- }
- const debouncedHandler_ = this.debouncedHandler_;
- let unloadListener_ = this.unloadListener_ = function () {
- off(this, 'resize', debouncedHandler_);
- off(this, 'unload', unloadListener_);
- unloadListener_ = null;
- };
-
- // safari and edge can unload the iframe before resizemanager dispose
- // we have to dispose of event handlers correctly before that happens
- on(this.el_.contentWindow, 'unload', unloadListener_);
- on(this.el_.contentWindow, 'resize', debouncedHandler_);
- };
- this.one('load', this.loadListener_);
- }
- }
- createEl() {
- return super.createEl('iframe', {
- className: 'vjs-resize-manager',
- tabIndex: -1,
- title: this.localize('No content')
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
- *
- * @fires Player#playerresize
- */
- resizeHandler() {
- /**
- * Called when the player size has changed
- *
- * @event Player#playerresize
- * @type {Event}
- */
- // make sure player is still around to trigger
- // prevents this from causing an error after dispose
- if (!this.player_ || !this.player_.trigger) {
- return;
- }
- this.player_.trigger('playerresize');
- }
- dispose() {
- if (this.debouncedHandler_) {
- this.debouncedHandler_.cancel();
- }
- if (this.resizeObserver_) {
- if (this.player_.el()) {
- this.resizeObserver_.unobserve(this.player_.el());
- }
- this.resizeObserver_.disconnect();
- }
- if (this.loadListener_) {
- this.off('load', this.loadListener_);
- }
- if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
- this.unloadListener_.call(this.el_.contentWindow);
- }
- this.ResizeObserver = null;
- this.resizeObserver = null;
- this.debouncedHandler_ = null;
- this.loadListener_ = null;
- super.dispose();
- }
- }
- Component.registerComponent('ResizeManager', ResizeManager);
-
- const defaults = {
- trackingThreshold: 20,
- liveTolerance: 15
- };
-
- /*
- track when we are at the live edge, and other helpers for live playback */
-
- /**
- * A class for checking live current time and determining when the player
- * is at or behind the live edge.
- */
- class LiveTracker extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {number} [options.trackingThreshold=20]
- * Number of seconds of live window (seekableEnd - seekableStart) that
- * media needs to have before the liveui will be shown.
- *
- * @param {number} [options.liveTolerance=15]
- * Number of seconds behind live that we have to be
- * before we will be considered non-live. Note that this will only
- * be used when playing at the live edge. This allows large seekable end
- * changes to not effect whether we are live or not.
- */
- constructor(player, options) {
- // LiveTracker does not need an element
- const options_ = merge(defaults, options, {
- createEl: false
- });
- super(player, options_);
- this.trackLiveHandler_ = () => this.trackLive_();
- this.handlePlay_ = e => this.handlePlay(e);
- this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
- this.handleSeeked_ = e => this.handleSeeked(e);
- this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
- this.reset_();
- this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
- // we should try to toggle tracking on canplay as native playback engines, like Safari
- // may not have the proper values for things like seekableEnd until then
- this.on(this.player_, 'canplay', () => this.toggleTracking());
- }
-
- /**
- * all the functionality for tracking when seek end changes
- * and for tracking how far past seek end we should be
- */
- trackLive_() {
- const seekable = this.player_.seekable();
-
- // skip undefined seekable
- if (!seekable || !seekable.length) {
- return;
- }
- const newTime = Number(window.performance.now().toFixed(4));
- const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
- this.lastTime_ = newTime;
- this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
- const liveCurrentTime = this.liveCurrentTime();
- const currentTime = this.player_.currentTime();
-
- // we are behind live if any are true
- // 1. the player is paused
- // 2. the user seeked to a location 2 seconds away from live
- // 3. the difference between live and current time is greater
- // liveTolerance which defaults to 15s
- let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
-
- // we cannot be behind if
- // 1. until we have not seen a timeupdate yet
- // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
- if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
- isBehind = false;
- }
- if (isBehind !== this.behindLiveEdge_) {
- this.behindLiveEdge_ = isBehind;
- this.trigger('liveedgechange');
- }
- }
-
- /**
- * handle a durationchange event on the player
- * and start/stop tracking accordingly.
- */
- handleDurationchange() {
- this.toggleTracking();
- }
-
- /**
- * start/stop tracking
- */
- toggleTracking() {
- if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
- if (this.player_.options_.liveui) {
- this.player_.addClass('vjs-liveui');
- }
- this.startTracking();
- } else {
- this.player_.removeClass('vjs-liveui');
- this.stopTracking();
- }
- }
-
- /**
- * start tracking live playback
- */
- startTracking() {
- if (this.isTracking()) {
- return;
- }
-
- // If we haven't seen a timeupdate, we need to check whether playback
- // began before this component started tracking. This can happen commonly
- // when using autoplay.
- if (!this.timeupdateSeen_) {
- this.timeupdateSeen_ = this.player_.hasStarted();
- }
- this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
- this.trackLive_();
- this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- if (!this.timeupdateSeen_) {
- this.one(this.player_, 'play', this.handlePlay_);
- this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- } else {
- this.on(this.player_, 'seeked', this.handleSeeked_);
- }
- }
-
- /**
- * handle the first timeupdate on the player if it wasn't already playing
- * when live tracker started tracking.
- */
- handleFirstTimeupdate() {
- this.timeupdateSeen_ = true;
- this.on(this.player_, 'seeked', this.handleSeeked_);
- }
-
- /**
- * Keep track of what time a seek starts, and listen for seeked
- * to find where a seek ends.
- */
- handleSeeked() {
- const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
- this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
- this.nextSeekedFromUser_ = false;
- this.trackLive_();
- }
-
- /**
- * handle the first play on the player, and make sure that we seek
- * right to the live edge.
- */
- handlePlay() {
- this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
- }
-
- /**
- * Stop tracking, and set all internal variables to
- * their initial value.
- */
- reset_() {
- this.lastTime_ = -1;
- this.pastSeekEnd_ = 0;
- this.lastSeekEnd_ = -1;
- this.behindLiveEdge_ = true;
- this.timeupdateSeen_ = false;
- this.seekedBehindLive_ = false;
- this.nextSeekedFromUser_ = false;
- this.clearInterval(this.trackingInterval_);
- this.trackingInterval_ = null;
- this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- this.off(this.player_, 'seeked', this.handleSeeked_);
- this.off(this.player_, 'play', this.handlePlay_);
- this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
- }
-
- /**
- * The next seeked event is from the user. Meaning that any seek
- * > 2s behind live will be considered behind live for real and
- * liveTolerance will be ignored.
- */
- nextSeekedFromUser() {
- this.nextSeekedFromUser_ = true;
- }
-
- /**
- * stop tracking live playback
- */
- stopTracking() {
- if (!this.isTracking()) {
- return;
- }
- this.reset_();
- this.trigger('liveedgechange');
- }
-
- /**
- * A helper to get the player seekable end
- * so that we don't have to null check everywhere
- *
- * @return {number}
- * The furthest seekable end or Infinity.
- */
- seekableEnd() {
- const seekable = this.player_.seekable();
- const seekableEnds = [];
- let i = seekable ? seekable.length : 0;
- while (i--) {
- seekableEnds.push(seekable.end(i));
- }
-
- // grab the furthest seekable end after sorting, or if there are none
- // default to Infinity
- return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
- }
-
- /**
- * A helper to get the player seekable start
- * so that we don't have to null check everywhere
- *
- * @return {number}
- * The earliest seekable start or 0.
- */
- seekableStart() {
- const seekable = this.player_.seekable();
- const seekableStarts = [];
- let i = seekable ? seekable.length : 0;
- while (i--) {
- seekableStarts.push(seekable.start(i));
- }
-
- // grab the first seekable start after sorting, or if there are none
- // default to 0
- return seekableStarts.length ? seekableStarts.sort()[0] : 0;
- }
-
- /**
- * Get the live time window aka
- * the amount of time between seekable start and
- * live current time.
- *
- * @return {number}
- * The amount of seconds that are seekable in
- * the live video.
- */
- liveWindow() {
- const liveCurrentTime = this.liveCurrentTime();
-
- // if liveCurrenTime is Infinity then we don't have a liveWindow at all
- if (liveCurrentTime === Infinity) {
- return 0;
- }
- return liveCurrentTime - this.seekableStart();
- }
-
- /**
- * Determines if the player is live, only checks if this component
- * is tracking live playback or not
- *
- * @return {boolean}
- * Whether liveTracker is tracking
- */
- isLive() {
- return this.isTracking();
- }
-
- /**
- * Determines if currentTime is at the live edge and won't fall behind
- * on each seekableendchange
- *
- * @return {boolean}
- * Whether playback is at the live edge
- */
- atLiveEdge() {
- return !this.behindLiveEdge();
- }
-
- /**
- * get what we expect the live current time to be
- *
- * @return {number}
- * The expected live current time
- */
- liveCurrentTime() {
- return this.pastSeekEnd() + this.seekableEnd();
- }
-
- /**
- * The number of seconds that have occurred after seekable end
- * changed. This will be reset to 0 once seekable end changes.
- *
- * @return {number}
- * Seconds past the current seekable end
- */
- pastSeekEnd() {
- const seekableEnd = this.seekableEnd();
- if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
- this.pastSeekEnd_ = 0;
- }
- this.lastSeekEnd_ = seekableEnd;
- return this.pastSeekEnd_;
- }
-
- /**
- * If we are currently behind the live edge, aka currentTime will be
- * behind on a seekableendchange
- *
- * @return {boolean}
- * If we are behind the live edge
- */
- behindLiveEdge() {
- return this.behindLiveEdge_;
- }
-
- /**
- * Whether live tracker is currently tracking or not.
- */
- isTracking() {
- return typeof this.trackingInterval_ === 'number';
- }
-
- /**
- * Seek to the live edge if we are behind the live edge
- */
- seekToLiveEdge() {
- this.seekedBehindLive_ = false;
- if (this.atLiveEdge()) {
- return;
- }
- this.nextSeekedFromUser_ = false;
- this.player_.currentTime(this.liveCurrentTime());
- }
-
- /**
- * Dispose of liveTracker
- */
- dispose() {
- this.stopTracking();
- super.dispose();
- }
- }
- Component.registerComponent('LiveTracker', LiveTracker);
-
- /**
- * Displays an element over the player which contains an optional title and
- * description for the current content.
- *
- * Much of the code for this component originated in the now obsolete
- * videojs-dock plugin: https://github.com/brightcove/videojs-dock/
- *
- * @extends Component
- */
- class TitleBar extends Component {
- constructor(player, options) {
- super(player, options);
- this.on('statechanged', e => this.updateDom_());
- this.updateDom_();
- }
-
- /**
- * Create the `TitleBar`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- this.els = {
- title: createEl('div', {
- className: 'vjs-title-bar-title',
- id: `vjs-title-bar-title-${newGUID()}`
- }),
- description: createEl('div', {
- className: 'vjs-title-bar-description',
- id: `vjs-title-bar-description-${newGUID()}`
- })
- };
- return createEl('div', {
- className: 'vjs-title-bar'
- }, {}, values(this.els));
- }
-
- /**
- * Updates the DOM based on the component's state object.
- */
- updateDom_() {
- const tech = this.player_.tech_;
- const techEl = tech && tech.el_;
- const techAriaAttrs = {
- title: 'aria-labelledby',
- description: 'aria-describedby'
- };
- ['title', 'description'].forEach(k => {
- const value = this.state[k];
- const el = this.els[k];
- const techAriaAttr = techAriaAttrs[k];
- emptyEl(el);
- if (value) {
- textContent(el, value);
- }
-
- // If there is a tech element available, update its ARIA attributes
- // according to whether a title and/or description have been provided.
- if (techEl) {
- techEl.removeAttribute(techAriaAttr);
- if (value) {
- techEl.setAttribute(techAriaAttr, el.id);
- }
- }
- });
- if (this.state.title || this.state.description) {
- this.show();
- } else {
- this.hide();
- }
- }
-
- /**
- * Update the contents of the title bar component with new title and
- * description text.
- *
- * If both title and description are missing, the title bar will be hidden.
- *
- * If either title or description are present, the title bar will be visible.
- *
- * NOTE: Any previously set value will be preserved. To unset a previously
- * set value, you must pass an empty string or null.
- *
- * For example:
- *
- * ```
- * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
- * update({description: 'bar2'}) // title: 'foo', description: 'bar2'
- * update({title: ''}) // title: '', description: 'bar2'
- * update({title: 'foo', description: null}) // title: 'foo', description: null
- * ```
- *
- * @param {Object} [options={}]
- * An options object. When empty, the title bar will be hidden.
- *
- * @param {string} [options.title]
- * A title to display in the title bar.
- *
- * @param {string} [options.description]
- * A description to display in the title bar.
- */
- update(options) {
- this.setState(options);
- }
-
- /**
- * Dispose the component.
- */
- dispose() {
- const tech = this.player_.tech_;
- const techEl = tech && tech.el_;
- if (techEl) {
- techEl.removeAttribute('aria-labelledby');
- techEl.removeAttribute('aria-describedby');
- }
- super.dispose();
- this.els = null;
- }
- }
- Component.registerComponent('TitleBar', TitleBar);
-
- /**
- * This function is used to fire a sourceset when there is something
- * similar to `mediaEl.load()` being called. It will try to find the source via
- * the `src` attribute and then the `` elements. It will then fire `sourceset`
- * with the source that was found or empty string if we cannot know. If it cannot
- * find a source then `sourceset` will not be fired.
- *
- * @param { import('./html5').default } tech
- * The tech object that sourceset was setup on
- *
- * @return {boolean}
- * returns false if the sourceset was not fired and true otherwise.
- */
- const sourcesetLoad = tech => {
- const el = tech.el();
-
- // if `el.src` is set, that source will be loaded.
- if (el.hasAttribute('src')) {
- tech.triggerSourceset(el.src);
- return true;
- }
-
- /**
- * Since there isn't a src property on the media element, source elements will be used for
- * implementing the source selection algorithm. This happens asynchronously and
- * for most cases were there is more than one source we cannot tell what source will
- * be loaded, without re-implementing the source selection algorithm. At this time we are not
- * going to do that. There are three special cases that we do handle here though:
- *
- * 1. If there are no sources, do not fire `sourceset`.
- * 2. If there is only one `` with a `src` property/attribute that is our `src`
- * 3. If there is more than one `` but all of them have the same `src` url.
- * That will be our src.
- */
- const sources = tech.$$('source');
- const srcUrls = [];
- let src = '';
-
- // if there are no sources, do not fire sourceset
- if (!sources.length) {
- return false;
- }
-
- // only count valid/non-duplicate source elements
- for (let i = 0; i < sources.length; i++) {
- const url = sources[i].src;
- if (url && srcUrls.indexOf(url) === -1) {
- srcUrls.push(url);
- }
- }
-
- // there were no valid sources
- if (!srcUrls.length) {
- return false;
- }
-
- // there is only one valid source element url
- // use that
- if (srcUrls.length === 1) {
- src = srcUrls[0];
- }
- tech.triggerSourceset(src);
- return true;
- };
-
- /**
- * our implementation of an `innerHTML` descriptor for browsers
- * that do not have one.
- */
- const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
- get() {
- return this.cloneNode(true).innerHTML;
- },
- set(v) {
- // make a dummy node to use innerHTML on
- const dummy = document.createElement(this.nodeName.toLowerCase());
-
- // set innerHTML to the value provided
- dummy.innerHTML = v;
-
- // make a document fragment to hold the nodes from dummy
- const docFrag = document.createDocumentFragment();
-
- // copy all of the nodes created by the innerHTML on dummy
- // to the document fragment
- while (dummy.childNodes.length) {
- docFrag.appendChild(dummy.childNodes[0]);
- }
-
- // remove content
- this.innerText = '';
-
- // now we add all of that html in one by appending the
- // document fragment. This is how innerHTML does it.
- window.Element.prototype.appendChild.call(this, docFrag);
-
- // then return the result that innerHTML's setter would
- return this.innerHTML;
- }
- });
-
- /**
- * Get a property descriptor given a list of priorities and the
- * property to get.
- */
- const getDescriptor = (priority, prop) => {
- let descriptor = {};
- for (let i = 0; i < priority.length; i++) {
- descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
- if (descriptor && descriptor.set && descriptor.get) {
- break;
- }
- }
- descriptor.enumerable = true;
- descriptor.configurable = true;
- return descriptor;
- };
- const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, window.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
-
- /**
- * Patches browser internal functions so that we can tell synchronously
- * if a `` was appended to the media element. For some reason this
- * causes a `sourceset` if the the media element is ready and has no source.
- * This happens when:
- * - The page has just loaded and the media element does not have a source.
- * - The media element was emptied of all sources, then `load()` was called.
- *
- * It does this by patching the following functions/properties when they are supported:
- *
- * - `append()` - can be used to add a `` element to the media element
- * - `appendChild()` - can be used to add a `` element to the media element
- * - `insertAdjacentHTML()` - can be used to add a `` element to the media element
- * - `innerHTML` - can be used to add a `` element to the media element
- *
- * @param {Html5} tech
- * The tech object that sourceset is being setup on.
- */
- const firstSourceWatch = function (tech) {
- const el = tech.el();
-
- // make sure firstSourceWatch isn't setup twice.
- if (el.resetSourceWatch_) {
- return;
- }
- const old = {};
- const innerDescriptor = getInnerHTMLDescriptor(tech);
- const appendWrapper = appendFn => (...args) => {
- const retval = appendFn.apply(el, args);
- sourcesetLoad(tech);
- return retval;
- };
- ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
- if (!el[k]) {
- return;
- }
-
- // store the old function
- old[k] = el[k];
-
- // call the old function with a sourceset if a source
- // was loaded
- el[k] = appendWrapper(old[k]);
- });
- Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, {
- set: appendWrapper(innerDescriptor.set)
- }));
- el.resetSourceWatch_ = () => {
- el.resetSourceWatch_ = null;
- Object.keys(old).forEach(k => {
- el[k] = old[k];
- });
- Object.defineProperty(el, 'innerHTML', innerDescriptor);
- };
-
- // on the first sourceset, we need to revert our changes
- tech.one('sourceset', el.resetSourceWatch_);
- };
-
- /**
- * our implementation of a `src` descriptor for browsers
- * that do not have one
- */
- const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
- get() {
- if (this.hasAttribute('src')) {
- return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src'));
- }
- return '';
- },
- set(v) {
- window.Element.prototype.setAttribute.call(this, 'src', v);
- return v;
- }
- });
- const getSrcDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
-
- /**
- * setup `sourceset` handling on the `Html5` tech. This function
- * patches the following element properties/functions:
- *
- * - `src` - to determine when `src` is set
- * - `setAttribute()` - to determine when `src` is set
- * - `load()` - this re-triggers the source selection algorithm, and can
- * cause a sourceset.
- *
- * If there is no source when we are adding `sourceset` support or during a `load()`
- * we also patch the functions listed in `firstSourceWatch`.
- *
- * @param {Html5} tech
- * The tech to patch
- */
- const setupSourceset = function (tech) {
- if (!tech.featuresSourceset) {
- return;
- }
- const el = tech.el();
-
- // make sure sourceset isn't setup twice.
- if (el.resetSourceset_) {
- return;
- }
- const srcDescriptor = getSrcDescriptor(tech);
- const oldSetAttribute = el.setAttribute;
- const oldLoad = el.load;
- Object.defineProperty(el, 'src', merge(srcDescriptor, {
- set: v => {
- const retval = srcDescriptor.set.call(el, v);
-
- // we use the getter here to get the actual value set on src
- tech.triggerSourceset(el.src);
- return retval;
- }
- }));
- el.setAttribute = (n, v) => {
- const retval = oldSetAttribute.call(el, n, v);
- if (/src/i.test(n)) {
- tech.triggerSourceset(el.src);
- }
- return retval;
- };
- el.load = () => {
- const retval = oldLoad.call(el);
-
- // if load was called, but there was no source to fire
- // sourceset on. We have to watch for a source append
- // as that can trigger a `sourceset` when the media element
- // has no source
- if (!sourcesetLoad(tech)) {
- tech.triggerSourceset('');
- firstSourceWatch(tech);
- }
- return retval;
- };
- if (el.currentSrc) {
- tech.triggerSourceset(el.currentSrc);
- } else if (!sourcesetLoad(tech)) {
- firstSourceWatch(tech);
- }
- el.resetSourceset_ = () => {
- el.resetSourceset_ = null;
- el.load = oldLoad;
- el.setAttribute = oldSetAttribute;
- Object.defineProperty(el, 'src', srcDescriptor);
- if (el.resetSourceWatch_) {
- el.resetSourceWatch_();
- }
- };
- };
-
- /**
- * @file html5.js
- */
-
- /**
- * HTML5 Media Controller - Wrapper for HTML5 Media API
- *
- * @mixes Tech~SourceHandlerAdditions
- * @extends Tech
- */
- class Html5 extends Tech {
- /**
- * Create an instance of this Tech.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * Callback function to call when the `HTML5` Tech is ready.
- */
- constructor(options, ready) {
- super(options, ready);
- const source = options.source;
- let crossoriginTracks = false;
- this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
-
- // Set the source if one is provided
- // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
- // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
- // anyway so the error gets fired.
- if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
- this.setSource(source);
- } else {
- this.handleLateInit_(this.el_);
- }
-
- // setup sourceset after late sourceset/init
- if (options.enableSourceset) {
- this.setupSourcesetHandling_();
- }
- this.isScrubbing_ = false;
- if (this.el_.hasChildNodes()) {
- const nodes = this.el_.childNodes;
- let nodesLength = nodes.length;
- const removeNodes = [];
- while (nodesLength--) {
- const node = nodes[nodesLength];
- const nodeName = node.nodeName.toLowerCase();
- if (nodeName === 'track') {
- if (!this.featuresNativeTextTracks) {
- // Empty video tag tracks so the built-in player doesn't use them also.
- // This may not be fast enough to stop HTML5 browsers from reading the tags
- // so we'll need to turn off any default tracks if we're manually doing
- // captions and subtitles. videoElement.textTracks
- removeNodes.push(node);
- } else {
- // store HTMLTrackElement and TextTrack to remote list
- this.remoteTextTrackEls().addTrackElement_(node);
- this.remoteTextTracks().addTrack(node.track);
- this.textTracks().addTrack(node.track);
- if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
- crossoriginTracks = true;
- }
- }
- }
- }
- for (let i = 0; i < removeNodes.length; i++) {
- this.el_.removeChild(removeNodes[i]);
- }
- }
- this.proxyNativeTracks_();
- if (this.featuresNativeTextTracks && crossoriginTracks) {
- log.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.');
- }
-
- // prevent iOS Safari from disabling metadata text tracks during native playback
- this.restoreMetadataTracksInIOSNativePlayer_();
-
- // Determine if native controls should be used
- // Our goal should be to get the custom controls on mobile solid everywhere
- // so we can remove this all together. Right now this will block custom
- // controls on touch enabled laptops like the Chrome Pixel
- if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
- this.setControls(true);
- }
-
- // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
- // into a `fullscreenchange` event
- this.proxyWebkitFullscreen_();
- this.triggerReady();
- }
-
- /**
- * Dispose of `HTML5` media element and remove all tracks.
- */
- dispose() {
- if (this.el_ && this.el_.resetSourceset_) {
- this.el_.resetSourceset_();
- }
- Html5.disposeMediaElement(this.el_);
- this.options_ = null;
-
- // tech will handle clearing of the emulated track list
- super.dispose();
- }
-
- /**
- * Modify the media element so that we can detect when
- * the source is changed. Fires `sourceset` just after the source has changed
- */
- setupSourcesetHandling_() {
- setupSourceset(this);
- }
-
- /**
- * When a captions track is enabled in the iOS Safari native player, all other
- * tracks are disabled (including metadata tracks), which nulls all of their
- * associated cue points. This will restore metadata tracks to their pre-fullscreen
- * state in those cases so that cue points are not needlessly lost.
- *
- * @private
- */
- restoreMetadataTracksInIOSNativePlayer_() {
- const textTracks = this.textTracks();
- let metadataTracksPreFullscreenState;
-
- // captures a snapshot of every metadata track's current state
- const takeMetadataTrackSnapshot = () => {
- metadataTracksPreFullscreenState = [];
- for (let i = 0; i < textTracks.length; i++) {
- const track = textTracks[i];
- if (track.kind === 'metadata') {
- metadataTracksPreFullscreenState.push({
- track,
- storedMode: track.mode
- });
- }
- }
- };
-
- // snapshot each metadata track's initial state, and update the snapshot
- // each time there is a track 'change' event
- takeMetadataTrackSnapshot();
- textTracks.addEventListener('change', takeMetadataTrackSnapshot);
- this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
- const restoreTrackMode = () => {
- for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
- const storedTrack = metadataTracksPreFullscreenState[i];
- if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
- storedTrack.track.mode = storedTrack.storedMode;
- }
- }
- // we only want this handler to be executed on the first 'change' event
- textTracks.removeEventListener('change', restoreTrackMode);
- };
-
- // when we enter fullscreen playback, stop updating the snapshot and
- // restore all track modes to their pre-fullscreen state
- this.on('webkitbeginfullscreen', () => {
- textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
-
- // remove the listener before adding it just in case it wasn't previously removed
- textTracks.removeEventListener('change', restoreTrackMode);
- textTracks.addEventListener('change', restoreTrackMode);
- });
-
- // start updating the snapshot again after leaving fullscreen
- this.on('webkitendfullscreen', () => {
- // remove the listener before adding it just in case it wasn't previously removed
- textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
- textTracks.addEventListener('change', takeMetadataTrackSnapshot);
-
- // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
- textTracks.removeEventListener('change', restoreTrackMode);
- });
- }
-
- /**
- * Attempt to force override of tracks for the given type
- *
- * @param {string} type - Track type to override, possible values include 'Audio',
- * 'Video', and 'Text'.
- * @param {boolean} override - If set to true native audio/video will be overridden,
- * otherwise native audio/video will potentially be used.
- * @private
- */
- overrideNative_(type, override) {
- // If there is no behavioral change don't add/remove listeners
- if (override !== this[`featuresNative${type}Tracks`]) {
- return;
- }
- const lowerCaseType = type.toLowerCase();
- if (this[`${lowerCaseType}TracksListeners_`]) {
- Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
- const elTracks = this.el()[`${lowerCaseType}Tracks`];
- elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
- });
- }
- this[`featuresNative${type}Tracks`] = !override;
- this[`${lowerCaseType}TracksListeners_`] = null;
- this.proxyNativeTracksForType_(lowerCaseType);
- }
-
- /**
- * Attempt to force override of native audio tracks.
- *
- * @param {boolean} override - If set to true native audio will be overridden,
- * otherwise native audio will potentially be used.
- */
- overrideNativeAudioTracks(override) {
- this.overrideNative_('Audio', override);
- }
-
- /**
- * Attempt to force override of native video tracks.
- *
- * @param {boolean} override - If set to true native video will be overridden,
- * otherwise native video will potentially be used.
- */
- overrideNativeVideoTracks(override) {
- this.overrideNative_('Video', override);
- }
-
- /**
- * Proxy native track list events for the given type to our track
- * lists if the browser we are playing in supports that type of track list.
- *
- * @param {string} name - Track type; values include 'audio', 'video', and 'text'
- * @private
- */
- proxyNativeTracksForType_(name) {
- const props = NORMAL[name];
- const elTracks = this.el()[props.getterName];
- const techTracks = this[props.getterName]();
- if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
- return;
- }
- const listeners = {
- change: e => {
- const event = {
- type: 'change',
- target: techTracks,
- currentTarget: techTracks,
- srcElement: techTracks
- };
- techTracks.trigger(event);
-
- // if we are a text track change event, we should also notify the
- // remote text track list. This can potentially cause a false positive
- // if we were to get a change event on a non-remote track and
- // we triggered the event on the remote text track list which doesn't
- // contain that track. However, best practices mean looping through the
- // list of tracks and searching for the appropriate mode value, so,
- // this shouldn't pose an issue
- if (name === 'text') {
- this[REMOTE.remoteText.getterName]().trigger(event);
- }
- },
- addtrack(e) {
- techTracks.addTrack(e.track);
- },
- removetrack(e) {
- techTracks.removeTrack(e.track);
- }
- };
- const removeOldTracks = function () {
- const removeTracks = [];
- for (let i = 0; i < techTracks.length; i++) {
- let found = false;
- for (let j = 0; j < elTracks.length; j++) {
- if (elTracks[j] === techTracks[i]) {
- found = true;
- break;
- }
- }
- if (!found) {
- removeTracks.push(techTracks[i]);
- }
- }
- while (removeTracks.length) {
- techTracks.removeTrack(removeTracks.shift());
- }
- };
- this[props.getterName + 'Listeners_'] = listeners;
- Object.keys(listeners).forEach(eventName => {
- const listener = listeners[eventName];
- elTracks.addEventListener(eventName, listener);
- this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
- });
-
- // Remove (native) tracks that are not used anymore
- this.on('loadstart', removeOldTracks);
- this.on('dispose', e => this.off('loadstart', removeOldTracks));
- }
-
- /**
- * Proxy all native track list events to our track lists if the browser we are playing
- * in supports that type of track list.
- *
- * @private
- */
- proxyNativeTracks_() {
- NORMAL.names.forEach(name => {
- this.proxyNativeTracksForType_(name);
- });
- }
-
- /**
- * Create the `Html5` Tech's DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- let el = this.options_.tag;
-
- // Check if this browser supports moving the element into the box.
- // On the iPhone video will break if you move the element,
- // So we have to create a brand new element.
- // If we ingested the player div, we do not need to move the media element.
- if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
- // If the original tag is still there, clone and remove it.
- if (el) {
- const clone = el.cloneNode(true);
- if (el.parentNode) {
- el.parentNode.insertBefore(clone, el);
- }
- Html5.disposeMediaElement(el);
- el = clone;
- } else {
- el = document.createElement('video');
-
- // determine if native controls should be used
- const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
- const attributes = merge({}, tagAttributes);
- if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
- delete attributes.controls;
- }
- setAttributes(el, Object.assign(attributes, {
- id: this.options_.techId,
- class: 'vjs-tech'
- }));
- }
- el.playerId = this.options_.playerId;
- }
- if (typeof this.options_.preload !== 'undefined') {
- setAttribute(el, 'preload', this.options_.preload);
- }
- if (this.options_.disablePictureInPicture !== undefined) {
- el.disablePictureInPicture = this.options_.disablePictureInPicture;
- }
-
- // Update specific tag settings, in case they were overridden
- // `autoplay` has to be *last* so that `muted` and `playsinline` are present
- // when iOS/Safari or other browsers attempt to autoplay.
- const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
- for (let i = 0; i < settingsAttrs.length; i++) {
- const attr = settingsAttrs[i];
- const value = this.options_[attr];
- if (typeof value !== 'undefined') {
- if (value) {
- setAttribute(el, attr, attr);
- } else {
- removeAttribute(el, attr);
- }
- el[attr] = value;
- }
- }
- return el;
- }
-
- /**
- * This will be triggered if the loadstart event has already fired, before videojs was
- * ready. Two known examples of when this can happen are:
- * 1. If we're loading the playback object after it has started loading
- * 2. The media is already playing the (often with autoplay on) then
- *
- * This function will fire another loadstart so that videojs can catchup.
- *
- * @fires Tech#loadstart
- *
- * @return {undefined}
- * returns nothing.
- */
- handleLateInit_(el) {
- if (el.networkState === 0 || el.networkState === 3) {
- // The video element hasn't started loading the source yet
- // or didn't find a source
- return;
- }
- if (el.readyState === 0) {
- // NetworkState is set synchronously BUT loadstart is fired at the
- // end of the current stack, usually before setInterval(fn, 0).
- // So at this point we know loadstart may have already fired or is
- // about to fire, and either way the player hasn't seen it yet.
- // We don't want to fire loadstart prematurely here and cause a
- // double loadstart so we'll wait and see if it happens between now
- // and the next loop, and fire it if not.
- // HOWEVER, we also want to make sure it fires before loadedmetadata
- // which could also happen between now and the next loop, so we'll
- // watch for that also.
- let loadstartFired = false;
- const setLoadstartFired = function () {
- loadstartFired = true;
- };
- this.on('loadstart', setLoadstartFired);
- const triggerLoadstart = function () {
- // We did miss the original loadstart. Make sure the player
- // sees loadstart before loadedmetadata
- if (!loadstartFired) {
- this.trigger('loadstart');
- }
- };
- this.on('loadedmetadata', triggerLoadstart);
- this.ready(function () {
- this.off('loadstart', setLoadstartFired);
- this.off('loadedmetadata', triggerLoadstart);
- if (!loadstartFired) {
- // We did miss the original native loadstart. Fire it now.
- this.trigger('loadstart');
- }
- });
- return;
- }
-
- // From here on we know that loadstart already fired and we missed it.
- // The other readyState events aren't as much of a problem if we double
- // them, so not going to go to as much trouble as loadstart to prevent
- // that unless we find reason to.
- const eventsToTrigger = ['loadstart'];
-
- // loadedmetadata: newly equal to HAVE_METADATA (1) or greater
- eventsToTrigger.push('loadedmetadata');
-
- // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
- if (el.readyState >= 2) {
- eventsToTrigger.push('loadeddata');
- }
-
- // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
- if (el.readyState >= 3) {
- eventsToTrigger.push('canplay');
- }
-
- // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
- if (el.readyState >= 4) {
- eventsToTrigger.push('canplaythrough');
- }
-
- // We still need to give the player time to add event listeners
- this.ready(function () {
- eventsToTrigger.forEach(function (type) {
- this.trigger(type);
- }, this);
- });
- }
-
- /**
- * Set whether we are scrubbing or not.
- * This is used to decide whether we should use `fastSeek` or not.
- * `fastSeek` is used to provide trick play on Safari browsers.
- *
- * @param {boolean} isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- */
- setScrubbing(isScrubbing) {
- this.isScrubbing_ = isScrubbing;
- }
-
- /**
- * Get whether we are scrubbing or not.
- *
- * @return {boolean} isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- */
- scrubbing() {
- return this.isScrubbing_;
- }
-
- /**
- * Set current time for the `HTML5` tech.
- *
- * @param {number} seconds
- * Set the current time of the media to this.
- */
- setCurrentTime(seconds) {
- try {
- if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
- this.el_.fastSeek(seconds);
- } else {
- this.el_.currentTime = seconds;
- }
- } catch (e) {
- log(e, 'Video is not ready. (Video.js)');
- // this.warning(VideoJS.warnings.videoNotReady);
- }
- }
-
- /**
- * Get the current duration of the HTML5 media element.
- *
- * @return {number}
- * The duration of the media or 0 if there is no duration.
- */
- duration() {
- // Android Chrome will report duration as Infinity for VOD HLS until after
- // playback has started, which triggers the live display erroneously.
- // Return NaN if playback has not started and trigger a durationupdate once
- // the duration can be reliably known.
- if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
- // Wait for the first `timeupdate` with currentTime > 0 - there may be
- // several with 0
- const checkProgress = () => {
- if (this.el_.currentTime > 0) {
- // Trigger durationchange for genuinely live video
- if (this.el_.duration === Infinity) {
- this.trigger('durationchange');
- }
- this.off('timeupdate', checkProgress);
- }
- };
- this.on('timeupdate', checkProgress);
- return NaN;
- }
- return this.el_.duration || NaN;
- }
-
- /**
- * Get the current width of the HTML5 media element.
- *
- * @return {number}
- * The width of the HTML5 media element.
- */
- width() {
- return this.el_.offsetWidth;
- }
-
- /**
- * Get the current height of the HTML5 media element.
- *
- * @return {number}
- * The height of the HTML5 media element.
- */
- height() {
- return this.el_.offsetHeight;
- }
-
- /**
- * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
- * `fullscreenchange` event.
- *
- * @private
- * @fires fullscreenchange
- * @listens webkitendfullscreen
- * @listens webkitbeginfullscreen
- * @listens webkitbeginfullscreen
- */
- proxyWebkitFullscreen_() {
- if (!('webkitDisplayingFullscreen' in this.el_)) {
- return;
- }
- const endFn = function () {
- this.trigger('fullscreenchange', {
- isFullscreen: false
- });
- // Safari will sometimes set controls on the videoelement when existing fullscreen.
- if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
- this.el_.controls = false;
- }
- };
- const beginFn = function () {
- if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
- this.one('webkitendfullscreen', endFn);
- this.trigger('fullscreenchange', {
- isFullscreen: true,
- // set a flag in case another tech triggers fullscreenchange
- nativeIOSFullscreen: true
- });
- }
- };
- this.on('webkitbeginfullscreen', beginFn);
- this.on('dispose', () => {
- this.off('webkitbeginfullscreen', beginFn);
- this.off('webkitendfullscreen', endFn);
- });
- }
-
- /**
- * Check if fullscreen is supported on the video el.
- *
- * @return {boolean}
- * - True if fullscreen is supported.
- * - False if fullscreen is not supported.
- */
- supportsFullScreen() {
- return typeof this.el_.webkitEnterFullScreen === 'function';
- }
-
- /**
- * Request that the `HTML5` Tech enter fullscreen.
- */
- enterFullScreen() {
- const video = this.el_;
- if (video.paused && video.networkState <= video.HAVE_METADATA) {
- // attempt to prime the video element for programmatic access
- // this isn't necessary on the desktop but shouldn't hurt
- silencePromise(this.el_.play());
-
- // playing and pausing synchronously during the transition to fullscreen
- // can get iOS ~6.1 devices into a play/pause loop
- this.setTimeout(function () {
- video.pause();
- try {
- video.webkitEnterFullScreen();
- } catch (e) {
- this.trigger('fullscreenerror', e);
- }
- }, 0);
- } else {
- try {
- video.webkitEnterFullScreen();
- } catch (e) {
- this.trigger('fullscreenerror', e);
- }
- }
- }
-
- /**
- * Request that the `HTML5` Tech exit fullscreen.
- */
- exitFullScreen() {
- if (!this.el_.webkitDisplayingFullscreen) {
- this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
- return;
- }
- this.el_.webkitExitFullScreen();
- }
-
- /**
- * Create a floating video window always on top of other windows so that users may
- * continue consuming media while they interact with other content sites, or
- * applications on their device.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @return {Promise}
- * A promise with a Picture-in-Picture window.
- */
- requestPictureInPicture() {
- return this.el_.requestPictureInPicture();
- }
-
- /**
- * Native requestVideoFrameCallback if supported by browser/tech, or fallback
- * Don't use rVCF on Safari when DRM is playing, as it doesn't fire
- * Needs to be checked later than the constructor
- * This will be a false positive for clear sources loaded after a Fairplay source
- *
- * @param {function} cb function to call
- * @return {number} id of request
- */
- requestVideoFrameCallback(cb) {
- if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
- return this.el_.requestVideoFrameCallback(cb);
- }
- return super.requestVideoFrameCallback(cb);
- }
-
- /**
- * Native or fallback requestVideoFrameCallback
- *
- * @param {number} id request id to cancel
- */
- cancelVideoFrameCallback(id) {
- if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
- this.el_.cancelVideoFrameCallback(id);
- } else {
- super.cancelVideoFrameCallback(id);
- }
- }
-
- /**
- * A getter/setter for the `Html5` Tech's source object.
- * > Note: Please use {@link Html5#setSource}
- *
- * @param {Tech~SourceObject} [src]
- * The source object you want to set on the `HTML5` techs element.
- *
- * @return {Tech~SourceObject|undefined}
- * - The current source object when a source is not passed in.
- * - undefined when setting
- *
- * @deprecated Since version 5.
- */
- src(src) {
- if (src === undefined) {
- return this.el_.src;
- }
-
- // Setting src through `src` instead of `setSrc` will be deprecated
- this.setSrc(src);
- }
-
- /**
- * Reset the tech by removing all sources and then calling
- * {@link Html5.resetMediaElement}.
- */
- reset() {
- Html5.resetMediaElement(this.el_);
- }
-
- /**
- * Get the current source on the `HTML5` Tech. Falls back to returning the source from
- * the HTML5 media element.
- *
- * @return {Tech~SourceObject}
- * The current source object from the HTML5 tech. With a fallback to the
- * elements source.
- */
- currentSrc() {
- if (this.currentSource_) {
- return this.currentSource_.src;
- }
- return this.el_.currentSrc;
- }
-
- /**
- * Set controls attribute for the HTML5 media Element.
- *
- * @param {string} val
- * Value to set the controls attribute to
- */
- setControls(val) {
- this.el_.controls = !!val;
- }
-
- /**
- * Create and returns a remote {@link TextTrack} object.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @return {TextTrack}
- * The TextTrack that gets created.
- */
- addTextTrack(kind, label, language) {
- if (!this.featuresNativeTextTracks) {
- return super.addTextTrack(kind, label, language);
- }
- return this.el_.addTextTrack(kind, label, language);
- }
-
- /**
- * Creates either native TextTrack or an emulated TextTrack depending
- * on the value of `featuresNativeTextTracks`
- *
- * @param {Object} options
- * The object should contain the options to initialize the TextTrack with.
- *
- * @param {string} [options.kind]
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
- *
- * @param {string} [options.label]
- * Label to identify the text track
- *
- * @param {string} [options.language]
- * Two letter language abbreviation.
- *
- * @param {boolean} [options.default]
- * Default this track to on.
- *
- * @param {string} [options.id]
- * The internal id to assign this track.
- *
- * @param {string} [options.src]
- * A source url for the track.
- *
- * @return {HTMLTrackElement}
- * The track element that gets created.
- */
- createRemoteTextTrack(options) {
- if (!this.featuresNativeTextTracks) {
- return super.createRemoteTextTrack(options);
- }
- const htmlTrackElement = document.createElement('track');
- if (options.kind) {
- htmlTrackElement.kind = options.kind;
- }
- if (options.label) {
- htmlTrackElement.label = options.label;
- }
- if (options.language || options.srclang) {
- htmlTrackElement.srclang = options.language || options.srclang;
- }
- if (options.default) {
- htmlTrackElement.default = options.default;
- }
- if (options.id) {
- htmlTrackElement.id = options.id;
- }
- if (options.src) {
- htmlTrackElement.src = options.src;
- }
- return htmlTrackElement;
- }
-
- /**
- * Creates a remote text track object and returns an html track element.
- *
- * @param {Object} options The object should contain values for
- * kind, language, label, and src (location of the WebVTT file)
- * @param {boolean} [manualCleanup=false] if set to true, the TextTrack
- * will not be removed from the TextTrackList and HtmlTrackElementList
- * after a source change
- * @return {HTMLTrackElement} An Html Track Element.
- * This can be an emulated {@link HTMLTrackElement} or a native one.
- *
- */
- addRemoteTextTrack(options, manualCleanup) {
- const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
- if (this.featuresNativeTextTracks) {
- this.el().appendChild(htmlTrackElement);
- }
- return htmlTrackElement;
- }
-
- /**
- * Remove remote `TextTrack` from `TextTrackList` object
- *
- * @param {TextTrack} track
- * `TextTrack` object to remove
- */
- removeRemoteTextTrack(track) {
- super.removeRemoteTextTrack(track);
- if (this.featuresNativeTextTracks) {
- const tracks = this.$$('track');
- let i = tracks.length;
- while (i--) {
- if (track === tracks[i] || track === tracks[i].track) {
- this.el().removeChild(tracks[i]);
- }
- }
- }
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object}
- * An object with supported media playback quality metrics
- */
- getVideoPlaybackQuality() {
- if (typeof this.el().getVideoPlaybackQuality === 'function') {
- return this.el().getVideoPlaybackQuality();
- }
- const videoPlaybackQuality = {};
- if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
- videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
- videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
- }
- if (window.performance) {
- videoPlaybackQuality.creationTime = window.performance.now();
- }
- return videoPlaybackQuality;
- }
- }
-
- /* HTML5 Support Testing ---------------------------------------------------- */
-
- /**
- * Element for testing browser HTML5 media capabilities
- *
- * @type {Element}
- * @constant
- * @private
- */
- defineLazyProperty(Html5, 'TEST_VID', function () {
- if (!isReal()) {
- return;
- }
- const video = document.createElement('video');
- const track = document.createElement('track');
- track.kind = 'captions';
- track.srclang = 'en';
- track.label = 'English';
- video.appendChild(track);
- return video;
- });
-
- /**
- * Check if HTML5 media is supported by this browser/device.
- *
- * @return {boolean}
- * - True if HTML5 media is supported.
- * - False if HTML5 media is not supported.
- */
- Html5.isSupported = function () {
- // IE with no Media Player is a LIAR! (#984)
- try {
- Html5.TEST_VID.volume = 0.5;
- } catch (e) {
- return false;
- }
- return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
- };
-
- /**
- * Check if the tech can support the given type
- *
- * @param {string} type
- * The mimetype to check
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- Html5.canPlayType = function (type) {
- return Html5.TEST_VID.canPlayType(type);
- };
-
- /**
- * Check if the tech can support the given source
- *
- * @param {Object} srcObj
- * The source object
- * @param {Object} options
- * The options passed to the tech
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- Html5.canPlaySource = function (srcObj, options) {
- return Html5.canPlayType(srcObj.type);
- };
-
- /**
- * Check if the volume can be changed in this browser/device.
- * Volume cannot be changed in a lot of mobile devices.
- * Specifically, it can't be changed from 1 on iOS.
- *
- * @return {boolean}
- * - True if volume can be controlled
- * - False otherwise
- */
- Html5.canControlVolume = function () {
- // IE will error if Windows Media Player not installed #3315
- try {
- const volume = Html5.TEST_VID.volume;
- Html5.TEST_VID.volume = volume / 2 + 0.1;
- const canControl = volume !== Html5.TEST_VID.volume;
-
- // With the introduction of iOS 15, there are cases where the volume is read as
- // changed but reverts back to its original state at the start of the next tick.
- // To determine whether volume can be controlled on iOS,
- // a timeout is set and the volume is checked asynchronously.
- // Since `features` doesn't currently work asynchronously, the value is manually set.
- if (canControl && IS_IOS) {
- window.setTimeout(() => {
- if (Html5 && Html5.prototype) {
- Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
- }
- });
-
- // default iOS to false, which will be updated in the timeout above.
- return false;
- }
- return canControl;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if the volume can be muted in this browser/device.
- * Some devices, e.g. iOS, don't allow changing volume
- * but permits muting/unmuting.
- *
- * @return {boolean}
- * - True if volume can be muted
- * - False otherwise
- */
- Html5.canMuteVolume = function () {
- try {
- const muted = Html5.TEST_VID.muted;
-
- // in some versions of iOS muted property doesn't always
- // work, so we want to set both property and attribute
- Html5.TEST_VID.muted = !muted;
- if (Html5.TEST_VID.muted) {
- setAttribute(Html5.TEST_VID, 'muted', 'muted');
- } else {
- removeAttribute(Html5.TEST_VID, 'muted', 'muted');
- }
- return muted !== Html5.TEST_VID.muted;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if the playback rate can be changed in this browser/device.
- *
- * @return {boolean}
- * - True if playback rate can be controlled
- * - False otherwise
- */
- Html5.canControlPlaybackRate = function () {
- // Playback rate API is implemented in Android Chrome, but doesn't do anything
- // https://github.com/videojs/video.js/issues/3180
- if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
- return false;
- }
- // IE will error if Windows Media Player not installed #3315
- try {
- const playbackRate = Html5.TEST_VID.playbackRate;
- Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
- return playbackRate !== Html5.TEST_VID.playbackRate;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if we can override a video/audio elements attributes, with
- * Object.defineProperty.
- *
- * @return {boolean}
- * - True if builtin attributes can be overridden
- * - False otherwise
- */
- Html5.canOverrideAttributes = function () {
- // if we cannot overwrite the src/innerHTML property, there is no support
- // iOS 7 safari for instance cannot do this.
- try {
- const noop = () => {};
- Object.defineProperty(document.createElement('video'), 'src', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('audio'), 'src', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('video'), 'innerHTML', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('audio'), 'innerHTML', {
- get: noop,
- set: noop
- });
- } catch (e) {
- return false;
- }
- return true;
- };
-
- /**
- * Check to see if native `TextTrack`s are supported by this browser/device.
- *
- * @return {boolean}
- * - True if native `TextTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeTextTracks = function () {
- return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
- };
-
- /**
- * Check to see if native `VideoTrack`s are supported by this browser/device
- *
- * @return {boolean}
- * - True if native `VideoTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeVideoTracks = function () {
- return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
- };
-
- /**
- * Check to see if native `AudioTrack`s are supported by this browser/device
- *
- * @return {boolean}
- * - True if native `AudioTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeAudioTracks = function () {
- return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
- };
-
- /**
- * An array of events available on the Html5 tech.
- *
- * @private
- * @type {Array}
- */
- Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange'];
-
- /**
- * Boolean indicating whether the `Tech` supports volume control.
- *
- * @type {boolean}
- * @default {@link Html5.canControlVolume}
- */
- /**
- * Boolean indicating whether the `Tech` supports muting volume.
- *
- * @type {boolean}
- * @default {@link Html5.canMuteVolume}
- */
-
- /**
- * Boolean indicating whether the `Tech` supports changing the speed at which the media
- * plays. Examples:
- * - Set player to play 2x (twice) as fast
- * - Set player to play 0.5x (half) as fast
- *
- * @type {boolean}
- * @default {@link Html5.canControlPlaybackRate}
- */
-
- /**
- * Boolean indicating whether the `Tech` supports the `sourceset` event.
- *
- * @type {boolean}
- * @default
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeTextTracks}
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeVideoTracks}
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeAudioTracks}
- */
- [['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
- defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
- });
- Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the media element
- * moving in the DOM. iOS breaks if you move the media element, so this is set this to
- * false there. Everywhere else this should be true.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.movingMediaElementInDOM = !IS_IOS;
-
- // TODO: Previous comment: No longer appears to be used. Can probably be removed.
- // Is this true?
- /**
- * Boolean indicating whether the `HTML5` tech currently supports automatic media resize
- * when going into fullscreen.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.featuresFullscreenResize = true;
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the progress event.
- * If this is false, manual `progress` events will be triggered instead.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.featuresProgressEvents = true;
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
- * If this is false, manual `timeupdate` events will be triggered instead.
- *
- * @default
- */
- Html5.prototype.featuresTimeupdateEvents = true;
-
- /**
- * Whether the HTML5 el supports `requestVideoFrameCallback`
- *
- * @type {boolean}
- */
- Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
- Html5.disposeMediaElement = function (el) {
- if (!el) {
- return;
- }
- if (el.parentNode) {
- el.parentNode.removeChild(el);
- }
-
- // remove any child track or source nodes to prevent their loading
- while (el.hasChildNodes()) {
- el.removeChild(el.firstChild);
- }
-
- // remove any src reference. not setting `src=''` because that causes a warning
- // in firefox
- el.removeAttribute('src');
-
- // force the media element to update its loading state by calling load()
- // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
- if (typeof el.load === 'function') {
- // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
- (function () {
- try {
- el.load();
- } catch (e) {
- // not supported
- }
- })();
- }
- };
- Html5.resetMediaElement = function (el) {
- if (!el) {
- return;
- }
- const sources = el.querySelectorAll('source');
- let i = sources.length;
- while (i--) {
- el.removeChild(sources[i]);
- }
-
- // remove any src reference.
- // not setting `src=''` because that throws an error
- el.removeAttribute('src');
- if (typeof el.load === 'function') {
- // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
- (function () {
- try {
- el.load();
- } catch (e) {
- // satisfy linter
- }
- })();
- }
- };
-
- /* Native HTML5 element property wrapping ----------------------------------- */
- // Wrap native boolean attributes with getters that check both property and attribute
- // The list is as followed:
- // muted, defaultMuted, autoplay, controls, loop, playsinline
- [
- /**
- * Get the value of `muted` from the media element. `muted` indicates
- * that the volume for the media should be set to silent. This does not actually change
- * the `volume` attribute.
- *
- * @method Html5#muted
- * @return {boolean}
- * - True if the value of `volume` should be ignored and the audio set to silent.
- * - False if the value of `volume` should be used.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
- */
- 'muted',
- /**
- * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
- * whether the media should start muted or not. Only changes the default state of the
- * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
- * current state.
- *
- * @method Html5#defaultMuted
- * @return {boolean}
- * - The value of `defaultMuted` from the media element.
- * - True indicates that the media should start muted.
- * - False indicates that the media should not start muted
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
- */
- 'defaultMuted',
- /**
- * Get the value of `autoplay` from the media element. `autoplay` indicates
- * that the media should start to play as soon as the page is ready.
- *
- * @method Html5#autoplay
- * @return {boolean}
- * - The value of `autoplay` from the media element.
- * - True indicates that the media should start as soon as the page loads.
- * - False indicates that the media should not start as soon as the page loads.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
- */
- 'autoplay',
- /**
- * Get the value of `controls` from the media element. `controls` indicates
- * whether the native media controls should be shown or hidden.
- *
- * @method Html5#controls
- * @return {boolean}
- * - The value of `controls` from the media element.
- * - True indicates that native controls should be showing.
- * - False indicates that native controls should be hidden.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
- */
- 'controls',
- /**
- * Get the value of `loop` from the media element. `loop` indicates
- * that the media should return to the start of the media and continue playing once
- * it reaches the end.
- *
- * @method Html5#loop
- * @return {boolean}
- * - The value of `loop` from the media element.
- * - True indicates that playback should seek back to start once
- * the end of a media is reached.
- * - False indicates that playback should not loop back to the start when the
- * end of the media is reached.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
- */
- 'loop',
- /**
- * Get the value of `playsinline` from the media element. `playsinline` indicates
- * to the browser that non-fullscreen playback is preferred when fullscreen
- * playback is the native default, such as in iOS Safari.
- *
- * @method Html5#playsinline
- * @return {boolean}
- * - The value of `playsinline` from the media element.
- * - True indicates that the media should play inline.
- * - False indicates that the media should not play inline.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- 'playsinline'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop] || this.el_.hasAttribute(prop);
- };
- });
-
- // Wrap native boolean attributes with setters that set both property and attribute
- // The list is as followed:
- // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
- // setControls is special-cased above
- [
- /**
- * Set the value of `muted` on the media element. `muted` indicates that the current
- * audio level should be silent.
- *
- * @method Html5#setMuted
- * @param {boolean} muted
- * - True if the audio should be set to silent
- * - False otherwise
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
- */
- 'muted',
- /**
- * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
- * audio level should be silent, but will only effect the muted level on initial playback..
- *
- * @method Html5.prototype.setDefaultMuted
- * @param {boolean} defaultMuted
- * - True if the audio should be set to silent
- * - False otherwise
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
- */
- 'defaultMuted',
- /**
- * Set the value of `autoplay` on the media element. `autoplay` indicates
- * that the media should start to play as soon as the page is ready.
- *
- * @method Html5#setAutoplay
- * @param {boolean} autoplay
- * - True indicates that the media should start as soon as the page loads.
- * - False indicates that the media should not start as soon as the page loads.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
- */
- 'autoplay',
- /**
- * Set the value of `loop` on the media element. `loop` indicates
- * that the media should return to the start of the media and continue playing once
- * it reaches the end.
- *
- * @method Html5#setLoop
- * @param {boolean} loop
- * - True indicates that playback should seek back to start once
- * the end of a media is reached.
- * - False indicates that playback should not loop back to the start when the
- * end of the media is reached.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
- */
- 'loop',
- /**
- * Set the value of `playsinline` from the media element. `playsinline` indicates
- * to the browser that non-fullscreen playback is preferred when fullscreen
- * playback is the native default, such as in iOS Safari.
- *
- * @method Html5#setPlaysinline
- * @param {boolean} playsinline
- * - True indicates that the media should play inline.
- * - False indicates that the media should not play inline.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- 'playsinline'].forEach(function (prop) {
- Html5.prototype['set' + toTitleCase(prop)] = function (v) {
- this.el_[prop] = v;
- if (v) {
- this.el_.setAttribute(prop, prop);
- } else {
- this.el_.removeAttribute(prop);
- }
- };
- });
-
- // Wrap native properties with a getter
- // The list is as followed
- // paused, currentTime, buffered, volume, poster, preload, error, seeking
- // seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
- // played, networkState, readyState, videoWidth, videoHeight, crossOrigin
- [
- /**
- * Get the value of `paused` from the media element. `paused` indicates whether the media element
- * is currently paused or not.
- *
- * @method Html5#paused
- * @return {boolean}
- * The value of `paused` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
- */
- 'paused',
- /**
- * Get the value of `currentTime` from the media element. `currentTime` indicates
- * the current second that the media is at in playback.
- *
- * @method Html5#currentTime
- * @return {number}
- * The value of `currentTime` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
- */
- 'currentTime',
- /**
- * Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
- * object that represents the parts of the media that are already downloaded and
- * available for playback.
- *
- * @method Html5#buffered
- * @return {TimeRange}
- * The value of `buffered` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
- */
- 'buffered',
- /**
- * Get the value of `volume` from the media element. `volume` indicates
- * the current playback volume of audio for a media. `volume` will be a value from 0
- * (silent) to 1 (loudest and default).
- *
- * @method Html5#volume
- * @return {number}
- * The value of `volume` from the media element. Value will be between 0-1.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
- */
- 'volume',
- /**
- * Get the value of `poster` from the media element. `poster` indicates
- * that the url of an image file that can/will be shown when no media data is available.
- *
- * @method Html5#poster
- * @return {string}
- * The value of `poster` from the media element. Value will be a url to an
- * image.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
- */
- 'poster',
- /**
- * Get the value of `preload` from the media element. `preload` indicates
- * what should download before the media is interacted with. It can have the following
- * values:
- * - none: nothing should be downloaded
- * - metadata: poster and the first few frames of the media may be downloaded to get
- * media dimensions and other metadata
- * - auto: allow the media and metadata for the media to be downloaded before
- * interaction
- *
- * @method Html5#preload
- * @return {string}
- * The value of `preload` from the media element. Will be 'none', 'metadata',
- * or 'auto'.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
- */
- 'preload',
- /**
- * Get the value of the `error` from the media element. `error` indicates any
- * MediaError that may have occurred during playback. If error returns null there is no
- * current error.
- *
- * @method Html5#error
- * @return {MediaError|null}
- * The value of `error` from the media element. Will be `MediaError` if there
- * is a current error and null otherwise.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
- */
- 'error',
- /**
- * Get the value of `seeking` from the media element. `seeking` indicates whether the
- * media is currently seeking to a new position or not.
- *
- * @method Html5#seeking
- * @return {boolean}
- * - The value of `seeking` from the media element.
- * - True indicates that the media is currently seeking to a new position.
- * - False indicates that the media is not seeking to a new position at this time.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
- */
- 'seeking',
- /**
- * Get the value of `seekable` from the media element. `seekable` returns a
- * `TimeRange` object indicating ranges of time that can currently be `seeked` to.
- *
- * @method Html5#seekable
- * @return {TimeRange}
- * The value of `seekable` from the media element. A `TimeRange` object
- * indicating the current ranges of time that can be seeked to.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
- */
- 'seekable',
- /**
- * Get the value of `ended` from the media element. `ended` indicates whether
- * the media has reached the end or not.
- *
- * @method Html5#ended
- * @return {boolean}
- * - The value of `ended` from the media element.
- * - True indicates that the media has ended.
- * - False indicates that the media has not ended.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
- */
- 'ended',
- /**
- * Get the value of `playbackRate` from the media element. `playbackRate` indicates
- * the rate at which the media is currently playing back. Examples:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5#playbackRate
- * @return {number}
- * The value of `playbackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'playbackRate',
- /**
- * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
- * the rate at which the media is currently playing back. This value will not indicate the current
- * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
- *
- * Examples:
- * - if defaultPlaybackRate is set to 2, media will play twice as fast.
- * - if defaultPlaybackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5.prototype.defaultPlaybackRate
- * @return {number}
- * The value of `defaultPlaybackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'defaultPlaybackRate',
- /**
- * Get the value of 'disablePictureInPicture' from the video element.
- *
- * @method Html5#disablePictureInPicture
- * @return {boolean} value
- * - The value of `disablePictureInPicture` from the video element.
- * - True indicates that the video can't be played in Picture-In-Picture mode
- * - False indicates that the video can be played in Picture-In-Picture mode
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
- */
- 'disablePictureInPicture',
- /**
- * Get the value of `played` from the media element. `played` returns a `TimeRange`
- * object representing points in the media timeline that have been played.
- *
- * @method Html5#played
- * @return {TimeRange}
- * The value of `played` from the media element. A `TimeRange` object indicating
- * the ranges of time that have been played.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
- */
- 'played',
- /**
- * Get the value of `networkState` from the media element. `networkState` indicates
- * the current network state. It returns an enumeration from the following list:
- * - 0: NETWORK_EMPTY
- * - 1: NETWORK_IDLE
- * - 2: NETWORK_LOADING
- * - 3: NETWORK_NO_SOURCE
- *
- * @method Html5#networkState
- * @return {number}
- * The value of `networkState` from the media element. This will be a number
- * from the list in the description.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
- */
- 'networkState',
- /**
- * Get the value of `readyState` from the media element. `readyState` indicates
- * the current state of the media element. It returns an enumeration from the
- * following list:
- * - 0: HAVE_NOTHING
- * - 1: HAVE_METADATA
- * - 2: HAVE_CURRENT_DATA
- * - 3: HAVE_FUTURE_DATA
- * - 4: HAVE_ENOUGH_DATA
- *
- * @method Html5#readyState
- * @return {number}
- * The value of `readyState` from the media element. This will be a number
- * from the list in the description.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
- */
- 'readyState',
- /**
- * Get the value of `videoWidth` from the video element. `videoWidth` indicates
- * the current width of the video in css pixels.
- *
- * @method Html5#videoWidth
- * @return {number}
- * The value of `videoWidth` from the video element. This will be a number
- * in css pixels.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
- */
- 'videoWidth',
- /**
- * Get the value of `videoHeight` from the video element. `videoHeight` indicates
- * the current height of the video in css pixels.
- *
- * @method Html5#videoHeight
- * @return {number}
- * The value of `videoHeight` from the video element. This will be a number
- * in css pixels.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
- */
- 'videoHeight',
- /**
- * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
- * to the browser that should sent the cookies along with the requests for the
- * different assets/playlists
- *
- * @method Html5#crossOrigin
- * @return {string}
- * - anonymous indicates that the media should not sent cookies.
- * - use-credentials indicates that the media should sent cookies along the requests.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
- */
- 'crossOrigin'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop];
- };
- });
-
- // Wrap native properties with a setter in this format:
- // set + toTitleCase(name)
- // The list is as follows:
- // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
- // setDisablePictureInPicture, setCrossOrigin
- [
- /**
- * Set the value of `volume` on the media element. `volume` indicates the current
- * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
- * so on.
- *
- * @method Html5#setVolume
- * @param {number} percentAsDecimal
- * The volume percent as a decimal. Valid range is from 0-1.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
- */
- 'volume',
- /**
- * Set the value of `src` on the media element. `src` indicates the current
- * {@link Tech~SourceObject} for the media.
- *
- * @method Html5#setSrc
- * @param {Tech~SourceObject} src
- * The source object to set as the current source.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
- */
- 'src',
- /**
- * Set the value of `poster` on the media element. `poster` is the url to
- * an image file that can/will be shown when no media data is available.
- *
- * @method Html5#setPoster
- * @param {string} poster
- * The url to an image that should be used as the `poster` for the media
- * element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
- */
- 'poster',
- /**
- * Set the value of `preload` on the media element. `preload` indicates
- * what should download before the media is interacted with. It can have the following
- * values:
- * - none: nothing should be downloaded
- * - metadata: poster and the first few frames of the media may be downloaded to get
- * media dimensions and other metadata
- * - auto: allow the media and metadata for the media to be downloaded before
- * interaction
- *
- * @method Html5#setPreload
- * @param {string} preload
- * The value of `preload` to set on the media element. Must be 'none', 'metadata',
- * or 'auto'.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
- */
- 'preload',
- /**
- * Set the value of `playbackRate` on the media element. `playbackRate` indicates
- * the rate at which the media should play back. Examples:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5#setPlaybackRate
- * @return {number}
- * The value of `playbackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'playbackRate',
- /**
- * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
- * the rate at which the media should play back upon initial startup. Changing this value
- * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
- *
- * Example Values:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5.prototype.setDefaultPlaybackRate
- * @return {number}
- * The value of `defaultPlaybackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
- */
- 'defaultPlaybackRate',
- /**
- * Prevents the browser from suggesting a Picture-in-Picture context menu
- * or to request Picture-in-Picture automatically in some cases.
- *
- * @method Html5#setDisablePictureInPicture
- * @param {boolean} value
- * The true value will disable Picture-in-Picture mode.
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
- */
- 'disablePictureInPicture',
- /**
- * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
- * to the browser that should sent the cookies along with the requests for the
- * different assets/playlists
- *
- * @method Html5#setCrossOrigin
- * @param {string} crossOrigin
- * - anonymous indicates that the media should not sent cookies.
- * - use-credentials indicates that the media should sent cookies along the requests.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
- */
- 'crossOrigin'].forEach(function (prop) {
- Html5.prototype['set' + toTitleCase(prop)] = function (v) {
- this.el_[prop] = v;
- };
- });
-
- // wrap native functions with a function
- // The list is as follows:
- // pause, load, play
- [
- /**
- * A wrapper around the media elements `pause` function. This will call the `HTML5`
- * media elements `pause` function.
- *
- * @method Html5#pause
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
- */
- 'pause',
- /**
- * A wrapper around the media elements `load` function. This will call the `HTML5`s
- * media element `load` function.
- *
- * @method Html5#load
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
- */
- 'load',
- /**
- * A wrapper around the media elements `play` function. This will call the `HTML5`s
- * media element `play` function.
- *
- * @method Html5#play
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
- */
- 'play'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop]();
- };
- });
- Tech.withSourceHandlers(Html5);
-
- /**
- * Native source handler for Html5, simply passes the source to the media element.
- *
- * @property {Tech~SourceObject} source
- * The source object
- *
- * @property {Html5} tech
- * The instance of the HTML5 tech.
- */
- Html5.nativeSourceHandler = {};
-
- /**
- * Check if the media element can play the given mime type.
- *
- * @param {string} type
- * The mimetype to check
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- Html5.nativeSourceHandler.canPlayType = function (type) {
- // IE without MediaPlayer throws an error (#519)
- try {
- return Html5.TEST_VID.canPlayType(type);
- } catch (e) {
- return '';
- }
- };
-
- /**
- * Check if the media element can handle a source natively.
- *
- * @param {Tech~SourceObject} source
- * The source object
- *
- * @param {Object} [options]
- * Options to be passed to the tech.
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string).
- */
- Html5.nativeSourceHandler.canHandleSource = function (source, options) {
- // If a type was provided we should rely on that
- if (source.type) {
- return Html5.nativeSourceHandler.canPlayType(source.type);
-
- // If no type, fall back to checking 'video/[EXTENSION]'
- } else if (source.src) {
- const ext = getFileExtension(source.src);
- return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
- }
- return '';
- };
-
- /**
- * Pass the source to the native media element.
- *
- * @param {Tech~SourceObject} source
- * The source object
- *
- * @param {Html5} tech
- * The instance of the Html5 tech
- *
- * @param {Object} [options]
- * The options to pass to the source
- */
- Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
- tech.setSrc(source.src);
- };
-
- /**
- * A noop for the native dispose function, as cleanup is not needed.
- */
- Html5.nativeSourceHandler.dispose = function () {};
-
- // Register the native source handler
- Html5.registerSourceHandler(Html5.nativeSourceHandler);
- Tech.registerTech('Html5', Html5);
-
- /**
- * @file player.js
- */
-
- // The following tech events are simply re-triggered
- // on the player when they happen
- const TECH_EVENTS_RETRIGGER = [
- /**
- * Fired while the user agent is downloading media data.
- *
- * @event Player#progress
- * @type {Event}
- */
- /**
- * Retrigger the `progress` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechProgress_
- * @fires Player#progress
- * @listens Tech#progress
- */
- 'progress',
- /**
- * Fires when the loading of an audio/video is aborted.
- *
- * @event Player#abort
- * @type {Event}
- */
- /**
- * Retrigger the `abort` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechAbort_
- * @fires Player#abort
- * @listens Tech#abort
- */
- 'abort',
- /**
- * Fires when the browser is intentionally not getting media data.
- *
- * @event Player#suspend
- * @type {Event}
- */
- /**
- * Retrigger the `suspend` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechSuspend_
- * @fires Player#suspend
- * @listens Tech#suspend
- */
- 'suspend',
- /**
- * Fires when the current playlist is empty.
- *
- * @event Player#emptied
- * @type {Event}
- */
- /**
- * Retrigger the `emptied` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechEmptied_
- * @fires Player#emptied
- * @listens Tech#emptied
- */
- 'emptied',
- /**
- * Fires when the browser is trying to get media data, but data is not available.
- *
- * @event Player#stalled
- * @type {Event}
- */
- /**
- * Retrigger the `stalled` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechStalled_
- * @fires Player#stalled
- * @listens Tech#stalled
- */
- 'stalled',
- /**
- * Fires when the browser has loaded meta data for the audio/video.
- *
- * @event Player#loadedmetadata
- * @type {Event}
- */
- /**
- * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechLoadedmetadata_
- * @fires Player#loadedmetadata
- * @listens Tech#loadedmetadata
- */
- 'loadedmetadata',
- /**
- * Fires when the browser has loaded the current frame of the audio/video.
- *
- * @event Player#loadeddata
- * @type {event}
- */
- /**
- * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechLoaddeddata_
- * @fires Player#loadeddata
- * @listens Tech#loadeddata
- */
- 'loadeddata',
- /**
- * Fires when the current playback position has changed.
- *
- * @event Player#timeupdate
- * @type {event}
- */
- /**
- * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechTimeUpdate_
- * @fires Player#timeupdate
- * @listens Tech#timeupdate
- */
- 'timeupdate',
- /**
- * Fires when the video's intrinsic dimensions change
- *
- * @event Player#resize
- * @type {event}
- */
- /**
- * Retrigger the `resize` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechResize_
- * @fires Player#resize
- * @listens Tech#resize
- */
- 'resize',
- /**
- * Fires when the volume has been changed
- *
- * @event Player#volumechange
- * @type {event}
- */
- /**
- * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechVolumechange_
- * @fires Player#volumechange
- * @listens Tech#volumechange
- */
- 'volumechange',
- /**
- * Fires when the text track has been changed
- *
- * @event Player#texttrackchange
- * @type {event}
- */
- /**
- * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechTexttrackchange_
- * @fires Player#texttrackchange
- * @listens Tech#texttrackchange
- */
- 'texttrackchange'];
-
- // events to queue when playback rate is zero
- // this is a hash for the sole purpose of mapping non-camel-cased event names
- // to camel-cased function names
- const TECH_EVENTS_QUEUE = {
- canplay: 'CanPlay',
- canplaythrough: 'CanPlayThrough',
- playing: 'Playing',
- seeked: 'Seeked'
- };
- const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
- const BREAKPOINT_CLASSES = {};
-
- // grep: vjs-layout-tiny
- // grep: vjs-layout-x-small
- // grep: vjs-layout-small
- // grep: vjs-layout-medium
- // grep: vjs-layout-large
- // grep: vjs-layout-x-large
- // grep: vjs-layout-huge
- BREAKPOINT_ORDER.forEach(k => {
- const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
- BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
- });
- const DEFAULT_BREAKPOINTS = {
- tiny: 210,
- xsmall: 320,
- small: 425,
- medium: 768,
- large: 1440,
- xlarge: 2560,
- huge: Infinity
- };
-
- /**
- * An instance of the `Player` class is created when any of the Video.js setup methods
- * are used to initialize a video.
- *
- * After an instance has been created it can be accessed globally in three ways:
- * 1. By calling `videojs.getPlayer('example_video_1');`
- * 2. By calling `videojs('example_video_1');` (not recommended)
- * 2. By using it directly via `videojs.players.example_video_1;`
- *
- * @extends Component
- * @global
- */
- class Player extends Component {
- /**
- * Create an instance of this class.
- *
- * @param {Element} tag
- * The original video DOM element used for configuring options.
- *
- * @param {Object} [options]
- * Object of option names and values.
- *
- * @param {Function} [ready]
- * Ready callback function.
- */
- constructor(tag, options, ready) {
- // Make sure tag ID exists
- // also here.. probably better
- tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
-
- // Set Options
- // The options argument overrides options set in the video tag
- // which overrides globally set options.
- // This latter part coincides with the load order
- // (tag must exist before Player)
- options = Object.assign(Player.getTagSettings(tag), options);
-
- // Delay the initialization of children because we need to set up
- // player properties first, and can't use `this` before `super()`
- options.initChildren = false;
-
- // Same with creating the element
- options.createEl = false;
-
- // don't auto mixin the evented mixin
- options.evented = false;
-
- // we don't want the player to report touch activity on itself
- // see enableTouchActivity in Component
- options.reportTouchActivity = false;
-
- // If language is not set, get the closest lang attribute
- if (!options.language) {
- const closest = tag.closest('[lang]');
- if (closest) {
- options.language = closest.getAttribute('lang');
- }
- }
-
- // Run base component initializing with new options
- super(null, options, ready);
-
- // Create bound methods for document listeners.
- this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
- this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
- this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
- this.boundApplyInitTime_ = e => this.applyInitTime_(e);
- this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
- this.boundHandleTechClick_ = e => this.handleTechClick_(e);
- this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
- this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
- this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
- this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
- this.boundHandleTechTap_ = e => this.handleTechTap_(e);
-
- // default isFullscreen_ to false
- this.isFullscreen_ = false;
-
- // create logger
- this.log = createLogger(this.id_);
-
- // Hold our own reference to fullscreen api so it can be mocked in tests
- this.fsApi_ = FullscreenApi;
-
- // Tracks when a tech changes the poster
- this.isPosterFromTech_ = false;
-
- // Holds callback info that gets queued when playback rate is zero
- // and a seek is happening
- this.queuedCallbacks_ = [];
-
- // Turn off API access because we're loading a new tech that might load asynchronously
- this.isReady_ = false;
-
- // Init state hasStarted_
- this.hasStarted_ = false;
-
- // Init state userActive_
- this.userActive_ = false;
-
- // Init debugEnabled_
- this.debugEnabled_ = false;
-
- // Init state audioOnlyMode_
- this.audioOnlyMode_ = false;
-
- // Init state audioPosterMode_
- this.audioPosterMode_ = false;
-
- // Init state audioOnlyCache_
- this.audioOnlyCache_ = {
- playerHeight: null,
- hiddenChildren: []
- };
-
- // if the global option object was accidentally blown away by
- // someone, bail early with an informative error
- if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
- throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
- }
-
- // Store the original tag used to set options
- this.tag = tag;
-
- // Store the tag attributes used to restore html5 element
- this.tagAttributes = tag && getAttributes(tag);
-
- // Update current language
- this.language(this.options_.language);
-
- // Update Supported Languages
- if (options.languages) {
- // Normalise player option languages to lowercase
- const languagesToLower = {};
- Object.getOwnPropertyNames(options.languages).forEach(function (name) {
- languagesToLower[name.toLowerCase()] = options.languages[name];
- });
- this.languages_ = languagesToLower;
- } else {
- this.languages_ = Player.prototype.options_.languages;
- }
- this.resetCache_();
-
- // Set poster
- /** @type string */
- this.poster_ = options.poster || '';
-
- // Set controls
- /** @type {boolean} */
- this.controls_ = !!options.controls;
-
- // Original tag settings stored in options
- // now remove immediately so native controls don't flash.
- // May be turned back on by HTML5 tech if nativeControlsForTouch is true
- tag.controls = false;
- tag.removeAttribute('controls');
- this.changingSrc_ = false;
- this.playCallbacks_ = [];
- this.playTerminatedQueue_ = [];
-
- // the attribute overrides the option
- if (tag.hasAttribute('autoplay')) {
- this.autoplay(true);
- } else {
- // otherwise use the setter to validate and
- // set the correct value.
- this.autoplay(this.options_.autoplay);
- }
-
- // check plugins
- if (options.plugins) {
- Object.keys(options.plugins).forEach(name => {
- if (typeof this[name] !== 'function') {
- throw new Error(`plugin "${name}" does not exist`);
- }
- });
- }
-
- /*
- * Store the internal state of scrubbing
- *
- * @private
- * @return {Boolean} True if the user is scrubbing
- */
- this.scrubbing_ = false;
- this.el_ = this.createEl();
-
- // Make this an evented object and use `el_` as its event bus.
- evented(this, {
- eventBusKey: 'el_'
- });
-
- // listen to document and player fullscreenchange handlers so we receive those events
- // before a user can receive them so we can update isFullscreen appropriately.
- // make sure that we listen to fullscreenchange events before everything else to make sure that
- // our isFullscreen method is updated properly for internal components as well as external.
- if (this.fsApi_.requestFullscreen) {
- on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- }
- if (this.fluid_) {
- this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- }
- // We also want to pass the original player options to each component and plugin
- // as well so they don't need to reach back into the player for options later.
- // We also need to do another copy of this.options_ so we don't end up with
- // an infinite loop.
- const playerOptionsCopy = merge(this.options_);
-
- // Load plugins
- if (options.plugins) {
- Object.keys(options.plugins).forEach(name => {
- this[name](options.plugins[name]);
- });
- }
-
- // Enable debug mode to fire debugon event for all plugins.
- if (options.debug) {
- this.debug(true);
- }
- this.options_.playerOptions = playerOptionsCopy;
- this.middleware_ = [];
- this.playbackRates(options.playbackRates);
- if (options.experimentalSvgIcons) {
- // Add SVG Sprite to the DOM
- const parser = new window.DOMParser();
- const parsedSVG = parser.parseFromString(icons, 'image/svg+xml');
- const errorNode = parsedSVG.querySelector('parsererror');
- if (errorNode) {
- log.warn('Failed to load SVG Icons. Falling back to Font Icons.');
- this.options_.experimentalSvgIcons = null;
- } else {
- const sprite = parsedSVG.documentElement;
- sprite.style.display = 'none';
- this.el_.appendChild(sprite);
- this.addClass('vjs-svg-icons-enabled');
- }
- }
- this.initChildren();
-
- // Set isAudio based on whether or not an audio tag was used
- this.isAudio(tag.nodeName.toLowerCase() === 'audio');
-
- // Update controls className. Can't do this when the controls are initially
- // set because the element doesn't exist yet.
- if (this.controls()) {
- this.addClass('vjs-controls-enabled');
- } else {
- this.addClass('vjs-controls-disabled');
- }
-
- // Set ARIA label and region role depending on player type
- this.el_.setAttribute('role', 'region');
- if (this.isAudio()) {
- this.el_.setAttribute('aria-label', this.localize('Audio Player'));
- } else {
- this.el_.setAttribute('aria-label', this.localize('Video Player'));
- }
- if (this.isAudio()) {
- this.addClass('vjs-audio');
- }
-
- // TODO: Make this smarter. Toggle user state between touching/mousing
- // using events, since devices can have both touch and mouse events.
- // TODO: Make this check be performed again when the window switches between monitors
- // (See https://github.com/videojs/video.js/issues/5683)
- if (TOUCH_ENABLED) {
- this.addClass('vjs-touch-enabled');
- }
-
- // iOS Safari has broken hover handling
- if (!IS_IOS) {
- this.addClass('vjs-workinghover');
- }
-
- // Make player easily findable by ID
- Player.players[this.id_] = this;
-
- // Add a major version class to aid css in plugins
- const majorVersion = version.split('.')[0];
- this.addClass(`vjs-v${majorVersion}`);
-
- // When the player is first initialized, trigger activity so components
- // like the control bar show themselves if needed
- this.userActive(true);
- this.reportUserActivity();
- this.one('play', e => this.listenForUserActivity_(e));
- this.on('keydown', e => this.handleKeyDown(e));
- this.on('languagechange', e => this.handleLanguagechange(e));
- this.breakpoints(this.options_.breakpoints);
- this.responsive(this.options_.responsive);
-
- // Calling both the audio mode methods after the player is fully
- // setup to be able to listen to the events triggered by them
- this.on('ready', () => {
- // Calling the audioPosterMode method first so that
- // the audioOnlyMode can take precedence when both options are set to true
- this.audioPosterMode(this.options_.audioPosterMode);
- this.audioOnlyMode(this.options_.audioOnlyMode);
- });
- }
-
- /**
- * Destroys the video player and does any necessary cleanup.
- *
- * This is especially helpful if you are dynamically adding and removing videos
- * to/from the DOM.
- *
- * @fires Player#dispose
- */
- dispose() {
- /**
- * Called when the player is being disposed of.
- *
- * @event Player#dispose
- * @type {Event}
- */
- this.trigger('dispose');
- // prevent dispose from being called twice
- this.off('dispose');
-
- // Make sure all player-specific document listeners are unbound. This is
- off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- off(document, 'keydown', this.boundFullWindowOnEscKey_);
- if (this.styleEl_ && this.styleEl_.parentNode) {
- this.styleEl_.parentNode.removeChild(this.styleEl_);
- this.styleEl_ = null;
- }
-
- // Kill reference to this player
- Player.players[this.id_] = null;
- if (this.tag && this.tag.player) {
- this.tag.player = null;
- }
- if (this.el_ && this.el_.player) {
- this.el_.player = null;
- }
- if (this.tech_) {
- this.tech_.dispose();
- this.isPosterFromTech_ = false;
- this.poster_ = '';
- }
- if (this.playerElIngest_) {
- this.playerElIngest_ = null;
- }
- if (this.tag) {
- this.tag = null;
- }
- clearCacheForPlayer(this);
-
- // remove all event handlers for track lists
- // all tracks and track listeners are removed on
- // tech dispose
- ALL.names.forEach(name => {
- const props = ALL[name];
- const list = this[props.getterName]();
-
- // if it is not a native list
- // we have to manually remove event listeners
- if (list && list.off) {
- list.off();
- }
- });
-
- // the actual .el_ is removed here, or replaced if
- super.dispose({
- restoreEl: this.options_.restoreEl
- });
- }
-
- /**
- * Create the `Player`'s DOM element.
- *
- * @return {Element}
- * The DOM element that gets created.
- */
- createEl() {
- let tag = this.tag;
- let el;
- let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
- const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
- if (playerElIngest) {
- el = this.el_ = tag.parentNode;
- } else if (!divEmbed) {
- el = this.el_ = super.createEl('div');
- }
-
- // Copy over all the attributes from the tag, including ID and class
- // ID will now reference player box, not the video tag
- const attrs = getAttributes(tag);
- if (divEmbed) {
- el = this.el_ = tag;
- tag = this.tag = document.createElement('video');
- while (el.children.length) {
- tag.appendChild(el.firstChild);
- }
- if (!hasClass(el, 'video-js')) {
- addClass(el, 'video-js');
- }
- el.appendChild(tag);
- playerElIngest = this.playerElIngest_ = el;
- // move properties over from our custom `video-js` element
- // to our new `video` element. This will move things like
- // `src` or `controls` that were set via js before the player
- // was initialized.
- Object.keys(el).forEach(k => {
- try {
- tag[k] = el[k];
- } catch (e) {
- // we got a a property like outerHTML which we can't actually copy, ignore it
- }
- });
- }
-
- // set tabindex to -1 to remove the video element from the focus order
- tag.setAttribute('tabindex', '-1');
- attrs.tabindex = '-1';
-
- // Workaround for #4583 on Chrome (on Windows) with JAWS.
- // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
- // Note that we can't detect if JAWS is being used, but this ARIA attribute
- // doesn't change behavior of Chrome if JAWS is not being used
- if (IS_CHROME && IS_WINDOWS) {
- tag.setAttribute('role', 'application');
- attrs.role = 'application';
- }
-
- // Remove width/height attrs from tag so CSS can make it 100% width/height
- tag.removeAttribute('width');
- tag.removeAttribute('height');
- if ('width' in attrs) {
- delete attrs.width;
- }
- if ('height' in attrs) {
- delete attrs.height;
- }
- Object.getOwnPropertyNames(attrs).forEach(function (attr) {
- // don't copy over the class attribute to the player element when we're in a div embed
- // the class is already set up properly in the divEmbed case
- // and we want to make sure that the `video-js` class doesn't get lost
- if (!(divEmbed && attr === 'class')) {
- el.setAttribute(attr, attrs[attr]);
- }
- if (divEmbed) {
- tag.setAttribute(attr, attrs[attr]);
- }
- });
-
- // Update tag id/class for use as HTML5 playback tech
- // Might think we should do this after embedding in container so .vjs-tech class
- // doesn't flash 100% width/height, but class only applies with .video-js parent
- tag.playerId = tag.id;
- tag.id += '_html5_api';
- tag.className = 'vjs-tech';
-
- // Make player findable on elements
- tag.player = el.player = this;
- // Default state of video is paused
- this.addClass('vjs-paused');
- const deviceClassNames = ['IS_SMART_TV', 'IS_TIZEN', 'IS_WEBOS', 'IS_ANDROID', 'IS_IPAD', 'IS_IPHONE'].filter(key => browser[key]).map(key => {
- return 'vjs-device-' + key.substring(3).toLowerCase().replace(/\_/g, '-');
- });
- this.addClass(...deviceClassNames);
-
- // Add a style element in the player that we'll use to set the width/height
- // of the player in a way that's still overridable by CSS, just like the
- // video element
- if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
- this.styleEl_ = createStyleElement('vjs-styles-dimensions');
- const defaultsStyleEl = $('.vjs-styles-defaults');
- const head = $('head');
- head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
- }
- this.fill_ = false;
- this.fluid_ = false;
-
- // Pass in the width/height/aspectRatio options which will update the style el
- this.width(this.options_.width);
- this.height(this.options_.height);
- this.fill(this.options_.fill);
- this.fluid(this.options_.fluid);
- this.aspectRatio(this.options_.aspectRatio);
- // support both crossOrigin and crossorigin to reduce confusion and issues around the name
- this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
-
- // Hide any links within the video/audio tag,
- // because IE doesn't hide them completely from screen readers.
- const links = tag.getElementsByTagName('a');
- for (let i = 0; i < links.length; i++) {
- const linkEl = links.item(i);
- addClass(linkEl, 'vjs-hidden');
- linkEl.setAttribute('hidden', 'hidden');
- }
-
- // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
- // keep track of the original for later so we can know if the source originally failed
- tag.initNetworkState_ = tag.networkState;
-
- // Wrap video tag in div (el/box) container
- if (tag.parentNode && !playerElIngest) {
- tag.parentNode.insertBefore(el, tag);
- }
-
- // insert the tag as the first child of the player element
- // then manually add it to the children array so that this.addChild
- // will work properly for other components
- //
- // Breaks iPhone, fixed in HTML5 setup.
- prependTo(tag, el);
- this.children_.unshift(tag);
-
- // Set lang attr on player to ensure CSS :lang() in consistent with player
- // if it's been set to something different to the doc
- this.el_.setAttribute('lang', this.language_);
- this.el_.setAttribute('translate', 'no');
- this.el_ = el;
- return el;
- }
-
- /**
- * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the `` tag to control the CORS
- * behavior.
- *
- * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
- *
- * @param {string|null} [value]
- * The value to set the `Player`'s crossOrigin to. If an argument is
- * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
- *
- * @return {string|null|undefined}
- * - The current crossOrigin value of the `Player` when getting.
- * - undefined when setting
- */
- crossOrigin(value) {
- // `null` can be set to unset a value
- if (typeof value === 'undefined') {
- return this.techGet_('crossOrigin');
- }
- if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
- log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
- return;
- }
- this.techCall_('setCrossOrigin', value);
- if (this.posterImage) {
- this.posterImage.crossOrigin(value);
- }
- return;
- }
-
- /**
- * A getter/setter for the `Player`'s width. Returns the player's configured value.
- * To get the current width use `currentWidth()`.
- *
- * @param {number|string} [value]
- * CSS value to set the `Player`'s width to.
- *
- * @return {number|undefined}
- * - The current width of the `Player` when getting.
- * - Nothing when setting
- */
- width(value) {
- return this.dimension('width', value);
- }
-
- /**
- * A getter/setter for the `Player`'s height. Returns the player's configured value.
- * To get the current height use `currentheight()`.
- *
- * @param {number|string} [value]
- * CSS value to set the `Player`'s height to.
- *
- * @return {number|undefined}
- * - The current height of the `Player` when getting.
- * - Nothing when setting
- */
- height(value) {
- return this.dimension('height', value);
- }
-
- /**
- * A getter/setter for the `Player`'s width & height.
- *
- * @param {string} dimension
- * This string can be:
- * - 'width'
- * - 'height'
- *
- * @param {number|string} [value]
- * Value for dimension specified in the first argument.
- *
- * @return {number}
- * The dimension arguments value when getting (width/height).
- */
- dimension(dimension, value) {
- const privDimension = dimension + '_';
- if (value === undefined) {
- return this[privDimension] || 0;
- }
- if (value === '' || value === 'auto') {
- // If an empty string is given, reset the dimension to be automatic
- this[privDimension] = undefined;
- this.updateStyleEl_();
- return;
- }
- const parsedVal = parseFloat(value);
- if (isNaN(parsedVal)) {
- log.error(`Improper value "${value}" supplied for for ${dimension}`);
- return;
- }
- this[privDimension] = parsedVal;
- this.updateStyleEl_();
- }
-
- /**
- * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
- *
- * Turning this on will turn off fill mode.
- *
- * @param {boolean} [bool]
- * - A value of true adds the class.
- * - A value of false removes the class.
- * - No value will be a getter.
- *
- * @return {boolean|undefined}
- * - The value of fluid when getting.
- * - `undefined` when setting.
- */
- fluid(bool) {
- if (bool === undefined) {
- return !!this.fluid_;
- }
- this.fluid_ = !!bool;
- if (isEvented(this)) {
- this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- }
- if (bool) {
- this.addClass('vjs-fluid');
- this.fill(false);
- addEventedCallback(this, () => {
- this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- });
- } else {
- this.removeClass('vjs-fluid');
- }
- this.updateStyleEl_();
- }
-
- /**
- * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
- *
- * Turning this on will turn off fluid mode.
- *
- * @param {boolean} [bool]
- * - A value of true adds the class.
- * - A value of false removes the class.
- * - No value will be a getter.
- *
- * @return {boolean|undefined}
- * - The value of fluid when getting.
- * - `undefined` when setting.
- */
- fill(bool) {
- if (bool === undefined) {
- return !!this.fill_;
- }
- this.fill_ = !!bool;
- if (bool) {
- this.addClass('vjs-fill');
- this.fluid(false);
- } else {
- this.removeClass('vjs-fill');
- }
- }
-
- /**
- * Get/Set the aspect ratio
- *
- * @param {string} [ratio]
- * Aspect ratio for player
- *
- * @return {string|undefined}
- * returns the current aspect ratio when getting
- */
-
- /**
- * A getter/setter for the `Player`'s aspect ratio.
- *
- * @param {string} [ratio]
- * The value to set the `Player`'s aspect ratio to.
- *
- * @return {string|undefined}
- * - The current aspect ratio of the `Player` when getting.
- * - undefined when setting
- */
- aspectRatio(ratio) {
- if (ratio === undefined) {
- return this.aspectRatio_;
- }
-
- // Check for width:height format
- if (!/^\d+\:\d+$/.test(ratio)) {
- throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
- }
- this.aspectRatio_ = ratio;
-
- // We're assuming if you set an aspect ratio you want fluid mode,
- // because in fixed mode you could calculate width and height yourself.
- this.fluid(true);
- this.updateStyleEl_();
- }
-
- /**
- * Update styles of the `Player` element (height, width and aspect ratio).
- *
- * @private
- * @listens Tech#loadedmetadata
- */
- updateStyleEl_() {
- if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) {
- const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
- const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
- const techEl = this.tech_ && this.tech_.el();
- if (techEl) {
- if (width >= 0) {
- techEl.width = width;
- }
- if (height >= 0) {
- techEl.height = height;
- }
- }
- return;
- }
- let width;
- let height;
- let aspectRatio;
- let idClass;
-
- // The aspect ratio is either used directly or to calculate width and height.
- if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
- // Use any aspectRatio that's been specifically set
- aspectRatio = this.aspectRatio_;
- } else if (this.videoWidth() > 0) {
- // Otherwise try to get the aspect ratio from the video metadata
- aspectRatio = this.videoWidth() + ':' + this.videoHeight();
- } else {
- // Or use a default. The video element's is 2:1, but 16:9 is more common.
- aspectRatio = '16:9';
- }
-
- // Get the ratio as a decimal we can use to calculate dimensions
- const ratioParts = aspectRatio.split(':');
- const ratioMultiplier = ratioParts[1] / ratioParts[0];
- if (this.width_ !== undefined) {
- // Use any width that's been specifically set
- width = this.width_;
- } else if (this.height_ !== undefined) {
- // Or calculate the width from the aspect ratio if a height has been set
- width = this.height_ / ratioMultiplier;
- } else {
- // Or use the video's metadata, or use the video el's default of 300
- width = this.videoWidth() || 300;
- }
- if (this.height_ !== undefined) {
- // Use any height that's been specifically set
- height = this.height_;
- } else {
- // Otherwise calculate the height from the ratio and the width
- height = width * ratioMultiplier;
- }
-
- // Ensure the CSS class is valid by starting with an alpha character
- if (/^[^a-zA-Z]/.test(this.id())) {
- idClass = 'dimensions-' + this.id();
- } else {
- idClass = this.id() + '-dimensions';
- }
-
- // Ensure the right class is still on the player for the style element
- this.addClass(idClass);
- setTextContent(this.styleEl_, `
- .${idClass} {
- width: ${width}px;
- height: ${height}px;
- }
-
- .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: ${ratioMultiplier * 100}%;
- }
- `);
- }
-
- /**
- * Load/Create an instance of playback {@link Tech} including element
- * and API methods. Then append the `Tech` element in `Player` as a child.
- *
- * @param {string} techName
- * name of the playback technology
- *
- * @param {string} source
- * video source
- *
- * @private
- */
- loadTech_(techName, source) {
- // Pause and remove current playback technology
- if (this.tech_) {
- this.unloadTech_();
- }
- const titleTechName = toTitleCase(techName);
- const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
-
- // get rid of the HTML5 video tag as soon as we are using another tech
- if (titleTechName !== 'Html5' && this.tag) {
- Tech.getTech('Html5').disposeMediaElement(this.tag);
- this.tag.player = null;
- this.tag = null;
- }
- this.techName_ = titleTechName;
-
- // Turn off API access because we're loading a new tech that might load asynchronously
- this.isReady_ = false;
- let autoplay = this.autoplay();
-
- // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
- // because the player is going to handle autoplay on `loadstart`
- if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
- autoplay = false;
- }
-
- // Grab tech-specific options from player options and add source and parent element to use.
- const techOptions = {
- source,
- autoplay,
- 'nativeControlsForTouch': this.options_.nativeControlsForTouch,
- 'playerId': this.id(),
- 'techId': `${this.id()}_${camelTechName}_api`,
- 'playsinline': this.options_.playsinline,
- 'preload': this.options_.preload,
- 'loop': this.options_.loop,
- 'disablePictureInPicture': this.options_.disablePictureInPicture,
- 'muted': this.options_.muted,
- 'poster': this.poster(),
- 'language': this.language(),
- 'playerElIngest': this.playerElIngest_ || false,
- 'vtt.js': this.options_['vtt.js'],
- 'canOverridePoster': !!this.options_.techCanOverridePoster,
- 'enableSourceset': this.options_.enableSourceset
- };
- ALL.names.forEach(name => {
- const props = ALL[name];
- techOptions[props.getterName] = this[props.privateName];
- });
- Object.assign(techOptions, this.options_[titleTechName]);
- Object.assign(techOptions, this.options_[camelTechName]);
- Object.assign(techOptions, this.options_[techName.toLowerCase()]);
- if (this.tag) {
- techOptions.tag = this.tag;
- }
- if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
- techOptions.startTime = this.cache_.currentTime;
- }
-
- // Initialize tech instance
- const TechClass = Tech.getTech(techName);
- if (!TechClass) {
- throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
- }
- this.tech_ = new TechClass(techOptions);
-
- // player.triggerReady is always async, so don't need this to be async
- this.tech_.ready(bind_(this, this.handleTechReady_), true);
- textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
-
- // Listen to all HTML5-defined events and trigger them on the player
- TECH_EVENTS_RETRIGGER.forEach(event => {
- this.on(this.tech_, event, e => this[`handleTech${toTitleCase(event)}_`](e));
- });
- Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
- this.on(this.tech_, event, eventObj => {
- if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
- this.queuedCallbacks_.push({
- callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
- event: eventObj
- });
- return;
- }
- this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
- });
- });
- this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
- this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
- this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
- this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
- this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
- this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
- this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
- this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
- this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
- this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
- this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
- this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
- this.on(this.tech_, 'error', e => this.handleTechError_(e));
- this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
- this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
- this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
- this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
- this.usingNativeControls(this.techGet_('controls'));
- if (this.controls() && !this.usingNativeControls()) {
- this.addTechControlsListeners_();
- }
-
- // Add the tech element in the DOM if it was not already there
- // Make sure to not insert the original video element if using Html5
- if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
- prependTo(this.tech_.el(), this.el());
- }
-
- // Get rid of the original video tag reference after the first tech is loaded
- if (this.tag) {
- this.tag.player = null;
- this.tag = null;
- }
- }
-
- /**
- * Unload and dispose of the current playback {@link Tech}.
- *
- * @private
- */
- unloadTech_() {
- // Save the current text tracks so that we can reuse the same text tracks with the next tech
- ALL.names.forEach(name => {
- const props = ALL[name];
- this[props.privateName] = this[props.getterName]();
- });
- this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
- this.isReady_ = false;
- this.tech_.dispose();
- this.tech_ = false;
- if (this.isPosterFromTech_) {
- this.poster_ = '';
- this.trigger('posterchange');
- }
- this.isPosterFromTech_ = false;
- }
-
- /**
- * Return a reference to the current {@link Tech}.
- * It will print a warning by default about the danger of using the tech directly
- * but any argument that is passed in will silence the warning.
- *
- * @param {*} [safety]
- * Anything passed in to silence the warning
- *
- * @return {Tech}
- * The Tech
- */
- tech(safety) {
- if (safety === undefined) {
- log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
- }
- return this.tech_;
- }
-
- /**
- * An object that contains Video.js version.
- *
- * @typedef {Object} PlayerVersion
- *
- * @property {string} 'video.js' - Video.js version
- */
-
- /**
- * Returns an object with Video.js version.
- *
- * @return {PlayerVersion}
- * An object with Video.js version.
- */
- version() {
- return {
- 'video.js': version
- };
- }
-
- /**
- * Set up click and touch listeners for the playback element
- *
- * - On desktops: a click on the video itself will toggle playback
- * - On mobile devices: a click on the video toggles controls
- * which is done by toggling the user state between active and
- * inactive
- * - A tap can signal that a user has become active or has become inactive
- * e.g. a quick tap on an iPhone movie should reveal the controls. Another
- * quick tap should hide them again (signaling the user is in an inactive
- * viewing state)
- * - In addition to this, we still want the user to be considered inactive after
- * a few seconds of inactivity.
- *
- * > Note: the only part of iOS interaction we can't mimic with this setup
- * is a touch and hold on the video element counting as activity in order to
- * keep the controls showing, but that shouldn't be an issue. A touch and hold
- * on any controls will still keep the user active
- *
- * @private
- */
- addTechControlsListeners_() {
- // Make sure to remove all the previous listeners in case we are called multiple times.
- this.removeTechControlsListeners_();
- this.on(this.tech_, 'click', this.boundHandleTechClick_);
- this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
-
- // If the controls were hidden we don't want that to change without a tap event
- // so we'll check if the controls were already showing before reporting user
- // activity
- this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
- this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
- this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
-
- // The tap listener needs to come after the touchend listener because the tap
- // listener cancels out any reportedUserActivity when setting userActive(false)
- this.on(this.tech_, 'tap', this.boundHandleTechTap_);
- }
-
- /**
- * Remove the listeners used for click and tap controls. This is needed for
- * toggling to controls disabled, where a tap/touch should do nothing.
- *
- * @private
- */
- removeTechControlsListeners_() {
- // We don't want to just use `this.off()` because there might be other needed
- // listeners added by techs that extend this.
- this.off(this.tech_, 'tap', this.boundHandleTechTap_);
- this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
- this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
- this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
- this.off(this.tech_, 'click', this.boundHandleTechClick_);
- this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
- }
-
- /**
- * Player waits for the tech to be ready
- *
- * @private
- */
- handleTechReady_() {
- this.triggerReady();
-
- // Keep the same volume as before
- if (this.cache_.volume) {
- this.techCall_('setVolume', this.cache_.volume);
- }
-
- // Look if the tech found a higher resolution poster while loading
- this.handleTechPosterChange_();
-
- // Update the duration if available
- this.handleTechDurationChange_();
- }
-
- /**
- * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
- *
- * @fires Player#loadstart
- * @listens Tech#loadstart
- * @private
- */
- handleTechLoadStart_() {
- // TODO: Update to use `emptied` event instead. See #1277.
-
- this.removeClass('vjs-ended', 'vjs-seeking');
-
- // reset the error state
- this.error(null);
-
- // Update the duration
- this.handleTechDurationChange_();
- if (!this.paused()) {
- /**
- * Fired when the user agent begins looking for media data
- *
- * @event Player#loadstart
- * @type {Event}
- */
- this.trigger('loadstart');
- } else {
- // reset the hasStarted state
- this.hasStarted(false);
- this.trigger('loadstart');
- }
-
- // autoplay happens after loadstart for the browser,
- // so we mimic that behavior
- this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
- }
-
- /**
- * Handle autoplay string values, rather than the typical boolean
- * values that should be handled by the tech. Note that this is not
- * part of any specification. Valid values and what they do can be
- * found on the autoplay getter at Player#autoplay()
- */
- manualAutoplay_(type) {
- if (!this.tech_ || typeof type !== 'string') {
- return;
- }
-
- // Save original muted() value, set muted to true, and attempt to play().
- // On promise rejection, restore muted from saved value
- const resolveMuted = () => {
- const previouslyMuted = this.muted();
- this.muted(true);
- const restoreMuted = () => {
- this.muted(previouslyMuted);
- };
-
- // restore muted on play terminatation
- this.playTerminatedQueue_.push(restoreMuted);
- const mutedPromise = this.play();
- if (!isPromise(mutedPromise)) {
- return;
- }
- return mutedPromise.catch(err => {
- restoreMuted();
- throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
- });
- };
- let promise;
-
- // if muted defaults to true
- // the only thing we can do is call play
- if (type === 'any' && !this.muted()) {
- promise = this.play();
- if (isPromise(promise)) {
- promise = promise.catch(resolveMuted);
- }
- } else if (type === 'muted' && !this.muted()) {
- promise = resolveMuted();
- } else {
- promise = this.play();
- }
- if (!isPromise(promise)) {
- return;
- }
- return promise.then(() => {
- this.trigger({
- type: 'autoplay-success',
- autoplay: type
- });
- }).catch(() => {
- this.trigger({
- type: 'autoplay-failure',
- autoplay: type
- });
- });
- }
-
- /**
- * Update the internal source caches so that we return the correct source from
- * `src()`, `currentSource()`, and `currentSources()`.
- *
- * > Note: `currentSources` will not be updated if the source that is passed in exists
- * in the current `currentSources` cache.
- *
- *
- * @param {Tech~SourceObject} srcObj
- * A string or object source to update our caches to.
- */
- updateSourceCaches_(srcObj = '') {
- let src = srcObj;
- let type = '';
- if (typeof src !== 'string') {
- src = srcObj.src;
- type = srcObj.type;
- }
-
- // make sure all the caches are set to default values
- // to prevent null checking
- this.cache_.source = this.cache_.source || {};
- this.cache_.sources = this.cache_.sources || [];
-
- // try to get the type of the src that was passed in
- if (src && !type) {
- type = findMimetype(this, src);
- }
-
- // update `currentSource` cache always
- this.cache_.source = merge({}, srcObj, {
- src,
- type
- });
- const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
- const sourceElSources = [];
- const sourceEls = this.$$('source');
- const matchingSourceEls = [];
- for (let i = 0; i < sourceEls.length; i++) {
- const sourceObj = getAttributes(sourceEls[i]);
- sourceElSources.push(sourceObj);
- if (sourceObj.src && sourceObj.src === src) {
- matchingSourceEls.push(sourceObj.src);
- }
- }
-
- // if we have matching source els but not matching sources
- // the current source cache is not up to date
- if (matchingSourceEls.length && !matchingSources.length) {
- this.cache_.sources = sourceElSources;
- // if we don't have matching source or source els set the
- // sources cache to the `currentSource` cache
- } else if (!matchingSources.length) {
- this.cache_.sources = [this.cache_.source];
- }
-
- // update the tech `src` cache
- this.cache_.src = src;
- }
-
- /**
- * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
- * causing the media element to reload.
- *
- * It will fire for the initial source and each subsequent source.
- * This event is a custom event from Video.js and is triggered by the {@link Tech}.
- *
- * The event object for this event contains a `src` property that will contain the source
- * that was available when the event was triggered. This is generally only necessary if Video.js
- * is switching techs while the source was being changed.
- *
- * It is also fired when `load` is called on the player (or media element)
- * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
- * says that the resource selection algorithm needs to be aborted and restarted.
- * In this case, it is very likely that the `src` property will be set to the
- * empty string `""` to indicate we do not know what the source will be but
- * that it is changing.
- *
- * *This event is currently still experimental and may change in minor releases.*
- * __To use this, pass `enableSourceset` option to the player.__
- *
- * @event Player#sourceset
- * @type {Event}
- * @prop {string} src
- * The source url available when the `sourceset` was triggered.
- * It will be an empty string if we cannot know what the source is
- * but know that the source will change.
- */
- /**
- * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
- *
- * @fires Player#sourceset
- * @listens Tech#sourceset
- * @private
- */
- handleTechSourceset_(event) {
- // only update the source cache when the source
- // was not updated using the player api
- if (!this.changingSrc_) {
- let updateSourceCaches = src => this.updateSourceCaches_(src);
- const playerSrc = this.currentSource().src;
- const eventSrc = event.src;
-
- // if we have a playerSrc that is not a blob, and a tech src that is a blob
- if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
- // if both the tech source and the player source were updated we assume
- // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
- if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
- updateSourceCaches = () => {};
- }
- }
-
- // update the source to the initial source right away
- // in some cases this will be empty string
- updateSourceCaches(eventSrc);
-
- // if the `sourceset` `src` was an empty string
- // wait for a `loadstart` to update the cache to `currentSrc`.
- // If a sourceset happens before a `loadstart`, we reset the state
- if (!event.src) {
- this.tech_.any(['sourceset', 'loadstart'], e => {
- // if a sourceset happens before a `loadstart` there
- // is nothing to do as this `handleTechSourceset_`
- // will be called again and this will be handled there.
- if (e.type === 'sourceset') {
- return;
- }
- const techSrc = this.techGet_('currentSrc');
- this.lastSource_.tech = techSrc;
- this.updateSourceCaches_(techSrc);
- });
- }
- }
- this.lastSource_ = {
- player: this.currentSource().src,
- tech: event.src
- };
- this.trigger({
- src: event.src,
- type: 'sourceset'
- });
- }
-
- /**
- * Add/remove the vjs-has-started class
- *
- *
- * @param {boolean} request
- * - true: adds the class
- * - false: remove the class
- *
- * @return {boolean}
- * the boolean value of hasStarted_
- */
- hasStarted(request) {
- if (request === undefined) {
- // act as getter, if we have no request to change
- return this.hasStarted_;
- }
- if (request === this.hasStarted_) {
- return;
- }
- this.hasStarted_ = request;
- if (this.hasStarted_) {
- this.addClass('vjs-has-started');
- } else {
- this.removeClass('vjs-has-started');
- }
- }
-
- /**
- * Fired whenever the media begins or resumes playback
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
- * @fires Player#play
- * @listens Tech#play
- * @private
- */
- handleTechPlay_() {
- this.removeClass('vjs-ended', 'vjs-paused');
- this.addClass('vjs-playing');
-
- // hide the poster when the user hits play
- this.hasStarted(true);
- /**
- * Triggered whenever an {@link Tech#play} event happens. Indicates that
- * playback has started or resumed.
- *
- * @event Player#play
- * @type {Event}
- */
- this.trigger('play');
- }
-
- /**
- * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
- *
- * If there were any events queued while the playback rate was zero, fire
- * those events now.
- *
- * @private
- * @method Player#handleTechRateChange_
- * @fires Player#ratechange
- * @listens Tech#ratechange
- */
- handleTechRateChange_() {
- if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
- this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
- this.queuedCallbacks_ = [];
- }
- this.cache_.lastPlaybackRate = this.tech_.playbackRate();
- /**
- * Fires when the playing speed of the audio/video is changed
- *
- * @event Player#ratechange
- * @type {event}
- */
- this.trigger('ratechange');
- }
-
- /**
- * Retrigger the `waiting` event that was triggered by the {@link Tech}.
- *
- * @fires Player#waiting
- * @listens Tech#waiting
- * @private
- */
- handleTechWaiting_() {
- this.addClass('vjs-waiting');
- /**
- * A readyState change on the DOM element has caused playback to stop.
- *
- * @event Player#waiting
- * @type {Event}
- */
- this.trigger('waiting');
-
- // Browsers may emit a timeupdate event after a waiting event. In order to prevent
- // premature removal of the waiting class, wait for the time to change.
- const timeWhenWaiting = this.currentTime();
- const timeUpdateListener = () => {
- if (timeWhenWaiting !== this.currentTime()) {
- this.removeClass('vjs-waiting');
- this.off('timeupdate', timeUpdateListener);
- }
- };
- this.on('timeupdate', timeUpdateListener);
- }
-
- /**
- * Retrigger the `canplay` event that was triggered by the {@link Tech}.
- * > Note: This is not consistent between browsers. See #1351
- *
- * @fires Player#canplay
- * @listens Tech#canplay
- * @private
- */
- handleTechCanPlay_() {
- this.removeClass('vjs-waiting');
- /**
- * The media has a readyState of HAVE_FUTURE_DATA or greater.
- *
- * @event Player#canplay
- * @type {Event}
- */
- this.trigger('canplay');
- }
-
- /**
- * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
- *
- * @fires Player#canplaythrough
- * @listens Tech#canplaythrough
- * @private
- */
- handleTechCanPlayThrough_() {
- this.removeClass('vjs-waiting');
- /**
- * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
- * entire media file can be played without buffering.
- *
- * @event Player#canplaythrough
- * @type {Event}
- */
- this.trigger('canplaythrough');
- }
-
- /**
- * Retrigger the `playing` event that was triggered by the {@link Tech}.
- *
- * @fires Player#playing
- * @listens Tech#playing
- * @private
- */
- handleTechPlaying_() {
- this.removeClass('vjs-waiting');
- /**
- * The media is no longer blocked from playback, and has started playing.
- *
- * @event Player#playing
- * @type {Event}
- */
- this.trigger('playing');
- }
-
- /**
- * Retrigger the `seeking` event that was triggered by the {@link Tech}.
- *
- * @fires Player#seeking
- * @listens Tech#seeking
- * @private
- */
- handleTechSeeking_() {
- this.addClass('vjs-seeking');
- /**
- * Fired whenever the player is jumping to a new time
- *
- * @event Player#seeking
- * @type {Event}
- */
- this.trigger('seeking');
- }
-
- /**
- * Retrigger the `seeked` event that was triggered by the {@link Tech}.
- *
- * @fires Player#seeked
- * @listens Tech#seeked
- * @private
- */
- handleTechSeeked_() {
- this.removeClass('vjs-seeking', 'vjs-ended');
- /**
- * Fired when the player has finished jumping to a new time
- *
- * @event Player#seeked
- * @type {Event}
- */
- this.trigger('seeked');
- }
-
- /**
- * Retrigger the `pause` event that was triggered by the {@link Tech}.
- *
- * @fires Player#pause
- * @listens Tech#pause
- * @private
- */
- handleTechPause_() {
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- /**
- * Fired whenever the media has been paused
- *
- * @event Player#pause
- * @type {Event}
- */
- this.trigger('pause');
- }
-
- /**
- * Retrigger the `ended` event that was triggered by the {@link Tech}.
- *
- * @fires Player#ended
- * @listens Tech#ended
- * @private
- */
- handleTechEnded_() {
- this.addClass('vjs-ended');
- this.removeClass('vjs-waiting');
- if (this.options_.loop) {
- this.currentTime(0);
- this.play();
- } else if (!this.paused()) {
- this.pause();
- }
-
- /**
- * Fired when the end of the media resource is reached (currentTime == duration)
- *
- * @event Player#ended
- * @type {Event}
- */
- this.trigger('ended');
- }
-
- /**
- * Fired when the duration of the media resource is first known or changed
- *
- * @listens Tech#durationchange
- * @private
- */
- handleTechDurationChange_() {
- this.duration(this.techGet_('duration'));
- }
-
- /**
- * Handle a click on the media element to play/pause
- *
- * @param {Event} event
- * the event that caused this function to trigger
- *
- * @listens Tech#click
- * @private
- */
- handleTechClick_(event) {
- // When controls are disabled a click should not toggle playback because
- // the click is considered a control
- if (!this.controls_) {
- return;
- }
- if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
- if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
- this.options_.userActions.click.call(this, event);
- } else if (this.paused()) {
- silencePromise(this.play());
- } else {
- this.pause();
- }
- }
- }
-
- /**
- * Handle a double-click on the media element to enter/exit fullscreen
- *
- * @param {Event} event
- * the event that caused this function to trigger
- *
- * @listens Tech#dblclick
- * @private
- */
- handleTechDoubleClick_(event) {
- if (!this.controls_) {
- return;
- }
-
- // we do not want to toggle fullscreen state
- // when double-clicking inside a control bar or a modal
- const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
- if (!inAllowedEls) {
- /*
- * options.userActions.doubleClick
- *
- * If `undefined` or `true`, double-click toggles fullscreen if controls are present
- * Set to `false` to disable double-click handling
- * Set to a function to substitute an external double-click handler
- */
- if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
- if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
- this.options_.userActions.doubleClick.call(this, event);
- } else if (this.isFullscreen()) {
- this.exitFullscreen();
- } else {
- this.requestFullscreen();
- }
- }
- }
- }
-
- /**
- * Handle a tap on the media element. It will toggle the user
- * activity state, which hides and shows the controls.
- *
- * @listens Tech#tap
- * @private
- */
- handleTechTap_() {
- this.userActive(!this.userActive());
- }
-
- /**
- * Handle touch to start
- *
- * @listens Tech#touchstart
- * @private
- */
- handleTechTouchStart_() {
- this.userWasActive = this.userActive();
- }
-
- /**
- * Handle touch to move
- *
- * @listens Tech#touchmove
- * @private
- */
- handleTechTouchMove_() {
- if (this.userWasActive) {
- this.reportUserActivity();
- }
- }
-
- /**
- * Handle touch to end
- *
- * @param {Event} event
- * the touchend event that triggered
- * this function
- *
- * @listens Tech#touchend
- * @private
- */
- handleTechTouchEnd_(event) {
- // Stop the mouse events from also happening
- if (event.cancelable) {
- event.preventDefault();
- }
- }
-
- /**
- * @private
- */
- toggleFullscreenClass_() {
- if (this.isFullscreen()) {
- this.addClass('vjs-fullscreen');
- } else {
- this.removeClass('vjs-fullscreen');
- }
- }
-
- /**
- * when the document fschange event triggers it calls this
- */
- documentFullscreenChange_(e) {
- const targetPlayer = e.target.player;
-
- // if another player was fullscreen
- // do a null check for targetPlayer because older firefox's would put document as e.target
- if (targetPlayer && targetPlayer !== this) {
- return;
- }
- const el = this.el();
- let isFs = document[this.fsApi_.fullscreenElement] === el;
- if (!isFs && el.matches) {
- isFs = el.matches(':' + this.fsApi_.fullscreen);
- }
- this.isFullscreen(isFs);
- }
-
- /**
- * Handle Tech Fullscreen Change
- *
- * @param {Event} event
- * the fullscreenchange event that triggered this function
- *
- * @param {Object} data
- * the data that was sent with the event
- *
- * @private
- * @listens Tech#fullscreenchange
- * @fires Player#fullscreenchange
- */
- handleTechFullscreenChange_(event, data) {
- if (data) {
- if (data.nativeIOSFullscreen) {
- this.addClass('vjs-ios-native-fs');
- this.tech_.one('webkitendfullscreen', () => {
- this.removeClass('vjs-ios-native-fs');
- });
- }
- this.isFullscreen(data.isFullscreen);
- }
- }
- handleTechFullscreenError_(event, err) {
- this.trigger('fullscreenerror', err);
- }
-
- /**
- * @private
- */
- togglePictureInPictureClass_() {
- if (this.isInPictureInPicture()) {
- this.addClass('vjs-picture-in-picture');
- } else {
- this.removeClass('vjs-picture-in-picture');
- }
- }
-
- /**
- * Handle Tech Enter Picture-in-Picture.
- *
- * @param {Event} event
- * the enterpictureinpicture event that triggered this function
- *
- * @private
- * @listens Tech#enterpictureinpicture
- */
- handleTechEnterPictureInPicture_(event) {
- this.isInPictureInPicture(true);
- }
-
- /**
- * Handle Tech Leave Picture-in-Picture.
- *
- * @param {Event} event
- * the leavepictureinpicture event that triggered this function
- *
- * @private
- * @listens Tech#leavepictureinpicture
- */
- handleTechLeavePictureInPicture_(event) {
- this.isInPictureInPicture(false);
- }
-
- /**
- * Fires when an error occurred during the loading of an audio/video.
- *
- * @private
- * @listens Tech#error
- */
- handleTechError_() {
- const error = this.tech_.error();
- if (error) {
- this.error(error);
- }
- }
-
- /**
- * Retrigger the `textdata` event that was triggered by the {@link Tech}.
- *
- * @fires Player#textdata
- * @listens Tech#textdata
- * @private
- */
- handleTechTextData_() {
- let data = null;
- if (arguments.length > 1) {
- data = arguments[1];
- }
-
- /**
- * Fires when we get a textdata event from tech
- *
- * @event Player#textdata
- * @type {Event}
- */
- this.trigger('textdata', data);
- }
-
- /**
- * Get object for cached values.
- *
- * @return {Object}
- * get the current object cache
- */
- getCache() {
- return this.cache_;
- }
-
- /**
- * Resets the internal cache object.
- *
- * Using this function outside the player constructor or reset method may
- * have unintended side-effects.
- *
- * @private
- */
- resetCache_() {
- this.cache_ = {
- // Right now, the currentTime is not _really_ cached because it is always
- // retrieved from the tech (see: currentTime). However, for completeness,
- // we set it to zero here to ensure that if we do start actually caching
- // it, we reset it along with everything else.
- currentTime: 0,
- initTime: 0,
- inactivityTimeout: this.options_.inactivityTimeout,
- duration: NaN,
- lastVolume: 1,
- lastPlaybackRate: this.defaultPlaybackRate(),
- media: null,
- src: '',
- source: {},
- sources: [],
- playbackRates: [],
- volume: 1
- };
- }
-
- /**
- * Pass values to the playback tech
- *
- * @param {string} [method]
- * the method to call
- *
- * @param {Object} [arg]
- * the argument to pass
- *
- * @private
- */
- techCall_(method, arg) {
- // If it's not ready yet, call method when it is
-
- this.ready(function () {
- if (method in allowedSetters) {
- return set(this.middleware_, this.tech_, method, arg);
- } else if (method in allowedMediators) {
- return mediate(this.middleware_, this.tech_, method, arg);
- }
- try {
- if (this.tech_) {
- this.tech_[method](arg);
- }
- } catch (e) {
- log(e);
- throw e;
- }
- }, true);
- }
-
- /**
- * Mediate attempt to call playback tech method
- * and return the value of the method called.
- *
- * @param {string} method
- * Tech method
- *
- * @return {*}
- * Value returned by the tech method called, undefined if tech
- * is not ready or tech method is not present
- *
- * @private
- */
- techGet_(method) {
- if (!this.tech_ || !this.tech_.isReady_) {
- return;
- }
- if (method in allowedGetters) {
- return get(this.middleware_, this.tech_, method);
- } else if (method in allowedMediators) {
- return mediate(this.middleware_, this.tech_, method);
- }
-
- // Log error when playback tech object is present but method
- // is undefined or unavailable
- try {
- return this.tech_[method]();
- } catch (e) {
- // When building additional tech libs, an expected method may not be defined yet
- if (this.tech_[method] === undefined) {
- log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
- throw e;
- }
-
- // When a method isn't available on the object it throws a TypeError
- if (e.name === 'TypeError') {
- log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
- this.tech_.isReady_ = false;
- throw e;
- }
-
- // If error unknown, just log and throw
- log(e);
- throw e;
- }
- }
-
- /**
- * Attempt to begin playback at the first opportunity.
- *
- * @return {Promise|undefined}
- * Returns a promise if the browser supports Promises (or one
- * was passed in as an option). This promise will be resolved on
- * the return value of play. If this is undefined it will fulfill the
- * promise chain otherwise the promise chain will be fulfilled when
- * the promise from play is fulfilled.
- */
- play() {
- return new Promise(resolve => {
- this.play_(resolve);
- });
- }
-
- /**
- * The actual logic for play, takes a callback that will be resolved on the
- * return value of play. This allows us to resolve to the play promise if there
- * is one on modern browsers.
- *
- * @private
- * @param {Function} [callback]
- * The callback that should be called when the techs play is actually called
- */
- play_(callback = silencePromise) {
- this.playCallbacks_.push(callback);
- const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
- const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
-
- // treat calls to play_ somewhat like the `one` event function
- if (this.waitToPlay_) {
- this.off(['ready', 'loadstart'], this.waitToPlay_);
- this.waitToPlay_ = null;
- }
-
- // if the player/tech is not ready or the src itself is not ready
- // queue up a call to play on `ready` or `loadstart`
- if (!this.isReady_ || !isSrcReady) {
- this.waitToPlay_ = e => {
- this.play_();
- };
- this.one(['ready', 'loadstart'], this.waitToPlay_);
-
- // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
- // in that case, we need to prime the video element by calling load so it'll be ready in time
- if (!isSrcReady && isSafariOrIOS) {
- this.load();
- }
- return;
- }
-
- // If the player/tech is ready and we have a source, we can attempt playback.
- const val = this.techGet_('play');
-
- // For native playback, reset the progress bar if we get a play call from a replay.
- const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
- if (isNativeReplay) {
- this.resetProgressBar_();
- }
- // play was terminated if the returned value is null
- if (val === null) {
- this.runPlayTerminatedQueue_();
- } else {
- this.runPlayCallbacks_(val);
- }
- }
-
- /**
- * These functions will be run when if play is terminated. If play
- * runPlayCallbacks_ is run these function will not be run. This allows us
- * to differentiate between a terminated play and an actual call to play.
- */
- runPlayTerminatedQueue_() {
- const queue = this.playTerminatedQueue_.slice(0);
- this.playTerminatedQueue_ = [];
- queue.forEach(function (q) {
- q();
- });
- }
-
- /**
- * When a callback to play is delayed we have to run these
- * callbacks when play is actually called on the tech. This function
- * runs the callbacks that were delayed and accepts the return value
- * from the tech.
- *
- * @param {undefined|Promise} val
- * The return value from the tech.
- */
- runPlayCallbacks_(val) {
- const callbacks = this.playCallbacks_.slice(0);
- this.playCallbacks_ = [];
- // clear play terminatedQueue since we finished a real play
- this.playTerminatedQueue_ = [];
- callbacks.forEach(function (cb) {
- cb(val);
- });
- }
-
- /**
- * Pause the video playback
- */
- pause() {
- this.techCall_('pause');
- }
-
- /**
- * Check if the player is paused or has yet to play
- *
- * @return {boolean}
- * - false: if the media is currently playing
- * - true: if media is not currently playing
- */
- paused() {
- // The initial state of paused should be true (in Safari it's actually false)
- return this.techGet_('paused') === false ? false : true;
- }
-
- /**
- * Get a TimeRange object representing the current ranges of time that the user
- * has played.
- *
- * @return { import('./utils/time').TimeRange }
- * A time range object that represents all the increments of time that have
- * been played.
- */
- played() {
- return this.techGet_('played') || createTimeRanges(0, 0);
- }
-
- /**
- * Sets or returns whether or not the user is "scrubbing". Scrubbing is
- * when the user has clicked the progress bar handle and is
- * dragging it along the progress bar.
- *
- * @param {boolean} [isScrubbing]
- * whether the user is or is not scrubbing
- *
- * @return {boolean|undefined}
- * - The value of scrubbing when getting
- * - Nothing when setting
- */
- scrubbing(isScrubbing) {
- if (typeof isScrubbing === 'undefined') {
- return this.scrubbing_;
- }
- this.scrubbing_ = !!isScrubbing;
- this.techCall_('setScrubbing', this.scrubbing_);
- if (isScrubbing) {
- this.addClass('vjs-scrubbing');
- } else {
- this.removeClass('vjs-scrubbing');
- }
- }
-
- /**
- * Get or set the current time (in seconds)
- *
- * @param {number|string} [seconds]
- * The time to seek to in seconds
- *
- * @return {number|undefined}
- * - the current time in seconds when getting
- * - Nothing when setting
- */
- currentTime(seconds) {
- if (seconds === undefined) {
- // cache last currentTime and return. default to 0 seconds
- //
- // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
- // currentTime when scrubbing, but may not provide much performance benefit after all.
- // Should be tested. Also something has to read the actual current time or the cache will
- // never get updated.
- this.cache_.currentTime = this.techGet_('currentTime') || 0;
- return this.cache_.currentTime;
- }
- if (seconds < 0) {
- seconds = 0;
- }
- if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
- this.cache_.initTime = seconds;
- this.off('canplay', this.boundApplyInitTime_);
- this.one('canplay', this.boundApplyInitTime_);
- return;
- }
- this.techCall_('setCurrentTime', seconds);
- this.cache_.initTime = 0;
- if (isFinite(seconds)) {
- this.cache_.currentTime = Number(seconds);
- }
- }
-
- /**
- * Apply the value of initTime stored in cache as currentTime.
- *
- * @private
- */
- applyInitTime_() {
- this.currentTime(this.cache_.initTime);
- }
-
- /**
- * Normally gets the length in time of the video in seconds;
- * in all but the rarest use cases an argument will NOT be passed to the method
- *
- * > **NOTE**: The video must have started loading before the duration can be
- * known, and depending on preload behaviour may not be known until the video starts
- * playing.
- *
- * @fires Player#durationchange
- *
- * @param {number} [seconds]
- * The duration of the video to set in seconds
- *
- * @return {number|undefined}
- * - The duration of the video in seconds when getting
- * - Nothing when setting
- */
- duration(seconds) {
- if (seconds === undefined) {
- // return NaN if the duration is not known
- return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
- }
- seconds = parseFloat(seconds);
-
- // Standardize on Infinity for signaling video is live
- if (seconds < 0) {
- seconds = Infinity;
- }
- if (seconds !== this.cache_.duration) {
- // Cache the last set value for optimized scrubbing
- this.cache_.duration = seconds;
- if (seconds === Infinity) {
- this.addClass('vjs-live');
- } else {
- this.removeClass('vjs-live');
- }
- if (!isNaN(seconds)) {
- // Do not fire durationchange unless the duration value is known.
- // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
-
- /**
- * @event Player#durationchange
- * @type {Event}
- */
- this.trigger('durationchange');
- }
- }
- }
-
- /**
- * Calculates how much time is left in the video. Not part
- * of the native video API.
- *
- * @return {number}
- * The time remaining in seconds
- */
- remainingTime() {
- return this.duration() - this.currentTime();
- }
-
- /**
- * A remaining time function that is intended to be used when
- * the time is to be displayed directly to the user.
- *
- * @return {number}
- * The rounded time remaining in seconds
- */
- remainingTimeDisplay() {
- return Math.floor(this.duration()) - Math.floor(this.currentTime());
- }
-
- //
- // Kind of like an array of portions of the video that have been downloaded.
-
- /**
- * Get a TimeRange object with an array of the times of the video
- * that have been downloaded. If you just want the percent of the
- * video that's been downloaded, use bufferedPercent.
- *
- * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
- *
- * @return { import('./utils/time').TimeRange }
- * A mock {@link TimeRanges} object (following HTML spec)
- */
- buffered() {
- let buffered = this.techGet_('buffered');
- if (!buffered || !buffered.length) {
- buffered = createTimeRanges(0, 0);
- }
- return buffered;
- }
-
- /**
- * Get the TimeRanges of the media that are currently available
- * for seeking to.
- *
- * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable}
- *
- * @return { import('./utils/time').TimeRange }
- * A mock {@link TimeRanges} object (following HTML spec)
- */
- seekable() {
- let seekable = this.techGet_('seekable');
- if (!seekable || !seekable.length) {
- seekable = createTimeRanges(0, 0);
- }
- return seekable;
- }
-
- /**
- * Returns whether the player is in the "seeking" state.
- *
- * @return {boolean} True if the player is in the seeking state, false if not.
- */
- seeking() {
- return this.techGet_('seeking');
- }
-
- /**
- * Returns whether the player is in the "ended" state.
- *
- * @return {boolean} True if the player is in the ended state, false if not.
- */
- ended() {
- return this.techGet_('ended');
- }
-
- /**
- * Returns the current state of network activity for the element, from
- * the codes in the list below.
- * - NETWORK_EMPTY (numeric value 0)
- * The element has not yet been initialised. All attributes are in
- * their initial states.
- * - NETWORK_IDLE (numeric value 1)
- * The element's resource selection algorithm is active and has
- * selected a resource, but it is not actually using the network at
- * this time.
- * - NETWORK_LOADING (numeric value 2)
- * The user agent is actively trying to download data.
- * - NETWORK_NO_SOURCE (numeric value 3)
- * The element's resource selection algorithm is active, but it has
- * not yet found a resource to use.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
- * @return {number} the current network activity state
- */
- networkState() {
- return this.techGet_('networkState');
- }
-
- /**
- * Returns a value that expresses the current state of the element
- * with respect to rendering the current playback position, from the
- * codes in the list below.
- * - HAVE_NOTHING (numeric value 0)
- * No information regarding the media resource is available.
- * - HAVE_METADATA (numeric value 1)
- * Enough of the resource has been obtained that the duration of the
- * resource is available.
- * - HAVE_CURRENT_DATA (numeric value 2)
- * Data for the immediate current playback position is available.
- * - HAVE_FUTURE_DATA (numeric value 3)
- * Data for the immediate current playback position is available, as
- * well as enough data for the user agent to advance the current
- * playback position in the direction of playback.
- * - HAVE_ENOUGH_DATA (numeric value 4)
- * The user agent estimates that enough data is available for
- * playback to proceed uninterrupted.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
- * @return {number} the current playback rendering state
- */
- readyState() {
- return this.techGet_('readyState');
- }
-
- /**
- * Get the percent (as a decimal) of the video that's been downloaded.
- * This method is not a part of the native HTML video API.
- *
- * @return {number}
- * A decimal between 0 and 1 representing the percent
- * that is buffered 0 being 0% and 1 being 100%
- */
- bufferedPercent() {
- return bufferedPercent(this.buffered(), this.duration());
- }
-
- /**
- * Get the ending time of the last buffered time range
- * This is used in the progress bar to encapsulate all time ranges.
- *
- * @return {number}
- * The end of the last buffered time range
- */
- bufferedEnd() {
- const buffered = this.buffered();
- const duration = this.duration();
- let end = buffered.end(buffered.length - 1);
- if (end > duration) {
- end = duration;
- }
- return end;
- }
-
- /**
- * Get or set the current volume of the media
- *
- * @param {number} [percentAsDecimal]
- * The new volume as a decimal percent:
- * - 0 is muted/0%/off
- * - 1.0 is 100%/full
- * - 0.5 is half volume or 50%
- *
- * @return {number|undefined}
- * The current volume as a percent when getting
- */
- volume(percentAsDecimal) {
- let vol;
- if (percentAsDecimal !== undefined) {
- // Force value to between 0 and 1
- vol = Math.max(0, Math.min(1, percentAsDecimal));
- this.cache_.volume = vol;
- this.techCall_('setVolume', vol);
- if (vol > 0) {
- this.lastVolume_(vol);
- }
- return;
- }
-
- // Default to 1 when returning current volume.
- vol = parseFloat(this.techGet_('volume'));
- return isNaN(vol) ? 1 : vol;
- }
-
- /**
- * Get the current muted state, or turn mute on or off
- *
- * @param {boolean} [muted]
- * - true to mute
- * - false to unmute
- *
- * @return {boolean|undefined}
- * - true if mute is on and getting
- * - false if mute is off and getting
- * - nothing if setting
- */
- muted(muted) {
- if (muted !== undefined) {
- this.techCall_('setMuted', muted);
- return;
- }
- return this.techGet_('muted') || false;
- }
-
- /**
- * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
- * indicates the state of muted on initial playback.
- *
- * ```js
- * var myPlayer = videojs('some-player-id');
- *
- * myPlayer.src("http://www.example.com/path/to/video.mp4");
- *
- * // get, should be false
- * console.log(myPlayer.defaultMuted());
- * // set to true
- * myPlayer.defaultMuted(true);
- * // get should be true
- * console.log(myPlayer.defaultMuted());
- * ```
- *
- * @param {boolean} [defaultMuted]
- * - true to mute
- * - false to unmute
- *
- * @return {boolean|undefined}
- * - true if defaultMuted is on and getting
- * - false if defaultMuted is off and getting
- * - Nothing when setting
- */
- defaultMuted(defaultMuted) {
- if (defaultMuted !== undefined) {
- this.techCall_('setDefaultMuted', defaultMuted);
- }
- return this.techGet_('defaultMuted') || false;
- }
-
- /**
- * Get the last volume, or set it
- *
- * @param {number} [percentAsDecimal]
- * The new last volume as a decimal percent:
- * - 0 is muted/0%/off
- * - 1.0 is 100%/full
- * - 0.5 is half volume or 50%
- *
- * @return {number|undefined}
- * - The current value of lastVolume as a percent when getting
- * - Nothing when setting
- *
- * @private
- */
- lastVolume_(percentAsDecimal) {
- if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
- this.cache_.lastVolume = percentAsDecimal;
- return;
- }
- return this.cache_.lastVolume;
- }
-
- /**
- * Check if current tech can support native fullscreen
- * (e.g. with built in controls like iOS)
- *
- * @return {boolean}
- * if native fullscreen is supported
- */
- supportsFullScreen() {
- return this.techGet_('supportsFullScreen') || false;
- }
-
- /**
- * Check if the player is in fullscreen mode or tell the player that it
- * is or is not in fullscreen mode.
- *
- * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
- * property and instead document.fullscreenElement is used. But isFullscreen is
- * still a valuable property for internal player workings.
- *
- * @param {boolean} [isFS]
- * Set the players current fullscreen state
- *
- * @return {boolean|undefined}
- * - true if fullscreen is on and getting
- * - false if fullscreen is off and getting
- * - Nothing when setting
- */
- isFullscreen(isFS) {
- if (isFS !== undefined) {
- const oldValue = this.isFullscreen_;
- this.isFullscreen_ = Boolean(isFS);
-
- // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
- // this is the only place where we trigger fullscreenchange events for older browsers
- // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
- if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
- /**
- * @event Player#fullscreenchange
- * @type {Event}
- */
- this.trigger('fullscreenchange');
- }
- this.toggleFullscreenClass_();
- return;
- }
- return this.isFullscreen_;
- }
-
- /**
- * Increase the size of the video to full screen
- * In some browsers, full screen is not supported natively, so it enters
- * "full window mode", where the video fills the browser window.
- * In browsers and devices that support native full screen, sometimes the
- * browser's default controls will be shown, and not the Video.js custom skin.
- * This includes most mobile devices (iOS, Android) and older versions of
- * Safari.
- *
- * @param {Object} [fullscreenOptions]
- * Override the player fullscreen options
- *
- * @fires Player#fullscreenchange
- */
- requestFullscreen(fullscreenOptions) {
- if (this.isInPictureInPicture()) {
- this.exitPictureInPicture();
- }
- const self = this;
- return new Promise((resolve, reject) => {
- function offHandler() {
- self.off('fullscreenerror', errorHandler);
- self.off('fullscreenchange', changeHandler);
- }
- function changeHandler() {
- offHandler();
- resolve();
- }
- function errorHandler(e, err) {
- offHandler();
- reject(err);
- }
- self.one('fullscreenchange', changeHandler);
- self.one('fullscreenerror', errorHandler);
- const promise = self.requestFullscreenHelper_(fullscreenOptions);
- if (promise) {
- promise.then(offHandler, offHandler);
- promise.then(resolve, reject);
- }
- });
- }
- requestFullscreenHelper_(fullscreenOptions) {
- let fsOptions;
-
- // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
- // Use defaults or player configured option unless passed directly to this method.
- if (!this.fsApi_.prefixed) {
- fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
- if (fullscreenOptions !== undefined) {
- fsOptions = fullscreenOptions;
- }
- }
-
- // This method works as follows:
- // 1. if a fullscreen api is available, use it
- // 1. call requestFullscreen with potential options
- // 2. if we got a promise from above, use it to update isFullscreen()
- // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
- // This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
- // 3. otherwise, use "fullWindow" mode
- if (this.fsApi_.requestFullscreen) {
- const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
-
- // Even on browsers with promise support this may not return a promise
- if (promise) {
- promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
- }
- return promise;
- } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
- // we can't take the video.js controls fullscreen but we can go fullscreen
- // with native controls
- this.techCall_('enterFullScreen');
- } else {
- // fullscreen isn't supported so we'll just stretch the video element to
- // fill the viewport
- this.enterFullWindow();
- }
- }
-
- /**
- * Return the video to its normal size after having been in full screen mode
- *
- * @fires Player#fullscreenchange
- */
- exitFullscreen() {
- const self = this;
- return new Promise((resolve, reject) => {
- function offHandler() {
- self.off('fullscreenerror', errorHandler);
- self.off('fullscreenchange', changeHandler);
- }
- function changeHandler() {
- offHandler();
- resolve();
- }
- function errorHandler(e, err) {
- offHandler();
- reject(err);
- }
- self.one('fullscreenchange', changeHandler);
- self.one('fullscreenerror', errorHandler);
- const promise = self.exitFullscreenHelper_();
- if (promise) {
- promise.then(offHandler, offHandler);
- // map the promise to our resolve/reject methods
- promise.then(resolve, reject);
- }
- });
- }
- exitFullscreenHelper_() {
- if (this.fsApi_.requestFullscreen) {
- const promise = document[this.fsApi_.exitFullscreen]();
-
- // Even on browsers with promise support this may not return a promise
- if (promise) {
- // we're splitting the promise here, so, we want to catch the
- // potential error so that this chain doesn't have unhandled errors
- silencePromise(promise.then(() => this.isFullscreen(false)));
- }
- return promise;
- } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
- this.techCall_('exitFullScreen');
- } else {
- this.exitFullWindow();
- }
- }
-
- /**
- * When fullscreen isn't supported we can stretch the
- * video container to as wide as the browser will let us.
- *
- * @fires Player#enterFullWindow
- */
- enterFullWindow() {
- this.isFullscreen(true);
- this.isFullWindow = true;
-
- // Storing original doc overflow value to return to when fullscreen is off
- this.docOrigOverflow = document.documentElement.style.overflow;
-
- // Add listener for esc key to exit fullscreen
- on(document, 'keydown', this.boundFullWindowOnEscKey_);
-
- // Hide any scroll bars
- document.documentElement.style.overflow = 'hidden';
-
- // Apply fullscreen styles
- addClass(document.body, 'vjs-full-window');
-
- /**
- * @event Player#enterFullWindow
- * @type {Event}
- */
- this.trigger('enterFullWindow');
- }
-
- /**
- * Check for call to either exit full window or
- * full screen on ESC key
- *
- * @param {string} event
- * Event to check for key press
- */
- fullWindowOnEscKey(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- if (this.isFullscreen() === true) {
- if (!this.isFullWindow) {
- this.exitFullscreen();
- } else {
- this.exitFullWindow();
- }
- }
- }
- }
-
- /**
- * Exit full window
- *
- * @fires Player#exitFullWindow
- */
- exitFullWindow() {
- this.isFullscreen(false);
- this.isFullWindow = false;
- off(document, 'keydown', this.boundFullWindowOnEscKey_);
-
- // Unhide scroll bars.
- document.documentElement.style.overflow = this.docOrigOverflow;
-
- // Remove fullscreen styles
- removeClass(document.body, 'vjs-full-window');
-
- // Resize the box, controller, and poster to original sizes
- // this.positionAll();
- /**
- * @event Player#exitFullWindow
- * @type {Event}
- */
- this.trigger('exitFullWindow');
- }
-
- /**
- * Get or set disable Picture-in-Picture mode.
- *
- * @param {boolean} [value]
- * - true will disable Picture-in-Picture mode
- * - false will enable Picture-in-Picture mode
- */
- disablePictureInPicture(value) {
- if (value === undefined) {
- return this.techGet_('disablePictureInPicture');
- }
- this.techCall_('setDisablePictureInPicture', value);
- this.options_.disablePictureInPicture = value;
- this.trigger('disablepictureinpicturechanged');
- }
-
- /**
- * Check if the player is in Picture-in-Picture mode or tell the player that it
- * is or is not in Picture-in-Picture mode.
- *
- * @param {boolean} [isPiP]
- * Set the players current Picture-in-Picture state
- *
- * @return {boolean|undefined}
- * - true if Picture-in-Picture is on and getting
- * - false if Picture-in-Picture is off and getting
- * - nothing if setting
- */
- isInPictureInPicture(isPiP) {
- if (isPiP !== undefined) {
- this.isInPictureInPicture_ = !!isPiP;
- this.togglePictureInPictureClass_();
- return;
- }
- return !!this.isInPictureInPicture_;
- }
-
- /**
- * Create a floating video window always on top of other windows so that users may
- * continue consuming media while they interact with other content sites, or
- * applications on their device.
- *
- * This can use document picture-in-picture or element picture in picture
- *
- * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
- * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
- *
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
- * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
- *
- * @fires Player#enterpictureinpicture
- *
- * @return {Promise}
- * A promise with a Picture-in-Picture window.
- */
- requestPictureInPicture() {
- if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) {
- const pipContainer = document.createElement(this.el().tagName);
- pipContainer.classList = this.el().classList;
- pipContainer.classList.add('vjs-pip-container');
- if (this.posterImage) {
- pipContainer.appendChild(this.posterImage.el().cloneNode(true));
- }
- if (this.titleBar) {
- pipContainer.appendChild(this.titleBar.el().cloneNode(true));
- }
- pipContainer.appendChild(createEl('p', {
- className: 'vjs-pip-text'
- }, {}, this.localize('Playing in picture-in-picture')));
- return window.documentPictureInPicture.requestWindow({
- // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
- width: this.videoWidth(),
- height: this.videoHeight()
- }).then(pipWindow => {
- copyStyleSheetsToWindow(pipWindow);
- this.el_.parentNode.insertBefore(pipContainer, this.el_);
- pipWindow.document.body.appendChild(this.el_);
- pipWindow.document.body.classList.add('vjs-pip-window');
- this.player_.isInPictureInPicture(true);
- this.player_.trigger({
- type: 'enterpictureinpicture',
- pipWindow
- });
-
- // Listen for the PiP closing event to move the video back.
- pipWindow.addEventListener('pagehide', event => {
- const pipVideo = event.target.querySelector('.video-js');
- pipContainer.parentNode.replaceChild(pipVideo, pipContainer);
- this.player_.isInPictureInPicture(false);
- this.player_.trigger('leavepictureinpicture');
- });
- return pipWindow;
- });
- }
- if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
- /**
- * This event fires when the player enters picture in picture mode
- *
- * @event Player#enterpictureinpicture
- * @type {Event}
- */
- return this.techGet_('requestPictureInPicture');
- }
- return Promise.reject('No PiP mode is available');
- }
-
- /**
- * Exit Picture-in-Picture mode.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @fires Player#leavepictureinpicture
- *
- * @return {Promise}
- * A promise.
- */
- exitPictureInPicture() {
- if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
- // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler
- window.documentPictureInPicture.window.close();
- return Promise.resolve();
- }
- if ('pictureInPictureEnabled' in document) {
- /**
- * This event fires when the player leaves picture in picture mode
- *
- * @event Player#leavepictureinpicture
- * @type {Event}
- */
- return document.exitPictureInPicture();
- }
- }
-
- /**
- * Called when this Player has focus and a key gets pressed down, or when
- * any Component of this player receives a key press that it doesn't handle.
- * This allows player-wide hotkeys (either as defined below, or optionally
- * by an external function).
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- const {
- userActions
- } = this.options_;
-
- // Bail out if hotkeys are not configured.
- if (!userActions || !userActions.hotkeys) {
- return;
- }
-
- // Function that determines whether or not to exclude an element from
- // hotkeys handling.
- const excludeElement = el => {
- const tagName = el.tagName.toLowerCase();
-
- // The first and easiest test is for `contenteditable` elements.
- if (el.isContentEditable) {
- return true;
- }
-
- // Inputs matching these types will still trigger hotkey handling as
- // they are not text inputs.
- const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
- if (tagName === 'input') {
- return allowedInputTypes.indexOf(el.type) === -1;
- }
-
- // The final test is by tag name. These tags will be excluded entirely.
- const excludedTags = ['textarea'];
- return excludedTags.indexOf(tagName) !== -1;
- };
-
- // Bail out if the user is focused on an interactive form element.
- if (excludeElement(this.el_.ownerDocument.activeElement)) {
- return;
- }
- if (typeof userActions.hotkeys === 'function') {
- userActions.hotkeys.call(this, event);
- } else {
- this.handleHotkeys(event);
- }
- }
-
- /**
- * Called when this Player receives a hotkey keydown event.
- * Supported player-wide hotkeys are:
- *
- * f - toggle fullscreen
- * m - toggle mute
- * k or Space - toggle play/pause
- *
- * @param {Event} event
- * The `keydown` event that caused this function to be called.
- */
- handleHotkeys(event) {
- const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
-
- // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
- const {
- fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
- muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
- playPauseKey = keydownEvent => keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space')
- } = hotkeys;
- if (fullscreenKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const FSToggle = Component.getComponent('FullscreenToggle');
- if (document[this.fsApi_.fullscreenEnabled] !== false) {
- FSToggle.prototype.handleClick.call(this, event);
- }
- } else if (muteKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const MuteToggle = Component.getComponent('MuteToggle');
- MuteToggle.prototype.handleClick.call(this, event);
- } else if (playPauseKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const PlayToggle = Component.getComponent('PlayToggle');
- PlayToggle.prototype.handleClick.call(this, event);
- }
- }
-
- /**
- * Check whether the player can play a given mimetype
- *
- * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
- *
- * @param {string} type
- * The mimetype to check
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- canPlayType(type) {
- let can;
-
- // Loop through each playback technology in the options order
- for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
- const techName = j[i];
- let tech = Tech.getTech(techName);
-
- // Support old behavior of techs being registered as components.
- // Remove once that deprecated behavior is removed.
- if (!tech) {
- tech = Component.getComponent(techName);
- }
-
- // Check if the current tech is defined before continuing
- if (!tech) {
- log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
- continue;
- }
-
- // Check if the browser supports this technology
- if (tech.isSupported()) {
- can = tech.canPlayType(type);
- if (can) {
- return can;
- }
- }
- }
- return '';
- }
-
- /**
- * Select source based on tech-order or source-order
- * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
- * defaults to tech-order selection
- *
- * @param {Array} sources
- * The sources for a media asset
- *
- * @return {Object|boolean}
- * Object of source and tech order or false
- */
- selectSource(sources) {
- // Get only the techs specified in `techOrder` that exist and are supported by the
- // current platform
- const techs = this.options_.techOrder.map(techName => {
- return [techName, Tech.getTech(techName)];
- }).filter(([techName, tech]) => {
- // Check if the current tech is defined before continuing
- if (tech) {
- // Check if the browser supports this technology
- return tech.isSupported();
- }
- log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
- return false;
- });
-
- // Iterate over each `innerArray` element once per `outerArray` element and execute
- // `tester` with both. If `tester` returns a non-falsy value, exit early and return
- // that value.
- const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
- let found;
- outerArray.some(outerChoice => {
- return innerArray.some(innerChoice => {
- found = tester(outerChoice, innerChoice);
- if (found) {
- return true;
- }
- });
- });
- return found;
- };
- let foundSourceAndTech;
- const flip = fn => (a, b) => fn(b, a);
- const finder = ([techName, tech], source) => {
- if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
- return {
- source,
- tech: techName
- };
- }
- };
-
- // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
- // to select from them based on their priority.
- if (this.options_.sourceOrder) {
- // Source-first ordering
- foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
- } else {
- // Tech-first ordering
- foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
- }
- return foundSourceAndTech || false;
- }
-
- /**
- * Executes source setting and getting logic
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
- * A SourceObject, an array of SourceObjects, or a string referencing
- * a URL to a media source. It is _highly recommended_ that an object
- * or array of objects is used here, so that source selection
- * algorithms can take the `type` into account.
- *
- * If not provided, this method acts as a getter.
- * @param {boolean} [isRetry]
- * Indicates whether this is being called internally as a result of a retry
- *
- * @return {string|undefined}
- * If the `source` argument is missing, returns the current source
- * URL. Otherwise, returns nothing/undefined.
- */
- handleSrc_(source, isRetry) {
- // getter usage
- if (typeof source === 'undefined') {
- return this.cache_.src || '';
- }
-
- // Reset retry behavior for new source
- if (this.resetRetryOnError_) {
- this.resetRetryOnError_();
- }
-
- // filter out invalid sources and turn our source into
- // an array of source objects
- const sources = filterSource(source);
-
- // if a source was passed in then it is invalid because
- // it was filtered to a zero length Array. So we have to
- // show an error
- if (!sources.length) {
- this.setTimeout(function () {
- this.error({
- code: 4,
- message: this.options_.notSupportedMessage
- });
- }, 0);
- return;
- }
-
- // initial sources
- this.changingSrc_ = true;
-
- // Only update the cached source list if we are not retrying a new source after error,
- // since in that case we want to include the failed source(s) in the cache
- if (!isRetry) {
- this.cache_.sources = sources;
- }
- this.updateSourceCaches_(sources[0]);
-
- // middlewareSource is the source after it has been changed by middleware
- setSource(this, sources[0], (middlewareSource, mws) => {
- this.middleware_ = mws;
-
- // since sourceSet is async we have to update the cache again after we select a source since
- // the source that is selected could be out of order from the cache update above this callback.
- if (!isRetry) {
- this.cache_.sources = sources;
- }
- this.updateSourceCaches_(middlewareSource);
- const err = this.src_(middlewareSource);
- if (err) {
- if (sources.length > 1) {
- return this.handleSrc_(sources.slice(1));
- }
- this.changingSrc_ = false;
-
- // We need to wrap this in a timeout to give folks a chance to add error event handlers
- this.setTimeout(function () {
- this.error({
- code: 4,
- message: this.options_.notSupportedMessage
- });
- }, 0);
-
- // we could not find an appropriate tech, but let's still notify the delegate that this is it
- // this needs a better comment about why this is needed
- this.triggerReady();
- return;
- }
- setTech(mws, this.tech_);
- });
-
- // Try another available source if this one fails before playback.
- if (sources.length > 1) {
- const retry = () => {
- // Remove the error modal
- this.error(null);
- this.handleSrc_(sources.slice(1), true);
- };
- const stopListeningForErrors = () => {
- this.off('error', retry);
- };
- this.one('error', retry);
- this.one('playing', stopListeningForErrors);
- this.resetRetryOnError_ = () => {
- this.off('error', retry);
- this.off('playing', stopListeningForErrors);
- };
- }
- }
-
- /**
- * Get or set the video source.
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
- * A SourceObject, an array of SourceObjects, or a string referencing
- * a URL to a media source. It is _highly recommended_ that an object
- * or array of objects is used here, so that source selection
- * algorithms can take the `type` into account.
- *
- * If not provided, this method acts as a getter.
- *
- * @return {string|undefined}
- * If the `source` argument is missing, returns the current source
- * URL. Otherwise, returns nothing/undefined.
- */
- src(source) {
- return this.handleSrc_(source, false);
- }
-
- /**
- * Set the source object on the tech, returns a boolean that indicates whether
- * there is a tech that can play the source or not
- *
- * @param {Tech~SourceObject} source
- * The source object to set on the Tech
- *
- * @return {boolean}
- * - True if there is no Tech to playback this source
- * - False otherwise
- *
- * @private
- */
- src_(source) {
- const sourceTech = this.selectSource([source]);
- if (!sourceTech) {
- return true;
- }
- if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
- this.changingSrc_ = true;
- // load this technology with the chosen source
- this.loadTech_(sourceTech.tech, sourceTech.source);
- this.tech_.ready(() => {
- this.changingSrc_ = false;
- });
- return false;
- }
-
- // wait until the tech is ready to set the source
- // and set it synchronously if possible (#2326)
- this.ready(function () {
- // The setSource tech method was added with source handlers
- // so older techs won't support it
- // We need to check the direct prototype for the case where subclasses
- // of the tech do not support source handlers
- if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
- this.techCall_('setSource', source);
- } else {
- this.techCall_('src', source.src);
- }
- this.changingSrc_ = false;
- }, true);
- return false;
- }
-
- /**
- * Begin loading the src data.
- */
- load() {
- // Workaround to use the load method with the VHS.
- // Does not cover the case when the load method is called directly from the mediaElement.
- if (this.tech_ && this.tech_.vhs) {
- this.src(this.currentSource());
- return;
- }
- this.techCall_('load');
- }
-
- /**
- * Reset the player. Loads the first tech in the techOrder,
- * removes all the text tracks in the existing `tech`,
- * and calls `reset` on the `tech`.
- */
- reset() {
- if (this.paused()) {
- this.doReset_();
- } else {
- const playPromise = this.play();
- silencePromise(playPromise.then(() => this.doReset_()));
- }
- }
- doReset_() {
- if (this.tech_) {
- this.tech_.clearTracks('text');
- }
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- this.resetCache_();
- this.poster('');
- this.loadTech_(this.options_.techOrder[0], null);
- this.techCall_('reset');
- this.resetControlBarUI_();
- this.error(null);
- if (this.titleBar) {
- this.titleBar.update({
- title: undefined,
- description: undefined
- });
- }
- if (isEvented(this)) {
- this.trigger('playerreset');
- }
- }
-
- /**
- * Reset Control Bar's UI by calling sub-methods that reset
- * all of Control Bar's components
- */
- resetControlBarUI_() {
- this.resetProgressBar_();
- this.resetPlaybackRate_();
- this.resetVolumeBar_();
- }
-
- /**
- * Reset tech's progress so progress bar is reset in the UI
- */
- resetProgressBar_() {
- this.currentTime(0);
- const {
- currentTimeDisplay,
- durationDisplay,
- progressControl,
- remainingTimeDisplay
- } = this.controlBar || {};
- const {
- seekBar
- } = progressControl || {};
- if (currentTimeDisplay) {
- currentTimeDisplay.updateContent();
- }
- if (durationDisplay) {
- durationDisplay.updateContent();
- }
- if (remainingTimeDisplay) {
- remainingTimeDisplay.updateContent();
- }
- if (seekBar) {
- seekBar.update();
- if (seekBar.loadProgressBar) {
- seekBar.loadProgressBar.update();
- }
- }
- }
-
- /**
- * Reset Playback ratio
- */
- resetPlaybackRate_() {
- this.playbackRate(this.defaultPlaybackRate());
- this.handleTechRateChange_();
- }
-
- /**
- * Reset Volume bar
- */
- resetVolumeBar_() {
- this.volume(1.0);
- this.trigger('volumechange');
- }
-
- /**
- * Returns all of the current source objects.
- *
- * @return {Tech~SourceObject[]}
- * The current source objects
- */
- currentSources() {
- const source = this.currentSource();
- const sources = [];
-
- // assume `{}` or `{ src }`
- if (Object.keys(source).length !== 0) {
- sources.push(source);
- }
- return this.cache_.sources || sources;
- }
-
- /**
- * Returns the current source object.
- *
- * @return {Tech~SourceObject}
- * The current source object
- */
- currentSource() {
- return this.cache_.source || {};
- }
-
- /**
- * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
- * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
- *
- * @return {string}
- * The current source
- */
- currentSrc() {
- return this.currentSource() && this.currentSource().src || '';
- }
-
- /**
- * Get the current source type e.g. video/mp4
- * This can allow you rebuild the current source object so that you could load the same
- * source and tech later
- *
- * @return {string}
- * The source MIME type
- */
- currentType() {
- return this.currentSource() && this.currentSource().type || '';
- }
-
- /**
- * Get or set the preload attribute
- *
- * @param {'none'|'auto'|'metadata'} [value]
- * Preload mode to pass to tech
- *
- * @return {string|undefined}
- * - The preload attribute value when getting
- * - Nothing when setting
- */
- preload(value) {
- if (value !== undefined) {
- this.techCall_('setPreload', value);
- this.options_.preload = value;
- return;
- }
- return this.techGet_('preload');
- }
-
- /**
- * Get or set the autoplay option. When this is a boolean it will
- * modify the attribute on the tech. When this is a string the attribute on
- * the tech will be removed and `Player` will handle autoplay on loadstarts.
- *
- * @param {boolean|'play'|'muted'|'any'} [value]
- * - true: autoplay using the browser behavior
- * - false: do not autoplay
- * - 'play': call play() on every loadstart
- * - 'muted': call muted() then play() on every loadstart
- * - 'any': call play() on every loadstart. if that fails call muted() then play().
- * - *: values other than those listed here will be set `autoplay` to true
- *
- * @return {boolean|string|undefined}
- * - The current value of autoplay when getting
- * - Nothing when setting
- */
- autoplay(value) {
- // getter usage
- if (value === undefined) {
- return this.options_.autoplay || false;
- }
- let techAutoplay;
-
- // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
- if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
- this.options_.autoplay = value;
- this.manualAutoplay_(typeof value === 'string' ? value : 'play');
- techAutoplay = false;
-
- // any falsy value sets autoplay to false in the browser,
- // lets do the same
- } else if (!value) {
- this.options_.autoplay = false;
-
- // any other value (ie truthy) sets autoplay to true
- } else {
- this.options_.autoplay = true;
- }
- techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
-
- // if we don't have a tech then we do not queue up
- // a setAutoplay call on tech ready. We do this because the
- // autoplay option will be passed in the constructor and we
- // do not need to set it twice
- if (this.tech_) {
- this.techCall_('setAutoplay', techAutoplay);
- }
- }
-
- /**
- * Set or unset the playsinline attribute.
- * Playsinline tells the browser that non-fullscreen playback is preferred.
- *
- * @param {boolean} [value]
- * - true means that we should try to play inline by default
- * - false means that we should use the browser's default playback mode,
- * which in most cases is inline. iOS Safari is a notable exception
- * and plays fullscreen by default.
- *
- * @return {string|undefined}
- * - the current value of playsinline
- * - Nothing when setting
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- playsinline(value) {
- if (value !== undefined) {
- this.techCall_('setPlaysinline', value);
- this.options_.playsinline = value;
- }
- return this.techGet_('playsinline');
- }
-
- /**
- * Get or set the loop attribute on the video element.
- *
- * @param {boolean} [value]
- * - true means that we should loop the video
- * - false means that we should not loop the video
- *
- * @return {boolean|undefined}
- * - The current value of loop when getting
- * - Nothing when setting
- */
- loop(value) {
- if (value !== undefined) {
- this.techCall_('setLoop', value);
- this.options_.loop = value;
- return;
- }
- return this.techGet_('loop');
- }
-
- /**
- * Get or set the poster image source url
- *
- * @fires Player#posterchange
- *
- * @param {string} [src]
- * Poster image source URL
- *
- * @return {string|undefined}
- * - The current value of poster when getting
- * - Nothing when setting
- */
- poster(src) {
- if (src === undefined) {
- return this.poster_;
- }
-
- // The correct way to remove a poster is to set as an empty string
- // other falsey values will throw errors
- if (!src) {
- src = '';
- }
- if (src === this.poster_) {
- return;
- }
-
- // update the internal poster variable
- this.poster_ = src;
-
- // update the tech's poster
- this.techCall_('setPoster', src);
- this.isPosterFromTech_ = false;
-
- // alert components that the poster has been set
- /**
- * This event fires when the poster image is changed on the player.
- *
- * @event Player#posterchange
- * @type {Event}
- */
- this.trigger('posterchange');
- }
-
- /**
- * Some techs (e.g. YouTube) can provide a poster source in an
- * asynchronous way. We want the poster component to use this
- * poster source so that it covers up the tech's controls.
- * (YouTube's play button). However we only want to use this
- * source if the player user hasn't set a poster through
- * the normal APIs.
- *
- * @fires Player#posterchange
- * @listens Tech#posterchange
- * @private
- */
- handleTechPosterChange_() {
- if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
- const newPoster = this.tech_.poster() || '';
- if (newPoster !== this.poster_) {
- this.poster_ = newPoster;
- this.isPosterFromTech_ = true;
-
- // Let components know the poster has changed
- this.trigger('posterchange');
- }
- }
- }
-
- /**
- * Get or set whether or not the controls are showing.
- *
- * @fires Player#controlsenabled
- *
- * @param {boolean} [bool]
- * - true to turn controls on
- * - false to turn controls off
- *
- * @return {boolean|undefined}
- * - The current value of controls when getting
- * - Nothing when setting
- */
- controls(bool) {
- if (bool === undefined) {
- return !!this.controls_;
- }
- bool = !!bool;
-
- // Don't trigger a change event unless it actually changed
- if (this.controls_ === bool) {
- return;
- }
- this.controls_ = bool;
- if (this.usingNativeControls()) {
- this.techCall_('setControls', bool);
- }
- if (this.controls_) {
- this.removeClass('vjs-controls-disabled');
- this.addClass('vjs-controls-enabled');
- /**
- * @event Player#controlsenabled
- * @type {Event}
- */
- this.trigger('controlsenabled');
- if (!this.usingNativeControls()) {
- this.addTechControlsListeners_();
- }
- } else {
- this.removeClass('vjs-controls-enabled');
- this.addClass('vjs-controls-disabled');
- /**
- * @event Player#controlsdisabled
- * @type {Event}
- */
- this.trigger('controlsdisabled');
- if (!this.usingNativeControls()) {
- this.removeTechControlsListeners_();
- }
- }
- }
-
- /**
- * Toggle native controls on/off. Native controls are the controls built into
- * devices (e.g. default iPhone controls) or other techs
- * (e.g. Vimeo Controls)
- * **This should only be set by the current tech, because only the tech knows
- * if it can support native controls**
- *
- * @fires Player#usingnativecontrols
- * @fires Player#usingcustomcontrols
- *
- * @param {boolean} [bool]
- * - true to turn native controls on
- * - false to turn native controls off
- *
- * @return {boolean|undefined}
- * - The current value of native controls when getting
- * - Nothing when setting
- */
- usingNativeControls(bool) {
- if (bool === undefined) {
- return !!this.usingNativeControls_;
- }
- bool = !!bool;
-
- // Don't trigger a change event unless it actually changed
- if (this.usingNativeControls_ === bool) {
- return;
- }
- this.usingNativeControls_ = bool;
- if (this.usingNativeControls_) {
- this.addClass('vjs-using-native-controls');
-
- /**
- * player is using the native device controls
- *
- * @event Player#usingnativecontrols
- * @type {Event}
- */
- this.trigger('usingnativecontrols');
- } else {
- this.removeClass('vjs-using-native-controls');
-
- /**
- * player is using the custom HTML controls
- *
- * @event Player#usingcustomcontrols
- * @type {Event}
- */
- this.trigger('usingcustomcontrols');
- }
- }
-
- /**
- * Set or get the current MediaError
- *
- * @fires Player#error
- *
- * @param {MediaError|string|number} [err]
- * A MediaError or a string/number to be turned
- * into a MediaError
- *
- * @return {MediaError|null|undefined}
- * - The current MediaError when getting (or null)
- * - Nothing when setting
- */
- error(err) {
- if (err === undefined) {
- return this.error_ || null;
- }
-
- // allow hooks to modify error object
- hooks('beforeerror').forEach(hookFunction => {
- const newErr = hookFunction(this, err);
- if (!(isObject(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
- this.log.error('please return a value that MediaError expects in beforeerror hooks');
- return;
- }
- err = newErr;
- });
-
- // Suppress the first error message for no compatible source until
- // user interaction
- if (this.options_.suppressNotSupportedError && err && err.code === 4) {
- const triggerSuppressedError = function () {
- this.error(err);
- };
- this.options_.suppressNotSupportedError = false;
- this.any(['click', 'touchstart'], triggerSuppressedError);
- this.one('loadstart', function () {
- this.off(['click', 'touchstart'], triggerSuppressedError);
- });
- return;
- }
-
- // restoring to default
- if (err === null) {
- this.error_ = null;
- this.removeClass('vjs-error');
- if (this.errorDisplay) {
- this.errorDisplay.close();
- }
- return;
- }
- this.error_ = new MediaError(err);
-
- // add the vjs-error classname to the player
- this.addClass('vjs-error');
-
- // log the name of the error type and any message
- // IE11 logs "[object object]" and required you to expand message to see error object
- log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
-
- /**
- * @event Player#error
- * @type {Event}
- */
- this.trigger('error');
-
- // notify hooks of the per player error
- hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
- return;
- }
-
- /**
- * Report user activity
- *
- * @param {Object} event
- * Event object
- */
- reportUserActivity(event) {
- this.userActivity_ = true;
- }
-
- /**
- * Get/set if user is active
- *
- * @fires Player#useractive
- * @fires Player#userinactive
- *
- * @param {boolean} [bool]
- * - true if the user is active
- * - false if the user is inactive
- *
- * @return {boolean|undefined}
- * - The current value of userActive when getting
- * - Nothing when setting
- */
- userActive(bool) {
- if (bool === undefined) {
- return this.userActive_;
- }
- bool = !!bool;
- if (bool === this.userActive_) {
- return;
- }
- this.userActive_ = bool;
- if (this.userActive_) {
- this.userActivity_ = true;
- this.removeClass('vjs-user-inactive');
- this.addClass('vjs-user-active');
- /**
- * @event Player#useractive
- * @type {Event}
- */
- this.trigger('useractive');
- return;
- }
-
- // Chrome/Safari/IE have bugs where when you change the cursor it can
- // trigger a mousemove event. This causes an issue when you're hiding
- // the cursor when the user is inactive, and a mousemove signals user
- // activity. Making it impossible to go into inactive mode. Specifically
- // this happens in fullscreen when we really need to hide the cursor.
- //
- // When this gets resolved in ALL browsers it can be removed
- // https://code.google.com/p/chromium/issues/detail?id=103041
- if (this.tech_) {
- this.tech_.one('mousemove', function (e) {
- e.stopPropagation();
- e.preventDefault();
- });
- }
- this.userActivity_ = false;
- this.removeClass('vjs-user-active');
- this.addClass('vjs-user-inactive');
- /**
- * @event Player#userinactive
- * @type {Event}
- */
- this.trigger('userinactive');
- }
-
- /**
- * Listen for user activity based on timeout value
- *
- * @private
- */
- listenForUserActivity_() {
- let mouseInProgress;
- let lastMoveX;
- let lastMoveY;
- const handleActivity = bind_(this, this.reportUserActivity);
- const handleMouseMove = function (e) {
- // #1068 - Prevent mousemove spamming
- // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
- if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
- lastMoveX = e.screenX;
- lastMoveY = e.screenY;
- handleActivity();
- }
- };
- const handleMouseDown = function () {
- handleActivity();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(mouseInProgress);
- // Setting userActivity=true now and setting the interval to the same time
- // as the activityCheck interval (250) should ensure we never miss the
- // next activityCheck
- mouseInProgress = this.setInterval(handleActivity, 250);
- };
- const handleMouseUpAndMouseLeave = function (event) {
- handleActivity();
- // Stop the interval that maintains activity if the mouse/touch is down
- this.clearInterval(mouseInProgress);
- };
-
- // Any mouse movement will be considered user activity
- this.on('mousedown', handleMouseDown);
- this.on('mousemove', handleMouseMove);
- this.on('mouseup', handleMouseUpAndMouseLeave);
- this.on('mouseleave', handleMouseUpAndMouseLeave);
- const controlBar = this.getChild('controlBar');
-
- // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
- // controlBar would no longer be hidden by default timeout.
- if (controlBar && !IS_IOS && !IS_ANDROID) {
- controlBar.on('mouseenter', function (event) {
- if (this.player().options_.inactivityTimeout !== 0) {
- this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
- }
- this.player().options_.inactivityTimeout = 0;
- });
- controlBar.on('mouseleave', function (event) {
- this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
- });
- }
-
- // Listen for keyboard navigation
- // Shouldn't need to use inProgress interval because of key repeat
- this.on('keydown', handleActivity);
- this.on('keyup', handleActivity);
-
- // Run an interval every 250 milliseconds instead of stuffing everything into
- // the mousemove/touchmove function itself, to prevent performance degradation.
- // `this.reportUserActivity` simply sets this.userActivity_ to true, which
- // then gets picked up by this loop
- // http://ejohn.org/blog/learning-from-twitter/
- let inactivityTimeout;
-
- /** @this Player */
- const activityCheck = function () {
- // Check to see if mouse/touch activity has happened
- if (!this.userActivity_) {
- return;
- }
-
- // Reset the activity tracker
- this.userActivity_ = false;
-
- // If the user state was inactive, set the state to active
- this.userActive(true);
-
- // Clear any existing inactivity timeout to start the timer over
- this.clearTimeout(inactivityTimeout);
- const timeout = this.options_.inactivityTimeout;
- if (timeout <= 0) {
- return;
- }
-
- // In milliseconds, if no more activity has occurred the
- // user will be considered inactive
- inactivityTimeout = this.setTimeout(function () {
- // Protect against the case where the inactivityTimeout can trigger just
- // before the next user activity is picked up by the activity check loop
- // causing a flicker
- if (!this.userActivity_) {
- this.userActive(false);
- }
- }, timeout);
- };
- this.setInterval(activityCheck, 250);
- }
-
- /**
- * Gets or sets the current playback rate. A playback rate of
- * 1.0 represents normal speed and 0.5 would indicate half-speed
- * playback, for instance.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
- *
- * @param {number} [rate]
- * New playback rate to set.
- *
- * @return {number|undefined}
- * - The current playback rate when getting or 1.0
- * - Nothing when setting
- */
- playbackRate(rate) {
- if (rate !== undefined) {
- // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
- // that is registered above
- this.techCall_('setPlaybackRate', rate);
- return;
- }
- if (this.tech_ && this.tech_.featuresPlaybackRate) {
- return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
- }
- return 1.0;
- }
-
- /**
- * Gets or sets the current default playback rate. A default playback rate of
- * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
- * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
- * not the current playbackRate.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
- *
- * @param {number} [rate]
- * New default playback rate to set.
- *
- * @return {number|undefined}
- * - The default playback rate when getting or 1.0
- * - Nothing when setting
- */
- defaultPlaybackRate(rate) {
- if (rate !== undefined) {
- return this.techCall_('setDefaultPlaybackRate', rate);
- }
- if (this.tech_ && this.tech_.featuresPlaybackRate) {
- return this.techGet_('defaultPlaybackRate');
- }
- return 1.0;
- }
-
- /**
- * Gets or sets the audio flag
- *
- * @param {boolean} [bool]
- * - true signals that this is an audio player
- * - false signals that this is not an audio player
- *
- * @return {boolean|undefined}
- * - The current value of isAudio when getting
- * - Nothing when setting
- */
- isAudio(bool) {
- if (bool !== undefined) {
- this.isAudio_ = !!bool;
- return;
- }
- return !!this.isAudio_;
- }
- enableAudioOnlyUI_() {
- // Update styling immediately to show the control bar so we can get its height
- this.addClass('vjs-audio-only-mode');
- const playerChildren = this.children();
- const controlBar = this.getChild('ControlBar');
- const controlBarHeight = controlBar && controlBar.currentHeight();
-
- // Hide all player components except the control bar. Control bar components
- // needed only for video are hidden with CSS
- playerChildren.forEach(child => {
- if (child === controlBar) {
- return;
- }
- if (child.el_ && !child.hasClass('vjs-hidden')) {
- child.hide();
- this.audioOnlyCache_.hiddenChildren.push(child);
- }
- });
- this.audioOnlyCache_.playerHeight = this.currentHeight();
-
- // Set the player height the same as the control bar
- this.height(controlBarHeight);
- this.trigger('audioonlymodechange');
- }
- disableAudioOnlyUI_() {
- this.removeClass('vjs-audio-only-mode');
-
- // Show player components that were previously hidden
- this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
-
- // Reset player height
- this.height(this.audioOnlyCache_.playerHeight);
- this.trigger('audioonlymodechange');
- }
-
- /**
- * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
- *
- * Setting this to `true` will hide all player components except the control bar,
- * as well as control bar components needed only for video.
- *
- * @param {boolean} [value]
- * The value to set audioOnlyMode to.
- *
- * @return {Promise|boolean}
- * A Promise is returned when setting the state, and a boolean when getting
- * the present state
- */
- audioOnlyMode(value) {
- if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
- return this.audioOnlyMode_;
- }
- this.audioOnlyMode_ = value;
-
- // Enable Audio Only Mode
- if (value) {
- const exitPromises = [];
-
- // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
- if (this.isInPictureInPicture()) {
- exitPromises.push(this.exitPictureInPicture());
- }
- if (this.isFullscreen()) {
- exitPromises.push(this.exitFullscreen());
- }
- if (this.audioPosterMode()) {
- exitPromises.push(this.audioPosterMode(false));
- }
- return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
- }
-
- // Disable Audio Only Mode
- return Promise.resolve().then(() => this.disableAudioOnlyUI_());
- }
- enablePosterModeUI_() {
- // Hide the video element and show the poster image to enable posterModeUI
- const tech = this.tech_ && this.tech_;
- tech.hide();
- this.addClass('vjs-audio-poster-mode');
- this.trigger('audiopostermodechange');
- }
- disablePosterModeUI_() {
- // Show the video element and hide the poster image to disable posterModeUI
- const tech = this.tech_ && this.tech_;
- tech.show();
- this.removeClass('vjs-audio-poster-mode');
- this.trigger('audiopostermodechange');
- }
-
- /**
- * Get the current audioPosterMode state or set audioPosterMode to true or false
- *
- * @param {boolean} [value]
- * The value to set audioPosterMode to.
- *
- * @return {Promise|boolean}
- * A Promise is returned when setting the state, and a boolean when getting
- * the present state
- */
- audioPosterMode(value) {
- if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
- return this.audioPosterMode_;
- }
- this.audioPosterMode_ = value;
- if (value) {
- if (this.audioOnlyMode()) {
- const audioOnlyModePromise = this.audioOnlyMode(false);
- return audioOnlyModePromise.then(() => {
- // enable audio poster mode after audio only mode is disabled
- this.enablePosterModeUI_();
- });
- }
- return Promise.resolve().then(() => {
- // enable audio poster mode
- this.enablePosterModeUI_();
- });
- }
- return Promise.resolve().then(() => {
- // disable audio poster mode
- this.disablePosterModeUI_();
- });
- }
-
- /**
- * A helper method for adding a {@link TextTrack} to our
- * {@link TextTrackList}.
- *
- * In addition to the W3C settings we allow adding additional info through options.
- *
- * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
- *
- * @param {string} [kind]
- * the kind of TextTrack you are adding
- *
- * @param {string} [label]
- * the label to give the TextTrack label
- *
- * @param {string} [language]
- * the language to set on the TextTrack
- *
- * @return {TextTrack|undefined}
- * the TextTrack that was added or undefined
- * if there is no tech
- */
- addTextTrack(kind, label, language) {
- if (this.tech_) {
- return this.tech_.addTextTrack(kind, label, language);
- }
- }
-
- /**
- * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
- *
- * @param {Object} options
- * Options to pass to {@link HTMLTrackElement} during creation. See
- * {@link HTMLTrackElement} for object properties that you should use.
- *
- * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
- * from the TextTrackList and HtmlTrackElementList
- * after a source change
- *
- * @return { import('./tracks/html-track-element').default }
- * the HTMLTrackElement that was created and added
- * to the HtmlTrackElementList and the remote
- * TextTrackList
- *
- */
- addRemoteTextTrack(options, manualCleanup) {
- if (this.tech_) {
- return this.tech_.addRemoteTextTrack(options, manualCleanup);
- }
- }
-
- /**
- * Remove a remote {@link TextTrack} from the respective
- * {@link TextTrackList} and {@link HtmlTrackElementList}.
- *
- * @param {Object} track
- * Remote {@link TextTrack} to remove
- *
- * @return {undefined}
- * does not return anything
- */
- removeRemoteTextTrack(obj = {}) {
- let {
- track
- } = obj;
- if (!track) {
- track = obj;
- }
-
- // destructure the input into an object with a track argument, defaulting to arguments[0]
- // default the whole argument to an empty object if nothing was passed in
-
- if (this.tech_) {
- return this.tech_.removeRemoteTextTrack(track);
- }
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object|undefined}
- * An object with supported media playback quality metrics or undefined if there
- * is no tech or the tech does not support it.
- */
- getVideoPlaybackQuality() {
- return this.techGet_('getVideoPlaybackQuality');
- }
-
- /**
- * Get video width
- *
- * @return {number}
- * current video width
- */
- videoWidth() {
- return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
- }
-
- /**
- * Get video height
- *
- * @return {number}
- * current video height
- */
- videoHeight() {
- return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
- }
-
- /**
- * Set or get the player's language code.
- *
- * Changing the language will trigger
- * [languagechange]{@link Player#event:languagechange}
- * which Components can use to update control text.
- * ClickableComponent will update its control text by default on
- * [languagechange]{@link Player#event:languagechange}.
- *
- * @fires Player#languagechange
- *
- * @param {string} [code]
- * the language code to set the player to
- *
- * @return {string|undefined}
- * - The current language code when getting
- * - Nothing when setting
- */
- language(code) {
- if (code === undefined) {
- return this.language_;
- }
- if (this.language_ !== String(code).toLowerCase()) {
- this.language_ = String(code).toLowerCase();
-
- // during first init, it's possible some things won't be evented
- if (isEvented(this)) {
- /**
- * fires when the player language change
- *
- * @event Player#languagechange
- * @type {Event}
- */
- this.trigger('languagechange');
- }
- }
- }
-
- /**
- * Get the player's language dictionary
- * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
- * Languages specified directly in the player options have precedence
- *
- * @return {Array}
- * An array of of supported languages
- */
- languages() {
- return merge(Player.prototype.options_.languages, this.languages_);
- }
-
- /**
- * returns a JavaScript object representing the current track
- * information. **DOES not return it as JSON**
- *
- * @return {Object}
- * Object representing the current of track info
- */
- toJSON() {
- const options = merge(this.options_);
- const tracks = options.tracks;
- options.tracks = [];
- for (let i = 0; i < tracks.length; i++) {
- let track = tracks[i];
-
- // deep merge tracks and null out player so no circular references
- track = merge(track);
- track.player = undefined;
- options.tracks[i] = track;
- }
- return options;
- }
-
- /**
- * Creates a simple modal dialog (an instance of the {@link ModalDialog}
- * component) that immediately overlays the player with arbitrary
- * content and removes itself when closed.
- *
- * @param {string|Function|Element|Array|null} content
- * Same as {@link ModalDialog#content}'s param of the same name.
- * The most straight-forward usage is to provide a string or DOM
- * element.
- *
- * @param {Object} [options]
- * Extra options which will be passed on to the {@link ModalDialog}.
- *
- * @return {ModalDialog}
- * the {@link ModalDialog} that was created
- */
- createModal(content, options) {
- options = options || {};
- options.content = content || '';
- const modal = new ModalDialog(this, options);
- this.addChild(modal);
- modal.on('dispose', () => {
- this.removeChild(modal);
- });
- modal.open();
- return modal;
- }
-
- /**
- * Change breakpoint classes when the player resizes.
- *
- * @private
- */
- updateCurrentBreakpoint_() {
- if (!this.responsive()) {
- return;
- }
- const currentBreakpoint = this.currentBreakpoint();
- const currentWidth = this.currentWidth();
- for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
- const candidateBreakpoint = BREAKPOINT_ORDER[i];
- const maxWidth = this.breakpoints_[candidateBreakpoint];
- if (currentWidth <= maxWidth) {
- // The current breakpoint did not change, nothing to do.
- if (currentBreakpoint === candidateBreakpoint) {
- return;
- }
-
- // Only remove a class if there is a current breakpoint.
- if (currentBreakpoint) {
- this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
- }
- this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
- this.breakpoint_ = candidateBreakpoint;
- break;
- }
- }
- }
-
- /**
- * Removes the current breakpoint.
- *
- * @private
- */
- removeCurrentBreakpoint_() {
- const className = this.currentBreakpointClass();
- this.breakpoint_ = '';
- if (className) {
- this.removeClass(className);
- }
- }
-
- /**
- * Get or set breakpoints on the player.
- *
- * Calling this method with an object or `true` will remove any previous
- * custom breakpoints and start from the defaults again.
- *
- * @param {Object|boolean} [breakpoints]
- * If an object is given, it can be used to provide custom
- * breakpoints. If `true` is given, will set default breakpoints.
- * If this argument is not given, will simply return the current
- * breakpoints.
- *
- * @param {number} [breakpoints.tiny]
- * The maximum width for the "vjs-layout-tiny" class.
- *
- * @param {number} [breakpoints.xsmall]
- * The maximum width for the "vjs-layout-x-small" class.
- *
- * @param {number} [breakpoints.small]
- * The maximum width for the "vjs-layout-small" class.
- *
- * @param {number} [breakpoints.medium]
- * The maximum width for the "vjs-layout-medium" class.
- *
- * @param {number} [breakpoints.large]
- * The maximum width for the "vjs-layout-large" class.
- *
- * @param {number} [breakpoints.xlarge]
- * The maximum width for the "vjs-layout-x-large" class.
- *
- * @param {number} [breakpoints.huge]
- * The maximum width for the "vjs-layout-huge" class.
- *
- * @return {Object}
- * An object mapping breakpoint names to maximum width values.
- */
- breakpoints(breakpoints) {
- // Used as a getter.
- if (breakpoints === undefined) {
- return Object.assign(this.breakpoints_);
- }
- this.breakpoint_ = '';
- this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
-
- // When breakpoint definitions change, we need to update the currently
- // selected breakpoint.
- this.updateCurrentBreakpoint_();
-
- // Clone the breakpoints before returning.
- return Object.assign(this.breakpoints_);
- }
-
- /**
- * Get or set a flag indicating whether or not this player should adjust
- * its UI based on its dimensions.
- *
- * @param {boolean} [value]
- * Should be `true` if the player should adjust its UI based on its
- * dimensions; otherwise, should be `false`.
- *
- * @return {boolean|undefined}
- * Will be `true` if this player should adjust its UI based on its
- * dimensions; otherwise, will be `false`.
- * Nothing if setting
- */
- responsive(value) {
- // Used as a getter.
- if (value === undefined) {
- return this.responsive_;
- }
- value = Boolean(value);
- const current = this.responsive_;
-
- // Nothing changed.
- if (value === current) {
- return;
- }
-
- // The value actually changed, set it.
- this.responsive_ = value;
-
- // Start listening for breakpoints and set the initial breakpoint if the
- // player is now responsive.
- if (value) {
- this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
- this.updateCurrentBreakpoint_();
-
- // Stop listening for breakpoints if the player is no longer responsive.
- } else {
- this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
- this.removeCurrentBreakpoint_();
- }
- return value;
- }
-
- /**
- * Get current breakpoint name, if any.
- *
- * @return {string}
- * If there is currently a breakpoint set, returns a the key from the
- * breakpoints object matching it. Otherwise, returns an empty string.
- */
- currentBreakpoint() {
- return this.breakpoint_;
- }
-
- /**
- * Get the current breakpoint class name.
- *
- * @return {string}
- * The matching class name (e.g. `"vjs-layout-tiny"` or
- * `"vjs-layout-large"`) for the current breakpoint. Empty string if
- * there is no current breakpoint.
- */
- currentBreakpointClass() {
- return BREAKPOINT_CLASSES[this.breakpoint_] || '';
- }
-
- /**
- * An object that describes a single piece of media.
- *
- * Properties that are not part of this type description will be retained; so,
- * this can be viewed as a generic metadata storage mechanism as well.
- *
- * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
- * @typedef {Object} Player~MediaObject
- *
- * @property {string} [album]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {string} [artist]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {Object[]} [artwork]
- * Unused, except if this object is passed to the `MediaSession`
- * API. If not specified, will be populated via the `poster`, if
- * available.
- *
- * @property {string} [poster]
- * URL to an image that will display before playback.
- *
- * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
- * A single source object, an array of source objects, or a string
- * referencing a URL to a media source. It is _highly recommended_
- * that an object or array of objects is used here, so that source
- * selection algorithms can take the `type` into account.
- *
- * @property {string} [title]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {Object[]} [textTracks]
- * An array of objects to be used to create text tracks, following
- * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
- * For ease of removal, these will be created as "remote" text
- * tracks and set to automatically clean up on source changes.
- *
- * These objects may have properties like `src`, `kind`, `label`,
- * and `language`, see {@link Tech#createRemoteTextTrack}.
- */
-
- /**
- * Populate the player using a {@link Player~MediaObject|MediaObject}.
- *
- * @param {Player~MediaObject} media
- * A media object.
- *
- * @param {Function} ready
- * A callback to be called when the player is ready.
- */
- loadMedia(media, ready) {
- if (!media || typeof media !== 'object') {
- return;
- }
- const crossOrigin = this.crossOrigin();
- this.reset();
-
- // Clone the media object so it cannot be mutated from outside.
- this.cache_.media = merge(media);
- const {
- artist,
- artwork,
- description,
- poster,
- src,
- textTracks,
- title
- } = this.cache_.media;
-
- // If `artwork` is not given, create it using `poster`.
- if (!artwork && poster) {
- this.cache_.media.artwork = [{
- src: poster,
- type: getMimetype(poster)
- }];
- }
- if (crossOrigin) {
- this.crossOrigin(crossOrigin);
- }
- if (src) {
- this.src(src);
- }
- if (poster) {
- this.poster(poster);
- }
- if (Array.isArray(textTracks)) {
- textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
- }
- if (this.titleBar) {
- this.titleBar.update({
- title,
- description: description || artist || ''
- });
- }
- this.ready(ready);
- }
-
- /**
- * Get a clone of the current {@link Player~MediaObject} for this player.
- *
- * If the `loadMedia` method has not been used, will attempt to return a
- * {@link Player~MediaObject} based on the current state of the player.
- *
- * @return {Player~MediaObject}
- */
- getMedia() {
- if (!this.cache_.media) {
- const poster = this.poster();
- const src = this.currentSources();
- const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
- kind: tt.kind,
- label: tt.label,
- language: tt.language,
- src: tt.src
- }));
- const media = {
- src,
- textTracks
- };
- if (poster) {
- media.poster = poster;
- media.artwork = [{
- src: media.poster,
- type: getMimetype(media.poster)
- }];
- }
- return media;
- }
- return merge(this.cache_.media);
- }
-
- /**
- * Gets tag settings
- *
- * @param {Element} tag
- * The player tag
- *
- * @return {Object}
- * An object containing all of the settings
- * for a player tag
- */
- static getTagSettings(tag) {
- const baseOptions = {
- sources: [],
- tracks: []
- };
- const tagOptions = getAttributes(tag);
- const dataSetup = tagOptions['data-setup'];
- if (hasClass(tag, 'vjs-fill')) {
- tagOptions.fill = true;
- }
- if (hasClass(tag, 'vjs-fluid')) {
- tagOptions.fluid = true;
- }
-
- // Check if data-setup attr exists.
- if (dataSetup !== null) {
- // Parse options JSON
- // If empty string, make it a parsable json object.
- const [err, data] = tuple(dataSetup || '{}');
- if (err) {
- log.error(err);
- }
- Object.assign(tagOptions, data);
- }
- Object.assign(baseOptions, tagOptions);
-
- // Get tag children settings
- if (tag.hasChildNodes()) {
- const children = tag.childNodes;
- for (let i = 0, j = children.length; i < j; i++) {
- const child = children[i];
- // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
- const childName = child.nodeName.toLowerCase();
- if (childName === 'source') {
- baseOptions.sources.push(getAttributes(child));
- } else if (childName === 'track') {
- baseOptions.tracks.push(getAttributes(child));
- }
- }
- }
- return baseOptions;
- }
-
- /**
- * Set debug mode to enable/disable logs at info level.
- *
- * @param {boolean} enabled
- * @fires Player#debugon
- * @fires Player#debugoff
- * @return {boolean|undefined}
- */
- debug(enabled) {
- if (enabled === undefined) {
- return this.debugEnabled_;
- }
- if (enabled) {
- this.trigger('debugon');
- this.previousLogLevel_ = this.log.level;
- this.log.level('debug');
- this.debugEnabled_ = true;
- } else {
- this.trigger('debugoff');
- this.log.level(this.previousLogLevel_);
- this.previousLogLevel_ = undefined;
- this.debugEnabled_ = false;
- }
- }
-
- /**
- * Set or get current playback rates.
- * Takes an array and updates the playback rates menu with the new items.
- * Pass in an empty array to hide the menu.
- * Values other than arrays are ignored.
- *
- * @fires Player#playbackrateschange
- * @param {number[]} [newRates]
- * The new rates that the playback rates menu should update to.
- * An empty array will hide the menu
- * @return {number[]} When used as a getter will return the current playback rates
- */
- playbackRates(newRates) {
- if (newRates === undefined) {
- return this.cache_.playbackRates;
- }
-
- // ignore any value that isn't an array
- if (!Array.isArray(newRates)) {
- return;
- }
-
- // ignore any arrays that don't only contain numbers
- if (!newRates.every(rate => typeof rate === 'number')) {
- return;
- }
- this.cache_.playbackRates = newRates;
-
- /**
- * fires when the playback rates in a player are changed
- *
- * @event Player#playbackrateschange
- * @type {Event}
- */
- this.trigger('playbackrateschange');
- }
- }
-
- /**
- * Get the {@link VideoTrackList}
- *
- * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
- *
- * @return {VideoTrackList}
- * the current video track list
- *
- * @method Player.prototype.videoTracks
- */
-
- /**
- * Get the {@link AudioTrackList}
- *
- * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
- *
- * @return {AudioTrackList}
- * the current audio track list
- *
- * @method Player.prototype.audioTracks
- */
-
- /**
- * Get the {@link TextTrackList}
- *
- * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
- *
- * @return {TextTrackList}
- * the current text track list
- *
- * @method Player.prototype.textTracks
- */
-
- /**
- * Get the remote {@link TextTrackList}
- *
- * @return {TextTrackList}
- * The current remote text track list
- *
- * @method Player.prototype.remoteTextTracks
- */
-
- /**
- * Get the remote {@link HtmlTrackElementList} tracks.
- *
- * @return {HtmlTrackElementList}
- * The current remote text track element list
- *
- * @method Player.prototype.remoteTextTrackEls
- */
-
- ALL.names.forEach(function (name) {
- const props = ALL[name];
- Player.prototype[props.getterName] = function () {
- if (this.tech_) {
- return this.tech_[props.getterName]();
- }
-
- // if we have not yet loadTech_, we create {video,audio,text}Tracks_
- // these will be passed to the tech during loading
- this[props.privateName] = this[props.privateName] || new props.ListClass();
- return this[props.privateName];
- };
- });
-
- /**
- * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the `` tag to control the CORS
- * behavior.
- *
- * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
- *
- * @param {string} [value]
- * The value to set the `Player`'s crossorigin to. If an argument is
- * given, must be one of `anonymous` or `use-credentials`.
- *
- * @return {string|undefined}
- * - The current crossorigin value of the `Player` when getting.
- * - undefined when setting
- */
- Player.prototype.crossorigin = Player.prototype.crossOrigin;
-
- /**
- * Global enumeration of players.
- *
- * The keys are the player IDs and the values are either the {@link Player}
- * instance or `null` for disposed players.
- *
- * @type {Object}
- */
- Player.players = {};
- const navigator = window.navigator;
-
- /*
- * Player instance options, surfaced using options
- * options = Player.prototype.options_
- * Make changes in options, not here.
- *
- * @type {Object}
- * @private
- */
- Player.prototype.options_ = {
- // Default order of fallback technology
- techOrder: Tech.defaultTechOrder_,
- html5: {},
- // enable sourceset by default
- enableSourceset: true,
- // default inactivity timeout
- inactivityTimeout: 2000,
- // default playback rates
- playbackRates: [],
- // Add playback rate selection by adding rates
- // 'playbackRates': [0.5, 1, 1.5, 2],
- liveui: false,
- // Included control sets
- children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
- language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
- // locales and their language translations
- languages: {},
- // Default message to show when a video cannot be played.
- notSupportedMessage: 'No compatible source was found for this media.',
- normalizeAutoplay: false,
- fullscreen: {
- options: {
- navigationUI: 'hide'
- }
- },
- breakpoints: {},
- responsive: false,
- audioOnlyMode: false,
- audioPosterMode: false,
- // Default smooth seeking to false
- enableSmoothSeeking: false
- };
- TECH_EVENTS_RETRIGGER.forEach(function (event) {
- Player.prototype[`handleTech${toTitleCase(event)}_`] = function () {
- return this.trigger(event);
- };
- });
-
- /**
- * Fired when the player has initial duration and dimension information
- *
- * @event Player#loadedmetadata
- * @type {Event}
- */
-
- /**
- * Fired when the player has downloaded data at the current playback position
- *
- * @event Player#loadeddata
- * @type {Event}
- */
-
- /**
- * Fired when the current playback position has changed *
- * During playback this is fired every 15-250 milliseconds, depending on the
- * playback technology in use.
- *
- * @event Player#timeupdate
- * @type {Event}
- */
-
- /**
- * Fired when the volume changes
- *
- * @event Player#volumechange
- * @type {Event}
- */
-
- /**
- * Reports whether or not a player has a plugin available.
- *
- * This does not report whether or not the plugin has ever been initialized
- * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
- *
- * @method Player#hasPlugin
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not this player has the requested plugin available.
- */
-
- /**
- * Reports whether or not a player is using a plugin by name.
- *
- * For basic plugins, this only reports whether the plugin has _ever_ been
- * initialized on this player.
- *
- * @method Player#usingPlugin
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not this player is using the requested plugin.
- */
-
- Component.registerComponent('Player', Player);
-
- /**
- * @file plugin.js
- */
-
- /**
- * The base plugin name.
- *
- * @private
- * @constant
- * @type {string}
- */
- const BASE_PLUGIN_NAME = 'plugin';
-
- /**
- * The key on which a player's active plugins cache is stored.
- *
- * @private
- * @constant
- * @type {string}
- */
- const PLUGIN_CACHE_KEY = 'activePlugins_';
-
- /**
- * Stores registered plugins in a private space.
- *
- * @private
- * @type {Object}
- */
- const pluginStorage = {};
-
- /**
- * Reports whether or not a plugin has been registered.
- *
- * @private
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not the plugin has been registered.
- */
- const pluginExists = name => pluginStorage.hasOwnProperty(name);
-
- /**
- * Get a single registered plugin by name.
- *
- * @private
- * @param {string} name
- * The name of a plugin.
- *
- * @return {typeof Plugin|Function|undefined}
- * The plugin (or undefined).
- */
- const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
-
- /**
- * Marks a plugin as "active" on a player.
- *
- * Also, ensures that the player has an object for tracking active plugins.
- *
- * @private
- * @param {Player} player
- * A Video.js player instance.
- *
- * @param {string} name
- * The name of a plugin.
- */
- const markPluginAsActive = (player, name) => {
- player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
- player[PLUGIN_CACHE_KEY][name] = true;
- };
-
- /**
- * Triggers a pair of plugin setup events.
- *
- * @private
- * @param {Player} player
- * A Video.js player instance.
- *
- * @param {PluginEventHash} hash
- * A plugin event hash.
- *
- * @param {boolean} [before]
- * If true, prefixes the event name with "before". In other words,
- * use this to trigger "beforepluginsetup" instead of "pluginsetup".
- */
- const triggerSetupEvent = (player, hash, before) => {
- const eventName = (before ? 'before' : '') + 'pluginsetup';
- player.trigger(eventName, hash);
- player.trigger(eventName + ':' + hash.name, hash);
- };
-
- /**
- * Takes a basic plugin function and returns a wrapper function which marks
- * on the player that the plugin has been activated.
- *
- * @private
- * @param {string} name
- * The name of the plugin.
- *
- * @param {Function} plugin
- * The basic plugin.
- *
- * @return {Function}
- * A wrapper function for the given plugin.
- */
- const createBasicPlugin = function (name, plugin) {
- const basicPluginWrapper = function () {
- // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
- // regardless, but we want the hash to be consistent with the hash provided
- // for advanced plugins.
- //
- // The only potentially counter-intuitive thing here is the `instance` in
- // the "pluginsetup" event is the value returned by the `plugin` function.
- triggerSetupEvent(this, {
- name,
- plugin,
- instance: null
- }, true);
- const instance = plugin.apply(this, arguments);
- markPluginAsActive(this, name);
- triggerSetupEvent(this, {
- name,
- plugin,
- instance
- });
- return instance;
- };
- Object.keys(plugin).forEach(function (prop) {
- basicPluginWrapper[prop] = plugin[prop];
- });
- return basicPluginWrapper;
- };
-
- /**
- * Takes a plugin sub-class and returns a factory function for generating
- * instances of it.
- *
- * This factory function will replace itself with an instance of the requested
- * sub-class of Plugin.
- *
- * @private
- * @param {string} name
- * The name of the plugin.
- *
- * @param {Plugin} PluginSubClass
- * The advanced plugin.
- *
- * @return {Function}
- */
- const createPluginFactory = (name, PluginSubClass) => {
- // Add a `name` property to the plugin prototype so that each plugin can
- // refer to itself by name.
- PluginSubClass.prototype.name = name;
- return function (...args) {
- triggerSetupEvent(this, {
- name,
- plugin: PluginSubClass,
- instance: null
- }, true);
- const instance = new PluginSubClass(...[this, ...args]);
-
- // The plugin is replaced by a function that returns the current instance.
- this[name] = () => instance;
- triggerSetupEvent(this, instance.getEventHash());
- return instance;
- };
- };
-
- /**
- * Parent class for all advanced plugins.
- *
- * @mixes module:evented~EventedMixin
- * @mixes module:stateful~StatefulMixin
- * @fires Player#beforepluginsetup
- * @fires Player#beforepluginsetup:$name
- * @fires Player#pluginsetup
- * @fires Player#pluginsetup:$name
- * @listens Player#dispose
- * @throws {Error}
- * If attempting to instantiate the base {@link Plugin} class
- * directly instead of via a sub-class.
- */
- class Plugin {
- /**
- * Creates an instance of this class.
- *
- * Sub-classes should call `super` to ensure plugins are properly initialized.
- *
- * @param {Player} player
- * A Video.js player instance.
- */
- constructor(player) {
- if (this.constructor === Plugin) {
- throw new Error('Plugin must be sub-classed; not directly instantiated.');
- }
- this.player = player;
- if (!this.log) {
- this.log = this.player.log.createLogger(this.name);
- }
-
- // Make this object evented, but remove the added `trigger` method so we
- // use the prototype version instead.
- evented(this);
- delete this.trigger;
- stateful(this, this.constructor.defaultState);
- markPluginAsActive(player, this.name);
-
- // Auto-bind the dispose method so we can use it as a listener and unbind
- // it later easily.
- this.dispose = this.dispose.bind(this);
-
- // If the player is disposed, dispose the plugin.
- player.on('dispose', this.dispose);
- }
-
- /**
- * Get the version of the plugin that was set on .VERSION
- */
- version() {
- return this.constructor.VERSION;
- }
-
- /**
- * Each event triggered by plugins includes a hash of additional data with
- * conventional properties.
- *
- * This returns that object or mutates an existing hash.
- *
- * @param {Object} [hash={}]
- * An object to be used as event an event hash.
- *
- * @return {PluginEventHash}
- * An event hash object with provided properties mixed-in.
- */
- getEventHash(hash = {}) {
- hash.name = this.name;
- hash.plugin = this.constructor;
- hash.instance = this;
- return hash;
- }
-
- /**
- * Triggers an event on the plugin object and overrides
- * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
- *
- * @param {string|Object} event
- * An event type or an object with a type property.
- *
- * @param {Object} [hash={}]
- * Additional data hash to merge with a
- * {@link PluginEventHash|PluginEventHash}.
- *
- * @return {boolean}
- * Whether or not default was prevented.
- */
- trigger(event, hash = {}) {
- return trigger(this.eventBusEl_, event, this.getEventHash(hash));
- }
-
- /**
- * Handles "statechanged" events on the plugin. No-op by default, override by
- * subclassing.
- *
- * @abstract
- * @param {Event} e
- * An event object provided by a "statechanged" event.
- *
- * @param {Object} e.changes
- * An object describing changes that occurred with the "statechanged"
- * event.
- */
- handleStateChanged(e) {}
-
- /**
- * Disposes a plugin.
- *
- * Subclasses can override this if they want, but for the sake of safety,
- * it's probably best to subscribe the "dispose" event.
- *
- * @fires Plugin#dispose
- */
- dispose() {
- const {
- name,
- player
- } = this;
-
- /**
- * Signals that a advanced plugin is about to be disposed.
- *
- * @event Plugin#dispose
- * @type {Event}
- */
- this.trigger('dispose');
- this.off();
- player.off('dispose', this.dispose);
-
- // Eliminate any possible sources of leaking memory by clearing up
- // references between the player and the plugin instance and nulling out
- // the plugin's state and replacing methods with a function that throws.
- player[PLUGIN_CACHE_KEY][name] = false;
- this.player = this.state = null;
-
- // Finally, replace the plugin name on the player with a new factory
- // function, so that the plugin is ready to be set up again.
- player[name] = createPluginFactory(name, pluginStorage[name]);
- }
-
- /**
- * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
- *
- * @param {string|Function} plugin
- * If a string, matches the name of a plugin. If a function, will be
- * tested directly.
- *
- * @return {boolean}
- * Whether or not a plugin is a basic plugin.
- */
- static isBasic(plugin) {
- const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
- return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
- }
-
- /**
- * Register a Video.js plugin.
- *
- * @param {string} name
- * The name of the plugin to be registered. Must be a string and
- * must not match an existing plugin or a method on the `Player`
- * prototype.
- *
- * @param {typeof Plugin|Function} plugin
- * A sub-class of `Plugin` or a function for basic plugins.
- *
- * @return {typeof Plugin|Function}
- * For advanced plugins, a factory function for that plugin. For
- * basic plugins, a wrapper function that initializes the plugin.
- */
- static registerPlugin(name, plugin) {
- if (typeof name !== 'string') {
- throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
- }
- if (pluginExists(name)) {
- log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
- } else if (Player.prototype.hasOwnProperty(name)) {
- throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
- }
- if (typeof plugin !== 'function') {
- throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
- }
- pluginStorage[name] = plugin;
-
- // Add a player prototype method for all sub-classed plugins (but not for
- // the base Plugin class).
- if (name !== BASE_PLUGIN_NAME) {
- if (Plugin.isBasic(plugin)) {
- Player.prototype[name] = createBasicPlugin(name, plugin);
- } else {
- Player.prototype[name] = createPluginFactory(name, plugin);
- }
- }
- return plugin;
- }
-
- /**
- * De-register a Video.js plugin.
- *
- * @param {string} name
- * The name of the plugin to be de-registered. Must be a string that
- * matches an existing plugin.
- *
- * @throws {Error}
- * If an attempt is made to de-register the base plugin.
- */
- static deregisterPlugin(name) {
- if (name === BASE_PLUGIN_NAME) {
- throw new Error('Cannot de-register base plugin.');
- }
- if (pluginExists(name)) {
- delete pluginStorage[name];
- delete Player.prototype[name];
- }
- }
-
- /**
- * Gets an object containing multiple Video.js plugins.
- *
- * @param {Array} [names]
- * If provided, should be an array of plugin names. Defaults to _all_
- * plugin names.
- *
- * @return {Object|undefined}
- * An object containing plugin(s) associated with their name(s) or
- * `undefined` if no matching plugins exist).
- */
- static getPlugins(names = Object.keys(pluginStorage)) {
- let result;
- names.forEach(name => {
- const plugin = getPlugin(name);
- if (plugin) {
- result = result || {};
- result[name] = plugin;
- }
- });
- return result;
- }
-
- /**
- * Gets a plugin's version, if available
- *
- * @param {string} name
- * The name of a plugin.
- *
- * @return {string}
- * The plugin's version or an empty string.
- */
- static getPluginVersion(name) {
- const plugin = getPlugin(name);
- return plugin && plugin.VERSION || '';
- }
- }
-
- /**
- * Gets a plugin by name if it exists.
- *
- * @static
- * @method getPlugin
- * @memberOf Plugin
- * @param {string} name
- * The name of a plugin.
- *
- * @returns {typeof Plugin|Function|undefined}
- * The plugin (or `undefined`).
- */
- Plugin.getPlugin = getPlugin;
-
- /**
- * The name of the base plugin class as it is registered.
- *
- * @type {string}
- */
- Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
- Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
-
- /**
- * Documented in player.js
- *
- * @ignore
- */
- Player.prototype.usingPlugin = function (name) {
- return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
- };
-
- /**
- * Documented in player.js
- *
- * @ignore
- */
- Player.prototype.hasPlugin = function (name) {
- return !!pluginExists(name);
- };
-
- /**
- * Signals that a plugin is about to be set up on a player.
- *
- * @event Player#beforepluginsetup
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin is about to be set up on a player - by name. The name
- * is the name of the plugin.
- *
- * @event Player#beforepluginsetup:$name
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin has just been set up on a player.
- *
- * @event Player#pluginsetup
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin has just been set up on a player - by name. The name
- * is the name of the plugin.
- *
- * @event Player#pluginsetup:$name
- * @type {PluginEventHash}
- */
-
- /**
- * @typedef {Object} PluginEventHash
- *
- * @property {string} instance
- * For basic plugins, the return value of the plugin function. For
- * advanced plugins, the plugin instance on which the event is fired.
- *
- * @property {string} name
- * The name of the plugin.
- *
- * @property {string} plugin
- * For basic plugins, the plugin function. For advanced plugins, the
- * plugin class/constructor.
- */
-
- /**
- * @file deprecate.js
- * @module deprecate
- */
-
- /**
- * Decorate a function with a deprecation message the first time it is called.
- *
- * @param {string} message
- * A deprecation message to log the first time the returned function
- * is called.
- *
- * @param {Function} fn
- * The function to be deprecated.
- *
- * @return {Function}
- * A wrapper function that will log a deprecation warning the first
- * time it is called. The return value will be the return value of
- * the wrapped function.
- */
- function deprecate(message, fn) {
- let warned = false;
- return function (...args) {
- if (!warned) {
- log.warn(message);
- }
- warned = true;
- return fn.apply(this, args);
- };
- }
-
- /**
- * Internal function used to mark a function as deprecated in the next major
- * version with consistent messaging.
- *
- * @param {number} major The major version where it will be removed
- * @param {string} oldName The old function name
- * @param {string} newName The new function name
- * @param {Function} fn The function to deprecate
- * @return {Function} The decorated function
- */
- function deprecateForMajor(major, oldName, newName, fn) {
- return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
- }
-
- var VjsErrors = {
- UnsupportedSidxContainer: 'unsupported-sidx-container-error',
- DashManifestSidxParsingError: 'dash-manifest-sidx-parsing-error',
- HlsPlaylistRequestError: 'hls-playlist-request-error',
- SegmentUnsupportedMediaFormat: 'segment-unsupported-media-format-error',
- UnsupportedMediaInitialization: 'unsupported-media-initialization-error',
- SegmentSwitchError: 'segment-switch-error',
- SegmentExceedsSourceBufferQuota: 'segment-exceeds-source-buffer-quota-error',
- SegmentAppendError: 'segment-append-error',
- VttLoadError: 'vtt-load-error',
- VttCueParsingError: 'vtt-cue-parsing-error',
- // Errors used in contrib-ads:
- AdsBeforePrerollError: 'ads-before-preroll-error',
- AdsPrerollError: 'ads-preroll-error',
- AdsMidrollError: 'ads-midroll-error',
- AdsPostrollError: 'ads-postroll-error',
- AdsMacroReplacementFailed: 'ads-macro-replacement-failed',
- AdsResumeContentFailed: 'ads-resume-content-failed',
- // Errors used in contrib-eme:
- EMEFailedToRequestMediaKeySystemAccess: 'eme-failed-request-media-key-system-access',
- EMEFailedToCreateMediaKeys: 'eme-failed-create-media-keys',
- EMEFailedToAttachMediaKeysToVideoElement: 'eme-failed-attach-media-keys-to-video',
- EMEFailedToCreateMediaKeySession: 'eme-failed-create-media-key-session',
- EMEFailedToSetServerCertificate: 'eme-failed-set-server-certificate',
- EMEFailedToGenerateLicenseRequest: 'eme-failed-generate-license-request',
- EMEFailedToUpdateSessionWithReceivedLicenseKeys: 'eme-failed-update-session',
- EMEFailedToCloseSession: 'eme-failed-close-session',
- EMEFailedToRemoveKeysFromSession: 'eme-failed-remove-keys',
- EMEFailedToLoadSessionBySessionId: 'eme-failed-load-session'
- };
-
- /**
- * @file video.js
- * @module videojs
- */
-
- /**
- * Normalize an `id` value by trimming off a leading `#`
- *
- * @private
- * @param {string} id
- * A string, maybe with a leading `#`.
- *
- * @return {string}
- * The string, without any leading `#`.
- */
- const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
-
- /**
- * A callback that is called when a component is ready. Does not have any
- * parameters and any callback value will be ignored. See: {@link Component~ReadyCallback}
- *
- * @callback ReadyCallback
- */
-
- /**
- * The `videojs()` function doubles as the main function for users to create a
- * {@link Player} instance as well as the main library namespace.
- *
- * It can also be used as a getter for a pre-existing {@link Player} instance.
- * However, we _strongly_ recommend using `videojs.getPlayer()` for this
- * purpose because it avoids any potential for unintended initialization.
- *
- * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
- * of our JSDoc template, we cannot properly document this as both a function
- * and a namespace, so its function signature is documented here.
- *
- * #### Arguments
- * ##### id
- * string|Element, **required**
- *
- * Video element or video element ID.
- *
- * ##### options
- * Object, optional
- *
- * Options object for providing settings.
- * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
- *
- * ##### ready
- * {@link Component~ReadyCallback}, optional
- *
- * A function to be called when the {@link Player} and {@link Tech} are ready.
- *
- * #### Return Value
- *
- * The `videojs()` function returns a {@link Player} instance.
- *
- * @namespace
- *
- * @borrows AudioTrack as AudioTrack
- * @borrows Component.getComponent as getComponent
- * @borrows module:events.on as on
- * @borrows module:events.one as one
- * @borrows module:events.off as off
- * @borrows module:events.trigger as trigger
- * @borrows EventTarget as EventTarget
- * @borrows module:middleware.use as use
- * @borrows Player.players as players
- * @borrows Plugin.registerPlugin as registerPlugin
- * @borrows Plugin.deregisterPlugin as deregisterPlugin
- * @borrows Plugin.getPlugins as getPlugins
- * @borrows Plugin.getPlugin as getPlugin
- * @borrows Plugin.getPluginVersion as getPluginVersion
- * @borrows Tech.getTech as getTech
- * @borrows Tech.registerTech as registerTech
- * @borrows TextTrack as TextTrack
- * @borrows VideoTrack as VideoTrack
- *
- * @param {string|Element} id
- * Video element or video element ID.
- *
- * @param {Object} [options]
- * Options object for providing settings.
- * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
- *
- * @param {ReadyCallback} [ready]
- * A function to be called when the {@link Player} and {@link Tech} are
- * ready.
- *
- * @return {Player}
- * The `videojs()` function returns a {@link Player|Player} instance.
- */
- function videojs(id, options, ready) {
- let player = videojs.getPlayer(id);
- if (player) {
- if (options) {
- log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
- }
- if (ready) {
- player.ready(ready);
- }
- return player;
- }
- const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
- if (!isEl(el)) {
- throw new TypeError('The element or ID supplied is not valid. (videojs)');
- }
-
- // document.body.contains(el) will only check if el is contained within that one document.
- // This causes problems for elements in iframes.
- // Instead, use the element's ownerDocument instead of the global document.
- // This will make sure that the element is indeed in the dom of that document.
- // Additionally, check that the document in question has a default view.
- // If the document is no longer attached to the dom, the defaultView of the document will be null.
- // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body
- // always returns false. Instead, use the Shadow DOM root.
- const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window.ShadowRoot : false;
- const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body;
- if (!el.ownerDocument.defaultView || !rootNode.contains(el)) {
- log.warn('The element supplied is not included in the DOM');
- }
- options = options || {};
-
- // Store a copy of the el before modification, if it is to be restored in destroy()
- // If div ingest, store the parent div
- if (options.restoreEl === true) {
- options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
- }
- hooks('beforesetup').forEach(hookFunction => {
- const opts = hookFunction(el, merge(options));
- if (!isObject(opts) || Array.isArray(opts)) {
- log.error('please return an object in beforesetup hooks');
- return;
- }
- options = merge(options, opts);
- });
-
- // We get the current "Player" component here in case an integration has
- // replaced it with a custom player.
- const PlayerComponent = Component.getComponent('Player');
- player = new PlayerComponent(el, options, ready);
- hooks('setup').forEach(hookFunction => hookFunction(player));
- return player;
- }
- videojs.hooks_ = hooks_;
- videojs.hooks = hooks;
- videojs.hook = hook;
- videojs.hookOnce = hookOnce;
- videojs.removeHook = removeHook;
-
- // Add default styles
- if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
- let style = $('.vjs-styles-defaults');
- if (!style) {
- style = createStyleElement('vjs-styles-defaults');
- const head = $('head');
- if (head) {
- head.insertBefore(style, head.firstChild);
- }
- setTextContent(style, `
- .video-js {
- width: 300px;
- height: 150px;
- }
-
- .vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: 56.25%
- }
- `);
- }
- }
-
- // Run Auto-load players
- // You have to wait at least once in case this script is loaded after your
- // video in the DOM (weird behavior only with minified version)
- autoSetupTimeout(1, videojs);
-
- /**
- * Current Video.js version. Follows [semantic versioning](https://semver.org/).
- *
- * @type {string}
- */
- videojs.VERSION = version;
-
- /**
- * The global options object. These are the settings that take effect
- * if no overrides are specified when the player is created.
- *
- * @type {Object}
- */
- videojs.options = Player.prototype.options_;
-
- /**
- * Get an object with the currently created players, keyed by player ID
- *
- * @return {Object}
- * The created players
- */
- videojs.getPlayers = () => Player.players;
-
- /**
- * Get a single player based on an ID or DOM element.
- *
- * This is useful if you want to check if an element or ID has an associated
- * Video.js player, but not create one if it doesn't.
- *
- * @param {string|Element} id
- * An HTML element - ``, ``, or `` -
- * or a string matching the `id` of such an element.
- *
- * @return {Player|undefined}
- * A player instance or `undefined` if there is no player instance
- * matching the argument.
- */
- videojs.getPlayer = id => {
- const players = Player.players;
- let tag;
- if (typeof id === 'string') {
- const nId = normalizeId(id);
- const player = players[nId];
- if (player) {
- return player;
- }
- tag = $('#' + nId);
- } else {
- tag = id;
- }
- if (isEl(tag)) {
- const {
- player,
- playerId
- } = tag;
-
- // Element may have a `player` property referring to an already created
- // player instance. If so, return that.
- if (player || players[playerId]) {
- return player || players[playerId];
- }
- }
- };
-
- /**
- * Returns an array of all current players.
- *
- * @return {Array}
- * An array of all players. The array will be in the order that
- * `Object.keys` provides, which could potentially vary between
- * JavaScript engines.
- *
- */
- videojs.getAllPlayers = () =>
- // Disposed players leave a key with a `null` value, so we need to make sure
- // we filter those out.
- Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
- videojs.players = Player.players;
- videojs.getComponent = Component.getComponent;
-
- /**
- * Register a component so it can referred to by name. Used when adding to other
- * components, either through addChild `component.addChild('myComponent')` or through
- * default children options `{ children: ['myComponent'] }`.
- *
- * > NOTE: You could also just initialize the component before adding.
- * `component.addChild(new MyComponent());`
- *
- * @param {string} name
- * The class name of the component
- *
- * @param {typeof Component} comp
- * The component class
- *
- * @return {typeof Component}
- * The newly registered component
- */
- videojs.registerComponent = (name, comp) => {
- if (Tech.isTech(comp)) {
- log.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
- }
- return Component.registerComponent.call(Component, name, comp);
- };
- videojs.getTech = Tech.getTech;
- videojs.registerTech = Tech.registerTech;
- videojs.use = use;
-
- /**
- * An object that can be returned by a middleware to signify
- * that the middleware is being terminated.
- *
- * @type {object}
- * @property {object} middleware.TERMINATOR
- */
- Object.defineProperty(videojs, 'middleware', {
- value: {},
- writeable: false,
- enumerable: true
- });
- Object.defineProperty(videojs.middleware, 'TERMINATOR', {
- value: TERMINATOR,
- writeable: false,
- enumerable: true
- });
-
- /**
- * A reference to the {@link module:browser|browser utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:browser|browser}
- */
- videojs.browser = browser;
-
- /**
- * A reference to the {@link module:obj|obj utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:obj|obj}
- */
- videojs.obj = Obj;
-
- /**
- * Deprecated reference to the {@link module:obj.merge|merge function}
- *
- * @type {Function}
- * @see {@link module:obj.merge|merge}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
- */
- videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge);
-
- /**
- * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
- *
- * @type {Function}
- * @see {@link module:obj.defineLazyProperty|defineLazyProperty}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
- */
- videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
-
- /**
- * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
- *
- * @type {Function}
- * @see {@link module:fn.bind_|fn.bind_}
- * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
- */
- videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
- videojs.registerPlugin = Plugin.registerPlugin;
- videojs.deregisterPlugin = Plugin.deregisterPlugin;
-
- /**
- * Deprecated method to register a plugin with Video.js
- *
- * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
- *
- * @param {string} name
- * The plugin name
- *
- * @param {typeof Plugin|Function} plugin
- * The plugin sub-class or function
- *
- * @return {typeof Plugin|Function}
- */
- videojs.plugin = (name, plugin) => {
- log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
- return Plugin.registerPlugin(name, plugin);
- };
- videojs.getPlugins = Plugin.getPlugins;
- videojs.getPlugin = Plugin.getPlugin;
- videojs.getPluginVersion = Plugin.getPluginVersion;
-
- /**
- * Adding languages so that they're available to all players.
- * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
- *
- * @param {string} code
- * The language code or dictionary property
- *
- * @param {Object} data
- * The data values to be translated
- *
- * @return {Object}
- * The resulting language dictionary object
- */
- videojs.addLanguage = function (code, data) {
- code = ('' + code).toLowerCase();
- videojs.options.languages = merge(videojs.options.languages, {
- [code]: data
- });
- return videojs.options.languages[code];
- };
-
- /**
- * A reference to the {@link module:log|log utility module} as an object.
- *
- * @type {Function}
- * @see {@link module:log|log}
- */
- videojs.log = log;
- videojs.createLogger = createLogger;
-
- /**
- * A reference to the {@link module:time|time utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:time|time}
- */
- videojs.time = Time;
-
- /**
- * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
- *
- * @type {Function}
- * @see {@link module:time.createTimeRanges|createTimeRanges}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
- */
- videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges);
-
- /**
- * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
- *
- * @type {Function}
- * @see {@link module:time.createTimeRanges|createTimeRanges}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
- */
- videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges);
-
- /**
- * Deprecated reference to the {@link module:time.formatTime|formatTime function}
- *
- * @type {Function}
- * @see {@link module:time.formatTime|formatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
- */
- videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
-
- /**
- * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
- *
- * @type {Function}
- * @see {@link module:time.setFormatTime|setFormatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
- */
- videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
-
- /**
- * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
- *
- * @type {Function}
- * @see {@link module:time.resetFormatTime|resetFormatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
- */
- videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
-
- /**
- * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
- *
- * @type {Function}
- * @see {@link module:url.parseUrl|parseUrl}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
- */
- videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
-
- /**
- * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
- *
- * @type {Function}
- * @see {@link module:url.isCrossOrigin|isCrossOrigin}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
- */
- videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
- videojs.EventTarget = EventTarget;
- videojs.any = any;
- videojs.on = on;
- videojs.one = one;
- videojs.off = off;
- videojs.trigger = trigger;
-
- /**
- * A cross-browser XMLHttpRequest wrapper.
- *
- * @function
- * @param {Object} options
- * Settings for the request.
- *
- * @return {XMLHttpRequest|XDomainRequest}
- * The request object.
- *
- * @see https://github.com/Raynos/xhr
- */
- videojs.xhr = lib;
- videojs.TextTrack = TextTrack;
- videojs.AudioTrack = AudioTrack;
- videojs.VideoTrack = VideoTrack;
- ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
- videojs[k] = function () {
- log.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
- return Dom[k].apply(null, arguments);
- };
- });
- videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
-
- /**
- * A reference to the {@link module:dom|DOM utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:dom|dom}
- */
- videojs.dom = Dom;
-
- /**
- * A reference to the {@link module:fn|fn utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:fn|fn}
- */
- videojs.fn = Fn;
-
- /**
- * A reference to the {@link module:num|num utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:num|num}
- */
- videojs.num = Num;
-
- /**
- * A reference to the {@link module:str|str utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:str|str}
- */
- videojs.str = Str;
-
- /**
- * A reference to the {@link module:url|URL utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:url|url}
- */
- videojs.url = Url;
-
- // The list of possible error types to occur in video.js
- videojs.Error = VjsErrors;
-
- return videojs;
-
-}));
diff --git a/source/src/public/twitch/video.js/alt/video.core.min.js b/source/src/public/twitch/video.js/alt/video.core.min.js
deleted file mode 100755
index 82b5015..0000000
--- a/source/src/public/twitch/video.js/alt/video.core.min.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Video.js 8.12.0
- * Copyright Brightcove, Inc.
- * Available under Apache License Version 2.0
- *
- *
- * Includes vtt.js
- * Available under Apache License Version 2.0
- *
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).videojs=t()}(this,function(){"use strict";var D="8.12.0";const B={},R=function(e,t){return B[e]=B[e]||[],t&&(B[e]=B[e].concat(t)),B[e]};function H(e,t){return!((t=R(e).indexOf(t))<=-1||(B[e]=B[e].slice(),B[e].splice(t,1),0))}const F={prefixed:!0};var V=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror","fullscreen"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror","-webkit-full-screen"]],z=V[0];let q;for(let e=0;e{var e,s=h.levels[s],r=new RegExp(`^(${s})$`);let n=l;if("log"!==t&&i.unshift(t.toUpperCase()+":"),c&&(n="%c"+l,i.unshift(c)),i.unshift(n+":"),d&&(d.push([].concat(i)),e=d.length-1e3,d.splice(0,0i(r+` ${t=void 0!==t?t:n} `+e,t,void 0!==s?s:a),o.createNewLogger=(e,t,s)=>i(e,t,s),o.levels={all:"debug|log|warn|error",off:"",debug:"debug|log|warn|error",info:"log|warn|error",warn:"warn|error",error:"error",DEFAULT:t},o.level=e=>{if("string"==typeof e){if(!o.levels.hasOwnProperty(e))throw new Error(`"${e}" in not a valid log level`);t=e}return t},o.history=()=>d?[].concat(d):[],o.history.filter=t=>(d||[]).filter(e=>new RegExp(`.*${t}.*`).test(e[0])),o.history.clear=()=>{d&&(d.length=0)},o.history.disable=()=>{null!==d&&(d.length=0,d=null)},o.history.enable=()=>{null===d&&(d=[])},o.error=(...e)=>s("error",t,e),o.warn=(...e)=>s("warn",t,e),o.debug=(...e)=>s("debug",t,e),o}("VIDEOJS"),U=l.createLogger,K=Object.prototype.toString;function W(t,s){$(t).forEach(e=>s(t[e],e))}function X(s,i,e=0){return $(s).reduce((e,t)=>i(e,s[t],t),e)}function n(e){return!!e&&"object"==typeof e}function G(e){return n(e)&&"[object Object]"===K.call(e)&&e.constructor===Object}function h(...e){const s={};return e.forEach(e=>{e&&W(e,(e,t)=>{G(e)?(G(s[t])||(s[t]={}),s[t]=h(s[t],e)):s[t]=e})}),s}function Y(e={}){var t,s=[];for(const i in e)e.hasOwnProperty(i)&&(t=e[i],s.push(t));return s}function Q(t,s,i,e=!0){const r=e=>Object.defineProperty(t,s,{value:e,enumerable:!0,writable:!0});var n={configurable:!0,enumerable:!0,get(){var e=i();return r(e),e}};return e&&(n.set=r),Object.defineProperty(t,s,n)}var J=Object.freeze({__proto__:null,each:W,reduce:X,isObject:n,isPlain:G,merge:h,values:Y,defineLazyProperty:Q});let Z=!1,ee=null,o=!1,te,se=!1,ie=!1,re=!1,c=!1,ne=null,ae=null,oe=null,le=!1,he=!1,ce=!1,de=!1,ue=!1,pe=!1,ge=!1;const me=Boolean(be()&&("ontouchstart"in window||window.navigator.maxTouchPoints||window.DocumentTouch&&window.document instanceof window.DocumentTouch));var ve,e=window.navigator&&window.navigator.userAgentData;if(e&&e.platform&&e.brands&&(o="Android"===e.platform,ie=Boolean(e.brands.find(e=>"Microsoft Edge"===e.brand)),re=Boolean(e.brands.find(e=>"Chromium"===e.brand)),c=!ie&&re,ne=ae=(e.brands.find(e=>"Chromium"===e.brand)||{}).version||null,he="Windows"===e.platform),!re){const N=window.navigator&&window.navigator.userAgent||"";Z=/iPod/i.test(N),ee=(e=N.match(/OS (\d+)_/i))&&e[1]?e[1]:null,o=/Android/i.test(N),te=(e=N.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i))?(Pt=e[1]&&parseFloat(e[1]),ve=e[2]&&parseFloat(e[2]),Pt&&ve?parseFloat(e[1]+"."+e[2]):Pt||null):null,se=/Firefox/i.test(N),ie=/Edg/i.test(N),re=/Chrome/i.test(N)||/CriOS/i.test(N),c=!ie&&re,ne=ae=(ve=N.match(/(Chrome|CriOS)\/(\d+)/))&&ve[2]?parseFloat(ve[2]):null,oe=function(){var e=/MSIE\s(\d+)\.\d/.exec(N);let t=e&&parseFloat(e[1]);return t=!t&&/Trident\/7.0/i.test(N)&&/rv:11.0/.test(N)?11:t}(),ue=/Tizen/i.test(N),pe=/Web0S/i.test(N),ge=ue||pe,le=/Safari/i.test(N)&&!c&&!o&&!ie&&!ge,he=/Windows/i.test(N),ce=/iPad/i.test(N)||le&&me&&!/iPhone/i.test(N),de=/iPhone/i.test(N)&&!ce}const u=de||ce||Z,_e=(le||u)&&!c;var fe=Object.freeze({__proto__:null,get IS_IPOD(){return Z},get IOS_VERSION(){return ee},get IS_ANDROID(){return o},get ANDROID_VERSION(){return te},get IS_FIREFOX(){return se},get IS_EDGE(){return ie},get IS_CHROMIUM(){return re},get IS_CHROME(){return c},get CHROMIUM_VERSION(){return ne},get CHROME_VERSION(){return ae},get IE_VERSION(){return oe},get IS_SAFARI(){return le},get IS_WINDOWS(){return he},get IS_IPAD(){return ce},get IS_IPHONE(){return de},get IS_TIZEN(){return ue},get IS_WEBOS(){return pe},get IS_SMART_TV(){return ge},TOUCH_ENABLED:me,IS_IOS:u,IS_ANY_SAFARI:_e});function ye(e){return"string"==typeof e&&Boolean(e.trim())}function be(){return document===window.document}function Te(e){return n(e)&&1===e.nodeType}function ke(){try{return window.parent!==window.self}catch(e){return!0}}function Ce(s){return function(e,t){return ye(e)?(t=Te(t=ye(t)?document.querySelector(t):t)?t:document)[s]&&t[s](e):document[s](null)}}function p(e="div",s={},t={},i){const r=document.createElement(e);return Object.getOwnPropertyNames(s).forEach(function(e){var t=s[e];"textContent"===e?we(r,t):r[e]===t&&"tabIndex"!==e||(r[e]=t)}),Object.getOwnPropertyNames(t).forEach(function(e){r.setAttribute(e,t[e])}),i&&qe(r,i),r}function we(e,t){return"undefined"==typeof e.textContent?e.innerText=t:e.textContent=t,e}function Ee(e,t){t.firstChild?t.insertBefore(e,t.firstChild):t.appendChild(e)}function Se(e,t){if(0<=t.indexOf(" "))throw new Error("class has illegal whitespace characters");return e.classList.contains(t)}function xe(e,...t){return e.classList.add(...t.reduce((e,t)=>e.concat(t.split(/\s+/)),[])),e}function je(e,...t){return e?(e.classList.remove(...t.reduce((e,t)=>e.concat(t.split(/\s+/)),[])),e):(l.warn("removeClass was called with an element that doesn't exist"),null)}function Pe(t,e,s){return"boolean"!=typeof(s="function"==typeof s?s(t,e):s)&&(s=void 0),e.split(/\s+/).forEach(e=>t.classList.toggle(e,s)),t}function Ie(s,i){Object.getOwnPropertyNames(i).forEach(function(e){var t=i[e];null===t||"undefined"==typeof t||!1===t?s.removeAttribute(e):s.setAttribute(e,!0===t?"":t)})}function Me(e){var s={},i=["autoplay","controls","playsinline","loop","muted","default","defaultMuted"];if(e&&e.attributes&&0{void 0!==t[e]&&(s[e]=t[e])}),s.height||(s.height=parseFloat(Xe(e,"height"))),s.width||(s.width=parseFloat(Xe(e,"width"))),s}}function Re(e){if(!e||!e.offsetParent)return{left:0,top:0,width:0,height:0};var t=e.offsetWidth,s=e.offsetHeight;let i=0,r=0;for(;e.offsetParent&&e!==document[F.fullscreenElement];)i+=e.offsetLeft,r+=e.offsetTop,e=e.offsetParent;return{left:i,top:r,width:t,height:s}}function He(t,e){var s={x:0,y:0};if(u){let e=t;for(;e&&"html"!==e.nodeName.toLowerCase();){var i,r=Xe(e,"transform");/^matrix/.test(r)?(i=r.slice(7,-1).split(/,\s/).map(Number),s.x+=i[4],s.y+=i[5]):/^matrix3d/.test(r)&&(i=r.slice(9,-1).split(/,\s/).map(Number),s.x+=i[12],s.y+=i[13]),e=e.parentNode}}var n={},a=Re(e.target),t=Re(t),o=t.width,l=t.height;let h=e.offsetY-(t.top-a.top),c=e.offsetX-(t.left-a.left);return e.changedTouches&&(c=e.changedTouches[0].pageX-t.left,h=e.changedTouches[0].pageY+t.top,u)&&(c-=s.x,h-=s.y),n.y=1-Math.max(0,Math.min(1,h/l)),n.x=Math.max(0,Math.min(1,c/o)),n}function Fe(e){return n(e)&&3===e.nodeType}function Ve(e){for(;e.firstChild;)e.removeChild(e.firstChild);return e}function ze(e){return"function"==typeof e&&(e=e()),(Array.isArray(e)?e:[e]).map(e=>Te(e="function"==typeof e?e():e)||Fe(e)?e:"string"==typeof e&&/\S/.test(e)?document.createTextNode(e):void 0).filter(e=>e)}function qe(t,e){return ze(e).forEach(e=>t.appendChild(e)),t}function $e(e,t){return qe(Ve(e),t)}function Ue(e){return void 0===e.button&&void 0===e.buttons||0===e.button&&void 0===e.buttons||"mouseup"===e.type&&0===e.button&&0===e.buttons||0===e.button&&1===e.buttons}const Ke=Ce("querySelector"),We=Ce("querySelectorAll");function Xe(t,s){if(!t||!s)return"";if("function"!=typeof window.getComputedStyle)return"";{let e;try{e=window.getComputedStyle(t)}catch(e){return""}return e?e.getPropertyValue(s)||e[s]:""}}function Ge(i){[...document.styleSheets].forEach(t=>{try{var s=[...t.cssRules].map(e=>e.cssText).join(""),e=document.createElement("style");e.textContent=s,i.document.head.appendChild(e)}catch(e){s=document.createElement("link");s.rel="stylesheet",s.type=t.type,s.media=t.media.mediaText,s.href=t.href,i.document.head.appendChild(s)}})}var Ye=Object.freeze({__proto__:null,isReal:be,isEl:Te,isInFrame:ke,createEl:p,textContent:we,prependTo:Ee,hasClass:Se,addClass:xe,removeClass:je,toggleClass:Pe,setAttributes:Ie,getAttributes:Me,getAttribute:Ae,setAttribute:Oe,removeAttribute:Le,blockTextSelection:Ne,unblockTextSelection:De,getBoundingClientRect:Be,findPosition:Re,getPointerPosition:He,isTextNode:Fe,emptyEl:Ve,normalizeContent:ze,appendContent:qe,insertContent:$e,isSingleLeftClick:Ue,$:Ke,$$:We,computedStyle:Xe,copyStyleSheetsToWindow:Ge});let Qe=!1,Je;function Ze(){if(!1!==Je.options.autoSetup){var e=Array.prototype.slice.call(document.getElementsByTagName("video")),t=Array.prototype.slice.call(document.getElementsByTagName("audio")),s=Array.prototype.slice.call(document.getElementsByTagName("video-js")),i=e.concat(t,s);if(i&&0=i&&(s(...e),r=t)}}function pt(i,r,n,a=window){let o;function e(){const e=this,t=arguments;let s=function(){o=null,s=null,n||i.apply(e,t)};!o&&n&&i.apply(e,t),a.clearTimeout(o),o=a.setTimeout(s,r)}return e.cancel=()=>{a.clearTimeout(o),o=null},e}e=Object.freeze({__proto__:null,UPDATE_REFRESH_INTERVAL:30,bind_:f,throttle:r,debounce:pt});let gt;class s{on(e,t){var s=this.addEventListener;this.addEventListener=()=>{},v(this,e,t),this.addEventListener=s}off(e,t){_(this,e,t)}one(e,t){var s=this.addEventListener;this.addEventListener=()=>{},ct(this,e,t),this.addEventListener=s}any(e,t){var s=this.addEventListener;this.addEventListener=()=>{},dt(this,e,t),this.addEventListener=s}trigger(e){var t=e.type||e;e=at(e="string"==typeof e?{type:t}:e),this.allowedEvents_[t]&&this["on"+t]&&this["on"+t](e),ht(this,e)}queueTrigger(e){gt=gt||new Map;const t=e.type||e;let s=gt.get(this);s||(s=new Map,gt.set(this,s));var i=s.get(t),i=(s.delete(t),window.clearTimeout(i),window.setTimeout(()=>{s.delete(t),0===s.size&&(s=null,gt.delete(this)),this.trigger(e)},0));s.set(t,i)}}s.prototype.allowedEvents_={},s.prototype.addEventListener=s.prototype.on,s.prototype.removeEventListener=s.prototype.off,s.prototype.dispatchEvent=s.prototype.trigger;const mt=e=>"function"==typeof e.name?e.name():"string"==typeof e.name?e.name:e.name_||(e.constructor&&e.constructor.name?e.constructor.name:typeof e),vt=t=>t instanceof s||!!t.eventBusEl_&&["on","one","off","trigger"].every(e=>"function"==typeof t[e]),_t=e=>"string"==typeof e&&/\S/.test(e)||Array.isArray(e)&&!!e.length,ft=(e,t,s)=>{if(!e||!e.nodeName&&!vt(e))throw new Error(`Invalid target for ${mt(t)}#${s}; must be a DOM node or evented object.`)},yt=(e,t,s)=>{if(!_t(e))throw new Error(`Invalid event type for ${mt(t)}#${s}; must be a non-empty string or array.`)},bt=(e,t,s)=>{if("function"!=typeof e)throw new Error(`Invalid listener for ${mt(t)}#${s}; must be a function.`)},Tt=(e,t,s)=>{var i=t.length<3||t[0]===e||t[0]===e.eventBusEl_;let r,n,a;return i?(r=e.eventBusEl_,3<=t.length&&t.shift(),[n,a]=t):[r,n,a]=t,ft(r,e,s),yt(n,e,s),bt(a,e,s),a=f(e,a),{isTargetingSelf:i,target:r,type:n,listener:a}},kt=(e,t,s,i)=>{ft(e,e,t),e.nodeName?ut[t](e,s,i):e[t](s,i)},Ct={on(...e){const{isTargetingSelf:t,target:s,type:i,listener:r}=Tt(this,e,"on");if(kt(s,"on",i,r),!t){const n=()=>this.off(s,i,r);n.guid=r.guid;e=()=>this.off("dispose",n);e.guid=r.guid,kt(this,"on","dispose",n),kt(s,"on","dispose",e)}},one(...e){const{isTargetingSelf:t,target:s,type:i,listener:r}=Tt(this,e,"one");if(t)kt(s,"one",i,r);else{const n=(...e)=>{this.off(s,i,n),r.apply(null,e)};n.guid=r.guid,kt(s,"one",i,n)}},any(...e){const{isTargetingSelf:t,target:s,type:i,listener:r}=Tt(this,e,"any");if(t)kt(s,"any",i,r);else{const n=(...e)=>{this.off(s,i,n),r.apply(null,e)};n.guid=r.guid,kt(s,"any",i,n)}},off(e,t,s){!e||_t(e)?_(this.eventBusEl_,e,t):(e=e,t=t,ft(e,this,"off"),yt(t,this,"off"),bt(s,this,"off"),s=f(this,s),this.off("dispose",s),e.nodeName?(_(e,t,s),_(e,"dispose",s)):vt(e)&&(e.off(t,s),e.off("dispose",s)))},trigger(e,t){ft(this.eventBusEl_,this,"trigger");var s=e&&"string"!=typeof e?e.type:e;if(_t(s))return ht(this.eventBusEl_,e,t);throw new Error(`Invalid event type for ${mt(this)}#trigger; `+"must be a non-empty string or object with a type key that has a non-empty value.")}};function wt(e,t={}){t=t.eventBusKey;if(t){if(!e[t].nodeName)throw new Error(`The eventBusKey "${t}" does not refer to an element.`);e.eventBusEl_=e[t]}else e.eventBusEl_=p("span",{className:"vjs-event-bus"});Object.assign(e,Ct),e.eventedCallbacks&&e.eventedCallbacks.forEach(e=>{e()}),e.on("dispose",()=>{e.off(),[e,e.el_,e.eventBusEl_].forEach(function(e){e&&g.has(e)&&g.delete(e)}),window.setTimeout(()=>{e.eventBusEl_=null},0)})}const Et={state:{},setState(e){"function"==typeof e&&(e=e());let s;return W(e,(e,t)=>{this.state[t]!==e&&((s=s||{})[t]={from:this.state[t],to:e}),this.state[t]=e}),s&&vt(this)&&this.trigger({changes:s,type:"statechanged"}),s}};function St(e,t){Object.assign(e,Et),e.state=Object.assign({},e.state,t),"function"==typeof e.handleStateChanged&&vt(e)&&e.on("statechanged",e.handleStateChanged)}function xt(e){return"string"!=typeof e?e:e.replace(/./,e=>e.toLowerCase())}function y(e){return"string"!=typeof e?e:e.replace(/./,e=>e.toUpperCase())}function jt(e,t){return y(e)===y(t)}var Pt=Object.freeze({__proto__:null,toLowerCase:xt,toTitleCase:y,titleCaseEquals:jt}),It="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function Mt(e,t){return e(t={exports:{}},t.exports),t.exports}var a=Mt(function(e,t){function s(e){var t;return"number"==typeof(e=e&&"object"==typeof e&&(t=e.which||e.keyCode||e.charCode)?t:e)?o[e]:(t=String(e),i[t.toLowerCase()]||r[t.toLowerCase()]||(1===t.length?t.charCodeAt(0):void 0))}s.isEventKey=function(e,t){if(e&&"object"==typeof e){e=e.which||e.keyCode||e.charCode;if(null!=e)if("string"==typeof t){var s=i[t.toLowerCase()];if(s)return s===e;if(s=r[t.toLowerCase()])return s===e}else if("number"==typeof t)return t===e;return!1}};for(var i=(t=e.exports=s).code=t.codes={backspace:8,tab:9,enter:13,shift:16,ctrl:17,alt:18,"pause/break":19,"caps lock":20,esc:27,space:32,"page up":33,"page down":34,end:35,home:36,left:37,up:38,right:39,down:40,insert:45,delete:46,command:91,"left command":91,"right command":93,"numpad *":106,"numpad +":107,"numpad -":109,"numpad .":110,"numpad /":111,"num lock":144,"scroll lock":145,"my computer":182,"my calculator":183,";":186,"=":187,",":188,"-":189,".":190,"/":191,"`":192,"[":219,"\\":220,"]":221,"'":222},r=t.aliases={windows:91,"⇧":16,"⌥":18,"⌃":17,"⌘":91,ctl:17,control:17,option:18,pause:19,break:19,caps:20,return:13,escape:27,spc:32,spacebar:32,pgup:33,pgdn:34,ins:45,del:46,cmd:91},n=97;n<123;n++)i[String.fromCharCode(n)]=n-32;for(var n=48;n<58;n++)i[n-48]=n;for(n=1;n<13;n++)i["f"+n]=n+111;for(n=0;n<10;n++)i["numpad "+n]=n+96;var a,o=t.names=t.title={};for(n in i)o[i[n]]=n;for(a in r)i[a]=r[a]});a.code,a.codes,a.aliases,a.names,a.title;class b{constructor(e,t,s){!e&&this.play?this.player_=e=this:this.player_=e,this.isDisposed_=!1,this.parentComponent_=null,this.options_=h({},this.options_),t=this.options_=h(this.options_,t),this.id_=t.id||t.el&&t.el.id,this.id_||(e=e&&e.id&&e.id()||"no_player",this.id_=e+"_component_"+m++),this.name_=t.name||null,t.el?this.el_=t.el:!1!==t.createEl&&(this.el_=this.createEl()),t.className&&this.el_&&t.className.split(" ").forEach(e=>this.addClass(e)),["on","off","one","any","trigger"].forEach(e=>{this[e]=void 0}),!1!==t.evented&&(wt(this,{eventBusKey:this.el_?"el_":null}),this.handleLanguagechange=this.handleLanguagechange.bind(this),this.on(this.player_,"languagechange",this.handleLanguagechange)),St(this,this.constructor.defaultState),this.children_=[],this.childIndex_={},this.childNameIndex_={},this.setTimeoutIds_=new Set,this.setIntervalIds_=new Set,this.rafIds_=new Set,this.namedRafs_=new Map,(this.clearingTimersOnDispose_=!1)!==t.initChildren&&this.initChildren(),this.ready(s),!1!==t.reportTouchActivity&&this.enableTouchActivity()}on(e,t){}off(e,t){}one(e,t){}any(e,t){}trigger(e,t){}dispose(e={}){if(!this.isDisposed_){if(this.readyQueue_&&(this.readyQueue_.length=0),this.trigger({type:"dispose",bubbles:!1}),this.isDisposed_=!0,this.children_)for(let e=this.children_.length-1;0<=e;e--)this.children_[e].dispose&&this.children_[e].dispose();this.children_=null,this.childIndex_=null,this.childNameIndex_=null,this.parentComponent_=null,this.el_&&(this.el_.parentNode&&(e.restoreEl?this.el_.parentNode.replaceChild(e.restoreEl,this.el_):this.el_.parentNode.removeChild(this.el_)),this.el_=null),this.player_=null}}isDisposed(){return Boolean(this.isDisposed_)}player(){return this.player_}options(e){return e&&(this.options_=h(this.options_,e)),this.options_}el(){return this.el_}createEl(e,t,s){return p(e,t,s)}localize(e,i,t=e){var s=this.player_.language&&this.player_.language(),r=this.player_.languages&&this.player_.languages(),n=r&&r[s],s=s&&s.split("-")[0],r=r&&r[s];let a=t;return n&&n[e]?a=n[e]:r&&r[e]&&(a=r[e]),a=i?a.replace(/\{(\d+)\}/g,function(e,t){t=i[t-1];let s="undefined"==typeof t?e:t;return s}):a}handleLanguagechange(){}contentEl(){return this.contentEl_||this.el_}id(){return this.id_}name(){return this.name_}children(){return this.children_}getChildById(e){return this.childIndex_[e]}getChild(e){if(e)return this.childNameIndex_[e]}getDescendant(...t){t=t.reduce((e,t)=>e.concat(t),[]);let s=this;for(let e=0;e{let t,s;return s="string"==typeof e?(t=e,i[t]||this.options_[t]||{}):(t=e.name,e),{name:t,opts:s}}).filter(e=>{e=b.getComponent(e.opts.componentClass||y(e.name));return e&&!t.isTech(e)}).forEach(e=>{var t=e.name;let s=e.opts;!1!==(s=void 0!==r[t]?r[t]:s)&&((s=!0===s?{}:s).playerOptions=this.options_.playerOptions,e=this.addChild(t,s))&&(this[t]=e)})}}buildCSSClass(){return""}ready(e,t=!1){e&&(this.isReady_?t?e.call(this):this.setTimeout(e,1):(this.readyQueue_=this.readyQueue_||[],this.readyQueue_.push(e)))}triggerReady(){this.isReady_=!0,this.setTimeout(function(){var e=this.readyQueue_;this.readyQueue_=[],e&&0{this.setTimeoutIds_.has(s)&&this.setTimeoutIds_.delete(s),e()},t),this.setTimeoutIds_.add(s),s}clearTimeout(e){return this.setTimeoutIds_.has(e)&&(this.setTimeoutIds_.delete(e),window.clearTimeout(e)),e}setInterval(e,t){e=f(this,e),this.clearTimersOnDispose_();e=window.setInterval(e,t);return this.setIntervalIds_.add(e),e}clearInterval(e){return this.setIntervalIds_.has(e)&&(this.setIntervalIds_.delete(e),window.clearInterval(e)),e}requestAnimationFrame(e){var t;return this.clearTimersOnDispose_(),e=f(this,e),t=window.requestAnimationFrame(()=>{this.rafIds_.has(t)&&this.rafIds_.delete(t),e()}),this.rafIds_.add(t),t}requestNamedAnimationFrame(e,t){var s;if(!this.namedRafs_.has(e))return this.clearTimersOnDispose_(),t=f(this,t),s=this.requestAnimationFrame(()=>{t(),this.namedRafs_.has(e)&&this.namedRafs_.delete(e)}),this.namedRafs_.set(e,s),e}cancelNamedAnimationFrame(e){this.namedRafs_.has(e)&&(this.cancelAnimationFrame(this.namedRafs_.get(e)),this.namedRafs_.delete(e))}cancelAnimationFrame(e){return this.rafIds_.has(e)&&(this.rafIds_.delete(e),window.cancelAnimationFrame(e)),e}clearTimersOnDispose_(){this.clearingTimersOnDispose_||(this.clearingTimersOnDispose_=!0,this.one("dispose",()=>{[["namedRafs_","cancelNamedAnimationFrame"],["rafIds_","cancelAnimationFrame"],["setTimeoutIds_","clearTimeout"],["setIntervalIds_","clearInterval"]].forEach(([e,s])=>{this[e].forEach((e,t)=>this[s](t))}),this.clearingTimersOnDispose_=!1}))}static registerComponent(t,e){if("string"!=typeof t||!t)throw new Error(`Illegal component name, "${t}"; must be a non-empty string.`);var s=b.getComponent("Tech"),s=s&&s.isTech(e),i=b===e||b.prototype.isPrototypeOf(e.prototype);if(s||!i){let e;throw e=s?"techs must be registered using Tech.registerTech()":"must be a Component subclass",new Error(`Illegal component, "${t}"; ${e}.`)}t=y(t),b.components_||(b.components_={});i=b.getComponent("Player");if("Player"===t&&i&&i.players){const r=i.players;s=Object.keys(r);if(r&&0r[e]).every(Boolean))throw new Error("Can not register Player component after player has been created.")}return b.components_[t]=e,b.components_[xt(t)]=e}static getComponent(e){if(e&&b.components_)return b.components_[e]}}function At(e,t,s,i){var r=i,n=s.length-1;if("number"!=typeof r||r<0||n(e||[]).values()),t}function T(e,t){return Array.isArray(e)?Ot(e):void 0===e||void 0===t?Ot():Ot([[e,t]])}b.registerComponent("Component",b);function Lt(e,t){e=e<0?0:e;let s=Math.floor(e%60),i=Math.floor(e/60%60),r=Math.floor(e/3600);var n=Math.floor(t/60%60),t=Math.floor(t/3600);return r=0<(r=!isNaN(e)&&e!==1/0?r:i=s="-")||0s&&(n=s),i+=n-r;return i/s}function i(e){if(e instanceof i)return e;"number"==typeof e?this.code=e:"string"==typeof e?this.message=e:n(e)&&("number"==typeof e.code&&(this.code=e.code),Object.assign(this,e)),this.message||(this.message=i.defaultMessages[this.code]||"")}i.prototype.code=0,i.prototype.message="",i.prototype.status=null,i.prototype.metadata=null,i.errorTypes=["MEDIA_ERR_CUSTOM","MEDIA_ERR_ABORTED","MEDIA_ERR_NETWORK","MEDIA_ERR_DECODE","MEDIA_ERR_SRC_NOT_SUPPORTED","MEDIA_ERR_ENCRYPTED"],i.defaultMessages={1:"You aborted the media playback",2:"A network error caused the media download to fail part-way.",3:"The media playback was aborted due to a corruption problem or because the media used features your browser did not support.",4:"The media could not be loaded, either because the server or network failed or because the format is not supported.",5:"The media is encrypted and we do not have the keys to decrypt it."},i.MEDIA_ERR_CUSTOM=0,i.prototype.MEDIA_ERR_CUSTOM=0,i.MEDIA_ERR_ABORTED=1,i.prototype.MEDIA_ERR_ABORTED=1,i.MEDIA_ERR_NETWORK=2,i.prototype.MEDIA_ERR_NETWORK=2,i.MEDIA_ERR_DECODE=3,i.prototype.MEDIA_ERR_DECODE=3,i.MEDIA_ERR_SRC_NOT_SUPPORTED=4,i.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED=4,i.MEDIA_ERR_ENCRYPTED=5,i.prototype.MEDIA_ERR_ENCRYPTED=5;var Vt=function(e,t){var s,i=null;try{s=JSON.parse(e,t)}catch(e){i=e}return[i,s]};function zt(e){return null!=e&&"function"==typeof e.then}function k(e){zt(e)&&e.then(null,e=>{})}function qt(i){return["kind","label","language","id","inBandMetadataTrackDispatchType","mode","src"].reduce((e,t,s)=>(i[t]&&(e[t]=i[t]),e),{cues:i.cues&&Array.prototype.map.call(i.cues,function(e){return{startTime:e.startTime,endTime:e.endTime,text:e.text,id:e.id}})})}var $t=function(e){var t=e.$$("track");const s=Array.prototype.map.call(t,e=>e.track);return Array.prototype.map.call(t,function(e){var t=qt(e.track);return e.src&&(t.src=e.src),t}).concat(Array.prototype.filter.call(e.textTracks(),function(e){return-1===s.indexOf(e)}).map(qt))},Ut=function(e,s){return e.forEach(function(e){const t=s.addRemoteTextTrack(e).track;!e.src&&e.cues&&e.cues.forEach(e=>t.addCue(e))}),s.textTracks()};qt;const Kt="vjs-modal-dialog";class Wt extends b{constructor(e,t){super(e,t),this.handleKeyDown_=e=>this.handleKeyDown(e),this.close_=e=>this.close(e),this.opened_=this.hasBeenOpened_=this.hasBeenFilled_=!1,this.closeable(!this.options_.uncloseable),this.content(this.options_.content),this.contentEl_=p("div",{className:Kt+"-content"},{role:"document"}),this.descEl_=p("p",{className:Kt+"-description vjs-control-text",id:this.el().getAttribute("aria-describedby")}),we(this.descEl_,this.description()),this.el_.appendChild(this.descEl_),this.el_.appendChild(this.contentEl_)}createEl(){return super.createEl("div",{className:this.buildCSSClass(),tabIndex:-1},{"aria-describedby":this.id()+"_description","aria-hidden":"true","aria-label":this.label(),role:"dialog","aria-live":"polite"})}dispose(){this.contentEl_=null,this.descEl_=null,this.previouslyActiveEl_=null,super.dispose()}buildCSSClass(){return Kt+" vjs-hidden "+super.buildCSSClass()}label(){return this.localize(this.options_.label||"Modal Window")}description(){let e=this.options_.description||this.localize("This is a modal window.");return this.closeable()&&(e+=" "+this.localize("This modal can be closed by pressing the Escape key or activating the close button.")),e}open(){var e;this.opened_?this.options_.fillAlways&&this.fill():(e=this.player(),this.trigger("beforemodalopen"),this.opened_=!0,!this.options_.fillAlways&&(this.hasBeenOpened_||this.hasBeenFilled_)||this.fill(),this.wasPlaying_=!e.paused(),this.options_.pauseOnOpen&&this.wasPlaying_&&e.pause(),this.on("keydown",this.handleKeyDown_),this.hadControls_=e.controls(),e.controls(!1),this.show(),this.conditionalFocus_(),this.el().setAttribute("aria-hidden","false"),this.trigger("modalopen"),this.hasBeenOpened_=!0)}opened(e){return"boolean"==typeof e&&this[e?"open":"close"](),this.opened_}close(){var e;this.opened_&&(e=this.player(),this.trigger("beforemodalclose"),this.opened_=!1,this.wasPlaying_&&this.options_.pauseOnOpen&&e.play(),this.off("keydown",this.handleKeyDown_),this.hadControls_&&e.controls(!0),this.hide(),this.el().setAttribute("aria-hidden","true"),this.trigger("modalclose"),this.conditionalBlur_(),this.options_.temporary)&&this.dispose()}closeable(t){if("boolean"==typeof t){var s,t=this.closeable_=!!t;let e=this.getChild("closeButton");t&&!e&&(s=this.contentEl_,this.contentEl_=this.el_,e=this.addChild("closeButton",{controlText:"Close Modal Dialog"}),this.contentEl_=s,this.on(e,"close",this.close_)),!t&&e&&(this.off(e,"close",this.close_),this.removeChild(e),e.dispose())}return this.closeable_}fill(){this.fillWith(this.content())}fillWith(e){var t=this.contentEl(),s=t.parentNode,i=t.nextSibling,e=(this.trigger("beforemodalfill"),this.hasBeenFilled_=!0,s.removeChild(t),this.empty(),$e(t,e),this.trigger("modalfill"),i?s.insertBefore(t,i):s.appendChild(t),this.getChild("closeButton"));e&&s.appendChild(e.el_)}empty(){this.trigger("beforemodalempty"),Ve(this.contentEl()),this.trigger("modalempty")}content(e){return"undefined"!=typeof e&&(this.content_=e),this.content_}conditionalFocus_(){var e=document.activeElement,t=this.player_.el_;this.previouslyActiveEl_=null,!t.contains(e)&&t!==e||(this.previouslyActiveEl_=e,this.focus())}conditionalBlur_(){this.previouslyActiveEl_&&(this.previouslyActiveEl_.focus(),this.previouslyActiveEl_=null)}handleKeyDown(e){if(e.stopPropagation(),a.isEventKey(e,"Escape")&&this.closeable())e.preventDefault(),this.close();else if(a.isEventKey(e,"Tab")){var s=this.focusableEls_(),i=this.el_.querySelector(":focus");let t;for(let e=0;e(e instanceof window.HTMLAnchorElement||e instanceof window.HTMLAreaElement)&&e.hasAttribute("href")||(e instanceof window.HTMLInputElement||e instanceof window.HTMLSelectElement||e instanceof window.HTMLTextAreaElement||e instanceof window.HTMLButtonElement)&&!e.hasAttribute("disabled")||e instanceof window.HTMLIFrameElement||e instanceof window.HTMLObjectElement||e instanceof window.HTMLEmbedElement||e.hasAttribute("tabindex")&&-1!==e.getAttribute("tabindex")||e.hasAttribute("contenteditable"))}}Wt.prototype.options_={pauseOnOpen:!0,temporary:!0},b.registerComponent("ModalDialog",Wt);class Xt extends s{constructor(t=[]){super(),this.tracks_=[],Object.defineProperty(this,"length",{get(){return this.tracks_.length}});for(let e=0;e{this.trigger({track:e,type:"labelchange",target:this})},vt(e)&&e.addEventListener("labelchange",e.labelchange_)}removeTrack(s){let i;for(let e=0,t=this.length;ethis.queueTrigger("change")),this.triggerSelectedlanguagechange||(this.triggerSelectedlanguagechange_=()=>this.trigger("selectedlanguagechange")),e.addEventListener("modechange",this.queueChange_);-1===["metadata","chapters"].indexOf(e.kind)&&e.addEventListener("modechange",this.triggerSelectedlanguagechange_)}removeTrack(e){super.removeTrack(e),e.removeEventListener&&(this.queueChange_&&e.removeEventListener("modechange",this.queueChange_),this.selectedlanguagechange_)&&e.removeEventListener("modechange",this.triggerSelectedlanguagechange_)}}class Jt{constructor(e){Jt.prototype.setCues_.call(this,e),Object.defineProperty(this,"length",{get(){return this.length_}})}setCues_(e){var t=this.length||0;let s=0;function i(e){""+e in this||Object.defineProperty(this,""+e,{get(){return this.cues_[e]}})}var r=e.length;this.cues_=e,this.length_=e.length;if(tl.error(e)),window.console)&&window.console.groupEnd&&window.console.groupEnd(),s.flush()}function Ts(e,i){var t={uri:e};(e=as(e))&&(t.cors=e),(e="use-credentials"===i.tech_.crossOrigin())&&(t.withCredentials=e),ps(t,f(this,function(e,t,s){if(e)return l.error(e,t);i.loaded_=!0,"function"!=typeof window.WebVTT?i.tech_&&i.tech_.any(["vttjsloaded","vttjserror"],e=>{if("vttjserror"!==e.type)return bs(s,i);l.error("vttjs failed to load, stopping trying to process "+i.src)}):bs(s,i)}))}class ks extends is{constructor(e={}){if(!e.tech)throw new Error("A tech was not provided.");e=h(e,{kind:ts[e.kind]||"subtitles",language:e.language||e.srclang||""});let t=ss[e.mode]||"disabled";const s=e.default,i=("metadata"!==e.kind&&"chapters"!==e.kind||(t="hidden"),super(e),this.tech_=e.tech,this.cues_=[],this.activeCues_=[],this.preload_=!1!==this.tech_.preloadTextTracks,new Jt(this.cues_)),n=new Jt(this.activeCues_);let a=!1;this.timeupdateHandler=f(this,function(e={}){this.tech_.isDisposed()||(this.tech_.isReady_&&(this.activeCues=this.activeCues,a)&&(this.trigger("cuechange"),a=!1),"timeupdate"!==e.type&&(this.rvf_=this.tech_.requestVideoFrameCallback(this.timeupdateHandler)))});this.tech_.one("dispose",()=>{this.stopTracking()}),"disabled"!==t&&this.startTracking(),Object.defineProperties(this,{default:{get(){return s},set(){}},mode:{get(){return t},set(e){ss[e]&&t!==e&&(t=e,this.preload_||"disabled"===t||0!==this.cues.length||Ts(this.src,this),this.stopTracking(),"disabled"!==t&&this.startTracking(),this.trigger("modechange"))}},cues:{get(){return this.loaded_?i:null},set(){}},activeCues:{get(){if(!this.loaded_)return null;if(0!==this.cues.length){var s=this.tech_.currentTime(),i=[];for(let e=0,t=this.cues.length;e=s&&i.push(r)}if(a=!1,i.length!==this.activeCues_.length)a=!0;else for(let e=0;e{t=Es.LOADED,this.trigger({type:"load",target:this})})}}Es.prototype.allowedEvents_={load:"load"},Es.NONE=0,Es.LOADING=1,Es.LOADED=2,Es.ERROR=3;const w={audio:{ListClass:class extends Xt{constructor(t=[]){for(let e=t.length-1;0<=e;e--)if(t[e].enabled){Gt(t,t[e]);break}super(t),this.changing_=!1}addTrack(e){e.enabled&&Gt(this,e),super.addTrack(e),e.addEventListener&&(e.enabledChange_=()=>{this.changing_||(this.changing_=!0,Gt(this,e),this.changing_=!1,this.trigger("change"))},e.addEventListener("enabledchange",e.enabledChange_))}removeTrack(e){super.removeTrack(e),e.removeEventListener&&e.enabledChange_&&(e.removeEventListener("enabledchange",e.enabledChange_),e.enabledChange_=null)}},TrackClass:Cs,capitalName:"Audio"},video:{ListClass:class extends Xt{constructor(t=[]){for(let e=t.length-1;0<=e;e--)if(t[e].selected){Yt(t,t[e]);break}super(t),this.changing_=!1,Object.defineProperty(this,"selectedIndex",{get(){for(let e=0;e{this.changing_||(this.changing_=!0,Yt(this,e),this.changing_=!1,this.trigger("change"))},e.addEventListener("selectedchange",e.selectedChange_))}removeTrack(e){super.removeTrack(e),e.removeEventListener&&e.selectedChange_&&(e.removeEventListener("selectedchange",e.selectedChange_),e.selectedChange_=null)}},TrackClass:ws,capitalName:"Video"},text:{ListClass:Qt,TrackClass:ks,capitalName:"Text"}},Ss=(Object.keys(w).forEach(function(e){w[e].getterName=e+"Tracks",w[e].privateName=e+"Tracks_"}),{remoteText:{ListClass:Qt,TrackClass:ks,capitalName:"RemoteText",getterName:"remoteTextTracks",privateName:"remoteTextTracks_"},remoteTextEl:{ListClass:class{constructor(s=[]){this.trackElements_=[],Object.defineProperty(this,"length",{get(){return this.trackElements_.length}});for(let e=0,t=s.length;e]*>?)?/))[1]||o[2],t=t.substr(o.length),o):null);)"<"===o[0]?"/"===o[1]?c.length&&c[c.length-1]===o.substr(2).replace(">","")&&(c.pop(),h=h.parentNode):(i=Is(o.substr(1,o.length-2)))?(s=e.document.createProcessingInstruction("timestamp",i),h.appendChild(s)):(i=o.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/))&&(r=i[1],n=i[3],a=void 0,a=Ls[r],s=a?(a=e.document.createElement(a),(r=Ds[r])&&n&&(a[r]=n.trim()),a):null)&&(r=h,Bs[(n=s).localName]&&Bs[n.localName]!==r.localName||(i[2]&&((a=i[2].split(".")).forEach(function(e){var t=/^bg_/.test(e),e=t?e.slice(3):e;Ns.hasOwnProperty(e)&&(e=Ns[e],s.style[t?"background-color":"color"]=e)}),s.className=a.join(" ")),c.push(i[1]),h.appendChild(s),h=s)):h.appendChild(e.document.createTextNode((n=o,Os.innerHTML=n,n=Os.textContent,Os.textContent="",n)));return l}var Hs=[[1470,1470],[1472,1472],[1475,1475],[1478,1478],[1488,1514],[1520,1524],[1544,1544],[1547,1547],[1549,1549],[1563,1563],[1566,1610],[1645,1647],[1649,1749],[1765,1766],[1774,1775],[1786,1805],[1807,1808],[1810,1839],[1869,1957],[1969,1969],[1984,2026],[2036,2037],[2042,2042],[2048,2069],[2074,2074],[2084,2084],[2088,2088],[2096,2110],[2112,2136],[2142,2142],[2208,2208],[2210,2220],[8207,8207],[64285,64285],[64287,64296],[64298,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64449],[64467,64829],[64848,64911],[64914,64967],[65008,65020],[65136,65140],[65142,65276],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67679],[67840,67867],[67872,67897],[67903,67903],[67968,68023],[68030,68031],[68096,68096],[68112,68115],[68117,68119],[68121,68147],[68160,68167],[68176,68184],[68192,68223],[68352,68405],[68416,68437],[68440,68466],[68472,68479],[68608,68680],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[1114109,1114109]];function Fs(e){var t=[],s="";if(e&&e.childNodes)for(n(t,e);s=function e(t){var s,i,r;return t&&t.length?(i=(s=t.pop()).textContent||s.innerText)?(r=i.match(/^.*(\n|\r)/))?r[t.length=0]:i:"ruby"===s.tagName?e(t):s.childNodes?(n(t,s),e(t)):void 0:null}(t);)for(var i=0;i=s[0]&&e<=s[1])return 1}}(s.charCodeAt(i)))return"rtl";return"ltr";function n(e,t){for(var s=t.childNodes.length-1;0<=s;s--)e.push(t.childNodes[s])}}function Vs(){}function zs(e,t,s){Vs.call(this),this.cue=t,this.cueDiv=Rs(e,t.text);var i={color:"rgba(255, 255, 255, 1)",backgroundColor:"rgba(0, 0, 0, 0.8)",position:"relative",left:0,right:0,top:0,bottom:0,display:"inline",writingMode:""===t.vertical?"horizontal-tb":"lr"===t.vertical?"vertical-lr":"vertical-rl",unicodeBidi:"plaintext"},r=(this.applyStyles(i,this.cueDiv),this.div=e.document.createElement("div"),i={direction:Fs(this.cueDiv),writingMode:""===t.vertical?"horizontal-tb":"lr"===t.vertical?"vertical-lr":"vertical-rl",unicodeBidi:"plaintext",textAlign:"middle"===t.align?"center":t.align,font:s.font,whiteSpace:"pre-line",position:"absolute"},this.applyStyles(i),this.div.appendChild(this.cueDiv),0);switch(t.positionAlign){case"start":case"line-left":r=t.position;break;case"center":r=t.position-t.size/2;break;case"end":case"line-right":r=t.position-t.size}""===t.vertical?this.applyStyles({left:this.formatStyle(r,"%"),width:this.formatStyle(t.size,"%")}):this.applyStyles({top:this.formatStyle(r,"%"),height:this.formatStyle(t.size,"%")}),this.move=function(e){this.applyStyles({top:this.formatStyle(e.top,"px"),bottom:this.formatStyle(e.bottom,"px"),left:this.formatStyle(e.left,"px"),right:this.formatStyle(e.right,"px"),height:this.formatStyle(e.height,"px"),width:this.formatStyle(e.width,"px")})}}function x(e){var t,s,i,r;e.div&&(t=e.div.offsetHeight,s=e.div.offsetWidth,i=e.div.offsetTop,r=(r=(r=e.div.childNodes)&&r[0])&&r.getClientRects&&r.getClientRects(),e=e.div.getBoundingClientRect(),r=r?Math.max(r[0]&&r[0].height||0,e.height/r.length):0),this.left=e.left,this.right=e.right,this.top=e.top||i,this.height=e.height||t,this.bottom=e.bottom||i+(e.height||t),this.width=e.width||s,this.lineHeight=void 0!==r?r:e.lineHeight}function qs(e,t,o,l){var s,i=new x(t),r=t.cue,n=function(e){if("number"==typeof e.line&&(e.snapToLines||0<=e.line&&e.line<=100))return e.line;if(!e.track||!e.track.textTrackList||!e.track.textTrackList.mediaElement)return-1;for(var t=e.track,s=t.textTrackList,i=0,r=0;rd&&(c=c<0?-1:1,c*=Math.ceil(d/h)*h),n<0&&(c+=""===r.vertical?o.height:o.width,a=a.reverse()),i.move(u,c)}else{var p=i.lineHeight/o.height*100;switch(r.lineAlign){case"center":n-=p/2;break;case"end":n-=p}switch(r.vertical){case"":t.applyStyles({top:t.formatStyle(n,"%")});break;case"rl":t.applyStyles({left:t.formatStyle(n,"%")});break;case"lr":t.applyStyles({right:t.formatStyle(n,"%")})}a=["+y","-x","+x","-y"],i=new x(t)}d=function(e,t){for(var s,i=new x(e),r=1,n=0;ne.left&&this.tope.top},x.prototype.overlapsAny=function(e){for(var t=0;t=e.top&&this.bottom<=e.bottom&&this.left>=e.left&&this.right<=e.right},x.prototype.overlapsOppositeAxis=function(e,t){switch(t){case"+x":return this.lefte.right;case"+y":return this.tope.bottom}},x.prototype.intersectPercentage=function(e){return Math.max(0,Math.min(this.right,e.right)-Math.max(this.left,e.left))*Math.max(0,Math.min(this.bottom,e.bottom)-Math.max(this.top,e.top))/(this.height*this.width)},x.prototype.toCSSCompatValues=function(e){return{top:this.top-e.top,bottom:e.bottom-this.bottom,left:this.left-e.left,right:e.right-this.right,height:this.height,width:this.width}},x.getSimpleBoxPosition=function(e){var t=e.div?e.div.offsetHeight:e.tagName?e.offsetHeight:0,s=e.div?e.div.offsetWidth:e.tagName?e.offsetWidth:0,i=e.div?e.div.offsetTop:e.tagName?e.offsetTop:0;return{left:(e=e.div?e.div.getBoundingClientRect():e.tagName?e.getBoundingClientRect():e).left,right:e.right,top:e.top||i,height:e.height||t,bottom:e.bottom||i+(e.height||t),width:e.width||s}},$s.StringDecoder=function(){return{decode:function(e){if(!e)return"";if("string"!=typeof e)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(e))}}},$s.convertCueToDOMTree=function(e,t){return e&&t?Rs(e,t):null};$s.processCues=function(e,t,s){if(!e||!t||!s)return null;for(;s.firstChild;)s.removeChild(s.firstChild);var i=e.document.createElement("div");if(i.style.position="absolute",i.style.left="0",i.style.right="0",i.style.top="0",i.style.bottom="0",i.style.margin="1.5%",s.appendChild(i),function(e){for(var t=0;tthis.onDurationChange(e),this.trackProgress_=e=>this.trackProgress(e),this.trackCurrentTime_=e=>this.trackCurrentTime(e),this.stopTrackingCurrentTime_=e=>this.stopTrackingCurrentTime(e),this.disposeSourceHandler_=e=>this.disposeSourceHandler(e),this.queuedHanders_=new Set,this.hasStarted_=!1,this.on("playing",function(){this.hasStarted_=!0}),this.on("loadstart",function(){this.hasStarted_=!1}),E.names.forEach(e=>{e=E[e];t&&t[e.getterName]&&(this[e.privateName]=t[e.getterName])}),this.featuresProgressEvents||this.manualProgressOn(),this.featuresTimeupdateEvents||this.manualTimeUpdatesOn(),["Text","Audio","Video"].forEach(e=>{!1===t[`native${e}Tracks`]&&(this[`featuresNative${e}Tracks`]=!1)}),!1===t.nativeCaptions||!1===t.nativeTextTracks?this.featuresNativeTextTracks=!1:!0!==t.nativeCaptions&&!0!==t.nativeTextTracks||(this.featuresNativeTextTracks=!0),this.featuresNativeTextTracks||this.emulateTextTracks(),this.preloadTextTracks=!1!==t.preloadTextTracks,this.autoRemoteTextTracks_=new E.text.ListClass,this.initTrackListeners(),t.nativeControlsForTouch||this.emitTapEvents(),this.constructor&&(this.name_=this.constructor.name||"Unknown Tech")}triggerSourceset(e){this.isReady_||this.one("ready",()=>this.setTimeout(()=>this.triggerSourceset(e),1)),this.trigger({src:e,type:"sourceset"})}manualProgressOn(){this.on("durationchange",this.onDurationChange_),this.manualProgress=!0,this.one("ready",this.trackProgress_)}manualProgressOff(){this.manualProgress=!1,this.stopTrackingProgress(),this.off("durationchange",this.onDurationChange_)}trackProgress(e){this.stopTrackingProgress(),this.progressInterval=this.setInterval(f(this,function(){var e=this.bufferedPercent();this.bufferedPercent_!==e&&this.trigger("progress"),1===(this.bufferedPercent_=e)&&this.stopTrackingProgress()}),500)}onDurationChange(e){this.duration_=this.duration()}buffered(){return T(0,0)}bufferedPercent(){return Ft(this.buffered(),this.duration_)}stopTrackingProgress(){this.clearInterval(this.progressInterval)}manualTimeUpdatesOn(){this.manualTimeUpdates=!0,this.on("play",this.trackCurrentTime_),this.on("pause",this.stopTrackingCurrentTime_)}manualTimeUpdatesOff(){this.manualTimeUpdates=!1,this.stopTrackingCurrentTime(),this.off("play",this.trackCurrentTime_),this.off("pause",this.stopTrackingCurrentTime_)}trackCurrentTime(){this.currentTimeInterval&&this.stopTrackingCurrentTime(),this.currentTimeInterval=this.setInterval(function(){this.trigger({type:"timeupdate",target:this,manuallyTriggered:!0})},250)}stopTrackingCurrentTime(){this.clearInterval(this.currentTimeInterval),this.trigger({type:"timeupdate",target:this,manuallyTriggered:!0})}dispose(){this.clearTracks(w.names),this.manualProgress&&this.manualProgressOff(),this.manualTimeUpdates&&this.manualTimeUpdatesOff(),super.dispose()}clearTracks(e){(e=[].concat(e)).forEach(e=>{var t=this[e+"Tracks"]()||[];let s=t.length;for(;s--;){var i=t[s];"text"===e&&this.removeRemoteTextTrack(i),t.removeTrack(i)}})}cleanupAutoTextTracks(){var e=this.autoRemoteTextTracks_||[];let t=e.length;for(;t--;){var s=e[t];this.removeRemoteTextTrack(s)}}reset(){}crossOrigin(){}setCrossOrigin(){}error(e){return void 0!==e&&(this.error_=new i(e),this.trigger("error")),this.error_}played(){return this.hasStarted_?T(0,0):T()}play(){}setScrubbing(e){}scrubbing(){}setCurrentTime(e){this.manualTimeUpdates&&this.trigger({type:"timeupdate",target:this,manuallyTriggered:!0})}initTrackListeners(){w.names.forEach(e=>{var t=w[e];const s=()=>{this.trigger(e+"trackchange")},i=this[t.getterName]();i.addEventListener("removetrack",s),i.addEventListener("addtrack",s),this.on("dispose",()=>{i.removeEventListener("removetrack",s),i.removeEventListener("addtrack",s)})})}addWebVttScript_(){if(!window.WebVTT)if(document.body.contains(this.el()))if(!this.options_["vtt.js"]&&G(ei)&&0{this.trigger("vttjsloaded")},e.onerror=()=>{this.trigger("vttjserror")},this.on("dispose",()=>{e.onload=null,e.onerror=null}),window.WebVTT=!0,this.el().parentNode.appendChild(e)}else this.ready(this.addWebVttScript_)}emulateTextTracks(){const s=this.textTracks(),e=this.remoteTextTracks(),t=e=>s.addTrack(e.track),i=e=>s.removeTrack(e.track),r=(e.on("addtrack",t),e.on("removetrack",i),this.addWebVttScript_(),()=>this.trigger("texttrackchange")),n=()=>{r();for(let e=0;ethis.autoRemoteTextTracks_.addTrack(s.track)),s}removeRemoteTextTrack(e){var t=this.remoteTextTrackEls().getTrackElementByTrack_(e);this.remoteTextTrackEls().removeTrackElement_(t),this.remoteTextTracks().removeTrack(e),this.autoRemoteTextTracks_.removeTrack(e)}getVideoPlaybackQuality(){return{}}requestPictureInPicture(){return Promise.reject()}disablePictureInPicture(){return!0}setDisablePictureInPicture(){}requestVideoFrameCallback(e){const t=m++;return!this.isReady_||this.paused()?(this.queuedHanders_.add(t),this.one("playing",()=>{this.queuedHanders_.has(t)&&(this.queuedHanders_.delete(t),e())})):this.requestNamedAnimationFrame(t,e),t}cancelVideoFrameCallback(e){this.queuedHanders_.has(e)?this.queuedHanders_.delete(e):this.cancelNamedAnimationFrame(e)}setPoster(){}playsinline(){}setPlaysinline(){}overrideNativeAudioTracks(e){}overrideNativeVideoTracks(e){}canPlayType(e){return""}static canPlayType(e){return""}static canPlaySource(e,t){return j.canPlayType(e.type)}static isTech(e){return e.prototype instanceof j||e instanceof j||e===j}static registerTech(e,t){if(j.techs_||(j.techs_={}),!j.isTech(t))throw new Error(`Tech ${e} must be a Tech`);if(!j.canPlayType)throw new Error("Techs must have a static canPlayType method on them");if(j.canPlaySource)return e=y(e),j.techs_[e]=t,j.techs_[xt(e)]=t,"Tech"!==e&&j.defaultTechOrder_.push(e),t;throw new Error("Techs must have a static canPlaySource method on them")}static getTech(e){if(e)return j.techs_&&j.techs_[e]?j.techs_[e]:(e=y(e),window&&window.videojs&&window.videojs[e]?(l.warn(`The ${e} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`),window.videojs[e]):void 0)}}E.names.forEach(function(e){const t=E[e];j.prototype[t.getterName]=function(){return this[t.privateName]=this[t.privateName]||new t.ListClass,this[t.privateName]}}),j.prototype.featuresVolumeControl=!0,j.prototype.featuresMuteControl=!0,j.prototype.featuresFullscreenResize=!1,j.prototype.featuresPlaybackRate=!1,j.prototype.featuresProgressEvents=!1,j.prototype.featuresSourceset=!1,j.prototype.featuresTimeupdateEvents=!1,j.prototype.featuresNativeTextTracks=!1,j.prototype.featuresVideoFrameCallback=!1,j.withSourceHandlers=function(r){r.registerSourceHandler=function(e,t){let s=r.sourceHandlers;s=s||(r.sourceHandlers=[]),void 0===t&&(t=s.length),s.splice(t,0,e)},r.canPlayType=function(t){var s,i=r.sourceHandlers||[];for(let e=0;efunction s(i={},e=[],r,n,a=[],o=!1){const[t,...l]=e;if("string"==typeof t)s(i,ti[t],r,n,a,o);else if(t){const h=ci(n,t);if(!h.setSource)return a.push(h),s(i,l,r,n,a,o);h.setSource(Object.assign({},i),function(e,t){if(e)return s(i,l,r,n,a,o);a.push(h),s(t,i.type===t.type?l:ti[t.type],r,n,a,o)})}else l.length?s(i,l,r,n,a,o):o?r(i,a):s(i,ti["*"],r,n,a,!0)}(t,ti[t.type],s,e),1)}function ni(e,t,s,i=null){var r="call"+y(s),r=e.reduce(hi(r),i),i=r===ii,t=i?null:t[s](r),n=e,a=s,o=t,l=i;for(let e=n.length-1;0<=e;e--){var h=n[e];h[a]&&h[a](l,o)}return t}const ai={buffered:1,currentTime:1,duration:1,muted:1,played:1,paused:1,seekable:1,volume:1,ended:1},oi={setCurrentTime:1,setMuted:1,setVolume:1},li={play:1,pause:1};function hi(s){return(e,t)=>e===ii?ii:t[s]?t[s](e):e}function ci(e,t){var s=si[e.id()];let i=null;if(null==s)i=t(e),si[e.id()]=[[t,i]];else{for(let e=0;ethis.handleMouseOver(e),this.handleMouseOut_=e=>this.handleMouseOut(e),this.handleClick_=e=>this.handleClick(e),this.handleKeyDown_=e=>this.handleKeyDown(e),this.emitTapEvents(),this.enable()}createEl(e="div",t={},s={}){t=Object.assign({className:this.buildCSSClass(),tabIndex:0},t),"button"===e&&l.error(`Creating a ClickableComponent with an HTML element of ${e} is not supported; use a Button instead.`),s=Object.assign({role:"button"},s),this.tabIndex_=t.tabIndex;e=p(e,t,s);return this.player_.options_.experimentalSvgIcons||e.appendChild(p("span",{className:"vjs-icon-placeholder"},{"aria-hidden":!0})),this.createControlTextEl(e),e}dispose(){this.controlTextEl_=null,super.dispose()}createControlTextEl(e){return this.controlTextEl_=p("span",{className:"vjs-control-text"},{"aria-live":"polite"}),e&&e.appendChild(this.controlTextEl_),this.controlText(this.controlText_,e),this.controlTextEl_}controlText(e,t=this.el()){if(void 0===e)return this.controlText_||"Need Text";var s=this.localize(e);this.controlText_=e,we(this.controlTextEl_,s),this.nonIconControl||this.player_.options_.noUITitleAttributes||t.setAttribute("title",s)}buildCSSClass(){return"vjs-control vjs-button "+super.buildCSSClass()}enable(){this.enabled_||(this.enabled_=!0,this.removeClass("vjs-disabled"),this.el_.setAttribute("aria-disabled","false"),"undefined"!=typeof this.tabIndex_&&this.el_.setAttribute("tabIndex",this.tabIndex_),this.on(["tap","click"],this.handleClick_),this.on("keydown",this.handleKeyDown_))}disable(){this.enabled_=!1,this.addClass("vjs-disabled"),this.el_.setAttribute("aria-disabled","true"),"undefined"!=typeof this.tabIndex_&&this.el_.removeAttribute("tabIndex"),this.off("mouseover",this.handleMouseOver_),this.off("mouseout",this.handleMouseOut_),this.off(["tap","click"],this.handleClick_),this.off("keydown",this.handleKeyDown_)}handleLanguagechange(){this.controlText(this.controlText_)}handleClick(e){this.options_.clickHandler&&this.options_.clickHandler.call(this,arguments)}handleKeyDown(e){a.isEventKey(e,"Space")||a.isEventKey(e,"Enter")?(e.preventDefault(),e.stopPropagation(),this.trigger("click")):super.handleKeyDown(e)}}b.registerComponent("ClickableComponent",vi);class _i extends vi{constructor(e,t){super(e,t),this.update(),this.update_=e=>this.update(e),e.on("posterchange",this.update_)}dispose(){this.player().off("posterchange",this.update_),super.dispose()}createEl(){return p("div",{className:"vjs-poster"})}crossOrigin(e){if("undefined"==typeof e)return this.$("img")?this.$("img").crossOrigin:this.player_.tech_&&this.player_.tech_.isReady_?this.player_.crossOrigin():this.player_.options_.crossOrigin||this.player_.options_.crossorigin||null;null!==e&&"anonymous"!==e&&"use-credentials"!==e?this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${e}"`):this.$("img")&&(this.$("img").crossOrigin=e)}update(e){var t=this.player().poster();this.setSrc(t),t?this.show():this.hide()}setSrc(e){e?(this.$("img")||this.el_.appendChild(p("picture",{className:"vjs-poster",tabIndex:-1},{},p("img",{loading:"lazy",crossOrigin:this.crossOrigin()},{alt:""}))),this.$("img").src=e):this.el_.textContent=""}handleClick(e){this.player_.controls()&&(this.player_.tech(!0)&&this.player_.tech(!0).focus(),this.player_.paused()?k(this.player_.play()):this.player_.pause())}}_i.prototype.crossorigin=_i.prototype.crossOrigin,b.registerComponent("PosterImage",_i);const fi={monospace:"monospace",sansSerif:"sans-serif",serif:"serif",monospaceSansSerif:'"Andale Mono", "Lucida Console", monospace',monospaceSerif:'"Courier New", monospace',proportionalSansSerif:"sans-serif",proportionalSerif:"serif",casual:'"Comic Sans MS", Impact, fantasy',script:'"Monotype Corsiva", cursive',smallcaps:'"Andale Mono", "Lucida Console", monospace, sans-serif'};function yi(e,t){let s;if(4===e.length)s=e[1]+e[1]+e[2]+e[2]+e[3]+e[3];else{if(7!==e.length)throw new Error("Invalid color code provided, "+e+"; must be formatted as e.g. #f0e or #f604e2.");s=e.slice(1)}return"rgba("+parseInt(s.slice(0,2),16)+","+parseInt(s.slice(2,4),16)+","+parseInt(s.slice(4,6),16)+","+t+")"}function bi(e,t,s){try{e.style[t]=s}catch(e){}}function Ti(e){return e?e+"px":""}class ki extends b{constructor(i,e,t){super(i,e,t);const r=e=>{this.updateDisplayOverlay(),this.updateDisplay(e)};i.on("loadstart",e=>this.toggleDisplay(e)),i.on("texttrackchange",e=>this.updateDisplay(e)),i.on("loadedmetadata",e=>{this.updateDisplayOverlay(),this.preselectTrack(e)}),i.ready(f(this,function(){if(i.tech_&&i.tech_.featuresNativeTextTracks)this.hide();else{i.on("fullscreenchange",r),i.on("playerresize",r);const e=window.screen.orientation||window,s=window.screen.orientation?"change":"orientationchange";e.addEventListener(s,r),i.on("dispose",()=>e.removeEventListener(s,r));var t=this.options_.playerOptions.tracks||[];for(let e=0;e!e.activeCues)){var t=[];for(let e=0;ethis.handleMouseDown(e))}buildCSSClass(){return"vjs-big-play-button"}handleClick(e){var t=this.player_.play();if(this.mouseused_&&"clientX"in e&&"clientY"in e)k(t),this.player_.tech(!0)&&this.player_.tech(!0).focus();else{var e=this.player_.getChild("controlBar");const s=e&&e.getChild("playToggle");s?(e=()=>s.focus(),zt(t)?t.then(e,()=>{}):this.setTimeout(e,1)):this.player_.tech(!0).focus()}}handleKeyDown(e){this.mouseused_=!1,super.handleKeyDown(e)}handleMouseDown(e){this.mouseused_=!0}}wi.prototype.controlText_="Play Video",b.registerComponent("BigPlayButton",wi);P;b.registerComponent("CloseButton",class extends P{constructor(e,t){super(e,t),this.setIcon("cancel"),this.controlText(t&&t.controlText||this.localize("Close"))}buildCSSClass(){return"vjs-close-button "+super.buildCSSClass()}handleClick(e){this.trigger({type:"close",bubbles:!1})}handleKeyDown(e){a.isEventKey(e,"Esc")?(e.preventDefault(),e.stopPropagation(),this.trigger("click")):super.handleKeyDown(e)}});class Ei extends P{constructor(e,t={}){super(e,t),t.replay=void 0===t.replay||t.replay,this.setIcon("play"),this.on(e,"play",e=>this.handlePlay(e)),this.on(e,"pause",e=>this.handlePause(e)),t.replay&&this.on(e,"ended",e=>this.handleEnded(e))}buildCSSClass(){return"vjs-play-control "+super.buildCSSClass()}handleClick(e){this.player_.paused()?k(this.player_.play()):this.player_.pause()}handleSeeked(e){this.removeClass("vjs-ended"),this.player_.paused()?this.handlePause(e):this.handlePlay(e)}handlePlay(e){this.removeClass("vjs-ended","vjs-paused"),this.addClass("vjs-playing"),this.setIcon("pause"),this.controlText("Pause")}handlePause(e){this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.setIcon("play"),this.controlText("Play")}handleEnded(e){this.removeClass("vjs-playing"),this.addClass("vjs-ended"),this.setIcon("replay"),this.controlText("Replay"),this.one(this.player_,"seeked",e=>this.handleSeeked(e))}}Ei.prototype.controlText_="Play",b.registerComponent("PlayToggle",Ei);class Si extends b{constructor(e,t){super(e,t),this.on(e,["timeupdate","ended","seeking"],e=>this.update(e)),this.updateTextNode_()}createEl(){var e=this.buildCSSClass(),t=super.createEl("div",{className:e+" vjs-time-control vjs-control"}),s=p("span",{className:"vjs-control-text",textContent:this.localize(this.labelText_)+" "},{role:"presentation"});return t.appendChild(s),this.contentEl_=p("span",{className:e+"-display"},{role:"presentation"}),t.appendChild(this.contentEl_),t}dispose(){this.contentEl_=null,this.textNode_=null,super.dispose()}update(e){!this.player_.options_.enableSmoothSeeking&&"seeking"===e.type||this.updateContent(e)}updateTextNode_(e=0){e=Rt(e),this.formattedTime_!==e&&(this.formattedTime_=e,this.requestNamedAnimationFrame("TimeDisplay#updateTextNode_",()=>{if(this.contentEl_){let e=this.textNode_;e&&this.contentEl_.firstChild!==e&&(e=null,l.warn("TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.")),this.textNode_=document.createTextNode(this.formattedTime_),this.textNode_&&(e?this.contentEl_.replaceChild(this.textNode_,e):this.contentEl_.appendChild(this.textNode_))}}))}updateContent(e){}}Si.prototype.labelText_="Time",Si.prototype.controlText_="Time",b.registerComponent("TimeDisplay",Si);class xi extends Si{buildCSSClass(){return"vjs-current-time"}updateContent(e){let t;t=this.player_.ended()?this.player_.duration():this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime(),this.updateTextNode_(t)}}xi.prototype.labelText_="Current Time",xi.prototype.controlText_="Current Time",b.registerComponent("CurrentTimeDisplay",xi);class ji extends Si{constructor(e,t){super(e,t);t=e=>this.updateContent(e);this.on(e,"durationchange",t),this.on(e,"loadstart",t),this.on(e,"loadedmetadata",t)}buildCSSClass(){return"vjs-duration"}updateContent(e){var t=this.player_.duration();this.updateTextNode_(t)}}ji.prototype.labelText_="Duration",ji.prototype.controlText_="Duration",b.registerComponent("DurationDisplay",ji);class Pi extends b{createEl(){var e=super.createEl("div",{className:"vjs-time-control vjs-time-divider"},{"aria-hidden":!0}),t=super.createEl("div"),s=super.createEl("span",{textContent:"/"});return t.appendChild(s),e.appendChild(t),e}}b.registerComponent("TimeDivider",Pi);class Ii extends Si{constructor(e,t){super(e,t),this.on(e,"durationchange",e=>this.updateContent(e))}buildCSSClass(){return"vjs-remaining-time"}createEl(){var e=super.createEl();return!1!==this.options_.displayNegative&&e.insertBefore(p("span",{},{"aria-hidden":!0},"-"),this.contentEl_),e}updateContent(e){if("number"==typeof this.player_.duration()){let e;e=this.player_.ended()?0:this.player_.remainingTimeDisplay?this.player_.remainingTimeDisplay():this.player_.remainingTime(),this.updateTextNode_(e)}}}Ii.prototype.labelText_="Remaining Time",Ii.prototype.controlText_="Remaining Time",b.registerComponent("RemainingTimeDisplay",Ii);class Mi extends b{constructor(e,t){super(e,t),this.updateShowing(),this.on(this.player(),"durationchange",e=>this.updateShowing(e))}createEl(){var e=super.createEl("div",{className:"vjs-live-control vjs-control"});return this.contentEl_=p("div",{className:"vjs-live-display"},{"aria-live":"off"}),this.contentEl_.appendChild(p("span",{className:"vjs-control-text",textContent:this.localize("Stream Type")+" "})),this.contentEl_.appendChild(document.createTextNode(this.localize("LIVE"))),e.appendChild(this.contentEl_),e}dispose(){this.contentEl_=null,super.dispose()}updateShowing(e){this.player().duration()===1/0?this.show():this.hide()}}b.registerComponent("LiveDisplay",Mi);class Ai extends P{constructor(e,t){super(e,t),this.updateLiveEdgeStatus(),this.player_.liveTracker&&(this.updateLiveEdgeStatusHandler_=e=>this.updateLiveEdgeStatus(e),this.on(this.player_.liveTracker,"liveedgechange",this.updateLiveEdgeStatusHandler_))}createEl(){var e=super.createEl("button",{className:"vjs-seek-to-live-control vjs-control"});return this.setIcon("circle",e),this.textEl_=p("span",{className:"vjs-seek-to-live-text",textContent:this.localize("LIVE")},{"aria-hidden":"true"}),e.appendChild(this.textEl_),e}updateLiveEdgeStatus(){!this.player_.liveTracker||this.player_.liveTracker.atLiveEdge()?(this.setAttribute("aria-disabled",!0),this.addClass("vjs-at-live-edge"),this.controlText("Seek to live, currently playing live")):(this.setAttribute("aria-disabled",!1),this.removeClass("vjs-at-live-edge"),this.controlText("Seek to live, currently behind live"))}handleClick(){this.player_.liveTracker.seekToLiveEdge()}dispose(){this.player_.liveTracker&&this.off(this.player_.liveTracker,"liveedgechange",this.updateLiveEdgeStatusHandler_),this.textEl_=null,super.dispose()}}function Oi(e,t,s){return e=Number(e),Math.min(s,Math.max(t,isNaN(e)?t:e))}Ai.prototype.controlText_="Seek to live, currently playing live",b.registerComponent("SeekToLive",Ai);t=Object.freeze({__proto__:null,clamp:Oi});class Li extends b{constructor(e,t){super(e,t),this.handleMouseDown_=e=>this.handleMouseDown(e),this.handleMouseUp_=e=>this.handleMouseUp(e),this.handleKeyDown_=e=>this.handleKeyDown(e),this.handleClick_=e=>this.handleClick(e),this.handleMouseMove_=e=>this.handleMouseMove(e),this.update_=e=>this.update(e),this.bar=this.getChild(this.options_.barName),this.vertical(!!this.options_.vertical),this.enable()}enabled(){return this.enabled_}enable(){this.enabled()||(this.on("mousedown",this.handleMouseDown_),this.on("touchstart",this.handleMouseDown_),this.on("keydown",this.handleKeyDown_),this.on("click",this.handleClick_),this.on(this.player_,"controlsvisible",this.update),this.playerEvent&&this.on(this.player_,this.playerEvent,this.update),this.removeClass("disabled"),this.setAttribute("tabindex",0),this.enabled_=!0)}disable(){var e;this.enabled()&&(e=this.bar.el_.ownerDocument,this.off("mousedown",this.handleMouseDown_),this.off("touchstart",this.handleMouseDown_),this.off("keydown",this.handleKeyDown_),this.off("click",this.handleClick_),this.off(this.player_,"controlsvisible",this.update_),this.off(e,"mousemove",this.handleMouseMove_),this.off(e,"mouseup",this.handleMouseUp_),this.off(e,"touchmove",this.handleMouseMove_),this.off(e,"touchend",this.handleMouseUp_),this.removeAttribute("tabindex"),this.addClass("disabled"),this.playerEvent&&this.off(this.player_,this.playerEvent,this.update),this.enabled_=!1)}createEl(e,t={},s={}){return t.className=t.className+" vjs-slider",t=Object.assign({tabIndex:0},t),s=Object.assign({role:"slider","aria-valuenow":0,"aria-valuemin":0,"aria-valuemax":100},s),super.createEl(e,t,s)}handleMouseDown(e){var t=this.bar.el_.ownerDocument;"mousedown"===e.type&&e.preventDefault(),"touchstart"!==e.type||c||e.preventDefault(),Ne(),this.addClass("vjs-sliding"),this.trigger("slideractive"),this.on(t,"mousemove",this.handleMouseMove_),this.on(t,"mouseup",this.handleMouseUp_),this.on(t,"touchmove",this.handleMouseMove_),this.on(t,"touchend",this.handleMouseUp_),this.handleMouseMove(e,!0)}handleMouseMove(e){}handleMouseUp(e){var t=this.bar.el_.ownerDocument;De(),this.removeClass("vjs-sliding"),this.trigger("sliderinactive"),this.off(t,"mousemove",this.handleMouseMove_),this.off(t,"mouseup",this.handleMouseUp_),this.off(t,"touchmove",this.handleMouseMove_),this.off(t,"touchend",this.handleMouseUp_),this.update()}update(){if(this.el_&&this.bar){const t=this.getProgress();return t!==this.progress_&&(this.progress_=t,this.requestNamedAnimationFrame("Slider#update",()=>{var e=this.vertical()?"height":"width";this.bar.el().style[e]=(100*t).toFixed(2)+"%"})),t}}getProgress(){return Number(Oi(this.getPercent(),0,1).toFixed(4))}calculateDistance(e){e=He(this.el_,e);return this.vertical()?e.y:e.x}handleKeyDown(e){a.isEventKey(e,"Left")||a.isEventKey(e,"Down")?(e.preventDefault(),e.stopPropagation(),this.stepBack()):a.isEventKey(e,"Right")||a.isEventKey(e,"Up")?(e.preventDefault(),e.stopPropagation(),this.stepForward()):super.handleKeyDown(e)}handleClick(e){e.stopPropagation(),e.preventDefault()}vertical(e){if(void 0===e)return this.vertical_||!1;this.vertical_=!!e,this.vertical_?this.addClass("vjs-slider-vertical"):this.addClass("vjs-slider-horizontal")}}b.registerComponent("Slider",Li);const Ni=(e,t)=>Oi(e/t*100,0,100).toFixed(2)+"%";class Di extends b{constructor(e,t){super(e,t),this.partEls_=[],this.on(e,"progress",e=>this.update(e))}createEl(){var e=super.createEl("div",{className:"vjs-load-progress"}),t=p("span",{className:"vjs-control-text"}),s=p("span",{textContent:this.localize("Loaded")}),i=document.createTextNode(": ");return this.percentageEl_=p("span",{className:"vjs-control-text-loaded-percentage",textContent:"0%"}),e.appendChild(t),t.appendChild(s),t.appendChild(i),t.appendChild(this.percentageEl_),e}dispose(){this.partEls_=null,this.percentageEl_=null,super.dispose()}update(e){this.requestNamedAnimationFrame("LoadProgressBar#update",()=>{var e=this.player_.liveTracker,s=this.player_.buffered(),e=e&&e.isLive()?e.seekableEnd():this.player_.duration(),i=this.player_.bufferedEnd(),r=this.partEls_,e=Ni(i,e);this.percent_!==e&&(this.el_.style.width=e,we(this.percentageEl_,e),this.percent_=e);for(let t=0;ts.length;e--)this.el_.removeChild(r[e-1]);r.length=s.length})}}b.registerComponent("LoadProgressBar",Di);class Bi extends b{constructor(e,t){super(e,t),this.update=r(f(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-time-tooltip"},{"aria-hidden":"true"})}update(i,r,n){var a=Re(this.el_),o=Be(this.player_.el()),r=i.width*r;if(o&&a){let e=i.left-o.left+r,t=i.width-r+(o.right-i.right),s=(t||(t=i.width-r,e=r),a.width/2);ea.width&&(s=a.width),s=Math.round(s),this.el_.style.right=`-${s}px`,this.write(n)}}write(e){we(this.el_,e)}updateTime(r,n,a,o){this.requestNamedAnimationFrame("TimeTooltip#updateTime",()=>{let e;var t,s,i=this.player_.duration();e=this.player_.liveTracker&&this.player_.liveTracker.isLive()?((s=(t=this.player_.liveTracker.liveWindow())-n*t)<1?"":"-")+Rt(s,t):Rt(a,i),this.update(r,n,e),o&&o()})}}b.registerComponent("TimeTooltip",Bi);class Ri extends b{constructor(e,t){super(e,t),this.setIcon("circle"),this.update=r(f(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-play-progress vjs-slider-bar"},{"aria-hidden":"true"})}update(e,t){var s,i=this.getChild("timeTooltip");i&&(s=this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime(),i.updateTime(e,t,s))}}Ri.prototype.options_={children:[]},u||o||Ri.prototype.options_.children.push("timeTooltip"),b.registerComponent("PlayProgressBar",Ri);class Hi extends b{constructor(e,t){super(e,t),this.update=r(f(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-mouse-display"})}update(e,t){var s=t*this.player_.duration();this.getChild("timeTooltip").updateTime(e,t,s,()=>{this.el_.style.left=e.width*t+"px"})}}Hi.prototype.options_={children:["timeTooltip"]},b.registerComponent("MouseTimeDisplay",Hi);class Fi extends Li{constructor(e,t){super(e,t),this.setEventHandlers_()}setEventHandlers_(){this.update_=f(this,this.update),this.update=r(this.update_,30),this.on(this.player_,["ended","durationchange","timeupdate"],this.update),this.player_.liveTracker&&this.on(this.player_.liveTracker,"liveedgechange",this.update),this.updateInterval=null,this.enableIntervalHandler_=e=>this.enableInterval_(e),this.disableIntervalHandler_=e=>this.disableInterval_(e),this.on(this.player_,["playing"],this.enableIntervalHandler_),this.on(this.player_,["ended","pause","waiting"],this.disableIntervalHandler_),"hidden"in document&&"visibilityState"in document&&this.on(document,"visibilitychange",this.toggleVisibility_)}toggleVisibility_(e){"hidden"===document.visibilityState?(this.cancelNamedAnimationFrame("SeekBar#update"),this.cancelNamedAnimationFrame("Slider#update"),this.disableInterval_(e)):(this.player_.ended()||this.player_.paused()||this.enableInterval_(),this.update())}enableInterval_(){this.updateInterval||(this.updateInterval=this.setInterval(this.update,30))}disableInterval_(e){this.player_.liveTracker&&this.player_.liveTracker.isLive()&&e&&"ended"!==e.type||this.updateInterval&&(this.clearInterval(this.updateInterval),this.updateInterval=null)}createEl(){return super.createEl("div",{className:"vjs-progress-holder"},{"aria-label":this.localize("Progress Bar")})}update(e){if("hidden"!==document.visibilityState){const i=super.update();return this.requestNamedAnimationFrame("SeekBar#update",()=>{var e=this.player_.ended()?this.player_.duration():this.getCurrentTime_(),t=this.player_.liveTracker;let s=this.player_.duration();t&&t.isLive()&&(s=this.player_.liveTracker.liveCurrentTime()),this.percent_!==i&&(this.el_.setAttribute("aria-valuenow",(100*i).toFixed(2)),this.percent_=i),this.currentTime_===e&&this.duration_===s||(this.el_.setAttribute("aria-valuetext",this.localize("progress bar timing: currentTime={1} duration={2}",[Rt(e,s),Rt(s,s)],"{1} of {2}")),this.currentTime_=e,this.duration_=s),this.bar&&this.bar.update(Be(this.el()),this.getProgress())}),i}}userSeek_(e){this.player_.liveTracker&&this.player_.liveTracker.isLive()&&this.player_.liveTracker.nextSeekedFromUser(),this.player_.currentTime(e)}getCurrentTime_(){return this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime()}getPercent(){var e=this.getCurrentTime_();let t;var s=this.player_.liveTracker;return s&&s.isLive()?(t=(e-s.seekableStart())/s.liveWindow(),s.atLiveEdge()&&(t=1)):t=e/this.player_.duration(),t}handleMouseDown(e){Ue(e)&&(e.stopPropagation(),this.videoWasPlaying=!this.player_.paused(),this.player_.pause(),super.handleMouseDown(e))}handleMouseMove(t,s=!1){if(Ue(t)&&!isNaN(this.player_.duration())){s||this.player_.scrubbing()||this.player_.scrubbing(!0);let e;s=this.calculateDistance(t),t=this.player_.liveTracker;if(t&&t.isLive()){if(.99<=s)return void t.seekToLiveEdge();var i=t.seekableStart(),r=t.liveCurrentTime();if((e=(e=(e=i+s*t.liveWindow())>=r?r:e)<=i?i+.1:e)===1/0)return}else(e=s*this.player_.duration())===this.player_.duration()&&(e-=.1);this.userSeek_(e),this.player_.options_.enableSmoothSeeking&&this.update()}}enable(){super.enable();var e=this.getChild("mouseTimeDisplay");e&&e.show()}disable(){super.disable();var e=this.getChild("mouseTimeDisplay");e&&e.hide()}handleMouseUp(e){super.handleMouseUp(e),e&&e.stopPropagation(),this.player_.scrubbing(!1),this.player_.trigger({type:"timeupdate",target:this,manuallyTriggered:!0}),this.videoWasPlaying?k(this.player_.play()):this.update_()}stepForward(){this.userSeek_(this.player_.currentTime()+5)}stepBack(){this.userSeek_(this.player_.currentTime()-5)}handleAction(e){this.player_.paused()?this.player_.play():this.player_.pause()}handleKeyDown(e){var t,s=this.player_.liveTracker;a.isEventKey(e,"Space")||a.isEventKey(e,"Enter")?(e.preventDefault(),e.stopPropagation(),this.handleAction(e)):a.isEventKey(e,"Home")?(e.preventDefault(),e.stopPropagation(),this.userSeek_(0)):a.isEventKey(e,"End")?(e.preventDefault(),e.stopPropagation(),s&&s.isLive()?this.userSeek_(s.liveCurrentTime()):this.userSeek_(this.player_.duration())):/^[0-9]$/.test(a(e))?(e.preventDefault(),e.stopPropagation(),t=10*(a.codes[a(e)]-a.codes[0])/100,s&&s.isLive()?this.userSeek_(s.seekableStart()+s.liveWindow()*t):this.userSeek_(this.player_.duration()*t)):a.isEventKey(e,"PgDn")?(e.preventDefault(),e.stopPropagation(),this.userSeek_(this.player_.currentTime()-60)):a.isEventKey(e,"PgUp")?(e.preventDefault(),e.stopPropagation(),this.userSeek_(this.player_.currentTime()+60)):super.handleKeyDown(e)}dispose(){this.disableInterval_(),this.off(this.player_,["ended","durationchange","timeupdate"],this.update),this.player_.liveTracker&&this.off(this.player_.liveTracker,"liveedgechange",this.update),this.off(this.player_,["playing"],this.enableIntervalHandler_),this.off(this.player_,["ended","pause","waiting"],this.disableIntervalHandler_),"hidden"in document&&"visibilityState"in document&&this.off(document,"visibilitychange",this.toggleVisibility_),super.dispose()}}Fi.prototype.options_={children:["loadProgressBar","playProgressBar"],barName:"playProgressBar"},u||o||Fi.prototype.options_.children.splice(1,0,"mouseTimeDisplay"),b.registerComponent("SeekBar",Fi);class Vi extends b{constructor(e,t){super(e,t),this.handleMouseMove=r(f(this,this.handleMouseMove),30),this.throttledHandleMouseSeek=r(f(this,this.handleMouseSeek),30),this.handleMouseUpHandler_=e=>this.handleMouseUp(e),this.handleMouseDownHandler_=e=>this.handleMouseDown(e),this.enable()}createEl(){return super.createEl("div",{className:"vjs-progress-control vjs-control"})}handleMouseMove(e){var t,s,i,r,n=this.getChild("seekBar");n&&(t=n.getChild("playProgressBar"),s=n.getChild("mouseTimeDisplay"),t||s)&&(i=Re(r=n.el()),r=Oi(r=He(r,e).x,0,1),s&&s.update(i,r),t)&&t.update(i,n.getProgress())}handleMouseSeek(e){var t=this.getChild("seekBar");t&&t.handleMouseMove(e)}enabled(){return this.enabled_}disable(){var e;this.children().forEach(e=>e.disable&&e.disable()),this.enabled()&&(this.off(["mousedown","touchstart"],this.handleMouseDownHandler_),this.off(this.el_,"mousemove",this.handleMouseMove),this.removeListenersAddedOnMousedownAndTouchstart(),this.addClass("disabled"),this.enabled_=!1,this.player_.scrubbing())&&(e=this.getChild("seekBar"),this.player_.scrubbing(!1),e.videoWasPlaying)&&k(this.player_.play())}enable(){this.children().forEach(e=>e.enable&&e.enable()),this.enabled()||(this.on(["mousedown","touchstart"],this.handleMouseDownHandler_),this.on(this.el_,"mousemove",this.handleMouseMove),this.removeClass("disabled"),this.enabled_=!0)}removeListenersAddedOnMousedownAndTouchstart(){var e=this.el_.ownerDocument;this.off(e,"mousemove",this.throttledHandleMouseSeek),this.off(e,"touchmove",this.throttledHandleMouseSeek),this.off(e,"mouseup",this.handleMouseUpHandler_),this.off(e,"touchend",this.handleMouseUpHandler_)}handleMouseDown(e){var t=this.el_.ownerDocument,s=this.getChild("seekBar");s&&s.handleMouseDown(e),this.on(t,"mousemove",this.throttledHandleMouseSeek),this.on(t,"touchmove",this.throttledHandleMouseSeek),this.on(t,"mouseup",this.handleMouseUpHandler_),this.on(t,"touchend",this.handleMouseUpHandler_)}handleMouseUp(e){var t=this.getChild("seekBar");t&&t.handleMouseUp(e),this.removeListenersAddedOnMousedownAndTouchstart()}}Vi.prototype.options_={children:["seekBar"]},b.registerComponent("ProgressControl",Vi);class zi extends P{constructor(e,t){super(e,t),this.setIcon("picture-in-picture-enter"),this.on(e,["enterpictureinpicture","leavepictureinpicture"],e=>this.handlePictureInPictureChange(e)),this.on(e,["disablepictureinpicturechanged","loadedmetadata"],e=>this.handlePictureInPictureEnabledChange(e)),this.on(e,["loadedmetadata","audioonlymodechange","audiopostermodechange"],()=>this.handlePictureInPictureAudioModeChange()),this.disable()}buildCSSClass(){return"vjs-picture-in-picture-control vjs-hidden "+super.buildCSSClass()}handlePictureInPictureAudioModeChange(){"audio"===this.player_.currentType().substring(0,5)||this.player_.audioPosterMode()||this.player_.audioOnlyMode()?(this.player_.isInPictureInPicture()&&this.player_.exitPictureInPicture(),this.hide()):this.show()}handlePictureInPictureEnabledChange(){document.pictureInPictureEnabled&&!1===this.player_.disablePictureInPicture()||this.player_.options_.enableDocumentPictureInPicture&&"documentPictureInPicture"in window?this.enable():this.disable()}handlePictureInPictureChange(e){this.player_.isInPictureInPicture()?(this.setIcon("picture-in-picture-exit"),this.controlText("Exit Picture-in-Picture")):(this.setIcon("picture-in-picture-enter"),this.controlText("Picture-in-Picture")),this.handlePictureInPictureEnabledChange()}handleClick(e){this.player_.isInPictureInPicture()?this.player_.exitPictureInPicture():this.player_.requestPictureInPicture()}show(){"function"==typeof document.exitPictureInPicture&&super.show()}}zi.prototype.controlText_="Picture-in-Picture",b.registerComponent("PictureInPictureToggle",zi);class qi extends P{constructor(e,t){super(e,t),this.setIcon("fullscreen-enter"),this.on(e,"fullscreenchange",e=>this.handleFullscreenChange(e)),!1===document[e.fsApi_.fullscreenEnabled]&&this.disable()}buildCSSClass(){return"vjs-fullscreen-control "+super.buildCSSClass()}handleFullscreenChange(e){this.player_.isFullscreen()?(this.controlText("Exit Fullscreen"),this.setIcon("fullscreen-exit")):(this.controlText("Fullscreen"),this.setIcon("fullscreen-enter"))}handleClick(e){this.player_.isFullscreen()?this.player_.exitFullscreen():this.player_.requestFullscreen()}}qi.prototype.controlText_="Fullscreen",b.registerComponent("FullscreenToggle",qi);class $i extends b{createEl(){var e=super.createEl("div",{className:"vjs-volume-level"});return this.setIcon("circle",e),e.appendChild(super.createEl("span",{className:"vjs-control-text"})),e}}b.registerComponent("VolumeLevel",$i);class Ui extends b{constructor(e,t){super(e,t),this.update=r(f(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-volume-tooltip"},{"aria-hidden":"true"})}update(t,s,i,e){if(!i){var i=Be(this.el_),r=Be(this.player_.el()),s=t.width*s;if(!r||!i)return;var n=t.left-r.left+s,s=t.width-s+(r.right-t.right);let e=i.width/2;ni.width&&(e=i.width),this.el_.style.right=`-${e}px`}this.write(e+"%")}write(e){we(this.el_,e)}updateVolume(e,t,s,i,r){this.requestNamedAnimationFrame("VolumeLevelTooltip#updateVolume",()=>{this.update(e,t,s,i.toFixed(0)),r&&r()})}}b.registerComponent("VolumeLevelTooltip",Ui);class Ki extends b{constructor(e,t){super(e,t),this.update=r(f(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-mouse-display"})}update(e,t,s){var i=100*t;this.getChild("volumeLevelTooltip").updateVolume(e,t,s,i,()=>{s?this.el_.style.bottom=e.height*t+"px":this.el_.style.left=e.width*t+"px"})}}Ki.prototype.options_={children:["volumeLevelTooltip"]},b.registerComponent("MouseVolumeLevelDisplay",Ki);class Wi extends Li{constructor(e,t){super(e,t),this.on("slideractive",e=>this.updateLastVolume_(e)),this.on(e,"volumechange",e=>this.updateARIAAttributes(e)),e.ready(()=>this.updateARIAAttributes())}createEl(){return super.createEl("div",{className:"vjs-volume-bar vjs-slider-bar"},{"aria-label":this.localize("Volume Level"),"aria-live":"polite"})}handleMouseDown(e){Ue(e)&&super.handleMouseDown(e)}handleMouseMove(e){var t,s,i,r=this.getChild("mouseVolumeLevelDisplay");r&&(t=Be(i=this.el()),s=this.vertical(),i=He(i,e),i=Oi(i=s?i.y:i.x,0,1),r.update(t,i,s)),Ue(e)&&(this.checkMuted(),this.player_.volume(this.calculateDistance(e)))}checkMuted(){this.player_.muted()&&this.player_.muted(!1)}getPercent(){return this.player_.muted()?0:this.player_.volume()}stepForward(){this.checkMuted(),this.player_.volume(this.player_.volume()+.1)}stepBack(){this.checkMuted(),this.player_.volume(this.player_.volume()-.1)}updateARIAAttributes(e){var t=this.player_.muted()?0:this.volumeAsPercentage_();this.el_.setAttribute("aria-valuenow",t),this.el_.setAttribute("aria-valuetext",t+"%")}volumeAsPercentage_(){return Math.round(100*this.player_.volume())}updateLastVolume_(){const e=this.player_.volume();this.one("sliderinactive",()=>{0===this.player_.volume()&&this.player_.lastVolume_(e)})}}Wi.prototype.options_={children:["volumeLevel"],barName:"volumeLevel"},u||o||Wi.prototype.options_.children.splice(0,0,"mouseVolumeLevelDisplay"),Wi.prototype.playerEvent="volumechange",b.registerComponent("VolumeBar",Wi);class Xi extends b{constructor(e,t={}){var s,i;t.vertical=t.vertical||!1,"undefined"!=typeof t.volumeBar&&!G(t.volumeBar)||(t.volumeBar=t.volumeBar||{},t.volumeBar.vertical=t.vertical),super(e,t),s=this,(i=e).tech_&&!i.tech_.featuresVolumeControl&&s.addClass("vjs-hidden"),s.on(i,"loadstart",function(){i.tech_.featuresVolumeControl?s.removeClass("vjs-hidden"):s.addClass("vjs-hidden")}),this.throttledHandleMouseMove=r(f(this,this.handleMouseMove),30),this.handleMouseUpHandler_=e=>this.handleMouseUp(e),this.on("mousedown",e=>this.handleMouseDown(e)),this.on("touchstart",e=>this.handleMouseDown(e)),this.on("mousemove",e=>this.handleMouseMove(e)),this.on(this.volumeBar,["focus","slideractive"],()=>{this.volumeBar.addClass("vjs-slider-active"),this.addClass("vjs-slider-active"),this.trigger("slideractive")}),this.on(this.volumeBar,["blur","sliderinactive"],()=>{this.volumeBar.removeClass("vjs-slider-active"),this.removeClass("vjs-slider-active"),this.trigger("sliderinactive")})}createEl(){let e="vjs-volume-horizontal";return this.options_.vertical&&(e="vjs-volume-vertical"),super.createEl("div",{className:"vjs-volume-control vjs-control "+e})}handleMouseDown(e){var t=this.el_.ownerDocument;this.on(t,"mousemove",this.throttledHandleMouseMove),this.on(t,"touchmove",this.throttledHandleMouseMove),this.on(t,"mouseup",this.handleMouseUpHandler_),this.on(t,"touchend",this.handleMouseUpHandler_)}handleMouseUp(e){var t=this.el_.ownerDocument;this.off(t,"mousemove",this.throttledHandleMouseMove),this.off(t,"touchmove",this.throttledHandleMouseMove),this.off(t,"mouseup",this.handleMouseUpHandler_),this.off(t,"touchend",this.handleMouseUpHandler_)}handleMouseMove(e){this.volumeBar.handleMouseMove(e)}}Xi.prototype.options_={children:["volumeBar"]},b.registerComponent("VolumeControl",Xi);class Gi extends P{constructor(e,t){var s,i;super(e,t),s=this,(i=e).tech_&&!i.tech_.featuresMuteControl&&s.addClass("vjs-hidden"),s.on(i,"loadstart",function(){i.tech_.featuresMuteControl?s.removeClass("vjs-hidden"):s.addClass("vjs-hidden")}),this.on(e,["loadstart","volumechange"],e=>this.update(e))}buildCSSClass(){return"vjs-mute-control "+super.buildCSSClass()}handleClick(e){var t=this.player_.volume(),s=this.player_.lastVolume_();0===t?(this.player_.volume(s<.1?.1:s),this.player_.muted(!1)):this.player_.muted(!this.player_.muted())}update(e){this.updateIcon_(),this.updateControlText_()}updateIcon_(){var e=this.player_.volume();let t=3;this.setIcon("volume-high"),u&&this.player_.tech_&&this.player_.tech_.el_&&this.player_.muted(this.player_.tech_.el_.muted),0===e||this.player_.muted()?(this.setIcon("volume-mute"),t=0):e<.33?(this.setIcon("volume-low"),t=1):e<.67&&(this.setIcon("volume-medium"),t=2),je(this.el_,[0,1,2,3].reduce((e,t)=>e+`${t?" ":""}vjs-vol-`+t,"")),xe(this.el_,"vjs-vol-"+t)}updateControlText_(){var e=this.player_.muted()||0===this.player_.volume()?"Unmute":"Mute";this.controlText()!==e&&this.controlText(e)}}Gi.prototype.controlText_="Mute",b.registerComponent("MuteToggle",Gi);class Yi extends b{constructor(e,t={}){"undefined"!=typeof t.inline?t.inline=t.inline:t.inline=!0,"undefined"!=typeof t.volumeControl&&!G(t.volumeControl)||(t.volumeControl=t.volumeControl||{},t.volumeControl.vertical=!t.inline),super(e,t),this.handleKeyPressHandler_=e=>this.handleKeyPress(e),this.on(e,["loadstart"],e=>this.volumePanelState_(e)),this.on(this.muteToggle,"keyup",e=>this.handleKeyPress(e)),this.on(this.volumeControl,"keyup",e=>this.handleVolumeControlKeyUp(e)),this.on("keydown",e=>this.handleKeyPress(e)),this.on("mouseover",e=>this.handleMouseOver(e)),this.on("mouseout",e=>this.handleMouseOut(e)),this.on(this.volumeControl,["slideractive"],this.sliderActive_),this.on(this.volumeControl,["sliderinactive"],this.sliderInactive_)}sliderActive_(){this.addClass("vjs-slider-active")}sliderInactive_(){this.removeClass("vjs-slider-active")}volumePanelState_(){this.volumeControl.hasClass("vjs-hidden")&&this.muteToggle.hasClass("vjs-hidden")&&this.addClass("vjs-hidden"),this.volumeControl.hasClass("vjs-hidden")&&!this.muteToggle.hasClass("vjs-hidden")&&this.addClass("vjs-mute-toggle-only")}createEl(){let e="vjs-volume-panel-horizontal";return this.options_.inline||(e="vjs-volume-panel-vertical"),super.createEl("div",{className:"vjs-volume-panel vjs-control "+e})}dispose(){this.handleMouseOut(),super.dispose()}handleVolumeControlKeyUp(e){a.isEventKey(e,"Esc")&&this.muteToggle.focus()}handleMouseOver(e){this.addClass("vjs-hover"),v(document,"keyup",this.handleKeyPressHandler_)}handleMouseOut(e){this.removeClass("vjs-hover"),_(document,"keyup",this.handleKeyPressHandler_)}handleKeyPress(e){a.isEventKey(e,"Esc")&&this.handleMouseOut()}}Yi.prototype.options_={children:["muteToggle","volumeControl"]},b.registerComponent("VolumePanel",Yi);class Qi extends P{constructor(e,t){super(e,t),this.validOptions=[5,10,30],this.skipTime=this.getSkipForwardTime(),this.skipTime&&this.validOptions.includes(this.skipTime)?(this.setIcon("forward-"+this.skipTime),this.controlText(this.localize("Skip forward {1} seconds",[this.skipTime.toLocaleString(e.language())])),this.show()):this.hide()}getSkipForwardTime(){var e=this.options_.playerOptions;return e.controlBar&&e.controlBar.skipButtons&&e.controlBar.skipButtons.forward}buildCSSClass(){return`vjs-skip-forward-${this.getSkipForwardTime()} `+super.buildCSSClass()}handleClick(e){if(!isNaN(this.player_.duration())){var t=this.player_.currentTime(),s=this.player_.liveTracker,s=s&&s.isLive()?s.seekableEnd():this.player_.duration();let e;e=t+this.skipTime<=s?t+this.skipTime:s,this.player_.currentTime(e)}}handleLanguagechange(){this.controlText(this.localize("Skip forward {1} seconds",[this.skipTime]))}}Qi.prototype.controlText_="Skip Forward",b.registerComponent("SkipForward",Qi);class Ji extends P{constructor(e,t){super(e,t),this.validOptions=[5,10,30],this.skipTime=this.getSkipBackwardTime(),this.skipTime&&this.validOptions.includes(this.skipTime)?(this.setIcon("replay-"+this.skipTime),this.controlText(this.localize("Skip backward {1} seconds",[this.skipTime.toLocaleString(e.language())])),this.show()):this.hide()}getSkipBackwardTime(){var e=this.options_.playerOptions;return e.controlBar&&e.controlBar.skipButtons&&e.controlBar.skipButtons.backward}buildCSSClass(){return`vjs-skip-backward-${this.getSkipBackwardTime()} `+super.buildCSSClass()}handleClick(e){var t=this.player_.currentTime(),s=this.player_.liveTracker,s=s&&s.isLive()&&s.seekableStart();let i;i=s&&t-this.skipTime<=s?s:t>=this.skipTime?t-this.skipTime:0,this.player_.currentTime(i)}handleLanguagechange(){this.controlText(this.localize("Skip backward {1} seconds",[this.skipTime]))}}Ji.prototype.controlText_="Skip Backward",b.registerComponent("SkipBackward",Ji);class Zi extends b{constructor(e,t){super(e,t),t&&(this.menuButton_=t.menuButton),this.focusedChild_=-1,this.on("keydown",e=>this.handleKeyDown(e)),this.boundHandleBlur_=e=>this.handleBlur(e),this.boundHandleTapClick_=e=>this.handleTapClick(e)}addEventListenerForItem(e){e instanceof b&&(this.on(e,"blur",this.boundHandleBlur_),this.on(e,["tap","click"],this.boundHandleTapClick_))}removeEventListenerForItem(e){e instanceof b&&(this.off(e,"blur",this.boundHandleBlur_),this.off(e,["tap","click"],this.boundHandleTapClick_))}removeChild(e){"string"==typeof e&&(e=this.getChild(e)),this.removeEventListenerForItem(e),super.removeChild(e)}addItem(e){e=this.addChild(e);e&&this.addEventListenerForItem(e)}createEl(){var e=this.options_.contentElType||"ul",e=(this.contentEl_=p(e,{className:"vjs-menu-content"}),this.contentEl_.setAttribute("role","menu"),super.createEl("div",{append:this.contentEl_,className:"vjs-menu"}));return e.appendChild(this.contentEl_),v(e,"click",function(e){e.preventDefault(),e.stopImmediatePropagation()}),e}dispose(){this.contentEl_=null,this.boundHandleBlur_=null,this.boundHandleTapClick_=null,super.dispose()}handleBlur(e){const t=e.relatedTarget||document.activeElement;this.children().some(e=>e.el()===t)||(e=this.menuButton_)&&e.buttonPressed_&&t!==e.el().firstChild&&e.unpressButton()}handleTapClick(t){var e;this.menuButton_&&(this.menuButton_.unpressButton(),e=this.children(),Array.isArray(e))&&(e=e.filter(e=>e.el()===t.target)[0])&&"CaptionSettingsMenuItem"!==e.name()&&this.menuButton_.focus()}handleKeyDown(e){a.isEventKey(e,"Left")||a.isEventKey(e,"Down")?(e.preventDefault(),e.stopPropagation(),this.stepForward()):(a.isEventKey(e,"Right")||a.isEventKey(e,"Up"))&&(e.preventDefault(),e.stopPropagation(),this.stepBack())}stepForward(){let e=0;void 0!==this.focusedChild_&&(e=this.focusedChild_+1),this.focus(e)}stepBack(){let e=0;void 0!==this.focusedChild_&&(e=this.focusedChild_-1),this.focus(e)}focus(e=0){var t=this.children().slice();t.length&&t[0].hasClass("vjs-menu-title")&&t.shift(),0=t.length&&(e=t.length-1),t[this.focusedChild_=e].el_.focus())}}b.registerComponent("Menu",Zi);class er extends b{constructor(e,t={}){super(e,t),this.menuButton_=new P(e,t),this.menuButton_.controlText(this.controlText_),this.menuButton_.el_.setAttribute("aria-haspopup","true");e=P.prototype.buildCSSClass(),this.menuButton_.el_.className=this.buildCSSClass()+" "+e,this.menuButton_.removeClass("vjs-control"),this.addChild(this.menuButton_),this.update(),this.enabled_=!0,t=e=>this.handleClick(e);this.handleMenuKeyUp_=e=>this.handleMenuKeyUp(e),this.on(this.menuButton_,"tap",t),this.on(this.menuButton_,"click",t),this.on(this.menuButton_,"keydown",e=>this.handleKeyDown(e)),this.on(this.menuButton_,"mouseenter",()=>{this.addClass("vjs-hover"),this.menu.show(),v(document,"keyup",this.handleMenuKeyUp_)}),this.on("mouseleave",e=>this.handleMouseLeave(e)),this.on("keydown",e=>this.handleSubmenuKeyDown(e))}update(){var e=this.createMenu();this.menu&&(this.menu.dispose(),this.removeChild(this.menu)),this.menu=e,this.addChild(e),this.buttonPressed_=!1,this.menuButton_.el_.setAttribute("aria-expanded","false"),this.items&&this.items.length<=this.hideThreshold_?(this.hide(),this.menu.contentEl_.removeAttribute("role")):(this.show(),this.menu.contentEl_.setAttribute("role","menu"))}createMenu(){var e,t=new Zi(this.player_,{menuButton:this});if(this.hideThreshold_=0,this.options_.title&&(e=p("li",{className:"vjs-menu-title",textContent:y(this.options_.title),tabIndex:-1}),e=new b(this.player_,{el:e}),t.addItem(e)),this.items=this.createItems(),this.items)for(let e=0;ea.isEventKey(t,e))||super.handleKeyDown(t)}handleClick(e){this.selected(!0)}selected(e){this.selectable&&(e?(this.addClass("vjs-selected"),this.el_.setAttribute("aria-checked","true"),this.controlText(", selected"),this.isSelected_=!0):(this.removeClass("vjs-selected"),this.el_.setAttribute("aria-checked","false"),this.controlText(""),this.isSelected_=!1))}}b.registerComponent("MenuItem",ir);class rr extends ir{constructor(e,t){var s=t.track;const i=e.textTracks(),r=(t.label=s.label||s.language||"Unknown",t.selected="showing"===s.mode,super(e,t),this.track=s,this.kinds=(t.kinds||[t.kind||this.track.kind]).filter(Boolean),(...e)=>{this.handleTracksChange.apply(this,e)}),n=(...e)=>{this.handleSelectedLanguageChange.apply(this,e)};if(e.on(["loadstart","texttrackchange"],r),i.addEventListener("change",r),i.addEventListener("selectedlanguagechange",n),this.on("dispose",function(){e.off(["loadstart","texttrackchange"],r),i.removeEventListener("change",r),i.removeEventListener("selectedlanguagechange",n)}),void 0===i.onchange){let e;this.on(["tap","click"],function(){if("object"!=typeof window.Event)try{e=new window.Event("change")}catch(e){}e||(e=document.createEvent("Event")).initEvent("change",!0,!0),i.dispatchEvent(e)})}this.handleTracksChange()}handleClick(e){var t=this.track,s=this.player_.textTracks();if(super.handleClick(e),s)for(let e=0;e{this.items.forEach(e=>{e.selected(this.track_.activeCues[0]===e.cue)})}}buildCSSClass(){return"vjs-chapters-button "+super.buildCSSClass()}buildWrapperCSSClass(){return"vjs-chapters-button "+super.buildWrapperCSSClass()}update(e){e&&e.track&&"chapters"!==e.track.kind||((e=this.findChaptersTrack())!==this.track_?(this.setTrack(e),super.update()):(!this.items||e&&e.cues&&e.cues.length!==this.items.length)&&super.update())}setTrack(e){var t;this.track_!==e&&(this.updateHandler_||(this.updateHandler_=this.update.bind(this)),this.track_&&((t=this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_))&&t.removeEventListener("load",this.updateHandler_),this.track_.removeEventListener("cuechange",this.selectCurrentItem_),this.track_=null),this.track_=e,this.track_)&&(this.track_.mode="hidden",(t=this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_))&&t.addEventListener("load",this.updateHandler_),this.track_.addEventListener("cuechange",this.selectCurrentItem_))}findChaptersTrack(){var t=this.player_.textTracks()||[];for(let e=t.length-1;0<=e;e--){var s=t[e];if(s.kind===this.kind_)return s}}getMenuCaption(){return this.track_&&this.track_.label?this.track_.label:this.localize(y(this.kind_))}createMenu(){return this.options_.title=this.getMenuCaption(),super.createMenu()}createItems(){var s=[];if(this.track_){var i=this.track_.cues;if(i)for(let e=0,t=i.length;e{this.handleTracksChange.apply(this,e)});i.addEventListener("change",r),this.on("dispose",()=>{i.removeEventListener("change",r)})}createEl(e,t,s){e=super.createEl(e,t,s),t=e.querySelector(".vjs-menu-item-text");return 0<=["main-desc","description"].indexOf(this.options_.track.kind)&&(t.appendChild(p("span",{className:"vjs-icon-placeholder"},{"aria-hidden":!0})),t.appendChild(p("span",{className:"vjs-control-text",textContent:" "+this.localize("Descriptions")}))),e}handleClick(e){if(super.handleClick(e),this.track.enabled=!0,this.player_.tech_.featuresNativeAudioTracks){var t=this.player_.audioTracks();for(let e=0;ethis.update(e))}handleClick(e){super.handleClick(),this.player().playbackRate(this.rate)}update(e){this.selected(this.player().playbackRate()===this.rate)}}_r.prototype.contentElType="button",b.registerComponent("PlaybackRateMenuItem",_r);class fr extends er{constructor(e,t){super(e,t),this.menuButton_.el_.setAttribute("aria-describedby",this.labelElId_),this.updateVisibility(),this.updateLabel(),this.on(e,"loadstart",e=>this.updateVisibility(e)),this.on(e,"ratechange",e=>this.updateLabel(e)),this.on(e,"playbackrateschange",e=>this.handlePlaybackRateschange(e))}createEl(){var e=super.createEl();return this.labelElId_="vjs-playback-rate-value-label-"+this.id_,this.labelEl_=p("div",{className:"vjs-playback-rate-value",id:this.labelElId_,textContent:"1x"}),e.appendChild(this.labelEl_),e}dispose(){this.labelEl_=null,super.dispose()}buildCSSClass(){return"vjs-playback-rate "+super.buildCSSClass()}buildWrapperCSSClass(){return"vjs-playback-rate "+super.buildWrapperCSSClass()}createItems(){var t=this.playbackRates(),s=[];for(let e=t.length-1;0<=e;e--)s.push(new _r(this.player(),{rate:t[e]+"x"}));return s}handlePlaybackRateschange(e){this.update()}playbackRates(){var e=this.player();return e.playbackRates&&e.playbackRates()||[]}playbackRateSupported(){return this.player().tech_&&this.player().tech_.featuresPlaybackRate&&this.playbackRates()&&0{this.open(e)})}buildCSSClass(){return"vjs-error-display "+super.buildCSSClass()}content(){var e=this.player().error();return e?this.localize(e.message):""}}Tr.prototype.options_=Object.assign({},Wt.prototype.options_,{pauseOnOpen:!1,fillAlways:!0,temporary:!1,uncloseable:!0}),b.registerComponent("ErrorDisplay",Tr);const kr="vjs-text-track-settings";var xs=["#000","Black"],It=["#00F","Blue"],Cr=["#0FF","Cyan"],wr=["#0F0","Green"],Er=["#F0F","Magenta"],Sr=["#F00","Red"],xr=["#FFF","White"],jr=["#FF0","Yellow"],Pr=["1","Opaque"],Ir=["0.5","Semi-Transparent"],Mr=["0","Transparent"];const Ar={backgroundColor:{selector:".vjs-bg-color > select",id:"captions-background-color-%s",label:"Color",options:[xs,xr,Sr,wr,It,jr,Er,Cr]},backgroundOpacity:{selector:".vjs-bg-opacity > select",id:"captions-background-opacity-%s",label:"Opacity",options:[Pr,Ir,Mr]},color:{selector:".vjs-text-color > select",id:"captions-foreground-color-%s",label:"Color",options:[xr,xs,Sr,wr,It,jr,Er,Cr]},edgeStyle:{selector:".vjs-edge-style > select",id:"%s",label:"Text Edge Style",options:[["none","None"],["raised","Raised"],["depressed","Depressed"],["uniform","Uniform"],["dropshadow","Drop shadow"]]},fontFamily:{selector:".vjs-font-family > select",id:"captions-font-family-%s",label:"Font Family",options:[["proportionalSansSerif","Proportional Sans-Serif"],["monospaceSansSerif","Monospace Sans-Serif"],["proportionalSerif","Proportional Serif"],["monospaceSerif","Monospace Serif"],["casual","Casual"],["script","Script"],["small-caps","Small Caps"]]},fontPercent:{selector:".vjs-font-percent > select",id:"captions-font-size-%s",label:"Font Size",options:[["0.50","50%"],["0.75","75%"],["1.00","100%"],["1.25","125%"],["1.50","150%"],["1.75","175%"],["2.00","200%"],["3.00","300%"],["4.00","400%"]],default:2,parser:e=>"1.00"===e?null:Number(e)},textOpacity:{selector:".vjs-text-opacity > select",id:"captions-foreground-opacity-%s",label:"Opacity",options:[Pr,Ir]},windowColor:{selector:".vjs-window-color > select",id:"captions-window-color-%s",label:"Color"},windowOpacity:{selector:".vjs-window-opacity > select",id:"captions-window-opacity-%s",label:"Opacity",options:[Mr,Ir,Pr]}};function Or(e,t){if((e=t?t(e):e)&&"none"!==e)return e}Ar.windowColor.options=Ar.backgroundColor.options;class Lr extends Wt{constructor(e,t){t.temporary=!1,super(e,t),this.updateDisplay=this.updateDisplay.bind(this),this.fill(),this.hasBeenOpened_=this.hasBeenFilled_=!0,this.endDialog=p("p",{className:"vjs-control-text",textContent:this.localize("End of dialog window.")}),this.el().appendChild(this.endDialog),this.setDefaults(),void 0===t.persistTextTrackSettings&&(this.options_.persistTextTrackSettings=this.options_.playerOptions.persistTextTrackSettings),this.on(this.$(".vjs-done-button"),"click",()=>{this.saveSettings(),this.close()}),this.on(this.$(".vjs-default-button"),"click",()=>{this.setDefaults(),this.updateDisplay()}),W(Ar,e=>{this.on(this.$(e.selector),"change",this.updateDisplay)}),this.options_.persistTextTrackSettings&&this.restoreSettings()}dispose(){this.endDialog=null,super.dispose()}createElSelect_(e,t="",s="label"){e=Ar[e];const i=e.id.replace("%s",this.id_),r=[t,i].join(" ").trim();t="vjs_select_"+m++;return[`<${s} id="${i}"${"label"===s?` for="${t}" class="vjs-label"`:""}>`,this.localize(e.label),`${s}>`,``].concat(e.options.map(e=>{var t=i+"-"+e[1].replace(/\W+/g,"");return[``,this.localize(e[1])," "].join("")})).concat(" ").join("")}createElFgColor_(){var e="captions-text-legend-"+this.id_;return['',``,this.localize("Text")," ",'',this.createElSelect_("color",e)," ",'',this.createElSelect_("textOpacity",e)," "," "].join("")}createElBgColor_(){var e="captions-background-"+this.id_;return['',``,this.localize("Text Background")," ",'',this.createElSelect_("backgroundColor",e)," ",'',this.createElSelect_("backgroundOpacity",e)," "," "].join("")}createElWinColor_(){var e="captions-window-"+this.id_;return['',``,this.localize("Caption Area Background")," ",'',this.createElSelect_("windowColor",e)," ",'',this.createElSelect_("windowOpacity",e)," "," "].join("")}createElColors_(){return p("div",{className:"vjs-track-settings-colors",innerHTML:[this.createElFgColor_(),this.createElBgColor_(),this.createElWinColor_()].join("")})}createElFont_(){return p("div",{className:"vjs-track-settings-font",innerHTML:['',this.createElSelect_("fontPercent","","legend")," ",'',this.createElSelect_("edgeStyle","","legend")," ",'',this.createElSelect_("fontFamily","","legend")," "].join("")})}createElControls_(){var e=this.localize("restore all settings to the default values");return p("div",{className:"vjs-track-settings-controls",innerHTML:[``,this.localize("Reset"),` ${e} `," ",`${this.localize("Done")} `].join("")})}content(){return[this.createElColors_(),this.createElFont_(),this.createElControls_()]}label(){return this.localize("Caption Settings Dialog")}description(){return this.localize("Beginning of dialog window. Escape will cancel and close the window.")}buildCSSClass(){return super.buildCSSClass()+" vjs-text-track-settings"}getValues(){return X(Ar,(e,t,s)=>{i=this.$(t.selector),t=t.parser;var i=Or(i.options[i.options.selectedIndex].value,t);return void 0!==i&&(e[s]=i),e},{})}setValues(n){W(Ar,(e,t)=>{var s=this.$(e.selector),i=n[t],r=e.parser;if(i)for(let e=0;e{var t=e.hasOwnProperty("default")?e.default:0;this.$(e.selector).selectedIndex=t})}restoreSettings(){let e;try{e=JSON.parse(window.localStorage.getItem(kr))}catch(e){l.warn(e)}e&&this.setValues(e)}saveSettings(){if(this.options_.persistTextTrackSettings){var e=this.getValues();try{Object.keys(e).length?window.localStorage.setItem(kr,JSON.stringify(e)):window.localStorage.removeItem(kr)}catch(e){l.warn(e)}}}updateDisplay(){var e=this.player_.getChild("textTrackDisplay");e&&e.updateDisplay()}conditionalBlur_(){this.previouslyActiveEl_=null;var e=this.player_.controlBar,t=e&&e.subsCapsButton,e=e&&e.captionsButton;t?t.focus():e&&e.focus()}handleLanguagechange(){this.fill()}}b.registerComponent("TextTrackSettings",Lr);class Nr extends b{constructor(e,t){let s=t.ResizeObserver||window.ResizeObserver;super(e,h({createEl:!(s=null===t.ResizeObserver?!1:s),reportTouchActivity:!1},t)),this.ResizeObserver=t.ResizeObserver||window.ResizeObserver,this.loadListener_=null,this.resizeObserver_=null,this.debouncedHandler_=pt(()=>{this.resizeHandler()},100,!1,this),s?(this.resizeObserver_=new this.ResizeObserver(this.debouncedHandler_),this.resizeObserver_.observe(e.el())):(this.loadListener_=()=>{if(this.el_&&this.el_.contentWindow){const t=this.debouncedHandler_;let e=this.unloadListener_=function(){_(this,"resize",t),_(this,"unload",e),e=null};v(this.el_.contentWindow,"unload",e),v(this.el_.contentWindow,"resize",t)}},this.one("load",this.loadListener_))}createEl(){return super.createEl("iframe",{className:"vjs-resize-manager",tabIndex:-1,title:this.localize("No content")},{"aria-hidden":"true"})}resizeHandler(){this.player_&&this.player_.trigger&&this.player_.trigger("playerresize")}dispose(){this.debouncedHandler_&&this.debouncedHandler_.cancel(),this.resizeObserver_&&(this.player_.el()&&this.resizeObserver_.unobserve(this.player_.el()),this.resizeObserver_.disconnect()),this.loadListener_&&this.off("load",this.loadListener_),this.el_&&this.el_.contentWindow&&this.unloadListener_&&this.unloadListener_.call(this.el_.contentWindow),this.ResizeObserver=null,this.resizeObserver=null,this.debouncedHandler_=null,this.loadListener_=null,super.dispose()}}b.registerComponent("ResizeManager",Nr);const Dr={trackingThreshold:20,liveTolerance:15};class Br extends b{constructor(e,t){super(e,h(Dr,t,{createEl:!1})),this.trackLiveHandler_=()=>this.trackLive_(),this.handlePlay_=e=>this.handlePlay(e),this.handleFirstTimeupdate_=e=>this.handleFirstTimeupdate(e),this.handleSeeked_=e=>this.handleSeeked(e),this.seekToLiveEdge_=e=>this.seekToLiveEdge(e),this.reset_(),this.on(this.player_,"durationchange",e=>this.handleDurationchange(e)),this.on(this.player_,"canplay",()=>this.toggleTracking())}trackLive_(){var t=this.player_.seekable();if(t&&t.length){var t=Number(window.performance.now().toFixed(4)),s=-1===this.lastTime_?0:(t-this.lastTime_)/1e3,t=(this.lastTime_=t,this.pastSeekEnd_=this.pastSeekEnd()+s,this.liveCurrentTime()),s=this.player_.currentTime();let e=this.player_.paused()||this.seekedBehindLive_||Math.abs(t-s)>this.options_.liveTolerance;(e=this.timeupdateSeen_&&t!==1/0?e:!1)!==this.behindLiveEdge_&&(this.behindLiveEdge_=e,this.trigger("liveedgechange"))}}handleDurationchange(){this.toggleTracking()}toggleTracking(){this.player_.duration()===1/0&&this.liveWindow()>=this.options_.trackingThreshold?(this.player_.options_.liveui&&this.player_.addClass("vjs-liveui"),this.startTracking()):(this.player_.removeClass("vjs-liveui"),this.stopTracking())}startTracking(){this.isTracking()||(this.timeupdateSeen_||(this.timeupdateSeen_=this.player_.hasStarted()),this.trackingInterval_=this.setInterval(this.trackLiveHandler_,30),this.trackLive_(),this.on(this.player_,["play","pause"],this.trackLiveHandler_),this.timeupdateSeen_?this.on(this.player_,"seeked",this.handleSeeked_):(this.one(this.player_,"play",this.handlePlay_),this.one(this.player_,"timeupdate",this.handleFirstTimeupdate_)))}handleFirstTimeupdate(){this.timeupdateSeen_=!0,this.on(this.player_,"seeked",this.handleSeeked_)}handleSeeked(){var e=Math.abs(this.liveCurrentTime()-this.player_.currentTime());this.seekedBehindLive_=this.nextSeekedFromUser_&&2this.updateDom_()),this.updateDom_()}createEl(){return this.els={title:p("div",{className:"vjs-title-bar-title",id:"vjs-title-bar-title-"+m++}),description:p("div",{className:"vjs-title-bar-description",id:"vjs-title-bar-description-"+m++})},p("div",{className:"vjs-title-bar"},{},Y(this.els))}updateDom_(){var e=this.player_.tech_;const i=e&&e.el_,r={title:"aria-labelledby",description:"aria-describedby"};["title","description"].forEach(e=>{var t=this.state[e],s=this.els[e],e=r[e];Ve(s),t&&we(s,t),i&&(i.removeAttribute(e),t)&&i.setAttribute(e,s.id)}),this.state.title||this.state.description?this.show():this.hide()}update(e){this.setState(e)}dispose(){var e=this.player_.tech_,e=e&&e.el_;e&&(e.removeAttribute("aria-labelledby"),e.removeAttribute("aria-describedby")),super.dispose(),this.els=null}}b.registerComponent("TitleBar",Rr);function Hr(s){const i=s.el();if(!i.resetSourceWatch_){const t={},e=$r(s),r=t=>(...e)=>{e=t.apply(i,e);return Vr(s),e};["append","appendChild","insertAdjacentHTML"].forEach(e=>{i[e]&&(t[e]=i[e],i[e]=r(t[e]))}),Object.defineProperty(i,"innerHTML",h(e,{set:r(e.set)})),i.resetSourceWatch_=()=>{i.resetSourceWatch_=null,Object.keys(t).forEach(e=>{i[e]=t[e]}),Object.defineProperty(i,"innerHTML",e)},s.one("sourceset",i.resetSourceWatch_)}}function Fr(s){if(s.featuresSourceset){const i=s.el();if(!i.resetSourceset_){e=s;const t=qr([e.el(),window.HTMLMediaElement.prototype,Ur],"src");var e;const r=i.setAttribute,n=i.load;Object.defineProperty(i,"src",h(t,{set:e=>{e=t.set.call(i,e);return s.triggerSourceset(i.src),e}})),i.setAttribute=(e,t)=>{t=r.call(i,e,t);return/src/i.test(e)&&s.triggerSourceset(i.src),t},i.load=()=>{var e=n.call(i);return Vr(s)||(s.triggerSourceset(""),Hr(s)),e},i.currentSrc?s.triggerSourceset(i.currentSrc):Vr(s)||Hr(s),i.resetSourceset_=()=>{i.resetSourceset_=null,i.load=n,i.setAttribute=r,Object.defineProperty(i,"src",t),i.resetSourceWatch_&&i.resetSourceWatch_()}}}}const Vr=t=>{var e=t.el();if(e.hasAttribute("src"))t.triggerSourceset(e.src);else{var s=t.$$("source"),i=[];let e="";if(!s.length)return!1;for(let e=0;e{let i={};for(let e=0;eqr([e.el(),window.HTMLMediaElement.prototype,window.Element.prototype,zr],"innerHTML"),Ur=Object.defineProperty({},"src",{get(){return this.hasAttribute("src")?ns(window.Element.prototype.getAttribute.call(this,"src")):""},set(e){return window.Element.prototype.setAttribute.call(this,"src",e),e}});class I extends j{constructor(e,t){super(e,t);t=e.source;let s=!1;if(this.featuresVideoFrameCallback=this.featuresVideoFrameCallback&&"VIDEO"===this.el_.tagName,t&&(this.el_.currentSrc!==t.src||e.tag&&3===e.tag.initNetworkState_)?this.setSource(t):this.handleLateInit_(this.el_),e.enableSourceset&&this.setupSourcesetHandling_(),this.isScrubbing_=!1,this.el_.hasChildNodes()){var i=this.el_.childNodes;let e=i.length;for(var r=[];e--;){var n=i[e];"track"===n.nodeName.toLowerCase()&&(this.featuresNativeTextTracks?(this.remoteTextTrackEls().addTrackElement_(n),this.remoteTextTracks().addTrack(n.track),this.textTracks().addTrack(n.track),s||this.el_.hasAttribute("crossorigin")||!as(n.src)||(s=!0)):r.push(n))}for(let e=0;e{i=[];for(let e=0;es.removeEventListener("change",e)),()=>{for(let e=0;e{s.removeEventListener("change",e),s.removeEventListener("change",r),s.addEventListener("change",r)}),this.on("webkitendfullscreen",()=>{s.removeEventListener("change",e),s.addEventListener("change",e),s.removeEventListener("change",r)})}overrideNative_(e,t){if(t===this[`featuresNative${e}Tracks`]){const s=e.toLowerCase();this[s+"TracksListeners_"]&&Object.keys(this[s+"TracksListeners_"]).forEach(e=>{this.el()[s+"Tracks"].removeEventListener(e,this[s+"TracksListeners_"][e])}),this[`featuresNative${e}Tracks`]=!t,this[s+"TracksListeners_"]=null,this.proxyNativeTracksForType_(s)}}overrideNativeAudioTracks(e){this.overrideNative_("Audio",e)}overrideNativeVideoTracks(e){this.overrideNative_("Video",e)}proxyNativeTracksForType_(s){var e=w[s];const i=this.el()[e.getterName],r=this[e.getterName]();if(this[`featuresNative${e.capitalName}Tracks`]&&i&&i.addEventListener){const n={change:e=>{var t={type:"change",target:r,currentTarget:r,srcElement:r};r.trigger(t),"text"===s&&this[Ss.remoteText.getterName]().trigger(t)},addtrack(e){r.addTrack(e.track)},removetrack(e){r.removeTrack(e.track)}},t=function(){var e=[];for(let s=0;s{const s=n[t];i.addEventListener(t,s),this.on("dispose",e=>i.removeEventListener(t,s))}),this.on("loadstart",t),this.on("dispose",e=>this.off("loadstart",t))}}proxyNativeTracks_(){w.names.forEach(e=>{this.proxyNativeTracksForType_(e)})}createEl(){let t=this.options_.tag;t&&(this.options_.playerElIngest||this.movingMediaElementInDOM)||(t?(e=t.cloneNode(!0),t.parentNode&&t.parentNode.insertBefore(e,t),I.disposeMediaElement(t),t=e):(t=document.createElement("video"),e=h({},this.options_.tag&&Me(this.options_.tag)),me&&!0===this.options_.nativeControlsForTouch||delete e.controls,Ie(t,Object.assign(e,{id:this.options_.techId,class:"vjs-tech"}))),t.playerId=this.options_.playerId),"undefined"!=typeof this.options_.preload&&Oe(t,"preload",this.options_.preload),void 0!==this.options_.disablePictureInPicture&&(t.disablePictureInPicture=this.options_.disablePictureInPicture);var e,s=["loop","muted","playsinline","autoplay"];for(let e=0;e{0{this.off("webkitbeginfullscreen",t),this.off("webkitendfullscreen",e)})}}supportsFullScreen(){return"function"==typeof this.el_.webkitEnterFullScreen}enterFullScreen(){const e=this.el_;if(e.paused&&e.networkState<=e.HAVE_METADATA)k(this.el_.play()),this.setTimeout(function(){e.pause();try{e.webkitEnterFullScreen()}catch(e){this.trigger("fullscreenerror",e)}},0);else try{e.webkitEnterFullScreen()}catch(e){this.trigger("fullscreenerror",e)}}exitFullScreen(){this.el_.webkitDisplayingFullscreen?this.el_.webkitExitFullScreen():this.trigger("fullscreenerror",new Error("The video is not fullscreen"))}requestPictureInPicture(){return this.el_.requestPictureInPicture()}requestVideoFrameCallback(e){return this.featuresVideoFrameCallback&&!this.el_.webkitKeys?this.el_.requestVideoFrameCallback(e):super.requestVideoFrameCallback(e)}cancelVideoFrameCallback(e){this.featuresVideoFrameCallback&&!this.el_.webkitKeys?this.el_.cancelVideoFrameCallback(e):super.cancelVideoFrameCallback(e)}src(e){if(void 0===e)return this.el_.src;this.setSrc(e)}reset(){I.resetMediaElement(this.el_)}currentSrc(){return this.currentSource_?this.currentSource_.src:this.el_.currentSrc}setControls(e){this.el_.controls=!!e}addTextTrack(e,t,s){return this.featuresNativeTextTracks?this.el_.addTextTrack(e,t,s):super.addTextTrack(e,t,s)}createRemoteTextTrack(e){var t;return this.featuresNativeTextTracks?(t=document.createElement("track"),e.kind&&(t.kind=e.kind),e.label&&(t.label=e.label),(e.language||e.srclang)&&(t.srclang=e.language||e.srclang),e.default&&(t.default=e.default),e.id&&(t.id=e.id),e.src&&(t.src=e.src),t):super.createRemoteTextTrack(e)}addRemoteTextTrack(e,t){e=super.addRemoteTextTrack(e,t);return this.featuresNativeTextTracks&&this.el().appendChild(e),e}removeRemoteTextTrack(t){if(super.removeRemoteTextTrack(t),this.featuresNativeTextTracks){var s=this.$$("track");let e=s.length;for(;e--;)t!==s[e]&&t!==s[e].track||this.el().removeChild(s[e])}}getVideoPlaybackQuality(){var e;return"function"==typeof this.el().getVideoPlaybackQuality?this.el().getVideoPlaybackQuality():(e={},"undefined"!=typeof this.el().webkitDroppedFrameCount&&"undefined"!=typeof this.el().webkitDecodedFrameCount&&(e.droppedVideoFrames=this.el().webkitDroppedFrameCount,e.totalVideoFrames=this.el().webkitDecodedFrameCount),window.performance&&(e.creationTime=window.performance.now()),e)}}Q(I,"TEST_VID",function(){var e,t;if(be())return e=document.createElement("video"),(t=document.createElement("track")).kind="captions",t.srclang="en",t.label="English",e.appendChild(t),e}),I.isSupported=function(){try{I.TEST_VID.volume=.5}catch(e){return!1}return!(!I.TEST_VID||!I.TEST_VID.canPlayType)},I.canPlayType=function(e){return I.TEST_VID.canPlayType(e)},I.canPlaySource=function(e,t){return I.canPlayType(e.type)},I.canControlVolume=function(){try{const t=I.TEST_VID.volume;I.TEST_VID.volume=t/2+.1;var e=t!==I.TEST_VID.volume;return e&&u?(window.setTimeout(()=>{I&&I.prototype&&(I.prototype.featuresVolumeControl=t!==I.TEST_VID.volume)}),!1):e}catch(e){return!1}},I.canMuteVolume=function(){try{var e=I.TEST_VID.muted;return I.TEST_VID.muted=!e,I.TEST_VID.muted?Oe(I.TEST_VID,"muted","muted"):Le(I.TEST_VID,"muted"),e!==I.TEST_VID.muted}catch(e){return!1}},I.canControlPlaybackRate=function(){if(o&&c&&ae<58)return!1;try{var e=I.TEST_VID.playbackRate;return I.TEST_VID.playbackRate=e/2+.1,e!==I.TEST_VID.playbackRate}catch(e){return!1}},I.canOverrideAttributes=function(){try{var e=()=>{};Object.defineProperty(document.createElement("video"),"src",{get:e,set:e}),Object.defineProperty(document.createElement("audio"),"src",{get:e,set:e}),Object.defineProperty(document.createElement("video"),"innerHTML",{get:e,set:e}),Object.defineProperty(document.createElement("audio"),"innerHTML",{get:e,set:e})}catch(e){return!1}return!0},I.supportsNativeTextTracks=function(){return _e||u&&c},I.supportsNativeVideoTracks=function(){return!(!I.TEST_VID||!I.TEST_VID.videoTracks)},I.supportsNativeAudioTracks=function(){return!(!I.TEST_VID||!I.TEST_VID.audioTracks)},I.Events=["loadstart","suspend","abort","error","emptied","stalled","loadedmetadata","loadeddata","canplay","canplaythrough","playing","waiting","seeking","seeked","ended","durationchange","timeupdate","progress","play","pause","ratechange","resize","volumechange"],[["featuresMuteControl","canMuteVolume"],["featuresPlaybackRate","canControlPlaybackRate"],["featuresSourceset","canOverrideAttributes"],["featuresNativeTextTracks","supportsNativeTextTracks"],["featuresNativeVideoTracks","supportsNativeVideoTracks"],["featuresNativeAudioTracks","supportsNativeAudioTracks"]].forEach(function([e,t]){Q(I.prototype,e,()=>I[t](),!0)}),I.prototype.featuresVolumeControl=I.canControlVolume(),I.prototype.movingMediaElementInDOM=!u,I.prototype.featuresFullscreenResize=!0,I.prototype.featuresProgressEvents=!0,I.prototype.featuresTimeupdateEvents=!0,I.prototype.featuresVideoFrameCallback=!(!I.TEST_VID||!I.TEST_VID.requestVideoFrameCallback),I.disposeMediaElement=function(e){if(e){for(e.parentNode&&e.parentNode.removeChild(e);e.hasChildNodes();)e.removeChild(e.firstChild);if(e.removeAttribute("src"),"function"==typeof e.load)try{e.load()}catch(e){}}},I.resetMediaElement=function(t){if(t){var s=t.querySelectorAll("source");let e=s.length;for(;e--;)t.removeChild(s[e]);if(t.removeAttribute("src"),"function"==typeof t.load)try{t.load()}catch(e){}}},["muted","defaultMuted","autoplay","controls","loop","playsinline"].forEach(function(e){I.prototype[e]=function(){return this.el_[e]||this.el_.hasAttribute(e)}}),["muted","defaultMuted","autoplay","loop","playsinline"].forEach(function(t){I.prototype["set"+y(t)]=function(e){(this.el_[t]=e)?this.el_.setAttribute(t,t):this.el_.removeAttribute(t)}}),["paused","currentTime","buffered","volume","poster","preload","error","seeking","seekable","ended","playbackRate","defaultPlaybackRate","disablePictureInPicture","played","networkState","readyState","videoWidth","videoHeight","crossOrigin"].forEach(function(e){I.prototype[e]=function(){return this.el_[e]}}),["volume","src","poster","preload","playbackRate","defaultPlaybackRate","disablePictureInPicture","crossOrigin"].forEach(function(t){I.prototype["set"+y(t)]=function(e){this.el_[t]=e}}),["pause","load","play"].forEach(function(e){I.prototype[e]=function(){return this.el_[e]()}}),j.withSourceHandlers(I),I.nativeSourceHandler={},I.nativeSourceHandler.canPlayType=function(e){try{return I.TEST_VID.canPlayType(e)}catch(e){return""}},I.nativeSourceHandler.canHandleSource=function(e,t){return e.type?I.nativeSourceHandler.canPlayType(e.type):e.src?(e=os(e.src),I.nativeSourceHandler.canPlayType("video/"+e)):""},I.nativeSourceHandler.handleSource=function(e,t,s){t.setSrc(e.src)},I.nativeSourceHandler.dispose=function(){},I.registerSourceHandler(I.nativeSourceHandler),j.registerTech("Html5",I);const Kr=["progress","abort","suspend","emptied","stalled","loadedmetadata","loadeddata","timeupdate","resize","volumechange","texttrackchange"],Wr={canplay:"CanPlay",canplaythrough:"CanPlayThrough",playing:"Playing",seeked:"Seeked"},Xr=["tiny","xsmall","small","medium","large","xlarge","huge"],Gr={},Yr=(Xr.forEach(e=>{var t="x"===e.charAt(0)?"x-"+e.substring(1):e;Gr[e]="vjs-layout-"+t}),{tiny:210,xsmall:320,small:425,medium:768,large:1440,xlarge:2560,huge:1/0});class M extends b{constructor(e,t,s){if(e.id=e.id||t.id||"vjs_video_"+m++,(t=Object.assign(M.getTagSettings(e),t)).initChildren=!1,t.createEl=!1,t.evented=!1,t.reportTouchActivity=!1,t.language||(i=e.closest("[lang]"))&&(t.language=i.getAttribute("lang")),super(null,t,s),this.boundDocumentFullscreenChange_=e=>this.documentFullscreenChange_(e),this.boundFullWindowOnEscKey_=e=>this.fullWindowOnEscKey(e),this.boundUpdateStyleEl_=e=>this.updateStyleEl_(e),this.boundApplyInitTime_=e=>this.applyInitTime_(e),this.boundUpdateCurrentBreakpoint_=e=>this.updateCurrentBreakpoint_(e),this.boundHandleTechClick_=e=>this.handleTechClick_(e),this.boundHandleTechDoubleClick_=e=>this.handleTechDoubleClick_(e),this.boundHandleTechTouchStart_=e=>this.handleTechTouchStart_(e),this.boundHandleTechTouchMove_=e=>this.handleTechTouchMove_(e),this.boundHandleTechTouchEnd_=e=>this.handleTechTouchEnd_(e),this.boundHandleTechTap_=e=>this.handleTechTap_(e),this.isFullscreen_=!1,this.log=U(this.id_),this.fsApi_=F,this.isPosterFromTech_=!1,this.queuedCallbacks_=[],this.isReady_=!1,this.hasStarted_=!1,this.userActive_=!1,this.debugEnabled_=!1,this.audioOnlyMode_=!1,this.audioPosterMode_=!1,this.audioOnlyCache_={playerHeight:null,hiddenChildren:[]},!this.options_||!this.options_.techOrder||!this.options_.techOrder.length)throw new Error("No techOrder specified. Did you overwrite videojs.options instead of just changing the properties you want to override?");if(this.tag=e,this.tagAttributes=e&&Me(e),this.language(this.options_.language),t.languages){const r={};Object.getOwnPropertyNames(t.languages).forEach(function(e){r[e.toLowerCase()]=t.languages[e]}),this.languages_=r}else this.languages_=M.prototype.options_.languages;this.resetCache_(),this.poster_=t.poster||"",this.controls_=!!t.controls,e.controls=!1,e.removeAttribute("controls"),this.changingSrc_=!1,this.playCallbacks_=[],this.playTerminatedQueue_=[],e.hasAttribute("autoplay")?this.autoplay(!0):this.autoplay(this.options_.autoplay),t.plugins&&Object.keys(t.plugins).forEach(e=>{if("function"!=typeof this[e])throw new Error(`plugin "${e}" does not exist`)}),this.scrubbing_=!1,this.el_=this.createEl(),wt(this,{eventBusKey:"el_"}),this.fsApi_.requestFullscreen&&(v(document,this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_),this.on(this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_)),this.fluid_&&this.on(["playerreset","resize"],this.boundUpdateStyleEl_);var i=h(this.options_),s=(t.plugins&&Object.keys(t.plugins).forEach(e=>{this[e](t.plugins[e])}),t.debug&&this.debug(!0),this.options_.playerOptions=i,this.middleware_=[],this.playbackRates(t.playbackRates),t.experimentalSvgIcons&&((s=(new window.DOMParser).parseFromString('\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ',"image/svg+xml")).querySelector("parsererror")?(l.warn("Failed to load SVG Icons. Falling back to Font Icons."),this.options_.experimentalSvgIcons=null):((i=s.documentElement).style.display="none",this.el_.appendChild(i),this.addClass("vjs-svg-icons-enabled"))),this.initChildren(),this.isAudio("audio"===e.nodeName.toLowerCase()),this.controls()?this.addClass("vjs-controls-enabled"):this.addClass("vjs-controls-disabled"),this.el_.setAttribute("role","region"),this.isAudio()?this.el_.setAttribute("aria-label",this.localize("Audio Player")):this.el_.setAttribute("aria-label",this.localize("Video Player")),this.isAudio()&&this.addClass("vjs-audio"),me&&this.addClass("vjs-touch-enabled"),u||this.addClass("vjs-workinghover"),M.players[this.id_]=this,D.split(".")[0]);this.addClass("vjs-v"+s),this.userActive(!0),this.reportUserActivity(),this.one("play",e=>this.listenForUserActivity_(e)),this.on("keydown",e=>this.handleKeyDown(e)),this.on("languagechange",e=>this.handleLanguagechange(e)),this.breakpoints(this.options_.breakpoints),this.responsive(this.options_.responsive),this.on("ready",()=>{this.audioPosterMode(this.options_.audioPosterMode),this.audioOnlyMode(this.options_.audioOnlyMode)})}dispose(){var e;this.trigger("dispose"),this.off("dispose"),_(document,this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_),_(document,"keydown",this.boundFullWindowOnEscKey_),this.styleEl_&&this.styleEl_.parentNode&&(this.styleEl_.parentNode.removeChild(this.styleEl_),this.styleEl_=null),M.players[this.id_]=null,this.tag&&this.tag.player&&(this.tag.player=null),this.el_&&this.el_.player&&(this.el_.player=null),this.tech_&&(this.tech_.dispose(),this.isPosterFromTech_=!1,this.poster_=""),this.playerElIngest_&&(this.playerElIngest_=null),this.tag&&(this.tag=null),e=this,si[e.id()]=null,E.names.forEach(e=>{e=this[E[e].getterName]();e&&e.off&&e.off()}),super.dispose({restoreEl:this.options_.restoreEl})}createEl(){let t=this.tag,s,e=this.playerElIngest_=t.parentNode&&t.parentNode.hasAttribute&&t.parentNode.hasAttribute("data-vjs-player");const i="video-js"===this.tag.tagName.toLowerCase(),r=(e?s=this.el_=t.parentNode:i||(s=this.el_=super.createEl("div")),Me(t));if(i){for(s=this.el_=t,t=this.tag=document.createElement("video");s.children.length;)t.appendChild(s.firstChild);Se(s,"video-js")||xe(s,"video-js"),s.appendChild(t),e=this.playerElIngest_=s,Object.keys(s).forEach(e=>{try{t[e]=s[e]}catch(e){}})}t.setAttribute("tabindex","-1"),r.tabindex="-1",c&&he&&(t.setAttribute("role","application"),r.role="application"),t.removeAttribute("width"),t.removeAttribute("height"),"width"in r&&delete r.width,"height"in r&&delete r.height,Object.getOwnPropertyNames(r).forEach(function(e){i&&"class"===e||s.setAttribute(e,r[e]),i&&t.setAttribute(e,r[e])}),t.playerId=t.id,t.id+="_html5_api",t.className="vjs-tech",(t.player=s.player=this).addClass("vjs-paused");var n,a=["IS_SMART_TV","IS_TIZEN","IS_WEBOS","IS_ANDROID","IS_IPAD","IS_IPHONE"].filter(e=>fe[e]).map(e=>"vjs-device-"+e.substring(3).toLowerCase().replace(/\_/g,"-")),o=(this.addClass(...a),!0!==window.VIDEOJS_NO_DYNAMIC_STYLE&&(this.styleEl_=st("vjs-styles-dimensions"),a=Ke(".vjs-styles-defaults"),(n=Ke("head")).insertBefore(this.styleEl_,a?a.nextSibling:n.firstChild)),this.fill_=!1,this.fluid_=!1,this.width(this.options_.width),this.height(this.options_.height),this.fill(this.options_.fill),this.fluid(this.options_.fluid),this.aspectRatio(this.options_.aspectRatio),this.crossOrigin(this.options_.crossOrigin||this.options_.crossorigin),t.getElementsByTagName("a"));for(let e=0;e{this.on(["playerreset","resize"],this.boundUpdateStyleEl_)},vt(e)?t():(e.eventedCallbacks||(e.eventedCallbacks=[]),e.eventedCallbacks.push(t))):this.removeClass("vjs-fluid"),this.updateStyleEl_()}fill(e){if(void 0===e)return!!this.fill_;this.fill_=!!e,e?(this.addClass("vjs-fill"),this.fluid(!1)):this.removeClass("vjs-fill")}aspectRatio(e){if(void 0===e)return this.aspectRatio_;if(!/^\d+\:\d+$/.test(e))throw new Error("Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.");this.aspectRatio_=e,this.fluid(!0),this.updateStyleEl_()}updateStyleEl_(){if(!0===window.VIDEOJS_NO_DYNAMIC_STYLE){const e="number"==typeof this.width_?this.width_:this.options_.width,t="number"==typeof this.height_?this.height_:this.options_.height;var r=this.tech_&&this.tech_.el();void(r&&(0<=e&&(r.width=e),0<=t)&&(r.height=t))}else{let e,t,s,i;r=(s=void 0!==this.aspectRatio_&&"auto"!==this.aspectRatio_?this.aspectRatio_:0{e=E[e];n[e.getterName]=this[e.privateName]}),Object.assign(n,this.options_[s]),Object.assign(n,this.options_[i]),Object.assign(n,this.options_[e.toLowerCase()]),this.tag&&(n.tag=this.tag),t&&t.src===this.cache_.src&&0{this.on(this.tech_,t,e=>this[`handleTech${y(t)}_`](e))}),Object.keys(Wr).forEach(t=>{this.on(this.tech_,t,e=>{0===this.tech_.playbackRate()&&this.tech_.seeking()?this.queuedCallbacks_.push({callback:this[`handleTech${Wr[t]}_`].bind(this),event:e}):this[`handleTech${Wr[t]}_`](e)})}),this.on(this.tech_,"loadstart",e=>this.handleTechLoadStart_(e)),this.on(this.tech_,"sourceset",e=>this.handleTechSourceset_(e)),this.on(this.tech_,"waiting",e=>this.handleTechWaiting_(e)),this.on(this.tech_,"ended",e=>this.handleTechEnded_(e)),this.on(this.tech_,"seeking",e=>this.handleTechSeeking_(e)),this.on(this.tech_,"play",e=>this.handleTechPlay_(e)),this.on(this.tech_,"pause",e=>this.handleTechPause_(e)),this.on(this.tech_,"durationchange",e=>this.handleTechDurationChange_(e)),this.on(this.tech_,"fullscreenchange",(e,t)=>this.handleTechFullscreenChange_(e,t)),this.on(this.tech_,"fullscreenerror",(e,t)=>this.handleTechFullscreenError_(e,t)),this.on(this.tech_,"enterpictureinpicture",e=>this.handleTechEnterPictureInPicture_(e)),this.on(this.tech_,"leavepictureinpicture",e=>this.handleTechLeavePictureInPicture_(e)),this.on(this.tech_,"error",e=>this.handleTechError_(e)),this.on(this.tech_,"posterchange",e=>this.handleTechPosterChange_(e)),this.on(this.tech_,"textdata",e=>this.handleTechTextData_(e)),this.on(this.tech_,"ratechange",e=>this.handleTechRateChange_(e)),this.on(this.tech_,"loadedmetadata",this.boundUpdateStyleEl_),this.usingNativeControls(this.techGet_("controls")),this.controls()&&!this.usingNativeControls()&&this.addTechControlsListeners_(),this.tech_.el().parentNode===this.el()||"Html5"===s&&this.tag||Ee(this.tech_.el(),this.el()),this.tag&&(this.tag.player=null,this.tag=null)}unloadTech_(){E.names.forEach(e=>{e=E[e];this[e.privateName]=this[e.getterName]()}),this.textTracksJson_=$t(this.tech_),this.isReady_=!1,this.tech_.dispose(),this.tech_=!1,this.isPosterFromTech_&&(this.poster_="",this.trigger("posterchange")),this.isPosterFromTech_=!1}tech(e){return void 0===e&&l.warn("Using the tech directly can be dangerous. I hope you know what you're doing.\nSee https://github.com/videojs/video.js/issues/2617 for more info.\n"),this.tech_}version(){return{"video.js":D}}addTechControlsListeners_(){this.removeTechControlsListeners_(),this.on(this.tech_,"click",this.boundHandleTechClick_),this.on(this.tech_,"dblclick",this.boundHandleTechDoubleClick_),this.on(this.tech_,"touchstart",this.boundHandleTechTouchStart_),this.on(this.tech_,"touchmove",this.boundHandleTechTouchMove_),this.on(this.tech_,"touchend",this.boundHandleTechTouchEnd_),this.on(this.tech_,"tap",this.boundHandleTechTap_)}removeTechControlsListeners_(){this.off(this.tech_,"tap",this.boundHandleTechTap_),this.off(this.tech_,"touchstart",this.boundHandleTechTouchStart_),this.off(this.tech_,"touchmove",this.boundHandleTechTouchMove_),this.off(this.tech_,"touchend",this.boundHandleTechTouchEnd_),this.off(this.tech_,"click",this.boundHandleTechClick_),this.off(this.tech_,"dblclick",this.boundHandleTechDoubleClick_)}handleTechReady_(){this.triggerReady(),this.cache_.volume&&this.techCall_("setVolume",this.cache_.volume),this.handleTechPosterChange_(),this.handleTechDurationChange_()}handleTechLoadStart_(){this.removeClass("vjs-ended","vjs-seeking"),this.error(null),this.handleTechDurationChange_(),this.paused()&&this.hasStarted(!1),this.trigger("loadstart"),this.manualAutoplay_(!0===this.autoplay()&&this.options_.normalizeAutoplay?"play":this.autoplay())}manualAutoplay_(t){if(this.tech_&&"string"==typeof t){var s=()=>{const e=this.muted(),t=(this.muted(!0),()=>{this.muted(e)});this.playTerminatedQueue_.push(t);var s=this.play();if(zt(s))return s.catch(e=>{throw t(),new Error("Rejection at manualAutoplay. Restoring muted value. "+(e||""))})};let e;if("any"!==t||this.muted()?e="muted"!==t||this.muted()?this.play():s():zt(e=this.play())&&(e=e.catch(s)),zt(e))return e.then(()=>{this.trigger({type:"autoplay-success",autoplay:t})}).catch(()=>{this.trigger({type:"autoplay-failure",autoplay:t})})}}updateSourceCaches_(e=""){let t=e,s="";"string"!=typeof t&&(t=e.src,s=e.type),this.cache_.source=this.cache_.source||{},this.cache_.sources=this.cache_.sources||[],t&&!s&&(s=((e,t)=>{if(!t)return"";if(e.cache_.source.src===t&&e.cache_.source.type)return e.cache_.source.type;var s=e.cache_.sources.filter(e=>e.src===t);if(s.length)return s[0].type;var i=e.$$("source");for(let e=0;ee.src&&e.src===t),i=[],r=this.$$("source"),n=[];for(let e=0;ethis.updateSourceCaches_(e);var s=this.currentSource().src,i=t.src;(e=!s||/^blob:/.test(s)||!/^blob:/.test(i)||this.lastSource_&&(this.lastSource_.tech===i||this.lastSource_.player===s)?e:()=>{})(i),t.src||this.tech_.any(["sourceset","loadstart"],e=>{"sourceset"!==e.type&&(e=this.techGet_("currentSrc"),this.lastSource_.tech=e,this.updateSourceCaches_(e))})}this.lastSource_={player:this.currentSource().src,tech:t.src},this.trigger({src:t.src,type:"sourceset"})}hasStarted(e){if(void 0===e)return this.hasStarted_;e!==this.hasStarted_&&(this.hasStarted_=e,this.hasStarted_?this.addClass("vjs-has-started"):this.removeClass("vjs-has-started"))}handleTechPlay_(){this.removeClass("vjs-ended","vjs-paused"),this.addClass("vjs-playing"),this.hasStarted(!0),this.trigger("play")}handleTechRateChange_(){0e.callback(e.event)),this.queuedCallbacks_=[]),this.cache_.lastPlaybackRate=this.tech_.playbackRate(),this.trigger("ratechange")}handleTechWaiting_(){this.addClass("vjs-waiting"),this.trigger("waiting");const e=this.currentTime(),t=()=>{e!==this.currentTime()&&(this.removeClass("vjs-waiting"),this.off("timeupdate",t))};this.on("timeupdate",t)}handleTechCanPlay_(){this.removeClass("vjs-waiting"),this.trigger("canplay")}handleTechCanPlayThrough_(){this.removeClass("vjs-waiting"),this.trigger("canplaythrough")}handleTechPlaying_(){this.removeClass("vjs-waiting"),this.trigger("playing")}handleTechSeeking_(){this.addClass("vjs-seeking"),this.trigger("seeking")}handleTechSeeked_(){this.removeClass("vjs-seeking","vjs-ended"),this.trigger("seeked")}handleTechPause_(){this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.trigger("pause")}handleTechEnded_(){this.addClass("vjs-ended"),this.removeClass("vjs-waiting"),this.options_.loop?(this.currentTime(0),this.play()):this.paused()||this.pause(),this.trigger("ended")}handleTechDurationChange_(){this.duration(this.techGet_("duration"))}handleTechClick_(e){!this.controls_||void 0!==this.options_&&void 0!==this.options_.userActions&&void 0!==this.options_.userActions.click&&!1===this.options_.userActions.click||(void 0!==this.options_&&void 0!==this.options_.userActions&&"function"==typeof this.options_.userActions.click?this.options_.userActions.click.call(this,e):this.paused()?k(this.play()):this.pause())}handleTechDoubleClick_(t){!this.controls_||Array.prototype.some.call(this.$$(".vjs-control-bar, .vjs-modal-dialog"),e=>e.contains(t.target))||void 0!==this.options_&&void 0!==this.options_.userActions&&void 0!==this.options_.userActions.doubleClick&&!1===this.options_.userActions.doubleClick||(void 0!==this.options_&&void 0!==this.options_.userActions&&"function"==typeof this.options_.userActions.doubleClick?this.options_.userActions.doubleClick.call(this,t):this.isFullscreen()?this.exitFullscreen():this.requestFullscreen())}handleTechTap_(){this.userActive(!this.userActive())}handleTechTouchStart_(){this.userWasActive=this.userActive()}handleTechTouchMove_(){this.userWasActive&&this.reportUserActivity()}handleTechTouchEnd_(e){e.cancelable&&e.preventDefault()}toggleFullscreenClass_(){this.isFullscreen()?this.addClass("vjs-fullscreen"):this.removeClass("vjs-fullscreen")}documentFullscreenChange_(t){t=t.target.player;if(!t||t===this){t=this.el();let e=document[this.fsApi_.fullscreenElement]===t;!e&&t.matches&&(e=t.matches(":"+this.fsApi_.fullscreen)),this.isFullscreen(e)}}handleTechFullscreenChange_(e,t){t&&(t.nativeIOSFullscreen&&(this.addClass("vjs-ios-native-fs"),this.tech_.one("webkitendfullscreen",()=>{this.removeClass("vjs-ios-native-fs")})),this.isFullscreen(t.isFullscreen))}handleTechFullscreenError_(e,t){this.trigger("fullscreenerror",t)}togglePictureInPictureClass_(){this.isInPictureInPicture()?this.addClass("vjs-picture-in-picture"):this.removeClass("vjs-picture-in-picture")}handleTechEnterPictureInPicture_(e){this.isInPictureInPicture(!0)}handleTechLeavePictureInPicture_(e){this.isInPictureInPicture(!1)}handleTechError_(){var e=this.tech_.error();e&&this.error(e)}handleTechTextData_(){let e=1{this.play_(e)})}play_(e=k){this.playCallbacks_.push(e);var t,e=Boolean(!this.changingSrc_&&(this.src()||this.currentSrc())),s=Boolean(_e||u);this.waitToPlay_&&(this.off(["ready","loadstart"],this.waitToPlay_),this.waitToPlay_=null),this.isReady_&&e?(t=this.techGet_("play"),s&&this.hasClass("vjs-ended")&&this.resetProgressBar_(),null===t?this.runPlayTerminatedQueue_():this.runPlayCallbacks_(t)):(this.waitToPlay_=e=>{this.play_()},this.one(["ready","loadstart"],this.waitToPlay_),!e&&s&&this.load())}runPlayTerminatedQueue_(){var e=this.playTerminatedQueue_.slice(0);this.playTerminatedQueue_=[],e.forEach(function(e){e()})}runPlayCallbacks_(t){var e=this.playCallbacks_.slice(0);this.playCallbacks_=[],this.playTerminatedQueue_=[],e.forEach(function(e){e(t)})}pause(){this.techCall_("pause")}paused(){return!1!==this.techGet_("paused")}played(){return this.techGet_("played")||T(0,0)}scrubbing(e){if("undefined"==typeof e)return this.scrubbing_;this.scrubbing_=!!e,this.techCall_("setScrubbing",this.scrubbing_),e?this.addClass("vjs-scrubbing"):this.removeClass("vjs-scrubbing")}currentTime(e){if(void 0===e)return this.cache_.currentTime=this.techGet_("currentTime")||0,this.cache_.currentTime;e<0&&(e=0),this.isReady_&&!this.changingSrc_&&this.tech_&&this.tech_.isReady_?(this.techCall_("setCurrentTime",e),this.cache_.initTime=0,isFinite(e)&&(this.cache_.currentTime=Number(e))):(this.cache_.initTime=e,this.off("canplay",this.boundApplyInitTime_),this.one("canplay",this.boundApplyInitTime_))}applyInitTime_(){this.currentTime(this.cache_.initTime)}duration(e){if(void 0===e)return void 0!==this.cache_.duration?this.cache_.duration:NaN;(e=(e=parseFloat(e))<0?1/0:e)!==this.cache_.duration&&((this.cache_.duration=e)===1/0?this.addClass("vjs-live"):this.removeClass("vjs-live"),isNaN(e)||this.trigger("durationchange"))}remainingTime(){return this.duration()-this.currentTime()}remainingTimeDisplay(){return Math.floor(this.duration())-Math.floor(this.currentTime())}buffered(){let e=this.techGet_("buffered");return e=e&&e.length?e:T(0,0)}seekable(){let e=this.techGet_("seekable");return e=e&&e.length?e:T(0,0)}seeking(){return this.techGet_("seeking")}ended(){return this.techGet_("ended")}networkState(){return this.techGet_("networkState")}readyState(){return this.techGet_("readyState")}bufferedPercent(){return Ft(this.buffered(),this.duration())}bufferedEnd(){var e=this.buffered(),t=this.duration();let s=e.end(e.length-1);return s=s>t?t:s}volume(e){let t;if(void 0===e)return t=parseFloat(this.techGet_("volume")),isNaN(t)?1:t;t=Math.max(0,Math.min(1,e)),this.cache_.volume=t,this.techCall_("setVolume",t),0{function i(){o.off("fullscreenerror",r),o.off("fullscreenchange",t)}function t(){i(),e()}function r(e,t){i(),s(t)}o.one("fullscreenchange",t),o.one("fullscreenerror",r);var n=o.requestFullscreenHelper_(a);n&&(n.then(i,i),n.then(e,s))})}requestFullscreenHelper_(e){let t;if(this.fsApi_.prefixed||(t=this.options_.fullscreen&&this.options_.fullscreen.options||{},void 0!==e&&(t=e)),this.fsApi_.requestFullscreen)return(e=this.el_[this.fsApi_.requestFullscreen](t))&&e.then(()=>this.isFullscreen(!0),()=>this.isFullscreen(!1)),e;this.tech_.supportsFullScreen()&&!0==!this.options_.preferFullWindow?this.techCall_("enterFullScreen"):this.enterFullWindow()}exitFullscreen(){const a=this;return new Promise((e,s)=>{function i(){a.off("fullscreenerror",r),a.off("fullscreenchange",t)}function t(){i(),e()}function r(e,t){i(),s(t)}a.one("fullscreenchange",t),a.one("fullscreenerror",r);var n=a.exitFullscreenHelper_();n&&(n.then(i,i),n.then(e,s))})}exitFullscreenHelper_(){var e;if(this.fsApi_.requestFullscreen)return(e=document[this.fsApi_.exitFullscreen]())&&k(e.then(()=>this.isFullscreen(!1))),e;this.tech_.supportsFullScreen()&&!0==!this.options_.preferFullWindow?this.techCall_("exitFullScreen"):this.exitFullWindow()}enterFullWindow(){this.isFullscreen(!0),this.isFullWindow=!0,this.docOrigOverflow=document.documentElement.style.overflow,v(document,"keydown",this.boundFullWindowOnEscKey_),document.documentElement.style.overflow="hidden",xe(document.body,"vjs-full-window"),this.trigger("enterFullWindow")}fullWindowOnEscKey(e){a.isEventKey(e,"Esc")&&!0===this.isFullscreen()&&(this.isFullWindow?this.exitFullWindow():this.exitFullscreen())}exitFullWindow(){this.isFullscreen(!1),this.isFullWindow=!1,_(document,"keydown",this.boundFullWindowOnEscKey_),document.documentElement.style.overflow=this.docOrigOverflow,je(document.body,"vjs-full-window"),this.trigger("exitFullWindow")}disablePictureInPicture(e){if(void 0===e)return this.techGet_("disablePictureInPicture");this.techCall_("setDisablePictureInPicture",e),this.options_.disablePictureInPicture=e,this.trigger("disablepictureinpicturechanged")}isInPictureInPicture(e){if(void 0===e)return!!this.isInPictureInPicture_;this.isInPictureInPicture_=!!e,this.togglePictureInPictureClass_()}requestPictureInPicture(){if(this.options_.enableDocumentPictureInPicture&&window.documentPictureInPicture){const t=document.createElement(this.el().tagName);return t.classList=this.el().classList,t.classList.add("vjs-pip-container"),this.posterImage&&t.appendChild(this.posterImage.el().cloneNode(!0)),this.titleBar&&t.appendChild(this.titleBar.el().cloneNode(!0)),t.appendChild(p("p",{className:"vjs-pip-text"},{},this.localize("Playing in picture-in-picture"))),window.documentPictureInPicture.requestWindow({width:this.videoWidth(),height:this.videoHeight()}).then(e=>(Ge(e),this.el_.parentNode.insertBefore(t,this.el_),e.document.body.appendChild(this.el_),e.document.body.classList.add("vjs-pip-window"),this.player_.isInPictureInPicture(!0),this.player_.trigger({type:"enterpictureinpicture",pipWindow:e}),e.addEventListener("pagehide",e=>{e=e.target.querySelector(".video-js");t.parentNode.replaceChild(e,t),this.player_.isInPictureInPicture(!1),this.player_.trigger("leavepictureinpicture")}),e))}return"pictureInPictureEnabled"in document&&!1===this.disablePictureInPicture()?this.techGet_("requestPictureInPicture"):Promise.reject("No PiP mode is available")}exitPictureInPicture(){return window.documentPictureInPicture&&window.documentPictureInPicture.window?(window.documentPictureInPicture.window.close(),Promise.resolve()):"pictureInPictureEnabled"in document?document.exitPictureInPicture():void 0}handleKeyDown(e){var t,s,i=this.options_["userActions"];i&&i.hotkeys&&(t=this.el_.ownerDocument.activeElement,s=t.tagName.toLowerCase(),t.isContentEditable||("input"===s?-1===["button","checkbox","hidden","radio","reset","submit"].indexOf(t.type):-1!==["textarea"].indexOf(s))||("function"==typeof i.hotkeys?i.hotkeys.call(this,e):this.handleHotkeys(e)))}handleHotkeys(e){var{fullscreenKey:t=e=>a.isEventKey(e,"f"),muteKey:s=e=>a.isEventKey(e,"m"),playPauseKey:i=e=>a.isEventKey(e,"k")||a.isEventKey(e,"Space")}=this.options_.userActions?this.options_.userActions.hotkeys:{};t.call(this,e)?(e.preventDefault(),e.stopPropagation(),t=b.getComponent("FullscreenToggle"),!1!==document[this.fsApi_.fullscreenEnabled]&&t.prototype.handleClick.call(this,e)):s.call(this,e)?(e.preventDefault(),e.stopPropagation(),b.getComponent("MuteToggle").prototype.handleClick.call(this,e)):i.call(this,e)&&(e.preventDefault(),e.stopPropagation(),b.getComponent("PlayToggle").prototype.handleClick.call(this,e))}canPlayType(i){var r;for(let t=0,s=this.options_.techOrder;ts.some(e=>{if(r=i(t,e))return!0})),r}var s=this.options_.techOrder.map(e=>[e,j.getTech(e)]).filter(([e,t])=>t?t.isSupported():(l.error(`The "${e}" tech is undefined. Skipped browser support check for that tech.`),!1));let i;var r,n=([e,t],s)=>{if(t.canPlaySource(s,this.options_[e.toLowerCase()]))return{source:s,tech:e}};return(i=this.options_.sourceOrder?t(e,s,(r=n,(e,t)=>r(t,e))):t(s,e,n))||!1}handleSrc_(e,i){if("undefined"==typeof e)return this.cache_.src||"";this.resetRetryOnError_&&this.resetRetryOnError_();const r=di(e);if(r.length){if(this.changingSrc_=!0,i||(this.cache_.sources=r),this.updateSourceCaches_(r[0]),ri(this,r[0],(e,t)=>{var s;if(this.middleware_=t,i||(this.cache_.sources=r),this.updateSourceCaches_(e),this.src_(e))return 1e.setTech&&e.setTech(s))}),1{this.error(null),this.handleSrc_(r.slice(1),!0)},s=()=>{this.off("error",t)};this.one("error",t),this.one("playing",s),this.resetRetryOnError_=()=>{this.off("error",t),this.off("playing",s)}}}else this.setTimeout(function(){this.error({code:4,message:this.options_.notSupportedMessage})},0)}src(e){return this.handleSrc_(e,!1)}src_(e){var t=this.selectSource([e]);return!t||(jt(t.tech,this.techName_)?this.ready(function(){this.tech_.constructor.prototype.hasOwnProperty("setSource")?this.techCall_("setSource",e):this.techCall_("src",e.src),this.changingSrc_=!1},!0):(this.changingSrc_=!0,this.loadTech_(t.tech,t.source),this.tech_.ready(()=>{this.changingSrc_=!1})),!1)}load(){this.tech_&&this.tech_.vhs?this.src(this.currentSource()):this.techCall_("load")}reset(){this.paused()?this.doReset_():k(this.play().then(()=>this.doReset_()))}doReset_(){this.tech_&&this.tech_.clearTracks("text"),this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.resetCache_(),this.poster(""),this.loadTech_(this.options_.techOrder[0],null),this.techCall_("reset"),this.resetControlBarUI_(),this.error(null),this.titleBar&&this.titleBar.update({title:void 0,description:void 0}),vt(this)&&this.trigger("playerreset")}resetControlBarUI_(){this.resetProgressBar_(),this.resetPlaybackRate_(),this.resetVolumeBar_()}resetProgressBar_(){this.currentTime(0);var{currentTimeDisplay:e,durationDisplay:t,progressControl:s,remainingTimeDisplay:i}=this.controlBar||{},s=(s||{})["seekBar"];e&&e.updateContent(),t&&t.updateContent(),i&&i.updateContent(),s&&(s.update(),s.loadProgressBar)&&s.loadProgressBar.update()}resetPlaybackRate_(){this.playbackRate(this.defaultPlaybackRate()),this.handleTechRateChange_()}resetVolumeBar_(){this.volume(1),this.trigger("volumechange")}currentSources(){var e=this.currentSource(),t=[];return 0!==Object.keys(e).length&&t.push(e),this.cache_.sources||t}currentSource(){return this.cache_.source||{}}currentSrc(){return this.currentSource()&&this.currentSource().src||""}currentType(){return this.currentSource()&&this.currentSource().type||""}preload(e){if(void 0===e)return this.techGet_("preload");this.techCall_("setPreload",e),this.options_.preload=e}autoplay(e){if(void 0===e)return this.options_.autoplay||!1;let t;"string"==typeof e&&/(any|play|muted)/.test(e)||!0===e&&this.options_.normalizeAutoplay?(this.options_.autoplay=e,this.manualAutoplay_("string"==typeof e?e:"play"),t=!1):this.options_.autoplay=!!e,t="undefined"==typeof t?this.options_.autoplay:t,this.tech_&&this.techCall_("setAutoplay",t)}playsinline(e){return void 0!==e&&(this.techCall_("setPlaysinline",e),this.options_.playsinline=e),this.techGet_("playsinline")}loop(e){if(void 0===e)return this.techGet_("loop");this.techCall_("setLoop",e),this.options_.loop=e}poster(e){if(void 0===e)return this.poster_;(e=e||"")!==this.poster_&&(this.poster_=e,this.techCall_("setPoster",e),this.isPosterFromTech_=!1,this.trigger("posterchange"))}handleTechPosterChange_(){var e;(!this.poster_||this.options_.techCanOverridePoster)&&this.tech_&&this.tech_.poster&&(e=this.tech_.poster()||"")!==this.poster_&&(this.poster_=e,this.isPosterFromTech_=!0,this.trigger("posterchange"))}controls(e){if(void 0===e)return!!this.controls_;this.controls_!==(e=!!e)&&(this.controls_=e,this.usingNativeControls()&&this.techCall_("setControls",e),this.controls_?(this.removeClass("vjs-controls-disabled"),this.addClass("vjs-controls-enabled"),this.trigger("controlsenabled"),this.usingNativeControls()||this.addTechControlsListeners_()):(this.removeClass("vjs-controls-enabled"),this.addClass("vjs-controls-disabled"),this.trigger("controlsdisabled"),this.usingNativeControls()||this.removeTechControlsListeners_()))}usingNativeControls(e){if(void 0===e)return!!this.usingNativeControls_;this.usingNativeControls_!==(e=!!e)&&(this.usingNativeControls_=e,this.usingNativeControls_?(this.addClass("vjs-using-native-controls"),this.trigger("usingnativecontrols")):(this.removeClass("vjs-using-native-controls"),this.trigger("usingcustomcontrols")))}error(t){if(void 0===t)return this.error_||null;if(R("beforeerror").forEach(e=>{e=e(this,t);n(e)&&!Array.isArray(e)||"string"==typeof e||"number"==typeof e||null===e?t=e:this.log.error("please return a value that MediaError expects in beforeerror hooks")}),this.options_.suppressNotSupportedError&&t&&4===t.code){const e=function(){this.error(t)};this.options_.suppressNotSupportedError=!1,this.any(["click","touchstart"],e),void this.one("loadstart",function(){this.off(["click","touchstart"],e)})}else null===t?(this.error_=null,this.removeClass("vjs-error"),this.errorDisplay&&this.errorDisplay.close()):(this.error_=new i(t),this.addClass("vjs-error"),l.error(`(CODE:${this.error_.code} ${i.errorTypes[this.error_.code]})`,this.error_.message,this.error_),this.trigger("error"),R("error").forEach(e=>e(this,this.error_)))}reportUserActivity(e){this.userActivity_=!0}userActive(e){if(void 0===e)return this.userActive_;(e=!!e)!==this.userActive_&&(this.userActive_=e,this.userActive_?(this.userActivity_=!0,this.removeClass("vjs-user-inactive"),this.addClass("vjs-user-active"),this.trigger("useractive")):(this.tech_&&this.tech_.one("mousemove",function(e){e.stopPropagation(),e.preventDefault()}),this.userActivity_=!1,this.removeClass("vjs-user-active"),this.addClass("vjs-user-inactive"),this.trigger("userinactive")))}listenForUserActivity_(){let t,s,i;const r=f(this,this.reportUserActivity);function e(e){r(),this.clearInterval(t)}this.on("mousedown",function(){r(),this.clearInterval(t),t=this.setInterval(r,250)}),this.on("mousemove",function(e){e.screenX===s&&e.screenY===i||(s=e.screenX,i=e.screenY,r())}),this.on("mouseup",e),this.on("mouseleave",e);var n=this.getChild("controlBar");!n||u||o||(n.on("mouseenter",function(e){0!==this.player().options_.inactivityTimeout&&(this.player().cache_.inactivityTimeout=this.player().options_.inactivityTimeout),this.player().options_.inactivityTimeout=0}),n.on("mouseleave",function(e){this.player().options_.inactivityTimeout=this.player().cache_.inactivityTimeout})),this.on("keydown",r),this.on("keyup",r);let a;this.setInterval(function(){var e;this.userActivity_&&(this.userActivity_=!1,this.userActive(!0),this.clearTimeout(a),(e=this.options_.inactivityTimeout)<=0||(a=this.setTimeout(function(){this.userActivity_||this.userActive(!1)},e)))},250)}playbackRate(e){if(void 0===e)return this.tech_&&this.tech_.featuresPlaybackRate?this.cache_.lastPlaybackRate||this.techGet_("playbackRate"):1;this.techCall_("setPlaybackRate",e)}defaultPlaybackRate(e){return void 0!==e?this.techCall_("setDefaultPlaybackRate",e):this.tech_&&this.tech_.featuresPlaybackRate?this.techGet_("defaultPlaybackRate"):1}isAudio(e){if(void 0===e)return!!this.isAudio_;this.isAudio_=!!e}enableAudioOnlyUI_(){this.addClass("vjs-audio-only-mode");var e=this.children();const t=this.getChild("ControlBar");var s=t&&t.currentHeight();e.forEach(e=>{e!==t&&e.el_&&!e.hasClass("vjs-hidden")&&(e.hide(),this.audioOnlyCache_.hiddenChildren.push(e))}),this.audioOnlyCache_.playerHeight=this.currentHeight(),this.height(s),this.trigger("audioonlymodechange")}disableAudioOnlyUI_(){this.removeClass("vjs-audio-only-mode"),this.audioOnlyCache_.hiddenChildren.forEach(e=>e.show()),this.height(this.audioOnlyCache_.playerHeight),this.trigger("audioonlymodechange")}audioOnlyMode(e){return"boolean"!=typeof e||e===this.audioOnlyMode_?this.audioOnlyMode_:(this.audioOnlyMode_=e)?(e=[],this.isInPictureInPicture()&&e.push(this.exitPictureInPicture()),this.isFullscreen()&&e.push(this.exitFullscreen()),this.audioPosterMode()&&e.push(this.audioPosterMode(!1)),Promise.all(e).then(()=>this.enableAudioOnlyUI_())):Promise.resolve().then(()=>this.disableAudioOnlyUI_())}enablePosterModeUI_(){(this.tech_&&this.tech_).hide(),this.addClass("vjs-audio-poster-mode"),this.trigger("audiopostermodechange")}disablePosterModeUI_(){(this.tech_&&this.tech_).show(),this.removeClass("vjs-audio-poster-mode"),this.trigger("audiopostermodechange")}audioPosterMode(e){return"boolean"!=typeof e||e===this.audioPosterMode_?this.audioPosterMode_:(this.audioPosterMode_=e)?(this.audioOnlyMode()?this.audioOnlyMode(!1):Promise.resolve()).then(()=>{this.enablePosterModeUI_()}):Promise.resolve().then(()=>{this.disablePosterModeUI_()})}addTextTrack(e,t,s){if(this.tech_)return this.tech_.addTextTrack(e,t,s)}addRemoteTextTrack(e,t){if(this.tech_)return this.tech_.addRemoteTextTrack(e,t)}removeRemoteTextTrack(e={}){let t=e["track"];if(t=t||e,this.tech_)return this.tech_.removeRemoteTextTrack(t)}getVideoPlaybackQuality(){return this.techGet_("getVideoPlaybackQuality")}videoWidth(){return this.tech_&&this.tech_.videoWidth&&this.tech_.videoWidth()||0}videoHeight(){return this.tech_&&this.tech_.videoHeight&&this.tech_.videoHeight()||0}language(e){if(void 0===e)return this.language_;this.language_!==String(e).toLowerCase()&&(this.language_=String(e).toLowerCase(),vt(this))&&this.trigger("languagechange")}languages(){return h(M.prototype.options_.languages,this.languages_)}toJSON(){var t=h(this.options_),s=t.tracks;t.tracks=[];for(let e=0;e{this.removeChild(s)}),s.open(),s}updateCurrentBreakpoint_(){if(this.responsive()){var t=this.currentBreakpoint(),s=this.currentWidth();for(let e=0;ethis.addRemoteTextTrack(e,!1)),this.titleBar&&this.titleBar.update({title:l,description:r||e||""}),this.ready(t))}getMedia(){var e,t;return this.cache_.media?h(this.cache_.media):(e=this.poster(),t={src:this.currentSources(),textTracks:Array.prototype.map.call(this.remoteTextTracks(),e=>({kind:e.kind,label:e.label,language:e.language,src:e.src}))},e&&(t.poster=e,t.artwork=[{src:t.poster,type:pi(t.poster)}]),t)}static getTagSettings(e){var t,s={sources:[],tracks:[]},i=Me(e),r=i["data-setup"];if(Se(e,"vjs-fill")&&(i.fill=!0),Se(e,"vjs-fluid")&&(i.fluid=!0),null!==r&&([r,t]=Vt(r||"{}"),r&&l.error(r),Object.assign(i,t)),Object.assign(s,i),e.hasChildNodes()){var n=e.childNodes;for(let e=0,t=n.length;e"number"==typeof e)&&(this.cache_.playbackRates=e,this.trigger("playbackrateschange"))}}E.names.forEach(function(e){const t=E[e];M.prototype[t.getterName]=function(){return this.tech_?this.tech_[t.getterName]():(this[t.privateName]=this[t.privateName]||new t.ListClass,this[t.privateName])}}),M.prototype.crossorigin=M.prototype.crossOrigin,M.players={};xr=window.navigator;M.prototype.options_={techOrder:j.defaultTechOrder_,html5:{},enableSourceset:!0,inactivityTimeout:2e3,playbackRates:[],liveui:!1,children:["mediaLoader","posterImage","titleBar","textTrackDisplay","loadingSpinner","bigPlayButton","liveTracker","controlBar","errorDisplay","textTrackSettings","resizeManager"],language:xr&&(xr.languages&&xr.languages[0]||xr.userLanguage||xr.language)||"en",languages:{},notSupportedMessage:"No compatible source was found for this media.",normalizeAutoplay:!1,fullscreen:{options:{navigationUI:"hide"}},breakpoints:{},responsive:!1,audioOnlyMode:!1,audioPosterMode:!1,enableSmoothSeeking:!1},Kr.forEach(function(e){M.prototype[`handleTech${y(e)}_`]=function(){return this.trigger(e)}}),b.registerComponent("Player",M);function Qr(t,s){function i(){nn(this,{name:t,plugin:s,instance:null},!0);var e=s.apply(this,arguments);return rn(this,t),nn(this,{name:t,plugin:s,instance:e}),e}return Object.keys(s).forEach(function(e){i[e]=s[e]}),i}const Jr="plugin",Zr="activePlugins_",en={},tn=e=>en.hasOwnProperty(e),sn=e=>tn(e)?en[e]:void 0,rn=(e,t)=>{e[Zr]=e[Zr]||{},e[Zr][t]=!0},nn=(e,t,s)=>{s=(s?"before":"")+"pluginsetup";e.trigger(s,t),e.trigger(s+":"+t.name,t)},an=(s,i)=>(i.prototype.name=s,function(...e){nn(this,{name:s,plugin:i,instance:null},!0);const t=new i(this,...e);return this[s]=()=>t,nn(this,t.getEventHash()),t});class A{constructor(e){if(this.constructor===A)throw new Error("Plugin must be sub-classed; not directly instantiated.");this.player=e,this.log||(this.log=this.player.log.createLogger(this.name)),wt(this),delete this.trigger,St(this,this.constructor.defaultState),rn(e,this.name),this.dispose=this.dispose.bind(this),e.on("dispose",this.dispose)}version(){return this.constructor.VERSION}getEventHash(e={}){return e.name=this.name,e.plugin=this.constructor,e.instance=this,e}trigger(e,t={}){return ht(this.eventBusEl_,e,this.getEventHash(t))}handleStateChanged(e){}dispose(){var{name:e,player:t}=this;this.trigger("dispose"),this.off(),t.off("dispose",this.dispose),t[Zr][e]=!1,this.player=this.state=null,t[e]=an(e,en[e])}static isBasic(e){e="string"==typeof e?sn(e):e;return"function"==typeof e&&!A.prototype.isPrototypeOf(e.prototype)}static registerPlugin(e,t){if("string"!=typeof e)throw new Error(`Illegal plugin name, "${e}", must be a string, was ${typeof e}.`);if(tn(e))l.warn(`A plugin named "${e}" already exists. You may want to avoid re-registering plugins!`);else if(M.prototype.hasOwnProperty(e))throw new Error(`Illegal plugin name, "${e}", cannot share a name with an existing player method!`);if("function"!=typeof t)throw new Error(`Illegal plugin for "${e}", must be a function, was ${typeof t}.`);return en[e]=t,e!==Jr&&(A.isBasic(t)?M.prototype[e]=Qr(e,t):M.prototype[e]=an(e,t)),t}static deregisterPlugin(e){if(e===Jr)throw new Error("Cannot de-register base plugin.");tn(e)&&(delete en[e],delete M.prototype[e])}static getPlugins(e=Object.keys(en)){let s;return e.forEach(e=>{var t=sn(e);t&&((s=s||{})[e]=t)}),s}static getPluginVersion(e){e=sn(e);return e&&e.VERSION||""}}function O(e,s,i,r){{var n=s+` is deprecated and will be removed in ${e}.0; please use ${i} instead.`,a=r;let t=!1;return function(...e){return t||l.warn(n),t=!0,a.apply(this,e)}}}A.getPlugin=sn,A.BASE_PLUGIN_NAME=Jr,A.registerPlugin(Jr,A),M.prototype.usingPlugin=function(e){return!!this[Zr]&&!0===this[Zr][e]},M.prototype.hasPlugin=function(e){return!!tn(e)};const on=e=>0===e.indexOf("#")?e.slice(1):e;function L(e,t,s){let i=L.getPlayer(e);if(i)t&&l.warn(`Player "${e}" is already initialised. Options will not be applied.`),s&&i.ready(s);else{const r="string"==typeof e?Ke("#"+on(e)):e;if(!Te(r))throw new TypeError("The element or ID supplied is not valid. (videojs)");e="getRootNode"in r&&r.getRootNode()instanceof window.ShadowRoot?r.getRootNode():r.ownerDocument.body,e=(r.ownerDocument.defaultView&&e.contains(r)||l.warn("The element supplied is not included in the DOM"),!0===(t=t||{}).restoreEl&&(t.restoreEl=(r.parentNode&&r.parentNode.hasAttribute("data-vjs-player")?r.parentNode:r).cloneNode(!0)),R("beforesetup").forEach(e=>{e=e(r,h(t));!n(e)||Array.isArray(e)?l.error("please return an object in beforesetup hooks"):t=h(t,e)}),b.getComponent("Player"));i=new e(r,t,s),R("setup").forEach(e=>e(i))}return i}return L.hooks_=B,L.hooks=R,L.hook=function(e,t){R(e,t)},L.hookOnce=function(i,e){R(i,[].concat(e).map(t=>{const s=(...e)=>(H(i,s),t(...e));return s}))},L.removeHook=H,!0!==window.VIDEOJS_NO_DYNAMIC_STYLE&&be()&&!(xs=Ke(".vjs-styles-defaults"))&&(xs=st("vjs-styles-defaults"),(Sr=Ke("head"))&&Sr.insertBefore(xs,Sr.firstChild),it(xs,`
- .video-js {
- width: 300px;
- height: 150px;
- }
-
- .vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: 56.25%
- }
- `)),et(1,L),L.VERSION=D,L.options=M.prototype.options_,L.getPlayers=()=>M.players,L.getPlayer=e=>{var t=M.players;let s;if("string"==typeof e){var i=on(e),r=t[i];if(r)return r;s=Ke("#"+i)}else s=e;if(Te(s)){var{player:r,playerId:i}=s;if(r||t[i])return r||t[i]}},L.getAllPlayers=()=>Object.keys(M.players).map(e=>M.players[e]).filter(Boolean),L.players=M.players,L.getComponent=b.getComponent,L.registerComponent=(e,t)=>(j.isTech(t)&&l.warn(`The ${e} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`),b.registerComponent.call(b,e,t)),L.getTech=j.getTech,L.registerTech=j.registerTech,L.use=function(e,t){ti[e]=ti[e]||[],ti[e].push(t)},Object.defineProperty(L,"middleware",{value:{},writeable:!1,enumerable:!0}),Object.defineProperty(L.middleware,"TERMINATOR",{value:ii,writeable:!1,enumerable:!0}),L.browser=fe,L.obj=J,L.mergeOptions=O(9,"videojs.mergeOptions","videojs.obj.merge",h),L.defineLazyProperty=O(9,"videojs.defineLazyProperty","videojs.obj.defineLazyProperty",Q),L.bind=O(9,"videojs.bind","native Function.prototype.bind",f),L.registerPlugin=A.registerPlugin,L.deregisterPlugin=A.deregisterPlugin,L.plugin=(e,t)=>(l.warn("videojs.plugin() is deprecated; use videojs.registerPlugin() instead"),A.registerPlugin(e,t)),L.getPlugins=A.getPlugins,L.getPlugin=A.getPlugin,L.getPluginVersion=A.getPluginVersion,L.addLanguage=function(e,t){return e=(""+e).toLowerCase(),L.options.languages=h(L.options.languages,{[e]:t}),L.options.languages[e]},L.log=l,L.createLogger=U,L.time=Ht,L.createTimeRange=O(9,"videojs.createTimeRange","videojs.time.createTimeRanges",T),L.createTimeRanges=O(9,"videojs.createTimeRanges","videojs.time.createTimeRanges",T),L.formatTime=O(9,"videojs.formatTime","videojs.time.formatTime",Rt),L.setFormatTime=O(9,"videojs.setFormatTime","videojs.time.setFormatTime",Dt),L.resetFormatTime=O(9,"videojs.resetFormatTime","videojs.time.resetFormatTime",Bt),L.parseUrl=O(9,"videojs.parseUrl","videojs.url.parseUrl",rs),L.isCrossOrigin=O(9,"videojs.isCrossOrigin","videojs.url.isCrossOrigin",as),L.EventTarget=s,L.any=dt,L.on=v,L.one=ct,L.off=_,L.trigger=ht,L.xhr=ps,L.TextTrack=ks,L.AudioTrack=Cs,L.VideoTrack=ws,["isEl","isTextNode","createEl","hasClass","addClass","removeClass","toggleClass","setAttributes","getAttributes","emptyEl","appendContent","insertContent"].forEach(e=>{L[e]=function(){return l.warn(`videojs.${e}() is deprecated; use videojs.dom.${e}() instead`),Ye[e].apply(null,arguments)}}),L.computedStyle=O(9,"videojs.computedStyle","videojs.dom.computedStyle",Xe),L.dom=Ye,L.fn=e,L.num=t,L.str=Pt,L.url=ls,L.Error={UnsupportedSidxContainer:"unsupported-sidx-container-error",DashManifestSidxParsingError:"dash-manifest-sidx-parsing-error",HlsPlaylistRequestError:"hls-playlist-request-error",SegmentUnsupportedMediaFormat:"segment-unsupported-media-format-error",UnsupportedMediaInitialization:"unsupported-media-initialization-error",SegmentSwitchError:"segment-switch-error",SegmentExceedsSourceBufferQuota:"segment-exceeds-source-buffer-quota-error",SegmentAppendError:"segment-append-error",VttLoadError:"vtt-load-error",VttCueParsingError:"vtt-cue-parsing-error",AdsBeforePrerollError:"ads-before-preroll-error",AdsPrerollError:"ads-preroll-error",AdsMidrollError:"ads-midroll-error",AdsPostrollError:"ads-postroll-error",AdsMacroReplacementFailed:"ads-macro-replacement-failed",AdsResumeContentFailed:"ads-resume-content-failed",EMEFailedToRequestMediaKeySystemAccess:"eme-failed-request-media-key-system-access",EMEFailedToCreateMediaKeys:"eme-failed-create-media-keys",EMEFailedToAttachMediaKeysToVideoElement:"eme-failed-attach-media-keys-to-video",EMEFailedToCreateMediaKeySession:"eme-failed-create-media-key-session",EMEFailedToSetServerCertificate:"eme-failed-set-server-certificate",EMEFailedToGenerateLicenseRequest:"eme-failed-generate-license-request",EMEFailedToUpdateSessionWithReceivedLicenseKeys:"eme-failed-update-session",EMEFailedToCloseSession:"eme-failed-close-session",EMEFailedToRemoveKeysFromSession:"eme-failed-remove-keys",EMEFailedToLoadSessionBySessionId:"eme-failed-load-session"},L});
\ No newline at end of file
diff --git a/source/src/public/twitch/video.js/alt/video.core.novtt.js b/source/src/public/twitch/video.js/alt/video.core.novtt.js
deleted file mode 100755
index f169b1b..0000000
--- a/source/src/public/twitch/video.js/alt/video.core.novtt.js
+++ /dev/null
@@ -1,26893 +0,0 @@
-/**
- * @license
- * Video.js 8.12.0
- * Copyright Brightcove, Inc.
- * Available under Apache License Version 2.0
- *
- *
- * Includes vtt.js
- * Available under Apache License Version 2.0
- *
- */
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
-})(this, (function () { 'use strict';
-
- var version = "8.12.0";
-
- /**
- * An Object that contains lifecycle hooks as keys which point to an array
- * of functions that are run when a lifecycle is triggered
- *
- * @private
- */
- const hooks_ = {};
-
- /**
- * Get a list of hooks for a specific lifecycle
- *
- * @param {string} type
- * the lifecycle to get hooks from
- *
- * @param {Function|Function[]} [fn]
- * Optionally add a hook (or hooks) to the lifecycle that your are getting.
- *
- * @return {Array}
- * an array of hooks, or an empty array if there are none.
- */
- const hooks = function (type, fn) {
- hooks_[type] = hooks_[type] || [];
- if (fn) {
- hooks_[type] = hooks_[type].concat(fn);
- }
- return hooks_[type];
- };
-
- /**
- * Add a function hook to a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle to hook the function to.
- *
- * @param {Function|Function[]}
- * The function or array of functions to attach.
- */
- const hook = function (type, fn) {
- hooks(type, fn);
- };
-
- /**
- * Remove a hook from a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle that the function hooked to
- *
- * @param {Function} fn
- * The hooked function to remove
- *
- * @return {boolean}
- * The function that was removed or undef
- */
- const removeHook = function (type, fn) {
- const index = hooks(type).indexOf(fn);
- if (index <= -1) {
- return false;
- }
- hooks_[type] = hooks_[type].slice();
- hooks_[type].splice(index, 1);
- return true;
- };
-
- /**
- * Add a function hook that will only run once to a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle to hook the function to.
- *
- * @param {Function|Function[]}
- * The function or array of functions to attach.
- */
- const hookOnce = function (type, fn) {
- hooks(type, [].concat(fn).map(original => {
- const wrapper = (...args) => {
- removeHook(type, wrapper);
- return original(...args);
- };
- return wrapper;
- }));
- };
-
- /**
- * @file fullscreen-api.js
- * @module fullscreen-api
- */
-
- /**
- * Store the browser-specific methods for the fullscreen API.
- *
- * @type {Object}
- * @see [Specification]{@link https://fullscreen.spec.whatwg.org}
- * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
- */
- const FullscreenApi = {
- prefixed: true
- };
-
- // browser API methods
- const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
- // WebKit
- ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
- const specApi = apiMap[0];
- let browserApi;
-
- // determine the supported set of functions
- for (let i = 0; i < apiMap.length; i++) {
- // check for exitFullscreen function
- if (apiMap[i][1] in document) {
- browserApi = apiMap[i];
- break;
- }
- }
-
- // map the browser API names to the spec API names
- if (browserApi) {
- for (let i = 0; i < browserApi.length; i++) {
- FullscreenApi[specApi[i]] = browserApi[i];
- }
- FullscreenApi.prefixed = browserApi[0] !== specApi[0];
- }
-
- /**
- * @file create-logger.js
- * @module create-logger
- */
-
- // This is the private tracking variable for the logging history.
- let history = [];
-
- /**
- * Log messages to the console and history based on the type of message
- *
- * @private
- * @param {string} name
- * The name of the console method to use.
- *
- * @param {Object} log
- * The arguments to be passed to the matching console method.
- *
- * @param {string} [styles]
- * styles for name
- */
- const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
- const lvl = log.levels[level];
- const lvlRegExp = new RegExp(`^(${lvl})$`);
- let resultName = name;
- if (type !== 'log') {
- // Add the type to the front of the message when it's not "log".
- args.unshift(type.toUpperCase() + ':');
- }
- if (styles) {
- resultName = `%c${name}`;
- args.unshift(styles);
- }
-
- // Add console prefix after adding to history.
- args.unshift(resultName + ':');
-
- // Add a clone of the args at this point to history.
- if (history) {
- history.push([].concat(args));
-
- // only store 1000 history entries
- const splice = history.length - 1000;
- history.splice(0, splice > 0 ? splice : 0);
- }
-
- // If there's no console then don't try to output messages, but they will
- // still be stored in history.
- if (!window.console) {
- return;
- }
-
- // Was setting these once outside of this function, but containing them
- // in the function makes it easier to test cases where console doesn't exist
- // when the module is executed.
- let fn = window.console[type];
- if (!fn && type === 'debug') {
- // Certain browsers don't have support for console.debug. For those, we
- // should default to the closest comparable log.
- fn = window.console.info || window.console.log;
- }
-
- // Bail out if there's no console or if this type is not allowed by the
- // current logging level.
- if (!fn || !lvl || !lvlRegExp.test(type)) {
- return;
- }
- fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
- };
- function createLogger$1(name, delimiter = ':', styles = '') {
- // This is the private tracking variable for logging level.
- let level = 'info';
-
- // the curried logByType bound to the specific log and history
- let logByType;
-
- /**
- * Logs plain debug messages. Similar to `console.log`.
- *
- * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
- * of our JSDoc template, we cannot properly document this as both a function
- * and a namespace, so its function signature is documented here.
- *
- * #### Arguments
- * ##### *args
- * *[]
- *
- * Any combination of values that could be passed to `console.log()`.
- *
- * #### Return Value
- *
- * `undefined`
- *
- * @namespace
- * @param {...*} args
- * One or more messages or objects that should be logged.
- */
- const log = function (...args) {
- logByType('log', level, args);
- };
-
- // This is the logByType helper that the logging methods below use
- logByType = LogByTypeFactory(name, log, styles);
-
- /**
- * Create a new subLogger which chains the old name to the new name.
- *
- * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
- * ```js
- * mylogger('foo');
- * // > VIDEOJS: player: foo
- * ```
- *
- * @param {string} subName
- * The name to add call the new logger
- * @param {string} [subDelimiter]
- * Optional delimiter
- * @param {string} [subStyles]
- * Optional styles
- * @return {Object}
- */
- log.createLogger = (subName, subDelimiter, subStyles) => {
- const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
- const resultStyles = subStyles !== undefined ? subStyles : styles;
- const resultName = `${name} ${resultDelimiter} ${subName}`;
- return createLogger$1(resultName, resultDelimiter, resultStyles);
- };
-
- /**
- * Create a new logger.
- *
- * @param {string} newName
- * The name for the new logger
- * @param {string} [newDelimiter]
- * Optional delimiter
- * @param {string} [newStyles]
- * Optional styles
- * @return {Object}
- */
- log.createNewLogger = (newName, newDelimiter, newStyles) => {
- return createLogger$1(newName, newDelimiter, newStyles);
- };
-
- /**
- * Enumeration of available logging levels, where the keys are the level names
- * and the values are `|`-separated strings containing logging methods allowed
- * in that logging level. These strings are used to create a regular expression
- * matching the function name being called.
- *
- * Levels provided by Video.js are:
- *
- * - `off`: Matches no calls. Any value that can be cast to `false` will have
- * this effect. The most restrictive.
- * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
- * `log.warn`, and `log.error`).
- * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
- * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
- * - `warn`: Matches `log.warn` and `log.error` calls.
- * - `error`: Matches only `log.error` calls.
- *
- * @type {Object}
- */
- log.levels = {
- all: 'debug|log|warn|error',
- off: '',
- debug: 'debug|log|warn|error',
- info: 'log|warn|error',
- warn: 'warn|error',
- error: 'error',
- DEFAULT: level
- };
-
- /**
- * Get or set the current logging level.
- *
- * If a string matching a key from {@link module:log.levels} is provided, acts
- * as a setter.
- *
- * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
- * Pass a valid level to set a new logging level.
- *
- * @return {string}
- * The current logging level.
- */
- log.level = lvl => {
- if (typeof lvl === 'string') {
- if (!log.levels.hasOwnProperty(lvl)) {
- throw new Error(`"${lvl}" in not a valid log level`);
- }
- level = lvl;
- }
- return level;
- };
-
- /**
- * Returns an array containing everything that has been logged to the history.
- *
- * This array is a shallow clone of the internal history record. However, its
- * contents are _not_ cloned; so, mutating objects inside this array will
- * mutate them in history.
- *
- * @return {Array}
- */
- log.history = () => history ? [].concat(history) : [];
-
- /**
- * Allows you to filter the history by the given logger name
- *
- * @param {string} fname
- * The name to filter by
- *
- * @return {Array}
- * The filtered list to return
- */
- log.history.filter = fname => {
- return (history || []).filter(historyItem => {
- // if the first item in each historyItem includes `fname`, then it's a match
- return new RegExp(`.*${fname}.*`).test(historyItem[0]);
- });
- };
-
- /**
- * Clears the internal history tracking, but does not prevent further history
- * tracking.
- */
- log.history.clear = () => {
- if (history) {
- history.length = 0;
- }
- };
-
- /**
- * Disable history tracking if it is currently enabled.
- */
- log.history.disable = () => {
- if (history !== null) {
- history.length = 0;
- history = null;
- }
- };
-
- /**
- * Enable history tracking if it is currently disabled.
- */
- log.history.enable = () => {
- if (history === null) {
- history = [];
- }
- };
-
- /**
- * Logs error messages. Similar to `console.error`.
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as an error
- */
- log.error = (...args) => logByType('error', level, args);
-
- /**
- * Logs warning messages. Similar to `console.warn`.
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as a warning.
- */
- log.warn = (...args) => logByType('warn', level, args);
-
- /**
- * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
- * log if `console.debug` is not available
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as debug.
- */
- log.debug = (...args) => logByType('debug', level, args);
- return log;
- }
-
- /**
- * @file log.js
- * @module log
- */
- const log = createLogger$1('VIDEOJS');
- const createLogger = log.createLogger;
-
- /**
- * @file obj.js
- * @module obj
- */
-
- /**
- * @callback obj:EachCallback
- *
- * @param {*} value
- * The current key for the object that is being iterated over.
- *
- * @param {string} key
- * The current key-value for object that is being iterated over
- */
-
- /**
- * @callback obj:ReduceCallback
- *
- * @param {*} accum
- * The value that is accumulating over the reduce loop.
- *
- * @param {*} value
- * The current key for the object that is being iterated over.
- *
- * @param {string} key
- * The current key-value for object that is being iterated over
- *
- * @return {*}
- * The new accumulated value.
- */
- const toString$1 = Object.prototype.toString;
-
- /**
- * Get the keys of an Object
- *
- * @param {Object}
- * The Object to get the keys from
- *
- * @return {string[]}
- * An array of the keys from the object. Returns an empty array if the
- * object passed in was invalid or had no keys.
- *
- * @private
- */
- const keys = function (object) {
- return isObject(object) ? Object.keys(object) : [];
- };
-
- /**
- * Array-like iteration for objects.
- *
- * @param {Object} object
- * The object to iterate over
- *
- * @param {obj:EachCallback} fn
- * The callback function which is called for each key in the object.
- */
- function each(object, fn) {
- keys(object).forEach(key => fn(object[key], key));
- }
-
- /**
- * Array-like reduce for objects.
- *
- * @param {Object} object
- * The Object that you want to reduce.
- *
- * @param {Function} fn
- * A callback function which is called for each key in the object. It
- * receives the accumulated value and the per-iteration value and key
- * as arguments.
- *
- * @param {*} [initial = 0]
- * Starting value
- *
- * @return {*}
- * The final accumulated value.
- */
- function reduce(object, fn, initial = 0) {
- return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
- }
-
- /**
- * Returns whether a value is an object of any kind - including DOM nodes,
- * arrays, regular expressions, etc. Not functions, though.
- *
- * This avoids the gotcha where using `typeof` on a `null` value
- * results in `'object'`.
- *
- * @param {Object} value
- * @return {boolean}
- */
- function isObject(value) {
- return !!value && typeof value === 'object';
- }
-
- /**
- * Returns whether an object appears to be a "plain" object - that is, a
- * direct instance of `Object`.
- *
- * @param {Object} value
- * @return {boolean}
- */
- function isPlain(value) {
- return isObject(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object;
- }
-
- /**
- * Merge two objects recursively.
- *
- * Performs a deep merge like
- * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
- * plain objects (not arrays, elements, or anything else).
- *
- * Non-plain object values will be copied directly from the right-most
- * argument.
- *
- * @param {Object[]} sources
- * One or more objects to merge into a new object.
- *
- * @return {Object}
- * A new object that is the merged result of all sources.
- */
- function merge(...sources) {
- const result = {};
- sources.forEach(source => {
- if (!source) {
- return;
- }
- each(source, (value, key) => {
- if (!isPlain(value)) {
- result[key] = value;
- return;
- }
- if (!isPlain(result[key])) {
- result[key] = {};
- }
- result[key] = merge(result[key], value);
- });
- });
- return result;
- }
-
- /**
- * Returns an array of values for a given object
- *
- * @param {Object} source - target object
- * @return {Array} - object values
- */
- function values(source = {}) {
- const result = [];
- for (const key in source) {
- if (source.hasOwnProperty(key)) {
- const value = source[key];
- result.push(value);
- }
- }
- return result;
- }
-
- /**
- * Object.defineProperty but "lazy", which means that the value is only set after
- * it is retrieved the first time, rather than being set right away.
- *
- * @param {Object} obj the object to set the property on
- * @param {string} key the key for the property to set
- * @param {Function} getValue the function used to get the value when it is needed.
- * @param {boolean} setter whether a setter should be allowed or not
- */
- function defineLazyProperty(obj, key, getValue, setter = true) {
- const set = value => Object.defineProperty(obj, key, {
- value,
- enumerable: true,
- writable: true
- });
- const options = {
- configurable: true,
- enumerable: true,
- get() {
- const value = getValue();
- set(value);
- return value;
- }
- };
- if (setter) {
- options.set = set;
- }
- return Object.defineProperty(obj, key, options);
- }
-
- var Obj = /*#__PURE__*/Object.freeze({
- __proto__: null,
- each: each,
- reduce: reduce,
- isObject: isObject,
- isPlain: isPlain,
- merge: merge,
- values: values,
- defineLazyProperty: defineLazyProperty
- });
-
- /**
- * @file browser.js
- * @module browser
- */
-
- /**
- * Whether or not this device is an iPod.
- *
- * @static
- * @type {Boolean}
- */
- let IS_IPOD = false;
-
- /**
- * The detected iOS version - or `null`.
- *
- * @static
- * @type {string|null}
- */
- let IOS_VERSION = null;
-
- /**
- * Whether or not this is an Android device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_ANDROID = false;
-
- /**
- * The detected Android version - or `null` if not Android or indeterminable.
- *
- * @static
- * @type {number|string|null}
- */
- let ANDROID_VERSION;
-
- /**
- * Whether or not this is Mozilla Firefox.
- *
- * @static
- * @type {Boolean}
- */
- let IS_FIREFOX = false;
-
- /**
- * Whether or not this is Microsoft Edge.
- *
- * @static
- * @type {Boolean}
- */
- let IS_EDGE = false;
-
- /**
- * Whether or not this is any Chromium Browser
- *
- * @static
- * @type {Boolean}
- */
- let IS_CHROMIUM = false;
-
- /**
- * Whether or not this is any Chromium browser that is not Edge.
- *
- * This will also be `true` for Chrome on iOS, which will have different support
- * as it is actually Safari under the hood.
- *
- * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
- * IS_CHROMIUM should be used instead.
- * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
- *
- * @static
- * @deprecated
- * @type {Boolean}
- */
- let IS_CHROME = false;
-
- /**
- * The detected Chromium version - or `null`.
- *
- * @static
- * @type {number|null}
- */
- let CHROMIUM_VERSION = null;
-
- /**
- * The detected Google Chrome version - or `null`.
- * This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
- * Deprecated, use CHROMIUM_VERSION instead.
- *
- * @static
- * @deprecated
- * @type {number|null}
- */
- let CHROME_VERSION = null;
-
- /**
- * The detected Internet Explorer version - or `null`.
- *
- * @static
- * @deprecated
- * @type {number|null}
- */
- let IE_VERSION = null;
-
- /**
- * Whether or not this is desktop Safari.
- *
- * @static
- * @type {Boolean}
- */
- let IS_SAFARI = false;
-
- /**
- * Whether or not this is a Windows machine.
- *
- * @static
- * @type {Boolean}
- */
- let IS_WINDOWS = false;
-
- /**
- * Whether or not this device is an iPad.
- *
- * @static
- * @type {Boolean}
- */
- let IS_IPAD = false;
-
- /**
- * Whether or not this device is an iPhone.
- *
- * @static
- * @type {Boolean}
- */
- // The Facebook app's UIWebView identifies as both an iPhone and iPad, so
- // to identify iPhones, we need to exclude iPads.
- // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
- let IS_IPHONE = false;
-
- /**
- * Whether or not this is a Tizen device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_TIZEN = false;
-
- /**
- * Whether or not this is a WebOS device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_WEBOS = false;
-
- /**
- * Whether or not this is a Smart TV (Tizen or WebOS) device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_SMART_TV = false;
-
- /**
- * Whether or not this device is touch-enabled.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch));
- const UAD = window.navigator && window.navigator.userAgentData;
- if (UAD && UAD.platform && UAD.brands) {
- // If userAgentData is present, use it instead of userAgent to avoid warnings
- // Currently only implemented on Chromium
- // userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
-
- IS_ANDROID = UAD.platform === 'Android';
- IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
- IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
- IS_CHROME = !IS_EDGE && IS_CHROMIUM;
- CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
- IS_WINDOWS = UAD.platform === 'Windows';
- }
-
- // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
- // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
- // the checks need to be made agiainst the regular userAgent string.
- if (!IS_CHROMIUM) {
- const USER_AGENT = window.navigator && window.navigator.userAgent || '';
- IS_IPOD = /iPod/i.test(USER_AGENT);
- IOS_VERSION = function () {
- const match = USER_AGENT.match(/OS (\d+)_/i);
- if (match && match[1]) {
- return match[1];
- }
- return null;
- }();
- IS_ANDROID = /Android/i.test(USER_AGENT);
- ANDROID_VERSION = function () {
- // This matches Android Major.Minor.Patch versions
- // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
- const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
- if (!match) {
- return null;
- }
- const major = match[1] && parseFloat(match[1]);
- const minor = match[2] && parseFloat(match[2]);
- if (major && minor) {
- return parseFloat(match[1] + '.' + match[2]);
- } else if (major) {
- return major;
- }
- return null;
- }();
- IS_FIREFOX = /Firefox/i.test(USER_AGENT);
- IS_EDGE = /Edg/i.test(USER_AGENT);
- IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
- IS_CHROME = !IS_EDGE && IS_CHROMIUM;
- CHROMIUM_VERSION = CHROME_VERSION = function () {
- const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
- if (match && match[2]) {
- return parseFloat(match[2]);
- }
- return null;
- }();
- IE_VERSION = function () {
- const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
- let version = result && parseFloat(result[1]);
- if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
- // IE 11 has a different user agent string than other IE versions
- version = 11.0;
- }
- return version;
- }();
- IS_TIZEN = /Tizen/i.test(USER_AGENT);
- IS_WEBOS = /Web0S/i.test(USER_AGENT);
- IS_SMART_TV = IS_TIZEN || IS_WEBOS;
- IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV;
- IS_WINDOWS = /Windows/i.test(USER_AGENT);
- IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
- IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
- }
-
- /**
- * Whether or not this is an iOS device.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
-
- /**
- * Whether or not this is any flavor of Safari - including iOS.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
-
- var browser = /*#__PURE__*/Object.freeze({
- __proto__: null,
- get IS_IPOD () { return IS_IPOD; },
- get IOS_VERSION () { return IOS_VERSION; },
- get IS_ANDROID () { return IS_ANDROID; },
- get ANDROID_VERSION () { return ANDROID_VERSION; },
- get IS_FIREFOX () { return IS_FIREFOX; },
- get IS_EDGE () { return IS_EDGE; },
- get IS_CHROMIUM () { return IS_CHROMIUM; },
- get IS_CHROME () { return IS_CHROME; },
- get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
- get CHROME_VERSION () { return CHROME_VERSION; },
- get IE_VERSION () { return IE_VERSION; },
- get IS_SAFARI () { return IS_SAFARI; },
- get IS_WINDOWS () { return IS_WINDOWS; },
- get IS_IPAD () { return IS_IPAD; },
- get IS_IPHONE () { return IS_IPHONE; },
- get IS_TIZEN () { return IS_TIZEN; },
- get IS_WEBOS () { return IS_WEBOS; },
- get IS_SMART_TV () { return IS_SMART_TV; },
- TOUCH_ENABLED: TOUCH_ENABLED,
- IS_IOS: IS_IOS,
- IS_ANY_SAFARI: IS_ANY_SAFARI
- });
-
- /**
- * @file dom.js
- * @module dom
- */
-
- /**
- * Detect if a value is a string with any non-whitespace characters.
- *
- * @private
- * @param {string} str
- * The string to check
- *
- * @return {boolean}
- * Will be `true` if the string is non-blank, `false` otherwise.
- *
- */
- function isNonBlankString(str) {
- // we use str.trim as it will trim any whitespace characters
- // from the front or back of non-whitespace characters. aka
- // Any string that contains non-whitespace characters will
- // still contain them after `trim` but whitespace only strings
- // will have a length of 0, failing this check.
- return typeof str === 'string' && Boolean(str.trim());
- }
-
- /**
- * Throws an error if the passed string has whitespace. This is used by
- * class methods to be relatively consistent with the classList API.
- *
- * @private
- * @param {string} str
- * The string to check for whitespace.
- *
- * @throws {Error}
- * Throws an error if there is whitespace in the string.
- */
- function throwIfWhitespace(str) {
- // str.indexOf instead of regex because str.indexOf is faster performance wise.
- if (str.indexOf(' ') >= 0) {
- throw new Error('class has illegal whitespace characters');
- }
- }
-
- /**
- * Whether the current DOM interface appears to be real (i.e. not simulated).
- *
- * @return {boolean}
- * Will be `true` if the DOM appears to be real, `false` otherwise.
- */
- function isReal() {
- // Both document and window will never be undefined thanks to `global`.
- return document === window.document;
- }
-
- /**
- * Determines, via duck typing, whether or not a value is a DOM element.
- *
- * @param {*} value
- * The value to check.
- *
- * @return {boolean}
- * Will be `true` if the value is a DOM element, `false` otherwise.
- */
- function isEl(value) {
- return isObject(value) && value.nodeType === 1;
- }
-
- /**
- * Determines if the current DOM is embedded in an iframe.
- *
- * @return {boolean}
- * Will be `true` if the DOM is embedded in an iframe, `false`
- * otherwise.
- */
- function isInFrame() {
- // We need a try/catch here because Safari will throw errors when attempting
- // to get either `parent` or `self`
- try {
- return window.parent !== window.self;
- } catch (x) {
- return true;
- }
- }
-
- /**
- * Creates functions to query the DOM using a given method.
- *
- * @private
- * @param {string} method
- * The method to create the query with.
- *
- * @return {Function}
- * The query method
- */
- function createQuerier(method) {
- return function (selector, context) {
- if (!isNonBlankString(selector)) {
- return document[method](null);
- }
- if (isNonBlankString(context)) {
- context = document.querySelector(context);
- }
- const ctx = isEl(context) ? context : document;
- return ctx[method] && ctx[method](selector);
- };
- }
-
- /**
- * Creates an element and applies properties, attributes, and inserts content.
- *
- * @param {string} [tagName='div']
- * Name of tag to be created.
- *
- * @param {Object} [properties={}]
- * Element properties to be applied.
- *
- * @param {Object} [attributes={}]
- * Element attributes to be applied.
- *
- * @param {ContentDescriptor} [content]
- * A content descriptor object.
- *
- * @return {Element}
- * The element that was created.
- */
- function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
- const el = document.createElement(tagName);
- Object.getOwnPropertyNames(properties).forEach(function (propName) {
- const val = properties[propName];
-
- // Handle textContent since it's not supported everywhere and we have a
- // method for it.
- if (propName === 'textContent') {
- textContent(el, val);
- } else if (el[propName] !== val || propName === 'tabIndex') {
- el[propName] = val;
- }
- });
- Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
- el.setAttribute(attrName, attributes[attrName]);
- });
- if (content) {
- appendContent(el, content);
- }
- return el;
- }
-
- /**
- * Injects text into an element, replacing any existing contents entirely.
- *
- * @param {HTMLElement} el
- * The element to add text content into
- *
- * @param {string} text
- * The text content to add.
- *
- * @return {Element}
- * The element with added text content.
- */
- function textContent(el, text) {
- if (typeof el.textContent === 'undefined') {
- el.innerText = text;
- } else {
- el.textContent = text;
- }
- return el;
- }
-
- /**
- * Insert an element as the first child node of another
- *
- * @param {Element} child
- * Element to insert
- *
- * @param {Element} parent
- * Element to insert child into
- */
- function prependTo(child, parent) {
- if (parent.firstChild) {
- parent.insertBefore(child, parent.firstChild);
- } else {
- parent.appendChild(child);
- }
- }
-
- /**
- * Check if an element has a class name.
- *
- * @param {Element} element
- * Element to check
- *
- * @param {string} classToCheck
- * Class name to check for
- *
- * @return {boolean}
- * Will be `true` if the element has a class, `false` otherwise.
- *
- * @throws {Error}
- * Throws an error if `classToCheck` has white space.
- */
- function hasClass(element, classToCheck) {
- throwIfWhitespace(classToCheck);
- return element.classList.contains(classToCheck);
- }
-
- /**
- * Add a class name to an element.
- *
- * @param {Element} element
- * Element to add class name to.
- *
- * @param {...string} classesToAdd
- * One or more class name to add.
- *
- * @return {Element}
- * The DOM element with the added class name.
- */
- function addClass(element, ...classesToAdd) {
- element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
- return element;
- }
-
- /**
- * Remove a class name from an element.
- *
- * @param {Element} element
- * Element to remove a class name from.
- *
- * @param {...string} classesToRemove
- * One or more class name to remove.
- *
- * @return {Element}
- * The DOM element with class name removed.
- */
- function removeClass(element, ...classesToRemove) {
- // Protect in case the player gets disposed
- if (!element) {
- log.warn("removeClass was called with an element that doesn't exist");
- return null;
- }
- element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
- return element;
- }
-
- /**
- * The callback definition for toggleClass.
- *
- * @callback module:dom~PredicateCallback
- * @param {Element} element
- * The DOM element of the Component.
- *
- * @param {string} classToToggle
- * The `className` that wants to be toggled
- *
- * @return {boolean|undefined}
- * If `true` is returned, the `classToToggle` will be added to the
- * `element`. If `false`, the `classToToggle` will be removed from
- * the `element`. If `undefined`, the callback will be ignored.
- */
-
- /**
- * Adds or removes a class name to/from an element depending on an optional
- * condition or the presence/absence of the class name.
- *
- * @param {Element} element
- * The element to toggle a class name on.
- *
- * @param {string} classToToggle
- * The class that should be toggled.
- *
- * @param {boolean|module:dom~PredicateCallback} [predicate]
- * See the return value for {@link module:dom~PredicateCallback}
- *
- * @return {Element}
- * The element with a class that has been toggled.
- */
- function toggleClass(element, classToToggle, predicate) {
- if (typeof predicate === 'function') {
- predicate = predicate(element, classToToggle);
- }
- if (typeof predicate !== 'boolean') {
- predicate = undefined;
- }
- classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
- return element;
- }
-
- /**
- * Apply attributes to an HTML element.
- *
- * @param {Element} el
- * Element to add attributes to.
- *
- * @param {Object} [attributes]
- * Attributes to be applied.
- */
- function setAttributes(el, attributes) {
- Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
- const attrValue = attributes[attrName];
- if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
- el.removeAttribute(attrName);
- } else {
- el.setAttribute(attrName, attrValue === true ? '' : attrValue);
- }
- });
- }
-
- /**
- * Get an element's attribute values, as defined on the HTML tag.
- *
- * Attributes are not the same as properties. They're defined on the tag
- * or with setAttribute.
- *
- * @param {Element} tag
- * Element from which to get tag attributes.
- *
- * @return {Object}
- * All attributes of the element. Boolean attributes will be `true` or
- * `false`, others will be strings.
- */
- function getAttributes(tag) {
- const obj = {};
-
- // known boolean attributes
- // we can check for matching boolean properties, but not all browsers
- // and not all tags know about these attributes, so, we still want to check them manually
- const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
- if (tag && tag.attributes && tag.attributes.length > 0) {
- const attrs = tag.attributes;
- for (let i = attrs.length - 1; i >= 0; i--) {
- const attrName = attrs[i].name;
- /** @type {boolean|string} */
- let attrVal = attrs[i].value;
-
- // check for known booleans
- // the matching element property will return a value for typeof
- if (knownBooleans.includes(attrName)) {
- // the value of an included boolean attribute is typically an empty
- // string ('') which would equal false if we just check for a false value.
- // we also don't want support bad code like autoplay='false'
- attrVal = attrVal !== null ? true : false;
- }
- obj[attrName] = attrVal;
- }
- }
- return obj;
- }
-
- /**
- * Get the value of an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to get the value of.
- *
- * @return {string}
- * The value of the attribute.
- */
- function getAttribute(el, attribute) {
- return el.getAttribute(attribute);
- }
-
- /**
- * Set the value of an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to set.
- *
- * @param {string} value
- * Value to set the attribute to.
- */
- function setAttribute(el, attribute, value) {
- el.setAttribute(attribute, value);
- }
-
- /**
- * Remove an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to remove.
- */
- function removeAttribute(el, attribute) {
- el.removeAttribute(attribute);
- }
-
- /**
- * Attempt to block the ability to select text.
- */
- function blockTextSelection() {
- document.body.focus();
- document.onselectstart = function () {
- return false;
- };
- }
-
- /**
- * Turn off text selection blocking.
- */
- function unblockTextSelection() {
- document.onselectstart = function () {
- return true;
- };
- }
-
- /**
- * Identical to the native `getBoundingClientRect` function, but ensures that
- * the method is supported at all (it is in all browsers we claim to support)
- * and that the element is in the DOM before continuing.
- *
- * This wrapper function also shims properties which are not provided by some
- * older browsers (namely, IE8).
- *
- * Additionally, some browsers do not support adding properties to a
- * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
- * properties (except `x` and `y` which are not widely supported). This helps
- * avoid implementations where keys are non-enumerable.
- *
- * @param {Element} el
- * Element whose `ClientRect` we want to calculate.
- *
- * @return {Object|undefined}
- * Always returns a plain object - or `undefined` if it cannot.
- */
- function getBoundingClientRect(el) {
- if (el && el.getBoundingClientRect && el.parentNode) {
- const rect = el.getBoundingClientRect();
- const result = {};
- ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
- if (rect[k] !== undefined) {
- result[k] = rect[k];
- }
- });
- if (!result.height) {
- result.height = parseFloat(computedStyle(el, 'height'));
- }
- if (!result.width) {
- result.width = parseFloat(computedStyle(el, 'width'));
- }
- return result;
- }
- }
-
- /**
- * Represents the position of a DOM element on the page.
- *
- * @typedef {Object} module:dom~Position
- *
- * @property {number} left
- * Pixels to the left.
- *
- * @property {number} top
- * Pixels from the top.
- */
-
- /**
- * Get the position of an element in the DOM.
- *
- * Uses `getBoundingClientRect` technique from John Resig.
- *
- * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
- *
- * @param {Element} el
- * Element from which to get offset.
- *
- * @return {module:dom~Position}
- * The position of the element that was passed in.
- */
- function findPosition(el) {
- if (!el || el && !el.offsetParent) {
- return {
- left: 0,
- top: 0,
- width: 0,
- height: 0
- };
- }
- const width = el.offsetWidth;
- const height = el.offsetHeight;
- let left = 0;
- let top = 0;
- while (el.offsetParent && el !== document[FullscreenApi.fullscreenElement]) {
- left += el.offsetLeft;
- top += el.offsetTop;
- el = el.offsetParent;
- }
- return {
- left,
- top,
- width,
- height
- };
- }
-
- /**
- * Represents x and y coordinates for a DOM element or mouse pointer.
- *
- * @typedef {Object} module:dom~Coordinates
- *
- * @property {number} x
- * x coordinate in pixels
- *
- * @property {number} y
- * y coordinate in pixels
- */
-
- /**
- * Get the pointer position within an element.
- *
- * The base on the coordinates are the bottom left of the element.
- *
- * @param {Element} el
- * Element on which to get the pointer position on.
- *
- * @param {Event} event
- * Event object.
- *
- * @return {module:dom~Coordinates}
- * A coordinates object corresponding to the mouse position.
- *
- */
- function getPointerPosition(el, event) {
- const translated = {
- x: 0,
- y: 0
- };
- if (IS_IOS) {
- let item = el;
- while (item && item.nodeName.toLowerCase() !== 'html') {
- const transform = computedStyle(item, 'transform');
- if (/^matrix/.test(transform)) {
- const values = transform.slice(7, -1).split(/,\s/).map(Number);
- translated.x += values[4];
- translated.y += values[5];
- } else if (/^matrix3d/.test(transform)) {
- const values = transform.slice(9, -1).split(/,\s/).map(Number);
- translated.x += values[12];
- translated.y += values[13];
- }
- item = item.parentNode;
- }
- }
- const position = {};
- const boxTarget = findPosition(event.target);
- const box = findPosition(el);
- const boxW = box.width;
- const boxH = box.height;
- let offsetY = event.offsetY - (box.top - boxTarget.top);
- let offsetX = event.offsetX - (box.left - boxTarget.left);
- if (event.changedTouches) {
- offsetX = event.changedTouches[0].pageX - box.left;
- offsetY = event.changedTouches[0].pageY + box.top;
- if (IS_IOS) {
- offsetX -= translated.x;
- offsetY -= translated.y;
- }
- }
- position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
- position.x = Math.max(0, Math.min(1, offsetX / boxW));
- return position;
- }
-
- /**
- * Determines, via duck typing, whether or not a value is a text node.
- *
- * @param {*} value
- * Check if this value is a text node.
- *
- * @return {boolean}
- * Will be `true` if the value is a text node, `false` otherwise.
- */
- function isTextNode(value) {
- return isObject(value) && value.nodeType === 3;
- }
-
- /**
- * Empties the contents of an element.
- *
- * @param {Element} el
- * The element to empty children from
- *
- * @return {Element}
- * The element with no children
- */
- function emptyEl(el) {
- while (el.firstChild) {
- el.removeChild(el.firstChild);
- }
- return el;
- }
-
- /**
- * This is a mixed value that describes content to be injected into the DOM
- * via some method. It can be of the following types:
- *
- * Type | Description
- * -----------|-------------
- * `string` | The value will be normalized into a text node.
- * `Element` | The value will be accepted as-is.
- * `Text` | A TextNode. The value will be accepted as-is.
- * `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
- * `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
- *
- * @typedef {string|Element|Text|Array|Function} ContentDescriptor
- */
-
- /**
- * Normalizes content for eventual insertion into the DOM.
- *
- * This allows a wide range of content definition methods, but helps protect
- * from falling into the trap of simply writing to `innerHTML`, which could
- * be an XSS concern.
- *
- * The content for an element can be passed in multiple types and
- * combinations, whose behavior is as follows:
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Array}
- * All of the content that was passed in, normalized to an array of
- * elements or text nodes.
- */
- function normalizeContent(content) {
- // First, invoke content if it is a function. If it produces an array,
- // that needs to happen before normalization.
- if (typeof content === 'function') {
- content = content();
- }
-
- // Next up, normalize to an array, so one or many items can be normalized,
- // filtered, and returned.
- return (Array.isArray(content) ? content : [content]).map(value => {
- // First, invoke value if it is a function to produce a new value,
- // which will be subsequently normalized to a Node of some kind.
- if (typeof value === 'function') {
- value = value();
- }
- if (isEl(value) || isTextNode(value)) {
- return value;
- }
- if (typeof value === 'string' && /\S/.test(value)) {
- return document.createTextNode(value);
- }
- }).filter(value => value);
- }
-
- /**
- * Normalizes and appends content to an element.
- *
- * @param {Element} el
- * Element to append normalized content to.
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Element}
- * The element with appended normalized content.
- */
- function appendContent(el, content) {
- normalizeContent(content).forEach(node => el.appendChild(node));
- return el;
- }
-
- /**
- * Normalizes and inserts content into an element; this is identical to
- * `appendContent()`, except it empties the element first.
- *
- * @param {Element} el
- * Element to insert normalized content into.
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Element}
- * The element with inserted normalized content.
- */
- function insertContent(el, content) {
- return appendContent(emptyEl(el), content);
- }
-
- /**
- * Check if an event was a single left click.
- *
- * @param {MouseEvent} event
- * Event object.
- *
- * @return {boolean}
- * Will be `true` if a single left click, `false` otherwise.
- */
- function isSingleLeftClick(event) {
- // Note: if you create something draggable, be sure to
- // call it on both `mousedown` and `mousemove` event,
- // otherwise `mousedown` should be enough for a button
-
- if (event.button === undefined && event.buttons === undefined) {
- // Why do we need `buttons` ?
- // Because, middle mouse sometimes have this:
- // e.button === 0 and e.buttons === 4
- // Furthermore, we want to prevent combination click, something like
- // HOLD middlemouse then left click, that would be
- // e.button === 0, e.buttons === 5
- // just `button` is not gonna work
-
- // Alright, then what this block does ?
- // this is for chrome `simulate mobile devices`
- // I want to support this as well
-
- return true;
- }
- if (event.button === 0 && event.buttons === undefined) {
- // Touch screen, sometimes on some specific device, `buttons`
- // doesn't have anything (safari on ios, blackberry...)
-
- return true;
- }
-
- // `mouseup` event on a single left click has
- // `button` and `buttons` equal to 0
- if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
- return true;
- }
- if (event.button !== 0 || event.buttons !== 1) {
- // This is the reason we have those if else block above
- // if any special case we can catch and let it slide
- // we do it above, when get to here, this definitely
- // is-not-left-click
-
- return false;
- }
- return true;
- }
-
- /**
- * Finds a single DOM element matching `selector` within the optional
- * `context` of another DOM element (defaulting to `document`).
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelector`.
- *
- * @param {Element|String} [context=document]
- * A DOM element within which to query. Can also be a selector
- * string in which case the first matching element will be used
- * as context. If missing (or no element matches selector), falls
- * back to `document`.
- *
- * @return {Element|null}
- * The element that was found or null.
- */
- const $ = createQuerier('querySelector');
-
- /**
- * Finds a all DOM elements matching `selector` within the optional
- * `context` of another DOM element (defaulting to `document`).
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelectorAll`.
- *
- * @param {Element|String} [context=document]
- * A DOM element within which to query. Can also be a selector
- * string in which case the first matching element will be used
- * as context. If missing (or no element matches selector), falls
- * back to `document`.
- *
- * @return {NodeList}
- * A element list of elements that were found. Will be empty if none
- * were found.
- *
- */
- const $$ = createQuerier('querySelectorAll');
-
- /**
- * A safe getComputedStyle.
- *
- * This is needed because in Firefox, if the player is loaded in an iframe with
- * `display:none`, then `getComputedStyle` returns `null`, so, we do a
- * null-check to make sure that the player doesn't break in these cases.
- *
- * @param {Element} el
- * The element you want the computed style of
- *
- * @param {string} prop
- * The property name you want
- *
- * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
- */
- function computedStyle(el, prop) {
- if (!el || !prop) {
- return '';
- }
- if (typeof window.getComputedStyle === 'function') {
- let computedStyleValue;
- try {
- computedStyleValue = window.getComputedStyle(el);
- } catch (e) {
- return '';
- }
- return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
- }
- return '';
- }
-
- /**
- * Copy document style sheets to another window.
- *
- * @param {Window} win
- * The window element you want to copy the document style sheets to.
- *
- */
- function copyStyleSheetsToWindow(win) {
- [...document.styleSheets].forEach(styleSheet => {
- try {
- const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
- const style = document.createElement('style');
- style.textContent = cssRules;
- win.document.head.appendChild(style);
- } catch (e) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.type = styleSheet.type;
- // For older Safari this has to be the string; on other browsers setting the MediaList works
- link.media = styleSheet.media.mediaText;
- link.href = styleSheet.href;
- win.document.head.appendChild(link);
- }
- });
- }
-
- var Dom = /*#__PURE__*/Object.freeze({
- __proto__: null,
- isReal: isReal,
- isEl: isEl,
- isInFrame: isInFrame,
- createEl: createEl,
- textContent: textContent,
- prependTo: prependTo,
- hasClass: hasClass,
- addClass: addClass,
- removeClass: removeClass,
- toggleClass: toggleClass,
- setAttributes: setAttributes,
- getAttributes: getAttributes,
- getAttribute: getAttribute,
- setAttribute: setAttribute,
- removeAttribute: removeAttribute,
- blockTextSelection: blockTextSelection,
- unblockTextSelection: unblockTextSelection,
- getBoundingClientRect: getBoundingClientRect,
- findPosition: findPosition,
- getPointerPosition: getPointerPosition,
- isTextNode: isTextNode,
- emptyEl: emptyEl,
- normalizeContent: normalizeContent,
- appendContent: appendContent,
- insertContent: insertContent,
- isSingleLeftClick: isSingleLeftClick,
- $: $,
- $$: $$,
- computedStyle: computedStyle,
- copyStyleSheetsToWindow: copyStyleSheetsToWindow
- });
-
- /**
- * @file setup.js - Functions for setting up a player without
- * user interaction based on the data-setup `attribute` of the video tag.
- *
- * @module setup
- */
- let _windowLoaded = false;
- let videojs$1;
-
- /**
- * Set up any tags that have a data-setup `attribute` when the player is started.
- */
- const autoSetup = function () {
- if (videojs$1.options.autoSetup === false) {
- return;
- }
- const vids = Array.prototype.slice.call(document.getElementsByTagName('video'));
- const audios = Array.prototype.slice.call(document.getElementsByTagName('audio'));
- const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js'));
- const mediaEls = vids.concat(audios, divs);
-
- // Check if any media elements exist
- if (mediaEls && mediaEls.length > 0) {
- for (let i = 0, e = mediaEls.length; i < e; i++) {
- const mediaEl = mediaEls[i];
-
- // Check if element exists, has getAttribute func.
- if (mediaEl && mediaEl.getAttribute) {
- // Make sure this player hasn't already been set up.
- if (mediaEl.player === undefined) {
- const options = mediaEl.getAttribute('data-setup');
-
- // Check if data-setup attr exists.
- // We only auto-setup if they've added the data-setup attr.
- if (options !== null) {
- // Create new video.js instance.
- videojs$1(mediaEl);
- }
- }
-
- // If getAttribute isn't defined, we need to wait for the DOM.
- } else {
- autoSetupTimeout(1);
- break;
- }
- }
-
- // No videos were found, so keep looping unless page is finished loading.
- } else if (!_windowLoaded) {
- autoSetupTimeout(1);
- }
- };
-
- /**
- * Wait until the page is loaded before running autoSetup. This will be called in
- * autoSetup if `hasLoaded` returns false.
- *
- * @param {number} wait
- * How long to wait in ms
- *
- * @param {module:videojs} [vjs]
- * The videojs library function
- */
- function autoSetupTimeout(wait, vjs) {
- // Protect against breakage in non-browser environments
- if (!isReal()) {
- return;
- }
- if (vjs) {
- videojs$1 = vjs;
- }
- window.setTimeout(autoSetup, wait);
- }
-
- /**
- * Used to set the internal tracking of window loaded state to true.
- *
- * @private
- */
- function setWindowLoaded() {
- _windowLoaded = true;
- window.removeEventListener('load', setWindowLoaded);
- }
- if (isReal()) {
- if (document.readyState === 'complete') {
- setWindowLoaded();
- } else {
- /**
- * Listen for the load event on window, and set _windowLoaded to true.
- *
- * We use a standard event listener here to avoid incrementing the GUID
- * before any players are created.
- *
- * @listens load
- */
- window.addEventListener('load', setWindowLoaded);
- }
- }
-
- /**
- * @file stylesheet.js
- * @module stylesheet
- */
-
- /**
- * Create a DOM style element given a className for it.
- *
- * @param {string} className
- * The className to add to the created style element.
- *
- * @return {Element}
- * The element that was created.
- */
- const createStyleElement = function (className) {
- const style = document.createElement('style');
- style.className = className;
- return style;
- };
-
- /**
- * Add text to a DOM element.
- *
- * @param {Element} el
- * The Element to add text content to.
- *
- * @param {string} content
- * The text to add to the element.
- */
- const setTextContent = function (el, content) {
- if (el.styleSheet) {
- el.styleSheet.cssText = content;
- } else {
- el.textContent = content;
- }
- };
-
- /**
- * @file dom-data.js
- * @module dom-data
- */
-
- /**
- * Element Data Store.
- *
- * Allows for binding data to an element without putting it directly on the
- * element. Ex. Event listeners are stored here.
- * (also from jsninja.com, slightly modified and updated for closure compiler)
- *
- * @type {Object}
- * @private
- */
- var DomData = new WeakMap();
-
- /**
- * @file guid.js
- * @module guid
- */
-
- // Default value for GUIDs. This allows us to reset the GUID counter in tests.
- //
- // The initial GUID is 3 because some users have come to rely on the first
- // default player ID ending up as `vjs_video_3`.
- //
- // See: https://github.com/videojs/video.js/pull/6216
- const _initialGuid = 3;
-
- /**
- * Unique ID for an element or function
- *
- * @type {Number}
- */
- let _guid = _initialGuid;
-
- /**
- * Get a unique auto-incrementing ID by number that has not been returned before.
- *
- * @return {number}
- * A new unique ID.
- */
- function newGUID() {
- return _guid++;
- }
-
- /**
- * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
- * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
- * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
- * robust as jquery's, so there's probably some differences.
- *
- * @file events.js
- * @module events
- */
-
- /**
- * Clean up the listener cache and dispatchers
- *
- * @param {Element|Object} elem
- * Element to clean up
- *
- * @param {string} type
- * Type of event to clean up
- */
- function _cleanUpEvents(elem, type) {
- if (!DomData.has(elem)) {
- return;
- }
- const data = DomData.get(elem);
-
- // Remove the events of a particular type if there are none left
- if (data.handlers[type].length === 0) {
- delete data.handlers[type];
- // data.handlers[type] = null;
- // Setting to null was causing an error with data.handlers
-
- // Remove the meta-handler from the element
- if (elem.removeEventListener) {
- elem.removeEventListener(type, data.dispatcher, false);
- } else if (elem.detachEvent) {
- elem.detachEvent('on' + type, data.dispatcher);
- }
- }
-
- // Remove the events object if there are no types left
- if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
- delete data.handlers;
- delete data.dispatcher;
- delete data.disabled;
- }
-
- // Finally remove the element data if there is no data left
- if (Object.getOwnPropertyNames(data).length === 0) {
- DomData.delete(elem);
- }
- }
-
- /**
- * Loops through an array of event types and calls the requested method for each type.
- *
- * @param {Function} fn
- * The event method we want to use.
- *
- * @param {Element|Object} elem
- * Element or object to bind listeners to
- *
- * @param {string[]} types
- * Type of event to bind to.
- *
- * @param {Function} callback
- * Event listener.
- */
- function _handleMultipleEvents(fn, elem, types, callback) {
- types.forEach(function (type) {
- // Call the event method for each one of the types
- fn(elem, type, callback);
- });
- }
-
- /**
- * Fix a native event to have standard property values
- *
- * @param {Object} event
- * Event object to fix.
- *
- * @return {Object}
- * Fixed event object.
- */
- function fixEvent(event) {
- if (event.fixed_) {
- return event;
- }
- function returnTrue() {
- return true;
- }
- function returnFalse() {
- return false;
- }
-
- // Test if fixing up is needed
- // Used to check if !event.stopPropagation instead of isPropagationStopped
- // But native events return true for stopPropagation, but don't have
- // other expected methods like isPropagationStopped. Seems to be a problem
- // with the Javascript Ninja code. So we're just overriding all events now.
- if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
- const old = event || window.event;
- event = {};
- // Clone the old object so that we can modify the values event = {};
- // IE8 Doesn't like when you mess with native event properties
- // Firefox returns false for event.hasOwnProperty('type') and other props
- // which makes copying more difficult.
- // TODO: Probably best to create a whitelist of event props
- for (const key in old) {
- // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
- // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
- // and webkitMovementX/Y
- // Lighthouse complains if Event.path is copied
- if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
- // Chrome 32+ warns if you try to copy deprecated returnValue, but
- // we still want to if preventDefault isn't supported (IE8).
- if (!(key === 'returnValue' && old.preventDefault)) {
- event[key] = old[key];
- }
- }
- }
-
- // The event occurred on this element
- if (!event.target) {
- event.target = event.srcElement || document;
- }
-
- // Handle which other element the event is related to
- if (!event.relatedTarget) {
- event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
- }
-
- // Stop the default browser action
- event.preventDefault = function () {
- if (old.preventDefault) {
- old.preventDefault();
- }
- event.returnValue = false;
- old.returnValue = false;
- event.defaultPrevented = true;
- };
- event.defaultPrevented = false;
-
- // Stop the event from bubbling
- event.stopPropagation = function () {
- if (old.stopPropagation) {
- old.stopPropagation();
- }
- event.cancelBubble = true;
- old.cancelBubble = true;
- event.isPropagationStopped = returnTrue;
- };
- event.isPropagationStopped = returnFalse;
-
- // Stop the event from bubbling and executing other handlers
- event.stopImmediatePropagation = function () {
- if (old.stopImmediatePropagation) {
- old.stopImmediatePropagation();
- }
- event.isImmediatePropagationStopped = returnTrue;
- event.stopPropagation();
- };
- event.isImmediatePropagationStopped = returnFalse;
-
- // Handle mouse position
- if (event.clientX !== null && event.clientX !== undefined) {
- const doc = document.documentElement;
- const body = document.body;
- event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
- event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
- }
-
- // Handle key presses
- event.which = event.charCode || event.keyCode;
-
- // Fix button for mouse clicks:
- // 0 == left; 1 == middle; 2 == right
- if (event.button !== null && event.button !== undefined) {
- // The following is disabled because it does not pass videojs-standard
- // and... yikes.
- /* eslint-disable */
- event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
- /* eslint-enable */
- }
- }
-
- event.fixed_ = true;
- // Returns fixed-up instance
- return event;
- }
-
- /**
- * Whether passive event listeners are supported
- */
- let _supportsPassive;
- const supportsPassive = function () {
- if (typeof _supportsPassive !== 'boolean') {
- _supportsPassive = false;
- try {
- const opts = Object.defineProperty({}, 'passive', {
- get() {
- _supportsPassive = true;
- }
- });
- window.addEventListener('test', null, opts);
- window.removeEventListener('test', null, opts);
- } catch (e) {
- // disregard
- }
- }
- return _supportsPassive;
- };
-
- /**
- * Touch events Chrome expects to be passive
- */
- const passiveEvents = ['touchstart', 'touchmove'];
-
- /**
- * Add an event listener to element
- * It stores the handler function in a separate cache object
- * and adds a generic handler to the element's event,
- * along with a unique id (guid) to the element.
- *
- * @param {Element|Object} elem
- * Element or object to bind listeners to
- *
- * @param {string|string[]} type
- * Type of event to bind to.
- *
- * @param {Function} fn
- * Event listener.
- */
- function on(elem, type, fn) {
- if (Array.isArray(type)) {
- return _handleMultipleEvents(on, elem, type, fn);
- }
- if (!DomData.has(elem)) {
- DomData.set(elem, {});
- }
- const data = DomData.get(elem);
-
- // We need a place to store all our handler data
- if (!data.handlers) {
- data.handlers = {};
- }
- if (!data.handlers[type]) {
- data.handlers[type] = [];
- }
- if (!fn.guid) {
- fn.guid = newGUID();
- }
- data.handlers[type].push(fn);
- if (!data.dispatcher) {
- data.disabled = false;
- data.dispatcher = function (event, hash) {
- if (data.disabled) {
- return;
- }
- event = fixEvent(event);
- const handlers = data.handlers[event.type];
- if (handlers) {
- // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
- const handlersCopy = handlers.slice(0);
- for (let m = 0, n = handlersCopy.length; m < n; m++) {
- if (event.isImmediatePropagationStopped()) {
- break;
- } else {
- try {
- handlersCopy[m].call(elem, event, hash);
- } catch (e) {
- log.error(e);
- }
- }
- }
- }
- };
- }
- if (data.handlers[type].length === 1) {
- if (elem.addEventListener) {
- let options = false;
- if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
- options = {
- passive: true
- };
- }
- elem.addEventListener(type, data.dispatcher, options);
- } else if (elem.attachEvent) {
- elem.attachEvent('on' + type, data.dispatcher);
- }
- }
- }
-
- /**
- * Removes event listeners from an element
- *
- * @param {Element|Object} elem
- * Object to remove listeners from.
- *
- * @param {string|string[]} [type]
- * Type of listener to remove. Don't include to remove all events from element.
- *
- * @param {Function} [fn]
- * Specific listener to remove. Don't include to remove listeners for an event
- * type.
- */
- function off(elem, type, fn) {
- // Don't want to add a cache object through getElData if not needed
- if (!DomData.has(elem)) {
- return;
- }
- const data = DomData.get(elem);
-
- // If no events exist, nothing to unbind
- if (!data.handlers) {
- return;
- }
- if (Array.isArray(type)) {
- return _handleMultipleEvents(off, elem, type, fn);
- }
-
- // Utility function
- const removeType = function (el, t) {
- data.handlers[t] = [];
- _cleanUpEvents(el, t);
- };
-
- // Are we removing all bound events?
- if (type === undefined) {
- for (const t in data.handlers) {
- if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
- removeType(elem, t);
- }
- }
- return;
- }
- const handlers = data.handlers[type];
-
- // If no handlers exist, nothing to unbind
- if (!handlers) {
- return;
- }
-
- // If no listener was provided, remove all listeners for type
- if (!fn) {
- removeType(elem, type);
- return;
- }
-
- // We're only removing a single handler
- if (fn.guid) {
- for (let n = 0; n < handlers.length; n++) {
- if (handlers[n].guid === fn.guid) {
- handlers.splice(n--, 1);
- }
- }
- }
- _cleanUpEvents(elem, type);
- }
-
- /**
- * Trigger an event for an element
- *
- * @param {Element|Object} elem
- * Element to trigger an event on
- *
- * @param {EventTarget~Event|string} event
- * A string (the type) or an event object with a type attribute
- *
- * @param {Object} [hash]
- * data hash to pass along with the event
- *
- * @return {boolean|undefined}
- * Returns the opposite of `defaultPrevented` if default was
- * prevented. Otherwise, returns `undefined`
- */
- function trigger(elem, event, hash) {
- // Fetches element data and a reference to the parent (for bubbling).
- // Don't want to add a data object to cache for every parent,
- // so checking hasElData first.
- const elemData = DomData.has(elem) ? DomData.get(elem) : {};
- const parent = elem.parentNode || elem.ownerDocument;
- // type = event.type || event,
- // handler;
-
- // If an event name was passed as a string, creates an event out of it
- if (typeof event === 'string') {
- event = {
- type: event,
- target: elem
- };
- } else if (!event.target) {
- event.target = elem;
- }
-
- // Normalizes the event properties.
- event = fixEvent(event);
-
- // If the passed element has a dispatcher, executes the established handlers.
- if (elemData.dispatcher) {
- elemData.dispatcher.call(elem, event, hash);
- }
-
- // Unless explicitly stopped or the event does not bubble (e.g. media events)
- // recursively calls this function to bubble the event up the DOM.
- if (parent && !event.isPropagationStopped() && event.bubbles === true) {
- trigger.call(null, parent, event, hash);
-
- // If at the top of the DOM, triggers the default action unless disabled.
- } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
- if (!DomData.has(event.target)) {
- DomData.set(event.target, {});
- }
- const targetData = DomData.get(event.target);
-
- // Checks if the target has a default action for this event.
- if (event.target[event.type]) {
- // Temporarily disables event dispatching on the target as we have already executed the handler.
- targetData.disabled = true;
- // Executes the default action.
- if (typeof event.target[event.type] === 'function') {
- event.target[event.type]();
- }
- // Re-enables event dispatching.
- targetData.disabled = false;
- }
- }
-
- // Inform the triggerer if the default was prevented by returning false
- return !event.defaultPrevented;
- }
-
- /**
- * Trigger a listener only once for an event.
- *
- * @param {Element|Object} elem
- * Element or object to bind to.
- *
- * @param {string|string[]} type
- * Name/type of event
- *
- * @param {Event~EventListener} fn
- * Event listener function
- */
- function one(elem, type, fn) {
- if (Array.isArray(type)) {
- return _handleMultipleEvents(one, elem, type, fn);
- }
- const func = function () {
- off(elem, type, func);
- fn.apply(this, arguments);
- };
-
- // copy the guid to the new function so it can removed using the original function's ID
- func.guid = fn.guid = fn.guid || newGUID();
- on(elem, type, func);
- }
-
- /**
- * Trigger a listener only once and then turn if off for all
- * configured events
- *
- * @param {Element|Object} elem
- * Element or object to bind to.
- *
- * @param {string|string[]} type
- * Name/type of event
- *
- * @param {Event~EventListener} fn
- * Event listener function
- */
- function any(elem, type, fn) {
- const func = function () {
- off(elem, type, func);
- fn.apply(this, arguments);
- };
-
- // copy the guid to the new function so it can removed using the original function's ID
- func.guid = fn.guid = fn.guid || newGUID();
-
- // multiple ons, but one off for everything
- on(elem, type, func);
- }
-
- var Events = /*#__PURE__*/Object.freeze({
- __proto__: null,
- fixEvent: fixEvent,
- on: on,
- off: off,
- trigger: trigger,
- one: one,
- any: any
- });
-
- /**
- * @file fn.js
- * @module fn
- */
- const UPDATE_REFRESH_INTERVAL = 30;
-
- /**
- * A private, internal-only function for changing the context of a function.
- *
- * It also stores a unique id on the function so it can be easily removed from
- * events.
- *
- * @private
- * @function
- * @param {*} context
- * The object to bind as scope.
- *
- * @param {Function} fn
- * The function to be bound to a scope.
- *
- * @param {number} [uid]
- * An optional unique ID for the function to be set
- *
- * @return {Function}
- * The new function that will be bound into the context given
- */
- const bind_ = function (context, fn, uid) {
- // Make sure the function has a unique ID
- if (!fn.guid) {
- fn.guid = newGUID();
- }
-
- // Create the new function that changes the context
- const bound = fn.bind(context);
-
- // Allow for the ability to individualize this function
- // Needed in the case where multiple objects might share the same prototype
- // IF both items add an event listener with the same function, then you try to remove just one
- // it will remove both because they both have the same guid.
- // when using this, you need to use the bind method when you remove the listener as well.
- // currently used in text tracks
- bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
- return bound;
- };
-
- /**
- * Wraps the given function, `fn`, with a new function that only invokes `fn`
- * at most once per every `wait` milliseconds.
- *
- * @function
- * @param {Function} fn
- * The function to be throttled.
- *
- * @param {number} wait
- * The number of milliseconds by which to throttle.
- *
- * @return {Function}
- */
- const throttle = function (fn, wait) {
- let last = window.performance.now();
- const throttled = function (...args) {
- const now = window.performance.now();
- if (now - last >= wait) {
- fn(...args);
- last = now;
- }
- };
- return throttled;
- };
-
- /**
- * Creates a debounced function that delays invoking `func` until after `wait`
- * milliseconds have elapsed since the last time the debounced function was
- * invoked.
- *
- * Inspired by lodash and underscore implementations.
- *
- * @function
- * @param {Function} func
- * The function to wrap with debounce behavior.
- *
- * @param {number} wait
- * The number of milliseconds to wait after the last invocation.
- *
- * @param {boolean} [immediate]
- * Whether or not to invoke the function immediately upon creation.
- *
- * @param {Object} [context=window]
- * The "context" in which the debounced function should debounce. For
- * example, if this function should be tied to a Video.js player,
- * the player can be passed here. Alternatively, defaults to the
- * global `window` object.
- *
- * @return {Function}
- * A debounced function.
- */
- const debounce = function (func, wait, immediate, context = window) {
- let timeout;
- const cancel = () => {
- context.clearTimeout(timeout);
- timeout = null;
- };
-
- /* eslint-disable consistent-this */
- const debounced = function () {
- const self = this;
- const args = arguments;
- let later = function () {
- timeout = null;
- later = null;
- if (!immediate) {
- func.apply(self, args);
- }
- };
- if (!timeout && immediate) {
- func.apply(self, args);
- }
- context.clearTimeout(timeout);
- timeout = context.setTimeout(later, wait);
- };
- /* eslint-enable consistent-this */
-
- debounced.cancel = cancel;
- return debounced;
- };
-
- var Fn = /*#__PURE__*/Object.freeze({
- __proto__: null,
- UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
- bind_: bind_,
- throttle: throttle,
- debounce: debounce
- });
-
- /**
- * @file src/js/event-target.js
- */
- let EVENT_MAP;
-
- /**
- * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
- * adds shorthand functions that wrap around lengthy functions. For example:
- * the `on` function is a wrapper around `addEventListener`.
- *
- * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
- * @class EventTarget
- */
- class EventTarget {
- /**
- * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
- * function that will get called when an event with a certain name gets triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to call with `EventTarget`s
- */
- on(type, fn) {
- // Remove the addEventListener alias before calling Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- on(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * Removes an `event listener` for a specific event from an instance of `EventTarget`.
- * This makes it so that the `event listener` will no longer get called when the
- * named event happens.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to remove.
- */
- off(type, fn) {
- off(this, type, fn);
- }
- /**
- * This function will add an `event listener` that gets triggered only once. After the
- * first trigger it will get removed. This is like adding an `event listener`
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- one(type, fn) {
- // Remove the addEventListener aliasing Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- one(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * This function will add an `event listener` that gets triggered only once and is
- * removed from all events. This is like adding an array of `event listener`s
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
- * first time it is triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- any(type, fn) {
- // Remove the addEventListener aliasing Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- any(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * This function causes an event to happen. This will then cause any `event listeners`
- * that are waiting for that event, to get called. If there are no `event listeners`
- * for an event then nothing will happen.
- *
- * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
- * Trigger will also call the `on` + `uppercaseEventName` function.
- *
- * Example:
- * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
- * `onClick` if it exists.
- *
- * @param {string|EventTarget~Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- */
- trigger(event) {
- const type = event.type || event;
-
- // deprecation
- // In a future version we should default target to `this`
- // similar to how we default the target to `elem` in
- // `Events.trigger`. Right now the default `target` will be
- // `document` due to the `Event.fixEvent` call.
- if (typeof event === 'string') {
- event = {
- type
- };
- }
- event = fixEvent(event);
- if (this.allowedEvents_[type] && this['on' + type]) {
- this['on' + type](event);
- }
- trigger(this, event);
- }
- queueTrigger(event) {
- // only set up EVENT_MAP if it'll be used
- if (!EVENT_MAP) {
- EVENT_MAP = new Map();
- }
- const type = event.type || event;
- let map = EVENT_MAP.get(this);
- if (!map) {
- map = new Map();
- EVENT_MAP.set(this, map);
- }
- const oldTimeout = map.get(type);
- map.delete(type);
- window.clearTimeout(oldTimeout);
- const timeout = window.setTimeout(() => {
- map.delete(type);
- // if we cleared out all timeouts for the current target, delete its map
- if (map.size === 0) {
- map = null;
- EVENT_MAP.delete(this);
- }
- this.trigger(event);
- }, 0);
- map.set(type, timeout);
- }
- }
-
- /**
- * A Custom DOM event.
- *
- * @typedef {CustomEvent} Event
- * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
- */
-
- /**
- * All event listeners should follow the following format.
- *
- * @callback EventListener
- * @this {EventTarget}
- *
- * @param {Event} event
- * the event that triggered this function
- *
- * @param {Object} [hash]
- * hash of data sent during the event
- */
-
- /**
- * An object containing event names as keys and booleans as values.
- *
- * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
- * will have extra functionality. See that function for more information.
- *
- * @property EventTarget.prototype.allowedEvents_
- * @protected
- */
- EventTarget.prototype.allowedEvents_ = {};
-
- /**
- * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#on}
- */
- EventTarget.prototype.addEventListener = EventTarget.prototype.on;
-
- /**
- * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#off}
- */
- EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
-
- /**
- * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#trigger}
- */
- EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
-
- /**
- * @file mixins/evented.js
- * @module evented
- */
- const objName = obj => {
- if (typeof obj.name === 'function') {
- return obj.name();
- }
- if (typeof obj.name === 'string') {
- return obj.name;
- }
- if (obj.name_) {
- return obj.name_;
- }
- if (obj.constructor && obj.constructor.name) {
- return obj.constructor.name;
- }
- return typeof obj;
- };
-
- /**
- * Returns whether or not an object has had the evented mixin applied.
- *
- * @param {Object} object
- * An object to test.
- *
- * @return {boolean}
- * Whether or not the object appears to be evented.
- */
- const isEvented = object => object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
-
- /**
- * Adds a callback to run after the evented mixin applied.
- *
- * @param {Object} target
- * An object to Add
- * @param {Function} callback
- * The callback to run.
- */
- const addEventedCallback = (target, callback) => {
- if (isEvented(target)) {
- callback();
- } else {
- if (!target.eventedCallbacks) {
- target.eventedCallbacks = [];
- }
- target.eventedCallbacks.push(callback);
- }
- };
-
- /**
- * Whether a value is a valid event type - non-empty string or array.
- *
- * @private
- * @param {string|Array} type
- * The type value to test.
- *
- * @return {boolean}
- * Whether or not the type is a valid event type.
- */
- const isValidEventType = type =>
- // The regex here verifies that the `type` contains at least one non-
- // whitespace character.
- typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
-
- /**
- * Validates a value to determine if it is a valid event target. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the target does not appear to be a valid event target.
- *
- * @param {Object} target
- * The object to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateTarget = (target, obj, fnName) => {
- if (!target || !target.nodeName && !isEvented(target)) {
- throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
- }
- };
-
- /**
- * Validates a value to determine if it is a valid event target. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the type does not appear to be a valid event type.
- *
- * @param {string|Array} type
- * The type to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateEventType = (type, obj, fnName) => {
- if (!isValidEventType(type)) {
- throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
- }
- };
-
- /**
- * Validates a value to determine if it is a valid listener. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the listener is not a function.
- *
- * @param {Function} listener
- * The listener to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateListener = (listener, obj, fnName) => {
- if (typeof listener !== 'function') {
- throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
- }
- };
-
- /**
- * Takes an array of arguments given to `on()` or `one()`, validates them, and
- * normalizes them into an object.
- *
- * @private
- * @param {Object} self
- * The evented object on which `on()` or `one()` was called. This
- * object will be bound as the `this` value for the listener.
- *
- * @param {Array} args
- * An array of arguments passed to `on()` or `one()`.
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- *
- * @return {Object}
- * An object containing useful values for `on()` or `one()` calls.
- */
- const normalizeListenArgs = (self, args, fnName) => {
- // If the number of arguments is less than 3, the target is always the
- // evented object itself.
- const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
- let target;
- let type;
- let listener;
- if (isTargetingSelf) {
- target = self.eventBusEl_;
-
- // Deal with cases where we got 3 arguments, but we are still listening to
- // the evented object itself.
- if (args.length >= 3) {
- args.shift();
- }
- [type, listener] = args;
- } else {
- [target, type, listener] = args;
- }
- validateTarget(target, self, fnName);
- validateEventType(type, self, fnName);
- validateListener(listener, self, fnName);
- listener = bind_(self, listener);
- return {
- isTargetingSelf,
- target,
- type,
- listener
- };
- };
-
- /**
- * Adds the listener to the event type(s) on the target, normalizing for
- * the type of target.
- *
- * @private
- * @param {Element|Object} target
- * A DOM node or evented object.
- *
- * @param {string} method
- * The event binding method to use ("on" or "one").
- *
- * @param {string|Array} type
- * One or more event type(s).
- *
- * @param {Function} listener
- * A listener function.
- */
- const listen = (target, method, type, listener) => {
- validateTarget(target, target, method);
- if (target.nodeName) {
- Events[method](target, type, listener);
- } else {
- target[method](type, listener);
- }
- };
-
- /**
- * Contains methods that provide event capabilities to an object which is passed
- * to {@link module:evented|evented}.
- *
- * @mixin EventedMixin
- */
- const EventedMixin = {
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- on(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'on');
- listen(target, 'on', type, listener);
-
- // If this object is listening to another evented object.
- if (!isTargetingSelf) {
- // If this object is disposed, remove the listener.
- const removeListenerOnDispose = () => this.off(target, type, listener);
-
- // Use the same function ID as the listener so we can remove it later it
- // using the ID of the original listener.
- removeListenerOnDispose.guid = listener.guid;
-
- // Add a listener to the target's dispose event as well. This ensures
- // that if the target is disposed BEFORE this object, we remove the
- // removal listener that was just added. Otherwise, we create a memory leak.
- const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- removeRemoverOnTargetDispose.guid = listener.guid;
- listen(this, 'on', 'dispose', removeListenerOnDispose);
- listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
- }
- },
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object. The listener will be called once per event and then removed.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- one(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'one');
-
- // Targeting this evented object.
- if (isTargetingSelf) {
- listen(target, 'one', type, listener);
-
- // Targeting another evented object.
- } else {
- // TODO: This wrapper is incorrect! It should only
- // remove the wrapper for the event type that called it.
- // Instead all listeners are removed on the first trigger!
- // see https://github.com/videojs/video.js/issues/5962
- const wrapper = (...largs) => {
- this.off(target, type, wrapper);
- listener.apply(null, largs);
- };
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- wrapper.guid = listener.guid;
- listen(target, 'one', type, wrapper);
- }
- },
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object. The listener will only be called once for the first event that is triggered
- * then removed.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- any(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'any');
-
- // Targeting this evented object.
- if (isTargetingSelf) {
- listen(target, 'any', type, listener);
-
- // Targeting another evented object.
- } else {
- const wrapper = (...largs) => {
- this.off(target, type, wrapper);
- listener.apply(null, largs);
- };
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- wrapper.guid = listener.guid;
- listen(target, 'any', type, wrapper);
- }
- },
- /**
- * Removes listener(s) from event(s) on an evented object.
- *
- * @param {string|Array|Element|Object} [targetOrType]
- * If this is a string or array, it represents the event type(s).
- *
- * Another evented object can be passed here instead, in which case
- * ALL 3 arguments are _required_.
- *
- * @param {string|Array|Function} [typeOrListener]
- * If the first argument was a string or array, this may be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function; otherwise, _all_ listeners bound to the
- * event type(s) will be removed.
- */
- off(targetOrType, typeOrListener, listener) {
- // Targeting this evented object.
- if (!targetOrType || isValidEventType(targetOrType)) {
- off(this.eventBusEl_, targetOrType, typeOrListener);
-
- // Targeting another evented object.
- } else {
- const target = targetOrType;
- const type = typeOrListener;
-
- // Fail fast and in a meaningful way!
- validateTarget(target, this, 'off');
- validateEventType(type, this, 'off');
- validateListener(listener, this, 'off');
-
- // Ensure there's at least a guid, even if the function hasn't been used
- listener = bind_(this, listener);
-
- // Remove the dispose listener on this evented object, which was given
- // the same guid as the event listener in on().
- this.off('dispose', listener);
- if (target.nodeName) {
- off(target, type, listener);
- off(target, 'dispose', listener);
- } else if (isEvented(target)) {
- target.off(type, listener);
- target.off('dispose', listener);
- }
- }
- },
- /**
- * Fire an event on this evented object, causing its listeners to be called.
- *
- * @param {string|Object} event
- * An event type or an object with a type property.
- *
- * @param {Object} [hash]
- * An additional object to pass along to listeners.
- *
- * @return {boolean}
- * Whether or not the default behavior was prevented.
- */
- trigger(event, hash) {
- validateTarget(this.eventBusEl_, this, 'trigger');
- const type = event && typeof event !== 'string' ? event.type : event;
- if (!isValidEventType(type)) {
- throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
- }
- return trigger(this.eventBusEl_, event, hash);
- }
- };
-
- /**
- * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
- *
- * @param {Object} target
- * The object to which to add event methods.
- *
- * @param {Object} [options={}]
- * Options for customizing the mixin behavior.
- *
- * @param {string} [options.eventBusKey]
- * By default, adds a `eventBusEl_` DOM element to the target object,
- * which is used as an event bus. If the target object already has a
- * DOM element that should be used, pass its key here.
- *
- * @return {Object}
- * The target object.
- */
- function evented(target, options = {}) {
- const {
- eventBusKey
- } = options;
-
- // Set or create the eventBusEl_.
- if (eventBusKey) {
- if (!target[eventBusKey].nodeName) {
- throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
- }
- target.eventBusEl_ = target[eventBusKey];
- } else {
- target.eventBusEl_ = createEl('span', {
- className: 'vjs-event-bus'
- });
- }
- Object.assign(target, EventedMixin);
- if (target.eventedCallbacks) {
- target.eventedCallbacks.forEach(callback => {
- callback();
- });
- }
-
- // When any evented object is disposed, it removes all its listeners.
- target.on('dispose', () => {
- target.off();
- [target, target.el_, target.eventBusEl_].forEach(function (val) {
- if (val && DomData.has(val)) {
- DomData.delete(val);
- }
- });
- window.setTimeout(() => {
- target.eventBusEl_ = null;
- }, 0);
- });
- return target;
- }
-
- /**
- * @file mixins/stateful.js
- * @module stateful
- */
-
- /**
- * Contains methods that provide statefulness to an object which is passed
- * to {@link module:stateful}.
- *
- * @mixin StatefulMixin
- */
- const StatefulMixin = {
- /**
- * A hash containing arbitrary keys and values representing the state of
- * the object.
- *
- * @type {Object}
- */
- state: {},
- /**
- * Set the state of an object by mutating its
- * {@link module:stateful~StatefulMixin.state|state} object in place.
- *
- * @fires module:stateful~StatefulMixin#statechanged
- * @param {Object|Function} stateUpdates
- * A new set of properties to shallow-merge into the plugin state.
- * Can be a plain object or a function returning a plain object.
- *
- * @return {Object|undefined}
- * An object containing changes that occurred. If no changes
- * occurred, returns `undefined`.
- */
- setState(stateUpdates) {
- // Support providing the `stateUpdates` state as a function.
- if (typeof stateUpdates === 'function') {
- stateUpdates = stateUpdates();
- }
- let changes;
- each(stateUpdates, (value, key) => {
- // Record the change if the value is different from what's in the
- // current state.
- if (this.state[key] !== value) {
- changes = changes || {};
- changes[key] = {
- from: this.state[key],
- to: value
- };
- }
- this.state[key] = value;
- });
-
- // Only trigger "statechange" if there were changes AND we have a trigger
- // function. This allows us to not require that the target object be an
- // evented object.
- if (changes && isEvented(this)) {
- /**
- * An event triggered on an object that is both
- * {@link module:stateful|stateful} and {@link module:evented|evented}
- * indicating that its state has changed.
- *
- * @event module:stateful~StatefulMixin#statechanged
- * @type {Object}
- * @property {Object} changes
- * A hash containing the properties that were changed and
- * the values they were changed `from` and `to`.
- */
- this.trigger({
- changes,
- type: 'statechanged'
- });
- }
- return changes;
- }
- };
-
- /**
- * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
- * object.
- *
- * If the target object is {@link module:evented|evented} and has a
- * `handleStateChanged` method, that method will be automatically bound to the
- * `statechanged` event on itself.
- *
- * @param {Object} target
- * The object to be made stateful.
- *
- * @param {Object} [defaultState]
- * A default set of properties to populate the newly-stateful object's
- * `state` property.
- *
- * @return {Object}
- * Returns the `target`.
- */
- function stateful(target, defaultState) {
- Object.assign(target, StatefulMixin);
-
- // This happens after the mixing-in because we need to replace the `state`
- // added in that step.
- target.state = Object.assign({}, target.state, defaultState);
-
- // Auto-bind the `handleStateChanged` method of the target object if it exists.
- if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
- target.on('statechanged', target.handleStateChanged);
- }
- return target;
- }
-
- /**
- * @file str.js
- * @module to-lower-case
- */
-
- /**
- * Lowercase the first letter of a string.
- *
- * @param {string} string
- * String to be lowercased
- *
- * @return {string}
- * The string with a lowercased first letter
- */
- const toLowerCase = function (string) {
- if (typeof string !== 'string') {
- return string;
- }
- return string.replace(/./, w => w.toLowerCase());
- };
-
- /**
- * Uppercase the first letter of a string.
- *
- * @param {string} string
- * String to be uppercased
- *
- * @return {string}
- * The string with an uppercased first letter
- */
- const toTitleCase = function (string) {
- if (typeof string !== 'string') {
- return string;
- }
- return string.replace(/./, w => w.toUpperCase());
- };
-
- /**
- * Compares the TitleCase versions of the two strings for equality.
- *
- * @param {string} str1
- * The first string to compare
- *
- * @param {string} str2
- * The second string to compare
- *
- * @return {boolean}
- * Whether the TitleCase versions of the strings are equal
- */
- const titleCaseEquals = function (str1, str2) {
- return toTitleCase(str1) === toTitleCase(str2);
- };
-
- var Str = /*#__PURE__*/Object.freeze({
- __proto__: null,
- toLowerCase: toLowerCase,
- toTitleCase: toTitleCase,
- titleCaseEquals: titleCaseEquals
- });
-
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
-
- function unwrapExports (x) {
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
- }
-
- function createCommonjsModule(fn, module) {
- return module = { exports: {} }, fn(module, module.exports), module.exports;
- }
-
- var keycode = createCommonjsModule(function (module, exports) {
- // Source: http://jsfiddle.net/vWx8V/
- // http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes
-
- /**
- * Conenience method returns corresponding value for given keyName or keyCode.
- *
- * @param {Mixed} keyCode {Number} or keyName {String}
- * @return {Mixed}
- * @api public
- */
-
- function keyCode(searchInput) {
- // Keyboard Events
- if (searchInput && 'object' === typeof searchInput) {
- var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
- if (hasKeyCode) searchInput = hasKeyCode;
- }
-
- // Numbers
- if ('number' === typeof searchInput) return names[searchInput];
-
- // Everything else (cast to string)
- var search = String(searchInput);
-
- // check codes
- var foundNamedKey = codes[search.toLowerCase()];
- if (foundNamedKey) return foundNamedKey;
-
- // check aliases
- var foundNamedKey = aliases[search.toLowerCase()];
- if (foundNamedKey) return foundNamedKey;
-
- // weird character?
- if (search.length === 1) return search.charCodeAt(0);
- return undefined;
- }
-
- /**
- * Compares a keyboard event with a given keyCode or keyName.
- *
- * @param {Event} event Keyboard event that should be tested
- * @param {Mixed} keyCode {Number} or keyName {String}
- * @return {Boolean}
- * @api public
- */
- keyCode.isEventKey = function isEventKey(event, nameOrCode) {
- if (event && 'object' === typeof event) {
- var keyCode = event.which || event.keyCode || event.charCode;
- if (keyCode === null || keyCode === undefined) {
- return false;
- }
- if (typeof nameOrCode === 'string') {
- // check codes
- var foundNamedKey = codes[nameOrCode.toLowerCase()];
- if (foundNamedKey) {
- return foundNamedKey === keyCode;
- }
-
- // check aliases
- var foundNamedKey = aliases[nameOrCode.toLowerCase()];
- if (foundNamedKey) {
- return foundNamedKey === keyCode;
- }
- } else if (typeof nameOrCode === 'number') {
- return nameOrCode === keyCode;
- }
- return false;
- }
- };
- exports = module.exports = keyCode;
-
- /**
- * Get by name
- *
- * exports.code['enter'] // => 13
- */
-
- var codes = exports.code = exports.codes = {
- 'backspace': 8,
- 'tab': 9,
- 'enter': 13,
- 'shift': 16,
- 'ctrl': 17,
- 'alt': 18,
- 'pause/break': 19,
- 'caps lock': 20,
- 'esc': 27,
- 'space': 32,
- 'page up': 33,
- 'page down': 34,
- 'end': 35,
- 'home': 36,
- 'left': 37,
- 'up': 38,
- 'right': 39,
- 'down': 40,
- 'insert': 45,
- 'delete': 46,
- 'command': 91,
- 'left command': 91,
- 'right command': 93,
- 'numpad *': 106,
- 'numpad +': 107,
- 'numpad -': 109,
- 'numpad .': 110,
- 'numpad /': 111,
- 'num lock': 144,
- 'scroll lock': 145,
- 'my computer': 182,
- 'my calculator': 183,
- ';': 186,
- '=': 187,
- ',': 188,
- '-': 189,
- '.': 190,
- '/': 191,
- '`': 192,
- '[': 219,
- '\\': 220,
- ']': 221,
- "'": 222
- };
-
- // Helper aliases
-
- var aliases = exports.aliases = {
- 'windows': 91,
- '⇧': 16,
- '⌥': 18,
- '⌃': 17,
- '⌘': 91,
- 'ctl': 17,
- 'control': 17,
- 'option': 18,
- 'pause': 19,
- 'break': 19,
- 'caps': 20,
- 'return': 13,
- 'escape': 27,
- 'spc': 32,
- 'spacebar': 32,
- 'pgup': 33,
- 'pgdn': 34,
- 'ins': 45,
- 'del': 46,
- 'cmd': 91
- };
-
- /*!
- * Programatically add the following
- */
-
- // lower case chars
- for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32;
-
- // numbers
- for (var i = 48; i < 58; i++) codes[i - 48] = i;
-
- // function keys
- for (i = 1; i < 13; i++) codes['f' + i] = i + 111;
-
- // numpad keys
- for (i = 0; i < 10; i++) codes['numpad ' + i] = i + 96;
-
- /**
- * Get by code
- *
- * exports.name[13] // => 'Enter'
- */
-
- var names = exports.names = exports.title = {}; // title for backward compat
-
- // Create reverse mapping
- for (i in codes) names[codes[i]] = i;
-
- // Add aliases
- for (var alias in aliases) {
- codes[alias] = aliases[alias];
- }
- });
- keycode.code;
- keycode.codes;
- keycode.aliases;
- keycode.names;
- keycode.title;
-
- /**
- * Player Component - Base class for all UI objects
- *
- * @file component.js
- */
-
- /**
- * Base class for all UI Components.
- * Components are UI objects which represent both a javascript object and an element
- * in the DOM. They can be children of other components, and can have
- * children themselves.
- *
- * Components can also use methods from {@link EventTarget}
- */
- class Component {
- /**
- * A callback that is called when a component is ready. Does not have any
- * parameters and any callback value will be ignored.
- *
- * @callback ReadyCallback
- * @this Component
- */
-
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of component options.
- *
- * @param {Object[]} [options.children]
- * An array of children objects to initialize this component with. Children objects have
- * a name property that will be used if more than one component of the same type needs to be
- * added.
- *
- * @param {string} [options.className]
- * A class or space separated list of classes to add the component
- *
- * @param {ReadyCallback} [ready]
- * Function that gets called when the `Component` is ready.
- */
- constructor(player, options, ready) {
- // The component might be the player itself and we can't pass `this` to super
- if (!player && this.play) {
- this.player_ = player = this; // eslint-disable-line
- } else {
- this.player_ = player;
- }
- this.isDisposed_ = false;
-
- // Hold the reference to the parent component via `addChild` method
- this.parentComponent_ = null;
-
- // Make a copy of prototype.options_ to protect against overriding defaults
- this.options_ = merge({}, this.options_);
-
- // Updated options with supplied options
- options = this.options_ = merge(this.options_, options);
-
- // Get ID from options or options element if one is supplied
- this.id_ = options.id || options.el && options.el.id;
-
- // If there was no ID from the options, generate one
- if (!this.id_) {
- // Don't require the player ID function in the case of mock players
- const id = player && player.id && player.id() || 'no_player';
- this.id_ = `${id}_component_${newGUID()}`;
- }
- this.name_ = options.name || null;
-
- // Create element if one wasn't provided in options
- if (options.el) {
- this.el_ = options.el;
- } else if (options.createEl !== false) {
- this.el_ = this.createEl();
- }
- if (options.className && this.el_) {
- options.className.split(' ').forEach(c => this.addClass(c));
- }
-
- // Remove the placeholder event methods. If the component is evented, the
- // real methods are added next
- ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
- this[fn] = undefined;
- });
-
- // if evented is anything except false, we want to mixin in evented
- if (options.evented !== false) {
- // Make this an evented object and use `el_`, if available, as its event bus
- evented(this, {
- eventBusKey: this.el_ ? 'el_' : null
- });
- this.handleLanguagechange = this.handleLanguagechange.bind(this);
- this.on(this.player_, 'languagechange', this.handleLanguagechange);
- }
- stateful(this, this.constructor.defaultState);
- this.children_ = [];
- this.childIndex_ = {};
- this.childNameIndex_ = {};
- this.setTimeoutIds_ = new Set();
- this.setIntervalIds_ = new Set();
- this.rafIds_ = new Set();
- this.namedRafs_ = new Map();
- this.clearingTimersOnDispose_ = false;
-
- // Add any child components in options
- if (options.initChildren !== false) {
- this.initChildren();
- }
-
- // Don't want to trigger ready here or it will go before init is actually
- // finished for all children that run this constructor
- this.ready(ready);
- if (options.reportTouchActivity !== false) {
- this.enableTouchActivity();
- }
- }
-
- // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
- // They are replaced or removed in the constructor
-
- /**
- * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
- * function that will get called when an event with a certain name gets triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to call with `EventTarget`s
- */
- on(type, fn) {}
-
- /**
- * Removes an `event listener` for a specific event from an instance of `EventTarget`.
- * This makes it so that the `event listener` will no longer get called when the
- * named event happens.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} [fn]
- * The function to remove. If not specified, all listeners managed by Video.js will be removed.
- */
- off(type, fn) {}
-
- /**
- * This function will add an `event listener` that gets triggered only once. After the
- * first trigger it will get removed. This is like adding an `event listener`
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- one(type, fn) {}
-
- /**
- * This function will add an `event listener` that gets triggered only once and is
- * removed from all events. This is like adding an array of `event listener`s
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
- * first time it is triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- any(type, fn) {}
-
- /**
- * This function causes an event to happen. This will then cause any `event listeners`
- * that are waiting for that event, to get called. If there are no `event listeners`
- * for an event then nothing will happen.
- *
- * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
- * Trigger will also call the `on` + `uppercaseEventName` function.
- *
- * Example:
- * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
- * `onClick` if it exists.
- *
- * @param {string|Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- *
- * @param {Object} [hash]
- * Optionally extra argument to pass through to an event listener
- */
- trigger(event, hash) {}
-
- /**
- * Dispose of the `Component` and all child components.
- *
- * @fires Component#dispose
- *
- * @param {Object} options
- * @param {Element} options.originalEl element with which to replace player element
- */
- dispose(options = {}) {
- // Bail out if the component has already been disposed.
- if (this.isDisposed_) {
- return;
- }
- if (this.readyQueue_) {
- this.readyQueue_.length = 0;
- }
-
- /**
- * Triggered when a `Component` is disposed.
- *
- * @event Component#dispose
- * @type {Event}
- *
- * @property {boolean} [bubbles=false]
- * set to false so that the dispose event does not
- * bubble up
- */
- this.trigger({
- type: 'dispose',
- bubbles: false
- });
- this.isDisposed_ = true;
-
- // Dispose all children.
- if (this.children_) {
- for (let i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i].dispose) {
- this.children_[i].dispose();
- }
- }
- }
-
- // Delete child references
- this.children_ = null;
- this.childIndex_ = null;
- this.childNameIndex_ = null;
- this.parentComponent_ = null;
- if (this.el_) {
- // Remove element from DOM
- if (this.el_.parentNode) {
- if (options.restoreEl) {
- this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
- } else {
- this.el_.parentNode.removeChild(this.el_);
- }
- }
- this.el_ = null;
- }
-
- // remove reference to the player after disposing of the element
- this.player_ = null;
- }
-
- /**
- * Determine whether or not this component has been disposed.
- *
- * @return {boolean}
- * If the component has been disposed, will be `true`. Otherwise, `false`.
- */
- isDisposed() {
- return Boolean(this.isDisposed_);
- }
-
- /**
- * Return the {@link Player} that the `Component` has attached to.
- *
- * @return { import('./player').default }
- * The player that this `Component` has attached to.
- */
- player() {
- return this.player_;
- }
-
- /**
- * Deep merge of options objects with new options.
- * > Note: When both `obj` and `options` contain properties whose values are objects.
- * The two properties get merged using {@link module:obj.merge}
- *
- * @param {Object} obj
- * The object that contains new options.
- *
- * @return {Object}
- * A new object of `this.options_` and `obj` merged together.
- */
- options(obj) {
- if (!obj) {
- return this.options_;
- }
- this.options_ = merge(this.options_, obj);
- return this.options_;
- }
-
- /**
- * Get the `Component`s DOM element
- *
- * @return {Element}
- * The DOM element for this `Component`.
- */
- el() {
- return this.el_;
- }
-
- /**
- * Create the `Component`s DOM element.
- *
- * @param {string} [tagName]
- * Element's DOM node type. e.g. 'div'
- *
- * @param {Object} [properties]
- * An object of properties that should be set.
- *
- * @param {Object} [attributes]
- * An object of attributes that should be set.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tagName, properties, attributes) {
- return createEl(tagName, properties, attributes);
- }
-
- /**
- * Localize a string given the string in english.
- *
- * If tokens are provided, it'll try and run a simple token replacement on the provided string.
- * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
- *
- * If a `defaultValue` is provided, it'll use that over `string`,
- * if a value isn't found in provided language files.
- * This is useful if you want to have a descriptive key for token replacement
- * but have a succinct localized string and not require `en.json` to be included.
- *
- * Currently, it is used for the progress bar timing.
- * ```js
- * {
- * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
- * }
- * ```
- * It is then used like so:
- * ```js
- * this.localize('progress bar timing: currentTime={1} duration{2}',
- * [this.player_.currentTime(), this.player_.duration()],
- * '{1} of {2}');
- * ```
- *
- * Which outputs something like: `01:23 of 24:56`.
- *
- *
- * @param {string} string
- * The string to localize and the key to lookup in the language files.
- * @param {string[]} [tokens]
- * If the current item has token replacements, provide the tokens here.
- * @param {string} [defaultValue]
- * Defaults to `string`. Can be a default value to use for token replacement
- * if the lookup key is needed to be separate.
- *
- * @return {string}
- * The localized string or if no localization exists the english string.
- */
- localize(string, tokens, defaultValue = string) {
- const code = this.player_.language && this.player_.language();
- const languages = this.player_.languages && this.player_.languages();
- const language = languages && languages[code];
- const primaryCode = code && code.split('-')[0];
- const primaryLang = languages && languages[primaryCode];
- let localizedString = defaultValue;
- if (language && language[string]) {
- localizedString = language[string];
- } else if (primaryLang && primaryLang[string]) {
- localizedString = primaryLang[string];
- }
- if (tokens) {
- localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
- const value = tokens[index - 1];
- let ret = value;
- if (typeof value === 'undefined') {
- ret = match;
- }
- return ret;
- });
- }
- return localizedString;
- }
-
- /**
- * Handles language change for the player in components. Should be overridden by sub-components.
- *
- * @abstract
- */
- handleLanguagechange() {}
-
- /**
- * Return the `Component`s DOM element. This is where children get inserted.
- * This will usually be the the same as the element returned in {@link Component#el}.
- *
- * @return {Element}
- * The content element for this `Component`.
- */
- contentEl() {
- return this.contentEl_ || this.el_;
- }
-
- /**
- * Get this `Component`s ID
- *
- * @return {string}
- * The id of this `Component`
- */
- id() {
- return this.id_;
- }
-
- /**
- * Get the `Component`s name. The name gets used to reference the `Component`
- * and is set during registration.
- *
- * @return {string}
- * The name of this `Component`.
- */
- name() {
- return this.name_;
- }
-
- /**
- * Get an array of all child components
- *
- * @return {Array}
- * The children
- */
- children() {
- return this.children_;
- }
-
- /**
- * Returns the child `Component` with the given `id`.
- *
- * @param {string} id
- * The id of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The child `Component` with the given `id` or undefined.
- */
- getChildById(id) {
- return this.childIndex_[id];
- }
-
- /**
- * Returns the child `Component` with the given `name`.
- *
- * @param {string} name
- * The name of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The child `Component` with the given `name` or undefined.
- */
- getChild(name) {
- if (!name) {
- return;
- }
- return this.childNameIndex_[name];
- }
-
- /**
- * Returns the descendant `Component` following the givent
- * descendant `names`. For instance ['foo', 'bar', 'baz'] would
- * try to get 'foo' on the current component, 'bar' on the 'foo'
- * component and 'baz' on the 'bar' component and return undefined
- * if any of those don't exist.
- *
- * @param {...string[]|...string} names
- * The name of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The descendant `Component` following the given descendant
- * `names` or undefined.
- */
- getDescendant(...names) {
- // flatten array argument into the main array
- names = names.reduce((acc, n) => acc.concat(n), []);
- let currentChild = this;
- for (let i = 0; i < names.length; i++) {
- currentChild = currentChild.getChild(names[i]);
- if (!currentChild || !currentChild.getChild) {
- return;
- }
- }
- return currentChild;
- }
-
- /**
- * Adds an SVG icon element to another element or component.
- *
- * @param {string} iconName
- * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
- *
- * @param {Element} [el=this.el()]
- * Element to set the title on. Defaults to the current Component's element.
- *
- * @return {Element}
- * The newly created icon element.
- */
- setIcon(iconName, el = this.el()) {
- // TODO: In v9 of video.js, we will want to remove font icons entirely.
- // This means this check, as well as the others throughout the code, and
- // the unecessary CSS for font icons, will need to be removed.
- // See https://github.com/videojs/video.js/pull/8260 as to which components
- // need updating.
- if (!this.player_.options_.experimentalSvgIcons) {
- return;
- }
- const xmlnsURL = 'http://www.w3.org/2000/svg';
-
- // The below creates an element in the format of:
- // ....
- const iconContainer = createEl('span', {
- className: 'vjs-icon-placeholder vjs-svg-icon'
- }, {
- 'aria-hidden': 'true'
- });
- const svgEl = document.createElementNS(xmlnsURL, 'svg');
- svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
- const useEl = document.createElementNS(xmlnsURL, 'use');
- svgEl.appendChild(useEl);
- useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
- iconContainer.appendChild(svgEl);
-
- // Replace a pre-existing icon if one exists.
- if (this.iconIsSet_) {
- el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
- } else {
- el.appendChild(iconContainer);
- }
- this.iconIsSet_ = true;
- return iconContainer;
- }
-
- /**
- * Add a child `Component` inside the current `Component`.
- *
- * @param {string|Component} child
- * The name or instance of a child to add.
- *
- * @param {Object} [options={}]
- * The key/value store of options that will get passed to children of
- * the child.
- *
- * @param {number} [index=this.children_.length]
- * The index to attempt to add a child into.
- *
- *
- * @return {Component}
- * The `Component` that gets added as a child. When using a string the
- * `Component` will get created by this process.
- */
- addChild(child, options = {}, index = this.children_.length) {
- let component;
- let componentName;
-
- // If child is a string, create component with options
- if (typeof child === 'string') {
- componentName = toTitleCase(child);
- const componentClassName = options.componentClass || componentName;
-
- // Set name through options
- options.name = componentName;
-
- // Create a new object & element for this controls set
- // If there's no .player_, this is a player
- const ComponentClass = Component.getComponent(componentClassName);
- if (!ComponentClass) {
- throw new Error(`Component ${componentClassName} does not exist`);
- }
-
- // data stored directly on the videojs object may be
- // misidentified as a component to retain
- // backwards-compatibility with 4.x. check to make sure the
- // component class can be instantiated.
- if (typeof ComponentClass !== 'function') {
- return null;
- }
- component = new ComponentClass(this.player_ || this, options);
-
- // child is a component instance
- } else {
- component = child;
- }
- if (component.parentComponent_) {
- component.parentComponent_.removeChild(component);
- }
- this.children_.splice(index, 0, component);
- component.parentComponent_ = this;
- if (typeof component.id === 'function') {
- this.childIndex_[component.id()] = component;
- }
-
- // If a name wasn't used to create the component, check if we can use the
- // name function of the component
- componentName = componentName || component.name && toTitleCase(component.name());
- if (componentName) {
- this.childNameIndex_[componentName] = component;
- this.childNameIndex_[toLowerCase(componentName)] = component;
- }
-
- // Add the UI object's element to the container div (box)
- // Having an element is not required
- if (typeof component.el === 'function' && component.el()) {
- // If inserting before a component, insert before that component's element
- let refNode = null;
- if (this.children_[index + 1]) {
- // Most children are components, but the video tech is an HTML element
- if (this.children_[index + 1].el_) {
- refNode = this.children_[index + 1].el_;
- } else if (isEl(this.children_[index + 1])) {
- refNode = this.children_[index + 1];
- }
- }
- this.contentEl().insertBefore(component.el(), refNode);
- }
-
- // Return so it can stored on parent object if desired.
- return component;
- }
-
- /**
- * Remove a child `Component` from this `Component`s list of children. Also removes
- * the child `Component`s element from this `Component`s element.
- *
- * @param {Component} component
- * The child `Component` to remove.
- */
- removeChild(component) {
- if (typeof component === 'string') {
- component = this.getChild(component);
- }
- if (!component || !this.children_) {
- return;
- }
- let childFound = false;
- for (let i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i] === component) {
- childFound = true;
- this.children_.splice(i, 1);
- break;
- }
- }
- if (!childFound) {
- return;
- }
- component.parentComponent_ = null;
- this.childIndex_[component.id()] = null;
- this.childNameIndex_[toTitleCase(component.name())] = null;
- this.childNameIndex_[toLowerCase(component.name())] = null;
- const compEl = component.el();
- if (compEl && compEl.parentNode === this.contentEl()) {
- this.contentEl().removeChild(component.el());
- }
- }
-
- /**
- * Add and initialize default child `Component`s based upon options.
- */
- initChildren() {
- const children = this.options_.children;
- if (children) {
- // `this` is `parent`
- const parentOptions = this.options_;
- const handleAdd = child => {
- const name = child.name;
- let opts = child.opts;
-
- // Allow options for children to be set at the parent options
- // e.g. videojs(id, { controlBar: false });
- // instead of videojs(id, { children: { controlBar: false });
- if (parentOptions[name] !== undefined) {
- opts = parentOptions[name];
- }
-
- // Allow for disabling default components
- // e.g. options['children']['posterImage'] = false
- if (opts === false) {
- return;
- }
-
- // Allow options to be passed as a simple boolean if no configuration
- // is necessary.
- if (opts === true) {
- opts = {};
- }
-
- // We also want to pass the original player options
- // to each component as well so they don't need to
- // reach back into the player for options later.
- opts.playerOptions = this.options_.playerOptions;
-
- // Create and add the child component.
- // Add a direct reference to the child by name on the parent instance.
- // If two of the same component are used, different names should be supplied
- // for each
- const newChild = this.addChild(name, opts);
- if (newChild) {
- this[name] = newChild;
- }
- };
-
- // Allow for an array of children details to passed in the options
- let workingChildren;
- const Tech = Component.getComponent('Tech');
- if (Array.isArray(children)) {
- workingChildren = children;
- } else {
- workingChildren = Object.keys(children);
- }
- workingChildren
- // children that are in this.options_ but also in workingChildren would
- // give us extra children we do not want. So, we want to filter them out.
- .concat(Object.keys(this.options_).filter(function (child) {
- return !workingChildren.some(function (wchild) {
- if (typeof wchild === 'string') {
- return child === wchild;
- }
- return child === wchild.name;
- });
- })).map(child => {
- let name;
- let opts;
- if (typeof child === 'string') {
- name = child;
- opts = children[name] || this.options_[name] || {};
- } else {
- name = child.name;
- opts = child;
- }
- return {
- name,
- opts
- };
- }).filter(child => {
- // we have to make sure that child.name isn't in the techOrder since
- // techs are registered as Components but can't aren't compatible
- // See https://github.com/videojs/video.js/issues/2772
- const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name));
- return c && !Tech.isTech(c);
- }).forEach(handleAdd);
- }
- }
-
- /**
- * Builds the default DOM class name. Should be overridden by sub-components.
- *
- * @return {string}
- * The DOM class name for this object.
- *
- * @abstract
- */
- buildCSSClass() {
- // Child classes can include a function that does:
- // return 'CLASS NAME' + this._super();
- return '';
- }
-
- /**
- * Bind a listener to the component's ready state.
- * Different from event listeners in that if the ready event has already happened
- * it will trigger the function immediately.
- *
- * @param {ReadyCallback} fn
- * Function that gets called when the `Component` is ready.
- *
- * @return {Component}
- * Returns itself; method can be chained.
- */
- ready(fn, sync = false) {
- if (!fn) {
- return;
- }
- if (!this.isReady_) {
- this.readyQueue_ = this.readyQueue_ || [];
- this.readyQueue_.push(fn);
- return;
- }
- if (sync) {
- fn.call(this);
- } else {
- // Call the function asynchronously by default for consistency
- this.setTimeout(fn, 1);
- }
- }
-
- /**
- * Trigger all the ready listeners for this `Component`.
- *
- * @fires Component#ready
- */
- triggerReady() {
- this.isReady_ = true;
-
- // Ensure ready is triggered asynchronously
- this.setTimeout(function () {
- const readyQueue = this.readyQueue_;
-
- // Reset Ready Queue
- this.readyQueue_ = [];
- if (readyQueue && readyQueue.length > 0) {
- readyQueue.forEach(function (fn) {
- fn.call(this);
- }, this);
- }
-
- // Allow for using event listeners also
- /**
- * Triggered when a `Component` is ready.
- *
- * @event Component#ready
- * @type {Event}
- */
- this.trigger('ready');
- }, 1);
- }
-
- /**
- * Find a single DOM element matching a `selector`. This can be within the `Component`s
- * `contentEl()` or another custom context.
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelector`.
- *
- * @param {Element|string} [context=this.contentEl()]
- * A DOM element within which to query. Can also be a selector string in
- * which case the first matching element will get used as context. If
- * missing `this.contentEl()` gets used. If `this.contentEl()` returns
- * nothing it falls back to `document`.
- *
- * @return {Element|null}
- * the dom element that was found, or null
- *
- * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
- */
- $(selector, context) {
- return $(selector, context || this.contentEl());
- }
-
- /**
- * Finds all DOM element matching a `selector`. This can be within the `Component`s
- * `contentEl()` or another custom context.
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelectorAll`.
- *
- * @param {Element|string} [context=this.contentEl()]
- * A DOM element within which to query. Can also be a selector string in
- * which case the first matching element will get used as context. If
- * missing `this.contentEl()` gets used. If `this.contentEl()` returns
- * nothing it falls back to `document`.
- *
- * @return {NodeList}
- * a list of dom elements that were found
- *
- * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
- */
- $$(selector, context) {
- return $$(selector, context || this.contentEl());
- }
-
- /**
- * Check if a component's element has a CSS class name.
- *
- * @param {string} classToCheck
- * CSS class name to check.
- *
- * @return {boolean}
- * - True if the `Component` has the class.
- * - False if the `Component` does not have the class`
- */
- hasClass(classToCheck) {
- return hasClass(this.el_, classToCheck);
- }
-
- /**
- * Add a CSS class name to the `Component`s element.
- *
- * @param {...string} classesToAdd
- * One or more CSS class name to add.
- */
- addClass(...classesToAdd) {
- addClass(this.el_, ...classesToAdd);
- }
-
- /**
- * Remove a CSS class name from the `Component`s element.
- *
- * @param {...string} classesToRemove
- * One or more CSS class name to remove.
- */
- removeClass(...classesToRemove) {
- removeClass(this.el_, ...classesToRemove);
- }
-
- /**
- * Add or remove a CSS class name from the component's element.
- * - `classToToggle` gets added when {@link Component#hasClass} would return false.
- * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
- *
- * @param {string} classToToggle
- * The class to add or remove based on (@link Component#hasClass}
- *
- * @param {boolean|Dom~predicate} [predicate]
- * An {@link Dom~predicate} function or a boolean
- */
- toggleClass(classToToggle, predicate) {
- toggleClass(this.el_, classToToggle, predicate);
- }
-
- /**
- * Show the `Component`s element if it is hidden by removing the
- * 'vjs-hidden' class name from it.
- */
- show() {
- this.removeClass('vjs-hidden');
- }
-
- /**
- * Hide the `Component`s element if it is currently showing by adding the
- * 'vjs-hidden` class name to it.
- */
- hide() {
- this.addClass('vjs-hidden');
- }
-
- /**
- * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
- * class name to it. Used during fadeIn/fadeOut.
- *
- * @private
- */
- lockShowing() {
- this.addClass('vjs-lock-showing');
- }
-
- /**
- * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
- * class name from it. Used during fadeIn/fadeOut.
- *
- * @private
- */
- unlockShowing() {
- this.removeClass('vjs-lock-showing');
- }
-
- /**
- * Get the value of an attribute on the `Component`s element.
- *
- * @param {string} attribute
- * Name of the attribute to get the value from.
- *
- * @return {string|null}
- * - The value of the attribute that was asked for.
- * - Can be an empty string on some browsers if the attribute does not exist
- * or has no value
- * - Most browsers will return null if the attribute does not exist or has
- * no value.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
- */
- getAttribute(attribute) {
- return getAttribute(this.el_, attribute);
- }
-
- /**
- * Set the value of an attribute on the `Component`'s element
- *
- * @param {string} attribute
- * Name of the attribute to set.
- *
- * @param {string} value
- * Value to set the attribute to.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
- */
- setAttribute(attribute, value) {
- setAttribute(this.el_, attribute, value);
- }
-
- /**
- * Remove an attribute from the `Component`s element.
- *
- * @param {string} attribute
- * Name of the attribute to remove.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
- */
- removeAttribute(attribute) {
- removeAttribute(this.el_, attribute);
- }
-
- /**
- * Get or set the width of the component based upon the CSS styles.
- * See {@link Component#dimension} for more detailed information.
- *
- * @param {number|string} [num]
- * The width that you want to set postfixed with '%', 'px' or nothing.
- *
- * @param {boolean} [skipListeners]
- * Skip the componentresize event trigger
- *
- * @return {number|undefined}
- * The width when getting, zero if there is no width
- */
- width(num, skipListeners) {
- return this.dimension('width', num, skipListeners);
- }
-
- /**
- * Get or set the height of the component based upon the CSS styles.
- * See {@link Component#dimension} for more detailed information.
- *
- * @param {number|string} [num]
- * The height that you want to set postfixed with '%', 'px' or nothing.
- *
- * @param {boolean} [skipListeners]
- * Skip the componentresize event trigger
- *
- * @return {number|undefined}
- * The height when getting, zero if there is no height
- */
- height(num, skipListeners) {
- return this.dimension('height', num, skipListeners);
- }
-
- /**
- * Set both the width and height of the `Component` element at the same time.
- *
- * @param {number|string} width
- * Width to set the `Component`s element to.
- *
- * @param {number|string} height
- * Height to set the `Component`s element to.
- */
- dimensions(width, height) {
- // Skip componentresize listeners on width for optimization
- this.width(width, true);
- this.height(height);
- }
-
- /**
- * Get or set width or height of the `Component` element. This is the shared code
- * for the {@link Component#width} and {@link Component#height}.
- *
- * Things to know:
- * - If the width or height in an number this will return the number postfixed with 'px'.
- * - If the width/height is a percent this will return the percent postfixed with '%'
- * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
- * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
- * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
- * for more information
- * - If you want the computed style of the component, use {@link Component#currentWidth}
- * and {@link {Component#currentHeight}
- *
- * @fires Component#componentresize
- *
- * @param {string} widthOrHeight
- 8 'width' or 'height'
- *
- * @param {number|string} [num]
- 8 New dimension
- *
- * @param {boolean} [skipListeners]
- * Skip componentresize event trigger
- *
- * @return {number|undefined}
- * The dimension when getting or 0 if unset
- */
- dimension(widthOrHeight, num, skipListeners) {
- if (num !== undefined) {
- // Set to zero if null or literally NaN (NaN !== NaN)
- if (num === null || num !== num) {
- num = 0;
- }
-
- // Check if using css width/height (% or px) and adjust
- if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
- this.el_.style[widthOrHeight] = num;
- } else if (num === 'auto') {
- this.el_.style[widthOrHeight] = '';
- } else {
- this.el_.style[widthOrHeight] = num + 'px';
- }
-
- // skipListeners allows us to avoid triggering the resize event when setting both width and height
- if (!skipListeners) {
- /**
- * Triggered when a component is resized.
- *
- * @event Component#componentresize
- * @type {Event}
- */
- this.trigger('componentresize');
- }
- return;
- }
-
- // Not setting a value, so getting it
- // Make sure element exists
- if (!this.el_) {
- return 0;
- }
-
- // Get dimension value from style
- const val = this.el_.style[widthOrHeight];
- const pxIndex = val.indexOf('px');
- if (pxIndex !== -1) {
- // Return the pixel value with no 'px'
- return parseInt(val.slice(0, pxIndex), 10);
- }
-
- // No px so using % or no style was set, so falling back to offsetWidth/height
- // If component has display:none, offset will return 0
- // TODO: handle display:none and no dimension style using px
- return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
- }
-
- /**
- * Get the computed width or the height of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @param {string} widthOrHeight
- * A string containing 'width' or 'height'. Whichever one you want to get.
- *
- * @return {number}
- * The dimension that gets asked for or 0 if nothing was set
- * for that dimension.
- */
- currentDimension(widthOrHeight) {
- let computedWidthOrHeight = 0;
- if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
- throw new Error('currentDimension only accepts width or height value');
- }
- computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
-
- // remove 'px' from variable and parse as integer
- computedWidthOrHeight = parseFloat(computedWidthOrHeight);
-
- // if the computed value is still 0, it's possible that the browser is lying
- // and we want to check the offset values.
- // This code also runs wherever getComputedStyle doesn't exist.
- if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
- const rule = `offset${toTitleCase(widthOrHeight)}`;
- computedWidthOrHeight = this.el_[rule];
- }
- return computedWidthOrHeight;
- }
-
- /**
- * An object that contains width and height values of the `Component`s
- * computed style. Uses `window.getComputedStyle`.
- *
- * @typedef {Object} Component~DimensionObject
- *
- * @property {number} width
- * The width of the `Component`s computed style.
- *
- * @property {number} height
- * The height of the `Component`s computed style.
- */
-
- /**
- * Get an object that contains computed width and height values of the
- * component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {Component~DimensionObject}
- * The computed dimensions of the component's element.
- */
- currentDimensions() {
- return {
- width: this.currentDimension('width'),
- height: this.currentDimension('height')
- };
- }
-
- /**
- * Get the computed width of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {number}
- * The computed width of the component's element.
- */
- currentWidth() {
- return this.currentDimension('width');
- }
-
- /**
- * Get the computed height of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {number}
- * The computed height of the component's element.
- */
- currentHeight() {
- return this.currentDimension('height');
- }
-
- /**
- * Set the focus to this component
- */
- focus() {
- this.el_.focus();
- }
-
- /**
- * Remove the focus from this component
- */
- blur() {
- this.el_.blur();
- }
-
- /**
- * When this Component receives a `keydown` event which it does not process,
- * it passes the event to the Player for handling.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- */
- handleKeyDown(event) {
- if (this.player_) {
- // We only stop propagation here because we want unhandled events to fall
- // back to the browser. Exclude Tab for focus trapping.
- if (!keycode.isEventKey(event, 'Tab')) {
- event.stopPropagation();
- }
- this.player_.handleKeyDown(event);
- }
- }
-
- /**
- * Many components used to have a `handleKeyPress` method, which was poorly
- * named because it listened to a `keydown` event. This method name now
- * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
- * will not see their method calls stop working.
- *
- * @param {KeyboardEvent} event
- * The event that caused this function to be called.
- */
- handleKeyPress(event) {
- this.handleKeyDown(event);
- }
-
- /**
- * Emit a 'tap' events when touch event support gets detected. This gets used to
- * support toggling the controls through a tap on the video. They get enabled
- * because every sub-component would have extra overhead otherwise.
- *
- * @protected
- * @fires Component#tap
- * @listens Component#touchstart
- * @listens Component#touchmove
- * @listens Component#touchleave
- * @listens Component#touchcancel
- * @listens Component#touchend
- */
- emitTapEvents() {
- // Track the start time so we can determine how long the touch lasted
- let touchStart = 0;
- let firstTouch = null;
-
- // Maximum movement allowed during a touch event to still be considered a tap
- // Other popular libs use anywhere from 2 (hammer.js) to 15,
- // so 10 seems like a nice, round number.
- const tapMovementThreshold = 10;
-
- // The maximum length a touch can be while still being considered a tap
- const touchTimeThreshold = 200;
- let couldBeTap;
- this.on('touchstart', function (event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length === 1) {
- // Copy pageX/pageY from the object
- firstTouch = {
- pageX: event.touches[0].pageX,
- pageY: event.touches[0].pageY
- };
- // Record start time so we can detect a tap vs. "touch and hold"
- touchStart = window.performance.now();
- // Reset couldBeTap tracking
- couldBeTap = true;
- }
- });
- this.on('touchmove', function (event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length > 1) {
- couldBeTap = false;
- } else if (firstTouch) {
- // Some devices will throw touchmoves for all but the slightest of taps.
- // So, if we moved only a small distance, this could still be a tap
- const xdiff = event.touches[0].pageX - firstTouch.pageX;
- const ydiff = event.touches[0].pageY - firstTouch.pageY;
- const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
- if (touchDistance > tapMovementThreshold) {
- couldBeTap = false;
- }
- }
- });
- const noTap = function () {
- couldBeTap = false;
- };
-
- // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
- this.on('touchleave', noTap);
- this.on('touchcancel', noTap);
-
- // When the touch ends, measure how long it took and trigger the appropriate
- // event
- this.on('touchend', function (event) {
- firstTouch = null;
- // Proceed only if the touchmove/leave/cancel event didn't happen
- if (couldBeTap === true) {
- // Measure how long the touch lasted
- const touchTime = window.performance.now() - touchStart;
-
- // Make sure the touch was less than the threshold to be considered a tap
- if (touchTime < touchTimeThreshold) {
- // Don't let browser turn this into a click
- event.preventDefault();
- /**
- * Triggered when a `Component` is tapped.
- *
- * @event Component#tap
- * @type {MouseEvent}
- */
- this.trigger('tap');
- // It may be good to copy the touchend event object and change the
- // type to tap, if the other event properties aren't exact after
- // Events.fixEvent runs (e.g. event.target)
- }
- }
- });
- }
-
- /**
- * This function reports user activity whenever touch events happen. This can get
- * turned off by any sub-components that wants touch events to act another way.
- *
- * Report user touch activity when touch events occur. User activity gets used to
- * determine when controls should show/hide. It is simple when it comes to mouse
- * events, because any mouse event should show the controls. So we capture mouse
- * events that bubble up to the player and report activity when that happens.
- * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
- * controls. So touch events can't help us at the player level either.
- *
- * User activity gets checked asynchronously. So what could happen is a tap event
- * on the video turns the controls off. Then the `touchend` event bubbles up to
- * the player. Which, if it reported user activity, would turn the controls right
- * back on. We also don't want to completely block touch events from bubbling up.
- * Furthermore a `touchmove` event and anything other than a tap, should not turn
- * controls back on.
- *
- * @listens Component#touchstart
- * @listens Component#touchmove
- * @listens Component#touchend
- * @listens Component#touchcancel
- */
- enableTouchActivity() {
- // Don't continue if the root player doesn't support reporting user activity
- if (!this.player() || !this.player().reportUserActivity) {
- return;
- }
-
- // listener for reporting that the user is active
- const report = bind_(this.player(), this.player().reportUserActivity);
- let touchHolding;
- this.on('touchstart', function () {
- report();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(touchHolding);
- // report at the same interval as activityCheck
- touchHolding = this.setInterval(report, 250);
- });
- const touchEnd = function (event) {
- report();
- // stop the interval that maintains activity if the touch is holding
- this.clearInterval(touchHolding);
- };
- this.on('touchmove', report);
- this.on('touchend', touchEnd);
- this.on('touchcancel', touchEnd);
- }
-
- /**
- * A callback that has no parameters and is bound into `Component`s context.
- *
- * @callback Component~GenericCallback
- * @this Component
- */
-
- /**
- * Creates a function that runs after an `x` millisecond timeout. This function is a
- * wrapper around `window.setTimeout`. There are a few reasons to use this one
- * instead though:
- * 1. It gets cleared via {@link Component#clearTimeout} when
- * {@link Component#dispose} gets called.
- * 2. The function callback will gets turned into a {@link Component~GenericCallback}
- *
- * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
- * will cause its dispose listener not to get cleaned up! Please use
- * {@link Component#clearTimeout} or {@link Component#dispose} instead.
- *
- * @param {Component~GenericCallback} fn
- * The function that will be run after `timeout`.
- *
- * @param {number} timeout
- * Timeout in milliseconds to delay before executing the specified function.
- *
- * @return {number}
- * Returns a timeout ID that gets used to identify the timeout. It can also
- * get used in {@link Component#clearTimeout} to clear the timeout that
- * was set.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
- */
- setTimeout(fn, timeout) {
- // declare as variables so they are properly available in timeout function
- // eslint-disable-next-line
- var timeoutId;
- fn = bind_(this, fn);
- this.clearTimersOnDispose_();
- timeoutId = window.setTimeout(() => {
- if (this.setTimeoutIds_.has(timeoutId)) {
- this.setTimeoutIds_.delete(timeoutId);
- }
- fn();
- }, timeout);
- this.setTimeoutIds_.add(timeoutId);
- return timeoutId;
- }
-
- /**
- * Clears a timeout that gets created via `window.setTimeout` or
- * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
- * use this function instead of `window.clearTimout`. If you don't your dispose
- * listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} timeoutId
- * The id of the timeout to clear. The return value of
- * {@link Component#setTimeout} or `window.setTimeout`.
- *
- * @return {number}
- * Returns the timeout id that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
- */
- clearTimeout(timeoutId) {
- if (this.setTimeoutIds_.has(timeoutId)) {
- this.setTimeoutIds_.delete(timeoutId);
- window.clearTimeout(timeoutId);
- }
- return timeoutId;
- }
-
- /**
- * Creates a function that gets run every `x` milliseconds. This function is a wrapper
- * around `window.setInterval`. There are a few reasons to use this one instead though.
- * 1. It gets cleared via {@link Component#clearInterval} when
- * {@link Component#dispose} gets called.
- * 2. The function callback will be a {@link Component~GenericCallback}
- *
- * @param {Component~GenericCallback} fn
- * The function to run every `x` seconds.
- *
- * @param {number} interval
- * Execute the specified function every `x` milliseconds.
- *
- * @return {number}
- * Returns an id that can be used to identify the interval. It can also be be used in
- * {@link Component#clearInterval} to clear the interval.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
- */
- setInterval(fn, interval) {
- fn = bind_(this, fn);
- this.clearTimersOnDispose_();
- const intervalId = window.setInterval(fn, interval);
- this.setIntervalIds_.add(intervalId);
- return intervalId;
- }
-
- /**
- * Clears an interval that gets created via `window.setInterval` or
- * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
- * use this function instead of `window.clearInterval`. If you don't your dispose
- * listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} intervalId
- * The id of the interval to clear. The return value of
- * {@link Component#setInterval} or `window.setInterval`.
- *
- * @return {number}
- * Returns the interval id that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
- */
- clearInterval(intervalId) {
- if (this.setIntervalIds_.has(intervalId)) {
- this.setIntervalIds_.delete(intervalId);
- window.clearInterval(intervalId);
- }
- return intervalId;
- }
-
- /**
- * Queues up a callback to be passed to requestAnimationFrame (rAF), but
- * with a few extra bonuses:
- *
- * - Supports browsers that do not support rAF by falling back to
- * {@link Component#setTimeout}.
- *
- * - The callback is turned into a {@link Component~GenericCallback} (i.e.
- * bound to the component).
- *
- * - Automatic cancellation of the rAF callback is handled if the component
- * is disposed before it is called.
- *
- * @param {Component~GenericCallback} fn
- * A function that will be bound to this component and executed just
- * before the browser's next repaint.
- *
- * @return {number}
- * Returns an rAF ID that gets used to identify the timeout. It can
- * also be used in {@link Component#cancelAnimationFrame} to cancel
- * the animation frame callback.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
- */
- requestAnimationFrame(fn) {
- this.clearTimersOnDispose_();
-
- // declare as variables so they are properly available in rAF function
- // eslint-disable-next-line
- var id;
- fn = bind_(this, fn);
- id = window.requestAnimationFrame(() => {
- if (this.rafIds_.has(id)) {
- this.rafIds_.delete(id);
- }
- fn();
- });
- this.rafIds_.add(id);
- return id;
- }
-
- /**
- * Request an animation frame, but only one named animation
- * frame will be queued. Another will never be added until
- * the previous one finishes.
- *
- * @param {string} name
- * The name to give this requestAnimationFrame
- *
- * @param {Component~GenericCallback} fn
- * A function that will be bound to this component and executed just
- * before the browser's next repaint.
- */
- requestNamedAnimationFrame(name, fn) {
- if (this.namedRafs_.has(name)) {
- return;
- }
- this.clearTimersOnDispose_();
- fn = bind_(this, fn);
- const id = this.requestAnimationFrame(() => {
- fn();
- if (this.namedRafs_.has(name)) {
- this.namedRafs_.delete(name);
- }
- });
- this.namedRafs_.set(name, id);
- return name;
- }
-
- /**
- * Cancels a current named animation frame if it exists.
- *
- * @param {string} name
- * The name of the requestAnimationFrame to cancel.
- */
- cancelNamedAnimationFrame(name) {
- if (!this.namedRafs_.has(name)) {
- return;
- }
- this.cancelAnimationFrame(this.namedRafs_.get(name));
- this.namedRafs_.delete(name);
- }
-
- /**
- * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
- * (rAF).
- *
- * If you queue an rAF callback via {@link Component#requestAnimationFrame},
- * use this function instead of `window.cancelAnimationFrame`. If you don't,
- * your dispose listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} id
- * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
- *
- * @return {number}
- * Returns the rAF ID that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
- */
- cancelAnimationFrame(id) {
- if (this.rafIds_.has(id)) {
- this.rafIds_.delete(id);
- window.cancelAnimationFrame(id);
- }
- return id;
- }
-
- /**
- * A function to setup `requestAnimationFrame`, `setTimeout`,
- * and `setInterval`, clearing on dispose.
- *
- * > Previously each timer added and removed dispose listeners on it's own.
- * For better performance it was decided to batch them all, and use `Set`s
- * to track outstanding timer ids.
- *
- * @private
- */
- clearTimersOnDispose_() {
- if (this.clearingTimersOnDispose_) {
- return;
- }
- this.clearingTimersOnDispose_ = true;
- this.one('dispose', () => {
- [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
- // for a `Set` key will actually be the value again
- // so forEach((val, val) =>` but for maps we want to use
- // the key.
- this[idName].forEach((val, key) => this[cancelName](key));
- });
- this.clearingTimersOnDispose_ = false;
- });
- }
-
- /**
- * Register a `Component` with `videojs` given the name and the component.
- *
- * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
- * should be registered using {@link Tech.registerTech} or
- * {@link videojs:videojs.registerTech}.
- *
- * > NOTE: This function can also be seen on videojs as
- * {@link videojs:videojs.registerComponent}.
- *
- * @param {string} name
- * The name of the `Component` to register.
- *
- * @param {Component} ComponentToRegister
- * The `Component` class to register.
- *
- * @return {Component}
- * The `Component` that was registered.
- */
- static registerComponent(name, ComponentToRegister) {
- if (typeof name !== 'string' || !name) {
- throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
- }
- const Tech = Component.getComponent('Tech');
-
- // We need to make sure this check is only done if Tech has been registered.
- const isTech = Tech && Tech.isTech(ComponentToRegister);
- const isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
- if (isTech || !isComp) {
- let reason;
- if (isTech) {
- reason = 'techs must be registered using Tech.registerTech()';
- } else {
- reason = 'must be a Component subclass';
- }
- throw new Error(`Illegal component, "${name}"; ${reason}.`);
- }
- name = toTitleCase(name);
- if (!Component.components_) {
- Component.components_ = {};
- }
- const Player = Component.getComponent('Player');
- if (name === 'Player' && Player && Player.players) {
- const players = Player.players;
- const playerNames = Object.keys(players);
-
- // If we have players that were disposed, then their name will still be
- // in Players.players. So, we must loop through and verify that the value
- // for each item is not null. This allows registration of the Player component
- // after all players have been disposed or before any were created.
- if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
- throw new Error('Can not register Player component after player has been created.');
- }
- }
- Component.components_[name] = ComponentToRegister;
- Component.components_[toLowerCase(name)] = ComponentToRegister;
- return ComponentToRegister;
- }
-
- /**
- * Get a `Component` based on the name it was registered with.
- *
- * @param {string} name
- * The Name of the component to get.
- *
- * @return {typeof Component}
- * The `Component` that got registered under the given name.
- */
- static getComponent(name) {
- if (!name || !Component.components_) {
- return;
- }
- return Component.components_[name];
- }
- }
- Component.registerComponent('Component', Component);
-
- /**
- * @file time.js
- * @module time
- */
-
- /**
- * Returns the time for the specified index at the start or end
- * of a TimeRange object.
- *
- * @typedef {Function} TimeRangeIndex
- *
- * @param {number} [index=0]
- * The range number to return the time for.
- *
- * @return {number}
- * The time offset at the specified index.
- *
- * @deprecated The index argument must be provided.
- * In the future, leaving it out will throw an error.
- */
-
- /**
- * An object that contains ranges of time, which mimics {@link TimeRanges}.
- *
- * @typedef {Object} TimeRange
- *
- * @property {number} length
- * The number of time ranges represented by this object.
- *
- * @property {module:time~TimeRangeIndex} start
- * Returns the time offset at which a specified time range begins.
- *
- * @property {module:time~TimeRangeIndex} end
- * Returns the time offset at which a specified time range ends.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
- */
-
- /**
- * Check if any of the time ranges are over the maximum index.
- *
- * @private
- * @param {string} fnName
- * The function name to use for logging
- *
- * @param {number} index
- * The index to check
- *
- * @param {number} maxIndex
- * The maximum possible index
- *
- * @throws {Error} if the timeRanges provided are over the maxIndex
- */
- function rangeCheck(fnName, index, maxIndex) {
- if (typeof index !== 'number' || index < 0 || index > maxIndex) {
- throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
- }
- }
-
- /**
- * Get the time for the specified index at the start or end
- * of a TimeRange object.
- *
- * @private
- * @param {string} fnName
- * The function name to use for logging
- *
- * @param {string} valueIndex
- * The property that should be used to get the time. should be
- * 'start' or 'end'
- *
- * @param {Array} ranges
- * An array of time ranges
- *
- * @param {Array} [rangeIndex=0]
- * The index to start the search at
- *
- * @return {number}
- * The time that offset at the specified index.
- *
- * @deprecated rangeIndex must be set to a value, in the future this will throw an error.
- * @throws {Error} if rangeIndex is more than the length of ranges
- */
- function getRange(fnName, valueIndex, ranges, rangeIndex) {
- rangeCheck(fnName, rangeIndex, ranges.length - 1);
- return ranges[rangeIndex][valueIndex];
- }
-
- /**
- * Create a time range object given ranges of time.
- *
- * @private
- * @param {Array} [ranges]
- * An array of time ranges.
- *
- * @return {TimeRange}
- */
- function createTimeRangesObj(ranges) {
- let timeRangesObj;
- if (ranges === undefined || ranges.length === 0) {
- timeRangesObj = {
- length: 0,
- start() {
- throw new Error('This TimeRanges object is empty');
- },
- end() {
- throw new Error('This TimeRanges object is empty');
- }
- };
- } else {
- timeRangesObj = {
- length: ranges.length,
- start: getRange.bind(null, 'start', 0, ranges),
- end: getRange.bind(null, 'end', 1, ranges)
- };
- }
- if (window.Symbol && window.Symbol.iterator) {
- timeRangesObj[window.Symbol.iterator] = () => (ranges || []).values();
- }
- return timeRangesObj;
- }
-
- /**
- * Create a `TimeRange` object which mimics an
- * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
- *
- * @param {number|Array[]} start
- * The start of a single range (a number) or an array of ranges (an
- * array of arrays of two numbers each).
- *
- * @param {number} end
- * The end of a single range. Cannot be used with the array form of
- * the `start` argument.
- *
- * @return {TimeRange}
- */
- function createTimeRanges(start, end) {
- if (Array.isArray(start)) {
- return createTimeRangesObj(start);
- } else if (start === undefined || end === undefined) {
- return createTimeRangesObj();
- }
- return createTimeRangesObj([[start, end]]);
- }
-
- /**
- * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
- * seconds) will force a number of leading zeros to cover the length of the
- * guide.
- *
- * @private
- * @param {number} seconds
- * Number of seconds to be turned into a string
- *
- * @param {number} guide
- * Number (in seconds) to model the string after
- *
- * @return {string}
- * Time formatted as H:MM:SS or M:SS
- */
- const defaultImplementation = function (seconds, guide) {
- seconds = seconds < 0 ? 0 : seconds;
- let s = Math.floor(seconds % 60);
- let m = Math.floor(seconds / 60 % 60);
- let h = Math.floor(seconds / 3600);
- const gm = Math.floor(guide / 60 % 60);
- const gh = Math.floor(guide / 3600);
-
- // handle invalid times
- if (isNaN(seconds) || seconds === Infinity) {
- // '-' is false for all relational operators (e.g. <, >=) so this setting
- // will add the minimum number of fields specified by the guide
- h = m = s = '-';
- }
-
- // Check if we need to show hours
- h = h > 0 || gh > 0 ? h + ':' : '';
-
- // If hours are showing, we may need to add a leading zero.
- // Always show at least one digit of minutes.
- m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
-
- // Check if leading zero is need for seconds
- s = s < 10 ? '0' + s : s;
- return h + m + s;
- };
-
- // Internal pointer to the current implementation.
- let implementation = defaultImplementation;
-
- /**
- * Replaces the default formatTime implementation with a custom implementation.
- *
- * @param {Function} customImplementation
- * A function which will be used in place of the default formatTime
- * implementation. Will receive the current time in seconds and the
- * guide (in seconds) as arguments.
- */
- function setFormatTime(customImplementation) {
- implementation = customImplementation;
- }
-
- /**
- * Resets formatTime to the default implementation.
- */
- function resetFormatTime() {
- implementation = defaultImplementation;
- }
-
- /**
- * Delegates to either the default time formatting function or a custom
- * function supplied via `setFormatTime`.
- *
- * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
- * guide (in seconds) will force a number of leading zeros to cover the
- * length of the guide.
- *
- * @example formatTime(125, 600) === "02:05"
- * @param {number} seconds
- * Number of seconds to be turned into a string
- *
- * @param {number} guide
- * Number (in seconds) to model the string after
- *
- * @return {string}
- * Time formatted as H:MM:SS or M:SS
- */
- function formatTime(seconds, guide = seconds) {
- return implementation(seconds, guide);
- }
-
- var Time = /*#__PURE__*/Object.freeze({
- __proto__: null,
- createTimeRanges: createTimeRanges,
- createTimeRange: createTimeRanges,
- setFormatTime: setFormatTime,
- resetFormatTime: resetFormatTime,
- formatTime: formatTime
- });
-
- /**
- * @file buffer.js
- * @module buffer
- */
-
- /**
- * Compute the percentage of the media that has been buffered.
- *
- * @param { import('./time').TimeRange } buffered
- * The current `TimeRanges` object representing buffered time ranges
- *
- * @param {number} duration
- * Total duration of the media
- *
- * @return {number}
- * Percent buffered of the total duration in decimal form.
- */
- function bufferedPercent(buffered, duration) {
- let bufferedDuration = 0;
- let start;
- let end;
- if (!duration) {
- return 0;
- }
- if (!buffered || !buffered.length) {
- buffered = createTimeRanges(0, 0);
- }
- for (let i = 0; i < buffered.length; i++) {
- start = buffered.start(i);
- end = buffered.end(i);
-
- // buffered end can be bigger than duration by a very small fraction
- if (end > duration) {
- end = duration;
- }
- bufferedDuration += end - start;
- }
- return bufferedDuration / duration;
- }
-
- /**
- * @file media-error.js
- */
-
- /**
- * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
- *
- * @param {number|string|Object|MediaError} value
- * This can be of multiple types:
- * - number: should be a standard error code
- * - string: an error message (the code will be 0)
- * - Object: arbitrary properties
- * - `MediaError` (native): used to populate a video.js `MediaError` object
- * - `MediaError` (video.js): will return itself if it's already a
- * video.js `MediaError` object.
- *
- * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
- * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
- *
- * @class MediaError
- */
- function MediaError(value) {
- // Allow redundant calls to this constructor to avoid having `instanceof`
- // checks peppered around the code.
- if (value instanceof MediaError) {
- return value;
- }
- if (typeof value === 'number') {
- this.code = value;
- } else if (typeof value === 'string') {
- // default code is zero, so this is a custom error
- this.message = value;
- } else if (isObject(value)) {
- // We assign the `code` property manually because native `MediaError` objects
- // do not expose it as an own/enumerable property of the object.
- if (typeof value.code === 'number') {
- this.code = value.code;
- }
- Object.assign(this, value);
- }
- if (!this.message) {
- this.message = MediaError.defaultMessages[this.code] || '';
- }
- }
-
- /**
- * The error code that refers two one of the defined `MediaError` types
- *
- * @type {Number}
- */
- MediaError.prototype.code = 0;
-
- /**
- * An optional message that to show with the error. Message is not part of the HTML5
- * video spec but allows for more informative custom errors.
- *
- * @type {String}
- */
- MediaError.prototype.message = '';
-
- /**
- * An optional status code that can be set by plugins to allow even more detail about
- * the error. For example a plugin might provide a specific HTTP status code and an
- * error message for that code. Then when the plugin gets that error this class will
- * know how to display an error message for it. This allows a custom message to show
- * up on the `Player` error overlay.
- *
- * @type {Array}
- */
- MediaError.prototype.status = null;
-
- /**
- * An object containing an error type, as well as other information regarding the error.
- *
- * @typedef {{errorType: string, [key: string]: any}} ErrorMetadata
- */
-
- /**
- * An optional object to give more detail about the error. This can be used to give
- * a higher level of specificity to an error versus the more generic MediaError codes.
- * `metadata` expects an `errorType` string that should align with the values from videojs.Error.
- *
- * @type {ErrorMetadata}
- */
- MediaError.prototype.metadata = null;
-
- /**
- * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
- * specification listed under {@link MediaError} for more information.
- *
- * @enum {array}
- * @readonly
- * @property {string} 0 - MEDIA_ERR_CUSTOM
- * @property {string} 1 - MEDIA_ERR_ABORTED
- * @property {string} 2 - MEDIA_ERR_NETWORK
- * @property {string} 3 - MEDIA_ERR_DECODE
- * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
- * @property {string} 5 - MEDIA_ERR_ENCRYPTED
- */
- MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
-
- /**
- * The default `MediaError` messages based on the {@link MediaError.errorTypes}.
- *
- * @type {Array}
- * @constant
- */
- MediaError.defaultMessages = {
- 1: 'You aborted the media playback',
- 2: 'A network error caused the media download to fail part-way.',
- 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
- 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
- 5: 'The media is encrypted and we do not have the keys to decrypt it.'
- };
-
- /**
- * W3C error code for any custom error.
- *
- * @member MediaError#MEDIA_ERR_CUSTOM
- * @constant {number}
- * @default 0
- */
- MediaError.MEDIA_ERR_CUSTOM = 0;
-
- /**
- * W3C error code for any custom error.
- *
- * @member MediaError.MEDIA_ERR_CUSTOM
- * @constant {number}
- * @default 0
- */
- MediaError.prototype.MEDIA_ERR_CUSTOM = 0;
-
- /**
- * W3C error code for media error aborted.
- *
- * @member MediaError#MEDIA_ERR_ABORTED
- * @constant {number}
- * @default 1
- */
- MediaError.MEDIA_ERR_ABORTED = 1;
-
- /**
- * W3C error code for media error aborted.
- *
- * @member MediaError.MEDIA_ERR_ABORTED
- * @constant {number}
- * @default 1
- */
- MediaError.prototype.MEDIA_ERR_ABORTED = 1;
-
- /**
- * W3C error code for any network error.
- *
- * @member MediaError#MEDIA_ERR_NETWORK
- * @constant {number}
- * @default 2
- */
- MediaError.MEDIA_ERR_NETWORK = 2;
-
- /**
- * W3C error code for any network error.
- *
- * @member MediaError.MEDIA_ERR_NETWORK
- * @constant {number}
- * @default 2
- */
- MediaError.prototype.MEDIA_ERR_NETWORK = 2;
-
- /**
- * W3C error code for any decoding error.
- *
- * @member MediaError#MEDIA_ERR_DECODE
- * @constant {number}
- * @default 3
- */
- MediaError.MEDIA_ERR_DECODE = 3;
-
- /**
- * W3C error code for any decoding error.
- *
- * @member MediaError.MEDIA_ERR_DECODE
- * @constant {number}
- * @default 3
- */
- MediaError.prototype.MEDIA_ERR_DECODE = 3;
-
- /**
- * W3C error code for any time that a source is not supported.
- *
- * @member MediaError#MEDIA_ERR_SRC_NOT_SUPPORTED
- * @constant {number}
- * @default 4
- */
- MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
-
- /**
- * W3C error code for any time that a source is not supported.
- *
- * @member MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
- * @constant {number}
- * @default 4
- */
- MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
-
- /**
- * W3C error code for any time that a source is encrypted.
- *
- * @member MediaError#MEDIA_ERR_ENCRYPTED
- * @constant {number}
- * @default 5
- */
- MediaError.MEDIA_ERR_ENCRYPTED = 5;
-
- /**
- * W3C error code for any time that a source is encrypted.
- *
- * @member MediaError.MEDIA_ERR_ENCRYPTED
- * @constant {number}
- * @default 5
- */
- MediaError.prototype.MEDIA_ERR_ENCRYPTED = 5;
-
- var tuple = SafeParseTuple;
- function SafeParseTuple(obj, reviver) {
- var json;
- var error = null;
- try {
- json = JSON.parse(obj, reviver);
- } catch (err) {
- error = err;
- }
- return [error, json];
- }
-
- /**
- * Returns whether an object is `Promise`-like (i.e. has a `then` method).
- *
- * @param {Object} value
- * An object that may or may not be `Promise`-like.
- *
- * @return {boolean}
- * Whether or not the object is `Promise`-like.
- */
- function isPromise(value) {
- return value !== undefined && value !== null && typeof value.then === 'function';
- }
-
- /**
- * Silence a Promise-like object.
- *
- * This is useful for avoiding non-harmful, but potentially confusing "uncaught
- * play promise" rejection error messages.
- *
- * @param {Object} value
- * An object that may or may not be `Promise`-like.
- */
- function silencePromise(value) {
- if (isPromise(value)) {
- value.then(null, e => {});
- }
- }
-
- /**
- * @file text-track-list-converter.js Utilities for capturing text track state and
- * re-creating tracks based on a capture.
- *
- * @module text-track-list-converter
- */
-
- /**
- * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
- * represents the {@link TextTrack}'s state.
- *
- * @param {TextTrack} track
- * The text track to query.
- *
- * @return {Object}
- * A serializable javascript representation of the TextTrack.
- * @private
- */
- const trackToJson_ = function (track) {
- const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
- if (track[prop]) {
- acc[prop] = track[prop];
- }
- return acc;
- }, {
- cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
- return {
- startTime: cue.startTime,
- endTime: cue.endTime,
- text: cue.text,
- id: cue.id
- };
- })
- });
- return ret;
- };
-
- /**
- * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
- * state of all {@link TextTrack}s currently configured. The return array is compatible with
- * {@link text-track-list-converter:jsonToTextTracks}.
- *
- * @param { import('../tech/tech').default } tech
- * The tech object to query
- *
- * @return {Array}
- * A serializable javascript representation of the {@link Tech}s
- * {@link TextTrackList}.
- */
- const textTracksToJson = function (tech) {
- const trackEls = tech.$$('track');
- const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
- const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
- const json = trackToJson_(trackEl.track);
- if (trackEl.src) {
- json.src = trackEl.src;
- }
- return json;
- });
- return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
- return trackObjs.indexOf(track) === -1;
- }).map(trackToJson_));
- };
-
- /**
- * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
- * object {@link TextTrack} representations.
- *
- * @param {Array} json
- * An array of `TextTrack` representation objects, like those that would be
- * produced by `textTracksToJson`.
- *
- * @param {Tech} tech
- * The `Tech` to create the `TextTrack`s on.
- */
- const jsonToTextTracks = function (json, tech) {
- json.forEach(function (track) {
- const addedTrack = tech.addRemoteTextTrack(track).track;
- if (!track.src && track.cues) {
- track.cues.forEach(cue => addedTrack.addCue(cue));
- }
- });
- return tech.textTracks();
- };
- var textTrackConverter = {
- textTracksToJson,
- jsonToTextTracks,
- trackToJson_
- };
-
- /**
- * @file modal-dialog.js
- */
- const MODAL_CLASS_NAME = 'vjs-modal-dialog';
-
- /**
- * The `ModalDialog` displays over the video and its controls, which blocks
- * interaction with the player until it is closed.
- *
- * Modal dialogs include a "Close" button and will close when that button
- * is activated - or when ESC is pressed anywhere.
- *
- * @extends Component
- */
- class ModalDialog extends Component {
- /**
- * Create an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param { import('./utils/dom').ContentDescriptor} [options.content=undefined]
- * Provide customized content for this modal.
- *
- * @param {string} [options.description]
- * A text description for the modal, primarily for accessibility.
- *
- * @param {boolean} [options.fillAlways=false]
- * Normally, modals are automatically filled only the first time
- * they open. This tells the modal to refresh its content
- * every time it opens.
- *
- * @param {string} [options.label]
- * A text label for the modal, primarily for accessibility.
- *
- * @param {boolean} [options.pauseOnOpen=true]
- * If `true`, playback will will be paused if playing when
- * the modal opens, and resumed when it closes.
- *
- * @param {boolean} [options.temporary=true]
- * If `true`, the modal can only be opened once; it will be
- * disposed as soon as it's closed.
- *
- * @param {boolean} [options.uncloseable=false]
- * If `true`, the user will not be able to close the modal
- * through the UI in the normal ways. Programmatic closing is
- * still possible.
- */
- constructor(player, options) {
- super(player, options);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.close_ = e => this.close(e);
- this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
- this.closeable(!this.options_.uncloseable);
- this.content(this.options_.content);
-
- // Make sure the contentEl is defined AFTER any children are initialized
- // because we only want the contents of the modal in the contentEl
- // (not the UI elements like the close button).
- this.contentEl_ = createEl('div', {
- className: `${MODAL_CLASS_NAME}-content`
- }, {
- role: 'document'
- });
- this.descEl_ = createEl('p', {
- className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
- id: this.el().getAttribute('aria-describedby')
- });
- textContent(this.descEl_, this.description());
- this.el_.appendChild(this.descEl_);
- this.el_.appendChild(this.contentEl_);
- }
-
- /**
- * Create the `ModalDialog`'s DOM element
- *
- * @return {Element}
- * The DOM element that gets created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildCSSClass(),
- tabIndex: -1
- }, {
- 'aria-describedby': `${this.id()}_description`,
- 'aria-hidden': 'true',
- 'aria-label': this.label(),
- 'role': 'dialog',
- 'aria-live': 'polite'
- });
- }
- dispose() {
- this.contentEl_ = null;
- this.descEl_ = null;
- this.previouslyActiveEl_ = null;
- super.dispose();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
- }
-
- /**
- * Returns the label string for this modal. Primarily used for accessibility.
- *
- * @return {string}
- * the localized or raw label of this modal.
- */
- label() {
- return this.localize(this.options_.label || 'Modal Window');
- }
-
- /**
- * Returns the description string for this modal. Primarily used for
- * accessibility.
- *
- * @return {string}
- * The localized or raw description of this modal.
- */
- description() {
- let desc = this.options_.description || this.localize('This is a modal window.');
-
- // Append a universal closeability message if the modal is closeable.
- if (this.closeable()) {
- desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
- }
- return desc;
- }
-
- /**
- * Opens the modal.
- *
- * @fires ModalDialog#beforemodalopen
- * @fires ModalDialog#modalopen
- */
- open() {
- if (this.opened_) {
- if (this.options_.fillAlways) {
- this.fill();
- }
- return;
- }
- const player = this.player();
-
- /**
- * Fired just before a `ModalDialog` is opened.
- *
- * @event ModalDialog#beforemodalopen
- * @type {Event}
- */
- this.trigger('beforemodalopen');
- this.opened_ = true;
-
- // Fill content if the modal has never opened before and
- // never been filled.
- if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
- this.fill();
- }
-
- // If the player was playing, pause it and take note of its previously
- // playing state.
- this.wasPlaying_ = !player.paused();
- if (this.options_.pauseOnOpen && this.wasPlaying_) {
- player.pause();
- }
- this.on('keydown', this.handleKeyDown_);
-
- // Hide controls and note if they were enabled.
- this.hadControls_ = player.controls();
- player.controls(false);
- this.show();
- this.conditionalFocus_();
- this.el().setAttribute('aria-hidden', 'false');
-
- /**
- * Fired just after a `ModalDialog` is opened.
- *
- * @event ModalDialog#modalopen
- * @type {Event}
- */
- this.trigger('modalopen');
- this.hasBeenOpened_ = true;
- }
-
- /**
- * If the `ModalDialog` is currently open or closed.
- *
- * @param {boolean} [value]
- * If given, it will open (`true`) or close (`false`) the modal.
- *
- * @return {boolean}
- * the current open state of the modaldialog
- */
- opened(value) {
- if (typeof value === 'boolean') {
- this[value ? 'open' : 'close']();
- }
- return this.opened_;
- }
-
- /**
- * Closes the modal, does nothing if the `ModalDialog` is
- * not open.
- *
- * @fires ModalDialog#beforemodalclose
- * @fires ModalDialog#modalclose
- */
- close() {
- if (!this.opened_) {
- return;
- }
- const player = this.player();
-
- /**
- * Fired just before a `ModalDialog` is closed.
- *
- * @event ModalDialog#beforemodalclose
- * @type {Event}
- */
- this.trigger('beforemodalclose');
- this.opened_ = false;
- if (this.wasPlaying_ && this.options_.pauseOnOpen) {
- player.play();
- }
- this.off('keydown', this.handleKeyDown_);
- if (this.hadControls_) {
- player.controls(true);
- }
- this.hide();
- this.el().setAttribute('aria-hidden', 'true');
-
- /**
- * Fired just after a `ModalDialog` is closed.
- *
- * @event ModalDialog#modalclose
- * @type {Event}
- */
- this.trigger('modalclose');
- this.conditionalBlur_();
- if (this.options_.temporary) {
- this.dispose();
- }
- }
-
- /**
- * Check to see if the `ModalDialog` is closeable via the UI.
- *
- * @param {boolean} [value]
- * If given as a boolean, it will set the `closeable` option.
- *
- * @return {boolean}
- * Returns the final value of the closable option.
- */
- closeable(value) {
- if (typeof value === 'boolean') {
- const closeable = this.closeable_ = !!value;
- let close = this.getChild('closeButton');
-
- // If this is being made closeable and has no close button, add one.
- if (closeable && !close) {
- // The close button should be a child of the modal - not its
- // content element, so temporarily change the content element.
- const temp = this.contentEl_;
- this.contentEl_ = this.el_;
- close = this.addChild('closeButton', {
- controlText: 'Close Modal Dialog'
- });
- this.contentEl_ = temp;
- this.on(close, 'close', this.close_);
- }
-
- // If this is being made uncloseable and has a close button, remove it.
- if (!closeable && close) {
- this.off(close, 'close', this.close_);
- this.removeChild(close);
- close.dispose();
- }
- }
- return this.closeable_;
- }
-
- /**
- * Fill the modal's content element with the modal's "content" option.
- * The content element will be emptied before this change takes place.
- */
- fill() {
- this.fillWith(this.content());
- }
-
- /**
- * Fill the modal's content element with arbitrary content.
- * The content element will be emptied before this change takes place.
- *
- * @fires ModalDialog#beforemodalfill
- * @fires ModalDialog#modalfill
- *
- * @param { import('./utils/dom').ContentDescriptor} [content]
- * The same rules apply to this as apply to the `content` option.
- */
- fillWith(content) {
- const contentEl = this.contentEl();
- const parentEl = contentEl.parentNode;
- const nextSiblingEl = contentEl.nextSibling;
-
- /**
- * Fired just before a `ModalDialog` is filled with content.
- *
- * @event ModalDialog#beforemodalfill
- * @type {Event}
- */
- this.trigger('beforemodalfill');
- this.hasBeenFilled_ = true;
-
- // Detach the content element from the DOM before performing
- // manipulation to avoid modifying the live DOM multiple times.
- parentEl.removeChild(contentEl);
- this.empty();
- insertContent(contentEl, content);
- /**
- * Fired just after a `ModalDialog` is filled with content.
- *
- * @event ModalDialog#modalfill
- * @type {Event}
- */
- this.trigger('modalfill');
-
- // Re-inject the re-filled content element.
- if (nextSiblingEl) {
- parentEl.insertBefore(contentEl, nextSiblingEl);
- } else {
- parentEl.appendChild(contentEl);
- }
-
- // make sure that the close button is last in the dialog DOM
- const closeButton = this.getChild('closeButton');
- if (closeButton) {
- parentEl.appendChild(closeButton.el_);
- }
- }
-
- /**
- * Empties the content element. This happens anytime the modal is filled.
- *
- * @fires ModalDialog#beforemodalempty
- * @fires ModalDialog#modalempty
- */
- empty() {
- /**
- * Fired just before a `ModalDialog` is emptied.
- *
- * @event ModalDialog#beforemodalempty
- * @type {Event}
- */
- this.trigger('beforemodalempty');
- emptyEl(this.contentEl());
-
- /**
- * Fired just after a `ModalDialog` is emptied.
- *
- * @event ModalDialog#modalempty
- * @type {Event}
- */
- this.trigger('modalempty');
- }
-
- /**
- * Gets or sets the modal content, which gets normalized before being
- * rendered into the DOM.
- *
- * This does not update the DOM or fill the modal, but it is called during
- * that process.
- *
- * @param { import('./utils/dom').ContentDescriptor} [value]
- * If defined, sets the internal content value to be used on the
- * next call(s) to `fill`. This value is normalized before being
- * inserted. To "clear" the internal content value, pass `null`.
- *
- * @return { import('./utils/dom').ContentDescriptor}
- * The current content of the modal dialog
- */
- content(value) {
- if (typeof value !== 'undefined') {
- this.content_ = value;
- }
- return this.content_;
- }
-
- /**
- * conditionally focus the modal dialog if focus was previously on the player.
- *
- * @private
- */
- conditionalFocus_() {
- const activeEl = document.activeElement;
- const playerEl = this.player_.el_;
- this.previouslyActiveEl_ = null;
- if (playerEl.contains(activeEl) || playerEl === activeEl) {
- this.previouslyActiveEl_ = activeEl;
- this.focus();
- }
- }
-
- /**
- * conditionally blur the element and refocus the last focused element
- *
- * @private
- */
- conditionalBlur_() {
- if (this.previouslyActiveEl_) {
- this.previouslyActiveEl_.focus();
- this.previouslyActiveEl_ = null;
- }
- }
-
- /**
- * Keydown handler. Attached when modal is focused.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Do not allow keydowns to reach out of the modal dialog.
- event.stopPropagation();
- if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
- event.preventDefault();
- this.close();
- return;
- }
-
- // exit early if it isn't a tab key
- if (!keycode.isEventKey(event, 'Tab')) {
- return;
- }
- const focusableEls = this.focusableEls_();
- const activeEl = this.el_.querySelector(':focus');
- let focusIndex;
- for (let i = 0; i < focusableEls.length; i++) {
- if (activeEl === focusableEls[i]) {
- focusIndex = i;
- break;
- }
- }
- if (document.activeElement === this.el_) {
- focusIndex = 0;
- }
- if (event.shiftKey && focusIndex === 0) {
- focusableEls[focusableEls.length - 1].focus();
- event.preventDefault();
- } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
- focusableEls[0].focus();
- event.preventDefault();
- }
- }
-
- /**
- * get all focusable elements
- *
- * @private
- */
- focusableEls_() {
- const allChildren = this.el_.querySelectorAll('*');
- return Array.prototype.filter.call(allChildren, child => {
- return (child instanceof window.HTMLAnchorElement || child instanceof window.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window.HTMLInputElement || child instanceof window.HTMLSelectElement || child instanceof window.HTMLTextAreaElement || child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window.HTMLIFrameElement || child instanceof window.HTMLObjectElement || child instanceof window.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
- });
- }
- }
-
- /**
- * Default options for `ModalDialog` default options.
- *
- * @type {Object}
- * @private
- */
- ModalDialog.prototype.options_ = {
- pauseOnOpen: true,
- temporary: true
- };
- Component.registerComponent('ModalDialog', ModalDialog);
-
- /**
- * @file track-list.js
- */
-
- /**
- * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
- * {@link VideoTrackList}
- *
- * @extends EventTarget
- */
- class TrackList extends EventTarget {
- /**
- * Create an instance of this class
- *
- * @param { import('./track').default[] } tracks
- * A list of tracks to initialize the list with.
- *
- * @abstract
- */
- constructor(tracks = []) {
- super();
- this.tracks_ = [];
-
- /**
- * @memberof TrackList
- * @member {number} length
- * The current number of `Track`s in the this Trackist.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.tracks_.length;
- }
- });
- for (let i = 0; i < tracks.length; i++) {
- this.addTrack(tracks[i]);
- }
- }
-
- /**
- * Add a {@link Track} to the `TrackList`
- *
- * @param { import('./track').default } track
- * The audio, video, or text track to add to the list.
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- const index = this.tracks_.length;
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.tracks_[index];
- }
- });
- }
-
- // Do not add duplicate tracks
- if (this.tracks_.indexOf(track) === -1) {
- this.tracks_.push(track);
- /**
- * Triggered when a track is added to a track list.
- *
- * @event TrackList#addtrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was added.
- */
- this.trigger({
- track,
- type: 'addtrack',
- target: this
- });
- }
-
- /**
- * Triggered when a track label is changed.
- *
- * @event TrackList#addtrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was added.
- */
- track.labelchange_ = () => {
- this.trigger({
- track,
- type: 'labelchange',
- target: this
- });
- };
- if (isEvented(track)) {
- track.addEventListener('labelchange', track.labelchange_);
- }
- }
-
- /**
- * Remove a {@link Track} from the `TrackList`
- *
- * @param { import('./track').default } rtrack
- * The audio, video, or text track to remove from the list.
- *
- * @fires TrackList#removetrack
- */
- removeTrack(rtrack) {
- let track;
- for (let i = 0, l = this.length; i < l; i++) {
- if (this[i] === rtrack) {
- track = this[i];
- if (track.off) {
- track.off();
- }
- this.tracks_.splice(i, 1);
- break;
- }
- }
- if (!track) {
- return;
- }
-
- /**
- * Triggered when a track is removed from track list.
- *
- * @event TrackList#removetrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was removed.
- */
- this.trigger({
- track,
- type: 'removetrack',
- target: this
- });
- }
-
- /**
- * Get a Track from the TrackList by a tracks id
- *
- * @param {string} id - the id of the track to get
- * @method getTrackById
- * @return { import('./track').default }
- * @private
- */
- getTrackById(id) {
- let result = null;
- for (let i = 0, l = this.length; i < l; i++) {
- const track = this[i];
- if (track.id === id) {
- result = track;
- break;
- }
- }
- return result;
- }
- }
-
- /**
- * Triggered when a different track is selected/enabled.
- *
- * @event TrackList#change
- * @type {Event}
- */
-
- /**
- * Events that can be called with on + eventName. See {@link EventHandler}.
- *
- * @property {Object} TrackList#allowedEvents_
- * @protected
- */
- TrackList.prototype.allowedEvents_ = {
- change: 'change',
- addtrack: 'addtrack',
- removetrack: 'removetrack',
- labelchange: 'labelchange'
- };
-
- // emulate attribute EventHandler support to allow for feature detection
- for (const event in TrackList.prototype.allowedEvents_) {
- TrackList.prototype['on' + event] = null;
- }
-
- /**
- * @file audio-track-list.js
- */
-
- /**
- * Anywhere we call this function we diverge from the spec
- * as we only support one enabled audiotrack at a time
- *
- * @param {AudioTrackList} list
- * list to work on
- *
- * @param { import('./audio-track').default } track
- * The track to skip
- *
- * @private
- */
- const disableOthers$1 = function (list, track) {
- for (let i = 0; i < list.length; i++) {
- if (!Object.keys(list[i]).length || track.id === list[i].id) {
- continue;
- }
- // another audio track is enabled, disable it
- list[i].enabled = false;
- }
- };
-
- /**
- * The current list of {@link AudioTrack} for a media file.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
- * @extends TrackList
- */
- class AudioTrackList extends TrackList {
- /**
- * Create an instance of this class.
- *
- * @param { import('./audio-track').default[] } [tracks=[]]
- * A list of `AudioTrack` to instantiate the list with.
- */
- constructor(tracks = []) {
- // make sure only 1 track is enabled
- // sorted from last index to first index
- for (let i = tracks.length - 1; i >= 0; i--) {
- if (tracks[i].enabled) {
- disableOthers$1(tracks, tracks[i]);
- break;
- }
- }
- super(tracks);
- this.changing_ = false;
- }
-
- /**
- * Add an {@link AudioTrack} to the `AudioTrackList`.
- *
- * @param { import('./audio-track').default } track
- * The AudioTrack to add to the list
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- if (track.enabled) {
- disableOthers$1(this, track);
- }
- super.addTrack(track);
- // native tracks don't have this
- if (!track.addEventListener) {
- return;
- }
- track.enabledChange_ = () => {
- // when we are disabling other tracks (since we don't support
- // more than one track at a time) we will set changing_
- // to true so that we don't trigger additional change events
- if (this.changing_) {
- return;
- }
- this.changing_ = true;
- disableOthers$1(this, track);
- this.changing_ = false;
- this.trigger('change');
- };
-
- /**
- * @listens AudioTrack#enabledchange
- * @fires TrackList#change
- */
- track.addEventListener('enabledchange', track.enabledChange_);
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
- if (rtrack.removeEventListener && rtrack.enabledChange_) {
- rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
- rtrack.enabledChange_ = null;
- }
- }
- }
-
- /**
- * @file video-track-list.js
- */
-
- /**
- * Un-select all other {@link VideoTrack}s that are selected.
- *
- * @param {VideoTrackList} list
- * list to work on
- *
- * @param { import('./video-track').default } track
- * The track to skip
- *
- * @private
- */
- const disableOthers = function (list, track) {
- for (let i = 0; i < list.length; i++) {
- if (!Object.keys(list[i]).length || track.id === list[i].id) {
- continue;
- }
- // another video track is enabled, disable it
- list[i].selected = false;
- }
- };
-
- /**
- * The current list of {@link VideoTrack} for a video.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
- * @extends TrackList
- */
- class VideoTrackList extends TrackList {
- /**
- * Create an instance of this class.
- *
- * @param {VideoTrack[]} [tracks=[]]
- * A list of `VideoTrack` to instantiate the list with.
- */
- constructor(tracks = []) {
- // make sure only 1 track is enabled
- // sorted from last index to first index
- for (let i = tracks.length - 1; i >= 0; i--) {
- if (tracks[i].selected) {
- disableOthers(tracks, tracks[i]);
- break;
- }
- }
- super(tracks);
- this.changing_ = false;
-
- /**
- * @member {number} VideoTrackList#selectedIndex
- * The current index of the selected {@link VideoTrack`}.
- */
- Object.defineProperty(this, 'selectedIndex', {
- get() {
- for (let i = 0; i < this.length; i++) {
- if (this[i].selected) {
- return i;
- }
- }
- return -1;
- },
- set() {}
- });
- }
-
- /**
- * Add a {@link VideoTrack} to the `VideoTrackList`.
- *
- * @param { import('./video-track').default } track
- * The VideoTrack to add to the list
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- if (track.selected) {
- disableOthers(this, track);
- }
- super.addTrack(track);
- // native tracks don't have this
- if (!track.addEventListener) {
- return;
- }
- track.selectedChange_ = () => {
- if (this.changing_) {
- return;
- }
- this.changing_ = true;
- disableOthers(this, track);
- this.changing_ = false;
- this.trigger('change');
- };
-
- /**
- * @listens VideoTrack#selectedchange
- * @fires TrackList#change
- */
- track.addEventListener('selectedchange', track.selectedChange_);
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
- if (rtrack.removeEventListener && rtrack.selectedChange_) {
- rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
- rtrack.selectedChange_ = null;
- }
- }
- }
-
- /**
- * @file text-track-list.js
- */
-
- /**
- * The current list of {@link TextTrack} for a media file.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
- * @extends TrackList
- */
- class TextTrackList extends TrackList {
- /**
- * Add a {@link TextTrack} to the `TextTrackList`
- *
- * @param { import('./text-track').default } track
- * The text track to add to the list.
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- super.addTrack(track);
- if (!this.queueChange_) {
- this.queueChange_ = () => this.queueTrigger('change');
- }
- if (!this.triggerSelectedlanguagechange) {
- this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
- }
-
- /**
- * @listens TextTrack#modechange
- * @fires TrackList#change
- */
- track.addEventListener('modechange', this.queueChange_);
- const nonLanguageTextTrackKind = ['metadata', 'chapters'];
- if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
- track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
- }
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
-
- // manually remove the event handlers we added
- if (rtrack.removeEventListener) {
- if (this.queueChange_) {
- rtrack.removeEventListener('modechange', this.queueChange_);
- }
- if (this.selectedlanguagechange_) {
- rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
- }
- }
- }
- }
-
- /**
- * @file html-track-element-list.js
- */
-
- /**
- * The current list of {@link HtmlTrackElement}s.
- */
- class HtmlTrackElementList {
- /**
- * Create an instance of this class.
- *
- * @param {HtmlTrackElement[]} [tracks=[]]
- * A list of `HtmlTrackElement` to instantiate the list with.
- */
- constructor(trackElements = []) {
- this.trackElements_ = [];
-
- /**
- * @memberof HtmlTrackElementList
- * @member {number} length
- * The current number of `Track`s in the this Trackist.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.trackElements_.length;
- }
- });
- for (let i = 0, length = trackElements.length; i < length; i++) {
- this.addTrackElement_(trackElements[i]);
- }
- }
-
- /**
- * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
- *
- * @param {HtmlTrackElement} trackElement
- * The track element to add to the list.
- *
- * @private
- */
- addTrackElement_(trackElement) {
- const index = this.trackElements_.length;
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.trackElements_[index];
- }
- });
- }
-
- // Do not add duplicate elements
- if (this.trackElements_.indexOf(trackElement) === -1) {
- this.trackElements_.push(trackElement);
- }
- }
-
- /**
- * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
- * {@link TextTrack}.
- *
- * @param {TextTrack} track
- * The track associated with a track element.
- *
- * @return {HtmlTrackElement|undefined}
- * The track element that was found or undefined.
- *
- * @private
- */
- getTrackElementByTrack_(track) {
- let trackElement_;
- for (let i = 0, length = this.trackElements_.length; i < length; i++) {
- if (track === this.trackElements_[i].track) {
- trackElement_ = this.trackElements_[i];
- break;
- }
- }
- return trackElement_;
- }
-
- /**
- * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
- *
- * @param {HtmlTrackElement} trackElement
- * The track element to remove from the list.
- *
- * @private
- */
- removeTrackElement_(trackElement) {
- for (let i = 0, length = this.trackElements_.length; i < length; i++) {
- if (trackElement === this.trackElements_[i]) {
- if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
- this.trackElements_[i].track.off();
- }
- if (typeof this.trackElements_[i].off === 'function') {
- this.trackElements_[i].off();
- }
- this.trackElements_.splice(i, 1);
- break;
- }
- }
- }
- }
-
- /**
- * @file text-track-cue-list.js
- */
-
- /**
- * @typedef {Object} TextTrackCueList~TextTrackCue
- *
- * @property {string} id
- * The unique id for this text track cue
- *
- * @property {number} startTime
- * The start time for this text track cue
- *
- * @property {number} endTime
- * The end time for this text track cue
- *
- * @property {boolean} pauseOnExit
- * Pause when the end time is reached if true.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
- */
-
- /**
- * A List of TextTrackCues.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
- */
- class TextTrackCueList {
- /**
- * Create an instance of this class..
- *
- * @param {Array} cues
- * A list of cues to be initialized with
- */
- constructor(cues) {
- TextTrackCueList.prototype.setCues_.call(this, cues);
-
- /**
- * @memberof TextTrackCueList
- * @member {number} length
- * The current number of `TextTrackCue`s in the TextTrackCueList.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.length_;
- }
- });
- }
-
- /**
- * A setter for cues in this list. Creates getters
- * an an index for the cues.
- *
- * @param {Array} cues
- * An array of cues to set
- *
- * @private
- */
- setCues_(cues) {
- const oldLength = this.length || 0;
- let i = 0;
- const l = cues.length;
- this.cues_ = cues;
- this.length_ = cues.length;
- const defineProp = function (index) {
- if (!('' + index in this)) {
- Object.defineProperty(this, '' + index, {
- get() {
- return this.cues_[index];
- }
- });
- }
- };
- if (oldLength < l) {
- i = oldLength;
- for (; i < l; i++) {
- defineProp.call(this, i);
- }
- }
- }
-
- /**
- * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
- *
- * @param {string} id
- * The id of the cue that should be searched for.
- *
- * @return {TextTrackCueList~TextTrackCue|null}
- * A single cue or null if none was found.
- */
- getCueById(id) {
- let result = null;
- for (let i = 0, l = this.length; i < l; i++) {
- const cue = this[i];
- if (cue.id === id) {
- result = cue;
- break;
- }
- }
- return result;
- }
- }
-
- /**
- * @file track-kinds.js
- */
-
- /**
- * All possible `VideoTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
- * @typedef VideoTrack~Kind
- * @enum
- */
- const VideoTrackKind = {
- alternative: 'alternative',
- captions: 'captions',
- main: 'main',
- sign: 'sign',
- subtitles: 'subtitles',
- commentary: 'commentary'
- };
-
- /**
- * All possible `AudioTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
- * @typedef AudioTrack~Kind
- * @enum
- */
- const AudioTrackKind = {
- 'alternative': 'alternative',
- 'descriptions': 'descriptions',
- 'main': 'main',
- 'main-desc': 'main-desc',
- 'translation': 'translation',
- 'commentary': 'commentary'
- };
-
- /**
- * All possible `TextTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
- * @typedef TextTrack~Kind
- * @enum
- */
- const TextTrackKind = {
- subtitles: 'subtitles',
- captions: 'captions',
- descriptions: 'descriptions',
- chapters: 'chapters',
- metadata: 'metadata'
- };
-
- /**
- * All possible `TextTrackMode`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
- * @typedef TextTrack~Mode
- * @enum
- */
- const TextTrackMode = {
- disabled: 'disabled',
- hidden: 'hidden',
- showing: 'showing'
- };
-
- /**
- * @file track.js
- */
-
- /**
- * A Track class that contains all of the common functionality for {@link AudioTrack},
- * {@link VideoTrack}, and {@link TextTrack}.
- *
- * > Note: This class should not be used directly
- *
- * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
- * @extends EventTarget
- * @abstract
- */
- class Track extends EventTarget {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {string} [options.kind='']
- * A valid kind for the track type you are creating.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @abstract
- */
- constructor(options = {}) {
- super();
- const trackProps = {
- id: options.id || 'vjs_track_' + newGUID(),
- kind: options.kind || '',
- language: options.language || ''
- };
- let label = options.label || '';
-
- /**
- * @memberof Track
- * @member {string} id
- * The id of this track. Cannot be changed after creation.
- * @instance
- *
- * @readonly
- */
-
- /**
- * @memberof Track
- * @member {string} kind
- * The kind of track that this is. Cannot be changed after creation.
- * @instance
- *
- * @readonly
- */
-
- /**
- * @memberof Track
- * @member {string} language
- * The two letter language code for this track. Cannot be changed after
- * creation.
- * @instance
- *
- * @readonly
- */
-
- for (const key in trackProps) {
- Object.defineProperty(this, key, {
- get() {
- return trackProps[key];
- },
- set() {}
- });
- }
-
- /**
- * @memberof Track
- * @member {string} label
- * The label of this track. Cannot be changed after creation.
- * @instance
- *
- * @fires Track#labelchange
- */
- Object.defineProperty(this, 'label', {
- get() {
- return label;
- },
- set(newLabel) {
- if (newLabel !== label) {
- label = newLabel;
-
- /**
- * An event that fires when label changes on this track.
- *
- * > Note: This is not part of the spec!
- *
- * @event Track#labelchange
- * @type {Event}
- */
- this.trigger('labelchange');
- }
- }
- });
- }
- }
-
- /**
- * @file url.js
- * @module url
- */
-
- /**
- * @typedef {Object} url:URLObject
- *
- * @property {string} protocol
- * The protocol of the url that was parsed.
- *
- * @property {string} hostname
- * The hostname of the url that was parsed.
- *
- * @property {string} port
- * The port of the url that was parsed.
- *
- * @property {string} pathname
- * The pathname of the url that was parsed.
- *
- * @property {string} search
- * The search query of the url that was parsed.
- *
- * @property {string} hash
- * The hash of the url that was parsed.
- *
- * @property {string} host
- * The host of the url that was parsed.
- */
-
- /**
- * Resolve and parse the elements of a URL.
- *
- * @function
- * @param {String} url
- * The url to parse
- *
- * @return {url:URLObject}
- * An object of url details
- */
- const parseUrl = function (url) {
- // This entire method can be replace with URL once we are able to drop IE11
-
- const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
-
- // add the url to an anchor and let the browser parse the URL
- const a = document.createElement('a');
- a.href = url;
-
- // Copy the specific URL properties to a new object
- // This is also needed for IE because the anchor loses its
- // properties when it's removed from the dom
- const details = {};
- for (let i = 0; i < props.length; i++) {
- details[props[i]] = a[props[i]];
- }
-
- // IE adds the port to the host property unlike everyone else. If
- // a port identifier is added for standard ports, strip it.
- if (details.protocol === 'http:') {
- details.host = details.host.replace(/:80$/, '');
- }
- if (details.protocol === 'https:') {
- details.host = details.host.replace(/:443$/, '');
- }
- if (!details.protocol) {
- details.protocol = window.location.protocol;
- }
-
- /* istanbul ignore if */
- if (!details.host) {
- details.host = window.location.host;
- }
- return details;
- };
-
- /**
- * Get absolute version of relative URL.
- *
- * @function
- * @param {string} url
- * URL to make absolute
- *
- * @return {string}
- * Absolute URL
- *
- * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
- */
- const getAbsoluteURL = function (url) {
- // Check if absolute URL
- if (!url.match(/^https?:\/\//)) {
- // Add the url to an anchor and let the browser parse it to convert to an absolute url
- const a = document.createElement('a');
- a.href = url;
- url = a.href;
- }
- return url;
- };
-
- /**
- * Returns the extension of the passed file name. It will return an empty string
- * if passed an invalid path.
- *
- * @function
- * @param {string} path
- * The fileName path like '/path/to/file.mp4'
- *
- * @return {string}
- * The extension in lower case or an empty string if no
- * extension could be found.
- */
- const getFileExtension = function (path) {
- if (typeof path === 'string') {
- const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
- const pathParts = splitPathRe.exec(path);
- if (pathParts) {
- return pathParts.pop().toLowerCase();
- }
- }
- return '';
- };
-
- /**
- * Returns whether the url passed is a cross domain request or not.
- *
- * @function
- * @param {string} url
- * The url to check.
- *
- * @param {Object} [winLoc]
- * the domain to check the url against, defaults to window.location
- *
- * @param {string} [winLoc.protocol]
- * The window location protocol defaults to window.location.protocol
- *
- * @param {string} [winLoc.host]
- * The window location host defaults to window.location.host
- *
- * @return {boolean}
- * Whether it is a cross domain request or not.
- */
- const isCrossOrigin = function (url, winLoc = window.location) {
- const urlInfo = parseUrl(url);
-
- // IE8 protocol relative urls will return ':' for protocol
- const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
-
- // Check if url is for another domain/origin
- // IE8 doesn't know location.origin, so we won't rely on it here
- const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
- return crossOrigin;
- };
-
- var Url = /*#__PURE__*/Object.freeze({
- __proto__: null,
- parseUrl: parseUrl,
- getAbsoluteURL: getAbsoluteURL,
- getFileExtension: getFileExtension,
- isCrossOrigin: isCrossOrigin
- });
-
- var win;
- if (typeof window !== "undefined") {
- win = window;
- } else if (typeof commonjsGlobal !== "undefined") {
- win = commonjsGlobal;
- } else if (typeof self !== "undefined") {
- win = self;
- } else {
- win = {};
- }
- var window_1 = win;
-
- var _extends_1 = createCommonjsModule(function (module) {
- function _extends() {
- module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
- return target;
- }, module.exports.__esModule = true, module.exports["default"] = module.exports;
- return _extends.apply(this, arguments);
- }
- module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports;
- });
- unwrapExports(_extends_1);
-
- var isFunction_1 = isFunction;
- var toString = Object.prototype.toString;
- function isFunction(fn) {
- if (!fn) {
- return false;
- }
- var string = toString.call(fn);
- return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && (
- // IE8 and below
- fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt);
- }
-
- var httpResponseHandler = function httpResponseHandler(callback, decodeResponseBody) {
- if (decodeResponseBody === void 0) {
- decodeResponseBody = false;
- }
- return function (err, response, responseBody) {
- // if the XHR failed, return that error
- if (err) {
- callback(err);
- return;
- } // if the HTTP status code is 4xx or 5xx, the request also failed
-
- if (response.statusCode >= 400 && response.statusCode <= 599) {
- var cause = responseBody;
- if (decodeResponseBody) {
- if (window_1.TextDecoder) {
- var charset = getCharset(response.headers && response.headers['content-type']);
- try {
- cause = new TextDecoder(charset).decode(responseBody);
- } catch (e) {}
- } else {
- cause = String.fromCharCode.apply(null, new Uint8Array(responseBody));
- }
- }
- callback({
- cause: cause
- });
- return;
- } // otherwise, request succeeded
-
- callback(null, responseBody);
- };
- };
- function getCharset(contentTypeHeader) {
- if (contentTypeHeader === void 0) {
- contentTypeHeader = '';
- }
- return contentTypeHeader.toLowerCase().split(';').reduce(function (charset, contentType) {
- var _contentType$split = contentType.split('='),
- type = _contentType$split[0],
- value = _contentType$split[1];
- if (type.trim() === 'charset') {
- return value.trim();
- }
- return charset;
- }, 'utf-8');
- }
- var httpHandler = httpResponseHandler;
-
- createXHR.httpHandler = httpHandler;
- /**
- * @license
- * slighly modified parse-headers 2.0.2
- * Copyright (c) 2014 David Björklund
- * Available under the MIT license
- *
- */
-
- var parseHeaders = function parseHeaders(headers) {
- var result = {};
- if (!headers) {
- return result;
- }
- headers.trim().split('\n').forEach(function (row) {
- var index = row.indexOf(':');
- var key = row.slice(0, index).trim().toLowerCase();
- var value = row.slice(index + 1).trim();
- if (typeof result[key] === 'undefined') {
- result[key] = value;
- } else if (Array.isArray(result[key])) {
- result[key].push(value);
- } else {
- result[key] = [result[key], value];
- }
- });
- return result;
- };
- var lib = createXHR; // Allow use of default import syntax in TypeScript
-
- var default_1 = createXHR;
- createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop;
- createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest;
- forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) {
- createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) {
- options = initParams(uri, options, callback);
- options.method = method.toUpperCase();
- return _createXHR(options);
- };
- });
- function forEachArray(array, iterator) {
- for (var i = 0; i < array.length; i++) {
- iterator(array[i]);
- }
- }
- function isEmpty(obj) {
- for (var i in obj) {
- if (obj.hasOwnProperty(i)) return false;
- }
- return true;
- }
- function initParams(uri, options, callback) {
- var params = uri;
- if (isFunction_1(options)) {
- callback = options;
- if (typeof uri === "string") {
- params = {
- uri: uri
- };
- }
- } else {
- params = _extends_1({}, options, {
- uri: uri
- });
- }
- params.callback = callback;
- return params;
- }
- function createXHR(uri, options, callback) {
- options = initParams(uri, options, callback);
- return _createXHR(options);
- }
- function _createXHR(options) {
- if (typeof options.callback === "undefined") {
- throw new Error("callback argument missing");
- }
- var called = false;
- var callback = function cbOnce(err, response, body) {
- if (!called) {
- called = true;
- options.callback(err, response, body);
- }
- };
- function readystatechange() {
- if (xhr.readyState === 4) {
- setTimeout(loadFunc, 0);
- }
- }
- function getBody() {
- // Chrome with requestType=blob throws errors arround when even testing access to responseText
- var body = undefined;
- if (xhr.response) {
- body = xhr.response;
- } else {
- body = xhr.responseText || getXml(xhr);
- }
- if (isJson) {
- try {
- body = JSON.parse(body);
- } catch (e) {}
- }
- return body;
- }
- function errorFunc(evt) {
- clearTimeout(timeoutTimer);
- if (!(evt instanceof Error)) {
- evt = new Error("" + (evt || "Unknown XMLHttpRequest Error"));
- }
- evt.statusCode = 0;
- return callback(evt, failureResponse);
- } // will load the data & process the response in a special response object
-
- function loadFunc() {
- if (aborted) return;
- var status;
- clearTimeout(timeoutTimer);
- if (options.useXDR && xhr.status === undefined) {
- //IE8 CORS GET successful response doesn't have a status field, but body is fine
- status = 200;
- } else {
- status = xhr.status === 1223 ? 204 : xhr.status;
- }
- var response = failureResponse;
- var err = null;
- if (status !== 0) {
- response = {
- body: getBody(),
- statusCode: status,
- method: method,
- headers: {},
- url: uri,
- rawRequest: xhr
- };
- if (xhr.getAllResponseHeaders) {
- //remember xhr can in fact be XDR for CORS in IE
- response.headers = parseHeaders(xhr.getAllResponseHeaders());
- }
- } else {
- err = new Error("Internal XMLHttpRequest Error");
- }
- return callback(err, response, response.body);
- }
- var xhr = options.xhr || null;
- if (!xhr) {
- if (options.cors || options.useXDR) {
- xhr = new createXHR.XDomainRequest();
- } else {
- xhr = new createXHR.XMLHttpRequest();
- }
- }
- var key;
- var aborted;
- var uri = xhr.url = options.uri || options.url;
- var method = xhr.method = options.method || "GET";
- var body = options.body || options.data;
- var headers = xhr.headers = options.headers || {};
- var sync = !!options.sync;
- var isJson = false;
- var timeoutTimer;
- var failureResponse = {
- body: undefined,
- headers: {},
- statusCode: 0,
- method: method,
- url: uri,
- rawRequest: xhr
- };
- if ("json" in options && options.json !== false) {
- isJson = true;
- headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
-
- if (method !== "GET" && method !== "HEAD") {
- headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
-
- body = JSON.stringify(options.json === true ? body : options.json);
- }
- }
- xhr.onreadystatechange = readystatechange;
- xhr.onload = loadFunc;
- xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function.
-
- xhr.onprogress = function () {// IE must die
- };
- xhr.onabort = function () {
- aborted = true;
- };
- xhr.ontimeout = errorFunc;
- xhr.open(method, uri, !sync, options.username, options.password); //has to be after open
-
- if (!sync) {
- xhr.withCredentials = !!options.withCredentials;
- } // Cannot set timeout with sync request
- // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
- // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
-
- if (!sync && options.timeout > 0) {
- timeoutTimer = setTimeout(function () {
- if (aborted) return;
- aborted = true; //IE9 may still call readystatechange
-
- xhr.abort("timeout");
- var e = new Error("XMLHttpRequest timeout");
- e.code = "ETIMEDOUT";
- errorFunc(e);
- }, options.timeout);
- }
- if (xhr.setRequestHeader) {
- for (key in headers) {
- if (headers.hasOwnProperty(key)) {
- xhr.setRequestHeader(key, headers[key]);
- }
- }
- } else if (options.headers && !isEmpty(options.headers)) {
- throw new Error("Headers cannot be set on an XDomainRequest object");
- }
- if ("responseType" in options) {
- xhr.responseType = options.responseType;
- }
- if ("beforeSend" in options && typeof options.beforeSend === "function") {
- options.beforeSend(xhr);
- } // Microsoft Edge browser sends "undefined" when send is called with undefined value.
- // XMLHttpRequest spec says to pass null as body to indicate no body
- // See https://github.com/naugtur/xhr/issues/100.
-
- xhr.send(body || null);
- return xhr;
- }
- function getXml(xhr) {
- // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException"
- // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML.
- try {
- if (xhr.responseType === "document") {
- return xhr.responseXML;
- }
- var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
- if (xhr.responseType === "" && !firefoxBugTakenEffect) {
- return xhr.responseXML;
- }
- } catch (e) {}
- return null;
- }
- function noop() {}
- lib.default = default_1;
-
- /**
- * @file text-track.js
- */
-
- /**
- * Takes a webvtt file contents and parses it into cues
- *
- * @param {string} srcContent
- * webVTT file contents
- *
- * @param {TextTrack} track
- * TextTrack to add cues to. Cues come from the srcContent.
- *
- * @private
- */
- const parseCues = function (srcContent, track) {
- const parser = new window.WebVTT.Parser(window, window.vttjs, window.WebVTT.StringDecoder());
- const errors = [];
- parser.oncue = function (cue) {
- track.addCue(cue);
- };
- parser.onparsingerror = function (error) {
- errors.push(error);
- };
- parser.onflush = function () {
- track.trigger({
- type: 'loadeddata',
- target: track
- });
- };
- parser.parse(srcContent);
- if (errors.length > 0) {
- if (window.console && window.console.groupCollapsed) {
- window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
- }
- errors.forEach(error => log.error(error));
- if (window.console && window.console.groupEnd) {
- window.console.groupEnd();
- }
- }
- parser.flush();
- };
-
- /**
- * Load a `TextTrack` from a specified url.
- *
- * @param {string} src
- * Url to load track from.
- *
- * @param {TextTrack} track
- * Track to add cues to. Comes from the content at the end of `url`.
- *
- * @private
- */
- const loadTrack = function (src, track) {
- const opts = {
- uri: src
- };
- const crossOrigin = isCrossOrigin(src);
- if (crossOrigin) {
- opts.cors = crossOrigin;
- }
- const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
- if (withCredentials) {
- opts.withCredentials = withCredentials;
- }
- lib(opts, bind_(this, function (err, response, responseBody) {
- if (err) {
- return log.error(err, response);
- }
- track.loaded_ = true;
-
- // Make sure that vttjs has loaded, otherwise, wait till it finished loading
- // NOTE: this is only used for the alt/video.novtt.js build
- if (typeof window.WebVTT !== 'function') {
- if (track.tech_) {
- // to prevent use before define eslint error, we define loadHandler
- // as a let here
- track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
- if (event.type === 'vttjserror') {
- log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
- return;
- }
- return parseCues(responseBody, track);
- });
- }
- } else {
- parseCues(responseBody, track);
- }
- }));
- };
-
- /**
- * A representation of a single `TextTrack`.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
- * @extends Track
- */
- class TextTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} options={}
- * Object of option names and values
- *
- * @param { import('../tech/tech').default } options.tech
- * A reference to the tech that owns this TextTrack.
- *
- * @param {TextTrack~Kind} [options.kind='subtitles']
- * A valid text track kind.
- *
- * @param {TextTrack~Mode} [options.mode='disabled']
- * A valid text track mode.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this TextTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {string} [options.srclang='']
- * A valid two character language code. An alternative, but deprioritized
- * version of `options.language`
- *
- * @param {string} [options.src]
- * A url to TextTrack cues.
- *
- * @param {boolean} [options.default]
- * If this track should default to on or off.
- */
- constructor(options = {}) {
- if (!options.tech) {
- throw new Error('A tech was not provided.');
- }
- const settings = merge(options, {
- kind: TextTrackKind[options.kind] || 'subtitles',
- language: options.language || options.srclang || ''
- });
- let mode = TextTrackMode[settings.mode] || 'disabled';
- const default_ = settings.default;
- if (settings.kind === 'metadata' || settings.kind === 'chapters') {
- mode = 'hidden';
- }
- super(settings);
- this.tech_ = settings.tech;
- this.cues_ = [];
- this.activeCues_ = [];
- this.preload_ = this.tech_.preloadTextTracks !== false;
- const cues = new TextTrackCueList(this.cues_);
- const activeCues = new TextTrackCueList(this.activeCues_);
- let changed = false;
- this.timeupdateHandler = bind_(this, function (event = {}) {
- if (this.tech_.isDisposed()) {
- return;
- }
- if (!this.tech_.isReady_) {
- if (event.type !== 'timeupdate') {
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- }
- return;
- }
-
- // Accessing this.activeCues for the side-effects of updating itself
- // due to its nature as a getter function. Do not remove or cues will
- // stop updating!
- // Use the setter to prevent deletion from uglify (pure_getters rule)
- this.activeCues = this.activeCues;
- if (changed) {
- this.trigger('cuechange');
- changed = false;
- }
- if (event.type !== 'timeupdate') {
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- }
- });
- const disposeHandler = () => {
- this.stopTracking();
- };
- this.tech_.one('dispose', disposeHandler);
- if (mode !== 'disabled') {
- this.startTracking();
- }
- Object.defineProperties(this, {
- /**
- * @memberof TextTrack
- * @member {boolean} default
- * If this track was set to be on or off by default. Cannot be changed after
- * creation.
- * @instance
- *
- * @readonly
- */
- default: {
- get() {
- return default_;
- },
- set() {}
- },
- /**
- * @memberof TextTrack
- * @member {string} mode
- * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
- * not be set if setting to an invalid mode.
- * @instance
- *
- * @fires TextTrack#modechange
- */
- mode: {
- get() {
- return mode;
- },
- set(newMode) {
- if (!TextTrackMode[newMode]) {
- return;
- }
- if (mode === newMode) {
- return;
- }
- mode = newMode;
- if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
- // On-demand load.
- loadTrack(this.src, this);
- }
- this.stopTracking();
- if (mode !== 'disabled') {
- this.startTracking();
- }
- /**
- * An event that fires when mode changes on this track. This allows
- * the TextTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec!
- *
- * @event TextTrack#modechange
- * @type {Event}
- */
- this.trigger('modechange');
- }
- },
- /**
- * @memberof TextTrack
- * @member {TextTrackCueList} cues
- * The text track cue list for this TextTrack.
- * @instance
- */
- cues: {
- get() {
- if (!this.loaded_) {
- return null;
- }
- return cues;
- },
- set() {}
- },
- /**
- * @memberof TextTrack
- * @member {TextTrackCueList} activeCues
- * The list text track cues that are currently active for this TextTrack.
- * @instance
- */
- activeCues: {
- get() {
- if (!this.loaded_) {
- return null;
- }
-
- // nothing to do
- if (this.cues.length === 0) {
- return activeCues;
- }
- const ct = this.tech_.currentTime();
- const active = [];
- for (let i = 0, l = this.cues.length; i < l; i++) {
- const cue = this.cues[i];
- if (cue.startTime <= ct && cue.endTime >= ct) {
- active.push(cue);
- }
- }
- changed = false;
- if (active.length !== this.activeCues_.length) {
- changed = true;
- } else {
- for (let i = 0; i < active.length; i++) {
- if (this.activeCues_.indexOf(active[i]) === -1) {
- changed = true;
- }
- }
- }
- this.activeCues_ = active;
- activeCues.setCues_(this.activeCues_);
- return activeCues;
- },
- // /!\ Keep this setter empty (see the timeupdate handler above)
- set() {}
- }
- });
- if (settings.src) {
- this.src = settings.src;
- if (!this.preload_) {
- // Tracks will load on-demand.
- // Act like we're loaded for other purposes.
- this.loaded_ = true;
- }
- if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
- loadTrack(this.src, this);
- }
- } else {
- this.loaded_ = true;
- }
- }
- startTracking() {
- // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
- this.tech_.on('timeupdate', this.timeupdateHandler);
- }
- stopTracking() {
- if (this.rvf_) {
- this.tech_.cancelVideoFrameCallback(this.rvf_);
- this.rvf_ = undefined;
- }
- this.tech_.off('timeupdate', this.timeupdateHandler);
- }
-
- /**
- * Add a cue to the internal list of cues.
- *
- * @param {TextTrack~Cue} cue
- * The cue to add to our internal list
- */
- addCue(originalCue) {
- let cue = originalCue;
-
- // Testing if the cue is a VTTCue in a way that survives minification
- if (!('getCueAsHTML' in cue)) {
- cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
- for (const prop in originalCue) {
- if (!(prop in cue)) {
- cue[prop] = originalCue[prop];
- }
- }
-
- // make sure that `id` is copied over
- cue.id = originalCue.id;
- cue.originalCue_ = originalCue;
- }
- const tracks = this.tech_.textTracks();
- for (let i = 0; i < tracks.length; i++) {
- if (tracks[i] !== this) {
- tracks[i].removeCue(cue);
- }
- }
- this.cues_.push(cue);
- this.cues.setCues_(this.cues_);
- }
-
- /**
- * Remove a cue from our internal list
- *
- * @param {TextTrack~Cue} removeCue
- * The cue to remove from our internal list
- */
- removeCue(removeCue) {
- let i = this.cues_.length;
- while (i--) {
- const cue = this.cues_[i];
- if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
- this.cues_.splice(i, 1);
- this.cues.setCues_(this.cues_);
- break;
- }
- }
- }
- }
-
- /**
- * cuechange - One or more cues in the track have become active or stopped being active.
- * @protected
- */
- TextTrack.prototype.allowedEvents_ = {
- cuechange: 'cuechange'
- };
-
- /**
- * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
- * only one `AudioTrack` in the list will be enabled at a time.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
- * @extends Track
- */
- class AudioTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {AudioTrack~Kind} [options.kind='']
- * A valid audio track kind
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {boolean} [options.enabled]
- * If this track is the one that is currently playing. If this track is part of
- * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
- */
- constructor(options = {}) {
- const settings = merge(options, {
- kind: AudioTrackKind[options.kind] || ''
- });
- super(settings);
- let enabled = false;
-
- /**
- * @memberof AudioTrack
- * @member {boolean} enabled
- * If this `AudioTrack` is enabled or not. When setting this will
- * fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
- * @instance
- *
- * @fires VideoTrack#selectedchange
- */
- Object.defineProperty(this, 'enabled', {
- get() {
- return enabled;
- },
- set(newEnabled) {
- // an invalid or unchanged value
- if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
- return;
- }
- enabled = newEnabled;
-
- /**
- * An event that fires when enabled changes on this track. This allows
- * the AudioTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec! Native tracks will do
- * this internally without an event.
- *
- * @event AudioTrack#enabledchange
- * @type {Event}
- */
- this.trigger('enabledchange');
- }
- });
-
- // if the user sets this track to selected then
- // set selected to that true value otherwise
- // we keep it false
- if (settings.enabled) {
- this.enabled = settings.enabled;
- }
- this.loaded_ = true;
- }
- }
-
- /**
- * A representation of a single `VideoTrack`.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
- * @extends Track
- */
- class VideoTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {string} [options.kind='']
- * A valid {@link VideoTrack~Kind}
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {boolean} [options.selected]
- * If this track is the one that is currently playing.
- */
- constructor(options = {}) {
- const settings = merge(options, {
- kind: VideoTrackKind[options.kind] || ''
- });
- super(settings);
- let selected = false;
-
- /**
- * @memberof VideoTrack
- * @member {boolean} selected
- * If this `VideoTrack` is selected or not. When setting this will
- * fire {@link VideoTrack#selectedchange} if the state of selected changed.
- * @instance
- *
- * @fires VideoTrack#selectedchange
- */
- Object.defineProperty(this, 'selected', {
- get() {
- return selected;
- },
- set(newSelected) {
- // an invalid or unchanged value
- if (typeof newSelected !== 'boolean' || newSelected === selected) {
- return;
- }
- selected = newSelected;
-
- /**
- * An event that fires when selected changes on this track. This allows
- * the VideoTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec! Native tracks will do
- * this internally without an event.
- *
- * @event VideoTrack#selectedchange
- * @type {Event}
- */
- this.trigger('selectedchange');
- }
- });
-
- // if the user sets this track to selected then
- // set selected to that true value otherwise
- // we keep it false
- if (settings.selected) {
- this.selected = settings.selected;
- }
- }
- }
-
- /**
- * @file html-track-element.js
- */
-
- /**
- * A single track represented in the DOM.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
- * @extends EventTarget
- */
- class HTMLTrackElement extends EventTarget {
- /**
- * Create an instance of this class.
- *
- * @param {Object} options={}
- * Object of option names and values
- *
- * @param { import('../tech/tech').default } options.tech
- * A reference to the tech that owns this HTMLTrackElement.
- *
- * @param {TextTrack~Kind} [options.kind='subtitles']
- * A valid text track kind.
- *
- * @param {TextTrack~Mode} [options.mode='disabled']
- * A valid text track mode.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this TextTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {string} [options.srclang='']
- * A valid two character language code. An alternative, but deprioritized
- * version of `options.language`
- *
- * @param {string} [options.src]
- * A url to TextTrack cues.
- *
- * @param {boolean} [options.default]
- * If this track should default to on or off.
- */
- constructor(options = {}) {
- super();
- let readyState;
- const track = new TextTrack(options);
- this.kind = track.kind;
- this.src = track.src;
- this.srclang = track.language;
- this.label = track.label;
- this.default = track.default;
- Object.defineProperties(this, {
- /**
- * @memberof HTMLTrackElement
- * @member {HTMLTrackElement~ReadyState} readyState
- * The current ready state of the track element.
- * @instance
- */
- readyState: {
- get() {
- return readyState;
- }
- },
- /**
- * @memberof HTMLTrackElement
- * @member {TextTrack} track
- * The underlying TextTrack object.
- * @instance
- *
- */
- track: {
- get() {
- return track;
- }
- }
- });
- readyState = HTMLTrackElement.NONE;
-
- /**
- * @listens TextTrack#loadeddata
- * @fires HTMLTrackElement#load
- */
- track.addEventListener('loadeddata', () => {
- readyState = HTMLTrackElement.LOADED;
- this.trigger({
- type: 'load',
- target: this
- });
- });
- }
- }
-
- /**
- * @protected
- */
- HTMLTrackElement.prototype.allowedEvents_ = {
- load: 'load'
- };
-
- /**
- * The text track not loaded state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.NONE = 0;
-
- /**
- * The text track loading state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.LOADING = 1;
-
- /**
- * The text track loaded state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.LOADED = 2;
-
- /**
- * The text track failed to load state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.ERROR = 3;
-
- /*
- * This file contains all track properties that are used in
- * player.js, tech.js, html5.js and possibly other techs in the future.
- */
-
- const NORMAL = {
- audio: {
- ListClass: AudioTrackList,
- TrackClass: AudioTrack,
- capitalName: 'Audio'
- },
- video: {
- ListClass: VideoTrackList,
- TrackClass: VideoTrack,
- capitalName: 'Video'
- },
- text: {
- ListClass: TextTrackList,
- TrackClass: TextTrack,
- capitalName: 'Text'
- }
- };
- Object.keys(NORMAL).forEach(function (type) {
- NORMAL[type].getterName = `${type}Tracks`;
- NORMAL[type].privateName = `${type}Tracks_`;
- });
- const REMOTE = {
- remoteText: {
- ListClass: TextTrackList,
- TrackClass: TextTrack,
- capitalName: 'RemoteText',
- getterName: 'remoteTextTracks',
- privateName: 'remoteTextTracks_'
- },
- remoteTextEl: {
- ListClass: HtmlTrackElementList,
- TrackClass: HTMLTrackElement,
- capitalName: 'RemoteTextTrackEls',
- getterName: 'remoteTextTrackEls',
- privateName: 'remoteTextTrackEls_'
- }
- };
- const ALL = Object.assign({}, NORMAL, REMOTE);
- REMOTE.names = Object.keys(REMOTE);
- NORMAL.names = Object.keys(NORMAL);
- ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
-
- var vtt = {};
-
- /**
- * @file tech.js
- */
-
- /**
- * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
- * that just contains the src url alone.
- * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
- * `var SourceString = 'http://example.com/some-video.mp4';`
- *
- * @typedef {Object|string} SourceObject
- *
- * @property {string} src
- * The url to the source
- *
- * @property {string} type
- * The mime type of the source
- */
-
- /**
- * A function used by {@link Tech} to create a new {@link TextTrack}.
- *
- * @private
- *
- * @param {Tech} self
- * An instance of the Tech class.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @param {Object} [options={}]
- * An object with additional text track options
- *
- * @return {TextTrack}
- * The text track that was created.
- */
- function createTrackHelper(self, kind, label, language, options = {}) {
- const tracks = self.textTracks();
- options.kind = kind;
- if (label) {
- options.label = label;
- }
- if (language) {
- options.language = language;
- }
- options.tech = self;
- const track = new ALL.text.TrackClass(options);
- tracks.addTrack(track);
- return track;
- }
-
- /**
- * This is the base class for media playback technology controllers, such as
- * {@link HTML5}
- *
- * @extends Component
- */
- class Tech extends Component {
- /**
- * Create an instance of this Tech.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * Callback function to call when the `HTML5` Tech is ready.
- */
- constructor(options = {}, ready = function () {}) {
- // we don't want the tech to report user activity automatically.
- // This is done manually in addControlsListeners
- options.reportTouchActivity = false;
- super(null, options, ready);
- this.onDurationChange_ = e => this.onDurationChange(e);
- this.trackProgress_ = e => this.trackProgress(e);
- this.trackCurrentTime_ = e => this.trackCurrentTime(e);
- this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
- this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
- this.queuedHanders_ = new Set();
-
- // keep track of whether the current source has played at all to
- // implement a very limited played()
- this.hasStarted_ = false;
- this.on('playing', function () {
- this.hasStarted_ = true;
- });
- this.on('loadstart', function () {
- this.hasStarted_ = false;
- });
- ALL.names.forEach(name => {
- const props = ALL[name];
- if (options && options[props.getterName]) {
- this[props.privateName] = options[props.getterName];
- }
- });
-
- // Manually track progress in cases where the browser/tech doesn't report it.
- if (!this.featuresProgressEvents) {
- this.manualProgressOn();
- }
-
- // Manually track timeupdates in cases where the browser/tech doesn't report it.
- if (!this.featuresTimeupdateEvents) {
- this.manualTimeUpdatesOn();
- }
- ['Text', 'Audio', 'Video'].forEach(track => {
- if (options[`native${track}Tracks`] === false) {
- this[`featuresNative${track}Tracks`] = false;
- }
- });
- if (options.nativeCaptions === false || options.nativeTextTracks === false) {
- this.featuresNativeTextTracks = false;
- } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
- this.featuresNativeTextTracks = true;
- }
- if (!this.featuresNativeTextTracks) {
- this.emulateTextTracks();
- }
- this.preloadTextTracks = options.preloadTextTracks !== false;
- this.autoRemoteTextTracks_ = new ALL.text.ListClass();
- this.initTrackListeners();
-
- // Turn on component tap events only if not using native controls
- if (!options.nativeControlsForTouch) {
- this.emitTapEvents();
- }
- if (this.constructor) {
- this.name_ = this.constructor.name || 'Unknown Tech';
- }
- }
-
- /**
- * A special function to trigger source set in a way that will allow player
- * to re-trigger if the player or tech are not ready yet.
- *
- * @fires Tech#sourceset
- * @param {string} src The source string at the time of the source changing.
- */
- triggerSourceset(src) {
- if (!this.isReady_) {
- // on initial ready we have to trigger source set
- // 1ms after ready so that player can watch for it.
- this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
- }
-
- /**
- * Fired when the source is set on the tech causing the media element
- * to reload.
- *
- * @see {@link Player#event:sourceset}
- * @event Tech#sourceset
- * @type {Event}
- */
- this.trigger({
- src,
- type: 'sourceset'
- });
- }
-
- /* Fallbacks for unsupported event types
- ================================================================================ */
-
- /**
- * Polyfill the `progress` event for browsers that don't support it natively.
- *
- * @see {@link Tech#trackProgress}
- */
- manualProgressOn() {
- this.on('durationchange', this.onDurationChange_);
- this.manualProgress = true;
-
- // Trigger progress watching when a source begins loading
- this.one('ready', this.trackProgress_);
- }
-
- /**
- * Turn off the polyfill for `progress` events that was created in
- * {@link Tech#manualProgressOn}
- */
- manualProgressOff() {
- this.manualProgress = false;
- this.stopTrackingProgress();
- this.off('durationchange', this.onDurationChange_);
- }
-
- /**
- * This is used to trigger a `progress` event when the buffered percent changes. It
- * sets an interval function that will be called every 500 milliseconds to check if the
- * buffer end percent has changed.
- *
- * > This function is called by {@link Tech#manualProgressOn}
- *
- * @param {Event} event
- * The `ready` event that caused this to run.
- *
- * @listens Tech#ready
- * @fires Tech#progress
- */
- trackProgress(event) {
- this.stopTrackingProgress();
- this.progressInterval = this.setInterval(bind_(this, function () {
- // Don't trigger unless buffered amount is greater than last time
-
- const numBufferedPercent = this.bufferedPercent();
- if (this.bufferedPercent_ !== numBufferedPercent) {
- /**
- * See {@link Player#progress}
- *
- * @event Tech#progress
- * @type {Event}
- */
- this.trigger('progress');
- }
- this.bufferedPercent_ = numBufferedPercent;
- if (numBufferedPercent === 1) {
- this.stopTrackingProgress();
- }
- }), 500);
- }
-
- /**
- * Update our internal duration on a `durationchange` event by calling
- * {@link Tech#duration}.
- *
- * @param {Event} event
- * The `durationchange` event that caused this to run.
- *
- * @listens Tech#durationchange
- */
- onDurationChange(event) {
- this.duration_ = this.duration();
- }
-
- /**
- * Get and create a `TimeRange` object for buffering.
- *
- * @return { import('../utils/time').TimeRange }
- * The time range object that was created.
- */
- buffered() {
- return createTimeRanges(0, 0);
- }
-
- /**
- * Get the percentage of the current video that is currently buffered.
- *
- * @return {number}
- * A number from 0 to 1 that represents the decimal percentage of the
- * video that is buffered.
- *
- */
- bufferedPercent() {
- return bufferedPercent(this.buffered(), this.duration_);
- }
-
- /**
- * Turn off the polyfill for `progress` events that was created in
- * {@link Tech#manualProgressOn}
- * Stop manually tracking progress events by clearing the interval that was set in
- * {@link Tech#trackProgress}.
- */
- stopTrackingProgress() {
- this.clearInterval(this.progressInterval);
- }
-
- /**
- * Polyfill the `timeupdate` event for browsers that don't support it.
- *
- * @see {@link Tech#trackCurrentTime}
- */
- manualTimeUpdatesOn() {
- this.manualTimeUpdates = true;
- this.on('play', this.trackCurrentTime_);
- this.on('pause', this.stopTrackingCurrentTime_);
- }
-
- /**
- * Turn off the polyfill for `timeupdate` events that was created in
- * {@link Tech#manualTimeUpdatesOn}
- */
- manualTimeUpdatesOff() {
- this.manualTimeUpdates = false;
- this.stopTrackingCurrentTime();
- this.off('play', this.trackCurrentTime_);
- this.off('pause', this.stopTrackingCurrentTime_);
- }
-
- /**
- * Sets up an interval function to track current time and trigger `timeupdate` every
- * 250 milliseconds.
- *
- * @listens Tech#play
- * @triggers Tech#timeupdate
- */
- trackCurrentTime() {
- if (this.currentTimeInterval) {
- this.stopTrackingCurrentTime();
- }
- this.currentTimeInterval = this.setInterval(function () {
- /**
- * Triggered at an interval of 250ms to indicated that time is passing in the video.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
-
- // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
- }, 250);
- }
-
- /**
- * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
- * `timeupdate` event is no longer triggered.
- *
- * @listens {Tech#pause}
- */
- stopTrackingCurrentTime() {
- this.clearInterval(this.currentTimeInterval);
-
- // #1002 - if the video ends right before the next timeupdate would happen,
- // the progress bar won't make it all the way to the end
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- }
-
- /**
- * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
- * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
- *
- * @fires Component#dispose
- */
- dispose() {
- // clear out all tracks because we can't reuse them between techs
- this.clearTracks(NORMAL.names);
-
- // Turn off any manual progress or timeupdate tracking
- if (this.manualProgress) {
- this.manualProgressOff();
- }
- if (this.manualTimeUpdates) {
- this.manualTimeUpdatesOff();
- }
- super.dispose();
- }
-
- /**
- * Clear out a single `TrackList` or an array of `TrackLists` given their names.
- *
- * > Note: Techs without source handlers should call this between sources for `video`
- * & `audio` tracks. You don't want to use them between tracks!
- *
- * @param {string[]|string} types
- * TrackList names to clear, valid names are `video`, `audio`, and
- * `text`.
- */
- clearTracks(types) {
- types = [].concat(types);
- // clear out all tracks because we can't reuse them between techs
- types.forEach(type => {
- const list = this[`${type}Tracks`]() || [];
- let i = list.length;
- while (i--) {
- const track = list[i];
- if (type === 'text') {
- this.removeRemoteTextTrack(track);
- }
- list.removeTrack(track);
- }
- });
- }
-
- /**
- * Remove any TextTracks added via addRemoteTextTrack that are
- * flagged for automatic garbage collection
- */
- cleanupAutoTextTracks() {
- const list = this.autoRemoteTextTracks_ || [];
- let i = list.length;
- while (i--) {
- const track = list[i];
- this.removeRemoteTextTrack(track);
- }
- }
-
- /**
- * Reset the tech, which will removes all sources and reset the internal readyState.
- *
- * @abstract
- */
- reset() {}
-
- /**
- * Get the value of `crossOrigin` from the tech.
- *
- * @abstract
- *
- * @see {Html5#crossOrigin}
- */
- crossOrigin() {}
-
- /**
- * Set the value of `crossOrigin` on the tech.
- *
- * @abstract
- *
- * @param {string} crossOrigin the crossOrigin value
- * @see {Html5#setCrossOrigin}
- */
- setCrossOrigin() {}
-
- /**
- * Get or set an error on the Tech.
- *
- * @param {MediaError} [err]
- * Error to set on the Tech
- *
- * @return {MediaError|null}
- * The current error object on the tech, or null if there isn't one.
- */
- error(err) {
- if (err !== undefined) {
- this.error_ = new MediaError(err);
- this.trigger('error');
- }
- return this.error_;
- }
-
- /**
- * Returns the `TimeRange`s that have been played through for the current source.
- *
- * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
- * It only checks whether the source has played at all or not.
- *
- * @return { import('../utils/time').TimeRange }
- * - A single time range if this video has played
- * - An empty set of ranges if not.
- */
- played() {
- if (this.hasStarted_) {
- return createTimeRanges(0, 0);
- }
- return createTimeRanges();
- }
-
- /**
- * Start playback
- *
- * @abstract
- *
- * @see {Html5#play}
- */
- play() {}
-
- /**
- * Set whether we are scrubbing or not
- *
- * @abstract
- * @param {boolean} _isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- *
- * @see {Html5#setScrubbing}
- */
- setScrubbing(_isScrubbing) {}
-
- /**
- * Get whether we are scrubbing or not
- *
- * @abstract
- *
- * @see {Html5#scrubbing}
- */
- scrubbing() {}
-
- /**
- * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
- * previously called.
- *
- * @param {number} _seconds
- * Set the current time of the media to this.
- * @fires Tech#timeupdate
- */
- setCurrentTime(_seconds) {
- // improve the accuracy of manual timeupdates
- if (this.manualTimeUpdates) {
- /**
- * A manual `timeupdate` event.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- }
- }
-
- /**
- * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
- * {@link TextTrackList} events.
- *
- * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
- *
- * @fires Tech#audiotrackchange
- * @fires Tech#videotrackchange
- * @fires Tech#texttrackchange
- */
- initTrackListeners() {
- /**
- * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
- *
- * @event Tech#audiotrackchange
- * @type {Event}
- */
-
- /**
- * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
- *
- * @event Tech#videotrackchange
- * @type {Event}
- */
-
- /**
- * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
- *
- * @event Tech#texttrackchange
- * @type {Event}
- */
- NORMAL.names.forEach(name => {
- const props = NORMAL[name];
- const trackListChanges = () => {
- this.trigger(`${name}trackchange`);
- };
- const tracks = this[props.getterName]();
- tracks.addEventListener('removetrack', trackListChanges);
- tracks.addEventListener('addtrack', trackListChanges);
- this.on('dispose', () => {
- tracks.removeEventListener('removetrack', trackListChanges);
- tracks.removeEventListener('addtrack', trackListChanges);
- });
- });
- }
-
- /**
- * Emulate TextTracks using vtt.js if necessary
- *
- * @fires Tech#vttjsloaded
- * @fires Tech#vttjserror
- */
- addWebVttScript_() {
- if (window.WebVTT) {
- return;
- }
-
- // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
- // signals that the Tech is ready at which point Tech.el_ is part of the DOM
- // before inserting the WebVTT script
- if (document.body.contains(this.el())) {
- // load via require if available and vtt.js script location was not passed in
- // as an option. novtt builds will turn the above require call into an empty object
- // which will cause this if check to always fail.
- if (!this.options_['vtt.js'] && isPlain(vtt) && Object.keys(vtt).length > 0) {
- this.trigger('vttjsloaded');
- return;
- }
-
- // load vtt.js via the script location option or the cdn of no location was
- // passed in
- const script = document.createElement('script');
- script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
- script.onload = () => {
- /**
- * Fired when vtt.js is loaded.
- *
- * @event Tech#vttjsloaded
- * @type {Event}
- */
- this.trigger('vttjsloaded');
- };
- script.onerror = () => {
- /**
- * Fired when vtt.js was not loaded due to an error
- *
- * @event Tech#vttjsloaded
- * @type {Event}
- */
- this.trigger('vttjserror');
- };
- this.on('dispose', () => {
- script.onload = null;
- script.onerror = null;
- });
- // but have not loaded yet and we set it to true before the inject so that
- // we don't overwrite the injected window.WebVTT if it loads right away
- window.WebVTT = true;
- this.el().parentNode.appendChild(script);
- } else {
- this.ready(this.addWebVttScript_);
- }
- }
-
- /**
- * Emulate texttracks
- *
- */
- emulateTextTracks() {
- const tracks = this.textTracks();
- const remoteTracks = this.remoteTextTracks();
- const handleAddTrack = e => tracks.addTrack(e.track);
- const handleRemoveTrack = e => tracks.removeTrack(e.track);
- remoteTracks.on('addtrack', handleAddTrack);
- remoteTracks.on('removetrack', handleRemoveTrack);
- this.addWebVttScript_();
- const updateDisplay = () => this.trigger('texttrackchange');
- const textTracksChanges = () => {
- updateDisplay();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- track.removeEventListener('cuechange', updateDisplay);
- if (track.mode === 'showing') {
- track.addEventListener('cuechange', updateDisplay);
- }
- }
- };
- textTracksChanges();
- tracks.addEventListener('change', textTracksChanges);
- tracks.addEventListener('addtrack', textTracksChanges);
- tracks.addEventListener('removetrack', textTracksChanges);
- this.on('dispose', function () {
- remoteTracks.off('addtrack', handleAddTrack);
- remoteTracks.off('removetrack', handleRemoveTrack);
- tracks.removeEventListener('change', textTracksChanges);
- tracks.removeEventListener('addtrack', textTracksChanges);
- tracks.removeEventListener('removetrack', textTracksChanges);
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- track.removeEventListener('cuechange', updateDisplay);
- }
- });
- }
-
- /**
- * Create and returns a remote {@link TextTrack} object.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @return {TextTrack}
- * The TextTrack that gets created.
- */
- addTextTrack(kind, label, language) {
- if (!kind) {
- throw new Error('TextTrack kind is required but was not provided');
- }
- return createTrackHelper(this, kind, label, language);
- }
-
- /**
- * Create an emulated TextTrack for use by addRemoteTextTrack
- *
- * This is intended to be overridden by classes that inherit from
- * Tech in order to create native or custom TextTracks.
- *
- * @param {Object} options
- * The object should contain the options to initialize the TextTrack with.
- *
- * @param {string} [options.kind]
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
- *
- * @param {string} [options.label].
- * Label to identify the text track
- *
- * @param {string} [options.language]
- * Two letter language abbreviation.
- *
- * @return {HTMLTrackElement}
- * The track element that gets created.
- */
- createRemoteTextTrack(options) {
- const track = merge(options, {
- tech: this
- });
- return new REMOTE.remoteTextEl.TrackClass(track);
- }
-
- /**
- * Creates a remote text track object and returns an html track element.
- *
- * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
- *
- * @param {Object} options
- * See {@link Tech#createRemoteTextTrack} for more detailed properties.
- *
- * @param {boolean} [manualCleanup=false]
- * - When false: the TextTrack will be automatically removed from the video
- * element whenever the source changes
- * - When True: The TextTrack will have to be cleaned up manually
- *
- * @return {HTMLTrackElement}
- * An Html Track Element.
- *
- */
- addRemoteTextTrack(options = {}, manualCleanup) {
- const htmlTrackElement = this.createRemoteTextTrack(options);
- if (typeof manualCleanup !== 'boolean') {
- manualCleanup = false;
- }
-
- // store HTMLTrackElement and TextTrack to remote list
- this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
- this.remoteTextTracks().addTrack(htmlTrackElement.track);
- if (manualCleanup === false) {
- // create the TextTrackList if it doesn't exist
- this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
- }
- return htmlTrackElement;
- }
-
- /**
- * Remove a remote text track from the remote `TextTrackList`.
- *
- * @param {TextTrack} track
- * `TextTrack` to remove from the `TextTrackList`
- */
- removeRemoteTextTrack(track) {
- const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
-
- // remove HTMLTrackElement and TextTrack from remote list
- this.remoteTextTrackEls().removeTrackElement_(trackElement);
- this.remoteTextTracks().removeTrack(track);
- this.autoRemoteTextTracks_.removeTrack(track);
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object}
- * An object with supported media playback quality metrics
- *
- * @abstract
- */
- getVideoPlaybackQuality() {
- return {};
- }
-
- /**
- * Attempt to create a floating video window always on top of other windows
- * so that users may continue consuming media while they interact with other
- * content sites, or applications on their device.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @return {Promise|undefined}
- * A promise with a Picture-in-Picture window if the browser supports
- * Promises (or one was passed in as an option). It returns undefined
- * otherwise.
- *
- * @abstract
- */
- requestPictureInPicture() {
- return Promise.reject();
- }
-
- /**
- * A method to check for the value of the 'disablePictureInPicture' property.
- * Defaults to true, as it should be considered disabled if the tech does not support pip
- *
- * @abstract
- */
- disablePictureInPicture() {
- return true;
- }
-
- /**
- * A method to set or unset the 'disablePictureInPicture' property.
- *
- * @abstract
- */
- setDisablePictureInPicture() {}
-
- /**
- * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
- *
- * @param {function} cb
- * @return {number} request id
- */
- requestVideoFrameCallback(cb) {
- const id = newGUID();
- if (!this.isReady_ || this.paused()) {
- this.queuedHanders_.add(id);
- this.one('playing', () => {
- if (this.queuedHanders_.has(id)) {
- this.queuedHanders_.delete(id);
- cb();
- }
- });
- } else {
- this.requestNamedAnimationFrame(id, cb);
- }
- return id;
- }
-
- /**
- * A fallback implementation of cancelVideoFrameCallback
- *
- * @param {number} id id of callback to be cancelled
- */
- cancelVideoFrameCallback(id) {
- if (this.queuedHanders_.has(id)) {
- this.queuedHanders_.delete(id);
- } else {
- this.cancelNamedAnimationFrame(id);
- }
- }
-
- /**
- * A method to set a poster from a `Tech`.
- *
- * @abstract
- */
- setPoster() {}
-
- /**
- * A method to check for the presence of the 'playsinline' attribute.
- *
- * @abstract
- */
- playsinline() {}
-
- /**
- * A method to set or unset the 'playsinline' attribute.
- *
- * @abstract
- */
- setPlaysinline() {}
-
- /**
- * Attempt to force override of native audio tracks.
- *
- * @param {boolean} override - If set to true native audio will be overridden,
- * otherwise native audio will potentially be used.
- *
- * @abstract
- */
- overrideNativeAudioTracks(override) {}
-
- /**
- * Attempt to force override of native video tracks.
- *
- * @param {boolean} override - If set to true native video will be overridden,
- * otherwise native video will potentially be used.
- *
- * @abstract
- */
- overrideNativeVideoTracks(override) {}
-
- /**
- * Check if the tech can support the given mime-type.
- *
- * The base tech does not support any type, but source handlers might
- * overwrite this.
- *
- * @param {string} _type
- * The mimetype to check for support
- *
- * @return {string}
- * 'probably', 'maybe', or empty string
- *
- * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
- *
- * @abstract
- */
- canPlayType(_type) {
- return '';
- }
-
- /**
- * Check if the type is supported by this tech.
- *
- * The base tech does not support any type, but source handlers might
- * overwrite this.
- *
- * @param {string} _type
- * The media type to check
- * @return {string} Returns the native video element's response
- */
- static canPlayType(_type) {
- return '';
- }
-
- /**
- * Check if the tech can support the given source
- *
- * @param {Object} srcObj
- * The source object
- * @param {Object} options
- * The options passed to the tech
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- static canPlaySource(srcObj, options) {
- return Tech.canPlayType(srcObj.type);
- }
-
- /*
- * Return whether the argument is a Tech or not.
- * Can be passed either a Class like `Html5` or a instance like `player.tech_`
- *
- * @param {Object} component
- * The item to check
- *
- * @return {boolean}
- * Whether it is a tech or not
- * - True if it is a tech
- * - False if it is not
- */
- static isTech(component) {
- return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
- }
-
- /**
- * Registers a `Tech` into a shared list for videojs.
- *
- * @param {string} name
- * Name of the `Tech` to register.
- *
- * @param {Object} tech
- * The `Tech` class to register.
- */
- static registerTech(name, tech) {
- if (!Tech.techs_) {
- Tech.techs_ = {};
- }
- if (!Tech.isTech(tech)) {
- throw new Error(`Tech ${name} must be a Tech`);
- }
- if (!Tech.canPlayType) {
- throw new Error('Techs must have a static canPlayType method on them');
- }
- if (!Tech.canPlaySource) {
- throw new Error('Techs must have a static canPlaySource method on them');
- }
- name = toTitleCase(name);
- Tech.techs_[name] = tech;
- Tech.techs_[toLowerCase(name)] = tech;
- if (name !== 'Tech') {
- // camel case the techName for use in techOrder
- Tech.defaultTechOrder_.push(name);
- }
- return tech;
- }
-
- /**
- * Get a `Tech` from the shared list by name.
- *
- * @param {string} name
- * `camelCase` or `TitleCase` name of the Tech to get
- *
- * @return {Tech|undefined}
- * The `Tech` or undefined if there was no tech with the name requested.
- */
- static getTech(name) {
- if (!name) {
- return;
- }
- if (Tech.techs_ && Tech.techs_[name]) {
- return Tech.techs_[name];
- }
- name = toTitleCase(name);
- if (window && window.videojs && window.videojs[name]) {
- log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
- return window.videojs[name];
- }
- }
- }
-
- /**
- * Get the {@link VideoTrackList}
- *
- * @returns {VideoTrackList}
- * @method Tech.prototype.videoTracks
- */
-
- /**
- * Get the {@link AudioTrackList}
- *
- * @returns {AudioTrackList}
- * @method Tech.prototype.audioTracks
- */
-
- /**
- * Get the {@link TextTrackList}
- *
- * @returns {TextTrackList}
- * @method Tech.prototype.textTracks
- */
-
- /**
- * Get the remote element {@link TextTrackList}
- *
- * @returns {TextTrackList}
- * @method Tech.prototype.remoteTextTracks
- */
-
- /**
- * Get the remote element {@link HtmlTrackElementList}
- *
- * @returns {HtmlTrackElementList}
- * @method Tech.prototype.remoteTextTrackEls
- */
-
- ALL.names.forEach(function (name) {
- const props = ALL[name];
- Tech.prototype[props.getterName] = function () {
- this[props.privateName] = this[props.privateName] || new props.ListClass();
- return this[props.privateName];
- };
- });
-
- /**
- * List of associated text tracks
- *
- * @type {TextTrackList}
- * @private
- * @property Tech#textTracks_
- */
-
- /**
- * List of associated audio tracks.
- *
- * @type {AudioTrackList}
- * @private
- * @property Tech#audioTracks_
- */
-
- /**
- * List of associated video tracks.
- *
- * @type {VideoTrackList}
- * @private
- * @property Tech#videoTracks_
- */
-
- /**
- * Boolean indicating whether the `Tech` supports volume control.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresVolumeControl = true;
-
- /**
- * Boolean indicating whether the `Tech` supports muting volume.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresMuteControl = true;
-
- /**
- * Boolean indicating whether the `Tech` supports fullscreen resize control.
- * Resizing plugins using request fullscreen reloads the plugin
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresFullscreenResize = false;
-
- /**
- * Boolean indicating whether the `Tech` supports changing the speed at which the video
- * plays. Examples:
- * - Set player to play 2x (twice) as fast
- * - Set player to play 0.5x (half) as fast
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresPlaybackRate = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `progress` event.
- * This will be used to determine if {@link Tech#manualProgressOn} should be called.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresProgressEvents = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `sourceset` event.
- *
- * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
- * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
- * a new source.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresSourceset = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `timeupdate` event.
- * This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresTimeupdateEvents = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
- * This will help us integrate with native `TextTrack`s if the browser supports them.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresNativeTextTracks = false;
-
- /**
- * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresVideoFrameCallback = false;
-
- /**
- * A functional mixin for techs that want to use the Source Handler pattern.
- * Source handlers are scripts for handling specific formats.
- * The source handler pattern is used for adaptive formats (HLS, DASH) that
- * manually load video data and feed it into a Source Buffer (Media Source Extensions)
- * Example: `Tech.withSourceHandlers.call(MyTech);`
- *
- * @param {Tech} _Tech
- * The tech to add source handler functions to.
- *
- * @mixes Tech~SourceHandlerAdditions
- */
- Tech.withSourceHandlers = function (_Tech) {
- /**
- * Register a source handler
- *
- * @param {Function} handler
- * The source handler class
- *
- * @param {number} [index]
- * Register it at the following index
- */
- _Tech.registerSourceHandler = function (handler, index) {
- let handlers = _Tech.sourceHandlers;
- if (!handlers) {
- handlers = _Tech.sourceHandlers = [];
- }
- if (index === undefined) {
- // add to the end of the list
- index = handlers.length;
- }
- handlers.splice(index, 0, handler);
- };
-
- /**
- * Check if the tech can support the given type. Also checks the
- * Techs sourceHandlers.
- *
- * @param {string} type
- * The mimetype to check.
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- _Tech.canPlayType = function (type) {
- const handlers = _Tech.sourceHandlers || [];
- let can;
- for (let i = 0; i < handlers.length; i++) {
- can = handlers[i].canPlayType(type);
- if (can) {
- return can;
- }
- }
- return '';
- };
-
- /**
- * Returns the first source handler that supports the source.
- *
- * TODO: Answer question: should 'probably' be prioritized over 'maybe'
- *
- * @param {SourceObject} source
- * The source object
- *
- * @param {Object} options
- * The options passed to the tech
- *
- * @return {SourceHandler|null}
- * The first source handler that supports the source or null if
- * no SourceHandler supports the source
- */
- _Tech.selectSourceHandler = function (source, options) {
- const handlers = _Tech.sourceHandlers || [];
- let can;
- for (let i = 0; i < handlers.length; i++) {
- can = handlers[i].canHandleSource(source, options);
- if (can) {
- return handlers[i];
- }
- }
- return null;
- };
-
- /**
- * Check if the tech can support the given source.
- *
- * @param {SourceObject} srcObj
- * The source object
- *
- * @param {Object} options
- * The options passed to the tech
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- _Tech.canPlaySource = function (srcObj, options) {
- const sh = _Tech.selectSourceHandler(srcObj, options);
- if (sh) {
- return sh.canHandleSource(srcObj, options);
- }
- return '';
- };
-
- /**
- * When using a source handler, prefer its implementation of
- * any function normally provided by the tech.
- */
- const deferrable = ['seekable', 'seeking', 'duration'];
-
- /**
- * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
- * function if it exists, with a fallback to the Techs seekable function.
- *
- * @method _Tech.seekable
- */
-
- /**
- * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
- * function if it exists, otherwise it will fallback to the techs duration function.
- *
- * @method _Tech.duration
- */
-
- deferrable.forEach(function (fnName) {
- const originalFn = this[fnName];
- if (typeof originalFn !== 'function') {
- return;
- }
- this[fnName] = function () {
- if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
- return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
- }
- return originalFn.apply(this, arguments);
- };
- }, _Tech.prototype);
-
- /**
- * Create a function for setting the source using a source object
- * and source handlers.
- * Should never be called unless a source handler was found.
- *
- * @param {SourceObject} source
- * A source object with src and type keys
- */
- _Tech.prototype.setSource = function (source) {
- let sh = _Tech.selectSourceHandler(source, this.options_);
- if (!sh) {
- // Fall back to a native source handler when unsupported sources are
- // deliberately set
- if (_Tech.nativeSourceHandler) {
- sh = _Tech.nativeSourceHandler;
- } else {
- log.error('No source handler found for the current source.');
- }
- }
-
- // Dispose any existing source handler
- this.disposeSourceHandler();
- this.off('dispose', this.disposeSourceHandler_);
- if (sh !== _Tech.nativeSourceHandler) {
- this.currentSource_ = source;
- }
- this.sourceHandler_ = sh.handleSource(source, this, this.options_);
- this.one('dispose', this.disposeSourceHandler_);
- };
-
- /**
- * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
- *
- * @listens Tech#dispose
- */
- _Tech.prototype.disposeSourceHandler = function () {
- // if we have a source and get another one
- // then we are loading something new
- // than clear all of our current tracks
- if (this.currentSource_) {
- this.clearTracks(['audio', 'video']);
- this.currentSource_ = null;
- }
-
- // always clean up auto-text tracks
- this.cleanupAutoTextTracks();
- if (this.sourceHandler_) {
- if (this.sourceHandler_.dispose) {
- this.sourceHandler_.dispose();
- }
- this.sourceHandler_ = null;
- }
- };
- };
-
- // The base Tech class needs to be registered as a Component. It is the only
- // Tech that can be registered as a Component.
- Component.registerComponent('Tech', Tech);
- Tech.registerTech('Tech', Tech);
-
- /**
- * A list of techs that should be added to techOrder on Players
- *
- * @private
- */
- Tech.defaultTechOrder_ = [];
-
- /**
- * @file middleware.js
- * @module middleware
- */
- const middlewares = {};
- const middlewareInstances = {};
- const TERMINATOR = {};
-
- /**
- * A middleware object is a plain JavaScript object that has methods that
- * match the {@link Tech} methods found in the lists of allowed
- * {@link module:middleware.allowedGetters|getters},
- * {@link module:middleware.allowedSetters|setters}, and
- * {@link module:middleware.allowedMediators|mediators}.
- *
- * @typedef {Object} MiddlewareObject
- */
-
- /**
- * A middleware factory function that should return a
- * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
- *
- * This factory will be called for each player when needed, with the player
- * passed in as an argument.
- *
- * @callback MiddlewareFactory
- * @param { import('../player').default } player
- * A Video.js player.
- */
-
- /**
- * Define a middleware that the player should use by way of a factory function
- * that returns a middleware object.
- *
- * @param {string} type
- * The MIME type to match or `"*"` for all MIME types.
- *
- * @param {MiddlewareFactory} middleware
- * A middleware factory function that will be executed for
- * matching types.
- */
- function use(type, middleware) {
- middlewares[type] = middlewares[type] || [];
- middlewares[type].push(middleware);
- }
-
- /**
- * Asynchronously sets a source using middleware by recursing through any
- * matching middlewares and calling `setSource` on each, passing along the
- * previous returned value each time.
- *
- * @param { import('../player').default } player
- * A {@link Player} instance.
- *
- * @param {Tech~SourceObject} src
- * A source object.
- *
- * @param {Function}
- * The next middleware to run.
- */
- function setSource(player, src, next) {
- player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
- }
-
- /**
- * When the tech is set, passes the tech to each middleware's `setTech` method.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * A Video.js tech.
- */
- function setTech(middleware, tech) {
- middleware.forEach(mw => mw.setTech && mw.setTech(tech));
- }
-
- /**
- * Calls a getter on the tech first, through each middleware
- * from right to left to the player.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @return {*}
- * The final value from the tech after middleware has intercepted it.
- */
- function get(middleware, tech, method) {
- return middleware.reduceRight(middlewareIterator(method), tech[method]());
- }
-
- /**
- * Takes the argument given to the player and calls the setter method on each
- * middleware from left to right to the tech.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @param {*} arg
- * The value to set on the tech.
- *
- * @return {*}
- * The return value of the `method` of the `tech`.
- */
- function set(middleware, tech, method, arg) {
- return tech[method](middleware.reduce(middlewareIterator(method), arg));
- }
-
- /**
- * Takes the argument given to the player and calls the `call` version of the
- * method on each middleware from left to right.
- *
- * Then, call the passed in method on the tech and return the result unchanged
- * back to the player, through middleware, this time from right to left.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @param {*} arg
- * The value to set on the tech.
- *
- * @return {*}
- * The return value of the `method` of the `tech`, regardless of the
- * return values of middlewares.
- */
- function mediate(middleware, tech, method, arg = null) {
- const callMethod = 'call' + toTitleCase(method);
- const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
- const terminated = middlewareValue === TERMINATOR;
- // deprecated. The `null` return value should instead return TERMINATOR to
- // prevent confusion if a techs method actually returns null.
- const returnValue = terminated ? null : tech[method](middlewareValue);
- executeRight(middleware, method, returnValue, terminated);
- return returnValue;
- }
-
- /**
- * Enumeration of allowed getters where the keys are method names.
- *
- * @type {Object}
- */
- const allowedGetters = {
- buffered: 1,
- currentTime: 1,
- duration: 1,
- muted: 1,
- played: 1,
- paused: 1,
- seekable: 1,
- volume: 1,
- ended: 1
- };
-
- /**
- * Enumeration of allowed setters where the keys are method names.
- *
- * @type {Object}
- */
- const allowedSetters = {
- setCurrentTime: 1,
- setMuted: 1,
- setVolume: 1
- };
-
- /**
- * Enumeration of allowed mediators where the keys are method names.
- *
- * @type {Object}
- */
- const allowedMediators = {
- play: 1,
- pause: 1
- };
- function middlewareIterator(method) {
- return (value, mw) => {
- // if the previous middleware terminated, pass along the termination
- if (value === TERMINATOR) {
- return TERMINATOR;
- }
- if (mw[method]) {
- return mw[method](value);
- }
- return value;
- };
- }
- function executeRight(mws, method, value, terminated) {
- for (let i = mws.length - 1; i >= 0; i--) {
- const mw = mws[i];
- if (mw[method]) {
- mw[method](terminated, value);
- }
- }
- }
-
- /**
- * Clear the middleware cache for a player.
- *
- * @param { import('../player').default } player
- * A {@link Player} instance.
- */
- function clearCacheForPlayer(player) {
- middlewareInstances[player.id()] = null;
- }
-
- /**
- * {
- * [playerId]: [[mwFactory, mwInstance], ...]
- * }
- *
- * @private
- */
- function getOrCreateFactory(player, mwFactory) {
- const mws = middlewareInstances[player.id()];
- let mw = null;
- if (mws === undefined || mws === null) {
- mw = mwFactory(player);
- middlewareInstances[player.id()] = [[mwFactory, mw]];
- return mw;
- }
- for (let i = 0; i < mws.length; i++) {
- const [mwf, mwi] = mws[i];
- if (mwf !== mwFactory) {
- continue;
- }
- mw = mwi;
- }
- if (mw === null) {
- mw = mwFactory(player);
- mws.push([mwFactory, mw]);
- }
- return mw;
- }
- function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
- const [mwFactory, ...mwrest] = middleware;
-
- // if mwFactory is a string, then we're at a fork in the road
- if (typeof mwFactory === 'string') {
- setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
-
- // if we have an mwFactory, call it with the player to get the mw,
- // then call the mw's setSource method
- } else if (mwFactory) {
- const mw = getOrCreateFactory(player, mwFactory);
-
- // if setSource isn't present, implicitly select this middleware
- if (!mw.setSource) {
- acc.push(mw);
- return setSourceHelper(src, mwrest, next, player, acc, lastRun);
- }
- mw.setSource(Object.assign({}, src), function (err, _src) {
- // something happened, try the next middleware on the current level
- // make sure to use the old src
- if (err) {
- return setSourceHelper(src, mwrest, next, player, acc, lastRun);
- }
-
- // we've succeeded, now we need to go deeper
- acc.push(mw);
-
- // if it's the same type, continue down the current chain
- // otherwise, we want to go down the new chain
- setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
- });
- } else if (mwrest.length) {
- setSourceHelper(src, mwrest, next, player, acc, lastRun);
- } else if (lastRun) {
- next(src, acc);
- } else {
- setSourceHelper(src, middlewares['*'], next, player, acc, true);
- }
- }
-
- /**
- * Mimetypes
- *
- * @see https://www.iana.org/assignments/media-types/media-types.xhtml
- * @typedef Mimetypes~Kind
- * @enum
- */
- const MimetypesKind = {
- opus: 'video/ogg',
- ogv: 'video/ogg',
- mp4: 'video/mp4',
- mov: 'video/mp4',
- m4v: 'video/mp4',
- mkv: 'video/x-matroska',
- m4a: 'audio/mp4',
- mp3: 'audio/mpeg',
- aac: 'audio/aac',
- caf: 'audio/x-caf',
- flac: 'audio/flac',
- oga: 'audio/ogg',
- wav: 'audio/wav',
- m3u8: 'application/x-mpegURL',
- mpd: 'application/dash+xml',
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- gif: 'image/gif',
- png: 'image/png',
- svg: 'image/svg+xml',
- webp: 'image/webp'
- };
-
- /**
- * Get the mimetype of a given src url if possible
- *
- * @param {string} src
- * The url to the src
- *
- * @return {string}
- * return the mimetype if it was known or empty string otherwise
- */
- const getMimetype = function (src = '') {
- const ext = getFileExtension(src);
- const mimetype = MimetypesKind[ext.toLowerCase()];
- return mimetype || '';
- };
-
- /**
- * Find the mime type of a given source string if possible. Uses the player
- * source cache.
- *
- * @param { import('../player').default } player
- * The player object
- *
- * @param {string} src
- * The source string
- *
- * @return {string}
- * The type that was found
- */
- const findMimetype = (player, src) => {
- if (!src) {
- return '';
- }
-
- // 1. check for the type in the `source` cache
- if (player.cache_.source.src === src && player.cache_.source.type) {
- return player.cache_.source.type;
- }
-
- // 2. see if we have this source in our `currentSources` cache
- const matchingSources = player.cache_.sources.filter(s => s.src === src);
- if (matchingSources.length) {
- return matchingSources[0].type;
- }
-
- // 3. look for the src url in source elements and use the type there
- const sources = player.$$('source');
- for (let i = 0; i < sources.length; i++) {
- const s = sources[i];
- if (s.type && s.src && s.src === src) {
- return s.type;
- }
- }
-
- // 4. finally fallback to our list of mime types based on src url extension
- return getMimetype(src);
- };
-
- /**
- * @module filter-source
- */
-
- /**
- * Filter out single bad source objects or multiple source objects in an
- * array. Also flattens nested source object arrays into a 1 dimensional
- * array of source objects.
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]} src
- * The src object to filter
- *
- * @return {Tech~SourceObject[]}
- * An array of sourceobjects containing only valid sources
- *
- * @private
- */
- const filterSource = function (src) {
- // traverse array
- if (Array.isArray(src)) {
- let newsrc = [];
- src.forEach(function (srcobj) {
- srcobj = filterSource(srcobj);
- if (Array.isArray(srcobj)) {
- newsrc = newsrc.concat(srcobj);
- } else if (isObject(srcobj)) {
- newsrc.push(srcobj);
- }
- });
- src = newsrc;
- } else if (typeof src === 'string' && src.trim()) {
- // convert string into object
- src = [fixSource({
- src
- })];
- } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
- // src is already valid
- src = [fixSource(src)];
- } else {
- // invalid source, turn it into an empty array
- src = [];
- }
- return src;
- };
-
- /**
- * Checks src mimetype, adding it when possible
- *
- * @param {Tech~SourceObject} src
- * The src object to check
- * @return {Tech~SourceObject}
- * src Object with known type
- */
- function fixSource(src) {
- if (!src.type) {
- const mimetype = getMimetype(src.src);
- if (mimetype) {
- src.type = mimetype;
- }
- }
- return src;
- }
-
- var icons = "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ";
-
- /**
- * @file loader.js
- */
-
- /**
- * The `MediaLoader` is the `Component` that decides which playback technology to load
- * when a player is initialized.
- *
- * @extends Component
- */
- class MediaLoader extends Component {
- /**
- * Create an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should attach to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function that is run when this component is ready.
- */
- constructor(player, options, ready) {
- // MediaLoader has no element
- const options_ = merge({
- createEl: false
- }, options);
- super(player, options_, ready);
-
- // If there are no sources when the player is initialized,
- // load the first supported playback technology.
-
- if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
- for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
- const techName = toTitleCase(j[i]);
- let tech = Tech.getTech(techName);
-
- // Support old behavior of techs being registered as components.
- // Remove once that deprecated behavior is removed.
- if (!techName) {
- tech = Component.getComponent(techName);
- }
-
- // Check if the browser supports this technology
- if (tech && tech.isSupported()) {
- player.loadTech_(techName);
- break;
- }
- }
- } else {
- // Loop through playback technologies (e.g. HTML5) and check for support.
- // Then load the best source.
- // A few assumptions here:
- // All playback technologies respect preload false.
- player.src(options.playerOptions.sources);
- }
- }
- }
- Component.registerComponent('MediaLoader', MediaLoader);
-
- /**
- * @file clickable-component.js
- */
-
- /**
- * Component which is clickable or keyboard actionable, but is not a
- * native HTML button.
- *
- * @extends Component
- */
- class ClickableComponent extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of component options.
- *
- * @param {function} [options.clickHandler]
- * The function to call when the button is clicked / activated
- *
- * @param {string} [options.controlText]
- * The text to set on the button
- *
- * @param {string} [options.className]
- * A class or space separated list of classes to add the component
- *
- */
- constructor(player, options) {
- super(player, options);
- if (this.options_.controlText) {
- this.controlText(this.options_.controlText);
- }
- this.handleMouseOver_ = e => this.handleMouseOver(e);
- this.handleMouseOut_ = e => this.handleMouseOut(e);
- this.handleClick_ = e => this.handleClick(e);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.emitTapEvents();
- this.enable();
- }
-
- /**
- * Create the `ClickableComponent`s DOM element.
- *
- * @param {string} [tag=div]
- * The element's node type.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element.
- *
- * @param {Object} [attributes={}]
- * An object of attributes that should be set on the element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tag = 'div', props = {}, attributes = {}) {
- props = Object.assign({
- className: this.buildCSSClass(),
- tabIndex: 0
- }, props);
- if (tag === 'button') {
- log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
- }
-
- // Add ARIA attributes for clickable element which is not a native HTML button
- attributes = Object.assign({
- role: 'button'
- }, attributes);
- this.tabIndex_ = props.tabIndex;
- const el = createEl(tag, props, attributes);
- if (!this.player_.options_.experimentalSvgIcons) {
- el.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- this.createControlTextEl(el);
- return el;
- }
- dispose() {
- // remove controlTextEl_ on dispose
- this.controlTextEl_ = null;
- super.dispose();
- }
-
- /**
- * Create a control text element on this `ClickableComponent`
- *
- * @param {Element} [el]
- * Parent element for the control text.
- *
- * @return {Element}
- * The control text element that gets created.
- */
- createControlTextEl(el) {
- this.controlTextEl_ = createEl('span', {
- className: 'vjs-control-text'
- }, {
- // let the screen reader user know that the text of the element may change
- 'aria-live': 'polite'
- });
- if (el) {
- el.appendChild(this.controlTextEl_);
- }
- this.controlText(this.controlText_, el);
- return this.controlTextEl_;
- }
-
- /**
- * Get or set the localize text to use for the controls on the `ClickableComponent`.
- *
- * @param {string} [text]
- * Control text for element.
- *
- * @param {Element} [el=this.el()]
- * Element to set the title on.
- *
- * @return {string}
- * - The control text when getting
- */
- controlText(text, el = this.el()) {
- if (text === undefined) {
- return this.controlText_ || 'Need Text';
- }
- const localizedText = this.localize(text);
-
- /** @protected */
- this.controlText_ = text;
- textContent(this.controlTextEl_, localizedText);
- if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
- // Set title attribute if only an icon is shown
- el.setAttribute('title', localizedText);
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-control vjs-button ${super.buildCSSClass()}`;
- }
-
- /**
- * Enable this `ClickableComponent`
- */
- enable() {
- if (!this.enabled_) {
- this.enabled_ = true;
- this.removeClass('vjs-disabled');
- this.el_.setAttribute('aria-disabled', 'false');
- if (typeof this.tabIndex_ !== 'undefined') {
- this.el_.setAttribute('tabIndex', this.tabIndex_);
- }
- this.on(['tap', 'click'], this.handleClick_);
- this.on('keydown', this.handleKeyDown_);
- }
- }
-
- /**
- * Disable this `ClickableComponent`
- */
- disable() {
- this.enabled_ = false;
- this.addClass('vjs-disabled');
- this.el_.setAttribute('aria-disabled', 'true');
- if (typeof this.tabIndex_ !== 'undefined') {
- this.el_.removeAttribute('tabIndex');
- }
- this.off('mouseover', this.handleMouseOver_);
- this.off('mouseout', this.handleMouseOut_);
- this.off(['tap', 'click'], this.handleClick_);
- this.off('keydown', this.handleKeyDown_);
- }
-
- /**
- * Handles language change in ClickableComponent for the player in components
- *
- *
- */
- handleLanguagechange() {
- this.controlText(this.controlText_);
- }
-
- /**
- * Event handler that is called when a `ClickableComponent` receives a
- * `click` or `tap` event.
- *
- * @param {Event} event
- * The `tap` or `click` event that caused this function to be called.
- *
- * @listens tap
- * @listens click
- * @abstract
- */
- handleClick(event) {
- if (this.options_.clickHandler) {
- this.options_.clickHandler.call(this, arguments);
- }
- }
-
- /**
- * Event handler that is called when a `ClickableComponent` receives a
- * `keydown` event.
- *
- * By default, if the key is Space or Enter, it will trigger a `click` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Support Space or Enter key operation to fire a click event. Also,
- // prevent the event from propagating through the DOM and triggering
- // Player hotkeys.
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.preventDefault();
- event.stopPropagation();
- this.trigger('click');
- } else {
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- }
- Component.registerComponent('ClickableComponent', ClickableComponent);
-
- /**
- * @file poster-image.js
- */
-
- /**
- * A `ClickableComponent` that handles showing the poster image for the player.
- *
- * @extends ClickableComponent
- */
- class PosterImage extends ClickableComponent {
- /**
- * Create an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should attach to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update();
- this.update_ = e => this.update(e);
- player.on('posterchange', this.update_);
- }
-
- /**
- * Clean up and dispose of the `PosterImage`.
- */
- dispose() {
- this.player().off('posterchange', this.update_);
- super.dispose();
- }
-
- /**
- * Create the `PosterImage`s DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- // The el is an empty div to keep position in the DOM
- // A picture and img el will be inserted when a source is set
- return createEl('div', {
- className: 'vjs-poster'
- });
- }
-
- /**
- * Get or set the `PosterImage`'s crossOrigin option.
- *
- * @param {string|null} [value]
- * The value to set the crossOrigin to. If an argument is
- * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
- *
- * @return {string|null}
- * - The current crossOrigin value of the `Player` when getting.
- * - undefined when setting
- */
- crossOrigin(value) {
- // `null` can be set to unset a value
- if (typeof value === 'undefined') {
- if (this.$('img')) {
- // If the poster's element exists, give its value
- return this.$('img').crossOrigin;
- } else if (this.player_.tech_ && this.player_.tech_.isReady_) {
- // If not but the tech is ready, query the tech
- return this.player_.crossOrigin();
- }
- // Otherwise check options as the poster is usually set before the state of crossorigin
- // can be retrieved by the getter
- return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
- }
- if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
- this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
- return;
- }
- if (this.$('img')) {
- this.$('img').crossOrigin = value;
- }
- return;
- }
-
- /**
- * An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
- *
- * @listens Player#posterchange
- *
- * @param {Event} [event]
- * The `Player#posterchange` event that triggered this function.
- */
- update(event) {
- const url = this.player().poster();
- this.setSrc(url);
-
- // If there's no poster source we should display:none on this component
- // so it's not still clickable or right-clickable
- if (url) {
- this.show();
- } else {
- this.hide();
- }
- }
-
- /**
- * Set the source of the `PosterImage` depending on the display method. (Re)creates
- * the inner picture and img elementss when needed.
- *
- * @param {string} [url]
- * The URL to the source for the `PosterImage`. If not specified or falsy,
- * any source and ant inner picture/img are removed.
- */
- setSrc(url) {
- if (!url) {
- this.el_.textContent = '';
- return;
- }
- if (!this.$('img')) {
- this.el_.appendChild(createEl('picture', {
- className: 'vjs-poster',
- // Don't want poster to be tabbable.
- tabIndex: -1
- }, {}, createEl('img', {
- loading: 'lazy',
- crossOrigin: this.crossOrigin()
- }, {
- alt: ''
- })));
- }
- this.$('img').src = url;
- }
-
- /**
- * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
- * {@link ClickableComponent#handleClick} for instances where this will be triggered.
- *
- * @listens tap
- * @listens click
- * @listens keydown
- *
- * @param {Event} event
- + The `click`, `tap` or `keydown` event that caused this function to be called.
- */
- handleClick(event) {
- // We don't want a click to trigger playback when controls are disabled
- if (!this.player_.controls()) {
- return;
- }
- if (this.player_.tech(true)) {
- this.player_.tech(true).focus();
- }
- if (this.player_.paused()) {
- silencePromise(this.player_.play());
- } else {
- this.player_.pause();
- }
- }
- }
-
- /**
- * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the ` ` tag to control the CORS
- * behavior.
- *
- * @param {string|null} [value]
- * The value to set the `PosterImages`'s crossorigin to. If an argument is
- * given, must be one of `anonymous` or `use-credentials`.
- *
- * @return {string|null|undefined}
- * - The current crossorigin value of the `Player` when getting.
- * - undefined when setting
- */
- PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
- Component.registerComponent('PosterImage', PosterImage);
-
- /**
- * @file text-track-display.js
- */
- const darkGray = '#222';
- const lightGray = '#ccc';
- const fontMap = {
- monospace: 'monospace',
- sansSerif: 'sans-serif',
- serif: 'serif',
- monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
- monospaceSerif: '"Courier New", monospace',
- proportionalSansSerif: 'sans-serif',
- proportionalSerif: 'serif',
- casual: '"Comic Sans MS", Impact, fantasy',
- script: '"Monotype Corsiva", cursive',
- smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
- };
-
- /**
- * Construct an rgba color from a given hex color code.
- *
- * @param {number} color
- * Hex number for color, like #f0e or #f604e2.
- *
- * @param {number} opacity
- * Value for opacity, 0.0 - 1.0.
- *
- * @return {string}
- * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
- */
- function constructColor(color, opacity) {
- let hex;
- if (color.length === 4) {
- // color looks like "#f0e"
- hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
- } else if (color.length === 7) {
- // color looks like "#f604e2"
- hex = color.slice(1);
- } else {
- throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
- }
- return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
- }
-
- /**
- * Try to update the style of a DOM element. Some style changes will throw an error,
- * particularly in IE8. Those should be noops.
- *
- * @param {Element} el
- * The DOM element to be styled.
- *
- * @param {string} style
- * The CSS property on the element that should be styled.
- *
- * @param {string} rule
- * The style rule that should be applied to the property.
- *
- * @private
- */
- function tryUpdateStyle(el, style, rule) {
- try {
- el.style[style] = rule;
- } catch (e) {
- // Satisfies linter.
- return;
- }
- }
-
- /**
- * Converts the CSS top/right/bottom/left property numeric value to string in pixels.
- *
- * @param {number} position
- * The CSS top/right/bottom/left property value.
- *
- * @return {string}
- * The CSS property value that was created, like '10px'.
- *
- * @private
- */
- function getCSSPositionValue(position) {
- return position ? `${position}px` : '';
- }
-
- /**
- * The component for displaying text track cues.
- *
- * @extends Component
- */
- class TextTrackDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when `TextTrackDisplay` is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- const updateDisplayTextHandler = e => this.updateDisplay(e);
- const updateDisplayHandler = e => {
- this.updateDisplayOverlay();
- this.updateDisplay(e);
- };
- player.on('loadstart', e => this.toggleDisplay(e));
- player.on('texttrackchange', updateDisplayTextHandler);
- player.on('loadedmetadata', e => {
- this.updateDisplayOverlay();
- this.preselectTrack(e);
- });
-
- // This used to be called during player init, but was causing an error
- // if a track should show by default and the display hadn't loaded yet.
- // Should probably be moved to an external track loader when we support
- // tracks that don't need a display.
- player.ready(bind_(this, function () {
- if (player.tech_ && player.tech_.featuresNativeTextTracks) {
- this.hide();
- return;
- }
- player.on('fullscreenchange', updateDisplayHandler);
- player.on('playerresize', updateDisplayHandler);
- const screenOrientation = window.screen.orientation || window;
- const changeOrientationEvent = window.screen.orientation ? 'change' : 'orientationchange';
- screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
- player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
- const tracks = this.options_.playerOptions.tracks || [];
- for (let i = 0; i < tracks.length; i++) {
- this.player_.addRemoteTextTrack(tracks[i], true);
- }
- this.preselectTrack();
- }));
- }
-
- /**
- * Preselect a track following this precedence:
- * - matches the previously selected {@link TextTrack}'s language and kind
- * - matches the previously selected {@link TextTrack}'s language only
- * - is the first default captions track
- * - is the first default descriptions track
- *
- * @listens Player#loadstart
- */
- preselectTrack() {
- const modes = {
- captions: 1,
- subtitles: 1
- };
- const trackList = this.player_.textTracks();
- const userPref = this.player_.cache_.selectedLanguage;
- let firstDesc;
- let firstCaptions;
- let preferredTrack;
- for (let i = 0; i < trackList.length; i++) {
- const track = trackList[i];
- if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
- // Always choose the track that matches both language and kind
- if (track.kind === userPref.kind) {
- preferredTrack = track;
- // or choose the first track that matches language
- } else if (!preferredTrack) {
- preferredTrack = track;
- }
-
- // clear everything if offTextTrackMenuItem was clicked
- } else if (userPref && !userPref.enabled) {
- preferredTrack = null;
- firstDesc = null;
- firstCaptions = null;
- } else if (track.default) {
- if (track.kind === 'descriptions' && !firstDesc) {
- firstDesc = track;
- } else if (track.kind in modes && !firstCaptions) {
- firstCaptions = track;
- }
- }
- }
-
- // The preferredTrack matches the user preference and takes
- // precedence over all the other tracks.
- // So, display the preferredTrack before the first default track
- // and the subtitles/captions track before the descriptions track
- if (preferredTrack) {
- preferredTrack.mode = 'showing';
- } else if (firstCaptions) {
- firstCaptions.mode = 'showing';
- } else if (firstDesc) {
- firstDesc.mode = 'showing';
- }
- }
-
- /**
- * Turn display of {@link TextTrack}'s from the current state into the other state.
- * There are only two states:
- * - 'shown'
- * - 'hidden'
- *
- * @listens Player#loadstart
- */
- toggleDisplay() {
- if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
- this.hide();
- } else {
- this.show();
- }
- }
-
- /**
- * Create the {@link Component}'s DOM element.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-text-track-display'
- }, {
- 'translate': 'yes',
- 'aria-live': 'off',
- 'aria-atomic': 'true'
- });
- }
-
- /**
- * Clear all displayed {@link TextTrack}s.
- */
- clearDisplay() {
- if (typeof window.WebVTT === 'function') {
- window.WebVTT.processCues(window, [], this.el_);
- }
- }
-
- /**
- * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
- * a {@link Player#fullscreenchange} is fired.
- *
- * @listens Player#texttrackchange
- * @listens Player#fullscreenchange
- */
- updateDisplay() {
- const tracks = this.player_.textTracks();
- const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
- this.clearDisplay();
- if (allowMultipleShowingTracks) {
- const showingTracks = [];
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- if (track.mode !== 'showing') {
- continue;
- }
- showingTracks.push(track);
- }
- this.updateForTrack(showingTracks);
- return;
- }
-
- // Track display prioritization model: if multiple tracks are 'showing',
- // display the first 'subtitles' or 'captions' track which is 'showing',
- // otherwise display the first 'descriptions' track which is 'showing'
-
- let descriptionsTrack = null;
- let captionsSubtitlesTrack = null;
- let i = tracks.length;
- while (i--) {
- const track = tracks[i];
- if (track.mode === 'showing') {
- if (track.kind === 'descriptions') {
- descriptionsTrack = track;
- } else {
- captionsSubtitlesTrack = track;
- }
- }
- }
- if (captionsSubtitlesTrack) {
- if (this.getAttribute('aria-live') !== 'off') {
- this.setAttribute('aria-live', 'off');
- }
- this.updateForTrack(captionsSubtitlesTrack);
- } else if (descriptionsTrack) {
- if (this.getAttribute('aria-live') !== 'assertive') {
- this.setAttribute('aria-live', 'assertive');
- }
- this.updateForTrack(descriptionsTrack);
- }
- }
-
- /**
- * Updates the displayed TextTrack to be sure it overlays the video when a either
- * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired.
- */
- updateDisplayOverlay() {
- // inset-inline and inset-block are not supprted on old chrome, but these are
- // only likely to be used on TV devices
- if (!this.player_.videoHeight() || !window.CSS.supports('inset-inline: 10px')) {
- return;
- }
- const playerWidth = this.player_.currentWidth();
- const playerHeight = this.player_.currentHeight();
- const playerAspectRatio = playerWidth / playerHeight;
- const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight();
- let insetInlineMatch = 0;
- let insetBlockMatch = 0;
- if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) {
- if (playerAspectRatio > videoAspectRatio) {
- insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2);
- } else {
- insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2);
- }
- }
- tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch));
- tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch));
- }
-
- /**
- * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
- *
- * @param {TextTrack} track
- * Text track object containing active cues to style.
- */
- updateDisplayState(track) {
- const overrides = this.player_.textTrackSettings.getValues();
- const cues = track.activeCues;
- let i = cues.length;
- while (i--) {
- const cue = cues[i];
- if (!cue) {
- continue;
- }
- const cueDiv = cue.displayState;
- if (overrides.color) {
- cueDiv.firstChild.style.color = overrides.color;
- }
- if (overrides.textOpacity) {
- tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
- }
- if (overrides.backgroundColor) {
- cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
- }
- if (overrides.backgroundOpacity) {
- tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
- }
- if (overrides.windowColor) {
- if (overrides.windowOpacity) {
- tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
- } else {
- cueDiv.style.backgroundColor = overrides.windowColor;
- }
- }
- if (overrides.edgeStyle) {
- if (overrides.edgeStyle === 'dropshadow') {
- cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
- } else if (overrides.edgeStyle === 'raised') {
- cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
- } else if (overrides.edgeStyle === 'depressed') {
- cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
- } else if (overrides.edgeStyle === 'uniform') {
- cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
- }
- }
- if (overrides.fontPercent && overrides.fontPercent !== 1) {
- const fontSize = window.parseFloat(cueDiv.style.fontSize);
- cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
- cueDiv.style.height = 'auto';
- cueDiv.style.top = 'auto';
- }
- if (overrides.fontFamily && overrides.fontFamily !== 'default') {
- if (overrides.fontFamily === 'small-caps') {
- cueDiv.firstChild.style.fontVariant = 'small-caps';
- } else {
- cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
- }
- }
- }
- }
-
- /**
- * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
- *
- * @param {TextTrack|TextTrack[]} tracks
- * Text track object or text track array to be added to the list.
- */
- updateForTrack(tracks) {
- if (!Array.isArray(tracks)) {
- tracks = [tracks];
- }
- if (typeof window.WebVTT !== 'function' || tracks.every(track => {
- return !track.activeCues;
- })) {
- return;
- }
- const cues = [];
-
- // push all active track cues
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- for (let j = 0; j < track.activeCues.length; ++j) {
- cues.push(track.activeCues[j]);
- }
- }
-
- // removes all cues before it processes new ones
- window.WebVTT.processCues(window, cues, this.el_);
-
- // add unique class to each language text track & add settings styling if necessary
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- for (let j = 0; j < track.activeCues.length; ++j) {
- const cueEl = track.activeCues[j].displayState;
- addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
- if (track.language) {
- setAttribute(cueEl, 'lang', track.language);
- }
- }
- if (this.player_.textTrackSettings) {
- this.updateDisplayState(track);
- }
- }
- }
- }
- Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
-
- /**
- * @file loading-spinner.js
- */
-
- /**
- * A loading spinner for use during waiting/loading events.
- *
- * @extends Component
- */
- class LoadingSpinner extends Component {
- /**
- * Create the `LoadingSpinner`s DOM element.
- *
- * @return {Element}
- * The dom element that gets created.
- */
- createEl() {
- const isAudio = this.player_.isAudio();
- const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
- const controlText = createEl('span', {
- className: 'vjs-control-text',
- textContent: this.localize('{1} is loading.', [playerType])
- });
- const el = super.createEl('div', {
- className: 'vjs-loading-spinner',
- dir: 'ltr'
- });
- el.appendChild(controlText);
- return el;
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
- }
- }
- Component.registerComponent('LoadingSpinner', LoadingSpinner);
-
- /**
- * @file button.js
- */
-
- /**
- * Base class for all buttons.
- *
- * @extends ClickableComponent
- */
- class Button extends ClickableComponent {
- /**
- * Create the `Button`s DOM element.
- *
- * @param {string} [tag="button"]
- * The element's node type. This argument is IGNORED: no matter what
- * is passed, it will always create a `button` element.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element.
- *
- * @param {Object} [attributes={}]
- * An object of attributes that should be set on the element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tag, props = {}, attributes = {}) {
- tag = 'button';
- props = Object.assign({
- className: this.buildCSSClass()
- }, props);
-
- // Add attributes for button element
- attributes = Object.assign({
- // Necessary since the default button type is "submit"
- type: 'button'
- }, attributes);
- const el = createEl(tag, props, attributes);
- if (!this.player_.options_.experimentalSvgIcons) {
- el.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- this.createControlTextEl(el);
- return el;
- }
-
- /**
- * Add a child `Component` inside of this `Button`.
- *
- * @param {string|Component} child
- * The name or instance of a child to add.
- *
- * @param {Object} [options={}]
- * The key/value store of options that will get passed to children of
- * the child.
- *
- * @return {Component}
- * The `Component` that gets added as a child. When using a string the
- * `Component` will get created by this process.
- *
- * @deprecated since version 5
- */
- addChild(child, options = {}) {
- const className = this.constructor.name;
- log.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
-
- // Avoid the error message generated by ClickableComponent's addChild method
- return Component.prototype.addChild.call(this, child, options);
- }
-
- /**
- * Enable the `Button` element so that it can be activated or clicked. Use this with
- * {@link Button#disable}.
- */
- enable() {
- super.enable();
- this.el_.removeAttribute('disabled');
- }
-
- /**
- * Disable the `Button` element so that it cannot be activated or clicked. Use this with
- * {@link Button#enable}.
- */
- disable() {
- super.disable();
- this.el_.setAttribute('disabled', 'disabled');
- }
-
- /**
- * This gets called when a `Button` has focus and `keydown` is triggered via a key
- * press.
- *
- * @param {KeyboardEvent} event
- * The event that caused this function to get called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Ignore Space or Enter key operation, which is handled by the browser for
- // a button - though not for its super class, ClickableComponent. Also,
- // prevent the event from propagating through the DOM and triggering Player
- // hotkeys. We do not preventDefault here because we _want_ the browser to
- // handle it.
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.stopPropagation();
- return;
- }
-
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- Component.registerComponent('Button', Button);
-
- /**
- * @file big-play-button.js
- */
-
- /**
- * The initial play button that shows before the video has played. The hiding of the
- * `BigPlayButton` get done via CSS and `Player` states.
- *
- * @extends Button
- */
- class BigPlayButton extends Button {
- constructor(player, options) {
- super(player, options);
- this.mouseused_ = false;
- this.setIcon('play');
- this.on('mousedown', e => this.handleMouseDown(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object. Always returns 'vjs-big-play-button'.
- */
- buildCSSClass() {
- return 'vjs-big-play-button';
- }
-
- /**
- * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
- * for more detailed information on what a click can be.
- *
- * @param {KeyboardEvent|MouseEvent|TouchEvent} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const playPromise = this.player_.play();
-
- // exit early if clicked via the mouse
- if (this.mouseused_ && 'clientX' in event && 'clientY' in event) {
- silencePromise(playPromise);
- if (this.player_.tech(true)) {
- this.player_.tech(true).focus();
- }
- return;
- }
- const cb = this.player_.getChild('controlBar');
- const playToggle = cb && cb.getChild('playToggle');
- if (!playToggle) {
- this.player_.tech(true).focus();
- return;
- }
- const playFocus = () => playToggle.focus();
- if (isPromise(playPromise)) {
- playPromise.then(playFocus, () => {});
- } else {
- this.setTimeout(playFocus, 1);
- }
- }
-
- /**
- * Event handler that is called when a `BigPlayButton` receives a
- * `keydown` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- this.mouseused_ = false;
- super.handleKeyDown(event);
- }
-
- /**
- * Handle `mousedown` events on the `BigPlayButton`.
- *
- * @param {MouseEvent} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- this.mouseused_ = true;
- }
- }
-
- /**
- * The text that should display over the `BigPlayButton`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- */
- BigPlayButton.prototype.controlText_ = 'Play Video';
- Component.registerComponent('BigPlayButton', BigPlayButton);
-
- /**
- * @file close-button.js
- */
-
- /**
- * The `CloseButton` is a `{@link Button}` that fires a `close` event when
- * it gets clicked.
- *
- * @extends Button
- */
- class CloseButton extends Button {
- /**
- * Creates an instance of the this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('cancel');
- this.controlText(options && options.controlText || this.localize('Close'));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-close-button ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when a `CloseButton` gets clicked. See
- * {@link ClickableComponent#handleClick} for more information on when
- * this will be triggered
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- * @fires CloseButton#close
- */
- handleClick(event) {
- /**
- * Triggered when the a `CloseButton` is clicked.
- *
- * @event CloseButton#close
- * @type {Event}
- *
- * @property {boolean} [bubbles=false]
- * set to false so that the close event does not
- * bubble up to parents if there is no listener
- */
- this.trigger({
- type: 'close',
- bubbles: false
- });
- }
- /**
- * Event handler that is called when a `CloseButton` receives a
- * `keydown` event.
- *
- * By default, if the key is Esc, it will trigger a `click` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Esc button will trigger `click` event
- if (keycode.isEventKey(event, 'Esc')) {
- event.preventDefault();
- event.stopPropagation();
- this.trigger('click');
- } else {
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- }
- Component.registerComponent('CloseButton', CloseButton);
-
- /**
- * @file play-toggle.js
- */
-
- /**
- * Button to toggle between play and pause.
- *
- * @extends Button
- */
- class PlayToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- super(player, options);
-
- // show or hide replay icon
- options.replay = options.replay === undefined || options.replay;
- this.setIcon('play');
- this.on(player, 'play', e => this.handlePlay(e));
- this.on(player, 'pause', e => this.handlePause(e));
- if (options.replay) {
- this.on(player, 'ended', e => this.handleEnded(e));
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-play-control ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when an `PlayToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (this.player_.paused()) {
- silencePromise(this.player_.play());
- } else {
- this.player_.pause();
- }
- }
-
- /**
- * This gets called once after the video has ended and the user seeks so that
- * we can change the replay button back to a play button.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#seeked
- */
- handleSeeked(event) {
- this.removeClass('vjs-ended');
- if (this.player_.paused()) {
- this.handlePause(event);
- } else {
- this.handlePlay(event);
- }
- }
-
- /**
- * Add the vjs-playing class to the element so it can change appearance.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#play
- */
- handlePlay(event) {
- this.removeClass('vjs-ended', 'vjs-paused');
- this.addClass('vjs-playing');
- // change the button text to "Pause"
- this.setIcon('pause');
- this.controlText('Pause');
- }
-
- /**
- * Add the vjs-paused class to the element so it can change appearance.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#pause
- */
- handlePause(event) {
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- // change the button text to "Play"
- this.setIcon('play');
- this.controlText('Play');
- }
-
- /**
- * Add the vjs-ended class to the element so it can change appearance
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#ended
- */
- handleEnded(event) {
- this.removeClass('vjs-playing');
- this.addClass('vjs-ended');
- // change the button text to "Replay"
- this.setIcon('replay');
- this.controlText('Replay');
-
- // on the next seek remove the replay button
- this.one(this.player_, 'seeked', e => this.handleSeeked(e));
- }
- }
-
- /**
- * The text that should display over the `PlayToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- PlayToggle.prototype.controlText_ = 'Play';
- Component.registerComponent('PlayToggle', PlayToggle);
-
- /**
- * @file time-display.js
- */
-
- /**
- * Displays time information about the video
- *
- * @extends Component
- */
- class TimeDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e));
- this.updateTextNode_();
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const className = this.buildCSSClass();
- const el = super.createEl('div', {
- className: `${className} vjs-time-control vjs-control`
- });
- const span = createEl('span', {
- className: 'vjs-control-text',
- textContent: `${this.localize(this.labelText_)}\u00a0`
- }, {
- role: 'presentation'
- });
- el.appendChild(span);
- this.contentEl_ = createEl('span', {
- className: `${className}-display`
- }, {
- // span elements have no implicit role, but some screen readers (notably VoiceOver)
- // treat them as a break between items in the DOM when using arrow keys
- // (or left-to-right swipes on iOS) to read contents of a page. Using
- // role='presentation' causes VoiceOver to NOT treat this span as a break.
- role: 'presentation'
- });
- el.appendChild(this.contentEl_);
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- this.textNode_ = null;
- super.dispose();
- }
-
- /**
- * Updates the displayed time according to the `updateContent` function which is defined in the child class.
- *
- * @param {Event} [event]
- * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called.
- */
- update(event) {
- if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') {
- return;
- }
- this.updateContent(event);
- }
-
- /**
- * Updates the time display text node with a new time
- *
- * @param {number} [time=0] the time to update to
- *
- * @private
- */
- updateTextNode_(time = 0) {
- time = formatTime(time);
- if (this.formattedTime_ === time) {
- return;
- }
- this.formattedTime_ = time;
- this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
- if (!this.contentEl_) {
- return;
- }
- let oldNode = this.textNode_;
- if (oldNode && this.contentEl_.firstChild !== oldNode) {
- oldNode = null;
- log.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.');
- }
- this.textNode_ = document.createTextNode(this.formattedTime_);
- if (!this.textNode_) {
- return;
- }
- if (oldNode) {
- this.contentEl_.replaceChild(this.textNode_, oldNode);
- } else {
- this.contentEl_.appendChild(this.textNode_);
- }
- });
- }
-
- /**
- * To be filled out in the child class, should update the displayed time
- * in accordance with the fact that the current time has changed.
- *
- * @param {Event} [event]
- * The `timeupdate` event that caused this to run.
- *
- * @listens Player#timeupdate
- */
- updateContent(event) {}
- }
-
- /**
- * The text that is added to the `TimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- TimeDisplay.prototype.labelText_ = 'Time';
-
- /**
- * The text that should display over the `TimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- TimeDisplay.prototype.controlText_ = 'Time';
- Component.registerComponent('TimeDisplay', TimeDisplay);
-
- /**
- * @file current-time-display.js
- */
-
- /**
- * Displays the current time
- *
- * @extends Component
- */
- class CurrentTimeDisplay extends TimeDisplay {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-current-time';
- }
-
- /**
- * Update current time display
- *
- * @param {Event} [event]
- * The `timeupdate` event that caused this function to run.
- *
- * @listens Player#timeupdate
- */
- updateContent(event) {
- // Allows for smooth scrubbing, when player can't keep up.
- let time;
- if (this.player_.ended()) {
- time = this.player_.duration();
- } else {
- time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- }
- this.updateTextNode_(time);
- }
- }
-
- /**
- * The text that is added to the `CurrentTimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
-
- /**
- * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
- Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
-
- /**
- * @file duration-display.js
- */
-
- /**
- * Displays the duration
- *
- * @extends Component
- */
- class DurationDisplay extends TimeDisplay {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- const updateContent = e => this.updateContent(e);
-
- // we do not want to/need to throttle duration changes,
- // as they should always display the changed duration as
- // it has changed
- this.on(player, 'durationchange', updateContent);
-
- // Listen to loadstart because the player duration is reset when a new media element is loaded,
- // but the durationchange on the user agent will not fire.
- // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
- this.on(player, 'loadstart', updateContent);
-
- // Also listen for timeupdate (in the parent) and loadedmetadata because removing those
- // listeners could have broken dependent applications/libraries. These
- // can likely be removed for 7.0.
- this.on(player, 'loadedmetadata', updateContent);
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-duration';
- }
-
- /**
- * Update duration time display.
- *
- * @param {Event} [event]
- * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
- * this function to be called.
- *
- * @listens Player#durationchange
- * @listens Player#timeupdate
- * @listens Player#loadedmetadata
- */
- updateContent(event) {
- const duration = this.player_.duration();
- this.updateTextNode_(duration);
- }
- }
-
- /**
- * The text that is added to the `DurationDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- DurationDisplay.prototype.labelText_ = 'Duration';
-
- /**
- * The text that should display over the `DurationDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- DurationDisplay.prototype.controlText_ = 'Duration';
- Component.registerComponent('DurationDisplay', DurationDisplay);
-
- /**
- * @file time-divider.js
- */
-
- /**
- * The separator between the current time and duration.
- * Can be hidden if it's not needed in the design.
- *
- * @extends Component
- */
- class TimeDivider extends Component {
- /**
- * Create the component's DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-time-control vjs-time-divider'
- }, {
- // this element and its contents can be hidden from assistive techs since
- // it is made extraneous by the announcement of the control text
- // for the current time and duration displays
- 'aria-hidden': true
- });
- const div = super.createEl('div');
- const span = super.createEl('span', {
- textContent: '/'
- });
- div.appendChild(span);
- el.appendChild(div);
- return el;
- }
- }
- Component.registerComponent('TimeDivider', TimeDivider);
-
- /**
- * @file remaining-time-display.js
- */
-
- /**
- * Displays the time left in the video
- *
- * @extends Component
- */
- class RemainingTimeDisplay extends TimeDisplay {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, 'durationchange', e => this.updateContent(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-remaining-time';
- }
-
- /**
- * Create the `Component`'s DOM element with the "minus" character prepend to the time
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl();
- if (this.options_.displayNegative !== false) {
- el.insertBefore(createEl('span', {}, {
- 'aria-hidden': true
- }, '-'), this.contentEl_);
- }
- return el;
- }
-
- /**
- * Update remaining time display.
- *
- * @param {Event} [event]
- * The `timeupdate` or `durationchange` event that caused this to run.
- *
- * @listens Player#timeupdate
- * @listens Player#durationchange
- */
- updateContent(event) {
- if (typeof this.player_.duration() !== 'number') {
- return;
- }
- let time;
-
- // @deprecated We should only use remainingTimeDisplay
- // as of video.js 7
- if (this.player_.ended()) {
- time = 0;
- } else if (this.player_.remainingTimeDisplay) {
- time = this.player_.remainingTimeDisplay();
- } else {
- time = this.player_.remainingTime();
- }
- this.updateTextNode_(time);
- }
- }
-
- /**
- * The text that is added to the `RemainingTimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
-
- /**
- * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
- Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
-
- /**
- * @file live-display.js
- */
-
- // TODO - Future make it click to snap to live
-
- /**
- * Displays the live indicator when duration is Infinity.
- *
- * @extends Component
- */
- class LiveDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.updateShowing();
- this.on(this.player(), 'durationchange', e => this.updateShowing(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-live-control vjs-control'
- });
- this.contentEl_ = createEl('div', {
- className: 'vjs-live-display'
- }, {
- 'aria-live': 'off'
- });
- this.contentEl_.appendChild(createEl('span', {
- className: 'vjs-control-text',
- textContent: `${this.localize('Stream Type')}\u00a0`
- }));
- this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE')));
- el.appendChild(this.contentEl_);
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- super.dispose();
- }
-
- /**
- * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
- * it accordingly
- *
- * @param {Event} [event]
- * The {@link Player#durationchange} event that caused this function to run.
- *
- * @listens Player#durationchange
- */
- updateShowing(event) {
- if (this.player().duration() === Infinity) {
- this.show();
- } else {
- this.hide();
- }
- }
- }
- Component.registerComponent('LiveDisplay', LiveDisplay);
-
- /**
- * @file seek-to-live.js
- */
-
- /**
- * Displays the live indicator when duration is Infinity.
- *
- * @extends Component
- */
- class SeekToLive extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.updateLiveEdgeStatus();
- if (this.player_.liveTracker) {
- this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
- this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
- }
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('button', {
- className: 'vjs-seek-to-live-control vjs-control'
- });
- this.setIcon('circle', el);
- this.textEl_ = createEl('span', {
- className: 'vjs-seek-to-live-text',
- textContent: this.localize('LIVE')
- }, {
- 'aria-hidden': 'true'
- });
- el.appendChild(this.textEl_);
- return el;
- }
-
- /**
- * Update the state of this button if we are at the live edge
- * or not
- */
- updateLiveEdgeStatus() {
- // default to live edge
- if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
- this.setAttribute('aria-disabled', true);
- this.addClass('vjs-at-live-edge');
- this.controlText('Seek to live, currently playing live');
- } else {
- this.setAttribute('aria-disabled', false);
- this.removeClass('vjs-at-live-edge');
- this.controlText('Seek to live, currently behind live');
- }
- }
-
- /**
- * On click bring us as near to the live point as possible.
- * This requires that we wait for the next `live-seekable-change`
- * event which will happen every segment length seconds.
- */
- handleClick() {
- this.player_.liveTracker.seekToLiveEdge();
- }
-
- /**
- * Dispose of the element and stop tracking
- */
- dispose() {
- if (this.player_.liveTracker) {
- this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
- }
- this.textEl_ = null;
- super.dispose();
- }
- }
- /**
- * The text that should display over the `SeekToLive`s control. Added for localization.
- *
- * @type {string}
- * @protected
- */
- SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
- Component.registerComponent('SeekToLive', SeekToLive);
-
- /**
- * @file num.js
- * @module num
- */
-
- /**
- * Keep a number between a min and a max value
- *
- * @param {number} number
- * The number to clamp
- *
- * @param {number} min
- * The minimum value
- * @param {number} max
- * The maximum value
- *
- * @return {number}
- * the clamped number
- */
- function clamp(number, min, max) {
- number = Number(number);
- return Math.min(max, Math.max(min, isNaN(number) ? min : number));
- }
-
- var Num = /*#__PURE__*/Object.freeze({
- __proto__: null,
- clamp: clamp
- });
-
- /**
- * @file slider.js
- */
-
- /**
- * The base functionality for a slider. Can be vertical or horizontal.
- * For instance the volume bar or the seek bar on a video is a slider.
- *
- * @extends Component
- */
- class Slider extends Component {
- /**
- * Create an instance of this class
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.handleMouseDown_ = e => this.handleMouseDown(e);
- this.handleMouseUp_ = e => this.handleMouseUp(e);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.handleClick_ = e => this.handleClick(e);
- this.handleMouseMove_ = e => this.handleMouseMove(e);
- this.update_ = e => this.update(e);
-
- // Set property names to bar to match with the child Slider class is looking for
- this.bar = this.getChild(this.options_.barName);
-
- // Set a horizontal or vertical class on the slider depending on the slider type
- this.vertical(!!this.options_.vertical);
- this.enable();
- }
-
- /**
- * Are controls are currently enabled for this slider or not.
- *
- * @return {boolean}
- * true if controls are enabled, false otherwise
- */
- enabled() {
- return this.enabled_;
- }
-
- /**
- * Enable controls for this slider if they are disabled
- */
- enable() {
- if (this.enabled()) {
- return;
- }
- this.on('mousedown', this.handleMouseDown_);
- this.on('touchstart', this.handleMouseDown_);
- this.on('keydown', this.handleKeyDown_);
- this.on('click', this.handleClick_);
-
- // TODO: deprecated, controlsvisible does not seem to be fired
- this.on(this.player_, 'controlsvisible', this.update);
- if (this.playerEvent) {
- this.on(this.player_, this.playerEvent, this.update);
- }
- this.removeClass('disabled');
- this.setAttribute('tabindex', 0);
- this.enabled_ = true;
- }
-
- /**
- * Disable controls for this slider if they are enabled
- */
- disable() {
- if (!this.enabled()) {
- return;
- }
- const doc = this.bar.el_.ownerDocument;
- this.off('mousedown', this.handleMouseDown_);
- this.off('touchstart', this.handleMouseDown_);
- this.off('keydown', this.handleKeyDown_);
- this.off('click', this.handleClick_);
- this.off(this.player_, 'controlsvisible', this.update_);
- this.off(doc, 'mousemove', this.handleMouseMove_);
- this.off(doc, 'mouseup', this.handleMouseUp_);
- this.off(doc, 'touchmove', this.handleMouseMove_);
- this.off(doc, 'touchend', this.handleMouseUp_);
- this.removeAttribute('tabindex');
- this.addClass('disabled');
- if (this.playerEvent) {
- this.off(this.player_, this.playerEvent, this.update);
- }
- this.enabled_ = false;
- }
-
- /**
- * Create the `Slider`s DOM element.
- *
- * @param {string} type
- * Type of element to create.
- *
- * @param {Object} [props={}]
- * List of properties in Object form.
- *
- * @param {Object} [attributes={}]
- * list of attributes in Object form.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(type, props = {}, attributes = {}) {
- // Add the slider element class to all sub classes
- props.className = props.className + ' vjs-slider';
- props = Object.assign({
- tabIndex: 0
- }, props);
- attributes = Object.assign({
- 'role': 'slider',
- 'aria-valuenow': 0,
- 'aria-valuemin': 0,
- 'aria-valuemax': 100
- }, attributes);
- return super.createEl(type, props, attributes);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `Slider`.
- *
- * @param {MouseEvent} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- * @fires Slider#slideractive
- */
- handleMouseDown(event) {
- const doc = this.bar.el_.ownerDocument;
- if (event.type === 'mousedown') {
- event.preventDefault();
- }
- // Do not call preventDefault() on touchstart in Chrome
- // to avoid console warnings. Use a 'touch-action: none' style
- // instead to prevent unintended scrolling.
- // https://developers.google.com/web/updates/2017/01/scrolling-intervention
- if (event.type === 'touchstart' && !IS_CHROME) {
- event.preventDefault();
- }
- blockTextSelection();
- this.addClass('vjs-sliding');
- /**
- * Triggered when the slider is in an active state
- *
- * @event Slider#slideractive
- * @type {MouseEvent}
- */
- this.trigger('slideractive');
- this.on(doc, 'mousemove', this.handleMouseMove_);
- this.on(doc, 'mouseup', this.handleMouseUp_);
- this.on(doc, 'touchmove', this.handleMouseMove_);
- this.on(doc, 'touchend', this.handleMouseUp_);
- this.handleMouseMove(event, true);
- }
-
- /**
- * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
- * The `mousemove` and `touchmove` events will only only trigger this function during
- * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
- * {@link Slider#handleMouseUp}.
- *
- * @param {MouseEvent} event
- * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
- * this function
- * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
- *
- * @listens mousemove
- * @listens touchmove
- */
- handleMouseMove(event) {}
-
- /**
- * Handle `mouseup` or `touchend` events on the `Slider`.
- *
- * @param {MouseEvent} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- * @fires Slider#sliderinactive
- */
- handleMouseUp(event) {
- const doc = this.bar.el_.ownerDocument;
- unblockTextSelection();
- this.removeClass('vjs-sliding');
- /**
- * Triggered when the slider is no longer in an active state.
- *
- * @event Slider#sliderinactive
- * @type {Event}
- */
- this.trigger('sliderinactive');
- this.off(doc, 'mousemove', this.handleMouseMove_);
- this.off(doc, 'mouseup', this.handleMouseUp_);
- this.off(doc, 'touchmove', this.handleMouseMove_);
- this.off(doc, 'touchend', this.handleMouseUp_);
- this.update();
- }
-
- /**
- * Update the progress bar of the `Slider`.
- *
- * @return {number}
- * The percentage of progress the progress bar represents as a
- * number from 0 to 1.
- */
- update() {
- // In VolumeBar init we have a setTimeout for update that pops and update
- // to the end of the execution stack. The player is destroyed before then
- // update will cause an error
- // If there's no bar...
- if (!this.el_ || !this.bar) {
- return;
- }
-
- // clamp progress between 0 and 1
- // and only round to four decimal places, as we round to two below
- const progress = this.getProgress();
- if (progress === this.progress_) {
- return progress;
- }
- this.progress_ = progress;
- this.requestNamedAnimationFrame('Slider#update', () => {
- // Set the new bar width or height
- const sizeKey = this.vertical() ? 'height' : 'width';
-
- // Convert to a percentage for css value
- this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
- });
- return progress;
- }
-
- /**
- * Get the percentage of the bar that should be filled
- * but clamped and rounded.
- *
- * @return {number}
- * percentage filled that the slider is
- */
- getProgress() {
- return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
- }
-
- /**
- * Calculate distance for slider
- *
- * @param {Event} event
- * The event that caused this function to run.
- *
- * @return {number}
- * The current position of the Slider.
- * - position.x for vertical `Slider`s
- * - position.y for horizontal `Slider`s
- */
- calculateDistance(event) {
- const position = getPointerPosition(this.el_, event);
- if (this.vertical()) {
- return position.y;
- }
- return position.x;
- }
-
- /**
- * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
- * arrow keys. This function will only be called when the slider has focus. See
- * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
- *
- * @param {KeyboardEvent} event
- * the `keydown` event that caused this function to run.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Left and Down Arrows
- if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepBack();
-
- // Up and Right Arrows
- } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepForward();
- } else {
- // Pass keydown handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
-
- /**
- * Listener for click events on slider, used to prevent clicks
- * from bubbling up to parent elements like button menus.
- *
- * @param {Object} event
- * Event that caused this object to run
- */
- handleClick(event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- /**
- * Get/set if slider is horizontal for vertical
- *
- * @param {boolean} [bool]
- * - true if slider is vertical,
- * - false is horizontal
- *
- * @return {boolean}
- * - true if slider is vertical, and getting
- * - false if the slider is horizontal, and getting
- */
- vertical(bool) {
- if (bool === undefined) {
- return this.vertical_ || false;
- }
- this.vertical_ = !!bool;
- if (this.vertical_) {
- this.addClass('vjs-slider-vertical');
- } else {
- this.addClass('vjs-slider-horizontal');
- }
- }
- }
- Component.registerComponent('Slider', Slider);
-
- /**
- * @file load-progress-bar.js
- */
-
- // get the percent width of a time compared to the total end
- const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
-
- /**
- * Shows loading progress
- *
- * @extends Component
- */
- class LoadProgressBar extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.partEls_ = [];
- this.on(player, 'progress', e => this.update(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-load-progress'
- });
- const wrapper = createEl('span', {
- className: 'vjs-control-text'
- });
- const loadedText = createEl('span', {
- textContent: this.localize('Loaded')
- });
- const separator = document.createTextNode(': ');
- this.percentageEl_ = createEl('span', {
- className: 'vjs-control-text-loaded-percentage',
- textContent: '0%'
- });
- el.appendChild(wrapper);
- wrapper.appendChild(loadedText);
- wrapper.appendChild(separator);
- wrapper.appendChild(this.percentageEl_);
- return el;
- }
- dispose() {
- this.partEls_ = null;
- this.percentageEl_ = null;
- super.dispose();
- }
-
- /**
- * Update progress bar
- *
- * @param {Event} [event]
- * The `progress` event that caused this function to run.
- *
- * @listens Player#progress
- */
- update(event) {
- this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
- const liveTracker = this.player_.liveTracker;
- const buffered = this.player_.buffered();
- const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
- const bufferedEnd = this.player_.bufferedEnd();
- const children = this.partEls_;
- const percent = percentify(bufferedEnd, duration);
- if (this.percent_ !== percent) {
- // update the width of the progress bar
- this.el_.style.width = percent;
- // update the control-text
- textContent(this.percentageEl_, percent);
- this.percent_ = percent;
- }
-
- // add child elements to represent the individual buffered time ranges
- for (let i = 0; i < buffered.length; i++) {
- const start = buffered.start(i);
- const end = buffered.end(i);
- let part = children[i];
- if (!part) {
- part = this.el_.appendChild(createEl());
- children[i] = part;
- }
-
- // only update if changed
- if (part.dataset.start === start && part.dataset.end === end) {
- continue;
- }
- part.dataset.start = start;
- part.dataset.end = end;
-
- // set the percent based on the width of the progress bar (bufferedEnd)
- part.style.left = percentify(start, bufferedEnd);
- part.style.width = percentify(end - start, bufferedEnd);
- }
-
- // remove unused buffered range elements
- for (let i = children.length; i > buffered.length; i--) {
- this.el_.removeChild(children[i - 1]);
- }
- children.length = buffered.length;
- });
- }
- }
- Component.registerComponent('LoadProgressBar', LoadProgressBar);
-
- /**
- * @file time-tooltip.js
- */
-
- /**
- * Time tooltips display a time above the progress bar.
- *
- * @extends Component
- */
- class TimeTooltip extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the time tooltip DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-time-tooltip'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Updates the position of the time tooltip relative to the `SeekBar`.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint, content) {
- const tooltipRect = findPosition(this.el_);
- const playerRect = getBoundingClientRect(this.player_.el());
- const seekBarPointPx = seekBarRect.width * seekBarPoint;
-
- // do nothing if either rect isn't available
- // for example, if the player isn't in the DOM for testing
- if (!playerRect || !tooltipRect) {
- return;
- }
-
- // This is the space left of the `seekBarPoint` available within the bounds
- // of the player. We calculate any gap between the left edge of the player
- // and the left edge of the `SeekBar` and add the number of pixels in the
- // `SeekBar` before hitting the `seekBarPoint`
- let spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
-
- // This is the space right of the `seekBarPoint` available within the bounds
- // of the player. We calculate the number of pixels from the `seekBarPoint`
- // to the right edge of the `SeekBar` and add to that any gap between the
- // right edge of the `SeekBar` and the player.
- let spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
-
- // spaceRightOfPoint is always NaN for mouse time display
- // because the seekbarRect does not have a right property. This causes
- // the mouse tool tip to be truncated when it's close to the right edge of the player.
- // In such cases, we ignore the `playerRect.right - seekBarRect.right` value when calculating.
- // For the sake of consistency, we ignore seekBarRect.left - playerRect.left for the left edge.
- if (!spaceRightOfPoint) {
- spaceRightOfPoint = seekBarRect.width - seekBarPointPx;
- spaceLeftOfPoint = seekBarPointPx;
- }
- // This is the number of pixels by which the tooltip will need to be pulled
- // further to the right to center it over the `seekBarPoint`.
- let pullTooltipBy = tooltipRect.width / 2;
-
- // Adjust the `pullTooltipBy` distance to the left or right depending on
- // the results of the space calculations above.
- if (spaceLeftOfPoint < pullTooltipBy) {
- pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
- } else if (spaceRightOfPoint < pullTooltipBy) {
- pullTooltipBy = spaceRightOfPoint;
- }
-
- // Due to the imprecision of decimal/ratio based calculations and varying
- // rounding behaviors, there are cases where the spacing adjustment is off
- // by a pixel or two. This adds insurance to these calculations.
- if (pullTooltipBy < 0) {
- pullTooltipBy = 0;
- } else if (pullTooltipBy > tooltipRect.width) {
- pullTooltipBy = tooltipRect.width;
- }
-
- // prevent small width fluctuations within 0.4px from
- // changing the value below.
- // This really helps for live to prevent the play
- // progress time tooltip from jittering
- pullTooltipBy = Math.round(pullTooltipBy);
- this.el_.style.right = `-${pullTooltipBy}px`;
- this.write(content);
- }
-
- /**
- * Write the time to the tooltip DOM element.
- *
- * @param {string} content
- * The formatted time for the tooltip.
- */
- write(content) {
- textContent(this.el_, content);
- }
-
- /**
- * Updates the position of the time tooltip relative to the `SeekBar`.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- *
- * @param {number} time
- * The time to update the tooltip to, not used during live playback
- *
- * @param {Function} cb
- * A function that will be called during the request animation frame
- * for tooltips that need to do additional animations from the default
- */
- updateTime(seekBarRect, seekBarPoint, time, cb) {
- this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
- let content;
- const duration = this.player_.duration();
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- const liveWindow = this.player_.liveTracker.liveWindow();
- const secondsBehind = liveWindow - seekBarPoint * liveWindow;
- content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
- } else {
- content = formatTime(time, duration);
- }
- this.update(seekBarRect, seekBarPoint, content);
- if (cb) {
- cb();
- }
- });
- }
- }
- Component.registerComponent('TimeTooltip', TimeTooltip);
-
- /**
- * @file play-progress-bar.js
- */
-
- /**
- * Used by {@link SeekBar} to display media playback progress as part of the
- * {@link ProgressControl}.
- *
- * @extends Component
- */
- class PlayProgressBar extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('circle');
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-play-progress vjs-slider-bar'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Enqueues updates to its own DOM as well as the DOM of its
- * {@link TimeTooltip} child.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint) {
- const timeTooltip = this.getChild('timeTooltip');
- if (!timeTooltip) {
- return;
- }
- const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
- }
- }
-
- /**
- * Default options for {@link PlayProgressBar}.
- *
- * @type {Object}
- * @private
- */
- PlayProgressBar.prototype.options_ = {
- children: []
- };
-
- // Time tooltips should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- PlayProgressBar.prototype.options_.children.push('timeTooltip');
- }
- Component.registerComponent('PlayProgressBar', PlayProgressBar);
-
- /**
- * @file mouse-time-display.js
- */
-
- /**
- * The {@link MouseTimeDisplay} component tracks mouse movement over the
- * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
- * indicating the time which is represented by a given point in the
- * {@link ProgressControl}.
- *
- * @extends Component
- */
- class MouseTimeDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-mouse-display'
- });
- }
-
- /**
- * Enqueues updates to its own DOM as well as the DOM of its
- * {@link TimeTooltip} child.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint) {
- const time = seekBarPoint * this.player_.duration();
- this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
- this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
- });
- }
- }
-
- /**
- * Default options for `MouseTimeDisplay`
- *
- * @type {Object}
- * @private
- */
- MouseTimeDisplay.prototype.options_ = {
- children: ['timeTooltip']
- };
- Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
-
- /**
- * @file seek-bar.js
- */
-
- // The number of seconds the `step*` functions move the timeline.
- const STEP_SECONDS = 5;
-
- // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
- const PAGE_KEY_MULTIPLIER = 12;
-
- /**
- * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
- * as its `bar`.
- *
- * @extends Slider
- */
- class SeekBar extends Slider {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setEventHandlers_();
- }
-
- /**
- * Sets the event handlers
- *
- * @private
- */
- setEventHandlers_() {
- this.update_ = bind_(this, this.update);
- this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
- this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- if (this.player_.liveTracker) {
- this.on(this.player_.liveTracker, 'liveedgechange', this.update);
- }
-
- // when playing, let's ensure we smoothly update the play progress bar
- // via an interval
- this.updateInterval = null;
- this.enableIntervalHandler_ = e => this.enableInterval_(e);
- this.disableIntervalHandler_ = e => this.disableInterval_(e);
- this.on(this.player_, ['playing'], this.enableIntervalHandler_);
- this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
-
- // we don't need to update the play progress if the document is hidden,
- // also, this causes the CPU to spike and eventually crash the page on IE11.
- if ('hidden' in document && 'visibilityState' in document) {
- this.on(document, 'visibilitychange', this.toggleVisibility_);
- }
- }
- toggleVisibility_(e) {
- if (document.visibilityState === 'hidden') {
- this.cancelNamedAnimationFrame('SeekBar#update');
- this.cancelNamedAnimationFrame('Slider#update');
- this.disableInterval_(e);
- } else {
- if (!this.player_.ended() && !this.player_.paused()) {
- this.enableInterval_();
- }
-
- // we just switched back to the page and someone may be looking, so, update ASAP
- this.update();
- }
- }
- enableInterval_() {
- if (this.updateInterval) {
- return;
- }
- this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
- }
- disableInterval_(e) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
- return;
- }
- if (!this.updateInterval) {
- return;
- }
- this.clearInterval(this.updateInterval);
- this.updateInterval = null;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-progress-holder'
- }, {
- 'aria-label': this.localize('Progress Bar')
- });
- }
-
- /**
- * This function updates the play progress bar and accessibility
- * attributes to whatever is passed in.
- *
- * @param {Event} [event]
- * The `timeupdate` or `ended` event that caused this to run.
- *
- * @listens Player#timeupdate
- *
- * @return {number}
- * The current percent at a number from 0-1
- */
- update(event) {
- // ignore updates while the tab is hidden
- if (document.visibilityState === 'hidden') {
- return;
- }
- const percent = super.update();
- this.requestNamedAnimationFrame('SeekBar#update', () => {
- const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
- const liveTracker = this.player_.liveTracker;
- let duration = this.player_.duration();
- if (liveTracker && liveTracker.isLive()) {
- duration = this.player_.liveTracker.liveCurrentTime();
- }
- if (this.percent_ !== percent) {
- // machine readable value of progress bar (percentage complete)
- this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
- this.percent_ = percent;
- }
- if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
- // human readable value of progress bar (time complete)
- this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
- this.currentTime_ = currentTime;
- this.duration_ = duration;
- }
-
- // update the progress bar time tooltip with the current time
- if (this.bar) {
- this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
- }
- });
- return percent;
- }
-
- /**
- * Prevent liveThreshold from causing seeks to seem like they
- * are not happening from a user perspective.
- *
- * @param {number} ct
- * current time to seek to
- */
- userSeek_(ct) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- this.player_.liveTracker.nextSeekedFromUser();
- }
- this.player_.currentTime(ct);
- }
-
- /**
- * Get the value of current time but allows for smooth scrubbing,
- * when player can't keep up.
- *
- * @return {number}
- * The current time value to display
- *
- * @private
- */
- getCurrentTime_() {
- return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- }
-
- /**
- * Get the percentage of media played so far.
- *
- * @return {number}
- * The percentage of media played so far (0 to 1).
- */
- getPercent() {
- const currentTime = this.getCurrentTime_();
- let percent;
- const liveTracker = this.player_.liveTracker;
- if (liveTracker && liveTracker.isLive()) {
- percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
-
- // prevent the percent from changing at the live edge
- if (liveTracker.atLiveEdge()) {
- percent = 1;
- }
- } else {
- percent = currentTime / this.player_.duration();
- }
- return percent;
- }
-
- /**
- * Handle mouse down on seek bar
- *
- * @param {MouseEvent} event
- * The `mousedown` event that caused this to run.
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- if (!isSingleLeftClick(event)) {
- return;
- }
-
- // Stop event propagation to prevent double fire in progress-control.js
- event.stopPropagation();
- this.videoWasPlaying = !this.player_.paused();
- this.player_.pause();
- super.handleMouseDown(event);
- }
-
- /**
- * Handle mouse move on seek bar
- *
- * @param {MouseEvent} event
- * The `mousemove` event that caused this to run.
- * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
- *
- * @listens mousemove
- */
- handleMouseMove(event, mouseDown = false) {
- if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) {
- return;
- }
- if (!mouseDown && !this.player_.scrubbing()) {
- this.player_.scrubbing(true);
- }
- let newTime;
- const distance = this.calculateDistance(event);
- const liveTracker = this.player_.liveTracker;
- if (!liveTracker || !liveTracker.isLive()) {
- newTime = distance * this.player_.duration();
-
- // Don't let video end while scrubbing.
- if (newTime === this.player_.duration()) {
- newTime = newTime - 0.1;
- }
- } else {
- if (distance >= 0.99) {
- liveTracker.seekToLiveEdge();
- return;
- }
- const seekableStart = liveTracker.seekableStart();
- const seekableEnd = liveTracker.liveCurrentTime();
- newTime = seekableStart + distance * liveTracker.liveWindow();
-
- // Don't let video end while scrubbing.
- if (newTime >= seekableEnd) {
- newTime = seekableEnd;
- }
-
- // Compensate for precision differences so that currentTime is not less
- // than seekable start
- if (newTime <= seekableStart) {
- newTime = seekableStart + 0.1;
- }
-
- // On android seekableEnd can be Infinity sometimes,
- // this will cause newTime to be Infinity, which is
- // not a valid currentTime.
- if (newTime === Infinity) {
- return;
- }
- }
-
- // Set new time (tell player to seek to new time)
- this.userSeek_(newTime);
- if (this.player_.options_.enableSmoothSeeking) {
- this.update();
- }
- }
- enable() {
- super.enable();
- const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- if (!mouseTimeDisplay) {
- return;
- }
- mouseTimeDisplay.show();
- }
- disable() {
- super.disable();
- const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- if (!mouseTimeDisplay) {
- return;
- }
- mouseTimeDisplay.hide();
- }
-
- /**
- * Handle mouse up on seek bar
- *
- * @param {MouseEvent} event
- * The `mouseup` event that caused this to run.
- *
- * @listens mouseup
- */
- handleMouseUp(event) {
- super.handleMouseUp(event);
-
- // Stop event propagation to prevent double fire in progress-control.js
- if (event) {
- event.stopPropagation();
- }
- this.player_.scrubbing(false);
-
- /**
- * Trigger timeupdate because we're done seeking and the time has changed.
- * This is particularly useful for if the player is paused to time the time displays.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.player_.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- if (this.videoWasPlaying) {
- silencePromise(this.player_.play());
- } else {
- // We're done seeking and the time has changed.
- // If the player is paused, make sure we display the correct time on the seek bar.
- this.update_();
- }
- }
-
- /**
- * Move more quickly fast forward for keyboard-only users
- */
- stepForward() {
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
- }
-
- /**
- * Move more quickly rewind for keyboard-only users
- */
- stepBack() {
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
- }
-
- /**
- * Toggles the playback state of the player
- * This gets called when enter or space is used on the seekbar
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called
- *
- */
- handleAction(event) {
- if (this.player_.paused()) {
- this.player_.play();
- } else {
- this.player_.pause();
- }
- }
-
- /**
- * Called when this SeekBar has focus and a key gets pressed down.
- * Supports the following keys:
- *
- * Space or Enter key fire a click event
- * Home key moves to start of the timeline
- * End key moves to end of the timeline
- * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
- * PageDown key moves back a larger step than ArrowDown
- * PageUp key moves forward a large step
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- const liveTracker = this.player_.liveTracker;
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.preventDefault();
- event.stopPropagation();
- this.handleAction(event);
- } else if (keycode.isEventKey(event, 'Home')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(0);
- } else if (keycode.isEventKey(event, 'End')) {
- event.preventDefault();
- event.stopPropagation();
- if (liveTracker && liveTracker.isLive()) {
- this.userSeek_(liveTracker.liveCurrentTime());
- } else {
- this.userSeek_(this.player_.duration());
- }
- } else if (/^[0-9]$/.test(keycode(event))) {
- event.preventDefault();
- event.stopPropagation();
- const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;
- if (liveTracker && liveTracker.isLive()) {
- this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
- } else {
- this.userSeek_(this.player_.duration() * gotoFraction);
- }
- } else if (keycode.isEventKey(event, 'PgDn')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
- } else if (keycode.isEventKey(event, 'PgUp')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
- } else {
- // Pass keydown handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- dispose() {
- this.disableInterval_();
- this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- if (this.player_.liveTracker) {
- this.off(this.player_.liveTracker, 'liveedgechange', this.update);
- }
- this.off(this.player_, ['playing'], this.enableIntervalHandler_);
- this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
-
- // we don't need to update the play progress if the document is hidden,
- // also, this causes the CPU to spike and eventually crash the page on IE11.
- if ('hidden' in document && 'visibilityState' in document) {
- this.off(document, 'visibilitychange', this.toggleVisibility_);
- }
- super.dispose();
- }
- }
-
- /**
- * Default options for the `SeekBar`
- *
- * @type {Object}
- * @private
- */
- SeekBar.prototype.options_ = {
- children: ['loadProgressBar', 'playProgressBar'],
- barName: 'playProgressBar'
- };
-
- // MouseTimeDisplay tooltips should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
- }
- Component.registerComponent('SeekBar', SeekBar);
-
- /**
- * @file progress-control.js
- */
-
- /**
- * The Progress Control component contains the seek bar, load progress,
- * and play progress.
- *
- * @extends Component
- */
- class ProgressControl extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
- this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
- this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
- this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
- this.enable();
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-progress-control vjs-control'
- });
- }
-
- /**
- * When the mouse moves over the `ProgressControl`, the pointer position
- * gets passed down to the `MouseTimeDisplay` component.
- *
- * @param {Event} event
- * The `mousemove` event that caused this function to run.
- *
- * @listen mousemove
- */
- handleMouseMove(event) {
- const seekBar = this.getChild('seekBar');
- if (!seekBar) {
- return;
- }
- const playProgressBar = seekBar.getChild('playProgressBar');
- const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
- if (!playProgressBar && !mouseTimeDisplay) {
- return;
- }
- const seekBarEl = seekBar.el();
- const seekBarRect = findPosition(seekBarEl);
- let seekBarPoint = getPointerPosition(seekBarEl, event).x;
-
- // The default skin has a gap on either side of the `SeekBar`. This means
- // that it's possible to trigger this behavior outside the boundaries of
- // the `SeekBar`. This ensures we stay within it at all times.
- seekBarPoint = clamp(seekBarPoint, 0, 1);
- if (mouseTimeDisplay) {
- mouseTimeDisplay.update(seekBarRect, seekBarPoint);
- }
- if (playProgressBar) {
- playProgressBar.update(seekBarRect, seekBar.getProgress());
- }
- }
-
- /**
- * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
- *
- * @method ProgressControl#throttledHandleMouseSeek
- * @param {Event} event
- * The `mousemove` event that caused this function to run.
- *
- * @listen mousemove
- * @listen touchmove
- */
-
- /**
- * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousemove
- * @listens touchmove
- */
- handleMouseSeek(event) {
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseMove(event);
- }
- }
-
- /**
- * Are controls are currently enabled for this progress control.
- *
- * @return {boolean}
- * true if controls are enabled, false otherwise
- */
- enabled() {
- return this.enabled_;
- }
-
- /**
- * Disable all controls on the progress control and its children
- */
- disable() {
- this.children().forEach(child => child.disable && child.disable());
- if (!this.enabled()) {
- return;
- }
- this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
- this.off(this.el_, 'mousemove', this.handleMouseMove);
- this.removeListenersAddedOnMousedownAndTouchstart();
- this.addClass('disabled');
- this.enabled_ = false;
-
- // Restore normal playback state if controls are disabled while scrubbing
- if (this.player_.scrubbing()) {
- const seekBar = this.getChild('seekBar');
- this.player_.scrubbing(false);
- if (seekBar.videoWasPlaying) {
- silencePromise(this.player_.play());
- }
- }
- }
-
- /**
- * Enable all controls on the progress control and its children
- */
- enable() {
- this.children().forEach(child => child.enable && child.enable());
- if (this.enabled()) {
- return;
- }
- this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
- this.on(this.el_, 'mousemove', this.handleMouseMove);
- this.removeClass('disabled');
- this.enabled_ = true;
- }
-
- /**
- * Cleanup listeners after the user finishes interacting with the progress controls
- */
- removeListenersAddedOnMousedownAndTouchstart() {
- const doc = this.el_.ownerDocument;
- this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
- this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
- this.off(doc, 'mouseup', this.handleMouseUpHandler_);
- this.off(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseDown(event) {
- const doc = this.el_.ownerDocument;
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseDown(event);
- }
- this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
- this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
- this.on(doc, 'mouseup', this.handleMouseUpHandler_);
- this.on(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mouseup` or `touchend` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- */
- handleMouseUp(event) {
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseUp(event);
- }
- this.removeListenersAddedOnMousedownAndTouchstart();
- }
- }
-
- /**
- * Default options for `ProgressControl`
- *
- * @type {Object}
- * @private
- */
- ProgressControl.prototype.options_ = {
- children: ['seekBar']
- };
- Component.registerComponent('ProgressControl', ProgressControl);
-
- /**
- * @file picture-in-picture-toggle.js
- */
-
- /**
- * Toggle Picture-in-Picture mode
- *
- * @extends Button
- */
- class PictureInPictureToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @listens Player#enterpictureinpicture
- * @listens Player#leavepictureinpicture
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('picture-in-picture-enter');
- this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
- this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
- this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange());
-
- // TODO: Deactivate button on player emptied event.
- this.disable();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`;
- }
-
- /**
- * Displays or hides the button depending on the audio mode detection.
- * Exits picture-in-picture if it is enabled when switching to audio mode.
- */
- handlePictureInPictureAudioModeChange() {
- // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
- const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio';
- const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode();
- if (!isAudioMode) {
- this.show();
- return;
- }
- if (this.player_.isInPictureInPicture()) {
- this.player_.exitPictureInPicture();
- }
- this.hide();
- }
-
- /**
- * Enables or disables button based on availability of a Picture-In-Picture mode.
- *
- * Enabled if
- * - `player.options().enableDocumentPictureInPicture` is true and
- * window.documentPictureInPicture is available; or
- * - `player.disablePictureInPicture()` is false and
- * element.requestPictureInPicture is available
- */
- handlePictureInPictureEnabledChange() {
- if (document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window) {
- this.enable();
- } else {
- this.disable();
- }
- }
-
- /**
- * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
- *
- * @param {Event} [event]
- * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
- * called.
- *
- * @listens Player#enterpictureinpicture
- * @listens Player#leavepictureinpicture
- */
- handlePictureInPictureChange(event) {
- if (this.player_.isInPictureInPicture()) {
- this.setIcon('picture-in-picture-exit');
- this.controlText('Exit Picture-in-Picture');
- } else {
- this.setIcon('picture-in-picture-enter');
- this.controlText('Picture-in-Picture');
- }
- this.handlePictureInPictureEnabledChange();
- }
-
- /**
- * This gets called when an `PictureInPictureToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (!this.player_.isInPictureInPicture()) {
- this.player_.requestPictureInPicture();
- } else {
- this.player_.exitPictureInPicture();
- }
- }
-
- /**
- * Show the `Component`s element if it is hidden by removing the
- * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API.
- */
- show() {
- // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox.
- if (typeof document.exitPictureInPicture !== 'function') {
- return;
- }
- super.show();
- }
- }
-
- /**
- * The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
- Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
-
- /**
- * @file fullscreen-toggle.js
- */
-
- /**
- * Toggle fullscreen video
- *
- * @extends Button
- */
- class FullscreenToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('fullscreen-enter');
- this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
- if (document[player.fsApi_.fullscreenEnabled] === false) {
- this.disable();
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-fullscreen-control ${super.buildCSSClass()}`;
- }
-
- /**
- * Handles fullscreenchange on the player and change control text accordingly.
- *
- * @param {Event} [event]
- * The {@link Player#fullscreenchange} event that caused this function to be
- * called.
- *
- * @listens Player#fullscreenchange
- */
- handleFullscreenChange(event) {
- if (this.player_.isFullscreen()) {
- this.controlText('Exit Fullscreen');
- this.setIcon('fullscreen-exit');
- } else {
- this.controlText('Fullscreen');
- this.setIcon('fullscreen-enter');
- }
- }
-
- /**
- * This gets called when an `FullscreenToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (!this.player_.isFullscreen()) {
- this.player_.requestFullscreen();
- } else {
- this.player_.exitFullscreen();
- }
- }
- }
-
- /**
- * The text that should display over the `FullscreenToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- FullscreenToggle.prototype.controlText_ = 'Fullscreen';
- Component.registerComponent('FullscreenToggle', FullscreenToggle);
-
- /**
- * Check if volume control is supported and if it isn't hide the
- * `Component` that was passed using the `vjs-hidden` class.
- *
- * @param { import('../../component').default } self
- * The component that should be hidden if volume is unsupported
- *
- * @param { import('../../player').default } player
- * A reference to the player
- *
- * @private
- */
- const checkVolumeSupport = function (self, player) {
- // hide volume controls when they're not supported by the current tech
- if (player.tech_ && !player.tech_.featuresVolumeControl) {
- self.addClass('vjs-hidden');
- }
- self.on(player, 'loadstart', function () {
- if (!player.tech_.featuresVolumeControl) {
- self.addClass('vjs-hidden');
- } else {
- self.removeClass('vjs-hidden');
- }
- });
- };
-
- /**
- * @file volume-level.js
- */
-
- /**
- * Shows volume level
- *
- * @extends Component
- */
- class VolumeLevel extends Component {
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-volume-level'
- });
- this.setIcon('circle', el);
- el.appendChild(super.createEl('span', {
- className: 'vjs-control-text'
- }));
- return el;
- }
- }
- Component.registerComponent('VolumeLevel', VolumeLevel);
-
- /**
- * @file volume-level-tooltip.js
- */
-
- /**
- * Volume level tooltips display a volume above or side by side the volume bar.
- *
- * @extends Component
- */
- class VolumeLevelTooltip extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the volume tooltip DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-volume-tooltip'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Updates the position of the tooltip relative to the `VolumeBar` and
- * its content text.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- */
- update(rangeBarRect, rangeBarPoint, vertical, content) {
- if (!vertical) {
- const tooltipRect = getBoundingClientRect(this.el_);
- const playerRect = getBoundingClientRect(this.player_.el());
- const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
- if (!playerRect || !tooltipRect) {
- return;
- }
- const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
- const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
- let pullTooltipBy = tooltipRect.width / 2;
- if (spaceLeftOfPoint < pullTooltipBy) {
- pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
- } else if (spaceRightOfPoint < pullTooltipBy) {
- pullTooltipBy = spaceRightOfPoint;
- }
- if (pullTooltipBy < 0) {
- pullTooltipBy = 0;
- } else if (pullTooltipBy > tooltipRect.width) {
- pullTooltipBy = tooltipRect.width;
- }
- this.el_.style.right = `-${pullTooltipBy}px`;
- }
- this.write(`${content}%`);
- }
-
- /**
- * Write the volume to the tooltip DOM element.
- *
- * @param {string} content
- * The formatted volume for the tooltip.
- */
- write(content) {
- textContent(this.el_, content);
- }
-
- /**
- * Updates the position of the volume tooltip relative to the `VolumeBar`.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- * @param {number} volume
- * The volume level to update the tooltip to
- *
- * @param {Function} cb
- * A function that will be called during the request animation frame
- * for tooltips that need to do additional animations from the default
- */
- updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
- this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
- this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
- if (cb) {
- cb();
- }
- });
- }
- }
- Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
-
- /**
- * @file mouse-volume-level-display.js
- */
-
- /**
- * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
- * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
- * indicating the volume level which is represented by a given point in the
- * {@link VolumeBar}.
- *
- * @extends Component
- */
- class MouseVolumeLevelDisplay extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-mouse-display'
- });
- }
-
- /**
- * Enquires updates to its own DOM as well as the DOM of its
- * {@link VolumeLevelTooltip} child.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- */
- update(rangeBarRect, rangeBarPoint, vertical) {
- const volume = 100 * rangeBarPoint;
- this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
- if (vertical) {
- this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
- } else {
- this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
- }
- });
- }
- }
-
- /**
- * Default options for `MouseVolumeLevelDisplay`
- *
- * @type {Object}
- * @private
- */
- MouseVolumeLevelDisplay.prototype.options_ = {
- children: ['volumeLevelTooltip']
- };
- Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
-
- /**
- * @file volume-bar.js
- */
-
- /**
- * The bar that contains the volume level and can be clicked on to adjust the level
- *
- * @extends Slider
- */
- class VolumeBar extends Slider {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on('slideractive', e => this.updateLastVolume_(e));
- this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
- player.ready(() => this.updateARIAAttributes());
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-volume-bar vjs-slider-bar'
- }, {
- 'aria-label': this.localize('Volume Level'),
- 'aria-live': 'polite'
- });
- }
-
- /**
- * Handle mouse down on volume bar
- *
- * @param {Event} event
- * The `mousedown` event that caused this to run.
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- if (!isSingleLeftClick(event)) {
- return;
- }
- super.handleMouseDown(event);
- }
-
- /**
- * Handle movement events on the {@link VolumeMenuButton}.
- *
- * @param {Event} event
- * The event that caused this function to run.
- *
- * @listens mousemove
- */
- handleMouseMove(event) {
- const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
- if (mouseVolumeLevelDisplay) {
- const volumeBarEl = this.el();
- const volumeBarRect = getBoundingClientRect(volumeBarEl);
- const vertical = this.vertical();
- let volumeBarPoint = getPointerPosition(volumeBarEl, event);
- volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
- // The default skin has a gap on either side of the `VolumeBar`. This means
- // that it's possible to trigger this behavior outside the boundaries of
- // the `VolumeBar`. This ensures we stay within it at all times.
- volumeBarPoint = clamp(volumeBarPoint, 0, 1);
- mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
- }
- if (!isSingleLeftClick(event)) {
- return;
- }
- this.checkMuted();
- this.player_.volume(this.calculateDistance(event));
- }
-
- /**
- * If the player is muted unmute it.
- */
- checkMuted() {
- if (this.player_.muted()) {
- this.player_.muted(false);
- }
- }
-
- /**
- * Get percent of volume level
- *
- * @return {number}
- * Volume level percent as a decimal number.
- */
- getPercent() {
- if (this.player_.muted()) {
- return 0;
- }
- return this.player_.volume();
- }
-
- /**
- * Increase volume level for keyboard users
- */
- stepForward() {
- this.checkMuted();
- this.player_.volume(this.player_.volume() + 0.1);
- }
-
- /**
- * Decrease volume level for keyboard users
- */
- stepBack() {
- this.checkMuted();
- this.player_.volume(this.player_.volume() - 0.1);
- }
-
- /**
- * Update ARIA accessibility attributes
- *
- * @param {Event} [event]
- * The `volumechange` event that caused this function to run.
- *
- * @listens Player#volumechange
- */
- updateARIAAttributes(event) {
- const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
- this.el_.setAttribute('aria-valuenow', ariaValue);
- this.el_.setAttribute('aria-valuetext', ariaValue + '%');
- }
-
- /**
- * Returns the current value of the player volume as a percentage
- *
- * @private
- */
- volumeAsPercentage_() {
- return Math.round(this.player_.volume() * 100);
- }
-
- /**
- * When user starts dragging the VolumeBar, store the volume and listen for
- * the end of the drag. When the drag ends, if the volume was set to zero,
- * set lastVolume to the stored volume.
- *
- * @listens slideractive
- * @private
- */
- updateLastVolume_() {
- const volumeBeforeDrag = this.player_.volume();
- this.one('sliderinactive', () => {
- if (this.player_.volume() === 0) {
- this.player_.lastVolume_(volumeBeforeDrag);
- }
- });
- }
- }
-
- /**
- * Default options for the `VolumeBar`
- *
- * @type {Object}
- * @private
- */
- VolumeBar.prototype.options_ = {
- children: ['volumeLevel'],
- barName: 'volumeLevel'
- };
-
- // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
- }
-
- /**
- * Call the update event for this Slider when this event happens on the player.
- *
- * @type {string}
- */
- VolumeBar.prototype.playerEvent = 'volumechange';
- Component.registerComponent('VolumeBar', VolumeBar);
-
- /**
- * @file volume-control.js
- */
-
- /**
- * The component for controlling the volume level
- *
- * @extends Component
- */
- class VolumeControl extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.vertical = options.vertical || false;
-
- // Pass the vertical option down to the VolumeBar if
- // the VolumeBar is turned on.
- if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
- options.volumeBar = options.volumeBar || {};
- options.volumeBar.vertical = options.vertical;
- }
- super(player, options);
-
- // hide this control if volume support is missing
- checkVolumeSupport(this, player);
- this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
- this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
- this.on('mousedown', e => this.handleMouseDown(e));
- this.on('touchstart', e => this.handleMouseDown(e));
- this.on('mousemove', e => this.handleMouseMove(e));
-
- // while the slider is active (the mouse has been pressed down and
- // is dragging) or in focus we do not want to hide the VolumeBar
- this.on(this.volumeBar, ['focus', 'slideractive'], () => {
- this.volumeBar.addClass('vjs-slider-active');
- this.addClass('vjs-slider-active');
- this.trigger('slideractive');
- });
- this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
- this.volumeBar.removeClass('vjs-slider-active');
- this.removeClass('vjs-slider-active');
- this.trigger('sliderinactive');
- });
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- let orientationClass = 'vjs-volume-horizontal';
- if (this.options_.vertical) {
- orientationClass = 'vjs-volume-vertical';
- }
- return super.createEl('div', {
- className: `vjs-volume-control vjs-control ${orientationClass}`
- });
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseDown(event) {
- const doc = this.el_.ownerDocument;
- this.on(doc, 'mousemove', this.throttledHandleMouseMove);
- this.on(doc, 'touchmove', this.throttledHandleMouseMove);
- this.on(doc, 'mouseup', this.handleMouseUpHandler_);
- this.on(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mouseup` or `touchend` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- */
- handleMouseUp(event) {
- const doc = this.el_.ownerDocument;
- this.off(doc, 'mousemove', this.throttledHandleMouseMove);
- this.off(doc, 'touchmove', this.throttledHandleMouseMove);
- this.off(doc, 'mouseup', this.handleMouseUpHandler_);
- this.off(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseMove(event) {
- this.volumeBar.handleMouseMove(event);
- }
- }
-
- /**
- * Default options for the `VolumeControl`
- *
- * @type {Object}
- * @private
- */
- VolumeControl.prototype.options_ = {
- children: ['volumeBar']
- };
- Component.registerComponent('VolumeControl', VolumeControl);
-
- /**
- * Check if muting volume is supported and if it isn't hide the mute toggle
- * button.
- *
- * @param { import('../../component').default } self
- * A reference to the mute toggle button
- *
- * @param { import('../../player').default } player
- * A reference to the player
- *
- * @private
- */
- const checkMuteSupport = function (self, player) {
- // hide mute toggle button if it's not supported by the current tech
- if (player.tech_ && !player.tech_.featuresMuteControl) {
- self.addClass('vjs-hidden');
- }
- self.on(player, 'loadstart', function () {
- if (!player.tech_.featuresMuteControl) {
- self.addClass('vjs-hidden');
- } else {
- self.removeClass('vjs-hidden');
- }
- });
- };
-
- /**
- * @file mute-toggle.js
- */
-
- /**
- * A button component for muting the audio.
- *
- * @extends Button
- */
- class MuteToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
-
- // hide this control if volume support is missing
- checkMuteSupport(this, player);
- this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-mute-control ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when an `MuteToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const vol = this.player_.volume();
- const lastVolume = this.player_.lastVolume_();
- if (vol === 0) {
- const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
- this.player_.volume(volumeToSet);
- this.player_.muted(false);
- } else {
- this.player_.muted(this.player_.muted() ? false : true);
- }
- }
-
- /**
- * Update the `MuteToggle` button based on the state of `volume` and `muted`
- * on the player.
- *
- * @param {Event} [event]
- * The {@link Player#loadstart} event if this function was called
- * through an event.
- *
- * @listens Player#loadstart
- * @listens Player#volumechange
- */
- update(event) {
- this.updateIcon_();
- this.updateControlText_();
- }
-
- /**
- * Update the appearance of the `MuteToggle` icon.
- *
- * Possible states (given `level` variable below):
- * - 0: crossed out
- * - 1: zero bars of volume
- * - 2: one bar of volume
- * - 3: two bars of volume
- *
- * @private
- */
- updateIcon_() {
- const vol = this.player_.volume();
- let level = 3;
- this.setIcon('volume-high');
-
- // in iOS when a player is loaded with muted attribute
- // and volume is changed with a native mute button
- // we want to make sure muted state is updated
- if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
- this.player_.muted(this.player_.tech_.el_.muted);
- }
- if (vol === 0 || this.player_.muted()) {
- this.setIcon('volume-mute');
- level = 0;
- } else if (vol < 0.33) {
- this.setIcon('volume-low');
- level = 1;
- } else if (vol < 0.67) {
- this.setIcon('volume-medium');
- level = 2;
- }
- removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
- addClass(this.el_, `vjs-vol-${level}`);
- }
-
- /**
- * If `muted` has changed on the player, update the control text
- * (`title` attribute on `vjs-mute-control` element and content of
- * `vjs-control-text` element).
- *
- * @private
- */
- updateControlText_() {
- const soundOff = this.player_.muted() || this.player_.volume() === 0;
- const text = soundOff ? 'Unmute' : 'Mute';
- if (this.controlText() !== text) {
- this.controlText(text);
- }
- }
- }
-
- /**
- * The text that should display over the `MuteToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- MuteToggle.prototype.controlText_ = 'Mute';
- Component.registerComponent('MuteToggle', MuteToggle);
-
- /**
- * @file volume-control.js
- */
-
- /**
- * A Component to contain the MuteToggle and VolumeControl so that
- * they can work together.
- *
- * @extends Component
- */
- class VolumePanel extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- if (typeof options.inline !== 'undefined') {
- options.inline = options.inline;
- } else {
- options.inline = true;
- }
-
- // pass the inline option down to the VolumeControl as vertical if
- // the VolumeControl is on.
- if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
- options.volumeControl = options.volumeControl || {};
- options.volumeControl.vertical = !options.inline;
- }
- super(player, options);
-
- // this handler is used by mouse handler methods below
- this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
- this.on(player, ['loadstart'], e => this.volumePanelState_(e));
- this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
- this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
- this.on('keydown', e => this.handleKeyPress(e));
- this.on('mouseover', e => this.handleMouseOver(e));
- this.on('mouseout', e => this.handleMouseOut(e));
-
- // while the slider is active (the mouse has been pressed down and
- // is dragging) we do not want to hide the VolumeBar
- this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
- this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
- }
-
- /**
- * Add vjs-slider-active class to the VolumePanel
- *
- * @listens VolumeControl#slideractive
- * @private
- */
- sliderActive_() {
- this.addClass('vjs-slider-active');
- }
-
- /**
- * Removes vjs-slider-active class to the VolumePanel
- *
- * @listens VolumeControl#sliderinactive
- * @private
- */
- sliderInactive_() {
- this.removeClass('vjs-slider-active');
- }
-
- /**
- * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
- * depending on MuteToggle and VolumeControl state
- *
- * @listens Player#loadstart
- * @private
- */
- volumePanelState_() {
- // hide volume panel if neither volume control or mute toggle
- // are displayed
- if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
- this.addClass('vjs-hidden');
- }
-
- // if only mute toggle is visible we don't want
- // volume panel expanding when hovered or active
- if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
- this.addClass('vjs-mute-toggle-only');
- }
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- let orientationClass = 'vjs-volume-panel-horizontal';
- if (!this.options_.inline) {
- orientationClass = 'vjs-volume-panel-vertical';
- }
- return super.createEl('div', {
- className: `vjs-volume-panel vjs-control ${orientationClass}`
- });
- }
-
- /**
- * Dispose of the `volume-panel` and all child components.
- */
- dispose() {
- this.handleMouseOut();
- super.dispose();
- }
-
- /**
- * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
- * the volume panel and sets focus on `MuteToggle`.
- *
- * @param {Event} event
- * The `keyup` event that caused this function to be called.
- *
- * @listens keyup
- */
- handleVolumeControlKeyUp(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- this.muteToggle.focus();
- }
- }
-
- /**
- * This gets called when a `VolumePanel` gains hover via a `mouseover` event.
- * Turns on listening for `mouseover` event. When they happen it
- * calls `this.handleMouseOver`.
- *
- * @param {Event} event
- * The `mouseover` event that caused this function to be called.
- *
- * @listens mouseover
- */
- handleMouseOver(event) {
- this.addClass('vjs-hover');
- on(document, 'keyup', this.handleKeyPressHandler_);
- }
-
- /**
- * This gets called when a `VolumePanel` gains hover via a `mouseout` event.
- * Turns on listening for `mouseout` event. When they happen it
- * calls `this.handleMouseOut`.
- *
- * @param {Event} event
- * The `mouseout` event that caused this function to be called.
- *
- * @listens mouseout
- */
- handleMouseOut(event) {
- this.removeClass('vjs-hover');
- off(document, 'keyup', this.handleKeyPressHandler_);
- }
-
- /**
- * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
- * looking for ESC, which hides the `VolumeControl`.
- *
- * @param {Event} event
- * The keypress that triggered this event.
- *
- * @listens keydown | keyup
- */
- handleKeyPress(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- this.handleMouseOut();
- }
- }
- }
-
- /**
- * Default options for the `VolumeControl`
- *
- * @type {Object}
- * @private
- */
- VolumePanel.prototype.options_ = {
- children: ['muteToggle', 'volumeControl']
- };
- Component.registerComponent('VolumePanel', VolumePanel);
-
- /**
- * Button to skip forward a configurable amount of time
- * through a video. Renders in the control bar.
- *
- * e.g. options: {controlBar: {skipButtons: forward: 5}}
- *
- * @extends Button
- */
- class SkipForward extends Button {
- constructor(player, options) {
- super(player, options);
- this.validOptions = [5, 10, 30];
- this.skipTime = this.getSkipForwardTime();
- if (this.skipTime && this.validOptions.includes(this.skipTime)) {
- this.setIcon(`forward-${this.skipTime}`);
- this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
- this.show();
- } else {
- this.hide();
- }
- }
- getSkipForwardTime() {
- const playerOptions = this.options_.playerOptions;
- return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
- }
- buildCSSClass() {
- return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
- }
-
- /**
- * On click, skips forward in the duration/seekable range by a configurable amount of seconds.
- * If the time left in the duration/seekable range is less than the configured 'skip forward' time,
- * skips to end of duration/seekable range.
- *
- * Handle a click on a `SkipForward` button
- *
- * @param {EventTarget~Event} event
- * The `click` event that caused this function
- * to be called
- */
- handleClick(event) {
- if (isNaN(this.player_.duration())) {
- return;
- }
- const currentVideoTime = this.player_.currentTime();
- const liveTracker = this.player_.liveTracker;
- const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
- let newTime;
- if (currentVideoTime + this.skipTime <= duration) {
- newTime = currentVideoTime + this.skipTime;
- } else {
- newTime = duration;
- }
- this.player_.currentTime(newTime);
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
- }
- }
- SkipForward.prototype.controlText_ = 'Skip Forward';
- Component.registerComponent('SkipForward', SkipForward);
-
- /**
- * Button to skip backward a configurable amount of time
- * through a video. Renders in the control bar.
- *
- * * e.g. options: {controlBar: {skipButtons: backward: 5}}
- *
- * @extends Button
- */
- class SkipBackward extends Button {
- constructor(player, options) {
- super(player, options);
- this.validOptions = [5, 10, 30];
- this.skipTime = this.getSkipBackwardTime();
- if (this.skipTime && this.validOptions.includes(this.skipTime)) {
- this.setIcon(`replay-${this.skipTime}`);
- this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
- this.show();
- } else {
- this.hide();
- }
- }
- getSkipBackwardTime() {
- const playerOptions = this.options_.playerOptions;
- return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
- }
- buildCSSClass() {
- return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
- }
-
- /**
- * On click, skips backward in the video by a configurable amount of seconds.
- * If the current time in the video is less than the configured 'skip backward' time,
- * skips to beginning of video or seekable range.
- *
- * Handle a click on a `SkipBackward` button
- *
- * @param {EventTarget~Event} event
- * The `click` event that caused this function
- * to be called
- */
- handleClick(event) {
- const currentVideoTime = this.player_.currentTime();
- const liveTracker = this.player_.liveTracker;
- const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
- let newTime;
- if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
- newTime = seekableStart;
- } else if (currentVideoTime >= this.skipTime) {
- newTime = currentVideoTime - this.skipTime;
- } else {
- newTime = 0;
- }
- this.player_.currentTime(newTime);
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
- }
- }
- SkipBackward.prototype.controlText_ = 'Skip Backward';
- Component.registerComponent('SkipBackward', SkipBackward);
-
- /**
- * @file menu.js
- */
-
- /**
- * The Menu component is used to build popup menus, including subtitle and
- * captions selection menus.
- *
- * @extends Component
- */
- class Menu extends Component {
- /**
- * Create an instance of this class.
- *
- * @param { import('../player').default } player
- * the player that this component should attach to
- *
- * @param {Object} [options]
- * Object of option names and values
- *
- */
- constructor(player, options) {
- super(player, options);
- if (options) {
- this.menuButton_ = options.menuButton;
- }
- this.focusedChild_ = -1;
- this.on('keydown', e => this.handleKeyDown(e));
-
- // All the menu item instances share the same blur handler provided by the menu container.
- this.boundHandleBlur_ = e => this.handleBlur(e);
- this.boundHandleTapClick_ = e => this.handleTapClick(e);
- }
-
- /**
- * Add event listeners to the {@link MenuItem}.
- *
- * @param {Object} component
- * The instance of the `MenuItem` to add listeners to.
- *
- */
- addEventListenerForItem(component) {
- if (!(component instanceof Component)) {
- return;
- }
- this.on(component, 'blur', this.boundHandleBlur_);
- this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
- }
-
- /**
- * Remove event listeners from the {@link MenuItem}.
- *
- * @param {Object} component
- * The instance of the `MenuItem` to remove listeners.
- *
- */
- removeEventListenerForItem(component) {
- if (!(component instanceof Component)) {
- return;
- }
- this.off(component, 'blur', this.boundHandleBlur_);
- this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
- }
-
- /**
- * This method will be called indirectly when the component has been added
- * before the component adds to the new menu instance by `addItem`.
- * In this case, the original menu instance will remove the component
- * by calling `removeChild`.
- *
- * @param {Object} component
- * The instance of the `MenuItem`
- */
- removeChild(component) {
- if (typeof component === 'string') {
- component = this.getChild(component);
- }
- this.removeEventListenerForItem(component);
- super.removeChild(component);
- }
-
- /**
- * Add a {@link MenuItem} to the menu.
- *
- * @param {Object|string} component
- * The name or instance of the `MenuItem` to add.
- *
- */
- addItem(component) {
- const childComponent = this.addChild(component);
- if (childComponent) {
- this.addEventListenerForItem(childComponent);
- }
- }
-
- /**
- * Create the `Menu`s DOM element.
- *
- * @return {Element}
- * the element that was created
- */
- createEl() {
- const contentElType = this.options_.contentElType || 'ul';
- this.contentEl_ = createEl(contentElType, {
- className: 'vjs-menu-content'
- });
- this.contentEl_.setAttribute('role', 'menu');
- const el = super.createEl('div', {
- append: this.contentEl_,
- className: 'vjs-menu'
- });
- el.appendChild(this.contentEl_);
-
- // Prevent clicks from bubbling up. Needed for Menu Buttons,
- // where a click on the parent is significant
- on(el, 'click', function (event) {
- event.preventDefault();
- event.stopImmediatePropagation();
- });
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- this.boundHandleBlur_ = null;
- this.boundHandleTapClick_ = null;
- super.dispose();
- }
-
- /**
- * Called when a `MenuItem` loses focus.
- *
- * @param {Event} event
- * The `blur` event that caused this function to be called.
- *
- * @listens blur
- */
- handleBlur(event) {
- const relatedTarget = event.relatedTarget || document.activeElement;
-
- // Close menu popup when a user clicks outside the menu
- if (!this.children().some(element => {
- return element.el() === relatedTarget;
- })) {
- const btn = this.menuButton_;
- if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
- btn.unpressButton();
- }
- }
- }
-
- /**
- * Called when a `MenuItem` gets clicked or tapped.
- *
- * @param {Event} event
- * The `click` or `tap` event that caused this function to be called.
- *
- * @listens click,tap
- */
- handleTapClick(event) {
- // Unpress the associated MenuButton, and move focus back to it
- if (this.menuButton_) {
- this.menuButton_.unpressButton();
- const childComponents = this.children();
- if (!Array.isArray(childComponents)) {
- return;
- }
- const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
- if (!foundComponent) {
- return;
- }
-
- // don't focus menu button if item is a caption settings item
- // because focus will move elsewhere
- if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
- this.menuButton_.focus();
- }
- }
- }
-
- /**
- * Handle a `keydown` event on this menu. This listener is added in the constructor.
- *
- * @param {KeyboardEvent} event
- * A `keydown` event that happened on the menu.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Left and Down Arrows
- if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepForward();
-
- // Up and Right Arrows
- } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepBack();
- }
- }
-
- /**
- * Move to next (lower) menu item for keyboard users.
- */
- stepForward() {
- let stepChild = 0;
- if (this.focusedChild_ !== undefined) {
- stepChild = this.focusedChild_ + 1;
- }
- this.focus(stepChild);
- }
-
- /**
- * Move to previous (higher) menu item for keyboard users.
- */
- stepBack() {
- let stepChild = 0;
- if (this.focusedChild_ !== undefined) {
- stepChild = this.focusedChild_ - 1;
- }
- this.focus(stepChild);
- }
-
- /**
- * Set focus on a {@link MenuItem} in the `Menu`.
- *
- * @param {Object|string} [item=0]
- * Index of child item set focus on.
- */
- focus(item = 0) {
- const children = this.children().slice();
- const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
- if (haveTitle) {
- children.shift();
- }
- if (children.length > 0) {
- if (item < 0) {
- item = 0;
- } else if (item >= children.length) {
- item = children.length - 1;
- }
- this.focusedChild_ = item;
- children[item].el_.focus();
- }
- }
- }
- Component.registerComponent('Menu', Menu);
-
- /**
- * @file menu-button.js
- */
-
- /**
- * A `MenuButton` class for any popup {@link Menu}.
- *
- * @extends Component
- */
- class MenuButton extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- super(player, options);
- this.menuButton_ = new Button(player, options);
- this.menuButton_.controlText(this.controlText_);
- this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
-
- // Add buildCSSClass values to the button, not the wrapper
- const buttonClass = Button.prototype.buildCSSClass();
- this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
- this.menuButton_.removeClass('vjs-control');
- this.addChild(this.menuButton_);
- this.update();
- this.enabled_ = true;
- const handleClick = e => this.handleClick(e);
- this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
- this.on(this.menuButton_, 'tap', handleClick);
- this.on(this.menuButton_, 'click', handleClick);
- this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
- this.on(this.menuButton_, 'mouseenter', () => {
- this.addClass('vjs-hover');
- this.menu.show();
- on(document, 'keyup', this.handleMenuKeyUp_);
- });
- this.on('mouseleave', e => this.handleMouseLeave(e));
- this.on('keydown', e => this.handleSubmenuKeyDown(e));
- }
-
- /**
- * Update the menu based on the current state of its items.
- */
- update() {
- const menu = this.createMenu();
- if (this.menu) {
- this.menu.dispose();
- this.removeChild(this.menu);
- }
- this.menu = menu;
- this.addChild(menu);
-
- /**
- * Track the state of the menu button
- *
- * @type {Boolean}
- * @private
- */
- this.buttonPressed_ = false;
- this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- if (this.items && this.items.length <= this.hideThreshold_) {
- this.hide();
- this.menu.contentEl_.removeAttribute('role');
- } else {
- this.show();
- this.menu.contentEl_.setAttribute('role', 'menu');
- }
- }
-
- /**
- * Create the menu and add all items to it.
- *
- * @return {Menu}
- * The constructed menu
- */
- createMenu() {
- const menu = new Menu(this.player_, {
- menuButton: this
- });
-
- /**
- * Hide the menu if the number of items is less than or equal to this threshold. This defaults
- * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
- * it here because every time we run `createMenu` we need to reset the value.
- *
- * @protected
- * @type {Number}
- */
- this.hideThreshold_ = 0;
-
- // Add a title list item to the top
- if (this.options_.title) {
- const titleEl = createEl('li', {
- className: 'vjs-menu-title',
- textContent: toTitleCase(this.options_.title),
- tabIndex: -1
- });
- const titleComponent = new Component(this.player_, {
- el: titleEl
- });
- menu.addItem(titleComponent);
- }
- this.items = this.createItems();
- if (this.items) {
- // Add menu items to the menu
- for (let i = 0; i < this.items.length; i++) {
- menu.addItem(this.items[i]);
- }
- }
- return menu;
- }
-
- /**
- * Create the list of menu items. Specific to each subclass.
- *
- * @abstract
- */
- createItems() {}
-
- /**
- * Create the `MenuButtons`s DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildWrapperCSSClass()
- }, {});
- }
-
- /**
- * Overwrites the `setIcon` method from `Component`.
- * In this case, we want the icon to be appended to the menuButton.
- *
- * @param {string} name
- * The icon name to be added.
- */
- setIcon(name) {
- super.setIcon(name, this.menuButton_.el_);
- }
-
- /**
- * Allow sub components to stack CSS class names for the wrapper element
- *
- * @return {string}
- * The constructed wrapper DOM `className`
- */
- buildWrapperCSSClass() {
- let menuButtonClass = 'vjs-menu-button';
-
- // If the inline option is passed, we want to use different styles altogether.
- if (this.options_.inline === true) {
- menuButtonClass += '-inline';
- } else {
- menuButtonClass += '-popup';
- }
-
- // TODO: Fix the CSS so that this isn't necessary
- const buttonClass = Button.prototype.buildCSSClass();
- return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- let menuButtonClass = 'vjs-menu-button';
-
- // If the inline option is passed, we want to use different styles altogether.
- if (this.options_.inline === true) {
- menuButtonClass += '-inline';
- } else {
- menuButtonClass += '-popup';
- }
- return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
- }
-
- /**
- * Get or set the localized control text that will be used for accessibility.
- *
- * > NOTE: This will come from the internal `menuButton_` element.
- *
- * @param {string} [text]
- * Control text for element.
- *
- * @param {Element} [el=this.menuButton_.el()]
- * Element to set the title on.
- *
- * @return {string}
- * - The control text when getting
- */
- controlText(text, el = this.menuButton_.el()) {
- return this.menuButton_.controlText(text, el);
- }
-
- /**
- * Dispose of the `menu-button` and all child components.
- */
- dispose() {
- this.handleMouseLeave();
- super.dispose();
- }
-
- /**
- * Handle a click on a `MenuButton`.
- * See {@link ClickableComponent#handleClick} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (this.buttonPressed_) {
- this.unpressButton();
- } else {
- this.pressButton();
- }
- }
-
- /**
- * Handle `mouseleave` for `MenuButton`.
- *
- * @param {Event} event
- * The `mouseleave` event that caused this function to be called.
- *
- * @listens mouseleave
- */
- handleMouseLeave(event) {
- this.removeClass('vjs-hover');
- off(document, 'keyup', this.handleMenuKeyUp_);
- }
-
- /**
- * Set the focus to the actual button, not to this element
- */
- focus() {
- this.menuButton_.focus();
- }
-
- /**
- * Remove the focus from the actual button, not this element
- */
- blur() {
- this.menuButton_.blur();
- }
-
- /**
- * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
- * {@link ClickableComponent#handleKeyDown} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Escape or Tab unpress the 'button'
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- if (this.buttonPressed_) {
- this.unpressButton();
- }
-
- // Don't preventDefault for Tab key - we still want to lose focus
- if (!keycode.isEventKey(event, 'Tab')) {
- event.preventDefault();
- // Set focus back to the menu button's button
- this.menuButton_.focus();
- }
- // Up Arrow or Down Arrow also 'press' the button to open the menu
- } else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) {
- if (!this.buttonPressed_) {
- event.preventDefault();
- this.pressButton();
- }
- }
- }
-
- /**
- * Handle a `keyup` event on a `MenuButton`. The listener for this is added in
- * the constructor.
- *
- * @param {Event} event
- * Key press event
- *
- * @listens keyup
- */
- handleMenuKeyUp(event) {
- // Escape hides popup menu
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- this.removeClass('vjs-hover');
- }
- }
-
- /**
- * This method name now delegates to `handleSubmenuKeyDown`. This means
- * anyone calling `handleSubmenuKeyPress` will not see their method calls
- * stop working.
- *
- * @param {Event} event
- * The event that caused this function to be called.
- */
- handleSubmenuKeyPress(event) {
- this.handleSubmenuKeyDown(event);
- }
-
- /**
- * Handle a `keydown` event on a sub-menu. The listener for this is added in
- * the constructor.
- *
- * @param {Event} event
- * Key press event
- *
- * @listens keydown
- */
- handleSubmenuKeyDown(event) {
- // Escape or Tab unpress the 'button'
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- if (this.buttonPressed_) {
- this.unpressButton();
- }
- // Don't preventDefault for Tab key - we still want to lose focus
- if (!keycode.isEventKey(event, 'Tab')) {
- event.preventDefault();
- // Set focus back to the menu button's button
- this.menuButton_.focus();
- }
- }
- }
-
- /**
- * Put the current `MenuButton` into a pressed state.
- */
- pressButton() {
- if (this.enabled_) {
- this.buttonPressed_ = true;
- this.menu.show();
- this.menu.lockShowing();
- this.menuButton_.el_.setAttribute('aria-expanded', 'true');
-
- // set the focus into the submenu, except on iOS where it is resulting in
- // undesired scrolling behavior when the player is in an iframe
- if (IS_IOS && isInFrame()) {
- // Return early so that the menu isn't focused
- return;
- }
- this.menu.focus();
- }
- }
-
- /**
- * Take the current `MenuButton` out of a pressed state.
- */
- unpressButton() {
- if (this.enabled_) {
- this.buttonPressed_ = false;
- this.menu.unlockShowing();
- this.menu.hide();
- this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- }
- }
-
- /**
- * Disable the `MenuButton`. Don't allow it to be clicked.
- */
- disable() {
- this.unpressButton();
- this.enabled_ = false;
- this.addClass('vjs-disabled');
- this.menuButton_.disable();
- }
-
- /**
- * Enable the `MenuButton`. Allow it to be clicked.
- */
- enable() {
- this.enabled_ = true;
- this.removeClass('vjs-disabled');
- this.menuButton_.enable();
- }
- }
- Component.registerComponent('MenuButton', MenuButton);
-
- /**
- * @file track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific track types (e.g. subtitles).
- *
- * @extends MenuButton
- */
- class TrackButton extends MenuButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const tracks = options.tracks;
- super(player, options);
- if (this.items.length <= 1) {
- this.hide();
- }
- if (!tracks) {
- return;
- }
- const updateHandler = bind_(this, this.update);
- tracks.addEventListener('removetrack', updateHandler);
- tracks.addEventListener('addtrack', updateHandler);
- tracks.addEventListener('labelchange', updateHandler);
- this.player_.on('ready', updateHandler);
- this.player_.on('dispose', function () {
- tracks.removeEventListener('removetrack', updateHandler);
- tracks.removeEventListener('addtrack', updateHandler);
- tracks.removeEventListener('labelchange', updateHandler);
- });
- }
- }
- Component.registerComponent('TrackButton', TrackButton);
-
- /**
- * @file menu-keys.js
- */
-
- /**
- * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`)
- * Note that 'Enter' and 'Space' are not included here (otherwise they would
- * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable)
- *
- * @typedef MenuKeys
- * @array
- */
- const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left'];
-
- /**
- * @file menu-item.js
- */
-
- /**
- * The component for a menu item. ``
- *
- * @extends ClickableComponent
- */
- class MenuItem extends ClickableComponent {
- /**
- * Creates an instance of the this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- *
- */
- constructor(player, options) {
- super(player, options);
- this.selectable = options.selectable;
- this.isSelected_ = options.selected || false;
- this.multiSelectable = options.multiSelectable;
- this.selected(this.isSelected_);
- if (this.selectable) {
- if (this.multiSelectable) {
- this.el_.setAttribute('role', 'menuitemcheckbox');
- } else {
- this.el_.setAttribute('role', 'menuitemradio');
- }
- } else {
- this.el_.setAttribute('role', 'menuitem');
- }
- }
-
- /**
- * Create the `MenuItem's DOM element
- *
- * @param {string} [type=li]
- * Element's node type, not actually used, always set to `li`.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element
- *
- * @param {Object} [attrs={}]
- * An object of attributes that should be set on the element
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(type, props, attrs) {
- // The control is textual, not just an icon
- this.nonIconControl = true;
- const el = super.createEl('li', Object.assign({
- className: 'vjs-menu-item',
- tabIndex: -1
- }, props), attrs);
-
- // swap icon with menu item text.
- const menuItemEl = createEl('span', {
- className: 'vjs-menu-item-text',
- textContent: this.localize(this.options_.label)
- });
-
- // If using SVG icons, the element with vjs-icon-placeholder will be added separately.
- if (this.player_.options_.experimentalSvgIcons) {
- el.appendChild(menuItemEl);
- } else {
- el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder'));
- }
- return el;
- }
-
- /**
- * Ignore keys which are used by the menu, but pass any other ones up. See
- * {@link ClickableComponent#handleKeyDown} for instances where this is called.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- if (!MenuKeys.some(key => keycode.isEventKey(event, key))) {
- // Pass keydown handling up for unused keys
- super.handleKeyDown(event);
- }
- }
-
- /**
- * Any click on a `MenuItem` puts it into the selected state.
- * See {@link ClickableComponent#handleClick} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- this.selected(true);
- }
-
- /**
- * Set the state for this menu item as selected or not.
- *
- * @param {boolean} selected
- * if the menu item is selected or not
- */
- selected(selected) {
- if (this.selectable) {
- if (selected) {
- this.addClass('vjs-selected');
- this.el_.setAttribute('aria-checked', 'true');
- // aria-checked isn't fully supported by browsers/screen readers,
- // so indicate selected state to screen reader in the control text.
- this.controlText(', selected');
- this.isSelected_ = true;
- } else {
- this.removeClass('vjs-selected');
- this.el_.setAttribute('aria-checked', 'false');
- // Indicate un-selected state to screen reader
- this.controlText('');
- this.isSelected_ = false;
- }
- }
- }
- }
- Component.registerComponent('MenuItem', MenuItem);
-
- /**
- * @file text-track-menu-item.js
- */
-
- /**
- * The specific menu item type for selecting a language within a text track kind
- *
- * @extends MenuItem
- */
- class TextTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const tracks = player.textTracks();
-
- // Modify options for parent MenuItem class's init.
- options.label = track.label || track.language || 'Unknown';
- options.selected = track.mode === 'showing';
- super(player, options);
- this.track = track;
- // Determine the relevant kind(s) of tracks for this component and filter
- // out empty kinds.
- this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
- const changeHandler = (...args) => {
- this.handleTracksChange.apply(this, args);
- };
- const selectedLanguageChangeHandler = (...args) => {
- this.handleSelectedLanguageChange.apply(this, args);
- };
- player.on(['loadstart', 'texttrackchange'], changeHandler);
- tracks.addEventListener('change', changeHandler);
- tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
- this.on('dispose', function () {
- player.off(['loadstart', 'texttrackchange'], changeHandler);
- tracks.removeEventListener('change', changeHandler);
- tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
- });
-
- // iOS7 doesn't dispatch change events to TextTrackLists when an
- // associated track's mode changes. Without something like
- // Object.observe() (also not present on iOS7), it's not
- // possible to detect changes to the mode attribute and polyfill
- // the change event. As a poor substitute, we manually dispatch
- // change events whenever the controls modify the mode.
- if (tracks.onchange === undefined) {
- let event;
- this.on(['tap', 'click'], function () {
- if (typeof window.Event !== 'object') {
- // Android 2.3 throws an Illegal Constructor error for window.Event
- try {
- event = new window.Event('change');
- } catch (err) {
- // continue regardless of error
- }
- }
- if (!event) {
- event = document.createEvent('Event');
- event.initEvent('change', true, true);
- }
- tracks.dispatchEvent(event);
- });
- }
-
- // set the default state based on current tracks
- this.handleTracksChange();
- }
-
- /**
- * This gets called when an `TextTrackMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const referenceTrack = this.track;
- const tracks = this.player_.textTracks();
- super.handleClick(event);
- if (!tracks) {
- return;
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // If the track from the text tracks list is not of the right kind,
- // skip it. We do not want to affect tracks of incompatible kind(s).
- if (this.kinds.indexOf(track.kind) === -1) {
- continue;
- }
-
- // If this text track is the component's track and it is not showing,
- // set it to showing.
- if (track === referenceTrack) {
- if (track.mode !== 'showing') {
- track.mode = 'showing';
- }
-
- // If this text track is not the component's track and it is not
- // disabled, set it to disabled.
- } else if (track.mode !== 'disabled') {
- track.mode = 'disabled';
- }
- }
- }
-
- /**
- * Handle text track list change
- *
- * @param {Event} event
- * The `change` event that caused this function to be called.
- *
- * @listens TextTrackList#change
- */
- handleTracksChange(event) {
- const shouldBeSelected = this.track.mode === 'showing';
-
- // Prevent redundant selected() calls because they may cause
- // screen readers to read the appended control text unnecessarily
- if (shouldBeSelected !== this.isSelected_) {
- this.selected(shouldBeSelected);
- }
- }
- handleSelectedLanguageChange(event) {
- if (this.track.mode === 'showing') {
- const selectedLanguage = this.player_.cache_.selectedLanguage;
-
- // Don't replace the kind of track across the same language
- if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
- return;
- }
- this.player_.cache_.selectedLanguage = {
- enabled: true,
- language: this.track.language,
- kind: this.track.kind
- };
- }
- }
- dispose() {
- // remove reference to track object on dispose
- this.track = null;
- super.dispose();
- }
- }
- Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
-
- /**
- * @file off-text-track-menu-item.js
- */
-
- /**
- * A special menu item for turning off a specific type of text track
- *
- * @extends TextTrackMenuItem
- */
- class OffTextTrackMenuItem extends TextTrackMenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- // Create pseudo track info
- // Requires options['kind']
- options.track = {
- player,
- // it is no longer necessary to store `kind` or `kinds` on the track itself
- // since they are now stored in the `kinds` property of all instances of
- // TextTrackMenuItem, but this will remain for backwards compatibility
- kind: options.kind,
- kinds: options.kinds,
- default: false,
- mode: 'disabled'
- };
- if (!options.kinds) {
- options.kinds = [options.kind];
- }
- if (options.label) {
- options.track.label = options.label;
- } else {
- options.track.label = options.kinds.join(' and ') + ' off';
- }
-
- // MenuItem is selectable
- options.selectable = true;
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- options.multiSelectable = false;
- super(player, options);
- }
-
- /**
- * Handle text track change
- *
- * @param {Event} event
- * The event that caused this function to run
- */
- handleTracksChange(event) {
- const tracks = this.player().textTracks();
- let shouldBeSelected = true;
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
- shouldBeSelected = false;
- break;
- }
- }
-
- // Prevent redundant selected() calls because they may cause
- // screen readers to read the appended control text unnecessarily
- if (shouldBeSelected !== this.isSelected_) {
- this.selected(shouldBeSelected);
- }
- }
- handleSelectedLanguageChange(event) {
- const tracks = this.player().textTracks();
- let allHidden = true;
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
- allHidden = false;
- break;
- }
- }
- if (allHidden) {
- this.player_.cache_.selectedLanguage = {
- enabled: false
- };
- }
- }
-
- /**
- * Update control text and label on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
- super.handleLanguagechange();
- }
- }
- Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
-
- /**
- * @file text-track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific text track types (e.g. subtitles)
- *
- * @extends MenuButton
- */
- class TextTrackButton extends TrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.tracks = player.textTracks();
- super(player, options);
- }
-
- /**
- * Create a menu item for each text track
- *
- * @param {TextTrackMenuItem[]} [items=[]]
- * Existing array of items to use during creation
- *
- * @return {TextTrackMenuItem[]}
- * Array of menu items that were created
- */
- createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
- // Label is an override for the [track] off label
- // USed to localise captions/subtitles
- let label;
- if (this.label_) {
- label = `${this.label_} off`;
- }
- // Add an OFF menu item to turn all tracks off
- items.push(new OffTextTrackMenuItem(this.player_, {
- kinds: this.kinds_,
- kind: this.kind_,
- label
- }));
- this.hideThreshold_ += 1;
- const tracks = this.player_.textTracks();
- if (!Array.isArray(this.kinds_)) {
- this.kinds_ = [this.kind_];
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // only add tracks that are of an appropriate kind and have a label
- if (this.kinds_.indexOf(track.kind) > -1) {
- const item = new TrackMenuItem(this.player_, {
- track,
- kinds: this.kinds_,
- kind: this.kind_,
- // MenuItem is selectable
- selectable: true,
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- multiSelectable: false
- });
- item.addClass(`vjs-${track.kind}-menu-item`);
- items.push(item);
- }
- }
- return items;
- }
- }
- Component.registerComponent('TextTrackButton', TextTrackButton);
-
- /**
- * @file chapters-track-menu-item.js
- */
-
- /**
- * The chapter track menu item
- *
- * @extends MenuItem
- */
- class ChaptersTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const cue = options.cue;
- const currentTime = player.currentTime();
-
- // Modify options for parent MenuItem class's init.
- options.selectable = true;
- options.multiSelectable = false;
- options.label = cue.text;
- options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
- super(player, options);
- this.track = track;
- this.cue = cue;
- }
-
- /**
- * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick();
- this.player_.currentTime(this.cue.startTime);
- }
- }
- Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
-
- /**
- * @file chapters-button.js
- */
-
- /**
- * The button component for toggling and selecting chapters
- * Chapters act much differently than other text tracks
- * Cues are navigation vs. other tracks of alternative languages
- *
- * @extends TextTrackButton
- */
- class ChaptersButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this function is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('chapters');
- this.selectCurrentItem_ = () => {
- this.items.forEach(item => {
- item.selected(this.track_.activeCues[0] === item.cue);
- });
- };
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-chapters-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Update the menu based on the current state of its items.
- *
- * @param {Event} [event]
- * An event that triggered this function to run.
- *
- * @listens TextTrackList#addtrack
- * @listens TextTrackList#removetrack
- * @listens TextTrackList#change
- */
- update(event) {
- if (event && event.track && event.track.kind !== 'chapters') {
- return;
- }
- const track = this.findChaptersTrack();
- if (track !== this.track_) {
- this.setTrack(track);
- super.update();
- } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
- // Update the menu initially or if the number of cues has changed since set
- super.update();
- }
- }
-
- /**
- * Set the currently selected track for the chapters button.
- *
- * @param {TextTrack} track
- * The new track to select. Nothing will change if this is the currently selected
- * track.
- */
- setTrack(track) {
- if (this.track_ === track) {
- return;
- }
- if (!this.updateHandler_) {
- this.updateHandler_ = this.update.bind(this);
- }
-
- // here this.track_ refers to the old track instance
- if (this.track_) {
- const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
- if (remoteTextTrackEl) {
- remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
- }
- this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
- this.track_ = null;
- }
- this.track_ = track;
-
- // here this.track_ refers to the new track instance
- if (this.track_) {
- this.track_.mode = 'hidden';
- const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
- if (remoteTextTrackEl) {
- remoteTextTrackEl.addEventListener('load', this.updateHandler_);
- }
- this.track_.addEventListener('cuechange', this.selectCurrentItem_);
- }
- }
-
- /**
- * Find the track object that is currently in use by this ChaptersButton
- *
- * @return {TextTrack|undefined}
- * The current track or undefined if none was found.
- */
- findChaptersTrack() {
- const tracks = this.player_.textTracks() || [];
- for (let i = tracks.length - 1; i >= 0; i--) {
- // We will always choose the last track as our chaptersTrack
- const track = tracks[i];
- if (track.kind === this.kind_) {
- return track;
- }
- }
- }
-
- /**
- * Get the caption for the ChaptersButton based on the track label. This will also
- * use the current tracks localized kind as a fallback if a label does not exist.
- *
- * @return {string}
- * The tracks current label or the localized track kind.
- */
- getMenuCaption() {
- if (this.track_ && this.track_.label) {
- return this.track_.label;
- }
- return this.localize(toTitleCase(this.kind_));
- }
-
- /**
- * Create menu from chapter track
- *
- * @return { import('../../menu/menu').default }
- * New menu for the chapter buttons
- */
- createMenu() {
- this.options_.title = this.getMenuCaption();
- return super.createMenu();
- }
-
- /**
- * Create a menu item for each text track
- *
- * @return { import('./text-track-menu-item').default[] }
- * Array of menu items
- */
- createItems() {
- const items = [];
- if (!this.track_) {
- return items;
- }
- const cues = this.track_.cues;
- if (!cues) {
- return items;
- }
- for (let i = 0, l = cues.length; i < l; i++) {
- const cue = cues[i];
- const mi = new ChaptersTrackMenuItem(this.player_, {
- track: this.track_,
- cue
- });
- items.push(mi);
- }
- return items;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- ChaptersButton.prototype.kind_ = 'chapters';
-
- /**
- * The text that should display over the `ChaptersButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- ChaptersButton.prototype.controlText_ = 'Chapters';
- Component.registerComponent('ChaptersButton', ChaptersButton);
-
- /**
- * @file descriptions-button.js
- */
-
- /**
- * The button component for toggling and selecting descriptions
- *
- * @extends TextTrackButton
- */
- class DescriptionsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('audio-description');
- const tracks = player.textTracks();
- const changeHandler = bind_(this, this.handleTracksChange);
- tracks.addEventListener('change', changeHandler);
- this.on('dispose', function () {
- tracks.removeEventListener('change', changeHandler);
- });
- }
-
- /**
- * Handle text track change
- *
- * @param {Event} event
- * The event that caused this function to run
- *
- * @listens TextTrackList#change
- */
- handleTracksChange(event) {
- const tracks = this.player().textTracks();
- let disabled = false;
-
- // Check whether a track of a different kind is showing
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (track.kind !== this.kind_ && track.mode === 'showing') {
- disabled = true;
- break;
- }
- }
-
- // If another track is showing, disable this menu button
- if (disabled) {
- this.disable();
- } else {
- this.enable();
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-descriptions-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- DescriptionsButton.prototype.kind_ = 'descriptions';
-
- /**
- * The text that should display over the `DescriptionsButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- DescriptionsButton.prototype.controlText_ = 'Descriptions';
- Component.registerComponent('DescriptionsButton', DescriptionsButton);
-
- /**
- * @file subtitles-button.js
- */
-
- /**
- * The button component for toggling and selecting subtitles
- *
- * @extends TextTrackButton
- */
- class SubtitlesButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('subtitles');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-subtitles-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- SubtitlesButton.prototype.kind_ = 'subtitles';
-
- /**
- * The text that should display over the `SubtitlesButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- SubtitlesButton.prototype.controlText_ = 'Subtitles';
- Component.registerComponent('SubtitlesButton', SubtitlesButton);
-
- /**
- * @file caption-settings-menu-item.js
- */
-
- /**
- * The menu item for caption track settings menu
- *
- * @extends TextTrackMenuItem
- */
- class CaptionSettingsMenuItem extends TextTrackMenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- options.track = {
- player,
- kind: options.kind,
- label: options.kind + ' settings',
- selectable: false,
- default: false,
- mode: 'disabled'
- };
-
- // CaptionSettingsMenuItem has no concept of 'selected'
- options.selectable = false;
- options.name = 'CaptionSettingsMenuItem';
- super(player, options);
- this.addClass('vjs-texttrack-settings');
- this.controlText(', opens ' + options.kind + ' settings dialog');
- }
-
- /**
- * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- this.player().getChild('textTrackSettings').open();
- }
-
- /**
- * Update control text and label on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
- super.handleLanguagechange();
- }
- }
- Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
-
- /**
- * @file captions-button.js
- */
-
- /**
- * The button component for toggling and selecting captions
- *
- * @extends TextTrackButton
- */
- class CaptionsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('captions');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-captions-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create caption menu items
- *
- * @return {CaptionSettingsMenuItem[]}
- * The array of current menu items.
- */
- createItems() {
- const items = [];
- if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
- items.push(new CaptionSettingsMenuItem(this.player_, {
- kind: this.kind_
- }));
- this.hideThreshold_ += 1;
- }
- return super.createItems(items);
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- CaptionsButton.prototype.kind_ = 'captions';
-
- /**
- * The text that should display over the `CaptionsButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- CaptionsButton.prototype.controlText_ = 'Captions';
- Component.registerComponent('CaptionsButton', CaptionsButton);
-
- /**
- * @file subs-caps-menu-item.js
- */
-
- /**
- * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
- * in the SubsCapsMenu.
- *
- * @extends TextTrackMenuItem
- */
- class SubsCapsMenuItem extends TextTrackMenuItem {
- createEl(type, props, attrs) {
- const el = super.createEl(type, props, attrs);
- const parentSpan = el.querySelector('.vjs-menu-item-text');
- if (this.options_.track.kind === 'captions') {
- if (this.player_.options_.experimentalSvgIcons) {
- this.setIcon('captions', el);
- } else {
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-control-text',
- // space added as the text will visually flow with the
- // label
- textContent: ` ${this.localize('Captions')}`
- }));
- }
- return el;
- }
- }
- Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
-
- /**
- * @file sub-caps-button.js
- */
-
- /**
- * The button component for toggling and selecting captions and/or subtitles
- *
- * @extends TextTrackButton
- */
- class SubsCapsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options = {}) {
- super(player, options);
-
- // Although North America uses "captions" in most cases for
- // "captions and subtitles" other locales use "subtitles"
- this.label_ = 'subtitles';
- this.setIcon('subtitles');
- if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
- this.label_ = 'captions';
- this.setIcon('captions');
- }
- this.menuButton_.controlText(toTitleCase(this.label_));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-subs-caps-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create caption/subtitles menu items
- *
- * @return {CaptionSettingsMenuItem[]}
- * The array of current menu items.
- */
- createItems() {
- let items = [];
- if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
- items.push(new CaptionSettingsMenuItem(this.player_, {
- kind: this.label_
- }));
- this.hideThreshold_ += 1;
- }
- items = super.createItems(items, SubsCapsMenuItem);
- return items;
- }
- }
-
- /**
- * `kind`s of TextTrack to look for to associate it with this menu.
- *
- * @type {array}
- * @private
- */
- SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
-
- /**
- * The text that should display over the `SubsCapsButton`s controls.
- *
- *
- * @type {string}
- * @protected
- */
- SubsCapsButton.prototype.controlText_ = 'Subtitles';
- Component.registerComponent('SubsCapsButton', SubsCapsButton);
-
- /**
- * @file audio-track-menu-item.js
- */
-
- /**
- * An {@link AudioTrack} {@link MenuItem}
- *
- * @extends MenuItem
- */
- class AudioTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const tracks = player.audioTracks();
-
- // Modify options for parent MenuItem class's init.
- options.label = track.label || track.language || 'Unknown';
- options.selected = track.enabled;
- super(player, options);
- this.track = track;
- this.addClass(`vjs-${track.kind}-menu-item`);
- const changeHandler = (...args) => {
- this.handleTracksChange.apply(this, args);
- };
- tracks.addEventListener('change', changeHandler);
- this.on('dispose', () => {
- tracks.removeEventListener('change', changeHandler);
- });
- }
- createEl(type, props, attrs) {
- const el = super.createEl(type, props, attrs);
- const parentSpan = el.querySelector('.vjs-menu-item-text');
- if (['main-desc', 'description'].indexOf(this.options_.track.kind) >= 0) {
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-control-text',
- textContent: ' ' + this.localize('Descriptions')
- }));
- }
- return el;
- }
-
- /**
- * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
- * for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick(event);
-
- // the audio track list will automatically toggle other tracks
- // off for us.
- this.track.enabled = true;
-
- // when native audio tracks are used, we want to make sure that other tracks are turned off
- if (this.player_.tech_.featuresNativeAudioTracks) {
- const tracks = this.player_.audioTracks();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // skip the current track since we enabled it above
- if (track === this.track) {
- continue;
- }
- track.enabled = track === this.track;
- }
- }
- }
-
- /**
- * Handle any {@link AudioTrack} change.
- *
- * @param {Event} [event]
- * The {@link AudioTrackList#change} event that caused this to run.
- *
- * @listens AudioTrackList#change
- */
- handleTracksChange(event) {
- this.selected(this.track.enabled);
- }
- }
- Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
-
- /**
- * @file audio-track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific {@link AudioTrack} types.
- *
- * @extends TrackButton
- */
- class AudioTrackButton extends TrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param {Player} player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.tracks = player.audioTracks();
- super(player, options);
- this.setIcon('audio');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-audio-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create a menu item for each audio track
- *
- * @param {AudioTrackMenuItem[]} [items=[]]
- * An array of existing menu items to use.
- *
- * @return {AudioTrackMenuItem[]}
- * An array of menu items
- */
- createItems(items = []) {
- // if there's only one audio track, there no point in showing it
- this.hideThreshold_ = 1;
- const tracks = this.player_.audioTracks();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- items.push(new AudioTrackMenuItem(this.player_, {
- track,
- // MenuItem is selectable
- selectable: true,
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- multiSelectable: false
- }));
- }
- return items;
- }
- }
-
- /**
- * The text that should display over the `AudioTrackButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- AudioTrackButton.prototype.controlText_ = 'Audio Track';
- Component.registerComponent('AudioTrackButton', AudioTrackButton);
-
- /**
- * @file playback-rate-menu-item.js
- */
-
- /**
- * The specific menu item type for selecting a playback rate.
- *
- * @extends MenuItem
- */
- class PlaybackRateMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const label = options.rate;
- const rate = parseFloat(label, 10);
-
- // Modify options for parent MenuItem class's init.
- options.label = label;
- options.selected = rate === player.playbackRate();
- options.selectable = true;
- options.multiSelectable = false;
- super(player, options);
- this.label = label;
- this.rate = rate;
- this.on(player, 'ratechange', e => this.update(e));
- }
-
- /**
- * This gets called when an `PlaybackRateMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick();
- this.player().playbackRate(this.rate);
- }
-
- /**
- * Update the PlaybackRateMenuItem when the playbackrate changes.
- *
- * @param {Event} [event]
- * The `ratechange` event that caused this function to run.
- *
- * @listens Player#ratechange
- */
- update(event) {
- this.selected(this.player().playbackRate() === this.rate);
- }
- }
-
- /**
- * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
- *
- * @type {string}
- * @private
- */
- PlaybackRateMenuItem.prototype.contentElType = 'button';
- Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
-
- /**
- * @file playback-rate-menu-button.js
- */
-
- /**
- * The component for controlling the playback rate.
- *
- * @extends MenuButton
- */
- class PlaybackRateMenuButton extends MenuButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
- this.updateVisibility();
- this.updateLabel();
- this.on(player, 'loadstart', e => this.updateVisibility(e));
- this.on(player, 'ratechange', e => this.updateLabel(e));
- this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl();
- this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
- this.labelEl_ = createEl('div', {
- className: 'vjs-playback-rate-value',
- id: this.labelElId_,
- textContent: '1x'
- });
- el.appendChild(this.labelEl_);
- return el;
- }
- dispose() {
- this.labelEl_ = null;
- super.dispose();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-playback-rate ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create the list of menu items. Specific to each subclass.
- *
- */
- createItems() {
- const rates = this.playbackRates();
- const items = [];
- for (let i = rates.length - 1; i >= 0; i--) {
- items.push(new PlaybackRateMenuItem(this.player(), {
- rate: rates[i] + 'x'
- }));
- }
- return items;
- }
-
- /**
- * On playbackrateschange, update the menu to account for the new items.
- *
- * @listens Player#playbackrateschange
- */
- handlePlaybackRateschange(event) {
- this.update();
- }
-
- /**
- * Get possible playback rates
- *
- * @return {Array}
- * All possible playback rates
- */
- playbackRates() {
- const player = this.player();
- return player.playbackRates && player.playbackRates() || [];
- }
-
- /**
- * Get whether playback rates is supported by the tech
- * and an array of playback rates exists
- *
- * @return {boolean}
- * Whether changing playback rate is supported
- */
- playbackRateSupported() {
- return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
- }
-
- /**
- * Hide playback rate controls when they're no playback rate options to select
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#loadstart
- */
- updateVisibility(event) {
- if (this.playbackRateSupported()) {
- this.removeClass('vjs-hidden');
- } else {
- this.addClass('vjs-hidden');
- }
- }
-
- /**
- * Update button label when rate changed
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#ratechange
- */
- updateLabel(event) {
- if (this.playbackRateSupported()) {
- this.labelEl_.textContent = this.player().playbackRate() + 'x';
- }
- }
- }
-
- /**
- * The text that should display over the `PlaybackRateMenuButton`s controls.
- *
- * Added for localization.
- *
- * @type {string}
- * @protected
- */
- PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
- Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
-
- /**
- * @file spacer.js
- */
-
- /**
- * Just an empty spacer element that can be used as an append point for plugins, etc.
- * Also can be used to create space between elements when necessary.
- *
- * @extends Component
- */
- class Spacer extends Component {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-spacer ${super.buildCSSClass()}`;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl(tag = 'div', props = {}, attributes = {}) {
- if (!props.className) {
- props.className = this.buildCSSClass();
- }
- return super.createEl(tag, props, attributes);
- }
- }
- Component.registerComponent('Spacer', Spacer);
-
- /**
- * @file custom-control-spacer.js
- */
-
- /**
- * Spacer specifically meant to be used as an insertion point for new plugins, etc.
- *
- * @extends Spacer
- */
- class CustomControlSpacer extends Spacer {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildCSSClass(),
- // No-flex/table-cell mode requires there be some content
- // in the cell to fill the remaining space of the table.
- textContent: '\u00a0'
- });
- }
- }
- Component.registerComponent('CustomControlSpacer', CustomControlSpacer);
-
- /**
- * @file control-bar.js
- */
-
- /**
- * Container of main controls.
- *
- * @extends Component
- */
- class ControlBar extends Component {
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-control-bar',
- dir: 'ltr'
- });
- }
- }
-
- /**
- * Default options for `ControlBar`
- *
- * @type {Object}
- * @private
- */
- ControlBar.prototype.options_ = {
- children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
- };
- Component.registerComponent('ControlBar', ControlBar);
-
- /**
- * @file error-display.js
- */
-
- /**
- * A display that indicates an error has occurred. This means that the video
- * is unplayable.
- *
- * @extends ModalDialog
- */
- class ErrorDisplay extends ModalDialog {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, 'error', e => {
- this.open(e);
- });
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- *
- * @deprecated Since version 5.
- */
- buildCSSClass() {
- return `vjs-error-display ${super.buildCSSClass()}`;
- }
-
- /**
- * Gets the localized error message based on the `Player`s error.
- *
- * @return {string}
- * The `Player`s error message localized or an empty string.
- */
- content() {
- const error = this.player().error();
- return error ? this.localize(error.message) : '';
- }
- }
-
- /**
- * The default options for an `ErrorDisplay`.
- *
- * @private
- */
- ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
- pauseOnOpen: false,
- fillAlways: true,
- temporary: false,
- uncloseable: true
- });
- Component.registerComponent('ErrorDisplay', ErrorDisplay);
-
- /**
- * @file text-track-settings.js
- */
- const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
- const COLOR_BLACK = ['#000', 'Black'];
- const COLOR_BLUE = ['#00F', 'Blue'];
- const COLOR_CYAN = ['#0FF', 'Cyan'];
- const COLOR_GREEN = ['#0F0', 'Green'];
- const COLOR_MAGENTA = ['#F0F', 'Magenta'];
- const COLOR_RED = ['#F00', 'Red'];
- const COLOR_WHITE = ['#FFF', 'White'];
- const COLOR_YELLOW = ['#FF0', 'Yellow'];
- const OPACITY_OPAQUE = ['1', 'Opaque'];
- const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
- const OPACITY_TRANS = ['0', 'Transparent'];
-
- // Configuration for the various elements in the DOM of this component.
- //
- // Possible keys include:
- //
- // `default`:
- // The default option index. Only needs to be provided if not zero.
- // `parser`:
- // A function which is used to parse the value from the selected option in
- // a customized way.
- // `selector`:
- // The selector used to find the associated element.
- const selectConfigs = {
- backgroundColor: {
- selector: '.vjs-bg-color > select',
- id: 'captions-background-color-%s',
- label: 'Color',
- options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
- },
- backgroundOpacity: {
- selector: '.vjs-bg-opacity > select',
- id: 'captions-background-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS]
- },
- color: {
- selector: '.vjs-text-color > select',
- id: 'captions-foreground-color-%s',
- label: 'Color',
- options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
- },
- edgeStyle: {
- selector: '.vjs-edge-style > select',
- id: '%s',
- label: 'Text Edge Style',
- options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']]
- },
- fontFamily: {
- selector: '.vjs-font-family > select',
- id: 'captions-font-family-%s',
- label: 'Font Family',
- options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
- },
- fontPercent: {
- selector: '.vjs-font-percent > select',
- id: 'captions-font-size-%s',
- label: 'Font Size',
- options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']],
- default: 2,
- parser: v => v === '1.00' ? null : Number(v)
- },
- textOpacity: {
- selector: '.vjs-text-opacity > select',
- id: 'captions-foreground-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_OPAQUE, OPACITY_SEMI]
- },
- // Options for this object are defined below.
- windowColor: {
- selector: '.vjs-window-color > select',
- id: 'captions-window-color-%s',
- label: 'Color'
- },
- // Options for this object are defined below.
- windowOpacity: {
- selector: '.vjs-window-opacity > select',
- id: 'captions-window-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE]
- }
- };
- selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
-
- /**
- * Get the actual value of an option.
- *
- * @param {string} value
- * The value to get
- *
- * @param {Function} [parser]
- * Optional function to adjust the value.
- *
- * @return {*}
- * - Will be `undefined` if no value exists
- * - Will be `undefined` if the given value is "none".
- * - Will be the actual value otherwise.
- *
- * @private
- */
- function parseOptionValue(value, parser) {
- if (parser) {
- value = parser(value);
- }
- if (value && value !== 'none') {
- return value;
- }
- }
-
- /**
- * Gets the value of the selected element within a element.
- *
- * @param {Element} el
- * the element to look in
- *
- * @param {Function} [parser]
- * Optional function to adjust the value.
- *
- * @return {*}
- * - Will be `undefined` if no value exists
- * - Will be `undefined` if the given value is "none".
- * - Will be the actual value otherwise.
- *
- * @private
- */
- function getSelectedOptionValue(el, parser) {
- const value = el.options[el.options.selectedIndex].value;
- return parseOptionValue(value, parser);
- }
-
- /**
- * Sets the selected element within a element based on a
- * given value.
- *
- * @param {Element} el
- * The element to look in.
- *
- * @param {string} value
- * the property to look on.
- *
- * @param {Function} [parser]
- * Optional function to adjust the value before comparing.
- *
- * @private
- */
- function setSelectedOption(el, value, parser) {
- if (!value) {
- return;
- }
- for (let i = 0; i < el.options.length; i++) {
- if (parseOptionValue(el.options[i].value, parser) === value) {
- el.selectedIndex = i;
- break;
- }
- }
- }
-
- /**
- * Manipulate Text Tracks settings.
- *
- * @extends ModalDialog
- */
- class TextTrackSettings extends ModalDialog {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- options.temporary = false;
- super(player, options);
- this.updateDisplay = this.updateDisplay.bind(this);
-
- // fill the modal and pretend we have opened it
- this.fill();
- this.hasBeenOpened_ = this.hasBeenFilled_ = true;
- this.endDialog = createEl('p', {
- className: 'vjs-control-text',
- textContent: this.localize('End of dialog window.')
- });
- this.el().appendChild(this.endDialog);
- this.setDefaults();
-
- // Grab `persistTextTrackSettings` from the player options if not passed in child options
- if (options.persistTextTrackSettings === undefined) {
- this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
- }
- this.on(this.$('.vjs-done-button'), 'click', () => {
- this.saveSettings();
- this.close();
- });
- this.on(this.$('.vjs-default-button'), 'click', () => {
- this.setDefaults();
- this.updateDisplay();
- });
- each(selectConfigs, config => {
- this.on(this.$(config.selector), 'change', this.updateDisplay);
- });
- if (this.options_.persistTextTrackSettings) {
- this.restoreSettings();
- }
- }
- dispose() {
- this.endDialog = null;
- super.dispose();
- }
-
- /**
- * Create a element with configured options.
- *
- * @param {string} key
- * Configuration key to use during creation.
- *
- * @param {string} [legendId]
- * Id of associated .
- *
- * @param {string} [type=label]
- * Type of labelling element, `label` or `legend`
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElSelect_(key, legendId = '', type = 'label') {
- const config = selectConfigs[key];
- const id = config.id.replace('%s', this.id_);
- const selectLabelledbyIds = [legendId, id].join(' ').trim();
- const guid = `vjs_select_${newGUID()}`;
- return [`<${type} id="${id}"${type === 'label' ? ` for="${guid}" class="vjs-label"` : ''}>`, this.localize(config.label), `${type}>`, ``].concat(config.options.map(o => {
- const optionId = id + '-' + o[1].replace(/\W+/g, '');
- return [``, this.localize(o[1]), ' '].join('');
- })).concat(' ').join('');
- }
-
- /**
- * Create foreground color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElFgColor_() {
- const legendId = `captions-text-legend-${this.id_}`;
- return [' ', ``, this.localize('Text'), ' ', '', this.createElSelect_('color', legendId), ' ', '', this.createElSelect_('textOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create background color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElBgColor_() {
- const legendId = `captions-background-${this.id_}`;
- return ['', ``, this.localize('Text Background'), ' ', '', this.createElSelect_('backgroundColor', legendId), ' ', '', this.createElSelect_('backgroundOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create window color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElWinColor_() {
- const legendId = `captions-window-${this.id_}`;
- return ['', ``, this.localize('Caption Area Background'), ' ', '', this.createElSelect_('windowColor', legendId), ' ', '', this.createElSelect_('windowOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create color elements for the component
- *
- * @return {Element}
- * The element that was created
- *
- * @private
- */
- createElColors_() {
- return createEl('div', {
- className: 'vjs-track-settings-colors',
- innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('')
- });
- }
-
- /**
- * Create font elements for the component
- *
- * @return {Element}
- * The element that was created.
- *
- * @private
- */
- createElFont_() {
- return createEl('div', {
- className: 'vjs-track-settings-font',
- innerHTML: ['', this.createElSelect_('fontPercent', '', 'legend'), ' ', '', this.createElSelect_('edgeStyle', '', 'legend'), ' ', '', this.createElSelect_('fontFamily', '', 'legend'), ' '].join('')
- });
- }
-
- /**
- * Create controls for the component
- *
- * @return {Element}
- * The element that was created.
- *
- * @private
- */
- createElControls_() {
- const defaultsDescription = this.localize('restore all settings to the default values');
- return createEl('div', {
- className: 'vjs-track-settings-controls',
- innerHTML: [``, this.localize('Reset'), ` ${defaultsDescription} `, ' ', `${this.localize('Done')} `].join('')
- });
- }
- content() {
- return [this.createElColors_(), this.createElFont_(), this.createElControls_()];
- }
- label() {
- return this.localize('Caption Settings Dialog');
- }
- description() {
- return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
- }
- buildCSSClass() {
- return super.buildCSSClass() + ' vjs-text-track-settings';
- }
-
- /**
- * Gets an object of text track settings (or null).
- *
- * @return {Object}
- * An object with config values parsed from the DOM or localStorage.
- */
- getValues() {
- return reduce(selectConfigs, (accum, config, key) => {
- const value = getSelectedOptionValue(this.$(config.selector), config.parser);
- if (value !== undefined) {
- accum[key] = value;
- }
- return accum;
- }, {});
- }
-
- /**
- * Sets text track settings from an object of values.
- *
- * @param {Object} values
- * An object with config values parsed from the DOM or localStorage.
- */
- setValues(values) {
- each(selectConfigs, (config, key) => {
- setSelectedOption(this.$(config.selector), values[key], config.parser);
- });
- }
-
- /**
- * Sets all `` elements to their default values.
- */
- setDefaults() {
- each(selectConfigs, config => {
- const index = config.hasOwnProperty('default') ? config.default : 0;
- this.$(config.selector).selectedIndex = index;
- });
- }
-
- /**
- * Restore texttrack settings from localStorage
- */
- restoreSettings() {
- let values;
- try {
- values = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY));
- } catch (err) {
- log.warn(err);
- }
- if (values) {
- this.setValues(values);
- }
- }
-
- /**
- * Save text track settings to localStorage
- */
- saveSettings() {
- if (!this.options_.persistTextTrackSettings) {
- return;
- }
- const values = this.getValues();
- try {
- if (Object.keys(values).length) {
- window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values));
- } else {
- window.localStorage.removeItem(LOCAL_STORAGE_KEY);
- }
- } catch (err) {
- log.warn(err);
- }
- }
-
- /**
- * Update display of text track settings
- */
- updateDisplay() {
- const ttDisplay = this.player_.getChild('textTrackDisplay');
- if (ttDisplay) {
- ttDisplay.updateDisplay();
- }
- }
-
- /**
- * conditionally blur the element and refocus the captions button
- *
- * @private
- */
- conditionalBlur_() {
- this.previouslyActiveEl_ = null;
- const cb = this.player_.controlBar;
- const subsCapsBtn = cb && cb.subsCapsButton;
- const ccBtn = cb && cb.captionsButton;
- if (subsCapsBtn) {
- subsCapsBtn.focus();
- } else if (ccBtn) {
- ccBtn.focus();
- }
- }
-
- /**
- * Repopulate dialog with new localizations on languagechange
- */
- handleLanguagechange() {
- this.fill();
- }
- }
- Component.registerComponent('TextTrackSettings', TextTrackSettings);
-
- /**
- * @file resize-manager.js
- */
-
- /**
- * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
- *
- * It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
- *
- * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
- * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
- *
- * @example How to disable the resize manager
- * const player = videojs('#vid', {
- * resizeManager: false
- * });
- *
- * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
- *
- * @extends Component
- */
- class ResizeManager extends Component {
- /**
- * Create the ResizeManager.
- *
- * @param {Object} player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of ResizeManager options.
- *
- * @param {Object} [options.ResizeObserver]
- * A polyfill for ResizeObserver can be passed in here.
- * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
- */
- constructor(player, options) {
- let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver;
-
- // if `null` was passed, we want to disable the ResizeObserver
- if (options.ResizeObserver === null) {
- RESIZE_OBSERVER_AVAILABLE = false;
- }
-
- // Only create an element when ResizeObserver isn't available
- const options_ = merge({
- createEl: !RESIZE_OBSERVER_AVAILABLE,
- reportTouchActivity: false
- }, options);
- super(player, options_);
- this.ResizeObserver = options.ResizeObserver || window.ResizeObserver;
- this.loadListener_ = null;
- this.resizeObserver_ = null;
- this.debouncedHandler_ = debounce(() => {
- this.resizeHandler();
- }, 100, false, this);
- if (RESIZE_OBSERVER_AVAILABLE) {
- this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
- this.resizeObserver_.observe(player.el());
- } else {
- this.loadListener_ = () => {
- if (!this.el_ || !this.el_.contentWindow) {
- return;
- }
- const debouncedHandler_ = this.debouncedHandler_;
- let unloadListener_ = this.unloadListener_ = function () {
- off(this, 'resize', debouncedHandler_);
- off(this, 'unload', unloadListener_);
- unloadListener_ = null;
- };
-
- // safari and edge can unload the iframe before resizemanager dispose
- // we have to dispose of event handlers correctly before that happens
- on(this.el_.contentWindow, 'unload', unloadListener_);
- on(this.el_.contentWindow, 'resize', debouncedHandler_);
- };
- this.one('load', this.loadListener_);
- }
- }
- createEl() {
- return super.createEl('iframe', {
- className: 'vjs-resize-manager',
- tabIndex: -1,
- title: this.localize('No content')
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
- *
- * @fires Player#playerresize
- */
- resizeHandler() {
- /**
- * Called when the player size has changed
- *
- * @event Player#playerresize
- * @type {Event}
- */
- // make sure player is still around to trigger
- // prevents this from causing an error after dispose
- if (!this.player_ || !this.player_.trigger) {
- return;
- }
- this.player_.trigger('playerresize');
- }
- dispose() {
- if (this.debouncedHandler_) {
- this.debouncedHandler_.cancel();
- }
- if (this.resizeObserver_) {
- if (this.player_.el()) {
- this.resizeObserver_.unobserve(this.player_.el());
- }
- this.resizeObserver_.disconnect();
- }
- if (this.loadListener_) {
- this.off('load', this.loadListener_);
- }
- if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
- this.unloadListener_.call(this.el_.contentWindow);
- }
- this.ResizeObserver = null;
- this.resizeObserver = null;
- this.debouncedHandler_ = null;
- this.loadListener_ = null;
- super.dispose();
- }
- }
- Component.registerComponent('ResizeManager', ResizeManager);
-
- const defaults = {
- trackingThreshold: 20,
- liveTolerance: 15
- };
-
- /*
- track when we are at the live edge, and other helpers for live playback */
-
- /**
- * A class for checking live current time and determining when the player
- * is at or behind the live edge.
- */
- class LiveTracker extends Component {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {number} [options.trackingThreshold=20]
- * Number of seconds of live window (seekableEnd - seekableStart) that
- * media needs to have before the liveui will be shown.
- *
- * @param {number} [options.liveTolerance=15]
- * Number of seconds behind live that we have to be
- * before we will be considered non-live. Note that this will only
- * be used when playing at the live edge. This allows large seekable end
- * changes to not effect whether we are live or not.
- */
- constructor(player, options) {
- // LiveTracker does not need an element
- const options_ = merge(defaults, options, {
- createEl: false
- });
- super(player, options_);
- this.trackLiveHandler_ = () => this.trackLive_();
- this.handlePlay_ = e => this.handlePlay(e);
- this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
- this.handleSeeked_ = e => this.handleSeeked(e);
- this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
- this.reset_();
- this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
- // we should try to toggle tracking on canplay as native playback engines, like Safari
- // may not have the proper values for things like seekableEnd until then
- this.on(this.player_, 'canplay', () => this.toggleTracking());
- }
-
- /**
- * all the functionality for tracking when seek end changes
- * and for tracking how far past seek end we should be
- */
- trackLive_() {
- const seekable = this.player_.seekable();
-
- // skip undefined seekable
- if (!seekable || !seekable.length) {
- return;
- }
- const newTime = Number(window.performance.now().toFixed(4));
- const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
- this.lastTime_ = newTime;
- this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
- const liveCurrentTime = this.liveCurrentTime();
- const currentTime = this.player_.currentTime();
-
- // we are behind live if any are true
- // 1. the player is paused
- // 2. the user seeked to a location 2 seconds away from live
- // 3. the difference between live and current time is greater
- // liveTolerance which defaults to 15s
- let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
-
- // we cannot be behind if
- // 1. until we have not seen a timeupdate yet
- // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
- if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
- isBehind = false;
- }
- if (isBehind !== this.behindLiveEdge_) {
- this.behindLiveEdge_ = isBehind;
- this.trigger('liveedgechange');
- }
- }
-
- /**
- * handle a durationchange event on the player
- * and start/stop tracking accordingly.
- */
- handleDurationchange() {
- this.toggleTracking();
- }
-
- /**
- * start/stop tracking
- */
- toggleTracking() {
- if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
- if (this.player_.options_.liveui) {
- this.player_.addClass('vjs-liveui');
- }
- this.startTracking();
- } else {
- this.player_.removeClass('vjs-liveui');
- this.stopTracking();
- }
- }
-
- /**
- * start tracking live playback
- */
- startTracking() {
- if (this.isTracking()) {
- return;
- }
-
- // If we haven't seen a timeupdate, we need to check whether playback
- // began before this component started tracking. This can happen commonly
- // when using autoplay.
- if (!this.timeupdateSeen_) {
- this.timeupdateSeen_ = this.player_.hasStarted();
- }
- this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
- this.trackLive_();
- this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- if (!this.timeupdateSeen_) {
- this.one(this.player_, 'play', this.handlePlay_);
- this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- } else {
- this.on(this.player_, 'seeked', this.handleSeeked_);
- }
- }
-
- /**
- * handle the first timeupdate on the player if it wasn't already playing
- * when live tracker started tracking.
- */
- handleFirstTimeupdate() {
- this.timeupdateSeen_ = true;
- this.on(this.player_, 'seeked', this.handleSeeked_);
- }
-
- /**
- * Keep track of what time a seek starts, and listen for seeked
- * to find where a seek ends.
- */
- handleSeeked() {
- const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
- this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
- this.nextSeekedFromUser_ = false;
- this.trackLive_();
- }
-
- /**
- * handle the first play on the player, and make sure that we seek
- * right to the live edge.
- */
- handlePlay() {
- this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
- }
-
- /**
- * Stop tracking, and set all internal variables to
- * their initial value.
- */
- reset_() {
- this.lastTime_ = -1;
- this.pastSeekEnd_ = 0;
- this.lastSeekEnd_ = -1;
- this.behindLiveEdge_ = true;
- this.timeupdateSeen_ = false;
- this.seekedBehindLive_ = false;
- this.nextSeekedFromUser_ = false;
- this.clearInterval(this.trackingInterval_);
- this.trackingInterval_ = null;
- this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- this.off(this.player_, 'seeked', this.handleSeeked_);
- this.off(this.player_, 'play', this.handlePlay_);
- this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
- }
-
- /**
- * The next seeked event is from the user. Meaning that any seek
- * > 2s behind live will be considered behind live for real and
- * liveTolerance will be ignored.
- */
- nextSeekedFromUser() {
- this.nextSeekedFromUser_ = true;
- }
-
- /**
- * stop tracking live playback
- */
- stopTracking() {
- if (!this.isTracking()) {
- return;
- }
- this.reset_();
- this.trigger('liveedgechange');
- }
-
- /**
- * A helper to get the player seekable end
- * so that we don't have to null check everywhere
- *
- * @return {number}
- * The furthest seekable end or Infinity.
- */
- seekableEnd() {
- const seekable = this.player_.seekable();
- const seekableEnds = [];
- let i = seekable ? seekable.length : 0;
- while (i--) {
- seekableEnds.push(seekable.end(i));
- }
-
- // grab the furthest seekable end after sorting, or if there are none
- // default to Infinity
- return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
- }
-
- /**
- * A helper to get the player seekable start
- * so that we don't have to null check everywhere
- *
- * @return {number}
- * The earliest seekable start or 0.
- */
- seekableStart() {
- const seekable = this.player_.seekable();
- const seekableStarts = [];
- let i = seekable ? seekable.length : 0;
- while (i--) {
- seekableStarts.push(seekable.start(i));
- }
-
- // grab the first seekable start after sorting, or if there are none
- // default to 0
- return seekableStarts.length ? seekableStarts.sort()[0] : 0;
- }
-
- /**
- * Get the live time window aka
- * the amount of time between seekable start and
- * live current time.
- *
- * @return {number}
- * The amount of seconds that are seekable in
- * the live video.
- */
- liveWindow() {
- const liveCurrentTime = this.liveCurrentTime();
-
- // if liveCurrenTime is Infinity then we don't have a liveWindow at all
- if (liveCurrentTime === Infinity) {
- return 0;
- }
- return liveCurrentTime - this.seekableStart();
- }
-
- /**
- * Determines if the player is live, only checks if this component
- * is tracking live playback or not
- *
- * @return {boolean}
- * Whether liveTracker is tracking
- */
- isLive() {
- return this.isTracking();
- }
-
- /**
- * Determines if currentTime is at the live edge and won't fall behind
- * on each seekableendchange
- *
- * @return {boolean}
- * Whether playback is at the live edge
- */
- atLiveEdge() {
- return !this.behindLiveEdge();
- }
-
- /**
- * get what we expect the live current time to be
- *
- * @return {number}
- * The expected live current time
- */
- liveCurrentTime() {
- return this.pastSeekEnd() + this.seekableEnd();
- }
-
- /**
- * The number of seconds that have occurred after seekable end
- * changed. This will be reset to 0 once seekable end changes.
- *
- * @return {number}
- * Seconds past the current seekable end
- */
- pastSeekEnd() {
- const seekableEnd = this.seekableEnd();
- if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
- this.pastSeekEnd_ = 0;
- }
- this.lastSeekEnd_ = seekableEnd;
- return this.pastSeekEnd_;
- }
-
- /**
- * If we are currently behind the live edge, aka currentTime will be
- * behind on a seekableendchange
- *
- * @return {boolean}
- * If we are behind the live edge
- */
- behindLiveEdge() {
- return this.behindLiveEdge_;
- }
-
- /**
- * Whether live tracker is currently tracking or not.
- */
- isTracking() {
- return typeof this.trackingInterval_ === 'number';
- }
-
- /**
- * Seek to the live edge if we are behind the live edge
- */
- seekToLiveEdge() {
- this.seekedBehindLive_ = false;
- if (this.atLiveEdge()) {
- return;
- }
- this.nextSeekedFromUser_ = false;
- this.player_.currentTime(this.liveCurrentTime());
- }
-
- /**
- * Dispose of liveTracker
- */
- dispose() {
- this.stopTracking();
- super.dispose();
- }
- }
- Component.registerComponent('LiveTracker', LiveTracker);
-
- /**
- * Displays an element over the player which contains an optional title and
- * description for the current content.
- *
- * Much of the code for this component originated in the now obsolete
- * videojs-dock plugin: https://github.com/brightcove/videojs-dock/
- *
- * @extends Component
- */
- class TitleBar extends Component {
- constructor(player, options) {
- super(player, options);
- this.on('statechanged', e => this.updateDom_());
- this.updateDom_();
- }
-
- /**
- * Create the `TitleBar`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- this.els = {
- title: createEl('div', {
- className: 'vjs-title-bar-title',
- id: `vjs-title-bar-title-${newGUID()}`
- }),
- description: createEl('div', {
- className: 'vjs-title-bar-description',
- id: `vjs-title-bar-description-${newGUID()}`
- })
- };
- return createEl('div', {
- className: 'vjs-title-bar'
- }, {}, values(this.els));
- }
-
- /**
- * Updates the DOM based on the component's state object.
- */
- updateDom_() {
- const tech = this.player_.tech_;
- const techEl = tech && tech.el_;
- const techAriaAttrs = {
- title: 'aria-labelledby',
- description: 'aria-describedby'
- };
- ['title', 'description'].forEach(k => {
- const value = this.state[k];
- const el = this.els[k];
- const techAriaAttr = techAriaAttrs[k];
- emptyEl(el);
- if (value) {
- textContent(el, value);
- }
-
- // If there is a tech element available, update its ARIA attributes
- // according to whether a title and/or description have been provided.
- if (techEl) {
- techEl.removeAttribute(techAriaAttr);
- if (value) {
- techEl.setAttribute(techAriaAttr, el.id);
- }
- }
- });
- if (this.state.title || this.state.description) {
- this.show();
- } else {
- this.hide();
- }
- }
-
- /**
- * Update the contents of the title bar component with new title and
- * description text.
- *
- * If both title and description are missing, the title bar will be hidden.
- *
- * If either title or description are present, the title bar will be visible.
- *
- * NOTE: Any previously set value will be preserved. To unset a previously
- * set value, you must pass an empty string or null.
- *
- * For example:
- *
- * ```
- * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
- * update({description: 'bar2'}) // title: 'foo', description: 'bar2'
- * update({title: ''}) // title: '', description: 'bar2'
- * update({title: 'foo', description: null}) // title: 'foo', description: null
- * ```
- *
- * @param {Object} [options={}]
- * An options object. When empty, the title bar will be hidden.
- *
- * @param {string} [options.title]
- * A title to display in the title bar.
- *
- * @param {string} [options.description]
- * A description to display in the title bar.
- */
- update(options) {
- this.setState(options);
- }
-
- /**
- * Dispose the component.
- */
- dispose() {
- const tech = this.player_.tech_;
- const techEl = tech && tech.el_;
- if (techEl) {
- techEl.removeAttribute('aria-labelledby');
- techEl.removeAttribute('aria-describedby');
- }
- super.dispose();
- this.els = null;
- }
- }
- Component.registerComponent('TitleBar', TitleBar);
-
- /**
- * This function is used to fire a sourceset when there is something
- * similar to `mediaEl.load()` being called. It will try to find the source via
- * the `src` attribute and then the `` elements. It will then fire `sourceset`
- * with the source that was found or empty string if we cannot know. If it cannot
- * find a source then `sourceset` will not be fired.
- *
- * @param { import('./html5').default } tech
- * The tech object that sourceset was setup on
- *
- * @return {boolean}
- * returns false if the sourceset was not fired and true otherwise.
- */
- const sourcesetLoad = tech => {
- const el = tech.el();
-
- // if `el.src` is set, that source will be loaded.
- if (el.hasAttribute('src')) {
- tech.triggerSourceset(el.src);
- return true;
- }
-
- /**
- * Since there isn't a src property on the media element, source elements will be used for
- * implementing the source selection algorithm. This happens asynchronously and
- * for most cases were there is more than one source we cannot tell what source will
- * be loaded, without re-implementing the source selection algorithm. At this time we are not
- * going to do that. There are three special cases that we do handle here though:
- *
- * 1. If there are no sources, do not fire `sourceset`.
- * 2. If there is only one `` with a `src` property/attribute that is our `src`
- * 3. If there is more than one `` but all of them have the same `src` url.
- * That will be our src.
- */
- const sources = tech.$$('source');
- const srcUrls = [];
- let src = '';
-
- // if there are no sources, do not fire sourceset
- if (!sources.length) {
- return false;
- }
-
- // only count valid/non-duplicate source elements
- for (let i = 0; i < sources.length; i++) {
- const url = sources[i].src;
- if (url && srcUrls.indexOf(url) === -1) {
- srcUrls.push(url);
- }
- }
-
- // there were no valid sources
- if (!srcUrls.length) {
- return false;
- }
-
- // there is only one valid source element url
- // use that
- if (srcUrls.length === 1) {
- src = srcUrls[0];
- }
- tech.triggerSourceset(src);
- return true;
- };
-
- /**
- * our implementation of an `innerHTML` descriptor for browsers
- * that do not have one.
- */
- const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
- get() {
- return this.cloneNode(true).innerHTML;
- },
- set(v) {
- // make a dummy node to use innerHTML on
- const dummy = document.createElement(this.nodeName.toLowerCase());
-
- // set innerHTML to the value provided
- dummy.innerHTML = v;
-
- // make a document fragment to hold the nodes from dummy
- const docFrag = document.createDocumentFragment();
-
- // copy all of the nodes created by the innerHTML on dummy
- // to the document fragment
- while (dummy.childNodes.length) {
- docFrag.appendChild(dummy.childNodes[0]);
- }
-
- // remove content
- this.innerText = '';
-
- // now we add all of that html in one by appending the
- // document fragment. This is how innerHTML does it.
- window.Element.prototype.appendChild.call(this, docFrag);
-
- // then return the result that innerHTML's setter would
- return this.innerHTML;
- }
- });
-
- /**
- * Get a property descriptor given a list of priorities and the
- * property to get.
- */
- const getDescriptor = (priority, prop) => {
- let descriptor = {};
- for (let i = 0; i < priority.length; i++) {
- descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
- if (descriptor && descriptor.set && descriptor.get) {
- break;
- }
- }
- descriptor.enumerable = true;
- descriptor.configurable = true;
- return descriptor;
- };
- const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, window.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
-
- /**
- * Patches browser internal functions so that we can tell synchronously
- * if a `` was appended to the media element. For some reason this
- * causes a `sourceset` if the the media element is ready and has no source.
- * This happens when:
- * - The page has just loaded and the media element does not have a source.
- * - The media element was emptied of all sources, then `load()` was called.
- *
- * It does this by patching the following functions/properties when they are supported:
- *
- * - `append()` - can be used to add a `` element to the media element
- * - `appendChild()` - can be used to add a `` element to the media element
- * - `insertAdjacentHTML()` - can be used to add a `` element to the media element
- * - `innerHTML` - can be used to add a `` element to the media element
- *
- * @param {Html5} tech
- * The tech object that sourceset is being setup on.
- */
- const firstSourceWatch = function (tech) {
- const el = tech.el();
-
- // make sure firstSourceWatch isn't setup twice.
- if (el.resetSourceWatch_) {
- return;
- }
- const old = {};
- const innerDescriptor = getInnerHTMLDescriptor(tech);
- const appendWrapper = appendFn => (...args) => {
- const retval = appendFn.apply(el, args);
- sourcesetLoad(tech);
- return retval;
- };
- ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
- if (!el[k]) {
- return;
- }
-
- // store the old function
- old[k] = el[k];
-
- // call the old function with a sourceset if a source
- // was loaded
- el[k] = appendWrapper(old[k]);
- });
- Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, {
- set: appendWrapper(innerDescriptor.set)
- }));
- el.resetSourceWatch_ = () => {
- el.resetSourceWatch_ = null;
- Object.keys(old).forEach(k => {
- el[k] = old[k];
- });
- Object.defineProperty(el, 'innerHTML', innerDescriptor);
- };
-
- // on the first sourceset, we need to revert our changes
- tech.one('sourceset', el.resetSourceWatch_);
- };
-
- /**
- * our implementation of a `src` descriptor for browsers
- * that do not have one
- */
- const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
- get() {
- if (this.hasAttribute('src')) {
- return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src'));
- }
- return '';
- },
- set(v) {
- window.Element.prototype.setAttribute.call(this, 'src', v);
- return v;
- }
- });
- const getSrcDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
-
- /**
- * setup `sourceset` handling on the `Html5` tech. This function
- * patches the following element properties/functions:
- *
- * - `src` - to determine when `src` is set
- * - `setAttribute()` - to determine when `src` is set
- * - `load()` - this re-triggers the source selection algorithm, and can
- * cause a sourceset.
- *
- * If there is no source when we are adding `sourceset` support or during a `load()`
- * we also patch the functions listed in `firstSourceWatch`.
- *
- * @param {Html5} tech
- * The tech to patch
- */
- const setupSourceset = function (tech) {
- if (!tech.featuresSourceset) {
- return;
- }
- const el = tech.el();
-
- // make sure sourceset isn't setup twice.
- if (el.resetSourceset_) {
- return;
- }
- const srcDescriptor = getSrcDescriptor(tech);
- const oldSetAttribute = el.setAttribute;
- const oldLoad = el.load;
- Object.defineProperty(el, 'src', merge(srcDescriptor, {
- set: v => {
- const retval = srcDescriptor.set.call(el, v);
-
- // we use the getter here to get the actual value set on src
- tech.triggerSourceset(el.src);
- return retval;
- }
- }));
- el.setAttribute = (n, v) => {
- const retval = oldSetAttribute.call(el, n, v);
- if (/src/i.test(n)) {
- tech.triggerSourceset(el.src);
- }
- return retval;
- };
- el.load = () => {
- const retval = oldLoad.call(el);
-
- // if load was called, but there was no source to fire
- // sourceset on. We have to watch for a source append
- // as that can trigger a `sourceset` when the media element
- // has no source
- if (!sourcesetLoad(tech)) {
- tech.triggerSourceset('');
- firstSourceWatch(tech);
- }
- return retval;
- };
- if (el.currentSrc) {
- tech.triggerSourceset(el.currentSrc);
- } else if (!sourcesetLoad(tech)) {
- firstSourceWatch(tech);
- }
- el.resetSourceset_ = () => {
- el.resetSourceset_ = null;
- el.load = oldLoad;
- el.setAttribute = oldSetAttribute;
- Object.defineProperty(el, 'src', srcDescriptor);
- if (el.resetSourceWatch_) {
- el.resetSourceWatch_();
- }
- };
- };
-
- /**
- * @file html5.js
- */
-
- /**
- * HTML5 Media Controller - Wrapper for HTML5 Media API
- *
- * @mixes Tech~SourceHandlerAdditions
- * @extends Tech
- */
- class Html5 extends Tech {
- /**
- * Create an instance of this Tech.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * Callback function to call when the `HTML5` Tech is ready.
- */
- constructor(options, ready) {
- super(options, ready);
- const source = options.source;
- let crossoriginTracks = false;
- this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
-
- // Set the source if one is provided
- // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
- // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
- // anyway so the error gets fired.
- if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
- this.setSource(source);
- } else {
- this.handleLateInit_(this.el_);
- }
-
- // setup sourceset after late sourceset/init
- if (options.enableSourceset) {
- this.setupSourcesetHandling_();
- }
- this.isScrubbing_ = false;
- if (this.el_.hasChildNodes()) {
- const nodes = this.el_.childNodes;
- let nodesLength = nodes.length;
- const removeNodes = [];
- while (nodesLength--) {
- const node = nodes[nodesLength];
- const nodeName = node.nodeName.toLowerCase();
- if (nodeName === 'track') {
- if (!this.featuresNativeTextTracks) {
- // Empty video tag tracks so the built-in player doesn't use them also.
- // This may not be fast enough to stop HTML5 browsers from reading the tags
- // so we'll need to turn off any default tracks if we're manually doing
- // captions and subtitles. videoElement.textTracks
- removeNodes.push(node);
- } else {
- // store HTMLTrackElement and TextTrack to remote list
- this.remoteTextTrackEls().addTrackElement_(node);
- this.remoteTextTracks().addTrack(node.track);
- this.textTracks().addTrack(node.track);
- if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
- crossoriginTracks = true;
- }
- }
- }
- }
- for (let i = 0; i < removeNodes.length; i++) {
- this.el_.removeChild(removeNodes[i]);
- }
- }
- this.proxyNativeTracks_();
- if (this.featuresNativeTextTracks && crossoriginTracks) {
- log.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.');
- }
-
- // prevent iOS Safari from disabling metadata text tracks during native playback
- this.restoreMetadataTracksInIOSNativePlayer_();
-
- // Determine if native controls should be used
- // Our goal should be to get the custom controls on mobile solid everywhere
- // so we can remove this all together. Right now this will block custom
- // controls on touch enabled laptops like the Chrome Pixel
- if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
- this.setControls(true);
- }
-
- // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
- // into a `fullscreenchange` event
- this.proxyWebkitFullscreen_();
- this.triggerReady();
- }
-
- /**
- * Dispose of `HTML5` media element and remove all tracks.
- */
- dispose() {
- if (this.el_ && this.el_.resetSourceset_) {
- this.el_.resetSourceset_();
- }
- Html5.disposeMediaElement(this.el_);
- this.options_ = null;
-
- // tech will handle clearing of the emulated track list
- super.dispose();
- }
-
- /**
- * Modify the media element so that we can detect when
- * the source is changed. Fires `sourceset` just after the source has changed
- */
- setupSourcesetHandling_() {
- setupSourceset(this);
- }
-
- /**
- * When a captions track is enabled in the iOS Safari native player, all other
- * tracks are disabled (including metadata tracks), which nulls all of their
- * associated cue points. This will restore metadata tracks to their pre-fullscreen
- * state in those cases so that cue points are not needlessly lost.
- *
- * @private
- */
- restoreMetadataTracksInIOSNativePlayer_() {
- const textTracks = this.textTracks();
- let metadataTracksPreFullscreenState;
-
- // captures a snapshot of every metadata track's current state
- const takeMetadataTrackSnapshot = () => {
- metadataTracksPreFullscreenState = [];
- for (let i = 0; i < textTracks.length; i++) {
- const track = textTracks[i];
- if (track.kind === 'metadata') {
- metadataTracksPreFullscreenState.push({
- track,
- storedMode: track.mode
- });
- }
- }
- };
-
- // snapshot each metadata track's initial state, and update the snapshot
- // each time there is a track 'change' event
- takeMetadataTrackSnapshot();
- textTracks.addEventListener('change', takeMetadataTrackSnapshot);
- this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
- const restoreTrackMode = () => {
- for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
- const storedTrack = metadataTracksPreFullscreenState[i];
- if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
- storedTrack.track.mode = storedTrack.storedMode;
- }
- }
- // we only want this handler to be executed on the first 'change' event
- textTracks.removeEventListener('change', restoreTrackMode);
- };
-
- // when we enter fullscreen playback, stop updating the snapshot and
- // restore all track modes to their pre-fullscreen state
- this.on('webkitbeginfullscreen', () => {
- textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
-
- // remove the listener before adding it just in case it wasn't previously removed
- textTracks.removeEventListener('change', restoreTrackMode);
- textTracks.addEventListener('change', restoreTrackMode);
- });
-
- // start updating the snapshot again after leaving fullscreen
- this.on('webkitendfullscreen', () => {
- // remove the listener before adding it just in case it wasn't previously removed
- textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
- textTracks.addEventListener('change', takeMetadataTrackSnapshot);
-
- // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
- textTracks.removeEventListener('change', restoreTrackMode);
- });
- }
-
- /**
- * Attempt to force override of tracks for the given type
- *
- * @param {string} type - Track type to override, possible values include 'Audio',
- * 'Video', and 'Text'.
- * @param {boolean} override - If set to true native audio/video will be overridden,
- * otherwise native audio/video will potentially be used.
- * @private
- */
- overrideNative_(type, override) {
- // If there is no behavioral change don't add/remove listeners
- if (override !== this[`featuresNative${type}Tracks`]) {
- return;
- }
- const lowerCaseType = type.toLowerCase();
- if (this[`${lowerCaseType}TracksListeners_`]) {
- Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
- const elTracks = this.el()[`${lowerCaseType}Tracks`];
- elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
- });
- }
- this[`featuresNative${type}Tracks`] = !override;
- this[`${lowerCaseType}TracksListeners_`] = null;
- this.proxyNativeTracksForType_(lowerCaseType);
- }
-
- /**
- * Attempt to force override of native audio tracks.
- *
- * @param {boolean} override - If set to true native audio will be overridden,
- * otherwise native audio will potentially be used.
- */
- overrideNativeAudioTracks(override) {
- this.overrideNative_('Audio', override);
- }
-
- /**
- * Attempt to force override of native video tracks.
- *
- * @param {boolean} override - If set to true native video will be overridden,
- * otherwise native video will potentially be used.
- */
- overrideNativeVideoTracks(override) {
- this.overrideNative_('Video', override);
- }
-
- /**
- * Proxy native track list events for the given type to our track
- * lists if the browser we are playing in supports that type of track list.
- *
- * @param {string} name - Track type; values include 'audio', 'video', and 'text'
- * @private
- */
- proxyNativeTracksForType_(name) {
- const props = NORMAL[name];
- const elTracks = this.el()[props.getterName];
- const techTracks = this[props.getterName]();
- if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
- return;
- }
- const listeners = {
- change: e => {
- const event = {
- type: 'change',
- target: techTracks,
- currentTarget: techTracks,
- srcElement: techTracks
- };
- techTracks.trigger(event);
-
- // if we are a text track change event, we should also notify the
- // remote text track list. This can potentially cause a false positive
- // if we were to get a change event on a non-remote track and
- // we triggered the event on the remote text track list which doesn't
- // contain that track. However, best practices mean looping through the
- // list of tracks and searching for the appropriate mode value, so,
- // this shouldn't pose an issue
- if (name === 'text') {
- this[REMOTE.remoteText.getterName]().trigger(event);
- }
- },
- addtrack(e) {
- techTracks.addTrack(e.track);
- },
- removetrack(e) {
- techTracks.removeTrack(e.track);
- }
- };
- const removeOldTracks = function () {
- const removeTracks = [];
- for (let i = 0; i < techTracks.length; i++) {
- let found = false;
- for (let j = 0; j < elTracks.length; j++) {
- if (elTracks[j] === techTracks[i]) {
- found = true;
- break;
- }
- }
- if (!found) {
- removeTracks.push(techTracks[i]);
- }
- }
- while (removeTracks.length) {
- techTracks.removeTrack(removeTracks.shift());
- }
- };
- this[props.getterName + 'Listeners_'] = listeners;
- Object.keys(listeners).forEach(eventName => {
- const listener = listeners[eventName];
- elTracks.addEventListener(eventName, listener);
- this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
- });
-
- // Remove (native) tracks that are not used anymore
- this.on('loadstart', removeOldTracks);
- this.on('dispose', e => this.off('loadstart', removeOldTracks));
- }
-
- /**
- * Proxy all native track list events to our track lists if the browser we are playing
- * in supports that type of track list.
- *
- * @private
- */
- proxyNativeTracks_() {
- NORMAL.names.forEach(name => {
- this.proxyNativeTracksForType_(name);
- });
- }
-
- /**
- * Create the `Html5` Tech's DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- let el = this.options_.tag;
-
- // Check if this browser supports moving the element into the box.
- // On the iPhone video will break if you move the element,
- // So we have to create a brand new element.
- // If we ingested the player div, we do not need to move the media element.
- if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
- // If the original tag is still there, clone and remove it.
- if (el) {
- const clone = el.cloneNode(true);
- if (el.parentNode) {
- el.parentNode.insertBefore(clone, el);
- }
- Html5.disposeMediaElement(el);
- el = clone;
- } else {
- el = document.createElement('video');
-
- // determine if native controls should be used
- const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
- const attributes = merge({}, tagAttributes);
- if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
- delete attributes.controls;
- }
- setAttributes(el, Object.assign(attributes, {
- id: this.options_.techId,
- class: 'vjs-tech'
- }));
- }
- el.playerId = this.options_.playerId;
- }
- if (typeof this.options_.preload !== 'undefined') {
- setAttribute(el, 'preload', this.options_.preload);
- }
- if (this.options_.disablePictureInPicture !== undefined) {
- el.disablePictureInPicture = this.options_.disablePictureInPicture;
- }
-
- // Update specific tag settings, in case they were overridden
- // `autoplay` has to be *last* so that `muted` and `playsinline` are present
- // when iOS/Safari or other browsers attempt to autoplay.
- const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
- for (let i = 0; i < settingsAttrs.length; i++) {
- const attr = settingsAttrs[i];
- const value = this.options_[attr];
- if (typeof value !== 'undefined') {
- if (value) {
- setAttribute(el, attr, attr);
- } else {
- removeAttribute(el, attr);
- }
- el[attr] = value;
- }
- }
- return el;
- }
-
- /**
- * This will be triggered if the loadstart event has already fired, before videojs was
- * ready. Two known examples of when this can happen are:
- * 1. If we're loading the playback object after it has started loading
- * 2. The media is already playing the (often with autoplay on) then
- *
- * This function will fire another loadstart so that videojs can catchup.
- *
- * @fires Tech#loadstart
- *
- * @return {undefined}
- * returns nothing.
- */
- handleLateInit_(el) {
- if (el.networkState === 0 || el.networkState === 3) {
- // The video element hasn't started loading the source yet
- // or didn't find a source
- return;
- }
- if (el.readyState === 0) {
- // NetworkState is set synchronously BUT loadstart is fired at the
- // end of the current stack, usually before setInterval(fn, 0).
- // So at this point we know loadstart may have already fired or is
- // about to fire, and either way the player hasn't seen it yet.
- // We don't want to fire loadstart prematurely here and cause a
- // double loadstart so we'll wait and see if it happens between now
- // and the next loop, and fire it if not.
- // HOWEVER, we also want to make sure it fires before loadedmetadata
- // which could also happen between now and the next loop, so we'll
- // watch for that also.
- let loadstartFired = false;
- const setLoadstartFired = function () {
- loadstartFired = true;
- };
- this.on('loadstart', setLoadstartFired);
- const triggerLoadstart = function () {
- // We did miss the original loadstart. Make sure the player
- // sees loadstart before loadedmetadata
- if (!loadstartFired) {
- this.trigger('loadstart');
- }
- };
- this.on('loadedmetadata', triggerLoadstart);
- this.ready(function () {
- this.off('loadstart', setLoadstartFired);
- this.off('loadedmetadata', triggerLoadstart);
- if (!loadstartFired) {
- // We did miss the original native loadstart. Fire it now.
- this.trigger('loadstart');
- }
- });
- return;
- }
-
- // From here on we know that loadstart already fired and we missed it.
- // The other readyState events aren't as much of a problem if we double
- // them, so not going to go to as much trouble as loadstart to prevent
- // that unless we find reason to.
- const eventsToTrigger = ['loadstart'];
-
- // loadedmetadata: newly equal to HAVE_METADATA (1) or greater
- eventsToTrigger.push('loadedmetadata');
-
- // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
- if (el.readyState >= 2) {
- eventsToTrigger.push('loadeddata');
- }
-
- // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
- if (el.readyState >= 3) {
- eventsToTrigger.push('canplay');
- }
-
- // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
- if (el.readyState >= 4) {
- eventsToTrigger.push('canplaythrough');
- }
-
- // We still need to give the player time to add event listeners
- this.ready(function () {
- eventsToTrigger.forEach(function (type) {
- this.trigger(type);
- }, this);
- });
- }
-
- /**
- * Set whether we are scrubbing or not.
- * This is used to decide whether we should use `fastSeek` or not.
- * `fastSeek` is used to provide trick play on Safari browsers.
- *
- * @param {boolean} isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- */
- setScrubbing(isScrubbing) {
- this.isScrubbing_ = isScrubbing;
- }
-
- /**
- * Get whether we are scrubbing or not.
- *
- * @return {boolean} isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- */
- scrubbing() {
- return this.isScrubbing_;
- }
-
- /**
- * Set current time for the `HTML5` tech.
- *
- * @param {number} seconds
- * Set the current time of the media to this.
- */
- setCurrentTime(seconds) {
- try {
- if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
- this.el_.fastSeek(seconds);
- } else {
- this.el_.currentTime = seconds;
- }
- } catch (e) {
- log(e, 'Video is not ready. (Video.js)');
- // this.warning(VideoJS.warnings.videoNotReady);
- }
- }
-
- /**
- * Get the current duration of the HTML5 media element.
- *
- * @return {number}
- * The duration of the media or 0 if there is no duration.
- */
- duration() {
- // Android Chrome will report duration as Infinity for VOD HLS until after
- // playback has started, which triggers the live display erroneously.
- // Return NaN if playback has not started and trigger a durationupdate once
- // the duration can be reliably known.
- if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
- // Wait for the first `timeupdate` with currentTime > 0 - there may be
- // several with 0
- const checkProgress = () => {
- if (this.el_.currentTime > 0) {
- // Trigger durationchange for genuinely live video
- if (this.el_.duration === Infinity) {
- this.trigger('durationchange');
- }
- this.off('timeupdate', checkProgress);
- }
- };
- this.on('timeupdate', checkProgress);
- return NaN;
- }
- return this.el_.duration || NaN;
- }
-
- /**
- * Get the current width of the HTML5 media element.
- *
- * @return {number}
- * The width of the HTML5 media element.
- */
- width() {
- return this.el_.offsetWidth;
- }
-
- /**
- * Get the current height of the HTML5 media element.
- *
- * @return {number}
- * The height of the HTML5 media element.
- */
- height() {
- return this.el_.offsetHeight;
- }
-
- /**
- * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
- * `fullscreenchange` event.
- *
- * @private
- * @fires fullscreenchange
- * @listens webkitendfullscreen
- * @listens webkitbeginfullscreen
- * @listens webkitbeginfullscreen
- */
- proxyWebkitFullscreen_() {
- if (!('webkitDisplayingFullscreen' in this.el_)) {
- return;
- }
- const endFn = function () {
- this.trigger('fullscreenchange', {
- isFullscreen: false
- });
- // Safari will sometimes set controls on the videoelement when existing fullscreen.
- if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
- this.el_.controls = false;
- }
- };
- const beginFn = function () {
- if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
- this.one('webkitendfullscreen', endFn);
- this.trigger('fullscreenchange', {
- isFullscreen: true,
- // set a flag in case another tech triggers fullscreenchange
- nativeIOSFullscreen: true
- });
- }
- };
- this.on('webkitbeginfullscreen', beginFn);
- this.on('dispose', () => {
- this.off('webkitbeginfullscreen', beginFn);
- this.off('webkitendfullscreen', endFn);
- });
- }
-
- /**
- * Check if fullscreen is supported on the video el.
- *
- * @return {boolean}
- * - True if fullscreen is supported.
- * - False if fullscreen is not supported.
- */
- supportsFullScreen() {
- return typeof this.el_.webkitEnterFullScreen === 'function';
- }
-
- /**
- * Request that the `HTML5` Tech enter fullscreen.
- */
- enterFullScreen() {
- const video = this.el_;
- if (video.paused && video.networkState <= video.HAVE_METADATA) {
- // attempt to prime the video element for programmatic access
- // this isn't necessary on the desktop but shouldn't hurt
- silencePromise(this.el_.play());
-
- // playing and pausing synchronously during the transition to fullscreen
- // can get iOS ~6.1 devices into a play/pause loop
- this.setTimeout(function () {
- video.pause();
- try {
- video.webkitEnterFullScreen();
- } catch (e) {
- this.trigger('fullscreenerror', e);
- }
- }, 0);
- } else {
- try {
- video.webkitEnterFullScreen();
- } catch (e) {
- this.trigger('fullscreenerror', e);
- }
- }
- }
-
- /**
- * Request that the `HTML5` Tech exit fullscreen.
- */
- exitFullScreen() {
- if (!this.el_.webkitDisplayingFullscreen) {
- this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
- return;
- }
- this.el_.webkitExitFullScreen();
- }
-
- /**
- * Create a floating video window always on top of other windows so that users may
- * continue consuming media while they interact with other content sites, or
- * applications on their device.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @return {Promise}
- * A promise with a Picture-in-Picture window.
- */
- requestPictureInPicture() {
- return this.el_.requestPictureInPicture();
- }
-
- /**
- * Native requestVideoFrameCallback if supported by browser/tech, or fallback
- * Don't use rVCF on Safari when DRM is playing, as it doesn't fire
- * Needs to be checked later than the constructor
- * This will be a false positive for clear sources loaded after a Fairplay source
- *
- * @param {function} cb function to call
- * @return {number} id of request
- */
- requestVideoFrameCallback(cb) {
- if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
- return this.el_.requestVideoFrameCallback(cb);
- }
- return super.requestVideoFrameCallback(cb);
- }
-
- /**
- * Native or fallback requestVideoFrameCallback
- *
- * @param {number} id request id to cancel
- */
- cancelVideoFrameCallback(id) {
- if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
- this.el_.cancelVideoFrameCallback(id);
- } else {
- super.cancelVideoFrameCallback(id);
- }
- }
-
- /**
- * A getter/setter for the `Html5` Tech's source object.
- * > Note: Please use {@link Html5#setSource}
- *
- * @param {Tech~SourceObject} [src]
- * The source object you want to set on the `HTML5` techs element.
- *
- * @return {Tech~SourceObject|undefined}
- * - The current source object when a source is not passed in.
- * - undefined when setting
- *
- * @deprecated Since version 5.
- */
- src(src) {
- if (src === undefined) {
- return this.el_.src;
- }
-
- // Setting src through `src` instead of `setSrc` will be deprecated
- this.setSrc(src);
- }
-
- /**
- * Reset the tech by removing all sources and then calling
- * {@link Html5.resetMediaElement}.
- */
- reset() {
- Html5.resetMediaElement(this.el_);
- }
-
- /**
- * Get the current source on the `HTML5` Tech. Falls back to returning the source from
- * the HTML5 media element.
- *
- * @return {Tech~SourceObject}
- * The current source object from the HTML5 tech. With a fallback to the
- * elements source.
- */
- currentSrc() {
- if (this.currentSource_) {
- return this.currentSource_.src;
- }
- return this.el_.currentSrc;
- }
-
- /**
- * Set controls attribute for the HTML5 media Element.
- *
- * @param {string} val
- * Value to set the controls attribute to
- */
- setControls(val) {
- this.el_.controls = !!val;
- }
-
- /**
- * Create and returns a remote {@link TextTrack} object.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @return {TextTrack}
- * The TextTrack that gets created.
- */
- addTextTrack(kind, label, language) {
- if (!this.featuresNativeTextTracks) {
- return super.addTextTrack(kind, label, language);
- }
- return this.el_.addTextTrack(kind, label, language);
- }
-
- /**
- * Creates either native TextTrack or an emulated TextTrack depending
- * on the value of `featuresNativeTextTracks`
- *
- * @param {Object} options
- * The object should contain the options to initialize the TextTrack with.
- *
- * @param {string} [options.kind]
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
- *
- * @param {string} [options.label]
- * Label to identify the text track
- *
- * @param {string} [options.language]
- * Two letter language abbreviation.
- *
- * @param {boolean} [options.default]
- * Default this track to on.
- *
- * @param {string} [options.id]
- * The internal id to assign this track.
- *
- * @param {string} [options.src]
- * A source url for the track.
- *
- * @return {HTMLTrackElement}
- * The track element that gets created.
- */
- createRemoteTextTrack(options) {
- if (!this.featuresNativeTextTracks) {
- return super.createRemoteTextTrack(options);
- }
- const htmlTrackElement = document.createElement('track');
- if (options.kind) {
- htmlTrackElement.kind = options.kind;
- }
- if (options.label) {
- htmlTrackElement.label = options.label;
- }
- if (options.language || options.srclang) {
- htmlTrackElement.srclang = options.language || options.srclang;
- }
- if (options.default) {
- htmlTrackElement.default = options.default;
- }
- if (options.id) {
- htmlTrackElement.id = options.id;
- }
- if (options.src) {
- htmlTrackElement.src = options.src;
- }
- return htmlTrackElement;
- }
-
- /**
- * Creates a remote text track object and returns an html track element.
- *
- * @param {Object} options The object should contain values for
- * kind, language, label, and src (location of the WebVTT file)
- * @param {boolean} [manualCleanup=false] if set to true, the TextTrack
- * will not be removed from the TextTrackList and HtmlTrackElementList
- * after a source change
- * @return {HTMLTrackElement} An Html Track Element.
- * This can be an emulated {@link HTMLTrackElement} or a native one.
- *
- */
- addRemoteTextTrack(options, manualCleanup) {
- const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
- if (this.featuresNativeTextTracks) {
- this.el().appendChild(htmlTrackElement);
- }
- return htmlTrackElement;
- }
-
- /**
- * Remove remote `TextTrack` from `TextTrackList` object
- *
- * @param {TextTrack} track
- * `TextTrack` object to remove
- */
- removeRemoteTextTrack(track) {
- super.removeRemoteTextTrack(track);
- if (this.featuresNativeTextTracks) {
- const tracks = this.$$('track');
- let i = tracks.length;
- while (i--) {
- if (track === tracks[i] || track === tracks[i].track) {
- this.el().removeChild(tracks[i]);
- }
- }
- }
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object}
- * An object with supported media playback quality metrics
- */
- getVideoPlaybackQuality() {
- if (typeof this.el().getVideoPlaybackQuality === 'function') {
- return this.el().getVideoPlaybackQuality();
- }
- const videoPlaybackQuality = {};
- if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
- videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
- videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
- }
- if (window.performance) {
- videoPlaybackQuality.creationTime = window.performance.now();
- }
- return videoPlaybackQuality;
- }
- }
-
- /* HTML5 Support Testing ---------------------------------------------------- */
-
- /**
- * Element for testing browser HTML5 media capabilities
- *
- * @type {Element}
- * @constant
- * @private
- */
- defineLazyProperty(Html5, 'TEST_VID', function () {
- if (!isReal()) {
- return;
- }
- const video = document.createElement('video');
- const track = document.createElement('track');
- track.kind = 'captions';
- track.srclang = 'en';
- track.label = 'English';
- video.appendChild(track);
- return video;
- });
-
- /**
- * Check if HTML5 media is supported by this browser/device.
- *
- * @return {boolean}
- * - True if HTML5 media is supported.
- * - False if HTML5 media is not supported.
- */
- Html5.isSupported = function () {
- // IE with no Media Player is a LIAR! (#984)
- try {
- Html5.TEST_VID.volume = 0.5;
- } catch (e) {
- return false;
- }
- return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
- };
-
- /**
- * Check if the tech can support the given type
- *
- * @param {string} type
- * The mimetype to check
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- Html5.canPlayType = function (type) {
- return Html5.TEST_VID.canPlayType(type);
- };
-
- /**
- * Check if the tech can support the given source
- *
- * @param {Object} srcObj
- * The source object
- * @param {Object} options
- * The options passed to the tech
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- Html5.canPlaySource = function (srcObj, options) {
- return Html5.canPlayType(srcObj.type);
- };
-
- /**
- * Check if the volume can be changed in this browser/device.
- * Volume cannot be changed in a lot of mobile devices.
- * Specifically, it can't be changed from 1 on iOS.
- *
- * @return {boolean}
- * - True if volume can be controlled
- * - False otherwise
- */
- Html5.canControlVolume = function () {
- // IE will error if Windows Media Player not installed #3315
- try {
- const volume = Html5.TEST_VID.volume;
- Html5.TEST_VID.volume = volume / 2 + 0.1;
- const canControl = volume !== Html5.TEST_VID.volume;
-
- // With the introduction of iOS 15, there are cases where the volume is read as
- // changed but reverts back to its original state at the start of the next tick.
- // To determine whether volume can be controlled on iOS,
- // a timeout is set and the volume is checked asynchronously.
- // Since `features` doesn't currently work asynchronously, the value is manually set.
- if (canControl && IS_IOS) {
- window.setTimeout(() => {
- if (Html5 && Html5.prototype) {
- Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
- }
- });
-
- // default iOS to false, which will be updated in the timeout above.
- return false;
- }
- return canControl;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if the volume can be muted in this browser/device.
- * Some devices, e.g. iOS, don't allow changing volume
- * but permits muting/unmuting.
- *
- * @return {boolean}
- * - True if volume can be muted
- * - False otherwise
- */
- Html5.canMuteVolume = function () {
- try {
- const muted = Html5.TEST_VID.muted;
-
- // in some versions of iOS muted property doesn't always
- // work, so we want to set both property and attribute
- Html5.TEST_VID.muted = !muted;
- if (Html5.TEST_VID.muted) {
- setAttribute(Html5.TEST_VID, 'muted', 'muted');
- } else {
- removeAttribute(Html5.TEST_VID, 'muted', 'muted');
- }
- return muted !== Html5.TEST_VID.muted;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if the playback rate can be changed in this browser/device.
- *
- * @return {boolean}
- * - True if playback rate can be controlled
- * - False otherwise
- */
- Html5.canControlPlaybackRate = function () {
- // Playback rate API is implemented in Android Chrome, but doesn't do anything
- // https://github.com/videojs/video.js/issues/3180
- if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
- return false;
- }
- // IE will error if Windows Media Player not installed #3315
- try {
- const playbackRate = Html5.TEST_VID.playbackRate;
- Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
- return playbackRate !== Html5.TEST_VID.playbackRate;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if we can override a video/audio elements attributes, with
- * Object.defineProperty.
- *
- * @return {boolean}
- * - True if builtin attributes can be overridden
- * - False otherwise
- */
- Html5.canOverrideAttributes = function () {
- // if we cannot overwrite the src/innerHTML property, there is no support
- // iOS 7 safari for instance cannot do this.
- try {
- const noop = () => {};
- Object.defineProperty(document.createElement('video'), 'src', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('audio'), 'src', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('video'), 'innerHTML', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('audio'), 'innerHTML', {
- get: noop,
- set: noop
- });
- } catch (e) {
- return false;
- }
- return true;
- };
-
- /**
- * Check to see if native `TextTrack`s are supported by this browser/device.
- *
- * @return {boolean}
- * - True if native `TextTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeTextTracks = function () {
- return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
- };
-
- /**
- * Check to see if native `VideoTrack`s are supported by this browser/device
- *
- * @return {boolean}
- * - True if native `VideoTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeVideoTracks = function () {
- return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
- };
-
- /**
- * Check to see if native `AudioTrack`s are supported by this browser/device
- *
- * @return {boolean}
- * - True if native `AudioTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeAudioTracks = function () {
- return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
- };
-
- /**
- * An array of events available on the Html5 tech.
- *
- * @private
- * @type {Array}
- */
- Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange'];
-
- /**
- * Boolean indicating whether the `Tech` supports volume control.
- *
- * @type {boolean}
- * @default {@link Html5.canControlVolume}
- */
- /**
- * Boolean indicating whether the `Tech` supports muting volume.
- *
- * @type {boolean}
- * @default {@link Html5.canMuteVolume}
- */
-
- /**
- * Boolean indicating whether the `Tech` supports changing the speed at which the media
- * plays. Examples:
- * - Set player to play 2x (twice) as fast
- * - Set player to play 0.5x (half) as fast
- *
- * @type {boolean}
- * @default {@link Html5.canControlPlaybackRate}
- */
-
- /**
- * Boolean indicating whether the `Tech` supports the `sourceset` event.
- *
- * @type {boolean}
- * @default
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeTextTracks}
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeVideoTracks}
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeAudioTracks}
- */
- [['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
- defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
- });
- Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the media element
- * moving in the DOM. iOS breaks if you move the media element, so this is set this to
- * false there. Everywhere else this should be true.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.movingMediaElementInDOM = !IS_IOS;
-
- // TODO: Previous comment: No longer appears to be used. Can probably be removed.
- // Is this true?
- /**
- * Boolean indicating whether the `HTML5` tech currently supports automatic media resize
- * when going into fullscreen.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.featuresFullscreenResize = true;
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the progress event.
- * If this is false, manual `progress` events will be triggered instead.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.featuresProgressEvents = true;
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
- * If this is false, manual `timeupdate` events will be triggered instead.
- *
- * @default
- */
- Html5.prototype.featuresTimeupdateEvents = true;
-
- /**
- * Whether the HTML5 el supports `requestVideoFrameCallback`
- *
- * @type {boolean}
- */
- Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
- Html5.disposeMediaElement = function (el) {
- if (!el) {
- return;
- }
- if (el.parentNode) {
- el.parentNode.removeChild(el);
- }
-
- // remove any child track or source nodes to prevent their loading
- while (el.hasChildNodes()) {
- el.removeChild(el.firstChild);
- }
-
- // remove any src reference. not setting `src=''` because that causes a warning
- // in firefox
- el.removeAttribute('src');
-
- // force the media element to update its loading state by calling load()
- // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
- if (typeof el.load === 'function') {
- // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
- (function () {
- try {
- el.load();
- } catch (e) {
- // not supported
- }
- })();
- }
- };
- Html5.resetMediaElement = function (el) {
- if (!el) {
- return;
- }
- const sources = el.querySelectorAll('source');
- let i = sources.length;
- while (i--) {
- el.removeChild(sources[i]);
- }
-
- // remove any src reference.
- // not setting `src=''` because that throws an error
- el.removeAttribute('src');
- if (typeof el.load === 'function') {
- // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
- (function () {
- try {
- el.load();
- } catch (e) {
- // satisfy linter
- }
- })();
- }
- };
-
- /* Native HTML5 element property wrapping ----------------------------------- */
- // Wrap native boolean attributes with getters that check both property and attribute
- // The list is as followed:
- // muted, defaultMuted, autoplay, controls, loop, playsinline
- [
- /**
- * Get the value of `muted` from the media element. `muted` indicates
- * that the volume for the media should be set to silent. This does not actually change
- * the `volume` attribute.
- *
- * @method Html5#muted
- * @return {boolean}
- * - True if the value of `volume` should be ignored and the audio set to silent.
- * - False if the value of `volume` should be used.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
- */
- 'muted',
- /**
- * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
- * whether the media should start muted or not. Only changes the default state of the
- * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
- * current state.
- *
- * @method Html5#defaultMuted
- * @return {boolean}
- * - The value of `defaultMuted` from the media element.
- * - True indicates that the media should start muted.
- * - False indicates that the media should not start muted
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
- */
- 'defaultMuted',
- /**
- * Get the value of `autoplay` from the media element. `autoplay` indicates
- * that the media should start to play as soon as the page is ready.
- *
- * @method Html5#autoplay
- * @return {boolean}
- * - The value of `autoplay` from the media element.
- * - True indicates that the media should start as soon as the page loads.
- * - False indicates that the media should not start as soon as the page loads.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
- */
- 'autoplay',
- /**
- * Get the value of `controls` from the media element. `controls` indicates
- * whether the native media controls should be shown or hidden.
- *
- * @method Html5#controls
- * @return {boolean}
- * - The value of `controls` from the media element.
- * - True indicates that native controls should be showing.
- * - False indicates that native controls should be hidden.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
- */
- 'controls',
- /**
- * Get the value of `loop` from the media element. `loop` indicates
- * that the media should return to the start of the media and continue playing once
- * it reaches the end.
- *
- * @method Html5#loop
- * @return {boolean}
- * - The value of `loop` from the media element.
- * - True indicates that playback should seek back to start once
- * the end of a media is reached.
- * - False indicates that playback should not loop back to the start when the
- * end of the media is reached.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
- */
- 'loop',
- /**
- * Get the value of `playsinline` from the media element. `playsinline` indicates
- * to the browser that non-fullscreen playback is preferred when fullscreen
- * playback is the native default, such as in iOS Safari.
- *
- * @method Html5#playsinline
- * @return {boolean}
- * - The value of `playsinline` from the media element.
- * - True indicates that the media should play inline.
- * - False indicates that the media should not play inline.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- 'playsinline'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop] || this.el_.hasAttribute(prop);
- };
- });
-
- // Wrap native boolean attributes with setters that set both property and attribute
- // The list is as followed:
- // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
- // setControls is special-cased above
- [
- /**
- * Set the value of `muted` on the media element. `muted` indicates that the current
- * audio level should be silent.
- *
- * @method Html5#setMuted
- * @param {boolean} muted
- * - True if the audio should be set to silent
- * - False otherwise
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
- */
- 'muted',
- /**
- * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
- * audio level should be silent, but will only effect the muted level on initial playback..
- *
- * @method Html5.prototype.setDefaultMuted
- * @param {boolean} defaultMuted
- * - True if the audio should be set to silent
- * - False otherwise
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
- */
- 'defaultMuted',
- /**
- * Set the value of `autoplay` on the media element. `autoplay` indicates
- * that the media should start to play as soon as the page is ready.
- *
- * @method Html5#setAutoplay
- * @param {boolean} autoplay
- * - True indicates that the media should start as soon as the page loads.
- * - False indicates that the media should not start as soon as the page loads.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
- */
- 'autoplay',
- /**
- * Set the value of `loop` on the media element. `loop` indicates
- * that the media should return to the start of the media and continue playing once
- * it reaches the end.
- *
- * @method Html5#setLoop
- * @param {boolean} loop
- * - True indicates that playback should seek back to start once
- * the end of a media is reached.
- * - False indicates that playback should not loop back to the start when the
- * end of the media is reached.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
- */
- 'loop',
- /**
- * Set the value of `playsinline` from the media element. `playsinline` indicates
- * to the browser that non-fullscreen playback is preferred when fullscreen
- * playback is the native default, such as in iOS Safari.
- *
- * @method Html5#setPlaysinline
- * @param {boolean} playsinline
- * - True indicates that the media should play inline.
- * - False indicates that the media should not play inline.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- 'playsinline'].forEach(function (prop) {
- Html5.prototype['set' + toTitleCase(prop)] = function (v) {
- this.el_[prop] = v;
- if (v) {
- this.el_.setAttribute(prop, prop);
- } else {
- this.el_.removeAttribute(prop);
- }
- };
- });
-
- // Wrap native properties with a getter
- // The list is as followed
- // paused, currentTime, buffered, volume, poster, preload, error, seeking
- // seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
- // played, networkState, readyState, videoWidth, videoHeight, crossOrigin
- [
- /**
- * Get the value of `paused` from the media element. `paused` indicates whether the media element
- * is currently paused or not.
- *
- * @method Html5#paused
- * @return {boolean}
- * The value of `paused` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
- */
- 'paused',
- /**
- * Get the value of `currentTime` from the media element. `currentTime` indicates
- * the current second that the media is at in playback.
- *
- * @method Html5#currentTime
- * @return {number}
- * The value of `currentTime` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
- */
- 'currentTime',
- /**
- * Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
- * object that represents the parts of the media that are already downloaded and
- * available for playback.
- *
- * @method Html5#buffered
- * @return {TimeRange}
- * The value of `buffered` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
- */
- 'buffered',
- /**
- * Get the value of `volume` from the media element. `volume` indicates
- * the current playback volume of audio for a media. `volume` will be a value from 0
- * (silent) to 1 (loudest and default).
- *
- * @method Html5#volume
- * @return {number}
- * The value of `volume` from the media element. Value will be between 0-1.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
- */
- 'volume',
- /**
- * Get the value of `poster` from the media element. `poster` indicates
- * that the url of an image file that can/will be shown when no media data is available.
- *
- * @method Html5#poster
- * @return {string}
- * The value of `poster` from the media element. Value will be a url to an
- * image.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
- */
- 'poster',
- /**
- * Get the value of `preload` from the media element. `preload` indicates
- * what should download before the media is interacted with. It can have the following
- * values:
- * - none: nothing should be downloaded
- * - metadata: poster and the first few frames of the media may be downloaded to get
- * media dimensions and other metadata
- * - auto: allow the media and metadata for the media to be downloaded before
- * interaction
- *
- * @method Html5#preload
- * @return {string}
- * The value of `preload` from the media element. Will be 'none', 'metadata',
- * or 'auto'.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
- */
- 'preload',
- /**
- * Get the value of the `error` from the media element. `error` indicates any
- * MediaError that may have occurred during playback. If error returns null there is no
- * current error.
- *
- * @method Html5#error
- * @return {MediaError|null}
- * The value of `error` from the media element. Will be `MediaError` if there
- * is a current error and null otherwise.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
- */
- 'error',
- /**
- * Get the value of `seeking` from the media element. `seeking` indicates whether the
- * media is currently seeking to a new position or not.
- *
- * @method Html5#seeking
- * @return {boolean}
- * - The value of `seeking` from the media element.
- * - True indicates that the media is currently seeking to a new position.
- * - False indicates that the media is not seeking to a new position at this time.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
- */
- 'seeking',
- /**
- * Get the value of `seekable` from the media element. `seekable` returns a
- * `TimeRange` object indicating ranges of time that can currently be `seeked` to.
- *
- * @method Html5#seekable
- * @return {TimeRange}
- * The value of `seekable` from the media element. A `TimeRange` object
- * indicating the current ranges of time that can be seeked to.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
- */
- 'seekable',
- /**
- * Get the value of `ended` from the media element. `ended` indicates whether
- * the media has reached the end or not.
- *
- * @method Html5#ended
- * @return {boolean}
- * - The value of `ended` from the media element.
- * - True indicates that the media has ended.
- * - False indicates that the media has not ended.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
- */
- 'ended',
- /**
- * Get the value of `playbackRate` from the media element. `playbackRate` indicates
- * the rate at which the media is currently playing back. Examples:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5#playbackRate
- * @return {number}
- * The value of `playbackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'playbackRate',
- /**
- * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
- * the rate at which the media is currently playing back. This value will not indicate the current
- * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
- *
- * Examples:
- * - if defaultPlaybackRate is set to 2, media will play twice as fast.
- * - if defaultPlaybackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5.prototype.defaultPlaybackRate
- * @return {number}
- * The value of `defaultPlaybackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'defaultPlaybackRate',
- /**
- * Get the value of 'disablePictureInPicture' from the video element.
- *
- * @method Html5#disablePictureInPicture
- * @return {boolean} value
- * - The value of `disablePictureInPicture` from the video element.
- * - True indicates that the video can't be played in Picture-In-Picture mode
- * - False indicates that the video can be played in Picture-In-Picture mode
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
- */
- 'disablePictureInPicture',
- /**
- * Get the value of `played` from the media element. `played` returns a `TimeRange`
- * object representing points in the media timeline that have been played.
- *
- * @method Html5#played
- * @return {TimeRange}
- * The value of `played` from the media element. A `TimeRange` object indicating
- * the ranges of time that have been played.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
- */
- 'played',
- /**
- * Get the value of `networkState` from the media element. `networkState` indicates
- * the current network state. It returns an enumeration from the following list:
- * - 0: NETWORK_EMPTY
- * - 1: NETWORK_IDLE
- * - 2: NETWORK_LOADING
- * - 3: NETWORK_NO_SOURCE
- *
- * @method Html5#networkState
- * @return {number}
- * The value of `networkState` from the media element. This will be a number
- * from the list in the description.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
- */
- 'networkState',
- /**
- * Get the value of `readyState` from the media element. `readyState` indicates
- * the current state of the media element. It returns an enumeration from the
- * following list:
- * - 0: HAVE_NOTHING
- * - 1: HAVE_METADATA
- * - 2: HAVE_CURRENT_DATA
- * - 3: HAVE_FUTURE_DATA
- * - 4: HAVE_ENOUGH_DATA
- *
- * @method Html5#readyState
- * @return {number}
- * The value of `readyState` from the media element. This will be a number
- * from the list in the description.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
- */
- 'readyState',
- /**
- * Get the value of `videoWidth` from the video element. `videoWidth` indicates
- * the current width of the video in css pixels.
- *
- * @method Html5#videoWidth
- * @return {number}
- * The value of `videoWidth` from the video element. This will be a number
- * in css pixels.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
- */
- 'videoWidth',
- /**
- * Get the value of `videoHeight` from the video element. `videoHeight` indicates
- * the current height of the video in css pixels.
- *
- * @method Html5#videoHeight
- * @return {number}
- * The value of `videoHeight` from the video element. This will be a number
- * in css pixels.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
- */
- 'videoHeight',
- /**
- * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
- * to the browser that should sent the cookies along with the requests for the
- * different assets/playlists
- *
- * @method Html5#crossOrigin
- * @return {string}
- * - anonymous indicates that the media should not sent cookies.
- * - use-credentials indicates that the media should sent cookies along the requests.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
- */
- 'crossOrigin'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop];
- };
- });
-
- // Wrap native properties with a setter in this format:
- // set + toTitleCase(name)
- // The list is as follows:
- // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
- // setDisablePictureInPicture, setCrossOrigin
- [
- /**
- * Set the value of `volume` on the media element. `volume` indicates the current
- * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
- * so on.
- *
- * @method Html5#setVolume
- * @param {number} percentAsDecimal
- * The volume percent as a decimal. Valid range is from 0-1.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
- */
- 'volume',
- /**
- * Set the value of `src` on the media element. `src` indicates the current
- * {@link Tech~SourceObject} for the media.
- *
- * @method Html5#setSrc
- * @param {Tech~SourceObject} src
- * The source object to set as the current source.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
- */
- 'src',
- /**
- * Set the value of `poster` on the media element. `poster` is the url to
- * an image file that can/will be shown when no media data is available.
- *
- * @method Html5#setPoster
- * @param {string} poster
- * The url to an image that should be used as the `poster` for the media
- * element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
- */
- 'poster',
- /**
- * Set the value of `preload` on the media element. `preload` indicates
- * what should download before the media is interacted with. It can have the following
- * values:
- * - none: nothing should be downloaded
- * - metadata: poster and the first few frames of the media may be downloaded to get
- * media dimensions and other metadata
- * - auto: allow the media and metadata for the media to be downloaded before
- * interaction
- *
- * @method Html5#setPreload
- * @param {string} preload
- * The value of `preload` to set on the media element. Must be 'none', 'metadata',
- * or 'auto'.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
- */
- 'preload',
- /**
- * Set the value of `playbackRate` on the media element. `playbackRate` indicates
- * the rate at which the media should play back. Examples:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5#setPlaybackRate
- * @return {number}
- * The value of `playbackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'playbackRate',
- /**
- * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
- * the rate at which the media should play back upon initial startup. Changing this value
- * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
- *
- * Example Values:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5.prototype.setDefaultPlaybackRate
- * @return {number}
- * The value of `defaultPlaybackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
- */
- 'defaultPlaybackRate',
- /**
- * Prevents the browser from suggesting a Picture-in-Picture context menu
- * or to request Picture-in-Picture automatically in some cases.
- *
- * @method Html5#setDisablePictureInPicture
- * @param {boolean} value
- * The true value will disable Picture-in-Picture mode.
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
- */
- 'disablePictureInPicture',
- /**
- * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
- * to the browser that should sent the cookies along with the requests for the
- * different assets/playlists
- *
- * @method Html5#setCrossOrigin
- * @param {string} crossOrigin
- * - anonymous indicates that the media should not sent cookies.
- * - use-credentials indicates that the media should sent cookies along the requests.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
- */
- 'crossOrigin'].forEach(function (prop) {
- Html5.prototype['set' + toTitleCase(prop)] = function (v) {
- this.el_[prop] = v;
- };
- });
-
- // wrap native functions with a function
- // The list is as follows:
- // pause, load, play
- [
- /**
- * A wrapper around the media elements `pause` function. This will call the `HTML5`
- * media elements `pause` function.
- *
- * @method Html5#pause
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
- */
- 'pause',
- /**
- * A wrapper around the media elements `load` function. This will call the `HTML5`s
- * media element `load` function.
- *
- * @method Html5#load
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
- */
- 'load',
- /**
- * A wrapper around the media elements `play` function. This will call the `HTML5`s
- * media element `play` function.
- *
- * @method Html5#play
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
- */
- 'play'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop]();
- };
- });
- Tech.withSourceHandlers(Html5);
-
- /**
- * Native source handler for Html5, simply passes the source to the media element.
- *
- * @property {Tech~SourceObject} source
- * The source object
- *
- * @property {Html5} tech
- * The instance of the HTML5 tech.
- */
- Html5.nativeSourceHandler = {};
-
- /**
- * Check if the media element can play the given mime type.
- *
- * @param {string} type
- * The mimetype to check
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- Html5.nativeSourceHandler.canPlayType = function (type) {
- // IE without MediaPlayer throws an error (#519)
- try {
- return Html5.TEST_VID.canPlayType(type);
- } catch (e) {
- return '';
- }
- };
-
- /**
- * Check if the media element can handle a source natively.
- *
- * @param {Tech~SourceObject} source
- * The source object
- *
- * @param {Object} [options]
- * Options to be passed to the tech.
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string).
- */
- Html5.nativeSourceHandler.canHandleSource = function (source, options) {
- // If a type was provided we should rely on that
- if (source.type) {
- return Html5.nativeSourceHandler.canPlayType(source.type);
-
- // If no type, fall back to checking 'video/[EXTENSION]'
- } else if (source.src) {
- const ext = getFileExtension(source.src);
- return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
- }
- return '';
- };
-
- /**
- * Pass the source to the native media element.
- *
- * @param {Tech~SourceObject} source
- * The source object
- *
- * @param {Html5} tech
- * The instance of the Html5 tech
- *
- * @param {Object} [options]
- * The options to pass to the source
- */
- Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
- tech.setSrc(source.src);
- };
-
- /**
- * A noop for the native dispose function, as cleanup is not needed.
- */
- Html5.nativeSourceHandler.dispose = function () {};
-
- // Register the native source handler
- Html5.registerSourceHandler(Html5.nativeSourceHandler);
- Tech.registerTech('Html5', Html5);
-
- /**
- * @file player.js
- */
-
- // The following tech events are simply re-triggered
- // on the player when they happen
- const TECH_EVENTS_RETRIGGER = [
- /**
- * Fired while the user agent is downloading media data.
- *
- * @event Player#progress
- * @type {Event}
- */
- /**
- * Retrigger the `progress` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechProgress_
- * @fires Player#progress
- * @listens Tech#progress
- */
- 'progress',
- /**
- * Fires when the loading of an audio/video is aborted.
- *
- * @event Player#abort
- * @type {Event}
- */
- /**
- * Retrigger the `abort` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechAbort_
- * @fires Player#abort
- * @listens Tech#abort
- */
- 'abort',
- /**
- * Fires when the browser is intentionally not getting media data.
- *
- * @event Player#suspend
- * @type {Event}
- */
- /**
- * Retrigger the `suspend` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechSuspend_
- * @fires Player#suspend
- * @listens Tech#suspend
- */
- 'suspend',
- /**
- * Fires when the current playlist is empty.
- *
- * @event Player#emptied
- * @type {Event}
- */
- /**
- * Retrigger the `emptied` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechEmptied_
- * @fires Player#emptied
- * @listens Tech#emptied
- */
- 'emptied',
- /**
- * Fires when the browser is trying to get media data, but data is not available.
- *
- * @event Player#stalled
- * @type {Event}
- */
- /**
- * Retrigger the `stalled` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechStalled_
- * @fires Player#stalled
- * @listens Tech#stalled
- */
- 'stalled',
- /**
- * Fires when the browser has loaded meta data for the audio/video.
- *
- * @event Player#loadedmetadata
- * @type {Event}
- */
- /**
- * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechLoadedmetadata_
- * @fires Player#loadedmetadata
- * @listens Tech#loadedmetadata
- */
- 'loadedmetadata',
- /**
- * Fires when the browser has loaded the current frame of the audio/video.
- *
- * @event Player#loadeddata
- * @type {event}
- */
- /**
- * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechLoaddeddata_
- * @fires Player#loadeddata
- * @listens Tech#loadeddata
- */
- 'loadeddata',
- /**
- * Fires when the current playback position has changed.
- *
- * @event Player#timeupdate
- * @type {event}
- */
- /**
- * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechTimeUpdate_
- * @fires Player#timeupdate
- * @listens Tech#timeupdate
- */
- 'timeupdate',
- /**
- * Fires when the video's intrinsic dimensions change
- *
- * @event Player#resize
- * @type {event}
- */
- /**
- * Retrigger the `resize` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechResize_
- * @fires Player#resize
- * @listens Tech#resize
- */
- 'resize',
- /**
- * Fires when the volume has been changed
- *
- * @event Player#volumechange
- * @type {event}
- */
- /**
- * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechVolumechange_
- * @fires Player#volumechange
- * @listens Tech#volumechange
- */
- 'volumechange',
- /**
- * Fires when the text track has been changed
- *
- * @event Player#texttrackchange
- * @type {event}
- */
- /**
- * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechTexttrackchange_
- * @fires Player#texttrackchange
- * @listens Tech#texttrackchange
- */
- 'texttrackchange'];
-
- // events to queue when playback rate is zero
- // this is a hash for the sole purpose of mapping non-camel-cased event names
- // to camel-cased function names
- const TECH_EVENTS_QUEUE = {
- canplay: 'CanPlay',
- canplaythrough: 'CanPlayThrough',
- playing: 'Playing',
- seeked: 'Seeked'
- };
- const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
- const BREAKPOINT_CLASSES = {};
-
- // grep: vjs-layout-tiny
- // grep: vjs-layout-x-small
- // grep: vjs-layout-small
- // grep: vjs-layout-medium
- // grep: vjs-layout-large
- // grep: vjs-layout-x-large
- // grep: vjs-layout-huge
- BREAKPOINT_ORDER.forEach(k => {
- const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
- BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
- });
- const DEFAULT_BREAKPOINTS = {
- tiny: 210,
- xsmall: 320,
- small: 425,
- medium: 768,
- large: 1440,
- xlarge: 2560,
- huge: Infinity
- };
-
- /**
- * An instance of the `Player` class is created when any of the Video.js setup methods
- * are used to initialize a video.
- *
- * After an instance has been created it can be accessed globally in three ways:
- * 1. By calling `videojs.getPlayer('example_video_1');`
- * 2. By calling `videojs('example_video_1');` (not recommended)
- * 2. By using it directly via `videojs.players.example_video_1;`
- *
- * @extends Component
- * @global
- */
- class Player extends Component {
- /**
- * Create an instance of this class.
- *
- * @param {Element} tag
- * The original video DOM element used for configuring options.
- *
- * @param {Object} [options]
- * Object of option names and values.
- *
- * @param {Function} [ready]
- * Ready callback function.
- */
- constructor(tag, options, ready) {
- // Make sure tag ID exists
- // also here.. probably better
- tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
-
- // Set Options
- // The options argument overrides options set in the video tag
- // which overrides globally set options.
- // This latter part coincides with the load order
- // (tag must exist before Player)
- options = Object.assign(Player.getTagSettings(tag), options);
-
- // Delay the initialization of children because we need to set up
- // player properties first, and can't use `this` before `super()`
- options.initChildren = false;
-
- // Same with creating the element
- options.createEl = false;
-
- // don't auto mixin the evented mixin
- options.evented = false;
-
- // we don't want the player to report touch activity on itself
- // see enableTouchActivity in Component
- options.reportTouchActivity = false;
-
- // If language is not set, get the closest lang attribute
- if (!options.language) {
- const closest = tag.closest('[lang]');
- if (closest) {
- options.language = closest.getAttribute('lang');
- }
- }
-
- // Run base component initializing with new options
- super(null, options, ready);
-
- // Create bound methods for document listeners.
- this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
- this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
- this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
- this.boundApplyInitTime_ = e => this.applyInitTime_(e);
- this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
- this.boundHandleTechClick_ = e => this.handleTechClick_(e);
- this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
- this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
- this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
- this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
- this.boundHandleTechTap_ = e => this.handleTechTap_(e);
-
- // default isFullscreen_ to false
- this.isFullscreen_ = false;
-
- // create logger
- this.log = createLogger(this.id_);
-
- // Hold our own reference to fullscreen api so it can be mocked in tests
- this.fsApi_ = FullscreenApi;
-
- // Tracks when a tech changes the poster
- this.isPosterFromTech_ = false;
-
- // Holds callback info that gets queued when playback rate is zero
- // and a seek is happening
- this.queuedCallbacks_ = [];
-
- // Turn off API access because we're loading a new tech that might load asynchronously
- this.isReady_ = false;
-
- // Init state hasStarted_
- this.hasStarted_ = false;
-
- // Init state userActive_
- this.userActive_ = false;
-
- // Init debugEnabled_
- this.debugEnabled_ = false;
-
- // Init state audioOnlyMode_
- this.audioOnlyMode_ = false;
-
- // Init state audioPosterMode_
- this.audioPosterMode_ = false;
-
- // Init state audioOnlyCache_
- this.audioOnlyCache_ = {
- playerHeight: null,
- hiddenChildren: []
- };
-
- // if the global option object was accidentally blown away by
- // someone, bail early with an informative error
- if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
- throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
- }
-
- // Store the original tag used to set options
- this.tag = tag;
-
- // Store the tag attributes used to restore html5 element
- this.tagAttributes = tag && getAttributes(tag);
-
- // Update current language
- this.language(this.options_.language);
-
- // Update Supported Languages
- if (options.languages) {
- // Normalise player option languages to lowercase
- const languagesToLower = {};
- Object.getOwnPropertyNames(options.languages).forEach(function (name) {
- languagesToLower[name.toLowerCase()] = options.languages[name];
- });
- this.languages_ = languagesToLower;
- } else {
- this.languages_ = Player.prototype.options_.languages;
- }
- this.resetCache_();
-
- // Set poster
- /** @type string */
- this.poster_ = options.poster || '';
-
- // Set controls
- /** @type {boolean} */
- this.controls_ = !!options.controls;
-
- // Original tag settings stored in options
- // now remove immediately so native controls don't flash.
- // May be turned back on by HTML5 tech if nativeControlsForTouch is true
- tag.controls = false;
- tag.removeAttribute('controls');
- this.changingSrc_ = false;
- this.playCallbacks_ = [];
- this.playTerminatedQueue_ = [];
-
- // the attribute overrides the option
- if (tag.hasAttribute('autoplay')) {
- this.autoplay(true);
- } else {
- // otherwise use the setter to validate and
- // set the correct value.
- this.autoplay(this.options_.autoplay);
- }
-
- // check plugins
- if (options.plugins) {
- Object.keys(options.plugins).forEach(name => {
- if (typeof this[name] !== 'function') {
- throw new Error(`plugin "${name}" does not exist`);
- }
- });
- }
-
- /*
- * Store the internal state of scrubbing
- *
- * @private
- * @return {Boolean} True if the user is scrubbing
- */
- this.scrubbing_ = false;
- this.el_ = this.createEl();
-
- // Make this an evented object and use `el_` as its event bus.
- evented(this, {
- eventBusKey: 'el_'
- });
-
- // listen to document and player fullscreenchange handlers so we receive those events
- // before a user can receive them so we can update isFullscreen appropriately.
- // make sure that we listen to fullscreenchange events before everything else to make sure that
- // our isFullscreen method is updated properly for internal components as well as external.
- if (this.fsApi_.requestFullscreen) {
- on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- }
- if (this.fluid_) {
- this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- }
- // We also want to pass the original player options to each component and plugin
- // as well so they don't need to reach back into the player for options later.
- // We also need to do another copy of this.options_ so we don't end up with
- // an infinite loop.
- const playerOptionsCopy = merge(this.options_);
-
- // Load plugins
- if (options.plugins) {
- Object.keys(options.plugins).forEach(name => {
- this[name](options.plugins[name]);
- });
- }
-
- // Enable debug mode to fire debugon event for all plugins.
- if (options.debug) {
- this.debug(true);
- }
- this.options_.playerOptions = playerOptionsCopy;
- this.middleware_ = [];
- this.playbackRates(options.playbackRates);
- if (options.experimentalSvgIcons) {
- // Add SVG Sprite to the DOM
- const parser = new window.DOMParser();
- const parsedSVG = parser.parseFromString(icons, 'image/svg+xml');
- const errorNode = parsedSVG.querySelector('parsererror');
- if (errorNode) {
- log.warn('Failed to load SVG Icons. Falling back to Font Icons.');
- this.options_.experimentalSvgIcons = null;
- } else {
- const sprite = parsedSVG.documentElement;
- sprite.style.display = 'none';
- this.el_.appendChild(sprite);
- this.addClass('vjs-svg-icons-enabled');
- }
- }
- this.initChildren();
-
- // Set isAudio based on whether or not an audio tag was used
- this.isAudio(tag.nodeName.toLowerCase() === 'audio');
-
- // Update controls className. Can't do this when the controls are initially
- // set because the element doesn't exist yet.
- if (this.controls()) {
- this.addClass('vjs-controls-enabled');
- } else {
- this.addClass('vjs-controls-disabled');
- }
-
- // Set ARIA label and region role depending on player type
- this.el_.setAttribute('role', 'region');
- if (this.isAudio()) {
- this.el_.setAttribute('aria-label', this.localize('Audio Player'));
- } else {
- this.el_.setAttribute('aria-label', this.localize('Video Player'));
- }
- if (this.isAudio()) {
- this.addClass('vjs-audio');
- }
-
- // TODO: Make this smarter. Toggle user state between touching/mousing
- // using events, since devices can have both touch and mouse events.
- // TODO: Make this check be performed again when the window switches between monitors
- // (See https://github.com/videojs/video.js/issues/5683)
- if (TOUCH_ENABLED) {
- this.addClass('vjs-touch-enabled');
- }
-
- // iOS Safari has broken hover handling
- if (!IS_IOS) {
- this.addClass('vjs-workinghover');
- }
-
- // Make player easily findable by ID
- Player.players[this.id_] = this;
-
- // Add a major version class to aid css in plugins
- const majorVersion = version.split('.')[0];
- this.addClass(`vjs-v${majorVersion}`);
-
- // When the player is first initialized, trigger activity so components
- // like the control bar show themselves if needed
- this.userActive(true);
- this.reportUserActivity();
- this.one('play', e => this.listenForUserActivity_(e));
- this.on('keydown', e => this.handleKeyDown(e));
- this.on('languagechange', e => this.handleLanguagechange(e));
- this.breakpoints(this.options_.breakpoints);
- this.responsive(this.options_.responsive);
-
- // Calling both the audio mode methods after the player is fully
- // setup to be able to listen to the events triggered by them
- this.on('ready', () => {
- // Calling the audioPosterMode method first so that
- // the audioOnlyMode can take precedence when both options are set to true
- this.audioPosterMode(this.options_.audioPosterMode);
- this.audioOnlyMode(this.options_.audioOnlyMode);
- });
- }
-
- /**
- * Destroys the video player and does any necessary cleanup.
- *
- * This is especially helpful if you are dynamically adding and removing videos
- * to/from the DOM.
- *
- * @fires Player#dispose
- */
- dispose() {
- /**
- * Called when the player is being disposed of.
- *
- * @event Player#dispose
- * @type {Event}
- */
- this.trigger('dispose');
- // prevent dispose from being called twice
- this.off('dispose');
-
- // Make sure all player-specific document listeners are unbound. This is
- off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- off(document, 'keydown', this.boundFullWindowOnEscKey_);
- if (this.styleEl_ && this.styleEl_.parentNode) {
- this.styleEl_.parentNode.removeChild(this.styleEl_);
- this.styleEl_ = null;
- }
-
- // Kill reference to this player
- Player.players[this.id_] = null;
- if (this.tag && this.tag.player) {
- this.tag.player = null;
- }
- if (this.el_ && this.el_.player) {
- this.el_.player = null;
- }
- if (this.tech_) {
- this.tech_.dispose();
- this.isPosterFromTech_ = false;
- this.poster_ = '';
- }
- if (this.playerElIngest_) {
- this.playerElIngest_ = null;
- }
- if (this.tag) {
- this.tag = null;
- }
- clearCacheForPlayer(this);
-
- // remove all event handlers for track lists
- // all tracks and track listeners are removed on
- // tech dispose
- ALL.names.forEach(name => {
- const props = ALL[name];
- const list = this[props.getterName]();
-
- // if it is not a native list
- // we have to manually remove event listeners
- if (list && list.off) {
- list.off();
- }
- });
-
- // the actual .el_ is removed here, or replaced if
- super.dispose({
- restoreEl: this.options_.restoreEl
- });
- }
-
- /**
- * Create the `Player`'s DOM element.
- *
- * @return {Element}
- * The DOM element that gets created.
- */
- createEl() {
- let tag = this.tag;
- let el;
- let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
- const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
- if (playerElIngest) {
- el = this.el_ = tag.parentNode;
- } else if (!divEmbed) {
- el = this.el_ = super.createEl('div');
- }
-
- // Copy over all the attributes from the tag, including ID and class
- // ID will now reference player box, not the video tag
- const attrs = getAttributes(tag);
- if (divEmbed) {
- el = this.el_ = tag;
- tag = this.tag = document.createElement('video');
- while (el.children.length) {
- tag.appendChild(el.firstChild);
- }
- if (!hasClass(el, 'video-js')) {
- addClass(el, 'video-js');
- }
- el.appendChild(tag);
- playerElIngest = this.playerElIngest_ = el;
- // move properties over from our custom `video-js` element
- // to our new `video` element. This will move things like
- // `src` or `controls` that were set via js before the player
- // was initialized.
- Object.keys(el).forEach(k => {
- try {
- tag[k] = el[k];
- } catch (e) {
- // we got a a property like outerHTML which we can't actually copy, ignore it
- }
- });
- }
-
- // set tabindex to -1 to remove the video element from the focus order
- tag.setAttribute('tabindex', '-1');
- attrs.tabindex = '-1';
-
- // Workaround for #4583 on Chrome (on Windows) with JAWS.
- // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
- // Note that we can't detect if JAWS is being used, but this ARIA attribute
- // doesn't change behavior of Chrome if JAWS is not being used
- if (IS_CHROME && IS_WINDOWS) {
- tag.setAttribute('role', 'application');
- attrs.role = 'application';
- }
-
- // Remove width/height attrs from tag so CSS can make it 100% width/height
- tag.removeAttribute('width');
- tag.removeAttribute('height');
- if ('width' in attrs) {
- delete attrs.width;
- }
- if ('height' in attrs) {
- delete attrs.height;
- }
- Object.getOwnPropertyNames(attrs).forEach(function (attr) {
- // don't copy over the class attribute to the player element when we're in a div embed
- // the class is already set up properly in the divEmbed case
- // and we want to make sure that the `video-js` class doesn't get lost
- if (!(divEmbed && attr === 'class')) {
- el.setAttribute(attr, attrs[attr]);
- }
- if (divEmbed) {
- tag.setAttribute(attr, attrs[attr]);
- }
- });
-
- // Update tag id/class for use as HTML5 playback tech
- // Might think we should do this after embedding in container so .vjs-tech class
- // doesn't flash 100% width/height, but class only applies with .video-js parent
- tag.playerId = tag.id;
- tag.id += '_html5_api';
- tag.className = 'vjs-tech';
-
- // Make player findable on elements
- tag.player = el.player = this;
- // Default state of video is paused
- this.addClass('vjs-paused');
- const deviceClassNames = ['IS_SMART_TV', 'IS_TIZEN', 'IS_WEBOS', 'IS_ANDROID', 'IS_IPAD', 'IS_IPHONE'].filter(key => browser[key]).map(key => {
- return 'vjs-device-' + key.substring(3).toLowerCase().replace(/\_/g, '-');
- });
- this.addClass(...deviceClassNames);
-
- // Add a style element in the player that we'll use to set the width/height
- // of the player in a way that's still overridable by CSS, just like the
- // video element
- if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
- this.styleEl_ = createStyleElement('vjs-styles-dimensions');
- const defaultsStyleEl = $('.vjs-styles-defaults');
- const head = $('head');
- head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
- }
- this.fill_ = false;
- this.fluid_ = false;
-
- // Pass in the width/height/aspectRatio options which will update the style el
- this.width(this.options_.width);
- this.height(this.options_.height);
- this.fill(this.options_.fill);
- this.fluid(this.options_.fluid);
- this.aspectRatio(this.options_.aspectRatio);
- // support both crossOrigin and crossorigin to reduce confusion and issues around the name
- this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
-
- // Hide any links within the video/audio tag,
- // because IE doesn't hide them completely from screen readers.
- const links = tag.getElementsByTagName('a');
- for (let i = 0; i < links.length; i++) {
- const linkEl = links.item(i);
- addClass(linkEl, 'vjs-hidden');
- linkEl.setAttribute('hidden', 'hidden');
- }
-
- // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
- // keep track of the original for later so we can know if the source originally failed
- tag.initNetworkState_ = tag.networkState;
-
- // Wrap video tag in div (el/box) container
- if (tag.parentNode && !playerElIngest) {
- tag.parentNode.insertBefore(el, tag);
- }
-
- // insert the tag as the first child of the player element
- // then manually add it to the children array so that this.addChild
- // will work properly for other components
- //
- // Breaks iPhone, fixed in HTML5 setup.
- prependTo(tag, el);
- this.children_.unshift(tag);
-
- // Set lang attr on player to ensure CSS :lang() in consistent with player
- // if it's been set to something different to the doc
- this.el_.setAttribute('lang', this.language_);
- this.el_.setAttribute('translate', 'no');
- this.el_ = el;
- return el;
- }
-
- /**
- * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the `` tag to control the CORS
- * behavior.
- *
- * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
- *
- * @param {string|null} [value]
- * The value to set the `Player`'s crossOrigin to. If an argument is
- * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
- *
- * @return {string|null|undefined}
- * - The current crossOrigin value of the `Player` when getting.
- * - undefined when setting
- */
- crossOrigin(value) {
- // `null` can be set to unset a value
- if (typeof value === 'undefined') {
- return this.techGet_('crossOrigin');
- }
- if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
- log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
- return;
- }
- this.techCall_('setCrossOrigin', value);
- if (this.posterImage) {
- this.posterImage.crossOrigin(value);
- }
- return;
- }
-
- /**
- * A getter/setter for the `Player`'s width. Returns the player's configured value.
- * To get the current width use `currentWidth()`.
- *
- * @param {number|string} [value]
- * CSS value to set the `Player`'s width to.
- *
- * @return {number|undefined}
- * - The current width of the `Player` when getting.
- * - Nothing when setting
- */
- width(value) {
- return this.dimension('width', value);
- }
-
- /**
- * A getter/setter for the `Player`'s height. Returns the player's configured value.
- * To get the current height use `currentheight()`.
- *
- * @param {number|string} [value]
- * CSS value to set the `Player`'s height to.
- *
- * @return {number|undefined}
- * - The current height of the `Player` when getting.
- * - Nothing when setting
- */
- height(value) {
- return this.dimension('height', value);
- }
-
- /**
- * A getter/setter for the `Player`'s width & height.
- *
- * @param {string} dimension
- * This string can be:
- * - 'width'
- * - 'height'
- *
- * @param {number|string} [value]
- * Value for dimension specified in the first argument.
- *
- * @return {number}
- * The dimension arguments value when getting (width/height).
- */
- dimension(dimension, value) {
- const privDimension = dimension + '_';
- if (value === undefined) {
- return this[privDimension] || 0;
- }
- if (value === '' || value === 'auto') {
- // If an empty string is given, reset the dimension to be automatic
- this[privDimension] = undefined;
- this.updateStyleEl_();
- return;
- }
- const parsedVal = parseFloat(value);
- if (isNaN(parsedVal)) {
- log.error(`Improper value "${value}" supplied for for ${dimension}`);
- return;
- }
- this[privDimension] = parsedVal;
- this.updateStyleEl_();
- }
-
- /**
- * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
- *
- * Turning this on will turn off fill mode.
- *
- * @param {boolean} [bool]
- * - A value of true adds the class.
- * - A value of false removes the class.
- * - No value will be a getter.
- *
- * @return {boolean|undefined}
- * - The value of fluid when getting.
- * - `undefined` when setting.
- */
- fluid(bool) {
- if (bool === undefined) {
- return !!this.fluid_;
- }
- this.fluid_ = !!bool;
- if (isEvented(this)) {
- this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- }
- if (bool) {
- this.addClass('vjs-fluid');
- this.fill(false);
- addEventedCallback(this, () => {
- this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- });
- } else {
- this.removeClass('vjs-fluid');
- }
- this.updateStyleEl_();
- }
-
- /**
- * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
- *
- * Turning this on will turn off fluid mode.
- *
- * @param {boolean} [bool]
- * - A value of true adds the class.
- * - A value of false removes the class.
- * - No value will be a getter.
- *
- * @return {boolean|undefined}
- * - The value of fluid when getting.
- * - `undefined` when setting.
- */
- fill(bool) {
- if (bool === undefined) {
- return !!this.fill_;
- }
- this.fill_ = !!bool;
- if (bool) {
- this.addClass('vjs-fill');
- this.fluid(false);
- } else {
- this.removeClass('vjs-fill');
- }
- }
-
- /**
- * Get/Set the aspect ratio
- *
- * @param {string} [ratio]
- * Aspect ratio for player
- *
- * @return {string|undefined}
- * returns the current aspect ratio when getting
- */
-
- /**
- * A getter/setter for the `Player`'s aspect ratio.
- *
- * @param {string} [ratio]
- * The value to set the `Player`'s aspect ratio to.
- *
- * @return {string|undefined}
- * - The current aspect ratio of the `Player` when getting.
- * - undefined when setting
- */
- aspectRatio(ratio) {
- if (ratio === undefined) {
- return this.aspectRatio_;
- }
-
- // Check for width:height format
- if (!/^\d+\:\d+$/.test(ratio)) {
- throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
- }
- this.aspectRatio_ = ratio;
-
- // We're assuming if you set an aspect ratio you want fluid mode,
- // because in fixed mode you could calculate width and height yourself.
- this.fluid(true);
- this.updateStyleEl_();
- }
-
- /**
- * Update styles of the `Player` element (height, width and aspect ratio).
- *
- * @private
- * @listens Tech#loadedmetadata
- */
- updateStyleEl_() {
- if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) {
- const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
- const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
- const techEl = this.tech_ && this.tech_.el();
- if (techEl) {
- if (width >= 0) {
- techEl.width = width;
- }
- if (height >= 0) {
- techEl.height = height;
- }
- }
- return;
- }
- let width;
- let height;
- let aspectRatio;
- let idClass;
-
- // The aspect ratio is either used directly or to calculate width and height.
- if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
- // Use any aspectRatio that's been specifically set
- aspectRatio = this.aspectRatio_;
- } else if (this.videoWidth() > 0) {
- // Otherwise try to get the aspect ratio from the video metadata
- aspectRatio = this.videoWidth() + ':' + this.videoHeight();
- } else {
- // Or use a default. The video element's is 2:1, but 16:9 is more common.
- aspectRatio = '16:9';
- }
-
- // Get the ratio as a decimal we can use to calculate dimensions
- const ratioParts = aspectRatio.split(':');
- const ratioMultiplier = ratioParts[1] / ratioParts[0];
- if (this.width_ !== undefined) {
- // Use any width that's been specifically set
- width = this.width_;
- } else if (this.height_ !== undefined) {
- // Or calculate the width from the aspect ratio if a height has been set
- width = this.height_ / ratioMultiplier;
- } else {
- // Or use the video's metadata, or use the video el's default of 300
- width = this.videoWidth() || 300;
- }
- if (this.height_ !== undefined) {
- // Use any height that's been specifically set
- height = this.height_;
- } else {
- // Otherwise calculate the height from the ratio and the width
- height = width * ratioMultiplier;
- }
-
- // Ensure the CSS class is valid by starting with an alpha character
- if (/^[^a-zA-Z]/.test(this.id())) {
- idClass = 'dimensions-' + this.id();
- } else {
- idClass = this.id() + '-dimensions';
- }
-
- // Ensure the right class is still on the player for the style element
- this.addClass(idClass);
- setTextContent(this.styleEl_, `
- .${idClass} {
- width: ${width}px;
- height: ${height}px;
- }
-
- .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: ${ratioMultiplier * 100}%;
- }
- `);
- }
-
- /**
- * Load/Create an instance of playback {@link Tech} including element
- * and API methods. Then append the `Tech` element in `Player` as a child.
- *
- * @param {string} techName
- * name of the playback technology
- *
- * @param {string} source
- * video source
- *
- * @private
- */
- loadTech_(techName, source) {
- // Pause and remove current playback technology
- if (this.tech_) {
- this.unloadTech_();
- }
- const titleTechName = toTitleCase(techName);
- const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
-
- // get rid of the HTML5 video tag as soon as we are using another tech
- if (titleTechName !== 'Html5' && this.tag) {
- Tech.getTech('Html5').disposeMediaElement(this.tag);
- this.tag.player = null;
- this.tag = null;
- }
- this.techName_ = titleTechName;
-
- // Turn off API access because we're loading a new tech that might load asynchronously
- this.isReady_ = false;
- let autoplay = this.autoplay();
-
- // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
- // because the player is going to handle autoplay on `loadstart`
- if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
- autoplay = false;
- }
-
- // Grab tech-specific options from player options and add source and parent element to use.
- const techOptions = {
- source,
- autoplay,
- 'nativeControlsForTouch': this.options_.nativeControlsForTouch,
- 'playerId': this.id(),
- 'techId': `${this.id()}_${camelTechName}_api`,
- 'playsinline': this.options_.playsinline,
- 'preload': this.options_.preload,
- 'loop': this.options_.loop,
- 'disablePictureInPicture': this.options_.disablePictureInPicture,
- 'muted': this.options_.muted,
- 'poster': this.poster(),
- 'language': this.language(),
- 'playerElIngest': this.playerElIngest_ || false,
- 'vtt.js': this.options_['vtt.js'],
- 'canOverridePoster': !!this.options_.techCanOverridePoster,
- 'enableSourceset': this.options_.enableSourceset
- };
- ALL.names.forEach(name => {
- const props = ALL[name];
- techOptions[props.getterName] = this[props.privateName];
- });
- Object.assign(techOptions, this.options_[titleTechName]);
- Object.assign(techOptions, this.options_[camelTechName]);
- Object.assign(techOptions, this.options_[techName.toLowerCase()]);
- if (this.tag) {
- techOptions.tag = this.tag;
- }
- if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
- techOptions.startTime = this.cache_.currentTime;
- }
-
- // Initialize tech instance
- const TechClass = Tech.getTech(techName);
- if (!TechClass) {
- throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
- }
- this.tech_ = new TechClass(techOptions);
-
- // player.triggerReady is always async, so don't need this to be async
- this.tech_.ready(bind_(this, this.handleTechReady_), true);
- textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
-
- // Listen to all HTML5-defined events and trigger them on the player
- TECH_EVENTS_RETRIGGER.forEach(event => {
- this.on(this.tech_, event, e => this[`handleTech${toTitleCase(event)}_`](e));
- });
- Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
- this.on(this.tech_, event, eventObj => {
- if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
- this.queuedCallbacks_.push({
- callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
- event: eventObj
- });
- return;
- }
- this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
- });
- });
- this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
- this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
- this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
- this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
- this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
- this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
- this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
- this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
- this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
- this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
- this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
- this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
- this.on(this.tech_, 'error', e => this.handleTechError_(e));
- this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
- this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
- this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
- this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
- this.usingNativeControls(this.techGet_('controls'));
- if (this.controls() && !this.usingNativeControls()) {
- this.addTechControlsListeners_();
- }
-
- // Add the tech element in the DOM if it was not already there
- // Make sure to not insert the original video element if using Html5
- if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
- prependTo(this.tech_.el(), this.el());
- }
-
- // Get rid of the original video tag reference after the first tech is loaded
- if (this.tag) {
- this.tag.player = null;
- this.tag = null;
- }
- }
-
- /**
- * Unload and dispose of the current playback {@link Tech}.
- *
- * @private
- */
- unloadTech_() {
- // Save the current text tracks so that we can reuse the same text tracks with the next tech
- ALL.names.forEach(name => {
- const props = ALL[name];
- this[props.privateName] = this[props.getterName]();
- });
- this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
- this.isReady_ = false;
- this.tech_.dispose();
- this.tech_ = false;
- if (this.isPosterFromTech_) {
- this.poster_ = '';
- this.trigger('posterchange');
- }
- this.isPosterFromTech_ = false;
- }
-
- /**
- * Return a reference to the current {@link Tech}.
- * It will print a warning by default about the danger of using the tech directly
- * but any argument that is passed in will silence the warning.
- *
- * @param {*} [safety]
- * Anything passed in to silence the warning
- *
- * @return {Tech}
- * The Tech
- */
- tech(safety) {
- if (safety === undefined) {
- log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
- }
- return this.tech_;
- }
-
- /**
- * An object that contains Video.js version.
- *
- * @typedef {Object} PlayerVersion
- *
- * @property {string} 'video.js' - Video.js version
- */
-
- /**
- * Returns an object with Video.js version.
- *
- * @return {PlayerVersion}
- * An object with Video.js version.
- */
- version() {
- return {
- 'video.js': version
- };
- }
-
- /**
- * Set up click and touch listeners for the playback element
- *
- * - On desktops: a click on the video itself will toggle playback
- * - On mobile devices: a click on the video toggles controls
- * which is done by toggling the user state between active and
- * inactive
- * - A tap can signal that a user has become active or has become inactive
- * e.g. a quick tap on an iPhone movie should reveal the controls. Another
- * quick tap should hide them again (signaling the user is in an inactive
- * viewing state)
- * - In addition to this, we still want the user to be considered inactive after
- * a few seconds of inactivity.
- *
- * > Note: the only part of iOS interaction we can't mimic with this setup
- * is a touch and hold on the video element counting as activity in order to
- * keep the controls showing, but that shouldn't be an issue. A touch and hold
- * on any controls will still keep the user active
- *
- * @private
- */
- addTechControlsListeners_() {
- // Make sure to remove all the previous listeners in case we are called multiple times.
- this.removeTechControlsListeners_();
- this.on(this.tech_, 'click', this.boundHandleTechClick_);
- this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
-
- // If the controls were hidden we don't want that to change without a tap event
- // so we'll check if the controls were already showing before reporting user
- // activity
- this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
- this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
- this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
-
- // The tap listener needs to come after the touchend listener because the tap
- // listener cancels out any reportedUserActivity when setting userActive(false)
- this.on(this.tech_, 'tap', this.boundHandleTechTap_);
- }
-
- /**
- * Remove the listeners used for click and tap controls. This is needed for
- * toggling to controls disabled, where a tap/touch should do nothing.
- *
- * @private
- */
- removeTechControlsListeners_() {
- // We don't want to just use `this.off()` because there might be other needed
- // listeners added by techs that extend this.
- this.off(this.tech_, 'tap', this.boundHandleTechTap_);
- this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
- this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
- this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
- this.off(this.tech_, 'click', this.boundHandleTechClick_);
- this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
- }
-
- /**
- * Player waits for the tech to be ready
- *
- * @private
- */
- handleTechReady_() {
- this.triggerReady();
-
- // Keep the same volume as before
- if (this.cache_.volume) {
- this.techCall_('setVolume', this.cache_.volume);
- }
-
- // Look if the tech found a higher resolution poster while loading
- this.handleTechPosterChange_();
-
- // Update the duration if available
- this.handleTechDurationChange_();
- }
-
- /**
- * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
- *
- * @fires Player#loadstart
- * @listens Tech#loadstart
- * @private
- */
- handleTechLoadStart_() {
- // TODO: Update to use `emptied` event instead. See #1277.
-
- this.removeClass('vjs-ended', 'vjs-seeking');
-
- // reset the error state
- this.error(null);
-
- // Update the duration
- this.handleTechDurationChange_();
- if (!this.paused()) {
- /**
- * Fired when the user agent begins looking for media data
- *
- * @event Player#loadstart
- * @type {Event}
- */
- this.trigger('loadstart');
- } else {
- // reset the hasStarted state
- this.hasStarted(false);
- this.trigger('loadstart');
- }
-
- // autoplay happens after loadstart for the browser,
- // so we mimic that behavior
- this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
- }
-
- /**
- * Handle autoplay string values, rather than the typical boolean
- * values that should be handled by the tech. Note that this is not
- * part of any specification. Valid values and what they do can be
- * found on the autoplay getter at Player#autoplay()
- */
- manualAutoplay_(type) {
- if (!this.tech_ || typeof type !== 'string') {
- return;
- }
-
- // Save original muted() value, set muted to true, and attempt to play().
- // On promise rejection, restore muted from saved value
- const resolveMuted = () => {
- const previouslyMuted = this.muted();
- this.muted(true);
- const restoreMuted = () => {
- this.muted(previouslyMuted);
- };
-
- // restore muted on play terminatation
- this.playTerminatedQueue_.push(restoreMuted);
- const mutedPromise = this.play();
- if (!isPromise(mutedPromise)) {
- return;
- }
- return mutedPromise.catch(err => {
- restoreMuted();
- throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
- });
- };
- let promise;
-
- // if muted defaults to true
- // the only thing we can do is call play
- if (type === 'any' && !this.muted()) {
- promise = this.play();
- if (isPromise(promise)) {
- promise = promise.catch(resolveMuted);
- }
- } else if (type === 'muted' && !this.muted()) {
- promise = resolveMuted();
- } else {
- promise = this.play();
- }
- if (!isPromise(promise)) {
- return;
- }
- return promise.then(() => {
- this.trigger({
- type: 'autoplay-success',
- autoplay: type
- });
- }).catch(() => {
- this.trigger({
- type: 'autoplay-failure',
- autoplay: type
- });
- });
- }
-
- /**
- * Update the internal source caches so that we return the correct source from
- * `src()`, `currentSource()`, and `currentSources()`.
- *
- * > Note: `currentSources` will not be updated if the source that is passed in exists
- * in the current `currentSources` cache.
- *
- *
- * @param {Tech~SourceObject} srcObj
- * A string or object source to update our caches to.
- */
- updateSourceCaches_(srcObj = '') {
- let src = srcObj;
- let type = '';
- if (typeof src !== 'string') {
- src = srcObj.src;
- type = srcObj.type;
- }
-
- // make sure all the caches are set to default values
- // to prevent null checking
- this.cache_.source = this.cache_.source || {};
- this.cache_.sources = this.cache_.sources || [];
-
- // try to get the type of the src that was passed in
- if (src && !type) {
- type = findMimetype(this, src);
- }
-
- // update `currentSource` cache always
- this.cache_.source = merge({}, srcObj, {
- src,
- type
- });
- const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
- const sourceElSources = [];
- const sourceEls = this.$$('source');
- const matchingSourceEls = [];
- for (let i = 0; i < sourceEls.length; i++) {
- const sourceObj = getAttributes(sourceEls[i]);
- sourceElSources.push(sourceObj);
- if (sourceObj.src && sourceObj.src === src) {
- matchingSourceEls.push(sourceObj.src);
- }
- }
-
- // if we have matching source els but not matching sources
- // the current source cache is not up to date
- if (matchingSourceEls.length && !matchingSources.length) {
- this.cache_.sources = sourceElSources;
- // if we don't have matching source or source els set the
- // sources cache to the `currentSource` cache
- } else if (!matchingSources.length) {
- this.cache_.sources = [this.cache_.source];
- }
-
- // update the tech `src` cache
- this.cache_.src = src;
- }
-
- /**
- * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
- * causing the media element to reload.
- *
- * It will fire for the initial source and each subsequent source.
- * This event is a custom event from Video.js and is triggered by the {@link Tech}.
- *
- * The event object for this event contains a `src` property that will contain the source
- * that was available when the event was triggered. This is generally only necessary if Video.js
- * is switching techs while the source was being changed.
- *
- * It is also fired when `load` is called on the player (or media element)
- * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
- * says that the resource selection algorithm needs to be aborted and restarted.
- * In this case, it is very likely that the `src` property will be set to the
- * empty string `""` to indicate we do not know what the source will be but
- * that it is changing.
- *
- * *This event is currently still experimental and may change in minor releases.*
- * __To use this, pass `enableSourceset` option to the player.__
- *
- * @event Player#sourceset
- * @type {Event}
- * @prop {string} src
- * The source url available when the `sourceset` was triggered.
- * It will be an empty string if we cannot know what the source is
- * but know that the source will change.
- */
- /**
- * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
- *
- * @fires Player#sourceset
- * @listens Tech#sourceset
- * @private
- */
- handleTechSourceset_(event) {
- // only update the source cache when the source
- // was not updated using the player api
- if (!this.changingSrc_) {
- let updateSourceCaches = src => this.updateSourceCaches_(src);
- const playerSrc = this.currentSource().src;
- const eventSrc = event.src;
-
- // if we have a playerSrc that is not a blob, and a tech src that is a blob
- if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
- // if both the tech source and the player source were updated we assume
- // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
- if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
- updateSourceCaches = () => {};
- }
- }
-
- // update the source to the initial source right away
- // in some cases this will be empty string
- updateSourceCaches(eventSrc);
-
- // if the `sourceset` `src` was an empty string
- // wait for a `loadstart` to update the cache to `currentSrc`.
- // If a sourceset happens before a `loadstart`, we reset the state
- if (!event.src) {
- this.tech_.any(['sourceset', 'loadstart'], e => {
- // if a sourceset happens before a `loadstart` there
- // is nothing to do as this `handleTechSourceset_`
- // will be called again and this will be handled there.
- if (e.type === 'sourceset') {
- return;
- }
- const techSrc = this.techGet_('currentSrc');
- this.lastSource_.tech = techSrc;
- this.updateSourceCaches_(techSrc);
- });
- }
- }
- this.lastSource_ = {
- player: this.currentSource().src,
- tech: event.src
- };
- this.trigger({
- src: event.src,
- type: 'sourceset'
- });
- }
-
- /**
- * Add/remove the vjs-has-started class
- *
- *
- * @param {boolean} request
- * - true: adds the class
- * - false: remove the class
- *
- * @return {boolean}
- * the boolean value of hasStarted_
- */
- hasStarted(request) {
- if (request === undefined) {
- // act as getter, if we have no request to change
- return this.hasStarted_;
- }
- if (request === this.hasStarted_) {
- return;
- }
- this.hasStarted_ = request;
- if (this.hasStarted_) {
- this.addClass('vjs-has-started');
- } else {
- this.removeClass('vjs-has-started');
- }
- }
-
- /**
- * Fired whenever the media begins or resumes playback
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
- * @fires Player#play
- * @listens Tech#play
- * @private
- */
- handleTechPlay_() {
- this.removeClass('vjs-ended', 'vjs-paused');
- this.addClass('vjs-playing');
-
- // hide the poster when the user hits play
- this.hasStarted(true);
- /**
- * Triggered whenever an {@link Tech#play} event happens. Indicates that
- * playback has started or resumed.
- *
- * @event Player#play
- * @type {Event}
- */
- this.trigger('play');
- }
-
- /**
- * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
- *
- * If there were any events queued while the playback rate was zero, fire
- * those events now.
- *
- * @private
- * @method Player#handleTechRateChange_
- * @fires Player#ratechange
- * @listens Tech#ratechange
- */
- handleTechRateChange_() {
- if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
- this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
- this.queuedCallbacks_ = [];
- }
- this.cache_.lastPlaybackRate = this.tech_.playbackRate();
- /**
- * Fires when the playing speed of the audio/video is changed
- *
- * @event Player#ratechange
- * @type {event}
- */
- this.trigger('ratechange');
- }
-
- /**
- * Retrigger the `waiting` event that was triggered by the {@link Tech}.
- *
- * @fires Player#waiting
- * @listens Tech#waiting
- * @private
- */
- handleTechWaiting_() {
- this.addClass('vjs-waiting');
- /**
- * A readyState change on the DOM element has caused playback to stop.
- *
- * @event Player#waiting
- * @type {Event}
- */
- this.trigger('waiting');
-
- // Browsers may emit a timeupdate event after a waiting event. In order to prevent
- // premature removal of the waiting class, wait for the time to change.
- const timeWhenWaiting = this.currentTime();
- const timeUpdateListener = () => {
- if (timeWhenWaiting !== this.currentTime()) {
- this.removeClass('vjs-waiting');
- this.off('timeupdate', timeUpdateListener);
- }
- };
- this.on('timeupdate', timeUpdateListener);
- }
-
- /**
- * Retrigger the `canplay` event that was triggered by the {@link Tech}.
- * > Note: This is not consistent between browsers. See #1351
- *
- * @fires Player#canplay
- * @listens Tech#canplay
- * @private
- */
- handleTechCanPlay_() {
- this.removeClass('vjs-waiting');
- /**
- * The media has a readyState of HAVE_FUTURE_DATA or greater.
- *
- * @event Player#canplay
- * @type {Event}
- */
- this.trigger('canplay');
- }
-
- /**
- * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
- *
- * @fires Player#canplaythrough
- * @listens Tech#canplaythrough
- * @private
- */
- handleTechCanPlayThrough_() {
- this.removeClass('vjs-waiting');
- /**
- * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
- * entire media file can be played without buffering.
- *
- * @event Player#canplaythrough
- * @type {Event}
- */
- this.trigger('canplaythrough');
- }
-
- /**
- * Retrigger the `playing` event that was triggered by the {@link Tech}.
- *
- * @fires Player#playing
- * @listens Tech#playing
- * @private
- */
- handleTechPlaying_() {
- this.removeClass('vjs-waiting');
- /**
- * The media is no longer blocked from playback, and has started playing.
- *
- * @event Player#playing
- * @type {Event}
- */
- this.trigger('playing');
- }
-
- /**
- * Retrigger the `seeking` event that was triggered by the {@link Tech}.
- *
- * @fires Player#seeking
- * @listens Tech#seeking
- * @private
- */
- handleTechSeeking_() {
- this.addClass('vjs-seeking');
- /**
- * Fired whenever the player is jumping to a new time
- *
- * @event Player#seeking
- * @type {Event}
- */
- this.trigger('seeking');
- }
-
- /**
- * Retrigger the `seeked` event that was triggered by the {@link Tech}.
- *
- * @fires Player#seeked
- * @listens Tech#seeked
- * @private
- */
- handleTechSeeked_() {
- this.removeClass('vjs-seeking', 'vjs-ended');
- /**
- * Fired when the player has finished jumping to a new time
- *
- * @event Player#seeked
- * @type {Event}
- */
- this.trigger('seeked');
- }
-
- /**
- * Retrigger the `pause` event that was triggered by the {@link Tech}.
- *
- * @fires Player#pause
- * @listens Tech#pause
- * @private
- */
- handleTechPause_() {
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- /**
- * Fired whenever the media has been paused
- *
- * @event Player#pause
- * @type {Event}
- */
- this.trigger('pause');
- }
-
- /**
- * Retrigger the `ended` event that was triggered by the {@link Tech}.
- *
- * @fires Player#ended
- * @listens Tech#ended
- * @private
- */
- handleTechEnded_() {
- this.addClass('vjs-ended');
- this.removeClass('vjs-waiting');
- if (this.options_.loop) {
- this.currentTime(0);
- this.play();
- } else if (!this.paused()) {
- this.pause();
- }
-
- /**
- * Fired when the end of the media resource is reached (currentTime == duration)
- *
- * @event Player#ended
- * @type {Event}
- */
- this.trigger('ended');
- }
-
- /**
- * Fired when the duration of the media resource is first known or changed
- *
- * @listens Tech#durationchange
- * @private
- */
- handleTechDurationChange_() {
- this.duration(this.techGet_('duration'));
- }
-
- /**
- * Handle a click on the media element to play/pause
- *
- * @param {Event} event
- * the event that caused this function to trigger
- *
- * @listens Tech#click
- * @private
- */
- handleTechClick_(event) {
- // When controls are disabled a click should not toggle playback because
- // the click is considered a control
- if (!this.controls_) {
- return;
- }
- if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
- if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
- this.options_.userActions.click.call(this, event);
- } else if (this.paused()) {
- silencePromise(this.play());
- } else {
- this.pause();
- }
- }
- }
-
- /**
- * Handle a double-click on the media element to enter/exit fullscreen
- *
- * @param {Event} event
- * the event that caused this function to trigger
- *
- * @listens Tech#dblclick
- * @private
- */
- handleTechDoubleClick_(event) {
- if (!this.controls_) {
- return;
- }
-
- // we do not want to toggle fullscreen state
- // when double-clicking inside a control bar or a modal
- const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
- if (!inAllowedEls) {
- /*
- * options.userActions.doubleClick
- *
- * If `undefined` or `true`, double-click toggles fullscreen if controls are present
- * Set to `false` to disable double-click handling
- * Set to a function to substitute an external double-click handler
- */
- if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
- if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
- this.options_.userActions.doubleClick.call(this, event);
- } else if (this.isFullscreen()) {
- this.exitFullscreen();
- } else {
- this.requestFullscreen();
- }
- }
- }
- }
-
- /**
- * Handle a tap on the media element. It will toggle the user
- * activity state, which hides and shows the controls.
- *
- * @listens Tech#tap
- * @private
- */
- handleTechTap_() {
- this.userActive(!this.userActive());
- }
-
- /**
- * Handle touch to start
- *
- * @listens Tech#touchstart
- * @private
- */
- handleTechTouchStart_() {
- this.userWasActive = this.userActive();
- }
-
- /**
- * Handle touch to move
- *
- * @listens Tech#touchmove
- * @private
- */
- handleTechTouchMove_() {
- if (this.userWasActive) {
- this.reportUserActivity();
- }
- }
-
- /**
- * Handle touch to end
- *
- * @param {Event} event
- * the touchend event that triggered
- * this function
- *
- * @listens Tech#touchend
- * @private
- */
- handleTechTouchEnd_(event) {
- // Stop the mouse events from also happening
- if (event.cancelable) {
- event.preventDefault();
- }
- }
-
- /**
- * @private
- */
- toggleFullscreenClass_() {
- if (this.isFullscreen()) {
- this.addClass('vjs-fullscreen');
- } else {
- this.removeClass('vjs-fullscreen');
- }
- }
-
- /**
- * when the document fschange event triggers it calls this
- */
- documentFullscreenChange_(e) {
- const targetPlayer = e.target.player;
-
- // if another player was fullscreen
- // do a null check for targetPlayer because older firefox's would put document as e.target
- if (targetPlayer && targetPlayer !== this) {
- return;
- }
- const el = this.el();
- let isFs = document[this.fsApi_.fullscreenElement] === el;
- if (!isFs && el.matches) {
- isFs = el.matches(':' + this.fsApi_.fullscreen);
- }
- this.isFullscreen(isFs);
- }
-
- /**
- * Handle Tech Fullscreen Change
- *
- * @param {Event} event
- * the fullscreenchange event that triggered this function
- *
- * @param {Object} data
- * the data that was sent with the event
- *
- * @private
- * @listens Tech#fullscreenchange
- * @fires Player#fullscreenchange
- */
- handleTechFullscreenChange_(event, data) {
- if (data) {
- if (data.nativeIOSFullscreen) {
- this.addClass('vjs-ios-native-fs');
- this.tech_.one('webkitendfullscreen', () => {
- this.removeClass('vjs-ios-native-fs');
- });
- }
- this.isFullscreen(data.isFullscreen);
- }
- }
- handleTechFullscreenError_(event, err) {
- this.trigger('fullscreenerror', err);
- }
-
- /**
- * @private
- */
- togglePictureInPictureClass_() {
- if (this.isInPictureInPicture()) {
- this.addClass('vjs-picture-in-picture');
- } else {
- this.removeClass('vjs-picture-in-picture');
- }
- }
-
- /**
- * Handle Tech Enter Picture-in-Picture.
- *
- * @param {Event} event
- * the enterpictureinpicture event that triggered this function
- *
- * @private
- * @listens Tech#enterpictureinpicture
- */
- handleTechEnterPictureInPicture_(event) {
- this.isInPictureInPicture(true);
- }
-
- /**
- * Handle Tech Leave Picture-in-Picture.
- *
- * @param {Event} event
- * the leavepictureinpicture event that triggered this function
- *
- * @private
- * @listens Tech#leavepictureinpicture
- */
- handleTechLeavePictureInPicture_(event) {
- this.isInPictureInPicture(false);
- }
-
- /**
- * Fires when an error occurred during the loading of an audio/video.
- *
- * @private
- * @listens Tech#error
- */
- handleTechError_() {
- const error = this.tech_.error();
- if (error) {
- this.error(error);
- }
- }
-
- /**
- * Retrigger the `textdata` event that was triggered by the {@link Tech}.
- *
- * @fires Player#textdata
- * @listens Tech#textdata
- * @private
- */
- handleTechTextData_() {
- let data = null;
- if (arguments.length > 1) {
- data = arguments[1];
- }
-
- /**
- * Fires when we get a textdata event from tech
- *
- * @event Player#textdata
- * @type {Event}
- */
- this.trigger('textdata', data);
- }
-
- /**
- * Get object for cached values.
- *
- * @return {Object}
- * get the current object cache
- */
- getCache() {
- return this.cache_;
- }
-
- /**
- * Resets the internal cache object.
- *
- * Using this function outside the player constructor or reset method may
- * have unintended side-effects.
- *
- * @private
- */
- resetCache_() {
- this.cache_ = {
- // Right now, the currentTime is not _really_ cached because it is always
- // retrieved from the tech (see: currentTime). However, for completeness,
- // we set it to zero here to ensure that if we do start actually caching
- // it, we reset it along with everything else.
- currentTime: 0,
- initTime: 0,
- inactivityTimeout: this.options_.inactivityTimeout,
- duration: NaN,
- lastVolume: 1,
- lastPlaybackRate: this.defaultPlaybackRate(),
- media: null,
- src: '',
- source: {},
- sources: [],
- playbackRates: [],
- volume: 1
- };
- }
-
- /**
- * Pass values to the playback tech
- *
- * @param {string} [method]
- * the method to call
- *
- * @param {Object} [arg]
- * the argument to pass
- *
- * @private
- */
- techCall_(method, arg) {
- // If it's not ready yet, call method when it is
-
- this.ready(function () {
- if (method in allowedSetters) {
- return set(this.middleware_, this.tech_, method, arg);
- } else if (method in allowedMediators) {
- return mediate(this.middleware_, this.tech_, method, arg);
- }
- try {
- if (this.tech_) {
- this.tech_[method](arg);
- }
- } catch (e) {
- log(e);
- throw e;
- }
- }, true);
- }
-
- /**
- * Mediate attempt to call playback tech method
- * and return the value of the method called.
- *
- * @param {string} method
- * Tech method
- *
- * @return {*}
- * Value returned by the tech method called, undefined if tech
- * is not ready or tech method is not present
- *
- * @private
- */
- techGet_(method) {
- if (!this.tech_ || !this.tech_.isReady_) {
- return;
- }
- if (method in allowedGetters) {
- return get(this.middleware_, this.tech_, method);
- } else if (method in allowedMediators) {
- return mediate(this.middleware_, this.tech_, method);
- }
-
- // Log error when playback tech object is present but method
- // is undefined or unavailable
- try {
- return this.tech_[method]();
- } catch (e) {
- // When building additional tech libs, an expected method may not be defined yet
- if (this.tech_[method] === undefined) {
- log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
- throw e;
- }
-
- // When a method isn't available on the object it throws a TypeError
- if (e.name === 'TypeError') {
- log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
- this.tech_.isReady_ = false;
- throw e;
- }
-
- // If error unknown, just log and throw
- log(e);
- throw e;
- }
- }
-
- /**
- * Attempt to begin playback at the first opportunity.
- *
- * @return {Promise|undefined}
- * Returns a promise if the browser supports Promises (or one
- * was passed in as an option). This promise will be resolved on
- * the return value of play. If this is undefined it will fulfill the
- * promise chain otherwise the promise chain will be fulfilled when
- * the promise from play is fulfilled.
- */
- play() {
- return new Promise(resolve => {
- this.play_(resolve);
- });
- }
-
- /**
- * The actual logic for play, takes a callback that will be resolved on the
- * return value of play. This allows us to resolve to the play promise if there
- * is one on modern browsers.
- *
- * @private
- * @param {Function} [callback]
- * The callback that should be called when the techs play is actually called
- */
- play_(callback = silencePromise) {
- this.playCallbacks_.push(callback);
- const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
- const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
-
- // treat calls to play_ somewhat like the `one` event function
- if (this.waitToPlay_) {
- this.off(['ready', 'loadstart'], this.waitToPlay_);
- this.waitToPlay_ = null;
- }
-
- // if the player/tech is not ready or the src itself is not ready
- // queue up a call to play on `ready` or `loadstart`
- if (!this.isReady_ || !isSrcReady) {
- this.waitToPlay_ = e => {
- this.play_();
- };
- this.one(['ready', 'loadstart'], this.waitToPlay_);
-
- // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
- // in that case, we need to prime the video element by calling load so it'll be ready in time
- if (!isSrcReady && isSafariOrIOS) {
- this.load();
- }
- return;
- }
-
- // If the player/tech is ready and we have a source, we can attempt playback.
- const val = this.techGet_('play');
-
- // For native playback, reset the progress bar if we get a play call from a replay.
- const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
- if (isNativeReplay) {
- this.resetProgressBar_();
- }
- // play was terminated if the returned value is null
- if (val === null) {
- this.runPlayTerminatedQueue_();
- } else {
- this.runPlayCallbacks_(val);
- }
- }
-
- /**
- * These functions will be run when if play is terminated. If play
- * runPlayCallbacks_ is run these function will not be run. This allows us
- * to differentiate between a terminated play and an actual call to play.
- */
- runPlayTerminatedQueue_() {
- const queue = this.playTerminatedQueue_.slice(0);
- this.playTerminatedQueue_ = [];
- queue.forEach(function (q) {
- q();
- });
- }
-
- /**
- * When a callback to play is delayed we have to run these
- * callbacks when play is actually called on the tech. This function
- * runs the callbacks that were delayed and accepts the return value
- * from the tech.
- *
- * @param {undefined|Promise} val
- * The return value from the tech.
- */
- runPlayCallbacks_(val) {
- const callbacks = this.playCallbacks_.slice(0);
- this.playCallbacks_ = [];
- // clear play terminatedQueue since we finished a real play
- this.playTerminatedQueue_ = [];
- callbacks.forEach(function (cb) {
- cb(val);
- });
- }
-
- /**
- * Pause the video playback
- */
- pause() {
- this.techCall_('pause');
- }
-
- /**
- * Check if the player is paused or has yet to play
- *
- * @return {boolean}
- * - false: if the media is currently playing
- * - true: if media is not currently playing
- */
- paused() {
- // The initial state of paused should be true (in Safari it's actually false)
- return this.techGet_('paused') === false ? false : true;
- }
-
- /**
- * Get a TimeRange object representing the current ranges of time that the user
- * has played.
- *
- * @return { import('./utils/time').TimeRange }
- * A time range object that represents all the increments of time that have
- * been played.
- */
- played() {
- return this.techGet_('played') || createTimeRanges(0, 0);
- }
-
- /**
- * Sets or returns whether or not the user is "scrubbing". Scrubbing is
- * when the user has clicked the progress bar handle and is
- * dragging it along the progress bar.
- *
- * @param {boolean} [isScrubbing]
- * whether the user is or is not scrubbing
- *
- * @return {boolean|undefined}
- * - The value of scrubbing when getting
- * - Nothing when setting
- */
- scrubbing(isScrubbing) {
- if (typeof isScrubbing === 'undefined') {
- return this.scrubbing_;
- }
- this.scrubbing_ = !!isScrubbing;
- this.techCall_('setScrubbing', this.scrubbing_);
- if (isScrubbing) {
- this.addClass('vjs-scrubbing');
- } else {
- this.removeClass('vjs-scrubbing');
- }
- }
-
- /**
- * Get or set the current time (in seconds)
- *
- * @param {number|string} [seconds]
- * The time to seek to in seconds
- *
- * @return {number|undefined}
- * - the current time in seconds when getting
- * - Nothing when setting
- */
- currentTime(seconds) {
- if (seconds === undefined) {
- // cache last currentTime and return. default to 0 seconds
- //
- // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
- // currentTime when scrubbing, but may not provide much performance benefit after all.
- // Should be tested. Also something has to read the actual current time or the cache will
- // never get updated.
- this.cache_.currentTime = this.techGet_('currentTime') || 0;
- return this.cache_.currentTime;
- }
- if (seconds < 0) {
- seconds = 0;
- }
- if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
- this.cache_.initTime = seconds;
- this.off('canplay', this.boundApplyInitTime_);
- this.one('canplay', this.boundApplyInitTime_);
- return;
- }
- this.techCall_('setCurrentTime', seconds);
- this.cache_.initTime = 0;
- if (isFinite(seconds)) {
- this.cache_.currentTime = Number(seconds);
- }
- }
-
- /**
- * Apply the value of initTime stored in cache as currentTime.
- *
- * @private
- */
- applyInitTime_() {
- this.currentTime(this.cache_.initTime);
- }
-
- /**
- * Normally gets the length in time of the video in seconds;
- * in all but the rarest use cases an argument will NOT be passed to the method
- *
- * > **NOTE**: The video must have started loading before the duration can be
- * known, and depending on preload behaviour may not be known until the video starts
- * playing.
- *
- * @fires Player#durationchange
- *
- * @param {number} [seconds]
- * The duration of the video to set in seconds
- *
- * @return {number|undefined}
- * - The duration of the video in seconds when getting
- * - Nothing when setting
- */
- duration(seconds) {
- if (seconds === undefined) {
- // return NaN if the duration is not known
- return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
- }
- seconds = parseFloat(seconds);
-
- // Standardize on Infinity for signaling video is live
- if (seconds < 0) {
- seconds = Infinity;
- }
- if (seconds !== this.cache_.duration) {
- // Cache the last set value for optimized scrubbing
- this.cache_.duration = seconds;
- if (seconds === Infinity) {
- this.addClass('vjs-live');
- } else {
- this.removeClass('vjs-live');
- }
- if (!isNaN(seconds)) {
- // Do not fire durationchange unless the duration value is known.
- // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
-
- /**
- * @event Player#durationchange
- * @type {Event}
- */
- this.trigger('durationchange');
- }
- }
- }
-
- /**
- * Calculates how much time is left in the video. Not part
- * of the native video API.
- *
- * @return {number}
- * The time remaining in seconds
- */
- remainingTime() {
- return this.duration() - this.currentTime();
- }
-
- /**
- * A remaining time function that is intended to be used when
- * the time is to be displayed directly to the user.
- *
- * @return {number}
- * The rounded time remaining in seconds
- */
- remainingTimeDisplay() {
- return Math.floor(this.duration()) - Math.floor(this.currentTime());
- }
-
- //
- // Kind of like an array of portions of the video that have been downloaded.
-
- /**
- * Get a TimeRange object with an array of the times of the video
- * that have been downloaded. If you just want the percent of the
- * video that's been downloaded, use bufferedPercent.
- *
- * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
- *
- * @return { import('./utils/time').TimeRange }
- * A mock {@link TimeRanges} object (following HTML spec)
- */
- buffered() {
- let buffered = this.techGet_('buffered');
- if (!buffered || !buffered.length) {
- buffered = createTimeRanges(0, 0);
- }
- return buffered;
- }
-
- /**
- * Get the TimeRanges of the media that are currently available
- * for seeking to.
- *
- * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable}
- *
- * @return { import('./utils/time').TimeRange }
- * A mock {@link TimeRanges} object (following HTML spec)
- */
- seekable() {
- let seekable = this.techGet_('seekable');
- if (!seekable || !seekable.length) {
- seekable = createTimeRanges(0, 0);
- }
- return seekable;
- }
-
- /**
- * Returns whether the player is in the "seeking" state.
- *
- * @return {boolean} True if the player is in the seeking state, false if not.
- */
- seeking() {
- return this.techGet_('seeking');
- }
-
- /**
- * Returns whether the player is in the "ended" state.
- *
- * @return {boolean} True if the player is in the ended state, false if not.
- */
- ended() {
- return this.techGet_('ended');
- }
-
- /**
- * Returns the current state of network activity for the element, from
- * the codes in the list below.
- * - NETWORK_EMPTY (numeric value 0)
- * The element has not yet been initialised. All attributes are in
- * their initial states.
- * - NETWORK_IDLE (numeric value 1)
- * The element's resource selection algorithm is active and has
- * selected a resource, but it is not actually using the network at
- * this time.
- * - NETWORK_LOADING (numeric value 2)
- * The user agent is actively trying to download data.
- * - NETWORK_NO_SOURCE (numeric value 3)
- * The element's resource selection algorithm is active, but it has
- * not yet found a resource to use.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
- * @return {number} the current network activity state
- */
- networkState() {
- return this.techGet_('networkState');
- }
-
- /**
- * Returns a value that expresses the current state of the element
- * with respect to rendering the current playback position, from the
- * codes in the list below.
- * - HAVE_NOTHING (numeric value 0)
- * No information regarding the media resource is available.
- * - HAVE_METADATA (numeric value 1)
- * Enough of the resource has been obtained that the duration of the
- * resource is available.
- * - HAVE_CURRENT_DATA (numeric value 2)
- * Data for the immediate current playback position is available.
- * - HAVE_FUTURE_DATA (numeric value 3)
- * Data for the immediate current playback position is available, as
- * well as enough data for the user agent to advance the current
- * playback position in the direction of playback.
- * - HAVE_ENOUGH_DATA (numeric value 4)
- * The user agent estimates that enough data is available for
- * playback to proceed uninterrupted.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
- * @return {number} the current playback rendering state
- */
- readyState() {
- return this.techGet_('readyState');
- }
-
- /**
- * Get the percent (as a decimal) of the video that's been downloaded.
- * This method is not a part of the native HTML video API.
- *
- * @return {number}
- * A decimal between 0 and 1 representing the percent
- * that is buffered 0 being 0% and 1 being 100%
- */
- bufferedPercent() {
- return bufferedPercent(this.buffered(), this.duration());
- }
-
- /**
- * Get the ending time of the last buffered time range
- * This is used in the progress bar to encapsulate all time ranges.
- *
- * @return {number}
- * The end of the last buffered time range
- */
- bufferedEnd() {
- const buffered = this.buffered();
- const duration = this.duration();
- let end = buffered.end(buffered.length - 1);
- if (end > duration) {
- end = duration;
- }
- return end;
- }
-
- /**
- * Get or set the current volume of the media
- *
- * @param {number} [percentAsDecimal]
- * The new volume as a decimal percent:
- * - 0 is muted/0%/off
- * - 1.0 is 100%/full
- * - 0.5 is half volume or 50%
- *
- * @return {number|undefined}
- * The current volume as a percent when getting
- */
- volume(percentAsDecimal) {
- let vol;
- if (percentAsDecimal !== undefined) {
- // Force value to between 0 and 1
- vol = Math.max(0, Math.min(1, percentAsDecimal));
- this.cache_.volume = vol;
- this.techCall_('setVolume', vol);
- if (vol > 0) {
- this.lastVolume_(vol);
- }
- return;
- }
-
- // Default to 1 when returning current volume.
- vol = parseFloat(this.techGet_('volume'));
- return isNaN(vol) ? 1 : vol;
- }
-
- /**
- * Get the current muted state, or turn mute on or off
- *
- * @param {boolean} [muted]
- * - true to mute
- * - false to unmute
- *
- * @return {boolean|undefined}
- * - true if mute is on and getting
- * - false if mute is off and getting
- * - nothing if setting
- */
- muted(muted) {
- if (muted !== undefined) {
- this.techCall_('setMuted', muted);
- return;
- }
- return this.techGet_('muted') || false;
- }
-
- /**
- * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
- * indicates the state of muted on initial playback.
- *
- * ```js
- * var myPlayer = videojs('some-player-id');
- *
- * myPlayer.src("http://www.example.com/path/to/video.mp4");
- *
- * // get, should be false
- * console.log(myPlayer.defaultMuted());
- * // set to true
- * myPlayer.defaultMuted(true);
- * // get should be true
- * console.log(myPlayer.defaultMuted());
- * ```
- *
- * @param {boolean} [defaultMuted]
- * - true to mute
- * - false to unmute
- *
- * @return {boolean|undefined}
- * - true if defaultMuted is on and getting
- * - false if defaultMuted is off and getting
- * - Nothing when setting
- */
- defaultMuted(defaultMuted) {
- if (defaultMuted !== undefined) {
- this.techCall_('setDefaultMuted', defaultMuted);
- }
- return this.techGet_('defaultMuted') || false;
- }
-
- /**
- * Get the last volume, or set it
- *
- * @param {number} [percentAsDecimal]
- * The new last volume as a decimal percent:
- * - 0 is muted/0%/off
- * - 1.0 is 100%/full
- * - 0.5 is half volume or 50%
- *
- * @return {number|undefined}
- * - The current value of lastVolume as a percent when getting
- * - Nothing when setting
- *
- * @private
- */
- lastVolume_(percentAsDecimal) {
- if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
- this.cache_.lastVolume = percentAsDecimal;
- return;
- }
- return this.cache_.lastVolume;
- }
-
- /**
- * Check if current tech can support native fullscreen
- * (e.g. with built in controls like iOS)
- *
- * @return {boolean}
- * if native fullscreen is supported
- */
- supportsFullScreen() {
- return this.techGet_('supportsFullScreen') || false;
- }
-
- /**
- * Check if the player is in fullscreen mode or tell the player that it
- * is or is not in fullscreen mode.
- *
- * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
- * property and instead document.fullscreenElement is used. But isFullscreen is
- * still a valuable property for internal player workings.
- *
- * @param {boolean} [isFS]
- * Set the players current fullscreen state
- *
- * @return {boolean|undefined}
- * - true if fullscreen is on and getting
- * - false if fullscreen is off and getting
- * - Nothing when setting
- */
- isFullscreen(isFS) {
- if (isFS !== undefined) {
- const oldValue = this.isFullscreen_;
- this.isFullscreen_ = Boolean(isFS);
-
- // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
- // this is the only place where we trigger fullscreenchange events for older browsers
- // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
- if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
- /**
- * @event Player#fullscreenchange
- * @type {Event}
- */
- this.trigger('fullscreenchange');
- }
- this.toggleFullscreenClass_();
- return;
- }
- return this.isFullscreen_;
- }
-
- /**
- * Increase the size of the video to full screen
- * In some browsers, full screen is not supported natively, so it enters
- * "full window mode", where the video fills the browser window.
- * In browsers and devices that support native full screen, sometimes the
- * browser's default controls will be shown, and not the Video.js custom skin.
- * This includes most mobile devices (iOS, Android) and older versions of
- * Safari.
- *
- * @param {Object} [fullscreenOptions]
- * Override the player fullscreen options
- *
- * @fires Player#fullscreenchange
- */
- requestFullscreen(fullscreenOptions) {
- if (this.isInPictureInPicture()) {
- this.exitPictureInPicture();
- }
- const self = this;
- return new Promise((resolve, reject) => {
- function offHandler() {
- self.off('fullscreenerror', errorHandler);
- self.off('fullscreenchange', changeHandler);
- }
- function changeHandler() {
- offHandler();
- resolve();
- }
- function errorHandler(e, err) {
- offHandler();
- reject(err);
- }
- self.one('fullscreenchange', changeHandler);
- self.one('fullscreenerror', errorHandler);
- const promise = self.requestFullscreenHelper_(fullscreenOptions);
- if (promise) {
- promise.then(offHandler, offHandler);
- promise.then(resolve, reject);
- }
- });
- }
- requestFullscreenHelper_(fullscreenOptions) {
- let fsOptions;
-
- // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
- // Use defaults or player configured option unless passed directly to this method.
- if (!this.fsApi_.prefixed) {
- fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
- if (fullscreenOptions !== undefined) {
- fsOptions = fullscreenOptions;
- }
- }
-
- // This method works as follows:
- // 1. if a fullscreen api is available, use it
- // 1. call requestFullscreen with potential options
- // 2. if we got a promise from above, use it to update isFullscreen()
- // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
- // This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
- // 3. otherwise, use "fullWindow" mode
- if (this.fsApi_.requestFullscreen) {
- const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
-
- // Even on browsers with promise support this may not return a promise
- if (promise) {
- promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
- }
- return promise;
- } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
- // we can't take the video.js controls fullscreen but we can go fullscreen
- // with native controls
- this.techCall_('enterFullScreen');
- } else {
- // fullscreen isn't supported so we'll just stretch the video element to
- // fill the viewport
- this.enterFullWindow();
- }
- }
-
- /**
- * Return the video to its normal size after having been in full screen mode
- *
- * @fires Player#fullscreenchange
- */
- exitFullscreen() {
- const self = this;
- return new Promise((resolve, reject) => {
- function offHandler() {
- self.off('fullscreenerror', errorHandler);
- self.off('fullscreenchange', changeHandler);
- }
- function changeHandler() {
- offHandler();
- resolve();
- }
- function errorHandler(e, err) {
- offHandler();
- reject(err);
- }
- self.one('fullscreenchange', changeHandler);
- self.one('fullscreenerror', errorHandler);
- const promise = self.exitFullscreenHelper_();
- if (promise) {
- promise.then(offHandler, offHandler);
- // map the promise to our resolve/reject methods
- promise.then(resolve, reject);
- }
- });
- }
- exitFullscreenHelper_() {
- if (this.fsApi_.requestFullscreen) {
- const promise = document[this.fsApi_.exitFullscreen]();
-
- // Even on browsers with promise support this may not return a promise
- if (promise) {
- // we're splitting the promise here, so, we want to catch the
- // potential error so that this chain doesn't have unhandled errors
- silencePromise(promise.then(() => this.isFullscreen(false)));
- }
- return promise;
- } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
- this.techCall_('exitFullScreen');
- } else {
- this.exitFullWindow();
- }
- }
-
- /**
- * When fullscreen isn't supported we can stretch the
- * video container to as wide as the browser will let us.
- *
- * @fires Player#enterFullWindow
- */
- enterFullWindow() {
- this.isFullscreen(true);
- this.isFullWindow = true;
-
- // Storing original doc overflow value to return to when fullscreen is off
- this.docOrigOverflow = document.documentElement.style.overflow;
-
- // Add listener for esc key to exit fullscreen
- on(document, 'keydown', this.boundFullWindowOnEscKey_);
-
- // Hide any scroll bars
- document.documentElement.style.overflow = 'hidden';
-
- // Apply fullscreen styles
- addClass(document.body, 'vjs-full-window');
-
- /**
- * @event Player#enterFullWindow
- * @type {Event}
- */
- this.trigger('enterFullWindow');
- }
-
- /**
- * Check for call to either exit full window or
- * full screen on ESC key
- *
- * @param {string} event
- * Event to check for key press
- */
- fullWindowOnEscKey(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- if (this.isFullscreen() === true) {
- if (!this.isFullWindow) {
- this.exitFullscreen();
- } else {
- this.exitFullWindow();
- }
- }
- }
- }
-
- /**
- * Exit full window
- *
- * @fires Player#exitFullWindow
- */
- exitFullWindow() {
- this.isFullscreen(false);
- this.isFullWindow = false;
- off(document, 'keydown', this.boundFullWindowOnEscKey_);
-
- // Unhide scroll bars.
- document.documentElement.style.overflow = this.docOrigOverflow;
-
- // Remove fullscreen styles
- removeClass(document.body, 'vjs-full-window');
-
- // Resize the box, controller, and poster to original sizes
- // this.positionAll();
- /**
- * @event Player#exitFullWindow
- * @type {Event}
- */
- this.trigger('exitFullWindow');
- }
-
- /**
- * Get or set disable Picture-in-Picture mode.
- *
- * @param {boolean} [value]
- * - true will disable Picture-in-Picture mode
- * - false will enable Picture-in-Picture mode
- */
- disablePictureInPicture(value) {
- if (value === undefined) {
- return this.techGet_('disablePictureInPicture');
- }
- this.techCall_('setDisablePictureInPicture', value);
- this.options_.disablePictureInPicture = value;
- this.trigger('disablepictureinpicturechanged');
- }
-
- /**
- * Check if the player is in Picture-in-Picture mode or tell the player that it
- * is or is not in Picture-in-Picture mode.
- *
- * @param {boolean} [isPiP]
- * Set the players current Picture-in-Picture state
- *
- * @return {boolean|undefined}
- * - true if Picture-in-Picture is on and getting
- * - false if Picture-in-Picture is off and getting
- * - nothing if setting
- */
- isInPictureInPicture(isPiP) {
- if (isPiP !== undefined) {
- this.isInPictureInPicture_ = !!isPiP;
- this.togglePictureInPictureClass_();
- return;
- }
- return !!this.isInPictureInPicture_;
- }
-
- /**
- * Create a floating video window always on top of other windows so that users may
- * continue consuming media while they interact with other content sites, or
- * applications on their device.
- *
- * This can use document picture-in-picture or element picture in picture
- *
- * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
- * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
- *
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
- * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
- *
- * @fires Player#enterpictureinpicture
- *
- * @return {Promise}
- * A promise with a Picture-in-Picture window.
- */
- requestPictureInPicture() {
- if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) {
- const pipContainer = document.createElement(this.el().tagName);
- pipContainer.classList = this.el().classList;
- pipContainer.classList.add('vjs-pip-container');
- if (this.posterImage) {
- pipContainer.appendChild(this.posterImage.el().cloneNode(true));
- }
- if (this.titleBar) {
- pipContainer.appendChild(this.titleBar.el().cloneNode(true));
- }
- pipContainer.appendChild(createEl('p', {
- className: 'vjs-pip-text'
- }, {}, this.localize('Playing in picture-in-picture')));
- return window.documentPictureInPicture.requestWindow({
- // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
- width: this.videoWidth(),
- height: this.videoHeight()
- }).then(pipWindow => {
- copyStyleSheetsToWindow(pipWindow);
- this.el_.parentNode.insertBefore(pipContainer, this.el_);
- pipWindow.document.body.appendChild(this.el_);
- pipWindow.document.body.classList.add('vjs-pip-window');
- this.player_.isInPictureInPicture(true);
- this.player_.trigger({
- type: 'enterpictureinpicture',
- pipWindow
- });
-
- // Listen for the PiP closing event to move the video back.
- pipWindow.addEventListener('pagehide', event => {
- const pipVideo = event.target.querySelector('.video-js');
- pipContainer.parentNode.replaceChild(pipVideo, pipContainer);
- this.player_.isInPictureInPicture(false);
- this.player_.trigger('leavepictureinpicture');
- });
- return pipWindow;
- });
- }
- if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
- /**
- * This event fires when the player enters picture in picture mode
- *
- * @event Player#enterpictureinpicture
- * @type {Event}
- */
- return this.techGet_('requestPictureInPicture');
- }
- return Promise.reject('No PiP mode is available');
- }
-
- /**
- * Exit Picture-in-Picture mode.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @fires Player#leavepictureinpicture
- *
- * @return {Promise}
- * A promise.
- */
- exitPictureInPicture() {
- if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
- // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler
- window.documentPictureInPicture.window.close();
- return Promise.resolve();
- }
- if ('pictureInPictureEnabled' in document) {
- /**
- * This event fires when the player leaves picture in picture mode
- *
- * @event Player#leavepictureinpicture
- * @type {Event}
- */
- return document.exitPictureInPicture();
- }
- }
-
- /**
- * Called when this Player has focus and a key gets pressed down, or when
- * any Component of this player receives a key press that it doesn't handle.
- * This allows player-wide hotkeys (either as defined below, or optionally
- * by an external function).
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- const {
- userActions
- } = this.options_;
-
- // Bail out if hotkeys are not configured.
- if (!userActions || !userActions.hotkeys) {
- return;
- }
-
- // Function that determines whether or not to exclude an element from
- // hotkeys handling.
- const excludeElement = el => {
- const tagName = el.tagName.toLowerCase();
-
- // The first and easiest test is for `contenteditable` elements.
- if (el.isContentEditable) {
- return true;
- }
-
- // Inputs matching these types will still trigger hotkey handling as
- // they are not text inputs.
- const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
- if (tagName === 'input') {
- return allowedInputTypes.indexOf(el.type) === -1;
- }
-
- // The final test is by tag name. These tags will be excluded entirely.
- const excludedTags = ['textarea'];
- return excludedTags.indexOf(tagName) !== -1;
- };
-
- // Bail out if the user is focused on an interactive form element.
- if (excludeElement(this.el_.ownerDocument.activeElement)) {
- return;
- }
- if (typeof userActions.hotkeys === 'function') {
- userActions.hotkeys.call(this, event);
- } else {
- this.handleHotkeys(event);
- }
- }
-
- /**
- * Called when this Player receives a hotkey keydown event.
- * Supported player-wide hotkeys are:
- *
- * f - toggle fullscreen
- * m - toggle mute
- * k or Space - toggle play/pause
- *
- * @param {Event} event
- * The `keydown` event that caused this function to be called.
- */
- handleHotkeys(event) {
- const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
-
- // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
- const {
- fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
- muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
- playPauseKey = keydownEvent => keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space')
- } = hotkeys;
- if (fullscreenKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const FSToggle = Component.getComponent('FullscreenToggle');
- if (document[this.fsApi_.fullscreenEnabled] !== false) {
- FSToggle.prototype.handleClick.call(this, event);
- }
- } else if (muteKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const MuteToggle = Component.getComponent('MuteToggle');
- MuteToggle.prototype.handleClick.call(this, event);
- } else if (playPauseKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const PlayToggle = Component.getComponent('PlayToggle');
- PlayToggle.prototype.handleClick.call(this, event);
- }
- }
-
- /**
- * Check whether the player can play a given mimetype
- *
- * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
- *
- * @param {string} type
- * The mimetype to check
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- canPlayType(type) {
- let can;
-
- // Loop through each playback technology in the options order
- for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
- const techName = j[i];
- let tech = Tech.getTech(techName);
-
- // Support old behavior of techs being registered as components.
- // Remove once that deprecated behavior is removed.
- if (!tech) {
- tech = Component.getComponent(techName);
- }
-
- // Check if the current tech is defined before continuing
- if (!tech) {
- log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
- continue;
- }
-
- // Check if the browser supports this technology
- if (tech.isSupported()) {
- can = tech.canPlayType(type);
- if (can) {
- return can;
- }
- }
- }
- return '';
- }
-
- /**
- * Select source based on tech-order or source-order
- * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
- * defaults to tech-order selection
- *
- * @param {Array} sources
- * The sources for a media asset
- *
- * @return {Object|boolean}
- * Object of source and tech order or false
- */
- selectSource(sources) {
- // Get only the techs specified in `techOrder` that exist and are supported by the
- // current platform
- const techs = this.options_.techOrder.map(techName => {
- return [techName, Tech.getTech(techName)];
- }).filter(([techName, tech]) => {
- // Check if the current tech is defined before continuing
- if (tech) {
- // Check if the browser supports this technology
- return tech.isSupported();
- }
- log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
- return false;
- });
-
- // Iterate over each `innerArray` element once per `outerArray` element and execute
- // `tester` with both. If `tester` returns a non-falsy value, exit early and return
- // that value.
- const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
- let found;
- outerArray.some(outerChoice => {
- return innerArray.some(innerChoice => {
- found = tester(outerChoice, innerChoice);
- if (found) {
- return true;
- }
- });
- });
- return found;
- };
- let foundSourceAndTech;
- const flip = fn => (a, b) => fn(b, a);
- const finder = ([techName, tech], source) => {
- if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
- return {
- source,
- tech: techName
- };
- }
- };
-
- // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
- // to select from them based on their priority.
- if (this.options_.sourceOrder) {
- // Source-first ordering
- foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
- } else {
- // Tech-first ordering
- foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
- }
- return foundSourceAndTech || false;
- }
-
- /**
- * Executes source setting and getting logic
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
- * A SourceObject, an array of SourceObjects, or a string referencing
- * a URL to a media source. It is _highly recommended_ that an object
- * or array of objects is used here, so that source selection
- * algorithms can take the `type` into account.
- *
- * If not provided, this method acts as a getter.
- * @param {boolean} [isRetry]
- * Indicates whether this is being called internally as a result of a retry
- *
- * @return {string|undefined}
- * If the `source` argument is missing, returns the current source
- * URL. Otherwise, returns nothing/undefined.
- */
- handleSrc_(source, isRetry) {
- // getter usage
- if (typeof source === 'undefined') {
- return this.cache_.src || '';
- }
-
- // Reset retry behavior for new source
- if (this.resetRetryOnError_) {
- this.resetRetryOnError_();
- }
-
- // filter out invalid sources and turn our source into
- // an array of source objects
- const sources = filterSource(source);
-
- // if a source was passed in then it is invalid because
- // it was filtered to a zero length Array. So we have to
- // show an error
- if (!sources.length) {
- this.setTimeout(function () {
- this.error({
- code: 4,
- message: this.options_.notSupportedMessage
- });
- }, 0);
- return;
- }
-
- // initial sources
- this.changingSrc_ = true;
-
- // Only update the cached source list if we are not retrying a new source after error,
- // since in that case we want to include the failed source(s) in the cache
- if (!isRetry) {
- this.cache_.sources = sources;
- }
- this.updateSourceCaches_(sources[0]);
-
- // middlewareSource is the source after it has been changed by middleware
- setSource(this, sources[0], (middlewareSource, mws) => {
- this.middleware_ = mws;
-
- // since sourceSet is async we have to update the cache again after we select a source since
- // the source that is selected could be out of order from the cache update above this callback.
- if (!isRetry) {
- this.cache_.sources = sources;
- }
- this.updateSourceCaches_(middlewareSource);
- const err = this.src_(middlewareSource);
- if (err) {
- if (sources.length > 1) {
- return this.handleSrc_(sources.slice(1));
- }
- this.changingSrc_ = false;
-
- // We need to wrap this in a timeout to give folks a chance to add error event handlers
- this.setTimeout(function () {
- this.error({
- code: 4,
- message: this.options_.notSupportedMessage
- });
- }, 0);
-
- // we could not find an appropriate tech, but let's still notify the delegate that this is it
- // this needs a better comment about why this is needed
- this.triggerReady();
- return;
- }
- setTech(mws, this.tech_);
- });
-
- // Try another available source if this one fails before playback.
- if (sources.length > 1) {
- const retry = () => {
- // Remove the error modal
- this.error(null);
- this.handleSrc_(sources.slice(1), true);
- };
- const stopListeningForErrors = () => {
- this.off('error', retry);
- };
- this.one('error', retry);
- this.one('playing', stopListeningForErrors);
- this.resetRetryOnError_ = () => {
- this.off('error', retry);
- this.off('playing', stopListeningForErrors);
- };
- }
- }
-
- /**
- * Get or set the video source.
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
- * A SourceObject, an array of SourceObjects, or a string referencing
- * a URL to a media source. It is _highly recommended_ that an object
- * or array of objects is used here, so that source selection
- * algorithms can take the `type` into account.
- *
- * If not provided, this method acts as a getter.
- *
- * @return {string|undefined}
- * If the `source` argument is missing, returns the current source
- * URL. Otherwise, returns nothing/undefined.
- */
- src(source) {
- return this.handleSrc_(source, false);
- }
-
- /**
- * Set the source object on the tech, returns a boolean that indicates whether
- * there is a tech that can play the source or not
- *
- * @param {Tech~SourceObject} source
- * The source object to set on the Tech
- *
- * @return {boolean}
- * - True if there is no Tech to playback this source
- * - False otherwise
- *
- * @private
- */
- src_(source) {
- const sourceTech = this.selectSource([source]);
- if (!sourceTech) {
- return true;
- }
- if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
- this.changingSrc_ = true;
- // load this technology with the chosen source
- this.loadTech_(sourceTech.tech, sourceTech.source);
- this.tech_.ready(() => {
- this.changingSrc_ = false;
- });
- return false;
- }
-
- // wait until the tech is ready to set the source
- // and set it synchronously if possible (#2326)
- this.ready(function () {
- // The setSource tech method was added with source handlers
- // so older techs won't support it
- // We need to check the direct prototype for the case where subclasses
- // of the tech do not support source handlers
- if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
- this.techCall_('setSource', source);
- } else {
- this.techCall_('src', source.src);
- }
- this.changingSrc_ = false;
- }, true);
- return false;
- }
-
- /**
- * Begin loading the src data.
- */
- load() {
- // Workaround to use the load method with the VHS.
- // Does not cover the case when the load method is called directly from the mediaElement.
- if (this.tech_ && this.tech_.vhs) {
- this.src(this.currentSource());
- return;
- }
- this.techCall_('load');
- }
-
- /**
- * Reset the player. Loads the first tech in the techOrder,
- * removes all the text tracks in the existing `tech`,
- * and calls `reset` on the `tech`.
- */
- reset() {
- if (this.paused()) {
- this.doReset_();
- } else {
- const playPromise = this.play();
- silencePromise(playPromise.then(() => this.doReset_()));
- }
- }
- doReset_() {
- if (this.tech_) {
- this.tech_.clearTracks('text');
- }
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- this.resetCache_();
- this.poster('');
- this.loadTech_(this.options_.techOrder[0], null);
- this.techCall_('reset');
- this.resetControlBarUI_();
- this.error(null);
- if (this.titleBar) {
- this.titleBar.update({
- title: undefined,
- description: undefined
- });
- }
- if (isEvented(this)) {
- this.trigger('playerreset');
- }
- }
-
- /**
- * Reset Control Bar's UI by calling sub-methods that reset
- * all of Control Bar's components
- */
- resetControlBarUI_() {
- this.resetProgressBar_();
- this.resetPlaybackRate_();
- this.resetVolumeBar_();
- }
-
- /**
- * Reset tech's progress so progress bar is reset in the UI
- */
- resetProgressBar_() {
- this.currentTime(0);
- const {
- currentTimeDisplay,
- durationDisplay,
- progressControl,
- remainingTimeDisplay
- } = this.controlBar || {};
- const {
- seekBar
- } = progressControl || {};
- if (currentTimeDisplay) {
- currentTimeDisplay.updateContent();
- }
- if (durationDisplay) {
- durationDisplay.updateContent();
- }
- if (remainingTimeDisplay) {
- remainingTimeDisplay.updateContent();
- }
- if (seekBar) {
- seekBar.update();
- if (seekBar.loadProgressBar) {
- seekBar.loadProgressBar.update();
- }
- }
- }
-
- /**
- * Reset Playback ratio
- */
- resetPlaybackRate_() {
- this.playbackRate(this.defaultPlaybackRate());
- this.handleTechRateChange_();
- }
-
- /**
- * Reset Volume bar
- */
- resetVolumeBar_() {
- this.volume(1.0);
- this.trigger('volumechange');
- }
-
- /**
- * Returns all of the current source objects.
- *
- * @return {Tech~SourceObject[]}
- * The current source objects
- */
- currentSources() {
- const source = this.currentSource();
- const sources = [];
-
- // assume `{}` or `{ src }`
- if (Object.keys(source).length !== 0) {
- sources.push(source);
- }
- return this.cache_.sources || sources;
- }
-
- /**
- * Returns the current source object.
- *
- * @return {Tech~SourceObject}
- * The current source object
- */
- currentSource() {
- return this.cache_.source || {};
- }
-
- /**
- * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
- * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
- *
- * @return {string}
- * The current source
- */
- currentSrc() {
- return this.currentSource() && this.currentSource().src || '';
- }
-
- /**
- * Get the current source type e.g. video/mp4
- * This can allow you rebuild the current source object so that you could load the same
- * source and tech later
- *
- * @return {string}
- * The source MIME type
- */
- currentType() {
- return this.currentSource() && this.currentSource().type || '';
- }
-
- /**
- * Get or set the preload attribute
- *
- * @param {'none'|'auto'|'metadata'} [value]
- * Preload mode to pass to tech
- *
- * @return {string|undefined}
- * - The preload attribute value when getting
- * - Nothing when setting
- */
- preload(value) {
- if (value !== undefined) {
- this.techCall_('setPreload', value);
- this.options_.preload = value;
- return;
- }
- return this.techGet_('preload');
- }
-
- /**
- * Get or set the autoplay option. When this is a boolean it will
- * modify the attribute on the tech. When this is a string the attribute on
- * the tech will be removed and `Player` will handle autoplay on loadstarts.
- *
- * @param {boolean|'play'|'muted'|'any'} [value]
- * - true: autoplay using the browser behavior
- * - false: do not autoplay
- * - 'play': call play() on every loadstart
- * - 'muted': call muted() then play() on every loadstart
- * - 'any': call play() on every loadstart. if that fails call muted() then play().
- * - *: values other than those listed here will be set `autoplay` to true
- *
- * @return {boolean|string|undefined}
- * - The current value of autoplay when getting
- * - Nothing when setting
- */
- autoplay(value) {
- // getter usage
- if (value === undefined) {
- return this.options_.autoplay || false;
- }
- let techAutoplay;
-
- // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
- if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
- this.options_.autoplay = value;
- this.manualAutoplay_(typeof value === 'string' ? value : 'play');
- techAutoplay = false;
-
- // any falsy value sets autoplay to false in the browser,
- // lets do the same
- } else if (!value) {
- this.options_.autoplay = false;
-
- // any other value (ie truthy) sets autoplay to true
- } else {
- this.options_.autoplay = true;
- }
- techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
-
- // if we don't have a tech then we do not queue up
- // a setAutoplay call on tech ready. We do this because the
- // autoplay option will be passed in the constructor and we
- // do not need to set it twice
- if (this.tech_) {
- this.techCall_('setAutoplay', techAutoplay);
- }
- }
-
- /**
- * Set or unset the playsinline attribute.
- * Playsinline tells the browser that non-fullscreen playback is preferred.
- *
- * @param {boolean} [value]
- * - true means that we should try to play inline by default
- * - false means that we should use the browser's default playback mode,
- * which in most cases is inline. iOS Safari is a notable exception
- * and plays fullscreen by default.
- *
- * @return {string|undefined}
- * - the current value of playsinline
- * - Nothing when setting
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- playsinline(value) {
- if (value !== undefined) {
- this.techCall_('setPlaysinline', value);
- this.options_.playsinline = value;
- }
- return this.techGet_('playsinline');
- }
-
- /**
- * Get or set the loop attribute on the video element.
- *
- * @param {boolean} [value]
- * - true means that we should loop the video
- * - false means that we should not loop the video
- *
- * @return {boolean|undefined}
- * - The current value of loop when getting
- * - Nothing when setting
- */
- loop(value) {
- if (value !== undefined) {
- this.techCall_('setLoop', value);
- this.options_.loop = value;
- return;
- }
- return this.techGet_('loop');
- }
-
- /**
- * Get or set the poster image source url
- *
- * @fires Player#posterchange
- *
- * @param {string} [src]
- * Poster image source URL
- *
- * @return {string|undefined}
- * - The current value of poster when getting
- * - Nothing when setting
- */
- poster(src) {
- if (src === undefined) {
- return this.poster_;
- }
-
- // The correct way to remove a poster is to set as an empty string
- // other falsey values will throw errors
- if (!src) {
- src = '';
- }
- if (src === this.poster_) {
- return;
- }
-
- // update the internal poster variable
- this.poster_ = src;
-
- // update the tech's poster
- this.techCall_('setPoster', src);
- this.isPosterFromTech_ = false;
-
- // alert components that the poster has been set
- /**
- * This event fires when the poster image is changed on the player.
- *
- * @event Player#posterchange
- * @type {Event}
- */
- this.trigger('posterchange');
- }
-
- /**
- * Some techs (e.g. YouTube) can provide a poster source in an
- * asynchronous way. We want the poster component to use this
- * poster source so that it covers up the tech's controls.
- * (YouTube's play button). However we only want to use this
- * source if the player user hasn't set a poster through
- * the normal APIs.
- *
- * @fires Player#posterchange
- * @listens Tech#posterchange
- * @private
- */
- handleTechPosterChange_() {
- if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
- const newPoster = this.tech_.poster() || '';
- if (newPoster !== this.poster_) {
- this.poster_ = newPoster;
- this.isPosterFromTech_ = true;
-
- // Let components know the poster has changed
- this.trigger('posterchange');
- }
- }
- }
-
- /**
- * Get or set whether or not the controls are showing.
- *
- * @fires Player#controlsenabled
- *
- * @param {boolean} [bool]
- * - true to turn controls on
- * - false to turn controls off
- *
- * @return {boolean|undefined}
- * - The current value of controls when getting
- * - Nothing when setting
- */
- controls(bool) {
- if (bool === undefined) {
- return !!this.controls_;
- }
- bool = !!bool;
-
- // Don't trigger a change event unless it actually changed
- if (this.controls_ === bool) {
- return;
- }
- this.controls_ = bool;
- if (this.usingNativeControls()) {
- this.techCall_('setControls', bool);
- }
- if (this.controls_) {
- this.removeClass('vjs-controls-disabled');
- this.addClass('vjs-controls-enabled');
- /**
- * @event Player#controlsenabled
- * @type {Event}
- */
- this.trigger('controlsenabled');
- if (!this.usingNativeControls()) {
- this.addTechControlsListeners_();
- }
- } else {
- this.removeClass('vjs-controls-enabled');
- this.addClass('vjs-controls-disabled');
- /**
- * @event Player#controlsdisabled
- * @type {Event}
- */
- this.trigger('controlsdisabled');
- if (!this.usingNativeControls()) {
- this.removeTechControlsListeners_();
- }
- }
- }
-
- /**
- * Toggle native controls on/off. Native controls are the controls built into
- * devices (e.g. default iPhone controls) or other techs
- * (e.g. Vimeo Controls)
- * **This should only be set by the current tech, because only the tech knows
- * if it can support native controls**
- *
- * @fires Player#usingnativecontrols
- * @fires Player#usingcustomcontrols
- *
- * @param {boolean} [bool]
- * - true to turn native controls on
- * - false to turn native controls off
- *
- * @return {boolean|undefined}
- * - The current value of native controls when getting
- * - Nothing when setting
- */
- usingNativeControls(bool) {
- if (bool === undefined) {
- return !!this.usingNativeControls_;
- }
- bool = !!bool;
-
- // Don't trigger a change event unless it actually changed
- if (this.usingNativeControls_ === bool) {
- return;
- }
- this.usingNativeControls_ = bool;
- if (this.usingNativeControls_) {
- this.addClass('vjs-using-native-controls');
-
- /**
- * player is using the native device controls
- *
- * @event Player#usingnativecontrols
- * @type {Event}
- */
- this.trigger('usingnativecontrols');
- } else {
- this.removeClass('vjs-using-native-controls');
-
- /**
- * player is using the custom HTML controls
- *
- * @event Player#usingcustomcontrols
- * @type {Event}
- */
- this.trigger('usingcustomcontrols');
- }
- }
-
- /**
- * Set or get the current MediaError
- *
- * @fires Player#error
- *
- * @param {MediaError|string|number} [err]
- * A MediaError or a string/number to be turned
- * into a MediaError
- *
- * @return {MediaError|null|undefined}
- * - The current MediaError when getting (or null)
- * - Nothing when setting
- */
- error(err) {
- if (err === undefined) {
- return this.error_ || null;
- }
-
- // allow hooks to modify error object
- hooks('beforeerror').forEach(hookFunction => {
- const newErr = hookFunction(this, err);
- if (!(isObject(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
- this.log.error('please return a value that MediaError expects in beforeerror hooks');
- return;
- }
- err = newErr;
- });
-
- // Suppress the first error message for no compatible source until
- // user interaction
- if (this.options_.suppressNotSupportedError && err && err.code === 4) {
- const triggerSuppressedError = function () {
- this.error(err);
- };
- this.options_.suppressNotSupportedError = false;
- this.any(['click', 'touchstart'], triggerSuppressedError);
- this.one('loadstart', function () {
- this.off(['click', 'touchstart'], triggerSuppressedError);
- });
- return;
- }
-
- // restoring to default
- if (err === null) {
- this.error_ = null;
- this.removeClass('vjs-error');
- if (this.errorDisplay) {
- this.errorDisplay.close();
- }
- return;
- }
- this.error_ = new MediaError(err);
-
- // add the vjs-error classname to the player
- this.addClass('vjs-error');
-
- // log the name of the error type and any message
- // IE11 logs "[object object]" and required you to expand message to see error object
- log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
-
- /**
- * @event Player#error
- * @type {Event}
- */
- this.trigger('error');
-
- // notify hooks of the per player error
- hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
- return;
- }
-
- /**
- * Report user activity
- *
- * @param {Object} event
- * Event object
- */
- reportUserActivity(event) {
- this.userActivity_ = true;
- }
-
- /**
- * Get/set if user is active
- *
- * @fires Player#useractive
- * @fires Player#userinactive
- *
- * @param {boolean} [bool]
- * - true if the user is active
- * - false if the user is inactive
- *
- * @return {boolean|undefined}
- * - The current value of userActive when getting
- * - Nothing when setting
- */
- userActive(bool) {
- if (bool === undefined) {
- return this.userActive_;
- }
- bool = !!bool;
- if (bool === this.userActive_) {
- return;
- }
- this.userActive_ = bool;
- if (this.userActive_) {
- this.userActivity_ = true;
- this.removeClass('vjs-user-inactive');
- this.addClass('vjs-user-active');
- /**
- * @event Player#useractive
- * @type {Event}
- */
- this.trigger('useractive');
- return;
- }
-
- // Chrome/Safari/IE have bugs where when you change the cursor it can
- // trigger a mousemove event. This causes an issue when you're hiding
- // the cursor when the user is inactive, and a mousemove signals user
- // activity. Making it impossible to go into inactive mode. Specifically
- // this happens in fullscreen when we really need to hide the cursor.
- //
- // When this gets resolved in ALL browsers it can be removed
- // https://code.google.com/p/chromium/issues/detail?id=103041
- if (this.tech_) {
- this.tech_.one('mousemove', function (e) {
- e.stopPropagation();
- e.preventDefault();
- });
- }
- this.userActivity_ = false;
- this.removeClass('vjs-user-active');
- this.addClass('vjs-user-inactive');
- /**
- * @event Player#userinactive
- * @type {Event}
- */
- this.trigger('userinactive');
- }
-
- /**
- * Listen for user activity based on timeout value
- *
- * @private
- */
- listenForUserActivity_() {
- let mouseInProgress;
- let lastMoveX;
- let lastMoveY;
- const handleActivity = bind_(this, this.reportUserActivity);
- const handleMouseMove = function (e) {
- // #1068 - Prevent mousemove spamming
- // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
- if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
- lastMoveX = e.screenX;
- lastMoveY = e.screenY;
- handleActivity();
- }
- };
- const handleMouseDown = function () {
- handleActivity();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(mouseInProgress);
- // Setting userActivity=true now and setting the interval to the same time
- // as the activityCheck interval (250) should ensure we never miss the
- // next activityCheck
- mouseInProgress = this.setInterval(handleActivity, 250);
- };
- const handleMouseUpAndMouseLeave = function (event) {
- handleActivity();
- // Stop the interval that maintains activity if the mouse/touch is down
- this.clearInterval(mouseInProgress);
- };
-
- // Any mouse movement will be considered user activity
- this.on('mousedown', handleMouseDown);
- this.on('mousemove', handleMouseMove);
- this.on('mouseup', handleMouseUpAndMouseLeave);
- this.on('mouseleave', handleMouseUpAndMouseLeave);
- const controlBar = this.getChild('controlBar');
-
- // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
- // controlBar would no longer be hidden by default timeout.
- if (controlBar && !IS_IOS && !IS_ANDROID) {
- controlBar.on('mouseenter', function (event) {
- if (this.player().options_.inactivityTimeout !== 0) {
- this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
- }
- this.player().options_.inactivityTimeout = 0;
- });
- controlBar.on('mouseleave', function (event) {
- this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
- });
- }
-
- // Listen for keyboard navigation
- // Shouldn't need to use inProgress interval because of key repeat
- this.on('keydown', handleActivity);
- this.on('keyup', handleActivity);
-
- // Run an interval every 250 milliseconds instead of stuffing everything into
- // the mousemove/touchmove function itself, to prevent performance degradation.
- // `this.reportUserActivity` simply sets this.userActivity_ to true, which
- // then gets picked up by this loop
- // http://ejohn.org/blog/learning-from-twitter/
- let inactivityTimeout;
-
- /** @this Player */
- const activityCheck = function () {
- // Check to see if mouse/touch activity has happened
- if (!this.userActivity_) {
- return;
- }
-
- // Reset the activity tracker
- this.userActivity_ = false;
-
- // If the user state was inactive, set the state to active
- this.userActive(true);
-
- // Clear any existing inactivity timeout to start the timer over
- this.clearTimeout(inactivityTimeout);
- const timeout = this.options_.inactivityTimeout;
- if (timeout <= 0) {
- return;
- }
-
- // In milliseconds, if no more activity has occurred the
- // user will be considered inactive
- inactivityTimeout = this.setTimeout(function () {
- // Protect against the case where the inactivityTimeout can trigger just
- // before the next user activity is picked up by the activity check loop
- // causing a flicker
- if (!this.userActivity_) {
- this.userActive(false);
- }
- }, timeout);
- };
- this.setInterval(activityCheck, 250);
- }
-
- /**
- * Gets or sets the current playback rate. A playback rate of
- * 1.0 represents normal speed and 0.5 would indicate half-speed
- * playback, for instance.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
- *
- * @param {number} [rate]
- * New playback rate to set.
- *
- * @return {number|undefined}
- * - The current playback rate when getting or 1.0
- * - Nothing when setting
- */
- playbackRate(rate) {
- if (rate !== undefined) {
- // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
- // that is registered above
- this.techCall_('setPlaybackRate', rate);
- return;
- }
- if (this.tech_ && this.tech_.featuresPlaybackRate) {
- return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
- }
- return 1.0;
- }
-
- /**
- * Gets or sets the current default playback rate. A default playback rate of
- * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
- * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
- * not the current playbackRate.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
- *
- * @param {number} [rate]
- * New default playback rate to set.
- *
- * @return {number|undefined}
- * - The default playback rate when getting or 1.0
- * - Nothing when setting
- */
- defaultPlaybackRate(rate) {
- if (rate !== undefined) {
- return this.techCall_('setDefaultPlaybackRate', rate);
- }
- if (this.tech_ && this.tech_.featuresPlaybackRate) {
- return this.techGet_('defaultPlaybackRate');
- }
- return 1.0;
- }
-
- /**
- * Gets or sets the audio flag
- *
- * @param {boolean} [bool]
- * - true signals that this is an audio player
- * - false signals that this is not an audio player
- *
- * @return {boolean|undefined}
- * - The current value of isAudio when getting
- * - Nothing when setting
- */
- isAudio(bool) {
- if (bool !== undefined) {
- this.isAudio_ = !!bool;
- return;
- }
- return !!this.isAudio_;
- }
- enableAudioOnlyUI_() {
- // Update styling immediately to show the control bar so we can get its height
- this.addClass('vjs-audio-only-mode');
- const playerChildren = this.children();
- const controlBar = this.getChild('ControlBar');
- const controlBarHeight = controlBar && controlBar.currentHeight();
-
- // Hide all player components except the control bar. Control bar components
- // needed only for video are hidden with CSS
- playerChildren.forEach(child => {
- if (child === controlBar) {
- return;
- }
- if (child.el_ && !child.hasClass('vjs-hidden')) {
- child.hide();
- this.audioOnlyCache_.hiddenChildren.push(child);
- }
- });
- this.audioOnlyCache_.playerHeight = this.currentHeight();
-
- // Set the player height the same as the control bar
- this.height(controlBarHeight);
- this.trigger('audioonlymodechange');
- }
- disableAudioOnlyUI_() {
- this.removeClass('vjs-audio-only-mode');
-
- // Show player components that were previously hidden
- this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
-
- // Reset player height
- this.height(this.audioOnlyCache_.playerHeight);
- this.trigger('audioonlymodechange');
- }
-
- /**
- * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
- *
- * Setting this to `true` will hide all player components except the control bar,
- * as well as control bar components needed only for video.
- *
- * @param {boolean} [value]
- * The value to set audioOnlyMode to.
- *
- * @return {Promise|boolean}
- * A Promise is returned when setting the state, and a boolean when getting
- * the present state
- */
- audioOnlyMode(value) {
- if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
- return this.audioOnlyMode_;
- }
- this.audioOnlyMode_ = value;
-
- // Enable Audio Only Mode
- if (value) {
- const exitPromises = [];
-
- // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
- if (this.isInPictureInPicture()) {
- exitPromises.push(this.exitPictureInPicture());
- }
- if (this.isFullscreen()) {
- exitPromises.push(this.exitFullscreen());
- }
- if (this.audioPosterMode()) {
- exitPromises.push(this.audioPosterMode(false));
- }
- return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
- }
-
- // Disable Audio Only Mode
- return Promise.resolve().then(() => this.disableAudioOnlyUI_());
- }
- enablePosterModeUI_() {
- // Hide the video element and show the poster image to enable posterModeUI
- const tech = this.tech_ && this.tech_;
- tech.hide();
- this.addClass('vjs-audio-poster-mode');
- this.trigger('audiopostermodechange');
- }
- disablePosterModeUI_() {
- // Show the video element and hide the poster image to disable posterModeUI
- const tech = this.tech_ && this.tech_;
- tech.show();
- this.removeClass('vjs-audio-poster-mode');
- this.trigger('audiopostermodechange');
- }
-
- /**
- * Get the current audioPosterMode state or set audioPosterMode to true or false
- *
- * @param {boolean} [value]
- * The value to set audioPosterMode to.
- *
- * @return {Promise|boolean}
- * A Promise is returned when setting the state, and a boolean when getting
- * the present state
- */
- audioPosterMode(value) {
- if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
- return this.audioPosterMode_;
- }
- this.audioPosterMode_ = value;
- if (value) {
- if (this.audioOnlyMode()) {
- const audioOnlyModePromise = this.audioOnlyMode(false);
- return audioOnlyModePromise.then(() => {
- // enable audio poster mode after audio only mode is disabled
- this.enablePosterModeUI_();
- });
- }
- return Promise.resolve().then(() => {
- // enable audio poster mode
- this.enablePosterModeUI_();
- });
- }
- return Promise.resolve().then(() => {
- // disable audio poster mode
- this.disablePosterModeUI_();
- });
- }
-
- /**
- * A helper method for adding a {@link TextTrack} to our
- * {@link TextTrackList}.
- *
- * In addition to the W3C settings we allow adding additional info through options.
- *
- * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
- *
- * @param {string} [kind]
- * the kind of TextTrack you are adding
- *
- * @param {string} [label]
- * the label to give the TextTrack label
- *
- * @param {string} [language]
- * the language to set on the TextTrack
- *
- * @return {TextTrack|undefined}
- * the TextTrack that was added or undefined
- * if there is no tech
- */
- addTextTrack(kind, label, language) {
- if (this.tech_) {
- return this.tech_.addTextTrack(kind, label, language);
- }
- }
-
- /**
- * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
- *
- * @param {Object} options
- * Options to pass to {@link HTMLTrackElement} during creation. See
- * {@link HTMLTrackElement} for object properties that you should use.
- *
- * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
- * from the TextTrackList and HtmlTrackElementList
- * after a source change
- *
- * @return { import('./tracks/html-track-element').default }
- * the HTMLTrackElement that was created and added
- * to the HtmlTrackElementList and the remote
- * TextTrackList
- *
- */
- addRemoteTextTrack(options, manualCleanup) {
- if (this.tech_) {
- return this.tech_.addRemoteTextTrack(options, manualCleanup);
- }
- }
-
- /**
- * Remove a remote {@link TextTrack} from the respective
- * {@link TextTrackList} and {@link HtmlTrackElementList}.
- *
- * @param {Object} track
- * Remote {@link TextTrack} to remove
- *
- * @return {undefined}
- * does not return anything
- */
- removeRemoteTextTrack(obj = {}) {
- let {
- track
- } = obj;
- if (!track) {
- track = obj;
- }
-
- // destructure the input into an object with a track argument, defaulting to arguments[0]
- // default the whole argument to an empty object if nothing was passed in
-
- if (this.tech_) {
- return this.tech_.removeRemoteTextTrack(track);
- }
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object|undefined}
- * An object with supported media playback quality metrics or undefined if there
- * is no tech or the tech does not support it.
- */
- getVideoPlaybackQuality() {
- return this.techGet_('getVideoPlaybackQuality');
- }
-
- /**
- * Get video width
- *
- * @return {number}
- * current video width
- */
- videoWidth() {
- return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
- }
-
- /**
- * Get video height
- *
- * @return {number}
- * current video height
- */
- videoHeight() {
- return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
- }
-
- /**
- * Set or get the player's language code.
- *
- * Changing the language will trigger
- * [languagechange]{@link Player#event:languagechange}
- * which Components can use to update control text.
- * ClickableComponent will update its control text by default on
- * [languagechange]{@link Player#event:languagechange}.
- *
- * @fires Player#languagechange
- *
- * @param {string} [code]
- * the language code to set the player to
- *
- * @return {string|undefined}
- * - The current language code when getting
- * - Nothing when setting
- */
- language(code) {
- if (code === undefined) {
- return this.language_;
- }
- if (this.language_ !== String(code).toLowerCase()) {
- this.language_ = String(code).toLowerCase();
-
- // during first init, it's possible some things won't be evented
- if (isEvented(this)) {
- /**
- * fires when the player language change
- *
- * @event Player#languagechange
- * @type {Event}
- */
- this.trigger('languagechange');
- }
- }
- }
-
- /**
- * Get the player's language dictionary
- * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
- * Languages specified directly in the player options have precedence
- *
- * @return {Array}
- * An array of of supported languages
- */
- languages() {
- return merge(Player.prototype.options_.languages, this.languages_);
- }
-
- /**
- * returns a JavaScript object representing the current track
- * information. **DOES not return it as JSON**
- *
- * @return {Object}
- * Object representing the current of track info
- */
- toJSON() {
- const options = merge(this.options_);
- const tracks = options.tracks;
- options.tracks = [];
- for (let i = 0; i < tracks.length; i++) {
- let track = tracks[i];
-
- // deep merge tracks and null out player so no circular references
- track = merge(track);
- track.player = undefined;
- options.tracks[i] = track;
- }
- return options;
- }
-
- /**
- * Creates a simple modal dialog (an instance of the {@link ModalDialog}
- * component) that immediately overlays the player with arbitrary
- * content and removes itself when closed.
- *
- * @param {string|Function|Element|Array|null} content
- * Same as {@link ModalDialog#content}'s param of the same name.
- * The most straight-forward usage is to provide a string or DOM
- * element.
- *
- * @param {Object} [options]
- * Extra options which will be passed on to the {@link ModalDialog}.
- *
- * @return {ModalDialog}
- * the {@link ModalDialog} that was created
- */
- createModal(content, options) {
- options = options || {};
- options.content = content || '';
- const modal = new ModalDialog(this, options);
- this.addChild(modal);
- modal.on('dispose', () => {
- this.removeChild(modal);
- });
- modal.open();
- return modal;
- }
-
- /**
- * Change breakpoint classes when the player resizes.
- *
- * @private
- */
- updateCurrentBreakpoint_() {
- if (!this.responsive()) {
- return;
- }
- const currentBreakpoint = this.currentBreakpoint();
- const currentWidth = this.currentWidth();
- for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
- const candidateBreakpoint = BREAKPOINT_ORDER[i];
- const maxWidth = this.breakpoints_[candidateBreakpoint];
- if (currentWidth <= maxWidth) {
- // The current breakpoint did not change, nothing to do.
- if (currentBreakpoint === candidateBreakpoint) {
- return;
- }
-
- // Only remove a class if there is a current breakpoint.
- if (currentBreakpoint) {
- this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
- }
- this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
- this.breakpoint_ = candidateBreakpoint;
- break;
- }
- }
- }
-
- /**
- * Removes the current breakpoint.
- *
- * @private
- */
- removeCurrentBreakpoint_() {
- const className = this.currentBreakpointClass();
- this.breakpoint_ = '';
- if (className) {
- this.removeClass(className);
- }
- }
-
- /**
- * Get or set breakpoints on the player.
- *
- * Calling this method with an object or `true` will remove any previous
- * custom breakpoints and start from the defaults again.
- *
- * @param {Object|boolean} [breakpoints]
- * If an object is given, it can be used to provide custom
- * breakpoints. If `true` is given, will set default breakpoints.
- * If this argument is not given, will simply return the current
- * breakpoints.
- *
- * @param {number} [breakpoints.tiny]
- * The maximum width for the "vjs-layout-tiny" class.
- *
- * @param {number} [breakpoints.xsmall]
- * The maximum width for the "vjs-layout-x-small" class.
- *
- * @param {number} [breakpoints.small]
- * The maximum width for the "vjs-layout-small" class.
- *
- * @param {number} [breakpoints.medium]
- * The maximum width for the "vjs-layout-medium" class.
- *
- * @param {number} [breakpoints.large]
- * The maximum width for the "vjs-layout-large" class.
- *
- * @param {number} [breakpoints.xlarge]
- * The maximum width for the "vjs-layout-x-large" class.
- *
- * @param {number} [breakpoints.huge]
- * The maximum width for the "vjs-layout-huge" class.
- *
- * @return {Object}
- * An object mapping breakpoint names to maximum width values.
- */
- breakpoints(breakpoints) {
- // Used as a getter.
- if (breakpoints === undefined) {
- return Object.assign(this.breakpoints_);
- }
- this.breakpoint_ = '';
- this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
-
- // When breakpoint definitions change, we need to update the currently
- // selected breakpoint.
- this.updateCurrentBreakpoint_();
-
- // Clone the breakpoints before returning.
- return Object.assign(this.breakpoints_);
- }
-
- /**
- * Get or set a flag indicating whether or not this player should adjust
- * its UI based on its dimensions.
- *
- * @param {boolean} [value]
- * Should be `true` if the player should adjust its UI based on its
- * dimensions; otherwise, should be `false`.
- *
- * @return {boolean|undefined}
- * Will be `true` if this player should adjust its UI based on its
- * dimensions; otherwise, will be `false`.
- * Nothing if setting
- */
- responsive(value) {
- // Used as a getter.
- if (value === undefined) {
- return this.responsive_;
- }
- value = Boolean(value);
- const current = this.responsive_;
-
- // Nothing changed.
- if (value === current) {
- return;
- }
-
- // The value actually changed, set it.
- this.responsive_ = value;
-
- // Start listening for breakpoints and set the initial breakpoint if the
- // player is now responsive.
- if (value) {
- this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
- this.updateCurrentBreakpoint_();
-
- // Stop listening for breakpoints if the player is no longer responsive.
- } else {
- this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
- this.removeCurrentBreakpoint_();
- }
- return value;
- }
-
- /**
- * Get current breakpoint name, if any.
- *
- * @return {string}
- * If there is currently a breakpoint set, returns a the key from the
- * breakpoints object matching it. Otherwise, returns an empty string.
- */
- currentBreakpoint() {
- return this.breakpoint_;
- }
-
- /**
- * Get the current breakpoint class name.
- *
- * @return {string}
- * The matching class name (e.g. `"vjs-layout-tiny"` or
- * `"vjs-layout-large"`) for the current breakpoint. Empty string if
- * there is no current breakpoint.
- */
- currentBreakpointClass() {
- return BREAKPOINT_CLASSES[this.breakpoint_] || '';
- }
-
- /**
- * An object that describes a single piece of media.
- *
- * Properties that are not part of this type description will be retained; so,
- * this can be viewed as a generic metadata storage mechanism as well.
- *
- * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
- * @typedef {Object} Player~MediaObject
- *
- * @property {string} [album]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {string} [artist]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {Object[]} [artwork]
- * Unused, except if this object is passed to the `MediaSession`
- * API. If not specified, will be populated via the `poster`, if
- * available.
- *
- * @property {string} [poster]
- * URL to an image that will display before playback.
- *
- * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
- * A single source object, an array of source objects, or a string
- * referencing a URL to a media source. It is _highly recommended_
- * that an object or array of objects is used here, so that source
- * selection algorithms can take the `type` into account.
- *
- * @property {string} [title]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {Object[]} [textTracks]
- * An array of objects to be used to create text tracks, following
- * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
- * For ease of removal, these will be created as "remote" text
- * tracks and set to automatically clean up on source changes.
- *
- * These objects may have properties like `src`, `kind`, `label`,
- * and `language`, see {@link Tech#createRemoteTextTrack}.
- */
-
- /**
- * Populate the player using a {@link Player~MediaObject|MediaObject}.
- *
- * @param {Player~MediaObject} media
- * A media object.
- *
- * @param {Function} ready
- * A callback to be called when the player is ready.
- */
- loadMedia(media, ready) {
- if (!media || typeof media !== 'object') {
- return;
- }
- const crossOrigin = this.crossOrigin();
- this.reset();
-
- // Clone the media object so it cannot be mutated from outside.
- this.cache_.media = merge(media);
- const {
- artist,
- artwork,
- description,
- poster,
- src,
- textTracks,
- title
- } = this.cache_.media;
-
- // If `artwork` is not given, create it using `poster`.
- if (!artwork && poster) {
- this.cache_.media.artwork = [{
- src: poster,
- type: getMimetype(poster)
- }];
- }
- if (crossOrigin) {
- this.crossOrigin(crossOrigin);
- }
- if (src) {
- this.src(src);
- }
- if (poster) {
- this.poster(poster);
- }
- if (Array.isArray(textTracks)) {
- textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
- }
- if (this.titleBar) {
- this.titleBar.update({
- title,
- description: description || artist || ''
- });
- }
- this.ready(ready);
- }
-
- /**
- * Get a clone of the current {@link Player~MediaObject} for this player.
- *
- * If the `loadMedia` method has not been used, will attempt to return a
- * {@link Player~MediaObject} based on the current state of the player.
- *
- * @return {Player~MediaObject}
- */
- getMedia() {
- if (!this.cache_.media) {
- const poster = this.poster();
- const src = this.currentSources();
- const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
- kind: tt.kind,
- label: tt.label,
- language: tt.language,
- src: tt.src
- }));
- const media = {
- src,
- textTracks
- };
- if (poster) {
- media.poster = poster;
- media.artwork = [{
- src: media.poster,
- type: getMimetype(media.poster)
- }];
- }
- return media;
- }
- return merge(this.cache_.media);
- }
-
- /**
- * Gets tag settings
- *
- * @param {Element} tag
- * The player tag
- *
- * @return {Object}
- * An object containing all of the settings
- * for a player tag
- */
- static getTagSettings(tag) {
- const baseOptions = {
- sources: [],
- tracks: []
- };
- const tagOptions = getAttributes(tag);
- const dataSetup = tagOptions['data-setup'];
- if (hasClass(tag, 'vjs-fill')) {
- tagOptions.fill = true;
- }
- if (hasClass(tag, 'vjs-fluid')) {
- tagOptions.fluid = true;
- }
-
- // Check if data-setup attr exists.
- if (dataSetup !== null) {
- // Parse options JSON
- // If empty string, make it a parsable json object.
- const [err, data] = tuple(dataSetup || '{}');
- if (err) {
- log.error(err);
- }
- Object.assign(tagOptions, data);
- }
- Object.assign(baseOptions, tagOptions);
-
- // Get tag children settings
- if (tag.hasChildNodes()) {
- const children = tag.childNodes;
- for (let i = 0, j = children.length; i < j; i++) {
- const child = children[i];
- // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
- const childName = child.nodeName.toLowerCase();
- if (childName === 'source') {
- baseOptions.sources.push(getAttributes(child));
- } else if (childName === 'track') {
- baseOptions.tracks.push(getAttributes(child));
- }
- }
- }
- return baseOptions;
- }
-
- /**
- * Set debug mode to enable/disable logs at info level.
- *
- * @param {boolean} enabled
- * @fires Player#debugon
- * @fires Player#debugoff
- * @return {boolean|undefined}
- */
- debug(enabled) {
- if (enabled === undefined) {
- return this.debugEnabled_;
- }
- if (enabled) {
- this.trigger('debugon');
- this.previousLogLevel_ = this.log.level;
- this.log.level('debug');
- this.debugEnabled_ = true;
- } else {
- this.trigger('debugoff');
- this.log.level(this.previousLogLevel_);
- this.previousLogLevel_ = undefined;
- this.debugEnabled_ = false;
- }
- }
-
- /**
- * Set or get current playback rates.
- * Takes an array and updates the playback rates menu with the new items.
- * Pass in an empty array to hide the menu.
- * Values other than arrays are ignored.
- *
- * @fires Player#playbackrateschange
- * @param {number[]} [newRates]
- * The new rates that the playback rates menu should update to.
- * An empty array will hide the menu
- * @return {number[]} When used as a getter will return the current playback rates
- */
- playbackRates(newRates) {
- if (newRates === undefined) {
- return this.cache_.playbackRates;
- }
-
- // ignore any value that isn't an array
- if (!Array.isArray(newRates)) {
- return;
- }
-
- // ignore any arrays that don't only contain numbers
- if (!newRates.every(rate => typeof rate === 'number')) {
- return;
- }
- this.cache_.playbackRates = newRates;
-
- /**
- * fires when the playback rates in a player are changed
- *
- * @event Player#playbackrateschange
- * @type {Event}
- */
- this.trigger('playbackrateschange');
- }
- }
-
- /**
- * Get the {@link VideoTrackList}
- *
- * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
- *
- * @return {VideoTrackList}
- * the current video track list
- *
- * @method Player.prototype.videoTracks
- */
-
- /**
- * Get the {@link AudioTrackList}
- *
- * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
- *
- * @return {AudioTrackList}
- * the current audio track list
- *
- * @method Player.prototype.audioTracks
- */
-
- /**
- * Get the {@link TextTrackList}
- *
- * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
- *
- * @return {TextTrackList}
- * the current text track list
- *
- * @method Player.prototype.textTracks
- */
-
- /**
- * Get the remote {@link TextTrackList}
- *
- * @return {TextTrackList}
- * The current remote text track list
- *
- * @method Player.prototype.remoteTextTracks
- */
-
- /**
- * Get the remote {@link HtmlTrackElementList} tracks.
- *
- * @return {HtmlTrackElementList}
- * The current remote text track element list
- *
- * @method Player.prototype.remoteTextTrackEls
- */
-
- ALL.names.forEach(function (name) {
- const props = ALL[name];
- Player.prototype[props.getterName] = function () {
- if (this.tech_) {
- return this.tech_[props.getterName]();
- }
-
- // if we have not yet loadTech_, we create {video,audio,text}Tracks_
- // these will be passed to the tech during loading
- this[props.privateName] = this[props.privateName] || new props.ListClass();
- return this[props.privateName];
- };
- });
-
- /**
- * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the `` tag to control the CORS
- * behavior.
- *
- * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
- *
- * @param {string} [value]
- * The value to set the `Player`'s crossorigin to. If an argument is
- * given, must be one of `anonymous` or `use-credentials`.
- *
- * @return {string|undefined}
- * - The current crossorigin value of the `Player` when getting.
- * - undefined when setting
- */
- Player.prototype.crossorigin = Player.prototype.crossOrigin;
-
- /**
- * Global enumeration of players.
- *
- * The keys are the player IDs and the values are either the {@link Player}
- * instance or `null` for disposed players.
- *
- * @type {Object}
- */
- Player.players = {};
- const navigator = window.navigator;
-
- /*
- * Player instance options, surfaced using options
- * options = Player.prototype.options_
- * Make changes in options, not here.
- *
- * @type {Object}
- * @private
- */
- Player.prototype.options_ = {
- // Default order of fallback technology
- techOrder: Tech.defaultTechOrder_,
- html5: {},
- // enable sourceset by default
- enableSourceset: true,
- // default inactivity timeout
- inactivityTimeout: 2000,
- // default playback rates
- playbackRates: [],
- // Add playback rate selection by adding rates
- // 'playbackRates': [0.5, 1, 1.5, 2],
- liveui: false,
- // Included control sets
- children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
- language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
- // locales and their language translations
- languages: {},
- // Default message to show when a video cannot be played.
- notSupportedMessage: 'No compatible source was found for this media.',
- normalizeAutoplay: false,
- fullscreen: {
- options: {
- navigationUI: 'hide'
- }
- },
- breakpoints: {},
- responsive: false,
- audioOnlyMode: false,
- audioPosterMode: false,
- // Default smooth seeking to false
- enableSmoothSeeking: false
- };
- TECH_EVENTS_RETRIGGER.forEach(function (event) {
- Player.prototype[`handleTech${toTitleCase(event)}_`] = function () {
- return this.trigger(event);
- };
- });
-
- /**
- * Fired when the player has initial duration and dimension information
- *
- * @event Player#loadedmetadata
- * @type {Event}
- */
-
- /**
- * Fired when the player has downloaded data at the current playback position
- *
- * @event Player#loadeddata
- * @type {Event}
- */
-
- /**
- * Fired when the current playback position has changed *
- * During playback this is fired every 15-250 milliseconds, depending on the
- * playback technology in use.
- *
- * @event Player#timeupdate
- * @type {Event}
- */
-
- /**
- * Fired when the volume changes
- *
- * @event Player#volumechange
- * @type {Event}
- */
-
- /**
- * Reports whether or not a player has a plugin available.
- *
- * This does not report whether or not the plugin has ever been initialized
- * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
- *
- * @method Player#hasPlugin
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not this player has the requested plugin available.
- */
-
- /**
- * Reports whether or not a player is using a plugin by name.
- *
- * For basic plugins, this only reports whether the plugin has _ever_ been
- * initialized on this player.
- *
- * @method Player#usingPlugin
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not this player is using the requested plugin.
- */
-
- Component.registerComponent('Player', Player);
-
- /**
- * @file plugin.js
- */
-
- /**
- * The base plugin name.
- *
- * @private
- * @constant
- * @type {string}
- */
- const BASE_PLUGIN_NAME = 'plugin';
-
- /**
- * The key on which a player's active plugins cache is stored.
- *
- * @private
- * @constant
- * @type {string}
- */
- const PLUGIN_CACHE_KEY = 'activePlugins_';
-
- /**
- * Stores registered plugins in a private space.
- *
- * @private
- * @type {Object}
- */
- const pluginStorage = {};
-
- /**
- * Reports whether or not a plugin has been registered.
- *
- * @private
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not the plugin has been registered.
- */
- const pluginExists = name => pluginStorage.hasOwnProperty(name);
-
- /**
- * Get a single registered plugin by name.
- *
- * @private
- * @param {string} name
- * The name of a plugin.
- *
- * @return {typeof Plugin|Function|undefined}
- * The plugin (or undefined).
- */
- const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
-
- /**
- * Marks a plugin as "active" on a player.
- *
- * Also, ensures that the player has an object for tracking active plugins.
- *
- * @private
- * @param {Player} player
- * A Video.js player instance.
- *
- * @param {string} name
- * The name of a plugin.
- */
- const markPluginAsActive = (player, name) => {
- player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
- player[PLUGIN_CACHE_KEY][name] = true;
- };
-
- /**
- * Triggers a pair of plugin setup events.
- *
- * @private
- * @param {Player} player
- * A Video.js player instance.
- *
- * @param {PluginEventHash} hash
- * A plugin event hash.
- *
- * @param {boolean} [before]
- * If true, prefixes the event name with "before". In other words,
- * use this to trigger "beforepluginsetup" instead of "pluginsetup".
- */
- const triggerSetupEvent = (player, hash, before) => {
- const eventName = (before ? 'before' : '') + 'pluginsetup';
- player.trigger(eventName, hash);
- player.trigger(eventName + ':' + hash.name, hash);
- };
-
- /**
- * Takes a basic plugin function and returns a wrapper function which marks
- * on the player that the plugin has been activated.
- *
- * @private
- * @param {string} name
- * The name of the plugin.
- *
- * @param {Function} plugin
- * The basic plugin.
- *
- * @return {Function}
- * A wrapper function for the given plugin.
- */
- const createBasicPlugin = function (name, plugin) {
- const basicPluginWrapper = function () {
- // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
- // regardless, but we want the hash to be consistent with the hash provided
- // for advanced plugins.
- //
- // The only potentially counter-intuitive thing here is the `instance` in
- // the "pluginsetup" event is the value returned by the `plugin` function.
- triggerSetupEvent(this, {
- name,
- plugin,
- instance: null
- }, true);
- const instance = plugin.apply(this, arguments);
- markPluginAsActive(this, name);
- triggerSetupEvent(this, {
- name,
- plugin,
- instance
- });
- return instance;
- };
- Object.keys(plugin).forEach(function (prop) {
- basicPluginWrapper[prop] = plugin[prop];
- });
- return basicPluginWrapper;
- };
-
- /**
- * Takes a plugin sub-class and returns a factory function for generating
- * instances of it.
- *
- * This factory function will replace itself with an instance of the requested
- * sub-class of Plugin.
- *
- * @private
- * @param {string} name
- * The name of the plugin.
- *
- * @param {Plugin} PluginSubClass
- * The advanced plugin.
- *
- * @return {Function}
- */
- const createPluginFactory = (name, PluginSubClass) => {
- // Add a `name` property to the plugin prototype so that each plugin can
- // refer to itself by name.
- PluginSubClass.prototype.name = name;
- return function (...args) {
- triggerSetupEvent(this, {
- name,
- plugin: PluginSubClass,
- instance: null
- }, true);
- const instance = new PluginSubClass(...[this, ...args]);
-
- // The plugin is replaced by a function that returns the current instance.
- this[name] = () => instance;
- triggerSetupEvent(this, instance.getEventHash());
- return instance;
- };
- };
-
- /**
- * Parent class for all advanced plugins.
- *
- * @mixes module:evented~EventedMixin
- * @mixes module:stateful~StatefulMixin
- * @fires Player#beforepluginsetup
- * @fires Player#beforepluginsetup:$name
- * @fires Player#pluginsetup
- * @fires Player#pluginsetup:$name
- * @listens Player#dispose
- * @throws {Error}
- * If attempting to instantiate the base {@link Plugin} class
- * directly instead of via a sub-class.
- */
- class Plugin {
- /**
- * Creates an instance of this class.
- *
- * Sub-classes should call `super` to ensure plugins are properly initialized.
- *
- * @param {Player} player
- * A Video.js player instance.
- */
- constructor(player) {
- if (this.constructor === Plugin) {
- throw new Error('Plugin must be sub-classed; not directly instantiated.');
- }
- this.player = player;
- if (!this.log) {
- this.log = this.player.log.createLogger(this.name);
- }
-
- // Make this object evented, but remove the added `trigger` method so we
- // use the prototype version instead.
- evented(this);
- delete this.trigger;
- stateful(this, this.constructor.defaultState);
- markPluginAsActive(player, this.name);
-
- // Auto-bind the dispose method so we can use it as a listener and unbind
- // it later easily.
- this.dispose = this.dispose.bind(this);
-
- // If the player is disposed, dispose the plugin.
- player.on('dispose', this.dispose);
- }
-
- /**
- * Get the version of the plugin that was set on .VERSION
- */
- version() {
- return this.constructor.VERSION;
- }
-
- /**
- * Each event triggered by plugins includes a hash of additional data with
- * conventional properties.
- *
- * This returns that object or mutates an existing hash.
- *
- * @param {Object} [hash={}]
- * An object to be used as event an event hash.
- *
- * @return {PluginEventHash}
- * An event hash object with provided properties mixed-in.
- */
- getEventHash(hash = {}) {
- hash.name = this.name;
- hash.plugin = this.constructor;
- hash.instance = this;
- return hash;
- }
-
- /**
- * Triggers an event on the plugin object and overrides
- * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
- *
- * @param {string|Object} event
- * An event type or an object with a type property.
- *
- * @param {Object} [hash={}]
- * Additional data hash to merge with a
- * {@link PluginEventHash|PluginEventHash}.
- *
- * @return {boolean}
- * Whether or not default was prevented.
- */
- trigger(event, hash = {}) {
- return trigger(this.eventBusEl_, event, this.getEventHash(hash));
- }
-
- /**
- * Handles "statechanged" events on the plugin. No-op by default, override by
- * subclassing.
- *
- * @abstract
- * @param {Event} e
- * An event object provided by a "statechanged" event.
- *
- * @param {Object} e.changes
- * An object describing changes that occurred with the "statechanged"
- * event.
- */
- handleStateChanged(e) {}
-
- /**
- * Disposes a plugin.
- *
- * Subclasses can override this if they want, but for the sake of safety,
- * it's probably best to subscribe the "dispose" event.
- *
- * @fires Plugin#dispose
- */
- dispose() {
- const {
- name,
- player
- } = this;
-
- /**
- * Signals that a advanced plugin is about to be disposed.
- *
- * @event Plugin#dispose
- * @type {Event}
- */
- this.trigger('dispose');
- this.off();
- player.off('dispose', this.dispose);
-
- // Eliminate any possible sources of leaking memory by clearing up
- // references between the player and the plugin instance and nulling out
- // the plugin's state and replacing methods with a function that throws.
- player[PLUGIN_CACHE_KEY][name] = false;
- this.player = this.state = null;
-
- // Finally, replace the plugin name on the player with a new factory
- // function, so that the plugin is ready to be set up again.
- player[name] = createPluginFactory(name, pluginStorage[name]);
- }
-
- /**
- * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
- *
- * @param {string|Function} plugin
- * If a string, matches the name of a plugin. If a function, will be
- * tested directly.
- *
- * @return {boolean}
- * Whether or not a plugin is a basic plugin.
- */
- static isBasic(plugin) {
- const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
- return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
- }
-
- /**
- * Register a Video.js plugin.
- *
- * @param {string} name
- * The name of the plugin to be registered. Must be a string and
- * must not match an existing plugin or a method on the `Player`
- * prototype.
- *
- * @param {typeof Plugin|Function} plugin
- * A sub-class of `Plugin` or a function for basic plugins.
- *
- * @return {typeof Plugin|Function}
- * For advanced plugins, a factory function for that plugin. For
- * basic plugins, a wrapper function that initializes the plugin.
- */
- static registerPlugin(name, plugin) {
- if (typeof name !== 'string') {
- throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
- }
- if (pluginExists(name)) {
- log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
- } else if (Player.prototype.hasOwnProperty(name)) {
- throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
- }
- if (typeof plugin !== 'function') {
- throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
- }
- pluginStorage[name] = plugin;
-
- // Add a player prototype method for all sub-classed plugins (but not for
- // the base Plugin class).
- if (name !== BASE_PLUGIN_NAME) {
- if (Plugin.isBasic(plugin)) {
- Player.prototype[name] = createBasicPlugin(name, plugin);
- } else {
- Player.prototype[name] = createPluginFactory(name, plugin);
- }
- }
- return plugin;
- }
-
- /**
- * De-register a Video.js plugin.
- *
- * @param {string} name
- * The name of the plugin to be de-registered. Must be a string that
- * matches an existing plugin.
- *
- * @throws {Error}
- * If an attempt is made to de-register the base plugin.
- */
- static deregisterPlugin(name) {
- if (name === BASE_PLUGIN_NAME) {
- throw new Error('Cannot de-register base plugin.');
- }
- if (pluginExists(name)) {
- delete pluginStorage[name];
- delete Player.prototype[name];
- }
- }
-
- /**
- * Gets an object containing multiple Video.js plugins.
- *
- * @param {Array} [names]
- * If provided, should be an array of plugin names. Defaults to _all_
- * plugin names.
- *
- * @return {Object|undefined}
- * An object containing plugin(s) associated with their name(s) or
- * `undefined` if no matching plugins exist).
- */
- static getPlugins(names = Object.keys(pluginStorage)) {
- let result;
- names.forEach(name => {
- const plugin = getPlugin(name);
- if (plugin) {
- result = result || {};
- result[name] = plugin;
- }
- });
- return result;
- }
-
- /**
- * Gets a plugin's version, if available
- *
- * @param {string} name
- * The name of a plugin.
- *
- * @return {string}
- * The plugin's version or an empty string.
- */
- static getPluginVersion(name) {
- const plugin = getPlugin(name);
- return plugin && plugin.VERSION || '';
- }
- }
-
- /**
- * Gets a plugin by name if it exists.
- *
- * @static
- * @method getPlugin
- * @memberOf Plugin
- * @param {string} name
- * The name of a plugin.
- *
- * @returns {typeof Plugin|Function|undefined}
- * The plugin (or `undefined`).
- */
- Plugin.getPlugin = getPlugin;
-
- /**
- * The name of the base plugin class as it is registered.
- *
- * @type {string}
- */
- Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
- Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
-
- /**
- * Documented in player.js
- *
- * @ignore
- */
- Player.prototype.usingPlugin = function (name) {
- return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
- };
-
- /**
- * Documented in player.js
- *
- * @ignore
- */
- Player.prototype.hasPlugin = function (name) {
- return !!pluginExists(name);
- };
-
- /**
- * Signals that a plugin is about to be set up on a player.
- *
- * @event Player#beforepluginsetup
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin is about to be set up on a player - by name. The name
- * is the name of the plugin.
- *
- * @event Player#beforepluginsetup:$name
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin has just been set up on a player.
- *
- * @event Player#pluginsetup
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin has just been set up on a player - by name. The name
- * is the name of the plugin.
- *
- * @event Player#pluginsetup:$name
- * @type {PluginEventHash}
- */
-
- /**
- * @typedef {Object} PluginEventHash
- *
- * @property {string} instance
- * For basic plugins, the return value of the plugin function. For
- * advanced plugins, the plugin instance on which the event is fired.
- *
- * @property {string} name
- * The name of the plugin.
- *
- * @property {string} plugin
- * For basic plugins, the plugin function. For advanced plugins, the
- * plugin class/constructor.
- */
-
- /**
- * @file deprecate.js
- * @module deprecate
- */
-
- /**
- * Decorate a function with a deprecation message the first time it is called.
- *
- * @param {string} message
- * A deprecation message to log the first time the returned function
- * is called.
- *
- * @param {Function} fn
- * The function to be deprecated.
- *
- * @return {Function}
- * A wrapper function that will log a deprecation warning the first
- * time it is called. The return value will be the return value of
- * the wrapped function.
- */
- function deprecate(message, fn) {
- let warned = false;
- return function (...args) {
- if (!warned) {
- log.warn(message);
- }
- warned = true;
- return fn.apply(this, args);
- };
- }
-
- /**
- * Internal function used to mark a function as deprecated in the next major
- * version with consistent messaging.
- *
- * @param {number} major The major version where it will be removed
- * @param {string} oldName The old function name
- * @param {string} newName The new function name
- * @param {Function} fn The function to deprecate
- * @return {Function} The decorated function
- */
- function deprecateForMajor(major, oldName, newName, fn) {
- return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
- }
-
- var VjsErrors = {
- UnsupportedSidxContainer: 'unsupported-sidx-container-error',
- DashManifestSidxParsingError: 'dash-manifest-sidx-parsing-error',
- HlsPlaylistRequestError: 'hls-playlist-request-error',
- SegmentUnsupportedMediaFormat: 'segment-unsupported-media-format-error',
- UnsupportedMediaInitialization: 'unsupported-media-initialization-error',
- SegmentSwitchError: 'segment-switch-error',
- SegmentExceedsSourceBufferQuota: 'segment-exceeds-source-buffer-quota-error',
- SegmentAppendError: 'segment-append-error',
- VttLoadError: 'vtt-load-error',
- VttCueParsingError: 'vtt-cue-parsing-error',
- // Errors used in contrib-ads:
- AdsBeforePrerollError: 'ads-before-preroll-error',
- AdsPrerollError: 'ads-preroll-error',
- AdsMidrollError: 'ads-midroll-error',
- AdsPostrollError: 'ads-postroll-error',
- AdsMacroReplacementFailed: 'ads-macro-replacement-failed',
- AdsResumeContentFailed: 'ads-resume-content-failed',
- // Errors used in contrib-eme:
- EMEFailedToRequestMediaKeySystemAccess: 'eme-failed-request-media-key-system-access',
- EMEFailedToCreateMediaKeys: 'eme-failed-create-media-keys',
- EMEFailedToAttachMediaKeysToVideoElement: 'eme-failed-attach-media-keys-to-video',
- EMEFailedToCreateMediaKeySession: 'eme-failed-create-media-key-session',
- EMEFailedToSetServerCertificate: 'eme-failed-set-server-certificate',
- EMEFailedToGenerateLicenseRequest: 'eme-failed-generate-license-request',
- EMEFailedToUpdateSessionWithReceivedLicenseKeys: 'eme-failed-update-session',
- EMEFailedToCloseSession: 'eme-failed-close-session',
- EMEFailedToRemoveKeysFromSession: 'eme-failed-remove-keys',
- EMEFailedToLoadSessionBySessionId: 'eme-failed-load-session'
- };
-
- /**
- * @file video.js
- * @module videojs
- */
-
- /**
- * Normalize an `id` value by trimming off a leading `#`
- *
- * @private
- * @param {string} id
- * A string, maybe with a leading `#`.
- *
- * @return {string}
- * The string, without any leading `#`.
- */
- const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
-
- /**
- * A callback that is called when a component is ready. Does not have any
- * parameters and any callback value will be ignored. See: {@link Component~ReadyCallback}
- *
- * @callback ReadyCallback
- */
-
- /**
- * The `videojs()` function doubles as the main function for users to create a
- * {@link Player} instance as well as the main library namespace.
- *
- * It can also be used as a getter for a pre-existing {@link Player} instance.
- * However, we _strongly_ recommend using `videojs.getPlayer()` for this
- * purpose because it avoids any potential for unintended initialization.
- *
- * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
- * of our JSDoc template, we cannot properly document this as both a function
- * and a namespace, so its function signature is documented here.
- *
- * #### Arguments
- * ##### id
- * string|Element, **required**
- *
- * Video element or video element ID.
- *
- * ##### options
- * Object, optional
- *
- * Options object for providing settings.
- * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
- *
- * ##### ready
- * {@link Component~ReadyCallback}, optional
- *
- * A function to be called when the {@link Player} and {@link Tech} are ready.
- *
- * #### Return Value
- *
- * The `videojs()` function returns a {@link Player} instance.
- *
- * @namespace
- *
- * @borrows AudioTrack as AudioTrack
- * @borrows Component.getComponent as getComponent
- * @borrows module:events.on as on
- * @borrows module:events.one as one
- * @borrows module:events.off as off
- * @borrows module:events.trigger as trigger
- * @borrows EventTarget as EventTarget
- * @borrows module:middleware.use as use
- * @borrows Player.players as players
- * @borrows Plugin.registerPlugin as registerPlugin
- * @borrows Plugin.deregisterPlugin as deregisterPlugin
- * @borrows Plugin.getPlugins as getPlugins
- * @borrows Plugin.getPlugin as getPlugin
- * @borrows Plugin.getPluginVersion as getPluginVersion
- * @borrows Tech.getTech as getTech
- * @borrows Tech.registerTech as registerTech
- * @borrows TextTrack as TextTrack
- * @borrows VideoTrack as VideoTrack
- *
- * @param {string|Element} id
- * Video element or video element ID.
- *
- * @param {Object} [options]
- * Options object for providing settings.
- * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
- *
- * @param {ReadyCallback} [ready]
- * A function to be called when the {@link Player} and {@link Tech} are
- * ready.
- *
- * @return {Player}
- * The `videojs()` function returns a {@link Player|Player} instance.
- */
- function videojs(id, options, ready) {
- let player = videojs.getPlayer(id);
- if (player) {
- if (options) {
- log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
- }
- if (ready) {
- player.ready(ready);
- }
- return player;
- }
- const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
- if (!isEl(el)) {
- throw new TypeError('The element or ID supplied is not valid. (videojs)');
- }
-
- // document.body.contains(el) will only check if el is contained within that one document.
- // This causes problems for elements in iframes.
- // Instead, use the element's ownerDocument instead of the global document.
- // This will make sure that the element is indeed in the dom of that document.
- // Additionally, check that the document in question has a default view.
- // If the document is no longer attached to the dom, the defaultView of the document will be null.
- // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body
- // always returns false. Instead, use the Shadow DOM root.
- const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window.ShadowRoot : false;
- const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body;
- if (!el.ownerDocument.defaultView || !rootNode.contains(el)) {
- log.warn('The element supplied is not included in the DOM');
- }
- options = options || {};
-
- // Store a copy of the el before modification, if it is to be restored in destroy()
- // If div ingest, store the parent div
- if (options.restoreEl === true) {
- options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
- }
- hooks('beforesetup').forEach(hookFunction => {
- const opts = hookFunction(el, merge(options));
- if (!isObject(opts) || Array.isArray(opts)) {
- log.error('please return an object in beforesetup hooks');
- return;
- }
- options = merge(options, opts);
- });
-
- // We get the current "Player" component here in case an integration has
- // replaced it with a custom player.
- const PlayerComponent = Component.getComponent('Player');
- player = new PlayerComponent(el, options, ready);
- hooks('setup').forEach(hookFunction => hookFunction(player));
- return player;
- }
- videojs.hooks_ = hooks_;
- videojs.hooks = hooks;
- videojs.hook = hook;
- videojs.hookOnce = hookOnce;
- videojs.removeHook = removeHook;
-
- // Add default styles
- if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
- let style = $('.vjs-styles-defaults');
- if (!style) {
- style = createStyleElement('vjs-styles-defaults');
- const head = $('head');
- if (head) {
- head.insertBefore(style, head.firstChild);
- }
- setTextContent(style, `
- .video-js {
- width: 300px;
- height: 150px;
- }
-
- .vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: 56.25%
- }
- `);
- }
- }
-
- // Run Auto-load players
- // You have to wait at least once in case this script is loaded after your
- // video in the DOM (weird behavior only with minified version)
- autoSetupTimeout(1, videojs);
-
- /**
- * Current Video.js version. Follows [semantic versioning](https://semver.org/).
- *
- * @type {string}
- */
- videojs.VERSION = version;
-
- /**
- * The global options object. These are the settings that take effect
- * if no overrides are specified when the player is created.
- *
- * @type {Object}
- */
- videojs.options = Player.prototype.options_;
-
- /**
- * Get an object with the currently created players, keyed by player ID
- *
- * @return {Object}
- * The created players
- */
- videojs.getPlayers = () => Player.players;
-
- /**
- * Get a single player based on an ID or DOM element.
- *
- * This is useful if you want to check if an element or ID has an associated
- * Video.js player, but not create one if it doesn't.
- *
- * @param {string|Element} id
- * An HTML element - ``, ``, or `` -
- * or a string matching the `id` of such an element.
- *
- * @return {Player|undefined}
- * A player instance or `undefined` if there is no player instance
- * matching the argument.
- */
- videojs.getPlayer = id => {
- const players = Player.players;
- let tag;
- if (typeof id === 'string') {
- const nId = normalizeId(id);
- const player = players[nId];
- if (player) {
- return player;
- }
- tag = $('#' + nId);
- } else {
- tag = id;
- }
- if (isEl(tag)) {
- const {
- player,
- playerId
- } = tag;
-
- // Element may have a `player` property referring to an already created
- // player instance. If so, return that.
- if (player || players[playerId]) {
- return player || players[playerId];
- }
- }
- };
-
- /**
- * Returns an array of all current players.
- *
- * @return {Array}
- * An array of all players. The array will be in the order that
- * `Object.keys` provides, which could potentially vary between
- * JavaScript engines.
- *
- */
- videojs.getAllPlayers = () =>
- // Disposed players leave a key with a `null` value, so we need to make sure
- // we filter those out.
- Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
- videojs.players = Player.players;
- videojs.getComponent = Component.getComponent;
-
- /**
- * Register a component so it can referred to by name. Used when adding to other
- * components, either through addChild `component.addChild('myComponent')` or through
- * default children options `{ children: ['myComponent'] }`.
- *
- * > NOTE: You could also just initialize the component before adding.
- * `component.addChild(new MyComponent());`
- *
- * @param {string} name
- * The class name of the component
- *
- * @param {typeof Component} comp
- * The component class
- *
- * @return {typeof Component}
- * The newly registered component
- */
- videojs.registerComponent = (name, comp) => {
- if (Tech.isTech(comp)) {
- log.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
- }
- return Component.registerComponent.call(Component, name, comp);
- };
- videojs.getTech = Tech.getTech;
- videojs.registerTech = Tech.registerTech;
- videojs.use = use;
-
- /**
- * An object that can be returned by a middleware to signify
- * that the middleware is being terminated.
- *
- * @type {object}
- * @property {object} middleware.TERMINATOR
- */
- Object.defineProperty(videojs, 'middleware', {
- value: {},
- writeable: false,
- enumerable: true
- });
- Object.defineProperty(videojs.middleware, 'TERMINATOR', {
- value: TERMINATOR,
- writeable: false,
- enumerable: true
- });
-
- /**
- * A reference to the {@link module:browser|browser utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:browser|browser}
- */
- videojs.browser = browser;
-
- /**
- * A reference to the {@link module:obj|obj utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:obj|obj}
- */
- videojs.obj = Obj;
-
- /**
- * Deprecated reference to the {@link module:obj.merge|merge function}
- *
- * @type {Function}
- * @see {@link module:obj.merge|merge}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
- */
- videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge);
-
- /**
- * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
- *
- * @type {Function}
- * @see {@link module:obj.defineLazyProperty|defineLazyProperty}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
- */
- videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
-
- /**
- * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
- *
- * @type {Function}
- * @see {@link module:fn.bind_|fn.bind_}
- * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
- */
- videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
- videojs.registerPlugin = Plugin.registerPlugin;
- videojs.deregisterPlugin = Plugin.deregisterPlugin;
-
- /**
- * Deprecated method to register a plugin with Video.js
- *
- * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
- *
- * @param {string} name
- * The plugin name
- *
- * @param {typeof Plugin|Function} plugin
- * The plugin sub-class or function
- *
- * @return {typeof Plugin|Function}
- */
- videojs.plugin = (name, plugin) => {
- log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
- return Plugin.registerPlugin(name, plugin);
- };
- videojs.getPlugins = Plugin.getPlugins;
- videojs.getPlugin = Plugin.getPlugin;
- videojs.getPluginVersion = Plugin.getPluginVersion;
-
- /**
- * Adding languages so that they're available to all players.
- * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
- *
- * @param {string} code
- * The language code or dictionary property
- *
- * @param {Object} data
- * The data values to be translated
- *
- * @return {Object}
- * The resulting language dictionary object
- */
- videojs.addLanguage = function (code, data) {
- code = ('' + code).toLowerCase();
- videojs.options.languages = merge(videojs.options.languages, {
- [code]: data
- });
- return videojs.options.languages[code];
- };
-
- /**
- * A reference to the {@link module:log|log utility module} as an object.
- *
- * @type {Function}
- * @see {@link module:log|log}
- */
- videojs.log = log;
- videojs.createLogger = createLogger;
-
- /**
- * A reference to the {@link module:time|time utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:time|time}
- */
- videojs.time = Time;
-
- /**
- * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
- *
- * @type {Function}
- * @see {@link module:time.createTimeRanges|createTimeRanges}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
- */
- videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges);
-
- /**
- * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
- *
- * @type {Function}
- * @see {@link module:time.createTimeRanges|createTimeRanges}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
- */
- videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges);
-
- /**
- * Deprecated reference to the {@link module:time.formatTime|formatTime function}
- *
- * @type {Function}
- * @see {@link module:time.formatTime|formatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
- */
- videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
-
- /**
- * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
- *
- * @type {Function}
- * @see {@link module:time.setFormatTime|setFormatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
- */
- videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
-
- /**
- * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
- *
- * @type {Function}
- * @see {@link module:time.resetFormatTime|resetFormatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
- */
- videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
-
- /**
- * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
- *
- * @type {Function}
- * @see {@link module:url.parseUrl|parseUrl}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
- */
- videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
-
- /**
- * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
- *
- * @type {Function}
- * @see {@link module:url.isCrossOrigin|isCrossOrigin}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
- */
- videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
- videojs.EventTarget = EventTarget;
- videojs.any = any;
- videojs.on = on;
- videojs.one = one;
- videojs.off = off;
- videojs.trigger = trigger;
-
- /**
- * A cross-browser XMLHttpRequest wrapper.
- *
- * @function
- * @param {Object} options
- * Settings for the request.
- *
- * @return {XMLHttpRequest|XDomainRequest}
- * The request object.
- *
- * @see https://github.com/Raynos/xhr
- */
- videojs.xhr = lib;
- videojs.TextTrack = TextTrack;
- videojs.AudioTrack = AudioTrack;
- videojs.VideoTrack = VideoTrack;
- ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
- videojs[k] = function () {
- log.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
- return Dom[k].apply(null, arguments);
- };
- });
- videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
-
- /**
- * A reference to the {@link module:dom|DOM utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:dom|dom}
- */
- videojs.dom = Dom;
-
- /**
- * A reference to the {@link module:fn|fn utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:fn|fn}
- */
- videojs.fn = Fn;
-
- /**
- * A reference to the {@link module:num|num utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:num|num}
- */
- videojs.num = Num;
-
- /**
- * A reference to the {@link module:str|str utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:str|str}
- */
- videojs.str = Str;
-
- /**
- * A reference to the {@link module:url|URL utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:url|url}
- */
- videojs.url = Url;
-
- // The list of possible error types to occur in video.js
- videojs.Error = VjsErrors;
-
- return videojs;
-
-}));
diff --git a/source/src/public/twitch/video.js/alt/video.core.novtt.min.js b/source/src/public/twitch/video.js/alt/video.core.novtt.min.js
deleted file mode 100755
index 92d2a4d..0000000
--- a/source/src/public/twitch/video.js/alt/video.core.novtt.min.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Video.js 8.12.0
- * Copyright Brightcove, Inc.
- * Available under Apache License Version 2.0
- *
- *
- * Includes vtt.js
- * Available under Apache License Version 2.0
- *
- */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).videojs=t()}(this,function(){"use strict";var D="8.12.0";const s={},B=function(e,t){return s[e]=s[e]||[],t&&(s[e]=s[e].concat(t)),s[e]};function R(e,t){return!((t=B(e).indexOf(t))<=-1||(s[e]=s[e].slice(),s[e].splice(t,1),0))}const F={prefixed:!0};var H=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror","fullscreen"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror","-webkit-full-screen"]],z=H[0];let V;for(let e=0;e{var e,s=h.levels[s],r=new RegExp(`^(${s})$`);let n=l;if("log"!==t&&i.unshift(t.toUpperCase()+":"),c&&(n="%c"+l,i.unshift(c)),i.unshift(n+":"),d&&(d.push([].concat(i)),e=d.length-1e3,d.splice(0,0i(r+` ${t=void 0!==t?t:n} `+e,t,void 0!==s?s:a),o.createNewLogger=(e,t,s)=>i(e,t,s),o.levels={all:"debug|log|warn|error",off:"",debug:"debug|log|warn|error",info:"log|warn|error",warn:"warn|error",error:"error",DEFAULT:t},o.level=e=>{if("string"==typeof e){if(!o.levels.hasOwnProperty(e))throw new Error(`"${e}" in not a valid log level`);t=e}return t},o.history=()=>d?[].concat(d):[],o.history.filter=t=>(d||[]).filter(e=>new RegExp(`.*${t}.*`).test(e[0])),o.history.clear=()=>{d&&(d.length=0)},o.history.disable=()=>{null!==d&&(d.length=0,d=null)},o.history.enable=()=>{null===d&&(d=[])},o.error=(...e)=>s("error",t,e),o.warn=(...e)=>s("warn",t,e),o.debug=(...e)=>s("debug",t,e),o}("VIDEOJS"),$=l.createLogger,K=Object.prototype.toString;function U(t,s){q(t).forEach(e=>s(t[e],e))}function W(s,i,e=0){return q(s).reduce((e,t)=>i(e,s[t],t),e)}function n(e){return!!e&&"object"==typeof e}function G(e){return n(e)&&"[object Object]"===K.call(e)&&e.constructor===Object}function h(...e){const s={};return e.forEach(e=>{e&&U(e,(e,t)=>{G(e)?(G(s[t])||(s[t]={}),s[t]=h(s[t],e)):s[t]=e})}),s}function X(e={}){var t,s=[];for(const i in e)e.hasOwnProperty(i)&&(t=e[i],s.push(t));return s}function Y(t,s,i,e=!0){const r=e=>Object.defineProperty(t,s,{value:e,enumerable:!0,writable:!0});var n={configurable:!0,enumerable:!0,get(){var e=i();return r(e),e}};return e&&(n.set=r),Object.defineProperty(t,s,n)}var Q=Object.freeze({__proto__:null,each:U,reduce:W,isObject:n,isPlain:G,merge:h,values:X,defineLazyProperty:Y});let J=!1,Z=null,o=!1,ee,te=!1,se=!1,ie=!1,c=!1,re=null,ne=null,ae=null,oe=!1,le=!1,he=!1,ce=!1,de=!1,ue=!1,pe=!1;const ge=Boolean(fe()&&("ontouchstart"in window||window.navigator.maxTouchPoints||window.DocumentTouch&&window.document instanceof window.DocumentTouch));var ve,e=window.navigator&&window.navigator.userAgentData;if(e&&e.platform&&e.brands&&(o="Android"===e.platform,se=Boolean(e.brands.find(e=>"Microsoft Edge"===e.brand)),ie=Boolean(e.brands.find(e=>"Chromium"===e.brand)),c=!se&&ie,re=ne=(e.brands.find(e=>"Chromium"===e.brand)||{}).version||null,le="Windows"===e.platform),!ie){const N=window.navigator&&window.navigator.userAgent||"";J=/iPod/i.test(N),Z=(e=N.match(/OS (\d+)_/i))&&e[1]?e[1]:null,o=/Android/i.test(N),ee=(e=N.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i))?(xt=e[1]&&parseFloat(e[1]),ve=e[2]&&parseFloat(e[2]),xt&&ve?parseFloat(e[1]+"."+e[2]):xt||null):null,te=/Firefox/i.test(N),se=/Edg/i.test(N),ie=/Chrome/i.test(N)||/CriOS/i.test(N),c=!se&&ie,re=ne=(ve=N.match(/(Chrome|CriOS)\/(\d+)/))&&ve[2]?parseFloat(ve[2]):null,ae=function(){var e=/MSIE\s(\d+)\.\d/.exec(N);let t=e&&parseFloat(e[1]);return t=!t&&/Trident\/7.0/i.test(N)&&/rv:11.0/.test(N)?11:t}(),de=/Tizen/i.test(N),ue=/Web0S/i.test(N),pe=de||ue,oe=/Safari/i.test(N)&&!c&&!o&&!se&&!pe,le=/Windows/i.test(N),he=/iPad/i.test(N)||oe&&ge&&!/iPhone/i.test(N),ce=/iPhone/i.test(N)&&!he}const u=ce||he||J,me=(oe||u)&&!c;var _e=Object.freeze({__proto__:null,get IS_IPOD(){return J},get IOS_VERSION(){return Z},get IS_ANDROID(){return o},get ANDROID_VERSION(){return ee},get IS_FIREFOX(){return te},get IS_EDGE(){return se},get IS_CHROMIUM(){return ie},get IS_CHROME(){return c},get CHROMIUM_VERSION(){return re},get CHROME_VERSION(){return ne},get IE_VERSION(){return ae},get IS_SAFARI(){return oe},get IS_WINDOWS(){return le},get IS_IPAD(){return he},get IS_IPHONE(){return ce},get IS_TIZEN(){return de},get IS_WEBOS(){return ue},get IS_SMART_TV(){return pe},TOUCH_ENABLED:ge,IS_IOS:u,IS_ANY_SAFARI:me});function ye(e){return"string"==typeof e&&Boolean(e.trim())}function fe(){return document===window.document}function be(e){return n(e)&&1===e.nodeType}function Te(){try{return window.parent!==window.self}catch(e){return!0}}function ke(s){return function(e,t){return ye(e)?(t=be(t=ye(t)?document.querySelector(t):t)?t:document)[s]&&t[s](e):document[s](null)}}function p(e="div",s={},t={},i){const r=document.createElement(e);return Object.getOwnPropertyNames(s).forEach(function(e){var t=s[e];"textContent"===e?Ce(r,t):r[e]===t&&"tabIndex"!==e||(r[e]=t)}),Object.getOwnPropertyNames(t).forEach(function(e){r.setAttribute(e,t[e])}),i&&Ve(r,i),r}function Ce(e,t){return"undefined"==typeof e.textContent?e.innerText=t:e.textContent=t,e}function we(e,t){t.firstChild?t.insertBefore(e,t.firstChild):t.appendChild(e)}function Ee(e,t){if(0<=t.indexOf(" "))throw new Error("class has illegal whitespace characters");return e.classList.contains(t)}function Se(e,...t){return e.classList.add(...t.reduce((e,t)=>e.concat(t.split(/\s+/)),[])),e}function xe(e,...t){return e?(e.classList.remove(...t.reduce((e,t)=>e.concat(t.split(/\s+/)),[])),e):(l.warn("removeClass was called with an element that doesn't exist"),null)}function je(t,e,s){return"boolean"!=typeof(s="function"==typeof s?s(t,e):s)&&(s=void 0),e.split(/\s+/).forEach(e=>t.classList.toggle(e,s)),t}function Pe(s,i){Object.getOwnPropertyNames(i).forEach(function(e){var t=i[e];null===t||"undefined"==typeof t||!1===t?s.removeAttribute(e):s.setAttribute(e,!0===t?"":t)})}function Ie(e){var s={},i=["autoplay","controls","playsinline","loop","muted","default","defaultMuted"];if(e&&e.attributes&&0{void 0!==t[e]&&(s[e]=t[e])}),s.height||(s.height=parseFloat(We(e,"height"))),s.width||(s.width=parseFloat(We(e,"width"))),s}}function Be(e){if(!e||!e.offsetParent)return{left:0,top:0,width:0,height:0};var t=e.offsetWidth,s=e.offsetHeight;let i=0,r=0;for(;e.offsetParent&&e!==document[F.fullscreenElement];)i+=e.offsetLeft,r+=e.offsetTop,e=e.offsetParent;return{left:i,top:r,width:t,height:s}}function Re(t,e){var s={x:0,y:0};if(u){let e=t;for(;e&&"html"!==e.nodeName.toLowerCase();){var i,r=We(e,"transform");/^matrix/.test(r)?(i=r.slice(7,-1).split(/,\s/).map(Number),s.x+=i[4],s.y+=i[5]):/^matrix3d/.test(r)&&(i=r.slice(9,-1).split(/,\s/).map(Number),s.x+=i[12],s.y+=i[13]),e=e.parentNode}}var n={},a=Be(e.target),t=Be(t),o=t.width,l=t.height;let h=e.offsetY-(t.top-a.top),c=e.offsetX-(t.left-a.left);return e.changedTouches&&(c=e.changedTouches[0].pageX-t.left,h=e.changedTouches[0].pageY+t.top,u)&&(c-=s.x,h-=s.y),n.y=1-Math.max(0,Math.min(1,h/l)),n.x=Math.max(0,Math.min(1,c/o)),n}function Fe(e){return n(e)&&3===e.nodeType}function He(e){for(;e.firstChild;)e.removeChild(e.firstChild);return e}function ze(e){return"function"==typeof e&&(e=e()),(Array.isArray(e)?e:[e]).map(e=>be(e="function"==typeof e?e():e)||Fe(e)?e:"string"==typeof e&&/\S/.test(e)?document.createTextNode(e):void 0).filter(e=>e)}function Ve(t,e){return ze(e).forEach(e=>t.appendChild(e)),t}function qe(e,t){return Ve(He(e),t)}function $e(e){return void 0===e.button&&void 0===e.buttons||0===e.button&&void 0===e.buttons||"mouseup"===e.type&&0===e.button&&0===e.buttons||0===e.button&&1===e.buttons}const Ke=ke("querySelector"),Ue=ke("querySelectorAll");function We(t,s){if(!t||!s)return"";if("function"!=typeof window.getComputedStyle)return"";{let e;try{e=window.getComputedStyle(t)}catch(e){return""}return e?e.getPropertyValue(s)||e[s]:""}}function Ge(i){[...document.styleSheets].forEach(t=>{try{var s=[...t.cssRules].map(e=>e.cssText).join(""),e=document.createElement("style");e.textContent=s,i.document.head.appendChild(e)}catch(e){s=document.createElement("link");s.rel="stylesheet",s.type=t.type,s.media=t.media.mediaText,s.href=t.href,i.document.head.appendChild(s)}})}var Xe=Object.freeze({__proto__:null,isReal:fe,isEl:be,isInFrame:Te,createEl:p,textContent:Ce,prependTo:we,hasClass:Ee,addClass:Se,removeClass:xe,toggleClass:je,setAttributes:Pe,getAttributes:Ie,getAttribute:Me,setAttribute:Oe,removeAttribute:Ae,blockTextSelection:Le,unblockTextSelection:Ne,getBoundingClientRect:De,findPosition:Be,getPointerPosition:Re,isTextNode:Fe,emptyEl:He,normalizeContent:ze,appendContent:Ve,insertContent:qe,isSingleLeftClick:$e,$:Ke,$$:Ue,computedStyle:We,copyStyleSheetsToWindow:Ge});let Ye=!1,Qe;function Je(){if(!1!==Qe.options.autoSetup){var e=Array.prototype.slice.call(document.getElementsByTagName("video")),t=Array.prototype.slice.call(document.getElementsByTagName("audio")),s=Array.prototype.slice.call(document.getElementsByTagName("video-js")),i=e.concat(t,s);if(i&&0=i&&(s(...e),r=t)}}function ut(i,r,n,a=window){let o;function e(){const e=this,t=arguments;let s=function(){o=null,s=null,n||i.apply(e,t)};!o&&n&&i.apply(e,t),a.clearTimeout(o),o=a.setTimeout(s,r)}return e.cancel=()=>{a.clearTimeout(o),o=null},e}e=Object.freeze({__proto__:null,UPDATE_REFRESH_INTERVAL:30,bind_:y,throttle:r,debounce:ut});let pt;class i{on(e,t){var s=this.addEventListener;this.addEventListener=()=>{},m(this,e,t),this.addEventListener=s}off(e,t){_(this,e,t)}one(e,t){var s=this.addEventListener;this.addEventListener=()=>{},ht(this,e,t),this.addEventListener=s}any(e,t){var s=this.addEventListener;this.addEventListener=()=>{},ct(this,e,t),this.addEventListener=s}trigger(e){var t=e.type||e;e=nt(e="string"==typeof e?{type:t}:e),this.allowedEvents_[t]&&this["on"+t]&&this["on"+t](e),lt(this,e)}queueTrigger(e){pt=pt||new Map;const t=e.type||e;let s=pt.get(this);s||(s=new Map,pt.set(this,s));var i=s.get(t),i=(s.delete(t),window.clearTimeout(i),window.setTimeout(()=>{s.delete(t),0===s.size&&(s=null,pt.delete(this)),this.trigger(e)},0));s.set(t,i)}}i.prototype.allowedEvents_={},i.prototype.addEventListener=i.prototype.on,i.prototype.removeEventListener=i.prototype.off,i.prototype.dispatchEvent=i.prototype.trigger;const gt=e=>"function"==typeof e.name?e.name():"string"==typeof e.name?e.name:e.name_||(e.constructor&&e.constructor.name?e.constructor.name:typeof e),a=t=>t instanceof i||!!t.eventBusEl_&&["on","one","off","trigger"].every(e=>"function"==typeof t[e]),vt=e=>"string"==typeof e&&/\S/.test(e)||Array.isArray(e)&&!!e.length,mt=(e,t,s)=>{if(!e||!e.nodeName&&!a(e))throw new Error(`Invalid target for ${gt(t)}#${s}; must be a DOM node or evented object.`)},_t=(e,t,s)=>{if(!vt(e))throw new Error(`Invalid event type for ${gt(t)}#${s}; must be a non-empty string or array.`)},yt=(e,t,s)=>{if("function"!=typeof e)throw new Error(`Invalid listener for ${gt(t)}#${s}; must be a function.`)},ft=(e,t,s)=>{var i=t.length<3||t[0]===e||t[0]===e.eventBusEl_;let r,n,a;return i?(r=e.eventBusEl_,3<=t.length&&t.shift(),[n,a]=t):[r,n,a]=t,mt(r,e,s),_t(n,e,s),yt(a,e,s),a=y(e,a),{isTargetingSelf:i,target:r,type:n,listener:a}},bt=(e,t,s,i)=>{mt(e,e,t),e.nodeName?dt[t](e,s,i):e[t](s,i)},Tt={on(...e){const{isTargetingSelf:t,target:s,type:i,listener:r}=ft(this,e,"on");if(bt(s,"on",i,r),!t){const n=()=>this.off(s,i,r);n.guid=r.guid;e=()=>this.off("dispose",n);e.guid=r.guid,bt(this,"on","dispose",n),bt(s,"on","dispose",e)}},one(...e){const{isTargetingSelf:t,target:s,type:i,listener:r}=ft(this,e,"one");if(t)bt(s,"one",i,r);else{const n=(...e)=>{this.off(s,i,n),r.apply(null,e)};n.guid=r.guid,bt(s,"one",i,n)}},any(...e){const{isTargetingSelf:t,target:s,type:i,listener:r}=ft(this,e,"any");if(t)bt(s,"any",i,r);else{const n=(...e)=>{this.off(s,i,n),r.apply(null,e)};n.guid=r.guid,bt(s,"any",i,n)}},off(e,t,s){!e||vt(e)?_(this.eventBusEl_,e,t):(e=e,t=t,mt(e,this,"off"),_t(t,this,"off"),yt(s,this,"off"),s=y(this,s),this.off("dispose",s),e.nodeName?(_(e,t,s),_(e,"dispose",s)):a(e)&&(e.off(t,s),e.off("dispose",s)))},trigger(e,t){mt(this.eventBusEl_,this,"trigger");var s=e&&"string"!=typeof e?e.type:e;if(vt(s))return lt(this.eventBusEl_,e,t);throw new Error(`Invalid event type for ${gt(this)}#trigger; `+"must be a non-empty string or object with a type key that has a non-empty value.")}};function kt(e,t={}){t=t.eventBusKey;if(t){if(!e[t].nodeName)throw new Error(`The eventBusKey "${t}" does not refer to an element.`);e.eventBusEl_=e[t]}else e.eventBusEl_=p("span",{className:"vjs-event-bus"});Object.assign(e,Tt),e.eventedCallbacks&&e.eventedCallbacks.forEach(e=>{e()}),e.on("dispose",()=>{e.off(),[e,e.el_,e.eventBusEl_].forEach(function(e){e&&g.has(e)&&g.delete(e)}),window.setTimeout(()=>{e.eventBusEl_=null},0)})}const Ct={state:{},setState(e){"function"==typeof e&&(e=e());let s;return U(e,(e,t)=>{this.state[t]!==e&&((s=s||{})[t]={from:this.state[t],to:e}),this.state[t]=e}),s&&a(this)&&this.trigger({changes:s,type:"statechanged"}),s}};function wt(e,t){Object.assign(e,Ct),e.state=Object.assign({},e.state,t),"function"==typeof e.handleStateChanged&&a(e)&&e.on("statechanged",e.handleStateChanged)}function Et(e){return"string"!=typeof e?e:e.replace(/./,e=>e.toLowerCase())}function f(e){return"string"!=typeof e?e:e.replace(/./,e=>e.toUpperCase())}function St(e,t){return f(e)===f(t)}var xt=Object.freeze({__proto__:null,toLowerCase:Et,toTitleCase:f,titleCaseEquals:St}),t="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function jt(e,t){return e(t={exports:{}},t.exports),t.exports}var b=jt(function(e,t){function s(e){var t;return"number"==typeof(e=e&&"object"==typeof e&&(t=e.which||e.keyCode||e.charCode)?t:e)?o[e]:(t=String(e),i[t.toLowerCase()]||r[t.toLowerCase()]||(1===t.length?t.charCodeAt(0):void 0))}s.isEventKey=function(e,t){if(e&&"object"==typeof e){e=e.which||e.keyCode||e.charCode;if(null!=e)if("string"==typeof t){var s=i[t.toLowerCase()];if(s)return s===e;if(s=r[t.toLowerCase()])return s===e}else if("number"==typeof t)return t===e;return!1}};for(var i=(t=e.exports=s).code=t.codes={backspace:8,tab:9,enter:13,shift:16,ctrl:17,alt:18,"pause/break":19,"caps lock":20,esc:27,space:32,"page up":33,"page down":34,end:35,home:36,left:37,up:38,right:39,down:40,insert:45,delete:46,command:91,"left command":91,"right command":93,"numpad *":106,"numpad +":107,"numpad -":109,"numpad .":110,"numpad /":111,"num lock":144,"scroll lock":145,"my computer":182,"my calculator":183,";":186,"=":187,",":188,"-":189,".":190,"/":191,"`":192,"[":219,"\\":220,"]":221,"'":222},r=t.aliases={windows:91,"⇧":16,"⌥":18,"⌃":17,"⌘":91,ctl:17,control:17,option:18,pause:19,break:19,caps:20,return:13,escape:27,spc:32,spacebar:32,pgup:33,pgdn:34,ins:45,del:46,cmd:91},n=97;n<123;n++)i[String.fromCharCode(n)]=n-32;for(var n=48;n<58;n++)i[n-48]=n;for(n=1;n<13;n++)i["f"+n]=n+111;for(n=0;n<10;n++)i["numpad "+n]=n+96;var a,o=t.names=t.title={};for(n in i)o[i[n]]=n;for(a in r)i[a]=r[a]});b.code,b.codes,b.aliases,b.names,b.title;class T{constructor(e,t,s){!e&&this.play?this.player_=e=this:this.player_=e,this.isDisposed_=!1,this.parentComponent_=null,this.options_=h({},this.options_),t=this.options_=h(this.options_,t),this.id_=t.id||t.el&&t.el.id,this.id_||(e=e&&e.id&&e.id()||"no_player",this.id_=e+"_component_"+v++),this.name_=t.name||null,t.el?this.el_=t.el:!1!==t.createEl&&(this.el_=this.createEl()),t.className&&this.el_&&t.className.split(" ").forEach(e=>this.addClass(e)),["on","off","one","any","trigger"].forEach(e=>{this[e]=void 0}),!1!==t.evented&&(kt(this,{eventBusKey:this.el_?"el_":null}),this.handleLanguagechange=this.handleLanguagechange.bind(this),this.on(this.player_,"languagechange",this.handleLanguagechange)),wt(this,this.constructor.defaultState),this.children_=[],this.childIndex_={},this.childNameIndex_={},this.setTimeoutIds_=new Set,this.setIntervalIds_=new Set,this.rafIds_=new Set,this.namedRafs_=new Map,(this.clearingTimersOnDispose_=!1)!==t.initChildren&&this.initChildren(),this.ready(s),!1!==t.reportTouchActivity&&this.enableTouchActivity()}on(e,t){}off(e,t){}one(e,t){}any(e,t){}trigger(e,t){}dispose(e={}){if(!this.isDisposed_){if(this.readyQueue_&&(this.readyQueue_.length=0),this.trigger({type:"dispose",bubbles:!1}),this.isDisposed_=!0,this.children_)for(let e=this.children_.length-1;0<=e;e--)this.children_[e].dispose&&this.children_[e].dispose();this.children_=null,this.childIndex_=null,this.childNameIndex_=null,this.parentComponent_=null,this.el_&&(this.el_.parentNode&&(e.restoreEl?this.el_.parentNode.replaceChild(e.restoreEl,this.el_):this.el_.parentNode.removeChild(this.el_)),this.el_=null),this.player_=null}}isDisposed(){return Boolean(this.isDisposed_)}player(){return this.player_}options(e){return e&&(this.options_=h(this.options_,e)),this.options_}el(){return this.el_}createEl(e,t,s){return p(e,t,s)}localize(e,i,t=e){var s=this.player_.language&&this.player_.language(),r=this.player_.languages&&this.player_.languages(),n=r&&r[s],s=s&&s.split("-")[0],r=r&&r[s];let a=t;return n&&n[e]?a=n[e]:r&&r[e]&&(a=r[e]),a=i?a.replace(/\{(\d+)\}/g,function(e,t){t=i[t-1];let s="undefined"==typeof t?e:t;return s}):a}handleLanguagechange(){}contentEl(){return this.contentEl_||this.el_}id(){return this.id_}name(){return this.name_}children(){return this.children_}getChildById(e){return this.childIndex_[e]}getChild(e){if(e)return this.childNameIndex_[e]}getDescendant(...t){t=t.reduce((e,t)=>e.concat(t),[]);let s=this;for(let e=0;e{let t,s;return s="string"==typeof e?(t=e,i[t]||this.options_[t]||{}):(t=e.name,e),{name:t,opts:s}}).filter(e=>{e=T.getComponent(e.opts.componentClass||f(e.name));return e&&!t.isTech(e)}).forEach(e=>{var t=e.name;let s=e.opts;!1!==(s=void 0!==r[t]?r[t]:s)&&((s=!0===s?{}:s).playerOptions=this.options_.playerOptions,e=this.addChild(t,s))&&(this[t]=e)})}}buildCSSClass(){return""}ready(e,t=!1){e&&(this.isReady_?t?e.call(this):this.setTimeout(e,1):(this.readyQueue_=this.readyQueue_||[],this.readyQueue_.push(e)))}triggerReady(){this.isReady_=!0,this.setTimeout(function(){var e=this.readyQueue_;this.readyQueue_=[],e&&0{this.setTimeoutIds_.has(s)&&this.setTimeoutIds_.delete(s),e()},t),this.setTimeoutIds_.add(s),s}clearTimeout(e){return this.setTimeoutIds_.has(e)&&(this.setTimeoutIds_.delete(e),window.clearTimeout(e)),e}setInterval(e,t){e=y(this,e),this.clearTimersOnDispose_();e=window.setInterval(e,t);return this.setIntervalIds_.add(e),e}clearInterval(e){return this.setIntervalIds_.has(e)&&(this.setIntervalIds_.delete(e),window.clearInterval(e)),e}requestAnimationFrame(e){var t;return this.clearTimersOnDispose_(),e=y(this,e),t=window.requestAnimationFrame(()=>{this.rafIds_.has(t)&&this.rafIds_.delete(t),e()}),this.rafIds_.add(t),t}requestNamedAnimationFrame(e,t){var s;if(!this.namedRafs_.has(e))return this.clearTimersOnDispose_(),t=y(this,t),s=this.requestAnimationFrame(()=>{t(),this.namedRafs_.has(e)&&this.namedRafs_.delete(e)}),this.namedRafs_.set(e,s),e}cancelNamedAnimationFrame(e){this.namedRafs_.has(e)&&(this.cancelAnimationFrame(this.namedRafs_.get(e)),this.namedRafs_.delete(e))}cancelAnimationFrame(e){return this.rafIds_.has(e)&&(this.rafIds_.delete(e),window.cancelAnimationFrame(e)),e}clearTimersOnDispose_(){this.clearingTimersOnDispose_||(this.clearingTimersOnDispose_=!0,this.one("dispose",()=>{[["namedRafs_","cancelNamedAnimationFrame"],["rafIds_","cancelAnimationFrame"],["setTimeoutIds_","clearTimeout"],["setIntervalIds_","clearInterval"]].forEach(([e,s])=>{this[e].forEach((e,t)=>this[s](t))}),this.clearingTimersOnDispose_=!1}))}static registerComponent(t,e){if("string"!=typeof t||!t)throw new Error(`Illegal component name, "${t}"; must be a non-empty string.`);var s=T.getComponent("Tech"),s=s&&s.isTech(e),i=T===e||T.prototype.isPrototypeOf(e.prototype);if(s||!i){let e;throw e=s?"techs must be registered using Tech.registerTech()":"must be a Component subclass",new Error(`Illegal component, "${t}"; ${e}.`)}t=f(t),T.components_||(T.components_={});i=T.getComponent("Player");if("Player"===t&&i&&i.players){const r=i.players;s=Object.keys(r);if(r&&0r[e]).every(Boolean))throw new Error("Can not register Player component after player has been created.")}return T.components_[t]=e,T.components_[Et(t)]=e}static getComponent(e){if(e&&T.components_)return T.components_[e]}}function Pt(e,t,s,i){var r=i,n=s.length-1;if("number"!=typeof r||r<0||n(e||[]).values()),t}function k(e,t){return Array.isArray(e)?It(e):void 0===e||void 0===t?It():It([[e,t]])}T.registerComponent("Component",T);function Mt(e,t){e=e<0?0:e;let s=Math.floor(e%60),i=Math.floor(e/60%60),r=Math.floor(e/3600);var n=Math.floor(t/60%60),t=Math.floor(t/3600);return r=0<(r=!isNaN(e)&&e!==1/0?r:i=s="-")||0s&&(n=s),i+=n-r;return i/s}function C(e){if(e instanceof C)return e;"number"==typeof e?this.code=e:"string"==typeof e?this.message=e:n(e)&&("number"==typeof e.code&&(this.code=e.code),Object.assign(this,e)),this.message||(this.message=C.defaultMessages[this.code]||"")}C.prototype.code=0,C.prototype.message="",C.prototype.status=null,C.prototype.metadata=null,C.errorTypes=["MEDIA_ERR_CUSTOM","MEDIA_ERR_ABORTED","MEDIA_ERR_NETWORK","MEDIA_ERR_DECODE","MEDIA_ERR_SRC_NOT_SUPPORTED","MEDIA_ERR_ENCRYPTED"],C.defaultMessages={1:"You aborted the media playback",2:"A network error caused the media download to fail part-way.",3:"The media playback was aborted due to a corruption problem or because the media used features your browser did not support.",4:"The media could not be loaded, either because the server or network failed or because the format is not supported.",5:"The media is encrypted and we do not have the keys to decrypt it."},C.MEDIA_ERR_CUSTOM=0,C.prototype.MEDIA_ERR_CUSTOM=0,C.MEDIA_ERR_ABORTED=1,C.prototype.MEDIA_ERR_ABORTED=1,C.MEDIA_ERR_NETWORK=2,C.prototype.MEDIA_ERR_NETWORK=2,C.MEDIA_ERR_DECODE=3,C.prototype.MEDIA_ERR_DECODE=3,C.MEDIA_ERR_SRC_NOT_SUPPORTED=4,C.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED=4,C.MEDIA_ERR_ENCRYPTED=5,C.prototype.MEDIA_ERR_ENCRYPTED=5;var Rt=function(e,t){var s,i=null;try{s=JSON.parse(e,t)}catch(e){i=e}return[i,s]};function Ft(e){return null!=e&&"function"==typeof e.then}function w(e){Ft(e)&&e.then(null,e=>{})}function Ht(i){return["kind","label","language","id","inBandMetadataTrackDispatchType","mode","src"].reduce((e,t,s)=>(i[t]&&(e[t]=i[t]),e),{cues:i.cues&&Array.prototype.map.call(i.cues,function(e){return{startTime:e.startTime,endTime:e.endTime,text:e.text,id:e.id}})})}var zt=function(e){var t=e.$$("track");const s=Array.prototype.map.call(t,e=>e.track);return Array.prototype.map.call(t,function(e){var t=Ht(e.track);return e.src&&(t.src=e.src),t}).concat(Array.prototype.filter.call(e.textTracks(),function(e){return-1===s.indexOf(e)}).map(Ht))},Vt=function(e,s){return e.forEach(function(e){const t=s.addRemoteTextTrack(e).track;!e.src&&e.cues&&e.cues.forEach(e=>t.addCue(e))}),s.textTracks()};Ht;const qt="vjs-modal-dialog";class $t extends T{constructor(e,t){super(e,t),this.handleKeyDown_=e=>this.handleKeyDown(e),this.close_=e=>this.close(e),this.opened_=this.hasBeenOpened_=this.hasBeenFilled_=!1,this.closeable(!this.options_.uncloseable),this.content(this.options_.content),this.contentEl_=p("div",{className:qt+"-content"},{role:"document"}),this.descEl_=p("p",{className:qt+"-description vjs-control-text",id:this.el().getAttribute("aria-describedby")}),Ce(this.descEl_,this.description()),this.el_.appendChild(this.descEl_),this.el_.appendChild(this.contentEl_)}createEl(){return super.createEl("div",{className:this.buildCSSClass(),tabIndex:-1},{"aria-describedby":this.id()+"_description","aria-hidden":"true","aria-label":this.label(),role:"dialog","aria-live":"polite"})}dispose(){this.contentEl_=null,this.descEl_=null,this.previouslyActiveEl_=null,super.dispose()}buildCSSClass(){return qt+" vjs-hidden "+super.buildCSSClass()}label(){return this.localize(this.options_.label||"Modal Window")}description(){let e=this.options_.description||this.localize("This is a modal window.");return this.closeable()&&(e+=" "+this.localize("This modal can be closed by pressing the Escape key or activating the close button.")),e}open(){var e;this.opened_?this.options_.fillAlways&&this.fill():(e=this.player(),this.trigger("beforemodalopen"),this.opened_=!0,!this.options_.fillAlways&&(this.hasBeenOpened_||this.hasBeenFilled_)||this.fill(),this.wasPlaying_=!e.paused(),this.options_.pauseOnOpen&&this.wasPlaying_&&e.pause(),this.on("keydown",this.handleKeyDown_),this.hadControls_=e.controls(),e.controls(!1),this.show(),this.conditionalFocus_(),this.el().setAttribute("aria-hidden","false"),this.trigger("modalopen"),this.hasBeenOpened_=!0)}opened(e){return"boolean"==typeof e&&this[e?"open":"close"](),this.opened_}close(){var e;this.opened_&&(e=this.player(),this.trigger("beforemodalclose"),this.opened_=!1,this.wasPlaying_&&this.options_.pauseOnOpen&&e.play(),this.off("keydown",this.handleKeyDown_),this.hadControls_&&e.controls(!0),this.hide(),this.el().setAttribute("aria-hidden","true"),this.trigger("modalclose"),this.conditionalBlur_(),this.options_.temporary)&&this.dispose()}closeable(t){if("boolean"==typeof t){var s,t=this.closeable_=!!t;let e=this.getChild("closeButton");t&&!e&&(s=this.contentEl_,this.contentEl_=this.el_,e=this.addChild("closeButton",{controlText:"Close Modal Dialog"}),this.contentEl_=s,this.on(e,"close",this.close_)),!t&&e&&(this.off(e,"close",this.close_),this.removeChild(e),e.dispose())}return this.closeable_}fill(){this.fillWith(this.content())}fillWith(e){var t=this.contentEl(),s=t.parentNode,i=t.nextSibling,e=(this.trigger("beforemodalfill"),this.hasBeenFilled_=!0,s.removeChild(t),this.empty(),qe(t,e),this.trigger("modalfill"),i?s.insertBefore(t,i):s.appendChild(t),this.getChild("closeButton"));e&&s.appendChild(e.el_)}empty(){this.trigger("beforemodalempty"),He(this.contentEl()),this.trigger("modalempty")}content(e){return"undefined"!=typeof e&&(this.content_=e),this.content_}conditionalFocus_(){var e=document.activeElement,t=this.player_.el_;this.previouslyActiveEl_=null,!t.contains(e)&&t!==e||(this.previouslyActiveEl_=e,this.focus())}conditionalBlur_(){this.previouslyActiveEl_&&(this.previouslyActiveEl_.focus(),this.previouslyActiveEl_=null)}handleKeyDown(e){if(e.stopPropagation(),b.isEventKey(e,"Escape")&&this.closeable())e.preventDefault(),this.close();else if(b.isEventKey(e,"Tab")){var s=this.focusableEls_(),i=this.el_.querySelector(":focus");let t;for(let e=0;e(e instanceof window.HTMLAnchorElement||e instanceof window.HTMLAreaElement)&&e.hasAttribute("href")||(e instanceof window.HTMLInputElement||e instanceof window.HTMLSelectElement||e instanceof window.HTMLTextAreaElement||e instanceof window.HTMLButtonElement)&&!e.hasAttribute("disabled")||e instanceof window.HTMLIFrameElement||e instanceof window.HTMLObjectElement||e instanceof window.HTMLEmbedElement||e.hasAttribute("tabindex")&&-1!==e.getAttribute("tabindex")||e.hasAttribute("contenteditable"))}}$t.prototype.options_={pauseOnOpen:!0,temporary:!0},T.registerComponent("ModalDialog",$t);class Kt extends i{constructor(t=[]){super(),this.tracks_=[],Object.defineProperty(this,"length",{get(){return this.tracks_.length}});for(let e=0;e{this.trigger({track:e,type:"labelchange",target:this})},a(e)&&e.addEventListener("labelchange",e.labelchange_)}removeTrack(s){let i;for(let e=0,t=this.length;ethis.queueTrigger("change")),this.triggerSelectedlanguagechange||(this.triggerSelectedlanguagechange_=()=>this.trigger("selectedlanguagechange")),e.addEventListener("modechange",this.queueChange_);-1===["metadata","chapters"].indexOf(e.kind)&&e.addEventListener("modechange",this.triggerSelectedlanguagechange_)}removeTrack(e){super.removeTrack(e),e.removeEventListener&&(this.queueChange_&&e.removeEventListener("modechange",this.queueChange_),this.selectedlanguagechange_)&&e.removeEventListener("modechange",this.triggerSelectedlanguagechange_)}}class Xt{constructor(e){Xt.prototype.setCues_.call(this,e),Object.defineProperty(this,"length",{get(){return this.length_}})}setCues_(e){var t=this.length||0;let s=0;function i(e){""+e in this||Object.defineProperty(this,""+e,{get(){return this.cues_[e]}})}var r=e.length;this.cues_=e,this.length_=e.length;if(tl.error(e)),window.console)&&window.console.groupEnd&&window.console.groupEnd(),s.flush()}function ys(e,i){var t={uri:e};(e=is(e))&&(t.cors=e),(e="use-credentials"===i.tech_.crossOrigin())&&(t.withCredentials=e),ds(t,y(this,function(e,t,s){if(e)return l.error(e,t);i.loaded_=!0,"function"!=typeof window.WebVTT?i.tech_&&i.tech_.any(["vttjsloaded","vttjserror"],e=>{if("vttjserror"!==e.type)return _s(s,i);l.error("vttjs failed to load, stopping trying to process "+i.src)}):_s(s,i)}))}class fs extends es{constructor(e={}){if(!e.tech)throw new Error("A tech was not provided.");e=h(e,{kind:Jt[e.kind]||"subtitles",language:e.language||e.srclang||""});let t=Zt[e.mode]||"disabled";const s=e.default,i=("metadata"!==e.kind&&"chapters"!==e.kind||(t="hidden"),super(e),this.tech_=e.tech,this.cues_=[],this.activeCues_=[],this.preload_=!1!==this.tech_.preloadTextTracks,new Xt(this.cues_)),n=new Xt(this.activeCues_);let a=!1;this.timeupdateHandler=y(this,function(e={}){this.tech_.isDisposed()||(this.tech_.isReady_&&(this.activeCues=this.activeCues,a)&&(this.trigger("cuechange"),a=!1),"timeupdate"!==e.type&&(this.rvf_=this.tech_.requestVideoFrameCallback(this.timeupdateHandler)))});this.tech_.one("dispose",()=>{this.stopTracking()}),"disabled"!==t&&this.startTracking(),Object.defineProperties(this,{default:{get(){return s},set(){}},mode:{get(){return t},set(e){Zt[e]&&t!==e&&(t=e,this.preload_||"disabled"===t||0!==this.cues.length||ys(this.src,this),this.stopTracking(),"disabled"!==t&&this.startTracking(),this.trigger("modechange"))}},cues:{get(){return this.loaded_?i:null},set(){}},activeCues:{get(){if(!this.loaded_)return null;if(0!==this.cues.length){var s=this.tech_.currentTime(),i=[];for(let e=0,t=this.cues.length;e=s&&i.push(r)}if(a=!1,i.length!==this.activeCues_.length)a=!0;else for(let e=0;e{t=ks.LOADED,this.trigger({type:"load",target:this})})}}ks.prototype.allowedEvents_={load:"load"},ks.NONE=0,ks.LOADING=1,ks.LOADED=2,ks.ERROR=3;const S={audio:{ListClass:class extends Kt{constructor(t=[]){for(let e=t.length-1;0<=e;e--)if(t[e].enabled){Ut(t,t[e]);break}super(t),this.changing_=!1}addTrack(e){e.enabled&&Ut(this,e),super.addTrack(e),e.addEventListener&&(e.enabledChange_=()=>{this.changing_||(this.changing_=!0,Ut(this,e),this.changing_=!1,this.trigger("change"))},e.addEventListener("enabledchange",e.enabledChange_))}removeTrack(e){super.removeTrack(e),e.removeEventListener&&e.enabledChange_&&(e.removeEventListener("enabledchange",e.enabledChange_),e.enabledChange_=null)}},TrackClass:bs,capitalName:"Audio"},video:{ListClass:class extends Kt{constructor(t=[]){for(let e=t.length-1;0<=e;e--)if(t[e].selected){Wt(t,t[e]);break}super(t),this.changing_=!1,Object.defineProperty(this,"selectedIndex",{get(){for(let e=0;e{this.changing_||(this.changing_=!0,Wt(this,e),this.changing_=!1,this.trigger("change"))},e.addEventListener("selectedchange",e.selectedChange_))}removeTrack(e){super.removeTrack(e),e.removeEventListener&&e.selectedChange_&&(e.removeEventListener("selectedchange",e.selectedChange_),e.selectedChange_=null)}},TrackClass:Ts,capitalName:"Video"},text:{ListClass:Gt,TrackClass:fs,capitalName:"Text"}},Cs=(Object.keys(S).forEach(function(e){S[e].getterName=e+"Tracks",S[e].privateName=e+"Tracks_"}),{remoteText:{ListClass:Gt,TrackClass:fs,capitalName:"RemoteText",getterName:"remoteTextTracks",privateName:"remoteTextTracks_"},remoteTextEl:{ListClass:class{constructor(s=[]){this.trackElements_=[],Object.defineProperty(this,"length",{get(){return this.trackElements_.length}});for(let e=0,t=s.length;ethis.onDurationChange(e),this.trackProgress_=e=>this.trackProgress(e),this.trackCurrentTime_=e=>this.trackCurrentTime(e),this.stopTrackingCurrentTime_=e=>this.stopTrackingCurrentTime(e),this.disposeSourceHandler_=e=>this.disposeSourceHandler(e),this.queuedHanders_=new Set,this.hasStarted_=!1,this.on("playing",function(){this.hasStarted_=!0}),this.on("loadstart",function(){this.hasStarted_=!1}),x.names.forEach(e=>{e=x[e];t&&t[e.getterName]&&(this[e.privateName]=t[e.getterName])}),this.featuresProgressEvents||this.manualProgressOn(),this.featuresTimeupdateEvents||this.manualTimeUpdatesOn(),["Text","Audio","Video"].forEach(e=>{!1===t[`native${e}Tracks`]&&(this[`featuresNative${e}Tracks`]=!1)}),!1===t.nativeCaptions||!1===t.nativeTextTracks?this.featuresNativeTextTracks=!1:!0!==t.nativeCaptions&&!0!==t.nativeTextTracks||(this.featuresNativeTextTracks=!0),this.featuresNativeTextTracks||this.emulateTextTracks(),this.preloadTextTracks=!1!==t.preloadTextTracks,this.autoRemoteTextTracks_=new x.text.ListClass,this.initTrackListeners(),t.nativeControlsForTouch||this.emitTapEvents(),this.constructor&&(this.name_=this.constructor.name||"Unknown Tech")}triggerSourceset(e){this.isReady_||this.one("ready",()=>this.setTimeout(()=>this.triggerSourceset(e),1)),this.trigger({src:e,type:"sourceset"})}manualProgressOn(){this.on("durationchange",this.onDurationChange_),this.manualProgress=!0,this.one("ready",this.trackProgress_)}manualProgressOff(){this.manualProgress=!1,this.stopTrackingProgress(),this.off("durationchange",this.onDurationChange_)}trackProgress(e){this.stopTrackingProgress(),this.progressInterval=this.setInterval(y(this,function(){var e=this.bufferedPercent();this.bufferedPercent_!==e&&this.trigger("progress"),1===(this.bufferedPercent_=e)&&this.stopTrackingProgress()}),500)}onDurationChange(e){this.duration_=this.duration()}buffered(){return k(0,0)}bufferedPercent(){return Bt(this.buffered(),this.duration_)}stopTrackingProgress(){this.clearInterval(this.progressInterval)}manualTimeUpdatesOn(){this.manualTimeUpdates=!0,this.on("play",this.trackCurrentTime_),this.on("pause",this.stopTrackingCurrentTime_)}manualTimeUpdatesOff(){this.manualTimeUpdates=!1,this.stopTrackingCurrentTime(),this.off("play",this.trackCurrentTime_),this.off("pause",this.stopTrackingCurrentTime_)}trackCurrentTime(){this.currentTimeInterval&&this.stopTrackingCurrentTime(),this.currentTimeInterval=this.setInterval(function(){this.trigger({type:"timeupdate",target:this,manuallyTriggered:!0})},250)}stopTrackingCurrentTime(){this.clearInterval(this.currentTimeInterval),this.trigger({type:"timeupdate",target:this,manuallyTriggered:!0})}dispose(){this.clearTracks(S.names),this.manualProgress&&this.manualProgressOff(),this.manualTimeUpdates&&this.manualTimeUpdatesOff(),super.dispose()}clearTracks(e){(e=[].concat(e)).forEach(e=>{var t=this[e+"Tracks"]()||[];let s=t.length;for(;s--;){var i=t[s];"text"===e&&this.removeRemoteTextTrack(i),t.removeTrack(i)}})}cleanupAutoTextTracks(){var e=this.autoRemoteTextTracks_||[];let t=e.length;for(;t--;){var s=e[t];this.removeRemoteTextTrack(s)}}reset(){}crossOrigin(){}setCrossOrigin(){}error(e){return void 0!==e&&(this.error_=new C(e),this.trigger("error")),this.error_}played(){return this.hasStarted_?k(0,0):k()}play(){}setScrubbing(e){}scrubbing(){}setCurrentTime(e){this.manualTimeUpdates&&this.trigger({type:"timeupdate",target:this,manuallyTriggered:!0})}initTrackListeners(){S.names.forEach(e=>{var t=S[e];const s=()=>{this.trigger(e+"trackchange")},i=this[t.getterName]();i.addEventListener("removetrack",s),i.addEventListener("addtrack",s),this.on("dispose",()=>{i.removeEventListener("removetrack",s),i.removeEventListener("addtrack",s)})})}addWebVttScript_(){if(!window.WebVTT)if(document.body.contains(this.el()))if(!this.options_["vtt.js"]&&G(ws)&&0{this.trigger("vttjsloaded")},e.onerror=()=>{this.trigger("vttjserror")},this.on("dispose",()=>{e.onload=null,e.onerror=null}),window.WebVTT=!0,this.el().parentNode.appendChild(e)}else this.ready(this.addWebVttScript_)}emulateTextTracks(){const s=this.textTracks(),e=this.remoteTextTracks(),t=e=>s.addTrack(e.track),i=e=>s.removeTrack(e.track),r=(e.on("addtrack",t),e.on("removetrack",i),this.addWebVttScript_(),()=>this.trigger("texttrackchange")),n=()=>{r();for(let e=0;ethis.autoRemoteTextTracks_.addTrack(s.track)),s}removeRemoteTextTrack(e){var t=this.remoteTextTrackEls().getTrackElementByTrack_(e);this.remoteTextTrackEls().removeTrackElement_(t),this.remoteTextTracks().removeTrack(e),this.autoRemoteTextTracks_.removeTrack(e)}getVideoPlaybackQuality(){return{}}requestPictureInPicture(){return Promise.reject()}disablePictureInPicture(){return!0}setDisablePictureInPicture(){}requestVideoFrameCallback(e){const t=v++;return!this.isReady_||this.paused()?(this.queuedHanders_.add(t),this.one("playing",()=>{this.queuedHanders_.has(t)&&(this.queuedHanders_.delete(t),e())})):this.requestNamedAnimationFrame(t,e),t}cancelVideoFrameCallback(e){this.queuedHanders_.has(e)?this.queuedHanders_.delete(e):this.cancelNamedAnimationFrame(e)}setPoster(){}playsinline(){}setPlaysinline(){}overrideNativeAudioTracks(e){}overrideNativeVideoTracks(e){}canPlayType(e){return""}static canPlayType(e){return""}static canPlaySource(e,t){return j.canPlayType(e.type)}static isTech(e){return e.prototype instanceof j||e instanceof j||e===j}static registerTech(e,t){if(j.techs_||(j.techs_={}),!j.isTech(t))throw new Error(`Tech ${e} must be a Tech`);if(!j.canPlayType)throw new Error("Techs must have a static canPlayType method on them");if(j.canPlaySource)return e=f(e),j.techs_[e]=t,j.techs_[Et(e)]=t,"Tech"!==e&&j.defaultTechOrder_.push(e),t;throw new Error("Techs must have a static canPlaySource method on them")}static getTech(e){if(e)return j.techs_&&j.techs_[e]?j.techs_[e]:(e=f(e),window&&window.videojs&&window.videojs[e]?(l.warn(`The ${e} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`),window.videojs[e]):void 0)}}x.names.forEach(function(e){const t=x[e];j.prototype[t.getterName]=function(){return this[t.privateName]=this[t.privateName]||new t.ListClass,this[t.privateName]}}),j.prototype.featuresVolumeControl=!0,j.prototype.featuresMuteControl=!0,j.prototype.featuresFullscreenResize=!1,j.prototype.featuresPlaybackRate=!1,j.prototype.featuresProgressEvents=!1,j.prototype.featuresSourceset=!1,j.prototype.featuresTimeupdateEvents=!1,j.prototype.featuresNativeTextTracks=!1,j.prototype.featuresVideoFrameCallback=!1,j.withSourceHandlers=function(r){r.registerSourceHandler=function(e,t){let s=r.sourceHandlers;s=s||(r.sourceHandlers=[]),void 0===t&&(t=s.length),s.splice(t,0,e)},r.canPlayType=function(t){var s,i=r.sourceHandlers||[];for(let e=0;efunction s(i={},e=[],r,n,a=[],o=!1){const[t,...l]=e;if("string"==typeof t)s(i,Es[t],r,n,a,o);else if(t){const h=Ls(n,t);if(!h.setSource)return a.push(h),s(i,l,r,n,a,o);h.setSource(Object.assign({},i),function(e,t){if(e)return s(i,l,r,n,a,o);a.push(h),s(t,i.type===t.type?l:Es[t.type],r,n,a,o)})}else l.length?s(i,l,r,n,a,o):o?r(i,a):s(i,Es["*"],r,n,a,!0)}(t,Es[t.type],s,e),1)}function Ps(e,t,s,i=null){var r="call"+f(s),r=e.reduce(As(r),i),i=r===xs,t=i?null:t[s](r),n=e,a=s,o=t,l=i;for(let e=n.length-1;0<=e;e--){var h=n[e];h[a]&&h[a](l,o)}return t}const Is={buffered:1,currentTime:1,duration:1,muted:1,played:1,paused:1,seekable:1,volume:1,ended:1},Ms={setCurrentTime:1,setMuted:1,setVolume:1},Os={play:1,pause:1};function As(s){return(e,t)=>e===xs?xs:t[s]?t[s](e):e}function Ls(e,t){var s=Ss[e.id()];let i=null;if(null==s)i=t(e),Ss[e.id()]=[[t,i]];else{for(let e=0;ethis.handleMouseOver(e),this.handleMouseOut_=e=>this.handleMouseOut(e),this.handleClick_=e=>this.handleClick(e),this.handleKeyDown_=e=>this.handleKeyDown(e),this.emitTapEvents(),this.enable()}createEl(e="div",t={},s={}){t=Object.assign({className:this.buildCSSClass(),tabIndex:0},t),"button"===e&&l.error(`Creating a ClickableComponent with an HTML element of ${e} is not supported; use a Button instead.`),s=Object.assign({role:"button"},s),this.tabIndex_=t.tabIndex;e=p(e,t,s);return this.player_.options_.experimentalSvgIcons||e.appendChild(p("span",{className:"vjs-icon-placeholder"},{"aria-hidden":!0})),this.createControlTextEl(e),e}dispose(){this.controlTextEl_=null,super.dispose()}createControlTextEl(e){return this.controlTextEl_=p("span",{className:"vjs-control-text"},{"aria-live":"polite"}),e&&e.appendChild(this.controlTextEl_),this.controlText(this.controlText_,e),this.controlTextEl_}controlText(e,t=this.el()){if(void 0===e)return this.controlText_||"Need Text";var s=this.localize(e);this.controlText_=e,Ce(this.controlTextEl_,s),this.nonIconControl||this.player_.options_.noUITitleAttributes||t.setAttribute("title",s)}buildCSSClass(){return"vjs-control vjs-button "+super.buildCSSClass()}enable(){this.enabled_||(this.enabled_=!0,this.removeClass("vjs-disabled"),this.el_.setAttribute("aria-disabled","false"),"undefined"!=typeof this.tabIndex_&&this.el_.setAttribute("tabIndex",this.tabIndex_),this.on(["tap","click"],this.handleClick_),this.on("keydown",this.handleKeyDown_))}disable(){this.enabled_=!1,this.addClass("vjs-disabled"),this.el_.setAttribute("aria-disabled","true"),"undefined"!=typeof this.tabIndex_&&this.el_.removeAttribute("tabIndex"),this.off("mouseover",this.handleMouseOver_),this.off("mouseout",this.handleMouseOut_),this.off(["tap","click"],this.handleClick_),this.off("keydown",this.handleKeyDown_)}handleLanguagechange(){this.controlText(this.controlText_)}handleClick(e){this.options_.clickHandler&&this.options_.clickHandler.call(this,arguments)}handleKeyDown(e){b.isEventKey(e,"Space")||b.isEventKey(e,"Enter")?(e.preventDefault(),e.stopPropagation(),this.trigger("click")):super.handleKeyDown(e)}}T.registerComponent("ClickableComponent",Hs);class zs extends Hs{constructor(e,t){super(e,t),this.update(),this.update_=e=>this.update(e),e.on("posterchange",this.update_)}dispose(){this.player().off("posterchange",this.update_),super.dispose()}createEl(){return p("div",{className:"vjs-poster"})}crossOrigin(e){if("undefined"==typeof e)return this.$("img")?this.$("img").crossOrigin:this.player_.tech_&&this.player_.tech_.isReady_?this.player_.crossOrigin():this.player_.options_.crossOrigin||this.player_.options_.crossorigin||null;null!==e&&"anonymous"!==e&&"use-credentials"!==e?this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${e}"`):this.$("img")&&(this.$("img").crossOrigin=e)}update(e){var t=this.player().poster();this.setSrc(t),t?this.show():this.hide()}setSrc(e){e?(this.$("img")||this.el_.appendChild(p("picture",{className:"vjs-poster",tabIndex:-1},{},p("img",{loading:"lazy",crossOrigin:this.crossOrigin()},{alt:""}))),this.$("img").src=e):this.el_.textContent=""}handleClick(e){this.player_.controls()&&(this.player_.tech(!0)&&this.player_.tech(!0).focus(),this.player_.paused()?w(this.player_.play()):this.player_.pause())}}zs.prototype.crossorigin=zs.prototype.crossOrigin,T.registerComponent("PosterImage",zs);const Vs={monospace:"monospace",sansSerif:"sans-serif",serif:"serif",monospaceSansSerif:'"Andale Mono", "Lucida Console", monospace',monospaceSerif:'"Courier New", monospace',proportionalSansSerif:"sans-serif",proportionalSerif:"serif",casual:'"Comic Sans MS", Impact, fantasy',script:'"Monotype Corsiva", cursive',smallcaps:'"Andale Mono", "Lucida Console", monospace, sans-serif'};function qs(e,t){let s;if(4===e.length)s=e[1]+e[1]+e[2]+e[2]+e[3]+e[3];else{if(7!==e.length)throw new Error("Invalid color code provided, "+e+"; must be formatted as e.g. #f0e or #f604e2.");s=e.slice(1)}return"rgba("+parseInt(s.slice(0,2),16)+","+parseInt(s.slice(2,4),16)+","+parseInt(s.slice(4,6),16)+","+t+")"}function $s(e,t,s){try{e.style[t]=s}catch(e){}}function Ks(e){return e?e+"px":""}class Us extends T{constructor(i,e,t){super(i,e,t);const r=e=>{this.updateDisplayOverlay(),this.updateDisplay(e)};i.on("loadstart",e=>this.toggleDisplay(e)),i.on("texttrackchange",e=>this.updateDisplay(e)),i.on("loadedmetadata",e=>{this.updateDisplayOverlay(),this.preselectTrack(e)}),i.ready(y(this,function(){if(i.tech_&&i.tech_.featuresNativeTextTracks)this.hide();else{i.on("fullscreenchange",r),i.on("playerresize",r);const e=window.screen.orientation||window,s=window.screen.orientation?"change":"orientationchange";e.addEventListener(s,r),i.on("dispose",()=>e.removeEventListener(s,r));var t=this.options_.playerOptions.tracks||[];for(let e=0;e!e.activeCues)){var t=[];for(let e=0;ethis.handleMouseDown(e))}buildCSSClass(){return"vjs-big-play-button"}handleClick(e){var t=this.player_.play();if(this.mouseused_&&"clientX"in e&&"clientY"in e)w(t),this.player_.tech(!0)&&this.player_.tech(!0).focus();else{var e=this.player_.getChild("controlBar");const s=e&&e.getChild("playToggle");s?(e=()=>s.focus(),Ft(t)?t.then(e,()=>{}):this.setTimeout(e,1)):this.player_.tech(!0).focus()}}handleKeyDown(e){this.mouseused_=!1,super.handleKeyDown(e)}handleMouseDown(e){this.mouseused_=!0}}Gs.prototype.controlText_="Play Video",T.registerComponent("BigPlayButton",Gs);P;T.registerComponent("CloseButton",class extends P{constructor(e,t){super(e,t),this.setIcon("cancel"),this.controlText(t&&t.controlText||this.localize("Close"))}buildCSSClass(){return"vjs-close-button "+super.buildCSSClass()}handleClick(e){this.trigger({type:"close",bubbles:!1})}handleKeyDown(e){b.isEventKey(e,"Esc")?(e.preventDefault(),e.stopPropagation(),this.trigger("click")):super.handleKeyDown(e)}});class Xs extends P{constructor(e,t={}){super(e,t),t.replay=void 0===t.replay||t.replay,this.setIcon("play"),this.on(e,"play",e=>this.handlePlay(e)),this.on(e,"pause",e=>this.handlePause(e)),t.replay&&this.on(e,"ended",e=>this.handleEnded(e))}buildCSSClass(){return"vjs-play-control "+super.buildCSSClass()}handleClick(e){this.player_.paused()?w(this.player_.play()):this.player_.pause()}handleSeeked(e){this.removeClass("vjs-ended"),this.player_.paused()?this.handlePause(e):this.handlePlay(e)}handlePlay(e){this.removeClass("vjs-ended","vjs-paused"),this.addClass("vjs-playing"),this.setIcon("pause"),this.controlText("Pause")}handlePause(e){this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.setIcon("play"),this.controlText("Play")}handleEnded(e){this.removeClass("vjs-playing"),this.addClass("vjs-ended"),this.setIcon("replay"),this.controlText("Replay"),this.one(this.player_,"seeked",e=>this.handleSeeked(e))}}Xs.prototype.controlText_="Play",T.registerComponent("PlayToggle",Xs);class Ys extends T{constructor(e,t){super(e,t),this.on(e,["timeupdate","ended","seeking"],e=>this.update(e)),this.updateTextNode_()}createEl(){var e=this.buildCSSClass(),t=super.createEl("div",{className:e+" vjs-time-control vjs-control"}),s=p("span",{className:"vjs-control-text",textContent:this.localize(this.labelText_)+" "},{role:"presentation"});return t.appendChild(s),this.contentEl_=p("span",{className:e+"-display"},{role:"presentation"}),t.appendChild(this.contentEl_),t}dispose(){this.contentEl_=null,this.textNode_=null,super.dispose()}update(e){!this.player_.options_.enableSmoothSeeking&&"seeking"===e.type||this.updateContent(e)}updateTextNode_(e=0){e=Nt(e),this.formattedTime_!==e&&(this.formattedTime_=e,this.requestNamedAnimationFrame("TimeDisplay#updateTextNode_",()=>{if(this.contentEl_){let e=this.textNode_;e&&this.contentEl_.firstChild!==e&&(e=null,l.warn("TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.")),this.textNode_=document.createTextNode(this.formattedTime_),this.textNode_&&(e?this.contentEl_.replaceChild(this.textNode_,e):this.contentEl_.appendChild(this.textNode_))}}))}updateContent(e){}}Ys.prototype.labelText_="Time",Ys.prototype.controlText_="Time",T.registerComponent("TimeDisplay",Ys);class Qs extends Ys{buildCSSClass(){return"vjs-current-time"}updateContent(e){let t;t=this.player_.ended()?this.player_.duration():this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime(),this.updateTextNode_(t)}}Qs.prototype.labelText_="Current Time",Qs.prototype.controlText_="Current Time",T.registerComponent("CurrentTimeDisplay",Qs);class Js extends Ys{constructor(e,t){super(e,t);t=e=>this.updateContent(e);this.on(e,"durationchange",t),this.on(e,"loadstart",t),this.on(e,"loadedmetadata",t)}buildCSSClass(){return"vjs-duration"}updateContent(e){var t=this.player_.duration();this.updateTextNode_(t)}}Js.prototype.labelText_="Duration",Js.prototype.controlText_="Duration",T.registerComponent("DurationDisplay",Js);class Zs extends T{createEl(){var e=super.createEl("div",{className:"vjs-time-control vjs-time-divider"},{"aria-hidden":!0}),t=super.createEl("div"),s=super.createEl("span",{textContent:"/"});return t.appendChild(s),e.appendChild(t),e}}T.registerComponent("TimeDivider",Zs);class ei extends Ys{constructor(e,t){super(e,t),this.on(e,"durationchange",e=>this.updateContent(e))}buildCSSClass(){return"vjs-remaining-time"}createEl(){var e=super.createEl();return!1!==this.options_.displayNegative&&e.insertBefore(p("span",{},{"aria-hidden":!0},"-"),this.contentEl_),e}updateContent(e){if("number"==typeof this.player_.duration()){let e;e=this.player_.ended()?0:this.player_.remainingTimeDisplay?this.player_.remainingTimeDisplay():this.player_.remainingTime(),this.updateTextNode_(e)}}}ei.prototype.labelText_="Remaining Time",ei.prototype.controlText_="Remaining Time",T.registerComponent("RemainingTimeDisplay",ei);class ti extends T{constructor(e,t){super(e,t),this.updateShowing(),this.on(this.player(),"durationchange",e=>this.updateShowing(e))}createEl(){var e=super.createEl("div",{className:"vjs-live-control vjs-control"});return this.contentEl_=p("div",{className:"vjs-live-display"},{"aria-live":"off"}),this.contentEl_.appendChild(p("span",{className:"vjs-control-text",textContent:this.localize("Stream Type")+" "})),this.contentEl_.appendChild(document.createTextNode(this.localize("LIVE"))),e.appendChild(this.contentEl_),e}dispose(){this.contentEl_=null,super.dispose()}updateShowing(e){this.player().duration()===1/0?this.show():this.hide()}}T.registerComponent("LiveDisplay",ti);class si extends P{constructor(e,t){super(e,t),this.updateLiveEdgeStatus(),this.player_.liveTracker&&(this.updateLiveEdgeStatusHandler_=e=>this.updateLiveEdgeStatus(e),this.on(this.player_.liveTracker,"liveedgechange",this.updateLiveEdgeStatusHandler_))}createEl(){var e=super.createEl("button",{className:"vjs-seek-to-live-control vjs-control"});return this.setIcon("circle",e),this.textEl_=p("span",{className:"vjs-seek-to-live-text",textContent:this.localize("LIVE")},{"aria-hidden":"true"}),e.appendChild(this.textEl_),e}updateLiveEdgeStatus(){!this.player_.liveTracker||this.player_.liveTracker.atLiveEdge()?(this.setAttribute("aria-disabled",!0),this.addClass("vjs-at-live-edge"),this.controlText("Seek to live, currently playing live")):(this.setAttribute("aria-disabled",!1),this.removeClass("vjs-at-live-edge"),this.controlText("Seek to live, currently behind live"))}handleClick(){this.player_.liveTracker.seekToLiveEdge()}dispose(){this.player_.liveTracker&&this.off(this.player_.liveTracker,"liveedgechange",this.updateLiveEdgeStatusHandler_),this.textEl_=null,super.dispose()}}function ii(e,t,s){return e=Number(e),Math.min(s,Math.max(t,isNaN(e)?t:e))}si.prototype.controlText_="Seek to live, currently playing live",T.registerComponent("SeekToLive",si);t=Object.freeze({__proto__:null,clamp:ii});class ri extends T{constructor(e,t){super(e,t),this.handleMouseDown_=e=>this.handleMouseDown(e),this.handleMouseUp_=e=>this.handleMouseUp(e),this.handleKeyDown_=e=>this.handleKeyDown(e),this.handleClick_=e=>this.handleClick(e),this.handleMouseMove_=e=>this.handleMouseMove(e),this.update_=e=>this.update(e),this.bar=this.getChild(this.options_.barName),this.vertical(!!this.options_.vertical),this.enable()}enabled(){return this.enabled_}enable(){this.enabled()||(this.on("mousedown",this.handleMouseDown_),this.on("touchstart",this.handleMouseDown_),this.on("keydown",this.handleKeyDown_),this.on("click",this.handleClick_),this.on(this.player_,"controlsvisible",this.update),this.playerEvent&&this.on(this.player_,this.playerEvent,this.update),this.removeClass("disabled"),this.setAttribute("tabindex",0),this.enabled_=!0)}disable(){var e;this.enabled()&&(e=this.bar.el_.ownerDocument,this.off("mousedown",this.handleMouseDown_),this.off("touchstart",this.handleMouseDown_),this.off("keydown",this.handleKeyDown_),this.off("click",this.handleClick_),this.off(this.player_,"controlsvisible",this.update_),this.off(e,"mousemove",this.handleMouseMove_),this.off(e,"mouseup",this.handleMouseUp_),this.off(e,"touchmove",this.handleMouseMove_),this.off(e,"touchend",this.handleMouseUp_),this.removeAttribute("tabindex"),this.addClass("disabled"),this.playerEvent&&this.off(this.player_,this.playerEvent,this.update),this.enabled_=!1)}createEl(e,t={},s={}){return t.className=t.className+" vjs-slider",t=Object.assign({tabIndex:0},t),s=Object.assign({role:"slider","aria-valuenow":0,"aria-valuemin":0,"aria-valuemax":100},s),super.createEl(e,t,s)}handleMouseDown(e){var t=this.bar.el_.ownerDocument;"mousedown"===e.type&&e.preventDefault(),"touchstart"!==e.type||c||e.preventDefault(),Le(),this.addClass("vjs-sliding"),this.trigger("slideractive"),this.on(t,"mousemove",this.handleMouseMove_),this.on(t,"mouseup",this.handleMouseUp_),this.on(t,"touchmove",this.handleMouseMove_),this.on(t,"touchend",this.handleMouseUp_),this.handleMouseMove(e,!0)}handleMouseMove(e){}handleMouseUp(e){var t=this.bar.el_.ownerDocument;Ne(),this.removeClass("vjs-sliding"),this.trigger("sliderinactive"),this.off(t,"mousemove",this.handleMouseMove_),this.off(t,"mouseup",this.handleMouseUp_),this.off(t,"touchmove",this.handleMouseMove_),this.off(t,"touchend",this.handleMouseUp_),this.update()}update(){if(this.el_&&this.bar){const t=this.getProgress();return t!==this.progress_&&(this.progress_=t,this.requestNamedAnimationFrame("Slider#update",()=>{var e=this.vertical()?"height":"width";this.bar.el().style[e]=(100*t).toFixed(2)+"%"})),t}}getProgress(){return Number(ii(this.getPercent(),0,1).toFixed(4))}calculateDistance(e){e=Re(this.el_,e);return this.vertical()?e.y:e.x}handleKeyDown(e){b.isEventKey(e,"Left")||b.isEventKey(e,"Down")?(e.preventDefault(),e.stopPropagation(),this.stepBack()):b.isEventKey(e,"Right")||b.isEventKey(e,"Up")?(e.preventDefault(),e.stopPropagation(),this.stepForward()):super.handleKeyDown(e)}handleClick(e){e.stopPropagation(),e.preventDefault()}vertical(e){if(void 0===e)return this.vertical_||!1;this.vertical_=!!e,this.vertical_?this.addClass("vjs-slider-vertical"):this.addClass("vjs-slider-horizontal")}}T.registerComponent("Slider",ri);const ni=(e,t)=>ii(e/t*100,0,100).toFixed(2)+"%";class ai extends T{constructor(e,t){super(e,t),this.partEls_=[],this.on(e,"progress",e=>this.update(e))}createEl(){var e=super.createEl("div",{className:"vjs-load-progress"}),t=p("span",{className:"vjs-control-text"}),s=p("span",{textContent:this.localize("Loaded")}),i=document.createTextNode(": ");return this.percentageEl_=p("span",{className:"vjs-control-text-loaded-percentage",textContent:"0%"}),e.appendChild(t),t.appendChild(s),t.appendChild(i),t.appendChild(this.percentageEl_),e}dispose(){this.partEls_=null,this.percentageEl_=null,super.dispose()}update(e){this.requestNamedAnimationFrame("LoadProgressBar#update",()=>{var e=this.player_.liveTracker,s=this.player_.buffered(),e=e&&e.isLive()?e.seekableEnd():this.player_.duration(),i=this.player_.bufferedEnd(),r=this.partEls_,e=ni(i,e);this.percent_!==e&&(this.el_.style.width=e,Ce(this.percentageEl_,e),this.percent_=e);for(let t=0;ts.length;e--)this.el_.removeChild(r[e-1]);r.length=s.length})}}T.registerComponent("LoadProgressBar",ai);class oi extends T{constructor(e,t){super(e,t),this.update=r(y(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-time-tooltip"},{"aria-hidden":"true"})}update(i,r,n){var a=Be(this.el_),o=De(this.player_.el()),r=i.width*r;if(o&&a){let e=i.left-o.left+r,t=i.width-r+(o.right-i.right),s=(t||(t=i.width-r,e=r),a.width/2);ea.width&&(s=a.width),s=Math.round(s),this.el_.style.right=`-${s}px`,this.write(n)}}write(e){Ce(this.el_,e)}updateTime(r,n,a,o){this.requestNamedAnimationFrame("TimeTooltip#updateTime",()=>{let e;var t,s,i=this.player_.duration();e=this.player_.liveTracker&&this.player_.liveTracker.isLive()?((s=(t=this.player_.liveTracker.liveWindow())-n*t)<1?"":"-")+Nt(s,t):Nt(a,i),this.update(r,n,e),o&&o()})}}T.registerComponent("TimeTooltip",oi);class li extends T{constructor(e,t){super(e,t),this.setIcon("circle"),this.update=r(y(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-play-progress vjs-slider-bar"},{"aria-hidden":"true"})}update(e,t){var s,i=this.getChild("timeTooltip");i&&(s=this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime(),i.updateTime(e,t,s))}}li.prototype.options_={children:[]},u||o||li.prototype.options_.children.push("timeTooltip"),T.registerComponent("PlayProgressBar",li);class hi extends T{constructor(e,t){super(e,t),this.update=r(y(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-mouse-display"})}update(e,t){var s=t*this.player_.duration();this.getChild("timeTooltip").updateTime(e,t,s,()=>{this.el_.style.left=e.width*t+"px"})}}hi.prototype.options_={children:["timeTooltip"]},T.registerComponent("MouseTimeDisplay",hi);class ci extends ri{constructor(e,t){super(e,t),this.setEventHandlers_()}setEventHandlers_(){this.update_=y(this,this.update),this.update=r(this.update_,30),this.on(this.player_,["ended","durationchange","timeupdate"],this.update),this.player_.liveTracker&&this.on(this.player_.liveTracker,"liveedgechange",this.update),this.updateInterval=null,this.enableIntervalHandler_=e=>this.enableInterval_(e),this.disableIntervalHandler_=e=>this.disableInterval_(e),this.on(this.player_,["playing"],this.enableIntervalHandler_),this.on(this.player_,["ended","pause","waiting"],this.disableIntervalHandler_),"hidden"in document&&"visibilityState"in document&&this.on(document,"visibilitychange",this.toggleVisibility_)}toggleVisibility_(e){"hidden"===document.visibilityState?(this.cancelNamedAnimationFrame("SeekBar#update"),this.cancelNamedAnimationFrame("Slider#update"),this.disableInterval_(e)):(this.player_.ended()||this.player_.paused()||this.enableInterval_(),this.update())}enableInterval_(){this.updateInterval||(this.updateInterval=this.setInterval(this.update,30))}disableInterval_(e){this.player_.liveTracker&&this.player_.liveTracker.isLive()&&e&&"ended"!==e.type||this.updateInterval&&(this.clearInterval(this.updateInterval),this.updateInterval=null)}createEl(){return super.createEl("div",{className:"vjs-progress-holder"},{"aria-label":this.localize("Progress Bar")})}update(e){if("hidden"!==document.visibilityState){const i=super.update();return this.requestNamedAnimationFrame("SeekBar#update",()=>{var e=this.player_.ended()?this.player_.duration():this.getCurrentTime_(),t=this.player_.liveTracker;let s=this.player_.duration();t&&t.isLive()&&(s=this.player_.liveTracker.liveCurrentTime()),this.percent_!==i&&(this.el_.setAttribute("aria-valuenow",(100*i).toFixed(2)),this.percent_=i),this.currentTime_===e&&this.duration_===s||(this.el_.setAttribute("aria-valuetext",this.localize("progress bar timing: currentTime={1} duration={2}",[Nt(e,s),Nt(s,s)],"{1} of {2}")),this.currentTime_=e,this.duration_=s),this.bar&&this.bar.update(De(this.el()),this.getProgress())}),i}}userSeek_(e){this.player_.liveTracker&&this.player_.liveTracker.isLive()&&this.player_.liveTracker.nextSeekedFromUser(),this.player_.currentTime(e)}getCurrentTime_(){return this.player_.scrubbing()?this.player_.getCache().currentTime:this.player_.currentTime()}getPercent(){var e=this.getCurrentTime_();let t;var s=this.player_.liveTracker;return s&&s.isLive()?(t=(e-s.seekableStart())/s.liveWindow(),s.atLiveEdge()&&(t=1)):t=e/this.player_.duration(),t}handleMouseDown(e){$e(e)&&(e.stopPropagation(),this.videoWasPlaying=!this.player_.paused(),this.player_.pause(),super.handleMouseDown(e))}handleMouseMove(t,s=!1){if($e(t)&&!isNaN(this.player_.duration())){s||this.player_.scrubbing()||this.player_.scrubbing(!0);let e;s=this.calculateDistance(t),t=this.player_.liveTracker;if(t&&t.isLive()){if(.99<=s)return void t.seekToLiveEdge();var i=t.seekableStart(),r=t.liveCurrentTime();if((e=(e=(e=i+s*t.liveWindow())>=r?r:e)<=i?i+.1:e)===1/0)return}else(e=s*this.player_.duration())===this.player_.duration()&&(e-=.1);this.userSeek_(e),this.player_.options_.enableSmoothSeeking&&this.update()}}enable(){super.enable();var e=this.getChild("mouseTimeDisplay");e&&e.show()}disable(){super.disable();var e=this.getChild("mouseTimeDisplay");e&&e.hide()}handleMouseUp(e){super.handleMouseUp(e),e&&e.stopPropagation(),this.player_.scrubbing(!1),this.player_.trigger({type:"timeupdate",target:this,manuallyTriggered:!0}),this.videoWasPlaying?w(this.player_.play()):this.update_()}stepForward(){this.userSeek_(this.player_.currentTime()+5)}stepBack(){this.userSeek_(this.player_.currentTime()-5)}handleAction(e){this.player_.paused()?this.player_.play():this.player_.pause()}handleKeyDown(e){var t,s=this.player_.liveTracker;b.isEventKey(e,"Space")||b.isEventKey(e,"Enter")?(e.preventDefault(),e.stopPropagation(),this.handleAction(e)):b.isEventKey(e,"Home")?(e.preventDefault(),e.stopPropagation(),this.userSeek_(0)):b.isEventKey(e,"End")?(e.preventDefault(),e.stopPropagation(),s&&s.isLive()?this.userSeek_(s.liveCurrentTime()):this.userSeek_(this.player_.duration())):/^[0-9]$/.test(b(e))?(e.preventDefault(),e.stopPropagation(),t=10*(b.codes[b(e)]-b.codes[0])/100,s&&s.isLive()?this.userSeek_(s.seekableStart()+s.liveWindow()*t):this.userSeek_(this.player_.duration()*t)):b.isEventKey(e,"PgDn")?(e.preventDefault(),e.stopPropagation(),this.userSeek_(this.player_.currentTime()-60)):b.isEventKey(e,"PgUp")?(e.preventDefault(),e.stopPropagation(),this.userSeek_(this.player_.currentTime()+60)):super.handleKeyDown(e)}dispose(){this.disableInterval_(),this.off(this.player_,["ended","durationchange","timeupdate"],this.update),this.player_.liveTracker&&this.off(this.player_.liveTracker,"liveedgechange",this.update),this.off(this.player_,["playing"],this.enableIntervalHandler_),this.off(this.player_,["ended","pause","waiting"],this.disableIntervalHandler_),"hidden"in document&&"visibilityState"in document&&this.off(document,"visibilitychange",this.toggleVisibility_),super.dispose()}}ci.prototype.options_={children:["loadProgressBar","playProgressBar"],barName:"playProgressBar"},u||o||ci.prototype.options_.children.splice(1,0,"mouseTimeDisplay"),T.registerComponent("SeekBar",ci);class di extends T{constructor(e,t){super(e,t),this.handleMouseMove=r(y(this,this.handleMouseMove),30),this.throttledHandleMouseSeek=r(y(this,this.handleMouseSeek),30),this.handleMouseUpHandler_=e=>this.handleMouseUp(e),this.handleMouseDownHandler_=e=>this.handleMouseDown(e),this.enable()}createEl(){return super.createEl("div",{className:"vjs-progress-control vjs-control"})}handleMouseMove(e){var t,s,i,r,n=this.getChild("seekBar");n&&(t=n.getChild("playProgressBar"),s=n.getChild("mouseTimeDisplay"),t||s)&&(i=Be(r=n.el()),r=ii(r=Re(r,e).x,0,1),s&&s.update(i,r),t)&&t.update(i,n.getProgress())}handleMouseSeek(e){var t=this.getChild("seekBar");t&&t.handleMouseMove(e)}enabled(){return this.enabled_}disable(){var e;this.children().forEach(e=>e.disable&&e.disable()),this.enabled()&&(this.off(["mousedown","touchstart"],this.handleMouseDownHandler_),this.off(this.el_,"mousemove",this.handleMouseMove),this.removeListenersAddedOnMousedownAndTouchstart(),this.addClass("disabled"),this.enabled_=!1,this.player_.scrubbing())&&(e=this.getChild("seekBar"),this.player_.scrubbing(!1),e.videoWasPlaying)&&w(this.player_.play())}enable(){this.children().forEach(e=>e.enable&&e.enable()),this.enabled()||(this.on(["mousedown","touchstart"],this.handleMouseDownHandler_),this.on(this.el_,"mousemove",this.handleMouseMove),this.removeClass("disabled"),this.enabled_=!0)}removeListenersAddedOnMousedownAndTouchstart(){var e=this.el_.ownerDocument;this.off(e,"mousemove",this.throttledHandleMouseSeek),this.off(e,"touchmove",this.throttledHandleMouseSeek),this.off(e,"mouseup",this.handleMouseUpHandler_),this.off(e,"touchend",this.handleMouseUpHandler_)}handleMouseDown(e){var t=this.el_.ownerDocument,s=this.getChild("seekBar");s&&s.handleMouseDown(e),this.on(t,"mousemove",this.throttledHandleMouseSeek),this.on(t,"touchmove",this.throttledHandleMouseSeek),this.on(t,"mouseup",this.handleMouseUpHandler_),this.on(t,"touchend",this.handleMouseUpHandler_)}handleMouseUp(e){var t=this.getChild("seekBar");t&&t.handleMouseUp(e),this.removeListenersAddedOnMousedownAndTouchstart()}}di.prototype.options_={children:["seekBar"]},T.registerComponent("ProgressControl",di);class ui extends P{constructor(e,t){super(e,t),this.setIcon("picture-in-picture-enter"),this.on(e,["enterpictureinpicture","leavepictureinpicture"],e=>this.handlePictureInPictureChange(e)),this.on(e,["disablepictureinpicturechanged","loadedmetadata"],e=>this.handlePictureInPictureEnabledChange(e)),this.on(e,["loadedmetadata","audioonlymodechange","audiopostermodechange"],()=>this.handlePictureInPictureAudioModeChange()),this.disable()}buildCSSClass(){return"vjs-picture-in-picture-control vjs-hidden "+super.buildCSSClass()}handlePictureInPictureAudioModeChange(){"audio"===this.player_.currentType().substring(0,5)||this.player_.audioPosterMode()||this.player_.audioOnlyMode()?(this.player_.isInPictureInPicture()&&this.player_.exitPictureInPicture(),this.hide()):this.show()}handlePictureInPictureEnabledChange(){document.pictureInPictureEnabled&&!1===this.player_.disablePictureInPicture()||this.player_.options_.enableDocumentPictureInPicture&&"documentPictureInPicture"in window?this.enable():this.disable()}handlePictureInPictureChange(e){this.player_.isInPictureInPicture()?(this.setIcon("picture-in-picture-exit"),this.controlText("Exit Picture-in-Picture")):(this.setIcon("picture-in-picture-enter"),this.controlText("Picture-in-Picture")),this.handlePictureInPictureEnabledChange()}handleClick(e){this.player_.isInPictureInPicture()?this.player_.exitPictureInPicture():this.player_.requestPictureInPicture()}show(){"function"==typeof document.exitPictureInPicture&&super.show()}}ui.prototype.controlText_="Picture-in-Picture",T.registerComponent("PictureInPictureToggle",ui);class pi extends P{constructor(e,t){super(e,t),this.setIcon("fullscreen-enter"),this.on(e,"fullscreenchange",e=>this.handleFullscreenChange(e)),!1===document[e.fsApi_.fullscreenEnabled]&&this.disable()}buildCSSClass(){return"vjs-fullscreen-control "+super.buildCSSClass()}handleFullscreenChange(e){this.player_.isFullscreen()?(this.controlText("Exit Fullscreen"),this.setIcon("fullscreen-exit")):(this.controlText("Fullscreen"),this.setIcon("fullscreen-enter"))}handleClick(e){this.player_.isFullscreen()?this.player_.exitFullscreen():this.player_.requestFullscreen()}}pi.prototype.controlText_="Fullscreen",T.registerComponent("FullscreenToggle",pi);class gi extends T{createEl(){var e=super.createEl("div",{className:"vjs-volume-level"});return this.setIcon("circle",e),e.appendChild(super.createEl("span",{className:"vjs-control-text"})),e}}T.registerComponent("VolumeLevel",gi);class vi extends T{constructor(e,t){super(e,t),this.update=r(y(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-volume-tooltip"},{"aria-hidden":"true"})}update(t,s,i,e){if(!i){var i=De(this.el_),r=De(this.player_.el()),s=t.width*s;if(!r||!i)return;var n=t.left-r.left+s,s=t.width-s+(r.right-t.right);let e=i.width/2;ni.width&&(e=i.width),this.el_.style.right=`-${e}px`}this.write(e+"%")}write(e){Ce(this.el_,e)}updateVolume(e,t,s,i,r){this.requestNamedAnimationFrame("VolumeLevelTooltip#updateVolume",()=>{this.update(e,t,s,i.toFixed(0)),r&&r()})}}T.registerComponent("VolumeLevelTooltip",vi);class mi extends T{constructor(e,t){super(e,t),this.update=r(y(this,this.update),30)}createEl(){return super.createEl("div",{className:"vjs-mouse-display"})}update(e,t,s){var i=100*t;this.getChild("volumeLevelTooltip").updateVolume(e,t,s,i,()=>{s?this.el_.style.bottom=e.height*t+"px":this.el_.style.left=e.width*t+"px"})}}mi.prototype.options_={children:["volumeLevelTooltip"]},T.registerComponent("MouseVolumeLevelDisplay",mi);class _i extends ri{constructor(e,t){super(e,t),this.on("slideractive",e=>this.updateLastVolume_(e)),this.on(e,"volumechange",e=>this.updateARIAAttributes(e)),e.ready(()=>this.updateARIAAttributes())}createEl(){return super.createEl("div",{className:"vjs-volume-bar vjs-slider-bar"},{"aria-label":this.localize("Volume Level"),"aria-live":"polite"})}handleMouseDown(e){$e(e)&&super.handleMouseDown(e)}handleMouseMove(e){var t,s,i,r=this.getChild("mouseVolumeLevelDisplay");r&&(t=De(i=this.el()),s=this.vertical(),i=Re(i,e),i=ii(i=s?i.y:i.x,0,1),r.update(t,i,s)),$e(e)&&(this.checkMuted(),this.player_.volume(this.calculateDistance(e)))}checkMuted(){this.player_.muted()&&this.player_.muted(!1)}getPercent(){return this.player_.muted()?0:this.player_.volume()}stepForward(){this.checkMuted(),this.player_.volume(this.player_.volume()+.1)}stepBack(){this.checkMuted(),this.player_.volume(this.player_.volume()-.1)}updateARIAAttributes(e){var t=this.player_.muted()?0:this.volumeAsPercentage_();this.el_.setAttribute("aria-valuenow",t),this.el_.setAttribute("aria-valuetext",t+"%")}volumeAsPercentage_(){return Math.round(100*this.player_.volume())}updateLastVolume_(){const e=this.player_.volume();this.one("sliderinactive",()=>{0===this.player_.volume()&&this.player_.lastVolume_(e)})}}_i.prototype.options_={children:["volumeLevel"],barName:"volumeLevel"},u||o||_i.prototype.options_.children.splice(0,0,"mouseVolumeLevelDisplay"),_i.prototype.playerEvent="volumechange",T.registerComponent("VolumeBar",_i);class yi extends T{constructor(e,t={}){var s,i;t.vertical=t.vertical||!1,"undefined"!=typeof t.volumeBar&&!G(t.volumeBar)||(t.volumeBar=t.volumeBar||{},t.volumeBar.vertical=t.vertical),super(e,t),s=this,(i=e).tech_&&!i.tech_.featuresVolumeControl&&s.addClass("vjs-hidden"),s.on(i,"loadstart",function(){i.tech_.featuresVolumeControl?s.removeClass("vjs-hidden"):s.addClass("vjs-hidden")}),this.throttledHandleMouseMove=r(y(this,this.handleMouseMove),30),this.handleMouseUpHandler_=e=>this.handleMouseUp(e),this.on("mousedown",e=>this.handleMouseDown(e)),this.on("touchstart",e=>this.handleMouseDown(e)),this.on("mousemove",e=>this.handleMouseMove(e)),this.on(this.volumeBar,["focus","slideractive"],()=>{this.volumeBar.addClass("vjs-slider-active"),this.addClass("vjs-slider-active"),this.trigger("slideractive")}),this.on(this.volumeBar,["blur","sliderinactive"],()=>{this.volumeBar.removeClass("vjs-slider-active"),this.removeClass("vjs-slider-active"),this.trigger("sliderinactive")})}createEl(){let e="vjs-volume-horizontal";return this.options_.vertical&&(e="vjs-volume-vertical"),super.createEl("div",{className:"vjs-volume-control vjs-control "+e})}handleMouseDown(e){var t=this.el_.ownerDocument;this.on(t,"mousemove",this.throttledHandleMouseMove),this.on(t,"touchmove",this.throttledHandleMouseMove),this.on(t,"mouseup",this.handleMouseUpHandler_),this.on(t,"touchend",this.handleMouseUpHandler_)}handleMouseUp(e){var t=this.el_.ownerDocument;this.off(t,"mousemove",this.throttledHandleMouseMove),this.off(t,"touchmove",this.throttledHandleMouseMove),this.off(t,"mouseup",this.handleMouseUpHandler_),this.off(t,"touchend",this.handleMouseUpHandler_)}handleMouseMove(e){this.volumeBar.handleMouseMove(e)}}yi.prototype.options_={children:["volumeBar"]},T.registerComponent("VolumeControl",yi);class fi extends P{constructor(e,t){var s,i;super(e,t),s=this,(i=e).tech_&&!i.tech_.featuresMuteControl&&s.addClass("vjs-hidden"),s.on(i,"loadstart",function(){i.tech_.featuresMuteControl?s.removeClass("vjs-hidden"):s.addClass("vjs-hidden")}),this.on(e,["loadstart","volumechange"],e=>this.update(e))}buildCSSClass(){return"vjs-mute-control "+super.buildCSSClass()}handleClick(e){var t=this.player_.volume(),s=this.player_.lastVolume_();0===t?(this.player_.volume(s<.1?.1:s),this.player_.muted(!1)):this.player_.muted(!this.player_.muted())}update(e){this.updateIcon_(),this.updateControlText_()}updateIcon_(){var e=this.player_.volume();let t=3;this.setIcon("volume-high"),u&&this.player_.tech_&&this.player_.tech_.el_&&this.player_.muted(this.player_.tech_.el_.muted),0===e||this.player_.muted()?(this.setIcon("volume-mute"),t=0):e<.33?(this.setIcon("volume-low"),t=1):e<.67&&(this.setIcon("volume-medium"),t=2),xe(this.el_,[0,1,2,3].reduce((e,t)=>e+`${t?" ":""}vjs-vol-`+t,"")),Se(this.el_,"vjs-vol-"+t)}updateControlText_(){var e=this.player_.muted()||0===this.player_.volume()?"Unmute":"Mute";this.controlText()!==e&&this.controlText(e)}}fi.prototype.controlText_="Mute",T.registerComponent("MuteToggle",fi);class bi extends T{constructor(e,t={}){"undefined"!=typeof t.inline?t.inline=t.inline:t.inline=!0,"undefined"!=typeof t.volumeControl&&!G(t.volumeControl)||(t.volumeControl=t.volumeControl||{},t.volumeControl.vertical=!t.inline),super(e,t),this.handleKeyPressHandler_=e=>this.handleKeyPress(e),this.on(e,["loadstart"],e=>this.volumePanelState_(e)),this.on(this.muteToggle,"keyup",e=>this.handleKeyPress(e)),this.on(this.volumeControl,"keyup",e=>this.handleVolumeControlKeyUp(e)),this.on("keydown",e=>this.handleKeyPress(e)),this.on("mouseover",e=>this.handleMouseOver(e)),this.on("mouseout",e=>this.handleMouseOut(e)),this.on(this.volumeControl,["slideractive"],this.sliderActive_),this.on(this.volumeControl,["sliderinactive"],this.sliderInactive_)}sliderActive_(){this.addClass("vjs-slider-active")}sliderInactive_(){this.removeClass("vjs-slider-active")}volumePanelState_(){this.volumeControl.hasClass("vjs-hidden")&&this.muteToggle.hasClass("vjs-hidden")&&this.addClass("vjs-hidden"),this.volumeControl.hasClass("vjs-hidden")&&!this.muteToggle.hasClass("vjs-hidden")&&this.addClass("vjs-mute-toggle-only")}createEl(){let e="vjs-volume-panel-horizontal";return this.options_.inline||(e="vjs-volume-panel-vertical"),super.createEl("div",{className:"vjs-volume-panel vjs-control "+e})}dispose(){this.handleMouseOut(),super.dispose()}handleVolumeControlKeyUp(e){b.isEventKey(e,"Esc")&&this.muteToggle.focus()}handleMouseOver(e){this.addClass("vjs-hover"),m(document,"keyup",this.handleKeyPressHandler_)}handleMouseOut(e){this.removeClass("vjs-hover"),_(document,"keyup",this.handleKeyPressHandler_)}handleKeyPress(e){b.isEventKey(e,"Esc")&&this.handleMouseOut()}}bi.prototype.options_={children:["muteToggle","volumeControl"]},T.registerComponent("VolumePanel",bi);class Ti extends P{constructor(e,t){super(e,t),this.validOptions=[5,10,30],this.skipTime=this.getSkipForwardTime(),this.skipTime&&this.validOptions.includes(this.skipTime)?(this.setIcon("forward-"+this.skipTime),this.controlText(this.localize("Skip forward {1} seconds",[this.skipTime.toLocaleString(e.language())])),this.show()):this.hide()}getSkipForwardTime(){var e=this.options_.playerOptions;return e.controlBar&&e.controlBar.skipButtons&&e.controlBar.skipButtons.forward}buildCSSClass(){return`vjs-skip-forward-${this.getSkipForwardTime()} `+super.buildCSSClass()}handleClick(e){if(!isNaN(this.player_.duration())){var t=this.player_.currentTime(),s=this.player_.liveTracker,s=s&&s.isLive()?s.seekableEnd():this.player_.duration();let e;e=t+this.skipTime<=s?t+this.skipTime:s,this.player_.currentTime(e)}}handleLanguagechange(){this.controlText(this.localize("Skip forward {1} seconds",[this.skipTime]))}}Ti.prototype.controlText_="Skip Forward",T.registerComponent("SkipForward",Ti);class ki extends P{constructor(e,t){super(e,t),this.validOptions=[5,10,30],this.skipTime=this.getSkipBackwardTime(),this.skipTime&&this.validOptions.includes(this.skipTime)?(this.setIcon("replay-"+this.skipTime),this.controlText(this.localize("Skip backward {1} seconds",[this.skipTime.toLocaleString(e.language())])),this.show()):this.hide()}getSkipBackwardTime(){var e=this.options_.playerOptions;return e.controlBar&&e.controlBar.skipButtons&&e.controlBar.skipButtons.backward}buildCSSClass(){return`vjs-skip-backward-${this.getSkipBackwardTime()} `+super.buildCSSClass()}handleClick(e){var t=this.player_.currentTime(),s=this.player_.liveTracker,s=s&&s.isLive()&&s.seekableStart();let i;i=s&&t-this.skipTime<=s?s:t>=this.skipTime?t-this.skipTime:0,this.player_.currentTime(i)}handleLanguagechange(){this.controlText(this.localize("Skip backward {1} seconds",[this.skipTime]))}}ki.prototype.controlText_="Skip Backward",T.registerComponent("SkipBackward",ki);class Ci extends T{constructor(e,t){super(e,t),t&&(this.menuButton_=t.menuButton),this.focusedChild_=-1,this.on("keydown",e=>this.handleKeyDown(e)),this.boundHandleBlur_=e=>this.handleBlur(e),this.boundHandleTapClick_=e=>this.handleTapClick(e)}addEventListenerForItem(e){e instanceof T&&(this.on(e,"blur",this.boundHandleBlur_),this.on(e,["tap","click"],this.boundHandleTapClick_))}removeEventListenerForItem(e){e instanceof T&&(this.off(e,"blur",this.boundHandleBlur_),this.off(e,["tap","click"],this.boundHandleTapClick_))}removeChild(e){"string"==typeof e&&(e=this.getChild(e)),this.removeEventListenerForItem(e),super.removeChild(e)}addItem(e){e=this.addChild(e);e&&this.addEventListenerForItem(e)}createEl(){var e=this.options_.contentElType||"ul",e=(this.contentEl_=p(e,{className:"vjs-menu-content"}),this.contentEl_.setAttribute("role","menu"),super.createEl("div",{append:this.contentEl_,className:"vjs-menu"}));return e.appendChild(this.contentEl_),m(e,"click",function(e){e.preventDefault(),e.stopImmediatePropagation()}),e}dispose(){this.contentEl_=null,this.boundHandleBlur_=null,this.boundHandleTapClick_=null,super.dispose()}handleBlur(e){const t=e.relatedTarget||document.activeElement;this.children().some(e=>e.el()===t)||(e=this.menuButton_)&&e.buttonPressed_&&t!==e.el().firstChild&&e.unpressButton()}handleTapClick(t){var e;this.menuButton_&&(this.menuButton_.unpressButton(),e=this.children(),Array.isArray(e))&&(e=e.filter(e=>e.el()===t.target)[0])&&"CaptionSettingsMenuItem"!==e.name()&&this.menuButton_.focus()}handleKeyDown(e){b.isEventKey(e,"Left")||b.isEventKey(e,"Down")?(e.preventDefault(),e.stopPropagation(),this.stepForward()):(b.isEventKey(e,"Right")||b.isEventKey(e,"Up"))&&(e.preventDefault(),e.stopPropagation(),this.stepBack())}stepForward(){let e=0;void 0!==this.focusedChild_&&(e=this.focusedChild_+1),this.focus(e)}stepBack(){let e=0;void 0!==this.focusedChild_&&(e=this.focusedChild_-1),this.focus(e)}focus(e=0){var t=this.children().slice();t.length&&t[0].hasClass("vjs-menu-title")&&t.shift(),0=t.length&&(e=t.length-1),t[this.focusedChild_=e].el_.focus())}}T.registerComponent("Menu",Ci);class wi extends T{constructor(e,t={}){super(e,t),this.menuButton_=new P(e,t),this.menuButton_.controlText(this.controlText_),this.menuButton_.el_.setAttribute("aria-haspopup","true");e=P.prototype.buildCSSClass(),this.menuButton_.el_.className=this.buildCSSClass()+" "+e,this.menuButton_.removeClass("vjs-control"),this.addChild(this.menuButton_),this.update(),this.enabled_=!0,t=e=>this.handleClick(e);this.handleMenuKeyUp_=e=>this.handleMenuKeyUp(e),this.on(this.menuButton_,"tap",t),this.on(this.menuButton_,"click",t),this.on(this.menuButton_,"keydown",e=>this.handleKeyDown(e)),this.on(this.menuButton_,"mouseenter",()=>{this.addClass("vjs-hover"),this.menu.show(),m(document,"keyup",this.handleMenuKeyUp_)}),this.on("mouseleave",e=>this.handleMouseLeave(e)),this.on("keydown",e=>this.handleSubmenuKeyDown(e))}update(){var e=this.createMenu();this.menu&&(this.menu.dispose(),this.removeChild(this.menu)),this.menu=e,this.addChild(e),this.buttonPressed_=!1,this.menuButton_.el_.setAttribute("aria-expanded","false"),this.items&&this.items.length<=this.hideThreshold_?(this.hide(),this.menu.contentEl_.removeAttribute("role")):(this.show(),this.menu.contentEl_.setAttribute("role","menu"))}createMenu(){var e,t=new Ci(this.player_,{menuButton:this});if(this.hideThreshold_=0,this.options_.title&&(e=p("li",{className:"vjs-menu-title",textContent:f(this.options_.title),tabIndex:-1}),e=new T(this.player_,{el:e}),t.addItem(e)),this.items=this.createItems(),this.items)for(let e=0;eb.isEventKey(t,e))||super.handleKeyDown(t)}handleClick(e){this.selected(!0)}selected(e){this.selectable&&(e?(this.addClass("vjs-selected"),this.el_.setAttribute("aria-checked","true"),this.controlText(", selected"),this.isSelected_=!0):(this.removeClass("vjs-selected"),this.el_.setAttribute("aria-checked","false"),this.controlText(""),this.isSelected_=!1))}}T.registerComponent("MenuItem",xi);class ji extends xi{constructor(e,t){var s=t.track;const i=e.textTracks(),r=(t.label=s.label||s.language||"Unknown",t.selected="showing"===s.mode,super(e,t),this.track=s,this.kinds=(t.kinds||[t.kind||this.track.kind]).filter(Boolean),(...e)=>{this.handleTracksChange.apply(this,e)}),n=(...e)=>{this.handleSelectedLanguageChange.apply(this,e)};if(e.on(["loadstart","texttrackchange"],r),i.addEventListener("change",r),i.addEventListener("selectedlanguagechange",n),this.on("dispose",function(){e.off(["loadstart","texttrackchange"],r),i.removeEventListener("change",r),i.removeEventListener("selectedlanguagechange",n)}),void 0===i.onchange){let e;this.on(["tap","click"],function(){if("object"!=typeof window.Event)try{e=new window.Event("change")}catch(e){}e||(e=document.createEvent("Event")).initEvent("change",!0,!0),i.dispatchEvent(e)})}this.handleTracksChange()}handleClick(e){var t=this.track,s=this.player_.textTracks();if(super.handleClick(e),s)for(let e=0;e{this.items.forEach(e=>{e.selected(this.track_.activeCues[0]===e.cue)})}}buildCSSClass(){return"vjs-chapters-button "+super.buildCSSClass()}buildWrapperCSSClass(){return"vjs-chapters-button "+super.buildWrapperCSSClass()}update(e){e&&e.track&&"chapters"!==e.track.kind||((e=this.findChaptersTrack())!==this.track_?(this.setTrack(e),super.update()):(!this.items||e&&e.cues&&e.cues.length!==this.items.length)&&super.update())}setTrack(e){var t;this.track_!==e&&(this.updateHandler_||(this.updateHandler_=this.update.bind(this)),this.track_&&((t=this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_))&&t.removeEventListener("load",this.updateHandler_),this.track_.removeEventListener("cuechange",this.selectCurrentItem_),this.track_=null),this.track_=e,this.track_)&&(this.track_.mode="hidden",(t=this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_))&&t.addEventListener("load",this.updateHandler_),this.track_.addEventListener("cuechange",this.selectCurrentItem_))}findChaptersTrack(){var t=this.player_.textTracks()||[];for(let e=t.length-1;0<=e;e--){var s=t[e];if(s.kind===this.kind_)return s}}getMenuCaption(){return this.track_&&this.track_.label?this.track_.label:this.localize(f(this.kind_))}createMenu(){return this.options_.title=this.getMenuCaption(),super.createMenu()}createItems(){var s=[];if(this.track_){var i=this.track_.cues;if(i)for(let e=0,t=i.length;e{this.handleTracksChange.apply(this,e)});i.addEventListener("change",r),this.on("dispose",()=>{i.removeEventListener("change",r)})}createEl(e,t,s){e=super.createEl(e,t,s),t=e.querySelector(".vjs-menu-item-text");return 0<=["main-desc","description"].indexOf(this.options_.track.kind)&&(t.appendChild(p("span",{className:"vjs-icon-placeholder"},{"aria-hidden":!0})),t.appendChild(p("span",{className:"vjs-control-text",textContent:" "+this.localize("Descriptions")}))),e}handleClick(e){if(super.handleClick(e),this.track.enabled=!0,this.player_.tech_.featuresNativeAudioTracks){var t=this.player_.audioTracks();for(let e=0;ethis.update(e))}handleClick(e){super.handleClick(),this.player().playbackRate(this.rate)}update(e){this.selected(this.player().playbackRate()===this.rate)}}zi.prototype.contentElType="button",T.registerComponent("PlaybackRateMenuItem",zi);class Vi extends wi{constructor(e,t){super(e,t),this.menuButton_.el_.setAttribute("aria-describedby",this.labelElId_),this.updateVisibility(),this.updateLabel(),this.on(e,"loadstart",e=>this.updateVisibility(e)),this.on(e,"ratechange",e=>this.updateLabel(e)),this.on(e,"playbackrateschange",e=>this.handlePlaybackRateschange(e))}createEl(){var e=super.createEl();return this.labelElId_="vjs-playback-rate-value-label-"+this.id_,this.labelEl_=p("div",{className:"vjs-playback-rate-value",id:this.labelElId_,textContent:"1x"}),e.appendChild(this.labelEl_),e}dispose(){this.labelEl_=null,super.dispose()}buildCSSClass(){return"vjs-playback-rate "+super.buildCSSClass()}buildWrapperCSSClass(){return"vjs-playback-rate "+super.buildWrapperCSSClass()}createItems(){var t=this.playbackRates(),s=[];for(let e=t.length-1;0<=e;e--)s.push(new zi(this.player(),{rate:t[e]+"x"}));return s}handlePlaybackRateschange(e){this.update()}playbackRates(){var e=this.player();return e.playbackRates&&e.playbackRates()||[]}playbackRateSupported(){return this.player().tech_&&this.player().tech_.featuresPlaybackRate&&this.playbackRates()&&0{this.open(e)})}buildCSSClass(){return"vjs-error-display "+super.buildCSSClass()}content(){var e=this.player().error();return e?this.localize(e.message):""}}Ki.prototype.options_=Object.assign({},$t.prototype.options_,{pauseOnOpen:!1,fillAlways:!0,temporary:!1,uncloseable:!0}),T.registerComponent("ErrorDisplay",Ki);const Ui="vjs-text-track-settings";var Wi=["#000","Black"],Gi=["#00F","Blue"],Xi=["#0FF","Cyan"],Yi=["#0F0","Green"],Qi=["#F0F","Magenta"],Ji=["#F00","Red"],Zi=["#FFF","White"],er=["#FF0","Yellow"],tr=["1","Opaque"],sr=["0.5","Semi-Transparent"],ir=["0","Transparent"];const rr={backgroundColor:{selector:".vjs-bg-color > select",id:"captions-background-color-%s",label:"Color",options:[Wi,Zi,Ji,Yi,Gi,er,Qi,Xi]},backgroundOpacity:{selector:".vjs-bg-opacity > select",id:"captions-background-opacity-%s",label:"Opacity",options:[tr,sr,ir]},color:{selector:".vjs-text-color > select",id:"captions-foreground-color-%s",label:"Color",options:[Zi,Wi,Ji,Yi,Gi,er,Qi,Xi]},edgeStyle:{selector:".vjs-edge-style > select",id:"%s",label:"Text Edge Style",options:[["none","None"],["raised","Raised"],["depressed","Depressed"],["uniform","Uniform"],["dropshadow","Drop shadow"]]},fontFamily:{selector:".vjs-font-family > select",id:"captions-font-family-%s",label:"Font Family",options:[["proportionalSansSerif","Proportional Sans-Serif"],["monospaceSansSerif","Monospace Sans-Serif"],["proportionalSerif","Proportional Serif"],["monospaceSerif","Monospace Serif"],["casual","Casual"],["script","Script"],["small-caps","Small Caps"]]},fontPercent:{selector:".vjs-font-percent > select",id:"captions-font-size-%s",label:"Font Size",options:[["0.50","50%"],["0.75","75%"],["1.00","100%"],["1.25","125%"],["1.50","150%"],["1.75","175%"],["2.00","200%"],["3.00","300%"],["4.00","400%"]],default:2,parser:e=>"1.00"===e?null:Number(e)},textOpacity:{selector:".vjs-text-opacity > select",id:"captions-foreground-opacity-%s",label:"Opacity",options:[tr,sr]},windowColor:{selector:".vjs-window-color > select",id:"captions-window-color-%s",label:"Color"},windowOpacity:{selector:".vjs-window-opacity > select",id:"captions-window-opacity-%s",label:"Opacity",options:[ir,sr,tr]}};function nr(e,t){if((e=t?t(e):e)&&"none"!==e)return e}rr.windowColor.options=rr.backgroundColor.options;class ar extends $t{constructor(e,t){t.temporary=!1,super(e,t),this.updateDisplay=this.updateDisplay.bind(this),this.fill(),this.hasBeenOpened_=this.hasBeenFilled_=!0,this.endDialog=p("p",{className:"vjs-control-text",textContent:this.localize("End of dialog window.")}),this.el().appendChild(this.endDialog),this.setDefaults(),void 0===t.persistTextTrackSettings&&(this.options_.persistTextTrackSettings=this.options_.playerOptions.persistTextTrackSettings),this.on(this.$(".vjs-done-button"),"click",()=>{this.saveSettings(),this.close()}),this.on(this.$(".vjs-default-button"),"click",()=>{this.setDefaults(),this.updateDisplay()}),U(rr,e=>{this.on(this.$(e.selector),"change",this.updateDisplay)}),this.options_.persistTextTrackSettings&&this.restoreSettings()}dispose(){this.endDialog=null,super.dispose()}createElSelect_(e,t="",s="label"){e=rr[e];const i=e.id.replace("%s",this.id_),r=[t,i].join(" ").trim();t="vjs_select_"+v++;return[`<${s} id="${i}"${"label"===s?` for="${t}" class="vjs-label"`:""}>`,this.localize(e.label),`${s}>`,``].concat(e.options.map(e=>{var t=i+"-"+e[1].replace(/\W+/g,"");return[``,this.localize(e[1])," "].join("")})).concat(" ").join("")}createElFgColor_(){var e="captions-text-legend-"+this.id_;return['',``,this.localize("Text")," ",'',this.createElSelect_("color",e)," ",'',this.createElSelect_("textOpacity",e)," "," "].join("")}createElBgColor_(){var e="captions-background-"+this.id_;return['',``,this.localize("Text Background")," ",'',this.createElSelect_("backgroundColor",e)," ",'',this.createElSelect_("backgroundOpacity",e)," "," "].join("")}createElWinColor_(){var e="captions-window-"+this.id_;return['',``,this.localize("Caption Area Background")," ",'',this.createElSelect_("windowColor",e)," ",'',this.createElSelect_("windowOpacity",e)," "," "].join("")}createElColors_(){return p("div",{className:"vjs-track-settings-colors",innerHTML:[this.createElFgColor_(),this.createElBgColor_(),this.createElWinColor_()].join("")})}createElFont_(){return p("div",{className:"vjs-track-settings-font",innerHTML:['',this.createElSelect_("fontPercent","","legend")," ",'',this.createElSelect_("edgeStyle","","legend")," ",'',this.createElSelect_("fontFamily","","legend")," "].join("")})}createElControls_(){var e=this.localize("restore all settings to the default values");return p("div",{className:"vjs-track-settings-controls",innerHTML:[``,this.localize("Reset"),` ${e} `," ",`${this.localize("Done")} `].join("")})}content(){return[this.createElColors_(),this.createElFont_(),this.createElControls_()]}label(){return this.localize("Caption Settings Dialog")}description(){return this.localize("Beginning of dialog window. Escape will cancel and close the window.")}buildCSSClass(){return super.buildCSSClass()+" vjs-text-track-settings"}getValues(){return W(rr,(e,t,s)=>{i=this.$(t.selector),t=t.parser;var i=nr(i.options[i.options.selectedIndex].value,t);return void 0!==i&&(e[s]=i),e},{})}setValues(n){U(rr,(e,t)=>{var s=this.$(e.selector),i=n[t],r=e.parser;if(i)for(let e=0;e{var t=e.hasOwnProperty("default")?e.default:0;this.$(e.selector).selectedIndex=t})}restoreSettings(){let e;try{e=JSON.parse(window.localStorage.getItem(Ui))}catch(e){l.warn(e)}e&&this.setValues(e)}saveSettings(){if(this.options_.persistTextTrackSettings){var e=this.getValues();try{Object.keys(e).length?window.localStorage.setItem(Ui,JSON.stringify(e)):window.localStorage.removeItem(Ui)}catch(e){l.warn(e)}}}updateDisplay(){var e=this.player_.getChild("textTrackDisplay");e&&e.updateDisplay()}conditionalBlur_(){this.previouslyActiveEl_=null;var e=this.player_.controlBar,t=e&&e.subsCapsButton,e=e&&e.captionsButton;t?t.focus():e&&e.focus()}handleLanguagechange(){this.fill()}}T.registerComponent("TextTrackSettings",ar);class or extends T{constructor(e,t){let s=t.ResizeObserver||window.ResizeObserver;super(e,h({createEl:!(s=null===t.ResizeObserver?!1:s),reportTouchActivity:!1},t)),this.ResizeObserver=t.ResizeObserver||window.ResizeObserver,this.loadListener_=null,this.resizeObserver_=null,this.debouncedHandler_=ut(()=>{this.resizeHandler()},100,!1,this),s?(this.resizeObserver_=new this.ResizeObserver(this.debouncedHandler_),this.resizeObserver_.observe(e.el())):(this.loadListener_=()=>{if(this.el_&&this.el_.contentWindow){const t=this.debouncedHandler_;let e=this.unloadListener_=function(){_(this,"resize",t),_(this,"unload",e),e=null};m(this.el_.contentWindow,"unload",e),m(this.el_.contentWindow,"resize",t)}},this.one("load",this.loadListener_))}createEl(){return super.createEl("iframe",{className:"vjs-resize-manager",tabIndex:-1,title:this.localize("No content")},{"aria-hidden":"true"})}resizeHandler(){this.player_&&this.player_.trigger&&this.player_.trigger("playerresize")}dispose(){this.debouncedHandler_&&this.debouncedHandler_.cancel(),this.resizeObserver_&&(this.player_.el()&&this.resizeObserver_.unobserve(this.player_.el()),this.resizeObserver_.disconnect()),this.loadListener_&&this.off("load",this.loadListener_),this.el_&&this.el_.contentWindow&&this.unloadListener_&&this.unloadListener_.call(this.el_.contentWindow),this.ResizeObserver=null,this.resizeObserver=null,this.debouncedHandler_=null,this.loadListener_=null,super.dispose()}}T.registerComponent("ResizeManager",or);const lr={trackingThreshold:20,liveTolerance:15};class hr extends T{constructor(e,t){super(e,h(lr,t,{createEl:!1})),this.trackLiveHandler_=()=>this.trackLive_(),this.handlePlay_=e=>this.handlePlay(e),this.handleFirstTimeupdate_=e=>this.handleFirstTimeupdate(e),this.handleSeeked_=e=>this.handleSeeked(e),this.seekToLiveEdge_=e=>this.seekToLiveEdge(e),this.reset_(),this.on(this.player_,"durationchange",e=>this.handleDurationchange(e)),this.on(this.player_,"canplay",()=>this.toggleTracking())}trackLive_(){var t=this.player_.seekable();if(t&&t.length){var t=Number(window.performance.now().toFixed(4)),s=-1===this.lastTime_?0:(t-this.lastTime_)/1e3,t=(this.lastTime_=t,this.pastSeekEnd_=this.pastSeekEnd()+s,this.liveCurrentTime()),s=this.player_.currentTime();let e=this.player_.paused()||this.seekedBehindLive_||Math.abs(t-s)>this.options_.liveTolerance;(e=this.timeupdateSeen_&&t!==1/0?e:!1)!==this.behindLiveEdge_&&(this.behindLiveEdge_=e,this.trigger("liveedgechange"))}}handleDurationchange(){this.toggleTracking()}toggleTracking(){this.player_.duration()===1/0&&this.liveWindow()>=this.options_.trackingThreshold?(this.player_.options_.liveui&&this.player_.addClass("vjs-liveui"),this.startTracking()):(this.player_.removeClass("vjs-liveui"),this.stopTracking())}startTracking(){this.isTracking()||(this.timeupdateSeen_||(this.timeupdateSeen_=this.player_.hasStarted()),this.trackingInterval_=this.setInterval(this.trackLiveHandler_,30),this.trackLive_(),this.on(this.player_,["play","pause"],this.trackLiveHandler_),this.timeupdateSeen_?this.on(this.player_,"seeked",this.handleSeeked_):(this.one(this.player_,"play",this.handlePlay_),this.one(this.player_,"timeupdate",this.handleFirstTimeupdate_)))}handleFirstTimeupdate(){this.timeupdateSeen_=!0,this.on(this.player_,"seeked",this.handleSeeked_)}handleSeeked(){var e=Math.abs(this.liveCurrentTime()-this.player_.currentTime());this.seekedBehindLive_=this.nextSeekedFromUser_&&2this.updateDom_()),this.updateDom_()}createEl(){return this.els={title:p("div",{className:"vjs-title-bar-title",id:"vjs-title-bar-title-"+v++}),description:p("div",{className:"vjs-title-bar-description",id:"vjs-title-bar-description-"+v++})},p("div",{className:"vjs-title-bar"},{},X(this.els))}updateDom_(){var e=this.player_.tech_;const i=e&&e.el_,r={title:"aria-labelledby",description:"aria-describedby"};["title","description"].forEach(e=>{var t=this.state[e],s=this.els[e],e=r[e];He(s),t&&Ce(s,t),i&&(i.removeAttribute(e),t)&&i.setAttribute(e,s.id)}),this.state.title||this.state.description?this.show():this.hide()}update(e){this.setState(e)}dispose(){var e=this.player_.tech_,e=e&&e.el_;e&&(e.removeAttribute("aria-labelledby"),e.removeAttribute("aria-describedby")),super.dispose(),this.els=null}}T.registerComponent("TitleBar",cr);function dr(s){const i=s.el();if(!i.resetSourceWatch_){const t={},e=mr(s),r=t=>(...e)=>{e=t.apply(i,e);return pr(s),e};["append","appendChild","insertAdjacentHTML"].forEach(e=>{i[e]&&(t[e]=i[e],i[e]=r(t[e]))}),Object.defineProperty(i,"innerHTML",h(e,{set:r(e.set)})),i.resetSourceWatch_=()=>{i.resetSourceWatch_=null,Object.keys(t).forEach(e=>{i[e]=t[e]}),Object.defineProperty(i,"innerHTML",e)},s.one("sourceset",i.resetSourceWatch_)}}function ur(s){if(s.featuresSourceset){const i=s.el();if(!i.resetSourceset_){e=s;const t=vr([e.el(),window.HTMLMediaElement.prototype,_r],"src");var e;const r=i.setAttribute,n=i.load;Object.defineProperty(i,"src",h(t,{set:e=>{e=t.set.call(i,e);return s.triggerSourceset(i.src),e}})),i.setAttribute=(e,t)=>{t=r.call(i,e,t);return/src/i.test(e)&&s.triggerSourceset(i.src),t},i.load=()=>{var e=n.call(i);return pr(s)||(s.triggerSourceset(""),dr(s)),e},i.currentSrc?s.triggerSourceset(i.currentSrc):pr(s)||dr(s),i.resetSourceset_=()=>{i.resetSourceset_=null,i.load=n,i.setAttribute=r,Object.defineProperty(i,"src",t),i.resetSourceWatch_&&i.resetSourceWatch_()}}}}const pr=t=>{var e=t.el();if(e.hasAttribute("src"))t.triggerSourceset(e.src);else{var s=t.$$("source"),i=[];let e="";if(!s.length)return!1;for(let e=0;e{let i={};for(let e=0;evr([e.el(),window.HTMLMediaElement.prototype,window.Element.prototype,gr],"innerHTML"),_r=Object.defineProperty({},"src",{get(){return this.hasAttribute("src")?ss(window.Element.prototype.getAttribute.call(this,"src")):""},set(e){return window.Element.prototype.setAttribute.call(this,"src",e),e}});class I extends j{constructor(e,t){super(e,t);t=e.source;let s=!1;if(this.featuresVideoFrameCallback=this.featuresVideoFrameCallback&&"VIDEO"===this.el_.tagName,t&&(this.el_.currentSrc!==t.src||e.tag&&3===e.tag.initNetworkState_)?this.setSource(t):this.handleLateInit_(this.el_),e.enableSourceset&&this.setupSourcesetHandling_(),this.isScrubbing_=!1,this.el_.hasChildNodes()){var i=this.el_.childNodes;let e=i.length;for(var r=[];e--;){var n=i[e];"track"===n.nodeName.toLowerCase()&&(this.featuresNativeTextTracks?(this.remoteTextTrackEls().addTrackElement_(n),this.remoteTextTracks().addTrack(n.track),this.textTracks().addTrack(n.track),s||this.el_.hasAttribute("crossorigin")||!is(n.src)||(s=!0)):r.push(n))}for(let e=0;e{i=[];for(let e=0;es.removeEventListener("change",e)),()=>{for(let e=0;e{s.removeEventListener("change",e),s.removeEventListener("change",r),s.addEventListener("change",r)}),this.on("webkitendfullscreen",()=>{s.removeEventListener("change",e),s.addEventListener("change",e),s.removeEventListener("change",r)})}overrideNative_(e,t){if(t===this[`featuresNative${e}Tracks`]){const s=e.toLowerCase();this[s+"TracksListeners_"]&&Object.keys(this[s+"TracksListeners_"]).forEach(e=>{this.el()[s+"Tracks"].removeEventListener(e,this[s+"TracksListeners_"][e])}),this[`featuresNative${e}Tracks`]=!t,this[s+"TracksListeners_"]=null,this.proxyNativeTracksForType_(s)}}overrideNativeAudioTracks(e){this.overrideNative_("Audio",e)}overrideNativeVideoTracks(e){this.overrideNative_("Video",e)}proxyNativeTracksForType_(s){var e=S[s];const i=this.el()[e.getterName],r=this[e.getterName]();if(this[`featuresNative${e.capitalName}Tracks`]&&i&&i.addEventListener){const n={change:e=>{var t={type:"change",target:r,currentTarget:r,srcElement:r};r.trigger(t),"text"===s&&this[Cs.remoteText.getterName]().trigger(t)},addtrack(e){r.addTrack(e.track)},removetrack(e){r.removeTrack(e.track)}},t=function(){var e=[];for(let s=0;s{const s=n[t];i.addEventListener(t,s),this.on("dispose",e=>i.removeEventListener(t,s))}),this.on("loadstart",t),this.on("dispose",e=>this.off("loadstart",t))}}proxyNativeTracks_(){S.names.forEach(e=>{this.proxyNativeTracksForType_(e)})}createEl(){let t=this.options_.tag;t&&(this.options_.playerElIngest||this.movingMediaElementInDOM)||(t?(e=t.cloneNode(!0),t.parentNode&&t.parentNode.insertBefore(e,t),I.disposeMediaElement(t),t=e):(t=document.createElement("video"),e=h({},this.options_.tag&&Ie(this.options_.tag)),ge&&!0===this.options_.nativeControlsForTouch||delete e.controls,Pe(t,Object.assign(e,{id:this.options_.techId,class:"vjs-tech"}))),t.playerId=this.options_.playerId),"undefined"!=typeof this.options_.preload&&Oe(t,"preload",this.options_.preload),void 0!==this.options_.disablePictureInPicture&&(t.disablePictureInPicture=this.options_.disablePictureInPicture);var e,s=["loop","muted","playsinline","autoplay"];for(let e=0;e{0{this.off("webkitbeginfullscreen",t),this.off("webkitendfullscreen",e)})}}supportsFullScreen(){return"function"==typeof this.el_.webkitEnterFullScreen}enterFullScreen(){const e=this.el_;if(e.paused&&e.networkState<=e.HAVE_METADATA)w(this.el_.play()),this.setTimeout(function(){e.pause();try{e.webkitEnterFullScreen()}catch(e){this.trigger("fullscreenerror",e)}},0);else try{e.webkitEnterFullScreen()}catch(e){this.trigger("fullscreenerror",e)}}exitFullScreen(){this.el_.webkitDisplayingFullscreen?this.el_.webkitExitFullScreen():this.trigger("fullscreenerror",new Error("The video is not fullscreen"))}requestPictureInPicture(){return this.el_.requestPictureInPicture()}requestVideoFrameCallback(e){return this.featuresVideoFrameCallback&&!this.el_.webkitKeys?this.el_.requestVideoFrameCallback(e):super.requestVideoFrameCallback(e)}cancelVideoFrameCallback(e){this.featuresVideoFrameCallback&&!this.el_.webkitKeys?this.el_.cancelVideoFrameCallback(e):super.cancelVideoFrameCallback(e)}src(e){if(void 0===e)return this.el_.src;this.setSrc(e)}reset(){I.resetMediaElement(this.el_)}currentSrc(){return this.currentSource_?this.currentSource_.src:this.el_.currentSrc}setControls(e){this.el_.controls=!!e}addTextTrack(e,t,s){return this.featuresNativeTextTracks?this.el_.addTextTrack(e,t,s):super.addTextTrack(e,t,s)}createRemoteTextTrack(e){var t;return this.featuresNativeTextTracks?(t=document.createElement("track"),e.kind&&(t.kind=e.kind),e.label&&(t.label=e.label),(e.language||e.srclang)&&(t.srclang=e.language||e.srclang),e.default&&(t.default=e.default),e.id&&(t.id=e.id),e.src&&(t.src=e.src),t):super.createRemoteTextTrack(e)}addRemoteTextTrack(e,t){e=super.addRemoteTextTrack(e,t);return this.featuresNativeTextTracks&&this.el().appendChild(e),e}removeRemoteTextTrack(t){if(super.removeRemoteTextTrack(t),this.featuresNativeTextTracks){var s=this.$$("track");let e=s.length;for(;e--;)t!==s[e]&&t!==s[e].track||this.el().removeChild(s[e])}}getVideoPlaybackQuality(){var e;return"function"==typeof this.el().getVideoPlaybackQuality?this.el().getVideoPlaybackQuality():(e={},"undefined"!=typeof this.el().webkitDroppedFrameCount&&"undefined"!=typeof this.el().webkitDecodedFrameCount&&(e.droppedVideoFrames=this.el().webkitDroppedFrameCount,e.totalVideoFrames=this.el().webkitDecodedFrameCount),window.performance&&(e.creationTime=window.performance.now()),e)}}Y(I,"TEST_VID",function(){var e,t;if(fe())return e=document.createElement("video"),(t=document.createElement("track")).kind="captions",t.srclang="en",t.label="English",e.appendChild(t),e}),I.isSupported=function(){try{I.TEST_VID.volume=.5}catch(e){return!1}return!(!I.TEST_VID||!I.TEST_VID.canPlayType)},I.canPlayType=function(e){return I.TEST_VID.canPlayType(e)},I.canPlaySource=function(e,t){return I.canPlayType(e.type)},I.canControlVolume=function(){try{const t=I.TEST_VID.volume;I.TEST_VID.volume=t/2+.1;var e=t!==I.TEST_VID.volume;return e&&u?(window.setTimeout(()=>{I&&I.prototype&&(I.prototype.featuresVolumeControl=t!==I.TEST_VID.volume)}),!1):e}catch(e){return!1}},I.canMuteVolume=function(){try{var e=I.TEST_VID.muted;return I.TEST_VID.muted=!e,I.TEST_VID.muted?Oe(I.TEST_VID,"muted","muted"):Ae(I.TEST_VID,"muted"),e!==I.TEST_VID.muted}catch(e){return!1}},I.canControlPlaybackRate=function(){if(o&&c&&ne<58)return!1;try{var e=I.TEST_VID.playbackRate;return I.TEST_VID.playbackRate=e/2+.1,e!==I.TEST_VID.playbackRate}catch(e){return!1}},I.canOverrideAttributes=function(){try{var e=()=>{};Object.defineProperty(document.createElement("video"),"src",{get:e,set:e}),Object.defineProperty(document.createElement("audio"),"src",{get:e,set:e}),Object.defineProperty(document.createElement("video"),"innerHTML",{get:e,set:e}),Object.defineProperty(document.createElement("audio"),"innerHTML",{get:e,set:e})}catch(e){return!1}return!0},I.supportsNativeTextTracks=function(){return me||u&&c},I.supportsNativeVideoTracks=function(){return!(!I.TEST_VID||!I.TEST_VID.videoTracks)},I.supportsNativeAudioTracks=function(){return!(!I.TEST_VID||!I.TEST_VID.audioTracks)},I.Events=["loadstart","suspend","abort","error","emptied","stalled","loadedmetadata","loadeddata","canplay","canplaythrough","playing","waiting","seeking","seeked","ended","durationchange","timeupdate","progress","play","pause","ratechange","resize","volumechange"],[["featuresMuteControl","canMuteVolume"],["featuresPlaybackRate","canControlPlaybackRate"],["featuresSourceset","canOverrideAttributes"],["featuresNativeTextTracks","supportsNativeTextTracks"],["featuresNativeVideoTracks","supportsNativeVideoTracks"],["featuresNativeAudioTracks","supportsNativeAudioTracks"]].forEach(function([e,t]){Y(I.prototype,e,()=>I[t](),!0)}),I.prototype.featuresVolumeControl=I.canControlVolume(),I.prototype.movingMediaElementInDOM=!u,I.prototype.featuresFullscreenResize=!0,I.prototype.featuresProgressEvents=!0,I.prototype.featuresTimeupdateEvents=!0,I.prototype.featuresVideoFrameCallback=!(!I.TEST_VID||!I.TEST_VID.requestVideoFrameCallback),I.disposeMediaElement=function(e){if(e){for(e.parentNode&&e.parentNode.removeChild(e);e.hasChildNodes();)e.removeChild(e.firstChild);if(e.removeAttribute("src"),"function"==typeof e.load)try{e.load()}catch(e){}}},I.resetMediaElement=function(t){if(t){var s=t.querySelectorAll("source");let e=s.length;for(;e--;)t.removeChild(s[e]);if(t.removeAttribute("src"),"function"==typeof t.load)try{t.load()}catch(e){}}},["muted","defaultMuted","autoplay","controls","loop","playsinline"].forEach(function(e){I.prototype[e]=function(){return this.el_[e]||this.el_.hasAttribute(e)}}),["muted","defaultMuted","autoplay","loop","playsinline"].forEach(function(t){I.prototype["set"+f(t)]=function(e){(this.el_[t]=e)?this.el_.setAttribute(t,t):this.el_.removeAttribute(t)}}),["paused","currentTime","buffered","volume","poster","preload","error","seeking","seekable","ended","playbackRate","defaultPlaybackRate","disablePictureInPicture","played","networkState","readyState","videoWidth","videoHeight","crossOrigin"].forEach(function(e){I.prototype[e]=function(){return this.el_[e]}}),["volume","src","poster","preload","playbackRate","defaultPlaybackRate","disablePictureInPicture","crossOrigin"].forEach(function(t){I.prototype["set"+f(t)]=function(e){this.el_[t]=e}}),["pause","load","play"].forEach(function(e){I.prototype[e]=function(){return this.el_[e]()}}),j.withSourceHandlers(I),I.nativeSourceHandler={},I.nativeSourceHandler.canPlayType=function(e){try{return I.TEST_VID.canPlayType(e)}catch(e){return""}},I.nativeSourceHandler.canHandleSource=function(e,t){return e.type?I.nativeSourceHandler.canPlayType(e.type):e.src?(e=rs(e.src),I.nativeSourceHandler.canPlayType("video/"+e)):""},I.nativeSourceHandler.handleSource=function(e,t,s){t.setSrc(e.src)},I.nativeSourceHandler.dispose=function(){},I.registerSourceHandler(I.nativeSourceHandler),j.registerTech("Html5",I);const yr=["progress","abort","suspend","emptied","stalled","loadedmetadata","loadeddata","timeupdate","resize","volumechange","texttrackchange"],fr={canplay:"CanPlay",canplaythrough:"CanPlayThrough",playing:"Playing",seeked:"Seeked"},br=["tiny","xsmall","small","medium","large","xlarge","huge"],Tr={},kr=(br.forEach(e=>{var t="x"===e.charAt(0)?"x-"+e.substring(1):e;Tr[e]="vjs-layout-"+t}),{tiny:210,xsmall:320,small:425,medium:768,large:1440,xlarge:2560,huge:1/0});class M extends T{constructor(e,t,s){if(e.id=e.id||t.id||"vjs_video_"+v++,(t=Object.assign(M.getTagSettings(e),t)).initChildren=!1,t.createEl=!1,t.evented=!1,t.reportTouchActivity=!1,t.language||(i=e.closest("[lang]"))&&(t.language=i.getAttribute("lang")),super(null,t,s),this.boundDocumentFullscreenChange_=e=>this.documentFullscreenChange_(e),this.boundFullWindowOnEscKey_=e=>this.fullWindowOnEscKey(e),this.boundUpdateStyleEl_=e=>this.updateStyleEl_(e),this.boundApplyInitTime_=e=>this.applyInitTime_(e),this.boundUpdateCurrentBreakpoint_=e=>this.updateCurrentBreakpoint_(e),this.boundHandleTechClick_=e=>this.handleTechClick_(e),this.boundHandleTechDoubleClick_=e=>this.handleTechDoubleClick_(e),this.boundHandleTechTouchStart_=e=>this.handleTechTouchStart_(e),this.boundHandleTechTouchMove_=e=>this.handleTechTouchMove_(e),this.boundHandleTechTouchEnd_=e=>this.handleTechTouchEnd_(e),this.boundHandleTechTap_=e=>this.handleTechTap_(e),this.isFullscreen_=!1,this.log=$(this.id_),this.fsApi_=F,this.isPosterFromTech_=!1,this.queuedCallbacks_=[],this.isReady_=!1,this.hasStarted_=!1,this.userActive_=!1,this.debugEnabled_=!1,this.audioOnlyMode_=!1,this.audioPosterMode_=!1,this.audioOnlyCache_={playerHeight:null,hiddenChildren:[]},!this.options_||!this.options_.techOrder||!this.options_.techOrder.length)throw new Error("No techOrder specified. Did you overwrite videojs.options instead of just changing the properties you want to override?");if(this.tag=e,this.tagAttributes=e&&Ie(e),this.language(this.options_.language),t.languages){const r={};Object.getOwnPropertyNames(t.languages).forEach(function(e){r[e.toLowerCase()]=t.languages[e]}),this.languages_=r}else this.languages_=M.prototype.options_.languages;this.resetCache_(),this.poster_=t.poster||"",this.controls_=!!t.controls,e.controls=!1,e.removeAttribute("controls"),this.changingSrc_=!1,this.playCallbacks_=[],this.playTerminatedQueue_=[],e.hasAttribute("autoplay")?this.autoplay(!0):this.autoplay(this.options_.autoplay),t.plugins&&Object.keys(t.plugins).forEach(e=>{if("function"!=typeof this[e])throw new Error(`plugin "${e}" does not exist`)}),this.scrubbing_=!1,this.el_=this.createEl(),kt(this,{eventBusKey:"el_"}),this.fsApi_.requestFullscreen&&(m(document,this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_),this.on(this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_)),this.fluid_&&this.on(["playerreset","resize"],this.boundUpdateStyleEl_);var i=h(this.options_),s=(t.plugins&&Object.keys(t.plugins).forEach(e=>{this[e](t.plugins[e])}),t.debug&&this.debug(!0),this.options_.playerOptions=i,this.middleware_=[],this.playbackRates(t.playbackRates),t.experimentalSvgIcons&&((s=(new window.DOMParser).parseFromString('\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ',"image/svg+xml")).querySelector("parsererror")?(l.warn("Failed to load SVG Icons. Falling back to Font Icons."),this.options_.experimentalSvgIcons=null):((i=s.documentElement).style.display="none",this.el_.appendChild(i),this.addClass("vjs-svg-icons-enabled"))),this.initChildren(),this.isAudio("audio"===e.nodeName.toLowerCase()),this.controls()?this.addClass("vjs-controls-enabled"):this.addClass("vjs-controls-disabled"),this.el_.setAttribute("role","region"),this.isAudio()?this.el_.setAttribute("aria-label",this.localize("Audio Player")):this.el_.setAttribute("aria-label",this.localize("Video Player")),this.isAudio()&&this.addClass("vjs-audio"),ge&&this.addClass("vjs-touch-enabled"),u||this.addClass("vjs-workinghover"),M.players[this.id_]=this,D.split(".")[0]);this.addClass("vjs-v"+s),this.userActive(!0),this.reportUserActivity(),this.one("play",e=>this.listenForUserActivity_(e)),this.on("keydown",e=>this.handleKeyDown(e)),this.on("languagechange",e=>this.handleLanguagechange(e)),this.breakpoints(this.options_.breakpoints),this.responsive(this.options_.responsive),this.on("ready",()=>{this.audioPosterMode(this.options_.audioPosterMode),this.audioOnlyMode(this.options_.audioOnlyMode)})}dispose(){var e;this.trigger("dispose"),this.off("dispose"),_(document,this.fsApi_.fullscreenchange,this.boundDocumentFullscreenChange_),_(document,"keydown",this.boundFullWindowOnEscKey_),this.styleEl_&&this.styleEl_.parentNode&&(this.styleEl_.parentNode.removeChild(this.styleEl_),this.styleEl_=null),M.players[this.id_]=null,this.tag&&this.tag.player&&(this.tag.player=null),this.el_&&this.el_.player&&(this.el_.player=null),this.tech_&&(this.tech_.dispose(),this.isPosterFromTech_=!1,this.poster_=""),this.playerElIngest_&&(this.playerElIngest_=null),this.tag&&(this.tag=null),e=this,Ss[e.id()]=null,x.names.forEach(e=>{e=this[x[e].getterName]();e&&e.off&&e.off()}),super.dispose({restoreEl:this.options_.restoreEl})}createEl(){let t=this.tag,s,e=this.playerElIngest_=t.parentNode&&t.parentNode.hasAttribute&&t.parentNode.hasAttribute("data-vjs-player");const i="video-js"===this.tag.tagName.toLowerCase(),r=(e?s=this.el_=t.parentNode:i||(s=this.el_=super.createEl("div")),Ie(t));if(i){for(s=this.el_=t,t=this.tag=document.createElement("video");s.children.length;)t.appendChild(s.firstChild);Ee(s,"video-js")||Se(s,"video-js"),s.appendChild(t),e=this.playerElIngest_=s,Object.keys(s).forEach(e=>{try{t[e]=s[e]}catch(e){}})}t.setAttribute("tabindex","-1"),r.tabindex="-1",c&&le&&(t.setAttribute("role","application"),r.role="application"),t.removeAttribute("width"),t.removeAttribute("height"),"width"in r&&delete r.width,"height"in r&&delete r.height,Object.getOwnPropertyNames(r).forEach(function(e){i&&"class"===e||s.setAttribute(e,r[e]),i&&t.setAttribute(e,r[e])}),t.playerId=t.id,t.id+="_html5_api",t.className="vjs-tech",(t.player=s.player=this).addClass("vjs-paused");var n,a=["IS_SMART_TV","IS_TIZEN","IS_WEBOS","IS_ANDROID","IS_IPAD","IS_IPHONE"].filter(e=>_e[e]).map(e=>"vjs-device-"+e.substring(3).toLowerCase().replace(/\_/g,"-")),o=(this.addClass(...a),!0!==window.VIDEOJS_NO_DYNAMIC_STYLE&&(this.styleEl_=tt("vjs-styles-dimensions"),a=Ke(".vjs-styles-defaults"),(n=Ke("head")).insertBefore(this.styleEl_,a?a.nextSibling:n.firstChild)),this.fill_=!1,this.fluid_=!1,this.width(this.options_.width),this.height(this.options_.height),this.fill(this.options_.fill),this.fluid(this.options_.fluid),this.aspectRatio(this.options_.aspectRatio),this.crossOrigin(this.options_.crossOrigin||this.options_.crossorigin),t.getElementsByTagName("a"));for(let e=0;e{this.on(["playerreset","resize"],this.boundUpdateStyleEl_)},a(e)?t():(e.eventedCallbacks||(e.eventedCallbacks=[]),e.eventedCallbacks.push(t))):this.removeClass("vjs-fluid"),this.updateStyleEl_()}fill(e){if(void 0===e)return!!this.fill_;this.fill_=!!e,e?(this.addClass("vjs-fill"),this.fluid(!1)):this.removeClass("vjs-fill")}aspectRatio(e){if(void 0===e)return this.aspectRatio_;if(!/^\d+\:\d+$/.test(e))throw new Error("Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.");this.aspectRatio_=e,this.fluid(!0),this.updateStyleEl_()}updateStyleEl_(){if(!0===window.VIDEOJS_NO_DYNAMIC_STYLE){const e="number"==typeof this.width_?this.width_:this.options_.width,t="number"==typeof this.height_?this.height_:this.options_.height;var r=this.tech_&&this.tech_.el();void(r&&(0<=e&&(r.width=e),0<=t)&&(r.height=t))}else{let e,t,s,i;r=(s=void 0!==this.aspectRatio_&&"auto"!==this.aspectRatio_?this.aspectRatio_:0{e=x[e];n[e.getterName]=this[e.privateName]}),Object.assign(n,this.options_[s]),Object.assign(n,this.options_[i]),Object.assign(n,this.options_[e.toLowerCase()]),this.tag&&(n.tag=this.tag),t&&t.src===this.cache_.src&&0{this.on(this.tech_,t,e=>this[`handleTech${f(t)}_`](e))}),Object.keys(fr).forEach(t=>{this.on(this.tech_,t,e=>{0===this.tech_.playbackRate()&&this.tech_.seeking()?this.queuedCallbacks_.push({callback:this[`handleTech${fr[t]}_`].bind(this),event:e}):this[`handleTech${fr[t]}_`](e)})}),this.on(this.tech_,"loadstart",e=>this.handleTechLoadStart_(e)),this.on(this.tech_,"sourceset",e=>this.handleTechSourceset_(e)),this.on(this.tech_,"waiting",e=>this.handleTechWaiting_(e)),this.on(this.tech_,"ended",e=>this.handleTechEnded_(e)),this.on(this.tech_,"seeking",e=>this.handleTechSeeking_(e)),this.on(this.tech_,"play",e=>this.handleTechPlay_(e)),this.on(this.tech_,"pause",e=>this.handleTechPause_(e)),this.on(this.tech_,"durationchange",e=>this.handleTechDurationChange_(e)),this.on(this.tech_,"fullscreenchange",(e,t)=>this.handleTechFullscreenChange_(e,t)),this.on(this.tech_,"fullscreenerror",(e,t)=>this.handleTechFullscreenError_(e,t)),this.on(this.tech_,"enterpictureinpicture",e=>this.handleTechEnterPictureInPicture_(e)),this.on(this.tech_,"leavepictureinpicture",e=>this.handleTechLeavePictureInPicture_(e)),this.on(this.tech_,"error",e=>this.handleTechError_(e)),this.on(this.tech_,"posterchange",e=>this.handleTechPosterChange_(e)),this.on(this.tech_,"textdata",e=>this.handleTechTextData_(e)),this.on(this.tech_,"ratechange",e=>this.handleTechRateChange_(e)),this.on(this.tech_,"loadedmetadata",this.boundUpdateStyleEl_),this.usingNativeControls(this.techGet_("controls")),this.controls()&&!this.usingNativeControls()&&this.addTechControlsListeners_(),this.tech_.el().parentNode===this.el()||"Html5"===s&&this.tag||we(this.tech_.el(),this.el()),this.tag&&(this.tag.player=null,this.tag=null)}unloadTech_(){x.names.forEach(e=>{e=x[e];this[e.privateName]=this[e.getterName]()}),this.textTracksJson_=zt(this.tech_),this.isReady_=!1,this.tech_.dispose(),this.tech_=!1,this.isPosterFromTech_&&(this.poster_="",this.trigger("posterchange")),this.isPosterFromTech_=!1}tech(e){return void 0===e&&l.warn("Using the tech directly can be dangerous. I hope you know what you're doing.\nSee https://github.com/videojs/video.js/issues/2617 for more info.\n"),this.tech_}version(){return{"video.js":D}}addTechControlsListeners_(){this.removeTechControlsListeners_(),this.on(this.tech_,"click",this.boundHandleTechClick_),this.on(this.tech_,"dblclick",this.boundHandleTechDoubleClick_),this.on(this.tech_,"touchstart",this.boundHandleTechTouchStart_),this.on(this.tech_,"touchmove",this.boundHandleTechTouchMove_),this.on(this.tech_,"touchend",this.boundHandleTechTouchEnd_),this.on(this.tech_,"tap",this.boundHandleTechTap_)}removeTechControlsListeners_(){this.off(this.tech_,"tap",this.boundHandleTechTap_),this.off(this.tech_,"touchstart",this.boundHandleTechTouchStart_),this.off(this.tech_,"touchmove",this.boundHandleTechTouchMove_),this.off(this.tech_,"touchend",this.boundHandleTechTouchEnd_),this.off(this.tech_,"click",this.boundHandleTechClick_),this.off(this.tech_,"dblclick",this.boundHandleTechDoubleClick_)}handleTechReady_(){this.triggerReady(),this.cache_.volume&&this.techCall_("setVolume",this.cache_.volume),this.handleTechPosterChange_(),this.handleTechDurationChange_()}handleTechLoadStart_(){this.removeClass("vjs-ended","vjs-seeking"),this.error(null),this.handleTechDurationChange_(),this.paused()&&this.hasStarted(!1),this.trigger("loadstart"),this.manualAutoplay_(!0===this.autoplay()&&this.options_.normalizeAutoplay?"play":this.autoplay())}manualAutoplay_(t){if(this.tech_&&"string"==typeof t){var s=()=>{const e=this.muted(),t=(this.muted(!0),()=>{this.muted(e)});this.playTerminatedQueue_.push(t);var s=this.play();if(Ft(s))return s.catch(e=>{throw t(),new Error("Rejection at manualAutoplay. Restoring muted value. "+(e||""))})};let e;if("any"!==t||this.muted()?e="muted"!==t||this.muted()?this.play():s():Ft(e=this.play())&&(e=e.catch(s)),Ft(e))return e.then(()=>{this.trigger({type:"autoplay-success",autoplay:t})}).catch(()=>{this.trigger({type:"autoplay-failure",autoplay:t})})}}updateSourceCaches_(e=""){let t=e,s="";"string"!=typeof t&&(t=e.src,s=e.type),this.cache_.source=this.cache_.source||{},this.cache_.sources=this.cache_.sources||[],t&&!s&&(s=((e,t)=>{if(!t)return"";if(e.cache_.source.src===t&&e.cache_.source.type)return e.cache_.source.type;var s=e.cache_.sources.filter(e=>e.src===t);if(s.length)return s[0].type;var i=e.$$("source");for(let e=0;ee.src&&e.src===t),i=[],r=this.$$("source"),n=[];for(let e=0;ethis.updateSourceCaches_(e);var s=this.currentSource().src,i=t.src;(e=!s||/^blob:/.test(s)||!/^blob:/.test(i)||this.lastSource_&&(this.lastSource_.tech===i||this.lastSource_.player===s)?e:()=>{})(i),t.src||this.tech_.any(["sourceset","loadstart"],e=>{"sourceset"!==e.type&&(e=this.techGet_("currentSrc"),this.lastSource_.tech=e,this.updateSourceCaches_(e))})}this.lastSource_={player:this.currentSource().src,tech:t.src},this.trigger({src:t.src,type:"sourceset"})}hasStarted(e){if(void 0===e)return this.hasStarted_;e!==this.hasStarted_&&(this.hasStarted_=e,this.hasStarted_?this.addClass("vjs-has-started"):this.removeClass("vjs-has-started"))}handleTechPlay_(){this.removeClass("vjs-ended","vjs-paused"),this.addClass("vjs-playing"),this.hasStarted(!0),this.trigger("play")}handleTechRateChange_(){0e.callback(e.event)),this.queuedCallbacks_=[]),this.cache_.lastPlaybackRate=this.tech_.playbackRate(),this.trigger("ratechange")}handleTechWaiting_(){this.addClass("vjs-waiting"),this.trigger("waiting");const e=this.currentTime(),t=()=>{e!==this.currentTime()&&(this.removeClass("vjs-waiting"),this.off("timeupdate",t))};this.on("timeupdate",t)}handleTechCanPlay_(){this.removeClass("vjs-waiting"),this.trigger("canplay")}handleTechCanPlayThrough_(){this.removeClass("vjs-waiting"),this.trigger("canplaythrough")}handleTechPlaying_(){this.removeClass("vjs-waiting"),this.trigger("playing")}handleTechSeeking_(){this.addClass("vjs-seeking"),this.trigger("seeking")}handleTechSeeked_(){this.removeClass("vjs-seeking","vjs-ended"),this.trigger("seeked")}handleTechPause_(){this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.trigger("pause")}handleTechEnded_(){this.addClass("vjs-ended"),this.removeClass("vjs-waiting"),this.options_.loop?(this.currentTime(0),this.play()):this.paused()||this.pause(),this.trigger("ended")}handleTechDurationChange_(){this.duration(this.techGet_("duration"))}handleTechClick_(e){!this.controls_||void 0!==this.options_&&void 0!==this.options_.userActions&&void 0!==this.options_.userActions.click&&!1===this.options_.userActions.click||(void 0!==this.options_&&void 0!==this.options_.userActions&&"function"==typeof this.options_.userActions.click?this.options_.userActions.click.call(this,e):this.paused()?w(this.play()):this.pause())}handleTechDoubleClick_(t){!this.controls_||Array.prototype.some.call(this.$$(".vjs-control-bar, .vjs-modal-dialog"),e=>e.contains(t.target))||void 0!==this.options_&&void 0!==this.options_.userActions&&void 0!==this.options_.userActions.doubleClick&&!1===this.options_.userActions.doubleClick||(void 0!==this.options_&&void 0!==this.options_.userActions&&"function"==typeof this.options_.userActions.doubleClick?this.options_.userActions.doubleClick.call(this,t):this.isFullscreen()?this.exitFullscreen():this.requestFullscreen())}handleTechTap_(){this.userActive(!this.userActive())}handleTechTouchStart_(){this.userWasActive=this.userActive()}handleTechTouchMove_(){this.userWasActive&&this.reportUserActivity()}handleTechTouchEnd_(e){e.cancelable&&e.preventDefault()}toggleFullscreenClass_(){this.isFullscreen()?this.addClass("vjs-fullscreen"):this.removeClass("vjs-fullscreen")}documentFullscreenChange_(t){t=t.target.player;if(!t||t===this){t=this.el();let e=document[this.fsApi_.fullscreenElement]===t;!e&&t.matches&&(e=t.matches(":"+this.fsApi_.fullscreen)),this.isFullscreen(e)}}handleTechFullscreenChange_(e,t){t&&(t.nativeIOSFullscreen&&(this.addClass("vjs-ios-native-fs"),this.tech_.one("webkitendfullscreen",()=>{this.removeClass("vjs-ios-native-fs")})),this.isFullscreen(t.isFullscreen))}handleTechFullscreenError_(e,t){this.trigger("fullscreenerror",t)}togglePictureInPictureClass_(){this.isInPictureInPicture()?this.addClass("vjs-picture-in-picture"):this.removeClass("vjs-picture-in-picture")}handleTechEnterPictureInPicture_(e){this.isInPictureInPicture(!0)}handleTechLeavePictureInPicture_(e){this.isInPictureInPicture(!1)}handleTechError_(){var e=this.tech_.error();e&&this.error(e)}handleTechTextData_(){let e=1{this.play_(e)})}play_(e=w){this.playCallbacks_.push(e);var t,e=Boolean(!this.changingSrc_&&(this.src()||this.currentSrc())),s=Boolean(me||u);this.waitToPlay_&&(this.off(["ready","loadstart"],this.waitToPlay_),this.waitToPlay_=null),this.isReady_&&e?(t=this.techGet_("play"),s&&this.hasClass("vjs-ended")&&this.resetProgressBar_(),null===t?this.runPlayTerminatedQueue_():this.runPlayCallbacks_(t)):(this.waitToPlay_=e=>{this.play_()},this.one(["ready","loadstart"],this.waitToPlay_),!e&&s&&this.load())}runPlayTerminatedQueue_(){var e=this.playTerminatedQueue_.slice(0);this.playTerminatedQueue_=[],e.forEach(function(e){e()})}runPlayCallbacks_(t){var e=this.playCallbacks_.slice(0);this.playCallbacks_=[],this.playTerminatedQueue_=[],e.forEach(function(e){e(t)})}pause(){this.techCall_("pause")}paused(){return!1!==this.techGet_("paused")}played(){return this.techGet_("played")||k(0,0)}scrubbing(e){if("undefined"==typeof e)return this.scrubbing_;this.scrubbing_=!!e,this.techCall_("setScrubbing",this.scrubbing_),e?this.addClass("vjs-scrubbing"):this.removeClass("vjs-scrubbing")}currentTime(e){if(void 0===e)return this.cache_.currentTime=this.techGet_("currentTime")||0,this.cache_.currentTime;e<0&&(e=0),this.isReady_&&!this.changingSrc_&&this.tech_&&this.tech_.isReady_?(this.techCall_("setCurrentTime",e),this.cache_.initTime=0,isFinite(e)&&(this.cache_.currentTime=Number(e))):(this.cache_.initTime=e,this.off("canplay",this.boundApplyInitTime_),this.one("canplay",this.boundApplyInitTime_))}applyInitTime_(){this.currentTime(this.cache_.initTime)}duration(e){if(void 0===e)return void 0!==this.cache_.duration?this.cache_.duration:NaN;(e=(e=parseFloat(e))<0?1/0:e)!==this.cache_.duration&&((this.cache_.duration=e)===1/0?this.addClass("vjs-live"):this.removeClass("vjs-live"),isNaN(e)||this.trigger("durationchange"))}remainingTime(){return this.duration()-this.currentTime()}remainingTimeDisplay(){return Math.floor(this.duration())-Math.floor(this.currentTime())}buffered(){let e=this.techGet_("buffered");return e=e&&e.length?e:k(0,0)}seekable(){let e=this.techGet_("seekable");return e=e&&e.length?e:k(0,0)}seeking(){return this.techGet_("seeking")}ended(){return this.techGet_("ended")}networkState(){return this.techGet_("networkState")}readyState(){return this.techGet_("readyState")}bufferedPercent(){return Bt(this.buffered(),this.duration())}bufferedEnd(){var e=this.buffered(),t=this.duration();let s=e.end(e.length-1);return s=s>t?t:s}volume(e){let t;if(void 0===e)return t=parseFloat(this.techGet_("volume")),isNaN(t)?1:t;t=Math.max(0,Math.min(1,e)),this.cache_.volume=t,this.techCall_("setVolume",t),0{function i(){o.off("fullscreenerror",r),o.off("fullscreenchange",t)}function t(){i(),e()}function r(e,t){i(),s(t)}o.one("fullscreenchange",t),o.one("fullscreenerror",r);var n=o.requestFullscreenHelper_(a);n&&(n.then(i,i),n.then(e,s))})}requestFullscreenHelper_(e){let t;if(this.fsApi_.prefixed||(t=this.options_.fullscreen&&this.options_.fullscreen.options||{},void 0!==e&&(t=e)),this.fsApi_.requestFullscreen)return(e=this.el_[this.fsApi_.requestFullscreen](t))&&e.then(()=>this.isFullscreen(!0),()=>this.isFullscreen(!1)),e;this.tech_.supportsFullScreen()&&!0==!this.options_.preferFullWindow?this.techCall_("enterFullScreen"):this.enterFullWindow()}exitFullscreen(){const a=this;return new Promise((e,s)=>{function i(){a.off("fullscreenerror",r),a.off("fullscreenchange",t)}function t(){i(),e()}function r(e,t){i(),s(t)}a.one("fullscreenchange",t),a.one("fullscreenerror",r);var n=a.exitFullscreenHelper_();n&&(n.then(i,i),n.then(e,s))})}exitFullscreenHelper_(){var e;if(this.fsApi_.requestFullscreen)return(e=document[this.fsApi_.exitFullscreen]())&&w(e.then(()=>this.isFullscreen(!1))),e;this.tech_.supportsFullScreen()&&!0==!this.options_.preferFullWindow?this.techCall_("exitFullScreen"):this.exitFullWindow()}enterFullWindow(){this.isFullscreen(!0),this.isFullWindow=!0,this.docOrigOverflow=document.documentElement.style.overflow,m(document,"keydown",this.boundFullWindowOnEscKey_),document.documentElement.style.overflow="hidden",Se(document.body,"vjs-full-window"),this.trigger("enterFullWindow")}fullWindowOnEscKey(e){b.isEventKey(e,"Esc")&&!0===this.isFullscreen()&&(this.isFullWindow?this.exitFullWindow():this.exitFullscreen())}exitFullWindow(){this.isFullscreen(!1),this.isFullWindow=!1,_(document,"keydown",this.boundFullWindowOnEscKey_),document.documentElement.style.overflow=this.docOrigOverflow,xe(document.body,"vjs-full-window"),this.trigger("exitFullWindow")}disablePictureInPicture(e){if(void 0===e)return this.techGet_("disablePictureInPicture");this.techCall_("setDisablePictureInPicture",e),this.options_.disablePictureInPicture=e,this.trigger("disablepictureinpicturechanged")}isInPictureInPicture(e){if(void 0===e)return!!this.isInPictureInPicture_;this.isInPictureInPicture_=!!e,this.togglePictureInPictureClass_()}requestPictureInPicture(){if(this.options_.enableDocumentPictureInPicture&&window.documentPictureInPicture){const t=document.createElement(this.el().tagName);return t.classList=this.el().classList,t.classList.add("vjs-pip-container"),this.posterImage&&t.appendChild(this.posterImage.el().cloneNode(!0)),this.titleBar&&t.appendChild(this.titleBar.el().cloneNode(!0)),t.appendChild(p("p",{className:"vjs-pip-text"},{},this.localize("Playing in picture-in-picture"))),window.documentPictureInPicture.requestWindow({width:this.videoWidth(),height:this.videoHeight()}).then(e=>(Ge(e),this.el_.parentNode.insertBefore(t,this.el_),e.document.body.appendChild(this.el_),e.document.body.classList.add("vjs-pip-window"),this.player_.isInPictureInPicture(!0),this.player_.trigger({type:"enterpictureinpicture",pipWindow:e}),e.addEventListener("pagehide",e=>{e=e.target.querySelector(".video-js");t.parentNode.replaceChild(e,t),this.player_.isInPictureInPicture(!1),this.player_.trigger("leavepictureinpicture")}),e))}return"pictureInPictureEnabled"in document&&!1===this.disablePictureInPicture()?this.techGet_("requestPictureInPicture"):Promise.reject("No PiP mode is available")}exitPictureInPicture(){return window.documentPictureInPicture&&window.documentPictureInPicture.window?(window.documentPictureInPicture.window.close(),Promise.resolve()):"pictureInPictureEnabled"in document?document.exitPictureInPicture():void 0}handleKeyDown(e){var t,s,i=this.options_["userActions"];i&&i.hotkeys&&(t=this.el_.ownerDocument.activeElement,s=t.tagName.toLowerCase(),t.isContentEditable||("input"===s?-1===["button","checkbox","hidden","radio","reset","submit"].indexOf(t.type):-1!==["textarea"].indexOf(s))||("function"==typeof i.hotkeys?i.hotkeys.call(this,e):this.handleHotkeys(e)))}handleHotkeys(e){var{fullscreenKey:t=e=>b.isEventKey(e,"f"),muteKey:s=e=>b.isEventKey(e,"m"),playPauseKey:i=e=>b.isEventKey(e,"k")||b.isEventKey(e,"Space")}=this.options_.userActions?this.options_.userActions.hotkeys:{};t.call(this,e)?(e.preventDefault(),e.stopPropagation(),t=T.getComponent("FullscreenToggle"),!1!==document[this.fsApi_.fullscreenEnabled]&&t.prototype.handleClick.call(this,e)):s.call(this,e)?(e.preventDefault(),e.stopPropagation(),T.getComponent("MuteToggle").prototype.handleClick.call(this,e)):i.call(this,e)&&(e.preventDefault(),e.stopPropagation(),T.getComponent("PlayToggle").prototype.handleClick.call(this,e))}canPlayType(i){var r;for(let t=0,s=this.options_.techOrder;ts.some(e=>{if(r=i(t,e))return!0})),r}var s=this.options_.techOrder.map(e=>[e,j.getTech(e)]).filter(([e,t])=>t?t.isSupported():(l.error(`The "${e}" tech is undefined. Skipped browser support check for that tech.`),!1));let i;var r,n=([e,t],s)=>{if(t.canPlaySource(s,this.options_[e.toLowerCase()]))return{source:s,tech:e}};return(i=this.options_.sourceOrder?t(e,s,(r=n,(e,t)=>r(t,e))):t(s,e,n))||!1}handleSrc_(e,i){if("undefined"==typeof e)return this.cache_.src||"";this.resetRetryOnError_&&this.resetRetryOnError_();const r=Ns(e);if(r.length){if(this.changingSrc_=!0,i||(this.cache_.sources=r),this.updateSourceCaches_(r[0]),js(this,r[0],(e,t)=>{var s;if(this.middleware_=t,i||(this.cache_.sources=r),this.updateSourceCaches_(e),this.src_(e))return 1e.setTech&&e.setTech(s))}),1{this.error(null),this.handleSrc_(r.slice(1),!0)},s=()=>{this.off("error",t)};this.one("error",t),this.one("playing",s),this.resetRetryOnError_=()=>{this.off("error",t),this.off("playing",s)}}}else this.setTimeout(function(){this.error({code:4,message:this.options_.notSupportedMessage})},0)}src(e){return this.handleSrc_(e,!1)}src_(e){var t=this.selectSource([e]);return!t||(St(t.tech,this.techName_)?this.ready(function(){this.tech_.constructor.prototype.hasOwnProperty("setSource")?this.techCall_("setSource",e):this.techCall_("src",e.src),this.changingSrc_=!1},!0):(this.changingSrc_=!0,this.loadTech_(t.tech,t.source),this.tech_.ready(()=>{this.changingSrc_=!1})),!1)}load(){this.tech_&&this.tech_.vhs?this.src(this.currentSource()):this.techCall_("load")}reset(){this.paused()?this.doReset_():w(this.play().then(()=>this.doReset_()))}doReset_(){this.tech_&&this.tech_.clearTracks("text"),this.removeClass("vjs-playing"),this.addClass("vjs-paused"),this.resetCache_(),this.poster(""),this.loadTech_(this.options_.techOrder[0],null),this.techCall_("reset"),this.resetControlBarUI_(),this.error(null),this.titleBar&&this.titleBar.update({title:void 0,description:void 0}),a(this)&&this.trigger("playerreset")}resetControlBarUI_(){this.resetProgressBar_(),this.resetPlaybackRate_(),this.resetVolumeBar_()}resetProgressBar_(){this.currentTime(0);var{currentTimeDisplay:e,durationDisplay:t,progressControl:s,remainingTimeDisplay:i}=this.controlBar||{},s=(s||{})["seekBar"];e&&e.updateContent(),t&&t.updateContent(),i&&i.updateContent(),s&&(s.update(),s.loadProgressBar)&&s.loadProgressBar.update()}resetPlaybackRate_(){this.playbackRate(this.defaultPlaybackRate()),this.handleTechRateChange_()}resetVolumeBar_(){this.volume(1),this.trigger("volumechange")}currentSources(){var e=this.currentSource(),t=[];return 0!==Object.keys(e).length&&t.push(e),this.cache_.sources||t}currentSource(){return this.cache_.source||{}}currentSrc(){return this.currentSource()&&this.currentSource().src||""}currentType(){return this.currentSource()&&this.currentSource().type||""}preload(e){if(void 0===e)return this.techGet_("preload");this.techCall_("setPreload",e),this.options_.preload=e}autoplay(e){if(void 0===e)return this.options_.autoplay||!1;let t;"string"==typeof e&&/(any|play|muted)/.test(e)||!0===e&&this.options_.normalizeAutoplay?(this.options_.autoplay=e,this.manualAutoplay_("string"==typeof e?e:"play"),t=!1):this.options_.autoplay=!!e,t="undefined"==typeof t?this.options_.autoplay:t,this.tech_&&this.techCall_("setAutoplay",t)}playsinline(e){return void 0!==e&&(this.techCall_("setPlaysinline",e),this.options_.playsinline=e),this.techGet_("playsinline")}loop(e){if(void 0===e)return this.techGet_("loop");this.techCall_("setLoop",e),this.options_.loop=e}poster(e){if(void 0===e)return this.poster_;(e=e||"")!==this.poster_&&(this.poster_=e,this.techCall_("setPoster",e),this.isPosterFromTech_=!1,this.trigger("posterchange"))}handleTechPosterChange_(){var e;(!this.poster_||this.options_.techCanOverridePoster)&&this.tech_&&this.tech_.poster&&(e=this.tech_.poster()||"")!==this.poster_&&(this.poster_=e,this.isPosterFromTech_=!0,this.trigger("posterchange"))}controls(e){if(void 0===e)return!!this.controls_;this.controls_!==(e=!!e)&&(this.controls_=e,this.usingNativeControls()&&this.techCall_("setControls",e),this.controls_?(this.removeClass("vjs-controls-disabled"),this.addClass("vjs-controls-enabled"),this.trigger("controlsenabled"),this.usingNativeControls()||this.addTechControlsListeners_()):(this.removeClass("vjs-controls-enabled"),this.addClass("vjs-controls-disabled"),this.trigger("controlsdisabled"),this.usingNativeControls()||this.removeTechControlsListeners_()))}usingNativeControls(e){if(void 0===e)return!!this.usingNativeControls_;this.usingNativeControls_!==(e=!!e)&&(this.usingNativeControls_=e,this.usingNativeControls_?(this.addClass("vjs-using-native-controls"),this.trigger("usingnativecontrols")):(this.removeClass("vjs-using-native-controls"),this.trigger("usingcustomcontrols")))}error(t){if(void 0===t)return this.error_||null;if(B("beforeerror").forEach(e=>{e=e(this,t);n(e)&&!Array.isArray(e)||"string"==typeof e||"number"==typeof e||null===e?t=e:this.log.error("please return a value that MediaError expects in beforeerror hooks")}),this.options_.suppressNotSupportedError&&t&&4===t.code){const e=function(){this.error(t)};this.options_.suppressNotSupportedError=!1,this.any(["click","touchstart"],e),void this.one("loadstart",function(){this.off(["click","touchstart"],e)})}else null===t?(this.error_=null,this.removeClass("vjs-error"),this.errorDisplay&&this.errorDisplay.close()):(this.error_=new C(t),this.addClass("vjs-error"),l.error(`(CODE:${this.error_.code} ${C.errorTypes[this.error_.code]})`,this.error_.message,this.error_),this.trigger("error"),B("error").forEach(e=>e(this,this.error_)))}reportUserActivity(e){this.userActivity_=!0}userActive(e){if(void 0===e)return this.userActive_;(e=!!e)!==this.userActive_&&(this.userActive_=e,this.userActive_?(this.userActivity_=!0,this.removeClass("vjs-user-inactive"),this.addClass("vjs-user-active"),this.trigger("useractive")):(this.tech_&&this.tech_.one("mousemove",function(e){e.stopPropagation(),e.preventDefault()}),this.userActivity_=!1,this.removeClass("vjs-user-active"),this.addClass("vjs-user-inactive"),this.trigger("userinactive")))}listenForUserActivity_(){let t,s,i;const r=y(this,this.reportUserActivity);function e(e){r(),this.clearInterval(t)}this.on("mousedown",function(){r(),this.clearInterval(t),t=this.setInterval(r,250)}),this.on("mousemove",function(e){e.screenX===s&&e.screenY===i||(s=e.screenX,i=e.screenY,r())}),this.on("mouseup",e),this.on("mouseleave",e);var n=this.getChild("controlBar");!n||u||o||(n.on("mouseenter",function(e){0!==this.player().options_.inactivityTimeout&&(this.player().cache_.inactivityTimeout=this.player().options_.inactivityTimeout),this.player().options_.inactivityTimeout=0}),n.on("mouseleave",function(e){this.player().options_.inactivityTimeout=this.player().cache_.inactivityTimeout})),this.on("keydown",r),this.on("keyup",r);let a;this.setInterval(function(){var e;this.userActivity_&&(this.userActivity_=!1,this.userActive(!0),this.clearTimeout(a),(e=this.options_.inactivityTimeout)<=0||(a=this.setTimeout(function(){this.userActivity_||this.userActive(!1)},e)))},250)}playbackRate(e){if(void 0===e)return this.tech_&&this.tech_.featuresPlaybackRate?this.cache_.lastPlaybackRate||this.techGet_("playbackRate"):1;this.techCall_("setPlaybackRate",e)}defaultPlaybackRate(e){return void 0!==e?this.techCall_("setDefaultPlaybackRate",e):this.tech_&&this.tech_.featuresPlaybackRate?this.techGet_("defaultPlaybackRate"):1}isAudio(e){if(void 0===e)return!!this.isAudio_;this.isAudio_=!!e}enableAudioOnlyUI_(){this.addClass("vjs-audio-only-mode");var e=this.children();const t=this.getChild("ControlBar");var s=t&&t.currentHeight();e.forEach(e=>{e!==t&&e.el_&&!e.hasClass("vjs-hidden")&&(e.hide(),this.audioOnlyCache_.hiddenChildren.push(e))}),this.audioOnlyCache_.playerHeight=this.currentHeight(),this.height(s),this.trigger("audioonlymodechange")}disableAudioOnlyUI_(){this.removeClass("vjs-audio-only-mode"),this.audioOnlyCache_.hiddenChildren.forEach(e=>e.show()),this.height(this.audioOnlyCache_.playerHeight),this.trigger("audioonlymodechange")}audioOnlyMode(e){return"boolean"!=typeof e||e===this.audioOnlyMode_?this.audioOnlyMode_:(this.audioOnlyMode_=e)?(e=[],this.isInPictureInPicture()&&e.push(this.exitPictureInPicture()),this.isFullscreen()&&e.push(this.exitFullscreen()),this.audioPosterMode()&&e.push(this.audioPosterMode(!1)),Promise.all(e).then(()=>this.enableAudioOnlyUI_())):Promise.resolve().then(()=>this.disableAudioOnlyUI_())}enablePosterModeUI_(){(this.tech_&&this.tech_).hide(),this.addClass("vjs-audio-poster-mode"),this.trigger("audiopostermodechange")}disablePosterModeUI_(){(this.tech_&&this.tech_).show(),this.removeClass("vjs-audio-poster-mode"),this.trigger("audiopostermodechange")}audioPosterMode(e){return"boolean"!=typeof e||e===this.audioPosterMode_?this.audioPosterMode_:(this.audioPosterMode_=e)?(this.audioOnlyMode()?this.audioOnlyMode(!1):Promise.resolve()).then(()=>{this.enablePosterModeUI_()}):Promise.resolve().then(()=>{this.disablePosterModeUI_()})}addTextTrack(e,t,s){if(this.tech_)return this.tech_.addTextTrack(e,t,s)}addRemoteTextTrack(e,t){if(this.tech_)return this.tech_.addRemoteTextTrack(e,t)}removeRemoteTextTrack(e={}){let t=e["track"];if(t=t||e,this.tech_)return this.tech_.removeRemoteTextTrack(t)}getVideoPlaybackQuality(){return this.techGet_("getVideoPlaybackQuality")}videoWidth(){return this.tech_&&this.tech_.videoWidth&&this.tech_.videoWidth()||0}videoHeight(){return this.tech_&&this.tech_.videoHeight&&this.tech_.videoHeight()||0}language(e){if(void 0===e)return this.language_;this.language_!==String(e).toLowerCase()&&(this.language_=String(e).toLowerCase(),a(this))&&this.trigger("languagechange")}languages(){return h(M.prototype.options_.languages,this.languages_)}toJSON(){var t=h(this.options_),s=t.tracks;t.tracks=[];for(let e=0;e{this.removeChild(s)}),s.open(),s}updateCurrentBreakpoint_(){if(this.responsive()){var t=this.currentBreakpoint(),s=this.currentWidth();for(let e=0;ethis.addRemoteTextTrack(e,!1)),this.titleBar&&this.titleBar.update({title:l,description:r||e||""}),this.ready(t))}getMedia(){var e,t;return this.cache_.media?h(this.cache_.media):(e=this.poster(),t={src:this.currentSources(),textTracks:Array.prototype.map.call(this.remoteTextTracks(),e=>({kind:e.kind,label:e.label,language:e.language,src:e.src}))},e&&(t.poster=e,t.artwork=[{src:t.poster,type:Bs(t.poster)}]),t)}static getTagSettings(e){var t,s={sources:[],tracks:[]},i=Ie(e),r=i["data-setup"];if(Ee(e,"vjs-fill")&&(i.fill=!0),Ee(e,"vjs-fluid")&&(i.fluid=!0),null!==r&&([r,t]=Rt(r||"{}"),r&&l.error(r),Object.assign(i,t)),Object.assign(s,i),e.hasChildNodes()){var n=e.childNodes;for(let e=0,t=n.length;e"number"==typeof e)&&(this.cache_.playbackRates=e,this.trigger("playbackrateschange"))}}x.names.forEach(function(e){const t=x[e];M.prototype[t.getterName]=function(){return this.tech_?this.tech_[t.getterName]():(this[t.privateName]=this[t.privateName]||new t.ListClass,this[t.privateName])}}),M.prototype.crossorigin=M.prototype.crossOrigin,M.players={};Zi=window.navigator;M.prototype.options_={techOrder:j.defaultTechOrder_,html5:{},enableSourceset:!0,inactivityTimeout:2e3,playbackRates:[],liveui:!1,children:["mediaLoader","posterImage","titleBar","textTrackDisplay","loadingSpinner","bigPlayButton","liveTracker","controlBar","errorDisplay","textTrackSettings","resizeManager"],language:Zi&&(Zi.languages&&Zi.languages[0]||Zi.userLanguage||Zi.language)||"en",languages:{},notSupportedMessage:"No compatible source was found for this media.",normalizeAutoplay:!1,fullscreen:{options:{navigationUI:"hide"}},breakpoints:{},responsive:!1,audioOnlyMode:!1,audioPosterMode:!1,enableSmoothSeeking:!1},yr.forEach(function(e){M.prototype[`handleTech${f(e)}_`]=function(){return this.trigger(e)}}),T.registerComponent("Player",M);function Cr(t,s){function i(){Ir(this,{name:t,plugin:s,instance:null},!0);var e=s.apply(this,arguments);return Pr(this,t),Ir(this,{name:t,plugin:s,instance:e}),e}return Object.keys(s).forEach(function(e){i[e]=s[e]}),i}const wr="plugin",Er="activePlugins_",Sr={},xr=e=>Sr.hasOwnProperty(e),jr=e=>xr(e)?Sr[e]:void 0,Pr=(e,t)=>{e[Er]=e[Er]||{},e[Er][t]=!0},Ir=(e,t,s)=>{s=(s?"before":"")+"pluginsetup";e.trigger(s,t),e.trigger(s+":"+t.name,t)},Mr=(s,i)=>(i.prototype.name=s,function(...e){Ir(this,{name:s,plugin:i,instance:null},!0);const t=new i(this,...e);return this[s]=()=>t,Ir(this,t.getEventHash()),t});class O{constructor(e){if(this.constructor===O)throw new Error("Plugin must be sub-classed; not directly instantiated.");this.player=e,this.log||(this.log=this.player.log.createLogger(this.name)),kt(this),delete this.trigger,wt(this,this.constructor.defaultState),Pr(e,this.name),this.dispose=this.dispose.bind(this),e.on("dispose",this.dispose)}version(){return this.constructor.VERSION}getEventHash(e={}){return e.name=this.name,e.plugin=this.constructor,e.instance=this,e}trigger(e,t={}){return lt(this.eventBusEl_,e,this.getEventHash(t))}handleStateChanged(e){}dispose(){var{name:e,player:t}=this;this.trigger("dispose"),this.off(),t.off("dispose",this.dispose),t[Er][e]=!1,this.player=this.state=null,t[e]=Mr(e,Sr[e])}static isBasic(e){e="string"==typeof e?jr(e):e;return"function"==typeof e&&!O.prototype.isPrototypeOf(e.prototype)}static registerPlugin(e,t){if("string"!=typeof e)throw new Error(`Illegal plugin name, "${e}", must be a string, was ${typeof e}.`);if(xr(e))l.warn(`A plugin named "${e}" already exists. You may want to avoid re-registering plugins!`);else if(M.prototype.hasOwnProperty(e))throw new Error(`Illegal plugin name, "${e}", cannot share a name with an existing player method!`);if("function"!=typeof t)throw new Error(`Illegal plugin for "${e}", must be a function, was ${typeof t}.`);return Sr[e]=t,e!==wr&&(O.isBasic(t)?M.prototype[e]=Cr(e,t):M.prototype[e]=Mr(e,t)),t}static deregisterPlugin(e){if(e===wr)throw new Error("Cannot de-register base plugin.");xr(e)&&(delete Sr[e],delete M.prototype[e])}static getPlugins(e=Object.keys(Sr)){let s;return e.forEach(e=>{var t=jr(e);t&&((s=s||{})[e]=t)}),s}static getPluginVersion(e){e=jr(e);return e&&e.VERSION||""}}function A(e,s,i,r){{var n=s+` is deprecated and will be removed in ${e}.0; please use ${i} instead.`,a=r;let t=!1;return function(...e){return t||l.warn(n),t=!0,a.apply(this,e)}}}O.getPlugin=jr,O.BASE_PLUGIN_NAME=wr,O.registerPlugin(wr,O),M.prototype.usingPlugin=function(e){return!!this[Er]&&!0===this[Er][e]},M.prototype.hasPlugin=function(e){return!!xr(e)};const Or=e=>0===e.indexOf("#")?e.slice(1):e;function L(e,t,s){let i=L.getPlayer(e);if(i)t&&l.warn(`Player "${e}" is already initialised. Options will not be applied.`),s&&i.ready(s);else{const r="string"==typeof e?Ke("#"+Or(e)):e;if(!be(r))throw new TypeError("The element or ID supplied is not valid. (videojs)");e="getRootNode"in r&&r.getRootNode()instanceof window.ShadowRoot?r.getRootNode():r.ownerDocument.body,e=(r.ownerDocument.defaultView&&e.contains(r)||l.warn("The element supplied is not included in the DOM"),!0===(t=t||{}).restoreEl&&(t.restoreEl=(r.parentNode&&r.parentNode.hasAttribute("data-vjs-player")?r.parentNode:r).cloneNode(!0)),B("beforesetup").forEach(e=>{e=e(r,h(t));!n(e)||Array.isArray(e)?l.error("please return an object in beforesetup hooks"):t=h(t,e)}),T.getComponent("Player"));i=new e(r,t,s),B("setup").forEach(e=>e(i))}return i}return L.hooks_=s,L.hooks=B,L.hook=function(e,t){B(e,t)},L.hookOnce=function(i,e){B(i,[].concat(e).map(t=>{const s=(...e)=>(R(i,s),t(...e));return s}))},L.removeHook=R,!0!==window.VIDEOJS_NO_DYNAMIC_STYLE&&fe()&&!(Wi=Ke(".vjs-styles-defaults"))&&(Wi=tt("vjs-styles-defaults"),(Ji=Ke("head"))&&Ji.insertBefore(Wi,Ji.firstChild),st(Wi,`
- .video-js {
- width: 300px;
- height: 150px;
- }
-
- .vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: 56.25%
- }
- `)),Ze(1,L),L.VERSION=D,L.options=M.prototype.options_,L.getPlayers=()=>M.players,L.getPlayer=e=>{var t=M.players;let s;if("string"==typeof e){var i=Or(e),r=t[i];if(r)return r;s=Ke("#"+i)}else s=e;if(be(s)){var{player:r,playerId:i}=s;if(r||t[i])return r||t[i]}},L.getAllPlayers=()=>Object.keys(M.players).map(e=>M.players[e]).filter(Boolean),L.players=M.players,L.getComponent=T.getComponent,L.registerComponent=(e,t)=>(j.isTech(t)&&l.warn(`The ${e} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`),T.registerComponent.call(T,e,t)),L.getTech=j.getTech,L.registerTech=j.registerTech,L.use=function(e,t){Es[e]=Es[e]||[],Es[e].push(t)},Object.defineProperty(L,"middleware",{value:{},writeable:!1,enumerable:!0}),Object.defineProperty(L.middleware,"TERMINATOR",{value:xs,writeable:!1,enumerable:!0}),L.browser=_e,L.obj=Q,L.mergeOptions=A(9,"videojs.mergeOptions","videojs.obj.merge",h),L.defineLazyProperty=A(9,"videojs.defineLazyProperty","videojs.obj.defineLazyProperty",Y),L.bind=A(9,"videojs.bind","native Function.prototype.bind",y),L.registerPlugin=O.registerPlugin,L.deregisterPlugin=O.deregisterPlugin,L.plugin=(e,t)=>(l.warn("videojs.plugin() is deprecated; use videojs.registerPlugin() instead"),O.registerPlugin(e,t)),L.getPlugins=O.getPlugins,L.getPlugin=O.getPlugin,L.getPluginVersion=O.getPluginVersion,L.addLanguage=function(e,t){return e=(""+e).toLowerCase(),L.options.languages=h(L.options.languages,{[e]:t}),L.options.languages[e]},L.log=l,L.createLogger=$,L.time=Dt,L.createTimeRange=A(9,"videojs.createTimeRange","videojs.time.createTimeRanges",k),L.createTimeRanges=A(9,"videojs.createTimeRanges","videojs.time.createTimeRanges",k),L.formatTime=A(9,"videojs.formatTime","videojs.time.formatTime",Nt),L.setFormatTime=A(9,"videojs.setFormatTime","videojs.time.setFormatTime",At),L.resetFormatTime=A(9,"videojs.resetFormatTime","videojs.time.resetFormatTime",Lt),L.parseUrl=A(9,"videojs.parseUrl","videojs.url.parseUrl",ts),L.isCrossOrigin=A(9,"videojs.isCrossOrigin","videojs.url.isCrossOrigin",is),L.EventTarget=i,L.any=ct,L.on=m,L.one=ht,L.off=_,L.trigger=lt,L.xhr=ds,L.TextTrack=fs,L.AudioTrack=bs,L.VideoTrack=Ts,["isEl","isTextNode","createEl","hasClass","addClass","removeClass","toggleClass","setAttributes","getAttributes","emptyEl","appendContent","insertContent"].forEach(e=>{L[e]=function(){return l.warn(`videojs.${e}() is deprecated; use videojs.dom.${e}() instead`),Xe[e].apply(null,arguments)}}),L.computedStyle=A(9,"videojs.computedStyle","videojs.dom.computedStyle",We),L.dom=Xe,L.fn=e,L.num=t,L.str=xt,L.url=ns,L.Error={UnsupportedSidxContainer:"unsupported-sidx-container-error",DashManifestSidxParsingError:"dash-manifest-sidx-parsing-error",HlsPlaylistRequestError:"hls-playlist-request-error",SegmentUnsupportedMediaFormat:"segment-unsupported-media-format-error",UnsupportedMediaInitialization:"unsupported-media-initialization-error",SegmentSwitchError:"segment-switch-error",SegmentExceedsSourceBufferQuota:"segment-exceeds-source-buffer-quota-error",SegmentAppendError:"segment-append-error",VttLoadError:"vtt-load-error",VttCueParsingError:"vtt-cue-parsing-error",AdsBeforePrerollError:"ads-before-preroll-error",AdsPrerollError:"ads-preroll-error",AdsMidrollError:"ads-midroll-error",AdsPostrollError:"ads-postroll-error",AdsMacroReplacementFailed:"ads-macro-replacement-failed",AdsResumeContentFailed:"ads-resume-content-failed",EMEFailedToRequestMediaKeySystemAccess:"eme-failed-request-media-key-system-access",EMEFailedToCreateMediaKeys:"eme-failed-create-media-keys",EMEFailedToAttachMediaKeysToVideoElement:"eme-failed-attach-media-keys-to-video",EMEFailedToCreateMediaKeySession:"eme-failed-create-media-key-session",EMEFailedToSetServerCertificate:"eme-failed-set-server-certificate",EMEFailedToGenerateLicenseRequest:"eme-failed-generate-license-request",EMEFailedToUpdateSessionWithReceivedLicenseKeys:"eme-failed-update-session",EMEFailedToCloseSession:"eme-failed-close-session",EMEFailedToRemoveKeysFromSession:"eme-failed-remove-keys",EMEFailedToLoadSessionBySessionId:"eme-failed-load-session"},L});
\ No newline at end of file
diff --git a/source/src/public/twitch/video.js/alt/video.debug.js b/source/src/public/twitch/video.js/alt/video.debug.js
deleted file mode 100755
index 0a687f7..0000000
--- a/source/src/public/twitch/video.js/alt/video.debug.js
+++ /dev/null
@@ -1,65227 +0,0 @@
-/**
- * @license
- * Video.js 8.12.0
- * Copyright Brightcove, Inc.
- * Available under Apache License Version 2.0
- *
- *
- * Includes vtt.js
- * Available under Apache License Version 2.0
- *
- */
-
-(function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
- typeof define === 'function' && define.amd ? define(factory) :
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
-})(this, (function () { 'use strict';
-
- var version$5 = "8.12.0";
-
- /**
- * An Object that contains lifecycle hooks as keys which point to an array
- * of functions that are run when a lifecycle is triggered
- *
- * @private
- */
- const hooks_ = {};
-
- /**
- * Get a list of hooks for a specific lifecycle
- *
- * @param {string} type
- * the lifecycle to get hooks from
- *
- * @param {Function|Function[]} [fn]
- * Optionally add a hook (or hooks) to the lifecycle that your are getting.
- *
- * @return {Array}
- * an array of hooks, or an empty array if there are none.
- */
- const hooks = function (type, fn) {
- hooks_[type] = hooks_[type] || [];
- if (fn) {
- hooks_[type] = hooks_[type].concat(fn);
- }
- return hooks_[type];
- };
-
- /**
- * Add a function hook to a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle to hook the function to.
- *
- * @param {Function|Function[]}
- * The function or array of functions to attach.
- */
- const hook = function (type, fn) {
- hooks(type, fn);
- };
-
- /**
- * Remove a hook from a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle that the function hooked to
- *
- * @param {Function} fn
- * The hooked function to remove
- *
- * @return {boolean}
- * The function that was removed or undef
- */
- const removeHook = function (type, fn) {
- const index = hooks(type).indexOf(fn);
- if (index <= -1) {
- return false;
- }
- hooks_[type] = hooks_[type].slice();
- hooks_[type].splice(index, 1);
- return true;
- };
-
- /**
- * Add a function hook that will only run once to a specific videojs lifecycle.
- *
- * @param {string} type
- * the lifecycle to hook the function to.
- *
- * @param {Function|Function[]}
- * The function or array of functions to attach.
- */
- const hookOnce = function (type, fn) {
- hooks(type, [].concat(fn).map(original => {
- const wrapper = (...args) => {
- removeHook(type, wrapper);
- return original(...args);
- };
- return wrapper;
- }));
- };
-
- /**
- * @file fullscreen-api.js
- * @module fullscreen-api
- */
-
- /**
- * Store the browser-specific methods for the fullscreen API.
- *
- * @type {Object}
- * @see [Specification]{@link https://fullscreen.spec.whatwg.org}
- * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
- */
- const FullscreenApi = {
- prefixed: true
- };
-
- // browser API methods
- const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
- // WebKit
- ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
- const specApi = apiMap[0];
- let browserApi;
-
- // determine the supported set of functions
- for (let i = 0; i < apiMap.length; i++) {
- // check for exitFullscreen function
- if (apiMap[i][1] in document) {
- browserApi = apiMap[i];
- break;
- }
- }
-
- // map the browser API names to the spec API names
- if (browserApi) {
- for (let i = 0; i < browserApi.length; i++) {
- FullscreenApi[specApi[i]] = browserApi[i];
- }
- FullscreenApi.prefixed = browserApi[0] !== specApi[0];
- }
-
- /**
- * @file create-logger.js
- * @module create-logger
- */
-
- // This is the private tracking variable for the logging history.
- let history = [];
-
- /**
- * Log messages to the console and history based on the type of message
- *
- * @private
- * @param {string} name
- * The name of the console method to use.
- *
- * @param {Object} log
- * The arguments to be passed to the matching console method.
- *
- * @param {string} [styles]
- * styles for name
- */
- const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
- const lvl = log.levels[level];
- const lvlRegExp = new RegExp(`^(${lvl})$`);
- let resultName = name;
- if (type !== 'log') {
- // Add the type to the front of the message when it's not "log".
- args.unshift(type.toUpperCase() + ':');
- }
- if (styles) {
- resultName = `%c${name}`;
- args.unshift(styles);
- }
-
- // Add console prefix after adding to history.
- args.unshift(resultName + ':');
-
- // Add a clone of the args at this point to history.
- if (history) {
- history.push([].concat(args));
-
- // only store 1000 history entries
- const splice = history.length - 1000;
- history.splice(0, splice > 0 ? splice : 0);
- }
-
- // If there's no console then don't try to output messages, but they will
- // still be stored in history.
- if (!window.console) {
- return;
- }
-
- // Was setting these once outside of this function, but containing them
- // in the function makes it easier to test cases where console doesn't exist
- // when the module is executed.
- let fn = window.console[type];
- if (!fn && type === 'debug') {
- // Certain browsers don't have support for console.debug. For those, we
- // should default to the closest comparable log.
- fn = window.console.info || window.console.log;
- }
-
- // Bail out if there's no console or if this type is not allowed by the
- // current logging level.
- if (!fn || !lvl || !lvlRegExp.test(type)) {
- return;
- }
- fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args);
- };
- function createLogger$1(name, delimiter = ':', styles = '') {
- // This is the private tracking variable for logging level.
- let level = 'info';
-
- // the curried logByType bound to the specific log and history
- let logByType;
-
- /**
- * Logs plain debug messages. Similar to `console.log`.
- *
- * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
- * of our JSDoc template, we cannot properly document this as both a function
- * and a namespace, so its function signature is documented here.
- *
- * #### Arguments
- * ##### *args
- * *[]
- *
- * Any combination of values that could be passed to `console.log()`.
- *
- * #### Return Value
- *
- * `undefined`
- *
- * @namespace
- * @param {...*} args
- * One or more messages or objects that should be logged.
- */
- const log = function (...args) {
- logByType('log', level, args);
- };
-
- // This is the logByType helper that the logging methods below use
- logByType = LogByTypeFactory(name, log, styles);
-
- /**
- * Create a new subLogger which chains the old name to the new name.
- *
- * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following:
- * ```js
- * mylogger('foo');
- * // > VIDEOJS: player: foo
- * ```
- *
- * @param {string} subName
- * The name to add call the new logger
- * @param {string} [subDelimiter]
- * Optional delimiter
- * @param {string} [subStyles]
- * Optional styles
- * @return {Object}
- */
- log.createLogger = (subName, subDelimiter, subStyles) => {
- const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
- const resultStyles = subStyles !== undefined ? subStyles : styles;
- const resultName = `${name} ${resultDelimiter} ${subName}`;
- return createLogger$1(resultName, resultDelimiter, resultStyles);
- };
-
- /**
- * Create a new logger.
- *
- * @param {string} newName
- * The name for the new logger
- * @param {string} [newDelimiter]
- * Optional delimiter
- * @param {string} [newStyles]
- * Optional styles
- * @return {Object}
- */
- log.createNewLogger = (newName, newDelimiter, newStyles) => {
- return createLogger$1(newName, newDelimiter, newStyles);
- };
-
- /**
- * Enumeration of available logging levels, where the keys are the level names
- * and the values are `|`-separated strings containing logging methods allowed
- * in that logging level. These strings are used to create a regular expression
- * matching the function name being called.
- *
- * Levels provided by Video.js are:
- *
- * - `off`: Matches no calls. Any value that can be cast to `false` will have
- * this effect. The most restrictive.
- * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
- * `log.warn`, and `log.error`).
- * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
- * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
- * - `warn`: Matches `log.warn` and `log.error` calls.
- * - `error`: Matches only `log.error` calls.
- *
- * @type {Object}
- */
- log.levels = {
- all: 'debug|log|warn|error',
- off: '',
- debug: 'debug|log|warn|error',
- info: 'log|warn|error',
- warn: 'warn|error',
- error: 'error',
- DEFAULT: level
- };
-
- /**
- * Get or set the current logging level.
- *
- * If a string matching a key from {@link module:log.levels} is provided, acts
- * as a setter.
- *
- * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
- * Pass a valid level to set a new logging level.
- *
- * @return {string}
- * The current logging level.
- */
- log.level = lvl => {
- if (typeof lvl === 'string') {
- if (!log.levels.hasOwnProperty(lvl)) {
- throw new Error(`"${lvl}" in not a valid log level`);
- }
- level = lvl;
- }
- return level;
- };
-
- /**
- * Returns an array containing everything that has been logged to the history.
- *
- * This array is a shallow clone of the internal history record. However, its
- * contents are _not_ cloned; so, mutating objects inside this array will
- * mutate them in history.
- *
- * @return {Array}
- */
- log.history = () => history ? [].concat(history) : [];
-
- /**
- * Allows you to filter the history by the given logger name
- *
- * @param {string} fname
- * The name to filter by
- *
- * @return {Array}
- * The filtered list to return
- */
- log.history.filter = fname => {
- return (history || []).filter(historyItem => {
- // if the first item in each historyItem includes `fname`, then it's a match
- return new RegExp(`.*${fname}.*`).test(historyItem[0]);
- });
- };
-
- /**
- * Clears the internal history tracking, but does not prevent further history
- * tracking.
- */
- log.history.clear = () => {
- if (history) {
- history.length = 0;
- }
- };
-
- /**
- * Disable history tracking if it is currently enabled.
- */
- log.history.disable = () => {
- if (history !== null) {
- history.length = 0;
- history = null;
- }
- };
-
- /**
- * Enable history tracking if it is currently disabled.
- */
- log.history.enable = () => {
- if (history === null) {
- history = [];
- }
- };
-
- /**
- * Logs error messages. Similar to `console.error`.
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as an error
- */
- log.error = (...args) => logByType('error', level, args);
-
- /**
- * Logs warning messages. Similar to `console.warn`.
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as a warning.
- */
- log.warn = (...args) => logByType('warn', level, args);
-
- /**
- * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
- * log if `console.debug` is not available
- *
- * @param {...*} args
- * One or more messages or objects that should be logged as debug.
- */
- log.debug = (...args) => logByType('debug', level, args);
- return log;
- }
-
- /**
- * @file log.js
- * @module log
- */
- const log$1 = createLogger$1('VIDEOJS');
- const createLogger = log$1.createLogger;
-
- /**
- * @file obj.js
- * @module obj
- */
-
- /**
- * @callback obj:EachCallback
- *
- * @param {*} value
- * The current key for the object that is being iterated over.
- *
- * @param {string} key
- * The current key-value for object that is being iterated over
- */
-
- /**
- * @callback obj:ReduceCallback
- *
- * @param {*} accum
- * The value that is accumulating over the reduce loop.
- *
- * @param {*} value
- * The current key for the object that is being iterated over.
- *
- * @param {string} key
- * The current key-value for object that is being iterated over
- *
- * @return {*}
- * The new accumulated value.
- */
- const toString$1 = Object.prototype.toString;
-
- /**
- * Get the keys of an Object
- *
- * @param {Object}
- * The Object to get the keys from
- *
- * @return {string[]}
- * An array of the keys from the object. Returns an empty array if the
- * object passed in was invalid or had no keys.
- *
- * @private
- */
- const keys = function (object) {
- return isObject$1(object) ? Object.keys(object) : [];
- };
-
- /**
- * Array-like iteration for objects.
- *
- * @param {Object} object
- * The object to iterate over
- *
- * @param {obj:EachCallback} fn
- * The callback function which is called for each key in the object.
- */
- function each(object, fn) {
- keys(object).forEach(key => fn(object[key], key));
- }
-
- /**
- * Array-like reduce for objects.
- *
- * @param {Object} object
- * The Object that you want to reduce.
- *
- * @param {Function} fn
- * A callback function which is called for each key in the object. It
- * receives the accumulated value and the per-iteration value and key
- * as arguments.
- *
- * @param {*} [initial = 0]
- * Starting value
- *
- * @return {*}
- * The final accumulated value.
- */
- function reduce(object, fn, initial = 0) {
- return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
- }
-
- /**
- * Returns whether a value is an object of any kind - including DOM nodes,
- * arrays, regular expressions, etc. Not functions, though.
- *
- * This avoids the gotcha where using `typeof` on a `null` value
- * results in `'object'`.
- *
- * @param {Object} value
- * @return {boolean}
- */
- function isObject$1(value) {
- return !!value && typeof value === 'object';
- }
-
- /**
- * Returns whether an object appears to be a "plain" object - that is, a
- * direct instance of `Object`.
- *
- * @param {Object} value
- * @return {boolean}
- */
- function isPlain(value) {
- return isObject$1(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object;
- }
-
- /**
- * Merge two objects recursively.
- *
- * Performs a deep merge like
- * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
- * plain objects (not arrays, elements, or anything else).
- *
- * Non-plain object values will be copied directly from the right-most
- * argument.
- *
- * @param {Object[]} sources
- * One or more objects to merge into a new object.
- *
- * @return {Object}
- * A new object that is the merged result of all sources.
- */
- function merge$2(...sources) {
- const result = {};
- sources.forEach(source => {
- if (!source) {
- return;
- }
- each(source, (value, key) => {
- if (!isPlain(value)) {
- result[key] = value;
- return;
- }
- if (!isPlain(result[key])) {
- result[key] = {};
- }
- result[key] = merge$2(result[key], value);
- });
- });
- return result;
- }
-
- /**
- * Returns an array of values for a given object
- *
- * @param {Object} source - target object
- * @return {Array} - object values
- */
- function values$1(source = {}) {
- const result = [];
- for (const key in source) {
- if (source.hasOwnProperty(key)) {
- const value = source[key];
- result.push(value);
- }
- }
- return result;
- }
-
- /**
- * Object.defineProperty but "lazy", which means that the value is only set after
- * it is retrieved the first time, rather than being set right away.
- *
- * @param {Object} obj the object to set the property on
- * @param {string} key the key for the property to set
- * @param {Function} getValue the function used to get the value when it is needed.
- * @param {boolean} setter whether a setter should be allowed or not
- */
- function defineLazyProperty(obj, key, getValue, setter = true) {
- const set = value => Object.defineProperty(obj, key, {
- value,
- enumerable: true,
- writable: true
- });
- const options = {
- configurable: true,
- enumerable: true,
- get() {
- const value = getValue();
- set(value);
- return value;
- }
- };
- if (setter) {
- options.set = set;
- }
- return Object.defineProperty(obj, key, options);
- }
-
- var Obj = /*#__PURE__*/Object.freeze({
- __proto__: null,
- each: each,
- reduce: reduce,
- isObject: isObject$1,
- isPlain: isPlain,
- merge: merge$2,
- values: values$1,
- defineLazyProperty: defineLazyProperty
- });
-
- /**
- * @file browser.js
- * @module browser
- */
-
- /**
- * Whether or not this device is an iPod.
- *
- * @static
- * @type {Boolean}
- */
- let IS_IPOD = false;
-
- /**
- * The detected iOS version - or `null`.
- *
- * @static
- * @type {string|null}
- */
- let IOS_VERSION = null;
-
- /**
- * Whether or not this is an Android device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_ANDROID = false;
-
- /**
- * The detected Android version - or `null` if not Android or indeterminable.
- *
- * @static
- * @type {number|string|null}
- */
- let ANDROID_VERSION;
-
- /**
- * Whether or not this is Mozilla Firefox.
- *
- * @static
- * @type {Boolean}
- */
- let IS_FIREFOX = false;
-
- /**
- * Whether or not this is Microsoft Edge.
- *
- * @static
- * @type {Boolean}
- */
- let IS_EDGE = false;
-
- /**
- * Whether or not this is any Chromium Browser
- *
- * @static
- * @type {Boolean}
- */
- let IS_CHROMIUM = false;
-
- /**
- * Whether or not this is any Chromium browser that is not Edge.
- *
- * This will also be `true` for Chrome on iOS, which will have different support
- * as it is actually Safari under the hood.
- *
- * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
- * IS_CHROMIUM should be used instead.
- * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
- *
- * @static
- * @deprecated
- * @type {Boolean}
- */
- let IS_CHROME = false;
-
- /**
- * The detected Chromium version - or `null`.
- *
- * @static
- * @type {number|null}
- */
- let CHROMIUM_VERSION = null;
-
- /**
- * The detected Google Chrome version - or `null`.
- * This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
- * Deprecated, use CHROMIUM_VERSION instead.
- *
- * @static
- * @deprecated
- * @type {number|null}
- */
- let CHROME_VERSION = null;
-
- /**
- * The detected Internet Explorer version - or `null`.
- *
- * @static
- * @deprecated
- * @type {number|null}
- */
- let IE_VERSION = null;
-
- /**
- * Whether or not this is desktop Safari.
- *
- * @static
- * @type {Boolean}
- */
- let IS_SAFARI = false;
-
- /**
- * Whether or not this is a Windows machine.
- *
- * @static
- * @type {Boolean}
- */
- let IS_WINDOWS = false;
-
- /**
- * Whether or not this device is an iPad.
- *
- * @static
- * @type {Boolean}
- */
- let IS_IPAD = false;
-
- /**
- * Whether or not this device is an iPhone.
- *
- * @static
- * @type {Boolean}
- */
- // The Facebook app's UIWebView identifies as both an iPhone and iPad, so
- // to identify iPhones, we need to exclude iPads.
- // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
- let IS_IPHONE = false;
-
- /**
- * Whether or not this is a Tizen device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_TIZEN = false;
-
- /**
- * Whether or not this is a WebOS device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_WEBOS = false;
-
- /**
- * Whether or not this is a Smart TV (Tizen or WebOS) device.
- *
- * @static
- * @type {Boolean}
- */
- let IS_SMART_TV = false;
-
- /**
- * Whether or not this device is touch-enabled.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch));
- const UAD = window.navigator && window.navigator.userAgentData;
- if (UAD && UAD.platform && UAD.brands) {
- // If userAgentData is present, use it instead of userAgent to avoid warnings
- // Currently only implemented on Chromium
- // userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
-
- IS_ANDROID = UAD.platform === 'Android';
- IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
- IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
- IS_CHROME = !IS_EDGE && IS_CHROMIUM;
- CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
- IS_WINDOWS = UAD.platform === 'Windows';
- }
-
- // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
- // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
- // the checks need to be made agiainst the regular userAgent string.
- if (!IS_CHROMIUM) {
- const USER_AGENT = window.navigator && window.navigator.userAgent || '';
- IS_IPOD = /iPod/i.test(USER_AGENT);
- IOS_VERSION = function () {
- const match = USER_AGENT.match(/OS (\d+)_/i);
- if (match && match[1]) {
- return match[1];
- }
- return null;
- }();
- IS_ANDROID = /Android/i.test(USER_AGENT);
- ANDROID_VERSION = function () {
- // This matches Android Major.Minor.Patch versions
- // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
- const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
- if (!match) {
- return null;
- }
- const major = match[1] && parseFloat(match[1]);
- const minor = match[2] && parseFloat(match[2]);
- if (major && minor) {
- return parseFloat(match[1] + '.' + match[2]);
- } else if (major) {
- return major;
- }
- return null;
- }();
- IS_FIREFOX = /Firefox/i.test(USER_AGENT);
- IS_EDGE = /Edg/i.test(USER_AGENT);
- IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
- IS_CHROME = !IS_EDGE && IS_CHROMIUM;
- CHROMIUM_VERSION = CHROME_VERSION = function () {
- const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
- if (match && match[2]) {
- return parseFloat(match[2]);
- }
- return null;
- }();
- IE_VERSION = function () {
- const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
- let version = result && parseFloat(result[1]);
- if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
- // IE 11 has a different user agent string than other IE versions
- version = 11.0;
- }
- return version;
- }();
- IS_TIZEN = /Tizen/i.test(USER_AGENT);
- IS_WEBOS = /Web0S/i.test(USER_AGENT);
- IS_SMART_TV = IS_TIZEN || IS_WEBOS;
- IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV;
- IS_WINDOWS = /Windows/i.test(USER_AGENT);
- IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
- IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
- }
-
- /**
- * Whether or not this is an iOS device.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
-
- /**
- * Whether or not this is any flavor of Safari - including iOS.
- *
- * @static
- * @const
- * @type {Boolean}
- */
- const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
-
- var browser = /*#__PURE__*/Object.freeze({
- __proto__: null,
- get IS_IPOD () { return IS_IPOD; },
- get IOS_VERSION () { return IOS_VERSION; },
- get IS_ANDROID () { return IS_ANDROID; },
- get ANDROID_VERSION () { return ANDROID_VERSION; },
- get IS_FIREFOX () { return IS_FIREFOX; },
- get IS_EDGE () { return IS_EDGE; },
- get IS_CHROMIUM () { return IS_CHROMIUM; },
- get IS_CHROME () { return IS_CHROME; },
- get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
- get CHROME_VERSION () { return CHROME_VERSION; },
- get IE_VERSION () { return IE_VERSION; },
- get IS_SAFARI () { return IS_SAFARI; },
- get IS_WINDOWS () { return IS_WINDOWS; },
- get IS_IPAD () { return IS_IPAD; },
- get IS_IPHONE () { return IS_IPHONE; },
- get IS_TIZEN () { return IS_TIZEN; },
- get IS_WEBOS () { return IS_WEBOS; },
- get IS_SMART_TV () { return IS_SMART_TV; },
- TOUCH_ENABLED: TOUCH_ENABLED,
- IS_IOS: IS_IOS,
- IS_ANY_SAFARI: IS_ANY_SAFARI
- });
-
- /**
- * @file dom.js
- * @module dom
- */
-
- /**
- * Detect if a value is a string with any non-whitespace characters.
- *
- * @private
- * @param {string} str
- * The string to check
- *
- * @return {boolean}
- * Will be `true` if the string is non-blank, `false` otherwise.
- *
- */
- function isNonBlankString(str) {
- // we use str.trim as it will trim any whitespace characters
- // from the front or back of non-whitespace characters. aka
- // Any string that contains non-whitespace characters will
- // still contain them after `trim` but whitespace only strings
- // will have a length of 0, failing this check.
- return typeof str === 'string' && Boolean(str.trim());
- }
-
- /**
- * Throws an error if the passed string has whitespace. This is used by
- * class methods to be relatively consistent with the classList API.
- *
- * @private
- * @param {string} str
- * The string to check for whitespace.
- *
- * @throws {Error}
- * Throws an error if there is whitespace in the string.
- */
- function throwIfWhitespace(str) {
- // str.indexOf instead of regex because str.indexOf is faster performance wise.
- if (str.indexOf(' ') >= 0) {
- throw new Error('class has illegal whitespace characters');
- }
- }
-
- /**
- * Whether the current DOM interface appears to be real (i.e. not simulated).
- *
- * @return {boolean}
- * Will be `true` if the DOM appears to be real, `false` otherwise.
- */
- function isReal() {
- // Both document and window will never be undefined thanks to `global`.
- return document === window.document;
- }
-
- /**
- * Determines, via duck typing, whether or not a value is a DOM element.
- *
- * @param {*} value
- * The value to check.
- *
- * @return {boolean}
- * Will be `true` if the value is a DOM element, `false` otherwise.
- */
- function isEl(value) {
- return isObject$1(value) && value.nodeType === 1;
- }
-
- /**
- * Determines if the current DOM is embedded in an iframe.
- *
- * @return {boolean}
- * Will be `true` if the DOM is embedded in an iframe, `false`
- * otherwise.
- */
- function isInFrame() {
- // We need a try/catch here because Safari will throw errors when attempting
- // to get either `parent` or `self`
- try {
- return window.parent !== window.self;
- } catch (x) {
- return true;
- }
- }
-
- /**
- * Creates functions to query the DOM using a given method.
- *
- * @private
- * @param {string} method
- * The method to create the query with.
- *
- * @return {Function}
- * The query method
- */
- function createQuerier(method) {
- return function (selector, context) {
- if (!isNonBlankString(selector)) {
- return document[method](null);
- }
- if (isNonBlankString(context)) {
- context = document.querySelector(context);
- }
- const ctx = isEl(context) ? context : document;
- return ctx[method] && ctx[method](selector);
- };
- }
-
- /**
- * Creates an element and applies properties, attributes, and inserts content.
- *
- * @param {string} [tagName='div']
- * Name of tag to be created.
- *
- * @param {Object} [properties={}]
- * Element properties to be applied.
- *
- * @param {Object} [attributes={}]
- * Element attributes to be applied.
- *
- * @param {ContentDescriptor} [content]
- * A content descriptor object.
- *
- * @return {Element}
- * The element that was created.
- */
- function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
- const el = document.createElement(tagName);
- Object.getOwnPropertyNames(properties).forEach(function (propName) {
- const val = properties[propName];
-
- // Handle textContent since it's not supported everywhere and we have a
- // method for it.
- if (propName === 'textContent') {
- textContent(el, val);
- } else if (el[propName] !== val || propName === 'tabIndex') {
- el[propName] = val;
- }
- });
- Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
- el.setAttribute(attrName, attributes[attrName]);
- });
- if (content) {
- appendContent(el, content);
- }
- return el;
- }
-
- /**
- * Injects text into an element, replacing any existing contents entirely.
- *
- * @param {HTMLElement} el
- * The element to add text content into
- *
- * @param {string} text
- * The text content to add.
- *
- * @return {Element}
- * The element with added text content.
- */
- function textContent(el, text) {
- if (typeof el.textContent === 'undefined') {
- el.innerText = text;
- } else {
- el.textContent = text;
- }
- return el;
- }
-
- /**
- * Insert an element as the first child node of another
- *
- * @param {Element} child
- * Element to insert
- *
- * @param {Element} parent
- * Element to insert child into
- */
- function prependTo(child, parent) {
- if (parent.firstChild) {
- parent.insertBefore(child, parent.firstChild);
- } else {
- parent.appendChild(child);
- }
- }
-
- /**
- * Check if an element has a class name.
- *
- * @param {Element} element
- * Element to check
- *
- * @param {string} classToCheck
- * Class name to check for
- *
- * @return {boolean}
- * Will be `true` if the element has a class, `false` otherwise.
- *
- * @throws {Error}
- * Throws an error if `classToCheck` has white space.
- */
- function hasClass(element, classToCheck) {
- throwIfWhitespace(classToCheck);
- return element.classList.contains(classToCheck);
- }
-
- /**
- * Add a class name to an element.
- *
- * @param {Element} element
- * Element to add class name to.
- *
- * @param {...string} classesToAdd
- * One or more class name to add.
- *
- * @return {Element}
- * The DOM element with the added class name.
- */
- function addClass(element, ...classesToAdd) {
- element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
- return element;
- }
-
- /**
- * Remove a class name from an element.
- *
- * @param {Element} element
- * Element to remove a class name from.
- *
- * @param {...string} classesToRemove
- * One or more class name to remove.
- *
- * @return {Element}
- * The DOM element with class name removed.
- */
- function removeClass(element, ...classesToRemove) {
- // Protect in case the player gets disposed
- if (!element) {
- log$1.warn("removeClass was called with an element that doesn't exist");
- return null;
- }
- element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
- return element;
- }
-
- /**
- * The callback definition for toggleClass.
- *
- * @callback module:dom~PredicateCallback
- * @param {Element} element
- * The DOM element of the Component.
- *
- * @param {string} classToToggle
- * The `className` that wants to be toggled
- *
- * @return {boolean|undefined}
- * If `true` is returned, the `classToToggle` will be added to the
- * `element`. If `false`, the `classToToggle` will be removed from
- * the `element`. If `undefined`, the callback will be ignored.
- */
-
- /**
- * Adds or removes a class name to/from an element depending on an optional
- * condition or the presence/absence of the class name.
- *
- * @param {Element} element
- * The element to toggle a class name on.
- *
- * @param {string} classToToggle
- * The class that should be toggled.
- *
- * @param {boolean|module:dom~PredicateCallback} [predicate]
- * See the return value for {@link module:dom~PredicateCallback}
- *
- * @return {Element}
- * The element with a class that has been toggled.
- */
- function toggleClass(element, classToToggle, predicate) {
- if (typeof predicate === 'function') {
- predicate = predicate(element, classToToggle);
- }
- if (typeof predicate !== 'boolean') {
- predicate = undefined;
- }
- classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
- return element;
- }
-
- /**
- * Apply attributes to an HTML element.
- *
- * @param {Element} el
- * Element to add attributes to.
- *
- * @param {Object} [attributes]
- * Attributes to be applied.
- */
- function setAttributes(el, attributes) {
- Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
- const attrValue = attributes[attrName];
- if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
- el.removeAttribute(attrName);
- } else {
- el.setAttribute(attrName, attrValue === true ? '' : attrValue);
- }
- });
- }
-
- /**
- * Get an element's attribute values, as defined on the HTML tag.
- *
- * Attributes are not the same as properties. They're defined on the tag
- * or with setAttribute.
- *
- * @param {Element} tag
- * Element from which to get tag attributes.
- *
- * @return {Object}
- * All attributes of the element. Boolean attributes will be `true` or
- * `false`, others will be strings.
- */
- function getAttributes(tag) {
- const obj = {};
-
- // known boolean attributes
- // we can check for matching boolean properties, but not all browsers
- // and not all tags know about these attributes, so, we still want to check them manually
- const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
- if (tag && tag.attributes && tag.attributes.length > 0) {
- const attrs = tag.attributes;
- for (let i = attrs.length - 1; i >= 0; i--) {
- const attrName = attrs[i].name;
- /** @type {boolean|string} */
- let attrVal = attrs[i].value;
-
- // check for known booleans
- // the matching element property will return a value for typeof
- if (knownBooleans.includes(attrName)) {
- // the value of an included boolean attribute is typically an empty
- // string ('') which would equal false if we just check for a false value.
- // we also don't want support bad code like autoplay='false'
- attrVal = attrVal !== null ? true : false;
- }
- obj[attrName] = attrVal;
- }
- }
- return obj;
- }
-
- /**
- * Get the value of an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to get the value of.
- *
- * @return {string}
- * The value of the attribute.
- */
- function getAttribute(el, attribute) {
- return el.getAttribute(attribute);
- }
-
- /**
- * Set the value of an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to set.
- *
- * @param {string} value
- * Value to set the attribute to.
- */
- function setAttribute(el, attribute, value) {
- el.setAttribute(attribute, value);
- }
-
- /**
- * Remove an element's attribute.
- *
- * @param {Element} el
- * A DOM element.
- *
- * @param {string} attribute
- * Attribute to remove.
- */
- function removeAttribute(el, attribute) {
- el.removeAttribute(attribute);
- }
-
- /**
- * Attempt to block the ability to select text.
- */
- function blockTextSelection() {
- document.body.focus();
- document.onselectstart = function () {
- return false;
- };
- }
-
- /**
- * Turn off text selection blocking.
- */
- function unblockTextSelection() {
- document.onselectstart = function () {
- return true;
- };
- }
-
- /**
- * Identical to the native `getBoundingClientRect` function, but ensures that
- * the method is supported at all (it is in all browsers we claim to support)
- * and that the element is in the DOM before continuing.
- *
- * This wrapper function also shims properties which are not provided by some
- * older browsers (namely, IE8).
- *
- * Additionally, some browsers do not support adding properties to a
- * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
- * properties (except `x` and `y` which are not widely supported). This helps
- * avoid implementations where keys are non-enumerable.
- *
- * @param {Element} el
- * Element whose `ClientRect` we want to calculate.
- *
- * @return {Object|undefined}
- * Always returns a plain object - or `undefined` if it cannot.
- */
- function getBoundingClientRect(el) {
- if (el && el.getBoundingClientRect && el.parentNode) {
- const rect = el.getBoundingClientRect();
- const result = {};
- ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
- if (rect[k] !== undefined) {
- result[k] = rect[k];
- }
- });
- if (!result.height) {
- result.height = parseFloat(computedStyle(el, 'height'));
- }
- if (!result.width) {
- result.width = parseFloat(computedStyle(el, 'width'));
- }
- return result;
- }
- }
-
- /**
- * Represents the position of a DOM element on the page.
- *
- * @typedef {Object} module:dom~Position
- *
- * @property {number} left
- * Pixels to the left.
- *
- * @property {number} top
- * Pixels from the top.
- */
-
- /**
- * Get the position of an element in the DOM.
- *
- * Uses `getBoundingClientRect` technique from John Resig.
- *
- * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
- *
- * @param {Element} el
- * Element from which to get offset.
- *
- * @return {module:dom~Position}
- * The position of the element that was passed in.
- */
- function findPosition(el) {
- if (!el || el && !el.offsetParent) {
- return {
- left: 0,
- top: 0,
- width: 0,
- height: 0
- };
- }
- const width = el.offsetWidth;
- const height = el.offsetHeight;
- let left = 0;
- let top = 0;
- while (el.offsetParent && el !== document[FullscreenApi.fullscreenElement]) {
- left += el.offsetLeft;
- top += el.offsetTop;
- el = el.offsetParent;
- }
- return {
- left,
- top,
- width,
- height
- };
- }
-
- /**
- * Represents x and y coordinates for a DOM element or mouse pointer.
- *
- * @typedef {Object} module:dom~Coordinates
- *
- * @property {number} x
- * x coordinate in pixels
- *
- * @property {number} y
- * y coordinate in pixels
- */
-
- /**
- * Get the pointer position within an element.
- *
- * The base on the coordinates are the bottom left of the element.
- *
- * @param {Element} el
- * Element on which to get the pointer position on.
- *
- * @param {Event} event
- * Event object.
- *
- * @return {module:dom~Coordinates}
- * A coordinates object corresponding to the mouse position.
- *
- */
- function getPointerPosition(el, event) {
- const translated = {
- x: 0,
- y: 0
- };
- if (IS_IOS) {
- let item = el;
- while (item && item.nodeName.toLowerCase() !== 'html') {
- const transform = computedStyle(item, 'transform');
- if (/^matrix/.test(transform)) {
- const values = transform.slice(7, -1).split(/,\s/).map(Number);
- translated.x += values[4];
- translated.y += values[5];
- } else if (/^matrix3d/.test(transform)) {
- const values = transform.slice(9, -1).split(/,\s/).map(Number);
- translated.x += values[12];
- translated.y += values[13];
- }
- item = item.parentNode;
- }
- }
- const position = {};
- const boxTarget = findPosition(event.target);
- const box = findPosition(el);
- const boxW = box.width;
- const boxH = box.height;
- let offsetY = event.offsetY - (box.top - boxTarget.top);
- let offsetX = event.offsetX - (box.left - boxTarget.left);
- if (event.changedTouches) {
- offsetX = event.changedTouches[0].pageX - box.left;
- offsetY = event.changedTouches[0].pageY + box.top;
- if (IS_IOS) {
- offsetX -= translated.x;
- offsetY -= translated.y;
- }
- }
- position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
- position.x = Math.max(0, Math.min(1, offsetX / boxW));
- return position;
- }
-
- /**
- * Determines, via duck typing, whether or not a value is a text node.
- *
- * @param {*} value
- * Check if this value is a text node.
- *
- * @return {boolean}
- * Will be `true` if the value is a text node, `false` otherwise.
- */
- function isTextNode$1(value) {
- return isObject$1(value) && value.nodeType === 3;
- }
-
- /**
- * Empties the contents of an element.
- *
- * @param {Element} el
- * The element to empty children from
- *
- * @return {Element}
- * The element with no children
- */
- function emptyEl(el) {
- while (el.firstChild) {
- el.removeChild(el.firstChild);
- }
- return el;
- }
-
- /**
- * This is a mixed value that describes content to be injected into the DOM
- * via some method. It can be of the following types:
- *
- * Type | Description
- * -----------|-------------
- * `string` | The value will be normalized into a text node.
- * `Element` | The value will be accepted as-is.
- * `Text` | A TextNode. The value will be accepted as-is.
- * `Array` | A one-dimensional array of strings, elements, text nodes, or functions. These functions should return a string, element, or text node (any other return value, like an array, will be ignored).
- * `Function` | A function, which is expected to return a string, element, text node, or array - any of the other possible values described above. This means that a content descriptor could be a function that returns an array of functions, but those second-level functions must return strings, elements, or text nodes.
- *
- * @typedef {string|Element|Text|Array|Function} ContentDescriptor
- */
-
- /**
- * Normalizes content for eventual insertion into the DOM.
- *
- * This allows a wide range of content definition methods, but helps protect
- * from falling into the trap of simply writing to `innerHTML`, which could
- * be an XSS concern.
- *
- * The content for an element can be passed in multiple types and
- * combinations, whose behavior is as follows:
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Array}
- * All of the content that was passed in, normalized to an array of
- * elements or text nodes.
- */
- function normalizeContent(content) {
- // First, invoke content if it is a function. If it produces an array,
- // that needs to happen before normalization.
- if (typeof content === 'function') {
- content = content();
- }
-
- // Next up, normalize to an array, so one or many items can be normalized,
- // filtered, and returned.
- return (Array.isArray(content) ? content : [content]).map(value => {
- // First, invoke value if it is a function to produce a new value,
- // which will be subsequently normalized to a Node of some kind.
- if (typeof value === 'function') {
- value = value();
- }
- if (isEl(value) || isTextNode$1(value)) {
- return value;
- }
- if (typeof value === 'string' && /\S/.test(value)) {
- return document.createTextNode(value);
- }
- }).filter(value => value);
- }
-
- /**
- * Normalizes and appends content to an element.
- *
- * @param {Element} el
- * Element to append normalized content to.
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Element}
- * The element with appended normalized content.
- */
- function appendContent(el, content) {
- normalizeContent(content).forEach(node => el.appendChild(node));
- return el;
- }
-
- /**
- * Normalizes and inserts content into an element; this is identical to
- * `appendContent()`, except it empties the element first.
- *
- * @param {Element} el
- * Element to insert normalized content into.
- *
- * @param {ContentDescriptor} content
- * A content descriptor value.
- *
- * @return {Element}
- * The element with inserted normalized content.
- */
- function insertContent(el, content) {
- return appendContent(emptyEl(el), content);
- }
-
- /**
- * Check if an event was a single left click.
- *
- * @param {MouseEvent} event
- * Event object.
- *
- * @return {boolean}
- * Will be `true` if a single left click, `false` otherwise.
- */
- function isSingleLeftClick(event) {
- // Note: if you create something draggable, be sure to
- // call it on both `mousedown` and `mousemove` event,
- // otherwise `mousedown` should be enough for a button
-
- if (event.button === undefined && event.buttons === undefined) {
- // Why do we need `buttons` ?
- // Because, middle mouse sometimes have this:
- // e.button === 0 and e.buttons === 4
- // Furthermore, we want to prevent combination click, something like
- // HOLD middlemouse then left click, that would be
- // e.button === 0, e.buttons === 5
- // just `button` is not gonna work
-
- // Alright, then what this block does ?
- // this is for chrome `simulate mobile devices`
- // I want to support this as well
-
- return true;
- }
- if (event.button === 0 && event.buttons === undefined) {
- // Touch screen, sometimes on some specific device, `buttons`
- // doesn't have anything (safari on ios, blackberry...)
-
- return true;
- }
-
- // `mouseup` event on a single left click has
- // `button` and `buttons` equal to 0
- if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
- return true;
- }
- if (event.button !== 0 || event.buttons !== 1) {
- // This is the reason we have those if else block above
- // if any special case we can catch and let it slide
- // we do it above, when get to here, this definitely
- // is-not-left-click
-
- return false;
- }
- return true;
- }
-
- /**
- * Finds a single DOM element matching `selector` within the optional
- * `context` of another DOM element (defaulting to `document`).
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelector`.
- *
- * @param {Element|String} [context=document]
- * A DOM element within which to query. Can also be a selector
- * string in which case the first matching element will be used
- * as context. If missing (or no element matches selector), falls
- * back to `document`.
- *
- * @return {Element|null}
- * The element that was found or null.
- */
- const $ = createQuerier('querySelector');
-
- /**
- * Finds a all DOM elements matching `selector` within the optional
- * `context` of another DOM element (defaulting to `document`).
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelectorAll`.
- *
- * @param {Element|String} [context=document]
- * A DOM element within which to query. Can also be a selector
- * string in which case the first matching element will be used
- * as context. If missing (or no element matches selector), falls
- * back to `document`.
- *
- * @return {NodeList}
- * A element list of elements that were found. Will be empty if none
- * were found.
- *
- */
- const $$ = createQuerier('querySelectorAll');
-
- /**
- * A safe getComputedStyle.
- *
- * This is needed because in Firefox, if the player is loaded in an iframe with
- * `display:none`, then `getComputedStyle` returns `null`, so, we do a
- * null-check to make sure that the player doesn't break in these cases.
- *
- * @param {Element} el
- * The element you want the computed style of
- *
- * @param {string} prop
- * The property name you want
- *
- * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
- */
- function computedStyle(el, prop) {
- if (!el || !prop) {
- return '';
- }
- if (typeof window.getComputedStyle === 'function') {
- let computedStyleValue;
- try {
- computedStyleValue = window.getComputedStyle(el);
- } catch (e) {
- return '';
- }
- return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
- }
- return '';
- }
-
- /**
- * Copy document style sheets to another window.
- *
- * @param {Window} win
- * The window element you want to copy the document style sheets to.
- *
- */
- function copyStyleSheetsToWindow(win) {
- [...document.styleSheets].forEach(styleSheet => {
- try {
- const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
- const style = document.createElement('style');
- style.textContent = cssRules;
- win.document.head.appendChild(style);
- } catch (e) {
- const link = document.createElement('link');
- link.rel = 'stylesheet';
- link.type = styleSheet.type;
- // For older Safari this has to be the string; on other browsers setting the MediaList works
- link.media = styleSheet.media.mediaText;
- link.href = styleSheet.href;
- win.document.head.appendChild(link);
- }
- });
- }
-
- var Dom = /*#__PURE__*/Object.freeze({
- __proto__: null,
- isReal: isReal,
- isEl: isEl,
- isInFrame: isInFrame,
- createEl: createEl,
- textContent: textContent,
- prependTo: prependTo,
- hasClass: hasClass,
- addClass: addClass,
- removeClass: removeClass,
- toggleClass: toggleClass,
- setAttributes: setAttributes,
- getAttributes: getAttributes,
- getAttribute: getAttribute,
- setAttribute: setAttribute,
- removeAttribute: removeAttribute,
- blockTextSelection: blockTextSelection,
- unblockTextSelection: unblockTextSelection,
- getBoundingClientRect: getBoundingClientRect,
- findPosition: findPosition,
- getPointerPosition: getPointerPosition,
- isTextNode: isTextNode$1,
- emptyEl: emptyEl,
- normalizeContent: normalizeContent,
- appendContent: appendContent,
- insertContent: insertContent,
- isSingleLeftClick: isSingleLeftClick,
- $: $,
- $$: $$,
- computedStyle: computedStyle,
- copyStyleSheetsToWindow: copyStyleSheetsToWindow
- });
-
- /**
- * @file setup.js - Functions for setting up a player without
- * user interaction based on the data-setup `attribute` of the video tag.
- *
- * @module setup
- */
- let _windowLoaded = false;
- let videojs$1;
-
- /**
- * Set up any tags that have a data-setup `attribute` when the player is started.
- */
- const autoSetup = function () {
- if (videojs$1.options.autoSetup === false) {
- return;
- }
- const vids = Array.prototype.slice.call(document.getElementsByTagName('video'));
- const audios = Array.prototype.slice.call(document.getElementsByTagName('audio'));
- const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js'));
- const mediaEls = vids.concat(audios, divs);
-
- // Check if any media elements exist
- if (mediaEls && mediaEls.length > 0) {
- for (let i = 0, e = mediaEls.length; i < e; i++) {
- const mediaEl = mediaEls[i];
-
- // Check if element exists, has getAttribute func.
- if (mediaEl && mediaEl.getAttribute) {
- // Make sure this player hasn't already been set up.
- if (mediaEl.player === undefined) {
- const options = mediaEl.getAttribute('data-setup');
-
- // Check if data-setup attr exists.
- // We only auto-setup if they've added the data-setup attr.
- if (options !== null) {
- // Create new video.js instance.
- videojs$1(mediaEl);
- }
- }
-
- // If getAttribute isn't defined, we need to wait for the DOM.
- } else {
- autoSetupTimeout(1);
- break;
- }
- }
-
- // No videos were found, so keep looping unless page is finished loading.
- } else if (!_windowLoaded) {
- autoSetupTimeout(1);
- }
- };
-
- /**
- * Wait until the page is loaded before running autoSetup. This will be called in
- * autoSetup if `hasLoaded` returns false.
- *
- * @param {number} wait
- * How long to wait in ms
- *
- * @param {module:videojs} [vjs]
- * The videojs library function
- */
- function autoSetupTimeout(wait, vjs) {
- // Protect against breakage in non-browser environments
- if (!isReal()) {
- return;
- }
- if (vjs) {
- videojs$1 = vjs;
- }
- window.setTimeout(autoSetup, wait);
- }
-
- /**
- * Used to set the internal tracking of window loaded state to true.
- *
- * @private
- */
- function setWindowLoaded() {
- _windowLoaded = true;
- window.removeEventListener('load', setWindowLoaded);
- }
- if (isReal()) {
- if (document.readyState === 'complete') {
- setWindowLoaded();
- } else {
- /**
- * Listen for the load event on window, and set _windowLoaded to true.
- *
- * We use a standard event listener here to avoid incrementing the GUID
- * before any players are created.
- *
- * @listens load
- */
- window.addEventListener('load', setWindowLoaded);
- }
- }
-
- /**
- * @file stylesheet.js
- * @module stylesheet
- */
-
- /**
- * Create a DOM style element given a className for it.
- *
- * @param {string} className
- * The className to add to the created style element.
- *
- * @return {Element}
- * The element that was created.
- */
- const createStyleElement = function (className) {
- const style = document.createElement('style');
- style.className = className;
- return style;
- };
-
- /**
- * Add text to a DOM element.
- *
- * @param {Element} el
- * The Element to add text content to.
- *
- * @param {string} content
- * The text to add to the element.
- */
- const setTextContent = function (el, content) {
- if (el.styleSheet) {
- el.styleSheet.cssText = content;
- } else {
- el.textContent = content;
- }
- };
-
- /**
- * @file dom-data.js
- * @module dom-data
- */
-
- /**
- * Element Data Store.
- *
- * Allows for binding data to an element without putting it directly on the
- * element. Ex. Event listeners are stored here.
- * (also from jsninja.com, slightly modified and updated for closure compiler)
- *
- * @type {Object}
- * @private
- */
- var DomData = new WeakMap();
-
- /**
- * @file guid.js
- * @module guid
- */
-
- // Default value for GUIDs. This allows us to reset the GUID counter in tests.
- //
- // The initial GUID is 3 because some users have come to rely on the first
- // default player ID ending up as `vjs_video_3`.
- //
- // See: https://github.com/videojs/video.js/pull/6216
- const _initialGuid = 3;
-
- /**
- * Unique ID for an element or function
- *
- * @type {Number}
- */
- let _guid = _initialGuid;
-
- /**
- * Get a unique auto-incrementing ID by number that has not been returned before.
- *
- * @return {number}
- * A new unique ID.
- */
- function newGUID() {
- return _guid++;
- }
-
- /**
- * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
- * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
- * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
- * robust as jquery's, so there's probably some differences.
- *
- * @file events.js
- * @module events
- */
-
- /**
- * Clean up the listener cache and dispatchers
- *
- * @param {Element|Object} elem
- * Element to clean up
- *
- * @param {string} type
- * Type of event to clean up
- */
- function _cleanUpEvents(elem, type) {
- if (!DomData.has(elem)) {
- return;
- }
- const data = DomData.get(elem);
-
- // Remove the events of a particular type if there are none left
- if (data.handlers[type].length === 0) {
- delete data.handlers[type];
- // data.handlers[type] = null;
- // Setting to null was causing an error with data.handlers
-
- // Remove the meta-handler from the element
- if (elem.removeEventListener) {
- elem.removeEventListener(type, data.dispatcher, false);
- } else if (elem.detachEvent) {
- elem.detachEvent('on' + type, data.dispatcher);
- }
- }
-
- // Remove the events object if there are no types left
- if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
- delete data.handlers;
- delete data.dispatcher;
- delete data.disabled;
- }
-
- // Finally remove the element data if there is no data left
- if (Object.getOwnPropertyNames(data).length === 0) {
- DomData.delete(elem);
- }
- }
-
- /**
- * Loops through an array of event types and calls the requested method for each type.
- *
- * @param {Function} fn
- * The event method we want to use.
- *
- * @param {Element|Object} elem
- * Element or object to bind listeners to
- *
- * @param {string[]} types
- * Type of event to bind to.
- *
- * @param {Function} callback
- * Event listener.
- */
- function _handleMultipleEvents(fn, elem, types, callback) {
- types.forEach(function (type) {
- // Call the event method for each one of the types
- fn(elem, type, callback);
- });
- }
-
- /**
- * Fix a native event to have standard property values
- *
- * @param {Object} event
- * Event object to fix.
- *
- * @return {Object}
- * Fixed event object.
- */
- function fixEvent(event) {
- if (event.fixed_) {
- return event;
- }
- function returnTrue() {
- return true;
- }
- function returnFalse() {
- return false;
- }
-
- // Test if fixing up is needed
- // Used to check if !event.stopPropagation instead of isPropagationStopped
- // But native events return true for stopPropagation, but don't have
- // other expected methods like isPropagationStopped. Seems to be a problem
- // with the Javascript Ninja code. So we're just overriding all events now.
- if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
- const old = event || window.event;
- event = {};
- // Clone the old object so that we can modify the values event = {};
- // IE8 Doesn't like when you mess with native event properties
- // Firefox returns false for event.hasOwnProperty('type') and other props
- // which makes copying more difficult.
- // TODO: Probably best to create a whitelist of event props
- for (const key in old) {
- // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
- // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
- // and webkitMovementX/Y
- // Lighthouse complains if Event.path is copied
- if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') {
- // Chrome 32+ warns if you try to copy deprecated returnValue, but
- // we still want to if preventDefault isn't supported (IE8).
- if (!(key === 'returnValue' && old.preventDefault)) {
- event[key] = old[key];
- }
- }
- }
-
- // The event occurred on this element
- if (!event.target) {
- event.target = event.srcElement || document;
- }
-
- // Handle which other element the event is related to
- if (!event.relatedTarget) {
- event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
- }
-
- // Stop the default browser action
- event.preventDefault = function () {
- if (old.preventDefault) {
- old.preventDefault();
- }
- event.returnValue = false;
- old.returnValue = false;
- event.defaultPrevented = true;
- };
- event.defaultPrevented = false;
-
- // Stop the event from bubbling
- event.stopPropagation = function () {
- if (old.stopPropagation) {
- old.stopPropagation();
- }
- event.cancelBubble = true;
- old.cancelBubble = true;
- event.isPropagationStopped = returnTrue;
- };
- event.isPropagationStopped = returnFalse;
-
- // Stop the event from bubbling and executing other handlers
- event.stopImmediatePropagation = function () {
- if (old.stopImmediatePropagation) {
- old.stopImmediatePropagation();
- }
- event.isImmediatePropagationStopped = returnTrue;
- event.stopPropagation();
- };
- event.isImmediatePropagationStopped = returnFalse;
-
- // Handle mouse position
- if (event.clientX !== null && event.clientX !== undefined) {
- const doc = document.documentElement;
- const body = document.body;
- event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
- event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
- }
-
- // Handle key presses
- event.which = event.charCode || event.keyCode;
-
- // Fix button for mouse clicks:
- // 0 == left; 1 == middle; 2 == right
- if (event.button !== null && event.button !== undefined) {
- // The following is disabled because it does not pass videojs-standard
- // and... yikes.
- /* eslint-disable */
- event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
- /* eslint-enable */
- }
- }
-
- event.fixed_ = true;
- // Returns fixed-up instance
- return event;
- }
-
- /**
- * Whether passive event listeners are supported
- */
- let _supportsPassive;
- const supportsPassive = function () {
- if (typeof _supportsPassive !== 'boolean') {
- _supportsPassive = false;
- try {
- const opts = Object.defineProperty({}, 'passive', {
- get() {
- _supportsPassive = true;
- }
- });
- window.addEventListener('test', null, opts);
- window.removeEventListener('test', null, opts);
- } catch (e) {
- // disregard
- }
- }
- return _supportsPassive;
- };
-
- /**
- * Touch events Chrome expects to be passive
- */
- const passiveEvents = ['touchstart', 'touchmove'];
-
- /**
- * Add an event listener to element
- * It stores the handler function in a separate cache object
- * and adds a generic handler to the element's event,
- * along with a unique id (guid) to the element.
- *
- * @param {Element|Object} elem
- * Element or object to bind listeners to
- *
- * @param {string|string[]} type
- * Type of event to bind to.
- *
- * @param {Function} fn
- * Event listener.
- */
- function on(elem, type, fn) {
- if (Array.isArray(type)) {
- return _handleMultipleEvents(on, elem, type, fn);
- }
- if (!DomData.has(elem)) {
- DomData.set(elem, {});
- }
- const data = DomData.get(elem);
-
- // We need a place to store all our handler data
- if (!data.handlers) {
- data.handlers = {};
- }
- if (!data.handlers[type]) {
- data.handlers[type] = [];
- }
- if (!fn.guid) {
- fn.guid = newGUID();
- }
- data.handlers[type].push(fn);
- if (!data.dispatcher) {
- data.disabled = false;
- data.dispatcher = function (event, hash) {
- if (data.disabled) {
- return;
- }
- event = fixEvent(event);
- const handlers = data.handlers[event.type];
- if (handlers) {
- // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
- const handlersCopy = handlers.slice(0);
- for (let m = 0, n = handlersCopy.length; m < n; m++) {
- if (event.isImmediatePropagationStopped()) {
- break;
- } else {
- try {
- handlersCopy[m].call(elem, event, hash);
- } catch (e) {
- log$1.error(e);
- }
- }
- }
- }
- };
- }
- if (data.handlers[type].length === 1) {
- if (elem.addEventListener) {
- let options = false;
- if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
- options = {
- passive: true
- };
- }
- elem.addEventListener(type, data.dispatcher, options);
- } else if (elem.attachEvent) {
- elem.attachEvent('on' + type, data.dispatcher);
- }
- }
- }
-
- /**
- * Removes event listeners from an element
- *
- * @param {Element|Object} elem
- * Object to remove listeners from.
- *
- * @param {string|string[]} [type]
- * Type of listener to remove. Don't include to remove all events from element.
- *
- * @param {Function} [fn]
- * Specific listener to remove. Don't include to remove listeners for an event
- * type.
- */
- function off(elem, type, fn) {
- // Don't want to add a cache object through getElData if not needed
- if (!DomData.has(elem)) {
- return;
- }
- const data = DomData.get(elem);
-
- // If no events exist, nothing to unbind
- if (!data.handlers) {
- return;
- }
- if (Array.isArray(type)) {
- return _handleMultipleEvents(off, elem, type, fn);
- }
-
- // Utility function
- const removeType = function (el, t) {
- data.handlers[t] = [];
- _cleanUpEvents(el, t);
- };
-
- // Are we removing all bound events?
- if (type === undefined) {
- for (const t in data.handlers) {
- if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
- removeType(elem, t);
- }
- }
- return;
- }
- const handlers = data.handlers[type];
-
- // If no handlers exist, nothing to unbind
- if (!handlers) {
- return;
- }
-
- // If no listener was provided, remove all listeners for type
- if (!fn) {
- removeType(elem, type);
- return;
- }
-
- // We're only removing a single handler
- if (fn.guid) {
- for (let n = 0; n < handlers.length; n++) {
- if (handlers[n].guid === fn.guid) {
- handlers.splice(n--, 1);
- }
- }
- }
- _cleanUpEvents(elem, type);
- }
-
- /**
- * Trigger an event for an element
- *
- * @param {Element|Object} elem
- * Element to trigger an event on
- *
- * @param {EventTarget~Event|string} event
- * A string (the type) or an event object with a type attribute
- *
- * @param {Object} [hash]
- * data hash to pass along with the event
- *
- * @return {boolean|undefined}
- * Returns the opposite of `defaultPrevented` if default was
- * prevented. Otherwise, returns `undefined`
- */
- function trigger(elem, event, hash) {
- // Fetches element data and a reference to the parent (for bubbling).
- // Don't want to add a data object to cache for every parent,
- // so checking hasElData first.
- const elemData = DomData.has(elem) ? DomData.get(elem) : {};
- const parent = elem.parentNode || elem.ownerDocument;
- // type = event.type || event,
- // handler;
-
- // If an event name was passed as a string, creates an event out of it
- if (typeof event === 'string') {
- event = {
- type: event,
- target: elem
- };
- } else if (!event.target) {
- event.target = elem;
- }
-
- // Normalizes the event properties.
- event = fixEvent(event);
-
- // If the passed element has a dispatcher, executes the established handlers.
- if (elemData.dispatcher) {
- elemData.dispatcher.call(elem, event, hash);
- }
-
- // Unless explicitly stopped or the event does not bubble (e.g. media events)
- // recursively calls this function to bubble the event up the DOM.
- if (parent && !event.isPropagationStopped() && event.bubbles === true) {
- trigger.call(null, parent, event, hash);
-
- // If at the top of the DOM, triggers the default action unless disabled.
- } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
- if (!DomData.has(event.target)) {
- DomData.set(event.target, {});
- }
- const targetData = DomData.get(event.target);
-
- // Checks if the target has a default action for this event.
- if (event.target[event.type]) {
- // Temporarily disables event dispatching on the target as we have already executed the handler.
- targetData.disabled = true;
- // Executes the default action.
- if (typeof event.target[event.type] === 'function') {
- event.target[event.type]();
- }
- // Re-enables event dispatching.
- targetData.disabled = false;
- }
- }
-
- // Inform the triggerer if the default was prevented by returning false
- return !event.defaultPrevented;
- }
-
- /**
- * Trigger a listener only once for an event.
- *
- * @param {Element|Object} elem
- * Element or object to bind to.
- *
- * @param {string|string[]} type
- * Name/type of event
- *
- * @param {Event~EventListener} fn
- * Event listener function
- */
- function one(elem, type, fn) {
- if (Array.isArray(type)) {
- return _handleMultipleEvents(one, elem, type, fn);
- }
- const func = function () {
- off(elem, type, func);
- fn.apply(this, arguments);
- };
-
- // copy the guid to the new function so it can removed using the original function's ID
- func.guid = fn.guid = fn.guid || newGUID();
- on(elem, type, func);
- }
-
- /**
- * Trigger a listener only once and then turn if off for all
- * configured events
- *
- * @param {Element|Object} elem
- * Element or object to bind to.
- *
- * @param {string|string[]} type
- * Name/type of event
- *
- * @param {Event~EventListener} fn
- * Event listener function
- */
- function any(elem, type, fn) {
- const func = function () {
- off(elem, type, func);
- fn.apply(this, arguments);
- };
-
- // copy the guid to the new function so it can removed using the original function's ID
- func.guid = fn.guid = fn.guid || newGUID();
-
- // multiple ons, but one off for everything
- on(elem, type, func);
- }
-
- var Events = /*#__PURE__*/Object.freeze({
- __proto__: null,
- fixEvent: fixEvent,
- on: on,
- off: off,
- trigger: trigger,
- one: one,
- any: any
- });
-
- /**
- * @file fn.js
- * @module fn
- */
- const UPDATE_REFRESH_INTERVAL = 30;
-
- /**
- * A private, internal-only function for changing the context of a function.
- *
- * It also stores a unique id on the function so it can be easily removed from
- * events.
- *
- * @private
- * @function
- * @param {*} context
- * The object to bind as scope.
- *
- * @param {Function} fn
- * The function to be bound to a scope.
- *
- * @param {number} [uid]
- * An optional unique ID for the function to be set
- *
- * @return {Function}
- * The new function that will be bound into the context given
- */
- const bind_ = function (context, fn, uid) {
- // Make sure the function has a unique ID
- if (!fn.guid) {
- fn.guid = newGUID();
- }
-
- // Create the new function that changes the context
- const bound = fn.bind(context);
-
- // Allow for the ability to individualize this function
- // Needed in the case where multiple objects might share the same prototype
- // IF both items add an event listener with the same function, then you try to remove just one
- // it will remove both because they both have the same guid.
- // when using this, you need to use the bind method when you remove the listener as well.
- // currently used in text tracks
- bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
- return bound;
- };
-
- /**
- * Wraps the given function, `fn`, with a new function that only invokes `fn`
- * at most once per every `wait` milliseconds.
- *
- * @function
- * @param {Function} fn
- * The function to be throttled.
- *
- * @param {number} wait
- * The number of milliseconds by which to throttle.
- *
- * @return {Function}
- */
- const throttle = function (fn, wait) {
- let last = window.performance.now();
- const throttled = function (...args) {
- const now = window.performance.now();
- if (now - last >= wait) {
- fn(...args);
- last = now;
- }
- };
- return throttled;
- };
-
- /**
- * Creates a debounced function that delays invoking `func` until after `wait`
- * milliseconds have elapsed since the last time the debounced function was
- * invoked.
- *
- * Inspired by lodash and underscore implementations.
- *
- * @function
- * @param {Function} func
- * The function to wrap with debounce behavior.
- *
- * @param {number} wait
- * The number of milliseconds to wait after the last invocation.
- *
- * @param {boolean} [immediate]
- * Whether or not to invoke the function immediately upon creation.
- *
- * @param {Object} [context=window]
- * The "context" in which the debounced function should debounce. For
- * example, if this function should be tied to a Video.js player,
- * the player can be passed here. Alternatively, defaults to the
- * global `window` object.
- *
- * @return {Function}
- * A debounced function.
- */
- const debounce = function (func, wait, immediate, context = window) {
- let timeout;
- const cancel = () => {
- context.clearTimeout(timeout);
- timeout = null;
- };
-
- /* eslint-disable consistent-this */
- const debounced = function () {
- const self = this;
- const args = arguments;
- let later = function () {
- timeout = null;
- later = null;
- if (!immediate) {
- func.apply(self, args);
- }
- };
- if (!timeout && immediate) {
- func.apply(self, args);
- }
- context.clearTimeout(timeout);
- timeout = context.setTimeout(later, wait);
- };
- /* eslint-enable consistent-this */
-
- debounced.cancel = cancel;
- return debounced;
- };
-
- var Fn = /*#__PURE__*/Object.freeze({
- __proto__: null,
- UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
- bind_: bind_,
- throttle: throttle,
- debounce: debounce
- });
-
- /**
- * @file src/js/event-target.js
- */
- let EVENT_MAP;
-
- /**
- * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
- * adds shorthand functions that wrap around lengthy functions. For example:
- * the `on` function is a wrapper around `addEventListener`.
- *
- * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
- * @class EventTarget
- */
- class EventTarget$2 {
- /**
- * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
- * function that will get called when an event with a certain name gets triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to call with `EventTarget`s
- */
- on(type, fn) {
- // Remove the addEventListener alias before calling Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- on(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * Removes an `event listener` for a specific event from an instance of `EventTarget`.
- * This makes it so that the `event listener` will no longer get called when the
- * named event happens.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to remove.
- */
- off(type, fn) {
- off(this, type, fn);
- }
- /**
- * This function will add an `event listener` that gets triggered only once. After the
- * first trigger it will get removed. This is like adding an `event listener`
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- one(type, fn) {
- // Remove the addEventListener aliasing Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- one(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * This function will add an `event listener` that gets triggered only once and is
- * removed from all events. This is like adding an array of `event listener`s
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
- * first time it is triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- any(type, fn) {
- // Remove the addEventListener aliasing Events.on
- // so we don't get into an infinite type loop
- const ael = this.addEventListener;
- this.addEventListener = () => {};
- any(this, type, fn);
- this.addEventListener = ael;
- }
- /**
- * This function causes an event to happen. This will then cause any `event listeners`
- * that are waiting for that event, to get called. If there are no `event listeners`
- * for an event then nothing will happen.
- *
- * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
- * Trigger will also call the `on` + `uppercaseEventName` function.
- *
- * Example:
- * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
- * `onClick` if it exists.
- *
- * @param {string|EventTarget~Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- */
- trigger(event) {
- const type = event.type || event;
-
- // deprecation
- // In a future version we should default target to `this`
- // similar to how we default the target to `elem` in
- // `Events.trigger`. Right now the default `target` will be
- // `document` due to the `Event.fixEvent` call.
- if (typeof event === 'string') {
- event = {
- type
- };
- }
- event = fixEvent(event);
- if (this.allowedEvents_[type] && this['on' + type]) {
- this['on' + type](event);
- }
- trigger(this, event);
- }
- queueTrigger(event) {
- // only set up EVENT_MAP if it'll be used
- if (!EVENT_MAP) {
- EVENT_MAP = new Map();
- }
- const type = event.type || event;
- let map = EVENT_MAP.get(this);
- if (!map) {
- map = new Map();
- EVENT_MAP.set(this, map);
- }
- const oldTimeout = map.get(type);
- map.delete(type);
- window.clearTimeout(oldTimeout);
- const timeout = window.setTimeout(() => {
- map.delete(type);
- // if we cleared out all timeouts for the current target, delete its map
- if (map.size === 0) {
- map = null;
- EVENT_MAP.delete(this);
- }
- this.trigger(event);
- }, 0);
- map.set(type, timeout);
- }
- }
-
- /**
- * A Custom DOM event.
- *
- * @typedef {CustomEvent} Event
- * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
- */
-
- /**
- * All event listeners should follow the following format.
- *
- * @callback EventListener
- * @this {EventTarget}
- *
- * @param {Event} event
- * the event that triggered this function
- *
- * @param {Object} [hash]
- * hash of data sent during the event
- */
-
- /**
- * An object containing event names as keys and booleans as values.
- *
- * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
- * will have extra functionality. See that function for more information.
- *
- * @property EventTarget.prototype.allowedEvents_
- * @protected
- */
- EventTarget$2.prototype.allowedEvents_ = {};
-
- /**
- * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#on}
- */
- EventTarget$2.prototype.addEventListener = EventTarget$2.prototype.on;
-
- /**
- * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#off}
- */
- EventTarget$2.prototype.removeEventListener = EventTarget$2.prototype.off;
-
- /**
- * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
- * the standard DOM API.
- *
- * @function
- * @see {@link EventTarget#trigger}
- */
- EventTarget$2.prototype.dispatchEvent = EventTarget$2.prototype.trigger;
-
- /**
- * @file mixins/evented.js
- * @module evented
- */
- const objName = obj => {
- if (typeof obj.name === 'function') {
- return obj.name();
- }
- if (typeof obj.name === 'string') {
- return obj.name;
- }
- if (obj.name_) {
- return obj.name_;
- }
- if (obj.constructor && obj.constructor.name) {
- return obj.constructor.name;
- }
- return typeof obj;
- };
-
- /**
- * Returns whether or not an object has had the evented mixin applied.
- *
- * @param {Object} object
- * An object to test.
- *
- * @return {boolean}
- * Whether or not the object appears to be evented.
- */
- const isEvented = object => object instanceof EventTarget$2 || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
-
- /**
- * Adds a callback to run after the evented mixin applied.
- *
- * @param {Object} target
- * An object to Add
- * @param {Function} callback
- * The callback to run.
- */
- const addEventedCallback = (target, callback) => {
- if (isEvented(target)) {
- callback();
- } else {
- if (!target.eventedCallbacks) {
- target.eventedCallbacks = [];
- }
- target.eventedCallbacks.push(callback);
- }
- };
-
- /**
- * Whether a value is a valid event type - non-empty string or array.
- *
- * @private
- * @param {string|Array} type
- * The type value to test.
- *
- * @return {boolean}
- * Whether or not the type is a valid event type.
- */
- const isValidEventType = type =>
- // The regex here verifies that the `type` contains at least one non-
- // whitespace character.
- typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
-
- /**
- * Validates a value to determine if it is a valid event target. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the target does not appear to be a valid event target.
- *
- * @param {Object} target
- * The object to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateTarget = (target, obj, fnName) => {
- if (!target || !target.nodeName && !isEvented(target)) {
- throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
- }
- };
-
- /**
- * Validates a value to determine if it is a valid event target. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the type does not appear to be a valid event type.
- *
- * @param {string|Array} type
- * The type to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateEventType = (type, obj, fnName) => {
- if (!isValidEventType(type)) {
- throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
- }
- };
-
- /**
- * Validates a value to determine if it is a valid listener. Throws if not.
- *
- * @private
- * @throws {Error}
- * If the listener is not a function.
- *
- * @param {Function} listener
- * The listener to test.
- *
- * @param {Object} obj
- * The evented object we are validating for
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- */
- const validateListener = (listener, obj, fnName) => {
- if (typeof listener !== 'function') {
- throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
- }
- };
-
- /**
- * Takes an array of arguments given to `on()` or `one()`, validates them, and
- * normalizes them into an object.
- *
- * @private
- * @param {Object} self
- * The evented object on which `on()` or `one()` was called. This
- * object will be bound as the `this` value for the listener.
- *
- * @param {Array} args
- * An array of arguments passed to `on()` or `one()`.
- *
- * @param {string} fnName
- * The name of the evented mixin function that called this.
- *
- * @return {Object}
- * An object containing useful values for `on()` or `one()` calls.
- */
- const normalizeListenArgs = (self, args, fnName) => {
- // If the number of arguments is less than 3, the target is always the
- // evented object itself.
- const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
- let target;
- let type;
- let listener;
- if (isTargetingSelf) {
- target = self.eventBusEl_;
-
- // Deal with cases where we got 3 arguments, but we are still listening to
- // the evented object itself.
- if (args.length >= 3) {
- args.shift();
- }
- [type, listener] = args;
- } else {
- [target, type, listener] = args;
- }
- validateTarget(target, self, fnName);
- validateEventType(type, self, fnName);
- validateListener(listener, self, fnName);
- listener = bind_(self, listener);
- return {
- isTargetingSelf,
- target,
- type,
- listener
- };
- };
-
- /**
- * Adds the listener to the event type(s) on the target, normalizing for
- * the type of target.
- *
- * @private
- * @param {Element|Object} target
- * A DOM node or evented object.
- *
- * @param {string} method
- * The event binding method to use ("on" or "one").
- *
- * @param {string|Array} type
- * One or more event type(s).
- *
- * @param {Function} listener
- * A listener function.
- */
- const listen = (target, method, type, listener) => {
- validateTarget(target, target, method);
- if (target.nodeName) {
- Events[method](target, type, listener);
- } else {
- target[method](type, listener);
- }
- };
-
- /**
- * Contains methods that provide event capabilities to an object which is passed
- * to {@link module:evented|evented}.
- *
- * @mixin EventedMixin
- */
- const EventedMixin = {
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- on(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'on');
- listen(target, 'on', type, listener);
-
- // If this object is listening to another evented object.
- if (!isTargetingSelf) {
- // If this object is disposed, remove the listener.
- const removeListenerOnDispose = () => this.off(target, type, listener);
-
- // Use the same function ID as the listener so we can remove it later it
- // using the ID of the original listener.
- removeListenerOnDispose.guid = listener.guid;
-
- // Add a listener to the target's dispose event as well. This ensures
- // that if the target is disposed BEFORE this object, we remove the
- // removal listener that was just added. Otherwise, we create a memory leak.
- const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- removeRemoverOnTargetDispose.guid = listener.guid;
- listen(this, 'on', 'dispose', removeListenerOnDispose);
- listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
- }
- },
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object. The listener will be called once per event and then removed.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- one(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'one');
-
- // Targeting this evented object.
- if (isTargetingSelf) {
- listen(target, 'one', type, listener);
-
- // Targeting another evented object.
- } else {
- // TODO: This wrapper is incorrect! It should only
- // remove the wrapper for the event type that called it.
- // Instead all listeners are removed on the first trigger!
- // see https://github.com/videojs/video.js/issues/5962
- const wrapper = (...largs) => {
- this.off(target, type, wrapper);
- listener.apply(null, largs);
- };
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- wrapper.guid = listener.guid;
- listen(target, 'one', type, wrapper);
- }
- },
- /**
- * Add a listener to an event (or events) on this object or another evented
- * object. The listener will only be called once for the first event that is triggered
- * then removed.
- *
- * @param {string|Array|Element|Object} targetOrType
- * If this is a string or array, it represents the event type(s)
- * that will trigger the listener.
- *
- * Another evented object can be passed here instead, which will
- * cause the listener to listen for events on _that_ object.
- *
- * In either case, the listener's `this` value will be bound to
- * this object.
- *
- * @param {string|Array|Function} typeOrListener
- * If the first argument was a string or array, this should be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function.
- */
- any(...args) {
- const {
- isTargetingSelf,
- target,
- type,
- listener
- } = normalizeListenArgs(this, args, 'any');
-
- // Targeting this evented object.
- if (isTargetingSelf) {
- listen(target, 'any', type, listener);
-
- // Targeting another evented object.
- } else {
- const wrapper = (...largs) => {
- this.off(target, type, wrapper);
- listener.apply(null, largs);
- };
-
- // Use the same function ID as the listener so we can remove it later
- // it using the ID of the original listener.
- wrapper.guid = listener.guid;
- listen(target, 'any', type, wrapper);
- }
- },
- /**
- * Removes listener(s) from event(s) on an evented object.
- *
- * @param {string|Array|Element|Object} [targetOrType]
- * If this is a string or array, it represents the event type(s).
- *
- * Another evented object can be passed here instead, in which case
- * ALL 3 arguments are _required_.
- *
- * @param {string|Array|Function} [typeOrListener]
- * If the first argument was a string or array, this may be the
- * listener function. Otherwise, this is a string or array of event
- * type(s).
- *
- * @param {Function} [listener]
- * If the first argument was another evented object, this will be
- * the listener function; otherwise, _all_ listeners bound to the
- * event type(s) will be removed.
- */
- off(targetOrType, typeOrListener, listener) {
- // Targeting this evented object.
- if (!targetOrType || isValidEventType(targetOrType)) {
- off(this.eventBusEl_, targetOrType, typeOrListener);
-
- // Targeting another evented object.
- } else {
- const target = targetOrType;
- const type = typeOrListener;
-
- // Fail fast and in a meaningful way!
- validateTarget(target, this, 'off');
- validateEventType(type, this, 'off');
- validateListener(listener, this, 'off');
-
- // Ensure there's at least a guid, even if the function hasn't been used
- listener = bind_(this, listener);
-
- // Remove the dispose listener on this evented object, which was given
- // the same guid as the event listener in on().
- this.off('dispose', listener);
- if (target.nodeName) {
- off(target, type, listener);
- off(target, 'dispose', listener);
- } else if (isEvented(target)) {
- target.off(type, listener);
- target.off('dispose', listener);
- }
- }
- },
- /**
- * Fire an event on this evented object, causing its listeners to be called.
- *
- * @param {string|Object} event
- * An event type or an object with a type property.
- *
- * @param {Object} [hash]
- * An additional object to pass along to listeners.
- *
- * @return {boolean}
- * Whether or not the default behavior was prevented.
- */
- trigger(event, hash) {
- validateTarget(this.eventBusEl_, this, 'trigger');
- const type = event && typeof event !== 'string' ? event.type : event;
- if (!isValidEventType(type)) {
- throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
- }
- return trigger(this.eventBusEl_, event, hash);
- }
- };
-
- /**
- * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
- *
- * @param {Object} target
- * The object to which to add event methods.
- *
- * @param {Object} [options={}]
- * Options for customizing the mixin behavior.
- *
- * @param {string} [options.eventBusKey]
- * By default, adds a `eventBusEl_` DOM element to the target object,
- * which is used as an event bus. If the target object already has a
- * DOM element that should be used, pass its key here.
- *
- * @return {Object}
- * The target object.
- */
- function evented(target, options = {}) {
- const {
- eventBusKey
- } = options;
-
- // Set or create the eventBusEl_.
- if (eventBusKey) {
- if (!target[eventBusKey].nodeName) {
- throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
- }
- target.eventBusEl_ = target[eventBusKey];
- } else {
- target.eventBusEl_ = createEl('span', {
- className: 'vjs-event-bus'
- });
- }
- Object.assign(target, EventedMixin);
- if (target.eventedCallbacks) {
- target.eventedCallbacks.forEach(callback => {
- callback();
- });
- }
-
- // When any evented object is disposed, it removes all its listeners.
- target.on('dispose', () => {
- target.off();
- [target, target.el_, target.eventBusEl_].forEach(function (val) {
- if (val && DomData.has(val)) {
- DomData.delete(val);
- }
- });
- window.setTimeout(() => {
- target.eventBusEl_ = null;
- }, 0);
- });
- return target;
- }
-
- /**
- * @file mixins/stateful.js
- * @module stateful
- */
-
- /**
- * Contains methods that provide statefulness to an object which is passed
- * to {@link module:stateful}.
- *
- * @mixin StatefulMixin
- */
- const StatefulMixin = {
- /**
- * A hash containing arbitrary keys and values representing the state of
- * the object.
- *
- * @type {Object}
- */
- state: {},
- /**
- * Set the state of an object by mutating its
- * {@link module:stateful~StatefulMixin.state|state} object in place.
- *
- * @fires module:stateful~StatefulMixin#statechanged
- * @param {Object|Function} stateUpdates
- * A new set of properties to shallow-merge into the plugin state.
- * Can be a plain object or a function returning a plain object.
- *
- * @return {Object|undefined}
- * An object containing changes that occurred. If no changes
- * occurred, returns `undefined`.
- */
- setState(stateUpdates) {
- // Support providing the `stateUpdates` state as a function.
- if (typeof stateUpdates === 'function') {
- stateUpdates = stateUpdates();
- }
- let changes;
- each(stateUpdates, (value, key) => {
- // Record the change if the value is different from what's in the
- // current state.
- if (this.state[key] !== value) {
- changes = changes || {};
- changes[key] = {
- from: this.state[key],
- to: value
- };
- }
- this.state[key] = value;
- });
-
- // Only trigger "statechange" if there were changes AND we have a trigger
- // function. This allows us to not require that the target object be an
- // evented object.
- if (changes && isEvented(this)) {
- /**
- * An event triggered on an object that is both
- * {@link module:stateful|stateful} and {@link module:evented|evented}
- * indicating that its state has changed.
- *
- * @event module:stateful~StatefulMixin#statechanged
- * @type {Object}
- * @property {Object} changes
- * A hash containing the properties that were changed and
- * the values they were changed `from` and `to`.
- */
- this.trigger({
- changes,
- type: 'statechanged'
- });
- }
- return changes;
- }
- };
-
- /**
- * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
- * object.
- *
- * If the target object is {@link module:evented|evented} and has a
- * `handleStateChanged` method, that method will be automatically bound to the
- * `statechanged` event on itself.
- *
- * @param {Object} target
- * The object to be made stateful.
- *
- * @param {Object} [defaultState]
- * A default set of properties to populate the newly-stateful object's
- * `state` property.
- *
- * @return {Object}
- * Returns the `target`.
- */
- function stateful(target, defaultState) {
- Object.assign(target, StatefulMixin);
-
- // This happens after the mixing-in because we need to replace the `state`
- // added in that step.
- target.state = Object.assign({}, target.state, defaultState);
-
- // Auto-bind the `handleStateChanged` method of the target object if it exists.
- if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
- target.on('statechanged', target.handleStateChanged);
- }
- return target;
- }
-
- /**
- * @file str.js
- * @module to-lower-case
- */
-
- /**
- * Lowercase the first letter of a string.
- *
- * @param {string} string
- * String to be lowercased
- *
- * @return {string}
- * The string with a lowercased first letter
- */
- const toLowerCase = function (string) {
- if (typeof string !== 'string') {
- return string;
- }
- return string.replace(/./, w => w.toLowerCase());
- };
-
- /**
- * Uppercase the first letter of a string.
- *
- * @param {string} string
- * String to be uppercased
- *
- * @return {string}
- * The string with an uppercased first letter
- */
- const toTitleCase$1 = function (string) {
- if (typeof string !== 'string') {
- return string;
- }
- return string.replace(/./, w => w.toUpperCase());
- };
-
- /**
- * Compares the TitleCase versions of the two strings for equality.
- *
- * @param {string} str1
- * The first string to compare
- *
- * @param {string} str2
- * The second string to compare
- *
- * @return {boolean}
- * Whether the TitleCase versions of the strings are equal
- */
- const titleCaseEquals = function (str1, str2) {
- return toTitleCase$1(str1) === toTitleCase$1(str2);
- };
-
- var Str = /*#__PURE__*/Object.freeze({
- __proto__: null,
- toLowerCase: toLowerCase,
- toTitleCase: toTitleCase$1,
- titleCaseEquals: titleCaseEquals
- });
-
- var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
-
- function unwrapExports (x) {
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
- }
-
- function createCommonjsModule(fn, module) {
- return module = { exports: {} }, fn(module, module.exports), module.exports;
- }
-
- var keycode = createCommonjsModule(function (module, exports) {
- // Source: http://jsfiddle.net/vWx8V/
- // http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes
-
- /**
- * Conenience method returns corresponding value for given keyName or keyCode.
- *
- * @param {Mixed} keyCode {Number} or keyName {String}
- * @return {Mixed}
- * @api public
- */
-
- function keyCode(searchInput) {
- // Keyboard Events
- if (searchInput && 'object' === typeof searchInput) {
- var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode;
- if (hasKeyCode) searchInput = hasKeyCode;
- }
-
- // Numbers
- if ('number' === typeof searchInput) return names[searchInput];
-
- // Everything else (cast to string)
- var search = String(searchInput);
-
- // check codes
- var foundNamedKey = codes[search.toLowerCase()];
- if (foundNamedKey) return foundNamedKey;
-
- // check aliases
- var foundNamedKey = aliases[search.toLowerCase()];
- if (foundNamedKey) return foundNamedKey;
-
- // weird character?
- if (search.length === 1) return search.charCodeAt(0);
- return undefined;
- }
-
- /**
- * Compares a keyboard event with a given keyCode or keyName.
- *
- * @param {Event} event Keyboard event that should be tested
- * @param {Mixed} keyCode {Number} or keyName {String}
- * @return {Boolean}
- * @api public
- */
- keyCode.isEventKey = function isEventKey(event, nameOrCode) {
- if (event && 'object' === typeof event) {
- var keyCode = event.which || event.keyCode || event.charCode;
- if (keyCode === null || keyCode === undefined) {
- return false;
- }
- if (typeof nameOrCode === 'string') {
- // check codes
- var foundNamedKey = codes[nameOrCode.toLowerCase()];
- if (foundNamedKey) {
- return foundNamedKey === keyCode;
- }
-
- // check aliases
- var foundNamedKey = aliases[nameOrCode.toLowerCase()];
- if (foundNamedKey) {
- return foundNamedKey === keyCode;
- }
- } else if (typeof nameOrCode === 'number') {
- return nameOrCode === keyCode;
- }
- return false;
- }
- };
- exports = module.exports = keyCode;
-
- /**
- * Get by name
- *
- * exports.code['enter'] // => 13
- */
-
- var codes = exports.code = exports.codes = {
- 'backspace': 8,
- 'tab': 9,
- 'enter': 13,
- 'shift': 16,
- 'ctrl': 17,
- 'alt': 18,
- 'pause/break': 19,
- 'caps lock': 20,
- 'esc': 27,
- 'space': 32,
- 'page up': 33,
- 'page down': 34,
- 'end': 35,
- 'home': 36,
- 'left': 37,
- 'up': 38,
- 'right': 39,
- 'down': 40,
- 'insert': 45,
- 'delete': 46,
- 'command': 91,
- 'left command': 91,
- 'right command': 93,
- 'numpad *': 106,
- 'numpad +': 107,
- 'numpad -': 109,
- 'numpad .': 110,
- 'numpad /': 111,
- 'num lock': 144,
- 'scroll lock': 145,
- 'my computer': 182,
- 'my calculator': 183,
- ';': 186,
- '=': 187,
- ',': 188,
- '-': 189,
- '.': 190,
- '/': 191,
- '`': 192,
- '[': 219,
- '\\': 220,
- ']': 221,
- "'": 222
- };
-
- // Helper aliases
-
- var aliases = exports.aliases = {
- 'windows': 91,
- '⇧': 16,
- '⌥': 18,
- '⌃': 17,
- '⌘': 91,
- 'ctl': 17,
- 'control': 17,
- 'option': 18,
- 'pause': 19,
- 'break': 19,
- 'caps': 20,
- 'return': 13,
- 'escape': 27,
- 'spc': 32,
- 'spacebar': 32,
- 'pgup': 33,
- 'pgdn': 34,
- 'ins': 45,
- 'del': 46,
- 'cmd': 91
- };
-
- /*!
- * Programatically add the following
- */
-
- // lower case chars
- for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32;
-
- // numbers
- for (var i = 48; i < 58; i++) codes[i - 48] = i;
-
- // function keys
- for (i = 1; i < 13; i++) codes['f' + i] = i + 111;
-
- // numpad keys
- for (i = 0; i < 10; i++) codes['numpad ' + i] = i + 96;
-
- /**
- * Get by code
- *
- * exports.name[13] // => 'Enter'
- */
-
- var names = exports.names = exports.title = {}; // title for backward compat
-
- // Create reverse mapping
- for (i in codes) names[codes[i]] = i;
-
- // Add aliases
- for (var alias in aliases) {
- codes[alias] = aliases[alias];
- }
- });
- keycode.code;
- keycode.codes;
- keycode.aliases;
- keycode.names;
- keycode.title;
-
- /**
- * Player Component - Base class for all UI objects
- *
- * @file component.js
- */
-
- /**
- * Base class for all UI Components.
- * Components are UI objects which represent both a javascript object and an element
- * in the DOM. They can be children of other components, and can have
- * children themselves.
- *
- * Components can also use methods from {@link EventTarget}
- */
- class Component$1 {
- /**
- * A callback that is called when a component is ready. Does not have any
- * parameters and any callback value will be ignored.
- *
- * @callback ReadyCallback
- * @this Component
- */
-
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of component options.
- *
- * @param {Object[]} [options.children]
- * An array of children objects to initialize this component with. Children objects have
- * a name property that will be used if more than one component of the same type needs to be
- * added.
- *
- * @param {string} [options.className]
- * A class or space separated list of classes to add the component
- *
- * @param {ReadyCallback} [ready]
- * Function that gets called when the `Component` is ready.
- */
- constructor(player, options, ready) {
- // The component might be the player itself and we can't pass `this` to super
- if (!player && this.play) {
- this.player_ = player = this; // eslint-disable-line
- } else {
- this.player_ = player;
- }
- this.isDisposed_ = false;
-
- // Hold the reference to the parent component via `addChild` method
- this.parentComponent_ = null;
-
- // Make a copy of prototype.options_ to protect against overriding defaults
- this.options_ = merge$2({}, this.options_);
-
- // Updated options with supplied options
- options = this.options_ = merge$2(this.options_, options);
-
- // Get ID from options or options element if one is supplied
- this.id_ = options.id || options.el && options.el.id;
-
- // If there was no ID from the options, generate one
- if (!this.id_) {
- // Don't require the player ID function in the case of mock players
- const id = player && player.id && player.id() || 'no_player';
- this.id_ = `${id}_component_${newGUID()}`;
- }
- this.name_ = options.name || null;
-
- // Create element if one wasn't provided in options
- if (options.el) {
- this.el_ = options.el;
- } else if (options.createEl !== false) {
- this.el_ = this.createEl();
- }
- if (options.className && this.el_) {
- options.className.split(' ').forEach(c => this.addClass(c));
- }
-
- // Remove the placeholder event methods. If the component is evented, the
- // real methods are added next
- ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
- this[fn] = undefined;
- });
-
- // if evented is anything except false, we want to mixin in evented
- if (options.evented !== false) {
- // Make this an evented object and use `el_`, if available, as its event bus
- evented(this, {
- eventBusKey: this.el_ ? 'el_' : null
- });
- this.handleLanguagechange = this.handleLanguagechange.bind(this);
- this.on(this.player_, 'languagechange', this.handleLanguagechange);
- }
- stateful(this, this.constructor.defaultState);
- this.children_ = [];
- this.childIndex_ = {};
- this.childNameIndex_ = {};
- this.setTimeoutIds_ = new Set();
- this.setIntervalIds_ = new Set();
- this.rafIds_ = new Set();
- this.namedRafs_ = new Map();
- this.clearingTimersOnDispose_ = false;
-
- // Add any child components in options
- if (options.initChildren !== false) {
- this.initChildren();
- }
-
- // Don't want to trigger ready here or it will go before init is actually
- // finished for all children that run this constructor
- this.ready(ready);
- if (options.reportTouchActivity !== false) {
- this.enableTouchActivity();
- }
- }
-
- // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
- // They are replaced or removed in the constructor
-
- /**
- * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
- * function that will get called when an event with a certain name gets triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to call with `EventTarget`s
- */
- on(type, fn) {}
-
- /**
- * Removes an `event listener` for a specific event from an instance of `EventTarget`.
- * This makes it so that the `event listener` will no longer get called when the
- * named event happens.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} [fn]
- * The function to remove. If not specified, all listeners managed by Video.js will be removed.
- */
- off(type, fn) {}
-
- /**
- * This function will add an `event listener` that gets triggered only once. After the
- * first trigger it will get removed. This is like adding an `event listener`
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- one(type, fn) {}
-
- /**
- * This function will add an `event listener` that gets triggered only once and is
- * removed from all events. This is like adding an array of `event listener`s
- * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
- * first time it is triggered.
- *
- * @param {string|string[]} type
- * An event name or an array of event names.
- *
- * @param {Function} fn
- * The function to be called once for each event name.
- */
- any(type, fn) {}
-
- /**
- * This function causes an event to happen. This will then cause any `event listeners`
- * that are waiting for that event, to get called. If there are no `event listeners`
- * for an event then nothing will happen.
- *
- * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
- * Trigger will also call the `on` + `uppercaseEventName` function.
- *
- * Example:
- * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
- * `onClick` if it exists.
- *
- * @param {string|Event|Object} event
- * The name of the event, an `Event`, or an object with a key of type set to
- * an event name.
- *
- * @param {Object} [hash]
- * Optionally extra argument to pass through to an event listener
- */
- trigger(event, hash) {}
-
- /**
- * Dispose of the `Component` and all child components.
- *
- * @fires Component#dispose
- *
- * @param {Object} options
- * @param {Element} options.originalEl element with which to replace player element
- */
- dispose(options = {}) {
- // Bail out if the component has already been disposed.
- if (this.isDisposed_) {
- return;
- }
- if (this.readyQueue_) {
- this.readyQueue_.length = 0;
- }
-
- /**
- * Triggered when a `Component` is disposed.
- *
- * @event Component#dispose
- * @type {Event}
- *
- * @property {boolean} [bubbles=false]
- * set to false so that the dispose event does not
- * bubble up
- */
- this.trigger({
- type: 'dispose',
- bubbles: false
- });
- this.isDisposed_ = true;
-
- // Dispose all children.
- if (this.children_) {
- for (let i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i].dispose) {
- this.children_[i].dispose();
- }
- }
- }
-
- // Delete child references
- this.children_ = null;
- this.childIndex_ = null;
- this.childNameIndex_ = null;
- this.parentComponent_ = null;
- if (this.el_) {
- // Remove element from DOM
- if (this.el_.parentNode) {
- if (options.restoreEl) {
- this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
- } else {
- this.el_.parentNode.removeChild(this.el_);
- }
- }
- this.el_ = null;
- }
-
- // remove reference to the player after disposing of the element
- this.player_ = null;
- }
-
- /**
- * Determine whether or not this component has been disposed.
- *
- * @return {boolean}
- * If the component has been disposed, will be `true`. Otherwise, `false`.
- */
- isDisposed() {
- return Boolean(this.isDisposed_);
- }
-
- /**
- * Return the {@link Player} that the `Component` has attached to.
- *
- * @return { import('./player').default }
- * The player that this `Component` has attached to.
- */
- player() {
- return this.player_;
- }
-
- /**
- * Deep merge of options objects with new options.
- * > Note: When both `obj` and `options` contain properties whose values are objects.
- * The two properties get merged using {@link module:obj.merge}
- *
- * @param {Object} obj
- * The object that contains new options.
- *
- * @return {Object}
- * A new object of `this.options_` and `obj` merged together.
- */
- options(obj) {
- if (!obj) {
- return this.options_;
- }
- this.options_ = merge$2(this.options_, obj);
- return this.options_;
- }
-
- /**
- * Get the `Component`s DOM element
- *
- * @return {Element}
- * The DOM element for this `Component`.
- */
- el() {
- return this.el_;
- }
-
- /**
- * Create the `Component`s DOM element.
- *
- * @param {string} [tagName]
- * Element's DOM node type. e.g. 'div'
- *
- * @param {Object} [properties]
- * An object of properties that should be set.
- *
- * @param {Object} [attributes]
- * An object of attributes that should be set.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tagName, properties, attributes) {
- return createEl(tagName, properties, attributes);
- }
-
- /**
- * Localize a string given the string in english.
- *
- * If tokens are provided, it'll try and run a simple token replacement on the provided string.
- * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
- *
- * If a `defaultValue` is provided, it'll use that over `string`,
- * if a value isn't found in provided language files.
- * This is useful if you want to have a descriptive key for token replacement
- * but have a succinct localized string and not require `en.json` to be included.
- *
- * Currently, it is used for the progress bar timing.
- * ```js
- * {
- * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
- * }
- * ```
- * It is then used like so:
- * ```js
- * this.localize('progress bar timing: currentTime={1} duration{2}',
- * [this.player_.currentTime(), this.player_.duration()],
- * '{1} of {2}');
- * ```
- *
- * Which outputs something like: `01:23 of 24:56`.
- *
- *
- * @param {string} string
- * The string to localize and the key to lookup in the language files.
- * @param {string[]} [tokens]
- * If the current item has token replacements, provide the tokens here.
- * @param {string} [defaultValue]
- * Defaults to `string`. Can be a default value to use for token replacement
- * if the lookup key is needed to be separate.
- *
- * @return {string}
- * The localized string or if no localization exists the english string.
- */
- localize(string, tokens, defaultValue = string) {
- const code = this.player_.language && this.player_.language();
- const languages = this.player_.languages && this.player_.languages();
- const language = languages && languages[code];
- const primaryCode = code && code.split('-')[0];
- const primaryLang = languages && languages[primaryCode];
- let localizedString = defaultValue;
- if (language && language[string]) {
- localizedString = language[string];
- } else if (primaryLang && primaryLang[string]) {
- localizedString = primaryLang[string];
- }
- if (tokens) {
- localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
- const value = tokens[index - 1];
- let ret = value;
- if (typeof value === 'undefined') {
- ret = match;
- }
- return ret;
- });
- }
- return localizedString;
- }
-
- /**
- * Handles language change for the player in components. Should be overridden by sub-components.
- *
- * @abstract
- */
- handleLanguagechange() {}
-
- /**
- * Return the `Component`s DOM element. This is where children get inserted.
- * This will usually be the the same as the element returned in {@link Component#el}.
- *
- * @return {Element}
- * The content element for this `Component`.
- */
- contentEl() {
- return this.contentEl_ || this.el_;
- }
-
- /**
- * Get this `Component`s ID
- *
- * @return {string}
- * The id of this `Component`
- */
- id() {
- return this.id_;
- }
-
- /**
- * Get the `Component`s name. The name gets used to reference the `Component`
- * and is set during registration.
- *
- * @return {string}
- * The name of this `Component`.
- */
- name() {
- return this.name_;
- }
-
- /**
- * Get an array of all child components
- *
- * @return {Array}
- * The children
- */
- children() {
- return this.children_;
- }
-
- /**
- * Returns the child `Component` with the given `id`.
- *
- * @param {string} id
- * The id of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The child `Component` with the given `id` or undefined.
- */
- getChildById(id) {
- return this.childIndex_[id];
- }
-
- /**
- * Returns the child `Component` with the given `name`.
- *
- * @param {string} name
- * The name of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The child `Component` with the given `name` or undefined.
- */
- getChild(name) {
- if (!name) {
- return;
- }
- return this.childNameIndex_[name];
- }
-
- /**
- * Returns the descendant `Component` following the givent
- * descendant `names`. For instance ['foo', 'bar', 'baz'] would
- * try to get 'foo' on the current component, 'bar' on the 'foo'
- * component and 'baz' on the 'bar' component and return undefined
- * if any of those don't exist.
- *
- * @param {...string[]|...string} names
- * The name of the child `Component` to get.
- *
- * @return {Component|undefined}
- * The descendant `Component` following the given descendant
- * `names` or undefined.
- */
- getDescendant(...names) {
- // flatten array argument into the main array
- names = names.reduce((acc, n) => acc.concat(n), []);
- let currentChild = this;
- for (let i = 0; i < names.length; i++) {
- currentChild = currentChild.getChild(names[i]);
- if (!currentChild || !currentChild.getChild) {
- return;
- }
- }
- return currentChild;
- }
-
- /**
- * Adds an SVG icon element to another element or component.
- *
- * @param {string} iconName
- * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
- *
- * @param {Element} [el=this.el()]
- * Element to set the title on. Defaults to the current Component's element.
- *
- * @return {Element}
- * The newly created icon element.
- */
- setIcon(iconName, el = this.el()) {
- // TODO: In v9 of video.js, we will want to remove font icons entirely.
- // This means this check, as well as the others throughout the code, and
- // the unecessary CSS for font icons, will need to be removed.
- // See https://github.com/videojs/video.js/pull/8260 as to which components
- // need updating.
- if (!this.player_.options_.experimentalSvgIcons) {
- return;
- }
- const xmlnsURL = 'http://www.w3.org/2000/svg';
-
- // The below creates an element in the format of:
- // ....
- const iconContainer = createEl('span', {
- className: 'vjs-icon-placeholder vjs-svg-icon'
- }, {
- 'aria-hidden': 'true'
- });
- const svgEl = document.createElementNS(xmlnsURL, 'svg');
- svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
- const useEl = document.createElementNS(xmlnsURL, 'use');
- svgEl.appendChild(useEl);
- useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
- iconContainer.appendChild(svgEl);
-
- // Replace a pre-existing icon if one exists.
- if (this.iconIsSet_) {
- el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
- } else {
- el.appendChild(iconContainer);
- }
- this.iconIsSet_ = true;
- return iconContainer;
- }
-
- /**
- * Add a child `Component` inside the current `Component`.
- *
- * @param {string|Component} child
- * The name or instance of a child to add.
- *
- * @param {Object} [options={}]
- * The key/value store of options that will get passed to children of
- * the child.
- *
- * @param {number} [index=this.children_.length]
- * The index to attempt to add a child into.
- *
- *
- * @return {Component}
- * The `Component` that gets added as a child. When using a string the
- * `Component` will get created by this process.
- */
- addChild(child, options = {}, index = this.children_.length) {
- let component;
- let componentName;
-
- // If child is a string, create component with options
- if (typeof child === 'string') {
- componentName = toTitleCase$1(child);
- const componentClassName = options.componentClass || componentName;
-
- // Set name through options
- options.name = componentName;
-
- // Create a new object & element for this controls set
- // If there's no .player_, this is a player
- const ComponentClass = Component$1.getComponent(componentClassName);
- if (!ComponentClass) {
- throw new Error(`Component ${componentClassName} does not exist`);
- }
-
- // data stored directly on the videojs object may be
- // misidentified as a component to retain
- // backwards-compatibility with 4.x. check to make sure the
- // component class can be instantiated.
- if (typeof ComponentClass !== 'function') {
- return null;
- }
- component = new ComponentClass(this.player_ || this, options);
-
- // child is a component instance
- } else {
- component = child;
- }
- if (component.parentComponent_) {
- component.parentComponent_.removeChild(component);
- }
- this.children_.splice(index, 0, component);
- component.parentComponent_ = this;
- if (typeof component.id === 'function') {
- this.childIndex_[component.id()] = component;
- }
-
- // If a name wasn't used to create the component, check if we can use the
- // name function of the component
- componentName = componentName || component.name && toTitleCase$1(component.name());
- if (componentName) {
- this.childNameIndex_[componentName] = component;
- this.childNameIndex_[toLowerCase(componentName)] = component;
- }
-
- // Add the UI object's element to the container div (box)
- // Having an element is not required
- if (typeof component.el === 'function' && component.el()) {
- // If inserting before a component, insert before that component's element
- let refNode = null;
- if (this.children_[index + 1]) {
- // Most children are components, but the video tech is an HTML element
- if (this.children_[index + 1].el_) {
- refNode = this.children_[index + 1].el_;
- } else if (isEl(this.children_[index + 1])) {
- refNode = this.children_[index + 1];
- }
- }
- this.contentEl().insertBefore(component.el(), refNode);
- }
-
- // Return so it can stored on parent object if desired.
- return component;
- }
-
- /**
- * Remove a child `Component` from this `Component`s list of children. Also removes
- * the child `Component`s element from this `Component`s element.
- *
- * @param {Component} component
- * The child `Component` to remove.
- */
- removeChild(component) {
- if (typeof component === 'string') {
- component = this.getChild(component);
- }
- if (!component || !this.children_) {
- return;
- }
- let childFound = false;
- for (let i = this.children_.length - 1; i >= 0; i--) {
- if (this.children_[i] === component) {
- childFound = true;
- this.children_.splice(i, 1);
- break;
- }
- }
- if (!childFound) {
- return;
- }
- component.parentComponent_ = null;
- this.childIndex_[component.id()] = null;
- this.childNameIndex_[toTitleCase$1(component.name())] = null;
- this.childNameIndex_[toLowerCase(component.name())] = null;
- const compEl = component.el();
- if (compEl && compEl.parentNode === this.contentEl()) {
- this.contentEl().removeChild(component.el());
- }
- }
-
- /**
- * Add and initialize default child `Component`s based upon options.
- */
- initChildren() {
- const children = this.options_.children;
- if (children) {
- // `this` is `parent`
- const parentOptions = this.options_;
- const handleAdd = child => {
- const name = child.name;
- let opts = child.opts;
-
- // Allow options for children to be set at the parent options
- // e.g. videojs(id, { controlBar: false });
- // instead of videojs(id, { children: { controlBar: false });
- if (parentOptions[name] !== undefined) {
- opts = parentOptions[name];
- }
-
- // Allow for disabling default components
- // e.g. options['children']['posterImage'] = false
- if (opts === false) {
- return;
- }
-
- // Allow options to be passed as a simple boolean if no configuration
- // is necessary.
- if (opts === true) {
- opts = {};
- }
-
- // We also want to pass the original player options
- // to each component as well so they don't need to
- // reach back into the player for options later.
- opts.playerOptions = this.options_.playerOptions;
-
- // Create and add the child component.
- // Add a direct reference to the child by name on the parent instance.
- // If two of the same component are used, different names should be supplied
- // for each
- const newChild = this.addChild(name, opts);
- if (newChild) {
- this[name] = newChild;
- }
- };
-
- // Allow for an array of children details to passed in the options
- let workingChildren;
- const Tech = Component$1.getComponent('Tech');
- if (Array.isArray(children)) {
- workingChildren = children;
- } else {
- workingChildren = Object.keys(children);
- }
- workingChildren
- // children that are in this.options_ but also in workingChildren would
- // give us extra children we do not want. So, we want to filter them out.
- .concat(Object.keys(this.options_).filter(function (child) {
- return !workingChildren.some(function (wchild) {
- if (typeof wchild === 'string') {
- return child === wchild;
- }
- return child === wchild.name;
- });
- })).map(child => {
- let name;
- let opts;
- if (typeof child === 'string') {
- name = child;
- opts = children[name] || this.options_[name] || {};
- } else {
- name = child.name;
- opts = child;
- }
- return {
- name,
- opts
- };
- }).filter(child => {
- // we have to make sure that child.name isn't in the techOrder since
- // techs are registered as Components but can't aren't compatible
- // See https://github.com/videojs/video.js/issues/2772
- const c = Component$1.getComponent(child.opts.componentClass || toTitleCase$1(child.name));
- return c && !Tech.isTech(c);
- }).forEach(handleAdd);
- }
- }
-
- /**
- * Builds the default DOM class name. Should be overridden by sub-components.
- *
- * @return {string}
- * The DOM class name for this object.
- *
- * @abstract
- */
- buildCSSClass() {
- // Child classes can include a function that does:
- // return 'CLASS NAME' + this._super();
- return '';
- }
-
- /**
- * Bind a listener to the component's ready state.
- * Different from event listeners in that if the ready event has already happened
- * it will trigger the function immediately.
- *
- * @param {ReadyCallback} fn
- * Function that gets called when the `Component` is ready.
- *
- * @return {Component}
- * Returns itself; method can be chained.
- */
- ready(fn, sync = false) {
- if (!fn) {
- return;
- }
- if (!this.isReady_) {
- this.readyQueue_ = this.readyQueue_ || [];
- this.readyQueue_.push(fn);
- return;
- }
- if (sync) {
- fn.call(this);
- } else {
- // Call the function asynchronously by default for consistency
- this.setTimeout(fn, 1);
- }
- }
-
- /**
- * Trigger all the ready listeners for this `Component`.
- *
- * @fires Component#ready
- */
- triggerReady() {
- this.isReady_ = true;
-
- // Ensure ready is triggered asynchronously
- this.setTimeout(function () {
- const readyQueue = this.readyQueue_;
-
- // Reset Ready Queue
- this.readyQueue_ = [];
- if (readyQueue && readyQueue.length > 0) {
- readyQueue.forEach(function (fn) {
- fn.call(this);
- }, this);
- }
-
- // Allow for using event listeners also
- /**
- * Triggered when a `Component` is ready.
- *
- * @event Component#ready
- * @type {Event}
- */
- this.trigger('ready');
- }, 1);
- }
-
- /**
- * Find a single DOM element matching a `selector`. This can be within the `Component`s
- * `contentEl()` or another custom context.
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelector`.
- *
- * @param {Element|string} [context=this.contentEl()]
- * A DOM element within which to query. Can also be a selector string in
- * which case the first matching element will get used as context. If
- * missing `this.contentEl()` gets used. If `this.contentEl()` returns
- * nothing it falls back to `document`.
- *
- * @return {Element|null}
- * the dom element that was found, or null
- *
- * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
- */
- $(selector, context) {
- return $(selector, context || this.contentEl());
- }
-
- /**
- * Finds all DOM element matching a `selector`. This can be within the `Component`s
- * `contentEl()` or another custom context.
- *
- * @param {string} selector
- * A valid CSS selector, which will be passed to `querySelectorAll`.
- *
- * @param {Element|string} [context=this.contentEl()]
- * A DOM element within which to query. Can also be a selector string in
- * which case the first matching element will get used as context. If
- * missing `this.contentEl()` gets used. If `this.contentEl()` returns
- * nothing it falls back to `document`.
- *
- * @return {NodeList}
- * a list of dom elements that were found
- *
- * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
- */
- $$(selector, context) {
- return $$(selector, context || this.contentEl());
- }
-
- /**
- * Check if a component's element has a CSS class name.
- *
- * @param {string} classToCheck
- * CSS class name to check.
- *
- * @return {boolean}
- * - True if the `Component` has the class.
- * - False if the `Component` does not have the class`
- */
- hasClass(classToCheck) {
- return hasClass(this.el_, classToCheck);
- }
-
- /**
- * Add a CSS class name to the `Component`s element.
- *
- * @param {...string} classesToAdd
- * One or more CSS class name to add.
- */
- addClass(...classesToAdd) {
- addClass(this.el_, ...classesToAdd);
- }
-
- /**
- * Remove a CSS class name from the `Component`s element.
- *
- * @param {...string} classesToRemove
- * One or more CSS class name to remove.
- */
- removeClass(...classesToRemove) {
- removeClass(this.el_, ...classesToRemove);
- }
-
- /**
- * Add or remove a CSS class name from the component's element.
- * - `classToToggle` gets added when {@link Component#hasClass} would return false.
- * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
- *
- * @param {string} classToToggle
- * The class to add or remove based on (@link Component#hasClass}
- *
- * @param {boolean|Dom~predicate} [predicate]
- * An {@link Dom~predicate} function or a boolean
- */
- toggleClass(classToToggle, predicate) {
- toggleClass(this.el_, classToToggle, predicate);
- }
-
- /**
- * Show the `Component`s element if it is hidden by removing the
- * 'vjs-hidden' class name from it.
- */
- show() {
- this.removeClass('vjs-hidden');
- }
-
- /**
- * Hide the `Component`s element if it is currently showing by adding the
- * 'vjs-hidden` class name to it.
- */
- hide() {
- this.addClass('vjs-hidden');
- }
-
- /**
- * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
- * class name to it. Used during fadeIn/fadeOut.
- *
- * @private
- */
- lockShowing() {
- this.addClass('vjs-lock-showing');
- }
-
- /**
- * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
- * class name from it. Used during fadeIn/fadeOut.
- *
- * @private
- */
- unlockShowing() {
- this.removeClass('vjs-lock-showing');
- }
-
- /**
- * Get the value of an attribute on the `Component`s element.
- *
- * @param {string} attribute
- * Name of the attribute to get the value from.
- *
- * @return {string|null}
- * - The value of the attribute that was asked for.
- * - Can be an empty string on some browsers if the attribute does not exist
- * or has no value
- * - Most browsers will return null if the attribute does not exist or has
- * no value.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
- */
- getAttribute(attribute) {
- return getAttribute(this.el_, attribute);
- }
-
- /**
- * Set the value of an attribute on the `Component`'s element
- *
- * @param {string} attribute
- * Name of the attribute to set.
- *
- * @param {string} value
- * Value to set the attribute to.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
- */
- setAttribute(attribute, value) {
- setAttribute(this.el_, attribute, value);
- }
-
- /**
- * Remove an attribute from the `Component`s element.
- *
- * @param {string} attribute
- * Name of the attribute to remove.
- *
- * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
- */
- removeAttribute(attribute) {
- removeAttribute(this.el_, attribute);
- }
-
- /**
- * Get or set the width of the component based upon the CSS styles.
- * See {@link Component#dimension} for more detailed information.
- *
- * @param {number|string} [num]
- * The width that you want to set postfixed with '%', 'px' or nothing.
- *
- * @param {boolean} [skipListeners]
- * Skip the componentresize event trigger
- *
- * @return {number|undefined}
- * The width when getting, zero if there is no width
- */
- width(num, skipListeners) {
- return this.dimension('width', num, skipListeners);
- }
-
- /**
- * Get or set the height of the component based upon the CSS styles.
- * See {@link Component#dimension} for more detailed information.
- *
- * @param {number|string} [num]
- * The height that you want to set postfixed with '%', 'px' or nothing.
- *
- * @param {boolean} [skipListeners]
- * Skip the componentresize event trigger
- *
- * @return {number|undefined}
- * The height when getting, zero if there is no height
- */
- height(num, skipListeners) {
- return this.dimension('height', num, skipListeners);
- }
-
- /**
- * Set both the width and height of the `Component` element at the same time.
- *
- * @param {number|string} width
- * Width to set the `Component`s element to.
- *
- * @param {number|string} height
- * Height to set the `Component`s element to.
- */
- dimensions(width, height) {
- // Skip componentresize listeners on width for optimization
- this.width(width, true);
- this.height(height);
- }
-
- /**
- * Get or set width or height of the `Component` element. This is the shared code
- * for the {@link Component#width} and {@link Component#height}.
- *
- * Things to know:
- * - If the width or height in an number this will return the number postfixed with 'px'.
- * - If the width/height is a percent this will return the percent postfixed with '%'
- * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
- * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
- * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
- * for more information
- * - If you want the computed style of the component, use {@link Component#currentWidth}
- * and {@link {Component#currentHeight}
- *
- * @fires Component#componentresize
- *
- * @param {string} widthOrHeight
- 8 'width' or 'height'
- *
- * @param {number|string} [num]
- 8 New dimension
- *
- * @param {boolean} [skipListeners]
- * Skip componentresize event trigger
- *
- * @return {number|undefined}
- * The dimension when getting or 0 if unset
- */
- dimension(widthOrHeight, num, skipListeners) {
- if (num !== undefined) {
- // Set to zero if null or literally NaN (NaN !== NaN)
- if (num === null || num !== num) {
- num = 0;
- }
-
- // Check if using css width/height (% or px) and adjust
- if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
- this.el_.style[widthOrHeight] = num;
- } else if (num === 'auto') {
- this.el_.style[widthOrHeight] = '';
- } else {
- this.el_.style[widthOrHeight] = num + 'px';
- }
-
- // skipListeners allows us to avoid triggering the resize event when setting both width and height
- if (!skipListeners) {
- /**
- * Triggered when a component is resized.
- *
- * @event Component#componentresize
- * @type {Event}
- */
- this.trigger('componentresize');
- }
- return;
- }
-
- // Not setting a value, so getting it
- // Make sure element exists
- if (!this.el_) {
- return 0;
- }
-
- // Get dimension value from style
- const val = this.el_.style[widthOrHeight];
- const pxIndex = val.indexOf('px');
- if (pxIndex !== -1) {
- // Return the pixel value with no 'px'
- return parseInt(val.slice(0, pxIndex), 10);
- }
-
- // No px so using % or no style was set, so falling back to offsetWidth/height
- // If component has display:none, offset will return 0
- // TODO: handle display:none and no dimension style using px
- return parseInt(this.el_['offset' + toTitleCase$1(widthOrHeight)], 10);
- }
-
- /**
- * Get the computed width or the height of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @param {string} widthOrHeight
- * A string containing 'width' or 'height'. Whichever one you want to get.
- *
- * @return {number}
- * The dimension that gets asked for or 0 if nothing was set
- * for that dimension.
- */
- currentDimension(widthOrHeight) {
- let computedWidthOrHeight = 0;
- if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
- throw new Error('currentDimension only accepts width or height value');
- }
- computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
-
- // remove 'px' from variable and parse as integer
- computedWidthOrHeight = parseFloat(computedWidthOrHeight);
-
- // if the computed value is still 0, it's possible that the browser is lying
- // and we want to check the offset values.
- // This code also runs wherever getComputedStyle doesn't exist.
- if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
- const rule = `offset${toTitleCase$1(widthOrHeight)}`;
- computedWidthOrHeight = this.el_[rule];
- }
- return computedWidthOrHeight;
- }
-
- /**
- * An object that contains width and height values of the `Component`s
- * computed style. Uses `window.getComputedStyle`.
- *
- * @typedef {Object} Component~DimensionObject
- *
- * @property {number} width
- * The width of the `Component`s computed style.
- *
- * @property {number} height
- * The height of the `Component`s computed style.
- */
-
- /**
- * Get an object that contains computed width and height values of the
- * component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {Component~DimensionObject}
- * The computed dimensions of the component's element.
- */
- currentDimensions() {
- return {
- width: this.currentDimension('width'),
- height: this.currentDimension('height')
- };
- }
-
- /**
- * Get the computed width of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {number}
- * The computed width of the component's element.
- */
- currentWidth() {
- return this.currentDimension('width');
- }
-
- /**
- * Get the computed height of the component's element.
- *
- * Uses `window.getComputedStyle`.
- *
- * @return {number}
- * The computed height of the component's element.
- */
- currentHeight() {
- return this.currentDimension('height');
- }
-
- /**
- * Set the focus to this component
- */
- focus() {
- this.el_.focus();
- }
-
- /**
- * Remove the focus from this component
- */
- blur() {
- this.el_.blur();
- }
-
- /**
- * When this Component receives a `keydown` event which it does not process,
- * it passes the event to the Player for handling.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- */
- handleKeyDown(event) {
- if (this.player_) {
- // We only stop propagation here because we want unhandled events to fall
- // back to the browser. Exclude Tab for focus trapping.
- if (!keycode.isEventKey(event, 'Tab')) {
- event.stopPropagation();
- }
- this.player_.handleKeyDown(event);
- }
- }
-
- /**
- * Many components used to have a `handleKeyPress` method, which was poorly
- * named because it listened to a `keydown` event. This method name now
- * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
- * will not see their method calls stop working.
- *
- * @param {KeyboardEvent} event
- * The event that caused this function to be called.
- */
- handleKeyPress(event) {
- this.handleKeyDown(event);
- }
-
- /**
- * Emit a 'tap' events when touch event support gets detected. This gets used to
- * support toggling the controls through a tap on the video. They get enabled
- * because every sub-component would have extra overhead otherwise.
- *
- * @protected
- * @fires Component#tap
- * @listens Component#touchstart
- * @listens Component#touchmove
- * @listens Component#touchleave
- * @listens Component#touchcancel
- * @listens Component#touchend
- */
- emitTapEvents() {
- // Track the start time so we can determine how long the touch lasted
- let touchStart = 0;
- let firstTouch = null;
-
- // Maximum movement allowed during a touch event to still be considered a tap
- // Other popular libs use anywhere from 2 (hammer.js) to 15,
- // so 10 seems like a nice, round number.
- const tapMovementThreshold = 10;
-
- // The maximum length a touch can be while still being considered a tap
- const touchTimeThreshold = 200;
- let couldBeTap;
- this.on('touchstart', function (event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length === 1) {
- // Copy pageX/pageY from the object
- firstTouch = {
- pageX: event.touches[0].pageX,
- pageY: event.touches[0].pageY
- };
- // Record start time so we can detect a tap vs. "touch and hold"
- touchStart = window.performance.now();
- // Reset couldBeTap tracking
- couldBeTap = true;
- }
- });
- this.on('touchmove', function (event) {
- // If more than one finger, don't consider treating this as a click
- if (event.touches.length > 1) {
- couldBeTap = false;
- } else if (firstTouch) {
- // Some devices will throw touchmoves for all but the slightest of taps.
- // So, if we moved only a small distance, this could still be a tap
- const xdiff = event.touches[0].pageX - firstTouch.pageX;
- const ydiff = event.touches[0].pageY - firstTouch.pageY;
- const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
- if (touchDistance > tapMovementThreshold) {
- couldBeTap = false;
- }
- }
- });
- const noTap = function () {
- couldBeTap = false;
- };
-
- // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
- this.on('touchleave', noTap);
- this.on('touchcancel', noTap);
-
- // When the touch ends, measure how long it took and trigger the appropriate
- // event
- this.on('touchend', function (event) {
- firstTouch = null;
- // Proceed only if the touchmove/leave/cancel event didn't happen
- if (couldBeTap === true) {
- // Measure how long the touch lasted
- const touchTime = window.performance.now() - touchStart;
-
- // Make sure the touch was less than the threshold to be considered a tap
- if (touchTime < touchTimeThreshold) {
- // Don't let browser turn this into a click
- event.preventDefault();
- /**
- * Triggered when a `Component` is tapped.
- *
- * @event Component#tap
- * @type {MouseEvent}
- */
- this.trigger('tap');
- // It may be good to copy the touchend event object and change the
- // type to tap, if the other event properties aren't exact after
- // Events.fixEvent runs (e.g. event.target)
- }
- }
- });
- }
-
- /**
- * This function reports user activity whenever touch events happen. This can get
- * turned off by any sub-components that wants touch events to act another way.
- *
- * Report user touch activity when touch events occur. User activity gets used to
- * determine when controls should show/hide. It is simple when it comes to mouse
- * events, because any mouse event should show the controls. So we capture mouse
- * events that bubble up to the player and report activity when that happens.
- * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
- * controls. So touch events can't help us at the player level either.
- *
- * User activity gets checked asynchronously. So what could happen is a tap event
- * on the video turns the controls off. Then the `touchend` event bubbles up to
- * the player. Which, if it reported user activity, would turn the controls right
- * back on. We also don't want to completely block touch events from bubbling up.
- * Furthermore a `touchmove` event and anything other than a tap, should not turn
- * controls back on.
- *
- * @listens Component#touchstart
- * @listens Component#touchmove
- * @listens Component#touchend
- * @listens Component#touchcancel
- */
- enableTouchActivity() {
- // Don't continue if the root player doesn't support reporting user activity
- if (!this.player() || !this.player().reportUserActivity) {
- return;
- }
-
- // listener for reporting that the user is active
- const report = bind_(this.player(), this.player().reportUserActivity);
- let touchHolding;
- this.on('touchstart', function () {
- report();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(touchHolding);
- // report at the same interval as activityCheck
- touchHolding = this.setInterval(report, 250);
- });
- const touchEnd = function (event) {
- report();
- // stop the interval that maintains activity if the touch is holding
- this.clearInterval(touchHolding);
- };
- this.on('touchmove', report);
- this.on('touchend', touchEnd);
- this.on('touchcancel', touchEnd);
- }
-
- /**
- * A callback that has no parameters and is bound into `Component`s context.
- *
- * @callback Component~GenericCallback
- * @this Component
- */
-
- /**
- * Creates a function that runs after an `x` millisecond timeout. This function is a
- * wrapper around `window.setTimeout`. There are a few reasons to use this one
- * instead though:
- * 1. It gets cleared via {@link Component#clearTimeout} when
- * {@link Component#dispose} gets called.
- * 2. The function callback will gets turned into a {@link Component~GenericCallback}
- *
- * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
- * will cause its dispose listener not to get cleaned up! Please use
- * {@link Component#clearTimeout} or {@link Component#dispose} instead.
- *
- * @param {Component~GenericCallback} fn
- * The function that will be run after `timeout`.
- *
- * @param {number} timeout
- * Timeout in milliseconds to delay before executing the specified function.
- *
- * @return {number}
- * Returns a timeout ID that gets used to identify the timeout. It can also
- * get used in {@link Component#clearTimeout} to clear the timeout that
- * was set.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
- */
- setTimeout(fn, timeout) {
- // declare as variables so they are properly available in timeout function
- // eslint-disable-next-line
- var timeoutId;
- fn = bind_(this, fn);
- this.clearTimersOnDispose_();
- timeoutId = window.setTimeout(() => {
- if (this.setTimeoutIds_.has(timeoutId)) {
- this.setTimeoutIds_.delete(timeoutId);
- }
- fn();
- }, timeout);
- this.setTimeoutIds_.add(timeoutId);
- return timeoutId;
- }
-
- /**
- * Clears a timeout that gets created via `window.setTimeout` or
- * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
- * use this function instead of `window.clearTimout`. If you don't your dispose
- * listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} timeoutId
- * The id of the timeout to clear. The return value of
- * {@link Component#setTimeout} or `window.setTimeout`.
- *
- * @return {number}
- * Returns the timeout id that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
- */
- clearTimeout(timeoutId) {
- if (this.setTimeoutIds_.has(timeoutId)) {
- this.setTimeoutIds_.delete(timeoutId);
- window.clearTimeout(timeoutId);
- }
- return timeoutId;
- }
-
- /**
- * Creates a function that gets run every `x` milliseconds. This function is a wrapper
- * around `window.setInterval`. There are a few reasons to use this one instead though.
- * 1. It gets cleared via {@link Component#clearInterval} when
- * {@link Component#dispose} gets called.
- * 2. The function callback will be a {@link Component~GenericCallback}
- *
- * @param {Component~GenericCallback} fn
- * The function to run every `x` seconds.
- *
- * @param {number} interval
- * Execute the specified function every `x` milliseconds.
- *
- * @return {number}
- * Returns an id that can be used to identify the interval. It can also be be used in
- * {@link Component#clearInterval} to clear the interval.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
- */
- setInterval(fn, interval) {
- fn = bind_(this, fn);
- this.clearTimersOnDispose_();
- const intervalId = window.setInterval(fn, interval);
- this.setIntervalIds_.add(intervalId);
- return intervalId;
- }
-
- /**
- * Clears an interval that gets created via `window.setInterval` or
- * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
- * use this function instead of `window.clearInterval`. If you don't your dispose
- * listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} intervalId
- * The id of the interval to clear. The return value of
- * {@link Component#setInterval} or `window.setInterval`.
- *
- * @return {number}
- * Returns the interval id that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
- */
- clearInterval(intervalId) {
- if (this.setIntervalIds_.has(intervalId)) {
- this.setIntervalIds_.delete(intervalId);
- window.clearInterval(intervalId);
- }
- return intervalId;
- }
-
- /**
- * Queues up a callback to be passed to requestAnimationFrame (rAF), but
- * with a few extra bonuses:
- *
- * - Supports browsers that do not support rAF by falling back to
- * {@link Component#setTimeout}.
- *
- * - The callback is turned into a {@link Component~GenericCallback} (i.e.
- * bound to the component).
- *
- * - Automatic cancellation of the rAF callback is handled if the component
- * is disposed before it is called.
- *
- * @param {Component~GenericCallback} fn
- * A function that will be bound to this component and executed just
- * before the browser's next repaint.
- *
- * @return {number}
- * Returns an rAF ID that gets used to identify the timeout. It can
- * also be used in {@link Component#cancelAnimationFrame} to cancel
- * the animation frame callback.
- *
- * @listens Component#dispose
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
- */
- requestAnimationFrame(fn) {
- this.clearTimersOnDispose_();
-
- // declare as variables so they are properly available in rAF function
- // eslint-disable-next-line
- var id;
- fn = bind_(this, fn);
- id = window.requestAnimationFrame(() => {
- if (this.rafIds_.has(id)) {
- this.rafIds_.delete(id);
- }
- fn();
- });
- this.rafIds_.add(id);
- return id;
- }
-
- /**
- * Request an animation frame, but only one named animation
- * frame will be queued. Another will never be added until
- * the previous one finishes.
- *
- * @param {string} name
- * The name to give this requestAnimationFrame
- *
- * @param {Component~GenericCallback} fn
- * A function that will be bound to this component and executed just
- * before the browser's next repaint.
- */
- requestNamedAnimationFrame(name, fn) {
- if (this.namedRafs_.has(name)) {
- return;
- }
- this.clearTimersOnDispose_();
- fn = bind_(this, fn);
- const id = this.requestAnimationFrame(() => {
- fn();
- if (this.namedRafs_.has(name)) {
- this.namedRafs_.delete(name);
- }
- });
- this.namedRafs_.set(name, id);
- return name;
- }
-
- /**
- * Cancels a current named animation frame if it exists.
- *
- * @param {string} name
- * The name of the requestAnimationFrame to cancel.
- */
- cancelNamedAnimationFrame(name) {
- if (!this.namedRafs_.has(name)) {
- return;
- }
- this.cancelAnimationFrame(this.namedRafs_.get(name));
- this.namedRafs_.delete(name);
- }
-
- /**
- * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
- * (rAF).
- *
- * If you queue an rAF callback via {@link Component#requestAnimationFrame},
- * use this function instead of `window.cancelAnimationFrame`. If you don't,
- * your dispose listener will not get cleaned up until {@link Component#dispose}!
- *
- * @param {number} id
- * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
- *
- * @return {number}
- * Returns the rAF ID that was cleared.
- *
- * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
- */
- cancelAnimationFrame(id) {
- if (this.rafIds_.has(id)) {
- this.rafIds_.delete(id);
- window.cancelAnimationFrame(id);
- }
- return id;
- }
-
- /**
- * A function to setup `requestAnimationFrame`, `setTimeout`,
- * and `setInterval`, clearing on dispose.
- *
- * > Previously each timer added and removed dispose listeners on it's own.
- * For better performance it was decided to batch them all, and use `Set`s
- * to track outstanding timer ids.
- *
- * @private
- */
- clearTimersOnDispose_() {
- if (this.clearingTimersOnDispose_) {
- return;
- }
- this.clearingTimersOnDispose_ = true;
- this.one('dispose', () => {
- [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
- // for a `Set` key will actually be the value again
- // so forEach((val, val) =>` but for maps we want to use
- // the key.
- this[idName].forEach((val, key) => this[cancelName](key));
- });
- this.clearingTimersOnDispose_ = false;
- });
- }
-
- /**
- * Register a `Component` with `videojs` given the name and the component.
- *
- * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
- * should be registered using {@link Tech.registerTech} or
- * {@link videojs:videojs.registerTech}.
- *
- * > NOTE: This function can also be seen on videojs as
- * {@link videojs:videojs.registerComponent}.
- *
- * @param {string} name
- * The name of the `Component` to register.
- *
- * @param {Component} ComponentToRegister
- * The `Component` class to register.
- *
- * @return {Component}
- * The `Component` that was registered.
- */
- static registerComponent(name, ComponentToRegister) {
- if (typeof name !== 'string' || !name) {
- throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
- }
- const Tech = Component$1.getComponent('Tech');
-
- // We need to make sure this check is only done if Tech has been registered.
- const isTech = Tech && Tech.isTech(ComponentToRegister);
- const isComp = Component$1 === ComponentToRegister || Component$1.prototype.isPrototypeOf(ComponentToRegister.prototype);
- if (isTech || !isComp) {
- let reason;
- if (isTech) {
- reason = 'techs must be registered using Tech.registerTech()';
- } else {
- reason = 'must be a Component subclass';
- }
- throw new Error(`Illegal component, "${name}"; ${reason}.`);
- }
- name = toTitleCase$1(name);
- if (!Component$1.components_) {
- Component$1.components_ = {};
- }
- const Player = Component$1.getComponent('Player');
- if (name === 'Player' && Player && Player.players) {
- const players = Player.players;
- const playerNames = Object.keys(players);
-
- // If we have players that were disposed, then their name will still be
- // in Players.players. So, we must loop through and verify that the value
- // for each item is not null. This allows registration of the Player component
- // after all players have been disposed or before any were created.
- if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
- throw new Error('Can not register Player component after player has been created.');
- }
- }
- Component$1.components_[name] = ComponentToRegister;
- Component$1.components_[toLowerCase(name)] = ComponentToRegister;
- return ComponentToRegister;
- }
-
- /**
- * Get a `Component` based on the name it was registered with.
- *
- * @param {string} name
- * The Name of the component to get.
- *
- * @return {typeof Component}
- * The `Component` that got registered under the given name.
- */
- static getComponent(name) {
- if (!name || !Component$1.components_) {
- return;
- }
- return Component$1.components_[name];
- }
- }
- Component$1.registerComponent('Component', Component$1);
-
- /**
- * @file time.js
- * @module time
- */
-
- /**
- * Returns the time for the specified index at the start or end
- * of a TimeRange object.
- *
- * @typedef {Function} TimeRangeIndex
- *
- * @param {number} [index=0]
- * The range number to return the time for.
- *
- * @return {number}
- * The time offset at the specified index.
- *
- * @deprecated The index argument must be provided.
- * In the future, leaving it out will throw an error.
- */
-
- /**
- * An object that contains ranges of time, which mimics {@link TimeRanges}.
- *
- * @typedef {Object} TimeRange
- *
- * @property {number} length
- * The number of time ranges represented by this object.
- *
- * @property {module:time~TimeRangeIndex} start
- * Returns the time offset at which a specified time range begins.
- *
- * @property {module:time~TimeRangeIndex} end
- * Returns the time offset at which a specified time range ends.
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
- */
-
- /**
- * Check if any of the time ranges are over the maximum index.
- *
- * @private
- * @param {string} fnName
- * The function name to use for logging
- *
- * @param {number} index
- * The index to check
- *
- * @param {number} maxIndex
- * The maximum possible index
- *
- * @throws {Error} if the timeRanges provided are over the maxIndex
- */
- function rangeCheck(fnName, index, maxIndex) {
- if (typeof index !== 'number' || index < 0 || index > maxIndex) {
- throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
- }
- }
-
- /**
- * Get the time for the specified index at the start or end
- * of a TimeRange object.
- *
- * @private
- * @param {string} fnName
- * The function name to use for logging
- *
- * @param {string} valueIndex
- * The property that should be used to get the time. should be
- * 'start' or 'end'
- *
- * @param {Array} ranges
- * An array of time ranges
- *
- * @param {Array} [rangeIndex=0]
- * The index to start the search at
- *
- * @return {number}
- * The time that offset at the specified index.
- *
- * @deprecated rangeIndex must be set to a value, in the future this will throw an error.
- * @throws {Error} if rangeIndex is more than the length of ranges
- */
- function getRange(fnName, valueIndex, ranges, rangeIndex) {
- rangeCheck(fnName, rangeIndex, ranges.length - 1);
- return ranges[rangeIndex][valueIndex];
- }
-
- /**
- * Create a time range object given ranges of time.
- *
- * @private
- * @param {Array} [ranges]
- * An array of time ranges.
- *
- * @return {TimeRange}
- */
- function createTimeRangesObj(ranges) {
- let timeRangesObj;
- if (ranges === undefined || ranges.length === 0) {
- timeRangesObj = {
- length: 0,
- start() {
- throw new Error('This TimeRanges object is empty');
- },
- end() {
- throw new Error('This TimeRanges object is empty');
- }
- };
- } else {
- timeRangesObj = {
- length: ranges.length,
- start: getRange.bind(null, 'start', 0, ranges),
- end: getRange.bind(null, 'end', 1, ranges)
- };
- }
- if (window.Symbol && window.Symbol.iterator) {
- timeRangesObj[window.Symbol.iterator] = () => (ranges || []).values();
- }
- return timeRangesObj;
- }
-
- /**
- * Create a `TimeRange` object which mimics an
- * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
- *
- * @param {number|Array[]} start
- * The start of a single range (a number) or an array of ranges (an
- * array of arrays of two numbers each).
- *
- * @param {number} end
- * The end of a single range. Cannot be used with the array form of
- * the `start` argument.
- *
- * @return {TimeRange}
- */
- function createTimeRanges$1(start, end) {
- if (Array.isArray(start)) {
- return createTimeRangesObj(start);
- } else if (start === undefined || end === undefined) {
- return createTimeRangesObj();
- }
- return createTimeRangesObj([[start, end]]);
- }
-
- /**
- * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
- * seconds) will force a number of leading zeros to cover the length of the
- * guide.
- *
- * @private
- * @param {number} seconds
- * Number of seconds to be turned into a string
- *
- * @param {number} guide
- * Number (in seconds) to model the string after
- *
- * @return {string}
- * Time formatted as H:MM:SS or M:SS
- */
- const defaultImplementation = function (seconds, guide) {
- seconds = seconds < 0 ? 0 : seconds;
- let s = Math.floor(seconds % 60);
- let m = Math.floor(seconds / 60 % 60);
- let h = Math.floor(seconds / 3600);
- const gm = Math.floor(guide / 60 % 60);
- const gh = Math.floor(guide / 3600);
-
- // handle invalid times
- if (isNaN(seconds) || seconds === Infinity) {
- // '-' is false for all relational operators (e.g. <, >=) so this setting
- // will add the minimum number of fields specified by the guide
- h = m = s = '-';
- }
-
- // Check if we need to show hours
- h = h > 0 || gh > 0 ? h + ':' : '';
-
- // If hours are showing, we may need to add a leading zero.
- // Always show at least one digit of minutes.
- m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
-
- // Check if leading zero is need for seconds
- s = s < 10 ? '0' + s : s;
- return h + m + s;
- };
-
- // Internal pointer to the current implementation.
- let implementation = defaultImplementation;
-
- /**
- * Replaces the default formatTime implementation with a custom implementation.
- *
- * @param {Function} customImplementation
- * A function which will be used in place of the default formatTime
- * implementation. Will receive the current time in seconds and the
- * guide (in seconds) as arguments.
- */
- function setFormatTime(customImplementation) {
- implementation = customImplementation;
- }
-
- /**
- * Resets formatTime to the default implementation.
- */
- function resetFormatTime() {
- implementation = defaultImplementation;
- }
-
- /**
- * Delegates to either the default time formatting function or a custom
- * function supplied via `setFormatTime`.
- *
- * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
- * guide (in seconds) will force a number of leading zeros to cover the
- * length of the guide.
- *
- * @example formatTime(125, 600) === "02:05"
- * @param {number} seconds
- * Number of seconds to be turned into a string
- *
- * @param {number} guide
- * Number (in seconds) to model the string after
- *
- * @return {string}
- * Time formatted as H:MM:SS or M:SS
- */
- function formatTime(seconds, guide = seconds) {
- return implementation(seconds, guide);
- }
-
- var Time = /*#__PURE__*/Object.freeze({
- __proto__: null,
- createTimeRanges: createTimeRanges$1,
- createTimeRange: createTimeRanges$1,
- setFormatTime: setFormatTime,
- resetFormatTime: resetFormatTime,
- formatTime: formatTime
- });
-
- /**
- * @file buffer.js
- * @module buffer
- */
-
- /**
- * Compute the percentage of the media that has been buffered.
- *
- * @param { import('./time').TimeRange } buffered
- * The current `TimeRanges` object representing buffered time ranges
- *
- * @param {number} duration
- * Total duration of the media
- *
- * @return {number}
- * Percent buffered of the total duration in decimal form.
- */
- function bufferedPercent(buffered, duration) {
- let bufferedDuration = 0;
- let start;
- let end;
- if (!duration) {
- return 0;
- }
- if (!buffered || !buffered.length) {
- buffered = createTimeRanges$1(0, 0);
- }
- for (let i = 0; i < buffered.length; i++) {
- start = buffered.start(i);
- end = buffered.end(i);
-
- // buffered end can be bigger than duration by a very small fraction
- if (end > duration) {
- end = duration;
- }
- bufferedDuration += end - start;
- }
- return bufferedDuration / duration;
- }
-
- /**
- * @file media-error.js
- */
-
- /**
- * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
- *
- * @param {number|string|Object|MediaError} value
- * This can be of multiple types:
- * - number: should be a standard error code
- * - string: an error message (the code will be 0)
- * - Object: arbitrary properties
- * - `MediaError` (native): used to populate a video.js `MediaError` object
- * - `MediaError` (video.js): will return itself if it's already a
- * video.js `MediaError` object.
- *
- * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
- * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
- *
- * @class MediaError
- */
- function MediaError(value) {
- // Allow redundant calls to this constructor to avoid having `instanceof`
- // checks peppered around the code.
- if (value instanceof MediaError) {
- return value;
- }
- if (typeof value === 'number') {
- this.code = value;
- } else if (typeof value === 'string') {
- // default code is zero, so this is a custom error
- this.message = value;
- } else if (isObject$1(value)) {
- // We assign the `code` property manually because native `MediaError` objects
- // do not expose it as an own/enumerable property of the object.
- if (typeof value.code === 'number') {
- this.code = value.code;
- }
- Object.assign(this, value);
- }
- if (!this.message) {
- this.message = MediaError.defaultMessages[this.code] || '';
- }
- }
-
- /**
- * The error code that refers two one of the defined `MediaError` types
- *
- * @type {Number}
- */
- MediaError.prototype.code = 0;
-
- /**
- * An optional message that to show with the error. Message is not part of the HTML5
- * video spec but allows for more informative custom errors.
- *
- * @type {String}
- */
- MediaError.prototype.message = '';
-
- /**
- * An optional status code that can be set by plugins to allow even more detail about
- * the error. For example a plugin might provide a specific HTTP status code and an
- * error message for that code. Then when the plugin gets that error this class will
- * know how to display an error message for it. This allows a custom message to show
- * up on the `Player` error overlay.
- *
- * @type {Array}
- */
- MediaError.prototype.status = null;
-
- /**
- * An object containing an error type, as well as other information regarding the error.
- *
- * @typedef {{errorType: string, [key: string]: any}} ErrorMetadata
- */
-
- /**
- * An optional object to give more detail about the error. This can be used to give
- * a higher level of specificity to an error versus the more generic MediaError codes.
- * `metadata` expects an `errorType` string that should align with the values from videojs.Error.
- *
- * @type {ErrorMetadata}
- */
- MediaError.prototype.metadata = null;
-
- /**
- * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
- * specification listed under {@link MediaError} for more information.
- *
- * @enum {array}
- * @readonly
- * @property {string} 0 - MEDIA_ERR_CUSTOM
- * @property {string} 1 - MEDIA_ERR_ABORTED
- * @property {string} 2 - MEDIA_ERR_NETWORK
- * @property {string} 3 - MEDIA_ERR_DECODE
- * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
- * @property {string} 5 - MEDIA_ERR_ENCRYPTED
- */
- MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
-
- /**
- * The default `MediaError` messages based on the {@link MediaError.errorTypes}.
- *
- * @type {Array}
- * @constant
- */
- MediaError.defaultMessages = {
- 1: 'You aborted the media playback',
- 2: 'A network error caused the media download to fail part-way.',
- 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
- 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
- 5: 'The media is encrypted and we do not have the keys to decrypt it.'
- };
-
- /**
- * W3C error code for any custom error.
- *
- * @member MediaError#MEDIA_ERR_CUSTOM
- * @constant {number}
- * @default 0
- */
- MediaError.MEDIA_ERR_CUSTOM = 0;
-
- /**
- * W3C error code for any custom error.
- *
- * @member MediaError.MEDIA_ERR_CUSTOM
- * @constant {number}
- * @default 0
- */
- MediaError.prototype.MEDIA_ERR_CUSTOM = 0;
-
- /**
- * W3C error code for media error aborted.
- *
- * @member MediaError#MEDIA_ERR_ABORTED
- * @constant {number}
- * @default 1
- */
- MediaError.MEDIA_ERR_ABORTED = 1;
-
- /**
- * W3C error code for media error aborted.
- *
- * @member MediaError.MEDIA_ERR_ABORTED
- * @constant {number}
- * @default 1
- */
- MediaError.prototype.MEDIA_ERR_ABORTED = 1;
-
- /**
- * W3C error code for any network error.
- *
- * @member MediaError#MEDIA_ERR_NETWORK
- * @constant {number}
- * @default 2
- */
- MediaError.MEDIA_ERR_NETWORK = 2;
-
- /**
- * W3C error code for any network error.
- *
- * @member MediaError.MEDIA_ERR_NETWORK
- * @constant {number}
- * @default 2
- */
- MediaError.prototype.MEDIA_ERR_NETWORK = 2;
-
- /**
- * W3C error code for any decoding error.
- *
- * @member MediaError#MEDIA_ERR_DECODE
- * @constant {number}
- * @default 3
- */
- MediaError.MEDIA_ERR_DECODE = 3;
-
- /**
- * W3C error code for any decoding error.
- *
- * @member MediaError.MEDIA_ERR_DECODE
- * @constant {number}
- * @default 3
- */
- MediaError.prototype.MEDIA_ERR_DECODE = 3;
-
- /**
- * W3C error code for any time that a source is not supported.
- *
- * @member MediaError#MEDIA_ERR_SRC_NOT_SUPPORTED
- * @constant {number}
- * @default 4
- */
- MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
-
- /**
- * W3C error code for any time that a source is not supported.
- *
- * @member MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
- * @constant {number}
- * @default 4
- */
- MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
-
- /**
- * W3C error code for any time that a source is encrypted.
- *
- * @member MediaError#MEDIA_ERR_ENCRYPTED
- * @constant {number}
- * @default 5
- */
- MediaError.MEDIA_ERR_ENCRYPTED = 5;
-
- /**
- * W3C error code for any time that a source is encrypted.
- *
- * @member MediaError.MEDIA_ERR_ENCRYPTED
- * @constant {number}
- * @default 5
- */
- MediaError.prototype.MEDIA_ERR_ENCRYPTED = 5;
-
- var tuple = SafeParseTuple;
- function SafeParseTuple(obj, reviver) {
- var json;
- var error = null;
- try {
- json = JSON.parse(obj, reviver);
- } catch (err) {
- error = err;
- }
- return [error, json];
- }
-
- /**
- * Returns whether an object is `Promise`-like (i.e. has a `then` method).
- *
- * @param {Object} value
- * An object that may or may not be `Promise`-like.
- *
- * @return {boolean}
- * Whether or not the object is `Promise`-like.
- */
- function isPromise(value) {
- return value !== undefined && value !== null && typeof value.then === 'function';
- }
-
- /**
- * Silence a Promise-like object.
- *
- * This is useful for avoiding non-harmful, but potentially confusing "uncaught
- * play promise" rejection error messages.
- *
- * @param {Object} value
- * An object that may or may not be `Promise`-like.
- */
- function silencePromise(value) {
- if (isPromise(value)) {
- value.then(null, e => {});
- }
- }
-
- /**
- * @file text-track-list-converter.js Utilities for capturing text track state and
- * re-creating tracks based on a capture.
- *
- * @module text-track-list-converter
- */
-
- /**
- * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
- * represents the {@link TextTrack}'s state.
- *
- * @param {TextTrack} track
- * The text track to query.
- *
- * @return {Object}
- * A serializable javascript representation of the TextTrack.
- * @private
- */
- const trackToJson_ = function (track) {
- const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
- if (track[prop]) {
- acc[prop] = track[prop];
- }
- return acc;
- }, {
- cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
- return {
- startTime: cue.startTime,
- endTime: cue.endTime,
- text: cue.text,
- id: cue.id
- };
- })
- });
- return ret;
- };
-
- /**
- * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
- * state of all {@link TextTrack}s currently configured. The return array is compatible with
- * {@link text-track-list-converter:jsonToTextTracks}.
- *
- * @param { import('../tech/tech').default } tech
- * The tech object to query
- *
- * @return {Array}
- * A serializable javascript representation of the {@link Tech}s
- * {@link TextTrackList}.
- */
- const textTracksToJson = function (tech) {
- const trackEls = tech.$$('track');
- const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
- const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
- const json = trackToJson_(trackEl.track);
- if (trackEl.src) {
- json.src = trackEl.src;
- }
- return json;
- });
- return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
- return trackObjs.indexOf(track) === -1;
- }).map(trackToJson_));
- };
-
- /**
- * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
- * object {@link TextTrack} representations.
- *
- * @param {Array} json
- * An array of `TextTrack` representation objects, like those that would be
- * produced by `textTracksToJson`.
- *
- * @param {Tech} tech
- * The `Tech` to create the `TextTrack`s on.
- */
- const jsonToTextTracks = function (json, tech) {
- json.forEach(function (track) {
- const addedTrack = tech.addRemoteTextTrack(track).track;
- if (!track.src && track.cues) {
- track.cues.forEach(cue => addedTrack.addCue(cue));
- }
- });
- return tech.textTracks();
- };
- var textTrackConverter = {
- textTracksToJson,
- jsonToTextTracks,
- trackToJson_
- };
-
- /**
- * @file modal-dialog.js
- */
- const MODAL_CLASS_NAME = 'vjs-modal-dialog';
-
- /**
- * The `ModalDialog` displays over the video and its controls, which blocks
- * interaction with the player until it is closed.
- *
- * Modal dialogs include a "Close" button and will close when that button
- * is activated - or when ESC is pressed anywhere.
- *
- * @extends Component
- */
- class ModalDialog extends Component$1 {
- /**
- * Create an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param { import('./utils/dom').ContentDescriptor} [options.content=undefined]
- * Provide customized content for this modal.
- *
- * @param {string} [options.description]
- * A text description for the modal, primarily for accessibility.
- *
- * @param {boolean} [options.fillAlways=false]
- * Normally, modals are automatically filled only the first time
- * they open. This tells the modal to refresh its content
- * every time it opens.
- *
- * @param {string} [options.label]
- * A text label for the modal, primarily for accessibility.
- *
- * @param {boolean} [options.pauseOnOpen=true]
- * If `true`, playback will will be paused if playing when
- * the modal opens, and resumed when it closes.
- *
- * @param {boolean} [options.temporary=true]
- * If `true`, the modal can only be opened once; it will be
- * disposed as soon as it's closed.
- *
- * @param {boolean} [options.uncloseable=false]
- * If `true`, the user will not be able to close the modal
- * through the UI in the normal ways. Programmatic closing is
- * still possible.
- */
- constructor(player, options) {
- super(player, options);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.close_ = e => this.close(e);
- this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
- this.closeable(!this.options_.uncloseable);
- this.content(this.options_.content);
-
- // Make sure the contentEl is defined AFTER any children are initialized
- // because we only want the contents of the modal in the contentEl
- // (not the UI elements like the close button).
- this.contentEl_ = createEl('div', {
- className: `${MODAL_CLASS_NAME}-content`
- }, {
- role: 'document'
- });
- this.descEl_ = createEl('p', {
- className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
- id: this.el().getAttribute('aria-describedby')
- });
- textContent(this.descEl_, this.description());
- this.el_.appendChild(this.descEl_);
- this.el_.appendChild(this.contentEl_);
- }
-
- /**
- * Create the `ModalDialog`'s DOM element
- *
- * @return {Element}
- * The DOM element that gets created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildCSSClass(),
- tabIndex: -1
- }, {
- 'aria-describedby': `${this.id()}_description`,
- 'aria-hidden': 'true',
- 'aria-label': this.label(),
- 'role': 'dialog',
- 'aria-live': 'polite'
- });
- }
- dispose() {
- this.contentEl_ = null;
- this.descEl_ = null;
- this.previouslyActiveEl_ = null;
- super.dispose();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
- }
-
- /**
- * Returns the label string for this modal. Primarily used for accessibility.
- *
- * @return {string}
- * the localized or raw label of this modal.
- */
- label() {
- return this.localize(this.options_.label || 'Modal Window');
- }
-
- /**
- * Returns the description string for this modal. Primarily used for
- * accessibility.
- *
- * @return {string}
- * The localized or raw description of this modal.
- */
- description() {
- let desc = this.options_.description || this.localize('This is a modal window.');
-
- // Append a universal closeability message if the modal is closeable.
- if (this.closeable()) {
- desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
- }
- return desc;
- }
-
- /**
- * Opens the modal.
- *
- * @fires ModalDialog#beforemodalopen
- * @fires ModalDialog#modalopen
- */
- open() {
- if (this.opened_) {
- if (this.options_.fillAlways) {
- this.fill();
- }
- return;
- }
- const player = this.player();
-
- /**
- * Fired just before a `ModalDialog` is opened.
- *
- * @event ModalDialog#beforemodalopen
- * @type {Event}
- */
- this.trigger('beforemodalopen');
- this.opened_ = true;
-
- // Fill content if the modal has never opened before and
- // never been filled.
- if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
- this.fill();
- }
-
- // If the player was playing, pause it and take note of its previously
- // playing state.
- this.wasPlaying_ = !player.paused();
- if (this.options_.pauseOnOpen && this.wasPlaying_) {
- player.pause();
- }
- this.on('keydown', this.handleKeyDown_);
-
- // Hide controls and note if they were enabled.
- this.hadControls_ = player.controls();
- player.controls(false);
- this.show();
- this.conditionalFocus_();
- this.el().setAttribute('aria-hidden', 'false');
-
- /**
- * Fired just after a `ModalDialog` is opened.
- *
- * @event ModalDialog#modalopen
- * @type {Event}
- */
- this.trigger('modalopen');
- this.hasBeenOpened_ = true;
- }
-
- /**
- * If the `ModalDialog` is currently open or closed.
- *
- * @param {boolean} [value]
- * If given, it will open (`true`) or close (`false`) the modal.
- *
- * @return {boolean}
- * the current open state of the modaldialog
- */
- opened(value) {
- if (typeof value === 'boolean') {
- this[value ? 'open' : 'close']();
- }
- return this.opened_;
- }
-
- /**
- * Closes the modal, does nothing if the `ModalDialog` is
- * not open.
- *
- * @fires ModalDialog#beforemodalclose
- * @fires ModalDialog#modalclose
- */
- close() {
- if (!this.opened_) {
- return;
- }
- const player = this.player();
-
- /**
- * Fired just before a `ModalDialog` is closed.
- *
- * @event ModalDialog#beforemodalclose
- * @type {Event}
- */
- this.trigger('beforemodalclose');
- this.opened_ = false;
- if (this.wasPlaying_ && this.options_.pauseOnOpen) {
- player.play();
- }
- this.off('keydown', this.handleKeyDown_);
- if (this.hadControls_) {
- player.controls(true);
- }
- this.hide();
- this.el().setAttribute('aria-hidden', 'true');
-
- /**
- * Fired just after a `ModalDialog` is closed.
- *
- * @event ModalDialog#modalclose
- * @type {Event}
- */
- this.trigger('modalclose');
- this.conditionalBlur_();
- if (this.options_.temporary) {
- this.dispose();
- }
- }
-
- /**
- * Check to see if the `ModalDialog` is closeable via the UI.
- *
- * @param {boolean} [value]
- * If given as a boolean, it will set the `closeable` option.
- *
- * @return {boolean}
- * Returns the final value of the closable option.
- */
- closeable(value) {
- if (typeof value === 'boolean') {
- const closeable = this.closeable_ = !!value;
- let close = this.getChild('closeButton');
-
- // If this is being made closeable and has no close button, add one.
- if (closeable && !close) {
- // The close button should be a child of the modal - not its
- // content element, so temporarily change the content element.
- const temp = this.contentEl_;
- this.contentEl_ = this.el_;
- close = this.addChild('closeButton', {
- controlText: 'Close Modal Dialog'
- });
- this.contentEl_ = temp;
- this.on(close, 'close', this.close_);
- }
-
- // If this is being made uncloseable and has a close button, remove it.
- if (!closeable && close) {
- this.off(close, 'close', this.close_);
- this.removeChild(close);
- close.dispose();
- }
- }
- return this.closeable_;
- }
-
- /**
- * Fill the modal's content element with the modal's "content" option.
- * The content element will be emptied before this change takes place.
- */
- fill() {
- this.fillWith(this.content());
- }
-
- /**
- * Fill the modal's content element with arbitrary content.
- * The content element will be emptied before this change takes place.
- *
- * @fires ModalDialog#beforemodalfill
- * @fires ModalDialog#modalfill
- *
- * @param { import('./utils/dom').ContentDescriptor} [content]
- * The same rules apply to this as apply to the `content` option.
- */
- fillWith(content) {
- const contentEl = this.contentEl();
- const parentEl = contentEl.parentNode;
- const nextSiblingEl = contentEl.nextSibling;
-
- /**
- * Fired just before a `ModalDialog` is filled with content.
- *
- * @event ModalDialog#beforemodalfill
- * @type {Event}
- */
- this.trigger('beforemodalfill');
- this.hasBeenFilled_ = true;
-
- // Detach the content element from the DOM before performing
- // manipulation to avoid modifying the live DOM multiple times.
- parentEl.removeChild(contentEl);
- this.empty();
- insertContent(contentEl, content);
- /**
- * Fired just after a `ModalDialog` is filled with content.
- *
- * @event ModalDialog#modalfill
- * @type {Event}
- */
- this.trigger('modalfill');
-
- // Re-inject the re-filled content element.
- if (nextSiblingEl) {
- parentEl.insertBefore(contentEl, nextSiblingEl);
- } else {
- parentEl.appendChild(contentEl);
- }
-
- // make sure that the close button is last in the dialog DOM
- const closeButton = this.getChild('closeButton');
- if (closeButton) {
- parentEl.appendChild(closeButton.el_);
- }
- }
-
- /**
- * Empties the content element. This happens anytime the modal is filled.
- *
- * @fires ModalDialog#beforemodalempty
- * @fires ModalDialog#modalempty
- */
- empty() {
- /**
- * Fired just before a `ModalDialog` is emptied.
- *
- * @event ModalDialog#beforemodalempty
- * @type {Event}
- */
- this.trigger('beforemodalempty');
- emptyEl(this.contentEl());
-
- /**
- * Fired just after a `ModalDialog` is emptied.
- *
- * @event ModalDialog#modalempty
- * @type {Event}
- */
- this.trigger('modalempty');
- }
-
- /**
- * Gets or sets the modal content, which gets normalized before being
- * rendered into the DOM.
- *
- * This does not update the DOM or fill the modal, but it is called during
- * that process.
- *
- * @param { import('./utils/dom').ContentDescriptor} [value]
- * If defined, sets the internal content value to be used on the
- * next call(s) to `fill`. This value is normalized before being
- * inserted. To "clear" the internal content value, pass `null`.
- *
- * @return { import('./utils/dom').ContentDescriptor}
- * The current content of the modal dialog
- */
- content(value) {
- if (typeof value !== 'undefined') {
- this.content_ = value;
- }
- return this.content_;
- }
-
- /**
- * conditionally focus the modal dialog if focus was previously on the player.
- *
- * @private
- */
- conditionalFocus_() {
- const activeEl = document.activeElement;
- const playerEl = this.player_.el_;
- this.previouslyActiveEl_ = null;
- if (playerEl.contains(activeEl) || playerEl === activeEl) {
- this.previouslyActiveEl_ = activeEl;
- this.focus();
- }
- }
-
- /**
- * conditionally blur the element and refocus the last focused element
- *
- * @private
- */
- conditionalBlur_() {
- if (this.previouslyActiveEl_) {
- this.previouslyActiveEl_.focus();
- this.previouslyActiveEl_ = null;
- }
- }
-
- /**
- * Keydown handler. Attached when modal is focused.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Do not allow keydowns to reach out of the modal dialog.
- event.stopPropagation();
- if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
- event.preventDefault();
- this.close();
- return;
- }
-
- // exit early if it isn't a tab key
- if (!keycode.isEventKey(event, 'Tab')) {
- return;
- }
- const focusableEls = this.focusableEls_();
- const activeEl = this.el_.querySelector(':focus');
- let focusIndex;
- for (let i = 0; i < focusableEls.length; i++) {
- if (activeEl === focusableEls[i]) {
- focusIndex = i;
- break;
- }
- }
- if (document.activeElement === this.el_) {
- focusIndex = 0;
- }
- if (event.shiftKey && focusIndex === 0) {
- focusableEls[focusableEls.length - 1].focus();
- event.preventDefault();
- } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
- focusableEls[0].focus();
- event.preventDefault();
- }
- }
-
- /**
- * get all focusable elements
- *
- * @private
- */
- focusableEls_() {
- const allChildren = this.el_.querySelectorAll('*');
- return Array.prototype.filter.call(allChildren, child => {
- return (child instanceof window.HTMLAnchorElement || child instanceof window.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window.HTMLInputElement || child instanceof window.HTMLSelectElement || child instanceof window.HTMLTextAreaElement || child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window.HTMLIFrameElement || child instanceof window.HTMLObjectElement || child instanceof window.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
- });
- }
- }
-
- /**
- * Default options for `ModalDialog` default options.
- *
- * @type {Object}
- * @private
- */
- ModalDialog.prototype.options_ = {
- pauseOnOpen: true,
- temporary: true
- };
- Component$1.registerComponent('ModalDialog', ModalDialog);
-
- /**
- * @file track-list.js
- */
-
- /**
- * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
- * {@link VideoTrackList}
- *
- * @extends EventTarget
- */
- class TrackList extends EventTarget$2 {
- /**
- * Create an instance of this class
- *
- * @param { import('./track').default[] } tracks
- * A list of tracks to initialize the list with.
- *
- * @abstract
- */
- constructor(tracks = []) {
- super();
- this.tracks_ = [];
-
- /**
- * @memberof TrackList
- * @member {number} length
- * The current number of `Track`s in the this Trackist.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.tracks_.length;
- }
- });
- for (let i = 0; i < tracks.length; i++) {
- this.addTrack(tracks[i]);
- }
- }
-
- /**
- * Add a {@link Track} to the `TrackList`
- *
- * @param { import('./track').default } track
- * The audio, video, or text track to add to the list.
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- const index = this.tracks_.length;
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.tracks_[index];
- }
- });
- }
-
- // Do not add duplicate tracks
- if (this.tracks_.indexOf(track) === -1) {
- this.tracks_.push(track);
- /**
- * Triggered when a track is added to a track list.
- *
- * @event TrackList#addtrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was added.
- */
- this.trigger({
- track,
- type: 'addtrack',
- target: this
- });
- }
-
- /**
- * Triggered when a track label is changed.
- *
- * @event TrackList#addtrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was added.
- */
- track.labelchange_ = () => {
- this.trigger({
- track,
- type: 'labelchange',
- target: this
- });
- };
- if (isEvented(track)) {
- track.addEventListener('labelchange', track.labelchange_);
- }
- }
-
- /**
- * Remove a {@link Track} from the `TrackList`
- *
- * @param { import('./track').default } rtrack
- * The audio, video, or text track to remove from the list.
- *
- * @fires TrackList#removetrack
- */
- removeTrack(rtrack) {
- let track;
- for (let i = 0, l = this.length; i < l; i++) {
- if (this[i] === rtrack) {
- track = this[i];
- if (track.off) {
- track.off();
- }
- this.tracks_.splice(i, 1);
- break;
- }
- }
- if (!track) {
- return;
- }
-
- /**
- * Triggered when a track is removed from track list.
- *
- * @event TrackList#removetrack
- * @type {Event}
- * @property {Track} track
- * A reference to track that was removed.
- */
- this.trigger({
- track,
- type: 'removetrack',
- target: this
- });
- }
-
- /**
- * Get a Track from the TrackList by a tracks id
- *
- * @param {string} id - the id of the track to get
- * @method getTrackById
- * @return { import('./track').default }
- * @private
- */
- getTrackById(id) {
- let result = null;
- for (let i = 0, l = this.length; i < l; i++) {
- const track = this[i];
- if (track.id === id) {
- result = track;
- break;
- }
- }
- return result;
- }
- }
-
- /**
- * Triggered when a different track is selected/enabled.
- *
- * @event TrackList#change
- * @type {Event}
- */
-
- /**
- * Events that can be called with on + eventName. See {@link EventHandler}.
- *
- * @property {Object} TrackList#allowedEvents_
- * @protected
- */
- TrackList.prototype.allowedEvents_ = {
- change: 'change',
- addtrack: 'addtrack',
- removetrack: 'removetrack',
- labelchange: 'labelchange'
- };
-
- // emulate attribute EventHandler support to allow for feature detection
- for (const event in TrackList.prototype.allowedEvents_) {
- TrackList.prototype['on' + event] = null;
- }
-
- /**
- * @file audio-track-list.js
- */
-
- /**
- * Anywhere we call this function we diverge from the spec
- * as we only support one enabled audiotrack at a time
- *
- * @param {AudioTrackList} list
- * list to work on
- *
- * @param { import('./audio-track').default } track
- * The track to skip
- *
- * @private
- */
- const disableOthers$1 = function (list, track) {
- for (let i = 0; i < list.length; i++) {
- if (!Object.keys(list[i]).length || track.id === list[i].id) {
- continue;
- }
- // another audio track is enabled, disable it
- list[i].enabled = false;
- }
- };
-
- /**
- * The current list of {@link AudioTrack} for a media file.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
- * @extends TrackList
- */
- class AudioTrackList extends TrackList {
- /**
- * Create an instance of this class.
- *
- * @param { import('./audio-track').default[] } [tracks=[]]
- * A list of `AudioTrack` to instantiate the list with.
- */
- constructor(tracks = []) {
- // make sure only 1 track is enabled
- // sorted from last index to first index
- for (let i = tracks.length - 1; i >= 0; i--) {
- if (tracks[i].enabled) {
- disableOthers$1(tracks, tracks[i]);
- break;
- }
- }
- super(tracks);
- this.changing_ = false;
- }
-
- /**
- * Add an {@link AudioTrack} to the `AudioTrackList`.
- *
- * @param { import('./audio-track').default } track
- * The AudioTrack to add to the list
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- if (track.enabled) {
- disableOthers$1(this, track);
- }
- super.addTrack(track);
- // native tracks don't have this
- if (!track.addEventListener) {
- return;
- }
- track.enabledChange_ = () => {
- // when we are disabling other tracks (since we don't support
- // more than one track at a time) we will set changing_
- // to true so that we don't trigger additional change events
- if (this.changing_) {
- return;
- }
- this.changing_ = true;
- disableOthers$1(this, track);
- this.changing_ = false;
- this.trigger('change');
- };
-
- /**
- * @listens AudioTrack#enabledchange
- * @fires TrackList#change
- */
- track.addEventListener('enabledchange', track.enabledChange_);
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
- if (rtrack.removeEventListener && rtrack.enabledChange_) {
- rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
- rtrack.enabledChange_ = null;
- }
- }
- }
-
- /**
- * @file video-track-list.js
- */
-
- /**
- * Un-select all other {@link VideoTrack}s that are selected.
- *
- * @param {VideoTrackList} list
- * list to work on
- *
- * @param { import('./video-track').default } track
- * The track to skip
- *
- * @private
- */
- const disableOthers = function (list, track) {
- for (let i = 0; i < list.length; i++) {
- if (!Object.keys(list[i]).length || track.id === list[i].id) {
- continue;
- }
- // another video track is enabled, disable it
- list[i].selected = false;
- }
- };
-
- /**
- * The current list of {@link VideoTrack} for a video.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
- * @extends TrackList
- */
- class VideoTrackList extends TrackList {
- /**
- * Create an instance of this class.
- *
- * @param {VideoTrack[]} [tracks=[]]
- * A list of `VideoTrack` to instantiate the list with.
- */
- constructor(tracks = []) {
- // make sure only 1 track is enabled
- // sorted from last index to first index
- for (let i = tracks.length - 1; i >= 0; i--) {
- if (tracks[i].selected) {
- disableOthers(tracks, tracks[i]);
- break;
- }
- }
- super(tracks);
- this.changing_ = false;
-
- /**
- * @member {number} VideoTrackList#selectedIndex
- * The current index of the selected {@link VideoTrack`}.
- */
- Object.defineProperty(this, 'selectedIndex', {
- get() {
- for (let i = 0; i < this.length; i++) {
- if (this[i].selected) {
- return i;
- }
- }
- return -1;
- },
- set() {}
- });
- }
-
- /**
- * Add a {@link VideoTrack} to the `VideoTrackList`.
- *
- * @param { import('./video-track').default } track
- * The VideoTrack to add to the list
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- if (track.selected) {
- disableOthers(this, track);
- }
- super.addTrack(track);
- // native tracks don't have this
- if (!track.addEventListener) {
- return;
- }
- track.selectedChange_ = () => {
- if (this.changing_) {
- return;
- }
- this.changing_ = true;
- disableOthers(this, track);
- this.changing_ = false;
- this.trigger('change');
- };
-
- /**
- * @listens VideoTrack#selectedchange
- * @fires TrackList#change
- */
- track.addEventListener('selectedchange', track.selectedChange_);
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
- if (rtrack.removeEventListener && rtrack.selectedChange_) {
- rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
- rtrack.selectedChange_ = null;
- }
- }
- }
-
- /**
- * @file text-track-list.js
- */
-
- /**
- * The current list of {@link TextTrack} for a media file.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
- * @extends TrackList
- */
- class TextTrackList extends TrackList {
- /**
- * Add a {@link TextTrack} to the `TextTrackList`
- *
- * @param { import('./text-track').default } track
- * The text track to add to the list.
- *
- * @fires TrackList#addtrack
- */
- addTrack(track) {
- super.addTrack(track);
- if (!this.queueChange_) {
- this.queueChange_ = () => this.queueTrigger('change');
- }
- if (!this.triggerSelectedlanguagechange) {
- this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
- }
-
- /**
- * @listens TextTrack#modechange
- * @fires TrackList#change
- */
- track.addEventListener('modechange', this.queueChange_);
- const nonLanguageTextTrackKind = ['metadata', 'chapters'];
- if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
- track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
- }
- }
- removeTrack(rtrack) {
- super.removeTrack(rtrack);
-
- // manually remove the event handlers we added
- if (rtrack.removeEventListener) {
- if (this.queueChange_) {
- rtrack.removeEventListener('modechange', this.queueChange_);
- }
- if (this.selectedlanguagechange_) {
- rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
- }
- }
- }
- }
-
- /**
- * @file html-track-element-list.js
- */
-
- /**
- * The current list of {@link HtmlTrackElement}s.
- */
- class HtmlTrackElementList {
- /**
- * Create an instance of this class.
- *
- * @param {HtmlTrackElement[]} [tracks=[]]
- * A list of `HtmlTrackElement` to instantiate the list with.
- */
- constructor(trackElements = []) {
- this.trackElements_ = [];
-
- /**
- * @memberof HtmlTrackElementList
- * @member {number} length
- * The current number of `Track`s in the this Trackist.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.trackElements_.length;
- }
- });
- for (let i = 0, length = trackElements.length; i < length; i++) {
- this.addTrackElement_(trackElements[i]);
- }
- }
-
- /**
- * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
- *
- * @param {HtmlTrackElement} trackElement
- * The track element to add to the list.
- *
- * @private
- */
- addTrackElement_(trackElement) {
- const index = this.trackElements_.length;
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.trackElements_[index];
- }
- });
- }
-
- // Do not add duplicate elements
- if (this.trackElements_.indexOf(trackElement) === -1) {
- this.trackElements_.push(trackElement);
- }
- }
-
- /**
- * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
- * {@link TextTrack}.
- *
- * @param {TextTrack} track
- * The track associated with a track element.
- *
- * @return {HtmlTrackElement|undefined}
- * The track element that was found or undefined.
- *
- * @private
- */
- getTrackElementByTrack_(track) {
- let trackElement_;
- for (let i = 0, length = this.trackElements_.length; i < length; i++) {
- if (track === this.trackElements_[i].track) {
- trackElement_ = this.trackElements_[i];
- break;
- }
- }
- return trackElement_;
- }
-
- /**
- * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
- *
- * @param {HtmlTrackElement} trackElement
- * The track element to remove from the list.
- *
- * @private
- */
- removeTrackElement_(trackElement) {
- for (let i = 0, length = this.trackElements_.length; i < length; i++) {
- if (trackElement === this.trackElements_[i]) {
- if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
- this.trackElements_[i].track.off();
- }
- if (typeof this.trackElements_[i].off === 'function') {
- this.trackElements_[i].off();
- }
- this.trackElements_.splice(i, 1);
- break;
- }
- }
- }
- }
-
- /**
- * @file text-track-cue-list.js
- */
-
- /**
- * @typedef {Object} TextTrackCueList~TextTrackCue
- *
- * @property {string} id
- * The unique id for this text track cue
- *
- * @property {number} startTime
- * The start time for this text track cue
- *
- * @property {number} endTime
- * The end time for this text track cue
- *
- * @property {boolean} pauseOnExit
- * Pause when the end time is reached if true.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
- */
-
- /**
- * A List of TextTrackCues.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
- */
- class TextTrackCueList {
- /**
- * Create an instance of this class..
- *
- * @param {Array} cues
- * A list of cues to be initialized with
- */
- constructor(cues) {
- TextTrackCueList.prototype.setCues_.call(this, cues);
-
- /**
- * @memberof TextTrackCueList
- * @member {number} length
- * The current number of `TextTrackCue`s in the TextTrackCueList.
- * @instance
- */
- Object.defineProperty(this, 'length', {
- get() {
- return this.length_;
- }
- });
- }
-
- /**
- * A setter for cues in this list. Creates getters
- * an an index for the cues.
- *
- * @param {Array} cues
- * An array of cues to set
- *
- * @private
- */
- setCues_(cues) {
- const oldLength = this.length || 0;
- let i = 0;
- const l = cues.length;
- this.cues_ = cues;
- this.length_ = cues.length;
- const defineProp = function (index) {
- if (!('' + index in this)) {
- Object.defineProperty(this, '' + index, {
- get() {
- return this.cues_[index];
- }
- });
- }
- };
- if (oldLength < l) {
- i = oldLength;
- for (; i < l; i++) {
- defineProp.call(this, i);
- }
- }
- }
-
- /**
- * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
- *
- * @param {string} id
- * The id of the cue that should be searched for.
- *
- * @return {TextTrackCueList~TextTrackCue|null}
- * A single cue or null if none was found.
- */
- getCueById(id) {
- let result = null;
- for (let i = 0, l = this.length; i < l; i++) {
- const cue = this[i];
- if (cue.id === id) {
- result = cue;
- break;
- }
- }
- return result;
- }
- }
-
- /**
- * @file track-kinds.js
- */
-
- /**
- * All possible `VideoTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
- * @typedef VideoTrack~Kind
- * @enum
- */
- const VideoTrackKind = {
- alternative: 'alternative',
- captions: 'captions',
- main: 'main',
- sign: 'sign',
- subtitles: 'subtitles',
- commentary: 'commentary'
- };
-
- /**
- * All possible `AudioTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
- * @typedef AudioTrack~Kind
- * @enum
- */
- const AudioTrackKind = {
- 'alternative': 'alternative',
- 'descriptions': 'descriptions',
- 'main': 'main',
- 'main-desc': 'main-desc',
- 'translation': 'translation',
- 'commentary': 'commentary'
- };
-
- /**
- * All possible `TextTrackKind`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
- * @typedef TextTrack~Kind
- * @enum
- */
- const TextTrackKind = {
- subtitles: 'subtitles',
- captions: 'captions',
- descriptions: 'descriptions',
- chapters: 'chapters',
- metadata: 'metadata'
- };
-
- /**
- * All possible `TextTrackMode`s
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
- * @typedef TextTrack~Mode
- * @enum
- */
- const TextTrackMode = {
- disabled: 'disabled',
- hidden: 'hidden',
- showing: 'showing'
- };
-
- /**
- * @file track.js
- */
-
- /**
- * A Track class that contains all of the common functionality for {@link AudioTrack},
- * {@link VideoTrack}, and {@link TextTrack}.
- *
- * > Note: This class should not be used directly
- *
- * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
- * @extends EventTarget
- * @abstract
- */
- class Track extends EventTarget$2 {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {string} [options.kind='']
- * A valid kind for the track type you are creating.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @abstract
- */
- constructor(options = {}) {
- super();
- const trackProps = {
- id: options.id || 'vjs_track_' + newGUID(),
- kind: options.kind || '',
- language: options.language || ''
- };
- let label = options.label || '';
-
- /**
- * @memberof Track
- * @member {string} id
- * The id of this track. Cannot be changed after creation.
- * @instance
- *
- * @readonly
- */
-
- /**
- * @memberof Track
- * @member {string} kind
- * The kind of track that this is. Cannot be changed after creation.
- * @instance
- *
- * @readonly
- */
-
- /**
- * @memberof Track
- * @member {string} language
- * The two letter language code for this track. Cannot be changed after
- * creation.
- * @instance
- *
- * @readonly
- */
-
- for (const key in trackProps) {
- Object.defineProperty(this, key, {
- get() {
- return trackProps[key];
- },
- set() {}
- });
- }
-
- /**
- * @memberof Track
- * @member {string} label
- * The label of this track. Cannot be changed after creation.
- * @instance
- *
- * @fires Track#labelchange
- */
- Object.defineProperty(this, 'label', {
- get() {
- return label;
- },
- set(newLabel) {
- if (newLabel !== label) {
- label = newLabel;
-
- /**
- * An event that fires when label changes on this track.
- *
- * > Note: This is not part of the spec!
- *
- * @event Track#labelchange
- * @type {Event}
- */
- this.trigger('labelchange');
- }
- }
- });
- }
- }
-
- /**
- * @file url.js
- * @module url
- */
-
- /**
- * @typedef {Object} url:URLObject
- *
- * @property {string} protocol
- * The protocol of the url that was parsed.
- *
- * @property {string} hostname
- * The hostname of the url that was parsed.
- *
- * @property {string} port
- * The port of the url that was parsed.
- *
- * @property {string} pathname
- * The pathname of the url that was parsed.
- *
- * @property {string} search
- * The search query of the url that was parsed.
- *
- * @property {string} hash
- * The hash of the url that was parsed.
- *
- * @property {string} host
- * The host of the url that was parsed.
- */
-
- /**
- * Resolve and parse the elements of a URL.
- *
- * @function
- * @param {String} url
- * The url to parse
- *
- * @return {url:URLObject}
- * An object of url details
- */
- const parseUrl = function (url) {
- // This entire method can be replace with URL once we are able to drop IE11
-
- const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host'];
-
- // add the url to an anchor and let the browser parse the URL
- const a = document.createElement('a');
- a.href = url;
-
- // Copy the specific URL properties to a new object
- // This is also needed for IE because the anchor loses its
- // properties when it's removed from the dom
- const details = {};
- for (let i = 0; i < props.length; i++) {
- details[props[i]] = a[props[i]];
- }
-
- // IE adds the port to the host property unlike everyone else. If
- // a port identifier is added for standard ports, strip it.
- if (details.protocol === 'http:') {
- details.host = details.host.replace(/:80$/, '');
- }
- if (details.protocol === 'https:') {
- details.host = details.host.replace(/:443$/, '');
- }
- if (!details.protocol) {
- details.protocol = window.location.protocol;
- }
-
- /* istanbul ignore if */
- if (!details.host) {
- details.host = window.location.host;
- }
- return details;
- };
-
- /**
- * Get absolute version of relative URL.
- *
- * @function
- * @param {string} url
- * URL to make absolute
- *
- * @return {string}
- * Absolute URL
- *
- * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
- */
- const getAbsoluteURL = function (url) {
- // Check if absolute URL
- if (!url.match(/^https?:\/\//)) {
- // Add the url to an anchor and let the browser parse it to convert to an absolute url
- const a = document.createElement('a');
- a.href = url;
- url = a.href;
- }
- return url;
- };
-
- /**
- * Returns the extension of the passed file name. It will return an empty string
- * if passed an invalid path.
- *
- * @function
- * @param {string} path
- * The fileName path like '/path/to/file.mp4'
- *
- * @return {string}
- * The extension in lower case or an empty string if no
- * extension could be found.
- */
- const getFileExtension = function (path) {
- if (typeof path === 'string') {
- const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
- const pathParts = splitPathRe.exec(path);
- if (pathParts) {
- return pathParts.pop().toLowerCase();
- }
- }
- return '';
- };
-
- /**
- * Returns whether the url passed is a cross domain request or not.
- *
- * @function
- * @param {string} url
- * The url to check.
- *
- * @param {Object} [winLoc]
- * the domain to check the url against, defaults to window.location
- *
- * @param {string} [winLoc.protocol]
- * The window location protocol defaults to window.location.protocol
- *
- * @param {string} [winLoc.host]
- * The window location host defaults to window.location.host
- *
- * @return {boolean}
- * Whether it is a cross domain request or not.
- */
- const isCrossOrigin = function (url, winLoc = window.location) {
- const urlInfo = parseUrl(url);
-
- // IE8 protocol relative urls will return ':' for protocol
- const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol;
-
- // Check if url is for another domain/origin
- // IE8 doesn't know location.origin, so we won't rely on it here
- const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host;
- return crossOrigin;
- };
-
- var Url = /*#__PURE__*/Object.freeze({
- __proto__: null,
- parseUrl: parseUrl,
- getAbsoluteURL: getAbsoluteURL,
- getFileExtension: getFileExtension,
- isCrossOrigin: isCrossOrigin
- });
-
- var win;
- if (typeof window !== "undefined") {
- win = window;
- } else if (typeof commonjsGlobal !== "undefined") {
- win = commonjsGlobal;
- } else if (typeof self !== "undefined") {
- win = self;
- } else {
- win = {};
- }
- var window_1 = win;
-
- var _extends_1 = createCommonjsModule(function (module) {
- function _extends() {
- module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) {
- for (var i = 1; i < arguments.length; i++) {
- var source = arguments[i];
- for (var key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- target[key] = source[key];
- }
- }
- }
- return target;
- }, module.exports.__esModule = true, module.exports["default"] = module.exports;
- return _extends.apply(this, arguments);
- }
- module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports;
- });
- var _extends$1 = unwrapExports(_extends_1);
-
- var isFunction_1 = isFunction;
- var toString = Object.prototype.toString;
- function isFunction(fn) {
- if (!fn) {
- return false;
- }
- var string = toString.call(fn);
- return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && (
- // IE8 and below
- fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt);
- }
-
- var httpResponseHandler = function httpResponseHandler(callback, decodeResponseBody) {
- if (decodeResponseBody === void 0) {
- decodeResponseBody = false;
- }
- return function (err, response, responseBody) {
- // if the XHR failed, return that error
- if (err) {
- callback(err);
- return;
- } // if the HTTP status code is 4xx or 5xx, the request also failed
-
- if (response.statusCode >= 400 && response.statusCode <= 599) {
- var cause = responseBody;
- if (decodeResponseBody) {
- if (window_1.TextDecoder) {
- var charset = getCharset(response.headers && response.headers['content-type']);
- try {
- cause = new TextDecoder(charset).decode(responseBody);
- } catch (e) {}
- } else {
- cause = String.fromCharCode.apply(null, new Uint8Array(responseBody));
- }
- }
- callback({
- cause: cause
- });
- return;
- } // otherwise, request succeeded
-
- callback(null, responseBody);
- };
- };
- function getCharset(contentTypeHeader) {
- if (contentTypeHeader === void 0) {
- contentTypeHeader = '';
- }
- return contentTypeHeader.toLowerCase().split(';').reduce(function (charset, contentType) {
- var _contentType$split = contentType.split('='),
- type = _contentType$split[0],
- value = _contentType$split[1];
- if (type.trim() === 'charset') {
- return value.trim();
- }
- return charset;
- }, 'utf-8');
- }
- var httpHandler = httpResponseHandler;
-
- createXHR.httpHandler = httpHandler;
- /**
- * @license
- * slighly modified parse-headers 2.0.2
- * Copyright (c) 2014 David Björklund
- * Available under the MIT license
- *
- */
-
- var parseHeaders = function parseHeaders(headers) {
- var result = {};
- if (!headers) {
- return result;
- }
- headers.trim().split('\n').forEach(function (row) {
- var index = row.indexOf(':');
- var key = row.slice(0, index).trim().toLowerCase();
- var value = row.slice(index + 1).trim();
- if (typeof result[key] === 'undefined') {
- result[key] = value;
- } else if (Array.isArray(result[key])) {
- result[key].push(value);
- } else {
- result[key] = [result[key], value];
- }
- });
- return result;
- };
- var lib = createXHR; // Allow use of default import syntax in TypeScript
-
- var default_1 = createXHR;
- createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop$1;
- createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest;
- forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) {
- createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) {
- options = initParams(uri, options, callback);
- options.method = method.toUpperCase();
- return _createXHR(options);
- };
- });
- function forEachArray(array, iterator) {
- for (var i = 0; i < array.length; i++) {
- iterator(array[i]);
- }
- }
- function isEmpty(obj) {
- for (var i in obj) {
- if (obj.hasOwnProperty(i)) return false;
- }
- return true;
- }
- function initParams(uri, options, callback) {
- var params = uri;
- if (isFunction_1(options)) {
- callback = options;
- if (typeof uri === "string") {
- params = {
- uri: uri
- };
- }
- } else {
- params = _extends_1({}, options, {
- uri: uri
- });
- }
- params.callback = callback;
- return params;
- }
- function createXHR(uri, options, callback) {
- options = initParams(uri, options, callback);
- return _createXHR(options);
- }
- function _createXHR(options) {
- if (typeof options.callback === "undefined") {
- throw new Error("callback argument missing");
- }
- var called = false;
- var callback = function cbOnce(err, response, body) {
- if (!called) {
- called = true;
- options.callback(err, response, body);
- }
- };
- function readystatechange() {
- if (xhr.readyState === 4) {
- setTimeout(loadFunc, 0);
- }
- }
- function getBody() {
- // Chrome with requestType=blob throws errors arround when even testing access to responseText
- var body = undefined;
- if (xhr.response) {
- body = xhr.response;
- } else {
- body = xhr.responseText || getXml(xhr);
- }
- if (isJson) {
- try {
- body = JSON.parse(body);
- } catch (e) {}
- }
- return body;
- }
- function errorFunc(evt) {
- clearTimeout(timeoutTimer);
- if (!(evt instanceof Error)) {
- evt = new Error("" + (evt || "Unknown XMLHttpRequest Error"));
- }
- evt.statusCode = 0;
- return callback(evt, failureResponse);
- } // will load the data & process the response in a special response object
-
- function loadFunc() {
- if (aborted) return;
- var status;
- clearTimeout(timeoutTimer);
- if (options.useXDR && xhr.status === undefined) {
- //IE8 CORS GET successful response doesn't have a status field, but body is fine
- status = 200;
- } else {
- status = xhr.status === 1223 ? 204 : xhr.status;
- }
- var response = failureResponse;
- var err = null;
- if (status !== 0) {
- response = {
- body: getBody(),
- statusCode: status,
- method: method,
- headers: {},
- url: uri,
- rawRequest: xhr
- };
- if (xhr.getAllResponseHeaders) {
- //remember xhr can in fact be XDR for CORS in IE
- response.headers = parseHeaders(xhr.getAllResponseHeaders());
- }
- } else {
- err = new Error("Internal XMLHttpRequest Error");
- }
- return callback(err, response, response.body);
- }
- var xhr = options.xhr || null;
- if (!xhr) {
- if (options.cors || options.useXDR) {
- xhr = new createXHR.XDomainRequest();
- } else {
- xhr = new createXHR.XMLHttpRequest();
- }
- }
- var key;
- var aborted;
- var uri = xhr.url = options.uri || options.url;
- var method = xhr.method = options.method || "GET";
- var body = options.body || options.data;
- var headers = xhr.headers = options.headers || {};
- var sync = !!options.sync;
- var isJson = false;
- var timeoutTimer;
- var failureResponse = {
- body: undefined,
- headers: {},
- statusCode: 0,
- method: method,
- url: uri,
- rawRequest: xhr
- };
- if ("json" in options && options.json !== false) {
- isJson = true;
- headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user
-
- if (method !== "GET" && method !== "HEAD") {
- headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user
-
- body = JSON.stringify(options.json === true ? body : options.json);
- }
- }
- xhr.onreadystatechange = readystatechange;
- xhr.onload = loadFunc;
- xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function.
-
- xhr.onprogress = function () {// IE must die
- };
- xhr.onabort = function () {
- aborted = true;
- };
- xhr.ontimeout = errorFunc;
- xhr.open(method, uri, !sync, options.username, options.password); //has to be after open
-
- if (!sync) {
- xhr.withCredentials = !!options.withCredentials;
- } // Cannot set timeout with sync request
- // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly
- // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent
-
- if (!sync && options.timeout > 0) {
- timeoutTimer = setTimeout(function () {
- if (aborted) return;
- aborted = true; //IE9 may still call readystatechange
-
- xhr.abort("timeout");
- var e = new Error("XMLHttpRequest timeout");
- e.code = "ETIMEDOUT";
- errorFunc(e);
- }, options.timeout);
- }
- if (xhr.setRequestHeader) {
- for (key in headers) {
- if (headers.hasOwnProperty(key)) {
- xhr.setRequestHeader(key, headers[key]);
- }
- }
- } else if (options.headers && !isEmpty(options.headers)) {
- throw new Error("Headers cannot be set on an XDomainRequest object");
- }
- if ("responseType" in options) {
- xhr.responseType = options.responseType;
- }
- if ("beforeSend" in options && typeof options.beforeSend === "function") {
- options.beforeSend(xhr);
- } // Microsoft Edge browser sends "undefined" when send is called with undefined value.
- // XMLHttpRequest spec says to pass null as body to indicate no body
- // See https://github.com/naugtur/xhr/issues/100.
-
- xhr.send(body || null);
- return xhr;
- }
- function getXml(xhr) {
- // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException"
- // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML.
- try {
- if (xhr.responseType === "document") {
- return xhr.responseXML;
- }
- var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror";
- if (xhr.responseType === "" && !firefoxBugTakenEffect) {
- return xhr.responseXML;
- }
- } catch (e) {}
- return null;
- }
- function noop$1() {}
- lib.default = default_1;
-
- /**
- * @file text-track.js
- */
-
- /**
- * Takes a webvtt file contents and parses it into cues
- *
- * @param {string} srcContent
- * webVTT file contents
- *
- * @param {TextTrack} track
- * TextTrack to add cues to. Cues come from the srcContent.
- *
- * @private
- */
- const parseCues = function (srcContent, track) {
- const parser = new window.WebVTT.Parser(window, window.vttjs, window.WebVTT.StringDecoder());
- const errors = [];
- parser.oncue = function (cue) {
- track.addCue(cue);
- };
- parser.onparsingerror = function (error) {
- errors.push(error);
- };
- parser.onflush = function () {
- track.trigger({
- type: 'loadeddata',
- target: track
- });
- };
- parser.parse(srcContent);
- if (errors.length > 0) {
- if (window.console && window.console.groupCollapsed) {
- window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
- }
- errors.forEach(error => log$1.error(error));
- if (window.console && window.console.groupEnd) {
- window.console.groupEnd();
- }
- }
- parser.flush();
- };
-
- /**
- * Load a `TextTrack` from a specified url.
- *
- * @param {string} src
- * Url to load track from.
- *
- * @param {TextTrack} track
- * Track to add cues to. Comes from the content at the end of `url`.
- *
- * @private
- */
- const loadTrack = function (src, track) {
- const opts = {
- uri: src
- };
- const crossOrigin = isCrossOrigin(src);
- if (crossOrigin) {
- opts.cors = crossOrigin;
- }
- const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
- if (withCredentials) {
- opts.withCredentials = withCredentials;
- }
- lib(opts, bind_(this, function (err, response, responseBody) {
- if (err) {
- return log$1.error(err, response);
- }
- track.loaded_ = true;
-
- // Make sure that vttjs has loaded, otherwise, wait till it finished loading
- // NOTE: this is only used for the alt/video.novtt.js build
- if (typeof window.WebVTT !== 'function') {
- if (track.tech_) {
- // to prevent use before define eslint error, we define loadHandler
- // as a let here
- track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
- if (event.type === 'vttjserror') {
- log$1.error(`vttjs failed to load, stopping trying to process ${track.src}`);
- return;
- }
- return parseCues(responseBody, track);
- });
- }
- } else {
- parseCues(responseBody, track);
- }
- }));
- };
-
- /**
- * A representation of a single `TextTrack`.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
- * @extends Track
- */
- class TextTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} options={}
- * Object of option names and values
- *
- * @param { import('../tech/tech').default } options.tech
- * A reference to the tech that owns this TextTrack.
- *
- * @param {TextTrack~Kind} [options.kind='subtitles']
- * A valid text track kind.
- *
- * @param {TextTrack~Mode} [options.mode='disabled']
- * A valid text track mode.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this TextTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {string} [options.srclang='']
- * A valid two character language code. An alternative, but deprioritized
- * version of `options.language`
- *
- * @param {string} [options.src]
- * A url to TextTrack cues.
- *
- * @param {boolean} [options.default]
- * If this track should default to on or off.
- */
- constructor(options = {}) {
- if (!options.tech) {
- throw new Error('A tech was not provided.');
- }
- const settings = merge$2(options, {
- kind: TextTrackKind[options.kind] || 'subtitles',
- language: options.language || options.srclang || ''
- });
- let mode = TextTrackMode[settings.mode] || 'disabled';
- const default_ = settings.default;
- if (settings.kind === 'metadata' || settings.kind === 'chapters') {
- mode = 'hidden';
- }
- super(settings);
- this.tech_ = settings.tech;
- this.cues_ = [];
- this.activeCues_ = [];
- this.preload_ = this.tech_.preloadTextTracks !== false;
- const cues = new TextTrackCueList(this.cues_);
- const activeCues = new TextTrackCueList(this.activeCues_);
- let changed = false;
- this.timeupdateHandler = bind_(this, function (event = {}) {
- if (this.tech_.isDisposed()) {
- return;
- }
- if (!this.tech_.isReady_) {
- if (event.type !== 'timeupdate') {
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- }
- return;
- }
-
- // Accessing this.activeCues for the side-effects of updating itself
- // due to its nature as a getter function. Do not remove or cues will
- // stop updating!
- // Use the setter to prevent deletion from uglify (pure_getters rule)
- this.activeCues = this.activeCues;
- if (changed) {
- this.trigger('cuechange');
- changed = false;
- }
- if (event.type !== 'timeupdate') {
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- }
- });
- const disposeHandler = () => {
- this.stopTracking();
- };
- this.tech_.one('dispose', disposeHandler);
- if (mode !== 'disabled') {
- this.startTracking();
- }
- Object.defineProperties(this, {
- /**
- * @memberof TextTrack
- * @member {boolean} default
- * If this track was set to be on or off by default. Cannot be changed after
- * creation.
- * @instance
- *
- * @readonly
- */
- default: {
- get() {
- return default_;
- },
- set() {}
- },
- /**
- * @memberof TextTrack
- * @member {string} mode
- * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
- * not be set if setting to an invalid mode.
- * @instance
- *
- * @fires TextTrack#modechange
- */
- mode: {
- get() {
- return mode;
- },
- set(newMode) {
- if (!TextTrackMode[newMode]) {
- return;
- }
- if (mode === newMode) {
- return;
- }
- mode = newMode;
- if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
- // On-demand load.
- loadTrack(this.src, this);
- }
- this.stopTracking();
- if (mode !== 'disabled') {
- this.startTracking();
- }
- /**
- * An event that fires when mode changes on this track. This allows
- * the TextTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec!
- *
- * @event TextTrack#modechange
- * @type {Event}
- */
- this.trigger('modechange');
- }
- },
- /**
- * @memberof TextTrack
- * @member {TextTrackCueList} cues
- * The text track cue list for this TextTrack.
- * @instance
- */
- cues: {
- get() {
- if (!this.loaded_) {
- return null;
- }
- return cues;
- },
- set() {}
- },
- /**
- * @memberof TextTrack
- * @member {TextTrackCueList} activeCues
- * The list text track cues that are currently active for this TextTrack.
- * @instance
- */
- activeCues: {
- get() {
- if (!this.loaded_) {
- return null;
- }
-
- // nothing to do
- if (this.cues.length === 0) {
- return activeCues;
- }
- const ct = this.tech_.currentTime();
- const active = [];
- for (let i = 0, l = this.cues.length; i < l; i++) {
- const cue = this.cues[i];
- if (cue.startTime <= ct && cue.endTime >= ct) {
- active.push(cue);
- }
- }
- changed = false;
- if (active.length !== this.activeCues_.length) {
- changed = true;
- } else {
- for (let i = 0; i < active.length; i++) {
- if (this.activeCues_.indexOf(active[i]) === -1) {
- changed = true;
- }
- }
- }
- this.activeCues_ = active;
- activeCues.setCues_(this.activeCues_);
- return activeCues;
- },
- // /!\ Keep this setter empty (see the timeupdate handler above)
- set() {}
- }
- });
- if (settings.src) {
- this.src = settings.src;
- if (!this.preload_) {
- // Tracks will load on-demand.
- // Act like we're loaded for other purposes.
- this.loaded_ = true;
- }
- if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
- loadTrack(this.src, this);
- }
- } else {
- this.loaded_ = true;
- }
- }
- startTracking() {
- // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
- this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
- // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
- this.tech_.on('timeupdate', this.timeupdateHandler);
- }
- stopTracking() {
- if (this.rvf_) {
- this.tech_.cancelVideoFrameCallback(this.rvf_);
- this.rvf_ = undefined;
- }
- this.tech_.off('timeupdate', this.timeupdateHandler);
- }
-
- /**
- * Add a cue to the internal list of cues.
- *
- * @param {TextTrack~Cue} cue
- * The cue to add to our internal list
- */
- addCue(originalCue) {
- let cue = originalCue;
-
- // Testing if the cue is a VTTCue in a way that survives minification
- if (!('getCueAsHTML' in cue)) {
- cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
- for (const prop in originalCue) {
- if (!(prop in cue)) {
- cue[prop] = originalCue[prop];
- }
- }
-
- // make sure that `id` is copied over
- cue.id = originalCue.id;
- cue.originalCue_ = originalCue;
- }
- const tracks = this.tech_.textTracks();
- for (let i = 0; i < tracks.length; i++) {
- if (tracks[i] !== this) {
- tracks[i].removeCue(cue);
- }
- }
- this.cues_.push(cue);
- this.cues.setCues_(this.cues_);
- }
-
- /**
- * Remove a cue from our internal list
- *
- * @param {TextTrack~Cue} removeCue
- * The cue to remove from our internal list
- */
- removeCue(removeCue) {
- let i = this.cues_.length;
- while (i--) {
- const cue = this.cues_[i];
- if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
- this.cues_.splice(i, 1);
- this.cues.setCues_(this.cues_);
- break;
- }
- }
- }
- }
-
- /**
- * cuechange - One or more cues in the track have become active or stopped being active.
- * @protected
- */
- TextTrack.prototype.allowedEvents_ = {
- cuechange: 'cuechange'
- };
-
- /**
- * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
- * only one `AudioTrack` in the list will be enabled at a time.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
- * @extends Track
- */
- class AudioTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {AudioTrack~Kind} [options.kind='']
- * A valid audio track kind
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {boolean} [options.enabled]
- * If this track is the one that is currently playing. If this track is part of
- * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
- */
- constructor(options = {}) {
- const settings = merge$2(options, {
- kind: AudioTrackKind[options.kind] || ''
- });
- super(settings);
- let enabled = false;
-
- /**
- * @memberof AudioTrack
- * @member {boolean} enabled
- * If this `AudioTrack` is enabled or not. When setting this will
- * fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
- * @instance
- *
- * @fires VideoTrack#selectedchange
- */
- Object.defineProperty(this, 'enabled', {
- get() {
- return enabled;
- },
- set(newEnabled) {
- // an invalid or unchanged value
- if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
- return;
- }
- enabled = newEnabled;
-
- /**
- * An event that fires when enabled changes on this track. This allows
- * the AudioTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec! Native tracks will do
- * this internally without an event.
- *
- * @event AudioTrack#enabledchange
- * @type {Event}
- */
- this.trigger('enabledchange');
- }
- });
-
- // if the user sets this track to selected then
- // set selected to that true value otherwise
- // we keep it false
- if (settings.enabled) {
- this.enabled = settings.enabled;
- }
- this.loaded_ = true;
- }
- }
-
- /**
- * A representation of a single `VideoTrack`.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
- * @extends Track
- */
- class VideoTrack extends Track {
- /**
- * Create an instance of this class.
- *
- * @param {Object} [options={}]
- * Object of option names and values
- *
- * @param {string} [options.kind='']
- * A valid {@link VideoTrack~Kind}
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this AudioTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {boolean} [options.selected]
- * If this track is the one that is currently playing.
- */
- constructor(options = {}) {
- const settings = merge$2(options, {
- kind: VideoTrackKind[options.kind] || ''
- });
- super(settings);
- let selected = false;
-
- /**
- * @memberof VideoTrack
- * @member {boolean} selected
- * If this `VideoTrack` is selected or not. When setting this will
- * fire {@link VideoTrack#selectedchange} if the state of selected changed.
- * @instance
- *
- * @fires VideoTrack#selectedchange
- */
- Object.defineProperty(this, 'selected', {
- get() {
- return selected;
- },
- set(newSelected) {
- // an invalid or unchanged value
- if (typeof newSelected !== 'boolean' || newSelected === selected) {
- return;
- }
- selected = newSelected;
-
- /**
- * An event that fires when selected changes on this track. This allows
- * the VideoTrackList that holds this track to act accordingly.
- *
- * > Note: This is not part of the spec! Native tracks will do
- * this internally without an event.
- *
- * @event VideoTrack#selectedchange
- * @type {Event}
- */
- this.trigger('selectedchange');
- }
- });
-
- // if the user sets this track to selected then
- // set selected to that true value otherwise
- // we keep it false
- if (settings.selected) {
- this.selected = settings.selected;
- }
- }
- }
-
- /**
- * @file html-track-element.js
- */
-
- /**
- * A single track represented in the DOM.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
- * @extends EventTarget
- */
- class HTMLTrackElement extends EventTarget$2 {
- /**
- * Create an instance of this class.
- *
- * @param {Object} options={}
- * Object of option names and values
- *
- * @param { import('../tech/tech').default } options.tech
- * A reference to the tech that owns this HTMLTrackElement.
- *
- * @param {TextTrack~Kind} [options.kind='subtitles']
- * A valid text track kind.
- *
- * @param {TextTrack~Mode} [options.mode='disabled']
- * A valid text track mode.
- *
- * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
- * A unique id for this TextTrack.
- *
- * @param {string} [options.label='']
- * The menu label for this track.
- *
- * @param {string} [options.language='']
- * A valid two character language code.
- *
- * @param {string} [options.srclang='']
- * A valid two character language code. An alternative, but deprioritized
- * version of `options.language`
- *
- * @param {string} [options.src]
- * A url to TextTrack cues.
- *
- * @param {boolean} [options.default]
- * If this track should default to on or off.
- */
- constructor(options = {}) {
- super();
- let readyState;
- const track = new TextTrack(options);
- this.kind = track.kind;
- this.src = track.src;
- this.srclang = track.language;
- this.label = track.label;
- this.default = track.default;
- Object.defineProperties(this, {
- /**
- * @memberof HTMLTrackElement
- * @member {HTMLTrackElement~ReadyState} readyState
- * The current ready state of the track element.
- * @instance
- */
- readyState: {
- get() {
- return readyState;
- }
- },
- /**
- * @memberof HTMLTrackElement
- * @member {TextTrack} track
- * The underlying TextTrack object.
- * @instance
- *
- */
- track: {
- get() {
- return track;
- }
- }
- });
- readyState = HTMLTrackElement.NONE;
-
- /**
- * @listens TextTrack#loadeddata
- * @fires HTMLTrackElement#load
- */
- track.addEventListener('loadeddata', () => {
- readyState = HTMLTrackElement.LOADED;
- this.trigger({
- type: 'load',
- target: this
- });
- });
- }
- }
-
- /**
- * @protected
- */
- HTMLTrackElement.prototype.allowedEvents_ = {
- load: 'load'
- };
-
- /**
- * The text track not loaded state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.NONE = 0;
-
- /**
- * The text track loading state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.LOADING = 1;
-
- /**
- * The text track loaded state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.LOADED = 2;
-
- /**
- * The text track failed to load state.
- *
- * @type {number}
- * @static
- */
- HTMLTrackElement.ERROR = 3;
-
- /*
- * This file contains all track properties that are used in
- * player.js, tech.js, html5.js and possibly other techs in the future.
- */
-
- const NORMAL = {
- audio: {
- ListClass: AudioTrackList,
- TrackClass: AudioTrack,
- capitalName: 'Audio'
- },
- video: {
- ListClass: VideoTrackList,
- TrackClass: VideoTrack,
- capitalName: 'Video'
- },
- text: {
- ListClass: TextTrackList,
- TrackClass: TextTrack,
- capitalName: 'Text'
- }
- };
- Object.keys(NORMAL).forEach(function (type) {
- NORMAL[type].getterName = `${type}Tracks`;
- NORMAL[type].privateName = `${type}Tracks_`;
- });
- const REMOTE = {
- remoteText: {
- ListClass: TextTrackList,
- TrackClass: TextTrack,
- capitalName: 'RemoteText',
- getterName: 'remoteTextTracks',
- privateName: 'remoteTextTracks_'
- },
- remoteTextEl: {
- ListClass: HtmlTrackElementList,
- TrackClass: HTMLTrackElement,
- capitalName: 'RemoteTextTrackEls',
- getterName: 'remoteTextTrackEls',
- privateName: 'remoteTextTrackEls_'
- }
- };
- const ALL = Object.assign({}, NORMAL, REMOTE);
- REMOTE.names = Object.keys(REMOTE);
- NORMAL.names = Object.keys(NORMAL);
- ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
-
- var minDoc = {};
-
- var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {};
- var doccy;
- if (typeof document !== 'undefined') {
- doccy = document;
- } else {
- doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'];
- if (!doccy) {
- doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc;
- }
- }
- var document_1 = doccy;
-
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
- /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-
- var _objCreate = Object.create || function () {
- function F() {}
- return function (o) {
- if (arguments.length !== 1) {
- throw new Error('Object.create shim only accepts one parameter.');
- }
- F.prototype = o;
- return new F();
- };
- }();
-
- // Creates a new ParserError object from an errorData object. The errorData
- // object should have default code and message properties. The default message
- // property can be overriden by passing in a message parameter.
- // See ParsingError.Errors below for acceptable errors.
- function ParsingError(errorData, message) {
- this.name = "ParsingError";
- this.code = errorData.code;
- this.message = message || errorData.message;
- }
- ParsingError.prototype = _objCreate(Error.prototype);
- ParsingError.prototype.constructor = ParsingError;
-
- // ParsingError metadata for acceptable ParsingErrors.
- ParsingError.Errors = {
- BadSignature: {
- code: 0,
- message: "Malformed WebVTT signature."
- },
- BadTimeStamp: {
- code: 1,
- message: "Malformed time stamp."
- }
- };
-
- // Try to parse input as a time stamp.
- function parseTimeStamp(input) {
- function computeSeconds(h, m, s, f) {
- return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
- }
- var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/);
- if (!m) {
- return null;
- }
- if (m[3]) {
- // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
- return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]);
- } else if (m[1] > 59) {
- // Timestamp takes the form of [hours]:[minutes].[milliseconds]
- // First position is hours as it's over 59.
- return computeSeconds(m[1], m[2], 0, m[4]);
- } else {
- // Timestamp takes the form of [minutes]:[seconds].[milliseconds]
- return computeSeconds(0, m[1], m[2], m[4]);
- }
- }
-
- // A settings object holds key/value pairs and will ignore anything but the first
- // assignment to a specific key.
- function Settings() {
- this.values = _objCreate(null);
- }
- Settings.prototype = {
- // Only accept the first assignment to any key.
- set: function (k, v) {
- if (!this.get(k) && v !== "") {
- this.values[k] = v;
- }
- },
- // Return the value for a key, or a default value.
- // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
- // a number of possible default values as properties where 'defaultKey' is
- // the key of the property that will be chosen; otherwise it's assumed to be
- // a single value.
- get: function (k, dflt, defaultKey) {
- if (defaultKey) {
- return this.has(k) ? this.values[k] : dflt[defaultKey];
- }
- return this.has(k) ? this.values[k] : dflt;
- },
- // Check whether we have a value for a key.
- has: function (k) {
- return k in this.values;
- },
- // Accept a setting if its one of the given alternatives.
- alt: function (k, v, a) {
- for (var n = 0; n < a.length; ++n) {
- if (v === a[n]) {
- this.set(k, v);
- break;
- }
- }
- },
- // Accept a setting if its a valid (signed) integer.
- integer: function (k, v) {
- if (/^-?\d+$/.test(v)) {
- // integer
- this.set(k, parseInt(v, 10));
- }
- },
- // Accept a setting if its a valid percentage.
- percent: function (k, v) {
- if (v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) {
- v = parseFloat(v);
- if (v >= 0 && v <= 100) {
- this.set(k, v);
- return true;
- }
- }
- return false;
- }
- };
-
- // Helper function to parse input into groups separated by 'groupDelim', and
- // interprete each group as a key/value pair separated by 'keyValueDelim'.
- function parseOptions(input, callback, keyValueDelim, groupDelim) {
- var groups = groupDelim ? input.split(groupDelim) : [input];
- for (var i in groups) {
- if (typeof groups[i] !== "string") {
- continue;
- }
- var kv = groups[i].split(keyValueDelim);
- if (kv.length !== 2) {
- continue;
- }
- var k = kv[0].trim();
- var v = kv[1].trim();
- callback(k, v);
- }
- }
- function parseCue(input, cue, regionList) {
- // Remember the original input if we need to throw an error.
- var oInput = input;
- // 4.1 WebVTT timestamp
- function consumeTimeStamp() {
- var ts = parseTimeStamp(input);
- if (ts === null) {
- throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput);
- }
- // Remove time stamp from input.
- input = input.replace(/^[^\sa-zA-Z-]+/, "");
- return ts;
- }
-
- // 4.4.2 WebVTT cue settings
- function consumeCueSettings(input, cue) {
- var settings = new Settings();
- parseOptions(input, function (k, v) {
- switch (k) {
- case "region":
- // Find the last region we parsed with the same region id.
- for (var i = regionList.length - 1; i >= 0; i--) {
- if (regionList[i].id === v) {
- settings.set(k, regionList[i].region);
- break;
- }
- }
- break;
- case "vertical":
- settings.alt(k, v, ["rl", "lr"]);
- break;
- case "line":
- var vals = v.split(","),
- vals0 = vals[0];
- settings.integer(k, vals0);
- settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
- settings.alt(k, vals0, ["auto"]);
- if (vals.length === 2) {
- settings.alt("lineAlign", vals[1], ["start", "center", "end"]);
- }
- break;
- case "position":
- vals = v.split(",");
- settings.percent(k, vals[0]);
- if (vals.length === 2) {
- settings.alt("positionAlign", vals[1], ["start", "center", "end"]);
- }
- break;
- case "size":
- settings.percent(k, v);
- break;
- case "align":
- settings.alt(k, v, ["start", "center", "end", "left", "right"]);
- break;
- }
- }, /:/, /\s/);
-
- // Apply default values for any missing fields.
- cue.region = settings.get("region", null);
- cue.vertical = settings.get("vertical", "");
- try {
- cue.line = settings.get("line", "auto");
- } catch (e) {}
- cue.lineAlign = settings.get("lineAlign", "start");
- cue.snapToLines = settings.get("snapToLines", true);
- cue.size = settings.get("size", 100);
- // Safari still uses the old middle value and won't accept center
- try {
- cue.align = settings.get("align", "center");
- } catch (e) {
- cue.align = settings.get("align", "middle");
- }
- try {
- cue.position = settings.get("position", "auto");
- } catch (e) {
- cue.position = settings.get("position", {
- start: 0,
- left: 0,
- center: 50,
- middle: 50,
- end: 100,
- right: 100
- }, cue.align);
- }
- cue.positionAlign = settings.get("positionAlign", {
- start: "start",
- left: "start",
- center: "center",
- middle: "center",
- end: "end",
- right: "end"
- }, cue.align);
- }
- function skipWhitespace() {
- input = input.replace(/^\s+/, "");
- }
-
- // 4.1 WebVTT cue timings.
- skipWhitespace();
- cue.startTime = consumeTimeStamp(); // (1) collect cue start time
- skipWhitespace();
- if (input.substr(0, 3) !== "-->") {
- // (3) next characters must match "-->"
- throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput);
- }
- input = input.substr(3);
- skipWhitespace();
- cue.endTime = consumeTimeStamp(); // (5) collect cue end time
-
- // 4.1 WebVTT cue settings list.
- skipWhitespace();
- consumeCueSettings(input, cue);
- }
-
- // When evaluating this file as part of a Webpack bundle for server
- // side rendering, `document` is an empty object.
- var TEXTAREA_ELEMENT = document_1.createElement && document_1.createElement("textarea");
- var TAG_NAME = {
- c: "span",
- i: "i",
- b: "b",
- u: "u",
- ruby: "ruby",
- rt: "rt",
- v: "span",
- lang: "span"
- };
-
- // 5.1 default text color
- // 5.2 default text background color is equivalent to text color with bg_ prefix
- var DEFAULT_COLOR_CLASS = {
- white: 'rgba(255,255,255,1)',
- lime: 'rgba(0,255,0,1)',
- cyan: 'rgba(0,255,255,1)',
- red: 'rgba(255,0,0,1)',
- yellow: 'rgba(255,255,0,1)',
- magenta: 'rgba(255,0,255,1)',
- blue: 'rgba(0,0,255,1)',
- black: 'rgba(0,0,0,1)'
- };
- var TAG_ANNOTATION = {
- v: "title",
- lang: "lang"
- };
- var NEEDS_PARENT = {
- rt: "ruby"
- };
-
- // Parse content into a document fragment.
- function parseContent(window, input) {
- function nextToken() {
- // Check for end-of-string.
- if (!input) {
- return null;
- }
-
- // Consume 'n' characters from the input.
- function consume(result) {
- input = input.substr(result.length);
- return result;
- }
- var m = input.match(/^([^<]*)(<[^>]*>?)?/);
- // If there is some text before the next tag, return it, otherwise return
- // the tag.
- return consume(m[1] ? m[1] : m[2]);
- }
- function unescape(s) {
- TEXTAREA_ELEMENT.innerHTML = s;
- s = TEXTAREA_ELEMENT.textContent;
- TEXTAREA_ELEMENT.textContent = "";
- return s;
- }
- function shouldAdd(current, element) {
- return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName;
- }
-
- // Create an element for this tag.
- function createElement(type, annotation) {
- var tagName = TAG_NAME[type];
- if (!tagName) {
- return null;
- }
- var element = window.document.createElement(tagName);
- var name = TAG_ANNOTATION[type];
- if (name && annotation) {
- element[name] = annotation.trim();
- }
- return element;
- }
- var rootDiv = window.document.createElement("div"),
- current = rootDiv,
- t,
- tagStack = [];
- while ((t = nextToken()) !== null) {
- if (t[0] === '<') {
- if (t[1] === "/") {
- // If the closing tag matches, move back up to the parent node.
- if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
- tagStack.pop();
- current = current.parentNode;
- }
- // Otherwise just ignore the end tag.
- continue;
- }
- var ts = parseTimeStamp(t.substr(1, t.length - 2));
- var node;
- if (ts) {
- // Timestamps are lead nodes as well.
- node = window.document.createProcessingInstruction("timestamp", ts);
- current.appendChild(node);
- continue;
- }
- var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
- // If we can't parse the tag, skip to the next tag.
- if (!m) {
- continue;
- }
- // Try to construct an element, and ignore the tag if we couldn't.
- node = createElement(m[1], m[3]);
- if (!node) {
- continue;
- }
- // Determine if the tag should be added based on the context of where it
- // is placed in the cuetext.
- if (!shouldAdd(current, node)) {
- continue;
- }
- // Set the class list (as a list of classes, separated by space).
- if (m[2]) {
- var classes = m[2].split('.');
- classes.forEach(function (cl) {
- var bgColor = /^bg_/.test(cl);
- // slice out `bg_` if it's a background color
- var colorName = bgColor ? cl.slice(3) : cl;
- if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) {
- var propName = bgColor ? 'background-color' : 'color';
- var propValue = DEFAULT_COLOR_CLASS[colorName];
- node.style[propName] = propValue;
- }
- });
- node.className = classes.join(' ');
- }
- // Append the node to the current node, and enter the scope of the new
- // node.
- tagStack.push(m[1]);
- current.appendChild(node);
- current = node;
- continue;
- }
-
- // Text nodes are leaf nodes.
- current.appendChild(window.document.createTextNode(unescape(t)));
- }
- return rootDiv;
- }
-
- // This is a list of all the Unicode characters that have a strong
- // right-to-left category. What this means is that these characters are
- // written right-to-left for sure. It was generated by pulling all the strong
- // right-to-left characters out of the Unicode data table. That table can
- // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
- var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6], [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d], [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6], [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5], [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815], [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858], [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f], [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c], [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1], [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc], [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808], [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855], [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f], [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13], [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58], [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72], [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f], [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32], [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42], [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f], [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59], [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62], [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77], [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b], [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]];
- function isStrongRTLChar(charCode) {
- for (var i = 0; i < strongRTLRanges.length; i++) {
- var currentRange = strongRTLRanges[i];
- if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
- return true;
- }
- }
- return false;
- }
- function determineBidi(cueDiv) {
- var nodeStack = [],
- text = "",
- charCode;
- if (!cueDiv || !cueDiv.childNodes) {
- return "ltr";
- }
- function pushNodes(nodeStack, node) {
- for (var i = node.childNodes.length - 1; i >= 0; i--) {
- nodeStack.push(node.childNodes[i]);
- }
- }
- function nextTextNode(nodeStack) {
- if (!nodeStack || !nodeStack.length) {
- return null;
- }
- var node = nodeStack.pop(),
- text = node.textContent || node.innerText;
- if (text) {
- // TODO: This should match all unicode type B characters (paragraph
- // separator characters). See issue #115.
- var m = text.match(/^.*(\n|\r)/);
- if (m) {
- nodeStack.length = 0;
- return m[0];
- }
- return text;
- }
- if (node.tagName === "ruby") {
- return nextTextNode(nodeStack);
- }
- if (node.childNodes) {
- pushNodes(nodeStack, node);
- return nextTextNode(nodeStack);
- }
- }
- pushNodes(nodeStack, cueDiv);
- while (text = nextTextNode(nodeStack)) {
- for (var i = 0; i < text.length; i++) {
- charCode = text.charCodeAt(i);
- if (isStrongRTLChar(charCode)) {
- return "rtl";
- }
- }
- }
- return "ltr";
- }
- function computeLinePos(cue) {
- if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) {
- return cue.line;
- }
- if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) {
- return -1;
- }
- var track = cue.track,
- trackList = track.textTrackList,
- count = 0;
- for (var i = 0; i < trackList.length && trackList[i] !== track; i++) {
- if (trackList[i].mode === "showing") {
- count++;
- }
- }
- return ++count * -1;
- }
- function StyleBox() {}
-
- // Apply styles to a div. If there is no div passed then it defaults to the
- // div on 'this'.
- StyleBox.prototype.applyStyles = function (styles, div) {
- div = div || this.div;
- for (var prop in styles) {
- if (styles.hasOwnProperty(prop)) {
- div.style[prop] = styles[prop];
- }
- }
- };
- StyleBox.prototype.formatStyle = function (val, unit) {
- return val === 0 ? 0 : val + unit;
- };
-
- // Constructs the computed display state of the cue (a div). Places the div
- // into the overlay which should be a block level element (usually a div).
- function CueStyleBox(window, cue, styleOptions) {
- StyleBox.call(this);
- this.cue = cue;
-
- // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
- // have inline positioning and will function as the cue background box.
- this.cueDiv = parseContent(window, cue.text);
- var styles = {
- color: "rgba(255, 255, 255, 1)",
- backgroundColor: "rgba(0, 0, 0, 0.8)",
- position: "relative",
- left: 0,
- right: 0,
- top: 0,
- bottom: 0,
- display: "inline",
- writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
- unicodeBidi: "plaintext"
- };
- this.applyStyles(styles, this.cueDiv);
-
- // Create an absolutely positioned div that will be used to position the cue
- // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
- // mirrors of them except middle instead of center on Safari.
- this.div = window.document.createElement("div");
- styles = {
- direction: determineBidi(this.cueDiv),
- writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl",
- unicodeBidi: "plaintext",
- textAlign: cue.align === "middle" ? "center" : cue.align,
- font: styleOptions.font,
- whiteSpace: "pre-line",
- position: "absolute"
- };
- this.applyStyles(styles);
- this.div.appendChild(this.cueDiv);
-
- // Calculate the distance from the reference edge of the viewport to the text
- // position of the cue box. The reference edge will be resolved later when
- // the box orientation styles are applied.
- var textPos = 0;
- switch (cue.positionAlign) {
- case "start":
- case "line-left":
- textPos = cue.position;
- break;
- case "center":
- textPos = cue.position - cue.size / 2;
- break;
- case "end":
- case "line-right":
- textPos = cue.position - cue.size;
- break;
- }
-
- // Horizontal box orientation; textPos is the distance from the left edge of the
- // area to the left edge of the box and cue.size is the distance extending to
- // the right from there.
- if (cue.vertical === "") {
- this.applyStyles({
- left: this.formatStyle(textPos, "%"),
- width: this.formatStyle(cue.size, "%")
- });
- // Vertical box orientation; textPos is the distance from the top edge of the
- // area to the top edge of the box and cue.size is the height extending
- // downwards from there.
- } else {
- this.applyStyles({
- top: this.formatStyle(textPos, "%"),
- height: this.formatStyle(cue.size, "%")
- });
- }
- this.move = function (box) {
- this.applyStyles({
- top: this.formatStyle(box.top, "px"),
- bottom: this.formatStyle(box.bottom, "px"),
- left: this.formatStyle(box.left, "px"),
- right: this.formatStyle(box.right, "px"),
- height: this.formatStyle(box.height, "px"),
- width: this.formatStyle(box.width, "px")
- });
- };
- }
- CueStyleBox.prototype = _objCreate(StyleBox.prototype);
- CueStyleBox.prototype.constructor = CueStyleBox;
-
- // Represents the co-ordinates of an Element in a way that we can easily
- // compute things with such as if it overlaps or intersects with another Element.
- // Can initialize it with either a StyleBox or another BoxPosition.
- function BoxPosition(obj) {
- // Either a BoxPosition was passed in and we need to copy it, or a StyleBox
- // was passed in and we need to copy the results of 'getBoundingClientRect'
- // as the object returned is readonly. All co-ordinate values are in reference
- // to the viewport origin (top left).
- var lh, height, width, top;
- if (obj.div) {
- height = obj.div.offsetHeight;
- width = obj.div.offsetWidth;
- top = obj.div.offsetTop;
- var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects();
- obj = obj.div.getBoundingClientRect();
- // In certain cases the outter div will be slightly larger then the sum of
- // the inner div's lines. This could be due to bold text, etc, on some platforms.
- // In this case we should get the average line height and use that. This will
- // result in the desired behaviour.
- lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0;
- }
- this.left = obj.left;
- this.right = obj.right;
- this.top = obj.top || top;
- this.height = obj.height || height;
- this.bottom = obj.bottom || top + (obj.height || height);
- this.width = obj.width || width;
- this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
- }
-
- // Move the box along a particular axis. Optionally pass in an amount to move
- // the box. If no amount is passed then the default is the line height of the
- // box.
- BoxPosition.prototype.move = function (axis, toMove) {
- toMove = toMove !== undefined ? toMove : this.lineHeight;
- switch (axis) {
- case "+x":
- this.left += toMove;
- this.right += toMove;
- break;
- case "-x":
- this.left -= toMove;
- this.right -= toMove;
- break;
- case "+y":
- this.top += toMove;
- this.bottom += toMove;
- break;
- case "-y":
- this.top -= toMove;
- this.bottom -= toMove;
- break;
- }
- };
-
- // Check if this box overlaps another box, b2.
- BoxPosition.prototype.overlaps = function (b2) {
- return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top;
- };
-
- // Check if this box overlaps any other boxes in boxes.
- BoxPosition.prototype.overlapsAny = function (boxes) {
- for (var i = 0; i < boxes.length; i++) {
- if (this.overlaps(boxes[i])) {
- return true;
- }
- }
- return false;
- };
-
- // Check if this box is within another box.
- BoxPosition.prototype.within = function (container) {
- return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right;
- };
-
- // Check if this box is entirely within the container or it is overlapping
- // on the edge opposite of the axis direction passed. For example, if "+x" is
- // passed and the box is overlapping on the left edge of the container, then
- // return true.
- BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) {
- switch (axis) {
- case "+x":
- return this.left < container.left;
- case "-x":
- return this.right > container.right;
- case "+y":
- return this.top < container.top;
- case "-y":
- return this.bottom > container.bottom;
- }
- };
-
- // Find the percentage of the area that this box is overlapping with another
- // box.
- BoxPosition.prototype.intersectPercentage = function (b2) {
- var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
- y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
- intersectArea = x * y;
- return intersectArea / (this.height * this.width);
- };
-
- // Convert the positions from this box to CSS compatible positions using
- // the reference container's positions. This has to be done because this
- // box's positions are in reference to the viewport origin, whereas, CSS
- // values are in referecne to their respective edges.
- BoxPosition.prototype.toCSSCompatValues = function (reference) {
- return {
- top: this.top - reference.top,
- bottom: reference.bottom - this.bottom,
- left: this.left - reference.left,
- right: reference.right - this.right,
- height: this.height,
- width: this.width
- };
- };
-
- // Get an object that represents the box's position without anything extra.
- // Can pass a StyleBox, HTMLElement, or another BoxPositon.
- BoxPosition.getSimpleBoxPosition = function (obj) {
- var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0;
- var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0;
- var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0;
- obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj;
- var ret = {
- left: obj.left,
- right: obj.right,
- top: obj.top || top,
- height: obj.height || height,
- bottom: obj.bottom || top + (obj.height || height),
- width: obj.width || width
- };
- return ret;
- };
-
- // Move a StyleBox to its specified, or next best, position. The containerBox
- // is the box that contains the StyleBox, such as a div. boxPositions are
- // a list of other boxes that the styleBox can't overlap with.
- function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
- // Find the best position for a cue box, b, on the video. The axis parameter
- // is a list of axis, the order of which, it will move the box along. For example:
- // Passing ["+x", "-x"] will move the box first along the x axis in the positive
- // direction. If it doesn't find a good position for it there it will then move
- // it along the x axis in the negative direction.
- function findBestPosition(b, axis) {
- var bestPosition,
- specifiedPosition = new BoxPosition(b),
- percentage = 1; // Highest possible so the first thing we get is better.
-
- for (var i = 0; i < axis.length; i++) {
- while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) {
- b.move(axis[i]);
- }
- // We found a spot where we aren't overlapping anything. This is our
- // best position.
- if (b.within(containerBox)) {
- return b;
- }
- var p = b.intersectPercentage(containerBox);
- // If we're outside the container box less then we were on our last try
- // then remember this position as the best position.
- if (percentage > p) {
- bestPosition = new BoxPosition(b);
- percentage = p;
- }
- // Reset the box position to the specified position.
- b = new BoxPosition(specifiedPosition);
- }
- return bestPosition || specifiedPosition;
- }
- var boxPosition = new BoxPosition(styleBox),
- cue = styleBox.cue,
- linePos = computeLinePos(cue),
- axis = [];
-
- // If we have a line number to align the cue to.
- if (cue.snapToLines) {
- var size;
- switch (cue.vertical) {
- case "":
- axis = ["+y", "-y"];
- size = "height";
- break;
- case "rl":
- axis = ["+x", "-x"];
- size = "width";
- break;
- case "lr":
- axis = ["-x", "+x"];
- size = "width";
- break;
- }
- var step = boxPosition.lineHeight,
- position = step * Math.round(linePos),
- maxPosition = containerBox[size] + step,
- initialAxis = axis[0];
-
- // If the specified intial position is greater then the max position then
- // clamp the box to the amount of steps it would take for the box to
- // reach the max position.
- if (Math.abs(position) > maxPosition) {
- position = position < 0 ? -1 : 1;
- position *= Math.ceil(maxPosition / step) * step;
- }
-
- // If computed line position returns negative then line numbers are
- // relative to the bottom of the video instead of the top. Therefore, we
- // need to increase our initial position by the length or width of the
- // video, depending on the writing direction, and reverse our axis directions.
- if (linePos < 0) {
- position += cue.vertical === "" ? containerBox.height : containerBox.width;
- axis = axis.reverse();
- }
-
- // Move the box to the specified position. This may not be its best
- // position.
- boxPosition.move(initialAxis, position);
- } else {
- // If we have a percentage line value for the cue.
- var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100;
- switch (cue.lineAlign) {
- case "center":
- linePos -= calculatedPercentage / 2;
- break;
- case "end":
- linePos -= calculatedPercentage;
- break;
- }
-
- // Apply initial line position to the cue box.
- switch (cue.vertical) {
- case "":
- styleBox.applyStyles({
- top: styleBox.formatStyle(linePos, "%")
- });
- break;
- case "rl":
- styleBox.applyStyles({
- left: styleBox.formatStyle(linePos, "%")
- });
- break;
- case "lr":
- styleBox.applyStyles({
- right: styleBox.formatStyle(linePos, "%")
- });
- break;
- }
- axis = ["+y", "-x", "+x", "-y"];
-
- // Get the box position again after we've applied the specified positioning
- // to it.
- boxPosition = new BoxPosition(styleBox);
- }
- var bestPosition = findBestPosition(boxPosition, axis);
- styleBox.move(bestPosition.toCSSCompatValues(containerBox));
- }
- function WebVTT$1() {
- // Nothing
- }
-
- // Helper to allow strings to be decoded instead of the default binary utf8 data.
- WebVTT$1.StringDecoder = function () {
- return {
- decode: function (data) {
- if (!data) {
- return "";
- }
- if (typeof data !== "string") {
- throw new Error("Error - expected string data.");
- }
- return decodeURIComponent(encodeURIComponent(data));
- }
- };
- };
- WebVTT$1.convertCueToDOMTree = function (window, cuetext) {
- if (!window || !cuetext) {
- return null;
- }
- return parseContent(window, cuetext);
- };
- var FONT_SIZE_PERCENT = 0.05;
- var FONT_STYLE = "sans-serif";
- var CUE_BACKGROUND_PADDING = "1.5%";
-
- // Runs the processing model over the cues and regions passed to it.
- // @param overlay A block level element (usually a div) that the computed cues
- // and regions will be placed into.
- WebVTT$1.processCues = function (window, cues, overlay) {
- if (!window || !cues || !overlay) {
- return null;
- }
-
- // Remove all previous children.
- while (overlay.firstChild) {
- overlay.removeChild(overlay.firstChild);
- }
- var paddedOverlay = window.document.createElement("div");
- paddedOverlay.style.position = "absolute";
- paddedOverlay.style.left = "0";
- paddedOverlay.style.right = "0";
- paddedOverlay.style.top = "0";
- paddedOverlay.style.bottom = "0";
- paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
- overlay.appendChild(paddedOverlay);
-
- // Determine if we need to compute the display states of the cues. This could
- // be the case if a cue's state has been changed since the last computation or
- // if it has not been computed yet.
- function shouldCompute(cues) {
- for (var i = 0; i < cues.length; i++) {
- if (cues[i].hasBeenReset || !cues[i].displayState) {
- return true;
- }
- }
- return false;
- }
-
- // We don't need to recompute the cues' display states. Just reuse them.
- if (!shouldCompute(cues)) {
- for (var i = 0; i < cues.length; i++) {
- paddedOverlay.appendChild(cues[i].displayState);
- }
- return;
- }
- var boxPositions = [],
- containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
- fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
- var styleOptions = {
- font: fontSize + "px " + FONT_STYLE
- };
- (function () {
- var styleBox, cue;
- for (var i = 0; i < cues.length; i++) {
- cue = cues[i];
-
- // Compute the intial position and styles of the cue div.
- styleBox = new CueStyleBox(window, cue, styleOptions);
- paddedOverlay.appendChild(styleBox.div);
-
- // Move the cue div to it's correct line position.
- moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
-
- // Remember the computed div so that we don't have to recompute it later
- // if we don't have too.
- cue.displayState = styleBox.div;
- boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
- }
- })();
- };
- WebVTT$1.Parser = function (window, vttjs, decoder) {
- if (!decoder) {
- decoder = vttjs;
- vttjs = {};
- }
- if (!vttjs) {
- vttjs = {};
- }
- this.window = window;
- this.vttjs = vttjs;
- this.state = "INITIAL";
- this.buffer = "";
- this.decoder = decoder || new TextDecoder("utf8");
- this.regionList = [];
- };
- WebVTT$1.Parser.prototype = {
- // If the error is a ParsingError then report it to the consumer if
- // possible. If it's not a ParsingError then throw it like normal.
- reportOrThrowError: function (e) {
- if (e instanceof ParsingError) {
- this.onparsingerror && this.onparsingerror(e);
- } else {
- throw e;
- }
- },
- parse: function (data) {
- var self = this;
-
- // If there is no data then we won't decode it, but will just try to parse
- // whatever is in buffer already. This may occur in circumstances, for
- // example when flush() is called.
- if (data) {
- // Try to decode the data that we received.
- self.buffer += self.decoder.decode(data, {
- stream: true
- });
- }
- function collectNextLine() {
- var buffer = self.buffer;
- var pos = 0;
- while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
- ++pos;
- }
- var line = buffer.substr(0, pos);
- // Advance the buffer early in case we fail below.
- if (buffer[pos] === '\r') {
- ++pos;
- }
- if (buffer[pos] === '\n') {
- ++pos;
- }
- self.buffer = buffer.substr(pos);
- return line;
- }
-
- // 3.4 WebVTT region and WebVTT region settings syntax
- function parseRegion(input) {
- var settings = new Settings();
- parseOptions(input, function (k, v) {
- switch (k) {
- case "id":
- settings.set(k, v);
- break;
- case "width":
- settings.percent(k, v);
- break;
- case "lines":
- settings.integer(k, v);
- break;
- case "regionanchor":
- case "viewportanchor":
- var xy = v.split(',');
- if (xy.length !== 2) {
- break;
- }
- // We have to make sure both x and y parse, so use a temporary
- // settings object here.
- var anchor = new Settings();
- anchor.percent("x", xy[0]);
- anchor.percent("y", xy[1]);
- if (!anchor.has("x") || !anchor.has("y")) {
- break;
- }
- settings.set(k + "X", anchor.get("x"));
- settings.set(k + "Y", anchor.get("y"));
- break;
- case "scroll":
- settings.alt(k, v, ["up"]);
- break;
- }
- }, /=/, /\s/);
-
- // Create the region, using default values for any values that were not
- // specified.
- if (settings.has("id")) {
- var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)();
- region.width = settings.get("width", 100);
- region.lines = settings.get("lines", 3);
- region.regionAnchorX = settings.get("regionanchorX", 0);
- region.regionAnchorY = settings.get("regionanchorY", 100);
- region.viewportAnchorX = settings.get("viewportanchorX", 0);
- region.viewportAnchorY = settings.get("viewportanchorY", 100);
- region.scroll = settings.get("scroll", "");
- // Register the region.
- self.onregion && self.onregion(region);
- // Remember the VTTRegion for later in case we parse any VTTCues that
- // reference it.
- self.regionList.push({
- id: settings.get("id"),
- region: region
- });
- }
- }
-
- // draft-pantos-http-live-streaming-20
- // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
- // 3.5 WebVTT
- function parseTimestampMap(input) {
- var settings = new Settings();
- parseOptions(input, function (k, v) {
- switch (k) {
- case "MPEGT":
- settings.integer(k + 'S', v);
- break;
- case "LOCA":
- settings.set(k + 'L', parseTimeStamp(v));
- break;
- }
- }, /[^\d]:/, /,/);
- self.ontimestampmap && self.ontimestampmap({
- "MPEGTS": settings.get("MPEGTS"),
- "LOCAL": settings.get("LOCAL")
- });
- }
-
- // 3.2 WebVTT metadata header syntax
- function parseHeader(input) {
- if (input.match(/X-TIMESTAMP-MAP/)) {
- // This line contains HLS X-TIMESTAMP-MAP metadata
- parseOptions(input, function (k, v) {
- switch (k) {
- case "X-TIMESTAMP-MAP":
- parseTimestampMap(v);
- break;
- }
- }, /=/);
- } else {
- parseOptions(input, function (k, v) {
- switch (k) {
- case "Region":
- // 3.3 WebVTT region metadata header syntax
- parseRegion(v);
- break;
- }
- }, /:/);
- }
- }
-
- // 5.1 WebVTT file parsing.
- try {
- var line;
- if (self.state === "INITIAL") {
- // We can't start parsing until we have the first line.
- if (!/\r\n|\n/.test(self.buffer)) {
- return this;
- }
- line = collectNextLine();
- var m = line.match(/^WEBVTT([ \t].*)?$/);
- if (!m || !m[0]) {
- throw new ParsingError(ParsingError.Errors.BadSignature);
- }
- self.state = "HEADER";
- }
- var alreadyCollectedLine = false;
- while (self.buffer) {
- // We can't parse a line until we have the full line.
- if (!/\r\n|\n/.test(self.buffer)) {
- return this;
- }
- if (!alreadyCollectedLine) {
- line = collectNextLine();
- } else {
- alreadyCollectedLine = false;
- }
- switch (self.state) {
- case "HEADER":
- // 13-18 - Allow a header (metadata) under the WEBVTT line.
- if (/:/.test(line)) {
- parseHeader(line);
- } else if (!line) {
- // An empty line terminates the header and starts the body (cues).
- self.state = "ID";
- }
- continue;
- case "NOTE":
- // Ignore NOTE blocks.
- if (!line) {
- self.state = "ID";
- }
- continue;
- case "ID":
- // Check for the start of NOTE blocks.
- if (/^NOTE($|[ \t])/.test(line)) {
- self.state = "NOTE";
- break;
- }
- // 19-29 - Allow any number of line terminators, then initialize new cue values.
- if (!line) {
- continue;
- }
- self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, "");
- // Safari still uses the old middle value and won't accept center
- try {
- self.cue.align = "center";
- } catch (e) {
- self.cue.align = "middle";
- }
- self.state = "CUE";
- // 30-39 - Check if self line contains an optional identifier or timing data.
- if (line.indexOf("-->") === -1) {
- self.cue.id = line;
- continue;
- }
- // Process line as start of a cue.
- /*falls through*/
- case "CUE":
- // 40 - Collect cue timings and settings.
- try {
- parseCue(line, self.cue, self.regionList);
- } catch (e) {
- self.reportOrThrowError(e);
- // In case of an error ignore rest of the cue.
- self.cue = null;
- self.state = "BADCUE";
- continue;
- }
- self.state = "CUETEXT";
- continue;
- case "CUETEXT":
- var hasSubstring = line.indexOf("-->") !== -1;
- // 34 - If we have an empty line then report the cue.
- // 35 - If we have the special substring '-->' then report the cue,
- // but do not collect the line as we need to process the current
- // one as a new cue.
- if (!line || hasSubstring && (alreadyCollectedLine = true)) {
- // We are done parsing self cue.
- self.oncue && self.oncue(self.cue);
- self.cue = null;
- self.state = "ID";
- continue;
- }
- if (self.cue.text) {
- self.cue.text += "\n";
- }
- self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n');
- continue;
- case "BADCUE":
- // BADCUE
- // 54-62 - Collect and discard the remaining cue.
- if (!line) {
- self.state = "ID";
- }
- continue;
- }
- }
- } catch (e) {
- self.reportOrThrowError(e);
-
- // If we are currently parsing a cue, report what we have.
- if (self.state === "CUETEXT" && self.cue && self.oncue) {
- self.oncue(self.cue);
- }
- self.cue = null;
- // Enter BADWEBVTT state if header was not parsed correctly otherwise
- // another exception occurred so enter BADCUE state.
- self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
- }
- return this;
- },
- flush: function () {
- var self = this;
- try {
- // Finish decoding the stream.
- self.buffer += self.decoder.decode();
- // Synthesize the end of the current cue or region.
- if (self.cue || self.state === "HEADER") {
- self.buffer += "\n\n";
- self.parse();
- }
- // If we've flushed, parsed, and we're still on the INITIAL state then
- // that means we don't have enough of the stream to parse the first
- // line.
- if (self.state === "INITIAL") {
- throw new ParsingError(ParsingError.Errors.BadSignature);
- }
- } catch (e) {
- self.reportOrThrowError(e);
- }
- self.onflush && self.onflush();
- return this;
- }
- };
- var vtt = WebVTT$1;
-
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- var autoKeyword = "auto";
- var directionSetting = {
- "": 1,
- "lr": 1,
- "rl": 1
- };
- var alignSetting = {
- "start": 1,
- "center": 1,
- "end": 1,
- "left": 1,
- "right": 1,
- "auto": 1,
- "line-left": 1,
- "line-right": 1
- };
- function findDirectionSetting(value) {
- if (typeof value !== "string") {
- return false;
- }
- var dir = directionSetting[value.toLowerCase()];
- return dir ? value.toLowerCase() : false;
- }
- function findAlignSetting(value) {
- if (typeof value !== "string") {
- return false;
- }
- var align = alignSetting[value.toLowerCase()];
- return align ? value.toLowerCase() : false;
- }
- function VTTCue(startTime, endTime, text) {
- /**
- * Shim implementation specific properties. These properties are not in
- * the spec.
- */
-
- // Lets us know when the VTTCue's data has changed in such a way that we need
- // to recompute its display state. This lets us compute its display state
- // lazily.
- this.hasBeenReset = false;
-
- /**
- * VTTCue and TextTrackCue properties
- * http://dev.w3.org/html5/webvtt/#vttcue-interface
- */
-
- var _id = "";
- var _pauseOnExit = false;
- var _startTime = startTime;
- var _endTime = endTime;
- var _text = text;
- var _region = null;
- var _vertical = "";
- var _snapToLines = true;
- var _line = "auto";
- var _lineAlign = "start";
- var _position = "auto";
- var _positionAlign = "auto";
- var _size = 100;
- var _align = "center";
- Object.defineProperties(this, {
- "id": {
- enumerable: true,
- get: function () {
- return _id;
- },
- set: function (value) {
- _id = "" + value;
- }
- },
- "pauseOnExit": {
- enumerable: true,
- get: function () {
- return _pauseOnExit;
- },
- set: function (value) {
- _pauseOnExit = !!value;
- }
- },
- "startTime": {
- enumerable: true,
- get: function () {
- return _startTime;
- },
- set: function (value) {
- if (typeof value !== "number") {
- throw new TypeError("Start time must be set to a number.");
- }
- _startTime = value;
- this.hasBeenReset = true;
- }
- },
- "endTime": {
- enumerable: true,
- get: function () {
- return _endTime;
- },
- set: function (value) {
- if (typeof value !== "number") {
- throw new TypeError("End time must be set to a number.");
- }
- _endTime = value;
- this.hasBeenReset = true;
- }
- },
- "text": {
- enumerable: true,
- get: function () {
- return _text;
- },
- set: function (value) {
- _text = "" + value;
- this.hasBeenReset = true;
- }
- },
- "region": {
- enumerable: true,
- get: function () {
- return _region;
- },
- set: function (value) {
- _region = value;
- this.hasBeenReset = true;
- }
- },
- "vertical": {
- enumerable: true,
- get: function () {
- return _vertical;
- },
- set: function (value) {
- var setting = findDirectionSetting(value);
- // Have to check for false because the setting an be an empty string.
- if (setting === false) {
- throw new SyntaxError("Vertical: an invalid or illegal direction string was specified.");
- }
- _vertical = setting;
- this.hasBeenReset = true;
- }
- },
- "snapToLines": {
- enumerable: true,
- get: function () {
- return _snapToLines;
- },
- set: function (value) {
- _snapToLines = !!value;
- this.hasBeenReset = true;
- }
- },
- "line": {
- enumerable: true,
- get: function () {
- return _line;
- },
- set: function (value) {
- if (typeof value !== "number" && value !== autoKeyword) {
- throw new SyntaxError("Line: an invalid number or illegal string was specified.");
- }
- _line = value;
- this.hasBeenReset = true;
- }
- },
- "lineAlign": {
- enumerable: true,
- get: function () {
- return _lineAlign;
- },
- set: function (value) {
- var setting = findAlignSetting(value);
- if (!setting) {
- console.warn("lineAlign: an invalid or illegal string was specified.");
- } else {
- _lineAlign = setting;
- this.hasBeenReset = true;
- }
- }
- },
- "position": {
- enumerable: true,
- get: function () {
- return _position;
- },
- set: function (value) {
- if (value < 0 || value > 100) {
- throw new Error("Position must be between 0 and 100.");
- }
- _position = value;
- this.hasBeenReset = true;
- }
- },
- "positionAlign": {
- enumerable: true,
- get: function () {
- return _positionAlign;
- },
- set: function (value) {
- var setting = findAlignSetting(value);
- if (!setting) {
- console.warn("positionAlign: an invalid or illegal string was specified.");
- } else {
- _positionAlign = setting;
- this.hasBeenReset = true;
- }
- }
- },
- "size": {
- enumerable: true,
- get: function () {
- return _size;
- },
- set: function (value) {
- if (value < 0 || value > 100) {
- throw new Error("Size must be between 0 and 100.");
- }
- _size = value;
- this.hasBeenReset = true;
- }
- },
- "align": {
- enumerable: true,
- get: function () {
- return _align;
- },
- set: function (value) {
- var setting = findAlignSetting(value);
- if (!setting) {
- throw new SyntaxError("align: an invalid or illegal alignment string was specified.");
- }
- _align = setting;
- this.hasBeenReset = true;
- }
- }
- });
-
- /**
- * Other spec defined properties
- */
-
- // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state
- this.displayState = undefined;
- }
-
- /**
- * VTTCue methods
- */
-
- VTTCue.prototype.getCueAsHTML = function () {
- // Assume WebVTT.convertCueToDOMTree is on the global.
- return WebVTT.convertCueToDOMTree(window, this.text);
- };
- var vttcue = VTTCue;
-
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- var scrollSetting = {
- "": true,
- "up": true
- };
- function findScrollSetting(value) {
- if (typeof value !== "string") {
- return false;
- }
- var scroll = scrollSetting[value.toLowerCase()];
- return scroll ? value.toLowerCase() : false;
- }
- function isValidPercentValue(value) {
- return typeof value === "number" && value >= 0 && value <= 100;
- }
-
- // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface
- function VTTRegion() {
- var _width = 100;
- var _lines = 3;
- var _regionAnchorX = 0;
- var _regionAnchorY = 100;
- var _viewportAnchorX = 0;
- var _viewportAnchorY = 100;
- var _scroll = "";
- Object.defineProperties(this, {
- "width": {
- enumerable: true,
- get: function () {
- return _width;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("Width must be between 0 and 100.");
- }
- _width = value;
- }
- },
- "lines": {
- enumerable: true,
- get: function () {
- return _lines;
- },
- set: function (value) {
- if (typeof value !== "number") {
- throw new TypeError("Lines must be set to a number.");
- }
- _lines = value;
- }
- },
- "regionAnchorY": {
- enumerable: true,
- get: function () {
- return _regionAnchorY;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("RegionAnchorX must be between 0 and 100.");
- }
- _regionAnchorY = value;
- }
- },
- "regionAnchorX": {
- enumerable: true,
- get: function () {
- return _regionAnchorX;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("RegionAnchorY must be between 0 and 100.");
- }
- _regionAnchorX = value;
- }
- },
- "viewportAnchorY": {
- enumerable: true,
- get: function () {
- return _viewportAnchorY;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("ViewportAnchorY must be between 0 and 100.");
- }
- _viewportAnchorY = value;
- }
- },
- "viewportAnchorX": {
- enumerable: true,
- get: function () {
- return _viewportAnchorX;
- },
- set: function (value) {
- if (!isValidPercentValue(value)) {
- throw new Error("ViewportAnchorX must be between 0 and 100.");
- }
- _viewportAnchorX = value;
- }
- },
- "scroll": {
- enumerable: true,
- get: function () {
- return _scroll;
- },
- set: function (value) {
- var setting = findScrollSetting(value);
- // Have to check for false as an empty string is a legal value.
- if (setting === false) {
- console.warn("Scroll: an invalid or illegal string was specified.");
- } else {
- _scroll = setting;
- }
- }
- }
- });
- }
- var vttregion = VTTRegion;
-
- var browserIndex = createCommonjsModule(function (module) {
- /**
- * Copyright 2013 vtt.js Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- // Default exports for Node. Export the extended versions of VTTCue and
- // VTTRegion in Node since we likely want the capability to convert back and
- // forth between JSON. If we don't then it's not that big of a deal since we're
- // off browser.
-
- var vttjs = module.exports = {
- WebVTT: vtt,
- VTTCue: vttcue,
- VTTRegion: vttregion
- };
- window_1.vttjs = vttjs;
- window_1.WebVTT = vttjs.WebVTT;
- var cueShim = vttjs.VTTCue;
- var regionShim = vttjs.VTTRegion;
- var nativeVTTCue = window_1.VTTCue;
- var nativeVTTRegion = window_1.VTTRegion;
- vttjs.shim = function () {
- window_1.VTTCue = cueShim;
- window_1.VTTRegion = regionShim;
- };
- vttjs.restore = function () {
- window_1.VTTCue = nativeVTTCue;
- window_1.VTTRegion = nativeVTTRegion;
- };
- if (!window_1.VTTCue) {
- vttjs.shim();
- }
- });
- browserIndex.WebVTT;
- browserIndex.VTTCue;
- browserIndex.VTTRegion;
-
- /**
- * @file tech.js
- */
-
- /**
- * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
- * that just contains the src url alone.
- * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
- * `var SourceString = 'http://example.com/some-video.mp4';`
- *
- * @typedef {Object|string} SourceObject
- *
- * @property {string} src
- * The url to the source
- *
- * @property {string} type
- * The mime type of the source
- */
-
- /**
- * A function used by {@link Tech} to create a new {@link TextTrack}.
- *
- * @private
- *
- * @param {Tech} self
- * An instance of the Tech class.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @param {Object} [options={}]
- * An object with additional text track options
- *
- * @return {TextTrack}
- * The text track that was created.
- */
- function createTrackHelper(self, kind, label, language, options = {}) {
- const tracks = self.textTracks();
- options.kind = kind;
- if (label) {
- options.label = label;
- }
- if (language) {
- options.language = language;
- }
- options.tech = self;
- const track = new ALL.text.TrackClass(options);
- tracks.addTrack(track);
- return track;
- }
-
- /**
- * This is the base class for media playback technology controllers, such as
- * {@link HTML5}
- *
- * @extends Component
- */
- class Tech extends Component$1 {
- /**
- * Create an instance of this Tech.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * Callback function to call when the `HTML5` Tech is ready.
- */
- constructor(options = {}, ready = function () {}) {
- // we don't want the tech to report user activity automatically.
- // This is done manually in addControlsListeners
- options.reportTouchActivity = false;
- super(null, options, ready);
- this.onDurationChange_ = e => this.onDurationChange(e);
- this.trackProgress_ = e => this.trackProgress(e);
- this.trackCurrentTime_ = e => this.trackCurrentTime(e);
- this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
- this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
- this.queuedHanders_ = new Set();
-
- // keep track of whether the current source has played at all to
- // implement a very limited played()
- this.hasStarted_ = false;
- this.on('playing', function () {
- this.hasStarted_ = true;
- });
- this.on('loadstart', function () {
- this.hasStarted_ = false;
- });
- ALL.names.forEach(name => {
- const props = ALL[name];
- if (options && options[props.getterName]) {
- this[props.privateName] = options[props.getterName];
- }
- });
-
- // Manually track progress in cases where the browser/tech doesn't report it.
- if (!this.featuresProgressEvents) {
- this.manualProgressOn();
- }
-
- // Manually track timeupdates in cases where the browser/tech doesn't report it.
- if (!this.featuresTimeupdateEvents) {
- this.manualTimeUpdatesOn();
- }
- ['Text', 'Audio', 'Video'].forEach(track => {
- if (options[`native${track}Tracks`] === false) {
- this[`featuresNative${track}Tracks`] = false;
- }
- });
- if (options.nativeCaptions === false || options.nativeTextTracks === false) {
- this.featuresNativeTextTracks = false;
- } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
- this.featuresNativeTextTracks = true;
- }
- if (!this.featuresNativeTextTracks) {
- this.emulateTextTracks();
- }
- this.preloadTextTracks = options.preloadTextTracks !== false;
- this.autoRemoteTextTracks_ = new ALL.text.ListClass();
- this.initTrackListeners();
-
- // Turn on component tap events only if not using native controls
- if (!options.nativeControlsForTouch) {
- this.emitTapEvents();
- }
- if (this.constructor) {
- this.name_ = this.constructor.name || 'Unknown Tech';
- }
- }
-
- /**
- * A special function to trigger source set in a way that will allow player
- * to re-trigger if the player or tech are not ready yet.
- *
- * @fires Tech#sourceset
- * @param {string} src The source string at the time of the source changing.
- */
- triggerSourceset(src) {
- if (!this.isReady_) {
- // on initial ready we have to trigger source set
- // 1ms after ready so that player can watch for it.
- this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
- }
-
- /**
- * Fired when the source is set on the tech causing the media element
- * to reload.
- *
- * @see {@link Player#event:sourceset}
- * @event Tech#sourceset
- * @type {Event}
- */
- this.trigger({
- src,
- type: 'sourceset'
- });
- }
-
- /* Fallbacks for unsupported event types
- ================================================================================ */
-
- /**
- * Polyfill the `progress` event for browsers that don't support it natively.
- *
- * @see {@link Tech#trackProgress}
- */
- manualProgressOn() {
- this.on('durationchange', this.onDurationChange_);
- this.manualProgress = true;
-
- // Trigger progress watching when a source begins loading
- this.one('ready', this.trackProgress_);
- }
-
- /**
- * Turn off the polyfill for `progress` events that was created in
- * {@link Tech#manualProgressOn}
- */
- manualProgressOff() {
- this.manualProgress = false;
- this.stopTrackingProgress();
- this.off('durationchange', this.onDurationChange_);
- }
-
- /**
- * This is used to trigger a `progress` event when the buffered percent changes. It
- * sets an interval function that will be called every 500 milliseconds to check if the
- * buffer end percent has changed.
- *
- * > This function is called by {@link Tech#manualProgressOn}
- *
- * @param {Event} event
- * The `ready` event that caused this to run.
- *
- * @listens Tech#ready
- * @fires Tech#progress
- */
- trackProgress(event) {
- this.stopTrackingProgress();
- this.progressInterval = this.setInterval(bind_(this, function () {
- // Don't trigger unless buffered amount is greater than last time
-
- const numBufferedPercent = this.bufferedPercent();
- if (this.bufferedPercent_ !== numBufferedPercent) {
- /**
- * See {@link Player#progress}
- *
- * @event Tech#progress
- * @type {Event}
- */
- this.trigger('progress');
- }
- this.bufferedPercent_ = numBufferedPercent;
- if (numBufferedPercent === 1) {
- this.stopTrackingProgress();
- }
- }), 500);
- }
-
- /**
- * Update our internal duration on a `durationchange` event by calling
- * {@link Tech#duration}.
- *
- * @param {Event} event
- * The `durationchange` event that caused this to run.
- *
- * @listens Tech#durationchange
- */
- onDurationChange(event) {
- this.duration_ = this.duration();
- }
-
- /**
- * Get and create a `TimeRange` object for buffering.
- *
- * @return { import('../utils/time').TimeRange }
- * The time range object that was created.
- */
- buffered() {
- return createTimeRanges$1(0, 0);
- }
-
- /**
- * Get the percentage of the current video that is currently buffered.
- *
- * @return {number}
- * A number from 0 to 1 that represents the decimal percentage of the
- * video that is buffered.
- *
- */
- bufferedPercent() {
- return bufferedPercent(this.buffered(), this.duration_);
- }
-
- /**
- * Turn off the polyfill for `progress` events that was created in
- * {@link Tech#manualProgressOn}
- * Stop manually tracking progress events by clearing the interval that was set in
- * {@link Tech#trackProgress}.
- */
- stopTrackingProgress() {
- this.clearInterval(this.progressInterval);
- }
-
- /**
- * Polyfill the `timeupdate` event for browsers that don't support it.
- *
- * @see {@link Tech#trackCurrentTime}
- */
- manualTimeUpdatesOn() {
- this.manualTimeUpdates = true;
- this.on('play', this.trackCurrentTime_);
- this.on('pause', this.stopTrackingCurrentTime_);
- }
-
- /**
- * Turn off the polyfill for `timeupdate` events that was created in
- * {@link Tech#manualTimeUpdatesOn}
- */
- manualTimeUpdatesOff() {
- this.manualTimeUpdates = false;
- this.stopTrackingCurrentTime();
- this.off('play', this.trackCurrentTime_);
- this.off('pause', this.stopTrackingCurrentTime_);
- }
-
- /**
- * Sets up an interval function to track current time and trigger `timeupdate` every
- * 250 milliseconds.
- *
- * @listens Tech#play
- * @triggers Tech#timeupdate
- */
- trackCurrentTime() {
- if (this.currentTimeInterval) {
- this.stopTrackingCurrentTime();
- }
- this.currentTimeInterval = this.setInterval(function () {
- /**
- * Triggered at an interval of 250ms to indicated that time is passing in the video.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
-
- // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
- }, 250);
- }
-
- /**
- * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
- * `timeupdate` event is no longer triggered.
- *
- * @listens {Tech#pause}
- */
- stopTrackingCurrentTime() {
- this.clearInterval(this.currentTimeInterval);
-
- // #1002 - if the video ends right before the next timeupdate would happen,
- // the progress bar won't make it all the way to the end
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- }
-
- /**
- * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
- * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
- *
- * @fires Component#dispose
- */
- dispose() {
- // clear out all tracks because we can't reuse them between techs
- this.clearTracks(NORMAL.names);
-
- // Turn off any manual progress or timeupdate tracking
- if (this.manualProgress) {
- this.manualProgressOff();
- }
- if (this.manualTimeUpdates) {
- this.manualTimeUpdatesOff();
- }
- super.dispose();
- }
-
- /**
- * Clear out a single `TrackList` or an array of `TrackLists` given their names.
- *
- * > Note: Techs without source handlers should call this between sources for `video`
- * & `audio` tracks. You don't want to use them between tracks!
- *
- * @param {string[]|string} types
- * TrackList names to clear, valid names are `video`, `audio`, and
- * `text`.
- */
- clearTracks(types) {
- types = [].concat(types);
- // clear out all tracks because we can't reuse them between techs
- types.forEach(type => {
- const list = this[`${type}Tracks`]() || [];
- let i = list.length;
- while (i--) {
- const track = list[i];
- if (type === 'text') {
- this.removeRemoteTextTrack(track);
- }
- list.removeTrack(track);
- }
- });
- }
-
- /**
- * Remove any TextTracks added via addRemoteTextTrack that are
- * flagged for automatic garbage collection
- */
- cleanupAutoTextTracks() {
- const list = this.autoRemoteTextTracks_ || [];
- let i = list.length;
- while (i--) {
- const track = list[i];
- this.removeRemoteTextTrack(track);
- }
- }
-
- /**
- * Reset the tech, which will removes all sources and reset the internal readyState.
- *
- * @abstract
- */
- reset() {}
-
- /**
- * Get the value of `crossOrigin` from the tech.
- *
- * @abstract
- *
- * @see {Html5#crossOrigin}
- */
- crossOrigin() {}
-
- /**
- * Set the value of `crossOrigin` on the tech.
- *
- * @abstract
- *
- * @param {string} crossOrigin the crossOrigin value
- * @see {Html5#setCrossOrigin}
- */
- setCrossOrigin() {}
-
- /**
- * Get or set an error on the Tech.
- *
- * @param {MediaError} [err]
- * Error to set on the Tech
- *
- * @return {MediaError|null}
- * The current error object on the tech, or null if there isn't one.
- */
- error(err) {
- if (err !== undefined) {
- this.error_ = new MediaError(err);
- this.trigger('error');
- }
- return this.error_;
- }
-
- /**
- * Returns the `TimeRange`s that have been played through for the current source.
- *
- * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
- * It only checks whether the source has played at all or not.
- *
- * @return { import('../utils/time').TimeRange }
- * - A single time range if this video has played
- * - An empty set of ranges if not.
- */
- played() {
- if (this.hasStarted_) {
- return createTimeRanges$1(0, 0);
- }
- return createTimeRanges$1();
- }
-
- /**
- * Start playback
- *
- * @abstract
- *
- * @see {Html5#play}
- */
- play() {}
-
- /**
- * Set whether we are scrubbing or not
- *
- * @abstract
- * @param {boolean} _isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- *
- * @see {Html5#setScrubbing}
- */
- setScrubbing(_isScrubbing) {}
-
- /**
- * Get whether we are scrubbing or not
- *
- * @abstract
- *
- * @see {Html5#scrubbing}
- */
- scrubbing() {}
-
- /**
- * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
- * previously called.
- *
- * @param {number} _seconds
- * Set the current time of the media to this.
- * @fires Tech#timeupdate
- */
- setCurrentTime(_seconds) {
- // improve the accuracy of manual timeupdates
- if (this.manualTimeUpdates) {
- /**
- * A manual `timeupdate` event.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- }
- }
-
- /**
- * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
- * {@link TextTrackList} events.
- *
- * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
- *
- * @fires Tech#audiotrackchange
- * @fires Tech#videotrackchange
- * @fires Tech#texttrackchange
- */
- initTrackListeners() {
- /**
- * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
- *
- * @event Tech#audiotrackchange
- * @type {Event}
- */
-
- /**
- * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
- *
- * @event Tech#videotrackchange
- * @type {Event}
- */
-
- /**
- * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
- *
- * @event Tech#texttrackchange
- * @type {Event}
- */
- NORMAL.names.forEach(name => {
- const props = NORMAL[name];
- const trackListChanges = () => {
- this.trigger(`${name}trackchange`);
- };
- const tracks = this[props.getterName]();
- tracks.addEventListener('removetrack', trackListChanges);
- tracks.addEventListener('addtrack', trackListChanges);
- this.on('dispose', () => {
- tracks.removeEventListener('removetrack', trackListChanges);
- tracks.removeEventListener('addtrack', trackListChanges);
- });
- });
- }
-
- /**
- * Emulate TextTracks using vtt.js if necessary
- *
- * @fires Tech#vttjsloaded
- * @fires Tech#vttjserror
- */
- addWebVttScript_() {
- if (window.WebVTT) {
- return;
- }
-
- // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
- // signals that the Tech is ready at which point Tech.el_ is part of the DOM
- // before inserting the WebVTT script
- if (document.body.contains(this.el())) {
- // load via require if available and vtt.js script location was not passed in
- // as an option. novtt builds will turn the above require call into an empty object
- // which will cause this if check to always fail.
- if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) {
- this.trigger('vttjsloaded');
- return;
- }
-
- // load vtt.js via the script location option or the cdn of no location was
- // passed in
- const script = document.createElement('script');
- script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
- script.onload = () => {
- /**
- * Fired when vtt.js is loaded.
- *
- * @event Tech#vttjsloaded
- * @type {Event}
- */
- this.trigger('vttjsloaded');
- };
- script.onerror = () => {
- /**
- * Fired when vtt.js was not loaded due to an error
- *
- * @event Tech#vttjsloaded
- * @type {Event}
- */
- this.trigger('vttjserror');
- };
- this.on('dispose', () => {
- script.onload = null;
- script.onerror = null;
- });
- // but have not loaded yet and we set it to true before the inject so that
- // we don't overwrite the injected window.WebVTT if it loads right away
- window.WebVTT = true;
- this.el().parentNode.appendChild(script);
- } else {
- this.ready(this.addWebVttScript_);
- }
- }
-
- /**
- * Emulate texttracks
- *
- */
- emulateTextTracks() {
- const tracks = this.textTracks();
- const remoteTracks = this.remoteTextTracks();
- const handleAddTrack = e => tracks.addTrack(e.track);
- const handleRemoveTrack = e => tracks.removeTrack(e.track);
- remoteTracks.on('addtrack', handleAddTrack);
- remoteTracks.on('removetrack', handleRemoveTrack);
- this.addWebVttScript_();
- const updateDisplay = () => this.trigger('texttrackchange');
- const textTracksChanges = () => {
- updateDisplay();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- track.removeEventListener('cuechange', updateDisplay);
- if (track.mode === 'showing') {
- track.addEventListener('cuechange', updateDisplay);
- }
- }
- };
- textTracksChanges();
- tracks.addEventListener('change', textTracksChanges);
- tracks.addEventListener('addtrack', textTracksChanges);
- tracks.addEventListener('removetrack', textTracksChanges);
- this.on('dispose', function () {
- remoteTracks.off('addtrack', handleAddTrack);
- remoteTracks.off('removetrack', handleRemoveTrack);
- tracks.removeEventListener('change', textTracksChanges);
- tracks.removeEventListener('addtrack', textTracksChanges);
- tracks.removeEventListener('removetrack', textTracksChanges);
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- track.removeEventListener('cuechange', updateDisplay);
- }
- });
- }
-
- /**
- * Create and returns a remote {@link TextTrack} object.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @return {TextTrack}
- * The TextTrack that gets created.
- */
- addTextTrack(kind, label, language) {
- if (!kind) {
- throw new Error('TextTrack kind is required but was not provided');
- }
- return createTrackHelper(this, kind, label, language);
- }
-
- /**
- * Create an emulated TextTrack for use by addRemoteTextTrack
- *
- * This is intended to be overridden by classes that inherit from
- * Tech in order to create native or custom TextTracks.
- *
- * @param {Object} options
- * The object should contain the options to initialize the TextTrack with.
- *
- * @param {string} [options.kind]
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
- *
- * @param {string} [options.label].
- * Label to identify the text track
- *
- * @param {string} [options.language]
- * Two letter language abbreviation.
- *
- * @return {HTMLTrackElement}
- * The track element that gets created.
- */
- createRemoteTextTrack(options) {
- const track = merge$2(options, {
- tech: this
- });
- return new REMOTE.remoteTextEl.TrackClass(track);
- }
-
- /**
- * Creates a remote text track object and returns an html track element.
- *
- * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
- *
- * @param {Object} options
- * See {@link Tech#createRemoteTextTrack} for more detailed properties.
- *
- * @param {boolean} [manualCleanup=false]
- * - When false: the TextTrack will be automatically removed from the video
- * element whenever the source changes
- * - When True: The TextTrack will have to be cleaned up manually
- *
- * @return {HTMLTrackElement}
- * An Html Track Element.
- *
- */
- addRemoteTextTrack(options = {}, manualCleanup) {
- const htmlTrackElement = this.createRemoteTextTrack(options);
- if (typeof manualCleanup !== 'boolean') {
- manualCleanup = false;
- }
-
- // store HTMLTrackElement and TextTrack to remote list
- this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
- this.remoteTextTracks().addTrack(htmlTrackElement.track);
- if (manualCleanup === false) {
- // create the TextTrackList if it doesn't exist
- this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
- }
- return htmlTrackElement;
- }
-
- /**
- * Remove a remote text track from the remote `TextTrackList`.
- *
- * @param {TextTrack} track
- * `TextTrack` to remove from the `TextTrackList`
- */
- removeRemoteTextTrack(track) {
- const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
-
- // remove HTMLTrackElement and TextTrack from remote list
- this.remoteTextTrackEls().removeTrackElement_(trackElement);
- this.remoteTextTracks().removeTrack(track);
- this.autoRemoteTextTracks_.removeTrack(track);
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object}
- * An object with supported media playback quality metrics
- *
- * @abstract
- */
- getVideoPlaybackQuality() {
- return {};
- }
-
- /**
- * Attempt to create a floating video window always on top of other windows
- * so that users may continue consuming media while they interact with other
- * content sites, or applications on their device.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @return {Promise|undefined}
- * A promise with a Picture-in-Picture window if the browser supports
- * Promises (or one was passed in as an option). It returns undefined
- * otherwise.
- *
- * @abstract
- */
- requestPictureInPicture() {
- return Promise.reject();
- }
-
- /**
- * A method to check for the value of the 'disablePictureInPicture' property.
- * Defaults to true, as it should be considered disabled if the tech does not support pip
- *
- * @abstract
- */
- disablePictureInPicture() {
- return true;
- }
-
- /**
- * A method to set or unset the 'disablePictureInPicture' property.
- *
- * @abstract
- */
- setDisablePictureInPicture() {}
-
- /**
- * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
- *
- * @param {function} cb
- * @return {number} request id
- */
- requestVideoFrameCallback(cb) {
- const id = newGUID();
- if (!this.isReady_ || this.paused()) {
- this.queuedHanders_.add(id);
- this.one('playing', () => {
- if (this.queuedHanders_.has(id)) {
- this.queuedHanders_.delete(id);
- cb();
- }
- });
- } else {
- this.requestNamedAnimationFrame(id, cb);
- }
- return id;
- }
-
- /**
- * A fallback implementation of cancelVideoFrameCallback
- *
- * @param {number} id id of callback to be cancelled
- */
- cancelVideoFrameCallback(id) {
- if (this.queuedHanders_.has(id)) {
- this.queuedHanders_.delete(id);
- } else {
- this.cancelNamedAnimationFrame(id);
- }
- }
-
- /**
- * A method to set a poster from a `Tech`.
- *
- * @abstract
- */
- setPoster() {}
-
- /**
- * A method to check for the presence of the 'playsinline' attribute.
- *
- * @abstract
- */
- playsinline() {}
-
- /**
- * A method to set or unset the 'playsinline' attribute.
- *
- * @abstract
- */
- setPlaysinline() {}
-
- /**
- * Attempt to force override of native audio tracks.
- *
- * @param {boolean} override - If set to true native audio will be overridden,
- * otherwise native audio will potentially be used.
- *
- * @abstract
- */
- overrideNativeAudioTracks(override) {}
-
- /**
- * Attempt to force override of native video tracks.
- *
- * @param {boolean} override - If set to true native video will be overridden,
- * otherwise native video will potentially be used.
- *
- * @abstract
- */
- overrideNativeVideoTracks(override) {}
-
- /**
- * Check if the tech can support the given mime-type.
- *
- * The base tech does not support any type, but source handlers might
- * overwrite this.
- *
- * @param {string} _type
- * The mimetype to check for support
- *
- * @return {string}
- * 'probably', 'maybe', or empty string
- *
- * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
- *
- * @abstract
- */
- canPlayType(_type) {
- return '';
- }
-
- /**
- * Check if the type is supported by this tech.
- *
- * The base tech does not support any type, but source handlers might
- * overwrite this.
- *
- * @param {string} _type
- * The media type to check
- * @return {string} Returns the native video element's response
- */
- static canPlayType(_type) {
- return '';
- }
-
- /**
- * Check if the tech can support the given source
- *
- * @param {Object} srcObj
- * The source object
- * @param {Object} options
- * The options passed to the tech
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- static canPlaySource(srcObj, options) {
- return Tech.canPlayType(srcObj.type);
- }
-
- /*
- * Return whether the argument is a Tech or not.
- * Can be passed either a Class like `Html5` or a instance like `player.tech_`
- *
- * @param {Object} component
- * The item to check
- *
- * @return {boolean}
- * Whether it is a tech or not
- * - True if it is a tech
- * - False if it is not
- */
- static isTech(component) {
- return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
- }
-
- /**
- * Registers a `Tech` into a shared list for videojs.
- *
- * @param {string} name
- * Name of the `Tech` to register.
- *
- * @param {Object} tech
- * The `Tech` class to register.
- */
- static registerTech(name, tech) {
- if (!Tech.techs_) {
- Tech.techs_ = {};
- }
- if (!Tech.isTech(tech)) {
- throw new Error(`Tech ${name} must be a Tech`);
- }
- if (!Tech.canPlayType) {
- throw new Error('Techs must have a static canPlayType method on them');
- }
- if (!Tech.canPlaySource) {
- throw new Error('Techs must have a static canPlaySource method on them');
- }
- name = toTitleCase$1(name);
- Tech.techs_[name] = tech;
- Tech.techs_[toLowerCase(name)] = tech;
- if (name !== 'Tech') {
- // camel case the techName for use in techOrder
- Tech.defaultTechOrder_.push(name);
- }
- return tech;
- }
-
- /**
- * Get a `Tech` from the shared list by name.
- *
- * @param {string} name
- * `camelCase` or `TitleCase` name of the Tech to get
- *
- * @return {Tech|undefined}
- * The `Tech` or undefined if there was no tech with the name requested.
- */
- static getTech(name) {
- if (!name) {
- return;
- }
- if (Tech.techs_ && Tech.techs_[name]) {
- return Tech.techs_[name];
- }
- name = toTitleCase$1(name);
- if (window && window.videojs && window.videojs[name]) {
- log$1.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
- return window.videojs[name];
- }
- }
- }
-
- /**
- * Get the {@link VideoTrackList}
- *
- * @returns {VideoTrackList}
- * @method Tech.prototype.videoTracks
- */
-
- /**
- * Get the {@link AudioTrackList}
- *
- * @returns {AudioTrackList}
- * @method Tech.prototype.audioTracks
- */
-
- /**
- * Get the {@link TextTrackList}
- *
- * @returns {TextTrackList}
- * @method Tech.prototype.textTracks
- */
-
- /**
- * Get the remote element {@link TextTrackList}
- *
- * @returns {TextTrackList}
- * @method Tech.prototype.remoteTextTracks
- */
-
- /**
- * Get the remote element {@link HtmlTrackElementList}
- *
- * @returns {HtmlTrackElementList}
- * @method Tech.prototype.remoteTextTrackEls
- */
-
- ALL.names.forEach(function (name) {
- const props = ALL[name];
- Tech.prototype[props.getterName] = function () {
- this[props.privateName] = this[props.privateName] || new props.ListClass();
- return this[props.privateName];
- };
- });
-
- /**
- * List of associated text tracks
- *
- * @type {TextTrackList}
- * @private
- * @property Tech#textTracks_
- */
-
- /**
- * List of associated audio tracks.
- *
- * @type {AudioTrackList}
- * @private
- * @property Tech#audioTracks_
- */
-
- /**
- * List of associated video tracks.
- *
- * @type {VideoTrackList}
- * @private
- * @property Tech#videoTracks_
- */
-
- /**
- * Boolean indicating whether the `Tech` supports volume control.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresVolumeControl = true;
-
- /**
- * Boolean indicating whether the `Tech` supports muting volume.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresMuteControl = true;
-
- /**
- * Boolean indicating whether the `Tech` supports fullscreen resize control.
- * Resizing plugins using request fullscreen reloads the plugin
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresFullscreenResize = false;
-
- /**
- * Boolean indicating whether the `Tech` supports changing the speed at which the video
- * plays. Examples:
- * - Set player to play 2x (twice) as fast
- * - Set player to play 0.5x (half) as fast
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresPlaybackRate = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `progress` event.
- * This will be used to determine if {@link Tech#manualProgressOn} should be called.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresProgressEvents = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `sourceset` event.
- *
- * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
- * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
- * a new source.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresSourceset = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the `timeupdate` event.
- * This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresTimeupdateEvents = false;
-
- /**
- * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
- * This will help us integrate with native `TextTrack`s if the browser supports them.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresNativeTextTracks = false;
-
- /**
- * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
- *
- * @type {boolean}
- * @default
- */
- Tech.prototype.featuresVideoFrameCallback = false;
-
- /**
- * A functional mixin for techs that want to use the Source Handler pattern.
- * Source handlers are scripts for handling specific formats.
- * The source handler pattern is used for adaptive formats (HLS, DASH) that
- * manually load video data and feed it into a Source Buffer (Media Source Extensions)
- * Example: `Tech.withSourceHandlers.call(MyTech);`
- *
- * @param {Tech} _Tech
- * The tech to add source handler functions to.
- *
- * @mixes Tech~SourceHandlerAdditions
- */
- Tech.withSourceHandlers = function (_Tech) {
- /**
- * Register a source handler
- *
- * @param {Function} handler
- * The source handler class
- *
- * @param {number} [index]
- * Register it at the following index
- */
- _Tech.registerSourceHandler = function (handler, index) {
- let handlers = _Tech.sourceHandlers;
- if (!handlers) {
- handlers = _Tech.sourceHandlers = [];
- }
- if (index === undefined) {
- // add to the end of the list
- index = handlers.length;
- }
- handlers.splice(index, 0, handler);
- };
-
- /**
- * Check if the tech can support the given type. Also checks the
- * Techs sourceHandlers.
- *
- * @param {string} type
- * The mimetype to check.
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- _Tech.canPlayType = function (type) {
- const handlers = _Tech.sourceHandlers || [];
- let can;
- for (let i = 0; i < handlers.length; i++) {
- can = handlers[i].canPlayType(type);
- if (can) {
- return can;
- }
- }
- return '';
- };
-
- /**
- * Returns the first source handler that supports the source.
- *
- * TODO: Answer question: should 'probably' be prioritized over 'maybe'
- *
- * @param {SourceObject} source
- * The source object
- *
- * @param {Object} options
- * The options passed to the tech
- *
- * @return {SourceHandler|null}
- * The first source handler that supports the source or null if
- * no SourceHandler supports the source
- */
- _Tech.selectSourceHandler = function (source, options) {
- const handlers = _Tech.sourceHandlers || [];
- let can;
- for (let i = 0; i < handlers.length; i++) {
- can = handlers[i].canHandleSource(source, options);
- if (can) {
- return handlers[i];
- }
- }
- return null;
- };
-
- /**
- * Check if the tech can support the given source.
- *
- * @param {SourceObject} srcObj
- * The source object
- *
- * @param {Object} options
- * The options passed to the tech
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- _Tech.canPlaySource = function (srcObj, options) {
- const sh = _Tech.selectSourceHandler(srcObj, options);
- if (sh) {
- return sh.canHandleSource(srcObj, options);
- }
- return '';
- };
-
- /**
- * When using a source handler, prefer its implementation of
- * any function normally provided by the tech.
- */
- const deferrable = ['seekable', 'seeking', 'duration'];
-
- /**
- * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
- * function if it exists, with a fallback to the Techs seekable function.
- *
- * @method _Tech.seekable
- */
-
- /**
- * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
- * function if it exists, otherwise it will fallback to the techs duration function.
- *
- * @method _Tech.duration
- */
-
- deferrable.forEach(function (fnName) {
- const originalFn = this[fnName];
- if (typeof originalFn !== 'function') {
- return;
- }
- this[fnName] = function () {
- if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
- return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
- }
- return originalFn.apply(this, arguments);
- };
- }, _Tech.prototype);
-
- /**
- * Create a function for setting the source using a source object
- * and source handlers.
- * Should never be called unless a source handler was found.
- *
- * @param {SourceObject} source
- * A source object with src and type keys
- */
- _Tech.prototype.setSource = function (source) {
- let sh = _Tech.selectSourceHandler(source, this.options_);
- if (!sh) {
- // Fall back to a native source handler when unsupported sources are
- // deliberately set
- if (_Tech.nativeSourceHandler) {
- sh = _Tech.nativeSourceHandler;
- } else {
- log$1.error('No source handler found for the current source.');
- }
- }
-
- // Dispose any existing source handler
- this.disposeSourceHandler();
- this.off('dispose', this.disposeSourceHandler_);
- if (sh !== _Tech.nativeSourceHandler) {
- this.currentSource_ = source;
- }
- this.sourceHandler_ = sh.handleSource(source, this, this.options_);
- this.one('dispose', this.disposeSourceHandler_);
- };
-
- /**
- * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
- *
- * @listens Tech#dispose
- */
- _Tech.prototype.disposeSourceHandler = function () {
- // if we have a source and get another one
- // then we are loading something new
- // than clear all of our current tracks
- if (this.currentSource_) {
- this.clearTracks(['audio', 'video']);
- this.currentSource_ = null;
- }
-
- // always clean up auto-text tracks
- this.cleanupAutoTextTracks();
- if (this.sourceHandler_) {
- if (this.sourceHandler_.dispose) {
- this.sourceHandler_.dispose();
- }
- this.sourceHandler_ = null;
- }
- };
- };
-
- // The base Tech class needs to be registered as a Component. It is the only
- // Tech that can be registered as a Component.
- Component$1.registerComponent('Tech', Tech);
- Tech.registerTech('Tech', Tech);
-
- /**
- * A list of techs that should be added to techOrder on Players
- *
- * @private
- */
- Tech.defaultTechOrder_ = [];
-
- /**
- * @file middleware.js
- * @module middleware
- */
- const middlewares = {};
- const middlewareInstances = {};
- const TERMINATOR = {};
-
- /**
- * A middleware object is a plain JavaScript object that has methods that
- * match the {@link Tech} methods found in the lists of allowed
- * {@link module:middleware.allowedGetters|getters},
- * {@link module:middleware.allowedSetters|setters}, and
- * {@link module:middleware.allowedMediators|mediators}.
- *
- * @typedef {Object} MiddlewareObject
- */
-
- /**
- * A middleware factory function that should return a
- * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
- *
- * This factory will be called for each player when needed, with the player
- * passed in as an argument.
- *
- * @callback MiddlewareFactory
- * @param { import('../player').default } player
- * A Video.js player.
- */
-
- /**
- * Define a middleware that the player should use by way of a factory function
- * that returns a middleware object.
- *
- * @param {string} type
- * The MIME type to match or `"*"` for all MIME types.
- *
- * @param {MiddlewareFactory} middleware
- * A middleware factory function that will be executed for
- * matching types.
- */
- function use(type, middleware) {
- middlewares[type] = middlewares[type] || [];
- middlewares[type].push(middleware);
- }
-
- /**
- * Asynchronously sets a source using middleware by recursing through any
- * matching middlewares and calling `setSource` on each, passing along the
- * previous returned value each time.
- *
- * @param { import('../player').default } player
- * A {@link Player} instance.
- *
- * @param {Tech~SourceObject} src
- * A source object.
- *
- * @param {Function}
- * The next middleware to run.
- */
- function setSource(player, src, next) {
- player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
- }
-
- /**
- * When the tech is set, passes the tech to each middleware's `setTech` method.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * A Video.js tech.
- */
- function setTech(middleware, tech) {
- middleware.forEach(mw => mw.setTech && mw.setTech(tech));
- }
-
- /**
- * Calls a getter on the tech first, through each middleware
- * from right to left to the player.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @return {*}
- * The final value from the tech after middleware has intercepted it.
- */
- function get(middleware, tech, method) {
- return middleware.reduceRight(middlewareIterator(method), tech[method]());
- }
-
- /**
- * Takes the argument given to the player and calls the setter method on each
- * middleware from left to right to the tech.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @param {*} arg
- * The value to set on the tech.
- *
- * @return {*}
- * The return value of the `method` of the `tech`.
- */
- function set(middleware, tech, method, arg) {
- return tech[method](middleware.reduce(middlewareIterator(method), arg));
- }
-
- /**
- * Takes the argument given to the player and calls the `call` version of the
- * method on each middleware from left to right.
- *
- * Then, call the passed in method on the tech and return the result unchanged
- * back to the player, through middleware, this time from right to left.
- *
- * @param {Object[]} middleware
- * An array of middleware instances.
- *
- * @param { import('../tech/tech').default } tech
- * The current tech.
- *
- * @param {string} method
- * A method name.
- *
- * @param {*} arg
- * The value to set on the tech.
- *
- * @return {*}
- * The return value of the `method` of the `tech`, regardless of the
- * return values of middlewares.
- */
- function mediate(middleware, tech, method, arg = null) {
- const callMethod = 'call' + toTitleCase$1(method);
- const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
- const terminated = middlewareValue === TERMINATOR;
- // deprecated. The `null` return value should instead return TERMINATOR to
- // prevent confusion if a techs method actually returns null.
- const returnValue = terminated ? null : tech[method](middlewareValue);
- executeRight(middleware, method, returnValue, terminated);
- return returnValue;
- }
-
- /**
- * Enumeration of allowed getters where the keys are method names.
- *
- * @type {Object}
- */
- const allowedGetters = {
- buffered: 1,
- currentTime: 1,
- duration: 1,
- muted: 1,
- played: 1,
- paused: 1,
- seekable: 1,
- volume: 1,
- ended: 1
- };
-
- /**
- * Enumeration of allowed setters where the keys are method names.
- *
- * @type {Object}
- */
- const allowedSetters = {
- setCurrentTime: 1,
- setMuted: 1,
- setVolume: 1
- };
-
- /**
- * Enumeration of allowed mediators where the keys are method names.
- *
- * @type {Object}
- */
- const allowedMediators = {
- play: 1,
- pause: 1
- };
- function middlewareIterator(method) {
- return (value, mw) => {
- // if the previous middleware terminated, pass along the termination
- if (value === TERMINATOR) {
- return TERMINATOR;
- }
- if (mw[method]) {
- return mw[method](value);
- }
- return value;
- };
- }
- function executeRight(mws, method, value, terminated) {
- for (let i = mws.length - 1; i >= 0; i--) {
- const mw = mws[i];
- if (mw[method]) {
- mw[method](terminated, value);
- }
- }
- }
-
- /**
- * Clear the middleware cache for a player.
- *
- * @param { import('../player').default } player
- * A {@link Player} instance.
- */
- function clearCacheForPlayer(player) {
- middlewareInstances[player.id()] = null;
- }
-
- /**
- * {
- * [playerId]: [[mwFactory, mwInstance], ...]
- * }
- *
- * @private
- */
- function getOrCreateFactory(player, mwFactory) {
- const mws = middlewareInstances[player.id()];
- let mw = null;
- if (mws === undefined || mws === null) {
- mw = mwFactory(player);
- middlewareInstances[player.id()] = [[mwFactory, mw]];
- return mw;
- }
- for (let i = 0; i < mws.length; i++) {
- const [mwf, mwi] = mws[i];
- if (mwf !== mwFactory) {
- continue;
- }
- mw = mwi;
- }
- if (mw === null) {
- mw = mwFactory(player);
- mws.push([mwFactory, mw]);
- }
- return mw;
- }
- function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
- const [mwFactory, ...mwrest] = middleware;
-
- // if mwFactory is a string, then we're at a fork in the road
- if (typeof mwFactory === 'string') {
- setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
-
- // if we have an mwFactory, call it with the player to get the mw,
- // then call the mw's setSource method
- } else if (mwFactory) {
- const mw = getOrCreateFactory(player, mwFactory);
-
- // if setSource isn't present, implicitly select this middleware
- if (!mw.setSource) {
- acc.push(mw);
- return setSourceHelper(src, mwrest, next, player, acc, lastRun);
- }
- mw.setSource(Object.assign({}, src), function (err, _src) {
- // something happened, try the next middleware on the current level
- // make sure to use the old src
- if (err) {
- return setSourceHelper(src, mwrest, next, player, acc, lastRun);
- }
-
- // we've succeeded, now we need to go deeper
- acc.push(mw);
-
- // if it's the same type, continue down the current chain
- // otherwise, we want to go down the new chain
- setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
- });
- } else if (mwrest.length) {
- setSourceHelper(src, mwrest, next, player, acc, lastRun);
- } else if (lastRun) {
- next(src, acc);
- } else {
- setSourceHelper(src, middlewares['*'], next, player, acc, true);
- }
- }
-
- /**
- * Mimetypes
- *
- * @see https://www.iana.org/assignments/media-types/media-types.xhtml
- * @typedef Mimetypes~Kind
- * @enum
- */
- const MimetypesKind = {
- opus: 'video/ogg',
- ogv: 'video/ogg',
- mp4: 'video/mp4',
- mov: 'video/mp4',
- m4v: 'video/mp4',
- mkv: 'video/x-matroska',
- m4a: 'audio/mp4',
- mp3: 'audio/mpeg',
- aac: 'audio/aac',
- caf: 'audio/x-caf',
- flac: 'audio/flac',
- oga: 'audio/ogg',
- wav: 'audio/wav',
- m3u8: 'application/x-mpegURL',
- mpd: 'application/dash+xml',
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- gif: 'image/gif',
- png: 'image/png',
- svg: 'image/svg+xml',
- webp: 'image/webp'
- };
-
- /**
- * Get the mimetype of a given src url if possible
- *
- * @param {string} src
- * The url to the src
- *
- * @return {string}
- * return the mimetype if it was known or empty string otherwise
- */
- const getMimetype = function (src = '') {
- const ext = getFileExtension(src);
- const mimetype = MimetypesKind[ext.toLowerCase()];
- return mimetype || '';
- };
-
- /**
- * Find the mime type of a given source string if possible. Uses the player
- * source cache.
- *
- * @param { import('../player').default } player
- * The player object
- *
- * @param {string} src
- * The source string
- *
- * @return {string}
- * The type that was found
- */
- const findMimetype = (player, src) => {
- if (!src) {
- return '';
- }
-
- // 1. check for the type in the `source` cache
- if (player.cache_.source.src === src && player.cache_.source.type) {
- return player.cache_.source.type;
- }
-
- // 2. see if we have this source in our `currentSources` cache
- const matchingSources = player.cache_.sources.filter(s => s.src === src);
- if (matchingSources.length) {
- return matchingSources[0].type;
- }
-
- // 3. look for the src url in source elements and use the type there
- const sources = player.$$('source');
- for (let i = 0; i < sources.length; i++) {
- const s = sources[i];
- if (s.type && s.src && s.src === src) {
- return s.type;
- }
- }
-
- // 4. finally fallback to our list of mime types based on src url extension
- return getMimetype(src);
- };
-
- /**
- * @module filter-source
- */
-
- /**
- * Filter out single bad source objects or multiple source objects in an
- * array. Also flattens nested source object arrays into a 1 dimensional
- * array of source objects.
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]} src
- * The src object to filter
- *
- * @return {Tech~SourceObject[]}
- * An array of sourceobjects containing only valid sources
- *
- * @private
- */
- const filterSource = function (src) {
- // traverse array
- if (Array.isArray(src)) {
- let newsrc = [];
- src.forEach(function (srcobj) {
- srcobj = filterSource(srcobj);
- if (Array.isArray(srcobj)) {
- newsrc = newsrc.concat(srcobj);
- } else if (isObject$1(srcobj)) {
- newsrc.push(srcobj);
- }
- });
- src = newsrc;
- } else if (typeof src === 'string' && src.trim()) {
- // convert string into object
- src = [fixSource({
- src
- })];
- } else if (isObject$1(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
- // src is already valid
- src = [fixSource(src)];
- } else {
- // invalid source, turn it into an empty array
- src = [];
- }
- return src;
- };
-
- /**
- * Checks src mimetype, adding it when possible
- *
- * @param {Tech~SourceObject} src
- * The src object to check
- * @return {Tech~SourceObject}
- * src Object with known type
- */
- function fixSource(src) {
- if (!src.type) {
- const mimetype = getMimetype(src.src);
- if (mimetype) {
- src.type = mimetype;
- }
- }
- return src;
- }
-
- var icons = "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ";
-
- /**
- * @file loader.js
- */
-
- /**
- * The `MediaLoader` is the `Component` that decides which playback technology to load
- * when a player is initialized.
- *
- * @extends Component
- */
- class MediaLoader extends Component$1 {
- /**
- * Create an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should attach to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function that is run when this component is ready.
- */
- constructor(player, options, ready) {
- // MediaLoader has no element
- const options_ = merge$2({
- createEl: false
- }, options);
- super(player, options_, ready);
-
- // If there are no sources when the player is initialized,
- // load the first supported playback technology.
-
- if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
- for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
- const techName = toTitleCase$1(j[i]);
- let tech = Tech.getTech(techName);
-
- // Support old behavior of techs being registered as components.
- // Remove once that deprecated behavior is removed.
- if (!techName) {
- tech = Component$1.getComponent(techName);
- }
-
- // Check if the browser supports this technology
- if (tech && tech.isSupported()) {
- player.loadTech_(techName);
- break;
- }
- }
- } else {
- // Loop through playback technologies (e.g. HTML5) and check for support.
- // Then load the best source.
- // A few assumptions here:
- // All playback technologies respect preload false.
- player.src(options.playerOptions.sources);
- }
- }
- }
- Component$1.registerComponent('MediaLoader', MediaLoader);
-
- /**
- * @file clickable-component.js
- */
-
- /**
- * Component which is clickable or keyboard actionable, but is not a
- * native HTML button.
- *
- * @extends Component
- */
- class ClickableComponent extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of component options.
- *
- * @param {function} [options.clickHandler]
- * The function to call when the button is clicked / activated
- *
- * @param {string} [options.controlText]
- * The text to set on the button
- *
- * @param {string} [options.className]
- * A class or space separated list of classes to add the component
- *
- */
- constructor(player, options) {
- super(player, options);
- if (this.options_.controlText) {
- this.controlText(this.options_.controlText);
- }
- this.handleMouseOver_ = e => this.handleMouseOver(e);
- this.handleMouseOut_ = e => this.handleMouseOut(e);
- this.handleClick_ = e => this.handleClick(e);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.emitTapEvents();
- this.enable();
- }
-
- /**
- * Create the `ClickableComponent`s DOM element.
- *
- * @param {string} [tag=div]
- * The element's node type.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element.
- *
- * @param {Object} [attributes={}]
- * An object of attributes that should be set on the element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tag = 'div', props = {}, attributes = {}) {
- props = Object.assign({
- className: this.buildCSSClass(),
- tabIndex: 0
- }, props);
- if (tag === 'button') {
- log$1.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
- }
-
- // Add ARIA attributes for clickable element which is not a native HTML button
- attributes = Object.assign({
- role: 'button'
- }, attributes);
- this.tabIndex_ = props.tabIndex;
- const el = createEl(tag, props, attributes);
- if (!this.player_.options_.experimentalSvgIcons) {
- el.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- this.createControlTextEl(el);
- return el;
- }
- dispose() {
- // remove controlTextEl_ on dispose
- this.controlTextEl_ = null;
- super.dispose();
- }
-
- /**
- * Create a control text element on this `ClickableComponent`
- *
- * @param {Element} [el]
- * Parent element for the control text.
- *
- * @return {Element}
- * The control text element that gets created.
- */
- createControlTextEl(el) {
- this.controlTextEl_ = createEl('span', {
- className: 'vjs-control-text'
- }, {
- // let the screen reader user know that the text of the element may change
- 'aria-live': 'polite'
- });
- if (el) {
- el.appendChild(this.controlTextEl_);
- }
- this.controlText(this.controlText_, el);
- return this.controlTextEl_;
- }
-
- /**
- * Get or set the localize text to use for the controls on the `ClickableComponent`.
- *
- * @param {string} [text]
- * Control text for element.
- *
- * @param {Element} [el=this.el()]
- * Element to set the title on.
- *
- * @return {string}
- * - The control text when getting
- */
- controlText(text, el = this.el()) {
- if (text === undefined) {
- return this.controlText_ || 'Need Text';
- }
- const localizedText = this.localize(text);
-
- /** @protected */
- this.controlText_ = text;
- textContent(this.controlTextEl_, localizedText);
- if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
- // Set title attribute if only an icon is shown
- el.setAttribute('title', localizedText);
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-control vjs-button ${super.buildCSSClass()}`;
- }
-
- /**
- * Enable this `ClickableComponent`
- */
- enable() {
- if (!this.enabled_) {
- this.enabled_ = true;
- this.removeClass('vjs-disabled');
- this.el_.setAttribute('aria-disabled', 'false');
- if (typeof this.tabIndex_ !== 'undefined') {
- this.el_.setAttribute('tabIndex', this.tabIndex_);
- }
- this.on(['tap', 'click'], this.handleClick_);
- this.on('keydown', this.handleKeyDown_);
- }
- }
-
- /**
- * Disable this `ClickableComponent`
- */
- disable() {
- this.enabled_ = false;
- this.addClass('vjs-disabled');
- this.el_.setAttribute('aria-disabled', 'true');
- if (typeof this.tabIndex_ !== 'undefined') {
- this.el_.removeAttribute('tabIndex');
- }
- this.off('mouseover', this.handleMouseOver_);
- this.off('mouseout', this.handleMouseOut_);
- this.off(['tap', 'click'], this.handleClick_);
- this.off('keydown', this.handleKeyDown_);
- }
-
- /**
- * Handles language change in ClickableComponent for the player in components
- *
- *
- */
- handleLanguagechange() {
- this.controlText(this.controlText_);
- }
-
- /**
- * Event handler that is called when a `ClickableComponent` receives a
- * `click` or `tap` event.
- *
- * @param {Event} event
- * The `tap` or `click` event that caused this function to be called.
- *
- * @listens tap
- * @listens click
- * @abstract
- */
- handleClick(event) {
- if (this.options_.clickHandler) {
- this.options_.clickHandler.call(this, arguments);
- }
- }
-
- /**
- * Event handler that is called when a `ClickableComponent` receives a
- * `keydown` event.
- *
- * By default, if the key is Space or Enter, it will trigger a `click` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Support Space or Enter key operation to fire a click event. Also,
- // prevent the event from propagating through the DOM and triggering
- // Player hotkeys.
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.preventDefault();
- event.stopPropagation();
- this.trigger('click');
- } else {
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- }
- Component$1.registerComponent('ClickableComponent', ClickableComponent);
-
- /**
- * @file poster-image.js
- */
-
- /**
- * A `ClickableComponent` that handles showing the poster image for the player.
- *
- * @extends ClickableComponent
- */
- class PosterImage extends ClickableComponent {
- /**
- * Create an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should attach to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update();
- this.update_ = e => this.update(e);
- player.on('posterchange', this.update_);
- }
-
- /**
- * Clean up and dispose of the `PosterImage`.
- */
- dispose() {
- this.player().off('posterchange', this.update_);
- super.dispose();
- }
-
- /**
- * Create the `PosterImage`s DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- // The el is an empty div to keep position in the DOM
- // A picture and img el will be inserted when a source is set
- return createEl('div', {
- className: 'vjs-poster'
- });
- }
-
- /**
- * Get or set the `PosterImage`'s crossOrigin option.
- *
- * @param {string|null} [value]
- * The value to set the crossOrigin to. If an argument is
- * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
- *
- * @return {string|null}
- * - The current crossOrigin value of the `Player` when getting.
- * - undefined when setting
- */
- crossOrigin(value) {
- // `null` can be set to unset a value
- if (typeof value === 'undefined') {
- if (this.$('img')) {
- // If the poster's element exists, give its value
- return this.$('img').crossOrigin;
- } else if (this.player_.tech_ && this.player_.tech_.isReady_) {
- // If not but the tech is ready, query the tech
- return this.player_.crossOrigin();
- }
- // Otherwise check options as the poster is usually set before the state of crossorigin
- // can be retrieved by the getter
- return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
- }
- if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
- this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
- return;
- }
- if (this.$('img')) {
- this.$('img').crossOrigin = value;
- }
- return;
- }
-
- /**
- * An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
- *
- * @listens Player#posterchange
- *
- * @param {Event} [event]
- * The `Player#posterchange` event that triggered this function.
- */
- update(event) {
- const url = this.player().poster();
- this.setSrc(url);
-
- // If there's no poster source we should display:none on this component
- // so it's not still clickable or right-clickable
- if (url) {
- this.show();
- } else {
- this.hide();
- }
- }
-
- /**
- * Set the source of the `PosterImage` depending on the display method. (Re)creates
- * the inner picture and img elementss when needed.
- *
- * @param {string} [url]
- * The URL to the source for the `PosterImage`. If not specified or falsy,
- * any source and ant inner picture/img are removed.
- */
- setSrc(url) {
- if (!url) {
- this.el_.textContent = '';
- return;
- }
- if (!this.$('img')) {
- this.el_.appendChild(createEl('picture', {
- className: 'vjs-poster',
- // Don't want poster to be tabbable.
- tabIndex: -1
- }, {}, createEl('img', {
- loading: 'lazy',
- crossOrigin: this.crossOrigin()
- }, {
- alt: ''
- })));
- }
- this.$('img').src = url;
- }
-
- /**
- * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
- * {@link ClickableComponent#handleClick} for instances where this will be triggered.
- *
- * @listens tap
- * @listens click
- * @listens keydown
- *
- * @param {Event} event
- + The `click`, `tap` or `keydown` event that caused this function to be called.
- */
- handleClick(event) {
- // We don't want a click to trigger playback when controls are disabled
- if (!this.player_.controls()) {
- return;
- }
- if (this.player_.tech(true)) {
- this.player_.tech(true).focus();
- }
- if (this.player_.paused()) {
- silencePromise(this.player_.play());
- } else {
- this.player_.pause();
- }
- }
- }
-
- /**
- * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the ` ` tag to control the CORS
- * behavior.
- *
- * @param {string|null} [value]
- * The value to set the `PosterImages`'s crossorigin to. If an argument is
- * given, must be one of `anonymous` or `use-credentials`.
- *
- * @return {string|null|undefined}
- * - The current crossorigin value of the `Player` when getting.
- * - undefined when setting
- */
- PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
- Component$1.registerComponent('PosterImage', PosterImage);
-
- /**
- * @file text-track-display.js
- */
- const darkGray = '#222';
- const lightGray = '#ccc';
- const fontMap = {
- monospace: 'monospace',
- sansSerif: 'sans-serif',
- serif: 'serif',
- monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
- monospaceSerif: '"Courier New", monospace',
- proportionalSansSerif: 'sans-serif',
- proportionalSerif: 'serif',
- casual: '"Comic Sans MS", Impact, fantasy',
- script: '"Monotype Corsiva", cursive',
- smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
- };
-
- /**
- * Construct an rgba color from a given hex color code.
- *
- * @param {number} color
- * Hex number for color, like #f0e or #f604e2.
- *
- * @param {number} opacity
- * Value for opacity, 0.0 - 1.0.
- *
- * @return {string}
- * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
- */
- function constructColor(color, opacity) {
- let hex;
- if (color.length === 4) {
- // color looks like "#f0e"
- hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
- } else if (color.length === 7) {
- // color looks like "#f604e2"
- hex = color.slice(1);
- } else {
- throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
- }
- return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
- }
-
- /**
- * Try to update the style of a DOM element. Some style changes will throw an error,
- * particularly in IE8. Those should be noops.
- *
- * @param {Element} el
- * The DOM element to be styled.
- *
- * @param {string} style
- * The CSS property on the element that should be styled.
- *
- * @param {string} rule
- * The style rule that should be applied to the property.
- *
- * @private
- */
- function tryUpdateStyle(el, style, rule) {
- try {
- el.style[style] = rule;
- } catch (e) {
- // Satisfies linter.
- return;
- }
- }
-
- /**
- * Converts the CSS top/right/bottom/left property numeric value to string in pixels.
- *
- * @param {number} position
- * The CSS top/right/bottom/left property value.
- *
- * @return {string}
- * The CSS property value that was created, like '10px'.
- *
- * @private
- */
- function getCSSPositionValue(position) {
- return position ? `${position}px` : '';
- }
-
- /**
- * The component for displaying text track cues.
- *
- * @extends Component
- */
- class TextTrackDisplay extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when `TextTrackDisplay` is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- const updateDisplayTextHandler = e => this.updateDisplay(e);
- const updateDisplayHandler = e => {
- this.updateDisplayOverlay();
- this.updateDisplay(e);
- };
- player.on('loadstart', e => this.toggleDisplay(e));
- player.on('texttrackchange', updateDisplayTextHandler);
- player.on('loadedmetadata', e => {
- this.updateDisplayOverlay();
- this.preselectTrack(e);
- });
-
- // This used to be called during player init, but was causing an error
- // if a track should show by default and the display hadn't loaded yet.
- // Should probably be moved to an external track loader when we support
- // tracks that don't need a display.
- player.ready(bind_(this, function () {
- if (player.tech_ && player.tech_.featuresNativeTextTracks) {
- this.hide();
- return;
- }
- player.on('fullscreenchange', updateDisplayHandler);
- player.on('playerresize', updateDisplayHandler);
- const screenOrientation = window.screen.orientation || window;
- const changeOrientationEvent = window.screen.orientation ? 'change' : 'orientationchange';
- screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
- player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
- const tracks = this.options_.playerOptions.tracks || [];
- for (let i = 0; i < tracks.length; i++) {
- this.player_.addRemoteTextTrack(tracks[i], true);
- }
- this.preselectTrack();
- }));
- }
-
- /**
- * Preselect a track following this precedence:
- * - matches the previously selected {@link TextTrack}'s language and kind
- * - matches the previously selected {@link TextTrack}'s language only
- * - is the first default captions track
- * - is the first default descriptions track
- *
- * @listens Player#loadstart
- */
- preselectTrack() {
- const modes = {
- captions: 1,
- subtitles: 1
- };
- const trackList = this.player_.textTracks();
- const userPref = this.player_.cache_.selectedLanguage;
- let firstDesc;
- let firstCaptions;
- let preferredTrack;
- for (let i = 0; i < trackList.length; i++) {
- const track = trackList[i];
- if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
- // Always choose the track that matches both language and kind
- if (track.kind === userPref.kind) {
- preferredTrack = track;
- // or choose the first track that matches language
- } else if (!preferredTrack) {
- preferredTrack = track;
- }
-
- // clear everything if offTextTrackMenuItem was clicked
- } else if (userPref && !userPref.enabled) {
- preferredTrack = null;
- firstDesc = null;
- firstCaptions = null;
- } else if (track.default) {
- if (track.kind === 'descriptions' && !firstDesc) {
- firstDesc = track;
- } else if (track.kind in modes && !firstCaptions) {
- firstCaptions = track;
- }
- }
- }
-
- // The preferredTrack matches the user preference and takes
- // precedence over all the other tracks.
- // So, display the preferredTrack before the first default track
- // and the subtitles/captions track before the descriptions track
- if (preferredTrack) {
- preferredTrack.mode = 'showing';
- } else if (firstCaptions) {
- firstCaptions.mode = 'showing';
- } else if (firstDesc) {
- firstDesc.mode = 'showing';
- }
- }
-
- /**
- * Turn display of {@link TextTrack}'s from the current state into the other state.
- * There are only two states:
- * - 'shown'
- * - 'hidden'
- *
- * @listens Player#loadstart
- */
- toggleDisplay() {
- if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
- this.hide();
- } else {
- this.show();
- }
- }
-
- /**
- * Create the {@link Component}'s DOM element.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-text-track-display'
- }, {
- 'translate': 'yes',
- 'aria-live': 'off',
- 'aria-atomic': 'true'
- });
- }
-
- /**
- * Clear all displayed {@link TextTrack}s.
- */
- clearDisplay() {
- if (typeof window.WebVTT === 'function') {
- window.WebVTT.processCues(window, [], this.el_);
- }
- }
-
- /**
- * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
- * a {@link Player#fullscreenchange} is fired.
- *
- * @listens Player#texttrackchange
- * @listens Player#fullscreenchange
- */
- updateDisplay() {
- const tracks = this.player_.textTracks();
- const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
- this.clearDisplay();
- if (allowMultipleShowingTracks) {
- const showingTracks = [];
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- if (track.mode !== 'showing') {
- continue;
- }
- showingTracks.push(track);
- }
- this.updateForTrack(showingTracks);
- return;
- }
-
- // Track display prioritization model: if multiple tracks are 'showing',
- // display the first 'subtitles' or 'captions' track which is 'showing',
- // otherwise display the first 'descriptions' track which is 'showing'
-
- let descriptionsTrack = null;
- let captionsSubtitlesTrack = null;
- let i = tracks.length;
- while (i--) {
- const track = tracks[i];
- if (track.mode === 'showing') {
- if (track.kind === 'descriptions') {
- descriptionsTrack = track;
- } else {
- captionsSubtitlesTrack = track;
- }
- }
- }
- if (captionsSubtitlesTrack) {
- if (this.getAttribute('aria-live') !== 'off') {
- this.setAttribute('aria-live', 'off');
- }
- this.updateForTrack(captionsSubtitlesTrack);
- } else if (descriptionsTrack) {
- if (this.getAttribute('aria-live') !== 'assertive') {
- this.setAttribute('aria-live', 'assertive');
- }
- this.updateForTrack(descriptionsTrack);
- }
- }
-
- /**
- * Updates the displayed TextTrack to be sure it overlays the video when a either
- * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired.
- */
- updateDisplayOverlay() {
- // inset-inline and inset-block are not supprted on old chrome, but these are
- // only likely to be used on TV devices
- if (!this.player_.videoHeight() || !window.CSS.supports('inset-inline: 10px')) {
- return;
- }
- const playerWidth = this.player_.currentWidth();
- const playerHeight = this.player_.currentHeight();
- const playerAspectRatio = playerWidth / playerHeight;
- const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight();
- let insetInlineMatch = 0;
- let insetBlockMatch = 0;
- if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) {
- if (playerAspectRatio > videoAspectRatio) {
- insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2);
- } else {
- insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2);
- }
- }
- tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch));
- tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch));
- }
-
- /**
- * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
- *
- * @param {TextTrack} track
- * Text track object containing active cues to style.
- */
- updateDisplayState(track) {
- const overrides = this.player_.textTrackSettings.getValues();
- const cues = track.activeCues;
- let i = cues.length;
- while (i--) {
- const cue = cues[i];
- if (!cue) {
- continue;
- }
- const cueDiv = cue.displayState;
- if (overrides.color) {
- cueDiv.firstChild.style.color = overrides.color;
- }
- if (overrides.textOpacity) {
- tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
- }
- if (overrides.backgroundColor) {
- cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
- }
- if (overrides.backgroundOpacity) {
- tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
- }
- if (overrides.windowColor) {
- if (overrides.windowOpacity) {
- tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
- } else {
- cueDiv.style.backgroundColor = overrides.windowColor;
- }
- }
- if (overrides.edgeStyle) {
- if (overrides.edgeStyle === 'dropshadow') {
- cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
- } else if (overrides.edgeStyle === 'raised') {
- cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
- } else if (overrides.edgeStyle === 'depressed') {
- cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
- } else if (overrides.edgeStyle === 'uniform') {
- cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
- }
- }
- if (overrides.fontPercent && overrides.fontPercent !== 1) {
- const fontSize = window.parseFloat(cueDiv.style.fontSize);
- cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
- cueDiv.style.height = 'auto';
- cueDiv.style.top = 'auto';
- }
- if (overrides.fontFamily && overrides.fontFamily !== 'default') {
- if (overrides.fontFamily === 'small-caps') {
- cueDiv.firstChild.style.fontVariant = 'small-caps';
- } else {
- cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
- }
- }
- }
- }
-
- /**
- * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
- *
- * @param {TextTrack|TextTrack[]} tracks
- * Text track object or text track array to be added to the list.
- */
- updateForTrack(tracks) {
- if (!Array.isArray(tracks)) {
- tracks = [tracks];
- }
- if (typeof window.WebVTT !== 'function' || tracks.every(track => {
- return !track.activeCues;
- })) {
- return;
- }
- const cues = [];
-
- // push all active track cues
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- for (let j = 0; j < track.activeCues.length; ++j) {
- cues.push(track.activeCues[j]);
- }
- }
-
- // removes all cues before it processes new ones
- window.WebVTT.processCues(window, cues, this.el_);
-
- // add unique class to each language text track & add settings styling if necessary
- for (let i = 0; i < tracks.length; ++i) {
- const track = tracks[i];
- for (let j = 0; j < track.activeCues.length; ++j) {
- const cueEl = track.activeCues[j].displayState;
- addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
- if (track.language) {
- setAttribute(cueEl, 'lang', track.language);
- }
- }
- if (this.player_.textTrackSettings) {
- this.updateDisplayState(track);
- }
- }
- }
- }
- Component$1.registerComponent('TextTrackDisplay', TextTrackDisplay);
-
- /**
- * @file loading-spinner.js
- */
-
- /**
- * A loading spinner for use during waiting/loading events.
- *
- * @extends Component
- */
- class LoadingSpinner extends Component$1 {
- /**
- * Create the `LoadingSpinner`s DOM element.
- *
- * @return {Element}
- * The dom element that gets created.
- */
- createEl() {
- const isAudio = this.player_.isAudio();
- const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
- const controlText = createEl('span', {
- className: 'vjs-control-text',
- textContent: this.localize('{1} is loading.', [playerType])
- });
- const el = super.createEl('div', {
- className: 'vjs-loading-spinner',
- dir: 'ltr'
- });
- el.appendChild(controlText);
- return el;
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
- }
- }
- Component$1.registerComponent('LoadingSpinner', LoadingSpinner);
-
- /**
- * @file button.js
- */
-
- /**
- * Base class for all buttons.
- *
- * @extends ClickableComponent
- */
- class Button extends ClickableComponent {
- /**
- * Create the `Button`s DOM element.
- *
- * @param {string} [tag="button"]
- * The element's node type. This argument is IGNORED: no matter what
- * is passed, it will always create a `button` element.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element.
- *
- * @param {Object} [attributes={}]
- * An object of attributes that should be set on the element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(tag, props = {}, attributes = {}) {
- tag = 'button';
- props = Object.assign({
- className: this.buildCSSClass()
- }, props);
-
- // Add attributes for button element
- attributes = Object.assign({
- // Necessary since the default button type is "submit"
- type: 'button'
- }, attributes);
- const el = createEl(tag, props, attributes);
- if (!this.player_.options_.experimentalSvgIcons) {
- el.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- this.createControlTextEl(el);
- return el;
- }
-
- /**
- * Add a child `Component` inside of this `Button`.
- *
- * @param {string|Component} child
- * The name or instance of a child to add.
- *
- * @param {Object} [options={}]
- * The key/value store of options that will get passed to children of
- * the child.
- *
- * @return {Component}
- * The `Component` that gets added as a child. When using a string the
- * `Component` will get created by this process.
- *
- * @deprecated since version 5
- */
- addChild(child, options = {}) {
- const className = this.constructor.name;
- log$1.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
-
- // Avoid the error message generated by ClickableComponent's addChild method
- return Component$1.prototype.addChild.call(this, child, options);
- }
-
- /**
- * Enable the `Button` element so that it can be activated or clicked. Use this with
- * {@link Button#disable}.
- */
- enable() {
- super.enable();
- this.el_.removeAttribute('disabled');
- }
-
- /**
- * Disable the `Button` element so that it cannot be activated or clicked. Use this with
- * {@link Button#enable}.
- */
- disable() {
- super.disable();
- this.el_.setAttribute('disabled', 'disabled');
- }
-
- /**
- * This gets called when a `Button` has focus and `keydown` is triggered via a key
- * press.
- *
- * @param {KeyboardEvent} event
- * The event that caused this function to get called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Ignore Space or Enter key operation, which is handled by the browser for
- // a button - though not for its super class, ClickableComponent. Also,
- // prevent the event from propagating through the DOM and triggering Player
- // hotkeys. We do not preventDefault here because we _want_ the browser to
- // handle it.
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.stopPropagation();
- return;
- }
-
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- Component$1.registerComponent('Button', Button);
-
- /**
- * @file big-play-button.js
- */
-
- /**
- * The initial play button that shows before the video has played. The hiding of the
- * `BigPlayButton` get done via CSS and `Player` states.
- *
- * @extends Button
- */
- class BigPlayButton extends Button {
- constructor(player, options) {
- super(player, options);
- this.mouseused_ = false;
- this.setIcon('play');
- this.on('mousedown', e => this.handleMouseDown(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object. Always returns 'vjs-big-play-button'.
- */
- buildCSSClass() {
- return 'vjs-big-play-button';
- }
-
- /**
- * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
- * for more detailed information on what a click can be.
- *
- * @param {KeyboardEvent|MouseEvent|TouchEvent} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const playPromise = this.player_.play();
-
- // exit early if clicked via the mouse
- if (this.mouseused_ && 'clientX' in event && 'clientY' in event) {
- silencePromise(playPromise);
- if (this.player_.tech(true)) {
- this.player_.tech(true).focus();
- }
- return;
- }
- const cb = this.player_.getChild('controlBar');
- const playToggle = cb && cb.getChild('playToggle');
- if (!playToggle) {
- this.player_.tech(true).focus();
- return;
- }
- const playFocus = () => playToggle.focus();
- if (isPromise(playPromise)) {
- playPromise.then(playFocus, () => {});
- } else {
- this.setTimeout(playFocus, 1);
- }
- }
-
- /**
- * Event handler that is called when a `BigPlayButton` receives a
- * `keydown` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- this.mouseused_ = false;
- super.handleKeyDown(event);
- }
-
- /**
- * Handle `mousedown` events on the `BigPlayButton`.
- *
- * @param {MouseEvent} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- this.mouseused_ = true;
- }
- }
-
- /**
- * The text that should display over the `BigPlayButton`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- */
- BigPlayButton.prototype.controlText_ = 'Play Video';
- Component$1.registerComponent('BigPlayButton', BigPlayButton);
-
- /**
- * @file close-button.js
- */
-
- /**
- * The `CloseButton` is a `{@link Button}` that fires a `close` event when
- * it gets clicked.
- *
- * @extends Button
- */
- class CloseButton extends Button {
- /**
- * Creates an instance of the this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('cancel');
- this.controlText(options && options.controlText || this.localize('Close'));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-close-button ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when a `CloseButton` gets clicked. See
- * {@link ClickableComponent#handleClick} for more information on when
- * this will be triggered
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- * @fires CloseButton#close
- */
- handleClick(event) {
- /**
- * Triggered when the a `CloseButton` is clicked.
- *
- * @event CloseButton#close
- * @type {Event}
- *
- * @property {boolean} [bubbles=false]
- * set to false so that the close event does not
- * bubble up to parents if there is no listener
- */
- this.trigger({
- type: 'close',
- bubbles: false
- });
- }
- /**
- * Event handler that is called when a `CloseButton` receives a
- * `keydown` event.
- *
- * By default, if the key is Esc, it will trigger a `click` event.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Esc button will trigger `click` event
- if (keycode.isEventKey(event, 'Esc')) {
- event.preventDefault();
- event.stopPropagation();
- this.trigger('click');
- } else {
- // Pass keypress handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- }
- Component$1.registerComponent('CloseButton', CloseButton);
-
- /**
- * @file play-toggle.js
- */
-
- /**
- * Button to toggle between play and pause.
- *
- * @extends Button
- */
- class PlayToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- super(player, options);
-
- // show or hide replay icon
- options.replay = options.replay === undefined || options.replay;
- this.setIcon('play');
- this.on(player, 'play', e => this.handlePlay(e));
- this.on(player, 'pause', e => this.handlePause(e));
- if (options.replay) {
- this.on(player, 'ended', e => this.handleEnded(e));
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-play-control ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when an `PlayToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (this.player_.paused()) {
- silencePromise(this.player_.play());
- } else {
- this.player_.pause();
- }
- }
-
- /**
- * This gets called once after the video has ended and the user seeks so that
- * we can change the replay button back to a play button.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#seeked
- */
- handleSeeked(event) {
- this.removeClass('vjs-ended');
- if (this.player_.paused()) {
- this.handlePause(event);
- } else {
- this.handlePlay(event);
- }
- }
-
- /**
- * Add the vjs-playing class to the element so it can change appearance.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#play
- */
- handlePlay(event) {
- this.removeClass('vjs-ended', 'vjs-paused');
- this.addClass('vjs-playing');
- // change the button text to "Pause"
- this.setIcon('pause');
- this.controlText('Pause');
- }
-
- /**
- * Add the vjs-paused class to the element so it can change appearance.
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#pause
- */
- handlePause(event) {
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- // change the button text to "Play"
- this.setIcon('play');
- this.controlText('Play');
- }
-
- /**
- * Add the vjs-ended class to the element so it can change appearance
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#ended
- */
- handleEnded(event) {
- this.removeClass('vjs-playing');
- this.addClass('vjs-ended');
- // change the button text to "Replay"
- this.setIcon('replay');
- this.controlText('Replay');
-
- // on the next seek remove the replay button
- this.one(this.player_, 'seeked', e => this.handleSeeked(e));
- }
- }
-
- /**
- * The text that should display over the `PlayToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- PlayToggle.prototype.controlText_ = 'Play';
- Component$1.registerComponent('PlayToggle', PlayToggle);
-
- /**
- * @file time-display.js
- */
-
- /**
- * Displays time information about the video
- *
- * @extends Component
- */
- class TimeDisplay extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e));
- this.updateTextNode_();
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const className = this.buildCSSClass();
- const el = super.createEl('div', {
- className: `${className} vjs-time-control vjs-control`
- });
- const span = createEl('span', {
- className: 'vjs-control-text',
- textContent: `${this.localize(this.labelText_)}\u00a0`
- }, {
- role: 'presentation'
- });
- el.appendChild(span);
- this.contentEl_ = createEl('span', {
- className: `${className}-display`
- }, {
- // span elements have no implicit role, but some screen readers (notably VoiceOver)
- // treat them as a break between items in the DOM when using arrow keys
- // (or left-to-right swipes on iOS) to read contents of a page. Using
- // role='presentation' causes VoiceOver to NOT treat this span as a break.
- role: 'presentation'
- });
- el.appendChild(this.contentEl_);
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- this.textNode_ = null;
- super.dispose();
- }
-
- /**
- * Updates the displayed time according to the `updateContent` function which is defined in the child class.
- *
- * @param {Event} [event]
- * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called.
- */
- update(event) {
- if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') {
- return;
- }
- this.updateContent(event);
- }
-
- /**
- * Updates the time display text node with a new time
- *
- * @param {number} [time=0] the time to update to
- *
- * @private
- */
- updateTextNode_(time = 0) {
- time = formatTime(time);
- if (this.formattedTime_ === time) {
- return;
- }
- this.formattedTime_ = time;
- this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
- if (!this.contentEl_) {
- return;
- }
- let oldNode = this.textNode_;
- if (oldNode && this.contentEl_.firstChild !== oldNode) {
- oldNode = null;
- log$1.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.');
- }
- this.textNode_ = document.createTextNode(this.formattedTime_);
- if (!this.textNode_) {
- return;
- }
- if (oldNode) {
- this.contentEl_.replaceChild(this.textNode_, oldNode);
- } else {
- this.contentEl_.appendChild(this.textNode_);
- }
- });
- }
-
- /**
- * To be filled out in the child class, should update the displayed time
- * in accordance with the fact that the current time has changed.
- *
- * @param {Event} [event]
- * The `timeupdate` event that caused this to run.
- *
- * @listens Player#timeupdate
- */
- updateContent(event) {}
- }
-
- /**
- * The text that is added to the `TimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- TimeDisplay.prototype.labelText_ = 'Time';
-
- /**
- * The text that should display over the `TimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- TimeDisplay.prototype.controlText_ = 'Time';
- Component$1.registerComponent('TimeDisplay', TimeDisplay);
-
- /**
- * @file current-time-display.js
- */
-
- /**
- * Displays the current time
- *
- * @extends Component
- */
- class CurrentTimeDisplay extends TimeDisplay {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-current-time';
- }
-
- /**
- * Update current time display
- *
- * @param {Event} [event]
- * The `timeupdate` event that caused this function to run.
- *
- * @listens Player#timeupdate
- */
- updateContent(event) {
- // Allows for smooth scrubbing, when player can't keep up.
- let time;
- if (this.player_.ended()) {
- time = this.player_.duration();
- } else {
- time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- }
- this.updateTextNode_(time);
- }
- }
-
- /**
- * The text that is added to the `CurrentTimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
-
- /**
- * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
- Component$1.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
-
- /**
- * @file duration-display.js
- */
-
- /**
- * Displays the duration
- *
- * @extends Component
- */
- class DurationDisplay extends TimeDisplay {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- const updateContent = e => this.updateContent(e);
-
- // we do not want to/need to throttle duration changes,
- // as they should always display the changed duration as
- // it has changed
- this.on(player, 'durationchange', updateContent);
-
- // Listen to loadstart because the player duration is reset when a new media element is loaded,
- // but the durationchange on the user agent will not fire.
- // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
- this.on(player, 'loadstart', updateContent);
-
- // Also listen for timeupdate (in the parent) and loadedmetadata because removing those
- // listeners could have broken dependent applications/libraries. These
- // can likely be removed for 7.0.
- this.on(player, 'loadedmetadata', updateContent);
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-duration';
- }
-
- /**
- * Update duration time display.
- *
- * @param {Event} [event]
- * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
- * this function to be called.
- *
- * @listens Player#durationchange
- * @listens Player#timeupdate
- * @listens Player#loadedmetadata
- */
- updateContent(event) {
- const duration = this.player_.duration();
- this.updateTextNode_(duration);
- }
- }
-
- /**
- * The text that is added to the `DurationDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- DurationDisplay.prototype.labelText_ = 'Duration';
-
- /**
- * The text that should display over the `DurationDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- DurationDisplay.prototype.controlText_ = 'Duration';
- Component$1.registerComponent('DurationDisplay', DurationDisplay);
-
- /**
- * @file time-divider.js
- */
-
- /**
- * The separator between the current time and duration.
- * Can be hidden if it's not needed in the design.
- *
- * @extends Component
- */
- class TimeDivider extends Component$1 {
- /**
- * Create the component's DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-time-control vjs-time-divider'
- }, {
- // this element and its contents can be hidden from assistive techs since
- // it is made extraneous by the announcement of the control text
- // for the current time and duration displays
- 'aria-hidden': true
- });
- const div = super.createEl('div');
- const span = super.createEl('span', {
- textContent: '/'
- });
- div.appendChild(span);
- el.appendChild(div);
- return el;
- }
- }
- Component$1.registerComponent('TimeDivider', TimeDivider);
-
- /**
- * @file remaining-time-display.js
- */
-
- /**
- * Displays the time left in the video
- *
- * @extends Component
- */
- class RemainingTimeDisplay extends TimeDisplay {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, 'durationchange', e => this.updateContent(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return 'vjs-remaining-time';
- }
-
- /**
- * Create the `Component`'s DOM element with the "minus" character prepend to the time
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl();
- if (this.options_.displayNegative !== false) {
- el.insertBefore(createEl('span', {}, {
- 'aria-hidden': true
- }, '-'), this.contentEl_);
- }
- return el;
- }
-
- /**
- * Update remaining time display.
- *
- * @param {Event} [event]
- * The `timeupdate` or `durationchange` event that caused this to run.
- *
- * @listens Player#timeupdate
- * @listens Player#durationchange
- */
- updateContent(event) {
- if (typeof this.player_.duration() !== 'number') {
- return;
- }
- let time;
-
- // @deprecated We should only use remainingTimeDisplay
- // as of video.js 7
- if (this.player_.ended()) {
- time = 0;
- } else if (this.player_.remainingTimeDisplay) {
- time = this.player_.remainingTimeDisplay();
- } else {
- time = this.player_.remainingTime();
- }
- this.updateTextNode_(time);
- }
- }
-
- /**
- * The text that is added to the `RemainingTimeDisplay` for screen reader users.
- *
- * @type {string}
- * @private
- */
- RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
-
- /**
- * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
- *
- * @type {string}
- * @protected
- *
- * @deprecated in v7; controlText_ is not used in non-active display Components
- */
- RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
- Component$1.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
-
- /**
- * @file live-display.js
- */
-
- // TODO - Future make it click to snap to live
-
- /**
- * Displays the live indicator when duration is Infinity.
- *
- * @extends Component
- */
- class LiveDisplay extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.updateShowing();
- this.on(this.player(), 'durationchange', e => this.updateShowing(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-live-control vjs-control'
- });
- this.contentEl_ = createEl('div', {
- className: 'vjs-live-display'
- }, {
- 'aria-live': 'off'
- });
- this.contentEl_.appendChild(createEl('span', {
- className: 'vjs-control-text',
- textContent: `${this.localize('Stream Type')}\u00a0`
- }));
- this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE')));
- el.appendChild(this.contentEl_);
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- super.dispose();
- }
-
- /**
- * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
- * it accordingly
- *
- * @param {Event} [event]
- * The {@link Player#durationchange} event that caused this function to run.
- *
- * @listens Player#durationchange
- */
- updateShowing(event) {
- if (this.player().duration() === Infinity) {
- this.show();
- } else {
- this.hide();
- }
- }
- }
- Component$1.registerComponent('LiveDisplay', LiveDisplay);
-
- /**
- * @file seek-to-live.js
- */
-
- /**
- * Displays the live indicator when duration is Infinity.
- *
- * @extends Component
- */
- class SeekToLive extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.updateLiveEdgeStatus();
- if (this.player_.liveTracker) {
- this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
- this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
- }
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('button', {
- className: 'vjs-seek-to-live-control vjs-control'
- });
- this.setIcon('circle', el);
- this.textEl_ = createEl('span', {
- className: 'vjs-seek-to-live-text',
- textContent: this.localize('LIVE')
- }, {
- 'aria-hidden': 'true'
- });
- el.appendChild(this.textEl_);
- return el;
- }
-
- /**
- * Update the state of this button if we are at the live edge
- * or not
- */
- updateLiveEdgeStatus() {
- // default to live edge
- if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
- this.setAttribute('aria-disabled', true);
- this.addClass('vjs-at-live-edge');
- this.controlText('Seek to live, currently playing live');
- } else {
- this.setAttribute('aria-disabled', false);
- this.removeClass('vjs-at-live-edge');
- this.controlText('Seek to live, currently behind live');
- }
- }
-
- /**
- * On click bring us as near to the live point as possible.
- * This requires that we wait for the next `live-seekable-change`
- * event which will happen every segment length seconds.
- */
- handleClick() {
- this.player_.liveTracker.seekToLiveEdge();
- }
-
- /**
- * Dispose of the element and stop tracking
- */
- dispose() {
- if (this.player_.liveTracker) {
- this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
- }
- this.textEl_ = null;
- super.dispose();
- }
- }
- /**
- * The text that should display over the `SeekToLive`s control. Added for localization.
- *
- * @type {string}
- * @protected
- */
- SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
- Component$1.registerComponent('SeekToLive', SeekToLive);
-
- /**
- * @file num.js
- * @module num
- */
-
- /**
- * Keep a number between a min and a max value
- *
- * @param {number} number
- * The number to clamp
- *
- * @param {number} min
- * The minimum value
- * @param {number} max
- * The maximum value
- *
- * @return {number}
- * the clamped number
- */
- function clamp(number, min, max) {
- number = Number(number);
- return Math.min(max, Math.max(min, isNaN(number) ? min : number));
- }
-
- var Num = /*#__PURE__*/Object.freeze({
- __proto__: null,
- clamp: clamp
- });
-
- /**
- * @file slider.js
- */
-
- /**
- * The base functionality for a slider. Can be vertical or horizontal.
- * For instance the volume bar or the seek bar on a video is a slider.
- *
- * @extends Component
- */
- class Slider extends Component$1 {
- /**
- * Create an instance of this class
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.handleMouseDown_ = e => this.handleMouseDown(e);
- this.handleMouseUp_ = e => this.handleMouseUp(e);
- this.handleKeyDown_ = e => this.handleKeyDown(e);
- this.handleClick_ = e => this.handleClick(e);
- this.handleMouseMove_ = e => this.handleMouseMove(e);
- this.update_ = e => this.update(e);
-
- // Set property names to bar to match with the child Slider class is looking for
- this.bar = this.getChild(this.options_.barName);
-
- // Set a horizontal or vertical class on the slider depending on the slider type
- this.vertical(!!this.options_.vertical);
- this.enable();
- }
-
- /**
- * Are controls are currently enabled for this slider or not.
- *
- * @return {boolean}
- * true if controls are enabled, false otherwise
- */
- enabled() {
- return this.enabled_;
- }
-
- /**
- * Enable controls for this slider if they are disabled
- */
- enable() {
- if (this.enabled()) {
- return;
- }
- this.on('mousedown', this.handleMouseDown_);
- this.on('touchstart', this.handleMouseDown_);
- this.on('keydown', this.handleKeyDown_);
- this.on('click', this.handleClick_);
-
- // TODO: deprecated, controlsvisible does not seem to be fired
- this.on(this.player_, 'controlsvisible', this.update);
- if (this.playerEvent) {
- this.on(this.player_, this.playerEvent, this.update);
- }
- this.removeClass('disabled');
- this.setAttribute('tabindex', 0);
- this.enabled_ = true;
- }
-
- /**
- * Disable controls for this slider if they are enabled
- */
- disable() {
- if (!this.enabled()) {
- return;
- }
- const doc = this.bar.el_.ownerDocument;
- this.off('mousedown', this.handleMouseDown_);
- this.off('touchstart', this.handleMouseDown_);
- this.off('keydown', this.handleKeyDown_);
- this.off('click', this.handleClick_);
- this.off(this.player_, 'controlsvisible', this.update_);
- this.off(doc, 'mousemove', this.handleMouseMove_);
- this.off(doc, 'mouseup', this.handleMouseUp_);
- this.off(doc, 'touchmove', this.handleMouseMove_);
- this.off(doc, 'touchend', this.handleMouseUp_);
- this.removeAttribute('tabindex');
- this.addClass('disabled');
- if (this.playerEvent) {
- this.off(this.player_, this.playerEvent, this.update);
- }
- this.enabled_ = false;
- }
-
- /**
- * Create the `Slider`s DOM element.
- *
- * @param {string} type
- * Type of element to create.
- *
- * @param {Object} [props={}]
- * List of properties in Object form.
- *
- * @param {Object} [attributes={}]
- * list of attributes in Object form.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(type, props = {}, attributes = {}) {
- // Add the slider element class to all sub classes
- props.className = props.className + ' vjs-slider';
- props = Object.assign({
- tabIndex: 0
- }, props);
- attributes = Object.assign({
- 'role': 'slider',
- 'aria-valuenow': 0,
- 'aria-valuemin': 0,
- 'aria-valuemax': 100
- }, attributes);
- return super.createEl(type, props, attributes);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `Slider`.
- *
- * @param {MouseEvent} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- * @fires Slider#slideractive
- */
- handleMouseDown(event) {
- const doc = this.bar.el_.ownerDocument;
- if (event.type === 'mousedown') {
- event.preventDefault();
- }
- // Do not call preventDefault() on touchstart in Chrome
- // to avoid console warnings. Use a 'touch-action: none' style
- // instead to prevent unintended scrolling.
- // https://developers.google.com/web/updates/2017/01/scrolling-intervention
- if (event.type === 'touchstart' && !IS_CHROME) {
- event.preventDefault();
- }
- blockTextSelection();
- this.addClass('vjs-sliding');
- /**
- * Triggered when the slider is in an active state
- *
- * @event Slider#slideractive
- * @type {MouseEvent}
- */
- this.trigger('slideractive');
- this.on(doc, 'mousemove', this.handleMouseMove_);
- this.on(doc, 'mouseup', this.handleMouseUp_);
- this.on(doc, 'touchmove', this.handleMouseMove_);
- this.on(doc, 'touchend', this.handleMouseUp_);
- this.handleMouseMove(event, true);
- }
-
- /**
- * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
- * The `mousemove` and `touchmove` events will only only trigger this function during
- * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
- * {@link Slider#handleMouseUp}.
- *
- * @param {MouseEvent} event
- * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
- * this function
- * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
- *
- * @listens mousemove
- * @listens touchmove
- */
- handleMouseMove(event) {}
-
- /**
- * Handle `mouseup` or `touchend` events on the `Slider`.
- *
- * @param {MouseEvent} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- * @fires Slider#sliderinactive
- */
- handleMouseUp(event) {
- const doc = this.bar.el_.ownerDocument;
- unblockTextSelection();
- this.removeClass('vjs-sliding');
- /**
- * Triggered when the slider is no longer in an active state.
- *
- * @event Slider#sliderinactive
- * @type {Event}
- */
- this.trigger('sliderinactive');
- this.off(doc, 'mousemove', this.handleMouseMove_);
- this.off(doc, 'mouseup', this.handleMouseUp_);
- this.off(doc, 'touchmove', this.handleMouseMove_);
- this.off(doc, 'touchend', this.handleMouseUp_);
- this.update();
- }
-
- /**
- * Update the progress bar of the `Slider`.
- *
- * @return {number}
- * The percentage of progress the progress bar represents as a
- * number from 0 to 1.
- */
- update() {
- // In VolumeBar init we have a setTimeout for update that pops and update
- // to the end of the execution stack. The player is destroyed before then
- // update will cause an error
- // If there's no bar...
- if (!this.el_ || !this.bar) {
- return;
- }
-
- // clamp progress between 0 and 1
- // and only round to four decimal places, as we round to two below
- const progress = this.getProgress();
- if (progress === this.progress_) {
- return progress;
- }
- this.progress_ = progress;
- this.requestNamedAnimationFrame('Slider#update', () => {
- // Set the new bar width or height
- const sizeKey = this.vertical() ? 'height' : 'width';
-
- // Convert to a percentage for css value
- this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
- });
- return progress;
- }
-
- /**
- * Get the percentage of the bar that should be filled
- * but clamped and rounded.
- *
- * @return {number}
- * percentage filled that the slider is
- */
- getProgress() {
- return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
- }
-
- /**
- * Calculate distance for slider
- *
- * @param {Event} event
- * The event that caused this function to run.
- *
- * @return {number}
- * The current position of the Slider.
- * - position.x for vertical `Slider`s
- * - position.y for horizontal `Slider`s
- */
- calculateDistance(event) {
- const position = getPointerPosition(this.el_, event);
- if (this.vertical()) {
- return position.y;
- }
- return position.x;
- }
-
- /**
- * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
- * arrow keys. This function will only be called when the slider has focus. See
- * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
- *
- * @param {KeyboardEvent} event
- * the `keydown` event that caused this function to run.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Left and Down Arrows
- if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepBack();
-
- // Up and Right Arrows
- } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepForward();
- } else {
- // Pass keydown handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
-
- /**
- * Listener for click events on slider, used to prevent clicks
- * from bubbling up to parent elements like button menus.
- *
- * @param {Object} event
- * Event that caused this object to run
- */
- handleClick(event) {
- event.stopPropagation();
- event.preventDefault();
- }
-
- /**
- * Get/set if slider is horizontal for vertical
- *
- * @param {boolean} [bool]
- * - true if slider is vertical,
- * - false is horizontal
- *
- * @return {boolean}
- * - true if slider is vertical, and getting
- * - false if the slider is horizontal, and getting
- */
- vertical(bool) {
- if (bool === undefined) {
- return this.vertical_ || false;
- }
- this.vertical_ = !!bool;
- if (this.vertical_) {
- this.addClass('vjs-slider-vertical');
- } else {
- this.addClass('vjs-slider-horizontal');
- }
- }
- }
- Component$1.registerComponent('Slider', Slider);
-
- /**
- * @file load-progress-bar.js
- */
-
- // get the percent width of a time compared to the total end
- const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
-
- /**
- * Shows loading progress
- *
- * @extends Component
- */
- class LoadProgressBar extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.partEls_ = [];
- this.on(player, 'progress', e => this.update(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-load-progress'
- });
- const wrapper = createEl('span', {
- className: 'vjs-control-text'
- });
- const loadedText = createEl('span', {
- textContent: this.localize('Loaded')
- });
- const separator = document.createTextNode(': ');
- this.percentageEl_ = createEl('span', {
- className: 'vjs-control-text-loaded-percentage',
- textContent: '0%'
- });
- el.appendChild(wrapper);
- wrapper.appendChild(loadedText);
- wrapper.appendChild(separator);
- wrapper.appendChild(this.percentageEl_);
- return el;
- }
- dispose() {
- this.partEls_ = null;
- this.percentageEl_ = null;
- super.dispose();
- }
-
- /**
- * Update progress bar
- *
- * @param {Event} [event]
- * The `progress` event that caused this function to run.
- *
- * @listens Player#progress
- */
- update(event) {
- this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
- const liveTracker = this.player_.liveTracker;
- const buffered = this.player_.buffered();
- const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
- const bufferedEnd = this.player_.bufferedEnd();
- const children = this.partEls_;
- const percent = percentify(bufferedEnd, duration);
- if (this.percent_ !== percent) {
- // update the width of the progress bar
- this.el_.style.width = percent;
- // update the control-text
- textContent(this.percentageEl_, percent);
- this.percent_ = percent;
- }
-
- // add child elements to represent the individual buffered time ranges
- for (let i = 0; i < buffered.length; i++) {
- const start = buffered.start(i);
- const end = buffered.end(i);
- let part = children[i];
- if (!part) {
- part = this.el_.appendChild(createEl());
- children[i] = part;
- }
-
- // only update if changed
- if (part.dataset.start === start && part.dataset.end === end) {
- continue;
- }
- part.dataset.start = start;
- part.dataset.end = end;
-
- // set the percent based on the width of the progress bar (bufferedEnd)
- part.style.left = percentify(start, bufferedEnd);
- part.style.width = percentify(end - start, bufferedEnd);
- }
-
- // remove unused buffered range elements
- for (let i = children.length; i > buffered.length; i--) {
- this.el_.removeChild(children[i - 1]);
- }
- children.length = buffered.length;
- });
- }
- }
- Component$1.registerComponent('LoadProgressBar', LoadProgressBar);
-
- /**
- * @file time-tooltip.js
- */
-
- /**
- * Time tooltips display a time above the progress bar.
- *
- * @extends Component
- */
- class TimeTooltip extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the time tooltip DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-time-tooltip'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Updates the position of the time tooltip relative to the `SeekBar`.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint, content) {
- const tooltipRect = findPosition(this.el_);
- const playerRect = getBoundingClientRect(this.player_.el());
- const seekBarPointPx = seekBarRect.width * seekBarPoint;
-
- // do nothing if either rect isn't available
- // for example, if the player isn't in the DOM for testing
- if (!playerRect || !tooltipRect) {
- return;
- }
-
- // This is the space left of the `seekBarPoint` available within the bounds
- // of the player. We calculate any gap between the left edge of the player
- // and the left edge of the `SeekBar` and add the number of pixels in the
- // `SeekBar` before hitting the `seekBarPoint`
- let spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
-
- // This is the space right of the `seekBarPoint` available within the bounds
- // of the player. We calculate the number of pixels from the `seekBarPoint`
- // to the right edge of the `SeekBar` and add to that any gap between the
- // right edge of the `SeekBar` and the player.
- let spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
-
- // spaceRightOfPoint is always NaN for mouse time display
- // because the seekbarRect does not have a right property. This causes
- // the mouse tool tip to be truncated when it's close to the right edge of the player.
- // In such cases, we ignore the `playerRect.right - seekBarRect.right` value when calculating.
- // For the sake of consistency, we ignore seekBarRect.left - playerRect.left for the left edge.
- if (!spaceRightOfPoint) {
- spaceRightOfPoint = seekBarRect.width - seekBarPointPx;
- spaceLeftOfPoint = seekBarPointPx;
- }
- // This is the number of pixels by which the tooltip will need to be pulled
- // further to the right to center it over the `seekBarPoint`.
- let pullTooltipBy = tooltipRect.width / 2;
-
- // Adjust the `pullTooltipBy` distance to the left or right depending on
- // the results of the space calculations above.
- if (spaceLeftOfPoint < pullTooltipBy) {
- pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
- } else if (spaceRightOfPoint < pullTooltipBy) {
- pullTooltipBy = spaceRightOfPoint;
- }
-
- // Due to the imprecision of decimal/ratio based calculations and varying
- // rounding behaviors, there are cases where the spacing adjustment is off
- // by a pixel or two. This adds insurance to these calculations.
- if (pullTooltipBy < 0) {
- pullTooltipBy = 0;
- } else if (pullTooltipBy > tooltipRect.width) {
- pullTooltipBy = tooltipRect.width;
- }
-
- // prevent small width fluctuations within 0.4px from
- // changing the value below.
- // This really helps for live to prevent the play
- // progress time tooltip from jittering
- pullTooltipBy = Math.round(pullTooltipBy);
- this.el_.style.right = `-${pullTooltipBy}px`;
- this.write(content);
- }
-
- /**
- * Write the time to the tooltip DOM element.
- *
- * @param {string} content
- * The formatted time for the tooltip.
- */
- write(content) {
- textContent(this.el_, content);
- }
-
- /**
- * Updates the position of the time tooltip relative to the `SeekBar`.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- *
- * @param {number} time
- * The time to update the tooltip to, not used during live playback
- *
- * @param {Function} cb
- * A function that will be called during the request animation frame
- * for tooltips that need to do additional animations from the default
- */
- updateTime(seekBarRect, seekBarPoint, time, cb) {
- this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
- let content;
- const duration = this.player_.duration();
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- const liveWindow = this.player_.liveTracker.liveWindow();
- const secondsBehind = liveWindow - seekBarPoint * liveWindow;
- content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
- } else {
- content = formatTime(time, duration);
- }
- this.update(seekBarRect, seekBarPoint, content);
- if (cb) {
- cb();
- }
- });
- }
- }
- Component$1.registerComponent('TimeTooltip', TimeTooltip);
-
- /**
- * @file play-progress-bar.js
- */
-
- /**
- * Used by {@link SeekBar} to display media playback progress as part of the
- * {@link ProgressControl}.
- *
- * @extends Component
- */
- class PlayProgressBar extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('circle');
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-play-progress vjs-slider-bar'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Enqueues updates to its own DOM as well as the DOM of its
- * {@link TimeTooltip} child.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint) {
- const timeTooltip = this.getChild('timeTooltip');
- if (!timeTooltip) {
- return;
- }
- const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
- }
- }
-
- /**
- * Default options for {@link PlayProgressBar}.
- *
- * @type {Object}
- * @private
- */
- PlayProgressBar.prototype.options_ = {
- children: []
- };
-
- // Time tooltips should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- PlayProgressBar.prototype.options_.children.push('timeTooltip');
- }
- Component$1.registerComponent('PlayProgressBar', PlayProgressBar);
-
- /**
- * @file mouse-time-display.js
- */
-
- /**
- * The {@link MouseTimeDisplay} component tracks mouse movement over the
- * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
- * indicating the time which is represented by a given point in the
- * {@link ProgressControl}.
- *
- * @extends Component
- */
- class MouseTimeDisplay extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-mouse-display'
- });
- }
-
- /**
- * Enqueues updates to its own DOM as well as the DOM of its
- * {@link TimeTooltip} child.
- *
- * @param {Object} seekBarRect
- * The `ClientRect` for the {@link SeekBar} element.
- *
- * @param {number} seekBarPoint
- * A number from 0 to 1, representing a horizontal reference point
- * from the left edge of the {@link SeekBar}
- */
- update(seekBarRect, seekBarPoint) {
- const time = seekBarPoint * this.player_.duration();
- this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
- this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
- });
- }
- }
-
- /**
- * Default options for `MouseTimeDisplay`
- *
- * @type {Object}
- * @private
- */
- MouseTimeDisplay.prototype.options_ = {
- children: ['timeTooltip']
- };
- Component$1.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
-
- /**
- * @file seek-bar.js
- */
-
- // The number of seconds the `step*` functions move the timeline.
- const STEP_SECONDS = 5;
-
- // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
- const PAGE_KEY_MULTIPLIER = 12;
-
- /**
- * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
- * as its `bar`.
- *
- * @extends Slider
- */
- class SeekBar extends Slider {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setEventHandlers_();
- }
-
- /**
- * Sets the event handlers
- *
- * @private
- */
- setEventHandlers_() {
- this.update_ = bind_(this, this.update);
- this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
- this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- if (this.player_.liveTracker) {
- this.on(this.player_.liveTracker, 'liveedgechange', this.update);
- }
-
- // when playing, let's ensure we smoothly update the play progress bar
- // via an interval
- this.updateInterval = null;
- this.enableIntervalHandler_ = e => this.enableInterval_(e);
- this.disableIntervalHandler_ = e => this.disableInterval_(e);
- this.on(this.player_, ['playing'], this.enableIntervalHandler_);
- this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
-
- // we don't need to update the play progress if the document is hidden,
- // also, this causes the CPU to spike and eventually crash the page on IE11.
- if ('hidden' in document && 'visibilityState' in document) {
- this.on(document, 'visibilitychange', this.toggleVisibility_);
- }
- }
- toggleVisibility_(e) {
- if (document.visibilityState === 'hidden') {
- this.cancelNamedAnimationFrame('SeekBar#update');
- this.cancelNamedAnimationFrame('Slider#update');
- this.disableInterval_(e);
- } else {
- if (!this.player_.ended() && !this.player_.paused()) {
- this.enableInterval_();
- }
-
- // we just switched back to the page and someone may be looking, so, update ASAP
- this.update();
- }
- }
- enableInterval_() {
- if (this.updateInterval) {
- return;
- }
- this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
- }
- disableInterval_(e) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
- return;
- }
- if (!this.updateInterval) {
- return;
- }
- this.clearInterval(this.updateInterval);
- this.updateInterval = null;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-progress-holder'
- }, {
- 'aria-label': this.localize('Progress Bar')
- });
- }
-
- /**
- * This function updates the play progress bar and accessibility
- * attributes to whatever is passed in.
- *
- * @param {Event} [event]
- * The `timeupdate` or `ended` event that caused this to run.
- *
- * @listens Player#timeupdate
- *
- * @return {number}
- * The current percent at a number from 0-1
- */
- update(event) {
- // ignore updates while the tab is hidden
- if (document.visibilityState === 'hidden') {
- return;
- }
- const percent = super.update();
- this.requestNamedAnimationFrame('SeekBar#update', () => {
- const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
- const liveTracker = this.player_.liveTracker;
- let duration = this.player_.duration();
- if (liveTracker && liveTracker.isLive()) {
- duration = this.player_.liveTracker.liveCurrentTime();
- }
- if (this.percent_ !== percent) {
- // machine readable value of progress bar (percentage complete)
- this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
- this.percent_ = percent;
- }
- if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
- // human readable value of progress bar (time complete)
- this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
- this.currentTime_ = currentTime;
- this.duration_ = duration;
- }
-
- // update the progress bar time tooltip with the current time
- if (this.bar) {
- this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
- }
- });
- return percent;
- }
-
- /**
- * Prevent liveThreshold from causing seeks to seem like they
- * are not happening from a user perspective.
- *
- * @param {number} ct
- * current time to seek to
- */
- userSeek_(ct) {
- if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
- this.player_.liveTracker.nextSeekedFromUser();
- }
- this.player_.currentTime(ct);
- }
-
- /**
- * Get the value of current time but allows for smooth scrubbing,
- * when player can't keep up.
- *
- * @return {number}
- * The current time value to display
- *
- * @private
- */
- getCurrentTime_() {
- return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
- }
-
- /**
- * Get the percentage of media played so far.
- *
- * @return {number}
- * The percentage of media played so far (0 to 1).
- */
- getPercent() {
- const currentTime = this.getCurrentTime_();
- let percent;
- const liveTracker = this.player_.liveTracker;
- if (liveTracker && liveTracker.isLive()) {
- percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
-
- // prevent the percent from changing at the live edge
- if (liveTracker.atLiveEdge()) {
- percent = 1;
- }
- } else {
- percent = currentTime / this.player_.duration();
- }
- return percent;
- }
-
- /**
- * Handle mouse down on seek bar
- *
- * @param {MouseEvent} event
- * The `mousedown` event that caused this to run.
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- if (!isSingleLeftClick(event)) {
- return;
- }
-
- // Stop event propagation to prevent double fire in progress-control.js
- event.stopPropagation();
- this.videoWasPlaying = !this.player_.paused();
- this.player_.pause();
- super.handleMouseDown(event);
- }
-
- /**
- * Handle mouse move on seek bar
- *
- * @param {MouseEvent} event
- * The `mousemove` event that caused this to run.
- * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
- *
- * @listens mousemove
- */
- handleMouseMove(event, mouseDown = false) {
- if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) {
- return;
- }
- if (!mouseDown && !this.player_.scrubbing()) {
- this.player_.scrubbing(true);
- }
- let newTime;
- const distance = this.calculateDistance(event);
- const liveTracker = this.player_.liveTracker;
- if (!liveTracker || !liveTracker.isLive()) {
- newTime = distance * this.player_.duration();
-
- // Don't let video end while scrubbing.
- if (newTime === this.player_.duration()) {
- newTime = newTime - 0.1;
- }
- } else {
- if (distance >= 0.99) {
- liveTracker.seekToLiveEdge();
- return;
- }
- const seekableStart = liveTracker.seekableStart();
- const seekableEnd = liveTracker.liveCurrentTime();
- newTime = seekableStart + distance * liveTracker.liveWindow();
-
- // Don't let video end while scrubbing.
- if (newTime >= seekableEnd) {
- newTime = seekableEnd;
- }
-
- // Compensate for precision differences so that currentTime is not less
- // than seekable start
- if (newTime <= seekableStart) {
- newTime = seekableStart + 0.1;
- }
-
- // On android seekableEnd can be Infinity sometimes,
- // this will cause newTime to be Infinity, which is
- // not a valid currentTime.
- if (newTime === Infinity) {
- return;
- }
- }
-
- // Set new time (tell player to seek to new time)
- this.userSeek_(newTime);
- if (this.player_.options_.enableSmoothSeeking) {
- this.update();
- }
- }
- enable() {
- super.enable();
- const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- if (!mouseTimeDisplay) {
- return;
- }
- mouseTimeDisplay.show();
- }
- disable() {
- super.disable();
- const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
- if (!mouseTimeDisplay) {
- return;
- }
- mouseTimeDisplay.hide();
- }
-
- /**
- * Handle mouse up on seek bar
- *
- * @param {MouseEvent} event
- * The `mouseup` event that caused this to run.
- *
- * @listens mouseup
- */
- handleMouseUp(event) {
- super.handleMouseUp(event);
-
- // Stop event propagation to prevent double fire in progress-control.js
- if (event) {
- event.stopPropagation();
- }
- this.player_.scrubbing(false);
-
- /**
- * Trigger timeupdate because we're done seeking and the time has changed.
- * This is particularly useful for if the player is paused to time the time displays.
- *
- * @event Tech#timeupdate
- * @type {Event}
- */
- this.player_.trigger({
- type: 'timeupdate',
- target: this,
- manuallyTriggered: true
- });
- if (this.videoWasPlaying) {
- silencePromise(this.player_.play());
- } else {
- // We're done seeking and the time has changed.
- // If the player is paused, make sure we display the correct time on the seek bar.
- this.update_();
- }
- }
-
- /**
- * Move more quickly fast forward for keyboard-only users
- */
- stepForward() {
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
- }
-
- /**
- * Move more quickly rewind for keyboard-only users
- */
- stepBack() {
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
- }
-
- /**
- * Toggles the playback state of the player
- * This gets called when enter or space is used on the seekbar
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called
- *
- */
- handleAction(event) {
- if (this.player_.paused()) {
- this.player_.play();
- } else {
- this.player_.pause();
- }
- }
-
- /**
- * Called when this SeekBar has focus and a key gets pressed down.
- * Supports the following keys:
- *
- * Space or Enter key fire a click event
- * Home key moves to start of the timeline
- * End key moves to end of the timeline
- * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
- * PageDown key moves back a larger step than ArrowDown
- * PageUp key moves forward a large step
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- const liveTracker = this.player_.liveTracker;
- if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
- event.preventDefault();
- event.stopPropagation();
- this.handleAction(event);
- } else if (keycode.isEventKey(event, 'Home')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(0);
- } else if (keycode.isEventKey(event, 'End')) {
- event.preventDefault();
- event.stopPropagation();
- if (liveTracker && liveTracker.isLive()) {
- this.userSeek_(liveTracker.liveCurrentTime());
- } else {
- this.userSeek_(this.player_.duration());
- }
- } else if (/^[0-9]$/.test(keycode(event))) {
- event.preventDefault();
- event.stopPropagation();
- const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0;
- if (liveTracker && liveTracker.isLive()) {
- this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
- } else {
- this.userSeek_(this.player_.duration() * gotoFraction);
- }
- } else if (keycode.isEventKey(event, 'PgDn')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
- } else if (keycode.isEventKey(event, 'PgUp')) {
- event.preventDefault();
- event.stopPropagation();
- this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
- } else {
- // Pass keydown handling up for unsupported keys
- super.handleKeyDown(event);
- }
- }
- dispose() {
- this.disableInterval_();
- this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update);
- if (this.player_.liveTracker) {
- this.off(this.player_.liveTracker, 'liveedgechange', this.update);
- }
- this.off(this.player_, ['playing'], this.enableIntervalHandler_);
- this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
-
- // we don't need to update the play progress if the document is hidden,
- // also, this causes the CPU to spike and eventually crash the page on IE11.
- if ('hidden' in document && 'visibilityState' in document) {
- this.off(document, 'visibilitychange', this.toggleVisibility_);
- }
- super.dispose();
- }
- }
-
- /**
- * Default options for the `SeekBar`
- *
- * @type {Object}
- * @private
- */
- SeekBar.prototype.options_ = {
- children: ['loadProgressBar', 'playProgressBar'],
- barName: 'playProgressBar'
- };
-
- // MouseTimeDisplay tooltips should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay');
- }
- Component$1.registerComponent('SeekBar', SeekBar);
-
- /**
- * @file progress-control.js
- */
-
- /**
- * The Progress Control component contains the seek bar, load progress,
- * and play progress.
- *
- * @extends Component
- */
- class ProgressControl extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
- this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
- this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
- this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
- this.enable();
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-progress-control vjs-control'
- });
- }
-
- /**
- * When the mouse moves over the `ProgressControl`, the pointer position
- * gets passed down to the `MouseTimeDisplay` component.
- *
- * @param {Event} event
- * The `mousemove` event that caused this function to run.
- *
- * @listen mousemove
- */
- handleMouseMove(event) {
- const seekBar = this.getChild('seekBar');
- if (!seekBar) {
- return;
- }
- const playProgressBar = seekBar.getChild('playProgressBar');
- const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
- if (!playProgressBar && !mouseTimeDisplay) {
- return;
- }
- const seekBarEl = seekBar.el();
- const seekBarRect = findPosition(seekBarEl);
- let seekBarPoint = getPointerPosition(seekBarEl, event).x;
-
- // The default skin has a gap on either side of the `SeekBar`. This means
- // that it's possible to trigger this behavior outside the boundaries of
- // the `SeekBar`. This ensures we stay within it at all times.
- seekBarPoint = clamp(seekBarPoint, 0, 1);
- if (mouseTimeDisplay) {
- mouseTimeDisplay.update(seekBarRect, seekBarPoint);
- }
- if (playProgressBar) {
- playProgressBar.update(seekBarRect, seekBar.getProgress());
- }
- }
-
- /**
- * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
- *
- * @method ProgressControl#throttledHandleMouseSeek
- * @param {Event} event
- * The `mousemove` event that caused this function to run.
- *
- * @listen mousemove
- * @listen touchmove
- */
-
- /**
- * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousemove
- * @listens touchmove
- */
- handleMouseSeek(event) {
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseMove(event);
- }
- }
-
- /**
- * Are controls are currently enabled for this progress control.
- *
- * @return {boolean}
- * true if controls are enabled, false otherwise
- */
- enabled() {
- return this.enabled_;
- }
-
- /**
- * Disable all controls on the progress control and its children
- */
- disable() {
- this.children().forEach(child => child.disable && child.disable());
- if (!this.enabled()) {
- return;
- }
- this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
- this.off(this.el_, 'mousemove', this.handleMouseMove);
- this.removeListenersAddedOnMousedownAndTouchstart();
- this.addClass('disabled');
- this.enabled_ = false;
-
- // Restore normal playback state if controls are disabled while scrubbing
- if (this.player_.scrubbing()) {
- const seekBar = this.getChild('seekBar');
- this.player_.scrubbing(false);
- if (seekBar.videoWasPlaying) {
- silencePromise(this.player_.play());
- }
- }
- }
-
- /**
- * Enable all controls on the progress control and its children
- */
- enable() {
- this.children().forEach(child => child.enable && child.enable());
- if (this.enabled()) {
- return;
- }
- this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
- this.on(this.el_, 'mousemove', this.handleMouseMove);
- this.removeClass('disabled');
- this.enabled_ = true;
- }
-
- /**
- * Cleanup listeners after the user finishes interacting with the progress controls
- */
- removeListenersAddedOnMousedownAndTouchstart() {
- const doc = this.el_.ownerDocument;
- this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
- this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
- this.off(doc, 'mouseup', this.handleMouseUpHandler_);
- this.off(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseDown(event) {
- const doc = this.el_.ownerDocument;
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseDown(event);
- }
- this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
- this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
- this.on(doc, 'mouseup', this.handleMouseUpHandler_);
- this.on(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mouseup` or `touchend` events on the `ProgressControl`.
- *
- * @param {Event} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- */
- handleMouseUp(event) {
- const seekBar = this.getChild('seekBar');
- if (seekBar) {
- seekBar.handleMouseUp(event);
- }
- this.removeListenersAddedOnMousedownAndTouchstart();
- }
- }
-
- /**
- * Default options for `ProgressControl`
- *
- * @type {Object}
- * @private
- */
- ProgressControl.prototype.options_ = {
- children: ['seekBar']
- };
- Component$1.registerComponent('ProgressControl', ProgressControl);
-
- /**
- * @file picture-in-picture-toggle.js
- */
-
- /**
- * Toggle Picture-in-Picture mode
- *
- * @extends Button
- */
- class PictureInPictureToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @listens Player#enterpictureinpicture
- * @listens Player#leavepictureinpicture
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('picture-in-picture-enter');
- this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
- this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
- this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange());
-
- // TODO: Deactivate button on player emptied event.
- this.disable();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`;
- }
-
- /**
- * Displays or hides the button depending on the audio mode detection.
- * Exits picture-in-picture if it is enabled when switching to audio mode.
- */
- handlePictureInPictureAudioModeChange() {
- // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
- const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio';
- const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode();
- if (!isAudioMode) {
- this.show();
- return;
- }
- if (this.player_.isInPictureInPicture()) {
- this.player_.exitPictureInPicture();
- }
- this.hide();
- }
-
- /**
- * Enables or disables button based on availability of a Picture-In-Picture mode.
- *
- * Enabled if
- * - `player.options().enableDocumentPictureInPicture` is true and
- * window.documentPictureInPicture is available; or
- * - `player.disablePictureInPicture()` is false and
- * element.requestPictureInPicture is available
- */
- handlePictureInPictureEnabledChange() {
- if (document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window) {
- this.enable();
- } else {
- this.disable();
- }
- }
-
- /**
- * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
- *
- * @param {Event} [event]
- * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
- * called.
- *
- * @listens Player#enterpictureinpicture
- * @listens Player#leavepictureinpicture
- */
- handlePictureInPictureChange(event) {
- if (this.player_.isInPictureInPicture()) {
- this.setIcon('picture-in-picture-exit');
- this.controlText('Exit Picture-in-Picture');
- } else {
- this.setIcon('picture-in-picture-enter');
- this.controlText('Picture-in-Picture');
- }
- this.handlePictureInPictureEnabledChange();
- }
-
- /**
- * This gets called when an `PictureInPictureToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (!this.player_.isInPictureInPicture()) {
- this.player_.requestPictureInPicture();
- } else {
- this.player_.exitPictureInPicture();
- }
- }
-
- /**
- * Show the `Component`s element if it is hidden by removing the
- * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API.
- */
- show() {
- // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox.
- if (typeof document.exitPictureInPicture !== 'function') {
- return;
- }
- super.show();
- }
- }
-
- /**
- * The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
- Component$1.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
-
- /**
- * @file fullscreen-toggle.js
- */
-
- /**
- * Toggle fullscreen video
- *
- * @extends Button
- */
- class FullscreenToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.setIcon('fullscreen-enter');
- this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
- if (document[player.fsApi_.fullscreenEnabled] === false) {
- this.disable();
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-fullscreen-control ${super.buildCSSClass()}`;
- }
-
- /**
- * Handles fullscreenchange on the player and change control text accordingly.
- *
- * @param {Event} [event]
- * The {@link Player#fullscreenchange} event that caused this function to be
- * called.
- *
- * @listens Player#fullscreenchange
- */
- handleFullscreenChange(event) {
- if (this.player_.isFullscreen()) {
- this.controlText('Exit Fullscreen');
- this.setIcon('fullscreen-exit');
- } else {
- this.controlText('Fullscreen');
- this.setIcon('fullscreen-enter');
- }
- }
-
- /**
- * This gets called when an `FullscreenToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (!this.player_.isFullscreen()) {
- this.player_.requestFullscreen();
- } else {
- this.player_.exitFullscreen();
- }
- }
- }
-
- /**
- * The text that should display over the `FullscreenToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- FullscreenToggle.prototype.controlText_ = 'Fullscreen';
- Component$1.registerComponent('FullscreenToggle', FullscreenToggle);
-
- /**
- * Check if volume control is supported and if it isn't hide the
- * `Component` that was passed using the `vjs-hidden` class.
- *
- * @param { import('../../component').default } self
- * The component that should be hidden if volume is unsupported
- *
- * @param { import('../../player').default } player
- * A reference to the player
- *
- * @private
- */
- const checkVolumeSupport = function (self, player) {
- // hide volume controls when they're not supported by the current tech
- if (player.tech_ && !player.tech_.featuresVolumeControl) {
- self.addClass('vjs-hidden');
- }
- self.on(player, 'loadstart', function () {
- if (!player.tech_.featuresVolumeControl) {
- self.addClass('vjs-hidden');
- } else {
- self.removeClass('vjs-hidden');
- }
- });
- };
-
- /**
- * @file volume-level.js
- */
-
- /**
- * Shows volume level
- *
- * @extends Component
- */
- class VolumeLevel extends Component$1 {
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl('div', {
- className: 'vjs-volume-level'
- });
- this.setIcon('circle', el);
- el.appendChild(super.createEl('span', {
- className: 'vjs-control-text'
- }));
- return el;
- }
- }
- Component$1.registerComponent('VolumeLevel', VolumeLevel);
-
- /**
- * @file volume-level-tooltip.js
- */
-
- /**
- * Volume level tooltips display a volume above or side by side the volume bar.
- *
- * @extends Component
- */
- class VolumeLevelTooltip extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the volume tooltip DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-volume-tooltip'
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Updates the position of the tooltip relative to the `VolumeBar` and
- * its content text.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- */
- update(rangeBarRect, rangeBarPoint, vertical, content) {
- if (!vertical) {
- const tooltipRect = getBoundingClientRect(this.el_);
- const playerRect = getBoundingClientRect(this.player_.el());
- const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
- if (!playerRect || !tooltipRect) {
- return;
- }
- const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
- const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
- let pullTooltipBy = tooltipRect.width / 2;
- if (spaceLeftOfPoint < pullTooltipBy) {
- pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
- } else if (spaceRightOfPoint < pullTooltipBy) {
- pullTooltipBy = spaceRightOfPoint;
- }
- if (pullTooltipBy < 0) {
- pullTooltipBy = 0;
- } else if (pullTooltipBy > tooltipRect.width) {
- pullTooltipBy = tooltipRect.width;
- }
- this.el_.style.right = `-${pullTooltipBy}px`;
- }
- this.write(`${content}%`);
- }
-
- /**
- * Write the volume to the tooltip DOM element.
- *
- * @param {string} content
- * The formatted volume for the tooltip.
- */
- write(content) {
- textContent(this.el_, content);
- }
-
- /**
- * Updates the position of the volume tooltip relative to the `VolumeBar`.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- * @param {number} volume
- * The volume level to update the tooltip to
- *
- * @param {Function} cb
- * A function that will be called during the request animation frame
- * for tooltips that need to do additional animations from the default
- */
- updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
- this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
- this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
- if (cb) {
- cb();
- }
- });
- }
- }
- Component$1.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
-
- /**
- * @file mouse-volume-level-display.js
- */
-
- /**
- * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
- * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
- * indicating the volume level which is represented by a given point in the
- * {@link VolumeBar}.
- *
- * @extends Component
- */
- class MouseVolumeLevelDisplay extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The {@link Player} that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
- }
-
- /**
- * Create the DOM element for this class.
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-mouse-display'
- });
- }
-
- /**
- * Enquires updates to its own DOM as well as the DOM of its
- * {@link VolumeLevelTooltip} child.
- *
- * @param {Object} rangeBarRect
- * The `ClientRect` for the {@link VolumeBar} element.
- *
- * @param {number} rangeBarPoint
- * A number from 0 to 1, representing a horizontal/vertical reference point
- * from the left edge of the {@link VolumeBar}
- *
- * @param {boolean} vertical
- * Referees to the Volume control position
- * in the control bar{@link VolumeControl}
- *
- */
- update(rangeBarRect, rangeBarPoint, vertical) {
- const volume = 100 * rangeBarPoint;
- this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
- if (vertical) {
- this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
- } else {
- this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
- }
- });
- }
- }
-
- /**
- * Default options for `MouseVolumeLevelDisplay`
- *
- * @type {Object}
- * @private
- */
- MouseVolumeLevelDisplay.prototype.options_ = {
- children: ['volumeLevelTooltip']
- };
- Component$1.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
-
- /**
- * @file volume-bar.js
- */
-
- /**
- * The bar that contains the volume level and can be clicked on to adjust the level
- *
- * @extends Slider
- */
- class VolumeBar extends Slider {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on('slideractive', e => this.updateLastVolume_(e));
- this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
- player.ready(() => this.updateARIAAttributes());
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-volume-bar vjs-slider-bar'
- }, {
- 'aria-label': this.localize('Volume Level'),
- 'aria-live': 'polite'
- });
- }
-
- /**
- * Handle mouse down on volume bar
- *
- * @param {Event} event
- * The `mousedown` event that caused this to run.
- *
- * @listens mousedown
- */
- handleMouseDown(event) {
- if (!isSingleLeftClick(event)) {
- return;
- }
- super.handleMouseDown(event);
- }
-
- /**
- * Handle movement events on the {@link VolumeMenuButton}.
- *
- * @param {Event} event
- * The event that caused this function to run.
- *
- * @listens mousemove
- */
- handleMouseMove(event) {
- const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
- if (mouseVolumeLevelDisplay) {
- const volumeBarEl = this.el();
- const volumeBarRect = getBoundingClientRect(volumeBarEl);
- const vertical = this.vertical();
- let volumeBarPoint = getPointerPosition(volumeBarEl, event);
- volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
- // The default skin has a gap on either side of the `VolumeBar`. This means
- // that it's possible to trigger this behavior outside the boundaries of
- // the `VolumeBar`. This ensures we stay within it at all times.
- volumeBarPoint = clamp(volumeBarPoint, 0, 1);
- mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
- }
- if (!isSingleLeftClick(event)) {
- return;
- }
- this.checkMuted();
- this.player_.volume(this.calculateDistance(event));
- }
-
- /**
- * If the player is muted unmute it.
- */
- checkMuted() {
- if (this.player_.muted()) {
- this.player_.muted(false);
- }
- }
-
- /**
- * Get percent of volume level
- *
- * @return {number}
- * Volume level percent as a decimal number.
- */
- getPercent() {
- if (this.player_.muted()) {
- return 0;
- }
- return this.player_.volume();
- }
-
- /**
- * Increase volume level for keyboard users
- */
- stepForward() {
- this.checkMuted();
- this.player_.volume(this.player_.volume() + 0.1);
- }
-
- /**
- * Decrease volume level for keyboard users
- */
- stepBack() {
- this.checkMuted();
- this.player_.volume(this.player_.volume() - 0.1);
- }
-
- /**
- * Update ARIA accessibility attributes
- *
- * @param {Event} [event]
- * The `volumechange` event that caused this function to run.
- *
- * @listens Player#volumechange
- */
- updateARIAAttributes(event) {
- const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
- this.el_.setAttribute('aria-valuenow', ariaValue);
- this.el_.setAttribute('aria-valuetext', ariaValue + '%');
- }
-
- /**
- * Returns the current value of the player volume as a percentage
- *
- * @private
- */
- volumeAsPercentage_() {
- return Math.round(this.player_.volume() * 100);
- }
-
- /**
- * When user starts dragging the VolumeBar, store the volume and listen for
- * the end of the drag. When the drag ends, if the volume was set to zero,
- * set lastVolume to the stored volume.
- *
- * @listens slideractive
- * @private
- */
- updateLastVolume_() {
- const volumeBeforeDrag = this.player_.volume();
- this.one('sliderinactive', () => {
- if (this.player_.volume() === 0) {
- this.player_.lastVolume_(volumeBeforeDrag);
- }
- });
- }
- }
-
- /**
- * Default options for the `VolumeBar`
- *
- * @type {Object}
- * @private
- */
- VolumeBar.prototype.options_ = {
- children: ['volumeLevel'],
- barName: 'volumeLevel'
- };
-
- // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
- if (!IS_IOS && !IS_ANDROID) {
- VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
- }
-
- /**
- * Call the update event for this Slider when this event happens on the player.
- *
- * @type {string}
- */
- VolumeBar.prototype.playerEvent = 'volumechange';
- Component$1.registerComponent('VolumeBar', VolumeBar);
-
- /**
- * @file volume-control.js
- */
-
- /**
- * The component for controlling the volume level
- *
- * @extends Component
- */
- class VolumeControl extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.vertical = options.vertical || false;
-
- // Pass the vertical option down to the VolumeBar if
- // the VolumeBar is turned on.
- if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
- options.volumeBar = options.volumeBar || {};
- options.volumeBar.vertical = options.vertical;
- }
- super(player, options);
-
- // hide this control if volume support is missing
- checkVolumeSupport(this, player);
- this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
- this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
- this.on('mousedown', e => this.handleMouseDown(e));
- this.on('touchstart', e => this.handleMouseDown(e));
- this.on('mousemove', e => this.handleMouseMove(e));
-
- // while the slider is active (the mouse has been pressed down and
- // is dragging) or in focus we do not want to hide the VolumeBar
- this.on(this.volumeBar, ['focus', 'slideractive'], () => {
- this.volumeBar.addClass('vjs-slider-active');
- this.addClass('vjs-slider-active');
- this.trigger('slideractive');
- });
- this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
- this.volumeBar.removeClass('vjs-slider-active');
- this.removeClass('vjs-slider-active');
- this.trigger('sliderinactive');
- });
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- let orientationClass = 'vjs-volume-horizontal';
- if (this.options_.vertical) {
- orientationClass = 'vjs-volume-vertical';
- }
- return super.createEl('div', {
- className: `vjs-volume-control vjs-control ${orientationClass}`
- });
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseDown(event) {
- const doc = this.el_.ownerDocument;
- this.on(doc, 'mousemove', this.throttledHandleMouseMove);
- this.on(doc, 'touchmove', this.throttledHandleMouseMove);
- this.on(doc, 'mouseup', this.handleMouseUpHandler_);
- this.on(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mouseup` or `touchend` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mouseup` or `touchend` event that triggered this function.
- *
- * @listens touchend
- * @listens mouseup
- */
- handleMouseUp(event) {
- const doc = this.el_.ownerDocument;
- this.off(doc, 'mousemove', this.throttledHandleMouseMove);
- this.off(doc, 'touchmove', this.throttledHandleMouseMove);
- this.off(doc, 'mouseup', this.handleMouseUpHandler_);
- this.off(doc, 'touchend', this.handleMouseUpHandler_);
- }
-
- /**
- * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
- *
- * @param {Event} event
- * `mousedown` or `touchstart` event that triggered this function
- *
- * @listens mousedown
- * @listens touchstart
- */
- handleMouseMove(event) {
- this.volumeBar.handleMouseMove(event);
- }
- }
-
- /**
- * Default options for the `VolumeControl`
- *
- * @type {Object}
- * @private
- */
- VolumeControl.prototype.options_ = {
- children: ['volumeBar']
- };
- Component$1.registerComponent('VolumeControl', VolumeControl);
-
- /**
- * Check if muting volume is supported and if it isn't hide the mute toggle
- * button.
- *
- * @param { import('../../component').default } self
- * A reference to the mute toggle button
- *
- * @param { import('../../player').default } player
- * A reference to the player
- *
- * @private
- */
- const checkMuteSupport = function (self, player) {
- // hide mute toggle button if it's not supported by the current tech
- if (player.tech_ && !player.tech_.featuresMuteControl) {
- self.addClass('vjs-hidden');
- }
- self.on(player, 'loadstart', function () {
- if (!player.tech_.featuresMuteControl) {
- self.addClass('vjs-hidden');
- } else {
- self.removeClass('vjs-hidden');
- }
- });
- };
-
- /**
- * @file mute-toggle.js
- */
-
- /**
- * A button component for muting the audio.
- *
- * @extends Button
- */
- class MuteToggle extends Button {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
-
- // hide this control if volume support is missing
- checkMuteSupport(this, player);
- this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-mute-control ${super.buildCSSClass()}`;
- }
-
- /**
- * This gets called when an `MuteToggle` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const vol = this.player_.volume();
- const lastVolume = this.player_.lastVolume_();
- if (vol === 0) {
- const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
- this.player_.volume(volumeToSet);
- this.player_.muted(false);
- } else {
- this.player_.muted(this.player_.muted() ? false : true);
- }
- }
-
- /**
- * Update the `MuteToggle` button based on the state of `volume` and `muted`
- * on the player.
- *
- * @param {Event} [event]
- * The {@link Player#loadstart} event if this function was called
- * through an event.
- *
- * @listens Player#loadstart
- * @listens Player#volumechange
- */
- update(event) {
- this.updateIcon_();
- this.updateControlText_();
- }
-
- /**
- * Update the appearance of the `MuteToggle` icon.
- *
- * Possible states (given `level` variable below):
- * - 0: crossed out
- * - 1: zero bars of volume
- * - 2: one bar of volume
- * - 3: two bars of volume
- *
- * @private
- */
- updateIcon_() {
- const vol = this.player_.volume();
- let level = 3;
- this.setIcon('volume-high');
-
- // in iOS when a player is loaded with muted attribute
- // and volume is changed with a native mute button
- // we want to make sure muted state is updated
- if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
- this.player_.muted(this.player_.tech_.el_.muted);
- }
- if (vol === 0 || this.player_.muted()) {
- this.setIcon('volume-mute');
- level = 0;
- } else if (vol < 0.33) {
- this.setIcon('volume-low');
- level = 1;
- } else if (vol < 0.67) {
- this.setIcon('volume-medium');
- level = 2;
- }
- removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
- addClass(this.el_, `vjs-vol-${level}`);
- }
-
- /**
- * If `muted` has changed on the player, update the control text
- * (`title` attribute on `vjs-mute-control` element and content of
- * `vjs-control-text` element).
- *
- * @private
- */
- updateControlText_() {
- const soundOff = this.player_.muted() || this.player_.volume() === 0;
- const text = soundOff ? 'Unmute' : 'Mute';
- if (this.controlText() !== text) {
- this.controlText(text);
- }
- }
- }
-
- /**
- * The text that should display over the `MuteToggle`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- MuteToggle.prototype.controlText_ = 'Mute';
- Component$1.registerComponent('MuteToggle', MuteToggle);
-
- /**
- * @file volume-control.js
- */
-
- /**
- * A Component to contain the MuteToggle and VolumeControl so that
- * they can work together.
- *
- * @extends Component
- */
- class VolumePanel extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- if (typeof options.inline !== 'undefined') {
- options.inline = options.inline;
- } else {
- options.inline = true;
- }
-
- // pass the inline option down to the VolumeControl as vertical if
- // the VolumeControl is on.
- if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
- options.volumeControl = options.volumeControl || {};
- options.volumeControl.vertical = !options.inline;
- }
- super(player, options);
-
- // this handler is used by mouse handler methods below
- this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
- this.on(player, ['loadstart'], e => this.volumePanelState_(e));
- this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
- this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
- this.on('keydown', e => this.handleKeyPress(e));
- this.on('mouseover', e => this.handleMouseOver(e));
- this.on('mouseout', e => this.handleMouseOut(e));
-
- // while the slider is active (the mouse has been pressed down and
- // is dragging) we do not want to hide the VolumeBar
- this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
- this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
- }
-
- /**
- * Add vjs-slider-active class to the VolumePanel
- *
- * @listens VolumeControl#slideractive
- * @private
- */
- sliderActive_() {
- this.addClass('vjs-slider-active');
- }
-
- /**
- * Removes vjs-slider-active class to the VolumePanel
- *
- * @listens VolumeControl#sliderinactive
- * @private
- */
- sliderInactive_() {
- this.removeClass('vjs-slider-active');
- }
-
- /**
- * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
- * depending on MuteToggle and VolumeControl state
- *
- * @listens Player#loadstart
- * @private
- */
- volumePanelState_() {
- // hide volume panel if neither volume control or mute toggle
- // are displayed
- if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
- this.addClass('vjs-hidden');
- }
-
- // if only mute toggle is visible we don't want
- // volume panel expanding when hovered or active
- if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
- this.addClass('vjs-mute-toggle-only');
- }
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- let orientationClass = 'vjs-volume-panel-horizontal';
- if (!this.options_.inline) {
- orientationClass = 'vjs-volume-panel-vertical';
- }
- return super.createEl('div', {
- className: `vjs-volume-panel vjs-control ${orientationClass}`
- });
- }
-
- /**
- * Dispose of the `volume-panel` and all child components.
- */
- dispose() {
- this.handleMouseOut();
- super.dispose();
- }
-
- /**
- * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
- * the volume panel and sets focus on `MuteToggle`.
- *
- * @param {Event} event
- * The `keyup` event that caused this function to be called.
- *
- * @listens keyup
- */
- handleVolumeControlKeyUp(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- this.muteToggle.focus();
- }
- }
-
- /**
- * This gets called when a `VolumePanel` gains hover via a `mouseover` event.
- * Turns on listening for `mouseover` event. When they happen it
- * calls `this.handleMouseOver`.
- *
- * @param {Event} event
- * The `mouseover` event that caused this function to be called.
- *
- * @listens mouseover
- */
- handleMouseOver(event) {
- this.addClass('vjs-hover');
- on(document, 'keyup', this.handleKeyPressHandler_);
- }
-
- /**
- * This gets called when a `VolumePanel` gains hover via a `mouseout` event.
- * Turns on listening for `mouseout` event. When they happen it
- * calls `this.handleMouseOut`.
- *
- * @param {Event} event
- * The `mouseout` event that caused this function to be called.
- *
- * @listens mouseout
- */
- handleMouseOut(event) {
- this.removeClass('vjs-hover');
- off(document, 'keyup', this.handleKeyPressHandler_);
- }
-
- /**
- * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
- * looking for ESC, which hides the `VolumeControl`.
- *
- * @param {Event} event
- * The keypress that triggered this event.
- *
- * @listens keydown | keyup
- */
- handleKeyPress(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- this.handleMouseOut();
- }
- }
- }
-
- /**
- * Default options for the `VolumeControl`
- *
- * @type {Object}
- * @private
- */
- VolumePanel.prototype.options_ = {
- children: ['muteToggle', 'volumeControl']
- };
- Component$1.registerComponent('VolumePanel', VolumePanel);
-
- /**
- * Button to skip forward a configurable amount of time
- * through a video. Renders in the control bar.
- *
- * e.g. options: {controlBar: {skipButtons: forward: 5}}
- *
- * @extends Button
- */
- class SkipForward extends Button {
- constructor(player, options) {
- super(player, options);
- this.validOptions = [5, 10, 30];
- this.skipTime = this.getSkipForwardTime();
- if (this.skipTime && this.validOptions.includes(this.skipTime)) {
- this.setIcon(`forward-${this.skipTime}`);
- this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
- this.show();
- } else {
- this.hide();
- }
- }
- getSkipForwardTime() {
- const playerOptions = this.options_.playerOptions;
- return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
- }
- buildCSSClass() {
- return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
- }
-
- /**
- * On click, skips forward in the duration/seekable range by a configurable amount of seconds.
- * If the time left in the duration/seekable range is less than the configured 'skip forward' time,
- * skips to end of duration/seekable range.
- *
- * Handle a click on a `SkipForward` button
- *
- * @param {EventTarget~Event} event
- * The `click` event that caused this function
- * to be called
- */
- handleClick(event) {
- if (isNaN(this.player_.duration())) {
- return;
- }
- const currentVideoTime = this.player_.currentTime();
- const liveTracker = this.player_.liveTracker;
- const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
- let newTime;
- if (currentVideoTime + this.skipTime <= duration) {
- newTime = currentVideoTime + this.skipTime;
- } else {
- newTime = duration;
- }
- this.player_.currentTime(newTime);
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
- }
- }
- SkipForward.prototype.controlText_ = 'Skip Forward';
- Component$1.registerComponent('SkipForward', SkipForward);
-
- /**
- * Button to skip backward a configurable amount of time
- * through a video. Renders in the control bar.
- *
- * * e.g. options: {controlBar: {skipButtons: backward: 5}}
- *
- * @extends Button
- */
- class SkipBackward extends Button {
- constructor(player, options) {
- super(player, options);
- this.validOptions = [5, 10, 30];
- this.skipTime = this.getSkipBackwardTime();
- if (this.skipTime && this.validOptions.includes(this.skipTime)) {
- this.setIcon(`replay-${this.skipTime}`);
- this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
- this.show();
- } else {
- this.hide();
- }
- }
- getSkipBackwardTime() {
- const playerOptions = this.options_.playerOptions;
- return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
- }
- buildCSSClass() {
- return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
- }
-
- /**
- * On click, skips backward in the video by a configurable amount of seconds.
- * If the current time in the video is less than the configured 'skip backward' time,
- * skips to beginning of video or seekable range.
- *
- * Handle a click on a `SkipBackward` button
- *
- * @param {EventTarget~Event} event
- * The `click` event that caused this function
- * to be called
- */
- handleClick(event) {
- const currentVideoTime = this.player_.currentTime();
- const liveTracker = this.player_.liveTracker;
- const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
- let newTime;
- if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
- newTime = seekableStart;
- } else if (currentVideoTime >= this.skipTime) {
- newTime = currentVideoTime - this.skipTime;
- } else {
- newTime = 0;
- }
- this.player_.currentTime(newTime);
- }
-
- /**
- * Update control text on languagechange
- */
- handleLanguagechange() {
- this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
- }
- }
- SkipBackward.prototype.controlText_ = 'Skip Backward';
- Component$1.registerComponent('SkipBackward', SkipBackward);
-
- /**
- * @file menu.js
- */
-
- /**
- * The Menu component is used to build popup menus, including subtitle and
- * captions selection menus.
- *
- * @extends Component
- */
- class Menu extends Component$1 {
- /**
- * Create an instance of this class.
- *
- * @param { import('../player').default } player
- * the player that this component should attach to
- *
- * @param {Object} [options]
- * Object of option names and values
- *
- */
- constructor(player, options) {
- super(player, options);
- if (options) {
- this.menuButton_ = options.menuButton;
- }
- this.focusedChild_ = -1;
- this.on('keydown', e => this.handleKeyDown(e));
-
- // All the menu item instances share the same blur handler provided by the menu container.
- this.boundHandleBlur_ = e => this.handleBlur(e);
- this.boundHandleTapClick_ = e => this.handleTapClick(e);
- }
-
- /**
- * Add event listeners to the {@link MenuItem}.
- *
- * @param {Object} component
- * The instance of the `MenuItem` to add listeners to.
- *
- */
- addEventListenerForItem(component) {
- if (!(component instanceof Component$1)) {
- return;
- }
- this.on(component, 'blur', this.boundHandleBlur_);
- this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
- }
-
- /**
- * Remove event listeners from the {@link MenuItem}.
- *
- * @param {Object} component
- * The instance of the `MenuItem` to remove listeners.
- *
- */
- removeEventListenerForItem(component) {
- if (!(component instanceof Component$1)) {
- return;
- }
- this.off(component, 'blur', this.boundHandleBlur_);
- this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
- }
-
- /**
- * This method will be called indirectly when the component has been added
- * before the component adds to the new menu instance by `addItem`.
- * In this case, the original menu instance will remove the component
- * by calling `removeChild`.
- *
- * @param {Object} component
- * The instance of the `MenuItem`
- */
- removeChild(component) {
- if (typeof component === 'string') {
- component = this.getChild(component);
- }
- this.removeEventListenerForItem(component);
- super.removeChild(component);
- }
-
- /**
- * Add a {@link MenuItem} to the menu.
- *
- * @param {Object|string} component
- * The name or instance of the `MenuItem` to add.
- *
- */
- addItem(component) {
- const childComponent = this.addChild(component);
- if (childComponent) {
- this.addEventListenerForItem(childComponent);
- }
- }
-
- /**
- * Create the `Menu`s DOM element.
- *
- * @return {Element}
- * the element that was created
- */
- createEl() {
- const contentElType = this.options_.contentElType || 'ul';
- this.contentEl_ = createEl(contentElType, {
- className: 'vjs-menu-content'
- });
- this.contentEl_.setAttribute('role', 'menu');
- const el = super.createEl('div', {
- append: this.contentEl_,
- className: 'vjs-menu'
- });
- el.appendChild(this.contentEl_);
-
- // Prevent clicks from bubbling up. Needed for Menu Buttons,
- // where a click on the parent is significant
- on(el, 'click', function (event) {
- event.preventDefault();
- event.stopImmediatePropagation();
- });
- return el;
- }
- dispose() {
- this.contentEl_ = null;
- this.boundHandleBlur_ = null;
- this.boundHandleTapClick_ = null;
- super.dispose();
- }
-
- /**
- * Called when a `MenuItem` loses focus.
- *
- * @param {Event} event
- * The `blur` event that caused this function to be called.
- *
- * @listens blur
- */
- handleBlur(event) {
- const relatedTarget = event.relatedTarget || document.activeElement;
-
- // Close menu popup when a user clicks outside the menu
- if (!this.children().some(element => {
- return element.el() === relatedTarget;
- })) {
- const btn = this.menuButton_;
- if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
- btn.unpressButton();
- }
- }
- }
-
- /**
- * Called when a `MenuItem` gets clicked or tapped.
- *
- * @param {Event} event
- * The `click` or `tap` event that caused this function to be called.
- *
- * @listens click,tap
- */
- handleTapClick(event) {
- // Unpress the associated MenuButton, and move focus back to it
- if (this.menuButton_) {
- this.menuButton_.unpressButton();
- const childComponents = this.children();
- if (!Array.isArray(childComponents)) {
- return;
- }
- const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
- if (!foundComponent) {
- return;
- }
-
- // don't focus menu button if item is a caption settings item
- // because focus will move elsewhere
- if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
- this.menuButton_.focus();
- }
- }
- }
-
- /**
- * Handle a `keydown` event on this menu. This listener is added in the constructor.
- *
- * @param {KeyboardEvent} event
- * A `keydown` event that happened on the menu.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Left and Down Arrows
- if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepForward();
-
- // Up and Right Arrows
- } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
- event.preventDefault();
- event.stopPropagation();
- this.stepBack();
- }
- }
-
- /**
- * Move to next (lower) menu item for keyboard users.
- */
- stepForward() {
- let stepChild = 0;
- if (this.focusedChild_ !== undefined) {
- stepChild = this.focusedChild_ + 1;
- }
- this.focus(stepChild);
- }
-
- /**
- * Move to previous (higher) menu item for keyboard users.
- */
- stepBack() {
- let stepChild = 0;
- if (this.focusedChild_ !== undefined) {
- stepChild = this.focusedChild_ - 1;
- }
- this.focus(stepChild);
- }
-
- /**
- * Set focus on a {@link MenuItem} in the `Menu`.
- *
- * @param {Object|string} [item=0]
- * Index of child item set focus on.
- */
- focus(item = 0) {
- const children = this.children().slice();
- const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
- if (haveTitle) {
- children.shift();
- }
- if (children.length > 0) {
- if (item < 0) {
- item = 0;
- } else if (item >= children.length) {
- item = children.length - 1;
- }
- this.focusedChild_ = item;
- children[item].el_.focus();
- }
- }
- }
- Component$1.registerComponent('Menu', Menu);
-
- /**
- * @file menu-button.js
- */
-
- /**
- * A `MenuButton` class for any popup {@link Menu}.
- *
- * @extends Component
- */
- class MenuButton extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- super(player, options);
- this.menuButton_ = new Button(player, options);
- this.menuButton_.controlText(this.controlText_);
- this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
-
- // Add buildCSSClass values to the button, not the wrapper
- const buttonClass = Button.prototype.buildCSSClass();
- this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
- this.menuButton_.removeClass('vjs-control');
- this.addChild(this.menuButton_);
- this.update();
- this.enabled_ = true;
- const handleClick = e => this.handleClick(e);
- this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
- this.on(this.menuButton_, 'tap', handleClick);
- this.on(this.menuButton_, 'click', handleClick);
- this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
- this.on(this.menuButton_, 'mouseenter', () => {
- this.addClass('vjs-hover');
- this.menu.show();
- on(document, 'keyup', this.handleMenuKeyUp_);
- });
- this.on('mouseleave', e => this.handleMouseLeave(e));
- this.on('keydown', e => this.handleSubmenuKeyDown(e));
- }
-
- /**
- * Update the menu based on the current state of its items.
- */
- update() {
- const menu = this.createMenu();
- if (this.menu) {
- this.menu.dispose();
- this.removeChild(this.menu);
- }
- this.menu = menu;
- this.addChild(menu);
-
- /**
- * Track the state of the menu button
- *
- * @type {Boolean}
- * @private
- */
- this.buttonPressed_ = false;
- this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- if (this.items && this.items.length <= this.hideThreshold_) {
- this.hide();
- this.menu.contentEl_.removeAttribute('role');
- } else {
- this.show();
- this.menu.contentEl_.setAttribute('role', 'menu');
- }
- }
-
- /**
- * Create the menu and add all items to it.
- *
- * @return {Menu}
- * The constructed menu
- */
- createMenu() {
- const menu = new Menu(this.player_, {
- menuButton: this
- });
-
- /**
- * Hide the menu if the number of items is less than or equal to this threshold. This defaults
- * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
- * it here because every time we run `createMenu` we need to reset the value.
- *
- * @protected
- * @type {Number}
- */
- this.hideThreshold_ = 0;
-
- // Add a title list item to the top
- if (this.options_.title) {
- const titleEl = createEl('li', {
- className: 'vjs-menu-title',
- textContent: toTitleCase$1(this.options_.title),
- tabIndex: -1
- });
- const titleComponent = new Component$1(this.player_, {
- el: titleEl
- });
- menu.addItem(titleComponent);
- }
- this.items = this.createItems();
- if (this.items) {
- // Add menu items to the menu
- for (let i = 0; i < this.items.length; i++) {
- menu.addItem(this.items[i]);
- }
- }
- return menu;
- }
-
- /**
- * Create the list of menu items. Specific to each subclass.
- *
- * @abstract
- */
- createItems() {}
-
- /**
- * Create the `MenuButtons`s DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildWrapperCSSClass()
- }, {});
- }
-
- /**
- * Overwrites the `setIcon` method from `Component`.
- * In this case, we want the icon to be appended to the menuButton.
- *
- * @param {string} name
- * The icon name to be added.
- */
- setIcon(name) {
- super.setIcon(name, this.menuButton_.el_);
- }
-
- /**
- * Allow sub components to stack CSS class names for the wrapper element
- *
- * @return {string}
- * The constructed wrapper DOM `className`
- */
- buildWrapperCSSClass() {
- let menuButtonClass = 'vjs-menu-button';
-
- // If the inline option is passed, we want to use different styles altogether.
- if (this.options_.inline === true) {
- menuButtonClass += '-inline';
- } else {
- menuButtonClass += '-popup';
- }
-
- // TODO: Fix the CSS so that this isn't necessary
- const buttonClass = Button.prototype.buildCSSClass();
- return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- let menuButtonClass = 'vjs-menu-button';
-
- // If the inline option is passed, we want to use different styles altogether.
- if (this.options_.inline === true) {
- menuButtonClass += '-inline';
- } else {
- menuButtonClass += '-popup';
- }
- return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
- }
-
- /**
- * Get or set the localized control text that will be used for accessibility.
- *
- * > NOTE: This will come from the internal `menuButton_` element.
- *
- * @param {string} [text]
- * Control text for element.
- *
- * @param {Element} [el=this.menuButton_.el()]
- * Element to set the title on.
- *
- * @return {string}
- * - The control text when getting
- */
- controlText(text, el = this.menuButton_.el()) {
- return this.menuButton_.controlText(text, el);
- }
-
- /**
- * Dispose of the `menu-button` and all child components.
- */
- dispose() {
- this.handleMouseLeave();
- super.dispose();
- }
-
- /**
- * Handle a click on a `MenuButton`.
- * See {@link ClickableComponent#handleClick} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- if (this.buttonPressed_) {
- this.unpressButton();
- } else {
- this.pressButton();
- }
- }
-
- /**
- * Handle `mouseleave` for `MenuButton`.
- *
- * @param {Event} event
- * The `mouseleave` event that caused this function to be called.
- *
- * @listens mouseleave
- */
- handleMouseLeave(event) {
- this.removeClass('vjs-hover');
- off(document, 'keyup', this.handleMenuKeyUp_);
- }
-
- /**
- * Set the focus to the actual button, not to this element
- */
- focus() {
- this.menuButton_.focus();
- }
-
- /**
- * Remove the focus from the actual button, not this element
- */
- blur() {
- this.menuButton_.blur();
- }
-
- /**
- * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
- * {@link ClickableComponent#handleKeyDown} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- // Escape or Tab unpress the 'button'
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- if (this.buttonPressed_) {
- this.unpressButton();
- }
-
- // Don't preventDefault for Tab key - we still want to lose focus
- if (!keycode.isEventKey(event, 'Tab')) {
- event.preventDefault();
- // Set focus back to the menu button's button
- this.menuButton_.focus();
- }
- // Up Arrow or Down Arrow also 'press' the button to open the menu
- } else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) {
- if (!this.buttonPressed_) {
- event.preventDefault();
- this.pressButton();
- }
- }
- }
-
- /**
- * Handle a `keyup` event on a `MenuButton`. The listener for this is added in
- * the constructor.
- *
- * @param {Event} event
- * Key press event
- *
- * @listens keyup
- */
- handleMenuKeyUp(event) {
- // Escape hides popup menu
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- this.removeClass('vjs-hover');
- }
- }
-
- /**
- * This method name now delegates to `handleSubmenuKeyDown`. This means
- * anyone calling `handleSubmenuKeyPress` will not see their method calls
- * stop working.
- *
- * @param {Event} event
- * The event that caused this function to be called.
- */
- handleSubmenuKeyPress(event) {
- this.handleSubmenuKeyDown(event);
- }
-
- /**
- * Handle a `keydown` event on a sub-menu. The listener for this is added in
- * the constructor.
- *
- * @param {Event} event
- * Key press event
- *
- * @listens keydown
- */
- handleSubmenuKeyDown(event) {
- // Escape or Tab unpress the 'button'
- if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) {
- if (this.buttonPressed_) {
- this.unpressButton();
- }
- // Don't preventDefault for Tab key - we still want to lose focus
- if (!keycode.isEventKey(event, 'Tab')) {
- event.preventDefault();
- // Set focus back to the menu button's button
- this.menuButton_.focus();
- }
- }
- }
-
- /**
- * Put the current `MenuButton` into a pressed state.
- */
- pressButton() {
- if (this.enabled_) {
- this.buttonPressed_ = true;
- this.menu.show();
- this.menu.lockShowing();
- this.menuButton_.el_.setAttribute('aria-expanded', 'true');
-
- // set the focus into the submenu, except on iOS where it is resulting in
- // undesired scrolling behavior when the player is in an iframe
- if (IS_IOS && isInFrame()) {
- // Return early so that the menu isn't focused
- return;
- }
- this.menu.focus();
- }
- }
-
- /**
- * Take the current `MenuButton` out of a pressed state.
- */
- unpressButton() {
- if (this.enabled_) {
- this.buttonPressed_ = false;
- this.menu.unlockShowing();
- this.menu.hide();
- this.menuButton_.el_.setAttribute('aria-expanded', 'false');
- }
- }
-
- /**
- * Disable the `MenuButton`. Don't allow it to be clicked.
- */
- disable() {
- this.unpressButton();
- this.enabled_ = false;
- this.addClass('vjs-disabled');
- this.menuButton_.disable();
- }
-
- /**
- * Enable the `MenuButton`. Allow it to be clicked.
- */
- enable() {
- this.enabled_ = true;
- this.removeClass('vjs-disabled');
- this.menuButton_.enable();
- }
- }
- Component$1.registerComponent('MenuButton', MenuButton);
-
- /**
- * @file track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific track types (e.g. subtitles).
- *
- * @extends MenuButton
- */
- class TrackButton extends MenuButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const tracks = options.tracks;
- super(player, options);
- if (this.items.length <= 1) {
- this.hide();
- }
- if (!tracks) {
- return;
- }
- const updateHandler = bind_(this, this.update);
- tracks.addEventListener('removetrack', updateHandler);
- tracks.addEventListener('addtrack', updateHandler);
- tracks.addEventListener('labelchange', updateHandler);
- this.player_.on('ready', updateHandler);
- this.player_.on('dispose', function () {
- tracks.removeEventListener('removetrack', updateHandler);
- tracks.removeEventListener('addtrack', updateHandler);
- tracks.removeEventListener('labelchange', updateHandler);
- });
- }
- }
- Component$1.registerComponent('TrackButton', TrackButton);
-
- /**
- * @file menu-keys.js
- */
-
- /**
- * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`)
- * Note that 'Enter' and 'Space' are not included here (otherwise they would
- * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable)
- *
- * @typedef MenuKeys
- * @array
- */
- const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left'];
-
- /**
- * @file menu-item.js
- */
-
- /**
- * The component for a menu item. ``
- *
- * @extends ClickableComponent
- */
- class MenuItem extends ClickableComponent {
- /**
- * Creates an instance of the this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- *
- */
- constructor(player, options) {
- super(player, options);
- this.selectable = options.selectable;
- this.isSelected_ = options.selected || false;
- this.multiSelectable = options.multiSelectable;
- this.selected(this.isSelected_);
- if (this.selectable) {
- if (this.multiSelectable) {
- this.el_.setAttribute('role', 'menuitemcheckbox');
- } else {
- this.el_.setAttribute('role', 'menuitemradio');
- }
- } else {
- this.el_.setAttribute('role', 'menuitem');
- }
- }
-
- /**
- * Create the `MenuItem's DOM element
- *
- * @param {string} [type=li]
- * Element's node type, not actually used, always set to `li`.
- *
- * @param {Object} [props={}]
- * An object of properties that should be set on the element
- *
- * @param {Object} [attrs={}]
- * An object of attributes that should be set on the element
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl(type, props, attrs) {
- // The control is textual, not just an icon
- this.nonIconControl = true;
- const el = super.createEl('li', Object.assign({
- className: 'vjs-menu-item',
- tabIndex: -1
- }, props), attrs);
-
- // swap icon with menu item text.
- const menuItemEl = createEl('span', {
- className: 'vjs-menu-item-text',
- textContent: this.localize(this.options_.label)
- });
-
- // If using SVG icons, the element with vjs-icon-placeholder will be added separately.
- if (this.player_.options_.experimentalSvgIcons) {
- el.appendChild(menuItemEl);
- } else {
- el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder'));
- }
- return el;
- }
-
- /**
- * Ignore keys which are used by the menu, but pass any other ones up. See
- * {@link ClickableComponent#handleKeyDown} for instances where this is called.
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- if (!MenuKeys.some(key => keycode.isEventKey(event, key))) {
- // Pass keydown handling up for unused keys
- super.handleKeyDown(event);
- }
- }
-
- /**
- * Any click on a `MenuItem` puts it into the selected state.
- * See {@link ClickableComponent#handleClick} for instances where this is called.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- this.selected(true);
- }
-
- /**
- * Set the state for this menu item as selected or not.
- *
- * @param {boolean} selected
- * if the menu item is selected or not
- */
- selected(selected) {
- if (this.selectable) {
- if (selected) {
- this.addClass('vjs-selected');
- this.el_.setAttribute('aria-checked', 'true');
- // aria-checked isn't fully supported by browsers/screen readers,
- // so indicate selected state to screen reader in the control text.
- this.controlText(', selected');
- this.isSelected_ = true;
- } else {
- this.removeClass('vjs-selected');
- this.el_.setAttribute('aria-checked', 'false');
- // Indicate un-selected state to screen reader
- this.controlText('');
- this.isSelected_ = false;
- }
- }
- }
- }
- Component$1.registerComponent('MenuItem', MenuItem);
-
- /**
- * @file text-track-menu-item.js
- */
-
- /**
- * The specific menu item type for selecting a language within a text track kind
- *
- * @extends MenuItem
- */
- class TextTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const tracks = player.textTracks();
-
- // Modify options for parent MenuItem class's init.
- options.label = track.label || track.language || 'Unknown';
- options.selected = track.mode === 'showing';
- super(player, options);
- this.track = track;
- // Determine the relevant kind(s) of tracks for this component and filter
- // out empty kinds.
- this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
- const changeHandler = (...args) => {
- this.handleTracksChange.apply(this, args);
- };
- const selectedLanguageChangeHandler = (...args) => {
- this.handleSelectedLanguageChange.apply(this, args);
- };
- player.on(['loadstart', 'texttrackchange'], changeHandler);
- tracks.addEventListener('change', changeHandler);
- tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
- this.on('dispose', function () {
- player.off(['loadstart', 'texttrackchange'], changeHandler);
- tracks.removeEventListener('change', changeHandler);
- tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
- });
-
- // iOS7 doesn't dispatch change events to TextTrackLists when an
- // associated track's mode changes. Without something like
- // Object.observe() (also not present on iOS7), it's not
- // possible to detect changes to the mode attribute and polyfill
- // the change event. As a poor substitute, we manually dispatch
- // change events whenever the controls modify the mode.
- if (tracks.onchange === undefined) {
- let event;
- this.on(['tap', 'click'], function () {
- if (typeof window.Event !== 'object') {
- // Android 2.3 throws an Illegal Constructor error for window.Event
- try {
- event = new window.Event('change');
- } catch (err) {
- // continue regardless of error
- }
- }
- if (!event) {
- event = document.createEvent('Event');
- event.initEvent('change', true, true);
- }
- tracks.dispatchEvent(event);
- });
- }
-
- // set the default state based on current tracks
- this.handleTracksChange();
- }
-
- /**
- * This gets called when an `TextTrackMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} event
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- const referenceTrack = this.track;
- const tracks = this.player_.textTracks();
- super.handleClick(event);
- if (!tracks) {
- return;
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // If the track from the text tracks list is not of the right kind,
- // skip it. We do not want to affect tracks of incompatible kind(s).
- if (this.kinds.indexOf(track.kind) === -1) {
- continue;
- }
-
- // If this text track is the component's track and it is not showing,
- // set it to showing.
- if (track === referenceTrack) {
- if (track.mode !== 'showing') {
- track.mode = 'showing';
- }
-
- // If this text track is not the component's track and it is not
- // disabled, set it to disabled.
- } else if (track.mode !== 'disabled') {
- track.mode = 'disabled';
- }
- }
- }
-
- /**
- * Handle text track list change
- *
- * @param {Event} event
- * The `change` event that caused this function to be called.
- *
- * @listens TextTrackList#change
- */
- handleTracksChange(event) {
- const shouldBeSelected = this.track.mode === 'showing';
-
- // Prevent redundant selected() calls because they may cause
- // screen readers to read the appended control text unnecessarily
- if (shouldBeSelected !== this.isSelected_) {
- this.selected(shouldBeSelected);
- }
- }
- handleSelectedLanguageChange(event) {
- if (this.track.mode === 'showing') {
- const selectedLanguage = this.player_.cache_.selectedLanguage;
-
- // Don't replace the kind of track across the same language
- if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
- return;
- }
- this.player_.cache_.selectedLanguage = {
- enabled: true,
- language: this.track.language,
- kind: this.track.kind
- };
- }
- }
- dispose() {
- // remove reference to track object on dispose
- this.track = null;
- super.dispose();
- }
- }
- Component$1.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
-
- /**
- * @file off-text-track-menu-item.js
- */
-
- /**
- * A special menu item for turning off a specific type of text track
- *
- * @extends TextTrackMenuItem
- */
- class OffTextTrackMenuItem extends TextTrackMenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- // Create pseudo track info
- // Requires options['kind']
- options.track = {
- player,
- // it is no longer necessary to store `kind` or `kinds` on the track itself
- // since they are now stored in the `kinds` property of all instances of
- // TextTrackMenuItem, but this will remain for backwards compatibility
- kind: options.kind,
- kinds: options.kinds,
- default: false,
- mode: 'disabled'
- };
- if (!options.kinds) {
- options.kinds = [options.kind];
- }
- if (options.label) {
- options.track.label = options.label;
- } else {
- options.track.label = options.kinds.join(' and ') + ' off';
- }
-
- // MenuItem is selectable
- options.selectable = true;
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- options.multiSelectable = false;
- super(player, options);
- }
-
- /**
- * Handle text track change
- *
- * @param {Event} event
- * The event that caused this function to run
- */
- handleTracksChange(event) {
- const tracks = this.player().textTracks();
- let shouldBeSelected = true;
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
- shouldBeSelected = false;
- break;
- }
- }
-
- // Prevent redundant selected() calls because they may cause
- // screen readers to read the appended control text unnecessarily
- if (shouldBeSelected !== this.isSelected_) {
- this.selected(shouldBeSelected);
- }
- }
- handleSelectedLanguageChange(event) {
- const tracks = this.player().textTracks();
- let allHidden = true;
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
- allHidden = false;
- break;
- }
- }
- if (allHidden) {
- this.player_.cache_.selectedLanguage = {
- enabled: false
- };
- }
- }
-
- /**
- * Update control text and label on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
- super.handleLanguagechange();
- }
- }
- Component$1.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
-
- /**
- * @file text-track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific text track types (e.g. subtitles)
- *
- * @extends MenuButton
- */
- class TextTrackButton extends TrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.tracks = player.textTracks();
- super(player, options);
- }
-
- /**
- * Create a menu item for each text track
- *
- * @param {TextTrackMenuItem[]} [items=[]]
- * Existing array of items to use during creation
- *
- * @return {TextTrackMenuItem[]}
- * Array of menu items that were created
- */
- createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
- // Label is an override for the [track] off label
- // USed to localise captions/subtitles
- let label;
- if (this.label_) {
- label = `${this.label_} off`;
- }
- // Add an OFF menu item to turn all tracks off
- items.push(new OffTextTrackMenuItem(this.player_, {
- kinds: this.kinds_,
- kind: this.kind_,
- label
- }));
- this.hideThreshold_ += 1;
- const tracks = this.player_.textTracks();
- if (!Array.isArray(this.kinds_)) {
- this.kinds_ = [this.kind_];
- }
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // only add tracks that are of an appropriate kind and have a label
- if (this.kinds_.indexOf(track.kind) > -1) {
- const item = new TrackMenuItem(this.player_, {
- track,
- kinds: this.kinds_,
- kind: this.kind_,
- // MenuItem is selectable
- selectable: true,
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- multiSelectable: false
- });
- item.addClass(`vjs-${track.kind}-menu-item`);
- items.push(item);
- }
- }
- return items;
- }
- }
- Component$1.registerComponent('TextTrackButton', TextTrackButton);
-
- /**
- * @file chapters-track-menu-item.js
- */
-
- /**
- * The chapter track menu item
- *
- * @extends MenuItem
- */
- class ChaptersTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const cue = options.cue;
- const currentTime = player.currentTime();
-
- // Modify options for parent MenuItem class's init.
- options.selectable = true;
- options.multiSelectable = false;
- options.label = cue.text;
- options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
- super(player, options);
- this.track = track;
- this.cue = cue;
- }
-
- /**
- * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick();
- this.player_.currentTime(this.cue.startTime);
- }
- }
- Component$1.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
-
- /**
- * @file chapters-button.js
- */
-
- /**
- * The button component for toggling and selecting chapters
- * Chapters act much differently than other text tracks
- * Cues are navigation vs. other tracks of alternative languages
- *
- * @extends TextTrackButton
- */
- class ChaptersButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this function is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('chapters');
- this.selectCurrentItem_ = () => {
- this.items.forEach(item => {
- item.selected(this.track_.activeCues[0] === item.cue);
- });
- };
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-chapters-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Update the menu based on the current state of its items.
- *
- * @param {Event} [event]
- * An event that triggered this function to run.
- *
- * @listens TextTrackList#addtrack
- * @listens TextTrackList#removetrack
- * @listens TextTrackList#change
- */
- update(event) {
- if (event && event.track && event.track.kind !== 'chapters') {
- return;
- }
- const track = this.findChaptersTrack();
- if (track !== this.track_) {
- this.setTrack(track);
- super.update();
- } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
- // Update the menu initially or if the number of cues has changed since set
- super.update();
- }
- }
-
- /**
- * Set the currently selected track for the chapters button.
- *
- * @param {TextTrack} track
- * The new track to select. Nothing will change if this is the currently selected
- * track.
- */
- setTrack(track) {
- if (this.track_ === track) {
- return;
- }
- if (!this.updateHandler_) {
- this.updateHandler_ = this.update.bind(this);
- }
-
- // here this.track_ refers to the old track instance
- if (this.track_) {
- const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
- if (remoteTextTrackEl) {
- remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
- }
- this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
- this.track_ = null;
- }
- this.track_ = track;
-
- // here this.track_ refers to the new track instance
- if (this.track_) {
- this.track_.mode = 'hidden';
- const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
- if (remoteTextTrackEl) {
- remoteTextTrackEl.addEventListener('load', this.updateHandler_);
- }
- this.track_.addEventListener('cuechange', this.selectCurrentItem_);
- }
- }
-
- /**
- * Find the track object that is currently in use by this ChaptersButton
- *
- * @return {TextTrack|undefined}
- * The current track or undefined if none was found.
- */
- findChaptersTrack() {
- const tracks = this.player_.textTracks() || [];
- for (let i = tracks.length - 1; i >= 0; i--) {
- // We will always choose the last track as our chaptersTrack
- const track = tracks[i];
- if (track.kind === this.kind_) {
- return track;
- }
- }
- }
-
- /**
- * Get the caption for the ChaptersButton based on the track label. This will also
- * use the current tracks localized kind as a fallback if a label does not exist.
- *
- * @return {string}
- * The tracks current label or the localized track kind.
- */
- getMenuCaption() {
- if (this.track_ && this.track_.label) {
- return this.track_.label;
- }
- return this.localize(toTitleCase$1(this.kind_));
- }
-
- /**
- * Create menu from chapter track
- *
- * @return { import('../../menu/menu').default }
- * New menu for the chapter buttons
- */
- createMenu() {
- this.options_.title = this.getMenuCaption();
- return super.createMenu();
- }
-
- /**
- * Create a menu item for each text track
- *
- * @return { import('./text-track-menu-item').default[] }
- * Array of menu items
- */
- createItems() {
- const items = [];
- if (!this.track_) {
- return items;
- }
- const cues = this.track_.cues;
- if (!cues) {
- return items;
- }
- for (let i = 0, l = cues.length; i < l; i++) {
- const cue = cues[i];
- const mi = new ChaptersTrackMenuItem(this.player_, {
- track: this.track_,
- cue
- });
- items.push(mi);
- }
- return items;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- ChaptersButton.prototype.kind_ = 'chapters';
-
- /**
- * The text that should display over the `ChaptersButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- ChaptersButton.prototype.controlText_ = 'Chapters';
- Component$1.registerComponent('ChaptersButton', ChaptersButton);
-
- /**
- * @file descriptions-button.js
- */
-
- /**
- * The button component for toggling and selecting descriptions
- *
- * @extends TextTrackButton
- */
- class DescriptionsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('audio-description');
- const tracks = player.textTracks();
- const changeHandler = bind_(this, this.handleTracksChange);
- tracks.addEventListener('change', changeHandler);
- this.on('dispose', function () {
- tracks.removeEventListener('change', changeHandler);
- });
- }
-
- /**
- * Handle text track change
- *
- * @param {Event} event
- * The event that caused this function to run
- *
- * @listens TextTrackList#change
- */
- handleTracksChange(event) {
- const tracks = this.player().textTracks();
- let disabled = false;
-
- // Check whether a track of a different kind is showing
- for (let i = 0, l = tracks.length; i < l; i++) {
- const track = tracks[i];
- if (track.kind !== this.kind_ && track.mode === 'showing') {
- disabled = true;
- break;
- }
- }
-
- // If another track is showing, disable this menu button
- if (disabled) {
- this.disable();
- } else {
- this.enable();
- }
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-descriptions-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- DescriptionsButton.prototype.kind_ = 'descriptions';
-
- /**
- * The text that should display over the `DescriptionsButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- DescriptionsButton.prototype.controlText_ = 'Descriptions';
- Component$1.registerComponent('DescriptionsButton', DescriptionsButton);
-
- /**
- * @file subtitles-button.js
- */
-
- /**
- * The button component for toggling and selecting subtitles
- *
- * @extends TextTrackButton
- */
- class SubtitlesButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('subtitles');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-subtitles-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- SubtitlesButton.prototype.kind_ = 'subtitles';
-
- /**
- * The text that should display over the `SubtitlesButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- SubtitlesButton.prototype.controlText_ = 'Subtitles';
- Component$1.registerComponent('SubtitlesButton', SubtitlesButton);
-
- /**
- * @file caption-settings-menu-item.js
- */
-
- /**
- * The menu item for caption track settings menu
- *
- * @extends TextTrackMenuItem
- */
- class CaptionSettingsMenuItem extends TextTrackMenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- options.track = {
- player,
- kind: options.kind,
- label: options.kind + ' settings',
- selectable: false,
- default: false,
- mode: 'disabled'
- };
-
- // CaptionSettingsMenuItem has no concept of 'selected'
- options.selectable = false;
- options.name = 'CaptionSettingsMenuItem';
- super(player, options);
- this.addClass('vjs-texttrack-settings');
- this.controlText(', opens ' + options.kind + ' settings dialog');
- }
-
- /**
- * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- this.player().getChild('textTrackSettings').open();
- }
-
- /**
- * Update control text and label on languagechange
- */
- handleLanguagechange() {
- this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
- super.handleLanguagechange();
- }
- }
- Component$1.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
-
- /**
- * @file captions-button.js
- */
-
- /**
- * The button component for toggling and selecting captions
- *
- * @extends TextTrackButton
- */
- class CaptionsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options, ready) {
- super(player, options, ready);
- this.setIcon('captions');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-captions-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create caption menu items
- *
- * @return {CaptionSettingsMenuItem[]}
- * The array of current menu items.
- */
- createItems() {
- const items = [];
- if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
- items.push(new CaptionSettingsMenuItem(this.player_, {
- kind: this.kind_
- }));
- this.hideThreshold_ += 1;
- }
- return super.createItems(items);
- }
- }
-
- /**
- * `kind` of TextTrack to look for to associate it with this menu.
- *
- * @type {string}
- * @private
- */
- CaptionsButton.prototype.kind_ = 'captions';
-
- /**
- * The text that should display over the `CaptionsButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- CaptionsButton.prototype.controlText_ = 'Captions';
- Component$1.registerComponent('CaptionsButton', CaptionsButton);
-
- /**
- * @file subs-caps-menu-item.js
- */
-
- /**
- * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
- * in the SubsCapsMenu.
- *
- * @extends TextTrackMenuItem
- */
- class SubsCapsMenuItem extends TextTrackMenuItem {
- createEl(type, props, attrs) {
- const el = super.createEl(type, props, attrs);
- const parentSpan = el.querySelector('.vjs-menu-item-text');
- if (this.options_.track.kind === 'captions') {
- if (this.player_.options_.experimentalSvgIcons) {
- this.setIcon('captions', el);
- } else {
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- }
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-control-text',
- // space added as the text will visually flow with the
- // label
- textContent: ` ${this.localize('Captions')}`
- }));
- }
- return el;
- }
- }
- Component$1.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
-
- /**
- * @file sub-caps-button.js
- */
-
- /**
- * The button component for toggling and selecting captions and/or subtitles
- *
- * @extends TextTrackButton
- */
- class SubsCapsButton extends TextTrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * The function to call when this component is ready.
- */
- constructor(player, options = {}) {
- super(player, options);
-
- // Although North America uses "captions" in most cases for
- // "captions and subtitles" other locales use "subtitles"
- this.label_ = 'subtitles';
- this.setIcon('subtitles');
- if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
- this.label_ = 'captions';
- this.setIcon('captions');
- }
- this.menuButton_.controlText(toTitleCase$1(this.label_));
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-subs-caps-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create caption/subtitles menu items
- *
- * @return {CaptionSettingsMenuItem[]}
- * The array of current menu items.
- */
- createItems() {
- let items = [];
- if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
- items.push(new CaptionSettingsMenuItem(this.player_, {
- kind: this.label_
- }));
- this.hideThreshold_ += 1;
- }
- items = super.createItems(items, SubsCapsMenuItem);
- return items;
- }
- }
-
- /**
- * `kind`s of TextTrack to look for to associate it with this menu.
- *
- * @type {array}
- * @private
- */
- SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
-
- /**
- * The text that should display over the `SubsCapsButton`s controls.
- *
- *
- * @type {string}
- * @protected
- */
- SubsCapsButton.prototype.controlText_ = 'Subtitles';
- Component$1.registerComponent('SubsCapsButton', SubsCapsButton);
-
- /**
- * @file audio-track-menu-item.js
- */
-
- /**
- * An {@link AudioTrack} {@link MenuItem}
- *
- * @extends MenuItem
- */
- class AudioTrackMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const track = options.track;
- const tracks = player.audioTracks();
-
- // Modify options for parent MenuItem class's init.
- options.label = track.label || track.language || 'Unknown';
- options.selected = track.enabled;
- super(player, options);
- this.track = track;
- this.addClass(`vjs-${track.kind}-menu-item`);
- const changeHandler = (...args) => {
- this.handleTracksChange.apply(this, args);
- };
- tracks.addEventListener('change', changeHandler);
- this.on('dispose', () => {
- tracks.removeEventListener('change', changeHandler);
- });
- }
- createEl(type, props, attrs) {
- const el = super.createEl(type, props, attrs);
- const parentSpan = el.querySelector('.vjs-menu-item-text');
- if (['main-desc', 'description'].indexOf(this.options_.track.kind) >= 0) {
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-icon-placeholder'
- }, {
- 'aria-hidden': true
- }));
- parentSpan.appendChild(createEl('span', {
- className: 'vjs-control-text',
- textContent: ' ' + this.localize('Descriptions')
- }));
- }
- return el;
- }
-
- /**
- * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
- * for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick(event);
-
- // the audio track list will automatically toggle other tracks
- // off for us.
- this.track.enabled = true;
-
- // when native audio tracks are used, we want to make sure that other tracks are turned off
- if (this.player_.tech_.featuresNativeAudioTracks) {
- const tracks = this.player_.audioTracks();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
-
- // skip the current track since we enabled it above
- if (track === this.track) {
- continue;
- }
- track.enabled = track === this.track;
- }
- }
- }
-
- /**
- * Handle any {@link AudioTrack} change.
- *
- * @param {Event} [event]
- * The {@link AudioTrackList#change} event that caused this to run.
- *
- * @listens AudioTrackList#change
- */
- handleTracksChange(event) {
- this.selected(this.track.enabled);
- }
- }
- Component$1.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
-
- /**
- * @file audio-track-button.js
- */
-
- /**
- * The base class for buttons that toggle specific {@link AudioTrack} types.
- *
- * @extends TrackButton
- */
- class AudioTrackButton extends TrackButton {
- /**
- * Creates an instance of this class.
- *
- * @param {Player} player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options={}]
- * The key/value store of player options.
- */
- constructor(player, options = {}) {
- options.tracks = player.audioTracks();
- super(player, options);
- this.setIcon('audio');
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-audio-button ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create a menu item for each audio track
- *
- * @param {AudioTrackMenuItem[]} [items=[]]
- * An array of existing menu items to use.
- *
- * @return {AudioTrackMenuItem[]}
- * An array of menu items
- */
- createItems(items = []) {
- // if there's only one audio track, there no point in showing it
- this.hideThreshold_ = 1;
- const tracks = this.player_.audioTracks();
- for (let i = 0; i < tracks.length; i++) {
- const track = tracks[i];
- items.push(new AudioTrackMenuItem(this.player_, {
- track,
- // MenuItem is selectable
- selectable: true,
- // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
- multiSelectable: false
- }));
- }
- return items;
- }
- }
-
- /**
- * The text that should display over the `AudioTrackButton`s controls. Added for localization.
- *
- * @type {string}
- * @protected
- */
- AudioTrackButton.prototype.controlText_ = 'Audio Track';
- Component$1.registerComponent('AudioTrackButton', AudioTrackButton);
-
- /**
- * @file playback-rate-menu-item.js
- */
-
- /**
- * The specific menu item type for selecting a playback rate.
- *
- * @extends MenuItem
- */
- class PlaybackRateMenuItem extends MenuItem {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- const label = options.rate;
- const rate = parseFloat(label, 10);
-
- // Modify options for parent MenuItem class's init.
- options.label = label;
- options.selected = rate === player.playbackRate();
- options.selectable = true;
- options.multiSelectable = false;
- super(player, options);
- this.label = label;
- this.rate = rate;
- this.on(player, 'ratechange', e => this.update(e));
- }
-
- /**
- * This gets called when an `PlaybackRateMenuItem` is "clicked". See
- * {@link ClickableComponent} for more detailed information on what a click can be.
- *
- * @param {Event} [event]
- * The `keydown`, `tap`, or `click` event that caused this function to be
- * called.
- *
- * @listens tap
- * @listens click
- */
- handleClick(event) {
- super.handleClick();
- this.player().playbackRate(this.rate);
- }
-
- /**
- * Update the PlaybackRateMenuItem when the playbackrate changes.
- *
- * @param {Event} [event]
- * The `ratechange` event that caused this function to run.
- *
- * @listens Player#ratechange
- */
- update(event) {
- this.selected(this.player().playbackRate() === this.rate);
- }
- }
-
- /**
- * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
- *
- * @type {string}
- * @private
- */
- PlaybackRateMenuItem.prototype.contentElType = 'button';
- Component$1.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
-
- /**
- * @file playback-rate-menu-button.js
- */
-
- /**
- * The component for controlling the playback rate.
- *
- * @extends MenuButton
- */
- class PlaybackRateMenuButton extends MenuButton {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
- this.updateVisibility();
- this.updateLabel();
- this.on(player, 'loadstart', e => this.updateVisibility(e));
- this.on(player, 'ratechange', e => this.updateLabel(e));
- this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- const el = super.createEl();
- this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
- this.labelEl_ = createEl('div', {
- className: 'vjs-playback-rate-value',
- id: this.labelElId_,
- textContent: '1x'
- });
- el.appendChild(this.labelEl_);
- return el;
- }
- dispose() {
- this.labelEl_ = null;
- super.dispose();
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-playback-rate ${super.buildCSSClass()}`;
- }
- buildWrapperCSSClass() {
- return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
- }
-
- /**
- * Create the list of menu items. Specific to each subclass.
- *
- */
- createItems() {
- const rates = this.playbackRates();
- const items = [];
- for (let i = rates.length - 1; i >= 0; i--) {
- items.push(new PlaybackRateMenuItem(this.player(), {
- rate: rates[i] + 'x'
- }));
- }
- return items;
- }
-
- /**
- * On playbackrateschange, update the menu to account for the new items.
- *
- * @listens Player#playbackrateschange
- */
- handlePlaybackRateschange(event) {
- this.update();
- }
-
- /**
- * Get possible playback rates
- *
- * @return {Array}
- * All possible playback rates
- */
- playbackRates() {
- const player = this.player();
- return player.playbackRates && player.playbackRates() || [];
- }
-
- /**
- * Get whether playback rates is supported by the tech
- * and an array of playback rates exists
- *
- * @return {boolean}
- * Whether changing playback rate is supported
- */
- playbackRateSupported() {
- return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
- }
-
- /**
- * Hide playback rate controls when they're no playback rate options to select
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#loadstart
- */
- updateVisibility(event) {
- if (this.playbackRateSupported()) {
- this.removeClass('vjs-hidden');
- } else {
- this.addClass('vjs-hidden');
- }
- }
-
- /**
- * Update button label when rate changed
- *
- * @param {Event} [event]
- * The event that caused this function to run.
- *
- * @listens Player#ratechange
- */
- updateLabel(event) {
- if (this.playbackRateSupported()) {
- this.labelEl_.textContent = this.player().playbackRate() + 'x';
- }
- }
- }
-
- /**
- * The text that should display over the `PlaybackRateMenuButton`s controls.
- *
- * Added for localization.
- *
- * @type {string}
- * @protected
- */
- PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
- Component$1.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
-
- /**
- * @file spacer.js
- */
-
- /**
- * Just an empty spacer element that can be used as an append point for plugins, etc.
- * Also can be used to create space between elements when necessary.
- *
- * @extends Component
- */
- class Spacer extends Component$1 {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-spacer ${super.buildCSSClass()}`;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl(tag = 'div', props = {}, attributes = {}) {
- if (!props.className) {
- props.className = this.buildCSSClass();
- }
- return super.createEl(tag, props, attributes);
- }
- }
- Component$1.registerComponent('Spacer', Spacer);
-
- /**
- * @file custom-control-spacer.js
- */
-
- /**
- * Spacer specifically meant to be used as an insertion point for new plugins, etc.
- *
- * @extends Spacer
- */
- class CustomControlSpacer extends Spacer {
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- */
- buildCSSClass() {
- return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
- }
-
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: this.buildCSSClass(),
- // No-flex/table-cell mode requires there be some content
- // in the cell to fill the remaining space of the table.
- textContent: '\u00a0'
- });
- }
- }
- Component$1.registerComponent('CustomControlSpacer', CustomControlSpacer);
-
- /**
- * @file control-bar.js
- */
-
- /**
- * Container of main controls.
- *
- * @extends Component
- */
- class ControlBar extends Component$1 {
- /**
- * Create the `Component`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- return super.createEl('div', {
- className: 'vjs-control-bar',
- dir: 'ltr'
- });
- }
- }
-
- /**
- * Default options for `ControlBar`
- *
- * @type {Object}
- * @private
- */
- ControlBar.prototype.options_ = {
- children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
- };
- Component$1.registerComponent('ControlBar', ControlBar);
-
- /**
- * @file error-display.js
- */
-
- /**
- * A display that indicates an error has occurred. This means that the video
- * is unplayable.
- *
- * @extends ModalDialog
- */
- class ErrorDisplay extends ModalDialog {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- super(player, options);
- this.on(player, 'error', e => {
- this.open(e);
- });
- }
-
- /**
- * Builds the default DOM `className`.
- *
- * @return {string}
- * The DOM `className` for this object.
- *
- * @deprecated Since version 5.
- */
- buildCSSClass() {
- return `vjs-error-display ${super.buildCSSClass()}`;
- }
-
- /**
- * Gets the localized error message based on the `Player`s error.
- *
- * @return {string}
- * The `Player`s error message localized or an empty string.
- */
- content() {
- const error = this.player().error();
- return error ? this.localize(error.message) : '';
- }
- }
-
- /**
- * The default options for an `ErrorDisplay`.
- *
- * @private
- */
- ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
- pauseOnOpen: false,
- fillAlways: true,
- temporary: false,
- uncloseable: true
- });
- Component$1.registerComponent('ErrorDisplay', ErrorDisplay);
-
- /**
- * @file text-track-settings.js
- */
- const LOCAL_STORAGE_KEY$1 = 'vjs-text-track-settings';
- const COLOR_BLACK = ['#000', 'Black'];
- const COLOR_BLUE = ['#00F', 'Blue'];
- const COLOR_CYAN = ['#0FF', 'Cyan'];
- const COLOR_GREEN = ['#0F0', 'Green'];
- const COLOR_MAGENTA = ['#F0F', 'Magenta'];
- const COLOR_RED = ['#F00', 'Red'];
- const COLOR_WHITE = ['#FFF', 'White'];
- const COLOR_YELLOW = ['#FF0', 'Yellow'];
- const OPACITY_OPAQUE = ['1', 'Opaque'];
- const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
- const OPACITY_TRANS = ['0', 'Transparent'];
-
- // Configuration for the various elements in the DOM of this component.
- //
- // Possible keys include:
- //
- // `default`:
- // The default option index. Only needs to be provided if not zero.
- // `parser`:
- // A function which is used to parse the value from the selected option in
- // a customized way.
- // `selector`:
- // The selector used to find the associated element.
- const selectConfigs = {
- backgroundColor: {
- selector: '.vjs-bg-color > select',
- id: 'captions-background-color-%s',
- label: 'Color',
- options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
- },
- backgroundOpacity: {
- selector: '.vjs-bg-opacity > select',
- id: 'captions-background-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS]
- },
- color: {
- selector: '.vjs-text-color > select',
- id: 'captions-foreground-color-%s',
- label: 'Color',
- options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN]
- },
- edgeStyle: {
- selector: '.vjs-edge-style > select',
- id: '%s',
- label: 'Text Edge Style',
- options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']]
- },
- fontFamily: {
- selector: '.vjs-font-family > select',
- id: 'captions-font-family-%s',
- label: 'Font Family',
- options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
- },
- fontPercent: {
- selector: '.vjs-font-percent > select',
- id: 'captions-font-size-%s',
- label: 'Font Size',
- options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']],
- default: 2,
- parser: v => v === '1.00' ? null : Number(v)
- },
- textOpacity: {
- selector: '.vjs-text-opacity > select',
- id: 'captions-foreground-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_OPAQUE, OPACITY_SEMI]
- },
- // Options for this object are defined below.
- windowColor: {
- selector: '.vjs-window-color > select',
- id: 'captions-window-color-%s',
- label: 'Color'
- },
- // Options for this object are defined below.
- windowOpacity: {
- selector: '.vjs-window-opacity > select',
- id: 'captions-window-opacity-%s',
- label: 'Opacity',
- options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE]
- }
- };
- selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
-
- /**
- * Get the actual value of an option.
- *
- * @param {string} value
- * The value to get
- *
- * @param {Function} [parser]
- * Optional function to adjust the value.
- *
- * @return {*}
- * - Will be `undefined` if no value exists
- * - Will be `undefined` if the given value is "none".
- * - Will be the actual value otherwise.
- *
- * @private
- */
- function parseOptionValue(value, parser) {
- if (parser) {
- value = parser(value);
- }
- if (value && value !== 'none') {
- return value;
- }
- }
-
- /**
- * Gets the value of the selected element within a element.
- *
- * @param {Element} el
- * the element to look in
- *
- * @param {Function} [parser]
- * Optional function to adjust the value.
- *
- * @return {*}
- * - Will be `undefined` if no value exists
- * - Will be `undefined` if the given value is "none".
- * - Will be the actual value otherwise.
- *
- * @private
- */
- function getSelectedOptionValue(el, parser) {
- const value = el.options[el.options.selectedIndex].value;
- return parseOptionValue(value, parser);
- }
-
- /**
- * Sets the selected element within a element based on a
- * given value.
- *
- * @param {Element} el
- * The element to look in.
- *
- * @param {string} value
- * the property to look on.
- *
- * @param {Function} [parser]
- * Optional function to adjust the value before comparing.
- *
- * @private
- */
- function setSelectedOption(el, value, parser) {
- if (!value) {
- return;
- }
- for (let i = 0; i < el.options.length; i++) {
- if (parseOptionValue(el.options[i].value, parser) === value) {
- el.selectedIndex = i;
- break;
- }
- }
- }
-
- /**
- * Manipulate Text Tracks settings.
- *
- * @extends ModalDialog
- */
- class TextTrackSettings extends ModalDialog {
- /**
- * Creates an instance of this class.
- *
- * @param { import('../player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- */
- constructor(player, options) {
- options.temporary = false;
- super(player, options);
- this.updateDisplay = this.updateDisplay.bind(this);
-
- // fill the modal and pretend we have opened it
- this.fill();
- this.hasBeenOpened_ = this.hasBeenFilled_ = true;
- this.endDialog = createEl('p', {
- className: 'vjs-control-text',
- textContent: this.localize('End of dialog window.')
- });
- this.el().appendChild(this.endDialog);
- this.setDefaults();
-
- // Grab `persistTextTrackSettings` from the player options if not passed in child options
- if (options.persistTextTrackSettings === undefined) {
- this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
- }
- this.on(this.$('.vjs-done-button'), 'click', () => {
- this.saveSettings();
- this.close();
- });
- this.on(this.$('.vjs-default-button'), 'click', () => {
- this.setDefaults();
- this.updateDisplay();
- });
- each(selectConfigs, config => {
- this.on(this.$(config.selector), 'change', this.updateDisplay);
- });
- if (this.options_.persistTextTrackSettings) {
- this.restoreSettings();
- }
- }
- dispose() {
- this.endDialog = null;
- super.dispose();
- }
-
- /**
- * Create a element with configured options.
- *
- * @param {string} key
- * Configuration key to use during creation.
- *
- * @param {string} [legendId]
- * Id of associated .
- *
- * @param {string} [type=label]
- * Type of labelling element, `label` or `legend`
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElSelect_(key, legendId = '', type = 'label') {
- const config = selectConfigs[key];
- const id = config.id.replace('%s', this.id_);
- const selectLabelledbyIds = [legendId, id].join(' ').trim();
- const guid = `vjs_select_${newGUID()}`;
- return [`<${type} id="${id}"${type === 'label' ? ` for="${guid}" class="vjs-label"` : ''}>`, this.localize(config.label), `${type}>`, ``].concat(config.options.map(o => {
- const optionId = id + '-' + o[1].replace(/\W+/g, '');
- return [``, this.localize(o[1]), ' '].join('');
- })).concat(' ').join('');
- }
-
- /**
- * Create foreground color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElFgColor_() {
- const legendId = `captions-text-legend-${this.id_}`;
- return [' ', ``, this.localize('Text'), ' ', '', this.createElSelect_('color', legendId), ' ', '', this.createElSelect_('textOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create background color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElBgColor_() {
- const legendId = `captions-background-${this.id_}`;
- return ['', ``, this.localize('Text Background'), ' ', '', this.createElSelect_('backgroundColor', legendId), ' ', '', this.createElSelect_('backgroundOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create window color element for the component
- *
- * @return {string}
- * An HTML string.
- *
- * @private
- */
- createElWinColor_() {
- const legendId = `captions-window-${this.id_}`;
- return ['', ``, this.localize('Caption Area Background'), ' ', '', this.createElSelect_('windowColor', legendId), ' ', '', this.createElSelect_('windowOpacity', legendId), ' ', ' '].join('');
- }
-
- /**
- * Create color elements for the component
- *
- * @return {Element}
- * The element that was created
- *
- * @private
- */
- createElColors_() {
- return createEl('div', {
- className: 'vjs-track-settings-colors',
- innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('')
- });
- }
-
- /**
- * Create font elements for the component
- *
- * @return {Element}
- * The element that was created.
- *
- * @private
- */
- createElFont_() {
- return createEl('div', {
- className: 'vjs-track-settings-font',
- innerHTML: ['', this.createElSelect_('fontPercent', '', 'legend'), ' ', '', this.createElSelect_('edgeStyle', '', 'legend'), ' ', '', this.createElSelect_('fontFamily', '', 'legend'), ' '].join('')
- });
- }
-
- /**
- * Create controls for the component
- *
- * @return {Element}
- * The element that was created.
- *
- * @private
- */
- createElControls_() {
- const defaultsDescription = this.localize('restore all settings to the default values');
- return createEl('div', {
- className: 'vjs-track-settings-controls',
- innerHTML: [``, this.localize('Reset'), ` ${defaultsDescription} `, ' ', `${this.localize('Done')} `].join('')
- });
- }
- content() {
- return [this.createElColors_(), this.createElFont_(), this.createElControls_()];
- }
- label() {
- return this.localize('Caption Settings Dialog');
- }
- description() {
- return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
- }
- buildCSSClass() {
- return super.buildCSSClass() + ' vjs-text-track-settings';
- }
-
- /**
- * Gets an object of text track settings (or null).
- *
- * @return {Object}
- * An object with config values parsed from the DOM or localStorage.
- */
- getValues() {
- return reduce(selectConfigs, (accum, config, key) => {
- const value = getSelectedOptionValue(this.$(config.selector), config.parser);
- if (value !== undefined) {
- accum[key] = value;
- }
- return accum;
- }, {});
- }
-
- /**
- * Sets text track settings from an object of values.
- *
- * @param {Object} values
- * An object with config values parsed from the DOM or localStorage.
- */
- setValues(values) {
- each(selectConfigs, (config, key) => {
- setSelectedOption(this.$(config.selector), values[key], config.parser);
- });
- }
-
- /**
- * Sets all `` elements to their default values.
- */
- setDefaults() {
- each(selectConfigs, config => {
- const index = config.hasOwnProperty('default') ? config.default : 0;
- this.$(config.selector).selectedIndex = index;
- });
- }
-
- /**
- * Restore texttrack settings from localStorage
- */
- restoreSettings() {
- let values;
- try {
- values = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY$1));
- } catch (err) {
- log$1.warn(err);
- }
- if (values) {
- this.setValues(values);
- }
- }
-
- /**
- * Save text track settings to localStorage
- */
- saveSettings() {
- if (!this.options_.persistTextTrackSettings) {
- return;
- }
- const values = this.getValues();
- try {
- if (Object.keys(values).length) {
- window.localStorage.setItem(LOCAL_STORAGE_KEY$1, JSON.stringify(values));
- } else {
- window.localStorage.removeItem(LOCAL_STORAGE_KEY$1);
- }
- } catch (err) {
- log$1.warn(err);
- }
- }
-
- /**
- * Update display of text track settings
- */
- updateDisplay() {
- const ttDisplay = this.player_.getChild('textTrackDisplay');
- if (ttDisplay) {
- ttDisplay.updateDisplay();
- }
- }
-
- /**
- * conditionally blur the element and refocus the captions button
- *
- * @private
- */
- conditionalBlur_() {
- this.previouslyActiveEl_ = null;
- const cb = this.player_.controlBar;
- const subsCapsBtn = cb && cb.subsCapsButton;
- const ccBtn = cb && cb.captionsButton;
- if (subsCapsBtn) {
- subsCapsBtn.focus();
- } else if (ccBtn) {
- ccBtn.focus();
- }
- }
-
- /**
- * Repopulate dialog with new localizations on languagechange
- */
- handleLanguagechange() {
- this.fill();
- }
- }
- Component$1.registerComponent('TextTrackSettings', TextTrackSettings);
-
- /**
- * @file resize-manager.js
- */
-
- /**
- * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
- *
- * It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
- *
- * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
- * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
- *
- * @example How to disable the resize manager
- * const player = videojs('#vid', {
- * resizeManager: false
- * });
- *
- * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
- *
- * @extends Component
- */
- class ResizeManager extends Component$1 {
- /**
- * Create the ResizeManager.
- *
- * @param {Object} player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of ResizeManager options.
- *
- * @param {Object} [options.ResizeObserver]
- * A polyfill for ResizeObserver can be passed in here.
- * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
- */
- constructor(player, options) {
- let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver;
-
- // if `null` was passed, we want to disable the ResizeObserver
- if (options.ResizeObserver === null) {
- RESIZE_OBSERVER_AVAILABLE = false;
- }
-
- // Only create an element when ResizeObserver isn't available
- const options_ = merge$2({
- createEl: !RESIZE_OBSERVER_AVAILABLE,
- reportTouchActivity: false
- }, options);
- super(player, options_);
- this.ResizeObserver = options.ResizeObserver || window.ResizeObserver;
- this.loadListener_ = null;
- this.resizeObserver_ = null;
- this.debouncedHandler_ = debounce(() => {
- this.resizeHandler();
- }, 100, false, this);
- if (RESIZE_OBSERVER_AVAILABLE) {
- this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
- this.resizeObserver_.observe(player.el());
- } else {
- this.loadListener_ = () => {
- if (!this.el_ || !this.el_.contentWindow) {
- return;
- }
- const debouncedHandler_ = this.debouncedHandler_;
- let unloadListener_ = this.unloadListener_ = function () {
- off(this, 'resize', debouncedHandler_);
- off(this, 'unload', unloadListener_);
- unloadListener_ = null;
- };
-
- // safari and edge can unload the iframe before resizemanager dispose
- // we have to dispose of event handlers correctly before that happens
- on(this.el_.contentWindow, 'unload', unloadListener_);
- on(this.el_.contentWindow, 'resize', debouncedHandler_);
- };
- this.one('load', this.loadListener_);
- }
- }
- createEl() {
- return super.createEl('iframe', {
- className: 'vjs-resize-manager',
- tabIndex: -1,
- title: this.localize('No content')
- }, {
- 'aria-hidden': 'true'
- });
- }
-
- /**
- * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
- *
- * @fires Player#playerresize
- */
- resizeHandler() {
- /**
- * Called when the player size has changed
- *
- * @event Player#playerresize
- * @type {Event}
- */
- // make sure player is still around to trigger
- // prevents this from causing an error after dispose
- if (!this.player_ || !this.player_.trigger) {
- return;
- }
- this.player_.trigger('playerresize');
- }
- dispose() {
- if (this.debouncedHandler_) {
- this.debouncedHandler_.cancel();
- }
- if (this.resizeObserver_) {
- if (this.player_.el()) {
- this.resizeObserver_.unobserve(this.player_.el());
- }
- this.resizeObserver_.disconnect();
- }
- if (this.loadListener_) {
- this.off('load', this.loadListener_);
- }
- if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
- this.unloadListener_.call(this.el_.contentWindow);
- }
- this.ResizeObserver = null;
- this.resizeObserver = null;
- this.debouncedHandler_ = null;
- this.loadListener_ = null;
- super.dispose();
- }
- }
- Component$1.registerComponent('ResizeManager', ResizeManager);
-
- const defaults = {
- trackingThreshold: 20,
- liveTolerance: 15
- };
-
- /*
- track when we are at the live edge, and other helpers for live playback */
-
- /**
- * A class for checking live current time and determining when the player
- * is at or behind the live edge.
- */
- class LiveTracker extends Component$1 {
- /**
- * Creates an instance of this class.
- *
- * @param { import('./player').default } player
- * The `Player` that this class should be attached to.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {number} [options.trackingThreshold=20]
- * Number of seconds of live window (seekableEnd - seekableStart) that
- * media needs to have before the liveui will be shown.
- *
- * @param {number} [options.liveTolerance=15]
- * Number of seconds behind live that we have to be
- * before we will be considered non-live. Note that this will only
- * be used when playing at the live edge. This allows large seekable end
- * changes to not effect whether we are live or not.
- */
- constructor(player, options) {
- // LiveTracker does not need an element
- const options_ = merge$2(defaults, options, {
- createEl: false
- });
- super(player, options_);
- this.trackLiveHandler_ = () => this.trackLive_();
- this.handlePlay_ = e => this.handlePlay(e);
- this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
- this.handleSeeked_ = e => this.handleSeeked(e);
- this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
- this.reset_();
- this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
- // we should try to toggle tracking on canplay as native playback engines, like Safari
- // may not have the proper values for things like seekableEnd until then
- this.on(this.player_, 'canplay', () => this.toggleTracking());
- }
-
- /**
- * all the functionality for tracking when seek end changes
- * and for tracking how far past seek end we should be
- */
- trackLive_() {
- const seekable = this.player_.seekable();
-
- // skip undefined seekable
- if (!seekable || !seekable.length) {
- return;
- }
- const newTime = Number(window.performance.now().toFixed(4));
- const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
- this.lastTime_ = newTime;
- this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
- const liveCurrentTime = this.liveCurrentTime();
- const currentTime = this.player_.currentTime();
-
- // we are behind live if any are true
- // 1. the player is paused
- // 2. the user seeked to a location 2 seconds away from live
- // 3. the difference between live and current time is greater
- // liveTolerance which defaults to 15s
- let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
-
- // we cannot be behind if
- // 1. until we have not seen a timeupdate yet
- // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
- if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
- isBehind = false;
- }
- if (isBehind !== this.behindLiveEdge_) {
- this.behindLiveEdge_ = isBehind;
- this.trigger('liveedgechange');
- }
- }
-
- /**
- * handle a durationchange event on the player
- * and start/stop tracking accordingly.
- */
- handleDurationchange() {
- this.toggleTracking();
- }
-
- /**
- * start/stop tracking
- */
- toggleTracking() {
- if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
- if (this.player_.options_.liveui) {
- this.player_.addClass('vjs-liveui');
- }
- this.startTracking();
- } else {
- this.player_.removeClass('vjs-liveui');
- this.stopTracking();
- }
- }
-
- /**
- * start tracking live playback
- */
- startTracking() {
- if (this.isTracking()) {
- return;
- }
-
- // If we haven't seen a timeupdate, we need to check whether playback
- // began before this component started tracking. This can happen commonly
- // when using autoplay.
- if (!this.timeupdateSeen_) {
- this.timeupdateSeen_ = this.player_.hasStarted();
- }
- this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
- this.trackLive_();
- this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- if (!this.timeupdateSeen_) {
- this.one(this.player_, 'play', this.handlePlay_);
- this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- } else {
- this.on(this.player_, 'seeked', this.handleSeeked_);
- }
- }
-
- /**
- * handle the first timeupdate on the player if it wasn't already playing
- * when live tracker started tracking.
- */
- handleFirstTimeupdate() {
- this.timeupdateSeen_ = true;
- this.on(this.player_, 'seeked', this.handleSeeked_);
- }
-
- /**
- * Keep track of what time a seek starts, and listen for seeked
- * to find where a seek ends.
- */
- handleSeeked() {
- const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
- this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
- this.nextSeekedFromUser_ = false;
- this.trackLive_();
- }
-
- /**
- * handle the first play on the player, and make sure that we seek
- * right to the live edge.
- */
- handlePlay() {
- this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
- }
-
- /**
- * Stop tracking, and set all internal variables to
- * their initial value.
- */
- reset_() {
- this.lastTime_ = -1;
- this.pastSeekEnd_ = 0;
- this.lastSeekEnd_ = -1;
- this.behindLiveEdge_ = true;
- this.timeupdateSeen_ = false;
- this.seekedBehindLive_ = false;
- this.nextSeekedFromUser_ = false;
- this.clearInterval(this.trackingInterval_);
- this.trackingInterval_ = null;
- this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
- this.off(this.player_, 'seeked', this.handleSeeked_);
- this.off(this.player_, 'play', this.handlePlay_);
- this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
- this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
- }
-
- /**
- * The next seeked event is from the user. Meaning that any seek
- * > 2s behind live will be considered behind live for real and
- * liveTolerance will be ignored.
- */
- nextSeekedFromUser() {
- this.nextSeekedFromUser_ = true;
- }
-
- /**
- * stop tracking live playback
- */
- stopTracking() {
- if (!this.isTracking()) {
- return;
- }
- this.reset_();
- this.trigger('liveedgechange');
- }
-
- /**
- * A helper to get the player seekable end
- * so that we don't have to null check everywhere
- *
- * @return {number}
- * The furthest seekable end or Infinity.
- */
- seekableEnd() {
- const seekable = this.player_.seekable();
- const seekableEnds = [];
- let i = seekable ? seekable.length : 0;
- while (i--) {
- seekableEnds.push(seekable.end(i));
- }
-
- // grab the furthest seekable end after sorting, or if there are none
- // default to Infinity
- return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
- }
-
- /**
- * A helper to get the player seekable start
- * so that we don't have to null check everywhere
- *
- * @return {number}
- * The earliest seekable start or 0.
- */
- seekableStart() {
- const seekable = this.player_.seekable();
- const seekableStarts = [];
- let i = seekable ? seekable.length : 0;
- while (i--) {
- seekableStarts.push(seekable.start(i));
- }
-
- // grab the first seekable start after sorting, or if there are none
- // default to 0
- return seekableStarts.length ? seekableStarts.sort()[0] : 0;
- }
-
- /**
- * Get the live time window aka
- * the amount of time between seekable start and
- * live current time.
- *
- * @return {number}
- * The amount of seconds that are seekable in
- * the live video.
- */
- liveWindow() {
- const liveCurrentTime = this.liveCurrentTime();
-
- // if liveCurrenTime is Infinity then we don't have a liveWindow at all
- if (liveCurrentTime === Infinity) {
- return 0;
- }
- return liveCurrentTime - this.seekableStart();
- }
-
- /**
- * Determines if the player is live, only checks if this component
- * is tracking live playback or not
- *
- * @return {boolean}
- * Whether liveTracker is tracking
- */
- isLive() {
- return this.isTracking();
- }
-
- /**
- * Determines if currentTime is at the live edge and won't fall behind
- * on each seekableendchange
- *
- * @return {boolean}
- * Whether playback is at the live edge
- */
- atLiveEdge() {
- return !this.behindLiveEdge();
- }
-
- /**
- * get what we expect the live current time to be
- *
- * @return {number}
- * The expected live current time
- */
- liveCurrentTime() {
- return this.pastSeekEnd() + this.seekableEnd();
- }
-
- /**
- * The number of seconds that have occurred after seekable end
- * changed. This will be reset to 0 once seekable end changes.
- *
- * @return {number}
- * Seconds past the current seekable end
- */
- pastSeekEnd() {
- const seekableEnd = this.seekableEnd();
- if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
- this.pastSeekEnd_ = 0;
- }
- this.lastSeekEnd_ = seekableEnd;
- return this.pastSeekEnd_;
- }
-
- /**
- * If we are currently behind the live edge, aka currentTime will be
- * behind on a seekableendchange
- *
- * @return {boolean}
- * If we are behind the live edge
- */
- behindLiveEdge() {
- return this.behindLiveEdge_;
- }
-
- /**
- * Whether live tracker is currently tracking or not.
- */
- isTracking() {
- return typeof this.trackingInterval_ === 'number';
- }
-
- /**
- * Seek to the live edge if we are behind the live edge
- */
- seekToLiveEdge() {
- this.seekedBehindLive_ = false;
- if (this.atLiveEdge()) {
- return;
- }
- this.nextSeekedFromUser_ = false;
- this.player_.currentTime(this.liveCurrentTime());
- }
-
- /**
- * Dispose of liveTracker
- */
- dispose() {
- this.stopTracking();
- super.dispose();
- }
- }
- Component$1.registerComponent('LiveTracker', LiveTracker);
-
- /**
- * Displays an element over the player which contains an optional title and
- * description for the current content.
- *
- * Much of the code for this component originated in the now obsolete
- * videojs-dock plugin: https://github.com/brightcove/videojs-dock/
- *
- * @extends Component
- */
- class TitleBar extends Component$1 {
- constructor(player, options) {
- super(player, options);
- this.on('statechanged', e => this.updateDom_());
- this.updateDom_();
- }
-
- /**
- * Create the `TitleBar`'s DOM element
- *
- * @return {Element}
- * The element that was created.
- */
- createEl() {
- this.els = {
- title: createEl('div', {
- className: 'vjs-title-bar-title',
- id: `vjs-title-bar-title-${newGUID()}`
- }),
- description: createEl('div', {
- className: 'vjs-title-bar-description',
- id: `vjs-title-bar-description-${newGUID()}`
- })
- };
- return createEl('div', {
- className: 'vjs-title-bar'
- }, {}, values$1(this.els));
- }
-
- /**
- * Updates the DOM based on the component's state object.
- */
- updateDom_() {
- const tech = this.player_.tech_;
- const techEl = tech && tech.el_;
- const techAriaAttrs = {
- title: 'aria-labelledby',
- description: 'aria-describedby'
- };
- ['title', 'description'].forEach(k => {
- const value = this.state[k];
- const el = this.els[k];
- const techAriaAttr = techAriaAttrs[k];
- emptyEl(el);
- if (value) {
- textContent(el, value);
- }
-
- // If there is a tech element available, update its ARIA attributes
- // according to whether a title and/or description have been provided.
- if (techEl) {
- techEl.removeAttribute(techAriaAttr);
- if (value) {
- techEl.setAttribute(techAriaAttr, el.id);
- }
- }
- });
- if (this.state.title || this.state.description) {
- this.show();
- } else {
- this.hide();
- }
- }
-
- /**
- * Update the contents of the title bar component with new title and
- * description text.
- *
- * If both title and description are missing, the title bar will be hidden.
- *
- * If either title or description are present, the title bar will be visible.
- *
- * NOTE: Any previously set value will be preserved. To unset a previously
- * set value, you must pass an empty string or null.
- *
- * For example:
- *
- * ```
- * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
- * update({description: 'bar2'}) // title: 'foo', description: 'bar2'
- * update({title: ''}) // title: '', description: 'bar2'
- * update({title: 'foo', description: null}) // title: 'foo', description: null
- * ```
- *
- * @param {Object} [options={}]
- * An options object. When empty, the title bar will be hidden.
- *
- * @param {string} [options.title]
- * A title to display in the title bar.
- *
- * @param {string} [options.description]
- * A description to display in the title bar.
- */
- update(options) {
- this.setState(options);
- }
-
- /**
- * Dispose the component.
- */
- dispose() {
- const tech = this.player_.tech_;
- const techEl = tech && tech.el_;
- if (techEl) {
- techEl.removeAttribute('aria-labelledby');
- techEl.removeAttribute('aria-describedby');
- }
- super.dispose();
- this.els = null;
- }
- }
- Component$1.registerComponent('TitleBar', TitleBar);
-
- /**
- * This function is used to fire a sourceset when there is something
- * similar to `mediaEl.load()` being called. It will try to find the source via
- * the `src` attribute and then the `` elements. It will then fire `sourceset`
- * with the source that was found or empty string if we cannot know. If it cannot
- * find a source then `sourceset` will not be fired.
- *
- * @param { import('./html5').default } tech
- * The tech object that sourceset was setup on
- *
- * @return {boolean}
- * returns false if the sourceset was not fired and true otherwise.
- */
- const sourcesetLoad = tech => {
- const el = tech.el();
-
- // if `el.src` is set, that source will be loaded.
- if (el.hasAttribute('src')) {
- tech.triggerSourceset(el.src);
- return true;
- }
-
- /**
- * Since there isn't a src property on the media element, source elements will be used for
- * implementing the source selection algorithm. This happens asynchronously and
- * for most cases were there is more than one source we cannot tell what source will
- * be loaded, without re-implementing the source selection algorithm. At this time we are not
- * going to do that. There are three special cases that we do handle here though:
- *
- * 1. If there are no sources, do not fire `sourceset`.
- * 2. If there is only one `` with a `src` property/attribute that is our `src`
- * 3. If there is more than one `` but all of them have the same `src` url.
- * That will be our src.
- */
- const sources = tech.$$('source');
- const srcUrls = [];
- let src = '';
-
- // if there are no sources, do not fire sourceset
- if (!sources.length) {
- return false;
- }
-
- // only count valid/non-duplicate source elements
- for (let i = 0; i < sources.length; i++) {
- const url = sources[i].src;
- if (url && srcUrls.indexOf(url) === -1) {
- srcUrls.push(url);
- }
- }
-
- // there were no valid sources
- if (!srcUrls.length) {
- return false;
- }
-
- // there is only one valid source element url
- // use that
- if (srcUrls.length === 1) {
- src = srcUrls[0];
- }
- tech.triggerSourceset(src);
- return true;
- };
-
- /**
- * our implementation of an `innerHTML` descriptor for browsers
- * that do not have one.
- */
- const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
- get() {
- return this.cloneNode(true).innerHTML;
- },
- set(v) {
- // make a dummy node to use innerHTML on
- const dummy = document.createElement(this.nodeName.toLowerCase());
-
- // set innerHTML to the value provided
- dummy.innerHTML = v;
-
- // make a document fragment to hold the nodes from dummy
- const docFrag = document.createDocumentFragment();
-
- // copy all of the nodes created by the innerHTML on dummy
- // to the document fragment
- while (dummy.childNodes.length) {
- docFrag.appendChild(dummy.childNodes[0]);
- }
-
- // remove content
- this.innerText = '';
-
- // now we add all of that html in one by appending the
- // document fragment. This is how innerHTML does it.
- window.Element.prototype.appendChild.call(this, docFrag);
-
- // then return the result that innerHTML's setter would
- return this.innerHTML;
- }
- });
-
- /**
- * Get a property descriptor given a list of priorities and the
- * property to get.
- */
- const getDescriptor = (priority, prop) => {
- let descriptor = {};
- for (let i = 0; i < priority.length; i++) {
- descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
- if (descriptor && descriptor.set && descriptor.get) {
- break;
- }
- }
- descriptor.enumerable = true;
- descriptor.configurable = true;
- return descriptor;
- };
- const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, window.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
-
- /**
- * Patches browser internal functions so that we can tell synchronously
- * if a `` was appended to the media element. For some reason this
- * causes a `sourceset` if the the media element is ready and has no source.
- * This happens when:
- * - The page has just loaded and the media element does not have a source.
- * - The media element was emptied of all sources, then `load()` was called.
- *
- * It does this by patching the following functions/properties when they are supported:
- *
- * - `append()` - can be used to add a `` element to the media element
- * - `appendChild()` - can be used to add a `` element to the media element
- * - `insertAdjacentHTML()` - can be used to add a `` element to the media element
- * - `innerHTML` - can be used to add a `` element to the media element
- *
- * @param {Html5} tech
- * The tech object that sourceset is being setup on.
- */
- const firstSourceWatch = function (tech) {
- const el = tech.el();
-
- // make sure firstSourceWatch isn't setup twice.
- if (el.resetSourceWatch_) {
- return;
- }
- const old = {};
- const innerDescriptor = getInnerHTMLDescriptor(tech);
- const appendWrapper = appendFn => (...args) => {
- const retval = appendFn.apply(el, args);
- sourcesetLoad(tech);
- return retval;
- };
- ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
- if (!el[k]) {
- return;
- }
-
- // store the old function
- old[k] = el[k];
-
- // call the old function with a sourceset if a source
- // was loaded
- el[k] = appendWrapper(old[k]);
- });
- Object.defineProperty(el, 'innerHTML', merge$2(innerDescriptor, {
- set: appendWrapper(innerDescriptor.set)
- }));
- el.resetSourceWatch_ = () => {
- el.resetSourceWatch_ = null;
- Object.keys(old).forEach(k => {
- el[k] = old[k];
- });
- Object.defineProperty(el, 'innerHTML', innerDescriptor);
- };
-
- // on the first sourceset, we need to revert our changes
- tech.one('sourceset', el.resetSourceWatch_);
- };
-
- /**
- * our implementation of a `src` descriptor for browsers
- * that do not have one
- */
- const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
- get() {
- if (this.hasAttribute('src')) {
- return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src'));
- }
- return '';
- },
- set(v) {
- window.Element.prototype.setAttribute.call(this, 'src', v);
- return v;
- }
- });
- const getSrcDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
-
- /**
- * setup `sourceset` handling on the `Html5` tech. This function
- * patches the following element properties/functions:
- *
- * - `src` - to determine when `src` is set
- * - `setAttribute()` - to determine when `src` is set
- * - `load()` - this re-triggers the source selection algorithm, and can
- * cause a sourceset.
- *
- * If there is no source when we are adding `sourceset` support or during a `load()`
- * we also patch the functions listed in `firstSourceWatch`.
- *
- * @param {Html5} tech
- * The tech to patch
- */
- const setupSourceset = function (tech) {
- if (!tech.featuresSourceset) {
- return;
- }
- const el = tech.el();
-
- // make sure sourceset isn't setup twice.
- if (el.resetSourceset_) {
- return;
- }
- const srcDescriptor = getSrcDescriptor(tech);
- const oldSetAttribute = el.setAttribute;
- const oldLoad = el.load;
- Object.defineProperty(el, 'src', merge$2(srcDescriptor, {
- set: v => {
- const retval = srcDescriptor.set.call(el, v);
-
- // we use the getter here to get the actual value set on src
- tech.triggerSourceset(el.src);
- return retval;
- }
- }));
- el.setAttribute = (n, v) => {
- const retval = oldSetAttribute.call(el, n, v);
- if (/src/i.test(n)) {
- tech.triggerSourceset(el.src);
- }
- return retval;
- };
- el.load = () => {
- const retval = oldLoad.call(el);
-
- // if load was called, but there was no source to fire
- // sourceset on. We have to watch for a source append
- // as that can trigger a `sourceset` when the media element
- // has no source
- if (!sourcesetLoad(tech)) {
- tech.triggerSourceset('');
- firstSourceWatch(tech);
- }
- return retval;
- };
- if (el.currentSrc) {
- tech.triggerSourceset(el.currentSrc);
- } else if (!sourcesetLoad(tech)) {
- firstSourceWatch(tech);
- }
- el.resetSourceset_ = () => {
- el.resetSourceset_ = null;
- el.load = oldLoad;
- el.setAttribute = oldSetAttribute;
- Object.defineProperty(el, 'src', srcDescriptor);
- if (el.resetSourceWatch_) {
- el.resetSourceWatch_();
- }
- };
- };
-
- /**
- * @file html5.js
- */
-
- /**
- * HTML5 Media Controller - Wrapper for HTML5 Media API
- *
- * @mixes Tech~SourceHandlerAdditions
- * @extends Tech
- */
- class Html5 extends Tech {
- /**
- * Create an instance of this Tech.
- *
- * @param {Object} [options]
- * The key/value store of player options.
- *
- * @param {Function} [ready]
- * Callback function to call when the `HTML5` Tech is ready.
- */
- constructor(options, ready) {
- super(options, ready);
- const source = options.source;
- let crossoriginTracks = false;
- this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
-
- // Set the source if one is provided
- // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
- // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
- // anyway so the error gets fired.
- if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
- this.setSource(source);
- } else {
- this.handleLateInit_(this.el_);
- }
-
- // setup sourceset after late sourceset/init
- if (options.enableSourceset) {
- this.setupSourcesetHandling_();
- }
- this.isScrubbing_ = false;
- if (this.el_.hasChildNodes()) {
- const nodes = this.el_.childNodes;
- let nodesLength = nodes.length;
- const removeNodes = [];
- while (nodesLength--) {
- const node = nodes[nodesLength];
- const nodeName = node.nodeName.toLowerCase();
- if (nodeName === 'track') {
- if (!this.featuresNativeTextTracks) {
- // Empty video tag tracks so the built-in player doesn't use them also.
- // This may not be fast enough to stop HTML5 browsers from reading the tags
- // so we'll need to turn off any default tracks if we're manually doing
- // captions and subtitles. videoElement.textTracks
- removeNodes.push(node);
- } else {
- // store HTMLTrackElement and TextTrack to remote list
- this.remoteTextTrackEls().addTrackElement_(node);
- this.remoteTextTracks().addTrack(node.track);
- this.textTracks().addTrack(node.track);
- if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
- crossoriginTracks = true;
- }
- }
- }
- }
- for (let i = 0; i < removeNodes.length; i++) {
- this.el_.removeChild(removeNodes[i]);
- }
- }
- this.proxyNativeTracks_();
- if (this.featuresNativeTextTracks && crossoriginTracks) {
- log$1.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.');
- }
-
- // prevent iOS Safari from disabling metadata text tracks during native playback
- this.restoreMetadataTracksInIOSNativePlayer_();
-
- // Determine if native controls should be used
- // Our goal should be to get the custom controls on mobile solid everywhere
- // so we can remove this all together. Right now this will block custom
- // controls on touch enabled laptops like the Chrome Pixel
- if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
- this.setControls(true);
- }
-
- // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
- // into a `fullscreenchange` event
- this.proxyWebkitFullscreen_();
- this.triggerReady();
- }
-
- /**
- * Dispose of `HTML5` media element and remove all tracks.
- */
- dispose() {
- if (this.el_ && this.el_.resetSourceset_) {
- this.el_.resetSourceset_();
- }
- Html5.disposeMediaElement(this.el_);
- this.options_ = null;
-
- // tech will handle clearing of the emulated track list
- super.dispose();
- }
-
- /**
- * Modify the media element so that we can detect when
- * the source is changed. Fires `sourceset` just after the source has changed
- */
- setupSourcesetHandling_() {
- setupSourceset(this);
- }
-
- /**
- * When a captions track is enabled in the iOS Safari native player, all other
- * tracks are disabled (including metadata tracks), which nulls all of their
- * associated cue points. This will restore metadata tracks to their pre-fullscreen
- * state in those cases so that cue points are not needlessly lost.
- *
- * @private
- */
- restoreMetadataTracksInIOSNativePlayer_() {
- const textTracks = this.textTracks();
- let metadataTracksPreFullscreenState;
-
- // captures a snapshot of every metadata track's current state
- const takeMetadataTrackSnapshot = () => {
- metadataTracksPreFullscreenState = [];
- for (let i = 0; i < textTracks.length; i++) {
- const track = textTracks[i];
- if (track.kind === 'metadata') {
- metadataTracksPreFullscreenState.push({
- track,
- storedMode: track.mode
- });
- }
- }
- };
-
- // snapshot each metadata track's initial state, and update the snapshot
- // each time there is a track 'change' event
- takeMetadataTrackSnapshot();
- textTracks.addEventListener('change', takeMetadataTrackSnapshot);
- this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
- const restoreTrackMode = () => {
- for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
- const storedTrack = metadataTracksPreFullscreenState[i];
- if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
- storedTrack.track.mode = storedTrack.storedMode;
- }
- }
- // we only want this handler to be executed on the first 'change' event
- textTracks.removeEventListener('change', restoreTrackMode);
- };
-
- // when we enter fullscreen playback, stop updating the snapshot and
- // restore all track modes to their pre-fullscreen state
- this.on('webkitbeginfullscreen', () => {
- textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
-
- // remove the listener before adding it just in case it wasn't previously removed
- textTracks.removeEventListener('change', restoreTrackMode);
- textTracks.addEventListener('change', restoreTrackMode);
- });
-
- // start updating the snapshot again after leaving fullscreen
- this.on('webkitendfullscreen', () => {
- // remove the listener before adding it just in case it wasn't previously removed
- textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
- textTracks.addEventListener('change', takeMetadataTrackSnapshot);
-
- // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
- textTracks.removeEventListener('change', restoreTrackMode);
- });
- }
-
- /**
- * Attempt to force override of tracks for the given type
- *
- * @param {string} type - Track type to override, possible values include 'Audio',
- * 'Video', and 'Text'.
- * @param {boolean} override - If set to true native audio/video will be overridden,
- * otherwise native audio/video will potentially be used.
- * @private
- */
- overrideNative_(type, override) {
- // If there is no behavioral change don't add/remove listeners
- if (override !== this[`featuresNative${type}Tracks`]) {
- return;
- }
- const lowerCaseType = type.toLowerCase();
- if (this[`${lowerCaseType}TracksListeners_`]) {
- Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
- const elTracks = this.el()[`${lowerCaseType}Tracks`];
- elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
- });
- }
- this[`featuresNative${type}Tracks`] = !override;
- this[`${lowerCaseType}TracksListeners_`] = null;
- this.proxyNativeTracksForType_(lowerCaseType);
- }
-
- /**
- * Attempt to force override of native audio tracks.
- *
- * @param {boolean} override - If set to true native audio will be overridden,
- * otherwise native audio will potentially be used.
- */
- overrideNativeAudioTracks(override) {
- this.overrideNative_('Audio', override);
- }
-
- /**
- * Attempt to force override of native video tracks.
- *
- * @param {boolean} override - If set to true native video will be overridden,
- * otherwise native video will potentially be used.
- */
- overrideNativeVideoTracks(override) {
- this.overrideNative_('Video', override);
- }
-
- /**
- * Proxy native track list events for the given type to our track
- * lists if the browser we are playing in supports that type of track list.
- *
- * @param {string} name - Track type; values include 'audio', 'video', and 'text'
- * @private
- */
- proxyNativeTracksForType_(name) {
- const props = NORMAL[name];
- const elTracks = this.el()[props.getterName];
- const techTracks = this[props.getterName]();
- if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
- return;
- }
- const listeners = {
- change: e => {
- const event = {
- type: 'change',
- target: techTracks,
- currentTarget: techTracks,
- srcElement: techTracks
- };
- techTracks.trigger(event);
-
- // if we are a text track change event, we should also notify the
- // remote text track list. This can potentially cause a false positive
- // if we were to get a change event on a non-remote track and
- // we triggered the event on the remote text track list which doesn't
- // contain that track. However, best practices mean looping through the
- // list of tracks and searching for the appropriate mode value, so,
- // this shouldn't pose an issue
- if (name === 'text') {
- this[REMOTE.remoteText.getterName]().trigger(event);
- }
- },
- addtrack(e) {
- techTracks.addTrack(e.track);
- },
- removetrack(e) {
- techTracks.removeTrack(e.track);
- }
- };
- const removeOldTracks = function () {
- const removeTracks = [];
- for (let i = 0; i < techTracks.length; i++) {
- let found = false;
- for (let j = 0; j < elTracks.length; j++) {
- if (elTracks[j] === techTracks[i]) {
- found = true;
- break;
- }
- }
- if (!found) {
- removeTracks.push(techTracks[i]);
- }
- }
- while (removeTracks.length) {
- techTracks.removeTrack(removeTracks.shift());
- }
- };
- this[props.getterName + 'Listeners_'] = listeners;
- Object.keys(listeners).forEach(eventName => {
- const listener = listeners[eventName];
- elTracks.addEventListener(eventName, listener);
- this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
- });
-
- // Remove (native) tracks that are not used anymore
- this.on('loadstart', removeOldTracks);
- this.on('dispose', e => this.off('loadstart', removeOldTracks));
- }
-
- /**
- * Proxy all native track list events to our track lists if the browser we are playing
- * in supports that type of track list.
- *
- * @private
- */
- proxyNativeTracks_() {
- NORMAL.names.forEach(name => {
- this.proxyNativeTracksForType_(name);
- });
- }
-
- /**
- * Create the `Html5` Tech's DOM element.
- *
- * @return {Element}
- * The element that gets created.
- */
- createEl() {
- let el = this.options_.tag;
-
- // Check if this browser supports moving the element into the box.
- // On the iPhone video will break if you move the element,
- // So we have to create a brand new element.
- // If we ingested the player div, we do not need to move the media element.
- if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
- // If the original tag is still there, clone and remove it.
- if (el) {
- const clone = el.cloneNode(true);
- if (el.parentNode) {
- el.parentNode.insertBefore(clone, el);
- }
- Html5.disposeMediaElement(el);
- el = clone;
- } else {
- el = document.createElement('video');
-
- // determine if native controls should be used
- const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
- const attributes = merge$2({}, tagAttributes);
- if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
- delete attributes.controls;
- }
- setAttributes(el, Object.assign(attributes, {
- id: this.options_.techId,
- class: 'vjs-tech'
- }));
- }
- el.playerId = this.options_.playerId;
- }
- if (typeof this.options_.preload !== 'undefined') {
- setAttribute(el, 'preload', this.options_.preload);
- }
- if (this.options_.disablePictureInPicture !== undefined) {
- el.disablePictureInPicture = this.options_.disablePictureInPicture;
- }
-
- // Update specific tag settings, in case they were overridden
- // `autoplay` has to be *last* so that `muted` and `playsinline` are present
- // when iOS/Safari or other browsers attempt to autoplay.
- const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
- for (let i = 0; i < settingsAttrs.length; i++) {
- const attr = settingsAttrs[i];
- const value = this.options_[attr];
- if (typeof value !== 'undefined') {
- if (value) {
- setAttribute(el, attr, attr);
- } else {
- removeAttribute(el, attr);
- }
- el[attr] = value;
- }
- }
- return el;
- }
-
- /**
- * This will be triggered if the loadstart event has already fired, before videojs was
- * ready. Two known examples of when this can happen are:
- * 1. If we're loading the playback object after it has started loading
- * 2. The media is already playing the (often with autoplay on) then
- *
- * This function will fire another loadstart so that videojs can catchup.
- *
- * @fires Tech#loadstart
- *
- * @return {undefined}
- * returns nothing.
- */
- handleLateInit_(el) {
- if (el.networkState === 0 || el.networkState === 3) {
- // The video element hasn't started loading the source yet
- // or didn't find a source
- return;
- }
- if (el.readyState === 0) {
- // NetworkState is set synchronously BUT loadstart is fired at the
- // end of the current stack, usually before setInterval(fn, 0).
- // So at this point we know loadstart may have already fired or is
- // about to fire, and either way the player hasn't seen it yet.
- // We don't want to fire loadstart prematurely here and cause a
- // double loadstart so we'll wait and see if it happens between now
- // and the next loop, and fire it if not.
- // HOWEVER, we also want to make sure it fires before loadedmetadata
- // which could also happen between now and the next loop, so we'll
- // watch for that also.
- let loadstartFired = false;
- const setLoadstartFired = function () {
- loadstartFired = true;
- };
- this.on('loadstart', setLoadstartFired);
- const triggerLoadstart = function () {
- // We did miss the original loadstart. Make sure the player
- // sees loadstart before loadedmetadata
- if (!loadstartFired) {
- this.trigger('loadstart');
- }
- };
- this.on('loadedmetadata', triggerLoadstart);
- this.ready(function () {
- this.off('loadstart', setLoadstartFired);
- this.off('loadedmetadata', triggerLoadstart);
- if (!loadstartFired) {
- // We did miss the original native loadstart. Fire it now.
- this.trigger('loadstart');
- }
- });
- return;
- }
-
- // From here on we know that loadstart already fired and we missed it.
- // The other readyState events aren't as much of a problem if we double
- // them, so not going to go to as much trouble as loadstart to prevent
- // that unless we find reason to.
- const eventsToTrigger = ['loadstart'];
-
- // loadedmetadata: newly equal to HAVE_METADATA (1) or greater
- eventsToTrigger.push('loadedmetadata');
-
- // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
- if (el.readyState >= 2) {
- eventsToTrigger.push('loadeddata');
- }
-
- // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
- if (el.readyState >= 3) {
- eventsToTrigger.push('canplay');
- }
-
- // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
- if (el.readyState >= 4) {
- eventsToTrigger.push('canplaythrough');
- }
-
- // We still need to give the player time to add event listeners
- this.ready(function () {
- eventsToTrigger.forEach(function (type) {
- this.trigger(type);
- }, this);
- });
- }
-
- /**
- * Set whether we are scrubbing or not.
- * This is used to decide whether we should use `fastSeek` or not.
- * `fastSeek` is used to provide trick play on Safari browsers.
- *
- * @param {boolean} isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- */
- setScrubbing(isScrubbing) {
- this.isScrubbing_ = isScrubbing;
- }
-
- /**
- * Get whether we are scrubbing or not.
- *
- * @return {boolean} isScrubbing
- * - true for we are currently scrubbing
- * - false for we are no longer scrubbing
- */
- scrubbing() {
- return this.isScrubbing_;
- }
-
- /**
- * Set current time for the `HTML5` tech.
- *
- * @param {number} seconds
- * Set the current time of the media to this.
- */
- setCurrentTime(seconds) {
- try {
- if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
- this.el_.fastSeek(seconds);
- } else {
- this.el_.currentTime = seconds;
- }
- } catch (e) {
- log$1(e, 'Video is not ready. (Video.js)');
- // this.warning(VideoJS.warnings.videoNotReady);
- }
- }
-
- /**
- * Get the current duration of the HTML5 media element.
- *
- * @return {number}
- * The duration of the media or 0 if there is no duration.
- */
- duration() {
- // Android Chrome will report duration as Infinity for VOD HLS until after
- // playback has started, which triggers the live display erroneously.
- // Return NaN if playback has not started and trigger a durationupdate once
- // the duration can be reliably known.
- if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
- // Wait for the first `timeupdate` with currentTime > 0 - there may be
- // several with 0
- const checkProgress = () => {
- if (this.el_.currentTime > 0) {
- // Trigger durationchange for genuinely live video
- if (this.el_.duration === Infinity) {
- this.trigger('durationchange');
- }
- this.off('timeupdate', checkProgress);
- }
- };
- this.on('timeupdate', checkProgress);
- return NaN;
- }
- return this.el_.duration || NaN;
- }
-
- /**
- * Get the current width of the HTML5 media element.
- *
- * @return {number}
- * The width of the HTML5 media element.
- */
- width() {
- return this.el_.offsetWidth;
- }
-
- /**
- * Get the current height of the HTML5 media element.
- *
- * @return {number}
- * The height of the HTML5 media element.
- */
- height() {
- return this.el_.offsetHeight;
- }
-
- /**
- * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
- * `fullscreenchange` event.
- *
- * @private
- * @fires fullscreenchange
- * @listens webkitendfullscreen
- * @listens webkitbeginfullscreen
- * @listens webkitbeginfullscreen
- */
- proxyWebkitFullscreen_() {
- if (!('webkitDisplayingFullscreen' in this.el_)) {
- return;
- }
- const endFn = function () {
- this.trigger('fullscreenchange', {
- isFullscreen: false
- });
- // Safari will sometimes set controls on the videoelement when existing fullscreen.
- if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
- this.el_.controls = false;
- }
- };
- const beginFn = function () {
- if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
- this.one('webkitendfullscreen', endFn);
- this.trigger('fullscreenchange', {
- isFullscreen: true,
- // set a flag in case another tech triggers fullscreenchange
- nativeIOSFullscreen: true
- });
- }
- };
- this.on('webkitbeginfullscreen', beginFn);
- this.on('dispose', () => {
- this.off('webkitbeginfullscreen', beginFn);
- this.off('webkitendfullscreen', endFn);
- });
- }
-
- /**
- * Check if fullscreen is supported on the video el.
- *
- * @return {boolean}
- * - True if fullscreen is supported.
- * - False if fullscreen is not supported.
- */
- supportsFullScreen() {
- return typeof this.el_.webkitEnterFullScreen === 'function';
- }
-
- /**
- * Request that the `HTML5` Tech enter fullscreen.
- */
- enterFullScreen() {
- const video = this.el_;
- if (video.paused && video.networkState <= video.HAVE_METADATA) {
- // attempt to prime the video element for programmatic access
- // this isn't necessary on the desktop but shouldn't hurt
- silencePromise(this.el_.play());
-
- // playing and pausing synchronously during the transition to fullscreen
- // can get iOS ~6.1 devices into a play/pause loop
- this.setTimeout(function () {
- video.pause();
- try {
- video.webkitEnterFullScreen();
- } catch (e) {
- this.trigger('fullscreenerror', e);
- }
- }, 0);
- } else {
- try {
- video.webkitEnterFullScreen();
- } catch (e) {
- this.trigger('fullscreenerror', e);
- }
- }
- }
-
- /**
- * Request that the `HTML5` Tech exit fullscreen.
- */
- exitFullScreen() {
- if (!this.el_.webkitDisplayingFullscreen) {
- this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
- return;
- }
- this.el_.webkitExitFullScreen();
- }
-
- /**
- * Create a floating video window always on top of other windows so that users may
- * continue consuming media while they interact with other content sites, or
- * applications on their device.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @return {Promise}
- * A promise with a Picture-in-Picture window.
- */
- requestPictureInPicture() {
- return this.el_.requestPictureInPicture();
- }
-
- /**
- * Native requestVideoFrameCallback if supported by browser/tech, or fallback
- * Don't use rVCF on Safari when DRM is playing, as it doesn't fire
- * Needs to be checked later than the constructor
- * This will be a false positive for clear sources loaded after a Fairplay source
- *
- * @param {function} cb function to call
- * @return {number} id of request
- */
- requestVideoFrameCallback(cb) {
- if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
- return this.el_.requestVideoFrameCallback(cb);
- }
- return super.requestVideoFrameCallback(cb);
- }
-
- /**
- * Native or fallback requestVideoFrameCallback
- *
- * @param {number} id request id to cancel
- */
- cancelVideoFrameCallback(id) {
- if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
- this.el_.cancelVideoFrameCallback(id);
- } else {
- super.cancelVideoFrameCallback(id);
- }
- }
-
- /**
- * A getter/setter for the `Html5` Tech's source object.
- * > Note: Please use {@link Html5#setSource}
- *
- * @param {Tech~SourceObject} [src]
- * The source object you want to set on the `HTML5` techs element.
- *
- * @return {Tech~SourceObject|undefined}
- * - The current source object when a source is not passed in.
- * - undefined when setting
- *
- * @deprecated Since version 5.
- */
- src(src) {
- if (src === undefined) {
- return this.el_.src;
- }
-
- // Setting src through `src` instead of `setSrc` will be deprecated
- this.setSrc(src);
- }
-
- /**
- * Reset the tech by removing all sources and then calling
- * {@link Html5.resetMediaElement}.
- */
- reset() {
- Html5.resetMediaElement(this.el_);
- }
-
- /**
- * Get the current source on the `HTML5` Tech. Falls back to returning the source from
- * the HTML5 media element.
- *
- * @return {Tech~SourceObject}
- * The current source object from the HTML5 tech. With a fallback to the
- * elements source.
- */
- currentSrc() {
- if (this.currentSource_) {
- return this.currentSource_.src;
- }
- return this.el_.currentSrc;
- }
-
- /**
- * Set controls attribute for the HTML5 media Element.
- *
- * @param {string} val
- * Value to set the controls attribute to
- */
- setControls(val) {
- this.el_.controls = !!val;
- }
-
- /**
- * Create and returns a remote {@link TextTrack} object.
- *
- * @param {string} kind
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
- *
- * @param {string} [label]
- * Label to identify the text track
- *
- * @param {string} [language]
- * Two letter language abbreviation
- *
- * @return {TextTrack}
- * The TextTrack that gets created.
- */
- addTextTrack(kind, label, language) {
- if (!this.featuresNativeTextTracks) {
- return super.addTextTrack(kind, label, language);
- }
- return this.el_.addTextTrack(kind, label, language);
- }
-
- /**
- * Creates either native TextTrack or an emulated TextTrack depending
- * on the value of `featuresNativeTextTracks`
- *
- * @param {Object} options
- * The object should contain the options to initialize the TextTrack with.
- *
- * @param {string} [options.kind]
- * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
- *
- * @param {string} [options.label]
- * Label to identify the text track
- *
- * @param {string} [options.language]
- * Two letter language abbreviation.
- *
- * @param {boolean} [options.default]
- * Default this track to on.
- *
- * @param {string} [options.id]
- * The internal id to assign this track.
- *
- * @param {string} [options.src]
- * A source url for the track.
- *
- * @return {HTMLTrackElement}
- * The track element that gets created.
- */
- createRemoteTextTrack(options) {
- if (!this.featuresNativeTextTracks) {
- return super.createRemoteTextTrack(options);
- }
- const htmlTrackElement = document.createElement('track');
- if (options.kind) {
- htmlTrackElement.kind = options.kind;
- }
- if (options.label) {
- htmlTrackElement.label = options.label;
- }
- if (options.language || options.srclang) {
- htmlTrackElement.srclang = options.language || options.srclang;
- }
- if (options.default) {
- htmlTrackElement.default = options.default;
- }
- if (options.id) {
- htmlTrackElement.id = options.id;
- }
- if (options.src) {
- htmlTrackElement.src = options.src;
- }
- return htmlTrackElement;
- }
-
- /**
- * Creates a remote text track object and returns an html track element.
- *
- * @param {Object} options The object should contain values for
- * kind, language, label, and src (location of the WebVTT file)
- * @param {boolean} [manualCleanup=false] if set to true, the TextTrack
- * will not be removed from the TextTrackList and HtmlTrackElementList
- * after a source change
- * @return {HTMLTrackElement} An Html Track Element.
- * This can be an emulated {@link HTMLTrackElement} or a native one.
- *
- */
- addRemoteTextTrack(options, manualCleanup) {
- const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
- if (this.featuresNativeTextTracks) {
- this.el().appendChild(htmlTrackElement);
- }
- return htmlTrackElement;
- }
-
- /**
- * Remove remote `TextTrack` from `TextTrackList` object
- *
- * @param {TextTrack} track
- * `TextTrack` object to remove
- */
- removeRemoteTextTrack(track) {
- super.removeRemoteTextTrack(track);
- if (this.featuresNativeTextTracks) {
- const tracks = this.$$('track');
- let i = tracks.length;
- while (i--) {
- if (track === tracks[i] || track === tracks[i].track) {
- this.el().removeChild(tracks[i]);
- }
- }
- }
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object}
- * An object with supported media playback quality metrics
- */
- getVideoPlaybackQuality() {
- if (typeof this.el().getVideoPlaybackQuality === 'function') {
- return this.el().getVideoPlaybackQuality();
- }
- const videoPlaybackQuality = {};
- if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
- videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
- videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
- }
- if (window.performance) {
- videoPlaybackQuality.creationTime = window.performance.now();
- }
- return videoPlaybackQuality;
- }
- }
-
- /* HTML5 Support Testing ---------------------------------------------------- */
-
- /**
- * Element for testing browser HTML5 media capabilities
- *
- * @type {Element}
- * @constant
- * @private
- */
- defineLazyProperty(Html5, 'TEST_VID', function () {
- if (!isReal()) {
- return;
- }
- const video = document.createElement('video');
- const track = document.createElement('track');
- track.kind = 'captions';
- track.srclang = 'en';
- track.label = 'English';
- video.appendChild(track);
- return video;
- });
-
- /**
- * Check if HTML5 media is supported by this browser/device.
- *
- * @return {boolean}
- * - True if HTML5 media is supported.
- * - False if HTML5 media is not supported.
- */
- Html5.isSupported = function () {
- // IE with no Media Player is a LIAR! (#984)
- try {
- Html5.TEST_VID.volume = 0.5;
- } catch (e) {
- return false;
- }
- return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
- };
-
- /**
- * Check if the tech can support the given type
- *
- * @param {string} type
- * The mimetype to check
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- Html5.canPlayType = function (type) {
- return Html5.TEST_VID.canPlayType(type);
- };
-
- /**
- * Check if the tech can support the given source
- *
- * @param {Object} srcObj
- * The source object
- * @param {Object} options
- * The options passed to the tech
- * @return {string} 'probably', 'maybe', or '' (empty string)
- */
- Html5.canPlaySource = function (srcObj, options) {
- return Html5.canPlayType(srcObj.type);
- };
-
- /**
- * Check if the volume can be changed in this browser/device.
- * Volume cannot be changed in a lot of mobile devices.
- * Specifically, it can't be changed from 1 on iOS.
- *
- * @return {boolean}
- * - True if volume can be controlled
- * - False otherwise
- */
- Html5.canControlVolume = function () {
- // IE will error if Windows Media Player not installed #3315
- try {
- const volume = Html5.TEST_VID.volume;
- Html5.TEST_VID.volume = volume / 2 + 0.1;
- const canControl = volume !== Html5.TEST_VID.volume;
-
- // With the introduction of iOS 15, there are cases where the volume is read as
- // changed but reverts back to its original state at the start of the next tick.
- // To determine whether volume can be controlled on iOS,
- // a timeout is set and the volume is checked asynchronously.
- // Since `features` doesn't currently work asynchronously, the value is manually set.
- if (canControl && IS_IOS) {
- window.setTimeout(() => {
- if (Html5 && Html5.prototype) {
- Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
- }
- });
-
- // default iOS to false, which will be updated in the timeout above.
- return false;
- }
- return canControl;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if the volume can be muted in this browser/device.
- * Some devices, e.g. iOS, don't allow changing volume
- * but permits muting/unmuting.
- *
- * @return {boolean}
- * - True if volume can be muted
- * - False otherwise
- */
- Html5.canMuteVolume = function () {
- try {
- const muted = Html5.TEST_VID.muted;
-
- // in some versions of iOS muted property doesn't always
- // work, so we want to set both property and attribute
- Html5.TEST_VID.muted = !muted;
- if (Html5.TEST_VID.muted) {
- setAttribute(Html5.TEST_VID, 'muted', 'muted');
- } else {
- removeAttribute(Html5.TEST_VID, 'muted', 'muted');
- }
- return muted !== Html5.TEST_VID.muted;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if the playback rate can be changed in this browser/device.
- *
- * @return {boolean}
- * - True if playback rate can be controlled
- * - False otherwise
- */
- Html5.canControlPlaybackRate = function () {
- // Playback rate API is implemented in Android Chrome, but doesn't do anything
- // https://github.com/videojs/video.js/issues/3180
- if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
- return false;
- }
- // IE will error if Windows Media Player not installed #3315
- try {
- const playbackRate = Html5.TEST_VID.playbackRate;
- Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
- return playbackRate !== Html5.TEST_VID.playbackRate;
- } catch (e) {
- return false;
- }
- };
-
- /**
- * Check if we can override a video/audio elements attributes, with
- * Object.defineProperty.
- *
- * @return {boolean}
- * - True if builtin attributes can be overridden
- * - False otherwise
- */
- Html5.canOverrideAttributes = function () {
- // if we cannot overwrite the src/innerHTML property, there is no support
- // iOS 7 safari for instance cannot do this.
- try {
- const noop = () => {};
- Object.defineProperty(document.createElement('video'), 'src', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('audio'), 'src', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('video'), 'innerHTML', {
- get: noop,
- set: noop
- });
- Object.defineProperty(document.createElement('audio'), 'innerHTML', {
- get: noop,
- set: noop
- });
- } catch (e) {
- return false;
- }
- return true;
- };
-
- /**
- * Check to see if native `TextTrack`s are supported by this browser/device.
- *
- * @return {boolean}
- * - True if native `TextTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeTextTracks = function () {
- return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
- };
-
- /**
- * Check to see if native `VideoTrack`s are supported by this browser/device
- *
- * @return {boolean}
- * - True if native `VideoTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeVideoTracks = function () {
- return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
- };
-
- /**
- * Check to see if native `AudioTrack`s are supported by this browser/device
- *
- * @return {boolean}
- * - True if native `AudioTrack`s are supported.
- * - False otherwise
- */
- Html5.supportsNativeAudioTracks = function () {
- return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
- };
-
- /**
- * An array of events available on the Html5 tech.
- *
- * @private
- * @type {Array}
- */
- Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange'];
-
- /**
- * Boolean indicating whether the `Tech` supports volume control.
- *
- * @type {boolean}
- * @default {@link Html5.canControlVolume}
- */
- /**
- * Boolean indicating whether the `Tech` supports muting volume.
- *
- * @type {boolean}
- * @default {@link Html5.canMuteVolume}
- */
-
- /**
- * Boolean indicating whether the `Tech` supports changing the speed at which the media
- * plays. Examples:
- * - Set player to play 2x (twice) as fast
- * - Set player to play 0.5x (half) as fast
- *
- * @type {boolean}
- * @default {@link Html5.canControlPlaybackRate}
- */
-
- /**
- * Boolean indicating whether the `Tech` supports the `sourceset` event.
- *
- * @type {boolean}
- * @default
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeTextTracks}
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeVideoTracks}
- */
- /**
- * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
- *
- * @type {boolean}
- * @default {@link Html5.supportsNativeAudioTracks}
- */
- [['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
- defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
- });
- Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the media element
- * moving in the DOM. iOS breaks if you move the media element, so this is set this to
- * false there. Everywhere else this should be true.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.movingMediaElementInDOM = !IS_IOS;
-
- // TODO: Previous comment: No longer appears to be used. Can probably be removed.
- // Is this true?
- /**
- * Boolean indicating whether the `HTML5` tech currently supports automatic media resize
- * when going into fullscreen.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.featuresFullscreenResize = true;
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the progress event.
- * If this is false, manual `progress` events will be triggered instead.
- *
- * @type {boolean}
- * @default
- */
- Html5.prototype.featuresProgressEvents = true;
-
- /**
- * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
- * If this is false, manual `timeupdate` events will be triggered instead.
- *
- * @default
- */
- Html5.prototype.featuresTimeupdateEvents = true;
-
- /**
- * Whether the HTML5 el supports `requestVideoFrameCallback`
- *
- * @type {boolean}
- */
- Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
- Html5.disposeMediaElement = function (el) {
- if (!el) {
- return;
- }
- if (el.parentNode) {
- el.parentNode.removeChild(el);
- }
-
- // remove any child track or source nodes to prevent their loading
- while (el.hasChildNodes()) {
- el.removeChild(el.firstChild);
- }
-
- // remove any src reference. not setting `src=''` because that causes a warning
- // in firefox
- el.removeAttribute('src');
-
- // force the media element to update its loading state by calling load()
- // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
- if (typeof el.load === 'function') {
- // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
- (function () {
- try {
- el.load();
- } catch (e) {
- // not supported
- }
- })();
- }
- };
- Html5.resetMediaElement = function (el) {
- if (!el) {
- return;
- }
- const sources = el.querySelectorAll('source');
- let i = sources.length;
- while (i--) {
- el.removeChild(sources[i]);
- }
-
- // remove any src reference.
- // not setting `src=''` because that throws an error
- el.removeAttribute('src');
- if (typeof el.load === 'function') {
- // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
- (function () {
- try {
- el.load();
- } catch (e) {
- // satisfy linter
- }
- })();
- }
- };
-
- /* Native HTML5 element property wrapping ----------------------------------- */
- // Wrap native boolean attributes with getters that check both property and attribute
- // The list is as followed:
- // muted, defaultMuted, autoplay, controls, loop, playsinline
- [
- /**
- * Get the value of `muted` from the media element. `muted` indicates
- * that the volume for the media should be set to silent. This does not actually change
- * the `volume` attribute.
- *
- * @method Html5#muted
- * @return {boolean}
- * - True if the value of `volume` should be ignored and the audio set to silent.
- * - False if the value of `volume` should be used.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
- */
- 'muted',
- /**
- * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
- * whether the media should start muted or not. Only changes the default state of the
- * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
- * current state.
- *
- * @method Html5#defaultMuted
- * @return {boolean}
- * - The value of `defaultMuted` from the media element.
- * - True indicates that the media should start muted.
- * - False indicates that the media should not start muted
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
- */
- 'defaultMuted',
- /**
- * Get the value of `autoplay` from the media element. `autoplay` indicates
- * that the media should start to play as soon as the page is ready.
- *
- * @method Html5#autoplay
- * @return {boolean}
- * - The value of `autoplay` from the media element.
- * - True indicates that the media should start as soon as the page loads.
- * - False indicates that the media should not start as soon as the page loads.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
- */
- 'autoplay',
- /**
- * Get the value of `controls` from the media element. `controls` indicates
- * whether the native media controls should be shown or hidden.
- *
- * @method Html5#controls
- * @return {boolean}
- * - The value of `controls` from the media element.
- * - True indicates that native controls should be showing.
- * - False indicates that native controls should be hidden.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
- */
- 'controls',
- /**
- * Get the value of `loop` from the media element. `loop` indicates
- * that the media should return to the start of the media and continue playing once
- * it reaches the end.
- *
- * @method Html5#loop
- * @return {boolean}
- * - The value of `loop` from the media element.
- * - True indicates that playback should seek back to start once
- * the end of a media is reached.
- * - False indicates that playback should not loop back to the start when the
- * end of the media is reached.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
- */
- 'loop',
- /**
- * Get the value of `playsinline` from the media element. `playsinline` indicates
- * to the browser that non-fullscreen playback is preferred when fullscreen
- * playback is the native default, such as in iOS Safari.
- *
- * @method Html5#playsinline
- * @return {boolean}
- * - The value of `playsinline` from the media element.
- * - True indicates that the media should play inline.
- * - False indicates that the media should not play inline.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- 'playsinline'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop] || this.el_.hasAttribute(prop);
- };
- });
-
- // Wrap native boolean attributes with setters that set both property and attribute
- // The list is as followed:
- // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
- // setControls is special-cased above
- [
- /**
- * Set the value of `muted` on the media element. `muted` indicates that the current
- * audio level should be silent.
- *
- * @method Html5#setMuted
- * @param {boolean} muted
- * - True if the audio should be set to silent
- * - False otherwise
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
- */
- 'muted',
- /**
- * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
- * audio level should be silent, but will only effect the muted level on initial playback..
- *
- * @method Html5.prototype.setDefaultMuted
- * @param {boolean} defaultMuted
- * - True if the audio should be set to silent
- * - False otherwise
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
- */
- 'defaultMuted',
- /**
- * Set the value of `autoplay` on the media element. `autoplay` indicates
- * that the media should start to play as soon as the page is ready.
- *
- * @method Html5#setAutoplay
- * @param {boolean} autoplay
- * - True indicates that the media should start as soon as the page loads.
- * - False indicates that the media should not start as soon as the page loads.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
- */
- 'autoplay',
- /**
- * Set the value of `loop` on the media element. `loop` indicates
- * that the media should return to the start of the media and continue playing once
- * it reaches the end.
- *
- * @method Html5#setLoop
- * @param {boolean} loop
- * - True indicates that playback should seek back to start once
- * the end of a media is reached.
- * - False indicates that playback should not loop back to the start when the
- * end of the media is reached.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
- */
- 'loop',
- /**
- * Set the value of `playsinline` from the media element. `playsinline` indicates
- * to the browser that non-fullscreen playback is preferred when fullscreen
- * playback is the native default, such as in iOS Safari.
- *
- * @method Html5#setPlaysinline
- * @param {boolean} playsinline
- * - True indicates that the media should play inline.
- * - False indicates that the media should not play inline.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- 'playsinline'].forEach(function (prop) {
- Html5.prototype['set' + toTitleCase$1(prop)] = function (v) {
- this.el_[prop] = v;
- if (v) {
- this.el_.setAttribute(prop, prop);
- } else {
- this.el_.removeAttribute(prop);
- }
- };
- });
-
- // Wrap native properties with a getter
- // The list is as followed
- // paused, currentTime, buffered, volume, poster, preload, error, seeking
- // seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
- // played, networkState, readyState, videoWidth, videoHeight, crossOrigin
- [
- /**
- * Get the value of `paused` from the media element. `paused` indicates whether the media element
- * is currently paused or not.
- *
- * @method Html5#paused
- * @return {boolean}
- * The value of `paused` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
- */
- 'paused',
- /**
- * Get the value of `currentTime` from the media element. `currentTime` indicates
- * the current second that the media is at in playback.
- *
- * @method Html5#currentTime
- * @return {number}
- * The value of `currentTime` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
- */
- 'currentTime',
- /**
- * Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
- * object that represents the parts of the media that are already downloaded and
- * available for playback.
- *
- * @method Html5#buffered
- * @return {TimeRange}
- * The value of `buffered` from the media element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
- */
- 'buffered',
- /**
- * Get the value of `volume` from the media element. `volume` indicates
- * the current playback volume of audio for a media. `volume` will be a value from 0
- * (silent) to 1 (loudest and default).
- *
- * @method Html5#volume
- * @return {number}
- * The value of `volume` from the media element. Value will be between 0-1.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
- */
- 'volume',
- /**
- * Get the value of `poster` from the media element. `poster` indicates
- * that the url of an image file that can/will be shown when no media data is available.
- *
- * @method Html5#poster
- * @return {string}
- * The value of `poster` from the media element. Value will be a url to an
- * image.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
- */
- 'poster',
- /**
- * Get the value of `preload` from the media element. `preload` indicates
- * what should download before the media is interacted with. It can have the following
- * values:
- * - none: nothing should be downloaded
- * - metadata: poster and the first few frames of the media may be downloaded to get
- * media dimensions and other metadata
- * - auto: allow the media and metadata for the media to be downloaded before
- * interaction
- *
- * @method Html5#preload
- * @return {string}
- * The value of `preload` from the media element. Will be 'none', 'metadata',
- * or 'auto'.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
- */
- 'preload',
- /**
- * Get the value of the `error` from the media element. `error` indicates any
- * MediaError that may have occurred during playback. If error returns null there is no
- * current error.
- *
- * @method Html5#error
- * @return {MediaError|null}
- * The value of `error` from the media element. Will be `MediaError` if there
- * is a current error and null otherwise.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
- */
- 'error',
- /**
- * Get the value of `seeking` from the media element. `seeking` indicates whether the
- * media is currently seeking to a new position or not.
- *
- * @method Html5#seeking
- * @return {boolean}
- * - The value of `seeking` from the media element.
- * - True indicates that the media is currently seeking to a new position.
- * - False indicates that the media is not seeking to a new position at this time.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
- */
- 'seeking',
- /**
- * Get the value of `seekable` from the media element. `seekable` returns a
- * `TimeRange` object indicating ranges of time that can currently be `seeked` to.
- *
- * @method Html5#seekable
- * @return {TimeRange}
- * The value of `seekable` from the media element. A `TimeRange` object
- * indicating the current ranges of time that can be seeked to.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
- */
- 'seekable',
- /**
- * Get the value of `ended` from the media element. `ended` indicates whether
- * the media has reached the end or not.
- *
- * @method Html5#ended
- * @return {boolean}
- * - The value of `ended` from the media element.
- * - True indicates that the media has ended.
- * - False indicates that the media has not ended.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
- */
- 'ended',
- /**
- * Get the value of `playbackRate` from the media element. `playbackRate` indicates
- * the rate at which the media is currently playing back. Examples:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5#playbackRate
- * @return {number}
- * The value of `playbackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'playbackRate',
- /**
- * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
- * the rate at which the media is currently playing back. This value will not indicate the current
- * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
- *
- * Examples:
- * - if defaultPlaybackRate is set to 2, media will play twice as fast.
- * - if defaultPlaybackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5.prototype.defaultPlaybackRate
- * @return {number}
- * The value of `defaultPlaybackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'defaultPlaybackRate',
- /**
- * Get the value of 'disablePictureInPicture' from the video element.
- *
- * @method Html5#disablePictureInPicture
- * @return {boolean} value
- * - The value of `disablePictureInPicture` from the video element.
- * - True indicates that the video can't be played in Picture-In-Picture mode
- * - False indicates that the video can be played in Picture-In-Picture mode
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
- */
- 'disablePictureInPicture',
- /**
- * Get the value of `played` from the media element. `played` returns a `TimeRange`
- * object representing points in the media timeline that have been played.
- *
- * @method Html5#played
- * @return {TimeRange}
- * The value of `played` from the media element. A `TimeRange` object indicating
- * the ranges of time that have been played.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
- */
- 'played',
- /**
- * Get the value of `networkState` from the media element. `networkState` indicates
- * the current network state. It returns an enumeration from the following list:
- * - 0: NETWORK_EMPTY
- * - 1: NETWORK_IDLE
- * - 2: NETWORK_LOADING
- * - 3: NETWORK_NO_SOURCE
- *
- * @method Html5#networkState
- * @return {number}
- * The value of `networkState` from the media element. This will be a number
- * from the list in the description.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
- */
- 'networkState',
- /**
- * Get the value of `readyState` from the media element. `readyState` indicates
- * the current state of the media element. It returns an enumeration from the
- * following list:
- * - 0: HAVE_NOTHING
- * - 1: HAVE_METADATA
- * - 2: HAVE_CURRENT_DATA
- * - 3: HAVE_FUTURE_DATA
- * - 4: HAVE_ENOUGH_DATA
- *
- * @method Html5#readyState
- * @return {number}
- * The value of `readyState` from the media element. This will be a number
- * from the list in the description.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
- */
- 'readyState',
- /**
- * Get the value of `videoWidth` from the video element. `videoWidth` indicates
- * the current width of the video in css pixels.
- *
- * @method Html5#videoWidth
- * @return {number}
- * The value of `videoWidth` from the video element. This will be a number
- * in css pixels.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
- */
- 'videoWidth',
- /**
- * Get the value of `videoHeight` from the video element. `videoHeight` indicates
- * the current height of the video in css pixels.
- *
- * @method Html5#videoHeight
- * @return {number}
- * The value of `videoHeight` from the video element. This will be a number
- * in css pixels.
- *
- * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
- */
- 'videoHeight',
- /**
- * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
- * to the browser that should sent the cookies along with the requests for the
- * different assets/playlists
- *
- * @method Html5#crossOrigin
- * @return {string}
- * - anonymous indicates that the media should not sent cookies.
- * - use-credentials indicates that the media should sent cookies along the requests.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
- */
- 'crossOrigin'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop];
- };
- });
-
- // Wrap native properties with a setter in this format:
- // set + toTitleCase(name)
- // The list is as follows:
- // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
- // setDisablePictureInPicture, setCrossOrigin
- [
- /**
- * Set the value of `volume` on the media element. `volume` indicates the current
- * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
- * so on.
- *
- * @method Html5#setVolume
- * @param {number} percentAsDecimal
- * The volume percent as a decimal. Valid range is from 0-1.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
- */
- 'volume',
- /**
- * Set the value of `src` on the media element. `src` indicates the current
- * {@link Tech~SourceObject} for the media.
- *
- * @method Html5#setSrc
- * @param {Tech~SourceObject} src
- * The source object to set as the current source.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
- */
- 'src',
- /**
- * Set the value of `poster` on the media element. `poster` is the url to
- * an image file that can/will be shown when no media data is available.
- *
- * @method Html5#setPoster
- * @param {string} poster
- * The url to an image that should be used as the `poster` for the media
- * element.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
- */
- 'poster',
- /**
- * Set the value of `preload` on the media element. `preload` indicates
- * what should download before the media is interacted with. It can have the following
- * values:
- * - none: nothing should be downloaded
- * - metadata: poster and the first few frames of the media may be downloaded to get
- * media dimensions and other metadata
- * - auto: allow the media and metadata for the media to be downloaded before
- * interaction
- *
- * @method Html5#setPreload
- * @param {string} preload
- * The value of `preload` to set on the media element. Must be 'none', 'metadata',
- * or 'auto'.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
- */
- 'preload',
- /**
- * Set the value of `playbackRate` on the media element. `playbackRate` indicates
- * the rate at which the media should play back. Examples:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5#setPlaybackRate
- * @return {number}
- * The value of `playbackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
- */
- 'playbackRate',
- /**
- * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
- * the rate at which the media should play back upon initial startup. Changing this value
- * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
- *
- * Example Values:
- * - if playbackRate is set to 2, media will play twice as fast.
- * - if playbackRate is set to 0.5, media will play half as fast.
- *
- * @method Html5.prototype.setDefaultPlaybackRate
- * @return {number}
- * The value of `defaultPlaybackRate` from the media element. A number indicating
- * the current playback speed of the media, where 1 is normal speed.
- *
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
- */
- 'defaultPlaybackRate',
- /**
- * Prevents the browser from suggesting a Picture-in-Picture context menu
- * or to request Picture-in-Picture automatically in some cases.
- *
- * @method Html5#setDisablePictureInPicture
- * @param {boolean} value
- * The true value will disable Picture-in-Picture mode.
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
- */
- 'disablePictureInPicture',
- /**
- * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
- * to the browser that should sent the cookies along with the requests for the
- * different assets/playlists
- *
- * @method Html5#setCrossOrigin
- * @param {string} crossOrigin
- * - anonymous indicates that the media should not sent cookies.
- * - use-credentials indicates that the media should sent cookies along the requests.
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
- */
- 'crossOrigin'].forEach(function (prop) {
- Html5.prototype['set' + toTitleCase$1(prop)] = function (v) {
- this.el_[prop] = v;
- };
- });
-
- // wrap native functions with a function
- // The list is as follows:
- // pause, load, play
- [
- /**
- * A wrapper around the media elements `pause` function. This will call the `HTML5`
- * media elements `pause` function.
- *
- * @method Html5#pause
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
- */
- 'pause',
- /**
- * A wrapper around the media elements `load` function. This will call the `HTML5`s
- * media element `load` function.
- *
- * @method Html5#load
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
- */
- 'load',
- /**
- * A wrapper around the media elements `play` function. This will call the `HTML5`s
- * media element `play` function.
- *
- * @method Html5#play
- * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
- */
- 'play'].forEach(function (prop) {
- Html5.prototype[prop] = function () {
- return this.el_[prop]();
- };
- });
- Tech.withSourceHandlers(Html5);
-
- /**
- * Native source handler for Html5, simply passes the source to the media element.
- *
- * @property {Tech~SourceObject} source
- * The source object
- *
- * @property {Html5} tech
- * The instance of the HTML5 tech.
- */
- Html5.nativeSourceHandler = {};
-
- /**
- * Check if the media element can play the given mime type.
- *
- * @param {string} type
- * The mimetype to check
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- Html5.nativeSourceHandler.canPlayType = function (type) {
- // IE without MediaPlayer throws an error (#519)
- try {
- return Html5.TEST_VID.canPlayType(type);
- } catch (e) {
- return '';
- }
- };
-
- /**
- * Check if the media element can handle a source natively.
- *
- * @param {Tech~SourceObject} source
- * The source object
- *
- * @param {Object} [options]
- * Options to be passed to the tech.
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string).
- */
- Html5.nativeSourceHandler.canHandleSource = function (source, options) {
- // If a type was provided we should rely on that
- if (source.type) {
- return Html5.nativeSourceHandler.canPlayType(source.type);
-
- // If no type, fall back to checking 'video/[EXTENSION]'
- } else if (source.src) {
- const ext = getFileExtension(source.src);
- return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
- }
- return '';
- };
-
- /**
- * Pass the source to the native media element.
- *
- * @param {Tech~SourceObject} source
- * The source object
- *
- * @param {Html5} tech
- * The instance of the Html5 tech
- *
- * @param {Object} [options]
- * The options to pass to the source
- */
- Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
- tech.setSrc(source.src);
- };
-
- /**
- * A noop for the native dispose function, as cleanup is not needed.
- */
- Html5.nativeSourceHandler.dispose = function () {};
-
- // Register the native source handler
- Html5.registerSourceHandler(Html5.nativeSourceHandler);
- Tech.registerTech('Html5', Html5);
-
- /**
- * @file player.js
- */
-
- // The following tech events are simply re-triggered
- // on the player when they happen
- const TECH_EVENTS_RETRIGGER = [
- /**
- * Fired while the user agent is downloading media data.
- *
- * @event Player#progress
- * @type {Event}
- */
- /**
- * Retrigger the `progress` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechProgress_
- * @fires Player#progress
- * @listens Tech#progress
- */
- 'progress',
- /**
- * Fires when the loading of an audio/video is aborted.
- *
- * @event Player#abort
- * @type {Event}
- */
- /**
- * Retrigger the `abort` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechAbort_
- * @fires Player#abort
- * @listens Tech#abort
- */
- 'abort',
- /**
- * Fires when the browser is intentionally not getting media data.
- *
- * @event Player#suspend
- * @type {Event}
- */
- /**
- * Retrigger the `suspend` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechSuspend_
- * @fires Player#suspend
- * @listens Tech#suspend
- */
- 'suspend',
- /**
- * Fires when the current playlist is empty.
- *
- * @event Player#emptied
- * @type {Event}
- */
- /**
- * Retrigger the `emptied` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechEmptied_
- * @fires Player#emptied
- * @listens Tech#emptied
- */
- 'emptied',
- /**
- * Fires when the browser is trying to get media data, but data is not available.
- *
- * @event Player#stalled
- * @type {Event}
- */
- /**
- * Retrigger the `stalled` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechStalled_
- * @fires Player#stalled
- * @listens Tech#stalled
- */
- 'stalled',
- /**
- * Fires when the browser has loaded meta data for the audio/video.
- *
- * @event Player#loadedmetadata
- * @type {Event}
- */
- /**
- * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechLoadedmetadata_
- * @fires Player#loadedmetadata
- * @listens Tech#loadedmetadata
- */
- 'loadedmetadata',
- /**
- * Fires when the browser has loaded the current frame of the audio/video.
- *
- * @event Player#loadeddata
- * @type {event}
- */
- /**
- * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechLoaddeddata_
- * @fires Player#loadeddata
- * @listens Tech#loadeddata
- */
- 'loadeddata',
- /**
- * Fires when the current playback position has changed.
- *
- * @event Player#timeupdate
- * @type {event}
- */
- /**
- * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechTimeUpdate_
- * @fires Player#timeupdate
- * @listens Tech#timeupdate
- */
- 'timeupdate',
- /**
- * Fires when the video's intrinsic dimensions change
- *
- * @event Player#resize
- * @type {event}
- */
- /**
- * Retrigger the `resize` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechResize_
- * @fires Player#resize
- * @listens Tech#resize
- */
- 'resize',
- /**
- * Fires when the volume has been changed
- *
- * @event Player#volumechange
- * @type {event}
- */
- /**
- * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechVolumechange_
- * @fires Player#volumechange
- * @listens Tech#volumechange
- */
- 'volumechange',
- /**
- * Fires when the text track has been changed
- *
- * @event Player#texttrackchange
- * @type {event}
- */
- /**
- * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
- *
- * @private
- * @method Player#handleTechTexttrackchange_
- * @fires Player#texttrackchange
- * @listens Tech#texttrackchange
- */
- 'texttrackchange'];
-
- // events to queue when playback rate is zero
- // this is a hash for the sole purpose of mapping non-camel-cased event names
- // to camel-cased function names
- const TECH_EVENTS_QUEUE = {
- canplay: 'CanPlay',
- canplaythrough: 'CanPlayThrough',
- playing: 'Playing',
- seeked: 'Seeked'
- };
- const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
- const BREAKPOINT_CLASSES = {};
-
- // grep: vjs-layout-tiny
- // grep: vjs-layout-x-small
- // grep: vjs-layout-small
- // grep: vjs-layout-medium
- // grep: vjs-layout-large
- // grep: vjs-layout-x-large
- // grep: vjs-layout-huge
- BREAKPOINT_ORDER.forEach(k => {
- const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
- BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
- });
- const DEFAULT_BREAKPOINTS = {
- tiny: 210,
- xsmall: 320,
- small: 425,
- medium: 768,
- large: 1440,
- xlarge: 2560,
- huge: Infinity
- };
-
- /**
- * An instance of the `Player` class is created when any of the Video.js setup methods
- * are used to initialize a video.
- *
- * After an instance has been created it can be accessed globally in three ways:
- * 1. By calling `videojs.getPlayer('example_video_1');`
- * 2. By calling `videojs('example_video_1');` (not recommended)
- * 2. By using it directly via `videojs.players.example_video_1;`
- *
- * @extends Component
- * @global
- */
- class Player extends Component$1 {
- /**
- * Create an instance of this class.
- *
- * @param {Element} tag
- * The original video DOM element used for configuring options.
- *
- * @param {Object} [options]
- * Object of option names and values.
- *
- * @param {Function} [ready]
- * Ready callback function.
- */
- constructor(tag, options, ready) {
- // Make sure tag ID exists
- // also here.. probably better
- tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
-
- // Set Options
- // The options argument overrides options set in the video tag
- // which overrides globally set options.
- // This latter part coincides with the load order
- // (tag must exist before Player)
- options = Object.assign(Player.getTagSettings(tag), options);
-
- // Delay the initialization of children because we need to set up
- // player properties first, and can't use `this` before `super()`
- options.initChildren = false;
-
- // Same with creating the element
- options.createEl = false;
-
- // don't auto mixin the evented mixin
- options.evented = false;
-
- // we don't want the player to report touch activity on itself
- // see enableTouchActivity in Component
- options.reportTouchActivity = false;
-
- // If language is not set, get the closest lang attribute
- if (!options.language) {
- const closest = tag.closest('[lang]');
- if (closest) {
- options.language = closest.getAttribute('lang');
- }
- }
-
- // Run base component initializing with new options
- super(null, options, ready);
-
- // Create bound methods for document listeners.
- this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
- this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
- this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
- this.boundApplyInitTime_ = e => this.applyInitTime_(e);
- this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
- this.boundHandleTechClick_ = e => this.handleTechClick_(e);
- this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
- this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
- this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
- this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
- this.boundHandleTechTap_ = e => this.handleTechTap_(e);
-
- // default isFullscreen_ to false
- this.isFullscreen_ = false;
-
- // create logger
- this.log = createLogger(this.id_);
-
- // Hold our own reference to fullscreen api so it can be mocked in tests
- this.fsApi_ = FullscreenApi;
-
- // Tracks when a tech changes the poster
- this.isPosterFromTech_ = false;
-
- // Holds callback info that gets queued when playback rate is zero
- // and a seek is happening
- this.queuedCallbacks_ = [];
-
- // Turn off API access because we're loading a new tech that might load asynchronously
- this.isReady_ = false;
-
- // Init state hasStarted_
- this.hasStarted_ = false;
-
- // Init state userActive_
- this.userActive_ = false;
-
- // Init debugEnabled_
- this.debugEnabled_ = false;
-
- // Init state audioOnlyMode_
- this.audioOnlyMode_ = false;
-
- // Init state audioPosterMode_
- this.audioPosterMode_ = false;
-
- // Init state audioOnlyCache_
- this.audioOnlyCache_ = {
- playerHeight: null,
- hiddenChildren: []
- };
-
- // if the global option object was accidentally blown away by
- // someone, bail early with an informative error
- if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
- throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
- }
-
- // Store the original tag used to set options
- this.tag = tag;
-
- // Store the tag attributes used to restore html5 element
- this.tagAttributes = tag && getAttributes(tag);
-
- // Update current language
- this.language(this.options_.language);
-
- // Update Supported Languages
- if (options.languages) {
- // Normalise player option languages to lowercase
- const languagesToLower = {};
- Object.getOwnPropertyNames(options.languages).forEach(function (name) {
- languagesToLower[name.toLowerCase()] = options.languages[name];
- });
- this.languages_ = languagesToLower;
- } else {
- this.languages_ = Player.prototype.options_.languages;
- }
- this.resetCache_();
-
- // Set poster
- /** @type string */
- this.poster_ = options.poster || '';
-
- // Set controls
- /** @type {boolean} */
- this.controls_ = !!options.controls;
-
- // Original tag settings stored in options
- // now remove immediately so native controls don't flash.
- // May be turned back on by HTML5 tech if nativeControlsForTouch is true
- tag.controls = false;
- tag.removeAttribute('controls');
- this.changingSrc_ = false;
- this.playCallbacks_ = [];
- this.playTerminatedQueue_ = [];
-
- // the attribute overrides the option
- if (tag.hasAttribute('autoplay')) {
- this.autoplay(true);
- } else {
- // otherwise use the setter to validate and
- // set the correct value.
- this.autoplay(this.options_.autoplay);
- }
-
- // check plugins
- if (options.plugins) {
- Object.keys(options.plugins).forEach(name => {
- if (typeof this[name] !== 'function') {
- throw new Error(`plugin "${name}" does not exist`);
- }
- });
- }
-
- /*
- * Store the internal state of scrubbing
- *
- * @private
- * @return {Boolean} True if the user is scrubbing
- */
- this.scrubbing_ = false;
- this.el_ = this.createEl();
-
- // Make this an evented object and use `el_` as its event bus.
- evented(this, {
- eventBusKey: 'el_'
- });
-
- // listen to document and player fullscreenchange handlers so we receive those events
- // before a user can receive them so we can update isFullscreen appropriately.
- // make sure that we listen to fullscreenchange events before everything else to make sure that
- // our isFullscreen method is updated properly for internal components as well as external.
- if (this.fsApi_.requestFullscreen) {
- on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- }
- if (this.fluid_) {
- this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- }
- // We also want to pass the original player options to each component and plugin
- // as well so they don't need to reach back into the player for options later.
- // We also need to do another copy of this.options_ so we don't end up with
- // an infinite loop.
- const playerOptionsCopy = merge$2(this.options_);
-
- // Load plugins
- if (options.plugins) {
- Object.keys(options.plugins).forEach(name => {
- this[name](options.plugins[name]);
- });
- }
-
- // Enable debug mode to fire debugon event for all plugins.
- if (options.debug) {
- this.debug(true);
- }
- this.options_.playerOptions = playerOptionsCopy;
- this.middleware_ = [];
- this.playbackRates(options.playbackRates);
- if (options.experimentalSvgIcons) {
- // Add SVG Sprite to the DOM
- const parser = new window.DOMParser();
- const parsedSVG = parser.parseFromString(icons, 'image/svg+xml');
- const errorNode = parsedSVG.querySelector('parsererror');
- if (errorNode) {
- log$1.warn('Failed to load SVG Icons. Falling back to Font Icons.');
- this.options_.experimentalSvgIcons = null;
- } else {
- const sprite = parsedSVG.documentElement;
- sprite.style.display = 'none';
- this.el_.appendChild(sprite);
- this.addClass('vjs-svg-icons-enabled');
- }
- }
- this.initChildren();
-
- // Set isAudio based on whether or not an audio tag was used
- this.isAudio(tag.nodeName.toLowerCase() === 'audio');
-
- // Update controls className. Can't do this when the controls are initially
- // set because the element doesn't exist yet.
- if (this.controls()) {
- this.addClass('vjs-controls-enabled');
- } else {
- this.addClass('vjs-controls-disabled');
- }
-
- // Set ARIA label and region role depending on player type
- this.el_.setAttribute('role', 'region');
- if (this.isAudio()) {
- this.el_.setAttribute('aria-label', this.localize('Audio Player'));
- } else {
- this.el_.setAttribute('aria-label', this.localize('Video Player'));
- }
- if (this.isAudio()) {
- this.addClass('vjs-audio');
- }
-
- // TODO: Make this smarter. Toggle user state between touching/mousing
- // using events, since devices can have both touch and mouse events.
- // TODO: Make this check be performed again when the window switches between monitors
- // (See https://github.com/videojs/video.js/issues/5683)
- if (TOUCH_ENABLED) {
- this.addClass('vjs-touch-enabled');
- }
-
- // iOS Safari has broken hover handling
- if (!IS_IOS) {
- this.addClass('vjs-workinghover');
- }
-
- // Make player easily findable by ID
- Player.players[this.id_] = this;
-
- // Add a major version class to aid css in plugins
- const majorVersion = version$5.split('.')[0];
- this.addClass(`vjs-v${majorVersion}`);
-
- // When the player is first initialized, trigger activity so components
- // like the control bar show themselves if needed
- this.userActive(true);
- this.reportUserActivity();
- this.one('play', e => this.listenForUserActivity_(e));
- this.on('keydown', e => this.handleKeyDown(e));
- this.on('languagechange', e => this.handleLanguagechange(e));
- this.breakpoints(this.options_.breakpoints);
- this.responsive(this.options_.responsive);
-
- // Calling both the audio mode methods after the player is fully
- // setup to be able to listen to the events triggered by them
- this.on('ready', () => {
- // Calling the audioPosterMode method first so that
- // the audioOnlyMode can take precedence when both options are set to true
- this.audioPosterMode(this.options_.audioPosterMode);
- this.audioOnlyMode(this.options_.audioOnlyMode);
- });
- }
-
- /**
- * Destroys the video player and does any necessary cleanup.
- *
- * This is especially helpful if you are dynamically adding and removing videos
- * to/from the DOM.
- *
- * @fires Player#dispose
- */
- dispose() {
- /**
- * Called when the player is being disposed of.
- *
- * @event Player#dispose
- * @type {Event}
- */
- this.trigger('dispose');
- // prevent dispose from being called twice
- this.off('dispose');
-
- // Make sure all player-specific document listeners are unbound. This is
- off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
- off(document, 'keydown', this.boundFullWindowOnEscKey_);
- if (this.styleEl_ && this.styleEl_.parentNode) {
- this.styleEl_.parentNode.removeChild(this.styleEl_);
- this.styleEl_ = null;
- }
-
- // Kill reference to this player
- Player.players[this.id_] = null;
- if (this.tag && this.tag.player) {
- this.tag.player = null;
- }
- if (this.el_ && this.el_.player) {
- this.el_.player = null;
- }
- if (this.tech_) {
- this.tech_.dispose();
- this.isPosterFromTech_ = false;
- this.poster_ = '';
- }
- if (this.playerElIngest_) {
- this.playerElIngest_ = null;
- }
- if (this.tag) {
- this.tag = null;
- }
- clearCacheForPlayer(this);
-
- // remove all event handlers for track lists
- // all tracks and track listeners are removed on
- // tech dispose
- ALL.names.forEach(name => {
- const props = ALL[name];
- const list = this[props.getterName]();
-
- // if it is not a native list
- // we have to manually remove event listeners
- if (list && list.off) {
- list.off();
- }
- });
-
- // the actual .el_ is removed here, or replaced if
- super.dispose({
- restoreEl: this.options_.restoreEl
- });
- }
-
- /**
- * Create the `Player`'s DOM element.
- *
- * @return {Element}
- * The DOM element that gets created.
- */
- createEl() {
- let tag = this.tag;
- let el;
- let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
- const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
- if (playerElIngest) {
- el = this.el_ = tag.parentNode;
- } else if (!divEmbed) {
- el = this.el_ = super.createEl('div');
- }
-
- // Copy over all the attributes from the tag, including ID and class
- // ID will now reference player box, not the video tag
- const attrs = getAttributes(tag);
- if (divEmbed) {
- el = this.el_ = tag;
- tag = this.tag = document.createElement('video');
- while (el.children.length) {
- tag.appendChild(el.firstChild);
- }
- if (!hasClass(el, 'video-js')) {
- addClass(el, 'video-js');
- }
- el.appendChild(tag);
- playerElIngest = this.playerElIngest_ = el;
- // move properties over from our custom `video-js` element
- // to our new `video` element. This will move things like
- // `src` or `controls` that were set via js before the player
- // was initialized.
- Object.keys(el).forEach(k => {
- try {
- tag[k] = el[k];
- } catch (e) {
- // we got a a property like outerHTML which we can't actually copy, ignore it
- }
- });
- }
-
- // set tabindex to -1 to remove the video element from the focus order
- tag.setAttribute('tabindex', '-1');
- attrs.tabindex = '-1';
-
- // Workaround for #4583 on Chrome (on Windows) with JAWS.
- // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
- // Note that we can't detect if JAWS is being used, but this ARIA attribute
- // doesn't change behavior of Chrome if JAWS is not being used
- if (IS_CHROME && IS_WINDOWS) {
- tag.setAttribute('role', 'application');
- attrs.role = 'application';
- }
-
- // Remove width/height attrs from tag so CSS can make it 100% width/height
- tag.removeAttribute('width');
- tag.removeAttribute('height');
- if ('width' in attrs) {
- delete attrs.width;
- }
- if ('height' in attrs) {
- delete attrs.height;
- }
- Object.getOwnPropertyNames(attrs).forEach(function (attr) {
- // don't copy over the class attribute to the player element when we're in a div embed
- // the class is already set up properly in the divEmbed case
- // and we want to make sure that the `video-js` class doesn't get lost
- if (!(divEmbed && attr === 'class')) {
- el.setAttribute(attr, attrs[attr]);
- }
- if (divEmbed) {
- tag.setAttribute(attr, attrs[attr]);
- }
- });
-
- // Update tag id/class for use as HTML5 playback tech
- // Might think we should do this after embedding in container so .vjs-tech class
- // doesn't flash 100% width/height, but class only applies with .video-js parent
- tag.playerId = tag.id;
- tag.id += '_html5_api';
- tag.className = 'vjs-tech';
-
- // Make player findable on elements
- tag.player = el.player = this;
- // Default state of video is paused
- this.addClass('vjs-paused');
- const deviceClassNames = ['IS_SMART_TV', 'IS_TIZEN', 'IS_WEBOS', 'IS_ANDROID', 'IS_IPAD', 'IS_IPHONE'].filter(key => browser[key]).map(key => {
- return 'vjs-device-' + key.substring(3).toLowerCase().replace(/\_/g, '-');
- });
- this.addClass(...deviceClassNames);
-
- // Add a style element in the player that we'll use to set the width/height
- // of the player in a way that's still overridable by CSS, just like the
- // video element
- if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) {
- this.styleEl_ = createStyleElement('vjs-styles-dimensions');
- const defaultsStyleEl = $('.vjs-styles-defaults');
- const head = $('head');
- head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
- }
- this.fill_ = false;
- this.fluid_ = false;
-
- // Pass in the width/height/aspectRatio options which will update the style el
- this.width(this.options_.width);
- this.height(this.options_.height);
- this.fill(this.options_.fill);
- this.fluid(this.options_.fluid);
- this.aspectRatio(this.options_.aspectRatio);
- // support both crossOrigin and crossorigin to reduce confusion and issues around the name
- this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
-
- // Hide any links within the video/audio tag,
- // because IE doesn't hide them completely from screen readers.
- const links = tag.getElementsByTagName('a');
- for (let i = 0; i < links.length; i++) {
- const linkEl = links.item(i);
- addClass(linkEl, 'vjs-hidden');
- linkEl.setAttribute('hidden', 'hidden');
- }
-
- // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
- // keep track of the original for later so we can know if the source originally failed
- tag.initNetworkState_ = tag.networkState;
-
- // Wrap video tag in div (el/box) container
- if (tag.parentNode && !playerElIngest) {
- tag.parentNode.insertBefore(el, tag);
- }
-
- // insert the tag as the first child of the player element
- // then manually add it to the children array so that this.addChild
- // will work properly for other components
- //
- // Breaks iPhone, fixed in HTML5 setup.
- prependTo(tag, el);
- this.children_.unshift(tag);
-
- // Set lang attr on player to ensure CSS :lang() in consistent with player
- // if it's been set to something different to the doc
- this.el_.setAttribute('lang', this.language_);
- this.el_.setAttribute('translate', 'no');
- this.el_ = el;
- return el;
- }
-
- /**
- * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the `` tag to control the CORS
- * behavior.
- *
- * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
- *
- * @param {string|null} [value]
- * The value to set the `Player`'s crossOrigin to. If an argument is
- * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
- *
- * @return {string|null|undefined}
- * - The current crossOrigin value of the `Player` when getting.
- * - undefined when setting
- */
- crossOrigin(value) {
- // `null` can be set to unset a value
- if (typeof value === 'undefined') {
- return this.techGet_('crossOrigin');
- }
- if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
- log$1.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
- return;
- }
- this.techCall_('setCrossOrigin', value);
- if (this.posterImage) {
- this.posterImage.crossOrigin(value);
- }
- return;
- }
-
- /**
- * A getter/setter for the `Player`'s width. Returns the player's configured value.
- * To get the current width use `currentWidth()`.
- *
- * @param {number|string} [value]
- * CSS value to set the `Player`'s width to.
- *
- * @return {number|undefined}
- * - The current width of the `Player` when getting.
- * - Nothing when setting
- */
- width(value) {
- return this.dimension('width', value);
- }
-
- /**
- * A getter/setter for the `Player`'s height. Returns the player's configured value.
- * To get the current height use `currentheight()`.
- *
- * @param {number|string} [value]
- * CSS value to set the `Player`'s height to.
- *
- * @return {number|undefined}
- * - The current height of the `Player` when getting.
- * - Nothing when setting
- */
- height(value) {
- return this.dimension('height', value);
- }
-
- /**
- * A getter/setter for the `Player`'s width & height.
- *
- * @param {string} dimension
- * This string can be:
- * - 'width'
- * - 'height'
- *
- * @param {number|string} [value]
- * Value for dimension specified in the first argument.
- *
- * @return {number}
- * The dimension arguments value when getting (width/height).
- */
- dimension(dimension, value) {
- const privDimension = dimension + '_';
- if (value === undefined) {
- return this[privDimension] || 0;
- }
- if (value === '' || value === 'auto') {
- // If an empty string is given, reset the dimension to be automatic
- this[privDimension] = undefined;
- this.updateStyleEl_();
- return;
- }
- const parsedVal = parseFloat(value);
- if (isNaN(parsedVal)) {
- log$1.error(`Improper value "${value}" supplied for for ${dimension}`);
- return;
- }
- this[privDimension] = parsedVal;
- this.updateStyleEl_();
- }
-
- /**
- * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
- *
- * Turning this on will turn off fill mode.
- *
- * @param {boolean} [bool]
- * - A value of true adds the class.
- * - A value of false removes the class.
- * - No value will be a getter.
- *
- * @return {boolean|undefined}
- * - The value of fluid when getting.
- * - `undefined` when setting.
- */
- fluid(bool) {
- if (bool === undefined) {
- return !!this.fluid_;
- }
- this.fluid_ = !!bool;
- if (isEvented(this)) {
- this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- }
- if (bool) {
- this.addClass('vjs-fluid');
- this.fill(false);
- addEventedCallback(this, () => {
- this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
- });
- } else {
- this.removeClass('vjs-fluid');
- }
- this.updateStyleEl_();
- }
-
- /**
- * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
- *
- * Turning this on will turn off fluid mode.
- *
- * @param {boolean} [bool]
- * - A value of true adds the class.
- * - A value of false removes the class.
- * - No value will be a getter.
- *
- * @return {boolean|undefined}
- * - The value of fluid when getting.
- * - `undefined` when setting.
- */
- fill(bool) {
- if (bool === undefined) {
- return !!this.fill_;
- }
- this.fill_ = !!bool;
- if (bool) {
- this.addClass('vjs-fill');
- this.fluid(false);
- } else {
- this.removeClass('vjs-fill');
- }
- }
-
- /**
- * Get/Set the aspect ratio
- *
- * @param {string} [ratio]
- * Aspect ratio for player
- *
- * @return {string|undefined}
- * returns the current aspect ratio when getting
- */
-
- /**
- * A getter/setter for the `Player`'s aspect ratio.
- *
- * @param {string} [ratio]
- * The value to set the `Player`'s aspect ratio to.
- *
- * @return {string|undefined}
- * - The current aspect ratio of the `Player` when getting.
- * - undefined when setting
- */
- aspectRatio(ratio) {
- if (ratio === undefined) {
- return this.aspectRatio_;
- }
-
- // Check for width:height format
- if (!/^\d+\:\d+$/.test(ratio)) {
- throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
- }
- this.aspectRatio_ = ratio;
-
- // We're assuming if you set an aspect ratio you want fluid mode,
- // because in fixed mode you could calculate width and height yourself.
- this.fluid(true);
- this.updateStyleEl_();
- }
-
- /**
- * Update styles of the `Player` element (height, width and aspect ratio).
- *
- * @private
- * @listens Tech#loadedmetadata
- */
- updateStyleEl_() {
- if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) {
- const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
- const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
- const techEl = this.tech_ && this.tech_.el();
- if (techEl) {
- if (width >= 0) {
- techEl.width = width;
- }
- if (height >= 0) {
- techEl.height = height;
- }
- }
- return;
- }
- let width;
- let height;
- let aspectRatio;
- let idClass;
-
- // The aspect ratio is either used directly or to calculate width and height.
- if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
- // Use any aspectRatio that's been specifically set
- aspectRatio = this.aspectRatio_;
- } else if (this.videoWidth() > 0) {
- // Otherwise try to get the aspect ratio from the video metadata
- aspectRatio = this.videoWidth() + ':' + this.videoHeight();
- } else {
- // Or use a default. The video element's is 2:1, but 16:9 is more common.
- aspectRatio = '16:9';
- }
-
- // Get the ratio as a decimal we can use to calculate dimensions
- const ratioParts = aspectRatio.split(':');
- const ratioMultiplier = ratioParts[1] / ratioParts[0];
- if (this.width_ !== undefined) {
- // Use any width that's been specifically set
- width = this.width_;
- } else if (this.height_ !== undefined) {
- // Or calculate the width from the aspect ratio if a height has been set
- width = this.height_ / ratioMultiplier;
- } else {
- // Or use the video's metadata, or use the video el's default of 300
- width = this.videoWidth() || 300;
- }
- if (this.height_ !== undefined) {
- // Use any height that's been specifically set
- height = this.height_;
- } else {
- // Otherwise calculate the height from the ratio and the width
- height = width * ratioMultiplier;
- }
-
- // Ensure the CSS class is valid by starting with an alpha character
- if (/^[^a-zA-Z]/.test(this.id())) {
- idClass = 'dimensions-' + this.id();
- } else {
- idClass = this.id() + '-dimensions';
- }
-
- // Ensure the right class is still on the player for the style element
- this.addClass(idClass);
- setTextContent(this.styleEl_, `
- .${idClass} {
- width: ${width}px;
- height: ${height}px;
- }
-
- .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: ${ratioMultiplier * 100}%;
- }
- `);
- }
-
- /**
- * Load/Create an instance of playback {@link Tech} including element
- * and API methods. Then append the `Tech` element in `Player` as a child.
- *
- * @param {string} techName
- * name of the playback technology
- *
- * @param {string} source
- * video source
- *
- * @private
- */
- loadTech_(techName, source) {
- // Pause and remove current playback technology
- if (this.tech_) {
- this.unloadTech_();
- }
- const titleTechName = toTitleCase$1(techName);
- const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
-
- // get rid of the HTML5 video tag as soon as we are using another tech
- if (titleTechName !== 'Html5' && this.tag) {
- Tech.getTech('Html5').disposeMediaElement(this.tag);
- this.tag.player = null;
- this.tag = null;
- }
- this.techName_ = titleTechName;
-
- // Turn off API access because we're loading a new tech that might load asynchronously
- this.isReady_ = false;
- let autoplay = this.autoplay();
-
- // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
- // because the player is going to handle autoplay on `loadstart`
- if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
- autoplay = false;
- }
-
- // Grab tech-specific options from player options and add source and parent element to use.
- const techOptions = {
- source,
- autoplay,
- 'nativeControlsForTouch': this.options_.nativeControlsForTouch,
- 'playerId': this.id(),
- 'techId': `${this.id()}_${camelTechName}_api`,
- 'playsinline': this.options_.playsinline,
- 'preload': this.options_.preload,
- 'loop': this.options_.loop,
- 'disablePictureInPicture': this.options_.disablePictureInPicture,
- 'muted': this.options_.muted,
- 'poster': this.poster(),
- 'language': this.language(),
- 'playerElIngest': this.playerElIngest_ || false,
- 'vtt.js': this.options_['vtt.js'],
- 'canOverridePoster': !!this.options_.techCanOverridePoster,
- 'enableSourceset': this.options_.enableSourceset
- };
- ALL.names.forEach(name => {
- const props = ALL[name];
- techOptions[props.getterName] = this[props.privateName];
- });
- Object.assign(techOptions, this.options_[titleTechName]);
- Object.assign(techOptions, this.options_[camelTechName]);
- Object.assign(techOptions, this.options_[techName.toLowerCase()]);
- if (this.tag) {
- techOptions.tag = this.tag;
- }
- if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
- techOptions.startTime = this.cache_.currentTime;
- }
-
- // Initialize tech instance
- const TechClass = Tech.getTech(techName);
- if (!TechClass) {
- throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
- }
- this.tech_ = new TechClass(techOptions);
-
- // player.triggerReady is always async, so don't need this to be async
- this.tech_.ready(bind_(this, this.handleTechReady_), true);
- textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
-
- // Listen to all HTML5-defined events and trigger them on the player
- TECH_EVENTS_RETRIGGER.forEach(event => {
- this.on(this.tech_, event, e => this[`handleTech${toTitleCase$1(event)}_`](e));
- });
- Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
- this.on(this.tech_, event, eventObj => {
- if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
- this.queuedCallbacks_.push({
- callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
- event: eventObj
- });
- return;
- }
- this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
- });
- });
- this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
- this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
- this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
- this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
- this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
- this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
- this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
- this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
- this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
- this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
- this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
- this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
- this.on(this.tech_, 'error', e => this.handleTechError_(e));
- this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
- this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
- this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
- this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
- this.usingNativeControls(this.techGet_('controls'));
- if (this.controls() && !this.usingNativeControls()) {
- this.addTechControlsListeners_();
- }
-
- // Add the tech element in the DOM if it was not already there
- // Make sure to not insert the original video element if using Html5
- if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
- prependTo(this.tech_.el(), this.el());
- }
-
- // Get rid of the original video tag reference after the first tech is loaded
- if (this.tag) {
- this.tag.player = null;
- this.tag = null;
- }
- }
-
- /**
- * Unload and dispose of the current playback {@link Tech}.
- *
- * @private
- */
- unloadTech_() {
- // Save the current text tracks so that we can reuse the same text tracks with the next tech
- ALL.names.forEach(name => {
- const props = ALL[name];
- this[props.privateName] = this[props.getterName]();
- });
- this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
- this.isReady_ = false;
- this.tech_.dispose();
- this.tech_ = false;
- if (this.isPosterFromTech_) {
- this.poster_ = '';
- this.trigger('posterchange');
- }
- this.isPosterFromTech_ = false;
- }
-
- /**
- * Return a reference to the current {@link Tech}.
- * It will print a warning by default about the danger of using the tech directly
- * but any argument that is passed in will silence the warning.
- *
- * @param {*} [safety]
- * Anything passed in to silence the warning
- *
- * @return {Tech}
- * The Tech
- */
- tech(safety) {
- if (safety === undefined) {
- log$1.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
- }
- return this.tech_;
- }
-
- /**
- * An object that contains Video.js version.
- *
- * @typedef {Object} PlayerVersion
- *
- * @property {string} 'video.js' - Video.js version
- */
-
- /**
- * Returns an object with Video.js version.
- *
- * @return {PlayerVersion}
- * An object with Video.js version.
- */
- version() {
- return {
- 'video.js': version$5
- };
- }
-
- /**
- * Set up click and touch listeners for the playback element
- *
- * - On desktops: a click on the video itself will toggle playback
- * - On mobile devices: a click on the video toggles controls
- * which is done by toggling the user state between active and
- * inactive
- * - A tap can signal that a user has become active or has become inactive
- * e.g. a quick tap on an iPhone movie should reveal the controls. Another
- * quick tap should hide them again (signaling the user is in an inactive
- * viewing state)
- * - In addition to this, we still want the user to be considered inactive after
- * a few seconds of inactivity.
- *
- * > Note: the only part of iOS interaction we can't mimic with this setup
- * is a touch and hold on the video element counting as activity in order to
- * keep the controls showing, but that shouldn't be an issue. A touch and hold
- * on any controls will still keep the user active
- *
- * @private
- */
- addTechControlsListeners_() {
- // Make sure to remove all the previous listeners in case we are called multiple times.
- this.removeTechControlsListeners_();
- this.on(this.tech_, 'click', this.boundHandleTechClick_);
- this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
-
- // If the controls were hidden we don't want that to change without a tap event
- // so we'll check if the controls were already showing before reporting user
- // activity
- this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
- this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
- this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
-
- // The tap listener needs to come after the touchend listener because the tap
- // listener cancels out any reportedUserActivity when setting userActive(false)
- this.on(this.tech_, 'tap', this.boundHandleTechTap_);
- }
-
- /**
- * Remove the listeners used for click and tap controls. This is needed for
- * toggling to controls disabled, where a tap/touch should do nothing.
- *
- * @private
- */
- removeTechControlsListeners_() {
- // We don't want to just use `this.off()` because there might be other needed
- // listeners added by techs that extend this.
- this.off(this.tech_, 'tap', this.boundHandleTechTap_);
- this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
- this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
- this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
- this.off(this.tech_, 'click', this.boundHandleTechClick_);
- this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
- }
-
- /**
- * Player waits for the tech to be ready
- *
- * @private
- */
- handleTechReady_() {
- this.triggerReady();
-
- // Keep the same volume as before
- if (this.cache_.volume) {
- this.techCall_('setVolume', this.cache_.volume);
- }
-
- // Look if the tech found a higher resolution poster while loading
- this.handleTechPosterChange_();
-
- // Update the duration if available
- this.handleTechDurationChange_();
- }
-
- /**
- * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
- *
- * @fires Player#loadstart
- * @listens Tech#loadstart
- * @private
- */
- handleTechLoadStart_() {
- // TODO: Update to use `emptied` event instead. See #1277.
-
- this.removeClass('vjs-ended', 'vjs-seeking');
-
- // reset the error state
- this.error(null);
-
- // Update the duration
- this.handleTechDurationChange_();
- if (!this.paused()) {
- /**
- * Fired when the user agent begins looking for media data
- *
- * @event Player#loadstart
- * @type {Event}
- */
- this.trigger('loadstart');
- } else {
- // reset the hasStarted state
- this.hasStarted(false);
- this.trigger('loadstart');
- }
-
- // autoplay happens after loadstart for the browser,
- // so we mimic that behavior
- this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
- }
-
- /**
- * Handle autoplay string values, rather than the typical boolean
- * values that should be handled by the tech. Note that this is not
- * part of any specification. Valid values and what they do can be
- * found on the autoplay getter at Player#autoplay()
- */
- manualAutoplay_(type) {
- if (!this.tech_ || typeof type !== 'string') {
- return;
- }
-
- // Save original muted() value, set muted to true, and attempt to play().
- // On promise rejection, restore muted from saved value
- const resolveMuted = () => {
- const previouslyMuted = this.muted();
- this.muted(true);
- const restoreMuted = () => {
- this.muted(previouslyMuted);
- };
-
- // restore muted on play terminatation
- this.playTerminatedQueue_.push(restoreMuted);
- const mutedPromise = this.play();
- if (!isPromise(mutedPromise)) {
- return;
- }
- return mutedPromise.catch(err => {
- restoreMuted();
- throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
- });
- };
- let promise;
-
- // if muted defaults to true
- // the only thing we can do is call play
- if (type === 'any' && !this.muted()) {
- promise = this.play();
- if (isPromise(promise)) {
- promise = promise.catch(resolveMuted);
- }
- } else if (type === 'muted' && !this.muted()) {
- promise = resolveMuted();
- } else {
- promise = this.play();
- }
- if (!isPromise(promise)) {
- return;
- }
- return promise.then(() => {
- this.trigger({
- type: 'autoplay-success',
- autoplay: type
- });
- }).catch(() => {
- this.trigger({
- type: 'autoplay-failure',
- autoplay: type
- });
- });
- }
-
- /**
- * Update the internal source caches so that we return the correct source from
- * `src()`, `currentSource()`, and `currentSources()`.
- *
- * > Note: `currentSources` will not be updated if the source that is passed in exists
- * in the current `currentSources` cache.
- *
- *
- * @param {Tech~SourceObject} srcObj
- * A string or object source to update our caches to.
- */
- updateSourceCaches_(srcObj = '') {
- let src = srcObj;
- let type = '';
- if (typeof src !== 'string') {
- src = srcObj.src;
- type = srcObj.type;
- }
-
- // make sure all the caches are set to default values
- // to prevent null checking
- this.cache_.source = this.cache_.source || {};
- this.cache_.sources = this.cache_.sources || [];
-
- // try to get the type of the src that was passed in
- if (src && !type) {
- type = findMimetype(this, src);
- }
-
- // update `currentSource` cache always
- this.cache_.source = merge$2({}, srcObj, {
- src,
- type
- });
- const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
- const sourceElSources = [];
- const sourceEls = this.$$('source');
- const matchingSourceEls = [];
- for (let i = 0; i < sourceEls.length; i++) {
- const sourceObj = getAttributes(sourceEls[i]);
- sourceElSources.push(sourceObj);
- if (sourceObj.src && sourceObj.src === src) {
- matchingSourceEls.push(sourceObj.src);
- }
- }
-
- // if we have matching source els but not matching sources
- // the current source cache is not up to date
- if (matchingSourceEls.length && !matchingSources.length) {
- this.cache_.sources = sourceElSources;
- // if we don't have matching source or source els set the
- // sources cache to the `currentSource` cache
- } else if (!matchingSources.length) {
- this.cache_.sources = [this.cache_.source];
- }
-
- // update the tech `src` cache
- this.cache_.src = src;
- }
-
- /**
- * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
- * causing the media element to reload.
- *
- * It will fire for the initial source and each subsequent source.
- * This event is a custom event from Video.js and is triggered by the {@link Tech}.
- *
- * The event object for this event contains a `src` property that will contain the source
- * that was available when the event was triggered. This is generally only necessary if Video.js
- * is switching techs while the source was being changed.
- *
- * It is also fired when `load` is called on the player (or media element)
- * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
- * says that the resource selection algorithm needs to be aborted and restarted.
- * In this case, it is very likely that the `src` property will be set to the
- * empty string `""` to indicate we do not know what the source will be but
- * that it is changing.
- *
- * *This event is currently still experimental and may change in minor releases.*
- * __To use this, pass `enableSourceset` option to the player.__
- *
- * @event Player#sourceset
- * @type {Event}
- * @prop {string} src
- * The source url available when the `sourceset` was triggered.
- * It will be an empty string if we cannot know what the source is
- * but know that the source will change.
- */
- /**
- * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
- *
- * @fires Player#sourceset
- * @listens Tech#sourceset
- * @private
- */
- handleTechSourceset_(event) {
- // only update the source cache when the source
- // was not updated using the player api
- if (!this.changingSrc_) {
- let updateSourceCaches = src => this.updateSourceCaches_(src);
- const playerSrc = this.currentSource().src;
- const eventSrc = event.src;
-
- // if we have a playerSrc that is not a blob, and a tech src that is a blob
- if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
- // if both the tech source and the player source were updated we assume
- // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
- if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
- updateSourceCaches = () => {};
- }
- }
-
- // update the source to the initial source right away
- // in some cases this will be empty string
- updateSourceCaches(eventSrc);
-
- // if the `sourceset` `src` was an empty string
- // wait for a `loadstart` to update the cache to `currentSrc`.
- // If a sourceset happens before a `loadstart`, we reset the state
- if (!event.src) {
- this.tech_.any(['sourceset', 'loadstart'], e => {
- // if a sourceset happens before a `loadstart` there
- // is nothing to do as this `handleTechSourceset_`
- // will be called again and this will be handled there.
- if (e.type === 'sourceset') {
- return;
- }
- const techSrc = this.techGet_('currentSrc');
- this.lastSource_.tech = techSrc;
- this.updateSourceCaches_(techSrc);
- });
- }
- }
- this.lastSource_ = {
- player: this.currentSource().src,
- tech: event.src
- };
- this.trigger({
- src: event.src,
- type: 'sourceset'
- });
- }
-
- /**
- * Add/remove the vjs-has-started class
- *
- *
- * @param {boolean} request
- * - true: adds the class
- * - false: remove the class
- *
- * @return {boolean}
- * the boolean value of hasStarted_
- */
- hasStarted(request) {
- if (request === undefined) {
- // act as getter, if we have no request to change
- return this.hasStarted_;
- }
- if (request === this.hasStarted_) {
- return;
- }
- this.hasStarted_ = request;
- if (this.hasStarted_) {
- this.addClass('vjs-has-started');
- } else {
- this.removeClass('vjs-has-started');
- }
- }
-
- /**
- * Fired whenever the media begins or resumes playback
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
- * @fires Player#play
- * @listens Tech#play
- * @private
- */
- handleTechPlay_() {
- this.removeClass('vjs-ended', 'vjs-paused');
- this.addClass('vjs-playing');
-
- // hide the poster when the user hits play
- this.hasStarted(true);
- /**
- * Triggered whenever an {@link Tech#play} event happens. Indicates that
- * playback has started or resumed.
- *
- * @event Player#play
- * @type {Event}
- */
- this.trigger('play');
- }
-
- /**
- * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
- *
- * If there were any events queued while the playback rate was zero, fire
- * those events now.
- *
- * @private
- * @method Player#handleTechRateChange_
- * @fires Player#ratechange
- * @listens Tech#ratechange
- */
- handleTechRateChange_() {
- if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
- this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
- this.queuedCallbacks_ = [];
- }
- this.cache_.lastPlaybackRate = this.tech_.playbackRate();
- /**
- * Fires when the playing speed of the audio/video is changed
- *
- * @event Player#ratechange
- * @type {event}
- */
- this.trigger('ratechange');
- }
-
- /**
- * Retrigger the `waiting` event that was triggered by the {@link Tech}.
- *
- * @fires Player#waiting
- * @listens Tech#waiting
- * @private
- */
- handleTechWaiting_() {
- this.addClass('vjs-waiting');
- /**
- * A readyState change on the DOM element has caused playback to stop.
- *
- * @event Player#waiting
- * @type {Event}
- */
- this.trigger('waiting');
-
- // Browsers may emit a timeupdate event after a waiting event. In order to prevent
- // premature removal of the waiting class, wait for the time to change.
- const timeWhenWaiting = this.currentTime();
- const timeUpdateListener = () => {
- if (timeWhenWaiting !== this.currentTime()) {
- this.removeClass('vjs-waiting');
- this.off('timeupdate', timeUpdateListener);
- }
- };
- this.on('timeupdate', timeUpdateListener);
- }
-
- /**
- * Retrigger the `canplay` event that was triggered by the {@link Tech}.
- * > Note: This is not consistent between browsers. See #1351
- *
- * @fires Player#canplay
- * @listens Tech#canplay
- * @private
- */
- handleTechCanPlay_() {
- this.removeClass('vjs-waiting');
- /**
- * The media has a readyState of HAVE_FUTURE_DATA or greater.
- *
- * @event Player#canplay
- * @type {Event}
- */
- this.trigger('canplay');
- }
-
- /**
- * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
- *
- * @fires Player#canplaythrough
- * @listens Tech#canplaythrough
- * @private
- */
- handleTechCanPlayThrough_() {
- this.removeClass('vjs-waiting');
- /**
- * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
- * entire media file can be played without buffering.
- *
- * @event Player#canplaythrough
- * @type {Event}
- */
- this.trigger('canplaythrough');
- }
-
- /**
- * Retrigger the `playing` event that was triggered by the {@link Tech}.
- *
- * @fires Player#playing
- * @listens Tech#playing
- * @private
- */
- handleTechPlaying_() {
- this.removeClass('vjs-waiting');
- /**
- * The media is no longer blocked from playback, and has started playing.
- *
- * @event Player#playing
- * @type {Event}
- */
- this.trigger('playing');
- }
-
- /**
- * Retrigger the `seeking` event that was triggered by the {@link Tech}.
- *
- * @fires Player#seeking
- * @listens Tech#seeking
- * @private
- */
- handleTechSeeking_() {
- this.addClass('vjs-seeking');
- /**
- * Fired whenever the player is jumping to a new time
- *
- * @event Player#seeking
- * @type {Event}
- */
- this.trigger('seeking');
- }
-
- /**
- * Retrigger the `seeked` event that was triggered by the {@link Tech}.
- *
- * @fires Player#seeked
- * @listens Tech#seeked
- * @private
- */
- handleTechSeeked_() {
- this.removeClass('vjs-seeking', 'vjs-ended');
- /**
- * Fired when the player has finished jumping to a new time
- *
- * @event Player#seeked
- * @type {Event}
- */
- this.trigger('seeked');
- }
-
- /**
- * Retrigger the `pause` event that was triggered by the {@link Tech}.
- *
- * @fires Player#pause
- * @listens Tech#pause
- * @private
- */
- handleTechPause_() {
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- /**
- * Fired whenever the media has been paused
- *
- * @event Player#pause
- * @type {Event}
- */
- this.trigger('pause');
- }
-
- /**
- * Retrigger the `ended` event that was triggered by the {@link Tech}.
- *
- * @fires Player#ended
- * @listens Tech#ended
- * @private
- */
- handleTechEnded_() {
- this.addClass('vjs-ended');
- this.removeClass('vjs-waiting');
- if (this.options_.loop) {
- this.currentTime(0);
- this.play();
- } else if (!this.paused()) {
- this.pause();
- }
-
- /**
- * Fired when the end of the media resource is reached (currentTime == duration)
- *
- * @event Player#ended
- * @type {Event}
- */
- this.trigger('ended');
- }
-
- /**
- * Fired when the duration of the media resource is first known or changed
- *
- * @listens Tech#durationchange
- * @private
- */
- handleTechDurationChange_() {
- this.duration(this.techGet_('duration'));
- }
-
- /**
- * Handle a click on the media element to play/pause
- *
- * @param {Event} event
- * the event that caused this function to trigger
- *
- * @listens Tech#click
- * @private
- */
- handleTechClick_(event) {
- // When controls are disabled a click should not toggle playback because
- // the click is considered a control
- if (!this.controls_) {
- return;
- }
- if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
- if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
- this.options_.userActions.click.call(this, event);
- } else if (this.paused()) {
- silencePromise(this.play());
- } else {
- this.pause();
- }
- }
- }
-
- /**
- * Handle a double-click on the media element to enter/exit fullscreen
- *
- * @param {Event} event
- * the event that caused this function to trigger
- *
- * @listens Tech#dblclick
- * @private
- */
- handleTechDoubleClick_(event) {
- if (!this.controls_) {
- return;
- }
-
- // we do not want to toggle fullscreen state
- // when double-clicking inside a control bar or a modal
- const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
- if (!inAllowedEls) {
- /*
- * options.userActions.doubleClick
- *
- * If `undefined` or `true`, double-click toggles fullscreen if controls are present
- * Set to `false` to disable double-click handling
- * Set to a function to substitute an external double-click handler
- */
- if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
- if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
- this.options_.userActions.doubleClick.call(this, event);
- } else if (this.isFullscreen()) {
- this.exitFullscreen();
- } else {
- this.requestFullscreen();
- }
- }
- }
- }
-
- /**
- * Handle a tap on the media element. It will toggle the user
- * activity state, which hides and shows the controls.
- *
- * @listens Tech#tap
- * @private
- */
- handleTechTap_() {
- this.userActive(!this.userActive());
- }
-
- /**
- * Handle touch to start
- *
- * @listens Tech#touchstart
- * @private
- */
- handleTechTouchStart_() {
- this.userWasActive = this.userActive();
- }
-
- /**
- * Handle touch to move
- *
- * @listens Tech#touchmove
- * @private
- */
- handleTechTouchMove_() {
- if (this.userWasActive) {
- this.reportUserActivity();
- }
- }
-
- /**
- * Handle touch to end
- *
- * @param {Event} event
- * the touchend event that triggered
- * this function
- *
- * @listens Tech#touchend
- * @private
- */
- handleTechTouchEnd_(event) {
- // Stop the mouse events from also happening
- if (event.cancelable) {
- event.preventDefault();
- }
- }
-
- /**
- * @private
- */
- toggleFullscreenClass_() {
- if (this.isFullscreen()) {
- this.addClass('vjs-fullscreen');
- } else {
- this.removeClass('vjs-fullscreen');
- }
- }
-
- /**
- * when the document fschange event triggers it calls this
- */
- documentFullscreenChange_(e) {
- const targetPlayer = e.target.player;
-
- // if another player was fullscreen
- // do a null check for targetPlayer because older firefox's would put document as e.target
- if (targetPlayer && targetPlayer !== this) {
- return;
- }
- const el = this.el();
- let isFs = document[this.fsApi_.fullscreenElement] === el;
- if (!isFs && el.matches) {
- isFs = el.matches(':' + this.fsApi_.fullscreen);
- }
- this.isFullscreen(isFs);
- }
-
- /**
- * Handle Tech Fullscreen Change
- *
- * @param {Event} event
- * the fullscreenchange event that triggered this function
- *
- * @param {Object} data
- * the data that was sent with the event
- *
- * @private
- * @listens Tech#fullscreenchange
- * @fires Player#fullscreenchange
- */
- handleTechFullscreenChange_(event, data) {
- if (data) {
- if (data.nativeIOSFullscreen) {
- this.addClass('vjs-ios-native-fs');
- this.tech_.one('webkitendfullscreen', () => {
- this.removeClass('vjs-ios-native-fs');
- });
- }
- this.isFullscreen(data.isFullscreen);
- }
- }
- handleTechFullscreenError_(event, err) {
- this.trigger('fullscreenerror', err);
- }
-
- /**
- * @private
- */
- togglePictureInPictureClass_() {
- if (this.isInPictureInPicture()) {
- this.addClass('vjs-picture-in-picture');
- } else {
- this.removeClass('vjs-picture-in-picture');
- }
- }
-
- /**
- * Handle Tech Enter Picture-in-Picture.
- *
- * @param {Event} event
- * the enterpictureinpicture event that triggered this function
- *
- * @private
- * @listens Tech#enterpictureinpicture
- */
- handleTechEnterPictureInPicture_(event) {
- this.isInPictureInPicture(true);
- }
-
- /**
- * Handle Tech Leave Picture-in-Picture.
- *
- * @param {Event} event
- * the leavepictureinpicture event that triggered this function
- *
- * @private
- * @listens Tech#leavepictureinpicture
- */
- handleTechLeavePictureInPicture_(event) {
- this.isInPictureInPicture(false);
- }
-
- /**
- * Fires when an error occurred during the loading of an audio/video.
- *
- * @private
- * @listens Tech#error
- */
- handleTechError_() {
- const error = this.tech_.error();
- if (error) {
- this.error(error);
- }
- }
-
- /**
- * Retrigger the `textdata` event that was triggered by the {@link Tech}.
- *
- * @fires Player#textdata
- * @listens Tech#textdata
- * @private
- */
- handleTechTextData_() {
- let data = null;
- if (arguments.length > 1) {
- data = arguments[1];
- }
-
- /**
- * Fires when we get a textdata event from tech
- *
- * @event Player#textdata
- * @type {Event}
- */
- this.trigger('textdata', data);
- }
-
- /**
- * Get object for cached values.
- *
- * @return {Object}
- * get the current object cache
- */
- getCache() {
- return this.cache_;
- }
-
- /**
- * Resets the internal cache object.
- *
- * Using this function outside the player constructor or reset method may
- * have unintended side-effects.
- *
- * @private
- */
- resetCache_() {
- this.cache_ = {
- // Right now, the currentTime is not _really_ cached because it is always
- // retrieved from the tech (see: currentTime). However, for completeness,
- // we set it to zero here to ensure that if we do start actually caching
- // it, we reset it along with everything else.
- currentTime: 0,
- initTime: 0,
- inactivityTimeout: this.options_.inactivityTimeout,
- duration: NaN,
- lastVolume: 1,
- lastPlaybackRate: this.defaultPlaybackRate(),
- media: null,
- src: '',
- source: {},
- sources: [],
- playbackRates: [],
- volume: 1
- };
- }
-
- /**
- * Pass values to the playback tech
- *
- * @param {string} [method]
- * the method to call
- *
- * @param {Object} [arg]
- * the argument to pass
- *
- * @private
- */
- techCall_(method, arg) {
- // If it's not ready yet, call method when it is
-
- this.ready(function () {
- if (method in allowedSetters) {
- return set(this.middleware_, this.tech_, method, arg);
- } else if (method in allowedMediators) {
- return mediate(this.middleware_, this.tech_, method, arg);
- }
- try {
- if (this.tech_) {
- this.tech_[method](arg);
- }
- } catch (e) {
- log$1(e);
- throw e;
- }
- }, true);
- }
-
- /**
- * Mediate attempt to call playback tech method
- * and return the value of the method called.
- *
- * @param {string} method
- * Tech method
- *
- * @return {*}
- * Value returned by the tech method called, undefined if tech
- * is not ready or tech method is not present
- *
- * @private
- */
- techGet_(method) {
- if (!this.tech_ || !this.tech_.isReady_) {
- return;
- }
- if (method in allowedGetters) {
- return get(this.middleware_, this.tech_, method);
- } else if (method in allowedMediators) {
- return mediate(this.middleware_, this.tech_, method);
- }
-
- // Log error when playback tech object is present but method
- // is undefined or unavailable
- try {
- return this.tech_[method]();
- } catch (e) {
- // When building additional tech libs, an expected method may not be defined yet
- if (this.tech_[method] === undefined) {
- log$1(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
- throw e;
- }
-
- // When a method isn't available on the object it throws a TypeError
- if (e.name === 'TypeError') {
- log$1(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
- this.tech_.isReady_ = false;
- throw e;
- }
-
- // If error unknown, just log and throw
- log$1(e);
- throw e;
- }
- }
-
- /**
- * Attempt to begin playback at the first opportunity.
- *
- * @return {Promise|undefined}
- * Returns a promise if the browser supports Promises (or one
- * was passed in as an option). This promise will be resolved on
- * the return value of play. If this is undefined it will fulfill the
- * promise chain otherwise the promise chain will be fulfilled when
- * the promise from play is fulfilled.
- */
- play() {
- return new Promise(resolve => {
- this.play_(resolve);
- });
- }
-
- /**
- * The actual logic for play, takes a callback that will be resolved on the
- * return value of play. This allows us to resolve to the play promise if there
- * is one on modern browsers.
- *
- * @private
- * @param {Function} [callback]
- * The callback that should be called when the techs play is actually called
- */
- play_(callback = silencePromise) {
- this.playCallbacks_.push(callback);
- const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
- const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
-
- // treat calls to play_ somewhat like the `one` event function
- if (this.waitToPlay_) {
- this.off(['ready', 'loadstart'], this.waitToPlay_);
- this.waitToPlay_ = null;
- }
-
- // if the player/tech is not ready or the src itself is not ready
- // queue up a call to play on `ready` or `loadstart`
- if (!this.isReady_ || !isSrcReady) {
- this.waitToPlay_ = e => {
- this.play_();
- };
- this.one(['ready', 'loadstart'], this.waitToPlay_);
-
- // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
- // in that case, we need to prime the video element by calling load so it'll be ready in time
- if (!isSrcReady && isSafariOrIOS) {
- this.load();
- }
- return;
- }
-
- // If the player/tech is ready and we have a source, we can attempt playback.
- const val = this.techGet_('play');
-
- // For native playback, reset the progress bar if we get a play call from a replay.
- const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
- if (isNativeReplay) {
- this.resetProgressBar_();
- }
- // play was terminated if the returned value is null
- if (val === null) {
- this.runPlayTerminatedQueue_();
- } else {
- this.runPlayCallbacks_(val);
- }
- }
-
- /**
- * These functions will be run when if play is terminated. If play
- * runPlayCallbacks_ is run these function will not be run. This allows us
- * to differentiate between a terminated play and an actual call to play.
- */
- runPlayTerminatedQueue_() {
- const queue = this.playTerminatedQueue_.slice(0);
- this.playTerminatedQueue_ = [];
- queue.forEach(function (q) {
- q();
- });
- }
-
- /**
- * When a callback to play is delayed we have to run these
- * callbacks when play is actually called on the tech. This function
- * runs the callbacks that were delayed and accepts the return value
- * from the tech.
- *
- * @param {undefined|Promise} val
- * The return value from the tech.
- */
- runPlayCallbacks_(val) {
- const callbacks = this.playCallbacks_.slice(0);
- this.playCallbacks_ = [];
- // clear play terminatedQueue since we finished a real play
- this.playTerminatedQueue_ = [];
- callbacks.forEach(function (cb) {
- cb(val);
- });
- }
-
- /**
- * Pause the video playback
- */
- pause() {
- this.techCall_('pause');
- }
-
- /**
- * Check if the player is paused or has yet to play
- *
- * @return {boolean}
- * - false: if the media is currently playing
- * - true: if media is not currently playing
- */
- paused() {
- // The initial state of paused should be true (in Safari it's actually false)
- return this.techGet_('paused') === false ? false : true;
- }
-
- /**
- * Get a TimeRange object representing the current ranges of time that the user
- * has played.
- *
- * @return { import('./utils/time').TimeRange }
- * A time range object that represents all the increments of time that have
- * been played.
- */
- played() {
- return this.techGet_('played') || createTimeRanges$1(0, 0);
- }
-
- /**
- * Sets or returns whether or not the user is "scrubbing". Scrubbing is
- * when the user has clicked the progress bar handle and is
- * dragging it along the progress bar.
- *
- * @param {boolean} [isScrubbing]
- * whether the user is or is not scrubbing
- *
- * @return {boolean|undefined}
- * - The value of scrubbing when getting
- * - Nothing when setting
- */
- scrubbing(isScrubbing) {
- if (typeof isScrubbing === 'undefined') {
- return this.scrubbing_;
- }
- this.scrubbing_ = !!isScrubbing;
- this.techCall_('setScrubbing', this.scrubbing_);
- if (isScrubbing) {
- this.addClass('vjs-scrubbing');
- } else {
- this.removeClass('vjs-scrubbing');
- }
- }
-
- /**
- * Get or set the current time (in seconds)
- *
- * @param {number|string} [seconds]
- * The time to seek to in seconds
- *
- * @return {number|undefined}
- * - the current time in seconds when getting
- * - Nothing when setting
- */
- currentTime(seconds) {
- if (seconds === undefined) {
- // cache last currentTime and return. default to 0 seconds
- //
- // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
- // currentTime when scrubbing, but may not provide much performance benefit after all.
- // Should be tested. Also something has to read the actual current time or the cache will
- // never get updated.
- this.cache_.currentTime = this.techGet_('currentTime') || 0;
- return this.cache_.currentTime;
- }
- if (seconds < 0) {
- seconds = 0;
- }
- if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
- this.cache_.initTime = seconds;
- this.off('canplay', this.boundApplyInitTime_);
- this.one('canplay', this.boundApplyInitTime_);
- return;
- }
- this.techCall_('setCurrentTime', seconds);
- this.cache_.initTime = 0;
- if (isFinite(seconds)) {
- this.cache_.currentTime = Number(seconds);
- }
- }
-
- /**
- * Apply the value of initTime stored in cache as currentTime.
- *
- * @private
- */
- applyInitTime_() {
- this.currentTime(this.cache_.initTime);
- }
-
- /**
- * Normally gets the length in time of the video in seconds;
- * in all but the rarest use cases an argument will NOT be passed to the method
- *
- * > **NOTE**: The video must have started loading before the duration can be
- * known, and depending on preload behaviour may not be known until the video starts
- * playing.
- *
- * @fires Player#durationchange
- *
- * @param {number} [seconds]
- * The duration of the video to set in seconds
- *
- * @return {number|undefined}
- * - The duration of the video in seconds when getting
- * - Nothing when setting
- */
- duration(seconds) {
- if (seconds === undefined) {
- // return NaN if the duration is not known
- return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
- }
- seconds = parseFloat(seconds);
-
- // Standardize on Infinity for signaling video is live
- if (seconds < 0) {
- seconds = Infinity;
- }
- if (seconds !== this.cache_.duration) {
- // Cache the last set value for optimized scrubbing
- this.cache_.duration = seconds;
- if (seconds === Infinity) {
- this.addClass('vjs-live');
- } else {
- this.removeClass('vjs-live');
- }
- if (!isNaN(seconds)) {
- // Do not fire durationchange unless the duration value is known.
- // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
-
- /**
- * @event Player#durationchange
- * @type {Event}
- */
- this.trigger('durationchange');
- }
- }
- }
-
- /**
- * Calculates how much time is left in the video. Not part
- * of the native video API.
- *
- * @return {number}
- * The time remaining in seconds
- */
- remainingTime() {
- return this.duration() - this.currentTime();
- }
-
- /**
- * A remaining time function that is intended to be used when
- * the time is to be displayed directly to the user.
- *
- * @return {number}
- * The rounded time remaining in seconds
- */
- remainingTimeDisplay() {
- return Math.floor(this.duration()) - Math.floor(this.currentTime());
- }
-
- //
- // Kind of like an array of portions of the video that have been downloaded.
-
- /**
- * Get a TimeRange object with an array of the times of the video
- * that have been downloaded. If you just want the percent of the
- * video that's been downloaded, use bufferedPercent.
- *
- * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
- *
- * @return { import('./utils/time').TimeRange }
- * A mock {@link TimeRanges} object (following HTML spec)
- */
- buffered() {
- let buffered = this.techGet_('buffered');
- if (!buffered || !buffered.length) {
- buffered = createTimeRanges$1(0, 0);
- }
- return buffered;
- }
-
- /**
- * Get the TimeRanges of the media that are currently available
- * for seeking to.
- *
- * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable}
- *
- * @return { import('./utils/time').TimeRange }
- * A mock {@link TimeRanges} object (following HTML spec)
- */
- seekable() {
- let seekable = this.techGet_('seekable');
- if (!seekable || !seekable.length) {
- seekable = createTimeRanges$1(0, 0);
- }
- return seekable;
- }
-
- /**
- * Returns whether the player is in the "seeking" state.
- *
- * @return {boolean} True if the player is in the seeking state, false if not.
- */
- seeking() {
- return this.techGet_('seeking');
- }
-
- /**
- * Returns whether the player is in the "ended" state.
- *
- * @return {boolean} True if the player is in the ended state, false if not.
- */
- ended() {
- return this.techGet_('ended');
- }
-
- /**
- * Returns the current state of network activity for the element, from
- * the codes in the list below.
- * - NETWORK_EMPTY (numeric value 0)
- * The element has not yet been initialised. All attributes are in
- * their initial states.
- * - NETWORK_IDLE (numeric value 1)
- * The element's resource selection algorithm is active and has
- * selected a resource, but it is not actually using the network at
- * this time.
- * - NETWORK_LOADING (numeric value 2)
- * The user agent is actively trying to download data.
- * - NETWORK_NO_SOURCE (numeric value 3)
- * The element's resource selection algorithm is active, but it has
- * not yet found a resource to use.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
- * @return {number} the current network activity state
- */
- networkState() {
- return this.techGet_('networkState');
- }
-
- /**
- * Returns a value that expresses the current state of the element
- * with respect to rendering the current playback position, from the
- * codes in the list below.
- * - HAVE_NOTHING (numeric value 0)
- * No information regarding the media resource is available.
- * - HAVE_METADATA (numeric value 1)
- * Enough of the resource has been obtained that the duration of the
- * resource is available.
- * - HAVE_CURRENT_DATA (numeric value 2)
- * Data for the immediate current playback position is available.
- * - HAVE_FUTURE_DATA (numeric value 3)
- * Data for the immediate current playback position is available, as
- * well as enough data for the user agent to advance the current
- * playback position in the direction of playback.
- * - HAVE_ENOUGH_DATA (numeric value 4)
- * The user agent estimates that enough data is available for
- * playback to proceed uninterrupted.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
- * @return {number} the current playback rendering state
- */
- readyState() {
- return this.techGet_('readyState');
- }
-
- /**
- * Get the percent (as a decimal) of the video that's been downloaded.
- * This method is not a part of the native HTML video API.
- *
- * @return {number}
- * A decimal between 0 and 1 representing the percent
- * that is buffered 0 being 0% and 1 being 100%
- */
- bufferedPercent() {
- return bufferedPercent(this.buffered(), this.duration());
- }
-
- /**
- * Get the ending time of the last buffered time range
- * This is used in the progress bar to encapsulate all time ranges.
- *
- * @return {number}
- * The end of the last buffered time range
- */
- bufferedEnd() {
- const buffered = this.buffered();
- const duration = this.duration();
- let end = buffered.end(buffered.length - 1);
- if (end > duration) {
- end = duration;
- }
- return end;
- }
-
- /**
- * Get or set the current volume of the media
- *
- * @param {number} [percentAsDecimal]
- * The new volume as a decimal percent:
- * - 0 is muted/0%/off
- * - 1.0 is 100%/full
- * - 0.5 is half volume or 50%
- *
- * @return {number|undefined}
- * The current volume as a percent when getting
- */
- volume(percentAsDecimal) {
- let vol;
- if (percentAsDecimal !== undefined) {
- // Force value to between 0 and 1
- vol = Math.max(0, Math.min(1, percentAsDecimal));
- this.cache_.volume = vol;
- this.techCall_('setVolume', vol);
- if (vol > 0) {
- this.lastVolume_(vol);
- }
- return;
- }
-
- // Default to 1 when returning current volume.
- vol = parseFloat(this.techGet_('volume'));
- return isNaN(vol) ? 1 : vol;
- }
-
- /**
- * Get the current muted state, or turn mute on or off
- *
- * @param {boolean} [muted]
- * - true to mute
- * - false to unmute
- *
- * @return {boolean|undefined}
- * - true if mute is on and getting
- * - false if mute is off and getting
- * - nothing if setting
- */
- muted(muted) {
- if (muted !== undefined) {
- this.techCall_('setMuted', muted);
- return;
- }
- return this.techGet_('muted') || false;
- }
-
- /**
- * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
- * indicates the state of muted on initial playback.
- *
- * ```js
- * var myPlayer = videojs('some-player-id');
- *
- * myPlayer.src("http://www.example.com/path/to/video.mp4");
- *
- * // get, should be false
- * console.log(myPlayer.defaultMuted());
- * // set to true
- * myPlayer.defaultMuted(true);
- * // get should be true
- * console.log(myPlayer.defaultMuted());
- * ```
- *
- * @param {boolean} [defaultMuted]
- * - true to mute
- * - false to unmute
- *
- * @return {boolean|undefined}
- * - true if defaultMuted is on and getting
- * - false if defaultMuted is off and getting
- * - Nothing when setting
- */
- defaultMuted(defaultMuted) {
- if (defaultMuted !== undefined) {
- this.techCall_('setDefaultMuted', defaultMuted);
- }
- return this.techGet_('defaultMuted') || false;
- }
-
- /**
- * Get the last volume, or set it
- *
- * @param {number} [percentAsDecimal]
- * The new last volume as a decimal percent:
- * - 0 is muted/0%/off
- * - 1.0 is 100%/full
- * - 0.5 is half volume or 50%
- *
- * @return {number|undefined}
- * - The current value of lastVolume as a percent when getting
- * - Nothing when setting
- *
- * @private
- */
- lastVolume_(percentAsDecimal) {
- if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
- this.cache_.lastVolume = percentAsDecimal;
- return;
- }
- return this.cache_.lastVolume;
- }
-
- /**
- * Check if current tech can support native fullscreen
- * (e.g. with built in controls like iOS)
- *
- * @return {boolean}
- * if native fullscreen is supported
- */
- supportsFullScreen() {
- return this.techGet_('supportsFullScreen') || false;
- }
-
- /**
- * Check if the player is in fullscreen mode or tell the player that it
- * is or is not in fullscreen mode.
- *
- * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
- * property and instead document.fullscreenElement is used. But isFullscreen is
- * still a valuable property for internal player workings.
- *
- * @param {boolean} [isFS]
- * Set the players current fullscreen state
- *
- * @return {boolean|undefined}
- * - true if fullscreen is on and getting
- * - false if fullscreen is off and getting
- * - Nothing when setting
- */
- isFullscreen(isFS) {
- if (isFS !== undefined) {
- const oldValue = this.isFullscreen_;
- this.isFullscreen_ = Boolean(isFS);
-
- // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
- // this is the only place where we trigger fullscreenchange events for older browsers
- // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
- if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
- /**
- * @event Player#fullscreenchange
- * @type {Event}
- */
- this.trigger('fullscreenchange');
- }
- this.toggleFullscreenClass_();
- return;
- }
- return this.isFullscreen_;
- }
-
- /**
- * Increase the size of the video to full screen
- * In some browsers, full screen is not supported natively, so it enters
- * "full window mode", where the video fills the browser window.
- * In browsers and devices that support native full screen, sometimes the
- * browser's default controls will be shown, and not the Video.js custom skin.
- * This includes most mobile devices (iOS, Android) and older versions of
- * Safari.
- *
- * @param {Object} [fullscreenOptions]
- * Override the player fullscreen options
- *
- * @fires Player#fullscreenchange
- */
- requestFullscreen(fullscreenOptions) {
- if (this.isInPictureInPicture()) {
- this.exitPictureInPicture();
- }
- const self = this;
- return new Promise((resolve, reject) => {
- function offHandler() {
- self.off('fullscreenerror', errorHandler);
- self.off('fullscreenchange', changeHandler);
- }
- function changeHandler() {
- offHandler();
- resolve();
- }
- function errorHandler(e, err) {
- offHandler();
- reject(err);
- }
- self.one('fullscreenchange', changeHandler);
- self.one('fullscreenerror', errorHandler);
- const promise = self.requestFullscreenHelper_(fullscreenOptions);
- if (promise) {
- promise.then(offHandler, offHandler);
- promise.then(resolve, reject);
- }
- });
- }
- requestFullscreenHelper_(fullscreenOptions) {
- let fsOptions;
-
- // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
- // Use defaults or player configured option unless passed directly to this method.
- if (!this.fsApi_.prefixed) {
- fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
- if (fullscreenOptions !== undefined) {
- fsOptions = fullscreenOptions;
- }
- }
-
- // This method works as follows:
- // 1. if a fullscreen api is available, use it
- // 1. call requestFullscreen with potential options
- // 2. if we got a promise from above, use it to update isFullscreen()
- // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
- // This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
- // 3. otherwise, use "fullWindow" mode
- if (this.fsApi_.requestFullscreen) {
- const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
-
- // Even on browsers with promise support this may not return a promise
- if (promise) {
- promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
- }
- return promise;
- } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
- // we can't take the video.js controls fullscreen but we can go fullscreen
- // with native controls
- this.techCall_('enterFullScreen');
- } else {
- // fullscreen isn't supported so we'll just stretch the video element to
- // fill the viewport
- this.enterFullWindow();
- }
- }
-
- /**
- * Return the video to its normal size after having been in full screen mode
- *
- * @fires Player#fullscreenchange
- */
- exitFullscreen() {
- const self = this;
- return new Promise((resolve, reject) => {
- function offHandler() {
- self.off('fullscreenerror', errorHandler);
- self.off('fullscreenchange', changeHandler);
- }
- function changeHandler() {
- offHandler();
- resolve();
- }
- function errorHandler(e, err) {
- offHandler();
- reject(err);
- }
- self.one('fullscreenchange', changeHandler);
- self.one('fullscreenerror', errorHandler);
- const promise = self.exitFullscreenHelper_();
- if (promise) {
- promise.then(offHandler, offHandler);
- // map the promise to our resolve/reject methods
- promise.then(resolve, reject);
- }
- });
- }
- exitFullscreenHelper_() {
- if (this.fsApi_.requestFullscreen) {
- const promise = document[this.fsApi_.exitFullscreen]();
-
- // Even on browsers with promise support this may not return a promise
- if (promise) {
- // we're splitting the promise here, so, we want to catch the
- // potential error so that this chain doesn't have unhandled errors
- silencePromise(promise.then(() => this.isFullscreen(false)));
- }
- return promise;
- } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
- this.techCall_('exitFullScreen');
- } else {
- this.exitFullWindow();
- }
- }
-
- /**
- * When fullscreen isn't supported we can stretch the
- * video container to as wide as the browser will let us.
- *
- * @fires Player#enterFullWindow
- */
- enterFullWindow() {
- this.isFullscreen(true);
- this.isFullWindow = true;
-
- // Storing original doc overflow value to return to when fullscreen is off
- this.docOrigOverflow = document.documentElement.style.overflow;
-
- // Add listener for esc key to exit fullscreen
- on(document, 'keydown', this.boundFullWindowOnEscKey_);
-
- // Hide any scroll bars
- document.documentElement.style.overflow = 'hidden';
-
- // Apply fullscreen styles
- addClass(document.body, 'vjs-full-window');
-
- /**
- * @event Player#enterFullWindow
- * @type {Event}
- */
- this.trigger('enterFullWindow');
- }
-
- /**
- * Check for call to either exit full window or
- * full screen on ESC key
- *
- * @param {string} event
- * Event to check for key press
- */
- fullWindowOnEscKey(event) {
- if (keycode.isEventKey(event, 'Esc')) {
- if (this.isFullscreen() === true) {
- if (!this.isFullWindow) {
- this.exitFullscreen();
- } else {
- this.exitFullWindow();
- }
- }
- }
- }
-
- /**
- * Exit full window
- *
- * @fires Player#exitFullWindow
- */
- exitFullWindow() {
- this.isFullscreen(false);
- this.isFullWindow = false;
- off(document, 'keydown', this.boundFullWindowOnEscKey_);
-
- // Unhide scroll bars.
- document.documentElement.style.overflow = this.docOrigOverflow;
-
- // Remove fullscreen styles
- removeClass(document.body, 'vjs-full-window');
-
- // Resize the box, controller, and poster to original sizes
- // this.positionAll();
- /**
- * @event Player#exitFullWindow
- * @type {Event}
- */
- this.trigger('exitFullWindow');
- }
-
- /**
- * Get or set disable Picture-in-Picture mode.
- *
- * @param {boolean} [value]
- * - true will disable Picture-in-Picture mode
- * - false will enable Picture-in-Picture mode
- */
- disablePictureInPicture(value) {
- if (value === undefined) {
- return this.techGet_('disablePictureInPicture');
- }
- this.techCall_('setDisablePictureInPicture', value);
- this.options_.disablePictureInPicture = value;
- this.trigger('disablepictureinpicturechanged');
- }
-
- /**
- * Check if the player is in Picture-in-Picture mode or tell the player that it
- * is or is not in Picture-in-Picture mode.
- *
- * @param {boolean} [isPiP]
- * Set the players current Picture-in-Picture state
- *
- * @return {boolean|undefined}
- * - true if Picture-in-Picture is on and getting
- * - false if Picture-in-Picture is off and getting
- * - nothing if setting
- */
- isInPictureInPicture(isPiP) {
- if (isPiP !== undefined) {
- this.isInPictureInPicture_ = !!isPiP;
- this.togglePictureInPictureClass_();
- return;
- }
- return !!this.isInPictureInPicture_;
- }
-
- /**
- * Create a floating video window always on top of other windows so that users may
- * continue consuming media while they interact with other content sites, or
- * applications on their device.
- *
- * This can use document picture-in-picture or element picture in picture
- *
- * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
- * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
- *
- *
- * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
- * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
- *
- * @fires Player#enterpictureinpicture
- *
- * @return {Promise}
- * A promise with a Picture-in-Picture window.
- */
- requestPictureInPicture() {
- if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) {
- const pipContainer = document.createElement(this.el().tagName);
- pipContainer.classList = this.el().classList;
- pipContainer.classList.add('vjs-pip-container');
- if (this.posterImage) {
- pipContainer.appendChild(this.posterImage.el().cloneNode(true));
- }
- if (this.titleBar) {
- pipContainer.appendChild(this.titleBar.el().cloneNode(true));
- }
- pipContainer.appendChild(createEl('p', {
- className: 'vjs-pip-text'
- }, {}, this.localize('Playing in picture-in-picture')));
- return window.documentPictureInPicture.requestWindow({
- // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
- width: this.videoWidth(),
- height: this.videoHeight()
- }).then(pipWindow => {
- copyStyleSheetsToWindow(pipWindow);
- this.el_.parentNode.insertBefore(pipContainer, this.el_);
- pipWindow.document.body.appendChild(this.el_);
- pipWindow.document.body.classList.add('vjs-pip-window');
- this.player_.isInPictureInPicture(true);
- this.player_.trigger({
- type: 'enterpictureinpicture',
- pipWindow
- });
-
- // Listen for the PiP closing event to move the video back.
- pipWindow.addEventListener('pagehide', event => {
- const pipVideo = event.target.querySelector('.video-js');
- pipContainer.parentNode.replaceChild(pipVideo, pipContainer);
- this.player_.isInPictureInPicture(false);
- this.player_.trigger('leavepictureinpicture');
- });
- return pipWindow;
- });
- }
- if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
- /**
- * This event fires when the player enters picture in picture mode
- *
- * @event Player#enterpictureinpicture
- * @type {Event}
- */
- return this.techGet_('requestPictureInPicture');
- }
- return Promise.reject('No PiP mode is available');
- }
-
- /**
- * Exit Picture-in-Picture mode.
- *
- * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
- *
- * @fires Player#leavepictureinpicture
- *
- * @return {Promise}
- * A promise.
- */
- exitPictureInPicture() {
- if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
- // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler
- window.documentPictureInPicture.window.close();
- return Promise.resolve();
- }
- if ('pictureInPictureEnabled' in document) {
- /**
- * This event fires when the player leaves picture in picture mode
- *
- * @event Player#leavepictureinpicture
- * @type {Event}
- */
- return document.exitPictureInPicture();
- }
- }
-
- /**
- * Called when this Player has focus and a key gets pressed down, or when
- * any Component of this player receives a key press that it doesn't handle.
- * This allows player-wide hotkeys (either as defined below, or optionally
- * by an external function).
- *
- * @param {KeyboardEvent} event
- * The `keydown` event that caused this function to be called.
- *
- * @listens keydown
- */
- handleKeyDown(event) {
- const {
- userActions
- } = this.options_;
-
- // Bail out if hotkeys are not configured.
- if (!userActions || !userActions.hotkeys) {
- return;
- }
-
- // Function that determines whether or not to exclude an element from
- // hotkeys handling.
- const excludeElement = el => {
- const tagName = el.tagName.toLowerCase();
-
- // The first and easiest test is for `contenteditable` elements.
- if (el.isContentEditable) {
- return true;
- }
-
- // Inputs matching these types will still trigger hotkey handling as
- // they are not text inputs.
- const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
- if (tagName === 'input') {
- return allowedInputTypes.indexOf(el.type) === -1;
- }
-
- // The final test is by tag name. These tags will be excluded entirely.
- const excludedTags = ['textarea'];
- return excludedTags.indexOf(tagName) !== -1;
- };
-
- // Bail out if the user is focused on an interactive form element.
- if (excludeElement(this.el_.ownerDocument.activeElement)) {
- return;
- }
- if (typeof userActions.hotkeys === 'function') {
- userActions.hotkeys.call(this, event);
- } else {
- this.handleHotkeys(event);
- }
- }
-
- /**
- * Called when this Player receives a hotkey keydown event.
- * Supported player-wide hotkeys are:
- *
- * f - toggle fullscreen
- * m - toggle mute
- * k or Space - toggle play/pause
- *
- * @param {Event} event
- * The `keydown` event that caused this function to be called.
- */
- handleHotkeys(event) {
- const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
-
- // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
- const {
- fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'),
- muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'),
- playPauseKey = keydownEvent => keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space')
- } = hotkeys;
- if (fullscreenKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const FSToggle = Component$1.getComponent('FullscreenToggle');
- if (document[this.fsApi_.fullscreenEnabled] !== false) {
- FSToggle.prototype.handleClick.call(this, event);
- }
- } else if (muteKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const MuteToggle = Component$1.getComponent('MuteToggle');
- MuteToggle.prototype.handleClick.call(this, event);
- } else if (playPauseKey.call(this, event)) {
- event.preventDefault();
- event.stopPropagation();
- const PlayToggle = Component$1.getComponent('PlayToggle');
- PlayToggle.prototype.handleClick.call(this, event);
- }
- }
-
- /**
- * Check whether the player can play a given mimetype
- *
- * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
- *
- * @param {string} type
- * The mimetype to check
- *
- * @return {string}
- * 'probably', 'maybe', or '' (empty string)
- */
- canPlayType(type) {
- let can;
-
- // Loop through each playback technology in the options order
- for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
- const techName = j[i];
- let tech = Tech.getTech(techName);
-
- // Support old behavior of techs being registered as components.
- // Remove once that deprecated behavior is removed.
- if (!tech) {
- tech = Component$1.getComponent(techName);
- }
-
- // Check if the current tech is defined before continuing
- if (!tech) {
- log$1.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
- continue;
- }
-
- // Check if the browser supports this technology
- if (tech.isSupported()) {
- can = tech.canPlayType(type);
- if (can) {
- return can;
- }
- }
- }
- return '';
- }
-
- /**
- * Select source based on tech-order or source-order
- * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
- * defaults to tech-order selection
- *
- * @param {Array} sources
- * The sources for a media asset
- *
- * @return {Object|boolean}
- * Object of source and tech order or false
- */
- selectSource(sources) {
- // Get only the techs specified in `techOrder` that exist and are supported by the
- // current platform
- const techs = this.options_.techOrder.map(techName => {
- return [techName, Tech.getTech(techName)];
- }).filter(([techName, tech]) => {
- // Check if the current tech is defined before continuing
- if (tech) {
- // Check if the browser supports this technology
- return tech.isSupported();
- }
- log$1.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
- return false;
- });
-
- // Iterate over each `innerArray` element once per `outerArray` element and execute
- // `tester` with both. If `tester` returns a non-falsy value, exit early and return
- // that value.
- const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
- let found;
- outerArray.some(outerChoice => {
- return innerArray.some(innerChoice => {
- found = tester(outerChoice, innerChoice);
- if (found) {
- return true;
- }
- });
- });
- return found;
- };
- let foundSourceAndTech;
- const flip = fn => (a, b) => fn(b, a);
- const finder = ([techName, tech], source) => {
- if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
- return {
- source,
- tech: techName
- };
- }
- };
-
- // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
- // to select from them based on their priority.
- if (this.options_.sourceOrder) {
- // Source-first ordering
- foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
- } else {
- // Tech-first ordering
- foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
- }
- return foundSourceAndTech || false;
- }
-
- /**
- * Executes source setting and getting logic
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
- * A SourceObject, an array of SourceObjects, or a string referencing
- * a URL to a media source. It is _highly recommended_ that an object
- * or array of objects is used here, so that source selection
- * algorithms can take the `type` into account.
- *
- * If not provided, this method acts as a getter.
- * @param {boolean} [isRetry]
- * Indicates whether this is being called internally as a result of a retry
- *
- * @return {string|undefined}
- * If the `source` argument is missing, returns the current source
- * URL. Otherwise, returns nothing/undefined.
- */
- handleSrc_(source, isRetry) {
- // getter usage
- if (typeof source === 'undefined') {
- return this.cache_.src || '';
- }
-
- // Reset retry behavior for new source
- if (this.resetRetryOnError_) {
- this.resetRetryOnError_();
- }
-
- // filter out invalid sources and turn our source into
- // an array of source objects
- const sources = filterSource(source);
-
- // if a source was passed in then it is invalid because
- // it was filtered to a zero length Array. So we have to
- // show an error
- if (!sources.length) {
- this.setTimeout(function () {
- this.error({
- code: 4,
- message: this.options_.notSupportedMessage
- });
- }, 0);
- return;
- }
-
- // initial sources
- this.changingSrc_ = true;
-
- // Only update the cached source list if we are not retrying a new source after error,
- // since in that case we want to include the failed source(s) in the cache
- if (!isRetry) {
- this.cache_.sources = sources;
- }
- this.updateSourceCaches_(sources[0]);
-
- // middlewareSource is the source after it has been changed by middleware
- setSource(this, sources[0], (middlewareSource, mws) => {
- this.middleware_ = mws;
-
- // since sourceSet is async we have to update the cache again after we select a source since
- // the source that is selected could be out of order from the cache update above this callback.
- if (!isRetry) {
- this.cache_.sources = sources;
- }
- this.updateSourceCaches_(middlewareSource);
- const err = this.src_(middlewareSource);
- if (err) {
- if (sources.length > 1) {
- return this.handleSrc_(sources.slice(1));
- }
- this.changingSrc_ = false;
-
- // We need to wrap this in a timeout to give folks a chance to add error event handlers
- this.setTimeout(function () {
- this.error({
- code: 4,
- message: this.options_.notSupportedMessage
- });
- }, 0);
-
- // we could not find an appropriate tech, but let's still notify the delegate that this is it
- // this needs a better comment about why this is needed
- this.triggerReady();
- return;
- }
- setTech(mws, this.tech_);
- });
-
- // Try another available source if this one fails before playback.
- if (sources.length > 1) {
- const retry = () => {
- // Remove the error modal
- this.error(null);
- this.handleSrc_(sources.slice(1), true);
- };
- const stopListeningForErrors = () => {
- this.off('error', retry);
- };
- this.one('error', retry);
- this.one('playing', stopListeningForErrors);
- this.resetRetryOnError_ = () => {
- this.off('error', retry);
- this.off('playing', stopListeningForErrors);
- };
- }
- }
-
- /**
- * Get or set the video source.
- *
- * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
- * A SourceObject, an array of SourceObjects, or a string referencing
- * a URL to a media source. It is _highly recommended_ that an object
- * or array of objects is used here, so that source selection
- * algorithms can take the `type` into account.
- *
- * If not provided, this method acts as a getter.
- *
- * @return {string|undefined}
- * If the `source` argument is missing, returns the current source
- * URL. Otherwise, returns nothing/undefined.
- */
- src(source) {
- return this.handleSrc_(source, false);
- }
-
- /**
- * Set the source object on the tech, returns a boolean that indicates whether
- * there is a tech that can play the source or not
- *
- * @param {Tech~SourceObject} source
- * The source object to set on the Tech
- *
- * @return {boolean}
- * - True if there is no Tech to playback this source
- * - False otherwise
- *
- * @private
- */
- src_(source) {
- const sourceTech = this.selectSource([source]);
- if (!sourceTech) {
- return true;
- }
- if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
- this.changingSrc_ = true;
- // load this technology with the chosen source
- this.loadTech_(sourceTech.tech, sourceTech.source);
- this.tech_.ready(() => {
- this.changingSrc_ = false;
- });
- return false;
- }
-
- // wait until the tech is ready to set the source
- // and set it synchronously if possible (#2326)
- this.ready(function () {
- // The setSource tech method was added with source handlers
- // so older techs won't support it
- // We need to check the direct prototype for the case where subclasses
- // of the tech do not support source handlers
- if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
- this.techCall_('setSource', source);
- } else {
- this.techCall_('src', source.src);
- }
- this.changingSrc_ = false;
- }, true);
- return false;
- }
-
- /**
- * Begin loading the src data.
- */
- load() {
- // Workaround to use the load method with the VHS.
- // Does not cover the case when the load method is called directly from the mediaElement.
- if (this.tech_ && this.tech_.vhs) {
- this.src(this.currentSource());
- return;
- }
- this.techCall_('load');
- }
-
- /**
- * Reset the player. Loads the first tech in the techOrder,
- * removes all the text tracks in the existing `tech`,
- * and calls `reset` on the `tech`.
- */
- reset() {
- if (this.paused()) {
- this.doReset_();
- } else {
- const playPromise = this.play();
- silencePromise(playPromise.then(() => this.doReset_()));
- }
- }
- doReset_() {
- if (this.tech_) {
- this.tech_.clearTracks('text');
- }
- this.removeClass('vjs-playing');
- this.addClass('vjs-paused');
- this.resetCache_();
- this.poster('');
- this.loadTech_(this.options_.techOrder[0], null);
- this.techCall_('reset');
- this.resetControlBarUI_();
- this.error(null);
- if (this.titleBar) {
- this.titleBar.update({
- title: undefined,
- description: undefined
- });
- }
- if (isEvented(this)) {
- this.trigger('playerreset');
- }
- }
-
- /**
- * Reset Control Bar's UI by calling sub-methods that reset
- * all of Control Bar's components
- */
- resetControlBarUI_() {
- this.resetProgressBar_();
- this.resetPlaybackRate_();
- this.resetVolumeBar_();
- }
-
- /**
- * Reset tech's progress so progress bar is reset in the UI
- */
- resetProgressBar_() {
- this.currentTime(0);
- const {
- currentTimeDisplay,
- durationDisplay,
- progressControl,
- remainingTimeDisplay
- } = this.controlBar || {};
- const {
- seekBar
- } = progressControl || {};
- if (currentTimeDisplay) {
- currentTimeDisplay.updateContent();
- }
- if (durationDisplay) {
- durationDisplay.updateContent();
- }
- if (remainingTimeDisplay) {
- remainingTimeDisplay.updateContent();
- }
- if (seekBar) {
- seekBar.update();
- if (seekBar.loadProgressBar) {
- seekBar.loadProgressBar.update();
- }
- }
- }
-
- /**
- * Reset Playback ratio
- */
- resetPlaybackRate_() {
- this.playbackRate(this.defaultPlaybackRate());
- this.handleTechRateChange_();
- }
-
- /**
- * Reset Volume bar
- */
- resetVolumeBar_() {
- this.volume(1.0);
- this.trigger('volumechange');
- }
-
- /**
- * Returns all of the current source objects.
- *
- * @return {Tech~SourceObject[]}
- * The current source objects
- */
- currentSources() {
- const source = this.currentSource();
- const sources = [];
-
- // assume `{}` or `{ src }`
- if (Object.keys(source).length !== 0) {
- sources.push(source);
- }
- return this.cache_.sources || sources;
- }
-
- /**
- * Returns the current source object.
- *
- * @return {Tech~SourceObject}
- * The current source object
- */
- currentSource() {
- return this.cache_.source || {};
- }
-
- /**
- * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
- * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
- *
- * @return {string}
- * The current source
- */
- currentSrc() {
- return this.currentSource() && this.currentSource().src || '';
- }
-
- /**
- * Get the current source type e.g. video/mp4
- * This can allow you rebuild the current source object so that you could load the same
- * source and tech later
- *
- * @return {string}
- * The source MIME type
- */
- currentType() {
- return this.currentSource() && this.currentSource().type || '';
- }
-
- /**
- * Get or set the preload attribute
- *
- * @param {'none'|'auto'|'metadata'} [value]
- * Preload mode to pass to tech
- *
- * @return {string|undefined}
- * - The preload attribute value when getting
- * - Nothing when setting
- */
- preload(value) {
- if (value !== undefined) {
- this.techCall_('setPreload', value);
- this.options_.preload = value;
- return;
- }
- return this.techGet_('preload');
- }
-
- /**
- * Get or set the autoplay option. When this is a boolean it will
- * modify the attribute on the tech. When this is a string the attribute on
- * the tech will be removed and `Player` will handle autoplay on loadstarts.
- *
- * @param {boolean|'play'|'muted'|'any'} [value]
- * - true: autoplay using the browser behavior
- * - false: do not autoplay
- * - 'play': call play() on every loadstart
- * - 'muted': call muted() then play() on every loadstart
- * - 'any': call play() on every loadstart. if that fails call muted() then play().
- * - *: values other than those listed here will be set `autoplay` to true
- *
- * @return {boolean|string|undefined}
- * - The current value of autoplay when getting
- * - Nothing when setting
- */
- autoplay(value) {
- // getter usage
- if (value === undefined) {
- return this.options_.autoplay || false;
- }
- let techAutoplay;
-
- // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
- if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
- this.options_.autoplay = value;
- this.manualAutoplay_(typeof value === 'string' ? value : 'play');
- techAutoplay = false;
-
- // any falsy value sets autoplay to false in the browser,
- // lets do the same
- } else if (!value) {
- this.options_.autoplay = false;
-
- // any other value (ie truthy) sets autoplay to true
- } else {
- this.options_.autoplay = true;
- }
- techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
-
- // if we don't have a tech then we do not queue up
- // a setAutoplay call on tech ready. We do this because the
- // autoplay option will be passed in the constructor and we
- // do not need to set it twice
- if (this.tech_) {
- this.techCall_('setAutoplay', techAutoplay);
- }
- }
-
- /**
- * Set or unset the playsinline attribute.
- * Playsinline tells the browser that non-fullscreen playback is preferred.
- *
- * @param {boolean} [value]
- * - true means that we should try to play inline by default
- * - false means that we should use the browser's default playback mode,
- * which in most cases is inline. iOS Safari is a notable exception
- * and plays fullscreen by default.
- *
- * @return {string|undefined}
- * - the current value of playsinline
- * - Nothing when setting
- *
- * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
- */
- playsinline(value) {
- if (value !== undefined) {
- this.techCall_('setPlaysinline', value);
- this.options_.playsinline = value;
- }
- return this.techGet_('playsinline');
- }
-
- /**
- * Get or set the loop attribute on the video element.
- *
- * @param {boolean} [value]
- * - true means that we should loop the video
- * - false means that we should not loop the video
- *
- * @return {boolean|undefined}
- * - The current value of loop when getting
- * - Nothing when setting
- */
- loop(value) {
- if (value !== undefined) {
- this.techCall_('setLoop', value);
- this.options_.loop = value;
- return;
- }
- return this.techGet_('loop');
- }
-
- /**
- * Get or set the poster image source url
- *
- * @fires Player#posterchange
- *
- * @param {string} [src]
- * Poster image source URL
- *
- * @return {string|undefined}
- * - The current value of poster when getting
- * - Nothing when setting
- */
- poster(src) {
- if (src === undefined) {
- return this.poster_;
- }
-
- // The correct way to remove a poster is to set as an empty string
- // other falsey values will throw errors
- if (!src) {
- src = '';
- }
- if (src === this.poster_) {
- return;
- }
-
- // update the internal poster variable
- this.poster_ = src;
-
- // update the tech's poster
- this.techCall_('setPoster', src);
- this.isPosterFromTech_ = false;
-
- // alert components that the poster has been set
- /**
- * This event fires when the poster image is changed on the player.
- *
- * @event Player#posterchange
- * @type {Event}
- */
- this.trigger('posterchange');
- }
-
- /**
- * Some techs (e.g. YouTube) can provide a poster source in an
- * asynchronous way. We want the poster component to use this
- * poster source so that it covers up the tech's controls.
- * (YouTube's play button). However we only want to use this
- * source if the player user hasn't set a poster through
- * the normal APIs.
- *
- * @fires Player#posterchange
- * @listens Tech#posterchange
- * @private
- */
- handleTechPosterChange_() {
- if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
- const newPoster = this.tech_.poster() || '';
- if (newPoster !== this.poster_) {
- this.poster_ = newPoster;
- this.isPosterFromTech_ = true;
-
- // Let components know the poster has changed
- this.trigger('posterchange');
- }
- }
- }
-
- /**
- * Get or set whether or not the controls are showing.
- *
- * @fires Player#controlsenabled
- *
- * @param {boolean} [bool]
- * - true to turn controls on
- * - false to turn controls off
- *
- * @return {boolean|undefined}
- * - The current value of controls when getting
- * - Nothing when setting
- */
- controls(bool) {
- if (bool === undefined) {
- return !!this.controls_;
- }
- bool = !!bool;
-
- // Don't trigger a change event unless it actually changed
- if (this.controls_ === bool) {
- return;
- }
- this.controls_ = bool;
- if (this.usingNativeControls()) {
- this.techCall_('setControls', bool);
- }
- if (this.controls_) {
- this.removeClass('vjs-controls-disabled');
- this.addClass('vjs-controls-enabled');
- /**
- * @event Player#controlsenabled
- * @type {Event}
- */
- this.trigger('controlsenabled');
- if (!this.usingNativeControls()) {
- this.addTechControlsListeners_();
- }
- } else {
- this.removeClass('vjs-controls-enabled');
- this.addClass('vjs-controls-disabled');
- /**
- * @event Player#controlsdisabled
- * @type {Event}
- */
- this.trigger('controlsdisabled');
- if (!this.usingNativeControls()) {
- this.removeTechControlsListeners_();
- }
- }
- }
-
- /**
- * Toggle native controls on/off. Native controls are the controls built into
- * devices (e.g. default iPhone controls) or other techs
- * (e.g. Vimeo Controls)
- * **This should only be set by the current tech, because only the tech knows
- * if it can support native controls**
- *
- * @fires Player#usingnativecontrols
- * @fires Player#usingcustomcontrols
- *
- * @param {boolean} [bool]
- * - true to turn native controls on
- * - false to turn native controls off
- *
- * @return {boolean|undefined}
- * - The current value of native controls when getting
- * - Nothing when setting
- */
- usingNativeControls(bool) {
- if (bool === undefined) {
- return !!this.usingNativeControls_;
- }
- bool = !!bool;
-
- // Don't trigger a change event unless it actually changed
- if (this.usingNativeControls_ === bool) {
- return;
- }
- this.usingNativeControls_ = bool;
- if (this.usingNativeControls_) {
- this.addClass('vjs-using-native-controls');
-
- /**
- * player is using the native device controls
- *
- * @event Player#usingnativecontrols
- * @type {Event}
- */
- this.trigger('usingnativecontrols');
- } else {
- this.removeClass('vjs-using-native-controls');
-
- /**
- * player is using the custom HTML controls
- *
- * @event Player#usingcustomcontrols
- * @type {Event}
- */
- this.trigger('usingcustomcontrols');
- }
- }
-
- /**
- * Set or get the current MediaError
- *
- * @fires Player#error
- *
- * @param {MediaError|string|number} [err]
- * A MediaError or a string/number to be turned
- * into a MediaError
- *
- * @return {MediaError|null|undefined}
- * - The current MediaError when getting (or null)
- * - Nothing when setting
- */
- error(err) {
- if (err === undefined) {
- return this.error_ || null;
- }
-
- // allow hooks to modify error object
- hooks('beforeerror').forEach(hookFunction => {
- const newErr = hookFunction(this, err);
- if (!(isObject$1(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
- this.log.error('please return a value that MediaError expects in beforeerror hooks');
- return;
- }
- err = newErr;
- });
-
- // Suppress the first error message for no compatible source until
- // user interaction
- if (this.options_.suppressNotSupportedError && err && err.code === 4) {
- const triggerSuppressedError = function () {
- this.error(err);
- };
- this.options_.suppressNotSupportedError = false;
- this.any(['click', 'touchstart'], triggerSuppressedError);
- this.one('loadstart', function () {
- this.off(['click', 'touchstart'], triggerSuppressedError);
- });
- return;
- }
-
- // restoring to default
- if (err === null) {
- this.error_ = null;
- this.removeClass('vjs-error');
- if (this.errorDisplay) {
- this.errorDisplay.close();
- }
- return;
- }
- this.error_ = new MediaError(err);
-
- // add the vjs-error classname to the player
- this.addClass('vjs-error');
-
- // log the name of the error type and any message
- // IE11 logs "[object object]" and required you to expand message to see error object
- log$1.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
-
- /**
- * @event Player#error
- * @type {Event}
- */
- this.trigger('error');
-
- // notify hooks of the per player error
- hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
- return;
- }
-
- /**
- * Report user activity
- *
- * @param {Object} event
- * Event object
- */
- reportUserActivity(event) {
- this.userActivity_ = true;
- }
-
- /**
- * Get/set if user is active
- *
- * @fires Player#useractive
- * @fires Player#userinactive
- *
- * @param {boolean} [bool]
- * - true if the user is active
- * - false if the user is inactive
- *
- * @return {boolean|undefined}
- * - The current value of userActive when getting
- * - Nothing when setting
- */
- userActive(bool) {
- if (bool === undefined) {
- return this.userActive_;
- }
- bool = !!bool;
- if (bool === this.userActive_) {
- return;
- }
- this.userActive_ = bool;
- if (this.userActive_) {
- this.userActivity_ = true;
- this.removeClass('vjs-user-inactive');
- this.addClass('vjs-user-active');
- /**
- * @event Player#useractive
- * @type {Event}
- */
- this.trigger('useractive');
- return;
- }
-
- // Chrome/Safari/IE have bugs where when you change the cursor it can
- // trigger a mousemove event. This causes an issue when you're hiding
- // the cursor when the user is inactive, and a mousemove signals user
- // activity. Making it impossible to go into inactive mode. Specifically
- // this happens in fullscreen when we really need to hide the cursor.
- //
- // When this gets resolved in ALL browsers it can be removed
- // https://code.google.com/p/chromium/issues/detail?id=103041
- if (this.tech_) {
- this.tech_.one('mousemove', function (e) {
- e.stopPropagation();
- e.preventDefault();
- });
- }
- this.userActivity_ = false;
- this.removeClass('vjs-user-active');
- this.addClass('vjs-user-inactive');
- /**
- * @event Player#userinactive
- * @type {Event}
- */
- this.trigger('userinactive');
- }
-
- /**
- * Listen for user activity based on timeout value
- *
- * @private
- */
- listenForUserActivity_() {
- let mouseInProgress;
- let lastMoveX;
- let lastMoveY;
- const handleActivity = bind_(this, this.reportUserActivity);
- const handleMouseMove = function (e) {
- // #1068 - Prevent mousemove spamming
- // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
- if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
- lastMoveX = e.screenX;
- lastMoveY = e.screenY;
- handleActivity();
- }
- };
- const handleMouseDown = function () {
- handleActivity();
- // For as long as the they are touching the device or have their mouse down,
- // we consider them active even if they're not moving their finger or mouse.
- // So we want to continue to update that they are active
- this.clearInterval(mouseInProgress);
- // Setting userActivity=true now and setting the interval to the same time
- // as the activityCheck interval (250) should ensure we never miss the
- // next activityCheck
- mouseInProgress = this.setInterval(handleActivity, 250);
- };
- const handleMouseUpAndMouseLeave = function (event) {
- handleActivity();
- // Stop the interval that maintains activity if the mouse/touch is down
- this.clearInterval(mouseInProgress);
- };
-
- // Any mouse movement will be considered user activity
- this.on('mousedown', handleMouseDown);
- this.on('mousemove', handleMouseMove);
- this.on('mouseup', handleMouseUpAndMouseLeave);
- this.on('mouseleave', handleMouseUpAndMouseLeave);
- const controlBar = this.getChild('controlBar');
-
- // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
- // controlBar would no longer be hidden by default timeout.
- if (controlBar && !IS_IOS && !IS_ANDROID) {
- controlBar.on('mouseenter', function (event) {
- if (this.player().options_.inactivityTimeout !== 0) {
- this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
- }
- this.player().options_.inactivityTimeout = 0;
- });
- controlBar.on('mouseleave', function (event) {
- this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
- });
- }
-
- // Listen for keyboard navigation
- // Shouldn't need to use inProgress interval because of key repeat
- this.on('keydown', handleActivity);
- this.on('keyup', handleActivity);
-
- // Run an interval every 250 milliseconds instead of stuffing everything into
- // the mousemove/touchmove function itself, to prevent performance degradation.
- // `this.reportUserActivity` simply sets this.userActivity_ to true, which
- // then gets picked up by this loop
- // http://ejohn.org/blog/learning-from-twitter/
- let inactivityTimeout;
-
- /** @this Player */
- const activityCheck = function () {
- // Check to see if mouse/touch activity has happened
- if (!this.userActivity_) {
- return;
- }
-
- // Reset the activity tracker
- this.userActivity_ = false;
-
- // If the user state was inactive, set the state to active
- this.userActive(true);
-
- // Clear any existing inactivity timeout to start the timer over
- this.clearTimeout(inactivityTimeout);
- const timeout = this.options_.inactivityTimeout;
- if (timeout <= 0) {
- return;
- }
-
- // In milliseconds, if no more activity has occurred the
- // user will be considered inactive
- inactivityTimeout = this.setTimeout(function () {
- // Protect against the case where the inactivityTimeout can trigger just
- // before the next user activity is picked up by the activity check loop
- // causing a flicker
- if (!this.userActivity_) {
- this.userActive(false);
- }
- }, timeout);
- };
- this.setInterval(activityCheck, 250);
- }
-
- /**
- * Gets or sets the current playback rate. A playback rate of
- * 1.0 represents normal speed and 0.5 would indicate half-speed
- * playback, for instance.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
- *
- * @param {number} [rate]
- * New playback rate to set.
- *
- * @return {number|undefined}
- * - The current playback rate when getting or 1.0
- * - Nothing when setting
- */
- playbackRate(rate) {
- if (rate !== undefined) {
- // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
- // that is registered above
- this.techCall_('setPlaybackRate', rate);
- return;
- }
- if (this.tech_ && this.tech_.featuresPlaybackRate) {
- return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
- }
- return 1.0;
- }
-
- /**
- * Gets or sets the current default playback rate. A default playback rate of
- * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
- * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
- * not the current playbackRate.
- *
- * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
- *
- * @param {number} [rate]
- * New default playback rate to set.
- *
- * @return {number|undefined}
- * - The default playback rate when getting or 1.0
- * - Nothing when setting
- */
- defaultPlaybackRate(rate) {
- if (rate !== undefined) {
- return this.techCall_('setDefaultPlaybackRate', rate);
- }
- if (this.tech_ && this.tech_.featuresPlaybackRate) {
- return this.techGet_('defaultPlaybackRate');
- }
- return 1.0;
- }
-
- /**
- * Gets or sets the audio flag
- *
- * @param {boolean} [bool]
- * - true signals that this is an audio player
- * - false signals that this is not an audio player
- *
- * @return {boolean|undefined}
- * - The current value of isAudio when getting
- * - Nothing when setting
- */
- isAudio(bool) {
- if (bool !== undefined) {
- this.isAudio_ = !!bool;
- return;
- }
- return !!this.isAudio_;
- }
- enableAudioOnlyUI_() {
- // Update styling immediately to show the control bar so we can get its height
- this.addClass('vjs-audio-only-mode');
- const playerChildren = this.children();
- const controlBar = this.getChild('ControlBar');
- const controlBarHeight = controlBar && controlBar.currentHeight();
-
- // Hide all player components except the control bar. Control bar components
- // needed only for video are hidden with CSS
- playerChildren.forEach(child => {
- if (child === controlBar) {
- return;
- }
- if (child.el_ && !child.hasClass('vjs-hidden')) {
- child.hide();
- this.audioOnlyCache_.hiddenChildren.push(child);
- }
- });
- this.audioOnlyCache_.playerHeight = this.currentHeight();
-
- // Set the player height the same as the control bar
- this.height(controlBarHeight);
- this.trigger('audioonlymodechange');
- }
- disableAudioOnlyUI_() {
- this.removeClass('vjs-audio-only-mode');
-
- // Show player components that were previously hidden
- this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
-
- // Reset player height
- this.height(this.audioOnlyCache_.playerHeight);
- this.trigger('audioonlymodechange');
- }
-
- /**
- * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
- *
- * Setting this to `true` will hide all player components except the control bar,
- * as well as control bar components needed only for video.
- *
- * @param {boolean} [value]
- * The value to set audioOnlyMode to.
- *
- * @return {Promise|boolean}
- * A Promise is returned when setting the state, and a boolean when getting
- * the present state
- */
- audioOnlyMode(value) {
- if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
- return this.audioOnlyMode_;
- }
- this.audioOnlyMode_ = value;
-
- // Enable Audio Only Mode
- if (value) {
- const exitPromises = [];
-
- // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
- if (this.isInPictureInPicture()) {
- exitPromises.push(this.exitPictureInPicture());
- }
- if (this.isFullscreen()) {
- exitPromises.push(this.exitFullscreen());
- }
- if (this.audioPosterMode()) {
- exitPromises.push(this.audioPosterMode(false));
- }
- return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
- }
-
- // Disable Audio Only Mode
- return Promise.resolve().then(() => this.disableAudioOnlyUI_());
- }
- enablePosterModeUI_() {
- // Hide the video element and show the poster image to enable posterModeUI
- const tech = this.tech_ && this.tech_;
- tech.hide();
- this.addClass('vjs-audio-poster-mode');
- this.trigger('audiopostermodechange');
- }
- disablePosterModeUI_() {
- // Show the video element and hide the poster image to disable posterModeUI
- const tech = this.tech_ && this.tech_;
- tech.show();
- this.removeClass('vjs-audio-poster-mode');
- this.trigger('audiopostermodechange');
- }
-
- /**
- * Get the current audioPosterMode state or set audioPosterMode to true or false
- *
- * @param {boolean} [value]
- * The value to set audioPosterMode to.
- *
- * @return {Promise|boolean}
- * A Promise is returned when setting the state, and a boolean when getting
- * the present state
- */
- audioPosterMode(value) {
- if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
- return this.audioPosterMode_;
- }
- this.audioPosterMode_ = value;
- if (value) {
- if (this.audioOnlyMode()) {
- const audioOnlyModePromise = this.audioOnlyMode(false);
- return audioOnlyModePromise.then(() => {
- // enable audio poster mode after audio only mode is disabled
- this.enablePosterModeUI_();
- });
- }
- return Promise.resolve().then(() => {
- // enable audio poster mode
- this.enablePosterModeUI_();
- });
- }
- return Promise.resolve().then(() => {
- // disable audio poster mode
- this.disablePosterModeUI_();
- });
- }
-
- /**
- * A helper method for adding a {@link TextTrack} to our
- * {@link TextTrackList}.
- *
- * In addition to the W3C settings we allow adding additional info through options.
- *
- * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
- *
- * @param {string} [kind]
- * the kind of TextTrack you are adding
- *
- * @param {string} [label]
- * the label to give the TextTrack label
- *
- * @param {string} [language]
- * the language to set on the TextTrack
- *
- * @return {TextTrack|undefined}
- * the TextTrack that was added or undefined
- * if there is no tech
- */
- addTextTrack(kind, label, language) {
- if (this.tech_) {
- return this.tech_.addTextTrack(kind, label, language);
- }
- }
-
- /**
- * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
- *
- * @param {Object} options
- * Options to pass to {@link HTMLTrackElement} during creation. See
- * {@link HTMLTrackElement} for object properties that you should use.
- *
- * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
- * from the TextTrackList and HtmlTrackElementList
- * after a source change
- *
- * @return { import('./tracks/html-track-element').default }
- * the HTMLTrackElement that was created and added
- * to the HtmlTrackElementList and the remote
- * TextTrackList
- *
- */
- addRemoteTextTrack(options, manualCleanup) {
- if (this.tech_) {
- return this.tech_.addRemoteTextTrack(options, manualCleanup);
- }
- }
-
- /**
- * Remove a remote {@link TextTrack} from the respective
- * {@link TextTrackList} and {@link HtmlTrackElementList}.
- *
- * @param {Object} track
- * Remote {@link TextTrack} to remove
- *
- * @return {undefined}
- * does not return anything
- */
- removeRemoteTextTrack(obj = {}) {
- let {
- track
- } = obj;
- if (!track) {
- track = obj;
- }
-
- // destructure the input into an object with a track argument, defaulting to arguments[0]
- // default the whole argument to an empty object if nothing was passed in
-
- if (this.tech_) {
- return this.tech_.removeRemoteTextTrack(track);
- }
- }
-
- /**
- * Gets available media playback quality metrics as specified by the W3C's Media
- * Playback Quality API.
- *
- * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
- *
- * @return {Object|undefined}
- * An object with supported media playback quality metrics or undefined if there
- * is no tech or the tech does not support it.
- */
- getVideoPlaybackQuality() {
- return this.techGet_('getVideoPlaybackQuality');
- }
-
- /**
- * Get video width
- *
- * @return {number}
- * current video width
- */
- videoWidth() {
- return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
- }
-
- /**
- * Get video height
- *
- * @return {number}
- * current video height
- */
- videoHeight() {
- return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
- }
-
- /**
- * Set or get the player's language code.
- *
- * Changing the language will trigger
- * [languagechange]{@link Player#event:languagechange}
- * which Components can use to update control text.
- * ClickableComponent will update its control text by default on
- * [languagechange]{@link Player#event:languagechange}.
- *
- * @fires Player#languagechange
- *
- * @param {string} [code]
- * the language code to set the player to
- *
- * @return {string|undefined}
- * - The current language code when getting
- * - Nothing when setting
- */
- language(code) {
- if (code === undefined) {
- return this.language_;
- }
- if (this.language_ !== String(code).toLowerCase()) {
- this.language_ = String(code).toLowerCase();
-
- // during first init, it's possible some things won't be evented
- if (isEvented(this)) {
- /**
- * fires when the player language change
- *
- * @event Player#languagechange
- * @type {Event}
- */
- this.trigger('languagechange');
- }
- }
- }
-
- /**
- * Get the player's language dictionary
- * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
- * Languages specified directly in the player options have precedence
- *
- * @return {Array}
- * An array of of supported languages
- */
- languages() {
- return merge$2(Player.prototype.options_.languages, this.languages_);
- }
-
- /**
- * returns a JavaScript object representing the current track
- * information. **DOES not return it as JSON**
- *
- * @return {Object}
- * Object representing the current of track info
- */
- toJSON() {
- const options = merge$2(this.options_);
- const tracks = options.tracks;
- options.tracks = [];
- for (let i = 0; i < tracks.length; i++) {
- let track = tracks[i];
-
- // deep merge tracks and null out player so no circular references
- track = merge$2(track);
- track.player = undefined;
- options.tracks[i] = track;
- }
- return options;
- }
-
- /**
- * Creates a simple modal dialog (an instance of the {@link ModalDialog}
- * component) that immediately overlays the player with arbitrary
- * content and removes itself when closed.
- *
- * @param {string|Function|Element|Array|null} content
- * Same as {@link ModalDialog#content}'s param of the same name.
- * The most straight-forward usage is to provide a string or DOM
- * element.
- *
- * @param {Object} [options]
- * Extra options which will be passed on to the {@link ModalDialog}.
- *
- * @return {ModalDialog}
- * the {@link ModalDialog} that was created
- */
- createModal(content, options) {
- options = options || {};
- options.content = content || '';
- const modal = new ModalDialog(this, options);
- this.addChild(modal);
- modal.on('dispose', () => {
- this.removeChild(modal);
- });
- modal.open();
- return modal;
- }
-
- /**
- * Change breakpoint classes when the player resizes.
- *
- * @private
- */
- updateCurrentBreakpoint_() {
- if (!this.responsive()) {
- return;
- }
- const currentBreakpoint = this.currentBreakpoint();
- const currentWidth = this.currentWidth();
- for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
- const candidateBreakpoint = BREAKPOINT_ORDER[i];
- const maxWidth = this.breakpoints_[candidateBreakpoint];
- if (currentWidth <= maxWidth) {
- // The current breakpoint did not change, nothing to do.
- if (currentBreakpoint === candidateBreakpoint) {
- return;
- }
-
- // Only remove a class if there is a current breakpoint.
- if (currentBreakpoint) {
- this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
- }
- this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
- this.breakpoint_ = candidateBreakpoint;
- break;
- }
- }
- }
-
- /**
- * Removes the current breakpoint.
- *
- * @private
- */
- removeCurrentBreakpoint_() {
- const className = this.currentBreakpointClass();
- this.breakpoint_ = '';
- if (className) {
- this.removeClass(className);
- }
- }
-
- /**
- * Get or set breakpoints on the player.
- *
- * Calling this method with an object or `true` will remove any previous
- * custom breakpoints and start from the defaults again.
- *
- * @param {Object|boolean} [breakpoints]
- * If an object is given, it can be used to provide custom
- * breakpoints. If `true` is given, will set default breakpoints.
- * If this argument is not given, will simply return the current
- * breakpoints.
- *
- * @param {number} [breakpoints.tiny]
- * The maximum width for the "vjs-layout-tiny" class.
- *
- * @param {number} [breakpoints.xsmall]
- * The maximum width for the "vjs-layout-x-small" class.
- *
- * @param {number} [breakpoints.small]
- * The maximum width for the "vjs-layout-small" class.
- *
- * @param {number} [breakpoints.medium]
- * The maximum width for the "vjs-layout-medium" class.
- *
- * @param {number} [breakpoints.large]
- * The maximum width for the "vjs-layout-large" class.
- *
- * @param {number} [breakpoints.xlarge]
- * The maximum width for the "vjs-layout-x-large" class.
- *
- * @param {number} [breakpoints.huge]
- * The maximum width for the "vjs-layout-huge" class.
- *
- * @return {Object}
- * An object mapping breakpoint names to maximum width values.
- */
- breakpoints(breakpoints) {
- // Used as a getter.
- if (breakpoints === undefined) {
- return Object.assign(this.breakpoints_);
- }
- this.breakpoint_ = '';
- this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
-
- // When breakpoint definitions change, we need to update the currently
- // selected breakpoint.
- this.updateCurrentBreakpoint_();
-
- // Clone the breakpoints before returning.
- return Object.assign(this.breakpoints_);
- }
-
- /**
- * Get or set a flag indicating whether or not this player should adjust
- * its UI based on its dimensions.
- *
- * @param {boolean} [value]
- * Should be `true` if the player should adjust its UI based on its
- * dimensions; otherwise, should be `false`.
- *
- * @return {boolean|undefined}
- * Will be `true` if this player should adjust its UI based on its
- * dimensions; otherwise, will be `false`.
- * Nothing if setting
- */
- responsive(value) {
- // Used as a getter.
- if (value === undefined) {
- return this.responsive_;
- }
- value = Boolean(value);
- const current = this.responsive_;
-
- // Nothing changed.
- if (value === current) {
- return;
- }
-
- // The value actually changed, set it.
- this.responsive_ = value;
-
- // Start listening for breakpoints and set the initial breakpoint if the
- // player is now responsive.
- if (value) {
- this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
- this.updateCurrentBreakpoint_();
-
- // Stop listening for breakpoints if the player is no longer responsive.
- } else {
- this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
- this.removeCurrentBreakpoint_();
- }
- return value;
- }
-
- /**
- * Get current breakpoint name, if any.
- *
- * @return {string}
- * If there is currently a breakpoint set, returns a the key from the
- * breakpoints object matching it. Otherwise, returns an empty string.
- */
- currentBreakpoint() {
- return this.breakpoint_;
- }
-
- /**
- * Get the current breakpoint class name.
- *
- * @return {string}
- * The matching class name (e.g. `"vjs-layout-tiny"` or
- * `"vjs-layout-large"`) for the current breakpoint. Empty string if
- * there is no current breakpoint.
- */
- currentBreakpointClass() {
- return BREAKPOINT_CLASSES[this.breakpoint_] || '';
- }
-
- /**
- * An object that describes a single piece of media.
- *
- * Properties that are not part of this type description will be retained; so,
- * this can be viewed as a generic metadata storage mechanism as well.
- *
- * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
- * @typedef {Object} Player~MediaObject
- *
- * @property {string} [album]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {string} [artist]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {Object[]} [artwork]
- * Unused, except if this object is passed to the `MediaSession`
- * API. If not specified, will be populated via the `poster`, if
- * available.
- *
- * @property {string} [poster]
- * URL to an image that will display before playback.
- *
- * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
- * A single source object, an array of source objects, or a string
- * referencing a URL to a media source. It is _highly recommended_
- * that an object or array of objects is used here, so that source
- * selection algorithms can take the `type` into account.
- *
- * @property {string} [title]
- * Unused, except if this object is passed to the `MediaSession`
- * API.
- *
- * @property {Object[]} [textTracks]
- * An array of objects to be used to create text tracks, following
- * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
- * For ease of removal, these will be created as "remote" text
- * tracks and set to automatically clean up on source changes.
- *
- * These objects may have properties like `src`, `kind`, `label`,
- * and `language`, see {@link Tech#createRemoteTextTrack}.
- */
-
- /**
- * Populate the player using a {@link Player~MediaObject|MediaObject}.
- *
- * @param {Player~MediaObject} media
- * A media object.
- *
- * @param {Function} ready
- * A callback to be called when the player is ready.
- */
- loadMedia(media, ready) {
- if (!media || typeof media !== 'object') {
- return;
- }
- const crossOrigin = this.crossOrigin();
- this.reset();
-
- // Clone the media object so it cannot be mutated from outside.
- this.cache_.media = merge$2(media);
- const {
- artist,
- artwork,
- description,
- poster,
- src,
- textTracks,
- title
- } = this.cache_.media;
-
- // If `artwork` is not given, create it using `poster`.
- if (!artwork && poster) {
- this.cache_.media.artwork = [{
- src: poster,
- type: getMimetype(poster)
- }];
- }
- if (crossOrigin) {
- this.crossOrigin(crossOrigin);
- }
- if (src) {
- this.src(src);
- }
- if (poster) {
- this.poster(poster);
- }
- if (Array.isArray(textTracks)) {
- textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
- }
- if (this.titleBar) {
- this.titleBar.update({
- title,
- description: description || artist || ''
- });
- }
- this.ready(ready);
- }
-
- /**
- * Get a clone of the current {@link Player~MediaObject} for this player.
- *
- * If the `loadMedia` method has not been used, will attempt to return a
- * {@link Player~MediaObject} based on the current state of the player.
- *
- * @return {Player~MediaObject}
- */
- getMedia() {
- if (!this.cache_.media) {
- const poster = this.poster();
- const src = this.currentSources();
- const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
- kind: tt.kind,
- label: tt.label,
- language: tt.language,
- src: tt.src
- }));
- const media = {
- src,
- textTracks
- };
- if (poster) {
- media.poster = poster;
- media.artwork = [{
- src: media.poster,
- type: getMimetype(media.poster)
- }];
- }
- return media;
- }
- return merge$2(this.cache_.media);
- }
-
- /**
- * Gets tag settings
- *
- * @param {Element} tag
- * The player tag
- *
- * @return {Object}
- * An object containing all of the settings
- * for a player tag
- */
- static getTagSettings(tag) {
- const baseOptions = {
- sources: [],
- tracks: []
- };
- const tagOptions = getAttributes(tag);
- const dataSetup = tagOptions['data-setup'];
- if (hasClass(tag, 'vjs-fill')) {
- tagOptions.fill = true;
- }
- if (hasClass(tag, 'vjs-fluid')) {
- tagOptions.fluid = true;
- }
-
- // Check if data-setup attr exists.
- if (dataSetup !== null) {
- // Parse options JSON
- // If empty string, make it a parsable json object.
- const [err, data] = tuple(dataSetup || '{}');
- if (err) {
- log$1.error(err);
- }
- Object.assign(tagOptions, data);
- }
- Object.assign(baseOptions, tagOptions);
-
- // Get tag children settings
- if (tag.hasChildNodes()) {
- const children = tag.childNodes;
- for (let i = 0, j = children.length; i < j; i++) {
- const child = children[i];
- // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
- const childName = child.nodeName.toLowerCase();
- if (childName === 'source') {
- baseOptions.sources.push(getAttributes(child));
- } else if (childName === 'track') {
- baseOptions.tracks.push(getAttributes(child));
- }
- }
- }
- return baseOptions;
- }
-
- /**
- * Set debug mode to enable/disable logs at info level.
- *
- * @param {boolean} enabled
- * @fires Player#debugon
- * @fires Player#debugoff
- * @return {boolean|undefined}
- */
- debug(enabled) {
- if (enabled === undefined) {
- return this.debugEnabled_;
- }
- if (enabled) {
- this.trigger('debugon');
- this.previousLogLevel_ = this.log.level;
- this.log.level('debug');
- this.debugEnabled_ = true;
- } else {
- this.trigger('debugoff');
- this.log.level(this.previousLogLevel_);
- this.previousLogLevel_ = undefined;
- this.debugEnabled_ = false;
- }
- }
-
- /**
- * Set or get current playback rates.
- * Takes an array and updates the playback rates menu with the new items.
- * Pass in an empty array to hide the menu.
- * Values other than arrays are ignored.
- *
- * @fires Player#playbackrateschange
- * @param {number[]} [newRates]
- * The new rates that the playback rates menu should update to.
- * An empty array will hide the menu
- * @return {number[]} When used as a getter will return the current playback rates
- */
- playbackRates(newRates) {
- if (newRates === undefined) {
- return this.cache_.playbackRates;
- }
-
- // ignore any value that isn't an array
- if (!Array.isArray(newRates)) {
- return;
- }
-
- // ignore any arrays that don't only contain numbers
- if (!newRates.every(rate => typeof rate === 'number')) {
- return;
- }
- this.cache_.playbackRates = newRates;
-
- /**
- * fires when the playback rates in a player are changed
- *
- * @event Player#playbackrateschange
- * @type {Event}
- */
- this.trigger('playbackrateschange');
- }
- }
-
- /**
- * Get the {@link VideoTrackList}
- *
- * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
- *
- * @return {VideoTrackList}
- * the current video track list
- *
- * @method Player.prototype.videoTracks
- */
-
- /**
- * Get the {@link AudioTrackList}
- *
- * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
- *
- * @return {AudioTrackList}
- * the current audio track list
- *
- * @method Player.prototype.audioTracks
- */
-
- /**
- * Get the {@link TextTrackList}
- *
- * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
- *
- * @return {TextTrackList}
- * the current text track list
- *
- * @method Player.prototype.textTracks
- */
-
- /**
- * Get the remote {@link TextTrackList}
- *
- * @return {TextTrackList}
- * The current remote text track list
- *
- * @method Player.prototype.remoteTextTracks
- */
-
- /**
- * Get the remote {@link HtmlTrackElementList} tracks.
- *
- * @return {HtmlTrackElementList}
- * The current remote text track element list
- *
- * @method Player.prototype.remoteTextTrackEls
- */
-
- ALL.names.forEach(function (name) {
- const props = ALL[name];
- Player.prototype[props.getterName] = function () {
- if (this.tech_) {
- return this.tech_[props.getterName]();
- }
-
- // if we have not yet loadTech_, we create {video,audio,text}Tracks_
- // these will be passed to the tech during loading
- this[props.privateName] = this[props.privateName] || new props.ListClass();
- return this[props.privateName];
- };
- });
-
- /**
- * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
- * sets the `crossOrigin` property on the `` tag to control the CORS
- * behavior.
- *
- * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
- *
- * @param {string} [value]
- * The value to set the `Player`'s crossorigin to. If an argument is
- * given, must be one of `anonymous` or `use-credentials`.
- *
- * @return {string|undefined}
- * - The current crossorigin value of the `Player` when getting.
- * - undefined when setting
- */
- Player.prototype.crossorigin = Player.prototype.crossOrigin;
-
- /**
- * Global enumeration of players.
- *
- * The keys are the player IDs and the values are either the {@link Player}
- * instance or `null` for disposed players.
- *
- * @type {Object}
- */
- Player.players = {};
- const navigator = window.navigator;
-
- /*
- * Player instance options, surfaced using options
- * options = Player.prototype.options_
- * Make changes in options, not here.
- *
- * @type {Object}
- * @private
- */
- Player.prototype.options_ = {
- // Default order of fallback technology
- techOrder: Tech.defaultTechOrder_,
- html5: {},
- // enable sourceset by default
- enableSourceset: true,
- // default inactivity timeout
- inactivityTimeout: 2000,
- // default playback rates
- playbackRates: [],
- // Add playback rate selection by adding rates
- // 'playbackRates': [0.5, 1, 1.5, 2],
- liveui: false,
- // Included control sets
- children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
- language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
- // locales and their language translations
- languages: {},
- // Default message to show when a video cannot be played.
- notSupportedMessage: 'No compatible source was found for this media.',
- normalizeAutoplay: false,
- fullscreen: {
- options: {
- navigationUI: 'hide'
- }
- },
- breakpoints: {},
- responsive: false,
- audioOnlyMode: false,
- audioPosterMode: false,
- // Default smooth seeking to false
- enableSmoothSeeking: false
- };
- TECH_EVENTS_RETRIGGER.forEach(function (event) {
- Player.prototype[`handleTech${toTitleCase$1(event)}_`] = function () {
- return this.trigger(event);
- };
- });
-
- /**
- * Fired when the player has initial duration and dimension information
- *
- * @event Player#loadedmetadata
- * @type {Event}
- */
-
- /**
- * Fired when the player has downloaded data at the current playback position
- *
- * @event Player#loadeddata
- * @type {Event}
- */
-
- /**
- * Fired when the current playback position has changed *
- * During playback this is fired every 15-250 milliseconds, depending on the
- * playback technology in use.
- *
- * @event Player#timeupdate
- * @type {Event}
- */
-
- /**
- * Fired when the volume changes
- *
- * @event Player#volumechange
- * @type {Event}
- */
-
- /**
- * Reports whether or not a player has a plugin available.
- *
- * This does not report whether or not the plugin has ever been initialized
- * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
- *
- * @method Player#hasPlugin
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not this player has the requested plugin available.
- */
-
- /**
- * Reports whether or not a player is using a plugin by name.
- *
- * For basic plugins, this only reports whether the plugin has _ever_ been
- * initialized on this player.
- *
- * @method Player#usingPlugin
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not this player is using the requested plugin.
- */
-
- Component$1.registerComponent('Player', Player);
-
- /**
- * @file plugin.js
- */
-
- /**
- * The base plugin name.
- *
- * @private
- * @constant
- * @type {string}
- */
- const BASE_PLUGIN_NAME = 'plugin';
-
- /**
- * The key on which a player's active plugins cache is stored.
- *
- * @private
- * @constant
- * @type {string}
- */
- const PLUGIN_CACHE_KEY = 'activePlugins_';
-
- /**
- * Stores registered plugins in a private space.
- *
- * @private
- * @type {Object}
- */
- const pluginStorage = {};
-
- /**
- * Reports whether or not a plugin has been registered.
- *
- * @private
- * @param {string} name
- * The name of a plugin.
- *
- * @return {boolean}
- * Whether or not the plugin has been registered.
- */
- const pluginExists = name => pluginStorage.hasOwnProperty(name);
-
- /**
- * Get a single registered plugin by name.
- *
- * @private
- * @param {string} name
- * The name of a plugin.
- *
- * @return {typeof Plugin|Function|undefined}
- * The plugin (or undefined).
- */
- const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
-
- /**
- * Marks a plugin as "active" on a player.
- *
- * Also, ensures that the player has an object for tracking active plugins.
- *
- * @private
- * @param {Player} player
- * A Video.js player instance.
- *
- * @param {string} name
- * The name of a plugin.
- */
- const markPluginAsActive = (player, name) => {
- player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
- player[PLUGIN_CACHE_KEY][name] = true;
- };
-
- /**
- * Triggers a pair of plugin setup events.
- *
- * @private
- * @param {Player} player
- * A Video.js player instance.
- *
- * @param {PluginEventHash} hash
- * A plugin event hash.
- *
- * @param {boolean} [before]
- * If true, prefixes the event name with "before". In other words,
- * use this to trigger "beforepluginsetup" instead of "pluginsetup".
- */
- const triggerSetupEvent = (player, hash, before) => {
- const eventName = (before ? 'before' : '') + 'pluginsetup';
- player.trigger(eventName, hash);
- player.trigger(eventName + ':' + hash.name, hash);
- };
-
- /**
- * Takes a basic plugin function and returns a wrapper function which marks
- * on the player that the plugin has been activated.
- *
- * @private
- * @param {string} name
- * The name of the plugin.
- *
- * @param {Function} plugin
- * The basic plugin.
- *
- * @return {Function}
- * A wrapper function for the given plugin.
- */
- const createBasicPlugin = function (name, plugin) {
- const basicPluginWrapper = function () {
- // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
- // regardless, but we want the hash to be consistent with the hash provided
- // for advanced plugins.
- //
- // The only potentially counter-intuitive thing here is the `instance` in
- // the "pluginsetup" event is the value returned by the `plugin` function.
- triggerSetupEvent(this, {
- name,
- plugin,
- instance: null
- }, true);
- const instance = plugin.apply(this, arguments);
- markPluginAsActive(this, name);
- triggerSetupEvent(this, {
- name,
- plugin,
- instance
- });
- return instance;
- };
- Object.keys(plugin).forEach(function (prop) {
- basicPluginWrapper[prop] = plugin[prop];
- });
- return basicPluginWrapper;
- };
-
- /**
- * Takes a plugin sub-class and returns a factory function for generating
- * instances of it.
- *
- * This factory function will replace itself with an instance of the requested
- * sub-class of Plugin.
- *
- * @private
- * @param {string} name
- * The name of the plugin.
- *
- * @param {Plugin} PluginSubClass
- * The advanced plugin.
- *
- * @return {Function}
- */
- const createPluginFactory = (name, PluginSubClass) => {
- // Add a `name` property to the plugin prototype so that each plugin can
- // refer to itself by name.
- PluginSubClass.prototype.name = name;
- return function (...args) {
- triggerSetupEvent(this, {
- name,
- plugin: PluginSubClass,
- instance: null
- }, true);
- const instance = new PluginSubClass(...[this, ...args]);
-
- // The plugin is replaced by a function that returns the current instance.
- this[name] = () => instance;
- triggerSetupEvent(this, instance.getEventHash());
- return instance;
- };
- };
-
- /**
- * Parent class for all advanced plugins.
- *
- * @mixes module:evented~EventedMixin
- * @mixes module:stateful~StatefulMixin
- * @fires Player#beforepluginsetup
- * @fires Player#beforepluginsetup:$name
- * @fires Player#pluginsetup
- * @fires Player#pluginsetup:$name
- * @listens Player#dispose
- * @throws {Error}
- * If attempting to instantiate the base {@link Plugin} class
- * directly instead of via a sub-class.
- */
- class Plugin {
- /**
- * Creates an instance of this class.
- *
- * Sub-classes should call `super` to ensure plugins are properly initialized.
- *
- * @param {Player} player
- * A Video.js player instance.
- */
- constructor(player) {
- if (this.constructor === Plugin) {
- throw new Error('Plugin must be sub-classed; not directly instantiated.');
- }
- this.player = player;
- if (!this.log) {
- this.log = this.player.log.createLogger(this.name);
- }
-
- // Make this object evented, but remove the added `trigger` method so we
- // use the prototype version instead.
- evented(this);
- delete this.trigger;
- stateful(this, this.constructor.defaultState);
- markPluginAsActive(player, this.name);
-
- // Auto-bind the dispose method so we can use it as a listener and unbind
- // it later easily.
- this.dispose = this.dispose.bind(this);
-
- // If the player is disposed, dispose the plugin.
- player.on('dispose', this.dispose);
- }
-
- /**
- * Get the version of the plugin that was set on .VERSION
- */
- version() {
- return this.constructor.VERSION;
- }
-
- /**
- * Each event triggered by plugins includes a hash of additional data with
- * conventional properties.
- *
- * This returns that object or mutates an existing hash.
- *
- * @param {Object} [hash={}]
- * An object to be used as event an event hash.
- *
- * @return {PluginEventHash}
- * An event hash object with provided properties mixed-in.
- */
- getEventHash(hash = {}) {
- hash.name = this.name;
- hash.plugin = this.constructor;
- hash.instance = this;
- return hash;
- }
-
- /**
- * Triggers an event on the plugin object and overrides
- * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
- *
- * @param {string|Object} event
- * An event type or an object with a type property.
- *
- * @param {Object} [hash={}]
- * Additional data hash to merge with a
- * {@link PluginEventHash|PluginEventHash}.
- *
- * @return {boolean}
- * Whether or not default was prevented.
- */
- trigger(event, hash = {}) {
- return trigger(this.eventBusEl_, event, this.getEventHash(hash));
- }
-
- /**
- * Handles "statechanged" events on the plugin. No-op by default, override by
- * subclassing.
- *
- * @abstract
- * @param {Event} e
- * An event object provided by a "statechanged" event.
- *
- * @param {Object} e.changes
- * An object describing changes that occurred with the "statechanged"
- * event.
- */
- handleStateChanged(e) {}
-
- /**
- * Disposes a plugin.
- *
- * Subclasses can override this if they want, but for the sake of safety,
- * it's probably best to subscribe the "dispose" event.
- *
- * @fires Plugin#dispose
- */
- dispose() {
- const {
- name,
- player
- } = this;
-
- /**
- * Signals that a advanced plugin is about to be disposed.
- *
- * @event Plugin#dispose
- * @type {Event}
- */
- this.trigger('dispose');
- this.off();
- player.off('dispose', this.dispose);
-
- // Eliminate any possible sources of leaking memory by clearing up
- // references between the player and the plugin instance and nulling out
- // the plugin's state and replacing methods with a function that throws.
- player[PLUGIN_CACHE_KEY][name] = false;
- this.player = this.state = null;
-
- // Finally, replace the plugin name on the player with a new factory
- // function, so that the plugin is ready to be set up again.
- player[name] = createPluginFactory(name, pluginStorage[name]);
- }
-
- /**
- * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
- *
- * @param {string|Function} plugin
- * If a string, matches the name of a plugin. If a function, will be
- * tested directly.
- *
- * @return {boolean}
- * Whether or not a plugin is a basic plugin.
- */
- static isBasic(plugin) {
- const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
- return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
- }
-
- /**
- * Register a Video.js plugin.
- *
- * @param {string} name
- * The name of the plugin to be registered. Must be a string and
- * must not match an existing plugin or a method on the `Player`
- * prototype.
- *
- * @param {typeof Plugin|Function} plugin
- * A sub-class of `Plugin` or a function for basic plugins.
- *
- * @return {typeof Plugin|Function}
- * For advanced plugins, a factory function for that plugin. For
- * basic plugins, a wrapper function that initializes the plugin.
- */
- static registerPlugin(name, plugin) {
- if (typeof name !== 'string') {
- throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
- }
- if (pluginExists(name)) {
- log$1.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
- } else if (Player.prototype.hasOwnProperty(name)) {
- throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
- }
- if (typeof plugin !== 'function') {
- throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
- }
- pluginStorage[name] = plugin;
-
- // Add a player prototype method for all sub-classed plugins (but not for
- // the base Plugin class).
- if (name !== BASE_PLUGIN_NAME) {
- if (Plugin.isBasic(plugin)) {
- Player.prototype[name] = createBasicPlugin(name, plugin);
- } else {
- Player.prototype[name] = createPluginFactory(name, plugin);
- }
- }
- return plugin;
- }
-
- /**
- * De-register a Video.js plugin.
- *
- * @param {string} name
- * The name of the plugin to be de-registered. Must be a string that
- * matches an existing plugin.
- *
- * @throws {Error}
- * If an attempt is made to de-register the base plugin.
- */
- static deregisterPlugin(name) {
- if (name === BASE_PLUGIN_NAME) {
- throw new Error('Cannot de-register base plugin.');
- }
- if (pluginExists(name)) {
- delete pluginStorage[name];
- delete Player.prototype[name];
- }
- }
-
- /**
- * Gets an object containing multiple Video.js plugins.
- *
- * @param {Array} [names]
- * If provided, should be an array of plugin names. Defaults to _all_
- * plugin names.
- *
- * @return {Object|undefined}
- * An object containing plugin(s) associated with their name(s) or
- * `undefined` if no matching plugins exist).
- */
- static getPlugins(names = Object.keys(pluginStorage)) {
- let result;
- names.forEach(name => {
- const plugin = getPlugin(name);
- if (plugin) {
- result = result || {};
- result[name] = plugin;
- }
- });
- return result;
- }
-
- /**
- * Gets a plugin's version, if available
- *
- * @param {string} name
- * The name of a plugin.
- *
- * @return {string}
- * The plugin's version or an empty string.
- */
- static getPluginVersion(name) {
- const plugin = getPlugin(name);
- return plugin && plugin.VERSION || '';
- }
- }
-
- /**
- * Gets a plugin by name if it exists.
- *
- * @static
- * @method getPlugin
- * @memberOf Plugin
- * @param {string} name
- * The name of a plugin.
- *
- * @returns {typeof Plugin|Function|undefined}
- * The plugin (or `undefined`).
- */
- Plugin.getPlugin = getPlugin;
-
- /**
- * The name of the base plugin class as it is registered.
- *
- * @type {string}
- */
- Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
- Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
-
- /**
- * Documented in player.js
- *
- * @ignore
- */
- Player.prototype.usingPlugin = function (name) {
- return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
- };
-
- /**
- * Documented in player.js
- *
- * @ignore
- */
- Player.prototype.hasPlugin = function (name) {
- return !!pluginExists(name);
- };
-
- /**
- * Signals that a plugin is about to be set up on a player.
- *
- * @event Player#beforepluginsetup
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin is about to be set up on a player - by name. The name
- * is the name of the plugin.
- *
- * @event Player#beforepluginsetup:$name
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin has just been set up on a player.
- *
- * @event Player#pluginsetup
- * @type {PluginEventHash}
- */
-
- /**
- * Signals that a plugin has just been set up on a player - by name. The name
- * is the name of the plugin.
- *
- * @event Player#pluginsetup:$name
- * @type {PluginEventHash}
- */
-
- /**
- * @typedef {Object} PluginEventHash
- *
- * @property {string} instance
- * For basic plugins, the return value of the plugin function. For
- * advanced plugins, the plugin instance on which the event is fired.
- *
- * @property {string} name
- * The name of the plugin.
- *
- * @property {string} plugin
- * For basic plugins, the plugin function. For advanced plugins, the
- * plugin class/constructor.
- */
-
- /**
- * @file deprecate.js
- * @module deprecate
- */
-
- /**
- * Decorate a function with a deprecation message the first time it is called.
- *
- * @param {string} message
- * A deprecation message to log the first time the returned function
- * is called.
- *
- * @param {Function} fn
- * The function to be deprecated.
- *
- * @return {Function}
- * A wrapper function that will log a deprecation warning the first
- * time it is called. The return value will be the return value of
- * the wrapped function.
- */
- function deprecate(message, fn) {
- let warned = false;
- return function (...args) {
- if (!warned) {
- log$1.warn(message);
- }
- warned = true;
- return fn.apply(this, args);
- };
- }
-
- /**
- * Internal function used to mark a function as deprecated in the next major
- * version with consistent messaging.
- *
- * @param {number} major The major version where it will be removed
- * @param {string} oldName The old function name
- * @param {string} newName The new function name
- * @param {Function} fn The function to deprecate
- * @return {Function} The decorated function
- */
- function deprecateForMajor(major, oldName, newName, fn) {
- return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
- }
-
- var VjsErrors = {
- UnsupportedSidxContainer: 'unsupported-sidx-container-error',
- DashManifestSidxParsingError: 'dash-manifest-sidx-parsing-error',
- HlsPlaylistRequestError: 'hls-playlist-request-error',
- SegmentUnsupportedMediaFormat: 'segment-unsupported-media-format-error',
- UnsupportedMediaInitialization: 'unsupported-media-initialization-error',
- SegmentSwitchError: 'segment-switch-error',
- SegmentExceedsSourceBufferQuota: 'segment-exceeds-source-buffer-quota-error',
- SegmentAppendError: 'segment-append-error',
- VttLoadError: 'vtt-load-error',
- VttCueParsingError: 'vtt-cue-parsing-error',
- // Errors used in contrib-ads:
- AdsBeforePrerollError: 'ads-before-preroll-error',
- AdsPrerollError: 'ads-preroll-error',
- AdsMidrollError: 'ads-midroll-error',
- AdsPostrollError: 'ads-postroll-error',
- AdsMacroReplacementFailed: 'ads-macro-replacement-failed',
- AdsResumeContentFailed: 'ads-resume-content-failed',
- // Errors used in contrib-eme:
- EMEFailedToRequestMediaKeySystemAccess: 'eme-failed-request-media-key-system-access',
- EMEFailedToCreateMediaKeys: 'eme-failed-create-media-keys',
- EMEFailedToAttachMediaKeysToVideoElement: 'eme-failed-attach-media-keys-to-video',
- EMEFailedToCreateMediaKeySession: 'eme-failed-create-media-key-session',
- EMEFailedToSetServerCertificate: 'eme-failed-set-server-certificate',
- EMEFailedToGenerateLicenseRequest: 'eme-failed-generate-license-request',
- EMEFailedToUpdateSessionWithReceivedLicenseKeys: 'eme-failed-update-session',
- EMEFailedToCloseSession: 'eme-failed-close-session',
- EMEFailedToRemoveKeysFromSession: 'eme-failed-remove-keys',
- EMEFailedToLoadSessionBySessionId: 'eme-failed-load-session'
- };
-
- /**
- * @file video.js
- * @module videojs
- */
-
- /**
- * Normalize an `id` value by trimming off a leading `#`
- *
- * @private
- * @param {string} id
- * A string, maybe with a leading `#`.
- *
- * @return {string}
- * The string, without any leading `#`.
- */
- const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
-
- /**
- * A callback that is called when a component is ready. Does not have any
- * parameters and any callback value will be ignored. See: {@link Component~ReadyCallback}
- *
- * @callback ReadyCallback
- */
-
- /**
- * The `videojs()` function doubles as the main function for users to create a
- * {@link Player} instance as well as the main library namespace.
- *
- * It can also be used as a getter for a pre-existing {@link Player} instance.
- * However, we _strongly_ recommend using `videojs.getPlayer()` for this
- * purpose because it avoids any potential for unintended initialization.
- *
- * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
- * of our JSDoc template, we cannot properly document this as both a function
- * and a namespace, so its function signature is documented here.
- *
- * #### Arguments
- * ##### id
- * string|Element, **required**
- *
- * Video element or video element ID.
- *
- * ##### options
- * Object, optional
- *
- * Options object for providing settings.
- * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
- *
- * ##### ready
- * {@link Component~ReadyCallback}, optional
- *
- * A function to be called when the {@link Player} and {@link Tech} are ready.
- *
- * #### Return Value
- *
- * The `videojs()` function returns a {@link Player} instance.
- *
- * @namespace
- *
- * @borrows AudioTrack as AudioTrack
- * @borrows Component.getComponent as getComponent
- * @borrows module:events.on as on
- * @borrows module:events.one as one
- * @borrows module:events.off as off
- * @borrows module:events.trigger as trigger
- * @borrows EventTarget as EventTarget
- * @borrows module:middleware.use as use
- * @borrows Player.players as players
- * @borrows Plugin.registerPlugin as registerPlugin
- * @borrows Plugin.deregisterPlugin as deregisterPlugin
- * @borrows Plugin.getPlugins as getPlugins
- * @borrows Plugin.getPlugin as getPlugin
- * @borrows Plugin.getPluginVersion as getPluginVersion
- * @borrows Tech.getTech as getTech
- * @borrows Tech.registerTech as registerTech
- * @borrows TextTrack as TextTrack
- * @borrows VideoTrack as VideoTrack
- *
- * @param {string|Element} id
- * Video element or video element ID.
- *
- * @param {Object} [options]
- * Options object for providing settings.
- * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
- *
- * @param {ReadyCallback} [ready]
- * A function to be called when the {@link Player} and {@link Tech} are
- * ready.
- *
- * @return {Player}
- * The `videojs()` function returns a {@link Player|Player} instance.
- */
- function videojs(id, options, ready) {
- let player = videojs.getPlayer(id);
- if (player) {
- if (options) {
- log$1.warn(`Player "${id}" is already initialised. Options will not be applied.`);
- }
- if (ready) {
- player.ready(ready);
- }
- return player;
- }
- const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
- if (!isEl(el)) {
- throw new TypeError('The element or ID supplied is not valid. (videojs)');
- }
-
- // document.body.contains(el) will only check if el is contained within that one document.
- // This causes problems for elements in iframes.
- // Instead, use the element's ownerDocument instead of the global document.
- // This will make sure that the element is indeed in the dom of that document.
- // Additionally, check that the document in question has a default view.
- // If the document is no longer attached to the dom, the defaultView of the document will be null.
- // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body
- // always returns false. Instead, use the Shadow DOM root.
- const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window.ShadowRoot : false;
- const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body;
- if (!el.ownerDocument.defaultView || !rootNode.contains(el)) {
- log$1.warn('The element supplied is not included in the DOM');
- }
- options = options || {};
-
- // Store a copy of the el before modification, if it is to be restored in destroy()
- // If div ingest, store the parent div
- if (options.restoreEl === true) {
- options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
- }
- hooks('beforesetup').forEach(hookFunction => {
- const opts = hookFunction(el, merge$2(options));
- if (!isObject$1(opts) || Array.isArray(opts)) {
- log$1.error('please return an object in beforesetup hooks');
- return;
- }
- options = merge$2(options, opts);
- });
-
- // We get the current "Player" component here in case an integration has
- // replaced it with a custom player.
- const PlayerComponent = Component$1.getComponent('Player');
- player = new PlayerComponent(el, options, ready);
- hooks('setup').forEach(hookFunction => hookFunction(player));
- return player;
- }
- videojs.hooks_ = hooks_;
- videojs.hooks = hooks;
- videojs.hook = hook;
- videojs.hookOnce = hookOnce;
- videojs.removeHook = removeHook;
-
- // Add default styles
- if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
- let style = $('.vjs-styles-defaults');
- if (!style) {
- style = createStyleElement('vjs-styles-defaults');
- const head = $('head');
- if (head) {
- head.insertBefore(style, head.firstChild);
- }
- setTextContent(style, `
- .video-js {
- width: 300px;
- height: 150px;
- }
-
- .vjs-fluid:not(.vjs-audio-only-mode) {
- padding-top: 56.25%
- }
- `);
- }
- }
-
- // Run Auto-load players
- // You have to wait at least once in case this script is loaded after your
- // video in the DOM (weird behavior only with minified version)
- autoSetupTimeout(1, videojs);
-
- /**
- * Current Video.js version. Follows [semantic versioning](https://semver.org/).
- *
- * @type {string}
- */
- videojs.VERSION = version$5;
-
- /**
- * The global options object. These are the settings that take effect
- * if no overrides are specified when the player is created.
- *
- * @type {Object}
- */
- videojs.options = Player.prototype.options_;
-
- /**
- * Get an object with the currently created players, keyed by player ID
- *
- * @return {Object}
- * The created players
- */
- videojs.getPlayers = () => Player.players;
-
- /**
- * Get a single player based on an ID or DOM element.
- *
- * This is useful if you want to check if an element or ID has an associated
- * Video.js player, but not create one if it doesn't.
- *
- * @param {string|Element} id
- * An HTML element - ``, ``, or `` -
- * or a string matching the `id` of such an element.
- *
- * @return {Player|undefined}
- * A player instance or `undefined` if there is no player instance
- * matching the argument.
- */
- videojs.getPlayer = id => {
- const players = Player.players;
- let tag;
- if (typeof id === 'string') {
- const nId = normalizeId(id);
- const player = players[nId];
- if (player) {
- return player;
- }
- tag = $('#' + nId);
- } else {
- tag = id;
- }
- if (isEl(tag)) {
- const {
- player,
- playerId
- } = tag;
-
- // Element may have a `player` property referring to an already created
- // player instance. If so, return that.
- if (player || players[playerId]) {
- return player || players[playerId];
- }
- }
- };
-
- /**
- * Returns an array of all current players.
- *
- * @return {Array}
- * An array of all players. The array will be in the order that
- * `Object.keys` provides, which could potentially vary between
- * JavaScript engines.
- *
- */
- videojs.getAllPlayers = () =>
- // Disposed players leave a key with a `null` value, so we need to make sure
- // we filter those out.
- Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
- videojs.players = Player.players;
- videojs.getComponent = Component$1.getComponent;
-
- /**
- * Register a component so it can referred to by name. Used when adding to other
- * components, either through addChild `component.addChild('myComponent')` or through
- * default children options `{ children: ['myComponent'] }`.
- *
- * > NOTE: You could also just initialize the component before adding.
- * `component.addChild(new MyComponent());`
- *
- * @param {string} name
- * The class name of the component
- *
- * @param {typeof Component} comp
- * The component class
- *
- * @return {typeof Component}
- * The newly registered component
- */
- videojs.registerComponent = (name, comp) => {
- if (Tech.isTech(comp)) {
- log$1.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
- }
- return Component$1.registerComponent.call(Component$1, name, comp);
- };
- videojs.getTech = Tech.getTech;
- videojs.registerTech = Tech.registerTech;
- videojs.use = use;
-
- /**
- * An object that can be returned by a middleware to signify
- * that the middleware is being terminated.
- *
- * @type {object}
- * @property {object} middleware.TERMINATOR
- */
- Object.defineProperty(videojs, 'middleware', {
- value: {},
- writeable: false,
- enumerable: true
- });
- Object.defineProperty(videojs.middleware, 'TERMINATOR', {
- value: TERMINATOR,
- writeable: false,
- enumerable: true
- });
-
- /**
- * A reference to the {@link module:browser|browser utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:browser|browser}
- */
- videojs.browser = browser;
-
- /**
- * A reference to the {@link module:obj|obj utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:obj|obj}
- */
- videojs.obj = Obj;
-
- /**
- * Deprecated reference to the {@link module:obj.merge|merge function}
- *
- * @type {Function}
- * @see {@link module:obj.merge|merge}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
- */
- videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge$2);
-
- /**
- * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
- *
- * @type {Function}
- * @see {@link module:obj.defineLazyProperty|defineLazyProperty}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
- */
- videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
-
- /**
- * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
- *
- * @type {Function}
- * @see {@link module:fn.bind_|fn.bind_}
- * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
- */
- videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
- videojs.registerPlugin = Plugin.registerPlugin;
- videojs.deregisterPlugin = Plugin.deregisterPlugin;
-
- /**
- * Deprecated method to register a plugin with Video.js
- *
- * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
- *
- * @param {string} name
- * The plugin name
- *
- * @param {typeof Plugin|Function} plugin
- * The plugin sub-class or function
- *
- * @return {typeof Plugin|Function}
- */
- videojs.plugin = (name, plugin) => {
- log$1.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
- return Plugin.registerPlugin(name, plugin);
- };
- videojs.getPlugins = Plugin.getPlugins;
- videojs.getPlugin = Plugin.getPlugin;
- videojs.getPluginVersion = Plugin.getPluginVersion;
-
- /**
- * Adding languages so that they're available to all players.
- * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
- *
- * @param {string} code
- * The language code or dictionary property
- *
- * @param {Object} data
- * The data values to be translated
- *
- * @return {Object}
- * The resulting language dictionary object
- */
- videojs.addLanguage = function (code, data) {
- code = ('' + code).toLowerCase();
- videojs.options.languages = merge$2(videojs.options.languages, {
- [code]: data
- });
- return videojs.options.languages[code];
- };
-
- /**
- * A reference to the {@link module:log|log utility module} as an object.
- *
- * @type {Function}
- * @see {@link module:log|log}
- */
- videojs.log = log$1;
- videojs.createLogger = createLogger;
-
- /**
- * A reference to the {@link module:time|time utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:time|time}
- */
- videojs.time = Time;
-
- /**
- * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
- *
- * @type {Function}
- * @see {@link module:time.createTimeRanges|createTimeRanges}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
- */
- videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges$1);
-
- /**
- * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
- *
- * @type {Function}
- * @see {@link module:time.createTimeRanges|createTimeRanges}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
- */
- videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges$1);
-
- /**
- * Deprecated reference to the {@link module:time.formatTime|formatTime function}
- *
- * @type {Function}
- * @see {@link module:time.formatTime|formatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
- */
- videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
-
- /**
- * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
- *
- * @type {Function}
- * @see {@link module:time.setFormatTime|setFormatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
- */
- videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
-
- /**
- * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
- *
- * @type {Function}
- * @see {@link module:time.resetFormatTime|resetFormatTime}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
- */
- videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
-
- /**
- * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
- *
- * @type {Function}
- * @see {@link module:url.parseUrl|parseUrl}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
- */
- videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
-
- /**
- * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
- *
- * @type {Function}
- * @see {@link module:url.isCrossOrigin|isCrossOrigin}
- * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
- */
- videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
- videojs.EventTarget = EventTarget$2;
- videojs.any = any;
- videojs.on = on;
- videojs.one = one;
- videojs.off = off;
- videojs.trigger = trigger;
-
- /**
- * A cross-browser XMLHttpRequest wrapper.
- *
- * @function
- * @param {Object} options
- * Settings for the request.
- *
- * @return {XMLHttpRequest|XDomainRequest}
- * The request object.
- *
- * @see https://github.com/Raynos/xhr
- */
- videojs.xhr = lib;
- videojs.TextTrack = TextTrack;
- videojs.AudioTrack = AudioTrack;
- videojs.VideoTrack = VideoTrack;
- ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
- videojs[k] = function () {
- log$1.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
- return Dom[k].apply(null, arguments);
- };
- });
- videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
-
- /**
- * A reference to the {@link module:dom|DOM utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:dom|dom}
- */
- videojs.dom = Dom;
-
- /**
- * A reference to the {@link module:fn|fn utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:fn|fn}
- */
- videojs.fn = Fn;
-
- /**
- * A reference to the {@link module:num|num utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:num|num}
- */
- videojs.num = Num;
-
- /**
- * A reference to the {@link module:str|str utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:str|str}
- */
- videojs.str = Str;
-
- /**
- * A reference to the {@link module:url|URL utility module} as an object.
- *
- * @type {Object}
- * @see {@link module:url|url}
- */
- videojs.url = Url;
-
- // The list of possible error types to occur in video.js
- videojs.Error = VjsErrors;
-
- createCommonjsModule(function (module, exports) {
- /*! @name videojs-contrib-quality-levels @version 4.1.0 @license Apache-2.0 */
- (function (global, factory) {
- module.exports = factory(videojs) ;
- })(commonjsGlobal, function (videojs) {
-
- function _interopDefaultLegacy(e) {
- return e && typeof e === 'object' && 'default' in e ? e : {
- 'default': e
- };
- }
- var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs);
-
- /**
- * A single QualityLevel.
- *
- * interface QualityLevel {
- * readonly attribute DOMString id;
- * attribute DOMString label;
- * readonly attribute long width;
- * readonly attribute long height;
- * readonly attribute long bitrate;
- * attribute boolean enabled;
- * };
- *
- * @class QualityLevel
- */
- class QualityLevel {
- /**
- * Creates a QualityLevel
- *
- * @param {Representation|Object} representation The representation of the quality level
- * @param {string} representation.id Unique id of the QualityLevel
- * @param {number=} representation.width Resolution width of the QualityLevel
- * @param {number=} representation.height Resolution height of the QualityLevel
- * @param {number} representation.bandwidth Bitrate of the QualityLevel
- * @param {number=} representation.frameRate Frame-rate of the QualityLevel
- * @param {Function} representation.enabled Callback to enable/disable QualityLevel
- */
- constructor(representation) {
- let level = this; // eslint-disable-line
-
- level.id = representation.id;
- level.label = level.id;
- level.width = representation.width;
- level.height = representation.height;
- level.bitrate = representation.bandwidth;
- level.frameRate = representation.frameRate;
- level.enabled_ = representation.enabled;
- Object.defineProperty(level, 'enabled', {
- /**
- * Get whether the QualityLevel is enabled.
- *
- * @return {boolean} True if the QualityLevel is enabled.
- */
- get() {
- return level.enabled_();
- },
- /**
- * Enable or disable the QualityLevel.
- *
- * @param {boolean} enable true to enable QualityLevel, false to disable.
- */
- set(enable) {
- level.enabled_(enable);
- }
- });
- return level;
- }
- }
-
- /**
- * A list of QualityLevels.
- *
- * interface QualityLevelList : EventTarget {
- * getter QualityLevel (unsigned long index);
- * readonly attribute unsigned long length;
- * readonly attribute long selectedIndex;
- *
- * void addQualityLevel(QualityLevel qualityLevel)
- * void removeQualityLevel(QualityLevel remove)
- * QualityLevel? getQualityLevelById(DOMString id);
- *
- * attribute EventHandler onchange;
- * attribute EventHandler onaddqualitylevel;
- * attribute EventHandler onremovequalitylevel;
- * };
- *
- * @extends videojs.EventTarget
- * @class QualityLevelList
- */
- class QualityLevelList extends videojs__default["default"].EventTarget {
- /**
- * Creates a QualityLevelList.
- */
- constructor() {
- super();
- let list = this; // eslint-disable-line
-
- list.levels_ = [];
- list.selectedIndex_ = -1;
-
- /**
- * Get the index of the currently selected QualityLevel.
- *
- * @returns {number} The index of the selected QualityLevel. -1 if none selected.
- * @readonly
- */
- Object.defineProperty(list, 'selectedIndex', {
- get() {
- return list.selectedIndex_;
- }
- });
-
- /**
- * Get the length of the list of QualityLevels.
- *
- * @returns {number} The length of the list.
- * @readonly
- */
- Object.defineProperty(list, 'length', {
- get() {
- return list.levels_.length;
- }
- });
- list[Symbol.iterator] = () => list.levels_.values();
- return list;
- }
-
- /**
- * Adds a quality level to the list.
- *
- * @param {Representation|Object} representation The representation of the quality level
- * @param {string} representation.id Unique id of the QualityLevel
- * @param {number=} representation.width Resolution width of the QualityLevel
- * @param {number=} representation.height Resolution height of the QualityLevel
- * @param {number} representation.bandwidth Bitrate of the QualityLevel
- * @param {number=} representation.frameRate Frame-rate of the QualityLevel
- * @param {Function} representation.enabled Callback to enable/disable QualityLevel
- * @return {QualityLevel} the QualityLevel added to the list
- * @method addQualityLevel
- */
- addQualityLevel(representation) {
- let qualityLevel = this.getQualityLevelById(representation.id);
-
- // Do not add duplicate quality levels
- if (qualityLevel) {
- return qualityLevel;
- }
- const index = this.levels_.length;
- qualityLevel = new QualityLevel(representation);
- if (!('' + index in this)) {
- Object.defineProperty(this, index, {
- get() {
- return this.levels_[index];
- }
- });
- }
- this.levels_.push(qualityLevel);
- this.trigger({
- qualityLevel,
- type: 'addqualitylevel'
- });
- return qualityLevel;
- }
-
- /**
- * Removes a quality level from the list.
- *
- * @param {QualityLevel} qualityLevel The QualityLevel to remove from the list.
- * @return {QualityLevel|null} the QualityLevel removed or null if nothing removed
- * @method removeQualityLevel
- */
- removeQualityLevel(qualityLevel) {
- let removed = null;
- for (let i = 0, l = this.length; i < l; i++) {
- if (this[i] === qualityLevel) {
- removed = this.levels_.splice(i, 1)[0];
- if (this.selectedIndex_ === i) {
- this.selectedIndex_ = -1;
- } else if (this.selectedIndex_ > i) {
- this.selectedIndex_--;
- }
- break;
- }
- }
- if (removed) {
- this.trigger({
- qualityLevel,
- type: 'removequalitylevel'
- });
- }
- return removed;
- }
-
- /**
- * Searches for a QualityLevel with the given id.
- *
- * @param {string} id The id of the QualityLevel to find.
- * @return {QualityLevel|null} The QualityLevel with id, or null if not found.
- * @method getQualityLevelById
- */
- getQualityLevelById(id) {
- for (let i = 0, l = this.length; i < l; i++) {
- const level = this[i];
- if (level.id === id) {
- return level;
- }
- }
- return null;
- }
-
- /**
- * Resets the list of QualityLevels to empty
- *
- * @method dispose
- */
- dispose() {
- this.selectedIndex_ = -1;
- this.levels_.length = 0;
- }
- }
-
- /**
- * change - The selected QualityLevel has changed.
- * addqualitylevel - A QualityLevel has been added to the QualityLevelList.
- * removequalitylevel - A QualityLevel has been removed from the QualityLevelList.
- */
- QualityLevelList.prototype.allowedEvents_ = {
- change: 'change',
- addqualitylevel: 'addqualitylevel',
- removequalitylevel: 'removequalitylevel'
- };
-
- // emulate attribute EventHandler support to allow for feature detection
- for (const event in QualityLevelList.prototype.allowedEvents_) {
- QualityLevelList.prototype['on' + event] = null;
- }
- var version = "4.1.0";
-
- /**
- * Initialization function for the qualityLevels plugin. Sets up the QualityLevelList and
- * event handlers.
- *
- * @param {Player} player Player object.
- * @param {Object} options Plugin options object.
- * @return {QualityLevelList} a list of QualityLevels
- */
- const initPlugin = function (player, options) {
- const originalPluginFn = player.qualityLevels;
- const qualityLevelList = new QualityLevelList();
- const disposeHandler = function () {
- qualityLevelList.dispose();
- player.qualityLevels = originalPluginFn;
- player.off('dispose', disposeHandler);
- };
- player.on('dispose', disposeHandler);
- player.qualityLevels = () => qualityLevelList;
- player.qualityLevels.VERSION = version;
- return qualityLevelList;
- };
-
- /**
- * A video.js plugin.
- *
- * In the plugin function, the value of `this` is a video.js `Player`
- * instance. You cannot rely on the player being in a "ready" state here,
- * depending on how the plugin is invoked. This may or may not be important
- * to you; if not, remove the wait for "ready"!
- *
- * @param {Object} options Plugin options object
- * @return {QualityLevelList} a list of QualityLevels
- */
- const qualityLevels = function (options) {
- return initPlugin(this, videojs__default["default"].obj.merge({}, options));
- };
-
- // Register the plugin with video.js.
- videojs__default["default"].registerPlugin('qualityLevels', qualityLevels);
-
- // Include the version number.
- qualityLevels.VERSION = version;
- return qualityLevels;
- });
- });
-
- var urlToolkit = createCommonjsModule(function (module, exports) {
- // see https://tools.ietf.org/html/rfc1808
-
- (function (root) {
- var URL_REGEX = /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/;
- var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/;
- var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
- var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g;
- var URLToolkit = {
- // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
- // E.g
- // With opts.alwaysNormalize = false (default, spec compliant)
- // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
- // With opts.alwaysNormalize = true (not spec compliant)
- // http://a.com/b/cd + /e/f/../g => http://a.com/e/g
- buildAbsoluteURL: function (baseURL, relativeURL, opts) {
- opts = opts || {};
- // remove any remaining space and CRLF
- baseURL = baseURL.trim();
- relativeURL = relativeURL.trim();
- if (!relativeURL) {
- // 2a) If the embedded URL is entirely empty, it inherits the
- // entire base URL (i.e., is set equal to the base URL)
- // and we are done.
- if (!opts.alwaysNormalize) {
- return baseURL;
- }
- var basePartsForNormalise = URLToolkit.parseURL(baseURL);
- if (!basePartsForNormalise) {
- throw new Error('Error trying to parse base URL.');
- }
- basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
- return URLToolkit.buildURLFromParts(basePartsForNormalise);
- }
- var relativeParts = URLToolkit.parseURL(relativeURL);
- if (!relativeParts) {
- throw new Error('Error trying to parse relative URL.');
- }
- if (relativeParts.scheme) {
- // 2b) If the embedded URL starts with a scheme name, it is
- // interpreted as an absolute URL and we are done.
- if (!opts.alwaysNormalize) {
- return relativeURL;
- }
- relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
- return URLToolkit.buildURLFromParts(relativeParts);
- }
- var baseParts = URLToolkit.parseURL(baseURL);
- if (!baseParts) {
- throw new Error('Error trying to parse base URL.');
- }
- if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
- // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
- // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
- var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
- baseParts.netLoc = pathParts[1];
- baseParts.path = pathParts[2];
- }
- if (baseParts.netLoc && !baseParts.path) {
- baseParts.path = '/';
- }
- var builtParts = {
- // 2c) Otherwise, the embedded URL inherits the scheme of
- // the base URL.
- scheme: baseParts.scheme,
- netLoc: relativeParts.netLoc,
- path: null,
- params: relativeParts.params,
- query: relativeParts.query,
- fragment: relativeParts.fragment
- };
- if (!relativeParts.netLoc) {
- // 3) If the embedded URL's is non-empty, we skip to
- // Step 7. Otherwise, the embedded URL inherits the
- // (if any) of the base URL.
- builtParts.netLoc = baseParts.netLoc;
- // 4) If the embedded URL path is preceded by a slash "/", the
- // path is not relative and we skip to Step 7.
- if (relativeParts.path[0] !== '/') {
- if (!relativeParts.path) {
- // 5) If the embedded URL path is empty (and not preceded by a
- // slash), then the embedded URL inherits the base URL path
- builtParts.path = baseParts.path;
- // 5a) if the embedded URL's is non-empty, we skip to
- // step 7; otherwise, it inherits the of the base
- // URL (if any) and
- if (!relativeParts.params) {
- builtParts.params = baseParts.params;
- // 5b) if the embedded URL's is non-empty, we skip to
- // step 7; otherwise, it inherits the of the base
- // URL (if any) and we skip to step 7.
- if (!relativeParts.query) {
- builtParts.query = baseParts.query;
- }
- }
- } else {
- // 6) The last segment of the base URL's path (anything
- // following the rightmost slash "/", or the entire path if no
- // slash is present) is removed and the embedded URL's path is
- // appended in its place.
- var baseURLPath = baseParts.path;
- var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
- builtParts.path = URLToolkit.normalizePath(newPath);
- }
- }
- }
- if (builtParts.path === null) {
- builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
- }
- return URLToolkit.buildURLFromParts(builtParts);
- },
- parseURL: function (url) {
- var parts = URL_REGEX.exec(url);
- if (!parts) {
- return null;
- }
- return {
- scheme: parts[1] || '',
- netLoc: parts[2] || '',
- path: parts[3] || '',
- params: parts[4] || '',
- query: parts[5] || '',
- fragment: parts[6] || ''
- };
- },
- normalizePath: function (path) {
- // The following operations are
- // then applied, in order, to the new path:
- // 6a) All occurrences of "./", where "." is a complete path
- // segment, are removed.
- // 6b) If the path ends with "." as a complete path segment,
- // that "." is removed.
- path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, '');
- // 6c) All occurrences of "/../", where is a
- // complete path segment not equal to "..", are removed.
- // Removal of these path segments is performed iteratively,
- // removing the leftmost matching pattern on each iteration,
- // until no matching pattern remains.
- // 6d) If the path ends with "/..", where is a
- // complete path segment not equal to "..", that
- // "/.." is removed.
- while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {}
- return path.split('').reverse().join('');
- },
- buildURLFromParts: function (parts) {
- return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
- }
- };
- module.exports = URLToolkit;
- })();
- });
-
- var DEFAULT_LOCATION = 'http://example.com';
- var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) {
- // return early if we don't need to resolve
- if (/^[a-z]+:/i.test(relativeUrl)) {
- return relativeUrl;
- } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location
-
- if (/^data:/.test(baseUrl)) {
- baseUrl = window.location && window.location.href || '';
- } // IE11 supports URL but not the URL constructor
- // feature detect the behavior we want
-
- var nativeURL = typeof window.URL === 'function';
- var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node)
- // and if baseUrl isn't an absolute url
-
- var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location
-
- if (nativeURL) {
- baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION);
- } else if (!/\/\//i.test(baseUrl)) {
- baseUrl = urlToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl);
- }
- if (nativeURL) {
- var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol
- // and if we're location-less, remove the location
- // otherwise, return the url unmodified
-
- if (removeLocation) {
- return newUrl.href.slice(DEFAULT_LOCATION.length);
- } else if (protocolLess) {
- return newUrl.href.slice(newUrl.protocol.length);
- }
- return newUrl.href;
- }
- return urlToolkit.buildAbsoluteURL(baseUrl, relativeUrl);
- };
-
- /**
- * @file stream.js
- */
-
- /**
- * A lightweight readable stream implemention that handles event dispatching.
- *
- * @class Stream
- */
- var Stream = /*#__PURE__*/function () {
- function Stream() {
- this.listeners = {};
- }
- /**
- * Add a listener for a specified event type.
- *
- * @param {string} type the event name
- * @param {Function} listener the callback to be invoked when an event of
- * the specified type occurs
- */
-
- var _proto = Stream.prototype;
- _proto.on = function on(type, listener) {
- if (!this.listeners[type]) {
- this.listeners[type] = [];
- }
- this.listeners[type].push(listener);
- }
- /**
- * Remove a listener for a specified event type.
- *
- * @param {string} type the event name
- * @param {Function} listener a function previously registered for this
- * type of event through `on`
- * @return {boolean} if we could turn it off or not
- */;
-
- _proto.off = function off(type, listener) {
- if (!this.listeners[type]) {
- return false;
- }
- var index = this.listeners[type].indexOf(listener); // TODO: which is better?
- // In Video.js we slice listener functions
- // on trigger so that it does not mess up the order
- // while we loop through.
- //
- // Here we slice on off so that the loop in trigger
- // can continue using it's old reference to loop without
- // messing up the order.
-
- this.listeners[type] = this.listeners[type].slice(0);
- this.listeners[type].splice(index, 1);
- return index > -1;
- }
- /**
- * Trigger an event of the specified type on this stream. Any additional
- * arguments to this function are passed as parameters to event listeners.
- *
- * @param {string} type the event name
- */;
-
- _proto.trigger = function trigger(type) {
- var callbacks = this.listeners[type];
- if (!callbacks) {
- return;
- } // Slicing the arguments on every invocation of this method
- // can add a significant amount of overhead. Avoid the
- // intermediate object creation for the common case of a
- // single callback argument
-
- if (arguments.length === 2) {
- var length = callbacks.length;
- for (var i = 0; i < length; ++i) {
- callbacks[i].call(this, arguments[1]);
- }
- } else {
- var args = Array.prototype.slice.call(arguments, 1);
- var _length = callbacks.length;
- for (var _i = 0; _i < _length; ++_i) {
- callbacks[_i].apply(this, args);
- }
- }
- }
- /**
- * Destroys the stream and cleans up.
- */;
-
- _proto.dispose = function dispose() {
- this.listeners = {};
- }
- /**
- * Forwards all `data` events on this stream to the destination stream. The
- * destination stream should provide a method `push` to receive the data
- * events as they arrive.
- *
- * @param {Stream} destination the stream that will receive all `data` events
- * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
- */;
-
- _proto.pipe = function pipe(destination) {
- this.on('data', function (data) {
- destination.push(data);
- });
- };
- return Stream;
- }();
-
- var atob$1 = function atob(s) {
- return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
- };
- function decodeB64ToUint8Array$1(b64Text) {
- var decodedString = atob$1(b64Text);
- var array = new Uint8Array(decodedString.length);
- for (var i = 0; i < decodedString.length; i++) {
- array[i] = decodedString.charCodeAt(i);
- }
- return array;
- }
-
- /*! @name m3u8-parser @version 7.1.0 @license Apache-2.0 */
-
- /**
- * @file m3u8/line-stream.js
- */
- /**
- * A stream that buffers string input and generates a `data` event for each
- * line.
- *
- * @class LineStream
- * @extends Stream
- */
-
- class LineStream extends Stream {
- constructor() {
- super();
- this.buffer = '';
- }
- /**
- * Add new data to be parsed.
- *
- * @param {string} data the text to process
- */
-
- push(data) {
- let nextNewline;
- this.buffer += data;
- nextNewline = this.buffer.indexOf('\n');
- for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) {
- this.trigger('data', this.buffer.substring(0, nextNewline));
- this.buffer = this.buffer.substring(nextNewline + 1);
- }
- }
- }
- const TAB = String.fromCharCode(0x09);
- const parseByterange = function (byterangeString) {
- // optionally match and capture 0+ digits before `@`
- // optionally match and capture 0+ digits after `@`
- const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || '');
- const result = {};
- if (match[1]) {
- result.length = parseInt(match[1], 10);
- }
- if (match[2]) {
- result.offset = parseInt(match[2], 10);
- }
- return result;
- };
- /**
- * "forgiving" attribute list psuedo-grammar:
- * attributes -> keyvalue (',' keyvalue)*
- * keyvalue -> key '=' value
- * key -> [^=]*
- * value -> '"' [^"]* '"' | [^,]*
- */
-
- const attributeSeparator = function () {
- const key = '[^=]*';
- const value = '"[^"]*"|[^,]*';
- const keyvalue = '(?:' + key + ')=(?:' + value + ')';
- return new RegExp('(?:^|,)(' + keyvalue + ')');
- };
- /**
- * Parse attributes from a line given the separator
- *
- * @param {string} attributes the attribute line to parse
- */
-
- const parseAttributes$1 = function (attributes) {
- const result = {};
- if (!attributes) {
- return result;
- } // split the string using attributes as the separator
-
- const attrs = attributes.split(attributeSeparator());
- let i = attrs.length;
- let attr;
- while (i--) {
- // filter out unmatched portions of the string
- if (attrs[i] === '') {
- continue;
- } // split the key and value
-
- attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value
-
- attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
- attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
- attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1');
- result[attr[0]] = attr[1];
- }
- return result;
- };
- /**
- * A line-level M3U8 parser event stream. It expects to receive input one
- * line at a time and performs a context-free parse of its contents. A stream
- * interpretation of a manifest can be useful if the manifest is expected to
- * be too large to fit comfortably into memory or the entirety of the input
- * is not immediately available. Otherwise, it's probably much easier to work
- * with a regular `Parser` object.
- *
- * Produces `data` events with an object that captures the parser's
- * interpretation of the input. That object has a property `tag` that is one
- * of `uri`, `comment`, or `tag`. URIs only have a single additional
- * property, `line`, which captures the entirety of the input without
- * interpretation. Comments similarly have a single additional property
- * `text` which is the input without the leading `#`.
- *
- * Tags always have a property `tagType` which is the lower-cased version of
- * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
- * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
- * tags are given the tag type `unknown` and a single additional property
- * `data` with the remainder of the input.
- *
- * @class ParseStream
- * @extends Stream
- */
-
- class ParseStream extends Stream {
- constructor() {
- super();
- this.customParsers = [];
- this.tagMappers = [];
- }
- /**
- * Parses an additional line of input.
- *
- * @param {string} line a single line of an M3U8 file to parse
- */
-
- push(line) {
- let match;
- let event; // strip whitespace
-
- line = line.trim();
- if (line.length === 0) {
- // ignore empty lines
- return;
- } // URIs
-
- if (line[0] !== '#') {
- this.trigger('data', {
- type: 'uri',
- uri: line
- });
- return;
- } // map tags
-
- const newLines = this.tagMappers.reduce((acc, mapper) => {
- const mappedLine = mapper(line); // skip if unchanged
-
- if (mappedLine === line) {
- return acc;
- }
- return acc.concat([mappedLine]);
- }, [line]);
- newLines.forEach(newLine => {
- for (let i = 0; i < this.customParsers.length; i++) {
- if (this.customParsers[i].call(this, newLine)) {
- return;
- }
- } // Comments
-
- if (newLine.indexOf('#EXT') !== 0) {
- this.trigger('data', {
- type: 'comment',
- text: newLine.slice(1)
- });
- return;
- } // strip off any carriage returns here so the regex matching
- // doesn't have to account for them.
-
- newLine = newLine.replace('\r', ''); // Tags
-
- match = /^#EXTM3U/.exec(newLine);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'm3u'
- });
- return;
- }
- match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'inf'
- };
- if (match[1]) {
- event.duration = parseFloat(match[1]);
- }
- if (match[2]) {
- event.title = match[2];
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'targetduration'
- };
- if (match[1]) {
- event.duration = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'version'
- };
- if (match[1]) {
- event.version = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'media-sequence'
- };
- if (match[1]) {
- event.number = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'discontinuity-sequence'
- };
- if (match[1]) {
- event.number = parseInt(match[1], 10);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'playlist-type'
- };
- if (match[1]) {
- event.playlistType = match[1];
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine);
- if (match) {
- event = _extends$1(parseByterange(match[1]), {
- type: 'tag',
- tagType: 'byterange'
- });
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'allow-cache'
- };
- if (match[1]) {
- event.allowed = !/NO/.test(match[1]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-MAP:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'map'
- };
- if (match[1]) {
- const attributes = parseAttributes$1(match[1]);
- if (attributes.URI) {
- event.uri = attributes.URI;
- }
- if (attributes.BYTERANGE) {
- event.byterange = parseByterange(attributes.BYTERANGE);
- }
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'stream-inf'
- };
- if (match[1]) {
- event.attributes = parseAttributes$1(match[1]);
- if (event.attributes.RESOLUTION) {
- const split = event.attributes.RESOLUTION.split('x');
- const resolution = {};
- if (split[0]) {
- resolution.width = parseInt(split[0], 10);
- }
- if (split[1]) {
- resolution.height = parseInt(split[1], 10);
- }
- event.attributes.RESOLUTION = resolution;
- }
- if (event.attributes.BANDWIDTH) {
- event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
- }
- if (event.attributes['FRAME-RATE']) {
- event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']);
- }
- if (event.attributes['PROGRAM-ID']) {
- event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
- }
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'media'
- };
- if (match[1]) {
- event.attributes = parseAttributes$1(match[1]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-ENDLIST/.exec(newLine);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'endlist'
- });
- return;
- }
- match = /^#EXT-X-DISCONTINUITY/.exec(newLine);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'discontinuity'
- });
- return;
- }
- match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'program-date-time'
- };
- if (match[1]) {
- event.dateTimeString = match[1];
- event.dateTimeObject = new Date(match[1]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-KEY:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'key'
- };
- if (match[1]) {
- event.attributes = parseAttributes$1(match[1]); // parse the IV string into a Uint32Array
-
- if (event.attributes.IV) {
- if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') {
- event.attributes.IV = event.attributes.IV.substring(2);
- }
- event.attributes.IV = event.attributes.IV.match(/.{8}/g);
- event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16);
- event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16);
- event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16);
- event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16);
- event.attributes.IV = new Uint32Array(event.attributes.IV);
- }
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-START:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'start'
- };
- if (match[1]) {
- event.attributes = parseAttributes$1(match[1]);
- event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']);
- event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'cue-out-cont'
- };
- if (match[1]) {
- event.data = match[1];
- } else {
- event.data = '';
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'cue-out'
- };
- if (match[1]) {
- event.data = match[1];
- } else {
- event.data = '';
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-CUE-IN:(.*)?$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'cue-in'
- };
- if (match[1]) {
- event.data = match[1];
- } else {
- event.data = '';
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-SKIP:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'skip'
- };
- event.attributes = parseAttributes$1(match[1]);
- if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) {
- event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10);
- }
- if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) {
- event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-PART:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'part'
- };
- event.attributes = parseAttributes$1(match[1]);
- ['DURATION'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = parseFloat(event.attributes[key]);
- }
- });
- ['INDEPENDENT', 'GAP'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = /YES/.test(event.attributes[key]);
- }
- });
- if (event.attributes.hasOwnProperty('BYTERANGE')) {
- event.attributes.byterange = parseByterange(event.attributes.BYTERANGE);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'server-control'
- };
- event.attributes = parseAttributes$1(match[1]);
- ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = parseFloat(event.attributes[key]);
- }
- });
- ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = /YES/.test(event.attributes[key]);
- }
- });
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'part-inf'
- };
- event.attributes = parseAttributes$1(match[1]);
- ['PART-TARGET'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = parseFloat(event.attributes[key]);
- }
- });
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'preload-hint'
- };
- event.attributes = parseAttributes$1(match[1]);
- ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = parseInt(event.attributes[key], 10);
- const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset';
- event.attributes.byterange = event.attributes.byterange || {};
- event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object.
-
- delete event.attributes[key];
- }
- });
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'rendition-report'
- };
- event.attributes = parseAttributes$1(match[1]);
- ['LAST-MSN', 'LAST-PART'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = parseInt(event.attributes[key], 10);
- }
- });
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine);
- if (match && match[1]) {
- event = {
- type: 'tag',
- tagType: 'daterange'
- };
- event.attributes = parseAttributes$1(match[1]);
- ['ID', 'CLASS'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = String(event.attributes[key]);
- }
- });
- ['START-DATE', 'END-DATE'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = new Date(event.attributes[key]);
- }
- });
- ['DURATION', 'PLANNED-DURATION'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = parseFloat(event.attributes[key]);
- }
- });
- ['END-ON-NEXT'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = /YES/i.test(event.attributes[key]);
- }
- });
- ['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) {
- if (event.attributes.hasOwnProperty(key)) {
- event.attributes[key] = event.attributes[key].toString(16);
- }
- });
- const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/;
- for (const key in event.attributes) {
- if (!clientAttributePattern.test(key)) {
- continue;
- }
- const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]);
- const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]);
- event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]);
- }
- this.trigger('data', event);
- return;
- }
- match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine);
- if (match) {
- this.trigger('data', {
- type: 'tag',
- tagType: 'independent-segments'
- });
- return;
- }
- match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine);
- if (match) {
- event = {
- type: 'tag',
- tagType: 'content-steering'
- };
- event.attributes = parseAttributes$1(match[1]);
- this.trigger('data', event);
- return;
- } // unknown tag type
-
- this.trigger('data', {
- type: 'tag',
- data: newLine.slice(4)
- });
- });
- }
- /**
- * Add a parser for custom headers
- *
- * @param {Object} options a map of options for the added parser
- * @param {RegExp} options.expression a regular expression to match the custom header
- * @param {string} options.customType the custom type to register to the output
- * @param {Function} [options.dataParser] function to parse the line into an object
- * @param {boolean} [options.segment] should tag data be attached to the segment object
- */
-
- addParser({
- expression,
- customType,
- dataParser,
- segment
- }) {
- if (typeof dataParser !== 'function') {
- dataParser = line => line;
- }
- this.customParsers.push(line => {
- const match = expression.exec(line);
- if (match) {
- this.trigger('data', {
- type: 'custom',
- data: dataParser(line),
- customType,
- segment
- });
- return true;
- }
- });
- }
- /**
- * Add a custom header mapper
- *
- * @param {Object} options
- * @param {RegExp} options.expression a regular expression to match the custom header
- * @param {Function} options.map function to translate tag into a different tag
- */
-
- addTagMapper({
- expression,
- map
- }) {
- const mapFn = line => {
- if (expression.test(line)) {
- return map(line);
- }
- return line;
- };
- this.tagMappers.push(mapFn);
- }
- }
- const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase());
- const camelCaseKeys = function (attributes) {
- const result = {};
- Object.keys(attributes).forEach(function (key) {
- result[camelCase(key)] = attributes[key];
- });
- return result;
- }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration
- // we need this helper because defaults are based upon targetDuration and
- // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before
- // target durations are set.
-
- const setHoldBack = function (manifest) {
- const {
- serverControl,
- targetDuration,
- partTargetDuration
- } = manifest;
- if (!serverControl) {
- return;
- }
- const tag = '#EXT-X-SERVER-CONTROL';
- const hb = 'holdBack';
- const phb = 'partHoldBack';
- const minTargetDuration = targetDuration && targetDuration * 3;
- const minPartDuration = partTargetDuration && partTargetDuration * 2;
- if (targetDuration && !serverControl.hasOwnProperty(hb)) {
- serverControl[hb] = minTargetDuration;
- this.trigger('info', {
- message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).`
- });
- }
- if (minTargetDuration && serverControl[hb] < minTargetDuration) {
- this.trigger('warn', {
- message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})`
- });
- serverControl[hb] = minTargetDuration;
- } // default no part hold back to part target duration * 3
-
- if (partTargetDuration && !serverControl.hasOwnProperty(phb)) {
- serverControl[phb] = partTargetDuration * 3;
- this.trigger('info', {
- message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).`
- });
- } // if part hold back is too small default it to part target duration * 2
-
- if (partTargetDuration && serverControl[phb] < minPartDuration) {
- this.trigger('warn', {
- message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).`
- });
- serverControl[phb] = minPartDuration;
- }
- };
- /**
- * A parser for M3U8 files. The current interpretation of the input is
- * exposed as a property `manifest` on parser objects. It's just two lines to
- * create and parse a manifest once you have the contents available as a string:
- *
- * ```js
- * var parser = new m3u8.Parser();
- * parser.push(xhr.responseText);
- * ```
- *
- * New input can later be applied to update the manifest object by calling
- * `push` again.
- *
- * The parser attempts to create a usable manifest object even if the
- * underlying input is somewhat nonsensical. It emits `info` and `warning`
- * events during the parse if it encounters input that seems invalid or
- * requires some property of the manifest object to be defaulted.
- *
- * @class Parser
- * @extends Stream
- */
-
- class Parser extends Stream {
- constructor() {
- super();
- this.lineStream = new LineStream();
- this.parseStream = new ParseStream();
- this.lineStream.pipe(this.parseStream);
- this.lastProgramDateTime = null;
- /* eslint-disable consistent-this */
-
- const self = this;
- /* eslint-enable consistent-this */
-
- const uris = [];
- let currentUri = {}; // if specified, the active EXT-X-MAP definition
-
- let currentMap; // if specified, the active decryption key
-
- let key;
- let hasParts = false;
- const noop = function () {};
- const defaultMediaGroups = {
- 'AUDIO': {},
- 'VIDEO': {},
- 'CLOSED-CAPTIONS': {},
- 'SUBTITLES': {}
- }; // This is the Widevine UUID from DASH IF IOP. The same exact string is
- // used in MPDs with Widevine encrypted streams.
-
- const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities
-
- let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data
-
- this.manifest = {
- allowCache: true,
- discontinuityStarts: [],
- dateRanges: [],
- segments: []
- }; // keep track of the last seen segment's byte range end, as segments are not required
- // to provide the offset, in which case it defaults to the next byte after the
- // previous segment
-
- let lastByterangeEnd = 0; // keep track of the last seen part's byte range end.
-
- let lastPartByterangeEnd = 0;
- const dateRangeTags = {};
- this.on('end', () => {
- // only add preloadSegment if we don't yet have a uri for it.
- // and we actually have parts/preloadHints
- if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) {
- return;
- }
- if (!currentUri.map && currentMap) {
- currentUri.map = currentMap;
- }
- if (!currentUri.key && key) {
- currentUri.key = key;
- }
- if (!currentUri.timeline && typeof currentTimeline === 'number') {
- currentUri.timeline = currentTimeline;
- }
- this.manifest.preloadSegment = currentUri;
- }); // update the manifest with the m3u8 entry from the parse stream
-
- this.parseStream.on('data', function (entry) {
- let mediaGroup;
- let rendition;
- ({
- tag() {
- // switch based on the tag type
- (({
- version() {
- if (entry.version) {
- this.manifest.version = entry.version;
- }
- },
- 'allow-cache'() {
- this.manifest.allowCache = entry.allowed;
- if (!('allowed' in entry)) {
- this.trigger('info', {
- message: 'defaulting allowCache to YES'
- });
- this.manifest.allowCache = true;
- }
- },
- byterange() {
- const byterange = {};
- if ('length' in entry) {
- currentUri.byterange = byterange;
- byterange.length = entry.length;
- if (!('offset' in entry)) {
- /*
- * From the latest spec (as of this writing):
- * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2
- *
- * Same text since EXT-X-BYTERANGE's introduction in draft 7:
- * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1)
- *
- * "If o [offset] is not present, the sub-range begins at the next byte
- * following the sub-range of the previous media segment."
- */
- entry.offset = lastByterangeEnd;
- }
- }
- if ('offset' in entry) {
- currentUri.byterange = byterange;
- byterange.offset = entry.offset;
- }
- lastByterangeEnd = byterange.offset + byterange.length;
- },
- endlist() {
- this.manifest.endList = true;
- },
- inf() {
- if (!('mediaSequence' in this.manifest)) {
- this.manifest.mediaSequence = 0;
- this.trigger('info', {
- message: 'defaulting media sequence to zero'
- });
- }
- if (!('discontinuitySequence' in this.manifest)) {
- this.manifest.discontinuitySequence = 0;
- this.trigger('info', {
- message: 'defaulting discontinuity sequence to zero'
- });
- }
- if (entry.title) {
- currentUri.title = entry.title;
- }
- if (entry.duration > 0) {
- currentUri.duration = entry.duration;
- }
- if (entry.duration === 0) {
- currentUri.duration = 0.01;
- this.trigger('info', {
- message: 'updating zero segment duration to a small value'
- });
- }
- this.manifest.segments = uris;
- },
- key() {
- if (!entry.attributes) {
- this.trigger('warn', {
- message: 'ignoring key declaration without attribute list'
- });
- return;
- } // clear the active encryption key
-
- if (entry.attributes.METHOD === 'NONE') {
- key = null;
- return;
- }
- if (!entry.attributes.URI) {
- this.trigger('warn', {
- message: 'ignoring key declaration without URI'
- });
- return;
- }
- if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') {
- this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
-
- this.manifest.contentProtection['com.apple.fps.1_0'] = {
- attributes: entry.attributes
- };
- return;
- }
- if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') {
- this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this.
-
- this.manifest.contentProtection['com.microsoft.playready'] = {
- uri: entry.attributes.URI
- };
- return;
- } // check if the content is encrypted for Widevine
- // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf
-
- if (entry.attributes.KEYFORMAT === widevineUuid) {
- const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC'];
- if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) {
- this.trigger('warn', {
- message: 'invalid key method provided for Widevine'
- });
- return;
- }
- if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') {
- this.trigger('warn', {
- message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead'
- });
- }
- if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') {
- this.trigger('warn', {
- message: 'invalid key URI provided for Widevine'
- });
- return;
- }
- if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) {
- this.trigger('warn', {
- message: 'invalid key ID provided for Widevine'
- });
- return;
- } // if Widevine key attributes are valid, store them as `contentProtection`
- // on the manifest to emulate Widevine tag structure in a DASH mpd
-
- this.manifest.contentProtection = this.manifest.contentProtection || {};
- this.manifest.contentProtection['com.widevine.alpha'] = {
- attributes: {
- schemeIdUri: entry.attributes.KEYFORMAT,
- // remove '0x' from the key id string
- keyId: entry.attributes.KEYID.substring(2)
- },
- // decode the base64-encoded PSSH box
- pssh: decodeB64ToUint8Array$1(entry.attributes.URI.split(',')[1])
- };
- return;
- }
- if (!entry.attributes.METHOD) {
- this.trigger('warn', {
- message: 'defaulting key method to AES-128'
- });
- } // setup an encryption key for upcoming segments
-
- key = {
- method: entry.attributes.METHOD || 'AES-128',
- uri: entry.attributes.URI
- };
- if (typeof entry.attributes.IV !== 'undefined') {
- key.iv = entry.attributes.IV;
- }
- },
- 'media-sequence'() {
- if (!isFinite(entry.number)) {
- this.trigger('warn', {
- message: 'ignoring invalid media sequence: ' + entry.number
- });
- return;
- }
- this.manifest.mediaSequence = entry.number;
- },
- 'discontinuity-sequence'() {
- if (!isFinite(entry.number)) {
- this.trigger('warn', {
- message: 'ignoring invalid discontinuity sequence: ' + entry.number
- });
- return;
- }
- this.manifest.discontinuitySequence = entry.number;
- currentTimeline = entry.number;
- },
- 'playlist-type'() {
- if (!/VOD|EVENT/.test(entry.playlistType)) {
- this.trigger('warn', {
- message: 'ignoring unknown playlist type: ' + entry.playlist
- });
- return;
- }
- this.manifest.playlistType = entry.playlistType;
- },
- map() {
- currentMap = {};
- if (entry.uri) {
- currentMap.uri = entry.uri;
- }
- if (entry.byterange) {
- currentMap.byterange = entry.byterange;
- }
- if (key) {
- currentMap.key = key;
- }
- },
- 'stream-inf'() {
- this.manifest.playlists = uris;
- this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
- if (!entry.attributes) {
- this.trigger('warn', {
- message: 'ignoring empty stream-inf attributes'
- });
- return;
- }
- if (!currentUri.attributes) {
- currentUri.attributes = {};
- }
- _extends$1(currentUri.attributes, entry.attributes);
- },
- media() {
- this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups;
- if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) {
- this.trigger('warn', {
- message: 'ignoring incomplete or missing media group'
- });
- return;
- } // find the media group, creating defaults as necessary
-
- const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
- mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {};
- mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata
-
- rendition = {
- default: /yes/i.test(entry.attributes.DEFAULT)
- };
- if (rendition.default) {
- rendition.autoselect = true;
- } else {
- rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT);
- }
- if (entry.attributes.LANGUAGE) {
- rendition.language = entry.attributes.LANGUAGE;
- }
- if (entry.attributes.URI) {
- rendition.uri = entry.attributes.URI;
- }
- if (entry.attributes['INSTREAM-ID']) {
- rendition.instreamId = entry.attributes['INSTREAM-ID'];
- }
- if (entry.attributes.CHARACTERISTICS) {
- rendition.characteristics = entry.attributes.CHARACTERISTICS;
- }
- if (entry.attributes.FORCED) {
- rendition.forced = /yes/i.test(entry.attributes.FORCED);
- } // insert the new rendition
-
- mediaGroup[entry.attributes.NAME] = rendition;
- },
- discontinuity() {
- currentTimeline += 1;
- currentUri.discontinuity = true;
- this.manifest.discontinuityStarts.push(uris.length);
- },
- 'program-date-time'() {
- if (typeof this.manifest.dateTimeString === 'undefined') {
- // PROGRAM-DATE-TIME is a media-segment tag, but for backwards
- // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag
- // to the manifest object
- // TODO: Consider removing this in future major version
- this.manifest.dateTimeString = entry.dateTimeString;
- this.manifest.dateTimeObject = entry.dateTimeObject;
- }
- currentUri.dateTimeString = entry.dateTimeString;
- currentUri.dateTimeObject = entry.dateTimeObject;
- const {
- lastProgramDateTime
- } = this;
- this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence.
- // Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference.
-
- if (lastProgramDateTime === null) {
- // Extrapolate Program Date Time backward
- // Since it is first program date time occurrence we're assuming that
- // all this.manifest.segments have no program date time info
- this.manifest.segments.reduceRight((programDateTime, segment) => {
- segment.programDateTime = programDateTime - segment.duration * 1000;
- return segment.programDateTime;
- }, this.lastProgramDateTime);
- }
- },
- targetduration() {
- if (!isFinite(entry.duration) || entry.duration < 0) {
- this.trigger('warn', {
- message: 'ignoring invalid target duration: ' + entry.duration
- });
- return;
- }
- this.manifest.targetDuration = entry.duration;
- setHoldBack.call(this, this.manifest);
- },
- start() {
- if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) {
- this.trigger('warn', {
- message: 'ignoring start declaration without appropriate attribute list'
- });
- return;
- }
- this.manifest.start = {
- timeOffset: entry.attributes['TIME-OFFSET'],
- precise: entry.attributes.PRECISE
- };
- },
- 'cue-out'() {
- currentUri.cueOut = entry.data;
- },
- 'cue-out-cont'() {
- currentUri.cueOutCont = entry.data;
- },
- 'cue-in'() {
- currentUri.cueIn = entry.data;
- },
- 'skip'() {
- this.manifest.skip = camelCaseKeys(entry.attributes);
- this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']);
- },
- 'part'() {
- hasParts = true; // parts are always specifed before a segment
-
- const segmentIndex = this.manifest.segments.length;
- const part = camelCaseKeys(entry.attributes);
- currentUri.parts = currentUri.parts || [];
- currentUri.parts.push(part);
- if (part.byterange) {
- if (!part.byterange.hasOwnProperty('offset')) {
- part.byterange.offset = lastPartByterangeEnd;
- }
- lastPartByterangeEnd = part.byterange.offset + part.byterange.length;
- }
- const partIndex = currentUri.parts.length - 1;
- this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']);
- if (this.manifest.renditionReports) {
- this.manifest.renditionReports.forEach((r, i) => {
- if (!r.hasOwnProperty('lastPart')) {
- this.trigger('warn', {
- message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART`
- });
- }
- });
- }
- },
- 'server-control'() {
- const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes);
- if (!attrs.hasOwnProperty('canBlockReload')) {
- attrs.canBlockReload = false;
- this.trigger('info', {
- message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false'
- });
- }
- setHoldBack.call(this, this.manifest);
- if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) {
- this.trigger('warn', {
- message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set'
- });
- }
- },
- 'preload-hint'() {
- // parts are always specifed before a segment
- const segmentIndex = this.manifest.segments.length;
- const hint = camelCaseKeys(entry.attributes);
- const isPart = hint.type && hint.type === 'PART';
- currentUri.preloadHints = currentUri.preloadHints || [];
- currentUri.preloadHints.push(hint);
- if (hint.byterange) {
- if (!hint.byterange.hasOwnProperty('offset')) {
- // use last part byterange end or zero if not a part.
- hint.byterange.offset = isPart ? lastPartByterangeEnd : 0;
- if (isPart) {
- lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length;
- }
- }
- }
- const index = currentUri.preloadHints.length - 1;
- this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']);
- if (!hint.type) {
- return;
- } // search through all preload hints except for the current one for
- // a duplicate type.
-
- for (let i = 0; i < currentUri.preloadHints.length - 1; i++) {
- const otherHint = currentUri.preloadHints[i];
- if (!otherHint.type) {
- continue;
- }
- if (otherHint.type === hint.type) {
- this.trigger('warn', {
- message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}`
- });
- }
- }
- },
- 'rendition-report'() {
- const report = camelCaseKeys(entry.attributes);
- this.manifest.renditionReports = this.manifest.renditionReports || [];
- this.manifest.renditionReports.push(report);
- const index = this.manifest.renditionReports.length - 1;
- const required = ['LAST-MSN', 'URI'];
- if (hasParts) {
- required.push('LAST-PART');
- }
- this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required);
- },
- 'part-inf'() {
- this.manifest.partInf = camelCaseKeys(entry.attributes);
- this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']);
- if (this.manifest.partInf.partTarget) {
- this.manifest.partTargetDuration = this.manifest.partInf.partTarget;
- }
- setHoldBack.call(this, this.manifest);
- },
- 'daterange'() {
- this.manifest.dateRanges.push(camelCaseKeys(entry.attributes));
- const index = this.manifest.dateRanges.length - 1;
- this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']);
- const dateRange = this.manifest.dateRanges[index];
- if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) {
- this.trigger('warn', {
- message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
- });
- }
- if (dateRange.duration && dateRange.duration < 0) {
- this.trigger('warn', {
- message: 'EXT-X-DATERANGE DURATION must not be negative'
- });
- }
- if (dateRange.plannedDuration && dateRange.plannedDuration < 0) {
- this.trigger('warn', {
- message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
- });
- }
- const endOnNextYes = !!dateRange.endOnNext;
- if (endOnNextYes && !dateRange.class) {
- this.trigger('warn', {
- message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
- });
- }
- if (endOnNextYes && (dateRange.duration || dateRange.endDate)) {
- this.trigger('warn', {
- message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
- });
- }
- if (dateRange.duration && dateRange.endDate) {
- const startDate = dateRange.startDate;
- const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000;
- this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds);
- }
- if (!dateRangeTags[dateRange.id]) {
- dateRangeTags[dateRange.id] = dateRange;
- } else {
- for (const attribute in dateRangeTags[dateRange.id]) {
- if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) {
- this.trigger('warn', {
- message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
- });
- break;
- }
- } // if tags with the same ID do not have conflicting attributes, merge them
-
- const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id);
- this.manifest.dateRanges[dateRangeWithSameId] = _extends$1(this.manifest.dateRanges[dateRangeWithSameId], dateRange);
- dateRangeTags[dateRange.id] = _extends$1(dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last
-
- this.manifest.dateRanges.pop();
- }
- },
- 'independent-segments'() {
- this.manifest.independentSegments = true;
- },
- 'content-steering'() {
- this.manifest.contentSteering = camelCaseKeys(entry.attributes);
- this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']);
- }
- })[entry.tagType] || noop).call(self);
- },
- uri() {
- currentUri.uri = entry.uri;
- uris.push(currentUri); // if no explicit duration was declared, use the target duration
-
- if (this.manifest.targetDuration && !('duration' in currentUri)) {
- this.trigger('warn', {
- message: 'defaulting segment duration to the target duration'
- });
- currentUri.duration = this.manifest.targetDuration;
- } // annotate with encryption information, if necessary
-
- if (key) {
- currentUri.key = key;
- }
- currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary
-
- if (currentMap) {
- currentUri.map = currentMap;
- } // reset the last byterange end as it needs to be 0 between parts
-
- lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward
-
- if (this.lastProgramDateTime !== null) {
- currentUri.programDateTime = this.lastProgramDateTime;
- this.lastProgramDateTime += currentUri.duration * 1000;
- } // prepare for the next URI
-
- currentUri = {};
- },
- comment() {// comments are not important for playback
- },
- custom() {
- // if this is segment-level data attach the output to the segment
- if (entry.segment) {
- currentUri.custom = currentUri.custom || {};
- currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object
- } else {
- this.manifest.custom = this.manifest.custom || {};
- this.manifest.custom[entry.customType] = entry.data;
- }
- }
- })[entry.type].call(self);
- });
- }
- warnOnMissingAttributes_(identifier, attributes, required) {
- const missing = [];
- required.forEach(function (key) {
- if (!attributes.hasOwnProperty(key)) {
- missing.push(key);
- }
- });
- if (missing.length) {
- this.trigger('warn', {
- message: `${identifier} lacks required attribute(s): ${missing.join(', ')}`
- });
- }
- }
- /**
- * Parse the input string and update the manifest object.
- *
- * @param {string} chunk a potentially incomplete portion of the manifest
- */
-
- push(chunk) {
- this.lineStream.push(chunk);
- }
- /**
- * Flush any remaining input. This can be handy if the last line of an M3U8
- * manifest did not contain a trailing newline but the file has been
- * completely received.
- */
-
- end() {
- // flush any buffered input
- this.lineStream.push('\n');
- if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) {
- this.trigger('warn', {
- message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
- });
- }
- this.lastProgramDateTime = null;
- this.trigger('end');
- }
- /**
- * Add an additional parser for non-standard tags
- *
- * @param {Object} options a map of options for the added parser
- * @param {RegExp} options.expression a regular expression to match the custom header
- * @param {string} options.customType the custom type to register to the output
- * @param {Function} [options.dataParser] function to parse the line into an object
- * @param {boolean} [options.segment] should tag data be attached to the segment object
- */
-
- addParser(options) {
- this.parseStream.addParser(options);
- }
- /**
- * Add a custom header mapper
- *
- * @param {Object} options
- * @param {RegExp} options.expression a regular expression to match the custom header
- * @param {Function} options.map function to translate tag into a different tag
- */
-
- addTagMapper(options) {
- this.parseStream.addTagMapper(options);
- }
- }
-
- var regexs = {
- // to determine mime types
- mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v|stpp.ttml.im1t)/,
- webm: /^(vp0?[89]|av0?1|opus|vorbis)/,
- ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/,
- // to determine if a codec is audio or video
- video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/,
- audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3|speex|aac)/,
- text: /^(stpp.ttml.im1t)/,
- // mux.js support regex
- muxerVideo: /^(avc0?1)/,
- muxerAudio: /^(mp4a)/,
- // match nothing as muxer does not support text right now.
- // there cannot never be a character before the start of a string
- // so this matches nothing.
- muxerText: /a^/
- };
- var mediaTypes = ['video', 'audio', 'text'];
- var upperMediaTypes = ['Video', 'Audio', 'Text'];
- /**
- * Replace the old apple-style `avc1..` codec string with the standard
- * `avc1.`
- *
- * @param {string} codec
- * Codec string to translate
- * @return {string}
- * The translated codec string
- */
-
- var translateLegacyCodec = function translateLegacyCodec(codec) {
- if (!codec) {
- return codec;
- }
- return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) {
- var profileHex = ('00' + Number(profile).toString(16)).slice(-2);
- var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2);
- return 'avc1.' + profileHex + '00' + avcLevelHex;
- });
- };
- /**
- * @typedef {Object} ParsedCodecInfo
- * @property {number} codecCount
- * Number of codecs parsed
- * @property {string} [videoCodec]
- * Parsed video codec (if found)
- * @property {string} [videoObjectTypeIndicator]
- * Video object type indicator (if found)
- * @property {string|null} audioProfile
- * Audio profile
- */
-
- /**
- * Parses a codec string to retrieve the number of codecs specified, the video codec and
- * object type indicator, and the audio profile.
- *
- * @param {string} [codecString]
- * The codec string to parse
- * @return {ParsedCodecInfo}
- * Parsed codec info
- */
-
- var parseCodecs = function parseCodecs(codecString) {
- if (codecString === void 0) {
- codecString = '';
- }
- var codecs = codecString.split(',');
- var result = [];
- codecs.forEach(function (codec) {
- codec = codec.trim();
- var codecType;
- mediaTypes.forEach(function (name) {
- var match = regexs[name].exec(codec.toLowerCase());
- if (!match || match.length <= 1) {
- return;
- }
- codecType = name; // maintain codec case
-
- var type = codec.substring(0, match[1].length);
- var details = codec.replace(type, '');
- result.push({
- type: type,
- details: details,
- mediaType: name
- });
- });
- if (!codecType) {
- result.push({
- type: codec,
- details: '',
- mediaType: 'unknown'
- });
- }
- });
- return result;
- };
- /**
- * Returns a ParsedCodecInfo object for the default alternate audio playlist if there is
- * a default alternate audio playlist for the provided audio group.
- *
- * @param {Object} master
- * The master playlist
- * @param {string} audioGroupId
- * ID of the audio group for which to find the default codec info
- * @return {ParsedCodecInfo}
- * Parsed codec info
- */
-
- var codecsFromDefault = function codecsFromDefault(master, audioGroupId) {
- if (!master.mediaGroups.AUDIO || !audioGroupId) {
- return null;
- }
- var audioGroup = master.mediaGroups.AUDIO[audioGroupId];
- if (!audioGroup) {
- return null;
- }
- for (var name in audioGroup) {
- var audioType = audioGroup[name];
- if (audioType.default && audioType.playlists) {
- // codec should be the same for all playlists within the audio type
- return parseCodecs(audioType.playlists[0].attributes.CODECS);
- }
- }
- return null;
- };
- var isAudioCodec = function isAudioCodec(codec) {
- if (codec === void 0) {
- codec = '';
- }
- return regexs.audio.test(codec.trim().toLowerCase());
- };
- var isTextCodec = function isTextCodec(codec) {
- if (codec === void 0) {
- codec = '';
- }
- return regexs.text.test(codec.trim().toLowerCase());
- };
- var getMimeForCodec = function getMimeForCodec(codecString) {
- if (!codecString || typeof codecString !== 'string') {
- return;
- }
- var codecs = codecString.toLowerCase().split(',').map(function (c) {
- return translateLegacyCodec(c.trim());
- }); // default to video type
-
- var type = 'video'; // only change to audio type if the only codec we have is
- // audio
-
- if (codecs.length === 1 && isAudioCodec(codecs[0])) {
- type = 'audio';
- } else if (codecs.length === 1 && isTextCodec(codecs[0])) {
- // text uses application/ for now
- type = 'application';
- } // default the container to mp4
-
- var container = 'mp4'; // every codec must be able to go into the container
- // for that container to be the correct one
-
- if (codecs.every(function (c) {
- return regexs.mp4.test(c);
- })) {
- container = 'mp4';
- } else if (codecs.every(function (c) {
- return regexs.webm.test(c);
- })) {
- container = 'webm';
- } else if (codecs.every(function (c) {
- return regexs.ogg.test(c);
- })) {
- container = 'ogg';
- }
- return type + "/" + container + ";codecs=\"" + codecString + "\"";
- };
- var browserSupportsCodec = function browserSupportsCodec(codecString) {
- if (codecString === void 0) {
- codecString = '';
- }
- return window.MediaSource && window.MediaSource.isTypeSupported && window.MediaSource.isTypeSupported(getMimeForCodec(codecString)) || false;
- };
- var muxerSupportsCodec = function muxerSupportsCodec(codecString) {
- if (codecString === void 0) {
- codecString = '';
- }
- return codecString.toLowerCase().split(',').every(function (codec) {
- codec = codec.trim(); // any match is supported.
-
- for (var i = 0; i < upperMediaTypes.length; i++) {
- var type = upperMediaTypes[i];
- if (regexs["muxer" + type].test(codec)) {
- return true;
- }
- }
- return false;
- });
- };
- var DEFAULT_AUDIO_CODEC = 'mp4a.40.2';
- var DEFAULT_VIDEO_CODEC = 'avc1.4d400d';
-
- var MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
- var DASH_REGEX = /^application\/dash\+xml/i;
- /**
- * Returns a string that describes the type of source based on a video source object's
- * media type.
- *
- * @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type}
- *
- * @param {string} type
- * Video source object media type
- * @return {('hls'|'dash'|'vhs-json'|null)}
- * VHS source type string
- */
-
- var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) {
- if (MPEGURL_REGEX.test(type)) {
- return 'hls';
- }
- if (DASH_REGEX.test(type)) {
- return 'dash';
- } // Denotes the special case of a manifest object passed to http-streaming instead of a
- // source URL.
- //
- // See https://en.wikipedia.org/wiki/Media_type for details on specifying media types.
- //
- // In this case, vnd stands for vendor, video.js for the organization, VHS for this
- // project, and the +json suffix identifies the structure of the media type.
-
- if (type === 'application/vnd.videojs.vhs+json') {
- return 'vhs-json';
- }
- return null;
- };
-
- // const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2));
- // we used to do this with log2 but BigInt does not support builtin math
- // Math.ceil(log2(x));
-
- var countBits = function countBits(x) {
- return x.toString(2).length;
- }; // count the number of whole bytes it would take to represent a number
-
- var countBytes = function countBytes(x) {
- return Math.ceil(countBits(x) / 8);
- };
- var isArrayBufferView = function isArrayBufferView(obj) {
- if (ArrayBuffer.isView === 'function') {
- return ArrayBuffer.isView(obj);
- }
- return obj && obj.buffer instanceof ArrayBuffer;
- };
- var isTypedArray = function isTypedArray(obj) {
- return isArrayBufferView(obj);
- };
- var toUint8 = function toUint8(bytes) {
- if (bytes instanceof Uint8Array) {
- return bytes;
- }
- if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) {
- // any non-number or NaN leads to empty uint8array
- // eslint-disable-next-line
- if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) {
- bytes = 0;
- } else {
- bytes = [bytes];
- }
- }
- return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0);
- };
- var BigInt = window.BigInt || Number;
- var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')];
- (function () {
- var a = new Uint16Array([0xFFCC]);
- var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
- if (b[0] === 0xFF) {
- return 'big';
- }
- if (b[0] === 0xCC) {
- return 'little';
- }
- return 'unknown';
- })();
- var bytesToNumber = function bytesToNumber(bytes, _temp) {
- var _ref = _temp === void 0 ? {} : _temp,
- _ref$signed = _ref.signed,
- signed = _ref$signed === void 0 ? false : _ref$signed,
- _ref$le = _ref.le,
- le = _ref$le === void 0 ? false : _ref$le;
- bytes = toUint8(bytes);
- var fn = le ? 'reduce' : 'reduceRight';
- var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn];
- var number = obj.call(bytes, function (total, byte, i) {
- var exponent = le ? i : Math.abs(i + 1 - bytes.length);
- return total + BigInt(byte) * BYTE_TABLE[exponent];
- }, BigInt(0));
- if (signed) {
- var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1);
- number = BigInt(number);
- if (number > max) {
- number -= max;
- number -= max;
- number -= BigInt(2);
- }
- }
- return Number(number);
- };
- var numberToBytes = function numberToBytes(number, _temp2) {
- var _ref2 = _temp2 === void 0 ? {} : _temp2,
- _ref2$le = _ref2.le,
- le = _ref2$le === void 0 ? false : _ref2$le;
-
- // eslint-disable-next-line
- if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) {
- number = 0;
- }
- number = BigInt(number);
- var byteCount = countBytes(number);
- var bytes = new Uint8Array(new ArrayBuffer(byteCount));
- for (var i = 0; i < byteCount; i++) {
- var byteIndex = le ? i : Math.abs(i + 1 - bytes.length);
- bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF));
- if (number < 0) {
- bytes[byteIndex] = Math.abs(~bytes[byteIndex]);
- bytes[byteIndex] -= i === 0 ? 1 : 2;
- }
- }
- return bytes;
- };
- var stringToBytes = function stringToBytes(string, stringIsBytes) {
- if (typeof string !== 'string' && string && typeof string.toString === 'function') {
- string = string.toString();
- }
- if (typeof string !== 'string') {
- return new Uint8Array();
- } // If the string already is bytes, we don't have to do this
- // otherwise we do this so that we split multi length characters
- // into individual bytes
-
- if (!stringIsBytes) {
- string = unescape(encodeURIComponent(string));
- }
- var view = new Uint8Array(string.length);
- for (var i = 0; i < string.length; i++) {
- view[i] = string.charCodeAt(i);
- }
- return view;
- };
- var concatTypedArrays = function concatTypedArrays() {
- for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) {
- buffers[_key] = arguments[_key];
- }
- buffers = buffers.filter(function (b) {
- return b && (b.byteLength || b.length) && typeof b !== 'string';
- });
- if (buffers.length <= 1) {
- // for 0 length we will return empty uint8
- // for 1 length we return the first uint8
- return toUint8(buffers[0]);
- }
- var totalLen = buffers.reduce(function (total, buf, i) {
- return total + (buf.byteLength || buf.length);
- }, 0);
- var tempBuffer = new Uint8Array(totalLen);
- var offset = 0;
- buffers.forEach(function (buf) {
- buf = toUint8(buf);
- tempBuffer.set(buf, offset);
- offset += buf.byteLength;
- });
- return tempBuffer;
- };
- /**
- * Check if the bytes "b" are contained within bytes "a".
- *
- * @param {Uint8Array|Array} a
- * Bytes to check in
- *
- * @param {Uint8Array|Array} b
- * Bytes to check for
- *
- * @param {Object} options
- * options
- *
- * @param {Array|Uint8Array} [offset=0]
- * offset to use when looking at bytes in a
- *
- * @param {Array|Uint8Array} [mask=[]]
- * mask to use on bytes before comparison.
- *
- * @return {boolean}
- * If all bytes in b are inside of a, taking into account
- * bit masks.
- */
-
- var bytesMatch = function bytesMatch(a, b, _temp3) {
- var _ref3 = _temp3 === void 0 ? {} : _temp3,
- _ref3$offset = _ref3.offset,
- offset = _ref3$offset === void 0 ? 0 : _ref3$offset,
- _ref3$mask = _ref3.mask,
- mask = _ref3$mask === void 0 ? [] : _ref3$mask;
- a = toUint8(a);
- b = toUint8(b); // ie 11 does not support uint8 every
-
- var fn = b.every ? b.every : Array.prototype.every;
- return b.length && a.length - offset >= b.length &&
- // ie 11 doesn't support every on uin8
- fn.call(b, function (bByte, i) {
- var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i];
- return bByte === aByte;
- });
- };
-
- /**
- * Loops through all supported media groups in master and calls the provided
- * callback for each group
- *
- * @param {Object} master
- * The parsed master manifest object
- * @param {string[]} groups
- * The media groups to call the callback for
- * @param {Function} callback
- * Callback to call for each media group
- */
- var forEachMediaGroup$1 = function forEachMediaGroup(master, groups, callback) {
- groups.forEach(function (mediaType) {
- for (var groupKey in master.mediaGroups[mediaType]) {
- for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
- var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
- callback(mediaProperties, mediaType, groupKey, labelKey);
- }
- }
- });
- };
-
- var atob = function atob(s) {
- return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary');
- };
- function decodeB64ToUint8Array(b64Text) {
- var decodedString = atob(b64Text);
- var array = new Uint8Array(decodedString.length);
- for (var i = 0; i < decodedString.length; i++) {
- array[i] = decodedString.charCodeAt(i);
- }
- return array;
- }
-
- /**
- * Ponyfill for `Array.prototype.find` which is only available in ES6 runtimes.
- *
- * Works with anything that has a `length` property and index access properties, including NodeList.
- *
- * @template {unknown} T
- * @param {Array
{t("WATCH.COMMENTS")}
- {EnableComments ? - comments.comments.map((comment) => ( -{comment.author} - 2 Months Ago
-{comment.contentHtml}
- {comment.likeCount}
-Comments are disabled on this video.
- - } -