From 3e0a91141822758094b2cbd5f0ecdd8ce4142b5f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 26 May 2018 13:37:10 +1000 Subject: WIP --- src/sass/components/captions.scss | 4 +- src/sass/components/control.scss | 22 +++---- src/sass/lib/css-vars.scss | 131 ++++++++++++++++++++++++++++++++++++++ src/sass/plyr.scss | 3 + src/sass/settings/captions.scss | 12 +++- src/sass/settings/colors.scss | 10 +++ src/sass/settings/controls.scss | 24 ++++++- src/sass/settings/cosmetics.scss | 2 + src/sass/settings/sliders.scss | 2 +- 9 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 src/sass/lib/css-vars.scss (limited to 'src') diff --git a/src/sass/components/captions.scss b/src/sass/components/captions.scss index 9dfc2be8..66f34199 100644 --- a/src/sass/components/captions.scss +++ b/src/sass/components/captions.scss @@ -10,7 +10,7 @@ .plyr__captions { animation: plyr-fade-in 0.3s ease; bottom: 0; - color: $plyr-captions-color; + color: var(--plyr-captions-text-color); display: none; font-size: $plyr-font-size-captions-small; left: 0; @@ -22,7 +22,7 @@ width: 100%; span { - background: $plyr-captions-bg; + background: var(--plyr-captions-background); border-radius: 2px; box-decoration-break: clone; line-height: 185%; diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index 52716805..d93a6f34 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -5,21 +5,21 @@ .plyr__control { background: transparent; border: 0; - border-radius: $plyr-control-radius; + border-radius: var(--plyr-control-radius); color: inherit; cursor: pointer; flex-shrink: 0; overflow: visible; // IE11 - padding: $plyr-control-padding; + padding: var(--plyr-control-padding); position: relative; transition: all 0.3s ease; svg { display: block; fill: currentColor; - height: $plyr-control-icon-size; + height: var(--plyr-control-icon-size); pointer-events: none; - width: $plyr-control-icon-size; + width: var(--plyr-control-icon-size); } // Default focus @@ -46,18 +46,18 @@ &.plyr__tab-focus, &:hover, &[aria-expanded='true'] { - background: $plyr-audio-control-bg-hover; - color: $plyr-audio-control-color-hover; + background: var(--plyr-audio-control-bg-hover); + color: var(--plyr-audio-control-color-hover); } } // Large play button (video only) .plyr__control--overlaid { - background: rgba($plyr-video-control-bg-hover, 0.8); + background: var(--plyr-video-control-bg-hover); border: 0; border-radius: 100%; box-shadow: 0 1px 1px rgba(#000, 0.15); - color: $plyr-video-control-color; + color: var(--plyr-video-control-color); display: none; left: 50%; padding: ceil($plyr-control-spacing * 1.5); @@ -67,15 +67,15 @@ z-index: 2; svg { - height: $plyr-control-icon-size-large; + height: var(--plyr-control-icon-size-large); left: 2px; // Offset to make the play button look right position: relative; - width: $plyr-control-icon-size-large; + width: var(--plyr-control-icon-size-large); } &:hover, &:focus { - background: $plyr-video-control-bg-hover; + background: var(--plyr-video-control-bg-hover); } } diff --git a/src/sass/lib/css-vars.scss b/src/sass/lib/css-vars.scss new file mode 100644 index 00000000..cb251831 --- /dev/null +++ b/src/sass/lib/css-vars.scss @@ -0,0 +1,131 @@ +// Downloaded from https://github.com/malyw/css-vars + +//// VARIABLES //// + +// global map to be filled via variables +$css-vars: (); + +// the variable may be set to "true" anywhere in the code, +// so native CSS custom properties will be used instead of the Sass global map +$css-vars-use-native: false !default; + +// enables the output of debug messages +$css-vars-debug-log: false !default; + +//// FUNCTIONS //// + +/// +// Assigns a variable to the global map +/// +@function _cssVarAssign($varName: null, $varValue: null) { + // CHECK PARAMS + @if ($varName==null) { + @error 'Variable name is expected, instead got: null'; + } + @if ($varValue==null) { + @error 'Variable value is expected, instead got: null'; + } + + // assign to the global map + @if ($css-vars-debug-log and map-get($css-vars, $varName)) { + @debug "'#{$varName}' variable is reassigned"; + } + + @return map-merge($css-vars, ($varName: $varValue)); +} + +/// +// Emulates var() CSS native function behavior +// +// $args[0] {String} "--" + variable name +// [$args[1]] Optional default value if variable is not assigned yet +// +// E.G.: +// color: var(--main-color); +// background: var(--main-bg, green); +/// +@function var($args...) { + // CHECK PARAMS + @if (length($args) ==0) { + @error 'Variable name is expected to be passed to the var() function'; + } + @if (str-length(nth($args, 1)) < 2 or str-slice(nth($args, 1), 0, 2) != '--') { + @error "Variable name is expected to start from '--'"; + } + + // PROCESS + $varName: nth($args, 1); + $varValue: map-get($css-vars, $varName); + + @if ($css-vars-debug-log or not $css-vars-use-native) { + // Sass or debug + @if ($varValue==null) { + // variable is not provided so far + @if (length($args) ==2) { + // the default value is passed + @if ($css-vars-debug-log) { + @debug "Provided default value is used for the variable: '#{$varName}'"; + } + $varValue: nth($args, 2); + } @else if ($css-vars-debug-log) { + @debug "Variable '#{$varName}' is not assigned"; + @if (not $css-vars-use-native) { + @debug "The 'var(#{$varName}...)' usage will be skipped in the output CSS"; + } + } + } + } + + @if ($css-vars-use-native) { + // CSS variables + // Native CSS: don't process function in case of native + @return unquote('var(' + $args + ')'); + } @else { + // Sass: return value from the map + @return $varValue; + } +} + +//// MIXIN //// + +/// +// CSS mixin to provide variables +// E.G.: +// @include css-vars(( +// --color: rebeccapurple, +// --height: 68px, +// --margin-top: calc(2vh + 20px) +// )); +/// +@mixin css-vars($varMap: null) { + // CHECK PARAMS + @if ($varMap==null) { + @error 'Map of variables is expected, instead got: null'; + } + @if (type_of($varMap) !=map) { + @error 'Map of variables is expected, instead got another type passed: #{type_of($varMap)}'; + } + + // PROCESS + @if ($css-vars-debug-log or not $css-vars-use-native) { + // Sass or debug + // merge variables and values to the global map (provides no output) + @each $varName, $varValue in $varMap { + $css-vars: _cssVarAssign($varName, $varValue) !global; // store in global variable + } + } + + @if ($css-vars-use-native) { + // CSS variables + // Native CSS: assign CSS custom properties to the global scope + @at-root :root { + @each $varName, $varValue in $varMap { + @if (type_of($varValue) ==string) { + #{$varName}: $varValue; // to prevent quotes interpolation + } @else { + #{$varName}: #{$varValue}; + } + } + } + } +} diff --git a/src/sass/plyr.scss b/src/sass/plyr.scss index e934cf92..6a283dc1 100644 --- a/src/sass/plyr.scss +++ b/src/sass/plyr.scss @@ -5,6 +5,9 @@ // ========================================================================== @charset 'UTF-8'; +@import 'lib/css-vars'; +$css-vars-use-native: true; + @import 'settings/breakpoints'; @import 'settings/colors'; @import 'settings/cosmetics'; diff --git a/src/sass/settings/captions.scss b/src/sass/settings/captions.scss index 0c259046..46c9cfc8 100644 --- a/src/sass/settings/captions.scss +++ b/src/sass/settings/captions.scss @@ -2,9 +2,17 @@ // Captions // ========================================================================== -$plyr-captions-bg: rgba(#000, 0.8) !default; -$plyr-captions-color: #fff !default; +$plyr-captions-background: rgba(#000, 0.8) !default; +$plyr-captions-text-color: #fff !default; + $plyr-font-size-captions-base: $plyr-font-size-base !default; $plyr-font-size-captions-small: $plyr-font-size-small !default; $plyr-font-size-captions-medium: $plyr-font-size-large !default; $plyr-font-size-captions-large: $plyr-font-size-xlarge !default; + +@include css-vars( + ( + --plyr-captions-background: $plyr-captions-background, + --plyr-captions-text-color: $plyr-captions-text-color + ) +); diff --git a/src/sass/settings/colors.scss b/src/sass/settings/colors.scss index c9ea580c..5a2f4e82 100644 --- a/src/sass/settings/colors.scss +++ b/src/sass/settings/colors.scss @@ -7,3 +7,13 @@ $plyr-color-gunmetal: #2f343d !default; $plyr-color-fiord: #4f5b5f !default; $plyr-color-lynch: #6b7d85 !default; $plyr-color-heather: #b7c5cd !default; + +@include css-vars( + ( + --plyr-color-main: $plyr-color-main, + --plyr-color-gunmetal: $plyr-color-gunmetal, + --plyr-color-fiord: $plyr-color-fiord, + --plyr-color-lynch: $plyr-color-lynch, + --plyr-color-heather: $plyr-color-heather + ) +); diff --git a/src/sass/settings/controls.scss b/src/sass/settings/controls.scss index 64f05cec..77387567 100644 --- a/src/sass/settings/controls.scss +++ b/src/sass/settings/controls.scss @@ -11,9 +11,27 @@ $plyr-control-radius: 3px !default; $plyr-video-controls-bg: #000 !default; $plyr-video-control-color: #fff !default; $plyr-video-control-color-hover: #fff !default; -$plyr-video-control-bg-hover: $plyr-color-main !default; +$plyr-video-control-bg-hover: var(--plyr-color-main) !default; $plyr-audio-controls-bg: #fff !default; -$plyr-audio-control-color: $plyr-color-fiord !default; +$plyr-audio-control-color: var(--plyr-color-fiord) !default; $plyr-audio-control-color-hover: #fff !default; -$plyr-audio-control-bg-hover: $plyr-color-main !default; +$plyr-audio-control-bg-hover: var(--plyr-color-main) !default; + +@include css-vars( + ( + --plyr-control-icon-size: $plyr-control-icon-size, + --plyr-control-icon-size-large: $plyr-control-icon-size-large, + --plyr-control-spacing: $plyr-control-spacing, + --plyr-control-padding: $plyr-control-padding, + --plyr-control-radius: $plyr-control-radius, + --plyr-video-controls-bg: $plyr-video-controls-bg, + --plyr-video-control-color: $plyr-video-control-color, + --plyr-video-control-color-hover: $plyr-video-control-color-hover, + --plyr-video-control-bg-hover: $plyr-video-control-bg-hover, + --plyr-audio-controls-bg: $plyr-audio-controls-bg, + --plyr-audio-control-color: $plyr-audio-control-color, + --plyr-audio-control-color-hover: $plyr-audio-control-color-hover, + --plyr-audio-control-bg-hover: $plyr-audio-control-bg-hover + ) +); diff --git a/src/sass/settings/cosmetics.scss b/src/sass/settings/cosmetics.scss index d6e4b86d..80063850 100644 --- a/src/sass/settings/cosmetics.scss +++ b/src/sass/settings/cosmetics.scss @@ -3,3 +3,5 @@ // ========================================================================== $plyr-tab-focus-default-color: $plyr-color-main !default; + +@include css-vars((--plyr-tab-focus-default-color: $plyr-tab-focus-default-color)); diff --git a/src/sass/settings/sliders.scss b/src/sass/settings/sliders.scss index 3c75b797..edc3fe7e 100644 --- a/src/sass/settings/sliders.scss +++ b/src/sass/settings/sliders.scss @@ -16,7 +16,7 @@ $plyr-range-track-height: 6px !default; $plyr-range-max-height: ($plyr-range-thumb-active-shadow-width * 2) + $plyr-range-thumb-height !default; // Fill -$plyr-range-fill-bg: $plyr-color-main !default; +$plyr-range-fill-bg: var(--plyr-color-main); // Type specific $plyr-video-range-track-bg: $plyr-video-progress-buffered-bg !default; -- cgit v1.2.3 From bdd513635fffa33f66735c80209e6ae77e0426b4 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 8 Dec 2018 17:06:20 +1100 Subject: Work on outline/focus styles --- src/sass/components/control.scss | 4 ++-- src/sass/lib/mixins.scss | 10 +++++++--- src/sass/settings/controls.scss | 1 - 3 files changed, 9 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index d5a9847e..eedcab13 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -71,8 +71,8 @@ a.plyr__control { &.plyr__tab-focus, &:hover, &[aria-expanded='true'] { - background: var(--plyr-audio-control-bg-hover); - color: var(--plyr-audio-control-color-hover); + background: var(--plyr-video-control-bg-hover); + color: var(--plyr-video-control-color-hover); } } diff --git a/src/sass/lib/mixins.scss b/src/sass/lib/mixins.scss index e015ffee..0a0f7dcb 100644 --- a/src/sass/lib/mixins.scss +++ b/src/sass/lib/mixins.scss @@ -4,9 +4,13 @@ // Nicer focus styles // --------------------------------------- -@mixin plyr-tab-focus($color: $plyr-tab-focus-default-color) { - box-shadow: 0 0 0 5px rgba($color, 0.5); - outline: 0; +@mixin plyr-tab-focus() { + // box-shadow: 0 0 0 5px rgba($color, 0.5); + // outline: 0; + outline-color: var(--plyr-color-main); + outline-offset: 2px; + outline-style: dotted; + outline-width: 3px; } // Font smoothing diff --git a/src/sass/settings/controls.scss b/src/sass/settings/controls.scss index ee88434a..906744b2 100644 --- a/src/sass/settings/controls.scss +++ b/src/sass/settings/controls.scss @@ -20,7 +20,6 @@ $plyr-audio-control-bg-hover: var(--plyr-color-main) !default; @include css-vars( ( --plyr-control-icon-size: $plyr-control-icon-size, - --plyr-control-icon-size-large: $plyr-control-icon-size-large, --plyr-control-spacing: $plyr-control-spacing, --plyr-control-padding: $plyr-control-padding, --plyr-control-radius: $plyr-control-radius, -- cgit v1.2.3 From 996075decc6e8c0f0c5059dccea21b16020eb78b Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Thu, 11 Apr 2019 20:50:20 +1000 Subject: More work on variable usage --- src/sass/base.scss | 6 +-- src/sass/components/badges.scss | 2 +- src/sass/components/captions.scss | 1 - src/sass/components/control.scss | 2 +- src/sass/lib/css-vars.scss | 97 +++++++++++++-------------------------- src/sass/settings/colors.scss | 2 +- src/sass/settings/controls.scss | 2 +- src/sass/settings/type.scss | 20 +++++++- 8 files changed, 58 insertions(+), 74 deletions(-) (limited to 'src') diff --git a/src/sass/base.scss b/src/sass/base.scss index 9bb9d98a..84861e99 100644 --- a/src/sass/base.scss +++ b/src/sass/base.scss @@ -7,10 +7,10 @@ @include plyr-font-smoothing($plyr-font-smoothing); direction: ltr; - font-family: $plyr-font-family; + font-family: var(--plyr-font-family); font-variant-numeric: tabular-nums; // Force monosace-esque number widths - font-weight: $plyr-font-weight-regular; - line-height: $plyr-line-height; + font-weight: var(--plyr-font-weight-regular); + line-height: var(--plyr-line-height); max-width: 100%; min-width: 200px; position: relative; diff --git a/src/sass/components/badges.scss b/src/sass/components/badges.scss index 3a9a28b5..46c87d03 100644 --- a/src/sass/components/badges.scss +++ b/src/sass/components/badges.scss @@ -6,7 +6,7 @@ background: $plyr-badge-bg; border-radius: 2px; color: $plyr-badge-color; - font-size: $plyr-font-size-badge; + font-size: var(--plyr-font-size-badge); line-height: 1; padding: 3px 4px; } diff --git a/src/sass/components/captions.scss b/src/sass/components/captions.scss index b8e2d771..d7951b96 100644 --- a/src/sass/components/captions.scss +++ b/src/sass/components/captions.scss @@ -56,4 +56,3 @@ .plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions { transform: translateY(-($plyr-control-spacing * 4)); } - diff --git a/src/sass/components/control.scss b/src/sass/components/control.scss index eedcab13..ee24604e 100644 --- a/src/sass/components/control.scss +++ b/src/sass/components/control.scss @@ -82,7 +82,7 @@ a.plyr__control { border: 0; border-radius: 100%; box-shadow: 0 1px 1px rgba(#000, 0.15); - color: var(--plyr-video-control-color); + color: var(--plyr-video-control-color-hover); display: none; left: 50%; padding: ceil($plyr-control-spacing * 1.5); diff --git a/src/sass/lib/css-vars.scss b/src/sass/lib/css-vars.scss index cb251831..074c27c1 100644 --- a/src/sass/lib/css-vars.scss +++ b/src/sass/lib/css-vars.scss @@ -1,6 +1,4 @@ -// Downloaded from https://github.com/malyw/css-vars - -//// VARIABLES //// +// Downloaded from https://github.com/malyw/css-vars (and modified) // global map to be filled via variables $css-vars: (); @@ -9,29 +7,16 @@ $css-vars: (); // so native CSS custom properties will be used instead of the Sass global map $css-vars-use-native: false !default; -// enables the output of debug messages -$css-vars-debug-log: false !default; - -//// FUNCTIONS //// - /// // Assigns a variable to the global map /// -@function _cssVarAssign($varName: null, $varValue: null) { - // CHECK PARAMS - @if ($varName==null) { - @error 'Variable name is expected, instead got: null'; - } - @if ($varValue==null) { - @error 'Variable value is expected, instead got: null'; - } - - // assign to the global map - @if ($css-vars-debug-log and map-get($css-vars, $varName)) { - @debug "'#{$varName}' variable is reassigned"; - } - - @return map-merge($css-vars, ($varName: $varValue)); +@function css-var-assign($varName: null, $varValue: null) { + @return map-merge( + $css-vars, + ( + $varName: $varValue, + ) + ); } /// @@ -54,42 +39,28 @@ $css-vars-debug-log: false !default; } // PROCESS - $varName: nth($args, 1); - $varValue: map-get($css-vars, $varName); - - @if ($css-vars-debug-log or not $css-vars-use-native) { - // Sass or debug - @if ($varValue==null) { - // variable is not provided so far - @if (length($args) ==2) { - // the default value is passed - @if ($css-vars-debug-log) { - @debug "Provided default value is used for the variable: '#{$varName}'"; - } - $varValue: nth($args, 2); - } @else if ($css-vars-debug-log) { - @debug "Variable '#{$varName}' is not assigned"; - @if (not $css-vars-use-native) { - @debug "The 'var(#{$varName}...)' usage will be skipped in the output CSS"; - } - } - } - } + $var-name: nth($args, 1); + $var-value: map-get($css-vars, $var-name); @if ($css-vars-use-native) { // CSS variables // Native CSS: don't process function in case of native @return unquote('var(' + $args + ')'); } @else { + @if ($var-value == null) { + // variable is not provided so far + @if (length($args) == 2) { + $var-value: nth($args, 2); + } + } + // Sass: return value from the map - @return $varValue; + @return $var-value; } } -//// MIXIN //// - /// -// CSS mixin to provide variables +// SASS mixin to provide variables // E.G.: // @include css-vars(( // --color: rebeccapurple, @@ -97,35 +68,33 @@ $css-vars-debug-log: false !default; // --margin-top: calc(2vh + 20px) // )); /// -@mixin css-vars($varMap: null) { +@mixin css-vars($var-map: null) { // CHECK PARAMS - @if ($varMap==null) { + @if ($var-map == null) { @error 'Map of variables is expected, instead got: null'; } - @if (type_of($varMap) !=map) { - @error 'Map of variables is expected, instead got another type passed: #{type_of($varMap)}'; + @if (type_of($var-map) != map) { + @error 'Map of variables is expected, instead got another type passed: #{type_of($var, ap)}'; } // PROCESS - @if ($css-vars-debug-log or not $css-vars-use-native) { - // Sass or debug - // merge variables and values to the global map (provides no output) - @each $varName, $varValue in $varMap { - $css-vars: _cssVarAssign($varName, $varValue) !global; // store in global variable - } - } - @if ($css-vars-use-native) { // CSS variables // Native CSS: assign CSS custom properties to the global scope @at-root :root { - @each $varName, $varValue in $varMap { - @if (type_of($varValue) ==string) { - #{$varName}: $varValue; // to prevent quotes interpolation + @each $var-name, $var-value in $var-map { + @if (type_of($var-value) == string) { + #{$var-name}: $var-value; // to prevent quotes interpolation } @else { - #{$varName}: #{$varValue}; + #{$var-name}: #{$var-value}; } } } + } @else { + // Sass or debug + // merge variables and values to the global map (provides no output) + @each $var-name, $var-value in $var-map { + $css-vars: css-var-assign($varName, $varValue) !global; // store in global variable + } } } diff --git a/src/sass/settings/colors.scss b/src/sass/settings/colors.scss index 5a2f4e82..c0867b85 100644 --- a/src/sass/settings/colors.scss +++ b/src/sass/settings/colors.scss @@ -2,7 +2,7 @@ // Colors // ========================================================================== -$plyr-color-main: #1aafff !default; +$plyr-color-main: #fff !default; $plyr-color-gunmetal: #2f343d !default; $plyr-color-fiord: #4f5b5f !default; $plyr-color-lynch: #6b7d85 !default; diff --git a/src/sass/settings/controls.scss b/src/sass/settings/controls.scss index 906744b2..0c88163d 100644 --- a/src/sass/settings/controls.scss +++ b/src/sass/settings/controls.scss @@ -9,7 +9,7 @@ $plyr-control-radius: 3px !default; $plyr-video-controls-bg: #000 !default; $plyr-video-control-color: #fff !default; -$plyr-video-control-color-hover: #fff !default; +$plyr-video-control-color-hover: #000 !default; $plyr-video-control-bg-hover: var(--plyr-color-main) !default; $plyr-audio-controls-bg: #fff !default; diff --git a/src/sass/settings/type.scss b/src/sass/settings/type.scss index 79cb57de..e9ee2671 100644 --- a/src/sass/settings/type.scss +++ b/src/sass/settings/type.scss @@ -8,9 +8,9 @@ $plyr-font-size-small: 14px !default; $plyr-font-size-large: 18px !default; $plyr-font-size-xlarge: 21px !default; -$plyr-font-size-time: $plyr-font-size-small !default; +$plyr-font-size-time: var(--plyr-font-size-small) !default; $plyr-font-size-badge: 9px !default; -$plyr-font-size-menu: $plyr-font-size-small !default; +$plyr-font-size-menu: var(--plyr-font-size-small) !default; $plyr-font-weight-regular: 500 !default; $plyr-font-weight-bold: 600 !default; @@ -18,3 +18,19 @@ $plyr-font-weight-bold: 600 !default; $plyr-line-height: 1.7 !default; $plyr-font-smoothing: false !default; + +@include css-vars( + ( + --plyr-font-family: $plyr-font-family, + --plyr-font-size-base: $plyr-font-size-base, + --plyr-font-size-small: $plyr-font-size-small, + --plyr-font-size-large: $plyr-font-size-large, + --plyr-font-size-xlarge: $plyr-font-size-xlarge, + --plyr-font-size-time: $plyr-font-size-time, + --plyr-font-size-badge: $plyr-font-size-badge, + --plyr-font-size-menu: $plyr-font-size-menu, + --plyr-font-weight-regular: $plyr-font-weight-regular, + --plyr-font-weight-bold: $plyr-font-weight-bold, + --plyr-line-height: $plyr-line-height + ) +); -- cgit v1.2.3 From dc2e012cc9dd617b704103327d15e5de85e6dde7 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 1 Jun 2019 20:01:09 +1000 Subject: Clean up --- src/js/plugins/previewThumbnails.js | 1 - 1 file changed, 1 deletion(-) (limited to 'src') diff --git a/src/js/plugins/previewThumbnails.js b/src/js/plugins/previewThumbnails.js index b4714117..2d34fe46 100644 --- a/src/js/plugins/previewThumbnails.js +++ b/src/js/plugins/previewThumbnails.js @@ -2,7 +2,6 @@ import { createElement } from '../utils/elements'; import { once } from '../utils/events'; import fetch from '../utils/fetch'; import is from '../utils/is'; -import { extend } from '../utils/objects'; import { formatTime } from '../utils/time'; // Arg: vttDataString example: "WEBVTT\n\n1\n00:00:05.000 --> 00:00:10.000\n1080p-00001.jpg" -- cgit v1.2.3 From d06881783d7c8a9faa5d902da5ec33bc74f3aa38 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Mon, 30 Mar 2020 17:04:43 +1100 Subject: Formatting fixes --- src/js/plugins/preview-thumbnails.js | 15 ++++++++++++--- src/js/plugins/vimeo.js | 17 ++++++++++------- src/js/utils/elements.js | 2 +- src/js/utils/events.js | 2 +- 4 files changed, 24 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/js/plugins/preview-thumbnails.js b/src/js/plugins/preview-thumbnails.js index 290ce949..60033331 100644 --- a/src/js/plugins/preview-thumbnails.js +++ b/src/js/plugins/preview-thumbnails.js @@ -628,7 +628,10 @@ class PreviewThumbnails { } determineContainerAutoSizing() { - if (this.elements.thumb.imageContainer.clientHeight > 20 || this.elements.thumb.imageContainer.clientWidth > 20) { + if ( + this.elements.thumb.imageContainer.clientHeight > 20 || + this.elements.thumb.imageContainer.clientWidth > 20 + ) { // This will prevent auto sizing in this.setThumbContainerSizeAndPos() this.sizeSpecifiedInCSS = true; } @@ -640,10 +643,16 @@ class PreviewThumbnails { const thumbWidth = Math.floor(this.thumbContainerHeight * this.thumbAspectRatio); this.elements.thumb.imageContainer.style.height = `${this.thumbContainerHeight}px`; this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`; - } else if (this.elements.thumb.imageContainer.clientHeight > 20 && this.elements.thumb.imageContainer.clientWidth < 20) { + } else if ( + this.elements.thumb.imageContainer.clientHeight > 20 && + this.elements.thumb.imageContainer.clientWidth < 20 + ) { const thumbWidth = Math.floor(this.elements.thumb.imageContainer.clientHeight * this.thumbAspectRatio); this.elements.thumb.imageContainer.style.width = `${thumbWidth}px`; - } else if (this.elements.thumb.imageContainer.clientHeight < 20 && this.elements.thumb.imageContainer.clientWidth > 20) { + } else if ( + this.elements.thumb.imageContainer.clientHeight < 20 && + this.elements.thumb.imageContainer.clientWidth > 20 + ) { const thumbHeight = Math.floor(this.elements.thumb.imageContainer.clientWidth / this.thumbAspectRatio); this.elements.thumb.imageContainer.style.height = `${thumbHeight}px`; } diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 010cf5f7..8f999153 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -201,13 +201,16 @@ const vimeo = { return speed; }, set(input) { - player.embed.setPlaybackRate(input).then(() => { - speed = input; - triggerEvent.call(player, player.media, 'ratechange'); - }).catch(() => { - // Cannot set Playback Rate, Video is probably not on Pro account - player.options.speed = [1]; - }); + player.embed + .setPlaybackRate(input) + .then(() => { + speed = input; + triggerEvent.call(player, player.media, 'ratechange'); + }) + .catch(() => { + // Cannot set Playback Rate, Video is probably not on Pro account + player.options.speed = [1]; + }); }, }); diff --git a/src/js/utils/elements.js b/src/js/utils/elements.js index bdf18bfd..acff0dd9 100644 --- a/src/js/utils/elements.js +++ b/src/js/utils/elements.js @@ -221,7 +221,7 @@ export function hasClass(element, className) { // Element matches selector export function matches(element, selector) { - const {prototype} = Element; + const { prototype } = Element; function match() { return Array.from(document.querySelectorAll(selector)).includes(this); diff --git a/src/js/utils/events.js b/src/js/utils/events.js index 31571b2d..48300b6b 100644 --- a/src/js/utils/events.js +++ b/src/js/utils/events.js @@ -90,7 +90,7 @@ export function triggerEvent(element, type = '', bubbles = false, detail = {}) { // Create and dispatch the event const event = new CustomEvent(type, { bubbles, - detail: { ...detail, plyr: this,}, + detail: { ...detail, plyr: this }, }); // Dispatch the event -- cgit v1.2.3 From 502d5977d79148957828cbf313b7ef4c9f31973f Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 11 Apr 2020 16:23:14 +1000 Subject: Converted to 2 space indentation --- src/js/captions.js | 702 ++--- src/js/config/defaults.js | 822 +++--- src/js/config/states.js | 4 +- src/js/config/types.js | 28 +- src/js/console.js | 34 +- src/js/controls.js | 3244 ++++++++++----------- src/js/fullscreen.js | 477 ++- src/js/html5.js | 248 +- src/js/listeners.js | 1566 +++++----- src/js/media.js | 96 +- src/js/plugins/ads.js | 1120 +++---- src/js/plugins/preview-thumbnails.js | 1181 ++++---- src/js/plugins/vimeo.js | 738 ++--- src/js/plugins/youtube.js | 790 ++--- src/js/plyr.d.ts | 952 +++--- src/js/plyr.js | 2116 +++++++------- src/js/source.js | 274 +- src/js/storage.js | 104 +- src/js/support.js | 200 +- src/js/ui.js | 513 ++-- src/js/utils/animation.js | 46 +- src/js/utils/arrays.js | 16 +- src/js/utils/browser.js | 10 +- src/js/utils/elements.js | 336 +-- src/js/utils/events.js | 144 +- src/js/utils/fetch.js | 58 +- src/js/utils/i18n.js | 50 +- src/js/utils/is.js | 78 +- src/js/utils/load-image.js | 18 +- src/js/utils/load-script.js | 10 +- src/js/utils/load-sprite.js | 104 +- src/js/utils/numbers.js | 2 +- src/js/utils/objects.js | 40 +- src/js/utils/promise.js | 6 +- src/js/utils/strings.js | 75 +- src/js/utils/style.js | 108 +- src/js/utils/time.js | 36 +- src/js/utils/urls.js | 36 +- src/sass/base.scss | 94 +- src/sass/components/badges.scss | 12 +- src/sass/components/captions.scss | 76 +- src/sass/components/control.scss | 63 +- src/sass/components/controls.scss | 66 +- src/sass/components/menus.scss | 348 +-- src/sass/components/poster.scss | 26 +- src/sass/components/progress.scss | 130 +- src/sass/components/sliders.scss | 154 +- src/sass/components/times.scss | 18 +- src/sass/components/tooltips.scss | 108 +- src/sass/components/volume.scss | 28 +- src/sass/lib/animation.scss | 34 +- src/sass/lib/css-vars.scss | 110 +- src/sass/lib/functions.scss | 2 +- src/sass/lib/mixins.scss | 126 +- src/sass/plugins/ads.scss | 86 +- src/sass/plugins/preview-thumbnails.scss | 118 - src/sass/plugins/preview-thumbnails/index.scss | 108 + src/sass/plugins/preview-thumbnails/settings.scss | 15 + src/sass/plyr.scss | 2 +- src/sass/settings/badges.scss | 5 +- src/sass/settings/breakpoints.scss | 7 +- src/sass/settings/captions.scss | 12 +- src/sass/settings/colors.scss | 38 +- src/sass/settings/controls.scss | 38 +- src/sass/settings/cosmetics.scss | 4 +- src/sass/settings/menus.scss | 17 +- src/sass/settings/progress.scss | 11 +- src/sass/settings/sliders.scss | 43 +- src/sass/settings/tooltips.scss | 12 +- src/sass/settings/type.scss | 39 +- src/sass/states/fullscreen.scss | 24 +- src/sass/types/audio.scss | 69 +- src/sass/types/video.scss | 206 +- src/sass/utils/animation.scss | 2 +- src/sass/utils/hidden.scss | 34 +- 75 files changed, 9323 insertions(+), 9344 deletions(-) delete mode 100644 src/sass/plugins/preview-thumbnails.scss create mode 100644 src/sass/plugins/preview-thumbnails/index.scss create mode 100644 src/sass/plugins/preview-thumbnails/settings.scss (limited to 'src') diff --git a/src/js/captions.js b/src/js/captions.js index 04da4651..0e6173ca 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -8,12 +8,12 @@ import support from './support'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; import { - createElement, - emptyElement, - getAttributesFromSelector, - insertAfter, - removeElement, - toggleClass, + createElement, + emptyElement, + getAttributesFromSelector, + insertAfter, + removeElement, + toggleClass, } from './utils/elements'; import { on, triggerEvent } from './utils/events'; import fetch from './utils/fetch'; @@ -23,371 +23,371 @@ import { getHTML } from './utils/strings'; import { parseUrl } from './utils/urls'; const captions = { - // Setup captions - setup() { - // Requires UI support - if (!this.supported.ui) { - return; - } - - // Only Vimeo and HTML5 video supported at this point - if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { - // Clear menu and hide - if ( - is.array(this.config.controls) && - this.config.controls.includes('settings') && - this.config.settings.includes('captions') - ) { - controls.setCaptionsMenu.call(this); - } - - return; - } - - // Inject the container - if (!is.element(this.elements.captions)) { - this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); - - insertAfter(this.elements.captions, this.elements.wrapper); - } - - // Fix IE captions if CORS is used - // Fetch captions and inject as blobs instead (data URIs not supported!) - if (browser.isIE && window.URL) { - const elements = this.media.querySelectorAll('track'); - - Array.from(elements).forEach(track => { - const src = track.getAttribute('src'); - const url = parseUrl(src); - - if ( - url !== null && - url.hostname !== window.location.href.hostname && - ['http:', 'https:'].includes(url.protocol) - ) { - fetch(src, 'blob') - .then(blob => { - track.setAttribute('src', window.URL.createObjectURL(blob)); - }) - .catch(() => { - removeElement(track); - }); - } - }); - } - - // Get and set initial data - // The "preferred" options are not realized unless / until the wanted language has a match - // * languages: Array of user's browser languages. - // * language: The language preferred by user settings or config - // * active: The state preferred by user settings or config - // * toggled: The real captions state - - const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; - const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); - let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); - - // Use first browser language when language is 'auto' - if (language === 'auto') { - [language] = languages; - } - - let active = this.storage.get('captions'); - if (!is.boolean(active)) { - ({ active } = this.config.captions); - } - - Object.assign(this.captions, { - toggled: false, - active, - language, - languages, - }); - - // Watch changes to textTracks and update captions menu - if (this.isHTML5) { - const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; - on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); - } - - // Update available languages in list next tick (the event must not be triggered before the listeners) - setTimeout(captions.update.bind(this), 0); - }, - - // Update available language options in settings based on tracks - update() { - const tracks = captions.getTracks.call(this, true); - // Get the wanted language - const { active, language, meta, currentTrackNode } = this.captions; - const languageExists = Boolean(tracks.find(track => track.language === language)); - - // Handle tracks (add event listener and "pseudo"-default) - if (this.isHTML5 && this.isVideo) { - tracks - .filter(track => !meta.get(track)) - .forEach(track => { - this.debug.log('Track added', track); - // Attempt to store if the original dom element was "default" - meta.set(track, { - default: track.mode === 'showing', - }); - - // Turn off native caption rendering to avoid double captions - // eslint-disable-next-line no-param-reassign - track.mode = 'hidden'; - - // Add event listener for cue changes - on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); - }); - } - - // Update language first time it matches, or if the previous matching track was removed - if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { - captions.setLanguage.call(this, language); - captions.toggle.call(this, active && languageExists); - } + // Setup captions + setup() { + // Requires UI support + if (!this.supported.ui) { + return; + } + + // Only Vimeo and HTML5 video supported at this point + if (!this.isVideo || this.isYouTube || (this.isHTML5 && !support.textTracks)) { + // Clear menu and hide + if ( + is.array(this.config.controls) && + this.config.controls.includes('settings') && + this.config.settings.includes('captions') + ) { + controls.setCaptionsMenu.call(this); + } + + return; + } + + // Inject the container + if (!is.element(this.elements.captions)) { + this.elements.captions = createElement('div', getAttributesFromSelector(this.config.selectors.captions)); + + insertAfter(this.elements.captions, this.elements.wrapper); + } + + // Fix IE captions if CORS is used + // Fetch captions and inject as blobs instead (data URIs not supported!) + if (browser.isIE && window.URL) { + const elements = this.media.querySelectorAll('track'); + + Array.from(elements).forEach(track => { + const src = track.getAttribute('src'); + const url = parseUrl(src); - // Enable or disable captions based on track length - toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); - - // Update available languages in list if ( - is.array(this.config.controls) && - this.config.controls.includes('settings') && - this.config.settings.includes('captions') + url !== null && + url.hostname !== window.location.href.hostname && + ['http:', 'https:'].includes(url.protocol) ) { - controls.setCaptionsMenu.call(this); - } - }, - - // Toggle captions display - // Used internally for the toggleCaptions method, with the passive option forced to false - toggle(input, passive = true) { - // If there's no full support - if (!this.supported.ui) { - return; - } - - const { toggled } = this.captions; // Current state - const activeClass = this.config.classNames.captions.active; - // Get the next state - // If the method is called without parameter, toggle based on current value - const active = is.nullOrUndefined(input) ? !toggled : input; - - // Update state and trigger event - if (active !== toggled) { - // When passive, don't override user preferences - if (!passive) { - this.captions.active = active; - this.storage.set({ captions: active }); - } - - // Force language if the call isn't passive and there is no matching language to toggle to - if (!this.language && active && !passive) { - const tracks = captions.getTracks.call(this); - const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true); - - // Override user preferences to avoid switching languages if a matching track is added - this.captions.language = track.language; - - // Set caption, but don't store in localStorage as user preference - captions.set.call(this, tracks.indexOf(track)); - return; - } - - // Toggle button if it's enabled - if (this.elements.buttons.captions) { - this.elements.buttons.captions.pressed = active; - } - - // Add class hook - toggleClass(this.elements.container, activeClass, active); - - this.captions.toggled = active; - - // Update settings menu - controls.updateSetting.call(this, 'captions'); - - // Trigger event (not used internally) - triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); - } - }, - - // Set captions by track index - // Used internally for the currentTrack setter with the passive option forced to false - set(index, passive = true) { - const tracks = captions.getTracks.call(this); - - // Disable captions if setting to -1 - if (index === -1) { - captions.toggle.call(this, false, passive); - return; - } - - if (!is.number(index)) { - this.debug.warn('Invalid caption argument', index); - return; - } - - if (!(index in tracks)) { - this.debug.warn('Track not found', index); - return; - } - - if (this.captions.currentTrack !== index) { - this.captions.currentTrack = index; - const track = tracks[index]; - const { language } = track || {}; - - // Store reference to node for invalidation on remove - this.captions.currentTrackNode = track; - - // Update settings menu - controls.updateSetting.call(this, 'captions'); - - // When passive, don't override user preferences - if (!passive) { - this.captions.language = language; - this.storage.set({ language }); - } - - // Handle Vimeo captions - if (this.isVimeo) { - this.embed.enableTextTrack(language); - } - - // Trigger event - triggerEvent.call(this, this.media, 'languagechange'); - } - - // Show captions - captions.toggle.call(this, true, passive); - - if (this.isHTML5 && this.isVideo) { - // If we change the active track while a cue is already displayed we need to update it - captions.updateCues.call(this); - } - }, - - // Set captions by language - // Used internally for the language setter with the passive option forced to false - setLanguage(input, passive = true) { - if (!is.string(input)) { - this.debug.warn('Invalid language argument', input); - return; + fetch(src, 'blob') + .then(blob => { + track.setAttribute('src', window.URL.createObjectURL(blob)); + }) + .catch(() => { + removeElement(track); + }); } - // Normalize - const language = input.toLowerCase(); - this.captions.language = language; - - // Set currentTrack - const tracks = captions.getTracks.call(this); - const track = captions.findTrack.call(this, [language]); - captions.set.call(this, tracks.indexOf(track), passive); - }, - - // Get current valid caption tracks - // If update is false it will also ignore tracks without metadata - // This is used to "freeze" the language options when captions.update is false - getTracks(update = false) { - // Handle media or textTracks missing or null - const tracks = Array.from((this.media || {}).textTracks || []); - // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) - // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) - return tracks - .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) - .filter(track => ['captions', 'subtitles'].includes(track.kind)); - }, - - // Match tracks based on languages and get the first - findTrack(languages, force = false) { - const tracks = captions.getTracks.call(this); - const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); - const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); - let track; - - languages.every(language => { - track = sorted.find(t => t.language === language); - return !track; // Break iteration if there is a match + }); + } + + // Get and set initial data + // The "preferred" options are not realized unless / until the wanted language has a match + // * languages: Array of user's browser languages. + // * language: The language preferred by user settings or config + // * active: The state preferred by user settings or config + // * toggled: The real captions state + + const browserLanguages = navigator.languages || [navigator.language || navigator.userLanguage || 'en']; + const languages = dedupe(browserLanguages.map(language => language.split('-')[0])); + let language = (this.storage.get('language') || this.config.captions.language || 'auto').toLowerCase(); + + // Use first browser language when language is 'auto' + if (language === 'auto') { + [language] = languages; + } + + let active = this.storage.get('captions'); + if (!is.boolean(active)) { + ({ active } = this.config.captions); + } + + Object.assign(this.captions, { + toggled: false, + active, + language, + languages, + }); + + // Watch changes to textTracks and update captions menu + if (this.isHTML5) { + const trackEvents = this.config.captions.update ? 'addtrack removetrack' : 'removetrack'; + on.call(this, this.media.textTracks, trackEvents, captions.update.bind(this)); + } + + // Update available languages in list next tick (the event must not be triggered before the listeners) + setTimeout(captions.update.bind(this), 0); + }, + + // Update available language options in settings based on tracks + update() { + const tracks = captions.getTracks.call(this, true); + // Get the wanted language + const { active, language, meta, currentTrackNode } = this.captions; + const languageExists = Boolean(tracks.find(track => track.language === language)); + + // Handle tracks (add event listener and "pseudo"-default) + if (this.isHTML5 && this.isVideo) { + tracks + .filter(track => !meta.get(track)) + .forEach(track => { + this.debug.log('Track added', track); + // Attempt to store if the original dom element was "default" + meta.set(track, { + default: track.mode === 'showing', + }); + + // Turn off native caption rendering to avoid double captions + // eslint-disable-next-line no-param-reassign + track.mode = 'hidden'; + + // Add event listener for cue changes + on.call(this, track, 'cuechange', () => captions.updateCues.call(this)); }); + } + + // Update language first time it matches, or if the previous matching track was removed + if ((languageExists && this.language !== language) || !tracks.includes(currentTrackNode)) { + captions.setLanguage.call(this, language); + captions.toggle.call(this, active && languageExists); + } + + // Enable or disable captions based on track length + toggleClass(this.elements.container, this.config.classNames.captions.enabled, !is.empty(tracks)); + + // Update available languages in list + if ( + is.array(this.config.controls) && + this.config.controls.includes('settings') && + this.config.settings.includes('captions') + ) { + controls.setCaptionsMenu.call(this); + } + }, + + // Toggle captions display + // Used internally for the toggleCaptions method, with the passive option forced to false + toggle(input, passive = true) { + // If there's no full support + if (!this.supported.ui) { + return; + } + + const { toggled } = this.captions; // Current state + const activeClass = this.config.classNames.captions.active; + // Get the next state + // If the method is called without parameter, toggle based on current value + const active = is.nullOrUndefined(input) ? !toggled : input; + + // Update state and trigger event + if (active !== toggled) { + // When passive, don't override user preferences + if (!passive) { + this.captions.active = active; + this.storage.set({ captions: active }); + } + + // Force language if the call isn't passive and there is no matching language to toggle to + if (!this.language && active && !passive) { + const tracks = captions.getTracks.call(this); + const track = captions.findTrack.call(this, [this.captions.language, ...this.captions.languages], true); - // If no match is found but is required, get first - return track || (force ? sorted[0] : undefined); - }, - - // Get the current track - getCurrentTrack() { - return captions.getTracks.call(this)[this.currentTrack]; - }, - - // Get UI label for track - getLabel(track) { - let currentTrack = track; + // Override user preferences to avoid switching languages if a matching track is added + this.captions.language = track.language; - if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { - currentTrack = captions.getCurrentTrack.call(this); - } + // Set caption, but don't store in localStorage as user preference + captions.set.call(this, tracks.indexOf(track)); + return; + } - if (is.track(currentTrack)) { - if (!is.empty(currentTrack.label)) { - return currentTrack.label; - } + // Toggle button if it's enabled + if (this.elements.buttons.captions) { + this.elements.buttons.captions.pressed = active; + } - if (!is.empty(currentTrack.language)) { - return track.language.toUpperCase(); - } + // Add class hook + toggleClass(this.elements.container, activeClass, active); - return i18n.get('enabled', this.config); - } + this.captions.toggled = active; - return i18n.get('disabled', this.config); - }, + // Update settings menu + controls.updateSetting.call(this, 'captions'); - // Update captions using current track's active cues - // Also optional array argument in case there isn't any track (ex: vimeo) - updateCues(input) { - // Requires UI - if (!this.supported.ui) { - return; - } + // Trigger event (not used internally) + triggerEvent.call(this, this.media, active ? 'captionsenabled' : 'captionsdisabled'); + } + }, - if (!is.element(this.elements.captions)) { - this.debug.warn('No captions element to render to'); - return; - } + // Set captions by track index + // Used internally for the currentTrack setter with the passive option forced to false + set(index, passive = true) { + const tracks = captions.getTracks.call(this); - // Only accept array or empty input - if (!is.nullOrUndefined(input) && !Array.isArray(input)) { - this.debug.warn('updateCues: Invalid input', input); - return; - } + // Disable captions if setting to -1 + if (index === -1) { + captions.toggle.call(this, false, passive); + return; + } - let cues = input; + if (!is.number(index)) { + this.debug.warn('Invalid caption argument', index); + return; + } - // Get cues from track - if (!cues) { - const track = captions.getCurrentTrack.call(this); + if (!(index in tracks)) { + this.debug.warn('Track not found', index); + return; + } - cues = Array.from((track || {}).activeCues || []) - .map(cue => cue.getCueAsHTML()) - .map(getHTML); - } + if (this.captions.currentTrack !== index) { + this.captions.currentTrack = index; + const track = tracks[index]; + const { language } = track || {}; - // Set new caption text - const content = cues.map(cueText => cueText.trim()).join('\n'); - const changed = content !== this.elements.captions.innerHTML; + // Store reference to node for invalidation on remove + this.captions.currentTrackNode = track; - if (changed) { - // Empty the container and create a new child element - emptyElement(this.elements.captions); - const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); - caption.innerHTML = content; - this.elements.captions.appendChild(caption); + // Update settings menu + controls.updateSetting.call(this, 'captions'); - // Trigger event - triggerEvent.call(this, this.media, 'cuechange'); - } - }, + // When passive, don't override user preferences + if (!passive) { + this.captions.language = language; + this.storage.set({ language }); + } + + // Handle Vimeo captions + if (this.isVimeo) { + this.embed.enableTextTrack(language); + } + + // Trigger event + triggerEvent.call(this, this.media, 'languagechange'); + } + + // Show captions + captions.toggle.call(this, true, passive); + + if (this.isHTML5 && this.isVideo) { + // If we change the active track while a cue is already displayed we need to update it + captions.updateCues.call(this); + } + }, + + // Set captions by language + // Used internally for the language setter with the passive option forced to false + setLanguage(input, passive = true) { + if (!is.string(input)) { + this.debug.warn('Invalid language argument', input); + return; + } + // Normalize + const language = input.toLowerCase(); + this.captions.language = language; + + // Set currentTrack + const tracks = captions.getTracks.call(this); + const track = captions.findTrack.call(this, [language]); + captions.set.call(this, tracks.indexOf(track), passive); + }, + + // Get current valid caption tracks + // If update is false it will also ignore tracks without metadata + // This is used to "freeze" the language options when captions.update is false + getTracks(update = false) { + // Handle media or textTracks missing or null + const tracks = Array.from((this.media || {}).textTracks || []); + // For HTML5, use cache instead of current tracks when it exists (if captions.update is false) + // Filter out removed tracks and tracks that aren't captions/subtitles (for example metadata) + return tracks + .filter(track => !this.isHTML5 || update || this.captions.meta.has(track)) + .filter(track => ['captions', 'subtitles'].includes(track.kind)); + }, + + // Match tracks based on languages and get the first + findTrack(languages, force = false) { + const tracks = captions.getTracks.call(this); + const sortIsDefault = track => Number((this.captions.meta.get(track) || {}).default); + const sorted = Array.from(tracks).sort((a, b) => sortIsDefault(b) - sortIsDefault(a)); + let track; + + languages.every(language => { + track = sorted.find(t => t.language === language); + return !track; // Break iteration if there is a match + }); + + // If no match is found but is required, get first + return track || (force ? sorted[0] : undefined); + }, + + // Get the current track + getCurrentTrack() { + return captions.getTracks.call(this)[this.currentTrack]; + }, + + // Get UI label for track + getLabel(track) { + let currentTrack = track; + + if (!is.track(currentTrack) && support.textTracks && this.captions.toggled) { + currentTrack = captions.getCurrentTrack.call(this); + } + + if (is.track(currentTrack)) { + if (!is.empty(currentTrack.label)) { + return currentTrack.label; + } + + if (!is.empty(currentTrack.language)) { + return track.language.toUpperCase(); + } + + return i18n.get('enabled', this.config); + } + + return i18n.get('disabled', this.config); + }, + + // Update captions using current track's active cues + // Also optional array argument in case there isn't any track (ex: vimeo) + updateCues(input) { + // Requires UI + if (!this.supported.ui) { + return; + } + + if (!is.element(this.elements.captions)) { + this.debug.warn('No captions element to render to'); + return; + } + + // Only accept array or empty input + if (!is.nullOrUndefined(input) && !Array.isArray(input)) { + this.debug.warn('updateCues: Invalid input', input); + return; + } + + let cues = input; + + // Get cues from track + if (!cues) { + const track = captions.getCurrentTrack.call(this); + + cues = Array.from((track || {}).activeCues || []) + .map(cue => cue.getCueAsHTML()) + .map(getHTML); + } + + // Set new caption text + const content = cues.map(cueText => cueText.trim()).join('\n'); + const changed = content !== this.elements.captions.innerHTML; + + if (changed) { + // Empty the container and create a new child element + emptyElement(this.elements.captions); + const caption = createElement('span', getAttributesFromSelector(this.config.selectors.caption)); + caption.innerHTML = content; + this.elements.captions.appendChild(caption); + + // Trigger event + triggerEvent.call(this, this.media, 'cuechange'); + } + }, }; export default captions; diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index c299a3c9..bd4052be 100644 --- a/src/js/config/defaults.js +++ b/src/js/config/defaults.js @@ -3,437 +3,437 @@ // ========================================================================== const defaults = { - // Disable + // Disable + enabled: true, + + // Custom media title + title: '', + + // Logging to console + debug: false, + + // Auto play (if supported) + autoplay: false, + + // Only allow one media playing at once (vimeo only) + autopause: true, + + // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present) + // TODO: Remove iosNative fullscreen option in favour of this (logic needs work) + playsinline: true, + + // Default time to skip when rewind/fast forward + seekTime: 10, + + // Default volume + volume: 1, + muted: false, + + // Pass a custom duration + duration: null, + + // Display the media duration on load in the current time position + // If you have opted to display both duration and currentTime, this is ignored + displayDuration: true, + + // Invert the current time to be a countdown + invertTime: true, + + // Clicking the currentTime inverts it's value to show time left rather than elapsed + toggleInvert: true, + + // Force an aspect ratio + // The format must be `'w:h'` (e.g. `'16:9'`) + ratio: null, + + // Click video container to play/pause + clickToPlay: true, + + // Auto hide the controls + hideControls: true, + + // Reset to start when playback ended + resetOnEnd: false, + + // Disable the standard context menu + disableContextMenu: true, + + // Sprite (for icons) + loadSprite: true, + iconPrefix: 'plyr', + iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', + + // Blank video (used to prevent errors on source change) + blankVideo: 'https://cdn.plyr.io/static/blank.mp4', + + // Quality default + quality: { + default: 576, + // The options to display in the UI, if available for the source media + options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], + forced: false, + onChange: null, + }, + + // Set loops + loop: { + active: false, + // start: null, + // end: null, + }, + + // Speed default and options to display + speed: { + selected: 1, + // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x) + options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4], + }, + + // Keyboard shortcut settings + keyboard: { + focused: true, + global: false, + }, + + // Display tooltips + tooltips: { + controls: false, + seek: true, + }, + + // Captions settings + captions: { + active: false, + language: 'auto', + // Listen to new tracks added after Plyr is initialized. + // This is needed for streaming captions, but may result in unselectable options + update: false, + }, + + // Fullscreen settings + fullscreen: { + enabled: true, // Allow fullscreen? + fallback: true, // Fallback using full viewport/window + iosNative: false, // Use the native fullscreen in iOS (disables custom controls) + }, + + // Local storage + storage: { enabled: true, - - // Custom media title - title: '', - - // Logging to console - debug: false, - - // Auto play (if supported) - autoplay: false, - - // Only allow one media playing at once (vimeo only) - autopause: true, - - // Allow inline playback on iOS (this effects YouTube/Vimeo - HTML5 requires the attribute present) - // TODO: Remove iosNative fullscreen option in favour of this (logic needs work) - playsinline: true, - - // Default time to skip when rewind/fast forward - seekTime: 10, - - // Default volume - volume: 1, - muted: false, - - // Pass a custom duration - duration: null, - - // Display the media duration on load in the current time position - // If you have opted to display both duration and currentTime, this is ignored - displayDuration: true, - - // Invert the current time to be a countdown - invertTime: true, - - // Clicking the currentTime inverts it's value to show time left rather than elapsed - toggleInvert: true, - - // Force an aspect ratio - // The format must be `'w:h'` (e.g. `'16:9'`) - ratio: null, - - // Click video container to play/pause - clickToPlay: true, - - // Auto hide the controls - hideControls: true, - - // Reset to start when playback ended - resetOnEnd: false, - - // Disable the standard context menu - disableContextMenu: true, - - // Sprite (for icons) - loadSprite: true, - iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.5.10/plyr.svg', - - // Blank video (used to prevent errors on source change) - blankVideo: 'https://cdn.plyr.io/static/blank.mp4', - - // Quality default - quality: { - default: 576, - // The options to display in the UI, if available for the source media - options: [4320, 2880, 2160, 1440, 1080, 720, 576, 480, 360, 240], - forced: false, - onChange: null, + key: 'plyr', + }, + + // Default controls + controls: [ + 'play-large', + // 'restart', + // 'rewind', + 'play', + // 'fast-forward', + 'progress', + 'current-time', + // 'duration', + 'mute', + 'volume', + 'captions', + 'settings', + 'pip', + 'airplay', + // 'download', + 'fullscreen', + ], + settings: ['captions', 'quality', 'speed'], + + // Localisation + i18n: { + restart: 'Restart', + rewind: 'Rewind {seektime}s', + play: 'Play', + pause: 'Pause', + fastForward: 'Forward {seektime}s', + seek: 'Seek', + seekLabel: '{currentTime} of {duration}', + played: 'Played', + buffered: 'Buffered', + currentTime: 'Current time', + duration: 'Duration', + volume: 'Volume', + mute: 'Mute', + unmute: 'Unmute', + enableCaptions: 'Enable captions', + disableCaptions: 'Disable captions', + download: 'Download', + enterFullscreen: 'Enter fullscreen', + exitFullscreen: 'Exit fullscreen', + frameTitle: 'Player for {title}', + captions: 'Captions', + settings: 'Settings', + pip: 'PIP', + menuBack: 'Go back to previous menu', + speed: 'Speed', + normal: 'Normal', + quality: 'Quality', + loop: 'Loop', + start: 'Start', + end: 'End', + all: 'All', + reset: 'Reset', + disabled: 'Disabled', + enabled: 'Enabled', + advertisement: 'Ad', + qualityBadge: { + 2160: '4K', + 1440: 'HD', + 1080: 'HD', + 720: 'HD', + 576: 'SD', + 480: 'SD', }, + }, - // Set loops - loop: { - active: false, - // start: null, - // end: null, - }, - - // Speed default and options to display - speed: { - selected: 1, - // The options to display in the UI, if available for the source media (e.g. Vimeo and YouTube only support 0.5x-4x) - options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 4], + // URLs + urls: { + download: null, + vimeo: { + sdk: 'https://player.vimeo.com/api/player.js', + iframe: 'https://player.vimeo.com/video/{0}?{1}', + api: 'https://vimeo.com/api/v2/video/{0}.json', }, - - // Keyboard shortcut settings - keyboard: { - focused: true, - global: false, + youtube: { + sdk: 'https://www.youtube.com/iframe_api', + api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', }, - - // Display tooltips - tooltips: { - controls: false, - seek: true, + googleIMA: { + sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', }, - - // Captions settings - captions: { - active: false, - language: 'auto', - // Listen to new tracks added after Plyr is initialized. - // This is needed for streaming captions, but may result in unselectable options - update: false, + }, + + // Custom control listeners + listeners: { + seek: null, + play: null, + pause: null, + restart: null, + rewind: null, + fastForward: null, + mute: null, + volume: null, + captions: null, + download: null, + fullscreen: null, + pip: null, + airplay: null, + speed: null, + quality: null, + loop: null, + language: null, + }, + + // Events to watch and bubble + events: [ + // Events to watch on HTML5 media elements and bubble + // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events + 'ended', + 'progress', + 'stalled', + 'playing', + 'waiting', + 'canplay', + 'canplaythrough', + 'loadstart', + 'loadeddata', + 'loadedmetadata', + 'timeupdate', + 'volumechange', + 'play', + 'pause', + 'error', + 'seeking', + 'seeked', + 'emptied', + 'ratechange', + 'cuechange', + + // Custom events + 'download', + 'enterfullscreen', + 'exitfullscreen', + 'captionsenabled', + 'captionsdisabled', + 'languagechange', + 'controlshidden', + 'controlsshown', + 'ready', + + // YouTube + 'statechange', + + // Quality + 'qualitychange', + + // Ads + 'adsloaded', + 'adscontentpause', + 'adscontentresume', + 'adstarted', + 'adsmidpoint', + 'adscomplete', + 'adsallcomplete', + 'adsimpression', + 'adsclick', + ], + + // Selectors + // Change these to match your template if using custom HTML + selectors: { + editable: 'input, textarea, select, [contenteditable]', + container: '.plyr', + controls: { + container: null, + wrapper: '.plyr__controls', }, - - // Fullscreen settings - fullscreen: { - enabled: true, // Allow fullscreen? - fallback: true, // Fallback using full viewport/window - iosNative: false, // Use the native fullscreen in iOS (disables custom controls) + labels: '[data-plyr]', + buttons: { + play: '[data-plyr="play"]', + pause: '[data-plyr="pause"]', + restart: '[data-plyr="restart"]', + rewind: '[data-plyr="rewind"]', + fastForward: '[data-plyr="fast-forward"]', + mute: '[data-plyr="mute"]', + captions: '[data-plyr="captions"]', + download: '[data-plyr="download"]', + fullscreen: '[data-plyr="fullscreen"]', + pip: '[data-plyr="pip"]', + airplay: '[data-plyr="airplay"]', + settings: '[data-plyr="settings"]', + loop: '[data-plyr="loop"]', }, - - // Local storage - storage: { - enabled: true, - key: 'plyr', + inputs: { + seek: '[data-plyr="seek"]', + volume: '[data-plyr="volume"]', + speed: '[data-plyr="speed"]', + language: '[data-plyr="language"]', + quality: '[data-plyr="quality"]', }, - - // Default controls - controls: [ - 'play-large', - // 'restart', - // 'rewind', - 'play', - // 'fast-forward', - 'progress', - 'current-time', - // 'duration', - 'mute', - 'volume', - 'captions', - 'settings', - 'pip', - 'airplay', - // 'download', - 'fullscreen', - ], - settings: ['captions', 'quality', 'speed'], - - // Localisation - i18n: { - restart: 'Restart', - rewind: 'Rewind {seektime}s', - play: 'Play', - pause: 'Pause', - fastForward: 'Forward {seektime}s', - seek: 'Seek', - seekLabel: '{currentTime} of {duration}', - played: 'Played', - buffered: 'Buffered', - currentTime: 'Current time', - duration: 'Duration', - volume: 'Volume', - mute: 'Mute', - unmute: 'Unmute', - enableCaptions: 'Enable captions', - disableCaptions: 'Disable captions', - download: 'Download', - enterFullscreen: 'Enter fullscreen', - exitFullscreen: 'Exit fullscreen', - frameTitle: 'Player for {title}', - captions: 'Captions', - settings: 'Settings', - pip: 'PIP', - menuBack: 'Go back to previous menu', - speed: 'Speed', - normal: 'Normal', - quality: 'Quality', - loop: 'Loop', - start: 'Start', - end: 'End', - all: 'All', - reset: 'Reset', - disabled: 'Disabled', - enabled: 'Enabled', - advertisement: 'Ad', - qualityBadge: { - 2160: '4K', - 1440: 'HD', - 1080: 'HD', - 720: 'HD', - 576: 'SD', - 480: 'SD', - }, + display: { + currentTime: '.plyr__time--current', + duration: '.plyr__time--duration', + buffer: '.plyr__progress__buffer', + loop: '.plyr__progress__loop', // Used later + volume: '.plyr__volume--display', }, - - // URLs - urls: { - download: null, - vimeo: { - sdk: 'https://player.vimeo.com/api/player.js', - iframe: 'https://player.vimeo.com/video/{0}?{1}', - api: 'https://vimeo.com/api/v2/video/{0}.json', - }, - youtube: { - sdk: 'https://www.youtube.com/iframe_api', - api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}', - }, - googleIMA: { - sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', - }, + progress: '.plyr__progress', + captions: '.plyr__captions', + caption: '.plyr__caption', + }, + + // Class hooks added to the player in different states + classNames: { + type: 'plyr--{0}', + provider: 'plyr--{0}', + video: 'plyr__video-wrapper', + embed: 'plyr__video-embed', + videoFixedRatio: 'plyr__video-wrapper--fixed-ratio', + embedContainer: 'plyr__video-embed__container', + poster: 'plyr__poster', + posterEnabled: 'plyr__poster-enabled', + ads: 'plyr__ads', + control: 'plyr__control', + controlPressed: 'plyr__control--pressed', + playing: 'plyr--playing', + paused: 'plyr--paused', + stopped: 'plyr--stopped', + loading: 'plyr--loading', + hover: 'plyr--hover', + tooltip: 'plyr__tooltip', + cues: 'plyr__cues', + hidden: 'plyr__sr-only', + hideControls: 'plyr--hide-controls', + isIos: 'plyr--is-ios', + isTouch: 'plyr--is-touch', + uiSupported: 'plyr--full-ui', + noTransition: 'plyr--no-transition', + display: { + time: 'plyr__time', }, - - // Custom control listeners - listeners: { - seek: null, - play: null, - pause: null, - restart: null, - rewind: null, - fastForward: null, - mute: null, - volume: null, - captions: null, - download: null, - fullscreen: null, - pip: null, - airplay: null, - speed: null, - quality: null, - loop: null, - language: null, + menu: { + value: 'plyr__menu__value', + badge: 'plyr__badge', + open: 'plyr--menu-open', }, - - // Events to watch and bubble - events: [ - // Events to watch on HTML5 media elements and bubble - // https://developer.mozilla.org/en/docs/Web/Guide/Events/Media_events - 'ended', - 'progress', - 'stalled', - 'playing', - 'waiting', - 'canplay', - 'canplaythrough', - 'loadstart', - 'loadeddata', - 'loadedmetadata', - 'timeupdate', - 'volumechange', - 'play', - 'pause', - 'error', - 'seeking', - 'seeked', - 'emptied', - 'ratechange', - 'cuechange', - - // Custom events - 'download', - 'enterfullscreen', - 'exitfullscreen', - 'captionsenabled', - 'captionsdisabled', - 'languagechange', - 'controlshidden', - 'controlsshown', - 'ready', - - // YouTube - 'statechange', - - // Quality - 'qualitychange', - - // Ads - 'adsloaded', - 'adscontentpause', - 'adscontentresume', - 'adstarted', - 'adsmidpoint', - 'adscomplete', - 'adsallcomplete', - 'adsimpression', - 'adsclick', - ], - - // Selectors - // Change these to match your template if using custom HTML - selectors: { - editable: 'input, textarea, select, [contenteditable]', - container: '.plyr', - controls: { - container: null, - wrapper: '.plyr__controls', - }, - labels: '[data-plyr]', - buttons: { - play: '[data-plyr="play"]', - pause: '[data-plyr="pause"]', - restart: '[data-plyr="restart"]', - rewind: '[data-plyr="rewind"]', - fastForward: '[data-plyr="fast-forward"]', - mute: '[data-plyr="mute"]', - captions: '[data-plyr="captions"]', - download: '[data-plyr="download"]', - fullscreen: '[data-plyr="fullscreen"]', - pip: '[data-plyr="pip"]', - airplay: '[data-plyr="airplay"]', - settings: '[data-plyr="settings"]', - loop: '[data-plyr="loop"]', - }, - inputs: { - seek: '[data-plyr="seek"]', - volume: '[data-plyr="volume"]', - speed: '[data-plyr="speed"]', - language: '[data-plyr="language"]', - quality: '[data-plyr="quality"]', - }, - display: { - currentTime: '.plyr__time--current', - duration: '.plyr__time--duration', - buffer: '.plyr__progress__buffer', - loop: '.plyr__progress__loop', // Used later - volume: '.plyr__volume--display', - }, - progress: '.plyr__progress', - captions: '.plyr__captions', - caption: '.plyr__caption', + captions: { + enabled: 'plyr--captions-enabled', + active: 'plyr--captions-active', }, - - // Class hooks added to the player in different states - classNames: { - type: 'plyr--{0}', - provider: 'plyr--{0}', - video: 'plyr__video-wrapper', - embed: 'plyr__video-embed', - videoFixedRatio: 'plyr__video-wrapper--fixed-ratio', - embedContainer: 'plyr__video-embed__container', - poster: 'plyr__poster', - posterEnabled: 'plyr__poster-enabled', - ads: 'plyr__ads', - control: 'plyr__control', - controlPressed: 'plyr__control--pressed', - playing: 'plyr--playing', - paused: 'plyr--paused', - stopped: 'plyr--stopped', - loading: 'plyr--loading', - hover: 'plyr--hover', - tooltip: 'plyr__tooltip', - cues: 'plyr__cues', - hidden: 'plyr__sr-only', - hideControls: 'plyr--hide-controls', - isIos: 'plyr--is-ios', - isTouch: 'plyr--is-touch', - uiSupported: 'plyr--full-ui', - noTransition: 'plyr--no-transition', - display: { - time: 'plyr__time', - }, - menu: { - value: 'plyr__menu__value', - badge: 'plyr__badge', - open: 'plyr--menu-open', - }, - captions: { - enabled: 'plyr--captions-enabled', - active: 'plyr--captions-active', - }, - fullscreen: { - enabled: 'plyr--fullscreen-enabled', - fallback: 'plyr--fullscreen-fallback', - }, - pip: { - supported: 'plyr--pip-supported', - active: 'plyr--pip-active', - }, - airplay: { - supported: 'plyr--airplay-supported', - active: 'plyr--airplay-active', - }, - tabFocus: 'plyr__tab-focus', - previewThumbnails: { - // Tooltip thumbs - thumbContainer: 'plyr__preview-thumb', - thumbContainerShown: 'plyr__preview-thumb--is-shown', - imageContainer: 'plyr__preview-thumb__image-container', - timeContainer: 'plyr__preview-thumb__time-container', - // Scrubbing - scrubbingContainer: 'plyr__preview-scrubbing', - scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown', - }, + fullscreen: { + enabled: 'plyr--fullscreen-enabled', + fallback: 'plyr--fullscreen-fallback', }, - - // Embed attributes - attributes: { - embed: { - provider: 'data-plyr-provider', - id: 'data-plyr-embed-id', - }, + pip: { + supported: 'plyr--pip-supported', + active: 'plyr--pip-active', }, - - // Advertisements plugin - // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio - ads: { - enabled: false, - publisherId: '', - tagUrl: '', + airplay: { + supported: 'plyr--airplay-supported', + active: 'plyr--airplay-active', }, - - // Preview Thumbnails plugin + tabFocus: 'plyr__tab-focus', previewThumbnails: { - enabled: false, - src: '', + // Tooltip thumbs + thumbContainer: 'plyr__preview-thumb', + thumbContainerShown: 'plyr__preview-thumb--is-shown', + imageContainer: 'plyr__preview-thumb__image-container', + timeContainer: 'plyr__preview-thumb__time-container', + // Scrubbing + scrubbingContainer: 'plyr__preview-scrubbing', + scrubbingContainerShown: 'plyr__preview-scrubbing--is-shown', }, + }, - // Vimeo plugin - vimeo: { - byline: false, - portrait: false, - title: false, - speed: true, - transparent: false, - // These settings require a pro or premium account to work - sidedock: false, - controls: false, - // Custom settings from Plyr - referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy - }, - - // YouTube plugin - youtube: { - noCookie: false, // Whether to use an alternative version of YouTube without cookies - rel: 0, // No related vids - showinfo: 0, // Hide info - iv_load_policy: 3, // Hide annotations - modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) + // Embed attributes + attributes: { + embed: { + provider: 'data-plyr-provider', + id: 'data-plyr-embed-id', }, + }, + + // Advertisements plugin + // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio + ads: { + enabled: false, + publisherId: '', + tagUrl: '', + }, + + // Preview Thumbnails plugin + previewThumbnails: { + enabled: false, + src: '', + }, + + // Vimeo plugin + vimeo: { + byline: false, + portrait: false, + title: false, + speed: true, + transparent: false, + // These settings require a pro or premium account to work + sidedock: false, + controls: false, + // Custom settings from Plyr + referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy + }, + + // YouTube plugin + youtube: { + noCookie: false, // Whether to use an alternative version of YouTube without cookies + rel: 0, // No related vids + showinfo: 0, // Hide info + iv_load_policy: 3, // Hide annotations + modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused) + }, }; export default defaults; diff --git a/src/js/config/states.js b/src/js/config/states.js index 7dd1476b..1c1618e4 100644 --- a/src/js/config/states.js +++ b/src/js/config/states.js @@ -3,8 +3,8 @@ // ========================================================================== export const pip = { - active: 'picture-in-picture', - inactive: 'inline', + active: 'picture-in-picture', + inactive: 'inline', }; export default { pip }; diff --git a/src/js/config/types.js b/src/js/config/types.js index e0ccdaff..31e488eb 100644 --- a/src/js/config/types.js +++ b/src/js/config/types.js @@ -3,14 +3,14 @@ // ========================================================================== export const providers = { - html5: 'html5', - youtube: 'youtube', - vimeo: 'vimeo', + html5: 'html5', + youtube: 'youtube', + vimeo: 'vimeo', }; export const types = { - audio: 'audio', - video: 'video', + audio: 'audio', + video: 'video', }; /** @@ -18,17 +18,17 @@ export const types = { * @param {String} url */ export function getProviderByUrl(url) { - // YouTube - if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { - return providers.youtube; - } + // YouTube + if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtube-nocookie\.com|youtu\.?be)\/.+$/.test(url)) { + return providers.youtube; + } - // Vimeo - if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { - return providers.vimeo; - } + // Vimeo + if (/^https?:\/\/player.vimeo.com\/video\/\d{0,9}(?=\b|\/)/.test(url)) { + return providers.vimeo; + } - return null; + return null; } export default { providers, types }; diff --git a/src/js/console.js b/src/js/console.js index e8099569..f9d734aa 100644 --- a/src/js/console.js +++ b/src/js/console.js @@ -5,26 +5,26 @@ const noop = () => {}; export default class Console { - constructor(enabled = false) { - this.enabled = window.console && enabled; + constructor(enabled = false) { + this.enabled = window.console && enabled; - if (this.enabled) { - this.log('Debugging enabled'); - } + if (this.enabled) { + this.log('Debugging enabled'); } + } - get log() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; - } + get log() { + // eslint-disable-next-line no-console + return this.enabled ? Function.prototype.bind.call(console.log, console) : noop; + } - get warn() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; - } + get warn() { + // eslint-disable-next-line no-console + return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; + } - get error() { - // eslint-disable-next-line no-console - return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; - } + get error() { + // eslint-disable-next-line no-console + return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; + } } diff --git a/src/js/controls.js b/src/js/controls.js index 37df497f..5bdb5b0a 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -12,18 +12,18 @@ import { repaint, transitionEndEvent } from './utils/animation'; import { dedupe } from './utils/arrays'; import browser from './utils/browser'; import { - createElement, - emptyElement, - getAttributesFromSelector, - getElement, - getElements, - hasClass, - matches, - removeElement, - setAttributes, - setFocus, - toggleClass, - toggleHidden, + createElement, + emptyElement, + getAttributesFromSelector, + getElement, + getElements, + hasClass, + matches, + removeElement, + setAttributes, + setFocus, + toggleClass, + toggleHidden, } from './utils/elements'; import { off, on } from './utils/events'; import i18n from './utils/i18n'; @@ -35,917 +35,915 @@ import { formatTime, getHours } from './utils/time'; // TODO: Don't export a massive object - break down and create class const controls = { - // Get icon URL - getIconUrl() { - const url = new URL(this.config.iconUrl, window.location); - const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody); - - return { - url: this.config.iconUrl, - cors, - }; - }, - - // Find the UI controls - findElements() { - try { - this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); - - // Buttons - this.elements.buttons = { - play: getElements.call(this, this.config.selectors.buttons.play), - pause: getElement.call(this, this.config.selectors.buttons.pause), - restart: getElement.call(this, this.config.selectors.buttons.restart), - rewind: getElement.call(this, this.config.selectors.buttons.rewind), - fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), - mute: getElement.call(this, this.config.selectors.buttons.mute), - pip: getElement.call(this, this.config.selectors.buttons.pip), - airplay: getElement.call(this, this.config.selectors.buttons.airplay), - settings: getElement.call(this, this.config.selectors.buttons.settings), - captions: getElement.call(this, this.config.selectors.buttons.captions), - fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), - }; - - // Progress - this.elements.progress = getElement.call(this, this.config.selectors.progress); - - // Inputs - this.elements.inputs = { - seek: getElement.call(this, this.config.selectors.inputs.seek), - volume: getElement.call(this, this.config.selectors.inputs.volume), - }; - - // Display - this.elements.display = { - buffer: getElement.call(this, this.config.selectors.display.buffer), - currentTime: getElement.call(this, this.config.selectors.display.currentTime), - duration: getElement.call(this, this.config.selectors.display.duration), - }; - - // Seek tooltip - if (is.element(this.elements.progress)) { - this.elements.display.seekTooltip = this.elements.progress.querySelector( - `.${this.config.classNames.tooltip}`, - ); - } - - return true; - } catch (error) { - // Log it - this.debug.warn('It looks like there is a problem with your custom controls HTML', error); - - // Restore native video controls - this.toggleNativeControls(true); - - return false; - } - }, - - // Create icon - createIcon(type, attributes) { - const namespace = 'http://www.w3.org/2000/svg'; - const iconUrl = controls.getIconUrl.call(this); - const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; - // Create - const icon = document.createElementNS(namespace, 'svg'); - setAttributes( - icon, - extend(attributes, { - 'aria-hidden': 'true', - focusable: 'false', - }), - ); - - // Create the to reference sprite - const use = document.createElementNS(namespace, 'use'); - const path = `${iconPath}-${type}`; - - // Set `href` attributes - // https://github.com/sampotts/plyr/issues/460 - // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href - if ('href' in use) { - use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); - } - - // Always set the older attribute even though it's "deprecated" (it'll be around for ages) - use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); - - // Add to - icon.appendChild(use); - - return icon; - }, - - // Create hidden text label - createLabel(key, attr = {}) { - const text = i18n.get(key, this.config); - const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') }; - - return createElement('span', attributes, text); - }, - - // Create a badge - createBadge(text) { - if (is.empty(text)) { - return null; - } - - const badge = createElement('span', { - class: this.config.classNames.menu.value, + // Get icon URL + getIconUrl() { + const url = new URL(this.config.iconUrl, window.location); + const cors = url.host !== window.location.host || (browser.isIE && !window.svg4everybody); + + return { + url: this.config.iconUrl, + cors, + }; + }, + + // Find the UI controls + findElements() { + try { + this.elements.controls = getElement.call(this, this.config.selectors.controls.wrapper); + + // Buttons + this.elements.buttons = { + play: getElements.call(this, this.config.selectors.buttons.play), + pause: getElement.call(this, this.config.selectors.buttons.pause), + restart: getElement.call(this, this.config.selectors.buttons.restart), + rewind: getElement.call(this, this.config.selectors.buttons.rewind), + fastForward: getElement.call(this, this.config.selectors.buttons.fastForward), + mute: getElement.call(this, this.config.selectors.buttons.mute), + pip: getElement.call(this, this.config.selectors.buttons.pip), + airplay: getElement.call(this, this.config.selectors.buttons.airplay), + settings: getElement.call(this, this.config.selectors.buttons.settings), + captions: getElement.call(this, this.config.selectors.buttons.captions), + fullscreen: getElement.call(this, this.config.selectors.buttons.fullscreen), + }; + + // Progress + this.elements.progress = getElement.call(this, this.config.selectors.progress); + + // Inputs + this.elements.inputs = { + seek: getElement.call(this, this.config.selectors.inputs.seek), + volume: getElement.call(this, this.config.selectors.inputs.volume), + }; + + // Display + this.elements.display = { + buffer: getElement.call(this, this.config.selectors.display.buffer), + currentTime: getElement.call(this, this.config.selectors.display.currentTime), + duration: getElement.call(this, this.config.selectors.display.duration), + }; + + // Seek tooltip + if (is.element(this.elements.progress)) { + this.elements.display.seekTooltip = this.elements.progress.querySelector(`.${this.config.classNames.tooltip}`); + } + + return true; + } catch (error) { + // Log it + this.debug.warn('It looks like there is a problem with your custom controls HTML', error); + + // Restore native video controls + this.toggleNativeControls(true); + + return false; + } + }, + + // Create icon + createIcon(type, attributes) { + const namespace = 'http://www.w3.org/2000/svg'; + const iconUrl = controls.getIconUrl.call(this); + const iconPath = `${!iconUrl.cors ? iconUrl.url : ''}#${this.config.iconPrefix}`; + // Create + const icon = document.createElementNS(namespace, 'svg'); + setAttributes( + icon, + extend(attributes, { + 'aria-hidden': 'true', + focusable: 'false', + }), + ); + + // Create the to reference sprite + const use = document.createElementNS(namespace, 'use'); + const path = `${iconPath}-${type}`; + + // Set `href` attributes + // https://github.com/sampotts/plyr/issues/460 + // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/xlink:href + if ('href' in use) { + use.setAttributeNS('http://www.w3.org/1999/xlink', 'href', path); + } + + // Always set the older attribute even though it's "deprecated" (it'll be around for ages) + use.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', path); + + // Add to + icon.appendChild(use); + + return icon; + }, + + // Create hidden text label + createLabel(key, attr = {}) { + const text = i18n.get(key, this.config); + const attributes = { ...attr, class: [attr.class, this.config.classNames.hidden].filter(Boolean).join(' ') }; + + return createElement('span', attributes, text); + }, + + // Create a badge + createBadge(text) { + if (is.empty(text)) { + return null; + } + + const badge = createElement('span', { + class: this.config.classNames.menu.value, + }); + + badge.appendChild( + createElement( + 'span', + { + class: this.config.classNames.menu.badge, + }, + text, + ), + ); + + return badge; + }, + + // Create a
if needed - if (is.empty(source)) { - source = player.media.getAttribute(player.config.attributes.embed.id); - } - - const id = parseId(source); - // Build an iframe - const iframe = createElement('iframe'); - const src = format(player.config.urls.vimeo.iframe, id, params); - iframe.setAttribute('src', src); - iframe.setAttribute('allowfullscreen', ''); - iframe.setAttribute('allowtransparency', ''); - iframe.setAttribute('allow', 'autoplay'); - - // Set the referrer policy if required - if (!is.empty(config.referrerPolicy)) { - iframe.setAttribute('referrerPolicy', config.referrerPolicy); - } - - // Get poster, if already set - const { poster } = player; - // Inject the package - const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); - wrapper.appendChild(iframe); - player.media = replaceElement(wrapper, player.media); - - // Get poster image - fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { - if (is.empty(response)) { - return; - } - - // Get the URL for thumbnail - const url = new URL(response[0].thumbnail_large); - - // Get original image - url.pathname = `${url.pathname.split('_')[0]}.jpg`; - - // Set and show poster - ui.setPoster.call(player, url.href).catch(() => {}); - }); - - // Setup instance - // https://github.com/vimeo/player.js - player.embed = new window.Vimeo.Player(iframe, { - autopause: player.config.autopause, - muted: player.muted, - }); - - player.media.paused = true; - player.media.currentTime = 0; - - // Disable native text track rendering - if (player.supported.ui) { - player.embed.disableTextTrack(); - } - - // Create a faux HTML5 API using the Vimeo API - player.media.play = () => { - assurePlaybackState.call(player, true); - return player.embed.play(); - }; - - player.media.pause = () => { - assurePlaybackState.call(player, false); - return player.embed.pause(); - }; - - player.media.stop = () => { - player.pause(); - player.currentTime = 0; - }; - - // Seeking - let { currentTime } = player.media; - Object.defineProperty(player.media, 'currentTime', { - get() { - return currentTime; - }, - set(time) { - // Vimeo will automatically play on seek if the video hasn't been played before - - // Get current paused state and volume etc - const { embed, media, paused, volume } = player; - const restorePause = paused && !embed.hasPlayed; - - // Set seeking state and trigger event - media.seeking = true; - triggerEvent.call(player, media, 'seeking'); - - // If paused, mute until seek is complete - Promise.resolve(restorePause && embed.setVolume(0)) - // Seek - .then(() => embed.setCurrentTime(time)) - // Restore paused - .then(() => restorePause && embed.pause()) - // Restore volume - .then(() => restorePause && embed.setVolume(volume)) - .catch(() => { - // Do nothing - }); - }, - }); - - // Playback speed - let speed = player.config.speed.selected; - Object.defineProperty(player.media, 'playbackRate', { - get() { - return speed; - }, - set(input) { - player.embed - .setPlaybackRate(input) - .then(() => { - speed = input; - triggerEvent.call(player, player.media, 'ratechange'); - }) - .catch(() => { - // Cannot set Playback Rate, Video is probably not on Pro account - player.options.speed = [1]; - }); - }, - }); - - // Volume - let { volume } = player.config; - Object.defineProperty(player.media, 'volume', { - get() { - return volume; - }, - set(input) { - player.embed.setVolume(input).then(() => { - volume = input; - triggerEvent.call(player, player.media, 'volumechange'); - }); - }, + setup() { + const player = this; + + // Add embed class for responsive + toggleClass(player.elements.wrapper, player.config.classNames.embed, true); + + // Set speed options from config + player.options.speed = player.config.speed.options; + + // Set intial ratio + setAspectRatio.call(player); + + // Load the SDK if not already + if (!is.object(window.Vimeo)) { + loadScript(player.config.urls.vimeo.sdk) + .then(() => { + vimeo.ready.call(player); + }) + .catch(error => { + player.debug.warn('Vimeo SDK (player.js) failed to load', error); }); + } else { + vimeo.ready.call(player); + } + }, + + // API Ready + ready() { + const player = this; + const config = player.config.vimeo; + + // Get Vimeo params for the iframe + const params = buildUrlParams( + extend( + {}, + { + loop: player.config.loop.active, + autoplay: player.autoplay, + muted: player.muted, + gesture: 'media', + playsinline: !this.config.fullscreen.iosNative, + }, + config, + ), + ); + + // Get the source URL or ID + let source = player.media.getAttribute('src'); + + // Get from
if needed + if (is.empty(source)) { + source = player.media.getAttribute(player.config.attributes.embed.id); + } - // Muted - let { muted } = player.config; - Object.defineProperty(player.media, 'muted', { - get() { - return muted; - }, - set(input) { - const toggle = is.boolean(input) ? input : false; - - player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { - muted = toggle; - triggerEvent.call(player, player.media, 'volumechange'); - }); - }, - }); + const id = parseId(source); + // Build an iframe + const iframe = createElement('iframe'); + const src = format(player.config.urls.vimeo.iframe, id, params); + iframe.setAttribute('src', src); + iframe.setAttribute('allowfullscreen', ''); + iframe.setAttribute('allowtransparency', ''); + iframe.setAttribute('allow', 'autoplay'); + + // Set the referrer policy if required + if (!is.empty(config.referrerPolicy)) { + iframe.setAttribute('referrerPolicy', config.referrerPolicy); + } - // Loop - let { loop } = player.config; - Object.defineProperty(player.media, 'loop', { - get() { - return loop; - }, - set(input) { - const toggle = is.boolean(input) ? input : player.config.loop.active; - - player.embed.setLoop(toggle).then(() => { - loop = toggle; - }); - }, - }); + // Get poster, if already set + const { poster } = player; + // Inject the package + const wrapper = createElement('div', { poster, class: player.config.classNames.embedContainer }); + wrapper.appendChild(iframe); + player.media = replaceElement(wrapper, player.media); + + // Get poster image + fetch(format(player.config.urls.vimeo.api, id), 'json').then(response => { + if (is.empty(response)) { + return; + } + + // Get the URL for thumbnail + const url = new URL(response[0].thumbnail_large); + + // Get original image + url.pathname = `${url.pathname.split('_')[0]}.jpg`; + + // Set and show poster + ui.setPoster.call(player, url.href).catch(() => {}); + }); + + // Setup instance + // https://github.com/vimeo/player.js + player.embed = new window.Vimeo.Player(iframe, { + autopause: player.config.autopause, + muted: player.muted, + }); + + player.media.paused = true; + player.media.currentTime = 0; + + // Disable native text track rendering + if (player.supported.ui) { + player.embed.disableTextTrack(); + } - // Source - let currentSrc; + // Create a faux HTML5 API using the Vimeo API + player.media.play = () => { + assurePlaybackState.call(player, true); + return player.embed.play(); + }; + + player.media.pause = () => { + assurePlaybackState.call(player, false); + return player.embed.pause(); + }; + + player.media.stop = () => { + player.pause(); + player.currentTime = 0; + }; + + // Seeking + let { currentTime } = player.media; + Object.defineProperty(player.media, 'currentTime', { + get() { + return currentTime; + }, + set(time) { + // Vimeo will automatically play on seek if the video hasn't been played before + + // Get current paused state and volume etc + const { embed, media, paused, volume } = player; + const restorePause = paused && !embed.hasPlayed; + + // Set seeking state and trigger event + media.seeking = true; + triggerEvent.call(player, media, 'seeking'); + + // If paused, mute until seek is complete + Promise.resolve(restorePause && embed.setVolume(0)) + // Seek + .then(() => embed.setCurrentTime(time)) + // Restore paused + .then(() => restorePause && embed.pause()) + // Restore volume + .then(() => restorePause && embed.setVolume(volume)) + .catch(() => { + // Do nothing + }); + }, + }); + + // Playback speed + let speed = player.config.speed.selected; + Object.defineProperty(player.media, 'playbackRate', { + get() { + return speed; + }, + set(input) { player.embed - .getVideoUrl() - .then(value => { - currentSrc = value; - controls.setDownloadUrl.call(player); - }) - .catch(error => { - this.debug.warn(error); - }); - - Object.defineProperty(player.media, 'currentSrc', { - get() { - return currentSrc; - }, + .setPlaybackRate(input) + .then(() => { + speed = input; + triggerEvent.call(player, player.media, 'ratechange'); + }) + .catch(() => { + // Cannot set Playback Rate, Video is probably not on Pro account + player.options.speed = [1]; + }); + }, + }); + + // Volume + let { volume } = player.config; + Object.defineProperty(player.media, 'volume', { + get() { + return volume; + }, + set(input) { + player.embed.setVolume(input).then(() => { + volume = input; + triggerEvent.call(player, player.media, 'volumechange'); }); - - // Ended - Object.defineProperty(player.media, 'ended', { - get() { - return player.currentTime === player.duration; - }, - }); - - // Set aspect ratio based on video size - Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { - const [width, height] = dimensions; - player.embed.ratio = [width, height]; - setAspectRatio.call(this); + }, + }); + + // Muted + let { muted } = player.config; + Object.defineProperty(player.media, 'muted', { + get() { + return muted; + }, + set(input) { + const toggle = is.boolean(input) ? input : false; + + player.embed.setVolume(toggle ? 0 : player.config.volume).then(() => { + muted = toggle; + triggerEvent.call(player, player.media, 'volumechange'); }); - - // Set autopause - player.embed.setAutopause(player.config.autopause).then(state => { - player.config.autopause = state; + }, + }); + + // Loop + let { loop } = player.config; + Object.defineProperty(player.media, 'loop', { + get() { + return loop; + }, + set(input) { + const toggle = is.boolean(input) ? input : player.config.loop.active; + + player.embed.setLoop(toggle).then(() => { + loop = toggle; }); - - // Get title - player.embed.getVideoTitle().then(title => { - player.config.title = title; - ui.setTitle.call(this); - }); - - // Get current time - player.embed.getCurrentTime().then(value => { - currentTime = value; - triggerEvent.call(player, player.media, 'timeupdate'); - }); - - // Get duration - player.embed.getDuration().then(value => { - player.media.duration = value; - triggerEvent.call(player, player.media, 'durationchange'); - }); - - // Get captions - player.embed.getTextTracks().then(tracks => { - player.media.textTracks = tracks; - captions.setup.call(player); - }); - - player.embed.on('cuechange', ({ cues = [] }) => { - const strippedCues = cues.map(cue => stripHTML(cue.text)); - captions.updateCues.call(player, strippedCues); - }); - - player.embed.on('loaded', () => { - // Assure state and events are updated on autoplay - player.embed.getPaused().then(paused => { - assurePlaybackState.call(player, !paused); - if (!paused) { - triggerEvent.call(player, player.media, 'playing'); - } - }); - - if (is.element(player.embed.element) && player.supported.ui) { - const frame = player.embed.element; - - // Fix keyboard focus issues - // https://github.com/sampotts/plyr/issues/317 - frame.setAttribute('tabindex', -1); - } - }); - - player.embed.on('bufferstart', () => { - triggerEvent.call(player, player.media, 'waiting'); - }); - - player.embed.on('bufferend', () => { - triggerEvent.call(player, player.media, 'playing'); - }); - - player.embed.on('play', () => { - assurePlaybackState.call(player, true); - triggerEvent.call(player, player.media, 'playing'); - }); - - player.embed.on('pause', () => { - assurePlaybackState.call(player, false); - }); - - player.embed.on('timeupdate', data => { - player.media.seeking = false; - currentTime = data.seconds; - triggerEvent.call(player, player.media, 'timeupdate'); - }); - - player.embed.on('progress', data => { - player.media.buffered = data.percent; - triggerEvent.call(player, player.media, 'progress'); - - // Check all loaded - if (parseInt(data.percent, 10) === 1) { - triggerEvent.call(player, player.media, 'canplaythrough'); - } - - // Get duration as if we do it before load, it gives an incorrect value - // https://github.com/sampotts/plyr/issues/891 - player.embed.getDuration().then(value => { - if (value !== player.media.duration) { - player.media.duration = value; - triggerEvent.call(player, player.media, 'durationchange'); - } - }); - }); - - player.embed.on('seeked', () => { - player.media.seeking = false; - triggerEvent.call(player, player.media, 'seeked'); - }); - - player.embed.on('ended', () => { - player.media.paused = true; - triggerEvent.call(player, player.media, 'ended'); - }); - - player.embed.on('error', detail => { - player.media.error = detail; - triggerEvent.call(player, player.media, 'error'); - }); - - // Rebuild UI - setTimeout(() => ui.build.call(player), 0); - }, + }, + }); + + // Source + let currentSrc; + player.embed + .getVideoUrl() + .then(value => { + currentSrc = value; + controls.setDownloadUrl.call(player); + }) + .catch(error => { + this.debug.warn(error); + }); + + Object.defineProperty(player.media, 'currentSrc', { + get() { + return currentSrc; + }, + }); + + // Ended + Object.defineProperty(player.media, 'ended', { + get() { + return player.currentTime === player.duration; + }, + }); + + // Set aspect ratio based on video size + Promise.all([player.embed.getVideoWidth(), player.embed.getVideoHeight()]).then(dimensions => { + const [width, height] = dimensions; + player.embed.ratio = [width, height]; + setAspectRatio.call(this); + }); + + // Set autopause + player.embed.setAutopause(player.config.autopause).then(state => { + player.config.autopause = state; + }); + + // Get title + player.embed.getVideoTitle().then(title => { + player.config.title = title; + ui.setTitle.call(this); + }); + + // Get current time + player.embed.getCurrentTime().then(value => { + currentTime = value; + triggerEvent.call(player, player.media, 'timeupdate'); + }); + + // Get duration + player.embed.getDuration().then(value => { + player.media.duration = value; + triggerEvent.call(player, player.media, 'durationchange'); + }); + + // Get captions + player.embed.getTextTracks().then(tracks => { + player.media.textTracks = tracks; + captions.setup.call(player); + }); + + player.embed.on('cuechange', ({ cues = [] }) => { + const strippedCues = cues.map(cue => stripHTML(cue.text)); + captions.updateCues.call(player, strippedCues); + }); + + player.embed.on('loaded', () => { + // Assure state and events are updated on autoplay + player.embed.getPaused().then(paused => { + assurePlaybackState.call(player, !paused); + if (!paused) { + triggerEvent.call(player, player.media, 'playing'); + } + }); + + if (is.element(player.embed.element) && player.supported.ui) { + const frame = player.embed.element; + + // Fix keyboard focus issues + // https://github.com/sampotts/plyr/issues/317 + frame.setAttribute('tabindex', -1); + } + }); + + player.embed.on('bufferstart', () => { + triggerEvent.call(player, player.media, 'waiting'); + }); + + player.embed.on('bufferend', () => { + triggerEvent.call(player, player.media, 'playing'); + }); + + player.embed.on('play', () => { + assurePlaybackState.call(player, true); + triggerEvent.call(player, player.media, 'playing'); + }); + + player.embed.on('pause', () => { + assurePlaybackState.call(player, false); + }); + + player.embed.on('timeupdate', data => { + player.media.seeking = false; + currentTime = data.seconds; + triggerEvent.call(player, player.media, 'timeupdate'); + }); + + player.embed.on('progress', data => { + player.media.buffered = data.percent; + triggerEvent.call(player, player.media, 'progress'); + + // Check all loaded + if (parseInt(data.percent, 10) === 1) { + triggerEvent.call(player, player.media, 'canplaythrough'); + } + + // Get duration as if we do it before load, it gives an incorrect value + // https://github.com/sampotts/plyr/issues/891 + player.embed.getDuration().then(value => { + if (value !== player.media.duration) { + player.media.duration = value; + triggerEvent.call(player, player.media, 'durationchange'); + } + }); + }); + + player.embed.on('seeked', () => { + player.media.seeking = false; + triggerEvent.call(player, player.media, 'seeked'); + }); + + player.embed.on('ended', () => { + player.media.paused = true; + triggerEvent.call(player, player.media, 'ended'); + }); + + player.embed.on('error', detail => { + player.media.error = detail; + triggerEvent.call(player, player.media, 'error'); + }); + + // Rebuild UI + setTimeout(() => ui.build.call(player), 0); + }, }; export default vimeo; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 8c65b1dc..4de46395 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -15,426 +15,426 @@ import { setAspectRatio } from '../utils/style'; // Parse YouTube ID from URL function parseId(url) { - if (is.empty(url)) { - return null; - } + if (is.empty(url)) { + return null; + } - const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - return url.match(regex) ? RegExp.$2 : url; + const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; + return url.match(regex) ? RegExp.$2 : url; } // Set playback state and trigger change (only on actual change) function assurePlaybackState(play) { - if (play && !this.embed.hasPlayed) { - this.embed.hasPlayed = true; - } - if (this.media.paused === play) { - this.media.paused = !play; - triggerEvent.call(this, this.media, play ? 'play' : 'pause'); - } + if (play && !this.embed.hasPlayed) { + this.embed.hasPlayed = true; + } + if (this.media.paused === play) { + this.media.paused = !play; + triggerEvent.call(this, this.media, play ? 'play' : 'pause'); + } } function getHost(config) { - if (config.noCookie) { - return 'https://www.youtube-nocookie.com'; - } + if (config.noCookie) { + return 'https://www.youtube-nocookie.com'; + } - if (window.location.protocol === 'http:') { - return 'http://www.youtube.com'; - } + if (window.location.protocol === 'http:') { + return 'http://www.youtube.com'; + } - // Use YouTube's default - return undefined; + // Use YouTube's default + return undefined; } const youtube = { - setup() { - // Add embed class for responsive - toggleClass(this.elements.wrapper, this.config.classNames.embed, true); - - // Setup API - if (is.object(window.YT) && is.function(window.YT.Player)) { - youtube.ready.call(this); - } else { - // Reference current global callback - const callback = window.onYouTubeIframeAPIReady; - - // Set callback to process queue - window.onYouTubeIframeAPIReady = () => { - // Call global callback if set - if (is.function(callback)) { - callback(); - } - - youtube.ready.call(this); - }; - - // Load the SDK - loadScript(this.config.urls.youtube.sdk).catch(error => { - this.debug.warn('YouTube API failed to load', error); - }); + setup() { + // Add embed class for responsive + toggleClass(this.elements.wrapper, this.config.classNames.embed, true); + + // Setup API + if (is.object(window.YT) && is.function(window.YT.Player)) { + youtube.ready.call(this); + } else { + // Reference current global callback + const callback = window.onYouTubeIframeAPIReady; + + // Set callback to process queue + window.onYouTubeIframeAPIReady = () => { + // Call global callback if set + if (is.function(callback)) { + callback(); } - }, - // Get the media title - getTitle(videoId) { - const url = format(this.config.urls.youtube.api, videoId); + youtube.ready.call(this); + }; - fetch(url) - .then(data => { - if (is.object(data)) { - const { title, height, width } = data; + // Load the SDK + loadScript(this.config.urls.youtube.sdk).catch(error => { + this.debug.warn('YouTube API failed to load', error); + }); + } + }, - // Set title - this.config.title = title; - ui.setTitle.call(this); + // Get the media title + getTitle(videoId) { + const url = format(this.config.urls.youtube.api, videoId); - // Set aspect ratio - this.embed.ratio = [width, height]; - } + fetch(url) + .then(data => { + if (is.object(data)) { + const { title, height, width } = data; - setAspectRatio.call(this); - }) - .catch(() => { - // Set aspect ratio - setAspectRatio.call(this); - }); - }, - - // API ready - ready() { - const player = this; - // Ignore already setup (race condition) - const currentId = player.media && player.media.getAttribute('id'); - if (!is.empty(currentId) && currentId.startsWith('youtube-')) { - return; + // Set title + this.config.title = title; + ui.setTitle.call(this); + + // Set aspect ratio + this.embed.ratio = [width, height]; } - // Get the source URL or ID - let source = player.media.getAttribute('src'); + setAspectRatio.call(this); + }) + .catch(() => { + // Set aspect ratio + setAspectRatio.call(this); + }); + }, + + // API ready + ready() { + const player = this; + // Ignore already setup (race condition) + const currentId = player.media && player.media.getAttribute('id'); + if (!is.empty(currentId) && currentId.startsWith('youtube-')) { + return; + } + + // Get the source URL or ID + let source = player.media.getAttribute('src'); + + // Get from
if needed + if (is.empty(source)) { + source = player.media.getAttribute(this.config.attributes.embed.id); + } - // Get from
if needed - if (is.empty(source)) { - source = player.media.getAttribute(this.config.attributes.embed.id); + // Replace the