From 5d40a43e557a98a2839062014adbef1d991fdb9f Mon Sep 17 00:00:00 2001 From: dydwang <1836963576@qq.com> Date: Fri, 26 Nov 2021 09:03:45 +0800 Subject: [PATCH] =?UTF-8?q?2021.11.17=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 5 +- package.json | 102 +- public/index.html | 5 + public/red-test1.jpg | Bin 50828 -> 0 bytes public/red-test2.jpg | Bin 60936 -> 0 bytes public/static/video-js-cdn.min.css | 1 + public/static/video.js | 25674 +++++++++++++++++ public/static/videojs-contrib-hls.min.js | 8 + public/test1.jpg | Bin 211425 -> 0 bytes public/test2.jpg | Bin 239620 -> 0 bytes src/api/http/http-node-api.js | 17 +- src/layouts/AsideMenu.vue | 2 +- src/layouts/PageContent.vue | 4 +- src/main.js | 9 - src/plugins/axios/axios.js | 2 +- src/plugins/axios/index.js | 4 +- src/router/index.js | 18 +- src/store/modules/styles.js | 2 - src/views/alarmVideos/alarmLog.vue | 39 +- src/views/checkManage/Subsection.vue | 255 + src/views/checkManage/checkOperation.vue | 147 +- src/views/checkManage/index.vue | 417 +- src/views/historyMonitoring/index.vue | 20 +- src/views/realTimeMonitoring/VideoPlayer.vue | 94 + src/views/realTimeMonitoring/index.vue | 232 +- src/views/roadwayManage/model.vue | 8 +- vue.config.js | 3 +- 27 files changed, 26577 insertions(+), 491 deletions(-) delete mode 100644 public/red-test1.jpg delete mode 100644 public/red-test2.jpg create mode 100644 public/static/video-js-cdn.min.css create mode 100644 public/static/video.js create mode 100644 public/static/videojs-contrib-hls.min.js delete mode 100644 public/test1.jpg delete mode 100644 public/test2.jpg create mode 100644 src/views/checkManage/Subsection.vue create mode 100644 src/views/realTimeMonitoring/VideoPlayer.vue diff --git a/.eslintrc.js b/.eslintrc.js index 18bffc5..11a103d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,6 +15,9 @@ module.exports = { "no-tabs": 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', - "no-unused-vars":'off' //冗余变量报错 + "no-unused-vars":'off', //冗余变量报错 + "vue/no-use-v-if-with-v-for": "off", + "vue/no-unused-vars": "off", + "vue/no-unused-components": "off" } } diff --git a/package.json b/package.json index 3e19cbe..51043e1 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,54 @@ { - "name": "duoji-frontend", - "version": "0.1.0", - "private": true, - "scripts": { - "serve": "node antdTheme.js && vue-cli-service serve", - "build": "node antdTheme.js && vue-cli-service build", - "lint": "eslint --fix --ext .js,.vue src", - "start": "node antdTheme.js && npm run serve" - }, - "dependencies": { - "ant-design-vue": "^1.6.3", - "antd-theme-generator": "^1.2.8", - "axios": "^0.19.2", - "core-js": "^3.6.5", - "element-ui": "^2.15.6", - "path-to-regexp": "^6.2.0", - "style-loader": "^2.0.0", - "v-viewer": "^1.5.1", - "video.js": "^7.10.2", - "vue": "^2.6.11", - "vue-json-editor": "^1.4.3", - "vue-router": "^3.2.0", - "vuex": "^3.4.0", - "vuex-multi-tab-state": "^1.0.16" - }, - "devDependencies": { - "@vue/cli-plugin-babel": "^4.4.0", - "@vue/cli-plugin-eslint": "^4.4.0", - "@vue/cli-service": "^4.4.0", - "@vue/eslint-config-standard": "^5.1.2", - "babel-eslint": "^10.1.0", - "babel-plugin-import": "^1.13.0", - "eslint": "^6.7.2", - "eslint-plugin-import": "^2.20.2", - "eslint-plugin-vue": "^6.2.2", - "less": "^2.7.3", - "less-loader": "^6.2.0", - "node-sass": "^4.14.1", - "sass-loader": "^8.0.2", - "vue-happy-scroll": "^2.1.1", - "vue-template-compiler": "^2.6.11" - }, - "rules": { - "generator-star-spacing": "off", - "no-tabs": "off", - "no-unused-vars": "off", - "no-console": "off", - "no-irregular-whitespace": "off", - "no-debugger": "off" - } + "name": "duoji-frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "node antdTheme.js && vue-cli-service serve", + "build": "node antdTheme.js && vue-cli-service build", + "lint": "eslint --fix --ext .js,.vue src", + "start": "node antdTheme.js && npm run serve" + }, + "dependencies": { + "ant-design-vue": "^1.6.3", + "antd-theme-generator": "^1.2.8", + "axios": "^0.19.2", + "core-js": "^3.6.5", + "element-ui": "^2.15.6", + "path-to-regexp": "^6.2.0", + "style-loader": "^2.0.0", + "v-viewer": "^1.5.1", + "video.js": "^7.10.2", + "videojs-contrib-hls": "^5.15.0", + "vue": "^2.6.11", + "vue-json-editor": "^1.4.3", + "vue-router": "^3.2.0", + "vue-video-player": "^5.0.2", + "vuex": "^3.4.0", + "vuex-multi-tab-state": "^1.0.16" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^4.4.0", + "@vue/cli-plugin-eslint": "^4.4.0", + "@vue/cli-service": "^4.4.0", + "@vue/eslint-config-standard": "^5.1.2", + "babel-eslint": "^10.1.0", + "babel-plugin-import": "^1.13.0", + "eslint": "^6.7.2", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-vue": "^6.2.2", + "less": "^2.7.3", + "less-loader": "^6.2.0", + "node-sass": "^4.14.1", + "sass-loader": "^8.0.2", + "vue-happy-scroll": "^2.1.1", + "vue-template-compiler": "^2.6.11" + }, + "rules": { + "generator-star-spacing": "off", + "no-tabs": "off", + "no-unused-vars": "off", + "no-console": "off", + "no-irregular-whitespace": "off", + "no-debugger": "off" + } } diff --git a/public/index.html b/public/index.html index b5cbc9a..15eb6d6 100644 --- a/public/index.html +++ b/public/index.html @@ -6,6 +6,11 @@ + + + + + 垛机视觉系统 diff --git a/public/red-test1.jpg b/public/red-test1.jpg deleted file mode 100644 index d37840313376e2452869b96d312f9991c8e54eaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50828 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 ZD(&%dbE_^L1Ki}-|#Ucs; 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

Lsu72yjx(7u(}+pWOSy97T;N$XcD5b`W{E* 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%0A?d=+> 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.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,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) 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-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f10a"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10b"}.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:"\f10c"}.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:"\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-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f10f"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f110"}.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:"\f111"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f112"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f113"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f114"}.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:"\f115"}.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:"\f116"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f117"}.vjs-icon-gplus{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-gplus:before{content:"\f118"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f119"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f11a"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f11b"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f11c"}.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:"\f11d"}.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:"\f11e"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f11f"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f120"}.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:"\f121"}.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:"\f122"}.video-js{display: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-16-9,.video-js.vjs-4-3,.video-js.vjs-fluid{width:100%;max-width:100%;height:0}.video-js.vjs-16-9{padding-top:56.25%}.video-js.vjs-4-3{padding-top:75%}.video-js.vjs-fill{width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}body.vjs-full-window{padding:0;margin:0;height:100%}.vjs-full-window .video-js.vjs-fullscreen{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen{width:100%!important;height:100%!important;padding-top:0!important}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.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;visibility:visible}.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:10px;left:10px;padding:0;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-centered .vjs-big-play-button{top:50%;left:50%;margin-top:-.81666em;margin-left:-1.5em}.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 .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{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}.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-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,.video-js.vjs-no-flex .vjs-menu-button-inline{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-no-flex .vjs-menu-button-inline .vjs-menu{display:block;opacity:1;position:relative;width:auto}.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu{width:auto}.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)}.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;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.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible}.vjs-has-started.vjs-no-flex .vjs-control-bar{display:table}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;flex:none}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.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 .vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.vjs-no-flex .vjs-control{display:table-cell;vertical-align:middle}.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}.vjs-no-flex .vjs-progress-control{width:auto}.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;top:-.3333333333em;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}.vjs-no-flex .vjs-progress-control .vjs-mouse-display{z-index:0}.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}.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display{display:none}.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;-ms-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-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;visibility:visible;opacity:1;position:relative;transition:none}.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{position:absolute;bottom:3em;left:.5em}.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}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{top:-.3em;right:-.5em}.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}.vjs-poster{display:inline-block;vertical-align:middle;background-repeat:no-repeat;background-position:50% 50%;background-size:contain;background-color:#000;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-has-started .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster{display:block}.vjs-using-native-controls .vjs-poster{display:none}.video-js .vjs-live-control{display:flex;align-items:flex-start;flex:auto;font-size:1em;line-height:3em}.vjs-no-flex .vjs-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-liveui .vjs-live-control,.video-js:not(.vjs-live) .vjs-live-control{display:none}.video-js .vjs-seek-to-live-control{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}.vjs-no-flex .vjs-seek-to-live-control{display:table-cell;width:auto;text-align:left}.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-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder{color: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{display:none}.video-js .vjs-current-time,.vjs-no-flex .vjs-current-time{display:none}.video-js .vjs-duration,.vjs-no-flex .vjs-duration{display:none}.vjs-time-divider{display:none;line-height:3em}.vjs-live .vjs-time-divider{display:none}.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}.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-user-inactive.vjs-playing video::-webkit-media-text-track-display{transform:translateY(-1.5em)}.video-js .vjs-picture-in-picture-control{cursor:pointer;flex:none}.video-js .vjs-fullscreen-control{cursor:pointer;flex: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-error .vjs-error-display:before{color:#fff;content:"X";font-family:Arial,Helvetica,sans-serif;font-size:4em;left:0;line-height:1;margin-top:-.5em;position:absolute;text-shadow:.05em .05em .1em #000;text-align:center;top:50%;vertical-align:middle;width:100%}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;opacity:.85;text-align:left;border:6px solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;border-radius:25px;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:block;-webkit-animation:vjs-spinner-show 0s linear .3s forwards;animation:vjs-spinner-show 0s linear .3s forwards}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;margin:-6px;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{-webkit-animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite;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;-webkit-animation-delay:.44s;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@-webkit-keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@-webkit-keyframes vjs-spinner-spin{100%{-webkit-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}}@-webkit-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}}.vjs-chapters-button .vjs-menu ul{width:24em}.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:"";font-size:1.5em;line-height:inherit}.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-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:" ";font-size:1.5em;line-height:inherit}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-control{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:auto;width:initial}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-live) .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-liveui) .vjs-subs-caps-button{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-custom-control-spacer{flex:auto;display:block}.video-js:not(.vjs-fullscreen).vjs-layout-tiny.vjs-no-flex .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui.vjs-no-flex .vjs-custom-control-spacer{width:auto}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-progress-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-progress-control{display:none}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.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-track-setting>select{margin-right:1em;margin-bottom:.5em}.vjs-text-track-settings fieldset{margin:5px;padding:3px;border:none}.vjs-text-track-settings fieldset span{display:inline-block}.vjs-text-track-settings fieldset span>select{max-width:7.3em}.vjs-text-track-settings legend{color:#fff;margin:0 0 5px 0}.vjs-text-track-settings .vjs-label{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);display:block;margin:0 0 5px 0;padding:0;border:0;height:1px;width:1px;overflow:hidden}.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}@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;background:0 0}.video-js .vjs-menu :focus:not(:focus-visible),.video-js :focus:not(:focus-visible){outline:0;background:0 0} diff --git a/public/static/video.js b/public/static/video.js new file mode 100644 index 0000000..593aaec --- /dev/null +++ b/public/static/video.js @@ -0,0 +1,25674 @@ +/** + * @license + * Video.js 6.6.2 + * 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.videojs = factory()); +}(this, (function () { + + var version = "6.6.2"; + + var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + + + + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + 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 empty = {}; + + + var empty$1 = (Object.freeze || Object)({ + 'default': empty + }); + + var minDoc = ( empty$1 && empty ) || empty$1; + + 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; + + /** + * @file browser.js + * @module browser + */ + var USER_AGENT = window_1.navigator && window_1.navigator.userAgent || ''; + var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); + var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; + + /* + * Device is an iPhone + * + * @type {Boolean} + * @constant + * @private + */ + var IS_IPAD = /iPad/i.test(USER_AGENT); + +// 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/ + var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; + var IS_IPOD = /iPod/i.test(USER_AGENT); + var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; + + var IOS_VERSION = function () { + var match = USER_AGENT.match(/OS (\d+)_/i); + + if (match && match[1]) { + return match[1]; + } + return null; + }(); + + var IS_ANDROID = /Android/i.test(USER_AGENT); + var 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 + var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); + + if (!match) { + return null; + } + + var major = match[1] && parseFloat(match[1]); + var minor = match[2] && parseFloat(match[2]); + + if (major && minor) { + return parseFloat(match[1] + '.' + match[2]); + } else if (major) { + return major; + } + return null; + }(); + +// Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser + var IS_OLD_ANDROID = IS_ANDROID && /webkit/i.test(USER_AGENT) && ANDROID_VERSION < 2.3; + var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; + + var IS_FIREFOX = /Firefox/i.test(USER_AGENT); + var IS_EDGE = /Edge/i.test(USER_AGENT); + var IS_CHROME = !IS_EDGE && /Chrome/i.test(USER_AGENT); + var CHROME_VERSION = function () { + var match = USER_AGENT.match(/Chrome\/(\d+)/); + + if (match && match[1]) { + return parseFloat(match[1]); + } + return null; + }(); + var IS_IE8 = /MSIE\s8\.0/.test(USER_AGENT); + var IE_VERSION = function () { + var result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); + var 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; + }(); + + var IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; + var IS_ANY_SAFARI = IS_SAFARI || IS_IOS; + + var TOUCH_ENABLED = isReal() && ('ontouchstart' in window_1 || window_1.DocumentTouch && window_1.document instanceof window_1.DocumentTouch); + + var BACKGROUND_SIZE_SUPPORTED = isReal() && 'backgroundSize' in window_1.document.createElement('video').style; + + var browser = (Object.freeze || Object)({ + IS_IPAD: IS_IPAD, + IS_IPHONE: IS_IPHONE, + IS_IPOD: IS_IPOD, + IS_IOS: IS_IOS, + IOS_VERSION: IOS_VERSION, + IS_ANDROID: IS_ANDROID, + ANDROID_VERSION: ANDROID_VERSION, + IS_OLD_ANDROID: IS_OLD_ANDROID, + IS_NATIVE_ANDROID: IS_NATIVE_ANDROID, + IS_FIREFOX: IS_FIREFOX, + IS_EDGE: IS_EDGE, + IS_CHROME: IS_CHROME, + CHROME_VERSION: CHROME_VERSION, + IS_IE8: IS_IE8, + IE_VERSION: IE_VERSION, + IS_SAFARI: IS_SAFARI, + IS_ANY_SAFARI: IS_ANY_SAFARI, + TOUCH_ENABLED: TOUCH_ENABLED, + BACKGROUND_SIZE_SUPPORTED: BACKGROUND_SIZE_SUPPORTED + }); + + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + + + + + + + + + + + + var classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + }; + + + + + + + + + + + + var inherits = function (subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + enumerable: false, + writable: true, + configurable: true + } + }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; + }; + + + + + + + + + + + + var possibleConstructorReturn = function (self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return call && (typeof call === "object" || typeof call === "function") ? call : self; + }; + + + + + + + + + + + + var taggedTemplateLiteralLoose = function (strings, raw) { + strings.raw = raw; + return strings; + }; + + /** + * @file obj.js + * @module obj + */ + + /** + * @callback obj:EachCallback + * + * @param {Mixed} 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 {Mixed} accum + * The value that is accumulating over the reduce loop. + * + * @param {Mixed} 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 {Mixed} + * The new accumulated value. + */ + var toString = 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 + */ + var keys = function keys(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(function (key) { + return 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 {Mixed} [initial = 0] + * Starting value + * + * @return {Mixed} + * The final accumulated value. + */ + function reduce(object, fn) { + var initial = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + + return keys(object).reduce(function (accum, key) { + return fn(accum, object[key], key); + }, initial); + } + + /** + * Object.assign-style object shallow merge/extend. + * + * @param {Object} target + * @param {Object} ...sources + * @return {Object} + */ + function assign(target) { + for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + sources[_key - 1] = arguments[_key]; + } + + if (Object.assign) { + return Object.assign.apply(Object, [target].concat(sources)); + } + + sources.forEach(function (source) { + if (!source) { + return; + } + + each(source, function (value, key) { + target[key] = value; + }); + }); + + return target; + } + + /** + * 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 === 'undefined' ? 'undefined' : _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.call(value) === '[object Object]' && value.constructor === Object; + } + + /** + * @file log.js + * @module log + */ + var log = void 0; + +// This is the private tracking variable for logging level. + var level = 'info'; + +// This is the private tracking variable for the logging history. + var history = []; + + /** + * Log messages to the console and history based on the type of message + * + * @private + * @param {string} type + * The name of the console method to use. + * + * @param {Array} args + * The arguments to be passed to the matching console method. + * + * @param {boolean} [stringify] + * By default, only old IEs should get console argument stringification, + * but this is exposed as a parameter to facilitate testing. + */ + var logByType = function logByType(type, args) { + var stringify = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : !!IE_VERSION && IE_VERSION < 11; + + var lvl = log.levels[level]; + var lvlRegExp = new RegExp('^(' + lvl + ')$'); + + if (type !== 'log') { + + // Add the type to the front of the message when it's not "log". + args.unshift(type.toUpperCase() + ':'); + } + + // Add a clone of the args at this point to history. + if (history) { + history.push([].concat(args)); + } + + // Add console prefix after adding to history. + args.unshift('VIDEOJS:'); + + // If there's no console then don't try to output messages, but they will + // still be stored in history. + if (!window_1.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. + var fn = window_1.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_1.console.info || window_1.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; + } + + // IEs previous to 11 log objects uselessly as "[object Object]"; so, JSONify + // objects and arrays for those less-capable browsers. + if (stringify) { + args = args.map(function (a) { + if (isObject(a) || Array.isArray(a)) { + try { + return JSON.stringify(a); + } catch (x) { + return String(a); + } + } + + // Cast to string before joining, so we get null and undefined explicitly + // included in output (as we would in a modern console). + return String(a); + }).join(' '); + } + + // Old IE versions do not allow .apply() for console methods (they are + // reported as objects rather than functions). + if (!fn.apply) { + fn(args); + } else { + fn[Array.isArray(args) ? 'apply' : 'call'](window_1.console, args); + } + }; + + /** + * Logs plain debug messages. Similar to `console.log`. + * + * @class + * @param {Mixed[]} args + * One or more messages or objects that should be logged. + */ + log = function log() { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + logByType('log', args); + }; + + /** + * 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 log.levels} is provided, acts as a setter. Regardless of argument, + * returns the current logging level. + * + * @param {string} [lvl] + * Pass to set a new logging level. + * + * @return {string} + * The current logging level. + */ + log.level = function (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 = function () { + return history ? [].concat(history) : []; + }; + + /** + * Clears the internal history tracking, but does not prevent further history + * tracking. + */ + log.history.clear = function () { + if (history) { + history.length = 0; + } + }; + + /** + * Disable history tracking if it is currently enabled. + */ + log.history.disable = function () { + if (history !== null) { + history.length = 0; + history = null; + } + }; + + /** + * Enable history tracking if it is currently disabled. + */ + log.history.enable = function () { + if (history === null) { + history = []; + } + }; + + /** + * Logs error messages. Similar to `console.error`. + * + * @param {Mixed[]} args + * One or more messages or objects that should be logged as an error + */ + log.error = function () { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + return logByType('error', args); + }; + + /** + * Logs warning messages. Similar to `console.warn`. + * + * @param {Mixed[]} args + * One or more messages or objects that should be logged as a warning. + */ + log.warn = function () { + for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + return logByType('warn', args); + }; + + /** + * Logs debug messages. Similar to `console.debug`, but may also act as a comparable + * log if `console.debug` is not available + * + * @param {Mixed[]} args + * One or more messages or objects that should be logged as debug. + */ + log.debug = function () { + for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + + return logByType('debug', args); + }; + + var log$1 = log; + + function clean (s) { + return s.replace(/\n\r?\s*/g, '') + } + + + var tsml = function tsml (sa) { + var s = '' + , i = 0; + + for (; i < arguments.length; i++) + s += clean(sa[i]) + (arguments[i + 1] || ''); + + return s + }; + + /** + * @file computed-style.js + * @module computed-style + */ + /** + * A safe getComputedStyle with an IE8 fallback. + * + * 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 + * + * @static + * @const + */ + function computedStyle(el, prop) { + if (!el || !prop) { + return ''; + } + + if (typeof window_1.getComputedStyle === 'function') { + var cs = window_1.getComputedStyle(el); + + return cs ? cs[prop] : ''; + } + + return el.currentStyle[prop] || ''; + } + + var _templateObject = taggedTemplateLiteralLoose(['Setting attributes in the second argument of createEl()\n has been deprecated. Use the third argument instead.\n createEl(type, properties, attributes). Attempting to set ', ' to ', '.'], ['Setting attributes in the second argument of createEl()\n has been deprecated. Use the third argument instead.\n createEl(type, properties, attributes). Attempting to set ', ' to ', '.']); + + /** + * @file dom.js + * @module dom + */ + /** + * Detect if a value is a string with any non-whitespace characters. + * + * @param {string} str + * The string to check + * + * @return {boolean} + * - True if the string is non-blank + * - False otherwise + * + */ + function isNonBlankString(str) { + return typeof str === 'string' && /\S/.test(str); + } + + /** + * Throws an error if the passed string has whitespace. This is used by + * class methods to be relatively consistent with the classList API. + * + * @param {string} str + * The string to check for whitespace. + * + * @throws {Error} + * Throws an error if there is whitespace in the string. + * + */ + function throwIfWhitespace(str) { + if (/\s/.test(str)) { + throw new Error('class has illegal whitespace characters'); + } + } + + /** + * Produce a regular expression for matching a className within an elements className. + * + * @param {string} className + * The className to generate the RegExp for. + * + * @return {RegExp} + * The RegExp that will check for a specific `className` in an elements + * className. + */ + function classRegExp(className) { + return new RegExp('(^|\\s)' + className + '($|\\s)'); + } + + /** + * Whether the current DOM interface appears to be real. + * + * @return {Boolean} + */ + function isReal() { + return ( + + // Both document and window will never be undefined thanks to `global`. + document_1 === window_1.document && + + // In IE < 9, DOM methods return "object" as their type, so all we can + // confidently check is that it exists. + typeof document_1.createElement !== 'undefined' + ); + } + + /** + * Determines, via duck typing, whether or not a value is a DOM element. + * + * @param {Mixed} value + * The thing to check + * + * @return {boolean} + * - True if it 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} + * + */ + function isInFrame() { + + // We need a try/catch here because Safari will throw errors when attempting + // to get either `parent` or `self` + try { + return window_1.parent !== window_1.self; + } catch (x) { + return true; + } + } + + /** + * Creates functions to query the DOM using a given method. + * + * @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_1[method](null); + } + if (isNonBlankString(context)) { + context = document_1.querySelector(context); + } + + var ctx = isEl(context) ? context : document_1; + + return ctx[method] && ctx[method](selector); + }; + } + + /** + * Creates an element and applies properties. + * + * @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 {String|Element|TextNode|Array|Function} [content] + * Contents for the element (see: {@link dom:normalizeContent}) + * + * @return {Element} + * The element that was created. + */ + function createEl() { + var tagName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div'; + var properties = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var content = arguments[3]; + + var el = document_1.createElement(tagName); + + Object.getOwnPropertyNames(properties).forEach(function (propName) { + var val = properties[propName]; + + // See #2176 + // We originally were accepting both properties and attributes in the + // same object, but that doesn't work so well. + if (propName.indexOf('aria-') !== -1 || propName === 'role' || propName === 'type') { + log$1.warn(tsml(_templateObject, propName, val)); + el.setAttribute(propName, val); + + // Handle textContent since it's not supported everywhere and we have a + // method for it. + } else if (propName === 'textContent') { + textContent(el, val); + } else { + 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 {Element} 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 CSS class + * + * @param {Element} element + * Element to check + * + * @param {string} classToCheck + * Class name to check for + * + * @return {boolean} + * - True if the element had the class + * - False otherwise. + * + * @throws {Error} + * Throws an error if `classToCheck` has white space. + */ + function hasClass(element, classToCheck) { + throwIfWhitespace(classToCheck); + if (element.classList) { + return element.classList.contains(classToCheck); + } + return classRegExp(classToCheck).test(element.className); + } + + /** + * Add a CSS class name to an element + * + * @param {Element} element + * Element to add class name to. + * + * @param {string} classToAdd + * Class name to add. + * + * @return {Element} + * The dom element with the added class name. + */ + function addClass(element, classToAdd) { + if (element.classList) { + element.classList.add(classToAdd); + + // Don't need to `throwIfWhitespace` here because `hasElClass` will do it + // in the case of classList not being supported. + } else if (!hasClass(element, classToAdd)) { + element.className = (element.className + ' ' + classToAdd).trim(); + } + + return element; + } + + /** + * Remove a CSS class name from an element + * + * @param {Element} element + * Element to remove a class name from. + * + * @param {string} classToRemove + * Class name to remove + * + * @return {Element} + * The dom element with class name removed. + */ + function removeClass(element, classToRemove) { + if (element.classList) { + element.classList.remove(classToRemove); + } else { + throwIfWhitespace(classToRemove); + element.className = element.className.split(/\s+/).filter(function (c) { + return c !== classToRemove; + }).join(' '); + } + + return element; + } + + /** + * The callback definition for toggleElClass. + * + * @callback 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 the `classToToggle` will get added to `element`. + * - If false the `classToToggle` will get removed from `element`. + * - If undefined this callback will be ignored + */ + + /** + * Adds or removes a CSS class name on 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|PredicateCallback} [predicate] + * See the return value for {@link Dom~PredicateCallback} + * + * @return {Element} + * The element with a class that has been toggled. + */ + function toggleClass(element, classToToggle, predicate) { + + // This CANNOT use `classList` internally because IE does not support the + // second parameter to the `classList.toggle()` method! Which is fine because + // `classList` will be used by the add/remove functions. + var has = hasClass(element, classToToggle); + + if (typeof predicate === 'function') { + predicate = predicate(element, classToToggle); + } + + if (typeof predicate !== 'boolean') { + predicate = !has; + } + + // If the necessary class operation matches the current state of the + // element, no action is required. + if (predicate === has) { + return; + } + + if (predicate) { + addClass(element, classToToggle); + } else { + removeClass(element, classToToggle); + } + + 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) { + var 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 (which shouldn't be used with HTML) + * This will return true or false for boolean attributes. + * + * @param {Element} tag + * Element from which to get tag attributes. + * + * @return {Object} + * All attributes of the element. + */ + function getAttributes(tag) { + var obj = {}; + + // known boolean attributes + // we can check for matching boolean properties, but older browsers + // won't know about HTML5 boolean attributes that we still read from + var knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ','; + + if (tag && tag.attributes && tag.attributes.length > 0) { + var attrs = tag.attributes; + + for (var i = attrs.length - 1; i >= 0; i--) { + var attrName = attrs[i].name; + var attrVal = attrs[i].value; + + // check for known booleans + // the matching element property will return a value for typeof + if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) { + // 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} + * 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 while dragging controls + */ + function blockTextSelection() { + document_1.body.focus(); + document_1.onselectstart = function () { + return false; + }; + } + + /** + * Turn off text selection blocking + */ + function unblockTextSelection() { + document_1.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 + */ + function getBoundingClientRect(el) { + if (el && el.getBoundingClientRect && el.parentNode) { + var rect = el.getBoundingClientRect(); + var result = {}; + + ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(function (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; + } + } + + /** + * The postion of a DOM element on the page. + * + * @typedef {Object} module:dom~Position + * + * @property {number} left + * Pixels to the left + * + * @property {number} top + * Pixels on top + */ + + /** + * Offset Left. + * 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) { + var box = void 0; + + if (el.getBoundingClientRect && el.parentNode) { + box = el.getBoundingClientRect(); + } + + if (!box) { + return { + left: 0, + top: 0 + }; + } + + var docEl = document_1.documentElement; + var body = document_1.body; + + var clientLeft = docEl.clientLeft || body.clientLeft || 0; + var scrollLeft = window_1.pageXOffset || body.scrollLeft; + var left = box.left + scrollLeft - clientLeft; + + var clientTop = docEl.clientTop || body.clientTop || 0; + var scrollTop = window_1.pageYOffset || body.scrollTop; + var top = box.top + scrollTop - clientTop; + + // Android sometimes returns slightly off decimal values, so need to round + return { + left: Math.round(left), + top: Math.round(top) + }; + } + + /** + * x and y coordinates for a dom element or mouse pointer + * + * @typedef {Object} Dom~Coordinates + * + * @property {number} x + * x coordinate in pixels + * + * @property {number} y + * y coordinate in pixels + */ + + /** + * Get pointer position in element + * Returns an object with x and y coordinates. + * 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 {EventTarget~Event} event + * Event object + * + * @return {Dom~Coordinates} + * A Coordinates object corresponding to the mouse position. + * + */ + function getPointerPosition(el, event) { + var position = {}; + var box = findPosition(el); + var boxW = el.offsetWidth; + var boxH = el.offsetHeight; + + var boxY = box.top; + var boxX = box.left; + var pageY = event.pageY; + var pageX = event.pageX; + + if (event.changedTouches) { + pageX = event.changedTouches[0].pageX; + pageY = event.changedTouches[0].pageY; + } + + position.y = Math.max(0, Math.min(1, (boxY - pageY + boxH) / boxH)); + position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); + + return position; + } + + /** + * Determines, via duck typing, whether or not a value is a text node. + * + * @param {Mixed} value + * Check if this value is a text node. + * + * @return {boolean} + * - True if it 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; + } + + /** + * Normalizes content for eventual insertion into the DOM. + * + * This allows a wide range of content definition methods, but protects + * from falling into the trap of simply writing to `innerHTML`, which is + * an XSS concern. + * + * The content for an element can be passed in multiple types and + * combinations, whose behavior is as follows: + * + * @param {String|Element|TextNode|Array|Function} content + * - String: Normalized into a text node. + * - Element/TextNode: Passed through. + * - Array: A one-dimensional array of strings, elements, nodes, or functions + * (which return single strings, elements, or nodes). + * - Function: If the sole argument, is expected to produce a string, element, + * node, or array as defined above. + * + * @return {Array} + * All of the content that was passed in normalized. + */ + 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(function (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_1.createTextNode(value); + } + }).filter(function (value) { + return value; + }); + } + + /** + * Normalizes and appends content to an element. + * + * @param {Element} el + * Element to append normalized content to. + * + * + * @param {String|Element|TextNode|Array|Function} content + * See the `content` argument of {@link dom:normalizeContent} + * + * @return {Element} + * The element with appended normalized content. + */ + function appendContent(el, content) { + normalizeContent(content).forEach(function (node) { + return 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 {String|Element|TextNode|Array|Function} content + * See the `content` argument of {@link dom:normalizeContent} + * + * @return {Element} + * The element with inserted normalized content. + * + */ + function insertContent(el, content) { + return appendContent(emptyEl(el), content); + } + + /** + * Check if event was a single left click + * + * @param {EventTarget~Event} event + * Event object + * + * @return {boolean} + * - True if a left click + * - False if not a left click + */ + 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 `butttons` ? + // 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; + } + + if (IE_VERSION === 9) { + // Ignore IE9 + + 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. + */ + var $ = 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. + * + */ + var $$ = createQuerier('querySelectorAll'); + + + + var Dom = (Object.freeze || Object)({ + 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, + $: $, + $$: $$ + }); + + /** + * @file guid.js + * @module guid + */ + + /** + * Unique ID for an element or function + * @type {Number} + */ + var _guid = 1; + + /** + * 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 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 elData = {}; + + /* + * Unique attribute name to store an element's guid in + * + * @type {String} + * @constant + * @private + */ + var elIdAttr = 'vdata' + new Date().getTime(); + + /** + * Returns the cache object where data for an element is stored + * + * @param {Element} el + * Element to store data for. + * + * @return {Object} + * The cache object for that el that was passed in. + */ + function getData(el) { + var id = el[elIdAttr]; + + if (!id) { + id = el[elIdAttr] = newGUID(); + } + + if (!elData[id]) { + elData[id] = {}; + } + + return elData[id]; + } + + /** + * Returns whether or not an element has cached data + * + * @param {Element} el + * Check if this element has cached data. + * + * @return {boolean} + * - True if the DOM element has cached data. + * - False otherwise. + */ + function hasData(el) { + var id = el[elIdAttr]; + + if (!id) { + return false; + } + + return !!Object.getOwnPropertyNames(elData[id]).length; + } + + /** + * Delete data for the element from the cache and the guid attr from getElementById + * + * @param {Element} el + * Remove cached data for this element. + */ + function removeData(el) { + var id = el[elIdAttr]; + + if (!id) { + return; + } + + // Remove all stored data + delete elData[id]; + + // Remove the elIdAttr property from the DOM node + try { + delete el[elIdAttr]; + } catch (e) { + if (el.removeAttribute) { + el.removeAttribute(elIdAttr); + } else { + // IE doesn't appear to support removeAttribute on the document element + el[elIdAttr] = null; + } + } + } + + /** + * @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. + * + * @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) { + var data = getData(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) { + removeData(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} type + * Type of event to bind to. + * + * @param {EventTarget~EventListener} 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) { + + 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) { + var old = event || window_1.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 (var 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 + if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY') { + // 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_1; + } + + // 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) { + var doc = document_1.documentElement; + var body = document_1.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 */ + } + } + + // Returns fixed-up instance + return event; + } + + /** + * Whether passive event listeners are supported + */ + var _supportsPassive = false; + + (function () { + try { + var opts = Object.defineProperty({}, 'passive', { + get: function get() { + _supportsPassive = true; + } + }); + + window_1.addEventListener('test', null, opts); + window_1.removeEventListener('test', null, opts); + } catch (e) { + // disregard + } + })(); + + /** + * Touch events Chrome expects to be passive + */ + var 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 {EventTarget~EventListener} fn + * Event listener. + */ + function on(elem, type, fn) { + if (Array.isArray(type)) { + return _handleMultipleEvents(on, elem, type, fn); + } + + var data = getData(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); + + var handlers = data.handlers[event.type]; + + if (handlers) { + // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. + var handlersCopy = handlers.slice(0); + + for (var 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) { + var 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 {EventTarget~EventListener} [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 (!hasData(elem)) { + return; + } + + var data = getData(elem); + + // If no events exist, nothing to unbind + if (!data.handlers) { + return; + } + + if (Array.isArray(type)) { + return _handleMultipleEvents(off, elem, type, fn); + } + + // Utility function + var removeType = function removeType(el, t) { + data.handlers[t] = []; + _cleanUpEvents(el, t); + }; + + // Are we removing all bound events? + if (type === undefined) { + for (var t in data.handlers) { + if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { + removeType(elem, t); + } + } + return; + } + + var 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 (var 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. + var elemData = hasData(elem) ? getData(elem) : {}; + var 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 }; + } + // 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) { + var targetData = getData(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); + } + var func = function func() { + 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); + } + + var Events = (Object.freeze || Object)({ + fixEvent: fixEvent, + on: on, + off: off, + trigger: trigger, + one: one + }); + + /** + * @file setup.js - Functions for setting up a player without + * user interaction based on the data-setup `attribute` of the video tag. + * + * @module setup + */ + var _windowLoaded = false; + var videojs$2 = void 0; + + /** + * Set up any tags that have a data-setup `attribute` when the player is started. + */ + var autoSetup = function autoSetup() { + + // Protect against breakage in non-browser environments. + if (!isReal()) { + return; + } + + // One day, when we stop supporting IE8, go back to this, but in the meantime...*hack hack hack* + // var vids = Array.prototype.slice.call(document.getElementsByTagName('video')); + // var audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); + // var mediaEls = vids.concat(audios); + + // Because IE8 doesn't support calling slice on a node list, we need to loop + // through each list of elements to build up a new, combined list of elements. + var vids = document_1.getElementsByTagName('video'); + var audios = document_1.getElementsByTagName('audio'); + var divs = document_1.getElementsByTagName('video-js'); + var mediaEls = []; + + if (vids && vids.length > 0) { + for (var i = 0, e = vids.length; i < e; i++) { + mediaEls.push(vids[i]); + } + } + + if (audios && audios.length > 0) { + for (var _i = 0, _e = audios.length; _i < _e; _i++) { + mediaEls.push(audios[_i]); + } + } + + if (divs && divs.length > 0) { + for (var _i2 = 0, _e2 = divs.length; _i2 < _e2; _i2++) { + mediaEls.push(divs[_i2]); + } + } + + // Check if any media elements exist + if (mediaEls && mediaEls.length > 0) { + + for (var _i3 = 0, _e3 = mediaEls.length; _i3 < _e3; _i3++) { + var mediaEl = mediaEls[_i3]; + + // Check if element exists, has getAttribute func. + // IE seems to consider typeof el.getAttribute == 'object' instead of + // 'function' like expected, at least when loading the player immediately. + if (mediaEl && mediaEl.getAttribute) { + + // Make sure this player hasn't already been set up. + if (mediaEl.player === undefined) { + var 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$2(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) { + if (vjs) { + videojs$2 = vjs; + } + + window_1.setTimeout(autoSetup, wait); + } + + if (isReal() && document_1.readyState === 'complete') { + _windowLoaded = true; + } else { + /** + * Listen for the load event on window, and set _windowLoaded to true. + * + * @listens load + */ + one(window_1, 'load', function () { + _windowLoaded = true; + }); + } + + /** + * @file stylesheet.js + * @module stylesheet + */ + /** + * Create a DOM syle 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. + */ + var createStyleElement = function createStyleElement(className) { + var style = document_1.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. + */ + var setTextContent = function setTextContent(el, content) { + if (el.styleSheet) { + el.styleSheet.cssText = content; + } else { + el.textContent = content; + } + }; + + /** + * @file fn.js + * @module fn + */ + /** + * Bind (a.k.a proxy or Context). A simple method for changing the context of a function + * It also stores a unique id on the function so it can be easily removed from events. + * + * @param {Mixed} 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 + */ + var bind = function bind(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 + var bound = function bound() { + return fn.apply(context, arguments); + }; + + // 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. + * + * @param {Function} fn + * The function to be throttled. + * + * @param {Number} wait + * The number of milliseconds by which to throttle. + * + * @return {Function} + */ + var throttle = function throttle(fn, wait) { + var last = Date.now(); + + var throttled = function throttled() { + var now = Date.now(); + + if (now - last >= wait) { + fn.apply(undefined, arguments); + last = now; + } + }; + + return throttled; + }; + + /** + * @file src/js/event-target.js + */ + /** + * `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 + */ + var EventTarget = function EventTarget() {}; + + /** + * A Custom DOM event. + * + * @typedef {Object} EventTarget~Event + * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} + */ + + /** + * All event listeners should follow the following format. + * + * @callback EventTarget~EventListener + * @this {EventTarget} + * + * @param {EventTarget~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_ + * @private + */ + EventTarget.prototype.allowedEvents_ = {}; + + /** + * 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 {EventTarget~EventListener} fn + * The function to call with `EventTarget`s + */ + EventTarget.prototype.on = function (type, fn) { + // Remove the addEventListener alias before calling Events.on + // so we don't get into an infinite type loop + var ael = this.addEventListener; + + this.addEventListener = function () {}; + on(this, type, fn); + this.addEventListener = ael; + }; + + /** + * 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; + + /** + * 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 {EventTarget~EventListener} fn + * The function to remove. + */ + EventTarget.prototype.off = function (type, fn) { + off(this, type, fn); + }; + + /** + * 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; + + /** + * 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 {EventTarget~EventListener} fn + * The function to be called once for each event name. + */ + EventTarget.prototype.one = function (type, fn) { + // Remove the addEventListener alialing Events.on + // so we don't get into an infinite type loop + var ael = this.addEventListener; + + this.addEventListener = function () {}; + one(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. + */ + EventTarget.prototype.trigger = function (event) { + var type = event.type || event; + + if (typeof event === 'string') { + event = { type: type }; + } + event = fixEvent(event); + + if (this.allowedEvents_[type] && this['on' + type]) { + this['on' + type](event); + } + + trigger(this, event); + }; + + /** + * 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 + */ + /** + * 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. + */ + var isEvented = function isEvented(object) { + return object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(function (k) { + return typeof object[k] === 'function'; + }); + }; + + /** + * 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. + */ + var isValidEventType = function isValidEventType(type) { + return ( + // 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. + */ + var validateTarget = function validateTarget(target) { + if (!target.nodeName && !isEvented(target)) { + throw new Error('Invalid target; 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. + */ + var validateEventType = function validateEventType(type) { + if (!isValidEventType(type)) { + throw new Error('Invalid event type; 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. + */ + var validateListener = function validateListener(listener) { + if (typeof listener !== 'function') { + throw new Error('Invalid listener; 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()`. + * + * @return {Object} + * An object containing useful values for `on()` or `one()` calls. + */ + var normalizeListenArgs = function normalizeListenArgs(self, args) { + + // If the number of arguments is less than 3, the target is always the + // evented object itself. + var isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; + var target = void 0; + var type = void 0; + var listener = void 0; + + 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 = args[0]; + listener = args[1]; + } else { + target = args[0]; + type = args[1]; + listener = args[2]; + } + + validateTarget(target); + validateEventType(type); + validateListener(listener); + + listener = bind(self, listener); + + return { isTargetingSelf: isTargetingSelf, target: target, type: type, listener: 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. + */ + var listen = function listen(target, method, type, listener) { + validateTarget(target); + + if (target.nodeName) { + Events[method](target, type, listener); + } else { + target[method](type, listener); + } + }; + + /** + * Contains methods that provide event capabilites to an object which is passed + * to {@link module:evented|evented}. + * + * @mixin EventedMixin + */ + var 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: function on$$1() { + var _this = this; + + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var _normalizeListenArgs = normalizeListenArgs(this, args), + isTargetingSelf = _normalizeListenArgs.isTargetingSelf, + target = _normalizeListenArgs.target, + type = _normalizeListenArgs.type, + listener = _normalizeListenArgs.listener; + + listen(target, 'on', type, listener); + + // If this object is listening to another evented object. + if (!isTargetingSelf) { + + // If this object is disposed, remove the listener. + var removeListenerOnDispose = function removeListenerOnDispose() { + return _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. + var removeRemoverOnTargetDispose = function removeRemoverOnTargetDispose() { + return _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 only be called once 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: function one$$1() { + var _this2 = this; + + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + var _normalizeListenArgs2 = normalizeListenArgs(this, args), + isTargetingSelf = _normalizeListenArgs2.isTargetingSelf, + target = _normalizeListenArgs2.target, + type = _normalizeListenArgs2.type, + listener = _normalizeListenArgs2.listener; + + // Targeting this evented object. + + + if (isTargetingSelf) { + listen(target, 'one', type, listener); + + // Targeting another evented object. + } else { + var wrapper = function wrapper() { + for (var _len3 = arguments.length, largs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + largs[_key3] = arguments[_key3]; + } + + _this2.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); + } + }, + + + /** + * 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: function off$$1(targetOrType, typeOrListener, listener) { + + // Targeting this evented object. + if (!targetOrType || isValidEventType(targetOrType)) { + off(this.eventBusEl_, targetOrType, typeOrListener); + + // Targeting another evented object. + } else { + var target = targetOrType; + var type = typeOrListener; + + // Fail fast and in a meaningful way! + validateTarget(target); + validateEventType(type); + validateListener(listener); + + // 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. + * + * @returns {boolean} + * Whether or not the default behavior was prevented. + */ + trigger: function trigger$$1(event, hash) { + 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) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var eventBusKey = options.eventBusKey; + + // 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' }); + } + + assign(target, EventedMixin); + + // When any evented object is disposed, it removes all its listeners. + target.on('dispose', function () { + target.off(); + window_1.setTimeout(function () { + 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 + */ + var 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. + * + * @returns {Object|undefined} + * An object containing changes that occurred. If no changes + * occurred, returns `undefined`. + */ + setState: function setState(stateUpdates) { + var _this = this; + + // Support providing the `stateUpdates` state as a function. + if (typeof stateUpdates === 'function') { + stateUpdates = stateUpdates(); + } + + var changes = void 0; + + each(stateUpdates, function (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: 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. + * + * @returns {Object} + * Returns the `target`. + */ + function stateful(target, defaultState) { + assign(target, StatefulMixin); + + // This happens after the mixing-in because we need to replace the `state` + // added in that step. + target.state = 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 to-title-case.js + * @module to-title-case + */ + + /** + * Uppercase the first letter of a string. + * + * @param {string} string + * String to be uppercased + * + * @return {string} + * The string with an uppercased first letter + */ + function toTitleCase(string) { + if (typeof string !== 'string') { + return string; + } + + return string.charAt(0).toUpperCase() + string.slice(1); + } + + /** + * 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 + */ + function titleCaseEquals(str1, str2) { + return toTitleCase(str1) === toTitleCase(str2); + } + + /** + * @file merge-options.js + * @module merge-options + */ + /** + * Deep-merge one or more options objects, recursively merging **only** plain + * object properties. + * + * @param {Object[]} sources + * One or more objects to merge into a new object. + * + * @returns {Object} + * A new object that is the merged result of all sources. + */ + function mergeOptions() { + var result = {}; + + for (var _len = arguments.length, sources = Array(_len), _key = 0; _key < _len; _key++) { + sources[_key] = arguments[_key]; + } + + sources.forEach(function (source) { + if (!source) { + return; + } + + each(source, function (value, key) { + if (!isPlain(value)) { + result[key] = value; + return; + } + + if (!isPlain(result[key])) { + result[key] = {}; + } + + result[key] = mergeOptions(result[key], value); + }); + }); + + return result; + } + + /** + * 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} + */ + + var Component = function () { + + /** + * A callback that is called when a component is ready. Does not have any + * paramters and any callback value will be ignored. + * + * @callback Component~ReadyCallback + * @this Component + */ + + /** + * 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. + * + * @param {Object[]} [options.children] + * An array of children objects to intialize 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 {Component~ReadyCallback} [ready] + * Function that gets called when the `Component` is ready. + */ + function Component(player, options, ready) { + classCallCheck(this, Component); + + + // 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; + } + + // Make a copy of prototype.options_ to protect against overriding defaults + this.options_ = mergeOptions({}, this.options_); + + // Updated options with supplied options + options = this.options_ = mergeOptions(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 + var 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 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 }); + } + stateful(this, this.constructor.defaultState); + + this.children_ = []; + this.childIndex_ = {}; + this.childNameIndex_ = {}; + + // Add any child components in options + if (options.initChildren !== false) { + this.initChildren(); + } + + this.ready(ready); + // Don't want to trigger ready here or it will before init is actually + // finished for all children that run this constructor + + if (options.reportTouchActivity !== false) { + this.enableTouchActivity(); + } + } + + /** + * Dispose of the `Component` and all child components. + * + * @fires Component#dispose + */ + + + Component.prototype.dispose = function dispose() { + + /** + * Triggered when a `Component` is disposed. + * + * @event Component#dispose + * @type {EventTarget~Event} + * + * @property {boolean} [bubbles=false] + * set to false so that the close event does not + * bubble up + */ + this.trigger({ type: 'dispose', bubbles: false }); + + // Dispose all children. + if (this.children_) { + for (var 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; + + if (this.el_) { + // Remove element from DOM + if (this.el_.parentNode) { + this.el_.parentNode.removeChild(this.el_); + } + + removeData(this.el_); + this.el_ = null; + } + + // remove reference to the player after disposing of the element + this.player_ = null; + }; + + /** + * Return the {@link Player} that the `Component` has attached to. + * + * @return {Player} + * The player that this `Component` has attached to. + */ + + + Component.prototype.player = function 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:mergeOptions} + * + * @param {Object} obj + * The object that contains new options. + * + * @return {Object} + * A new object of `this.options_` and `obj` merged together. + * + * @deprecated since version 5 + */ + + + Component.prototype.options = function options(obj) { + log$1.warn('this.options() has been deprecated and will be moved to the constructor in 6.0'); + + if (!obj) { + return this.options_; + } + + this.options_ = mergeOptions(this.options_, obj); + return this.options_; + }; + + /** + * Get the `Component`s DOM element + * + * @return {Element} + * The DOM element for this `Component`. + */ + + + Component.prototype.el = function 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. + */ + + + Component.prototype.createEl = function createEl$$1(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 loooks 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. + */ + + + Component.prototype.localize = function localize(string, tokens) { + var defaultValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : string; + + var code = this.player_.language && this.player_.language(); + var languages = this.player_.languages && this.player_.languages(); + var language = languages && languages[code]; + var primaryCode = code && code.split('-')[0]; + var primaryLang = languages && languages[primaryCode]; + + var 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) { + var value = tokens[index - 1]; + var ret = value; + + if (typeof value === 'undefined') { + ret = match; + } + + return ret; + }); + } + + return localizedString; + }; + + /** + * 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`. + */ + + + Component.prototype.contentEl = function contentEl() { + return this.contentEl_ || this.el_; + }; + + /** + * Get this `Component`s ID + * + * @return {string} + * The id of this `Component` + */ + + + Component.prototype.id = function 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`. + */ + + + Component.prototype.name = function name() { + return this.name_; + }; + + /** + * Get an array of all child components + * + * @return {Array} + * The children + */ + + + Component.prototype.children = function 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. + */ + + + Component.prototype.getChildById = function 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. + */ + + + Component.prototype.getChild = function getChild(name) { + if (!name) { + return; + } + + name = toTitleCase(name); + + return this.childNameIndex_[name]; + }; + + /** + * 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. + */ + + + Component.prototype.addChild = function addChild(child) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.children_.length; + + var component = void 0; + var componentName = void 0; + + // If child is a string, create component with options + if (typeof child === 'string') { + componentName = toTitleCase(child); + + var 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 + var 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; + } + + this.children_.splice(index, 0, component); + + 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; + } + + // Add the UI object's element to the container div (box) + // Having an element is not required + if (typeof component.el === 'function' && component.el()) { + var childNodes = this.contentEl().children; + var refNode = childNodes[index] || null; + + 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. + */ + + + Component.prototype.removeChild = function removeChild(component) { + if (typeof component === 'string') { + component = this.getChild(component); + } + + if (!component || !this.children_) { + return; + } + + var childFound = false; + + for (var i = this.children_.length - 1; i >= 0; i--) { + if (this.children_[i] === component) { + childFound = true; + this.children_.splice(i, 1); + break; + } + } + + if (!childFound) { + return; + } + + this.childIndex_[component.id()] = null; + this.childNameIndex_[component.name()] = null; + + var compEl = component.el(); + + if (compEl && compEl.parentNode === this.contentEl()) { + this.contentEl().removeChild(component.el()); + } + }; + + /** + * Add and initialize default child `Component`s based upon options. + */ + + + Component.prototype.initChildren = function initChildren() { + var _this = this; + + var children = this.options_.children; + + if (children) { + // `this` is `parent` + var parentOptions = this.options_; + + var handleAdd = function handleAdd(child) { + var name = child.name; + var 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 + var newChild = _this.addChild(name, opts); + + if (newChild) { + _this[name] = newChild; + } + }; + + // Allow for an array of children details to passed in the options + var workingChildren = void 0; + var 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(function (child) { + var name = void 0; + var opts = void 0; + + if (typeof child === 'string') { + name = child; + opts = children[name] || _this.options_[name] || {}; + } else { + name = child.name; + opts = child; + } + + return { name: name, opts: opts }; + }).filter(function (child) { + // we have to make sure that child.name isn't in the techOrder since + // techs are registerd as Components but can't aren't compatible + // See https://github.com/videojs/video.js/issues/2772 + var c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name)); + + return c && !Tech.isTech(c); + }).forEach(handleAdd); + } + }; + + /** + * Builds the default DOM class name. Should be overriden by sub-components. + * + * @return {string} + * The DOM class name for this object. + * + * @abstract + */ + + + Component.prototype.buildCSSClass = function 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. + * + * @return {Component} + * Returns itself; method can be chained. + */ + + + Component.prototype.ready = function ready(fn) { + var sync = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 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 + */ + + + Component.prototype.triggerReady = function triggerReady() { + this.isReady_ = true; + + // Ensure ready is triggerd asynchronously + this.setTimeout(function () { + var 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 {EventTarget~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) + */ + + + Component.prototype.$ = function $$$1(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) + */ + + + Component.prototype.$$ = function $$$$1(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` + */ + + + Component.prototype.hasClass = function hasClass$$1(classToCheck) { + return hasClass(this.el_, classToCheck); + }; + + /** + * Add a CSS class name to the `Component`s element. + * + * @param {string} classToAdd + * CSS class name to add + */ + + + Component.prototype.addClass = function addClass$$1(classToAdd) { + addClass(this.el_, classToAdd); + }; + + /** + * Remove a CSS class name from the `Component`s element. + * + * @param {string} classToRemove + * CSS class name to remove + */ + + + Component.prototype.removeClass = function removeClass$$1(classToRemove) { + removeClass(this.el_, classToRemove); + }; + + /** + * 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 + */ + + + Component.prototype.toggleClass = function toggleClass$$1(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. + */ + + + Component.prototype.show = function show() { + this.removeClass('vjs-hidden'); + }; + + /** + * Hide the `Component`s element if it is currently showing by adding the + * 'vjs-hidden` class name to it. + */ + + + Component.prototype.hide = function 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 + */ + + + Component.prototype.lockShowing = function 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 + */ + + + Component.prototype.unlockShowing = function 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 attibute does not exist or has + * no value. + * + * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} + */ + + + Component.prototype.getAttribute = function getAttribute$$1(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} + */ + + + Component.prototype.setAttribute = function setAttribute$$1(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} + */ + + + Component.prototype.removeAttribute = function removeAttribute$$1(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|string} + * The width when getting, zero if there is no width. Can be a string + * postpixed with '%' or 'px'. + */ + + + Component.prototype.width = function 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|string} + * The width when getting, zero if there is no width. Can be a string + * postpixed with '%' or 'px'. + */ + + + Component.prototype.height = function 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. + */ + + + Component.prototype.dimensions = function 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} + * The dimension when getting or 0 if unset + */ + + + Component.prototype.dimension = function 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 {EventTarget~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 + var val = this.el_.style[widthOrHeight]; + var 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 width or the height of the `Component` elements computed style. 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. + */ + + + Component.prototype.currentDimension = function currentDimension(widthOrHeight) { + var computedWidthOrHeight = 0; + + if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { + throw new Error('currentDimension only accepts width or height value'); + } + + if (typeof window_1.getComputedStyle === 'function') { + var computedStyle = window_1.getComputedStyle(this.el_); + + computedWidthOrHeight = computedStyle.getPropertyValue(widthOrHeight) || computedStyle[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 on IE8 and wherever getComputedStyle doesn't exist. + if (computedWidthOrHeight === 0) { + var 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 width and height values of the `Component`s + * computed style. + * + * @return {Component~DimensionObject} + * The dimensions of the components element + */ + + + Component.prototype.currentDimensions = function currentDimensions() { + return { + width: this.currentDimension('width'), + height: this.currentDimension('height') + }; + }; + + /** + * Get the width of the `Component`s computed style. Uses `window.getComputedStyle`. + * + * @return {number} width + * The width of the `Component`s computed style. + */ + + + Component.prototype.currentWidth = function currentWidth() { + return this.currentDimension('width'); + }; + + /** + * Get the height of the `Component`s computed style. Uses `window.getComputedStyle`. + * + * @return {number} height + * The height of the `Component`s computed style. + */ + + + Component.prototype.currentHeight = function currentHeight() { + return this.currentDimension('height'); + }; + + /** + * Set the focus to this component + */ + + + Component.prototype.focus = function focus() { + this.el_.focus(); + }; + + /** + * Remove the focus from this component + */ + + + Component.prototype.blur = function blur() { + this.el_.blur(); + }; + + /** + * 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. + * + * @private + * @fires Component#tap + * @listens Component#touchstart + * @listens Component#touchmove + * @listens Component#touchleave + * @listens Component#touchcancel + * @listens Component#touchend + */ + + + Component.prototype.emitTapEvents = function emitTapEvents() { + // Track the start time so we can determine how long the touch lasted + var touchStart = 0; + var 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. + var tapMovementThreshold = 10; + + // The maximum length a touch can be while still being considered a tap + var touchTimeThreshold = 200; + + var couldBeTap = void 0; + + 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 = new Date().getTime(); + // 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 + var xdiff = event.touches[0].pageX - firstTouch.pageX; + var ydiff = event.touches[0].pageY - firstTouch.pageY; + var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); + + if (touchDistance > tapMovementThreshold) { + couldBeTap = false; + } + } + }); + + var noTap = function noTap() { + 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 + var touchTime = new Date().getTime() - 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 {EventTarget~Event} + */ + 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 + */ + + + Component.prototype.enableTouchActivity = function 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 + var report = bind(this.player(), this.player().reportUserActivity); + + var touchHolding = void 0; + + 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); + }); + + var touchEnd = function touchEnd(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 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}. + * + * @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} + */ + + + Component.prototype.setTimeout = function setTimeout(fn, timeout) { + var _this2 = this; + + fn = bind(this, fn); + + var timeoutId = window_1.setTimeout(fn, timeout); + var disposeFn = function disposeFn() { + return _this2.clearTimeout(timeoutId); + }; + + disposeFn.guid = 'vjs-timeout-' + timeoutId; + + this.on('dispose', disposeFn); + + 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} + */ + + + Component.prototype.clearTimeout = function clearTimeout(timeoutId) { + window_1.clearTimeout(timeoutId); + + var disposeFn = function disposeFn() {}; + + disposeFn.guid = 'vjs-timeout-' + timeoutId; + + this.off('dispose', disposeFn); + + 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} + */ + + + Component.prototype.setInterval = function setInterval(fn, interval) { + var _this3 = this; + + fn = bind(this, fn); + + var intervalId = window_1.setInterval(fn, interval); + + var disposeFn = function disposeFn() { + return _this3.clearInterval(intervalId); + }; + + disposeFn.guid = 'vjs-interval-' + intervalId; + + this.on('dispose', disposeFn); + + return intervalId; + }; + + /** + * Clears an interval that gets created via `window.setInterval` or + * {@link Component#setInterval}. If you set an inteval 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} + */ + + + Component.prototype.clearInterval = function clearInterval(intervalId) { + window_1.clearInterval(intervalId); + + var disposeFn = function disposeFn() {}; + + disposeFn.guid = 'vjs-interval-' + intervalId; + + this.off('dispose', disposeFn); + + 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} + */ + + + Component.prototype.requestAnimationFrame = function requestAnimationFrame(fn) { + var _this4 = this; + + if (this.supportsRaf_) { + fn = bind(this, fn); + + var id = window_1.requestAnimationFrame(fn); + var disposeFn = function disposeFn() { + return _this4.cancelAnimationFrame(id); + }; + + disposeFn.guid = 'vjs-raf-' + id; + this.on('dispose', disposeFn); + + return id; + } + + // Fall back to using a timer. + return this.setTimeout(fn, 1000 / 60); + }; + + /** + * 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} + */ + + + Component.prototype.cancelAnimationFrame = function cancelAnimationFrame(id) { + if (this.supportsRaf_) { + window_1.cancelAnimationFrame(id); + + var disposeFn = function disposeFn() {}; + + disposeFn.guid = 'vjs-raf-' + id; + + this.off('dispose', disposeFn); + + return id; + } + + // Fall back to using a timer. + return this.clearTimeout(id); + }; + + /** + * 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. + */ + + + Component.registerComponent = function registerComponent(name, ComponentToRegister) { + if (typeof name !== 'string' || !name) { + throw new Error('Illegal component name, "' + name + '"; must be a non-empty string.'); + } + + var Tech = Component.getComponent('Tech'); + + // We need to make sure this check is only done if Tech has been registered. + var isTech = Tech && Tech.isTech(ComponentToRegister); + var isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype); + + if (isTech || !isComp) { + var reason = void 0; + + 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_ = {}; + } + + var Player = Component.getComponent('Player'); + + if (name === 'Player' && Player && Player.players) { + var players = Player.players; + var 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(function (pname) { + return players[pname]; + }).every(Boolean)) { + throw new Error('Can not register Player component after player has been created.'); + } + } + + Component.components_[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 {Component} + * The `Component` that got registered under the given name. + * + * @deprecated In `videojs` 6 this will not return `Component`s that were not + * registered using {@link Component.registerComponent}. Currently we + * check the global `videojs` object for a `Component` name and + * return that if it exists. + */ + + + Component.getComponent = function getComponent(name) { + if (!name) { + return; + } + + name = toTitleCase(name); + + if (Component.components_ && Component.components_[name]) { + return Component.components_[name]; + } + }; + + return Component; + }(); + + /** + * Whether or not this component supports `requestAnimationFrame`. + * + * This is exposed primarily for testing purposes. + * + * @private + * @type {Boolean} + */ + + + Component.prototype.supportsRaf_ = typeof window_1.requestAnimationFrame === 'function' && typeof window_1.cancelAnimationFrame === 'function'; + + Component.registerComponent('Component', Component); + + /** + * @file time-ranges.js + * @module time-ranges + */ + + /** + * Returns the time for the specified index at the start or end + * of a TimeRange object. + * + * @function time-ranges:indexFunction + * + * @param {number} [index=0] + * The range number to return the time for. + * + * @return {number} + * The time that offset at the specified index. + * + * @depricated index must be set to a value, in the future this will throw an error. + */ + + /** + * An object that contains ranges of time for various reasons. + * + * @typedef {Object} TimeRange + * + * @property {number} length + * The number of time ranges represented by this Object + * + * @property {time-ranges:indexFunction} start + * Returns the time offset at which a specified time range begins. + * + * @property {time-ranges:indexFunction} end + * Returns the time offset at which a specified time range begins. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges + */ + + /** + * Check if any of the time ranges are over the maximum index. + * + * @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 + ').'); + } + } + + /** + * Check if any of the time ranges are over the maximum index. + * + * @param {string} fnName + * The function name to use for logging + * + * @param {string} valueIndex + * The proprety 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. + * + * + * @depricated 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 givent ranges of time. + * + * @param {Array} [ranges] + * An array of time ranges. + */ + function createTimeRangesObj(ranges) { + if (ranges === undefined || ranges.length === 0) { + return { + length: 0, + start: function start() { + throw new Error('This TimeRanges object is empty'); + }, + end: function end() { + throw new Error('This TimeRanges object is empty'); + } + }; + } + return { + length: ranges.length, + start: getRange.bind(null, 'start', 0, ranges), + end: getRange.bind(null, 'end', 1, ranges) + }; + } + + /** + * Should create a fake `TimeRange` object which mimics an HTML5 time range instance. + * + * @param {number|Array} start + * The start of a single range or an array of ranges + * + * @param {number} end + * The end of a single range. + * + * @private + */ + function createTimeRanges(start, end) { + if (Array.isArray(start)) { + return createTimeRangesObj(start); + } else if (start === undefined || end === undefined) { + return createTimeRangesObj(); + } + return createTimeRangesObj([[start, end]]); + } + + /** + * @file buffer.js + * @module buffer + */ + /** + * Compute the percentage of the media that has been buffered. + * + * @param {TimeRange} buffered + * The current `TimeRange` 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) { + var bufferedDuration = 0; + var start = void 0; + var end = void 0; + + if (!duration) { + return 0; + } + + if (!buffered || !buffered.length) { + buffered = createTimeRanges(0, 0); + } + + for (var 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 fullscreen-api.js + * @module fullscreen-api + * @private + */ + /** + * 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} + */ + var FullscreenApi = {}; + +// browser API methods + var apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror'], +// WebKit + ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror'], +// Old WebKit (Safari 5.1) + ['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror'], +// Mozilla + ['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror'], +// Microsoft + ['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError']]; + + var specApi = apiMap[0]; + var browserApi = void 0; + +// determine the supported set of functions + for (var i = 0; i < apiMap.length; i++) { + // check for exitFullscreen function + if (apiMap[i][1] in document_1) { + browserApi = apiMap[i]; + break; + } + } + +// map the browser API names to the spec API names + if (browserApi) { + for (var _i = 0; _i < browserApi.length; _i++) { + FullscreenApi[specApi[_i]] = browserApi[_i]; + } + } + + /** + * @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; + } + + 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; + + /** + * 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_CUSTOM + * @property {string} 2 - MEDIA_ERR_ABORTED + * @property {string} 3 - MEDIA_ERR_NETWORK + * @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.' + }; + +// Add types as properties on MediaError +// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; + for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { + MediaError[MediaError.errorTypes[errNum]] = errNum; + // values should be accessible on both the class and instance + MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; + } + + 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 && 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, function (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 + */ + var trackToJson_ = function trackToJson_(track) { + var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (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 {Tech} tech + * The tech object to query + * + * @return {Array} + * A serializable javascript representation of the {@link Tech}s + * {@link TextTrackList}. + */ + var textTracksToJson = function textTracksToJson(tech) { + + var trackEls = tech.$$('track'); + + var trackObjs = Array.prototype.map.call(trackEls, function (t) { + return t.track; + }); + var tracks = Array.prototype.map.call(trackEls, function (trackEl) { + var 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. + */ + var jsonToTextTracks = function jsonToTextTracks(json, tech) { + json.forEach(function (track) { + var addedTrack = tech.addRemoteTextTrack(track).track; + + if (!track.src && track.cues) { + track.cues.forEach(function (cue) { + return addedTrack.addCue(cue); + }); + } + }); + + return tech.textTracks(); + }; + + var textTrackConverter = { textTracksToJson: textTracksToJson, jsonToTextTracks: jsonToTextTracks, trackToJson_: trackToJson_ }; + + /** + * @file modal-dialog.js + */ + var MODAL_CLASS_NAME = 'vjs-modal-dialog'; + var ESC = 27; + + /** + * 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 + */ + + var ModalDialog = function (_Component) { + inherits(ModalDialog, _Component); + + /** + * Create 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. + * + * @param {Mixed} [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.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. + */ + function ModalDialog(player, options) { + classCallCheck(this, ModalDialog); + + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + _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_); + return _this; + } + + /** + * Create the `ModalDialog`'s DOM element + * + * @return {Element} + * The DOM element that gets created. + */ + + + ModalDialog.prototype.createEl = function createEl$$1() { + return _Component.prototype.createEl.call(this, 'div', { + className: this.buildCSSClass(), + tabIndex: -1 + }, { + 'aria-describedby': this.id() + '_description', + 'aria-hidden': 'true', + 'aria-label': this.label(), + 'role': 'dialog' + }); + }; + + ModalDialog.prototype.dispose = function dispose() { + this.contentEl_ = null; + this.descEl_ = null; + this.previouslyActiveEl_ = null; + + _Component.prototype.dispose.call(this); + }; + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + ModalDialog.prototype.buildCSSClass = function buildCSSClass() { + return MODAL_CLASS_NAME + ' vjs-hidden ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * Handles `keydown` events on the document, looking for ESC, which closes + * the modal. + * + * @param {EventTarget~Event} e + * The keypress that triggered this event. + * + * @listens keydown + */ + + + ModalDialog.prototype.handleKeyPress = function handleKeyPress(e) { + if (e.which === ESC && this.closeable()) { + this.close(); + } + }; + + /** + * Returns the label string for this modal. Primarily used for accessibility. + * + * @return {string} + * the localized or raw label of this modal. + */ + + + ModalDialog.prototype.label = function 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. + */ + + + ModalDialog.prototype.description = function description() { + var 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 + */ + + + ModalDialog.prototype.open = function open() { + if (!this.opened_) { + var player = this.player(); + + /** + * Fired just before a `ModalDialog` is opened. + * + * @event ModalDialog#beforemodalopen + * @type {EventTarget~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(); + } + + if (this.closeable()) { + this.on(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); + } + + // 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 {EventTarget~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 + */ + + + ModalDialog.prototype.opened = function 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 + */ + + + ModalDialog.prototype.close = function close() { + if (!this.opened_) { + return; + } + var player = this.player(); + + /** + * Fired just before a `ModalDialog` is closed. + * + * @event ModalDialog#beforemodalclose + * @type {EventTarget~Event} + */ + this.trigger('beforemodalclose'); + this.opened_ = false; + + if (this.wasPlaying_ && this.options_.pauseOnOpen) { + player.play(); + } + + if (this.closeable()) { + this.off(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); + } + + 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 {EventTarget~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. + */ + + + ModalDialog.prototype.closeable = function closeable(value) { + if (typeof value === 'boolean') { + var closeable = this.closeable_ = !!value; + var 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. + var 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. + */ + + + ModalDialog.prototype.fill = function 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 {Mixed} [content] + * The same rules apply to this as apply to the `content` option. + */ + + + ModalDialog.prototype.fillWith = function fillWith(content) { + var contentEl = this.contentEl(); + var parentEl = contentEl.parentNode; + var nextSiblingEl = contentEl.nextSibling; + + /** + * Fired just before a `ModalDialog` is filled with content. + * + * @event ModalDialog#beforemodalfill + * @type {EventTarget~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 {EventTarget~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 + var 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 + */ + + + ModalDialog.prototype.empty = function empty() { + /** + * Fired just before a `ModalDialog` is emptied. + * + * @event ModalDialog#beforemodalempty + * @type {EventTarget~Event} + */ + this.trigger('beforemodalempty'); + emptyEl(this.contentEl()); + + /** + * Fired just after a `ModalDialog` is emptied. + * + * @event ModalDialog#modalempty + * @type {EventTarget~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 {Mixed} [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 {Mixed} + * The current content of the modal dialog + */ + + + ModalDialog.prototype.content = function 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 + */ + + + ModalDialog.prototype.conditionalFocus_ = function conditionalFocus_() { + var activeEl = document_1.activeElement; + var playerEl = this.player_.el_; + + this.previouslyActiveEl_ = null; + + if (playerEl.contains(activeEl) || playerEl === activeEl) { + this.previouslyActiveEl_ = activeEl; + + this.focus(); + + this.on(document_1, 'keydown', this.handleKeyDown); + } + }; + + /** + * conditionally blur the element and refocus the last focused element + * + * @private + */ + + + ModalDialog.prototype.conditionalBlur_ = function conditionalBlur_() { + if (this.previouslyActiveEl_) { + this.previouslyActiveEl_.focus(); + this.previouslyActiveEl_ = null; + } + + this.off(document_1, 'keydown', this.handleKeyDown); + }; + + /** + * Keydown handler. Attached when modal is focused. + * + * @listens keydown + */ + + + ModalDialog.prototype.handleKeyDown = function handleKeyDown(event) { + // exit early if it isn't a tab key + if (event.which !== 9) { + return; + } + + var focusableEls = this.focusableEls_(); + var activeEl = this.el_.querySelector(':focus'); + var focusIndex = void 0; + + for (var i = 0; i < focusableEls.length; i++) { + if (activeEl === focusableEls[i]) { + focusIndex = i; + break; + } + } + + if (document_1.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 + */ + + + ModalDialog.prototype.focusableEls_ = function focusableEls_() { + var allChildren = this.el_.querySelectorAll('*'); + + return Array.prototype.filter.call(allChildren, function (child) { + return (child instanceof window_1.HTMLAnchorElement || child instanceof window_1.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window_1.HTMLInputElement || child instanceof window_1.HTMLSelectElement || child instanceof window_1.HTMLTextAreaElement || child instanceof window_1.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window_1.HTMLIFrameElement || child instanceof window_1.HTMLObjectElement || child instanceof window_1.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable'); + }); + }; + + return ModalDialog; + }(Component); + + /** + * 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 + */ + + var TrackList = function (_EventTarget) { + inherits(TrackList, _EventTarget); + + /** + * Create an instance of this class + * + * @param {Track[]} tracks + * A list of tracks to initialize the list with. + * + * @param {Object} [list] + * The child object with inheritance done manually for ie8. + * + * @abstract + */ + function TrackList() { + var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + + var _ret; + + var list = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + classCallCheck(this, TrackList); + + var _this = possibleConstructorReturn(this, _EventTarget.call(this)); + + if (!list) { + list = _this; // eslint-disable-line + if (IS_IE8) { + list = document_1.createElement('custom'); + for (var prop in TrackList.prototype) { + if (prop !== 'constructor') { + list[prop] = TrackList.prototype[prop]; + } + } + } + } + + list.tracks_ = []; + + /** + * @memberof TrackList + * @member {number} length + * The current number of `Track`s in the this Trackist. + * @instance + */ + Object.defineProperty(list, 'length', { + get: function get$$1() { + return this.tracks_.length; + } + }); + + for (var i = 0; i < tracks.length; i++) { + list.addTrack(tracks[i]); + } + + // must return the object, as for ie8 it will not be this + // but a reference to a document object + return _ret = list, possibleConstructorReturn(_this, _ret); + } + + /** + * Add a {@link Track} to the `TrackList` + * + * @param {Track} track + * The audio, video, or text track to add to the list. + * + * @fires TrackList#addtrack + */ + + + TrackList.prototype.addTrack = function addTrack(track) { + var index = this.tracks_.length; + + if (!('' + index in this)) { + Object.defineProperty(this, index, { + get: function get$$1() { + 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 {EventTarget~Event} + * @property {Track} track + * A reference to track that was added. + */ + this.trigger({ + track: track, + type: 'addtrack' + }); + } + }; + + /** + * Remove a {@link Track} from the `TrackList` + * + * @param {Track} rtrack + * The audio, video, or text track to remove from the list. + * + * @fires TrackList#removetrack + */ + + + TrackList.prototype.removeTrack = function removeTrack(rtrack) { + var track = void 0; + + for (var 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 {EventTarget~Event} + * @property {Track} track + * A reference to track that was removed. + */ + this.trigger({ + track: track, + type: 'removetrack' + }); + }; + + /** + * Get a Track from the TrackList by a tracks id + * + * @param {String} id - the id of the track to get + * @method getTrackById + * @return {Track} + * @private + */ + + + TrackList.prototype.getTrackById = function getTrackById(id) { + var result = null; + + for (var i = 0, l = this.length; i < l; i++) { + var track = this[i]; + + if (track.id === id) { + result = track; + break; + } + } + + return result; + }; + + return TrackList; + }(EventTarget); + + /** + * Triggered when a different track is selected/enabled. + * + * @event TrackList#change + * @type {EventTarget~Event} + */ + + /** + * Events that can be called with on + eventName. See {@link EventHandler}. + * + * @property {Object} TrackList#allowedEvents_ + * @private + */ + + + TrackList.prototype.allowedEvents_ = { + change: 'change', + addtrack: 'addtrack', + removetrack: 'removetrack' + }; + +// emulate attribute EventHandler support to allow for feature detection + for (var 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 {AudioTrack} track + * The track to skip + * + * @private + */ + var disableOthers = function disableOthers(list, track) { + for (var 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 + */ + + var AudioTrackList = function (_TrackList) { + inherits(AudioTrackList, _TrackList); + + /** + * Create an instance of this class. + * + * @param {AudioTrack[]} [tracks=[]] + * A list of `AudioTrack` to instantiate the list with. + */ + function AudioTrackList() { + var _this, _ret; + + var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + classCallCheck(this, AudioTrackList); + + var list = void 0; + + // make sure only 1 track is enabled + // sorted from last index to first index + for (var i = tracks.length - 1; i >= 0; i--) { + if (tracks[i].enabled) { + disableOthers(tracks, tracks[i]); + break; + } + } + + // IE8 forces us to implement inheritance ourselves + // as it does not support Object.defineProperty properly + if (IS_IE8) { + list = document_1.createElement('custom'); + for (var prop in TrackList.prototype) { + if (prop !== 'constructor') { + list[prop] = TrackList.prototype[prop]; + } + } + for (var _prop in AudioTrackList.prototype) { + if (_prop !== 'constructor') { + list[_prop] = AudioTrackList.prototype[_prop]; + } + } + } + + list = (_this = possibleConstructorReturn(this, _TrackList.call(this, tracks, list)), _this); + list.changing_ = false; + + return _ret = list, possibleConstructorReturn(_this, _ret); + } + + /** + * Add an {@link AudioTrack} to the `AudioTrackList`. + * + * @param {AudioTrack} track + * The AudioTrack to add to the list + * + * @fires TrackList#addtrack + */ + + + AudioTrackList.prototype.addTrack = function addTrack(track) { + var _this2 = this; + + if (track.enabled) { + disableOthers(this, track); + } + + _TrackList.prototype.addTrack.call(this, track); + // native tracks don't have this + if (!track.addEventListener) { + return; + } + + /** + * @listens AudioTrack#enabledchange + * @fires TrackList#change + */ + track.addEventListener('enabledchange', function () { + // 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 (_this2.changing_) { + return; + } + _this2.changing_ = true; + disableOthers(_this2, track); + _this2.changing_ = false; + _this2.trigger('change'); + }); + }; + + return AudioTrackList; + }(TrackList); + + /** + * @file video-track-list.js + */ + /** + * Un-select all other {@link VideoTrack}s that are selected. + * + * @param {VideoTrackList} list + * list to work on + * + * @param {VideoTrack} track + * The track to skip + * + * @private + */ + var disableOthers$1 = function disableOthers(list, track) { + for (var 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 + */ + + var VideoTrackList = function (_TrackList) { + inherits(VideoTrackList, _TrackList); + + /** + * Create an instance of this class. + * + * @param {VideoTrack[]} [tracks=[]] + * A list of `VideoTrack` to instantiate the list with. + */ + function VideoTrackList() { + var _this, _ret; + + var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + classCallCheck(this, VideoTrackList); + + var list = void 0; + + // make sure only 1 track is enabled + // sorted from last index to first index + for (var i = tracks.length - 1; i >= 0; i--) { + if (tracks[i].selected) { + disableOthers$1(tracks, tracks[i]); + break; + } + } + + // IE8 forces us to implement inheritance ourselves + // as it does not support Object.defineProperty properly + if (IS_IE8) { + list = document_1.createElement('custom'); + for (var prop in TrackList.prototype) { + if (prop !== 'constructor') { + list[prop] = TrackList.prototype[prop]; + } + } + for (var _prop in VideoTrackList.prototype) { + if (_prop !== 'constructor') { + list[_prop] = VideoTrackList.prototype[_prop]; + } + } + } + + list = (_this = possibleConstructorReturn(this, _TrackList.call(this, tracks, list)), _this); + list.changing_ = false; + + /** + * @member {number} VideoTrackList#selectedIndex + * The current index of the selected {@link VideoTrack`}. + */ + Object.defineProperty(list, 'selectedIndex', { + get: function get$$1() { + for (var _i = 0; _i < this.length; _i++) { + if (this[_i].selected) { + return _i; + } + } + return -1; + }, + set: function set$$1() {} + }); + + return _ret = list, possibleConstructorReturn(_this, _ret); + } + + /** + * Add a {@link VideoTrack} to the `VideoTrackList`. + * + * @param {VideoTrack} track + * The VideoTrack to add to the list + * + * @fires TrackList#addtrack + */ + + + VideoTrackList.prototype.addTrack = function addTrack(track) { + var _this2 = this; + + if (track.selected) { + disableOthers$1(this, track); + } + + _TrackList.prototype.addTrack.call(this, track); + // native tracks don't have this + if (!track.addEventListener) { + return; + } + + /** + * @listens VideoTrack#selectedchange + * @fires TrackList#change + */ + track.addEventListener('selectedchange', function () { + if (_this2.changing_) { + return; + } + _this2.changing_ = true; + disableOthers$1(_this2, track); + _this2.changing_ = false; + _this2.trigger('change'); + }); + }; + + return VideoTrackList; + }(TrackList); + + /** + * @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 + */ + + var TextTrackList = function (_TrackList) { + inherits(TextTrackList, _TrackList); + + /** + * Create an instance of this class. + * + * @param {TextTrack[]} [tracks=[]] + * A list of `TextTrack` to instantiate the list with. + */ + function TextTrackList() { + var _this, _ret; + + var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + classCallCheck(this, TextTrackList); + + var list = void 0; + + // IE8 forces us to implement inheritance ourselves + // as it does not support Object.defineProperty properly + if (IS_IE8) { + list = document_1.createElement('custom'); + for (var prop in TrackList.prototype) { + if (prop !== 'constructor') { + list[prop] = TrackList.prototype[prop]; + } + } + for (var _prop in TextTrackList.prototype) { + if (_prop !== 'constructor') { + list[_prop] = TextTrackList.prototype[_prop]; + } + } + } + + list = (_this = possibleConstructorReturn(this, _TrackList.call(this, tracks, list)), _this); + return _ret = list, possibleConstructorReturn(_this, _ret); + } + + /** + * Add a {@link TextTrack} to the `TextTrackList` + * + * @param {TextTrack} track + * The text track to add to the list. + * + * @fires TrackList#addtrack + */ + + + TextTrackList.prototype.addTrack = function addTrack(track) { + _TrackList.prototype.addTrack.call(this, track); + + /** + * @listens TextTrack#modechange + * @fires TrackList#change + */ + track.addEventListener('modechange', bind(this, function () { + this.trigger('change'); + })); + + var nonLanguageTextTrackKind = ['metadata', 'chapters']; + + if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { + track.addEventListener('modechange', bind(this, function () { + this.trigger('selectedlanguagechange'); + })); + } + }; + + return TextTrackList; + }(TrackList); + + /** + * @file html-track-element-list.js + */ + + /** + * The current list of {@link HtmlTrackElement}s. + */ + + var HtmlTrackElementList = function () { + + /** + * Create an instance of this class. + * + * @param {HtmlTrackElement[]} [tracks=[]] + * A list of `HtmlTrackElement` to instantiate the list with. + */ + function HtmlTrackElementList() { + var trackElements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + classCallCheck(this, HtmlTrackElementList); + + var list = this; // eslint-disable-line + + if (IS_IE8) { + list = document_1.createElement('custom'); + + for (var prop in HtmlTrackElementList.prototype) { + if (prop !== 'constructor') { + list[prop] = HtmlTrackElementList.prototype[prop]; + } + } + } + + list.trackElements_ = []; + + /** + * @memberof HtmlTrackElementList + * @member {number} length + * The current number of `Track`s in the this Trackist. + * @instance + */ + Object.defineProperty(list, 'length', { + get: function get$$1() { + return this.trackElements_.length; + } + }); + + for (var i = 0, length = trackElements.length; i < length; i++) { + list.addTrackElement_(trackElements[i]); + } + + if (IS_IE8) { + return list; + } + } + + /** + * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` + * + * @param {HtmlTrackElement} trackElement + * The track element to add to the list. + * + * @private + */ + + + HtmlTrackElementList.prototype.addTrackElement_ = function addTrackElement_(trackElement) { + var index = this.trackElements_.length; + + if (!('' + index in this)) { + Object.defineProperty(this, index, { + get: function get$$1() { + 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 + */ + + + HtmlTrackElementList.prototype.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { + var trackElement_ = void 0; + + for (var 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 + */ + + + HtmlTrackElementList.prototype.removeTrackElement_ = function removeTrackElement_(trackElement) { + for (var i = 0, length = this.trackElements_.length; i < length; i++) { + if (trackElement === this.trackElements_[i]) { + this.trackElements_.splice(i, 1); + + break; + } + } + }; + + return HtmlTrackElementList; + }(); + + /** + * @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} + */ + + var TextTrackCueList = function () { + + /** + * Create an instance of this class.. + * + * @param {Array} cues + * A list of cues to be initialized with + */ + function TextTrackCueList(cues) { + classCallCheck(this, TextTrackCueList); + + var list = this; // eslint-disable-line + + if (IS_IE8) { + list = document_1.createElement('custom'); + + for (var prop in TextTrackCueList.prototype) { + if (prop !== 'constructor') { + list[prop] = TextTrackCueList.prototype[prop]; + } + } + } + + TextTrackCueList.prototype.setCues_.call(list, cues); + + /** + * @memberof TextTrackCueList + * @member {number} length + * The current number of `TextTrackCue`s in the TextTrackCueList. + * @instance + */ + Object.defineProperty(list, 'length', { + get: function get$$1() { + return this.length_; + } + }); + + if (IS_IE8) { + return list; + } + } + + /** + * 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 + */ + + + TextTrackCueList.prototype.setCues_ = function setCues_(cues) { + var oldLength = this.length || 0; + var i = 0; + var l = cues.length; + + this.cues_ = cues; + this.length_ = cues.length; + + var defineProp = function defineProp(index) { + if (!('' + index in this)) { + Object.defineProperty(this, '' + index, { + get: function get$$1() { + 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. + */ + + + TextTrackCueList.prototype.getCueById = function getCueById(id) { + var result = null; + + for (var i = 0, l = this.length; i < l; i++) { + var cue = this[i]; + + if (cue.id === id) { + result = cue; + break; + } + } + + return result; + }; + + return TextTrackCueList; + }(); + + /** + * @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 + */ + var 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 + */ + var 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 + */ + var 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 + */ + var 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 + */ + + var Track = function (_EventTarget) { + inherits(Track, _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 + */ + function Track() { + var _ret; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, Track); + + var _this = possibleConstructorReturn(this, _EventTarget.call(this)); + + var track = _this; // eslint-disable-line + + if (IS_IE8) { + track = document_1.createElement('custom'); + for (var prop in Track.prototype) { + if (prop !== 'constructor') { + track[prop] = Track.prototype[prop]; + } + } + } + + var trackProps = { + id: options.id || 'vjs_track_' + newGUID(), + kind: options.kind || '', + label: options.label || '', + language: options.language || '' + }; + + /** + * @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} label + * The label of this track. 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 + */ + + var _loop = function _loop(key) { + Object.defineProperty(track, key, { + get: function get$$1() { + return trackProps[key]; + }, + set: function set$$1() {} + }); + }; + + for (var key in trackProps) { + _loop(key); + } + + return _ret = track, possibleConstructorReturn(_this, _ret); + } + + return Track; + }(EventTarget); + + /** + * @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. + * + * @param {String} url + * The url to parse + * + * @return {url:URLObject} + * An object of url details + */ + var parseUrl = function parseUrl(url) { + var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; + + // add the url to an anchor and let the browser parse the URL + var a = document_1.createElement('a'); + + a.href = url; + + // IE8 (and 9?) Fix + // ie8 doesn't parse the URL correctly until the anchor is actually + // added to the body, and an innerHTML is needed to trigger the parsing + var addToBody = a.host === '' && a.protocol !== 'file:'; + var div = void 0; + + if (addToBody) { + div = document_1.createElement('div'); + div.innerHTML = ''; + a = div.firstChild; + // prevent the div from affecting layout + div.setAttribute('style', 'display:none; position:absolute;'); + document_1.body.appendChild(div); + } + + // Copy the specific URL properties to a new object + // This is also needed for IE8 because the anchor loses its + // properties when it's removed from the dom + var details = {}; + + for (var i = 0; i < props.length; i++) { + details[props[i]] = a[props[i]]; + } + + // IE9 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_1.location.protocol; + } + + if (addToBody) { + document_1.body.removeChild(div); + } + + return details; + }; + + /** + * Get absolute version of relative URL. Used to tell flash correct URL. + * + * + * @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 + */ + var getAbsoluteURL = function getAbsoluteURL(url) { + // Check if absolute URL + if (!url.match(/^https?:\/\//)) { + // Convert to absolute URL. Flash hosted off-site needs an absolute URL. + var div = document_1.createElement('div'); + + div.innerHTML = 'x'; + url = div.firstChild.href; + } + + return url; + }; + + /** + * Returns the extension of the passed file name. It will return an empty string + * if passed an invalid path. + * + * @param {string} path + * The fileName path like '/path/to/file.mp4' + * + * @returns {string} + * The extension in lower case or an empty string if no + * extension could be found. + */ + var getFileExtension = function getFileExtension(path) { + if (typeof path === 'string') { + var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i; + var pathParts = splitPathRe.exec(path); + + if (pathParts) { + return pathParts.pop().toLowerCase(); + } + } + + return ''; + }; + + /** + * Returns whether the url passed is a cross domain request or not. + * + * @param {string} url + * The url to check. + * + * @return {boolean} + * Whether it is a cross domain request or not. + */ + var isCrossOrigin = function isCrossOrigin(url) { + var winLoc = window_1.location; + var urlInfo = parseUrl(url); + + // IE8 protocol relative urls will return ':' for protocol + var 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 + var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; + + return crossOrigin; + }; + + var Url = (Object.freeze || Object)({ + parseUrl: parseUrl, + getAbsoluteURL: getAbsoluteURL, + getFileExtension: getFileExtension, + isCrossOrigin: isCrossOrigin + }); + + var isFunction_1 = isFunction; + + var toString$1 = Object.prototype.toString; + + function isFunction (fn) { + var string = toString$1.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 trim_1 = createCommonjsModule(function (module, exports) { + exports = module.exports = trim; + + function trim(str){ + return str.replace(/^\s*|\s*$/g, ''); + } + + exports.left = function(str){ + return str.replace(/^\s*/, ''); + }; + + exports.right = function(str){ + return str.replace(/\s*$/, ''); + }; + }); + + var forEach_1 = forEach; + + var toString$2 = Object.prototype.toString; + var hasOwnProperty = Object.prototype.hasOwnProperty; + + function forEach(list, iterator, context) { + if (!isFunction_1(iterator)) { + throw new TypeError('iterator must be a function') + } + + if (arguments.length < 3) { + context = this; + } + + if (toString$2.call(list) === '[object Array]') + forEachArray$1(list, iterator, context); + else if (typeof list === 'string') + forEachString(list, iterator, context); + else + forEachObject(list, iterator, context); + } + + function forEachArray$1(array, iterator, context) { + for (var i = 0, len = array.length; i < len; i++) { + if (hasOwnProperty.call(array, i)) { + iterator.call(context, array[i], i, array); + } + } + } + + function forEachString(string, iterator, context) { + for (var i = 0, len = string.length; i < len; i++) { + // no such thing as a sparse string. + iterator.call(context, string.charAt(i), i, string); + } + } + + function forEachObject(object, iterator, context) { + for (var k in object) { + if (hasOwnProperty.call(object, k)) { + iterator.call(context, object[k], k, object); + } + } + } + + var isArray = function(arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + + var parseHeaders = function (headers) { + if (!headers) + return {} + + var result = {}; + + forEach_1( + trim_1(headers).split('\n') + , function (row) { + var index = row.indexOf(':') + , key = trim_1(row.slice(0, index)).toLowerCase() + , value = trim_1(row.slice(index + 1)); + + if (typeof(result[key]) === 'undefined') { + result[key] = value; + } else if (isArray(result[key])) { + result[key].push(value); + } else { + result[key] = [ result[key], value ]; + } + } + ); + + return result + }; + + var immutable = extend; + + var hasOwnProperty$1 = Object.prototype.hasOwnProperty; + + function extend() { + var target = {}; + + for (var i = 0; i < arguments.length; i++) { + var source = arguments[i]; + + for (var key in source) { + if (hasOwnProperty$1.call(source, key)) { + target[key] = source[key]; + } + } + } + + return target + } + + var xhr = 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 = immutable(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) { + if (xhr.responseType === "document") { + return xhr.responseXML + } + var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; + if (xhr.responseType === "" && !firefoxBugTakenEffect) { + return xhr.responseXML + } + + return null + } + + function noop() {} + + /** + * @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 + */ + var parseCues = function parseCues(srcContent, track) { + var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, window_1.WebVTT.StringDecoder()); + var 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_1.console && window_1.console.groupCollapsed) { + window_1.console.groupCollapsed('Text Track parsing errors for ' + track.src); + } + errors.forEach(function (error) { + return log$1.error(error); + }); + if (window_1.console && window_1.console.groupEnd) { + window_1.console.groupEnd(); + } + } + + parser.flush(); + }; + + /** + * Load a `TextTrack` from a specifed 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 + */ + var loadTrack = function loadTrack(src, track) { + var opts = { + uri: src + }; + var crossOrigin = isCrossOrigin(src); + + if (crossOrigin) { + opts.cors = crossOrigin; + } + + xhr(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_1.WebVTT !== 'function') { + if (track.tech_) { + var loadHandler = function loadHandler() { + return parseCues(responseBody, track); + }; + + track.tech_.on('vttjsloaded', loadHandler); + track.tech_.on('vttjserror', function () { + log$1.error('vttjs failed to load, stopping trying to process ' + track.src); + track.tech_.off('vttjsloaded', loadHandler); + }); + } + } 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 + */ + + var TextTrack = function (_Track) { + inherits(TextTrack, _Track); + + /** + * Create an instance of this class. + * + * @param {Object} options={} + * Object of option names and values + * + * @param {Tech} 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 + * vesion 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. + */ + function TextTrack() { + var _this, _ret; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, TextTrack); + + if (!options.tech) { + throw new Error('A tech was not provided.'); + } + + var settings = mergeOptions(options, { + kind: TextTrackKind[options.kind] || 'subtitles', + language: options.language || options.srclang || '' + }); + var mode = TextTrackMode[settings.mode] || 'disabled'; + var default_ = settings['default']; + + if (settings.kind === 'metadata' || settings.kind === 'chapters') { + mode = 'hidden'; + } + // on IE8 this will be a document element + // for every other browser this will be a normal object + var tt = (_this = possibleConstructorReturn(this, _Track.call(this, settings)), _this); + + tt.tech_ = settings.tech; + + if (IS_IE8) { + for (var prop in TextTrack.prototype) { + if (prop !== 'constructor') { + tt[prop] = TextTrack.prototype[prop]; + } + } + } + + tt.cues_ = []; + tt.activeCues_ = []; + + var cues = new TextTrackCueList(tt.cues_); + var activeCues = new TextTrackCueList(tt.activeCues_); + var changed = false; + var timeupdateHandler = bind(tt, function () { + + // Accessing this.activeCues for the side-effects of updating itself + // due to it's nature as a getter function. Do not remove or cues will + // stop updating! + /* eslint-disable no-unused-expressions */ + this.activeCues; + /* eslint-enable no-unused-expressions */ + if (changed) { + this.trigger('cuechange'); + changed = false; + } + }); + + if (mode !== 'disabled') { + tt.tech_.ready(function () { + tt.tech_.on('timeupdate', timeupdateHandler); + }, true); + } + + /** + * @memberof TextTrack + * @member {boolean} default + * If this track was set to be on or off by default. Cannot be changed after + * creation. + * @instance + * + * @readonly + */ + Object.defineProperty(tt, 'default', { + get: function get$$1() { + return default_; + }, + set: function set$$1() {} + }); + + /** + * @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 + */ + Object.defineProperty(tt, 'mode', { + get: function get$$1() { + return mode; + }, + set: function set$$1(newMode) { + var _this2 = this; + + if (!TextTrackMode[newMode]) { + return; + } + mode = newMode; + if (mode === 'showing') { + + this.tech_.ready(function () { + _this2.tech_.on('timeupdate', timeupdateHandler); + }, true); + } + /** + * 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 {EventTarget~Event} + */ + this.trigger('modechange'); + } + }); + + /** + * @memberof TextTrack + * @member {TextTrackCueList} cues + * The text track cue list for this TextTrack. + * @instance + */ + Object.defineProperty(tt, 'cues', { + get: function get$$1() { + if (!this.loaded_) { + return null; + } + + return cues; + }, + set: function set$$1() {} + }); + + /** + * @memberof TextTrack + * @member {TextTrackCueList} activeCues + * The list text track cues that are currently active for this TextTrack. + * @instance + */ + Object.defineProperty(tt, 'activeCues', { + get: function get$$1() { + if (!this.loaded_) { + return null; + } + + // nothing to do + if (this.cues.length === 0) { + return activeCues; + } + + var ct = this.tech_.currentTime(); + var active = []; + + for (var i = 0, l = this.cues.length; i < l; i++) { + var cue = this.cues[i]; + + if (cue.startTime <= ct && cue.endTime >= ct) { + active.push(cue); + } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { + active.push(cue); + } + } + + changed = false; + + if (active.length !== this.activeCues_.length) { + changed = true; + } else { + for (var _i = 0; _i < active.length; _i++) { + if (this.activeCues_.indexOf(active[_i]) === -1) { + changed = true; + } + } + } + + this.activeCues_ = active; + activeCues.setCues_(this.activeCues_); + + return activeCues; + }, + set: function set$$1() {} + }); + + if (settings.src) { + tt.src = settings.src; + loadTrack(settings.src, tt); + } else { + tt.loaded_ = true; + } + + return _ret = tt, possibleConstructorReturn(_this, _ret); + } + + /** + * Add a cue to the internal list of cues. + * + * @param {TextTrack~Cue} cue + * The cue to add to our internal list + */ + + + TextTrack.prototype.addCue = function addCue(originalCue) { + var cue = originalCue; + + if (window_1.vttjs && !(originalCue instanceof window_1.vttjs.VTTCue)) { + cue = new window_1.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); + + for (var 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; + } + + var tracks = this.tech_.textTracks(); + + for (var 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 + */ + + + TextTrack.prototype.removeCue = function removeCue(_removeCue) { + var i = this.cues_.length; + + while (i--) { + var cue = this.cues_[i]; + + if (cue === _removeCue || cue.originalCue_ && cue.originalCue_ === _removeCue) { + this.cues_.splice(i, 1); + this.cues.setCues_(this.cues_); + break; + } + } + }; + + return TextTrack; + }(Track); + + /** + * cuechange - One or more cues in the track have become active or stopped being active. + */ + + + 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 + */ + + var AudioTrack = function (_Track) { + inherits(AudioTrack, _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. + */ + function AudioTrack() { + var _this, _ret; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, AudioTrack); + + var settings = mergeOptions(options, { + kind: AudioTrackKind[options.kind] || '' + }); + // on IE8 this will be a document element + // for every other browser this will be a normal object + var track = (_this = possibleConstructorReturn(this, _Track.call(this, settings)), _this); + var enabled = false; + + if (IS_IE8) { + for (var prop in AudioTrack.prototype) { + if (prop !== 'constructor') { + track[prop] = AudioTrack.prototype[prop]; + } + } + } + /** + * @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(track, 'enabled', { + get: function get$$1() { + return enabled; + }, + set: function set$$1(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 {EventTarget~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) { + track.enabled = settings.enabled; + } + track.loaded_ = true; + + return _ret = track, possibleConstructorReturn(_this, _ret); + } + + return AudioTrack; + }(Track); + + /** + * A representation of a single `VideoTrack`. + * + * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} + * @extends Track + */ + + var VideoTrack = function (_Track) { + inherits(VideoTrack, _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. + */ + function VideoTrack() { + var _this, _ret; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, VideoTrack); + + var settings = mergeOptions(options, { + kind: VideoTrackKind[options.kind] || '' + }); + + // on IE8 this will be a document element + // for every other browser this will be a normal object + var track = (_this = possibleConstructorReturn(this, _Track.call(this, settings)), _this); + var selected = false; + + if (IS_IE8) { + for (var prop in VideoTrack.prototype) { + if (prop !== 'constructor') { + track[prop] = VideoTrack.prototype[prop]; + } + } + } + + /** + * @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(track, 'selected', { + get: function get$$1() { + return selected; + }, + set: function set$$1(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 {EventTarget~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) { + track.selected = settings.selected; + } + + return _ret = track, possibleConstructorReturn(_this, _ret); + } + + return VideoTrack; + }(Track); + + /** + * @file html-track-element.js + */ + + /** + * @memberof HTMLTrackElement + * @typedef {HTMLTrackElement~ReadyState} + * @enum {number} + */ + var NONE = 0; + var LOADING = 1; + var LOADED = 2; + var ERROR = 3; + + /** + * A single track represented in the DOM. + * + * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} + * @extends EventTarget + */ + + var HTMLTrackElement = function (_EventTarget) { + inherits(HTMLTrackElement, _EventTarget); + + /** + * Create an instance of this class. + * + * @param {Object} options={} + * Object of option names and values + * + * @param {Tech} 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 + * vesion 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. + */ + function HTMLTrackElement() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + classCallCheck(this, HTMLTrackElement); + + var _this = possibleConstructorReturn(this, _EventTarget.call(this)); + + var readyState = void 0; + var trackElement = _this; // eslint-disable-line + + if (IS_IE8) { + trackElement = document_1.createElement('custom'); + + for (var prop in HTMLTrackElement.prototype) { + if (prop !== 'constructor') { + trackElement[prop] = HTMLTrackElement.prototype[prop]; + } + } + } + + var track = new TextTrack(options); + + trackElement.kind = track.kind; + trackElement.src = track.src; + trackElement.srclang = track.language; + trackElement.label = track.label; + trackElement['default'] = track['default']; + + /** + * @memberof HTMLTrackElement + * @member {HTMLTrackElement~ReadyState} readyState + * The current ready state of the track element. + * @instance + */ + Object.defineProperty(trackElement, 'readyState', { + get: function get$$1() { + return readyState; + } + }); + + /** + * @memberof HTMLTrackElement + * @member {TextTrack} track + * The underlying TextTrack object. + * @instance + * + */ + Object.defineProperty(trackElement, 'track', { + get: function get$$1() { + return track; + } + }); + + readyState = NONE; + + /** + * @listens TextTrack#loadeddata + * @fires HTMLTrackElement#load + */ + track.addEventListener('loadeddata', function () { + readyState = LOADED; + + trackElement.trigger({ + type: 'load', + target: trackElement + }); + }); + + if (IS_IE8) { + var _ret; + + return _ret = trackElement, possibleConstructorReturn(_this, _ret); + } + return _this; + } + + return HTMLTrackElement; + }(EventTarget); + + HTMLTrackElement.prototype.allowedEvents_ = { + load: 'load' + }; + + HTMLTrackElement.NONE = NONE; + HTMLTrackElement.LOADING = LOADING; + HTMLTrackElement.LOADED = LOADED; + HTMLTrackElement.ERROR = ERROR; + + /* + * This file contains all track properties that are used in + * player.js, tech.js, html5.js and possibly other techs in the future. + */ + + var 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_'; + }); + + var REMOTE = { + remoteText: { + ListClass: TextTrackList, + TrackClass: TextTrack, + capitalName: 'RemoteText', + getterName: 'remoteTextTracks', + privateName: 'remoteTextTracks_' + }, + remoteTextEl: { + ListClass: HtmlTrackElementList, + TrackClass: HTMLTrackElement, + capitalName: 'RemoteTextTrackEls', + getterName: 'remoteTextTrackEls', + privateName: 'remoteTextTrackEls_' + } + }; + + var ALL = mergeOptions(NORMAL, REMOTE); + + REMOTE.names = Object.keys(REMOTE); + NORMAL.names = Object.keys(NORMAL); + ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); + + /** + * 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{2})(:\d{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) { + var m; + if ((m = 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]; + var v = kv[1]; + 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", "middle", "end"]); + } + break; + case "position": + vals = v.split(","); + settings.percent(k, vals[0]); + if (vals.length === 2) { + settings.alt("positionAlign", vals[1], ["start", "middle", "end"]); + } + break; + case "size": + settings.percent(k, v); + break; + case "align": + settings.alt(k, v, ["start", "middle", "end", "left", "right"]); + break; + } + }, /:/, /\s/); + + // Apply default values for any missing fields. + cue.region = settings.get("region", null); + cue.vertical = settings.get("vertical", ""); + cue.line = settings.get("line", "auto"); + cue.lineAlign = settings.get("lineAlign", "start"); + cue.snapToLines = settings.get("snapToLines", true); + cue.size = settings.get("size", 100); + cue.align = settings.get("align", "middle"); + cue.position = settings.get("position", { + start: 0, + left: 0, + middle: 50, + end: 100, + right: 100 + }, cue.align); + cue.positionAlign = settings.get("positionAlign", { + start: "start", + left: "start", + middle: "middle", + 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); + } + + var ESCAPE = { + "&": "&", + "<": "<", + ">": ">", + "‎": "\u200e", + "‏": "\u200f", + " ": "\u00a0" + }; + + var TAG_NAME = { + c: "span", + i: "i", + b: "b", + u: "u", + ruby: "ruby", + rt: "rt", + v: "span", + lang: "span" + }; + + 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]); + } + + // Unescape a string 's'. + function unescape1(e) { + return ESCAPE[e]; + } + function unescape(s) { + while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) { + s = s.replace(m[0], unescape1); + } + 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); + element.localName = 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]) { + node.className = m[2].substr(1).replace('.', ' '); + } + // 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) { + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + var color = "rgba(255, 255, 255, 1)"; + var backgroundColor = "rgba(0, 0, 0, 0.8)"; + + if (isIE8) { + color = "rgb(255, 255, 255)"; + backgroundColor = "rgb(0, 0, 0)"; + } + + 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: color, + backgroundColor: backgroundColor, + position: "relative", + left: 0, + right: 0, + top: 0, + bottom: 0, + display: "inline" + }; + + if (!isIE8) { + styles.writingMode = cue.vertical === "" ? "horizontal-tb" + : cue.vertical === "lr" ? "vertical-lr" + : "vertical-rl"; + styles.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" which is "center" in CSS. + this.div = window.document.createElement("div"); + styles = { + textAlign: cue.align === "middle" ? "center" : cue.align, + font: styleOptions.font, + whiteSpace: "pre-line", + position: "absolute" + }; + + if (!isIE8) { + styles.direction = determineBidi(this.cueDiv); + styles.writingMode = cue.vertical === "" ? "horizontal-tb" + : cue.vertical === "lr" ? "vertical-lr" + : "vertical-rl". + stylesunicodeBidi = "plaintext"; + } + + 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": + textPos = cue.position; + break; + case "middle": + textPos = cue.position - (cue.size / 2); + break; + case "end": + 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) { + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + + // 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; + + if (isIE8 && !this.lineHeight) { + this.lineHeight = 13; + } + } + +// 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 "middle": + 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, ""); + 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; + 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$1 = 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 = { + "": true, + "lr": true, + "rl": true + }; + var alignSetting = { + "start": true, + "middle": true, + "end": true, + "left": true, + "right": true + }; + + 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 extend$1(obj) { + var i = 1; + for (; i < arguments.length; i++) { + var cobj = arguments[i]; + for (var p in cobj) { + obj[p] = cobj[p]; + } + } + + return obj; + } + + function VTTCue(startTime, endTime, text) { + var cue = this; + var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent); + var baseObj = {}; + + if (isIE8) { + cue = document.createElement('custom'); + } else { + baseObj.enumerable = true; + } + + /** + * 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. + cue.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 = 50; + var _positionAlign = "middle"; + var _size = 50; + var _align = "middle"; + + Object.defineProperty(cue, + "id", extend$1({}, baseObj, { + get: function() { + return _id; + }, + set: function(value) { + _id = "" + value; + } + })); + + Object.defineProperty(cue, + "pauseOnExit", extend$1({}, baseObj, { + get: function() { + return _pauseOnExit; + }, + set: function(value) { + _pauseOnExit = !!value; + } + })); + + Object.defineProperty(cue, + "startTime", extend$1({}, baseObj, { + 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; + } + })); + + Object.defineProperty(cue, + "endTime", extend$1({}, baseObj, { + 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; + } + })); + + Object.defineProperty(cue, + "text", extend$1({}, baseObj, { + get: function() { + return _text; + }, + set: function(value) { + _text = "" + value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "region", extend$1({}, baseObj, { + get: function() { + return _region; + }, + set: function(value) { + _region = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "vertical", extend$1({}, baseObj, { + 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("An invalid or illegal string was specified."); + } + _vertical = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "snapToLines", extend$1({}, baseObj, { + get: function() { + return _snapToLines; + }, + set: function(value) { + _snapToLines = !!value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "line", extend$1({}, baseObj, { + get: function() { + return _line; + }, + set: function(value) { + if (typeof value !== "number" && value !== autoKeyword) { + throw new SyntaxError("An invalid number or illegal string was specified."); + } + _line = value; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "lineAlign", extend$1({}, baseObj, { + get: function() { + return _lineAlign; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _lineAlign = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "position", extend$1({}, baseObj, { + 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; + } + })); + + Object.defineProperty(cue, + "positionAlign", extend$1({}, baseObj, { + get: function() { + return _positionAlign; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _positionAlign = setting; + this.hasBeenReset = true; + } + })); + + Object.defineProperty(cue, + "size", extend$1({}, baseObj, { + 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; + } + })); + + Object.defineProperty(cue, + "align", extend$1({}, baseObj, { + get: function() { + return _align; + }, + set: function(value) { + var setting = findAlignSetting(value); + if (!setting) { + throw new SyntaxError("An invalid or illegal 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 + cue.displayState = undefined; + + if (isIE8) { + return cue; + } + } + + /** + * 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) { + throw new SyntaxError("An invalid or illegal string was specified."); + } + _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$1, + 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(); + } + }); + + /** + * @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} Tech~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) { + var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; + + var tracks = self.textTracks(); + + options.kind = kind; + + if (label) { + options.label = label; + } + if (language) { + options.language = language; + } + options.tech = self; + + var track = new ALL.text.TrackClass(options); + + tracks.addTrack(track); + + return track; + } + + /** + * This is the base class for media playback technology controllers, such as + * {@link Flash} and {@link HTML5} + * + * @extends Component + */ + + var Tech = function (_Component) { + inherits(Tech, _Component); + + /** + * Create an instance of this Tech. + * + * @param {Object} [options] + * The key/value store of player options. + * + * @param {Component~ReadyCallback} ready + * Callback function to call when the `HTML5` Tech is ready. + */ + function Tech() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var ready = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; + classCallCheck(this, Tech); + + // we don't want the tech to report user activity automatically. + // This is done manually in addControlsListeners + options.reportTouchActivity = false; + + // keep track of whether the current source has played at all to + // implement a very limited played() + var _this = possibleConstructorReturn(this, _Component.call(this, null, options, ready)); + + _this.hasStarted_ = false; + _this.on('playing', function () { + this.hasStarted_ = true; + }); + _this.on('loadstart', function () { + this.hasStarted_ = false; + }); + + ALL.names.forEach(function (name) { + var props = ALL[name]; + + if (options && options[props.getterName]) { + _this[props.privateName] = options[props.getterName]; + } + }); + + // Manually track progress in cases where the browser/flash player doesn't report it. + if (!_this.featuresProgressEvents) { + _this.manualProgressOn(); + } + + // Manually track timeupdates in cases where the browser/flash player doesn't report it. + if (!_this.featuresTimeupdateEvents) { + _this.manualTimeUpdatesOn(); + } + + ['Text', 'Audio', 'Video'].forEach(function (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.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'; + } + return _this; + } + + /* Fallbacks for unsupported event types + ================================================================================ */ + + /** + * Polyfill the `progress` event for browsers that don't support it natively. + * + * @see {@link Tech#trackProgress} + */ + + + Tech.prototype.manualProgressOn = function 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} + */ + + + Tech.prototype.manualProgressOff = function 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 {EventTarget~Event} event + * The `ready` event that caused this to run. + * + * @listens Tech#ready + * @fires Tech#progress + */ + + + Tech.prototype.trackProgress = function trackProgress(event) { + this.stopTrackingProgress(); + this.progressInterval = this.setInterval(bind(this, function () { + // Don't trigger unless buffered amount is greater than last time + + var numBufferedPercent = this.bufferedPercent(); + + if (this.bufferedPercent_ !== numBufferedPercent) { + /** + * See {@link Player#progress} + * + * @event Tech#progress + * @type {EventTarget~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 {EventTarget~Event} event + * The `durationchange` event that caused this to run. + * + * @listens Tech#durationchange + */ + + + Tech.prototype.onDurationChange = function onDurationChange(event) { + this.duration_ = this.duration(); + }; + + /** + * Get and create a `TimeRange` object for buffering. + * + * @return {TimeRange} + * The time range object that was created. + */ + + + Tech.prototype.buffered = function 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. + * + */ + + + Tech.prototype.bufferedPercent = function bufferedPercent$$1() { + 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}. + */ + + + Tech.prototype.stopTrackingProgress = function stopTrackingProgress() { + this.clearInterval(this.progressInterval); + }; + + /** + * Polyfill the `timeupdate` event for browsers that don't support it. + * + * @see {@link Tech#trackCurrentTime} + */ + + + Tech.prototype.manualTimeUpdatesOn = function 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} + */ + + + Tech.prototype.manualTimeUpdatesOff = function 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 + */ + + + Tech.prototype.trackCurrentTime = function 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 {EventTarget~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} + */ + + + Tech.prototype.stopTrackingCurrentTime = function 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 + */ + + + Tech.prototype.dispose = function 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(); + } + + _Component.prototype.dispose.call(this); + }; + + /** + * 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`. + */ + + + Tech.prototype.clearTracks = function clearTracks(types) { + var _this2 = this; + + types = [].concat(types); + // clear out all tracks because we can't reuse them between techs + types.forEach(function (type) { + var list = _this2[type + 'Tracks']() || []; + var i = list.length; + + while (i--) { + var track = list[i]; + + if (type === 'text') { + _this2.removeRemoteTextTrack(track); + } + list.removeTrack(track); + } + }); + }; + + /** + * Remove any TextTracks added via addRemoteTextTrack that are + * flagged for automatic garbage collection + */ + + + Tech.prototype.cleanupAutoTextTracks = function cleanupAutoTextTracks() { + var list = this.autoRemoteTextTracks_ || []; + var i = list.length; + + while (i--) { + var track = list[i]; + + this.removeRemoteTextTrack(track); + } + }; + + /** + * Reset the tech, which will removes all sources and reset the internal readyState. + * + * @abstract + */ + + + Tech.prototype.reset = function reset() {}; + + /** + * 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. + */ + + + Tech.prototype.error = function 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 wether the source has played at all or not. + * + * @return {TimeRange} + * - A single time range if this video has played + * - An empty set of ranges if not. + */ + + + Tech.prototype.played = function played() { + if (this.hasStarted_) { + return createTimeRanges(0, 0); + } + return createTimeRanges(); + }; + + /** + * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was + * previously called. + * + * @fires Tech#timeupdate + */ + + + Tech.prototype.setCurrentTime = function setCurrentTime() { + // improve the accuracy of manual timeupdates + if (this.manualTimeUpdates) { + /** + * A manual `timeupdate` event. + * + * @event Tech#timeupdate + * @type {EventTarget~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 + */ + + + Tech.prototype.initTrackListeners = function initTrackListeners() { + var _this3 = this; + + /** + * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} + * + * @event Tech#audiotrackchange + * @type {EventTarget~Event} + */ + + /** + * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} + * + * @event Tech#videotrackchange + * @type {EventTarget~Event} + */ + + /** + * Triggered when tracks are added or removed on the Tech {@link TextTrackList} + * + * @event Tech#texttrackchange + * @type {EventTarget~Event} + */ + NORMAL.names.forEach(function (name) { + var props = NORMAL[name]; + var trackListChanges = function trackListChanges() { + _this3.trigger(name + 'trackchange'); + }; + + var tracks = _this3[props.getterName](); + + tracks.addEventListener('removetrack', trackListChanges); + tracks.addEventListener('addtrack', trackListChanges); + + _this3.on('dispose', function () { + tracks.removeEventListener('removetrack', trackListChanges); + tracks.removeEventListener('addtrack', trackListChanges); + }); + }); + }; + + /** + * Emulate TextTracks using vtt.js if necessary + * + * @fires Tech#vttjsloaded + * @fires Tech#vttjserror + */ + + + Tech.prototype.addWebVttScript_ = function addWebVttScript_() { + var _this4 = this; + + if (window_1.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_1.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 + var script = document_1.createElement('script'); + + script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.12.4/vtt.min.js'; + script.onload = function () { + /** + * Fired when vtt.js is loaded. + * + * @event Tech#vttjsloaded + * @type {EventTarget~Event} + */ + _this4.trigger('vttjsloaded'); + }; + script.onerror = function () { + /** + * Fired when vtt.js was not loaded due to an error + * + * @event Tech#vttjsloaded + * @type {EventTarget~Event} + */ + _this4.trigger('vttjserror'); + }; + this.on('dispose', function () { + 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_1.WebVTT = true; + this.el().parentNode.appendChild(script); + } else { + this.ready(this.addWebVttScript_); + } + }; + + /** + * Emulate texttracks + * + */ + + + Tech.prototype.emulateTextTracks = function emulateTextTracks() { + var _this5 = this; + + var tracks = this.textTracks(); + var remoteTracks = this.remoteTextTracks(); + var handleAddTrack = function handleAddTrack(e) { + return tracks.addTrack(e.track); + }; + var handleRemoveTrack = function handleRemoveTrack(e) { + return tracks.removeTrack(e.track); + }; + + remoteTracks.on('addtrack', handleAddTrack); + remoteTracks.on('removetrack', handleRemoveTrack); + + this.addWebVttScript_(); + + var updateDisplay = function updateDisplay() { + return _this5.trigger('texttrackchange'); + }; + + var textTracksChanges = function textTracksChanges() { + updateDisplay(); + + for (var i = 0; i < tracks.length; i++) { + var 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 (var i = 0; i < tracks.length; i++) { + var 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. + */ + + + Tech.prototype.addTextTrack = function 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. + */ + + + Tech.prototype.createRemoteTextTrack = function createRemoteTextTrack(options) { + var track = mergeOptions(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=true] + * - 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. + * + * @deprecated The default functionality for this function will be equivalent + * to "manualCleanup=false" in the future. The manualCleanup parameter will + * also be removed. + */ + + + Tech.prototype.addRemoteTextTrack = function addRemoteTextTrack() { + var _this6 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var manualCleanup = arguments[1]; + + var htmlTrackElement = this.createRemoteTextTrack(options); + + if (manualCleanup !== true && manualCleanup !== false) { + // deprecation warning + log$1.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js'); + manualCleanup = true; + } + + // store HTMLTrackElement and TextTrack to remote list + this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); + this.remoteTextTracks().addTrack(htmlTrackElement.track); + + if (manualCleanup !== true) { + // create the TextTrackList if it doesn't exist + this.ready(function () { + return _this6.autoRemoteTextTracks_.addTrack(htmlTrackElement.track); + }); + } + + return htmlTrackElement; + }; + + /** + * Remove a remote text track from the remote `TextTrackList`. + * + * @param {TextTrack} track + * `TextTrack` to remove from the `TextTrackList` + */ + + + Tech.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { + var 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 + */ + + + Tech.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { + return {}; + }; + + /** + * A method to set a poster from a `Tech`. + * + * @abstract + */ + + + Tech.prototype.setPoster = function setPoster() {}; + + /** + * A method to check for the presence of the 'playsinine'

/
' + }); + }; + + return TimeDivider; + }(Component); + + Component.registerComponent('TimeDivider', TimeDivider); + + /** + * @file remaining-time-display.js + */ + /** + * Displays the time left in the video + * + * @extends Component + */ + + var RemainingTimeDisplay = function (_TimeDisplay) { + inherits(RemainingTimeDisplay, _TimeDisplay); + + /** + * 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. + */ + function RemainingTimeDisplay(player, options) { + classCallCheck(this, RemainingTimeDisplay); + + var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); + + _this.on(player, 'durationchange', _this.throttledUpdateContent); + _this.on(player, 'ended', _this.handleEnded); + return _this; + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + RemainingTimeDisplay.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-remaining-time'; + }; + + /** + * The remaining time display prefixes numbers with a "minus" character. + * + * @param {number} time + * A numeric time, in seconds. + * + * @return {string} + * A formatted time + * + * @private + */ + + + RemainingTimeDisplay.prototype.formatTime_ = function formatTime_(time) { + return '-' + _TimeDisplay.prototype.formatTime_.call(this, time); + }; + + /** + * Update remaining time display. + * + * @param {EventTarget~Event} [event] + * The `timeupdate` or `durationchange` event that caused this to run. + * + * @listens Player#timeupdate + * @listens Player#durationchange + */ + + + RemainingTimeDisplay.prototype.updateContent = function updateContent(event) { + if (!this.player_.duration()) { + return; + } + + // @deprecated We should only use remainingTimeDisplay + // as of video.js 7 + if (this.player_.remainingTimeDisplay) { + this.updateFormattedTime_(this.player_.remainingTimeDisplay()); + } else { + this.updateFormattedTime_(this.player_.remainingTime()); + } + }; + + /** + * When the player fires ended there should be no time left. Sadly + * this is not always the case, lets make it seem like that is the case + * for users. + * + * @param {EventTarget~Event} [event] + * The `ended` event that caused this to run. + * + * @listens Player#ended + */ + + + RemainingTimeDisplay.prototype.handleEnded = function handleEnded(event) { + if (!this.player_.duration()) { + return; + } + this.updateFormattedTime_(0); + }; + + return RemainingTimeDisplay; + }(TimeDisplay); + + /** + * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization. + * + * @type {string} + * @private + */ + + + 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 + */ + + var LiveDisplay = function (_Component) { + inherits(LiveDisplay, _Component); + + /** + * 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. + */ + function LiveDisplay(player, options) { + classCallCheck(this, LiveDisplay); + + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + _this.updateShowing(); + _this.on(_this.player(), 'durationchange', _this.updateShowing); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + LiveDisplay.prototype.createEl = function createEl$$1() { + var el = _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-live-control vjs-control' + }); + + this.contentEl_ = createEl('div', { + className: 'vjs-live-display', + innerHTML: '' + this.localize('Stream Type') + '' + this.localize('LIVE') + }, { + 'aria-live': 'off' + }); + + el.appendChild(this.contentEl_); + return el; + }; + + LiveDisplay.prototype.dispose = function dispose() { + this.contentEl_ = null; + + _Component.prototype.dispose.call(this); + }; + + /** + * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide + * it accordingly + * + * @param {EventTarget~Event} [event] + * The {@link Player#durationchange} event that caused this function to run. + * + * @listens Player#durationchange + */ + + + LiveDisplay.prototype.updateShowing = function updateShowing(event) { + if (this.player().duration() === Infinity) { + this.show(); + } else { + this.hide(); + } + }; + + return LiveDisplay; + }(Component); + + Component.registerComponent('LiveDisplay', LiveDisplay); + + /** + * @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 + */ + + var Slider = function (_Component) { + inherits(Slider, _Component); + + /** + * Create 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. + */ + function Slider(player, options) { + classCallCheck(this, Slider); + + // Set property names to bar to match with the child Slider class is looking for + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + _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(); + return _this; + } + + /** + * Are controls are currently enabled for this slider or not. + * + * @return {boolean} + * true if controls are enabled, false otherwise + */ + + + Slider.prototype.enabled = function enabled() { + return this.enabled_; + }; + + /** + * Enable controls for this slider if they are disabled + */ + + + Slider.prototype.enable = function enable() { + if (this.enabled()) { + return; + } + + this.on('mousedown', this.handleMouseDown); + this.on('touchstart', this.handleMouseDown); + this.on('focus', this.handleFocus); + this.on('blur', this.handleBlur); + this.on('click', this.handleClick); + + 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 + */ + + + Slider.prototype.disable = function disable() { + if (!this.enabled()) { + return; + } + var doc = this.bar.el_.ownerDocument; + + this.off('mousedown', this.handleMouseDown); + this.off('touchstart', this.handleMouseDown); + this.off('focus', this.handleFocus); + this.off('blur', this.handleBlur); + 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 `Button`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. + */ + + + Slider.prototype.createEl = function createEl$$1(type) { + var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + // Add the slider element class to all sub classes + props.className = props.className + ' vjs-slider'; + props = assign({ + tabIndex: 0 + }, props); + + attributes = assign({ + 'role': 'slider', + 'aria-valuenow': 0, + 'aria-valuemin': 0, + 'aria-valuemax': 100, + 'tabIndex': 0 + }, attributes); + + return _Component.prototype.createEl.call(this, type, props, attributes); + }; + + /** + * Handle `mousedown` or `touchstart` events on the `Slider`. + * + * @param {EventTarget~Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + * @fires Slider#slideractive + */ + + + Slider.prototype.handleMouseDown = function handleMouseDown(event) { + var doc = this.bar.el_.ownerDocument; + + event.preventDefault(); + blockTextSelection(); + + this.addClass('vjs-sliding'); + /** + * Triggered when the slider is in an active state + * + * @event Slider#slideractive + * @type {EventTarget~Event} + */ + 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); + }; + + /** + * 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 {EventTarget~Event} event + * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered + * this function + * + * @listens mousemove + * @listens touchmove + */ + + + Slider.prototype.handleMouseMove = function handleMouseMove(event) {}; + + /** + * Handle `mouseup` or `touchend` events on the `Slider`. + * + * @param {EventTarget~Event} event + * `mouseup` or `touchend` event that triggered this function. + * + * @listens touchend + * @listens mouseup + * @fires Slider#sliderinactive + */ + + + Slider.prototype.handleMouseUp = function handleMouseUp() { + var 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 {EventTarget~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`. + * + * @returns {number} + * The percentage of progress the progress bar represents as a + * number from 0 to 1. + */ + + + Slider.prototype.update = function 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 (!this.el_) { + return; + } + + // If scrubbing, we could use a cached value to make the handle keep up + // with the user's mouse. On HTML5 browsers scrubbing is really smooth, but + // some flash players are slow, so we might want to utilize this later. + // var progress = (this.player_.scrubbing()) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); + var progress = this.getPercent(); + var bar = this.bar; + + // If there's no bar... + if (!bar) { + return; + } + + // Protect against no duration and other division issues + if (typeof progress !== 'number' || progress !== progress || progress < 0 || progress === Infinity) { + progress = 0; + } + + // Convert to a percentage for setting + var percentage = (progress * 100).toFixed(2) + '%'; + var style = bar.el().style; + + // Set the new bar width or height + if (this.vertical()) { + style.height = percentage; + } else { + style.width = percentage; + } + + return progress; + }; + + /** + * Calculate distance for slider + * + * @param {EventTarget~Event} event + * The event that caused this function to run. + * + * @return {number} + * The current position of the Slider. + * - postition.x for vertical `Slider`s + * - postition.y for horizontal `Slider`s + */ + + + Slider.prototype.calculateDistance = function calculateDistance(event) { + var position = getPointerPosition(this.el_, event); + + if (this.vertical()) { + return position.y; + } + return position.x; + }; + + /** + * Handle a `focus` event on this `Slider`. + * + * @param {EventTarget~Event} event + * The `focus` event that caused this function to run. + * + * @listens focus + */ + + + Slider.prototype.handleFocus = function handleFocus() { + this.on(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress); + }; + + /** + * Handle a `keydown` event on the `Slider`. Watches for left, rigth, 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 {EventTarget~Event} event + * the `keydown` event that caused this function to run. + * + * @listens keydown + */ + + + Slider.prototype.handleKeyPress = function handleKeyPress(event) { + // Left and Down Arrows + if (event.which === 37 || event.which === 40) { + event.preventDefault(); + this.stepBack(); + + // Up and Right Arrows + } else if (event.which === 38 || event.which === 39) { + event.preventDefault(); + this.stepForward(); + } + }; + + /** + * Handle a `blur` event on this `Slider`. + * + * @param {EventTarget~Event} event + * The `blur` event that caused this function to run. + * + * @listens blur + */ + + Slider.prototype.handleBlur = function handleBlur() { + this.off(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress); + }; + + /** + * 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 + */ + + + Slider.prototype.handleClick = function handleClick(event) { + event.stopImmediatePropagation(); + 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 + */ + + + Slider.prototype.vertical = function 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'); + } + }; + + return Slider; + }(Component); + + Component.registerComponent('Slider', Slider); + + /** + * @file load-progress-bar.js + */ + /** + * Shows loading progress + * + * @extends Component + */ + + var LoadProgressBar = function (_Component) { + inherits(LoadProgressBar, _Component); + + /** + * 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. + */ + function LoadProgressBar(player, options) { + classCallCheck(this, LoadProgressBar); + + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + _this.partEls_ = []; + _this.on(player, 'progress', _this.update); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + LoadProgressBar.prototype.createEl = function createEl$$1() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-load-progress', + innerHTML: '' + this.localize('Loaded') + ': 0%' + }); + }; + + LoadProgressBar.prototype.dispose = function dispose() { + this.partEls_ = null; + + _Component.prototype.dispose.call(this); + }; + + /** + * Update progress bar + * + * @param {EventTarget~Event} [event] + * The `progress` event that caused this function to run. + * + * @listens Player#progress + */ + + + LoadProgressBar.prototype.update = function update(event) { + var buffered = this.player_.buffered(); + var duration = this.player_.duration(); + var bufferedEnd = this.player_.bufferedEnd(); + var children = this.partEls_; + + // get the percent width of a time compared to the total end + var percentify = function percentify(time, end) { + // no NaN + var percent = time / end || 0; + + return (percent >= 1 ? 1 : percent) * 100 + '%'; + }; + + // update the width of the progress bar + this.el_.style.width = percentify(bufferedEnd, duration); + + // add child elements to represent the individual buffered time ranges + for (var i = 0; i < buffered.length; i++) { + var start = buffered.start(i); + var end = buffered.end(i); + var part = children[i]; + + if (!part) { + part = this.el_.appendChild(createEl()); + children[i] = part; + } + + // 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 (var _i = children.length; _i > buffered.length; _i--) { + this.el_.removeChild(children[_i - 1]); + } + children.length = buffered.length; + }; + + return LoadProgressBar; + }(Component); + + Component.registerComponent('LoadProgressBar', LoadProgressBar); + + /** + * @file time-tooltip.js + */ + /** + * Time tooltips display a time above the progress bar. + * + * @extends Component + */ + + var TimeTooltip = function (_Component) { + inherits(TimeTooltip, _Component); + + function TimeTooltip() { + classCallCheck(this, TimeTooltip); + return possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + /** + * Create the time tooltip DOM element + * + * @return {Element} + * The element that was created. + */ + TimeTooltip.prototype.createEl = function createEl$$1() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-time-tooltip' + }); + }; + + /** + * 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} + */ + + + TimeTooltip.prototype.update = function update(seekBarRect, seekBarPoint, content) { + var tooltipRect = getBoundingClientRect(this.el_); + var playerRect = getBoundingClientRect(this.player_.el()); + var 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` + var 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. + var spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right); + + // 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`. + var 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; + } + + this.el_.style.right = '-' + pullTooltipBy + 'px'; + textContent(this.el_, content); + }; + + return TimeTooltip; + }(Component); + + 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 + */ + + var PlayProgressBar = function (_Component) { + inherits(PlayProgressBar, _Component); + + function PlayProgressBar() { + classCallCheck(this, PlayProgressBar); + return possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + /** + * Create the the DOM element for this class. + * + * @return {Element} + * The element that was created. + */ + PlayProgressBar.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-play-progress vjs-slider-bar', + innerHTML: '' + this.localize('Progress') + ': 0%' + }); + }; + + /** + * 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} + */ + + + PlayProgressBar.prototype.update = function update(seekBarRect, seekBarPoint) { + var _this2 = this; + + // If there is an existing rAF ID, cancel it so we don't over-queue. + if (this.rafId_) { + this.cancelAnimationFrame(this.rafId_); + } + + this.rafId_ = this.requestAnimationFrame(function () { + var time = _this2.player_.scrubbing() ? _this2.player_.getCache().currentTime : _this2.player_.currentTime(); + + var content = formatTime(time, _this2.player_.duration()); + var timeTooltip = _this2.getChild('timeTooltip'); + + if (timeTooltip) { + timeTooltip.update(seekBarRect, seekBarPoint, content); + } + }); + }; + + return PlayProgressBar; + }(Component); + + /** + * Default options for {@link PlayProgressBar}. + * + * @type {Object} + * @private + */ + + + PlayProgressBar.prototype.options_ = { + children: [] + }; + +// Time tooltips should not be added to a player on mobile devices or IE8 + if ((!IE_VERSION || IE_VERSION > 8) && !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 + */ + + var MouseTimeDisplay = function (_Component) { + inherits(MouseTimeDisplay, _Component); + + /** + * Creates an instance of this class. + * + * @param {Player} player + * The {@link Player} that this class should be attached to. + * + * @param {Object} [options] + * The key/value store of player options. + */ + function MouseTimeDisplay(player, options) { + classCallCheck(this, MouseTimeDisplay); + + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + _this.update = throttle(bind(_this, _this.update), 25); + return _this; + } + + /** + * Create the DOM element for this class. + * + * @return {Element} + * The element that was created. + */ + + + MouseTimeDisplay.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, '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} + */ + + + MouseTimeDisplay.prototype.update = function update(seekBarRect, seekBarPoint) { + var _this2 = this; + + // If there is an existing rAF ID, cancel it so we don't over-queue. + if (this.rafId_) { + this.cancelAnimationFrame(this.rafId_); + } + + this.rafId_ = this.requestAnimationFrame(function () { + var duration = _this2.player_.duration(); + var content = formatTime(seekBarPoint * duration, duration); + + _this2.el_.style.left = seekBarRect.width * seekBarPoint + 'px'; + _this2.getChild('timeTooltip').update(seekBarRect, seekBarPoint, content); + }); + }; + + return MouseTimeDisplay; + }(Component); + + /** + * 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. + var STEP_SECONDS = 5; + +// The interval at which the bar should update as it progresses. + var UPDATE_REFRESH_INTERVAL = 30; + + /** + * Seek bar and container for the progress bars. Uses {@link PlayProgressBar} + * as its `bar`. + * + * @extends Slider + */ + + var SeekBar = function (_Slider) { + inherits(SeekBar, _Slider); + + /** + * 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. + */ + function SeekBar(player, options) { + classCallCheck(this, SeekBar); + + var _this = possibleConstructorReturn(this, _Slider.call(this, player, options)); + + _this.update = throttle(bind(_this, _this.update), UPDATE_REFRESH_INTERVAL); + + _this.on(player, 'timeupdate', _this.update); + _this.on(player, 'ended', _this.handleEnded); + + // when playing, let's ensure we smoothly update the play progress bar + // via an interval + _this.updateInterval = null; + + _this.on(player, ['playing'], function () { + _this.clearInterval(_this.updateInterval); + + _this.updateInterval = _this.setInterval(function () { + _this.requestAnimationFrame(function () { + _this.update(); + }); + }, UPDATE_REFRESH_INTERVAL); + }); + + _this.on(player, ['ended', 'pause', 'waiting'], function () { + _this.clearInterval(_this.updateInterval); + }); + + _this.on(player, ['timeupdate', 'ended'], _this.update); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + SeekBar.prototype.createEl = function createEl$$1() { + return _Slider.prototype.createEl.call(this, 'div', { + className: 'vjs-progress-holder' + }, { + 'aria-label': this.localize('Progress Bar') + }); + }; + + /** + * This function updates the play progress bar and accessiblity + * attributes to whatever is passed in. + * + * @param {number} currentTime + * The currentTime value that should be used for accessiblity + * + * @param {number} percent + * The percentage as a decimal that the bar should be filled from 0-1. + * + * @private + */ + + + SeekBar.prototype.update_ = function update_(currentTime, percent) { + var duration = this.player_.duration(); + + // machine readable value of progress bar (percentage complete) + this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); + + // 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}')); + + // Update the `PlayProgressBar`. + this.bar.update(getBoundingClientRect(this.el_), percent); + }; + + /** + * Update the seek bar's UI. + * + * @param {EventTarget~Event} [event] + * The `timeupdate` or `ended` event that caused this to run. + * + * @listens Player#timeupdate + * + * @returns {number} + * The current percent at a number from 0-1 + */ + + + SeekBar.prototype.update = function update(event) { + var percent = _Slider.prototype.update.call(this); + + this.update_(this.getCurrentTime_(), percent); + return percent; + }; + + /** + * 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 + */ + + + SeekBar.prototype.getCurrentTime_ = function getCurrentTime_() { + return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); + }; + + /** + * We want the seek bar to be full on ended + * no matter what the actual internal values are. so we force it. + * + * @param {EventTarget~Event} [event] + * The `timeupdate` or `ended` event that caused this to run. + * + * @listens Player#ended + */ + + + SeekBar.prototype.handleEnded = function handleEnded(event) { + this.update_(this.player_.duration(), 1); + }; + + /** + * Get the percentage of media played so far. + * + * @return {number} + * The percentage of media played so far (0 to 1). + */ + + + SeekBar.prototype.getPercent = function getPercent() { + var percent = this.getCurrentTime_() / this.player_.duration(); + + return percent >= 1 ? 1 : percent; + }; + + /** + * Handle mouse down on seek bar + * + * @param {EventTarget~Event} event + * The `mousedown` event that caused this to run. + * + * @listens mousedown + */ + + + SeekBar.prototype.handleMouseDown = function handleMouseDown(event) { + if (!isSingleLeftClick(event)) { + return; + } + + this.player_.scrubbing(true); + + this.videoWasPlaying = !this.player_.paused(); + this.player_.pause(); + + _Slider.prototype.handleMouseDown.call(this, event); + }; + + /** + * Handle mouse move on seek bar + * + * @param {EventTarget~Event} event + * The `mousemove` event that caused this to run. + * + * @listens mousemove + */ + + + SeekBar.prototype.handleMouseMove = function handleMouseMove(event) { + if (!isSingleLeftClick(event)) { + return; + } + + var newTime = this.calculateDistance(event) * this.player_.duration(); + + // Don't let video end while scrubbing. + if (newTime === this.player_.duration()) { + newTime = newTime - 0.1; + } + + // Set new time (tell player to seek to new time) + this.player_.currentTime(newTime); + }; + + SeekBar.prototype.enable = function enable() { + _Slider.prototype.enable.call(this); + var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); + + if (!mouseTimeDisplay) { + return; + } + + mouseTimeDisplay.show(); + }; + + SeekBar.prototype.disable = function disable() { + _Slider.prototype.disable.call(this); + var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); + + if (!mouseTimeDisplay) { + return; + } + + mouseTimeDisplay.hide(); + }; + + /** + * Handle mouse up on seek bar + * + * @param {EventTarget~Event} event + * The `mouseup` event that caused this to run. + * + * @listens mouseup + */ + + + SeekBar.prototype.handleMouseUp = function handleMouseUp(event) { + _Slider.prototype.handleMouseUp.call(this, event); + + 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 {EventTarget~Event} + */ + this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); + if (this.videoWasPlaying) { + silencePromise(this.player_.play()); + } + }; + + /** + * Move more quickly fast forward for keyboard-only users + */ + + + SeekBar.prototype.stepForward = function stepForward() { + this.player_.currentTime(this.player_.currentTime() + STEP_SECONDS); + }; + + /** + * Move more quickly rewind for keyboard-only users + */ + + + SeekBar.prototype.stepBack = function stepBack() { + this.player_.currentTime(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 {EventTarget~Event} event + * The `keydown` event that caused this function to be called + * + */ + + + SeekBar.prototype.handleAction = function 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. By + * default it will call `this.handleAction` when the key is space or enter. + * + * @param {EventTarget~Event} event + * The `keydown` event that caused this function to be called. + * + * @listens keydown + */ + + + SeekBar.prototype.handleKeyPress = function handleKeyPress(event) { + + // Support Space (32) or Enter (13) key operation to fire a click event + if (event.which === 32 || event.which === 13) { + event.preventDefault(); + this.handleAction(event); + } else if (_Slider.prototype.handleKeyPress) { + + // Pass keypress handling up for unsupported keys + _Slider.prototype.handleKeyPress.call(this, event); + } + }; + + return SeekBar; + }(Slider); + + /** + * 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 or IE8 + if ((!IE_VERSION || IE_VERSION > 8) && !IS_IOS && !IS_ANDROID) { + SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); + } + + /** + * Call the update event for this Slider when this event happens on the player. + * + * @type {string} + */ + SeekBar.prototype.playerEvent = 'timeupdate'; + + Component.registerComponent('SeekBar', SeekBar); + + /** + * @file progress-control.js + */ + /** + * The Progress Control component contains the seek bar, load progress, + * and play progress. + * + * @extends Component + */ + + var ProgressControl = function (_Component) { + inherits(ProgressControl, _Component); + + /** + * 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. + */ + function ProgressControl(player, options) { + classCallCheck(this, ProgressControl); + + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + _this.handleMouseMove = throttle(bind(_this, _this.handleMouseMove), 25); + _this.throttledHandleMouseSeek = throttle(bind(_this, _this.handleMouseSeek), 25); + + _this.enable(); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + ProgressControl.prototype.createEl = function createEl$$1() { + return _Component.prototype.createEl.call(this, '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 {EventTarget~Event} event + * The `mousemove` event that caused this function to run. + * + * @listen mousemove + */ + + + ProgressControl.prototype.handleMouseMove = function handleMouseMove(event) { + var seekBar = this.getChild('seekBar'); + var mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); + var seekBarEl = seekBar.el(); + var seekBarRect = getBoundingClientRect(seekBarEl); + var 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. + if (seekBarPoint > 1) { + seekBarPoint = 1; + } else if (seekBarPoint < 0) { + seekBarPoint = 0; + } + + if (mouseTimeDisplay) { + mouseTimeDisplay.update(seekBarRect, seekBarPoint); + } + }; + + /** + * A throttled version of the {@link ProgressControl#handleMouseSeek} listener. + * + * @method ProgressControl#throttledHandleMouseSeek + * @param {EventTarget~Event} event + * The `mousemove` event that caused this function to run. + * + * @listen mousemove + * @listen touchmove + */ + + /** + * Handle `mousemove` or `touchmove` events on the `ProgressControl`. + * + * @param {EventTarget~Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousemove + * @listens touchmove + */ + + + ProgressControl.prototype.handleMouseSeek = function handleMouseSeek(event) { + var seekBar = this.getChild('seekBar'); + + seekBar.handleMouseMove(event); + }; + + /** + * Are controls are currently enabled for this progress control. + * + * @return {boolean} + * true if controls are enabled, false otherwise + */ + + + ProgressControl.prototype.enabled = function enabled() { + return this.enabled_; + }; + + /** + * Disable all controls on the progress control and its children + */ + + + ProgressControl.prototype.disable = function disable() { + this.children().forEach(function (child) { + return child.disable && child.disable(); + }); + + if (!this.enabled()) { + return; + } + + this.off(['mousedown', 'touchstart'], this.handleMouseDown); + this.off(this.el_, 'mousemove', this.handleMouseMove); + this.handleMouseUp(); + + this.addClass('disabled'); + + this.enabled_ = false; + }; + + /** + * Enable all controls on the progress control and its children + */ + + + ProgressControl.prototype.enable = function enable() { + this.children().forEach(function (child) { + return child.enable && child.enable(); + }); + + if (this.enabled()) { + return; + } + + this.on(['mousedown', 'touchstart'], this.handleMouseDown); + this.on(this.el_, 'mousemove', this.handleMouseMove); + this.removeClass('disabled'); + + this.enabled_ = true; + }; + + /** + * Handle `mousedown` or `touchstart` events on the `ProgressControl`. + * + * @param {EventTarget~Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + */ + + + ProgressControl.prototype.handleMouseDown = function handleMouseDown(event) { + var doc = this.el_.ownerDocument; + + this.on(doc, 'mousemove', this.throttledHandleMouseSeek); + this.on(doc, 'touchmove', this.throttledHandleMouseSeek); + this.on(doc, 'mouseup', this.handleMouseUp); + this.on(doc, 'touchend', this.handleMouseUp); + }; + + /** + * Handle `mouseup` or `touchend` events on the `ProgressControl`. + * + * @param {EventTarget~Event} event + * `mouseup` or `touchend` event that triggered this function. + * + * @listens touchend + * @listens mouseup + */ + + + ProgressControl.prototype.handleMouseUp = function handleMouseUp(event) { + var doc = this.el_.ownerDocument; + + this.off(doc, 'mousemove', this.throttledHandleMouseSeek); + this.off(doc, 'touchmove', this.throttledHandleMouseSeek); + this.off(doc, 'mouseup', this.handleMouseUp); + this.off(doc, 'touchend', this.handleMouseUp); + }; + + return ProgressControl; + }(Component); + + /** + * Default options for `ProgressControl` + * + * @type {Object} + * @private + */ + + + ProgressControl.prototype.options_ = { + children: ['seekBar'] + }; + + Component.registerComponent('ProgressControl', ProgressControl); + + /** + * @file fullscreen-toggle.js + */ + /** + * Toggle fullscreen video + * + * @extends Button + */ + + var FullscreenToggle = function (_Button) { + inherits(FullscreenToggle, _Button); + + /** + * 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. + */ + function FullscreenToggle(player, options) { + classCallCheck(this, FullscreenToggle); + + var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); + + _this.on(player, 'fullscreenchange', _this.handleFullscreenChange); + return _this; + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + FullscreenToggle.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-fullscreen-control ' + _Button.prototype.buildCSSClass.call(this); + }; + + /** + * Handles fullscreenchange on the player and change control text accordingly. + * + * @param {EventTarget~Event} [event] + * The {@link Player#fullscreenchange} event that caused this function to be + * called. + * + * @listens Player#fullscreenchange + */ + + + FullscreenToggle.prototype.handleFullscreenChange = function handleFullscreenChange(event) { + if (this.player_.isFullscreen()) { + this.controlText('Non-Fullscreen'); + } else { + this.controlText('Fullscreen'); + } + }; + + /** + * This gets called when an `FullscreenToggle` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + FullscreenToggle.prototype.handleClick = function handleClick(event) { + if (!this.player_.isFullscreen()) { + this.player_.requestFullscreen(); + } else { + this.player_.exitFullscreen(); + } + }; + + return FullscreenToggle; + }(Button); + + /** + * The text that should display over the `FullscreenToggle`s controls. Added for localization. + * + * @type {string} + * @private + */ + + + 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 {Component} self + * The component that should be hidden if volume is unsupported + * + * @param {Player} player + * A reference to the player + * + * @private + */ + var checkVolumeSupport = function checkVolumeSupport(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 + */ + + var VolumeLevel = function (_Component) { + inherits(VolumeLevel, _Component); + + function VolumeLevel() { + classCallCheck(this, VolumeLevel); + return possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + VolumeLevel.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-level', + innerHTML: '' + }); + }; + + return VolumeLevel; + }(Component); + + Component.registerComponent('VolumeLevel', VolumeLevel); + + /** + * @file volume-bar.js + */ +// Required children + /** + * The bar that contains the volume level and can be clicked on to adjust the level + * + * @extends Slider + */ + + var VolumeBar = function (_Slider) { + inherits(VolumeBar, _Slider); + + /** + * 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. + */ + function VolumeBar(player, options) { + classCallCheck(this, VolumeBar); + + var _this = possibleConstructorReturn(this, _Slider.call(this, player, options)); + + _this.on('slideractive', _this.updateLastVolume_); + _this.on(player, 'volumechange', _this.updateARIAAttributes); + player.ready(function () { + return _this.updateARIAAttributes(); + }); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + VolumeBar.prototype.createEl = function createEl$$1() { + return _Slider.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-bar vjs-slider-bar' + }, { + 'aria-label': this.localize('Volume Level'), + 'aria-live': 'polite' + }); + }; + + /** + * Handle mouse down on volume bar + * + * @param {EventTarget~Event} event + * The `mousedown` event that caused this to run. + * + * @listens mousedown + */ + + + VolumeBar.prototype.handleMouseDown = function handleMouseDown(event) { + if (!isSingleLeftClick(event)) { + return; + } + + _Slider.prototype.handleMouseDown.call(this, event); + }; + + /** + * Handle movement events on the {@link VolumeMenuButton}. + * + * @param {EventTarget~Event} event + * The event that caused this function to run. + * + * @listens mousemove + */ + + + VolumeBar.prototype.handleMouseMove = function handleMouseMove(event) { + if (!isSingleLeftClick(event)) { + return; + } + + this.checkMuted(); + this.player_.volume(this.calculateDistance(event)); + }; + + /** + * If the player is muted unmute it. + */ + + + VolumeBar.prototype.checkMuted = function checkMuted() { + if (this.player_.muted()) { + this.player_.muted(false); + } + }; + + /** + * Get percent of volume level + * + * @return {number} + * Volume level percent as a decimal number. + */ + + + VolumeBar.prototype.getPercent = function getPercent() { + if (this.player_.muted()) { + return 0; + } + return this.player_.volume(); + }; + + /** + * Increase volume level for keyboard users + */ + + + VolumeBar.prototype.stepForward = function stepForward() { + this.checkMuted(); + this.player_.volume(this.player_.volume() + 0.1); + }; + + /** + * Decrease volume level for keyboard users + */ + + + VolumeBar.prototype.stepBack = function stepBack() { + this.checkMuted(); + this.player_.volume(this.player_.volume() - 0.1); + }; + + /** + * Update ARIA accessibility attributes + * + * @param {EventTarget~Event} [event] + * The `volumechange` event that caused this function to run. + * + * @listens Player#volumechange + */ + + + VolumeBar.prototype.updateARIAAttributes = function updateARIAAttributes(event) { + var 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 + */ + + + VolumeBar.prototype.volumeAsPercentage_ = function 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 + */ + + + VolumeBar.prototype.updateLastVolume_ = function updateLastVolume_() { + var _this2 = this; + + var volumeBeforeDrag = this.player_.volume(); + + this.one('sliderinactive', function () { + if (_this2.player_.volume() === 0) { + _this2.player_.lastVolume_(volumeBeforeDrag); + } + }); + }; + + return VolumeBar; + }(Slider); + + /** + * Default options for the `VolumeBar` + * + * @type {Object} + * @private + */ + + + VolumeBar.prototype.options_ = { + children: ['volumeLevel'], + barName: 'volumeLevel' + }; + + /** + * 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 + */ +// Required children + /** + * The component for controlling the volume level + * + * @extends Component + */ + + var VolumeControl = function (_Component) { + inherits(VolumeControl, _Component); + + /** + * 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. + */ + function VolumeControl(player) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, VolumeControl); + + 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; + } + + // hide this control if volume support is missing + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + checkVolumeSupport(_this, player); + + _this.throttledHandleMouseMove = throttle(bind(_this, _this.handleMouseMove), 25); + + _this.on('mousedown', _this.handleMouseDown); + _this.on('touchstart', _this.handleMouseDown); + + // 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'], function () { + _this.volumeBar.addClass('vjs-slider-active'); + _this.addClass('vjs-slider-active'); + _this.trigger('slideractive'); + }); + + _this.on(_this.volumeBar, ['blur', 'sliderinactive'], function () { + _this.volumeBar.removeClass('vjs-slider-active'); + _this.removeClass('vjs-slider-active'); + _this.trigger('sliderinactive'); + }); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + VolumeControl.prototype.createEl = function createEl() { + var orientationClass = 'vjs-volume-horizontal'; + + if (this.options_.vertical) { + orientationClass = 'vjs-volume-vertical'; + } + + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-control vjs-control ' + orientationClass + }); + }; + + /** + * Handle `mousedown` or `touchstart` events on the `VolumeControl`. + * + * @param {EventTarget~Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + */ + + + VolumeControl.prototype.handleMouseDown = function handleMouseDown(event) { + var doc = this.el_.ownerDocument; + + this.on(doc, 'mousemove', this.throttledHandleMouseMove); + this.on(doc, 'touchmove', this.throttledHandleMouseMove); + this.on(doc, 'mouseup', this.handleMouseUp); + this.on(doc, 'touchend', this.handleMouseUp); + }; + + /** + * Handle `mouseup` or `touchend` events on the `VolumeControl`. + * + * @param {EventTarget~Event} event + * `mouseup` or `touchend` event that triggered this function. + * + * @listens touchend + * @listens mouseup + */ + + + VolumeControl.prototype.handleMouseUp = function handleMouseUp(event) { + var doc = this.el_.ownerDocument; + + this.off(doc, 'mousemove', this.throttledHandleMouseMove); + this.off(doc, 'touchmove', this.throttledHandleMouseMove); + this.off(doc, 'mouseup', this.handleMouseUp); + this.off(doc, 'touchend', this.handleMouseUp); + }; + + /** + * Handle `mousedown` or `touchstart` events on the `VolumeControl`. + * + * @param {EventTarget~Event} event + * `mousedown` or `touchstart` event that triggered this function + * + * @listens mousedown + * @listens touchstart + */ + + + VolumeControl.prototype.handleMouseMove = function handleMouseMove(event) { + this.volumeBar.handleMouseMove(event); + }; + + return VolumeControl; + }(Component); + + /** + * Default options for the `VolumeControl` + * + * @type {Object} + * @private + */ + + + VolumeControl.prototype.options_ = { + children: ['volumeBar'] + }; + + Component.registerComponent('VolumeControl', VolumeControl); + + /** + * @file mute-toggle.js + */ + /** + * A button component for muting the audio. + * + * @extends Button + */ + + var MuteToggle = function (_Button) { + inherits(MuteToggle, _Button); + + /** + * 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. + */ + function MuteToggle(player, options) { + classCallCheck(this, MuteToggle); + + // hide this control if volume support is missing + var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); + + checkVolumeSupport(_this, player); + + _this.on(player, ['loadstart', 'volumechange'], _this.update); + return _this; + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + MuteToggle.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-mute-control ' + _Button.prototype.buildCSSClass.call(this); + }; + + /** + * This gets called when an `MuteToggle` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + MuteToggle.prototype.handleClick = function handleClick(event) { + var vol = this.player_.volume(); + var lastVolume = this.player_.lastVolume_(); + + if (vol === 0) { + var 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 {EventTarget~Event} [event] + * The {@link Player#loadstart} event if this function was called + * through an event. + * + * @listens Player#loadstart + * @listens Player#volumechange + */ + + + MuteToggle.prototype.update = function 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 + */ + + + MuteToggle.prototype.updateIcon_ = function updateIcon_() { + var vol = this.player_.volume(); + var level = 3; + + if (vol === 0 || this.player_.muted()) { + level = 0; + } else if (vol < 0.33) { + level = 1; + } else if (vol < 0.67) { + level = 2; + } + + // TODO improve muted icon classes + for (var i = 0; i < 4; i++) { + removeClass(this.el_, '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 + */ + + + MuteToggle.prototype.updateControlText_ = function updateControlText_() { + var soundOff = this.player_.muted() || this.player_.volume() === 0; + var text = soundOff ? 'Unmute' : 'Mute'; + + if (this.controlText() !== text) { + this.controlText(text); + } + }; + + return MuteToggle; + }(Button); + + /** + * The text that should display over the `MuteToggle`s controls. Added for localization. + * + * @type {string} + * @private + */ + + + MuteToggle.prototype.controlText_ = 'Mute'; + + Component.registerComponent('MuteToggle', MuteToggle); + + /** + * @file volume-control.js + */ +// Required children + /** + * A Component to contain the MuteToggle and VolumeControl so that + * they can work together. + * + * @extends Component + */ + + var VolumePanel = function (_Component) { + inherits(VolumePanel, _Component); + + /** + * 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. + */ + function VolumePanel(player) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, VolumePanel); + + 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; + } + + // hide this control if volume support is missing + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + checkVolumeSupport(_this, player); + + // 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.volumeControl, ['slideractive'], _this.sliderActive_); + _this.on(_this.muteToggle, 'focus', _this.sliderActive_); + + _this.on(_this.volumeControl, ['sliderinactive'], _this.sliderInactive_); + _this.on(_this.muteToggle, 'blur', _this.sliderInactive_); + return _this; + } + + /** + * Add vjs-slider-active class to the VolumePanel + * + * @listens VolumeControl#slideractive + * @private + */ + + + VolumePanel.prototype.sliderActive_ = function sliderActive_() { + this.addClass('vjs-slider-active'); + }; + + /** + * Removes vjs-slider-active class to the VolumePanel + * + * @listens VolumeControl#sliderinactive + * @private + */ + + + VolumePanel.prototype.sliderInactive_ = function sliderInactive_() { + this.removeClass('vjs-slider-active'); + }; + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + VolumePanel.prototype.createEl = function createEl() { + var orientationClass = 'vjs-volume-panel-horizontal'; + + if (!this.options_.inline) { + orientationClass = 'vjs-volume-panel-vertical'; + } + + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-volume-panel vjs-control ' + orientationClass + }); + }; + + return VolumePanel; + }(Component); + + /** + * Default options for the `VolumeControl` + * + * @type {Object} + * @private + */ + + + VolumePanel.prototype.options_ = { + children: ['muteToggle', 'volumeControl'] + }; + + Component.registerComponent('VolumePanel', VolumePanel); + + /** + * @file menu.js + */ + /** + * The Menu component is used to build popup menus, including subtitle and + * captions selection menus. + * + * @extends Component + */ + + var Menu = function (_Component) { + inherits(Menu, _Component); + + /** + * Create an instance of this class. + * + * @param {Player} player + * the player that this component should attach to + * + * @param {Object} [options] + * Object of option names and values + * + */ + function Menu(player, options) { + classCallCheck(this, Menu); + + var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); + + if (options) { + _this.menuButton_ = options.menuButton; + } + + _this.focusedChild_ = -1; + + _this.on('keydown', _this.handleKeyPress); + return _this; + } + + /** + * Add a {@link MenuItem} to the menu. + * + * @param {Object|string} component + * The name or instance of the `MenuItem` to add. + * + */ + + + Menu.prototype.addItem = function addItem(component) { + this.addChild(component); + component.on('click', bind(this, function (event) { + // Unpress the associated MenuButton, and move focus back to it + if (this.menuButton_) { + this.menuButton_.unpressButton(); + + // don't focus menu button if item is a caption settings item + // because focus will move elsewhere and it logs an error on IE8 + if (component.name() !== 'CaptionSettingsMenuItem') { + this.menuButton_.focus(); + } + } + })); + }; + + /** + * Create the `Menu`s DOM element. + * + * @return {Element} + * the element that was created + */ + + + Menu.prototype.createEl = function createEl$$1() { + var contentElType = this.options_.contentElType || 'ul'; + + this.contentEl_ = createEl(contentElType, { + className: 'vjs-menu-content' + }); + + this.contentEl_.setAttribute('role', 'menu'); + + var el = _Component.prototype.createEl.call(this, '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; + }; + + Menu.prototype.dispose = function dispose() { + this.contentEl_ = null; + + _Component.prototype.dispose.call(this); + }; + + /** + * Handle a `keydown` event on this menu. This listener is added in the constructor. + * + * @param {EventTarget~Event} event + * A `keydown` event that happened on the menu. + * + * @listens keydown + */ + + + Menu.prototype.handleKeyPress = function handleKeyPress(event) { + // Left and Down Arrows + if (event.which === 37 || event.which === 40) { + event.preventDefault(); + this.stepForward(); + + // Up and Right Arrows + } else if (event.which === 38 || event.which === 39) { + event.preventDefault(); + this.stepBack(); + } + }; + + /** + * Move to next (lower) menu item for keyboard users. + */ + + + Menu.prototype.stepForward = function stepForward() { + var stepChild = 0; + + if (this.focusedChild_ !== undefined) { + stepChild = this.focusedChild_ + 1; + } + this.focus(stepChild); + }; + + /** + * Move to previous (higher) menu item for keyboard users. + */ + + + Menu.prototype.stepBack = function stepBack() { + var 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. + */ + + + Menu.prototype.focus = function focus() { + var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + + var children = this.children().slice(); + var haveTitle = children.length && children[0].className && /vjs-menu-title/.test(children[0].className); + + 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(); + } + }; + + return Menu; + }(Component); + + Component.registerComponent('Menu', Menu); + + /** + * @file menu-button.js + */ + /** + * A `MenuButton` class for any popup {@link Menu}. + * + * @extends Component + */ + + var MenuButton = function (_Component) { + inherits(MenuButton, _Component); + + /** + * 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. + */ + function MenuButton(player) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, MenuButton); + + var _this = possibleConstructorReturn(this, _Component.call(this, 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 + var 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; + + _this.on(_this.menuButton_, 'tap', _this.handleClick); + _this.on(_this.menuButton_, 'click', _this.handleClick); + _this.on(_this.menuButton_, 'focus', _this.handleFocus); + _this.on(_this.menuButton_, 'blur', _this.handleBlur); + + _this.on('keydown', _this.handleSubmenuKeyPress); + return _this; + } + + /** + * Update the menu based on the current state of its items. + */ + + + MenuButton.prototype.update = function update() { + var 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(); + } else { + this.show(); + } + }; + + /** + * Create the menu and add all items to it. + * + * @return {Menu} + * The constructed menu + */ + + + MenuButton.prototype.createMenu = function createMenu() { + var 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) { + var title = createEl('li', { + className: 'vjs-menu-title', + innerHTML: toTitleCase(this.options_.title), + tabIndex: -1 + }); + + this.hideThreshold_ += 1; + + menu.children_.unshift(title); + prependTo(title, menu.contentEl()); + } + + this.items = this.createItems(); + + if (this.items) { + // Add menu items to the menu + for (var 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 + */ + + + MenuButton.prototype.createItems = function createItems() {}; + + /** + * Create the `MenuButtons`s DOM element. + * + * @return {Element} + * The element that gets created. + */ + + + MenuButton.prototype.createEl = function createEl$$1() { + return _Component.prototype.createEl.call(this, 'div', { + className: this.buildWrapperCSSClass() + }, {}); + }; + + /** + * Allow sub components to stack CSS class names for the wrapper element + * + * @return {string} + * The constructed wrapper DOM `className` + */ + + + MenuButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + var 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 + var buttonClass = Button.prototype.buildCSSClass(); + + return 'vjs-menu-button ' + menuButtonClass + ' ' + buttonClass + ' ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + MenuButton.prototype.buildCSSClass = function buildCSSClass() { + var 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 + ' ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * 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 + */ + + + MenuButton.prototype.controlText = function controlText(text) { + var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.menuButton_.el(); + + return this.menuButton_.controlText(text, el); + }; + + /** + * Handle a click on a `MenuButton`. + * See {@link ClickableComponent#handleClick} for instances where this is called. + * + * @param {EventTarget~Event} event + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + MenuButton.prototype.handleClick = function handleClick(event) { + // When you click the button it adds focus, which will show the menu. + // So we'll remove focus when the mouse leaves the button. Focus is needed + // for tab navigation. + + this.one(this.menu.contentEl(), 'mouseleave', bind(this, function (e) { + this.unpressButton(); + this.el_.blur(); + })); + if (this.buttonPressed_) { + this.unpressButton(); + } else { + this.pressButton(); + } + }; + + /** + * Set the focus to the actual button, not to this element + */ + + + MenuButton.prototype.focus = function focus() { + this.menuButton_.focus(); + }; + + /** + * Remove the focus from the actual button, not this element + */ + + + MenuButton.prototype.blur = function blur() { + this.menuButton_.blur(); + }; + + /** + * This gets called when a `MenuButton` gains focus via a `focus` event. + * Turns on listening for `keydown` events. When they happen it + * calls `this.handleKeyPress`. + * + * @param {EventTarget~Event} event + * The `focus` event that caused this function to be called. + * + * @listens focus + */ + + + MenuButton.prototype.handleFocus = function handleFocus() { + on(document_1, 'keydown', bind(this, this.handleKeyPress)); + }; + + /** + * Called when a `MenuButton` loses focus. Turns off the listener for + * `keydown` events. Which Stops `this.handleKeyPress` from getting called. + * + * @param {EventTarget~Event} event + * The `blur` event that caused this function to be called. + * + * @listens blur + */ + + + MenuButton.prototype.handleBlur = function handleBlur() { + off(document_1, 'keydown', bind(this, this.handleKeyPress)); + }; + + /** + * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See + * {@link ClickableComponent#handleKeyPress} for instances where this is called. + * + * @param {EventTarget~Event} event + * The `keydown` event that caused this function to be called. + * + * @listens keydown + */ + + + MenuButton.prototype.handleKeyPress = function handleKeyPress(event) { + + // Escape (27) key or Tab (9) key unpress the 'button' + if (event.which === 27 || event.which === 9) { + if (this.buttonPressed_) { + this.unpressButton(); + } + // Don't preventDefault for Tab key - we still want to lose focus + if (event.which !== 9) { + event.preventDefault(); + // Set focus back to the menu button's button + this.menuButton_.el_.focus(); + } + // Up (38) key or Down (40) key press the 'button' + } else if (event.which === 38 || event.which === 40) { + if (!this.buttonPressed_) { + this.pressButton(); + event.preventDefault(); + } + } + }; + + /** + * Handle a `keydown` event on a sub-menu. The listener for this is added in + * the constructor. + * + * @param {EventTarget~Event} event + * Key press event + * + * @listens keydown + */ + + + MenuButton.prototype.handleSubmenuKeyPress = function handleSubmenuKeyPress(event) { + + // Escape (27) key or Tab (9) key unpress the 'button' + if (event.which === 27 || event.which === 9) { + if (this.buttonPressed_) { + this.unpressButton(); + } + // Don't preventDefault for Tab key - we still want to lose focus + if (event.which !== 9) { + event.preventDefault(); + // Set focus back to the menu button's button + this.menuButton_.el_.focus(); + } + } + }; + + /** + * Put the current `MenuButton` into a pressed state. + */ + + + MenuButton.prototype.pressButton = function pressButton() { + if (this.enabled_) { + this.buttonPressed_ = true; + 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. + */ + + + MenuButton.prototype.unpressButton = function unpressButton() { + if (this.enabled_) { + this.buttonPressed_ = false; + this.menu.unlockShowing(); + this.menuButton_.el_.setAttribute('aria-expanded', 'false'); + } + }; + + /** + * Disable the `MenuButton`. Don't allow it to be clicked. + */ + + + MenuButton.prototype.disable = function disable() { + this.unpressButton(); + + this.enabled_ = false; + this.addClass('vjs-disabled'); + + this.menuButton_.disable(); + }; + + /** + * Enable the `MenuButton`. Allow it to be clicked. + */ + + + MenuButton.prototype.enable = function enable() { + this.enabled_ = true; + this.removeClass('vjs-disabled'); + + this.menuButton_.enable(); + }; + + return MenuButton; + }(Component); + + Component.registerComponent('MenuButton', MenuButton); + + /** + * @file track-button.js + */ + /** + * The base class for buttons that toggle specific track types (e.g. subtitles). + * + * @extends MenuButton + */ + + var TrackButton = function (_MenuButton) { + inherits(TrackButton, _MenuButton); + + /** + * 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. + */ + function TrackButton(player, options) { + classCallCheck(this, TrackButton); + + var tracks = options.tracks; + + var _this = possibleConstructorReturn(this, _MenuButton.call(this, player, options)); + + if (_this.items.length <= 1) { + _this.hide(); + } + + if (!tracks) { + return possibleConstructorReturn(_this); + } + + var updateHandler = bind(_this, _this.update); + + tracks.addEventListener('removetrack', updateHandler); + tracks.addEventListener('addtrack', updateHandler); + _this.player_.on('ready', updateHandler); + + _this.player_.on('dispose', function () { + tracks.removeEventListener('removetrack', updateHandler); + tracks.removeEventListener('addtrack', updateHandler); + }); + return _this; + } + + return TrackButton; + }(MenuButton); + + Component.registerComponent('TrackButton', TrackButton); + + /** + * @file menu-item.js + */ + /** + * The component for a menu item. `
  • ` + * + * @extends ClickableComponent + */ + + var MenuItem = function (_ClickableComponent) { + inherits(MenuItem, _ClickableComponent); + + /** + * Creates an instance of the this class. + * + * @param {Player} player + * The `Player` that this class should be attached to. + * + * @param {Object} [options={}] + * The key/value store of player options. + * + */ + function MenuItem(player, options) { + classCallCheck(this, MenuItem); + + var _this = possibleConstructorReturn(this, _ClickableComponent.call(this, player, options)); + + _this.selectable = options.selectable; + + _this.selected(options.selected); + + if (_this.selectable) { + // TODO: May need to be either menuitemcheckbox or menuitemradio, + // and may need logical grouping of menu items. + _this.el_.setAttribute('role', 'menuitemcheckbox'); + } else { + _this.el_.setAttribute('role', 'menuitem'); + } + return _this; + } + + /** + * 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. + */ + + + MenuItem.prototype.createEl = function createEl(type, props, attrs) { + // The control is textual, not just an icon + this.nonIconControl = true; + + return _ClickableComponent.prototype.createEl.call(this, 'li', assign({ + className: 'vjs-menu-item', + innerHTML: '' + this.localize(this.options_.label) + '', + tabIndex: -1 + }, props), attrs); + }; + + /** + * Any click on a `MenuItem` puts int into the selected state. + * See {@link ClickableComponent#handleClick} for instances where this is called. + * + * @param {EventTarget~Event} event + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + MenuItem.prototype.handleClick = function 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 + */ + + + MenuItem.prototype.selected = function 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'); + } else { + this.removeClass('vjs-selected'); + this.el_.setAttribute('aria-checked', 'false'); + // Indicate un-selected state to screen reader + this.controlText(''); + } + } + }; + + return MenuItem; + }(ClickableComponent); + + 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 + */ + + var TextTrackMenuItem = function (_MenuItem) { + inherits(TextTrackMenuItem, _MenuItem); + + /** + * 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. + */ + function TextTrackMenuItem(player, options) { + classCallCheck(this, TextTrackMenuItem); + + var track = options.track; + var tracks = player.textTracks(); + + // Modify options for parent MenuItem class's init. + options.label = track.label || track.language || 'Unknown'; + options.selected = track.mode === 'showing'; + + var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); + + _this.track = track; + var changeHandler = function changeHandler() { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + _this.handleTracksChange.apply(_this, args); + }; + var selectedLanguageChangeHandler = function selectedLanguageChangeHandler() { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + _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) { + var event = void 0; + + _this.on(['tap', 'click'], function () { + if (_typeof(window_1.Event) !== 'object') { + // Android 2.3 throws an Illegal Constructor error for window.Event + try { + event = new window_1.Event('change'); + } catch (err) { + // continue regardless of error + } + } + + if (!event) { + event = document_1.createEvent('Event'); + event.initEvent('change', true, true); + } + + tracks.dispatchEvent(event); + }); + } + + // set the default state based on current tracks + _this.handleTracksChange(); + return _this; + } + + /** + * This gets called when an `TextTrackMenuItem` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} event + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + TextTrackMenuItem.prototype.handleClick = function handleClick(event) { + var kind = this.track.kind; + var kinds = this.track.kinds; + var tracks = this.player_.textTracks(); + + if (!kinds) { + kinds = [kind]; + } + + _MenuItem.prototype.handleClick.call(this, event); + + if (!tracks) { + return; + } + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + if (track === this.track && kinds.indexOf(track.kind) > -1) { + if (track.mode !== 'showing') { + track.mode = 'showing'; + } + } else if (track.mode !== 'disabled') { + track.mode = 'disabled'; + } + } + }; + + /** + * Handle text track list change + * + * @param {EventTarget~Event} event + * The `change` event that caused this function to be called. + * + * @listens TextTrackList#change + */ + + + TextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { + this.selected(this.track.mode === 'showing'); + }; + + TextTrackMenuItem.prototype.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { + if (this.track.mode === 'showing') { + var 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 + }; + } + }; + + TextTrackMenuItem.prototype.dispose = function dispose() { + // remove reference to track object on dispose + this.track = null; + + _MenuItem.prototype.dispose.call(this); + }; + + return TextTrackMenuItem; + }(MenuItem); + + Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem); + + /** + * @file off-text-track-menu-item.js + */ + /** + * A special menu item for turning of a specific type of text track + * + * @extends TextTrackMenuItem + */ + + var OffTextTrackMenuItem = function (_TextTrackMenuItem) { + inherits(OffTextTrackMenuItem, _TextTrackMenuItem); + + /** + * 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. + */ + function OffTextTrackMenuItem(player, options) { + classCallCheck(this, OffTextTrackMenuItem); + + // Create pseudo track info + // Requires options['kind'] + options.track = { + player: player, + 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; + + return possibleConstructorReturn(this, _TextTrackMenuItem.call(this, player, options)); + } + + /** + * Handle text track change + * + * @param {EventTarget~Event} event + * The event that caused this function to run + */ + + + OffTextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { + var tracks = this.player().textTracks(); + var selected = true; + + for (var i = 0, l = tracks.length; i < l; i++) { + var track = tracks[i]; + + if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') { + selected = false; + break; + } + } + + this.selected(selected); + }; + + OffTextTrackMenuItem.prototype.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { + var tracks = this.player().textTracks(); + var allHidden = true; + + for (var i = 0, l = tracks.length; i < l; i++) { + var 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 + }; + } + }; + + return OffTextTrackMenuItem; + }(TextTrackMenuItem); + + 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 + */ + + var TextTrackButton = function (_TrackButton) { + inherits(TextTrackButton, _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. + */ + function TextTrackButton(player) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, TextTrackButton); + + options.tracks = player.textTracks(); + + return possibleConstructorReturn(this, _TrackButton.call(this, 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 + */ + + + TextTrackButton.prototype.createItems = function createItems() { + var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + var TrackMenuItem = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : TextTrackMenuItem; + + + // Label is an overide for the [track] off label + // USed to localise captions/subtitles + var label = void 0; + + 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: label + })); + + this.hideThreshold_ += 1; + + var tracks = this.player_.textTracks(); + + if (!Array.isArray(this.kinds_)) { + this.kinds_ = [this.kind_]; + } + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + // only add tracks that are of an appropriate kind and have a label + if (this.kinds_.indexOf(track.kind) > -1) { + + var item = new TrackMenuItem(this.player_, { + track: track, + // MenuItem is selectable + selectable: true + }); + + item.addClass('vjs-' + track.kind + '-menu-item'); + items.push(item); + } + } + + return items; + }; + + return TextTrackButton; + }(TrackButton); + + Component.registerComponent('TextTrackButton', TextTrackButton); + + /** + * @file chapters-track-menu-item.js + */ + /** + * The chapter track menu item + * + * @extends MenuItem + */ + + var ChaptersTrackMenuItem = function (_MenuItem) { + inherits(ChaptersTrackMenuItem, _MenuItem); + + /** + * 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. + */ + function ChaptersTrackMenuItem(player, options) { + classCallCheck(this, ChaptersTrackMenuItem); + + var track = options.track; + var cue = options.cue; + var currentTime = player.currentTime(); + + // Modify options for parent MenuItem class's init. + options.selectable = true; + options.label = cue.text; + options.selected = cue.startTime <= currentTime && currentTime < cue.endTime; + + var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); + + _this.track = track; + _this.cue = cue; + track.addEventListener('cuechange', bind(_this, _this.update)); + return _this; + } + + /** + * This gets called when an `ChaptersTrackMenuItem` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + ChaptersTrackMenuItem.prototype.handleClick = function handleClick(event) { + _MenuItem.prototype.handleClick.call(this); + this.player_.currentTime(this.cue.startTime); + this.update(this.cue.startTime); + }; + + /** + * Update chapter menu item + * + * @param {EventTarget~Event} [event] + * The `cuechange` event that caused this function to run. + * + * @listens TextTrack#cuechange + */ + + + ChaptersTrackMenuItem.prototype.update = function update(event) { + var cue = this.cue; + var currentTime = this.player_.currentTime(); + + // vjs.log(currentTime, cue.startTime); + this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); + }; + + return ChaptersTrackMenuItem; + }(MenuItem); + + 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 + */ + + var ChaptersButton = function (_TextTrackButton) { + inherits(ChaptersButton, _TextTrackButton); + + /** + * 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. + * + * @param {Component~ReadyCallback} [ready] + * The function to call when this function is ready. + */ + function ChaptersButton(player, options, ready) { + classCallCheck(this, ChaptersButton); + return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + ChaptersButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + ChaptersButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); + }; + + /** + * Update the menu based on the current state of its items. + * + * @param {EventTarget~Event} [event] + * An event that triggered this function to run. + * + * @listens TextTrackList#addtrack + * @listens TextTrackList#removetrack + * @listens TextTrackList#change + */ + + + ChaptersButton.prototype.update = function update(event) { + if (!this.track_ || event && (event.type === 'addtrack' || event.type === 'removetrack')) { + this.setTrack(this.findChaptersTrack()); + } + _TextTrackButton.prototype.update.call(this); + }; + + /** + * 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. + */ + + + ChaptersButton.prototype.setTrack = function 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_) { + var remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); + + if (remoteTextTrackEl) { + remoteTextTrackEl.removeEventListener('load', this.updateHandler_); + } + + this.track_ = null; + } + + this.track_ = track; + + // here this.track_ refers to the new track instance + if (this.track_) { + this.track_.mode = 'hidden'; + + var _remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); + + if (_remoteTextTrackEl) { + _remoteTextTrackEl.addEventListener('load', this.updateHandler_); + } + } + }; + + /** + * Find the track object that is currently in use by this ChaptersButton + * + * @return {TextTrack|undefined} + * The current track or undefined if none was found. + */ + + + ChaptersButton.prototype.findChaptersTrack = function findChaptersTrack() { + var tracks = this.player_.textTracks() || []; + + for (var i = tracks.length - 1; i >= 0; i--) { + // We will always choose the last track as our chaptersTrack + var 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. + */ + + + ChaptersButton.prototype.getMenuCaption = function getMenuCaption() { + if (this.track_ && this.track_.label) { + return this.track_.label; + } + return this.localize(toTitleCase(this.kind_)); + }; + + /** + * Create menu from chapter track + * + * @return {Menu} + * New menu for the chapter buttons + */ + + + ChaptersButton.prototype.createMenu = function createMenu() { + this.options_.title = this.getMenuCaption(); + return _TextTrackButton.prototype.createMenu.call(this); + }; + + /** + * Create a menu item for each text track + * + * @return {TextTrackMenuItem[]} + * Array of menu items + */ + + + ChaptersButton.prototype.createItems = function createItems() { + var items = []; + + if (!this.track_) { + return items; + } + + var cues = this.track_.cues; + + if (!cues) { + return items; + } + + for (var i = 0, l = cues.length; i < l; i++) { + var cue = cues[i]; + var mi = new ChaptersTrackMenuItem(this.player_, { track: this.track_, cue: cue }); + + items.push(mi); + } + + return items; + }; + + return ChaptersButton; + }(TextTrackButton); + + /** + * `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} + * @private + */ + ChaptersButton.prototype.controlText_ = 'Chapters'; + + Component.registerComponent('ChaptersButton', ChaptersButton); + + /** + * @file descriptions-button.js + */ + /** + * The button component for toggling and selecting descriptions + * + * @extends TextTrackButton + */ + + var DescriptionsButton = function (_TextTrackButton) { + inherits(DescriptionsButton, _TextTrackButton); + + /** + * 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. + * + * @param {Component~ReadyCallback} [ready] + * The function to call when this component is ready. + */ + function DescriptionsButton(player, options, ready) { + classCallCheck(this, DescriptionsButton); + + var _this = possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); + + var tracks = player.textTracks(); + var changeHandler = bind(_this, _this.handleTracksChange); + + tracks.addEventListener('change', changeHandler); + _this.on('dispose', function () { + tracks.removeEventListener('change', changeHandler); + }); + return _this; + } + + /** + * Handle text track change + * + * @param {EventTarget~Event} event + * The event that caused this function to run + * + * @listens TextTrackList#change + */ + + + DescriptionsButton.prototype.handleTracksChange = function handleTracksChange(event) { + var tracks = this.player().textTracks(); + var disabled = false; + + // Check whether a track of a different kind is showing + for (var i = 0, l = tracks.length; i < l; i++) { + var 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. + */ + + + DescriptionsButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-descriptions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + DescriptionsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-descriptions-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); + }; + + return DescriptionsButton; + }(TextTrackButton); + + /** + * `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} + * @private + */ + DescriptionsButton.prototype.controlText_ = 'Descriptions'; + + Component.registerComponent('DescriptionsButton', DescriptionsButton); + + /** + * @file subtitles-button.js + */ + /** + * The button component for toggling and selecting subtitles + * + * @extends TextTrackButton + */ + + var SubtitlesButton = function (_TextTrackButton) { + inherits(SubtitlesButton, _TextTrackButton); + + /** + * 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. + * + * @param {Component~ReadyCallback} [ready] + * The function to call when this component is ready. + */ + function SubtitlesButton(player, options, ready) { + classCallCheck(this, SubtitlesButton); + return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + SubtitlesButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + SubtitlesButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); + }; + + return SubtitlesButton; + }(TextTrackButton); + + /** + * `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} + * @private + */ + SubtitlesButton.prototype.controlText_ = 'Subtitles'; + + Component.registerComponent('SubtitlesButton', SubtitlesButton); + + /** + * @file caption-settings-menu-item.js + */ + /** + * The menu item for caption track settings menu + * + * @extends TextTrackMenuItem + */ + + var CaptionSettingsMenuItem = function (_TextTrackMenuItem) { + inherits(CaptionSettingsMenuItem, _TextTrackMenuItem); + + /** + * 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. + */ + function CaptionSettingsMenuItem(player, options) { + classCallCheck(this, CaptionSettingsMenuItem); + + options.track = { + player: 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'; + + var _this = possibleConstructorReturn(this, _TextTrackMenuItem.call(this, player, options)); + + _this.addClass('vjs-texttrack-settings'); + _this.controlText(', opens ' + options.kind + ' settings dialog'); + return _this; + } + + /** + * This gets called when an `CaptionSettingsMenuItem` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + CaptionSettingsMenuItem.prototype.handleClick = function handleClick(event) { + this.player().getChild('textTrackSettings').open(); + }; + + return CaptionSettingsMenuItem; + }(TextTrackMenuItem); + + Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); + + /** + * @file captions-button.js + */ + /** + * The button component for toggling and selecting captions + * + * @extends TextTrackButton + */ + + var CaptionsButton = function (_TextTrackButton) { + inherits(CaptionsButton, _TextTrackButton); + + /** + * 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. + * + * @param {Component~ReadyCallback} [ready] + * The function to call when this component is ready. + */ + function CaptionsButton(player, options, ready) { + classCallCheck(this, CaptionsButton); + return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + CaptionsButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-captions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + CaptionsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-captions-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); + }; + + /** + * Create caption menu items + * + * @return {CaptionSettingsMenuItem[]} + * The array of current menu items. + */ + + + CaptionsButton.prototype.createItems = function createItems() { + var items = []; + + if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks)) { + items.push(new CaptionSettingsMenuItem(this.player_, { kind: this.kind_ })); + + this.hideThreshold_ += 1; + } + + return _TextTrackButton.prototype.createItems.call(this, items); + }; + + return CaptionsButton; + }(TextTrackButton); + + /** + * `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} + * @private + */ + 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 + */ + + var SubsCapsMenuItem = function (_TextTrackMenuItem) { + inherits(SubsCapsMenuItem, _TextTrackMenuItem); + + function SubsCapsMenuItem() { + classCallCheck(this, SubsCapsMenuItem); + return possibleConstructorReturn(this, _TextTrackMenuItem.apply(this, arguments)); + } + + SubsCapsMenuItem.prototype.createEl = function createEl(type, props, attrs) { + var innerHTML = '' + this.localize(this.options_.label); + + if (this.options_.track.kind === 'captions') { + innerHTML += '\n \n ' + this.localize('Captions') + '\n '; + } + + innerHTML += ''; + + var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({ + innerHTML: innerHTML + }, props), attrs); + + return el; + }; + + return SubsCapsMenuItem; + }(TextTrackMenuItem); + + Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); + + /** + * @file sub-caps-button.js + */ + /** + * The button component for toggling and selecting captions and/or subtitles + * + * @extends TextTrackButton + */ + + var SubsCapsButton = function (_TextTrackButton) { + inherits(SubsCapsButton, _TextTrackButton); + + function SubsCapsButton(player) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, SubsCapsButton); + + // Although North America uses "captions" in most cases for + // "captions and subtitles" other locales use "subtitles" + var _this = possibleConstructorReturn(this, _TextTrackButton.call(this, player, options)); + + _this.label_ = 'subtitles'; + if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(_this.player_.language_) > -1) { + _this.label_ = 'captions'; + } + _this.menuButton_.controlText(toTitleCase(_this.label_)); + return _this; + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + SubsCapsButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-subs-caps-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); + }; + + SubsCapsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-subs-caps-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); + }; + + /** + * Create caption/subtitles menu items + * + * @return {CaptionSettingsMenuItem[]} + * The array of current menu items. + */ + + + SubsCapsButton.prototype.createItems = function createItems() { + var items = []; + + if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks)) { + items.push(new CaptionSettingsMenuItem(this.player_, { kind: this.label_ })); + + this.hideThreshold_ += 1; + } + + items = _TextTrackButton.prototype.createItems.call(this, items, SubsCapsMenuItem); + return items; + }; + + return SubsCapsButton; + }(TextTrackButton); + + /** + * `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} + * @private + */ + SubsCapsButton.prototype.controlText_ = 'Subtitles'; + + Component.registerComponent('SubsCapsButton', SubsCapsButton); + + /** + * @file audio-track-menu-item.js + */ + /** + * An {@link AudioTrack} {@link MenuItem} + * + * @extends MenuItem + */ + + var AudioTrackMenuItem = function (_MenuItem) { + inherits(AudioTrackMenuItem, _MenuItem); + + /** + * 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. + */ + function AudioTrackMenuItem(player, options) { + classCallCheck(this, AudioTrackMenuItem); + + var track = options.track; + var tracks = player.audioTracks(); + + // Modify options for parent MenuItem class's init. + options.label = track.label || track.language || 'Unknown'; + options.selected = track.enabled; + + var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); + + _this.track = track; + + var changeHandler = function changeHandler() { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + _this.handleTracksChange.apply(_this, args); + }; + + tracks.addEventListener('change', changeHandler); + _this.on('dispose', function () { + tracks.removeEventListener('change', changeHandler); + }); + return _this; + } + + /** + * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent} + * for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + AudioTrackMenuItem.prototype.handleClick = function handleClick(event) { + var tracks = this.player_.audioTracks(); + + _MenuItem.prototype.handleClick.call(this, event); + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + track.enabled = track === this.track; + } + }; + + /** + * Handle any {@link AudioTrack} change. + * + * @param {EventTarget~Event} [event] + * The {@link AudioTrackList#change} event that caused this to run. + * + * @listens AudioTrackList#change + */ + + + AudioTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { + this.selected(this.track.enabled); + }; + + return AudioTrackMenuItem; + }(MenuItem); + + Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem); + + /** + * @file audio-track-button.js + */ + /** + * The base class for buttons that toggle specific {@link AudioTrack} types. + * + * @extends TrackButton + */ + + var AudioTrackButton = function (_TrackButton) { + inherits(AudioTrackButton, _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. + */ + function AudioTrackButton(player) { + var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + classCallCheck(this, AudioTrackButton); + + options.tracks = player.audioTracks(); + + return possibleConstructorReturn(this, _TrackButton.call(this, player, options)); + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + AudioTrackButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-audio-button ' + _TrackButton.prototype.buildCSSClass.call(this); + }; + + AudioTrackButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-audio-button ' + _TrackButton.prototype.buildWrapperCSSClass.call(this); + }; + + /** + * 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 + */ + + + AudioTrackButton.prototype.createItems = function createItems() { + var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + + // if there's only one audio track, there no point in showing it + this.hideThreshold_ = 1; + + var tracks = this.player_.audioTracks(); + + for (var i = 0; i < tracks.length; i++) { + var track = tracks[i]; + + items.push(new AudioTrackMenuItem(this.player_, { + track: track, + // MenuItem is selectable + selectable: true + })); + } + + return items; + }; + + return AudioTrackButton; + }(TrackButton); + + /** + * The text that should display over the `AudioTrackButton`s controls. Added for localization. + * + * @type {string} + * @private + */ + + + 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 + */ + + var PlaybackRateMenuItem = function (_MenuItem) { + inherits(PlaybackRateMenuItem, _MenuItem); + + /** + * 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. + */ + function PlaybackRateMenuItem(player, options) { + classCallCheck(this, PlaybackRateMenuItem); + + var label = options.rate; + var rate = parseFloat(label, 10); + + // Modify options for parent MenuItem class's init. + options.label = label; + options.selected = rate === 1; + options.selectable = true; + + var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); + + _this.label = label; + _this.rate = rate; + + _this.on(player, 'ratechange', _this.update); + return _this; + } + + /** + * This gets called when an `PlaybackRateMenuItem` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + PlaybackRateMenuItem.prototype.handleClick = function handleClick(event) { + _MenuItem.prototype.handleClick.call(this); + this.player().playbackRate(this.rate); + }; + + /** + * Update the PlaybackRateMenuItem when the playbackrate changes. + * + * @param {EventTarget~Event} [event] + * The `ratechange` event that caused this function to run. + * + * @listens Player#ratechange + */ + + + PlaybackRateMenuItem.prototype.update = function update(event) { + this.selected(this.player().playbackRate() === this.rate); + }; + + return PlaybackRateMenuItem; + }(MenuItem); + + /** + * 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 + */ + + var PlaybackRateMenuButton = function (_MenuButton) { + inherits(PlaybackRateMenuButton, _MenuButton); + + /** + * 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. + */ + function PlaybackRateMenuButton(player, options) { + classCallCheck(this, PlaybackRateMenuButton); + + var _this = possibleConstructorReturn(this, _MenuButton.call(this, player, options)); + + _this.updateVisibility(); + _this.updateLabel(); + + _this.on(player, 'loadstart', _this.updateVisibility); + _this.on(player, 'ratechange', _this.updateLabel); + return _this; + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + PlaybackRateMenuButton.prototype.createEl = function createEl$$1() { + var el = _MenuButton.prototype.createEl.call(this); + + this.labelEl_ = createEl('div', { + className: 'vjs-playback-rate-value', + innerHTML: '1x' + }); + + el.appendChild(this.labelEl_); + + return el; + }; + + PlaybackRateMenuButton.prototype.dispose = function dispose() { + this.labelEl_ = null; + + _MenuButton.prototype.dispose.call(this); + }; + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + + + PlaybackRateMenuButton.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-playback-rate ' + _MenuButton.prototype.buildCSSClass.call(this); + }; + + PlaybackRateMenuButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { + return 'vjs-playback-rate ' + _MenuButton.prototype.buildWrapperCSSClass.call(this); + }; + + /** + * Create the playback rate menu + * + * @return {Menu} + * Menu object populated with {@link PlaybackRateMenuItem}s + */ + + + PlaybackRateMenuButton.prototype.createMenu = function createMenu() { + var menu = new Menu(this.player()); + var rates = this.playbackRates(); + + if (rates) { + for (var i = rates.length - 1; i >= 0; i--) { + menu.addChild(new PlaybackRateMenuItem(this.player(), { rate: rates[i] + 'x' })); + } + } + + return menu; + }; + + /** + * Updates ARIA accessibility attributes + */ + + + PlaybackRateMenuButton.prototype.updateARIAAttributes = function updateARIAAttributes() { + // Current playback rate + this.el().setAttribute('aria-valuenow', this.player().playbackRate()); + }; + + /** + * This gets called when an `PlaybackRateMenuButton` is "clicked". See + * {@link ClickableComponent} for more detailed information on what a click can be. + * + * @param {EventTarget~Event} [event] + * The `keydown`, `tap`, or `click` event that caused this function to be + * called. + * + * @listens tap + * @listens click + */ + + + PlaybackRateMenuButton.prototype.handleClick = function handleClick(event) { + // select next rate option + var currentRate = this.player().playbackRate(); + var rates = this.playbackRates(); + + // this will select first one if the last one currently selected + var newRate = rates[0]; + + for (var i = 0; i < rates.length; i++) { + if (rates[i] > currentRate) { + newRate = rates[i]; + break; + } + } + this.player().playbackRate(newRate); + }; + + /** + * Get possible playback rates + * + * @return {Array} + * All possible playback rates + */ + + + PlaybackRateMenuButton.prototype.playbackRates = function playbackRates() { + return this.options_.playbackRates || this.options_.playerOptions && this.options_.playerOptions.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 + */ + + + PlaybackRateMenuButton.prototype.playbackRateSupported = function 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 {EventTarget~Event} [event] + * The event that caused this function to run. + * + * @listens Player#loadstart + */ + + + PlaybackRateMenuButton.prototype.updateVisibility = function updateVisibility(event) { + if (this.playbackRateSupported()) { + this.removeClass('vjs-hidden'); + } else { + this.addClass('vjs-hidden'); + } + }; + + /** + * Update button label when rate changed + * + * @param {EventTarget~Event} [event] + * The event that caused this function to run. + * + * @listens Player#ratechange + */ + + + PlaybackRateMenuButton.prototype.updateLabel = function updateLabel(event) { + if (this.playbackRateSupported()) { + this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; + } + }; + + return PlaybackRateMenuButton; + }(MenuButton); + + /** + * The text that should display over the `FullscreenToggle`s controls. Added for localization. + * + * @type {string} + * @private + */ + + + 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 + */ + + var Spacer = function (_Component) { + inherits(Spacer, _Component); + + function Spacer() { + classCallCheck(this, Spacer); + return possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + Spacer.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-spacer ' + _Component.prototype.buildCSSClass.call(this); + }; + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + Spacer.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: this.buildCSSClass() + }); + }; + + return Spacer; + }(Component); + + 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 + */ + + var CustomControlSpacer = function (_Spacer) { + inherits(CustomControlSpacer, _Spacer); + + function CustomControlSpacer() { + classCallCheck(this, CustomControlSpacer); + return possibleConstructorReturn(this, _Spacer.apply(this, arguments)); + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + */ + CustomControlSpacer.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-custom-control-spacer ' + _Spacer.prototype.buildCSSClass.call(this); + }; + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + + + CustomControlSpacer.prototype.createEl = function createEl() { + var el = _Spacer.prototype.createEl.call(this, { + className: this.buildCSSClass() + }); + + // No-flex/table-cell mode requires there be some content + // in the cell to fill the remaining space of the table. + el.innerHTML = '\xA0'; + return el; + }; + + return CustomControlSpacer; + }(Spacer); + + Component.registerComponent('CustomControlSpacer', CustomControlSpacer); + + /** + * @file control-bar.js + */ +// Required children + /** + * Container of main controls. + * + * @extends Component + */ + + var ControlBar = function (_Component) { + inherits(ControlBar, _Component); + + function ControlBar() { + classCallCheck(this, ControlBar); + return possibleConstructorReturn(this, _Component.apply(this, arguments)); + } + + /** + * Create the `Component`'s DOM element + * + * @return {Element} + * The element that was created. + */ + ControlBar.prototype.createEl = function createEl() { + return _Component.prototype.createEl.call(this, 'div', { + className: 'vjs-control-bar', + dir: 'ltr' + }, { + // The control bar is a group, but we don't aria-label it to avoid + // over-announcing by JAWS + role: 'group' + }); + }; + + return ControlBar; + }(Component); + + /** + * Default options for `ControlBar` + * + * @type {Object} + * @private + */ + + + ControlBar.prototype.options_ = { + children: ['playToggle', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', '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 + */ + + var ErrorDisplay = function (_ModalDialog) { + inherits(ErrorDisplay, _ModalDialog); + + /** + * 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. + */ + function ErrorDisplay(player, options) { + classCallCheck(this, ErrorDisplay); + + var _this = possibleConstructorReturn(this, _ModalDialog.call(this, player, options)); + + _this.on(player, 'error', _this.open); + return _this; + } + + /** + * Builds the default DOM `className`. + * + * @return {string} + * The DOM `className` for this object. + * + * @deprecated Since version 5. + */ + + + ErrorDisplay.prototype.buildCSSClass = function buildCSSClass() { + return 'vjs-error-display ' + _ModalDialog.prototype.buildCSSClass.call(this); + }; + + /** + * Gets the localized error message based on the `Player`s error. + * + * @return {string} + * The `Player`s error message localized or an empty string. + */ + + + ErrorDisplay.prototype.content = function content() { + var error = this.player().error(); + + return error ? this.localize(error.message) : ''; + }; + + return ErrorDisplay; + }(ModalDialog); + + /** + * The default options for an `ErrorDisplay`. + * + * @private + */ + + + ErrorDisplay.prototype.options_ = mergeOptions(ModalDialog.prototype.options_, { + pauseOnOpen: false, + fillAlways: true, + temporary: false, + uncloseable: true + }); + + Component.registerComponent('ErrorDisplay', ErrorDisplay); + + /** + * @file text-track-settings.js + */ + var LOCAL_STORAGE_KEY = 'vjs-text-track-settings'; + + var COLOR_BLACK = ['#000', 'Black']; + var COLOR_BLUE = ['#00F', 'Blue']; + var COLOR_CYAN = ['#0FF', 'Cyan']; + var COLOR_GREEN = ['#0F0', 'Green']; + var COLOR_MAGENTA = ['#F0F', 'Magenta']; + var COLOR_RED = ['#F00', 'Red']; + var COLOR_WHITE = ['#FFF', 'White']; + var COLOR_YELLOW = ['#FF0', 'Yellow']; + + var OPACITY_OPAQUE = ['1', 'Opaque']; + var OPACITY_SEMI = ['0.5', 'Semi-Transparent']; + var OPACITY_TRANS = ['0', 'Transparent']; + +// Configuration for the various element. + var 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: 'Transparency', + options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS] + }, + + color: { + selector: '.vjs-fg-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', 'Dropshadow']] + }, + + 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: function parser(v) { + return v === '1.00' ? null : Number(v); + } + }, + + textOpacity: { + selector: '.vjs-text-opacity > select', + id: 'captions-foreground-opacity-%s', + label: 'Transparency', + 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: 'Transparency', + 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 {Mixed} + * - 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 '].join(''); + })).concat('').join(''); + }; + + /** + * Create foreground color element for the component + * + * @return {string} + * An HTML string. + * + * @private + */ + + + TextTrackSettings.prototype.createElFgColor_ = function createElFgColor_() { + var 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 + */ + + + TextTrackSettings.prototype.createElBgColor_ = function createElBgColor_() { + var legendId = 'captions-background-' + this.id_; + + return ['
    ', '', this.localize('Background'), '', this.createElSelect_('backgroundColor', legendId), '', this.createElSelect_('backgroundOpacity', legendId), '', '
    '].join(''); + }; + + /** + * Create window color element for the component + * + * @return {string} + * An HTML string. + * + * @private + */ + + + TextTrackSettings.prototype.createElWinColor_ = function createElWinColor_() { + var legendId = 'captions-window-' + this.id_; + + return ['
    ', '', this.localize('Window'), '', this.createElSelect_('windowColor', legendId), '', this.createElSelect_('windowOpacity', legendId), '', '
    '].join(''); + }; + + /** + * Create color elements for the component + * + * @return {Element} + * The element that was created + * + * @private + */ + + + TextTrackSettings.prototype.createElColors_ = function 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 + */ + + + TextTrackSettings.prototype.createElFont_ = function 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 + */ + + + TextTrackSettings.prototype.createElControls_ = function createElControls_() { + var defaultsDescription = this.localize('restore all settings to the default values'); + + return createEl('div', { + className: 'vjs-track-settings-controls', + innerHTML: ['', ''].join('') + }); + }; + + TextTrackSettings.prototype.content = function content() { + return [this.createElColors_(), this.createElFont_(), this.createElControls_()]; + }; + + TextTrackSettings.prototype.label = function label() { + return this.localize('Caption Settings Dialog'); + }; + + TextTrackSettings.prototype.description = function description() { + return this.localize('Beginning of dialog window. Escape will cancel and close the window.'); + }; + + TextTrackSettings.prototype.buildCSSClass = function buildCSSClass() { + return _ModalDialog.prototype.buildCSSClass.call(this) + ' 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. + */ + + + TextTrackSettings.prototype.getValues = function getValues() { + var _this3 = this; + + return reduce(selectConfigs, function (accum, config, key) { + var value = getSelectedOptionValue(_this3.$(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. + */ + + + TextTrackSettings.prototype.setValues = function setValues(values) { + var _this4 = this; + + each(selectConfigs, function (config, key) { + setSelectedOption(_this4.$(config.selector), values[key], config.parser); + }); + }; + + /** + * Sets all `