diff --git a/src/components/BootstrapBlazor.Player/BootstrapBlazor.Player.csproj b/src/components/BootstrapBlazor.Player/BootstrapBlazor.Player.csproj index 4acc72e4..158a2fb4 100644 --- a/src/components/BootstrapBlazor.Player/BootstrapBlazor.Player.csproj +++ b/src/components/BootstrapBlazor.Player/BootstrapBlazor.Player.csproj @@ -1,7 +1,7 @@ - 10.0.1 + 10.0.2 diff --git a/src/components/BootstrapBlazor.Player/wwwroot/plyr.css b/src/components/BootstrapBlazor.Player/wwwroot/plyr.css index 62ab3f7b..6924c2cf 100644 --- a/src/components/BootstrapBlazor.Player/wwwroot/plyr.css +++ b/src/components/BootstrapBlazor.Player/wwwroot/plyr.css @@ -1 +1 @@ -@charset "UTF-8";@keyframes plyr-progress{to{background-position:25px 0;background-position:var(--plyr-progress-loading-size,25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;align-items:center;direction:ltr;display:flex;flex-direction:column;font-family:inherit;font-family:var(--plyr-font-family,inherit);font-variant-numeric:tabular-nums;font-weight:400;font-weight:var(--plyr-font-weight-regular,400);line-height:1.7;line-height:var(--plyr-line-height,1.7);max-width:100%;min-width:200px;position:relative;text-shadow:none;transition:box-shadow .3s ease;z-index:0}.plyr audio,.plyr iframe,.plyr video{display:block;height:100%;width:100%}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui :after,.plyr--full-ui :before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:#4a5464;background:var(--plyr-badge-background,#4a5464);border-radius:2px;border-radius:var(--plyr-badge-border-radius,2px);color:#fff;color:var(--plyr-badge-text-color,#fff);font-size:9px;font-size:var(--plyr-font-size-badge,9px);line-height:1;padding:3px 4px}.plyr--full-ui ::-webkit-media-text-track-container{display:none}.plyr__captions{animation:plyr-fade-in .3s ease;bottom:0;display:none;font-size:13px;font-size:var(--plyr-font-size-small,13px);left:0;padding:10px;padding:var(--plyr-control-spacing,10px);position:absolute;text-align:center;transition:transform .4s ease-in-out;width:100%}.plyr__captions span:empty{display:none}@media (min-width:480px){.plyr__captions{font-size:15px;font-size:var(--plyr-font-size-base,15px);padding:20px;padding:calc(var(--plyr-control-spacing, 10px)*2)}}@media (min-width:768px){.plyr__captions{font-size:18px;font-size:var(--plyr-font-size-large,18px)}}.plyr--captions-active .plyr__captions{display:block}.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty)~.plyr__captions{transform:translateY(-40px);transform:translateY(calc(var(--plyr-control-spacing, 10px)*-4))}.plyr__caption{background:#000c;background:var(--plyr-captions-background,#000c);border-radius:2px;-webkit-box-decoration-break:clone;box-decoration-break:clone;color:#fff;color:var(--plyr-captions-text-color,#fff);line-height:185%;padding:.2em .5em;white-space:pre-wrap}.plyr__caption div{display:inline}.plyr__control{background:#0000;border:0;border-radius:4px;border-radius:var(--plyr-control-radius,4px);color:inherit;cursor:pointer;flex-shrink:0;overflow:visible;padding:7px;padding:calc(var(--plyr-control-spacing, 10px)*.7);position:relative;transition:all .1s ease-in-out}.plyr__control svg{display:block;fill:currentColor;height:18px;height:var(--plyr-control-icon-size,18px);pointer-events:none;width:18px;width:var(--plyr-control-icon-size,18px)}.plyr__control:focus{outline:0}.plyr__control:focus-visible{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}a.plyr__control{text-decoration:none}.plyr__control.plyr__control--pressed .icon--not-pressed,.plyr__control.plyr__control--pressed .label--not-pressed,.plyr__control:not(.plyr__control--pressed) .icon--pressed,.plyr__control:not(.plyr__control--pressed) .label--pressed,a.plyr__control:after,a.plyr__control:before{display:none}.plyr--full-ui ::-webkit-media-controls{display:none}.plyr__controls{align-items:center;display:flex;justify-content:flex-end;text-align:center}.plyr__controls .plyr__progress__container{flex:1;min-width:0}.plyr__controls .plyr__controls__item{margin-left:2.5px;margin-left:calc(var(--plyr-control-spacing, 10px)/4)}.plyr__controls .plyr__controls__item:first-child{margin-left:0;margin-right:auto}.plyr__controls .plyr__controls__item.plyr__progress__container{padding-left:2.5px;padding-left:calc(var(--plyr-control-spacing, 10px)/4)}.plyr__controls .plyr__controls__item.plyr__time{padding:0 5px;padding:0 calc(var(--plyr-control-spacing, 10px)/2)}.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,.plyr__controls .plyr__controls__item.plyr__time+.plyr__time,.plyr__controls .plyr__controls__item.plyr__time:first-child{padding-left:0}.plyr [data-plyr=airplay],.plyr [data-plyr=captions],.plyr [data-plyr=fullscreen],.plyr [data-plyr=pip],.plyr__controls:empty{display:none}.plyr--airplay-supported [data-plyr=airplay],.plyr--captions-enabled [data-plyr=captions],.plyr--fullscreen-enabled [data-plyr=fullscreen],.plyr--pip-supported [data-plyr=pip]{display:inline-block}.plyr__menu{display:flex;position:relative}.plyr__menu .plyr__control svg{transition:transform .3s ease}.plyr__menu .plyr__control[aria-expanded=true] svg{transform:rotate(90deg)}.plyr__menu .plyr__control[aria-expanded=true] .plyr__tooltip{display:none}.plyr__menu__container{animation:plyr-popup .2s ease;background:#ffffffe6;background:var(--plyr-menu-background,#ffffffe6);border-radius:8px;border-radius:var(--plyr-menu-radius,8px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-menu-shadow,0 1px 2px #00000026);color:#4a5464;color:var(--plyr-menu-color,#4a5464);font-size:15px;font-size:var(--plyr-font-size-base,15px);margin-bottom:10px;position:absolute;right:-3px;text-align:left;white-space:nowrap;z-index:3}.plyr__menu__container>div{overflow:hidden;transition:height .35s cubic-bezier(.4,0,.2,1),width .35s cubic-bezier(.4,0,.2,1)}.plyr__menu__container:after{border:4px solid #0000;border-top-color:#ffffffe6;border:var(--plyr-menu-arrow-size,4px) solid #0000;border-top-color:var(--plyr-menu-background,#ffffffe6);content:"";height:0;position:absolute;right:14px;right:calc(var(--plyr-control-icon-size, 18px)/2 + var(--plyr-control-spacing, 10px)*.7 - var(--plyr-menu-arrow-size, 4px)/2);top:100%;width:0}.plyr__menu__container [role=menu]{padding:7px;padding:calc(var(--plyr-control-spacing, 10px)*.7)}.plyr__menu__container [role=menuitem],.plyr__menu__container [role=menuitemradio]{margin-top:2px}.plyr__menu__container [role=menuitem]:first-child,.plyr__menu__container [role=menuitemradio]:first-child{margin-top:0}.plyr__menu__container .plyr__control{align-items:center;color:#4a5464;color:var(--plyr-menu-color,#4a5464);display:flex;font-size:13px;font-size:var(--plyr-font-size-menu,var(--plyr-font-size-small,13px));padding:4.66667px 10.5px;padding:calc(var(--plyr-control-spacing, 10px)*.7/1.5) calc(var(--plyr-control-spacing, 10px)*.7*1.5);-webkit-user-select:none;user-select:none;width:100%}.plyr__menu__container .plyr__control>span{align-items:inherit;display:flex;width:100%}.plyr__menu__container .plyr__control:after{border:4px solid #0000;border:var(--plyr-menu-item-arrow-size,4px) solid #0000;content:"";position:absolute;top:50%;transform:translateY(-50%)}.plyr__menu__container .plyr__control--forward{padding-right:28px;padding-right:calc(var(--plyr-control-spacing, 10px)*.7*4)}.plyr__menu__container .plyr__control--forward:after{border-left-color:#728197;border-left-color:var(--plyr-menu-arrow-color,#728197);right:6.5px;right:calc(var(--plyr-control-spacing, 10px)*.7*1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--forward:focus-visible:after,.plyr__menu__container .plyr__control--forward:hover:after{border-left-color:initial}.plyr__menu__container .plyr__control--back{font-weight:400;font-weight:var(--plyr-font-weight-regular,400);margin:7px;margin:calc(var(--plyr-control-spacing, 10px)*.7);margin-bottom:3.5px;margin-bottom:calc(var(--plyr-control-spacing, 10px)*.7/2);padding-left:28px;padding-left:calc(var(--plyr-control-spacing, 10px)*.7*4);position:relative;width:calc(100% - 14px);width:calc(100% - var(--plyr-control-spacing, 10px)*.7*2)}.plyr__menu__container .plyr__control--back:after{border-right-color:#728197;border-right-color:var(--plyr-menu-arrow-color,#728197);left:6.5px;left:calc(var(--plyr-control-spacing, 10px)*.7*1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--back:before{background:#dcdfe5;background:var(--plyr-menu-back-border-color,#dcdfe5);box-shadow:0 1px 0 #fff;box-shadow:0 1px 0 var(--plyr-menu-back-border-shadow-color,#fff);content:"";height:1px;left:0;margin-top:3.5px;margin-top:calc(var(--plyr-control-spacing, 10px)*.7/2);overflow:hidden;position:absolute;right:0;top:100%}.plyr__menu__container .plyr__control--back:focus-visible:after,.plyr__menu__container .plyr__control--back:hover:after{border-right-color:initial}.plyr__menu__container .plyr__control[role=menuitemradio]{padding-left:7px;padding-left:calc(var(--plyr-control-spacing, 10px)*.7)}.plyr__menu__container .plyr__control[role=menuitemradio]:after,.plyr__menu__container .plyr__control[role=menuitemradio]:before{border-radius:100%}.plyr__menu__container .plyr__control[role=menuitemradio]:before{background:#0000001a;content:"";display:block;flex-shrink:0;height:16px;margin-right:10px;margin-right:var(--plyr-control-spacing,10px);transition:all .3s ease;width:16px}.plyr__menu__container .plyr__control[role=menuitemradio]:after{background:#fff;border:0;height:6px;left:12px;opacity:0;top:50%;transform:translateY(-50%) scale(0);transition:transform .3s ease,opacity .3s ease;width:6px}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:before{background:#00b2ff;background:var(--plyr-control-toggle-checked-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)))}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:after{opacity:1;transform:translateY(-50%) scale(1)}.plyr__menu__container .plyr__control[role=menuitemradio]:focus-visible:before,.plyr__menu__container .plyr__control[role=menuitemradio]:hover:before{background:#23282f1a}.plyr__menu__container .plyr__menu__value{align-items:center;display:flex;margin-left:auto;margin-right:-5px;margin-right:calc(var(--plyr-control-spacing, 10px)*.7*-1 - -2px);overflow:hidden;padding-left:24.5px;padding-left:calc(var(--plyr-control-spacing, 10px)*.7*3.5);pointer-events:none}.plyr--full-ui input[type=range]{appearance:none;background:#0000;border:0;border-radius:26px;border-radius:calc(var(--plyr-range-thumb-height, 13px)*2);color:#00b2ff;color:var(--plyr-range-fill-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));display:block;height:19px;height:calc(var(--plyr-range-thumb-active-shadow-width, 3px)*2 + var(--plyr-range-thumb-height, 13px));margin:0;min-width:0;padding:0;transition:box-shadow .3s ease;width:100%}.plyr--full-ui input[type=range]::-webkit-slider-runnable-track{background:#0000;background-image:linear-gradient(90deg,currentColor 0,#0000 0);background-image:linear-gradient(to right,currentColor var(--value,0),#0000 var(--value,0));border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-webkit-transition:box-shadow .3s ease;transition:box-shadow .3s ease;-webkit-user-select:none;user-select:none}.plyr--full-ui input[type=range]::-webkit-slider-thumb{appearance:none;background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);margin-top:-4px;margin-top:calc((var(--plyr-range-thumb-height, 13px) - var(--plyr-range-track-height, 5px))/2*-1);position:relative;-webkit-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-moz-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-moz-range-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);position:relative;-moz-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-progress{background:currentColor;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px)}.plyr--full-ui input[type=range]::-ms-track{color:#0000}.plyr--full-ui input[type=range]::-ms-fill-upper,.plyr--full-ui input[type=range]::-ms-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-fill-lower{background:#0000;background:currentColor;border:0;border-radius:2.5px;border-radius:calc(var(--plyr-range-track-height, 5px)/2);height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);margin-top:0;position:relative;-ms-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-ms-tooltip{display:none}.plyr--full-ui input[type=range]::-moz-focus-outer{border:0}.plyr--full-ui input[type=range]:focus{outline:0}.plyr--full-ui input[type=range]:focus-visible::-webkit-slider-runnable-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-moz-range-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-ms-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr__poster{background-color:#000;background-color:var(--plyr-video-background,var(--plyr-video-background,#000));background-position:50% 50%;background-repeat:no-repeat;background-size:contain;height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .2s ease;width:100%;z-index:1}.plyr--stopped.plyr__poster-enabled .plyr__poster{opacity:1}.plyr--youtube.plyr--paused.plyr__poster-enabled:not(.plyr--stopped) .plyr__poster{display:none}.plyr__time{font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__time+.plyr__time:before{content:"⁄";margin-right:10px;margin-right:var(--plyr-control-spacing,10px)}@media (max-width:767px){.plyr__time+.plyr__time{display:none}}.plyr__tooltip{background:#fff;background:var(--plyr-tooltip-background,#fff);border-radius:5px;border-radius:var(--plyr-tooltip-radius,5px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-tooltip-shadow,0 1px 2px #00000026);color:#4a5464;color:var(--plyr-tooltip-color,#4a5464);font-size:13px;font-size:var(--plyr-font-size-small,13px);font-weight:400;font-weight:var(--plyr-font-weight-regular,400);left:50%;line-height:1.3;margin-bottom:10px;margin-bottom:calc(var(--plyr-control-spacing, 10px)/2*2);opacity:0;padding:5px 7.5px;padding:calc(var(--plyr-control-spacing, 10px)/2) calc(var(--plyr-control-spacing, 10px)/2*1.5);pointer-events:none;position:absolute;transform:translate(-50%,10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;white-space:nowrap;z-index:2}.plyr__tooltip:before{border-left:4px solid #0000;border-left:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-right:4px solid #0000;border-right:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-top:4px solid #fff;border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,#fff);bottom:-4px;bottom:calc(var(--plyr-tooltip-arrow-size, 4px)*-1);content:"";height:0;left:50%;position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr .plyr__control:focus-visible .plyr__tooltip,.plyr .plyr__control:hover .plyr__tooltip,.plyr__tooltip--visible{opacity:1;transform:translate(-50%) scale(1)}.plyr .plyr__control:hover .plyr__tooltip{z-index:3}.plyr__controls>.plyr__control:first-child .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip{left:0;transform:translateY(10px) scale(.8);transform-origin:0 100%}.plyr__controls>.plyr__control:first-child .plyr__tooltip:before,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip:before{left:16px;left:calc(var(--plyr-control-icon-size, 18px)/2 + var(--plyr-control-spacing, 10px)*.7)}.plyr__controls>.plyr__control:last-child .plyr__tooltip{left:auto;right:0;transform:translateY(10px) scale(.8);transform-origin:100% 100%}.plyr__controls>.plyr__control:last-child .plyr__tooltip:before{left:auto;right:16px;right:calc(var(--plyr-control-icon-size, 18px)/2 + var(--plyr-control-spacing, 10px)*.7);transform:translateX(50%)}.plyr__controls>.plyr__control:first-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:last-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:last-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:last-child:hover .plyr__tooltip{transform:translate(0) scale(1)}.plyr__progress{left:6.5px;left:calc(var(--plyr-range-thumb-height, 13px)*.5);margin-right:13px;margin-right:var(--plyr-range-thumb-height,13px);position:relative}.plyr__progress input[type=range],.plyr__progress__buffer{margin-left:-6.5px;margin-left:calc(var(--plyr-range-thumb-height, 13px)*-.5);margin-right:-6.5px;margin-right:calc(var(--plyr-range-thumb-height, 13px)*-.5);width:calc(100% + 13px);width:calc(100% + var(--plyr-range-thumb-height, 13px))}.plyr__progress input[type=range]{position:relative;z-index:2}.plyr__progress .plyr__tooltip{left:0;max-width:120px;overflow-wrap:break-word}.plyr__progress__buffer{-webkit-appearance:none;background:#0000;border:0;border-radius:100px;height:5px;height:var(--plyr-range-track-height,5px);left:0;margin-top:-2.5px;margin-top:calc(var(--plyr-range-track-height, 5px)/2*-1);padding:0;position:absolute;top:50%}.plyr__progress__buffer::-webkit-progress-bar{background:#0000}.plyr__progress__buffer::-webkit-progress-value{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-webkit-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-moz-progress-bar{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-moz-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-ms-fill{border-radius:100px;-ms-transition:width .2s ease;transition:width .2s ease}.plyr--loading .plyr__progress__buffer{animation:plyr-progress 1s linear infinite;background-image:linear-gradient(-45deg,#23282f99 25%,#0000 0,#0000 50%,#23282f99 0,#23282f99 75%,#0000 0,#0000);background-image:linear-gradient(-45deg,var(--plyr-progress-loading-background,#23282f99) 25%,#0000 25%,#0000 50%,var(--plyr-progress-loading-background,#23282f99) 50%,var(--plyr-progress-loading-background,#23282f99) 75%,#0000 75%,#0000);background-repeat:repeat-x;background-size:25px 25px;background-size:var(--plyr-progress-loading-size,25px) var(--plyr-progress-loading-size,25px);color:#0000}.plyr--video.plyr--loading .plyr__progress__buffer{background-color:#ffffff40;background-color:var(--plyr-video-progress-buffered-background,#ffffff40)}.plyr--audio.plyr--loading .plyr__progress__buffer{background-color:#c1c8d199;background-color:var(--plyr-audio-progress-buffered-background,#c1c8d199)}.plyr__progress__marker{background-color:#fff;background-color:var(--plyr-progress-marker-background,#fff);border-radius:1px;height:5px;height:var(--plyr-range-track-height,5px);position:absolute;top:50%;transform:translate(-50%,-50%);width:3px;width:var(--plyr-progress-marker-width,3px);z-index:3}.plyr__volume{align-items:center;display:flex;position:relative}.plyr__volume input[type=range]{margin-left:5px;margin-left:calc(var(--plyr-control-spacing, 10px)/2);margin-right:5px;margin-right:calc(var(--plyr-control-spacing, 10px)/2);max-width:90px;min-width:60px;position:relative;z-index:2}.plyr--audio{display:block}.plyr--audio .plyr__controls{background:#fff;background:var(--plyr-audio-controls-background,#fff);border-radius:inherit;color:#4a5464;color:var(--plyr-audio-control-color,#4a5464);padding:10px;padding:var(--plyr-control-spacing,10px)}.plyr--audio .plyr__control:focus-visible,.plyr--audio .plyr__control:hover,.plyr--audio .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-audio-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-audio-control-color-hover,#fff)}.plyr--full-ui.plyr--audio input[type=range]::-webkit-slider-runnable-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]::-moz-range-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]::-ms-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--full-ui.plyr--audio input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--full-ui.plyr--audio input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--audio .plyr__progress__buffer{color:#c1c8d199;color:var(--plyr-audio-progress-buffered-background,#c1c8d199)}.plyr--video{overflow:hidden}.plyr--video.plyr--menu-open{overflow:visible}.plyr__video-wrapper{background:#000;background:var(--plyr-video-background,var(--plyr-video-background,#000));border-radius:inherit;height:100%;margin:auto;overflow:hidden;position:relative;width:100%}.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{aspect-ratio:16/9}@supports not (aspect-ratio:16/9){.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{height:0;padding-bottom:56.25%;position:relative}}.plyr__video-embed iframe,.plyr__video-wrapper--fixed-ratio video{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.plyr--full-ui .plyr__video-embed>.plyr__video-embed__container{padding-bottom:240%;position:relative;transform:translateY(-38.28125%)}.plyr--video .plyr__controls{background:linear-gradient(#0000,#000000bf);background:var(--plyr-video-controls-background,linear-gradient(#0000,#000000bf));border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;bottom:0;color:#fff;color:var(--plyr-video-control-color,#fff);left:0;padding:5px;padding:calc(var(--plyr-control-spacing, 10px)/2);padding-top:20px;padding-top:calc(var(--plyr-control-spacing, 10px)*2);position:absolute;right:0;transition:opacity .4s ease-in-out,transform .4s ease-in-out;z-index:3}@media (min-width:480px){.plyr--video .plyr__controls{padding:10px;padding:var(--plyr-control-spacing,10px);padding-top:35px;padding-top:calc(var(--plyr-control-spacing, 10px)*3.5)}}.plyr--video.plyr--hide-controls .plyr__controls{opacity:0;pointer-events:none;transform:translateY(100%)}.plyr--video .plyr__control:focus-visible,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-video-control-color-hover,#fff)}.plyr__control--overlaid{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));border:0;border-radius:100%;color:#fff;color:var(--plyr-video-control-color,#fff);display:none;left:50%;opacity:.9;padding:15px;padding:calc(var(--plyr-control-spacing, 10px)*1.5);position:absolute;top:50%;transform:translate(-50%,-50%);transition:.3s;z-index:2}.plyr__control--overlaid svg{left:2px;position:relative}.plyr__control--overlaid:focus,.plyr__control--overlaid:hover{opacity:1}.plyr--playing .plyr__control--overlaid{opacity:0;visibility:hidden}.plyr--full-ui.plyr--video .plyr__control--overlaid{display:block}.plyr--full-ui.plyr--video input[type=range]::-webkit-slider-runnable-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]::-moz-range-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]::-ms-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--full-ui.plyr--video input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--full-ui.plyr--video input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--video .plyr__progress__buffer{color:#ffffff40;color:var(--plyr-video-progress-buffered-background,#ffffff40)}.plyr:fullscreen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:fullscreen video{height:100%}.plyr:fullscreen .plyr__control .icon--exit-fullscreen{display:block}.plyr:fullscreen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:fullscreen.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr:fullscreen .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr--fullscreen-fallback{background:#000;border-radius:0!important;height:100%;inset:0;margin:0;position:fixed;width:100%;z-index:10000000}.plyr--fullscreen-fallback video{height:100%}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen{display:block}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr--fullscreen-fallback.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr--fullscreen-fallback .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr__ads{border-radius:inherit;cursor:pointer;inset:0;overflow:hidden;position:absolute;z-index:-1}.plyr__ads>div,.plyr__ads>div iframe{height:100%;position:absolute;width:100%}.plyr__ads:after{background:#23282f;border-radius:2px;bottom:10px;bottom:var(--plyr-control-spacing,10px);color:#fff;content:attr(data-badge-text);font-size:11px;padding:2px 6px;pointer-events:none;position:absolute;right:10px;right:var(--plyr-control-spacing,10px);z-index:3}.plyr__ads:empty:after{display:none}.plyr__cues{background:currentColor;display:block;height:5px;height:var(--plyr-range-track-height,5px);left:0;opacity:.8;position:absolute;top:50%;transform:translateY(-50%);width:3px;z-index:3}.plyr__preview-thumb{background-color:#fff;background-color:var(--plyr-tooltip-background,#fff);border-radius:8px;border-radius:var(--plyr-menu-radius,8px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-tooltip-shadow,0 1px 2px #00000026);margin-bottom:10px;margin-bottom:calc(var(--plyr-control-spacing, 10px)/2*2);opacity:0;padding:3px;pointer-events:none;position:absolute;transform:translateY(10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;z-index:2}.plyr__preview-thumb--is-shown{opacity:1;transform:translate(0) scale(1)}.plyr__preview-thumb:before{border-left:4px solid #0000;border-left:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-right:4px solid #0000;border-right:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-top:4px solid #fff;border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,#fff);bottom:-4px;bottom:calc(var(--plyr-tooltip-arrow-size, 4px)*-1);content:"";height:0;left:calc(50% + var(--preview-arrow-offset));position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr__preview-thumb__image-container{background:#c1c8d1;border-radius:7px;border-radius:calc(var(--plyr-menu-radius, 8px) - 1px);overflow:hidden;position:relative;z-index:0}.plyr__preview-thumb__image-container img,.plyr__preview-thumb__image-container:after{height:100%;left:0;position:absolute;top:0;width:100%}.plyr__preview-thumb__image-container:after{border-radius:inherit;box-shadow:inset 0 0 0 1px #00000026;content:"";pointer-events:none}.plyr__preview-thumb__image-container img{max-height:none;max-width:none}.plyr__preview-thumb__time-container{background:linear-gradient(#0000,#000000bf);background:var(--plyr-video-controls-background,linear-gradient(#0000,#000000bf));border-bottom-left-radius:7px;border-bottom-left-radius:calc(var(--plyr-menu-radius, 8px) - 1px);border-bottom-right-radius:7px;border-bottom-right-radius:calc(var(--plyr-menu-radius, 8px) - 1px);bottom:0;left:0;line-height:1.1;padding:20px 6px 6px;position:absolute;right:0;z-index:3}.plyr__preview-thumb__time-container span{color:#fff;font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__preview-scrubbing{filter:blur(1px);height:100%;inset:0;margin:auto;opacity:0;overflow:hidden;pointer-events:none;position:absolute;transition:opacity .3s ease;width:100%;z-index:1}.plyr__preview-scrubbing--is-shown{opacity:1}.plyr__preview-scrubbing img{height:100%;left:0;max-height:none;max-width:none;object-fit:contain;position:absolute;top:0;width:100%}.plyr--no-transition{transition:none!important}.plyr__sr-only{clip:rect(1px,1px,1px,1px);border:0!important;height:1px!important;overflow:hidden;padding:0!important;position:absolute!important;width:1px!important}.plyr [hidden]{display:none!important} \ No newline at end of file +@charset "UTF-8";@keyframes plyr-progress{to{background-position:25px 0;background-position:var(--plyr-progress-loading-size,25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;align-items:center;direction:ltr;display:flex;flex-direction:column;font-family:inherit;font-family:var(--plyr-font-family,inherit);font-variant-numeric:tabular-nums;font-weight:400;font-weight:var(--plyr-font-weight-regular,400);line-height:1.7;line-height:var(--plyr-line-height,1.7);max-width:100%;min-width:200px;position:relative;text-shadow:none;transition:box-shadow .3s ease;z-index:0}.plyr audio,.plyr iframe,.plyr video{display:block;height:100%;width:100%}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui :after,.plyr--full-ui :before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:#4a5464;background:var(--plyr-badge-background,#4a5464);border-radius:2px;border-radius:var(--plyr-badge-border-radius,2px);color:#fff;color:var(--plyr-badge-text-color,#fff);font-size:9px;font-size:var(--plyr-font-size-badge,9px);line-height:1;padding:3px 4px}.plyr--full-ui ::-webkit-media-text-track-container{display:none}.plyr__captions{animation:plyr-fade-in .3s ease;bottom:0;display:none;font-size:13px;font-size:var(--plyr-font-size-small,13px);left:0;padding:10px;padding:var(--plyr-control-spacing,10px);position:absolute;text-align:center;transition:transform .4s ease-in-out;width:100%}.plyr__captions span:empty{display:none}@media (min-width:480px){.plyr__captions{font-size:15px;font-size:var(--plyr-font-size-base,15px);padding:20px;padding:calc(2 * var(--plyr-control-spacing, 10px))}}@media (min-width:768px){.plyr__captions{font-size:18px;font-size:var(--plyr-font-size-large,18px)}}.plyr--captions-active .plyr__captions{display:block}.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty)~.plyr__captions{transform:translateY(calc(-40px));transform:translateY(calc(-4 * var(--plyr-control-spacing, 10px)))}.plyr__caption{background:#000c;background:var(--plyr-captions-background,#000c);border-radius:4px;-webkit-box-decoration-break:clone;box-decoration-break:clone;color:#fff;color:var(--plyr-captions-text-color,#fff);line-height:185%;padding:.2em .5em;white-space:pre-wrap}.plyr__caption div{display:inline}.plyr__control{background:#0000;border:0;border-radius:4px;border-radius:var(--plyr-control-radius,4px);color:inherit;cursor:pointer;flex-shrink:0;overflow:visible;padding:7px;padding:calc(.7 * var(--plyr-control-spacing, 10px));position:relative;transition:all .1s ease-in-out}.plyr__control svg{display:block;fill:currentColor;height:18px;height:var(--plyr-control-icon-size,18px);pointer-events:none;width:18px;width:var(--plyr-control-icon-size,18px)}.plyr__control:focus{outline:0}.plyr__control:focus-visible{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}a.plyr__control{text-decoration:none}.plyr__control.plyr__control--pressed .icon--not-pressed,.plyr__control.plyr__control--pressed .label--not-pressed,.plyr__control:not(.plyr__control--pressed) .icon--pressed,.plyr__control:not(.plyr__control--pressed) .label--pressed,a.plyr__control:after,a.plyr__control:before{display:none}.plyr--full-ui ::-webkit-media-controls{display:none}.plyr__controls{align-items:center;display:flex;justify-content:flex-end;text-align:center}.plyr__controls .plyr__progress__container{flex:1;min-width:0}.plyr__controls .plyr__controls__item{margin-left:2.5px;margin-left:calc(.25 * var(--plyr-control-spacing, 10px))}.plyr__controls .plyr__controls__item:first-child{margin-left:0;margin-right:auto}.plyr__controls .plyr__controls__item.plyr__progress__container{padding-left:2.5px;padding-left:calc(.25 * var(--plyr-control-spacing, 10px))}.plyr__controls .plyr__controls__item.plyr__time{padding:0 5px;padding:0 calc(.5 * var(--plyr-control-spacing, 10px))}.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,.plyr__controls .plyr__controls__item.plyr__time+.plyr__time,.plyr__controls .plyr__controls__item.plyr__time:first-child{padding-left:0}.plyr [data-plyr=airplay],.plyr [data-plyr=captions],.plyr [data-plyr=fullscreen],.plyr [data-plyr=pip],.plyr__controls:empty{display:none}.plyr--airplay-supported [data-plyr=airplay],.plyr--captions-enabled [data-plyr=captions],.plyr--fullscreen-enabled [data-plyr=fullscreen],.plyr--pip-supported [data-plyr=pip]{display:inline-block}.plyr__menu{display:flex;position:relative}.plyr__menu .plyr__control svg{transition:transform .3s ease}.plyr__menu .plyr__control[aria-expanded=true] svg{transform:rotate(90deg)}.plyr__menu .plyr__control[aria-expanded=true] .plyr__tooltip{display:none}.plyr__menu__container{animation:plyr-popup .2s ease;background:#ffffffe6;background:var(--plyr-menu-background,#ffffffe6);border-radius:8px;border-radius:var(--plyr-menu-radius,8px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-menu-shadow,0 1px 2px #00000026);color:#4a5464;color:var(--plyr-menu-color,#4a5464);font-size:15px;font-size:var(--plyr-font-size-base,15px);margin-bottom:10px;position:absolute;right:-3px;text-align:left;white-space:nowrap;z-index:3}.plyr__menu__container>div{overflow:hidden;transition:height .35s cubic-bezier(.4,0,.2,1),width .35s cubic-bezier(.4,0,.2,1)}.plyr__menu__container:after{border:4px solid #0000;border:var(--plyr-menu-arrow-size,4px) solid #0000;border-top-color:#ffffffe6;border-top-color:var(--plyr-menu-background,#ffffffe6);content:"";height:0;position:absolute;right:14px;right:calc(.5 * var(--plyr-control-icon-size, 18px) + .7 * var(--plyr-control-spacing, 10px) - .5 * var(--plyr-menu-arrow-size, 4px));top:100%;width:0}.plyr__menu__container [role=menu]{padding:7px;padding:calc(.7 * var(--plyr-control-spacing, 10px))}.plyr__menu__container [role=menuitem],.plyr__menu__container [role=menuitemradio]{margin-top:2px}.plyr__menu__container [role=menuitem]:first-child,.plyr__menu__container [role=menuitemradio]:first-child{margin-top:0}.plyr__menu__container .plyr__control{align-items:center;color:#4a5464;color:var(--plyr-menu-color,#4a5464);display:flex;font-size:13px;font-size:var(--plyr-font-size-menu,var(--plyr-font-size-small,13px));padding:4.66667px 10.5px;padding:calc(.46667 * var(--plyr-control-spacing, 10px)) calc(1.05 * var(--plyr-control-spacing, 10px));-webkit-user-select:none;user-select:none;width:100%}.plyr__menu__container .plyr__control>span{align-items:inherit;display:flex;width:100%}.plyr__menu__container .plyr__control:after{border:4px solid #0000;border:var(--plyr-menu-item-arrow-size,4px) solid #0000;content:"";position:absolute;top:50%;transform:translateY(-50%)}.plyr__menu__container .plyr__control--forward{padding-right:28px;padding-right:calc(2.8 * var(--plyr-control-spacing, 10px))}.plyr__menu__container .plyr__control--forward:after{border-left-color:#728197;border-left-color:var(--plyr-menu-arrow-color,#728197);right:6.5px;right:calc(1.05 * var(--plyr-control-spacing, 10px) - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--forward:focus-visible:after,.plyr__menu__container .plyr__control--forward:hover:after{border-left-color:initial}.plyr__menu__container .plyr__control--back{font-weight:400;font-weight:var(--plyr-font-weight-regular,400);margin:7px;margin:calc(.7 * var(--plyr-control-spacing, 10px));margin-bottom:3.5px;margin-bottom:calc(.35 * var(--plyr-control-spacing, 10px));padding-left:28px;padding-left:calc(2.8 * var(--plyr-control-spacing, 10px));position:relative;width:calc(100% - 14px);width:calc(100% - 1.4 * var(--plyr-control-spacing, 10px))}.plyr__menu__container .plyr__control--back:after{border-right-color:#728197;border-right-color:var(--plyr-menu-arrow-color,#728197);left:6.5px;left:calc(1.05 * var(--plyr-control-spacing, 10px) - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--back:before{background:#dcdfe5;background:var(--plyr-menu-back-border-color,#dcdfe5);box-shadow:0 1px 0 #fff;box-shadow:0 1px 0 var(--plyr-menu-back-border-shadow-color,#fff);content:"";height:1px;left:0;margin-top:3.5px;margin-top:calc(.35 * var(--plyr-control-spacing, 10px));overflow:hidden;position:absolute;right:0;top:100%}.plyr__menu__container .plyr__control--back:focus-visible:after,.plyr__menu__container .plyr__control--back:hover:after{border-right-color:initial}.plyr__menu__container .plyr__control[role=menuitemradio]{padding-left:7px;padding-left:calc(.7 * var(--plyr-control-spacing, 10px))}.plyr__menu__container .plyr__control[role=menuitemradio]:after,.plyr__menu__container .plyr__control[role=menuitemradio]:before{border-radius:100%}.plyr__menu__container .plyr__control[role=menuitemradio]:before{background:#0000001a;content:"";display:block;flex-shrink:0;height:16px;margin-right:10px;margin-right:var(--plyr-control-spacing,10px);transition:all .3s ease;width:16px}.plyr__menu__container .plyr__control[role=menuitemradio]:after{background:#fff;border:0;height:6px;left:12px;opacity:0;top:50%;transform:translateY(-50%) scale(0);transition:transform .3s ease,opacity .3s ease;width:6px}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:before{background:#00b2ff;background:var(--plyr-control-toggle-checked-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)))}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:after{opacity:1;transform:translateY(-50%) scale(1)}.plyr__menu__container .plyr__control[role=menuitemradio]:focus-visible:before,.plyr__menu__container .plyr__control[role=menuitemradio]:hover:before{background:#23282f1a}.plyr__menu__container .plyr__menu__value{align-items:center;display:flex;margin-left:auto;margin-right:calc(-5px);margin-right:calc(-1 * (-2px + .7 * var(--plyr-control-spacing, 10px)));overflow:hidden;padding-left:24.5px;padding-left:calc(2.45 * var(--plyr-control-spacing, 10px));pointer-events:none}.plyr--full-ui input[type=range]{appearance:none;background:#0000;border:0;border-radius:26px;border-radius:calc(2 * var(--plyr-range-thumb-height, 13px));color:#00b2ff;color:var(--plyr-range-fill-background,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));display:block;height:19px;height:calc(2 * var(--plyr-range-thumb-active-shadow-width, 3px) + var(--plyr-range-thumb-height, 13px));margin:0;min-width:0;padding:0;transition:box-shadow .3s ease;width:100%}.plyr--full-ui input[type=range]::-webkit-slider-runnable-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(.5 * var(--plyr-range-track-height, 5px));height:5px;height:var(--plyr-range-track-height,5px);-webkit-transition:box-shadow .3s ease;transition:box-shadow .3s ease;-webkit-user-select:none;user-select:none;background-image:linear-gradient(90deg,currentColor 0,#0000);background-image:linear-gradient(to right,currentColor var(--value,0),#0000 var(--value,0))}.plyr--full-ui input[type=range]::-webkit-slider-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);position:relative;-webkit-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px);appearance:none;margin-top:calc(-4px);margin-top:calc(-.5 * (var(--plyr-range-thumb-height, 13px) - var(--plyr-range-track-height, 5px)))}.plyr--full-ui input[type=range]::-moz-range-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(.5 * var(--plyr-range-track-height, 5px));height:5px;height:var(--plyr-range-track-height,5px);-moz-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-moz-range-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);position:relative;-moz-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px)}.plyr--full-ui input[type=range]::-moz-range-progress{background:currentColor;border-radius:2.5px;border-radius:calc(.5 * var(--plyr-range-track-height, 5px));height:5px;height:var(--plyr-range-track-height,5px)}.plyr--full-ui input[type=range]::-ms-track{color:#0000}.plyr--full-ui input[type=range]::-ms-fill-upper,.plyr--full-ui input[type=range]::-ms-track{background:#0000;border:0;border-radius:2.5px;border-radius:calc(.5 * var(--plyr-range-track-height, 5px));height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-fill-lower{background:#0000;border:0;border-radius:2.5px;border-radius:calc(.5 * var(--plyr-range-track-height, 5px));height:5px;height:var(--plyr-range-track-height,5px);-ms-transition:box-shadow .3s ease;transition:box-shadow .3s ease;user-select:none;background:currentColor}.plyr--full-ui input[type=range]::-ms-thumb{background:#fff;background:var(--plyr-range-thumb-background,#fff);border:0;border-radius:100%;box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33);height:13px;height:var(--plyr-range-thumb-height,13px);position:relative;-ms-transition:all .2s ease;transition:all .2s ease;width:13px;width:var(--plyr-range-thumb-height,13px);margin-top:0}.plyr--full-ui input[type=range]::-ms-tooltip{display:none}.plyr--full-ui input[type=range]::-moz-focus-outer{border:0}.plyr--full-ui input[type=range]:focus{outline:0}.plyr--full-ui input[type=range]:focus-visible::-webkit-slider-runnable-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-moz-range-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-ms-track{outline:2px dashed #00b2ff;outline:2px dashed var(--plyr-focus-visible-color,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));outline-offset:2px}.plyr__poster{background-color:#000;background-color:var(--plyr-video-background,var(--plyr-video-background,#000));background-position:50% 50%;background-repeat:no-repeat;background-size:contain;height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .2s ease;width:100%;z-index:1}.plyr--stopped.plyr__poster-enabled .plyr__poster{opacity:1}.plyr--youtube.plyr--paused.plyr__poster-enabled:not(.plyr--stopped) .plyr__poster{display:none}.plyr__time{font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__time+.plyr__time:before{content:"⁄";margin-right:10px;margin-right:var(--plyr-control-spacing,10px)}@media (max-width:767px){.plyr__time+.plyr__time{display:none}}.plyr__tooltip{background:#fff;background:var(--plyr-tooltip-background,#fff);border-radius:5px;border-radius:var(--plyr-tooltip-radius,5px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-tooltip-shadow,0 1px 2px #00000026);color:#4a5464;color:var(--plyr-tooltip-color,#4a5464);font-size:13px;font-size:var(--plyr-font-size-small,13px);font-weight:400;font-weight:var(--plyr-font-weight-regular,400);left:50%;line-height:1.3;margin-bottom:10px;margin-bottom:var(--plyr-control-spacing,10px);opacity:0;padding:5px 7.5px;padding:calc(.5 * var(--plyr-control-spacing, 10px)) calc(.75 * var(--plyr-control-spacing, 10px));pointer-events:none;position:absolute;transform:translate(-50%,10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;white-space:nowrap;z-index:2}.plyr__tooltip:before{border-left:4px solid #0000;border-left:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-right:4px solid #0000;border-right:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-top:4px solid #fff;border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,#fff);bottom:calc(-4px);bottom:calc(-1 * var(--plyr-tooltip-arrow-size, 4px));content:"";height:0;left:50%;position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr .plyr__control:focus-visible .plyr__tooltip,.plyr .plyr__control:hover .plyr__tooltip,.plyr__tooltip--visible{opacity:1;transform:translate(-50%) scale(1)}.plyr .plyr__control:hover .plyr__tooltip{z-index:3}.plyr__controls>.plyr__control:first-child .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip{left:0;transform:translateY(10px) scale(.8);transform-origin:0 100%}.plyr__controls>.plyr__control:first-child .plyr__tooltip:before,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip:before{left:16px;left:calc(.5 * var(--plyr-control-icon-size, 18px) + .7 * var(--plyr-control-spacing, 10px))}.plyr__controls>.plyr__control:last-child .plyr__tooltip{left:auto;right:0;transform:translateY(10px) scale(.8);transform-origin:100% 100%}.plyr__controls>.plyr__control:last-child .plyr__tooltip:before{left:auto;right:16px;right:calc(.5 * var(--plyr-control-icon-size, 18px) + .7 * var(--plyr-control-spacing, 10px));transform:translateX(50%)}.plyr__controls>.plyr__control:first-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:last-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:last-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:last-child:hover .plyr__tooltip{transform:translate(0) scale(1)}.plyr__progress{left:6.5px;left:calc(.5 * var(--plyr-range-thumb-height, 13px));margin-right:13px;margin-right:var(--plyr-range-thumb-height,13px);position:relative}.plyr__progress input[type=range],.plyr__progress__buffer{margin-left:calc(-6.5px);margin-left:calc(-.5 * var(--plyr-range-thumb-height, 13px));margin-right:calc(-6.5px);margin-right:calc(-.5 * var(--plyr-range-thumb-height, 13px));width:calc(100% + 13px);width:calc(100% + var(--plyr-range-thumb-height, 13px))}.plyr__progress input[type=range]{position:relative;z-index:2}.plyr__progress .plyr__tooltip{left:0;max-width:120px;overflow-wrap:break-word}.plyr__progress__buffer{-webkit-appearance:none;background:#0000;border:0;border-radius:100px;height:5px;height:var(--plyr-range-track-height,5px);left:0;margin-top:calc(-2.5px);margin-top:calc(-.5 * var(--plyr-range-track-height, 5px));padding:0;position:absolute;top:50%}.plyr__progress__buffer::-webkit-progress-bar{background:#0000}.plyr__progress__buffer::-webkit-progress-value{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-webkit-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-moz-progress-bar{background:currentColor;border-radius:100px;min-width:5px;min-width:var(--plyr-range-track-height,5px);-moz-transition:width .2s ease;transition:width .2s ease}.plyr__progress__buffer::-ms-fill{border-radius:100px;-ms-transition:width .2s ease;transition:width .2s ease}.plyr--loading .plyr__progress__buffer{animation:plyr-progress 1s linear infinite;background-image:linear-gradient(-45deg,#23282f99 25%,#0000 0,#0000 50%,#23282f99 0,#23282f99 75%,#0000 0,#0000);background-image:linear-gradient(-45deg,var(--plyr-progress-loading-background,#23282f99) 25%,#0000 25%,#0000 50%,var(--plyr-progress-loading-background,#23282f99) 50%,var(--plyr-progress-loading-background,#23282f99) 75%,#0000 75%,#0000);background-repeat:repeat-x;background-size:25px 25px;background-size:var(--plyr-progress-loading-size,25px) var(--plyr-progress-loading-size,25px);color:#0000}.plyr--video.plyr--loading .plyr__progress__buffer{background-color:#ffffff40;background-color:var(--plyr-video-progress-buffered-background,#ffffff40)}.plyr--audio.plyr--loading .plyr__progress__buffer{background-color:#c1c8d199;background-color:var(--plyr-audio-progress-buffered-background,#c1c8d199)}.plyr__progress__marker{background-color:#fff;background-color:var(--plyr-progress-marker-background,#fff);border-radius:1px;height:5px;height:var(--plyr-range-track-height,5px);position:absolute;top:50%;transform:translate(-50%,-50%);width:3px;width:var(--plyr-progress-marker-width,3px);z-index:3}.plyr__volume{align-items:center;display:flex;position:relative}.plyr__volume input[type=range]{margin-left:5px;margin-left:calc(.5 * var(--plyr-control-spacing, 10px));margin-right:5px;margin-right:calc(.5 * var(--plyr-control-spacing, 10px));max-width:90px;min-width:60px;position:relative;z-index:2}.plyr--audio{display:block}.plyr--audio .plyr__controls{background:#fff;background:var(--plyr-audio-controls-background,#fff);border-radius:inherit;color:#4a5464;color:var(--plyr-audio-control-color,#4a5464);padding:10px;padding:var(--plyr-control-spacing,10px)}.plyr--audio .plyr__control:focus-visible,.plyr--audio .plyr__control:hover,.plyr--audio .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-audio-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-audio-control-color-hover,#fff)}.plyr--full-ui.plyr--audio input[type=range]::-webkit-slider-runnable-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]::-moz-range-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]::-ms-track{background-color:#c1c8d199;background-color:var(--plyr-audio-range-track-background,var(--plyr-audio-progress-buffered-background,#c1c8d199))}.plyr--full-ui.plyr--audio input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--full-ui.plyr--audio input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--full-ui.plyr--audio input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #23282f1a;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#23282f1a)}.plyr--audio .plyr__progress__buffer{color:#c1c8d199;color:var(--plyr-audio-progress-buffered-background,#c1c8d199)}.plyr--video{overflow:hidden}.plyr--video.plyr--menu-open{overflow:visible}.plyr__video-wrapper{background:#000;background:var(--plyr-video-background,var(--plyr-video-background,#000));border-radius:inherit;height:100%;margin:auto;overflow:hidden;position:relative;width:100%}@supports not (aspect-ratio:16/9){.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{height:0;padding-bottom:56.25%;position:relative}}.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{aspect-ratio:16/9}.plyr__video-embed iframe,.plyr__video-wrapper--fixed-ratio video{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.plyr--full-ui .plyr__video-embed>.plyr__video-embed__container{padding-bottom:240%;position:relative;transform:translateY(-38.28125%)}.plyr--video .plyr__controls{background:linear-gradient(#0000,#000000bf);background:var(--plyr-video-controls-background,linear-gradient(#0000,#000000bf));border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;bottom:0;color:#fff;color:var(--plyr-video-control-color,#fff);left:0;padding:5px;padding:calc(.5 * var(--plyr-control-spacing, 10px));padding-top:20px;padding-top:calc(2 * var(--plyr-control-spacing, 10px));position:absolute;right:0;transition:opacity .4s ease-in-out,transform .4s ease-in-out;z-index:3}@media (min-width:480px){.plyr--video .plyr__controls{padding:10px;padding:var(--plyr-control-spacing,10px);padding-top:35px;padding-top:calc(3.5 * var(--plyr-control-spacing, 10px))}}.plyr--video.plyr--hide-controls .plyr__controls{opacity:0;pointer-events:none;transform:translateY(100%)}.plyr--video .plyr__control:focus-visible,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true]{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));color:#fff;color:var(--plyr-video-control-color-hover,#fff)}.plyr__control--overlaid{background:#00b2ff;background:var(--plyr-video-control-background-hover,var(--plyr-color-main,var(--plyr-color-main,#00b2ff)));border:0;border-radius:100%;color:#fff;color:var(--plyr-video-control-color,#fff);display:none;left:50%;opacity:.9;padding:15px;padding:calc(1.5 * var(--plyr-control-spacing, 10px));position:absolute;top:50%;transform:translate(-50%,-50%);transition:.3s;z-index:2}.plyr__control--overlaid svg{left:2px;position:relative}.plyr__control--overlaid:focus,.plyr__control--overlaid:hover{opacity:1}.plyr--playing .plyr__control--overlaid{opacity:0;visibility:hidden}.plyr--full-ui.plyr--video .plyr__control--overlaid{display:block}.plyr--full-ui.plyr--video input[type=range]::-webkit-slider-runnable-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]::-moz-range-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]::-ms-track{background-color:#ffffff40;background-color:var(--plyr-video-range-track-background,var(--plyr-video-progress-buffered-background,#ffffff40))}.plyr--full-ui.plyr--video input[type=range]:active::-webkit-slider-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--full-ui.plyr--video input[type=range]:active::-moz-range-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--full-ui.plyr--video input[type=range]:active::-ms-thumb{box-shadow:0 1px 1px #23282f26,0 0 0 1px #23282f33,0 0 0 3px #ffffff80;box-shadow:var(--plyr-range-thumb-shadow,0 1px 1px #23282f26,0 0 0 1px #23282f33),0 0 0 var(--plyr-range-thumb-active-shadow-width,3px) var(--plyr-audio-range-thumb-active-shadow-color,#ffffff80)}.plyr--video .plyr__progress__buffer{color:#ffffff40;color:var(--plyr-video-progress-buffered-background,#ffffff40)}.plyr:fullscreen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:fullscreen video{height:100%}.plyr:fullscreen .plyr__control .icon--exit-fullscreen{display:block}.plyr:fullscreen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:fullscreen.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr:fullscreen .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr--fullscreen-fallback{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr--fullscreen-fallback video{height:100%}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen{display:block}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr--fullscreen-fallback.plyr--hide-controls{cursor:none}@media (min-width:1024px){.plyr--fullscreen-fallback .plyr__captions{font-size:21px;font-size:var(--plyr-font-size-xlarge,21px)}}.plyr--fullscreen-fallback{position:fixed;inset:0;z-index:10000000}.plyr__ads{border-radius:inherit;cursor:pointer;inset:0;overflow:hidden;position:absolute;z-index:-1}.plyr__ads>div,.plyr__ads>div iframe{height:100%;position:absolute;width:100%}.plyr__ads:after{background:#23282f;border-radius:2px;bottom:10px;bottom:var(--plyr-control-spacing,10px);color:#fff;content:attr(data-badge-text);font-size:11px;padding:2px 6px;pointer-events:none;position:absolute;right:10px;right:var(--plyr-control-spacing,10px);z-index:3}.plyr__ads:empty:after{display:none}.plyr__cues{background:currentColor;display:block;height:5px;height:var(--plyr-range-track-height,5px);left:0;opacity:.8;position:absolute;top:50%;transform:translateY(-50%);width:3px;z-index:3}.plyr__preview-thumb{background-color:#fff;background-color:var(--plyr-tooltip-background,#fff);border-radius:8px;border-radius:var(--plyr-menu-radius,8px);bottom:100%;box-shadow:0 1px 2px #00000026;box-shadow:var(--plyr-tooltip-shadow,0 1px 2px #00000026);margin-bottom:10px;margin-bottom:var(--plyr-control-spacing,10px);opacity:0;padding:3px;pointer-events:none;position:absolute;transform:translateY(10px) scale(.8);transform-origin:50% 100%;transition:transform .2s ease .1s,opacity .2s ease .1s;z-index:2}.plyr__preview-thumb--is-shown{opacity:1;transform:translate(0) scale(1)}.plyr__preview-thumb:before{border-left:4px solid #0000;border-left:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-right:4px solid #0000;border-right:var(--plyr-tooltip-arrow-size,4px) solid #0000;border-top:4px solid #fff;border-top:var(--plyr-tooltip-arrow-size,4px) solid var(--plyr-tooltip-background,#fff);bottom:calc(-4px);bottom:calc(-1 * var(--plyr-tooltip-arrow-size, 4px));content:"";height:0;left:calc(50% + var(--preview-arrow-offset));position:absolute;transform:translateX(-50%);width:0;z-index:2}.plyr__preview-thumb__image-container{background:#c1c8d1;border-radius:7px;border-radius:calc(-1px + var(--plyr-menu-radius, 8px));overflow:hidden;position:relative;z-index:0}.plyr__preview-thumb__image-container img,.plyr__preview-thumb__image-container:after{height:100%;left:0;position:absolute;top:0;width:100%}.plyr__preview-thumb__image-container:after{border-radius:inherit;box-shadow:inset 0 0 0 1px #00000026;content:"";pointer-events:none}.plyr__preview-thumb__image-container img{max-height:none;max-width:none}.plyr__preview-thumb__time-container{background:linear-gradient(#0000,#000000bf);background:var(--plyr-video-controls-background,linear-gradient(#0000,#000000bf));border-bottom-left-radius:7px;border-bottom-left-radius:calc(-1px + var(--plyr-menu-radius, 8px));border-bottom-right-radius:7px;border-bottom-right-radius:calc(-1px + var(--plyr-menu-radius, 8px));bottom:0;left:0;line-height:1.1;padding:20px 6px 6px;position:absolute;right:0;z-index:3}.plyr__preview-thumb__time-container span{color:#fff;font-size:13px;font-size:var(--plyr-font-size-time,var(--plyr-font-size-small,13px))}.plyr__preview-scrubbing{filter:blur(1px);height:100%;inset:0;margin:auto;opacity:0;overflow:hidden;pointer-events:none;position:absolute;transition:opacity .3s ease;width:100%;z-index:1}.plyr__preview-scrubbing--is-shown{opacity:1}.plyr__preview-scrubbing img{height:100%;left:0;max-height:none;max-width:none;object-fit:contain;position:absolute;top:0;width:100%}.plyr--no-transition{transition:none!important}.plyr__sr-only{clip:rect(1px,1px,1px,1px);overflow:hidden;border:0!important;height:1px!important;padding:0!important;position:absolute!important;width:1px!important}.plyr [hidden]{display:none!important} \ No newline at end of file diff --git a/src/components/BootstrapBlazor.Player/wwwroot/plyr.js b/src/components/BootstrapBlazor.Player/wwwroot/plyr.js index f48afc3d..8e57afd5 100644 --- a/src/components/BootstrapBlazor.Player/wwwroot/plyr.js +++ b/src/components/BootstrapBlazor.Player/wwwroot/plyr.js @@ -1,8801 +1,5421 @@ -typeof navigator === "object" && (function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define('Plyr', factory) : - (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Plyr = factory()); -})(this, (function () { 'use strict'; - - function _defineProperty$1(e, r, t) { - return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { - value: t, - enumerable: true, - configurable: true, - writable: true - }) : e[r] = t, e; - } - function _toPrimitive(t, r) { - if ("object" != typeof t || !t) return t; - var e = t[Symbol.toPrimitive]; - if (void 0 !== e) { - var i = e.call(t, r); - if ("object" != typeof i) return i; - throw new TypeError("@@toPrimitive must return a primitive value."); - } - return ("string" === r ? String : Number)(t); - } - function _toPropertyKey(t) { - var i = _toPrimitive(t, "string"); - return "symbol" == typeof i ? i : i + ""; - } - - function _classCallCheck(e, t) { - if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function"); - } - function _defineProperties(e, t) { - for (var n = 0; n < t.length; n++) { - var r = t[n]; - r.enumerable = r.enumerable || false, r.configurable = true, "value" in r && (r.writable = true), Object.defineProperty(e, r.key, r); - } - } - function _createClass(e, t, n) { - return t && _defineProperties(e.prototype, t), n && _defineProperties(e, n), e; - } - function _defineProperty(e, t, n) { - return t in e ? Object.defineProperty(e, t, { - value: n, - enumerable: true, - configurable: true, - writable: true - }) : e[t] = n, e; - } - function ownKeys(e, t) { - var n = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var r = Object.getOwnPropertySymbols(e); - t && (r = r.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), n.push.apply(n, r); - } - return n; - } - function _objectSpread2(e) { - for (var t = 1; t < arguments.length; t++) { - var n = null != arguments[t] ? arguments[t] : {}; - t % 2 ? ownKeys(Object(n), true).forEach(function (t) { - _defineProperty(e, t, n[t]); - }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(n)) : ownKeys(Object(n)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(n, t)); - }); - } - return e; - } - var defaults$1 = { - addCSS: true, - thumbWidth: 15, - watch: true - }; - function matches$1(e, t) { - return function () { - return Array.from(document.querySelectorAll(t)).includes(this); - }.call(e, t); - } - function trigger(e, t) { - if (e && t) { - var n = new Event(t, { - bubbles: true - }); - e.dispatchEvent(n); - } - } - var getConstructor$1 = function (e) { - return null != e ? e.constructor : null; - }, - instanceOf$1 = function (e, t) { - return !!(e && t && e instanceof t); - }, - isNullOrUndefined$1 = function (e) { - return null == e; - }, - isObject$1 = function (e) { - return getConstructor$1(e) === Object; - }, - isNumber$1 = function (e) { - return getConstructor$1(e) === Number && !Number.isNaN(e); - }, - isString$1 = function (e) { - return getConstructor$1(e) === String; - }, - isBoolean$1 = function (e) { - return getConstructor$1(e) === Boolean; - }, - isFunction$1 = function (e) { - return getConstructor$1(e) === Function; - }, - isArray$1 = function (e) { - return Array.isArray(e); - }, - isNodeList$1 = function (e) { - return instanceOf$1(e, NodeList); - }, - isElement$1 = function (e) { - return instanceOf$1(e, Element); - }, - isEvent$1 = function (e) { - return instanceOf$1(e, Event); - }, - isEmpty$1 = function (e) { - return isNullOrUndefined$1(e) || (isString$1(e) || isArray$1(e) || isNodeList$1(e)) && !e.length || isObject$1(e) && !Object.keys(e).length; - }, - is$1 = { - nullOrUndefined: isNullOrUndefined$1, - object: isObject$1, - number: isNumber$1, - string: isString$1, - boolean: isBoolean$1, - function: isFunction$1, - array: isArray$1, - nodeList: isNodeList$1, - element: isElement$1, - event: isEvent$1, - empty: isEmpty$1 - }; - function getDecimalPlaces(e) { - var t = "".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); - return t ? Math.max(0, (t[1] ? t[1].length : 0) - (t[2] ? +t[2] : 0)) : 0; - } - function round(e, t) { - if (1 > t) { - var n = getDecimalPlaces(t); - return parseFloat(e.toFixed(n)); - } - return Math.round(e / t) * t; - } - var RangeTouch = function () { - function e(t, n) { - _classCallCheck(this, e), is$1.element(t) ? this.element = t : is$1.string(t) && (this.element = document.querySelector(t)), is$1.element(this.element) && is$1.empty(this.element.rangeTouch) && (this.config = _objectSpread2({}, defaults$1, {}, n), this.init()); - } - return _createClass(e, [{ - key: "init", - value: function () { - e.enabled && (this.config.addCSS && (this.element.style.userSelect = "none", this.element.style.webKitUserSelect = "none", this.element.style.touchAction = "manipulation"), this.listeners(true), this.element.rangeTouch = this); - } - }, { - key: "destroy", - value: function () { - e.enabled && (this.config.addCSS && (this.element.style.userSelect = "", this.element.style.webKitUserSelect = "", this.element.style.touchAction = ""), this.listeners(false), this.element.rangeTouch = null); - } - }, { - key: "listeners", - value: function (e) { - var t = this, - n = e ? "addEventListener" : "removeEventListener"; - ["touchstart", "touchmove", "touchend"].forEach(function (e) { - t.element[n](e, function (e) { - return t.set(e); - }, false); - }); - } - }, { - key: "get", - value: function (t) { - if (!e.enabled || !is$1.event(t)) return null; - var n, - r = t.target, - i = t.changedTouches[0], - o = parseFloat(r.getAttribute("min")) || 0, - s = parseFloat(r.getAttribute("max")) || 100, - u = parseFloat(r.getAttribute("step")) || 1, - c = r.getBoundingClientRect(), - a = 100 / c.width * (this.config.thumbWidth / 2) / 100; - return 0 > (n = 100 / c.width * (i.clientX - c.left)) ? n = 0 : 100 < n && (n = 100), 50 > n ? n -= (100 - 2 * n) * a : 50 < n && (n += 2 * (n - 50) * a), o + round(n / 100 * (s - o), u); - } - }, { - key: "set", - value: function (t) { - e.enabled && is$1.event(t) && !t.target.disabled && (t.preventDefault(), t.target.value = this.get(t), trigger(t.target, "touchend" === t.type ? "change" : "input")); - } - }], [{ - key: "setup", - value: function (t) { - var n = 1 < arguments.length && void 0 !== arguments[1] ? arguments[1] : {}, - r = null; - if (is$1.empty(t) || is$1.string(t) ? r = Array.from(document.querySelectorAll(is$1.string(t) ? t : 'input[type="range"]')) : is$1.element(t) ? r = [t] : is$1.nodeList(t) ? r = Array.from(t) : is$1.array(t) && (r = t.filter(is$1.element)), is$1.empty(r)) return null; - var i = _objectSpread2({}, defaults$1, {}, n); - if (is$1.string(t) && i.watch) { - var o = new MutationObserver(function (n) { - Array.from(n).forEach(function (n) { - Array.from(n.addedNodes).forEach(function (n) { - is$1.element(n) && matches$1(n, t) && new e(n, i); - }); - }); - }); - o.observe(document.body, { - childList: true, - subtree: true - }); - } - return r.map(function (t) { - return new e(t, n); - }); - } - }, { - key: "enabled", - get: function () { - return "ontouchstart" in document.documentElement; - } - }]), e; - }(); - - // ========================================================================== - // Type checking utils - // ========================================================================== - - const getConstructor = input => input !== null && typeof input !== 'undefined' ? input.constructor : null; - const instanceOf = (input, constructor) => Boolean(input && constructor && input instanceof constructor); - const isNullOrUndefined = input => input === null || typeof input === 'undefined'; - const isObject = input => getConstructor(input) === Object; - const isNumber = input => getConstructor(input) === Number && !Number.isNaN(input); - const isString = input => getConstructor(input) === String; - const isBoolean = input => getConstructor(input) === Boolean; - const isFunction = input => typeof input === 'function'; - const isArray = input => Array.isArray(input); - const isWeakMap = input => instanceOf(input, WeakMap); - const isNodeList = input => instanceOf(input, NodeList); - const isTextNode = input => getConstructor(input) === Text; - const isEvent = input => instanceOf(input, Event); - const isKeyboardEvent = input => instanceOf(input, KeyboardEvent); - const isCue = input => instanceOf(input, window.TextTrackCue) || instanceOf(input, window.VTTCue); - const isTrack = input => instanceOf(input, TextTrack) || !isNullOrUndefined(input) && isString(input.kind); - const isPromise = input => instanceOf(input, Promise) && isFunction(input.then); - function isElement(input) { - return input !== null && typeof input === 'object' && input.nodeType === 1 && typeof input.style === 'object' && typeof input.ownerDocument === 'object'; - } - function isEmpty(input) { - return isNullOrUndefined(input) || (isString(input) || isArray(input) || isNodeList(input)) && !input.length || isObject(input) && !Object.keys(input).length; - } - function isUrl(input) { - // Accept a URL object - if (instanceOf(input, window.URL)) { - return true; - } - - // Must be string from here - if (!isString(input)) { - return false; - } - - // Add the protocol if required - let string = input; - if (!input.startsWith('http://') || !input.startsWith('https://')) { - string = `http://${input}`; - } - try { - return !isEmpty(new URL(string).hostname); - } catch { - return false; - } - } - var is = { - nullOrUndefined: isNullOrUndefined, - object: isObject, - number: isNumber, - string: isString, - boolean: isBoolean, - function: isFunction, - array: isArray, - weakMap: isWeakMap, - nodeList: isNodeList, - element: isElement, - textNode: isTextNode, - event: isEvent, - keyboardEvent: isKeyboardEvent, - cue: isCue, - track: isTrack, - promise: isPromise, - url: isUrl, - empty: isEmpty - }; - - // ========================================================================== - // Animation utils - // ========================================================================== - - const transitionEndEvent = (() => { - const element = document.createElement('span'); - const events = { - WebkitTransition: 'webkitTransitionEnd', - MozTransition: 'transitionend', - OTransition: 'oTransitionEnd otransitionend', - transition: 'transitionend' - }; - const type = Object.keys(events).find(event => element.style[event] !== undefined); - return is.string(type) ? events[type] : false; - })(); - - // Force repaint of element - function repaint(element, delay) { - setTimeout(() => { - try { - element.hidden = true; - // eslint-disable-next-line no-unused-expressions - element.offsetHeight; - element.hidden = false; - } catch {} - }, delay); - } - - // ========================================================================== - // Object utils - // ========================================================================== - - - // Clone nested objects - function cloneDeep(object) { - return JSON.parse(JSON.stringify(object)); - } - - // Get a nested value in an object - function getDeep(object, path) { - return path.split('.').reduce((obj, key) => obj && obj[key], object); - } - - // Deep extend destination object with N more objects - function extend(target = {}, ...sources) { - if (!sources.length) { - return target; - } - const source = sources.shift(); - if (!is.object(source)) { - return target; - } - Object.keys(source).forEach(key => { - if (is.object(source[key])) { - if (!Object.keys(target).includes(key)) { - Object.assign(target, { - [key]: {} - }); - } - extend(target[key], source[key]); - } else { - Object.assign(target, { - [key]: source[key] - }); - } - }); - return extend(target, ...sources); - } - - // ========================================================================== - // Element utils - // ========================================================================== - - - // Wrap an element - function wrap(elements, wrapper) { - // Convert `elements` to an array, if necessary. - const targets = elements.length ? elements : [elements]; - - // Loops backwards to prevent having to clone the wrapper on the - // first element (see `child` below). - Array.from(targets).reverse().forEach((element, index) => { - const child = index > 0 ? wrapper.cloneNode(true) : wrapper; - // Cache the current parent and sibling. - const parent = element.parentNode; - const sibling = element.nextSibling; - - // Wrap the element (is automatically removed from its current - // parent). - child.appendChild(element); - - // If the element had a sibling, insert the wrapper before - // the sibling to maintain the HTML structure; otherwise, just - // append it to the parent. - if (sibling) { - parent.insertBefore(child, sibling); - } else { - parent.appendChild(child); - } - }); - } - - // Set attributes - function setAttributes(element, attributes) { - if (!is.element(element) || is.empty(attributes)) return; - - // Assume null and undefined attributes should be left out, - // Setting them would otherwise convert them to "null" and "undefined" - Object.entries(attributes).filter(([, value]) => !is.nullOrUndefined(value)).forEach(([key, value]) => element.setAttribute(key, value)); - } - - // Create a DocumentFragment - function createElement(type, attributes, text) { - // Create a new - const element = document.createElement(type); - - // Set all passed attributes - if (is.object(attributes)) { - setAttributes(element, attributes); - } - - // Add text node - if (is.string(text)) { - element.textContent = text; - } - - // Return built element - return element; - } - - // Insert an element after another - function insertAfter(element, target) { - if (!is.element(element) || !is.element(target)) return; - target.parentNode.insertBefore(element, target.nextSibling); - } - - // Insert a DocumentFragment - function insertElement(type, parent, attributes, text) { - if (!is.element(parent)) return; - parent.appendChild(createElement(type, attributes, text)); - } - - // Remove element(s) - function removeElement(element) { - if (is.nodeList(element) || is.array(element)) { - Array.from(element).forEach(removeElement); - return; - } - if (!is.element(element) || !is.element(element.parentNode)) { - return; - } - element.parentNode.removeChild(element); - } - - // Remove all child elements - function emptyElement(element) { - if (!is.element(element)) return; - let { - length - } = element.childNodes; - while (length > 0) { - element.removeChild(element.lastChild); - length -= 1; - } - } - - // Replace element - function replaceElement(newChild, oldChild) { - if (!is.element(oldChild) || !is.element(oldChild.parentNode) || !is.element(newChild)) return null; - oldChild.parentNode.replaceChild(newChild, oldChild); - return newChild; - } - - // Get an attribute object from a string selector - function getAttributesFromSelector(sel, existingAttributes) { - // For example: - // '.test' to { class: 'test' } - // '#test' to { id: 'test' } - // '[data-test="test"]' to { 'data-test': 'test' } - - if (!is.string(sel) || is.empty(sel)) return {}; - const attributes = {}; - const existing = extend({}, existingAttributes); - sel.split(',').forEach(s => { - // Remove whitespace - const selector = s.trim(); - const className = selector.replace('.', ''); - const stripped = selector.replace(/[[\]]/g, ''); - // Get the parts and value - const parts = stripped.split('='); - const [key] = parts; - const value = parts.length > 1 ? parts[1].replace(/["']/g, '') : ''; - // Get the first character - const start = selector.charAt(0); - switch (start) { - case '.': - // Add to existing classname - if (is.string(existing.class)) { - attributes.class = `${existing.class} ${className}`; - } else { - attributes.class = className; - } - break; - case '#': - // ID selector - attributes.id = selector.replace('#', ''); - break; - case '[': - // Attribute selector - attributes[key] = value; - break; - } - }); - return extend(existing, attributes); - } - - // Toggle hidden - function toggleHidden(element, hidden) { - if (!is.element(element)) return; - let hide = hidden; - if (!is.boolean(hide)) { - hide = !element.hidden; - } - element.hidden = hide; - } - - // Mirror Element.classList.toggle, with IE compatibility for "force" argument - function toggleClass(element, className, force) { - if (is.nodeList(element)) { - return Array.from(element).map(e => toggleClass(e, className, force)); - } - if (is.element(element)) { - let method = 'toggle'; - if (typeof force !== 'undefined') { - method = force ? 'add' : 'remove'; - } - element.classList[method](className); - return element.classList.contains(className); - } - return false; - } - - // Has class name - function hasClass(element, className) { - return is.element(element) && element.classList.contains(className); - } - - // Element matches selector - function matches(element, selector) { - const { - prototype - } = Element; - function match() { - return Array.from(document.querySelectorAll(selector)).includes(this); - } - const method = prototype.matches || prototype.webkitMatchesSelector || prototype.mozMatchesSelector || prototype.msMatchesSelector || match; - return method.call(element, selector); - } - - // Closest ancestor element matching selector (also tests element itself) - function closest$1(element, selector) { - const { - prototype - } = Element; - - // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill - function closestElement() { - let el = this; - do { - if (matches.matches(el, selector)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - } - const method = prototype.closest || closestElement; - return method.call(element, selector); - } - - // Find all elements - function getElements(selector) { - return this.elements.container.querySelectorAll(selector); - } - - // Find a single element - function getElement(selector) { - return this.elements.container.querySelector(selector); - } - - // Set focus and tab focus class - function setFocus(element = null, focusVisible = false) { - if (!is.element(element)) return; - - // Set regular focus - element.focus({ - preventScroll: true, - focusVisible - }); - } - - // ========================================================================== - // Plyr support checks - // ========================================================================== - - - // Default codecs for checking mimetype support - const defaultCodecs = { - 'audio/ogg': 'vorbis', - 'audio/wav': '1', - 'video/webm': 'vp8, vorbis', - 'video/mp4': 'avc1.42E01E, mp4a.40.2', - 'video/ogg': 'theora' - }; - - // Check for feature support - const support = { - // Basic support - audio: 'canPlayType' in document.createElement('audio'), - video: 'canPlayType' in document.createElement('video'), - // Check for support - // Basic functionality vs full UI - check(type, provider) { - const api = support[type] || provider !== 'html5'; - const ui = api && support.rangeInput; - return { - api, - ui - }; - }, - // Picture-in-picture support - pip: (() => { - return document.pictureInPictureEnabled && !createElement('video').disablePictureInPicture; - })(), - // Airplay support - // Safari only currently - airplay: is.function(window.WebKitPlaybackTargetAvailabilityEvent), - // Inline playback support - // https://webkit.org/blog/6784/new-video-policies-for-ios/ - playsinline: 'playsInline' in document.createElement('video'), - // Check for mime type support against a player instance - // Credits: http://diveintohtml5.info/everything.html - // Related: http://www.leanbackplayer.com/test/h5mt.html - mime(input) { - if (is.empty(input)) { - return false; - } - const [mediaType] = input.split('/'); - let type = input; - - // Verify we're using HTML5 and there's no media type mismatch - if (!this.isHTML5 || mediaType !== this.type) { - return false; - } - - // Add codec if required - if (Object.keys(defaultCodecs).includes(type)) { - type += `; codecs="${defaultCodecs[input]}"`; - } - try { - return Boolean(type && this.media.canPlayType(type).replace(/no/, '')); - } catch { - return false; - } - }, - // Check for textTracks support - textTracks: 'textTracks' in document.createElement('video'), - // Sliders - rangeInput: (() => { - const range = document.createElement('input'); - range.type = 'range'; - return range.type === 'range'; - })(), - // Touch - // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event - touch: 'ontouchstart' in document.documentElement, - // Detect transitions support - transitions: transitionEndEvent !== false, - // Reduced motion iOS & MacOS setting - // https://webkit.org/blog/7551/responsive-design-for-motion/ - reducedMotion: 'matchMedia' in window && window.matchMedia('(prefers-reduced-motion)').matches - }; - - // ========================================================================== - // Event utils - // ========================================================================== - - - // Check for passive event listener support - // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md - // https://www.youtube.com/watch?v=NPM6172J22g - const supportsPassiveListeners = (() => { - // Test via a getter in the options object to see if the passive property is accessed - let supported = false; - try { - const options = Object.defineProperty({}, 'passive', { - get() { - supported = true; - return null; - } - }); - window.addEventListener('test', null, options); - window.removeEventListener('test', null, options); - } catch {} - return supported; - })(); - - // Toggle event listener - function toggleListener(element, event, callback, toggle = false, passive = true, capture = false) { - // Bail if no element, event, or callback - if (!element || !('addEventListener' in element) || is.empty(event) || !is.function(callback)) { - return; - } - - // Allow multiple events - const events = event.split(' '); - // Build options - // Default to just the capture boolean for browsers with no passive listener support - let options = capture; - - // If passive events listeners are supported - if (supportsPassiveListeners) { - options = { - // Whether the listener can be passive (i.e. default never prevented) - passive, - // Whether the listener is a capturing listener or not - capture - }; - } - - // If a single node is passed, bind the event listener - events.forEach(type => { - if (this && this.eventListeners && toggle) { - // Cache event listener - this.eventListeners.push({ - element, - type, - callback, - options - }); - } - element[toggle ? 'addEventListener' : 'removeEventListener'](type, callback, options); - }); - } - - // Bind event handler - function on(element, events = '', callback, passive = true, capture = false) { - toggleListener.call(this, element, events, callback, true, passive, capture); - } - - // Unbind event handler - function off(element, events = '', callback, passive = true, capture = false) { - toggleListener.call(this, element, events, callback, false, passive, capture); - } - - // Bind once-only event handler - function once(element, events = '', callback, passive = true, capture = false) { - const onceCallback = (...args) => { - off(element, events, onceCallback, passive, capture); - callback.apply(this, args); - }; - toggleListener.call(this, element, events, onceCallback, true, passive, capture); - } - - // Trigger event - function triggerEvent(element, type = '', bubbles = false, detail = {}) { - // Bail if no element - if (!is.element(element) || is.empty(type)) { - return; - } - - // Create and dispatch the event - const event = new CustomEvent(type, { - bubbles, - detail: { - ...detail, - plyr: this - } - }); - - // Dispatch the event - element.dispatchEvent(event); - } - - // Unbind all cached event listeners - function unbindListeners() { - if (this && this.eventListeners) { - this.eventListeners.forEach(item => { - const { - element, - type, - callback, - options - } = item; - element.removeEventListener(type, callback, options); - }); - this.eventListeners = []; - } - } - - // Run method when / if player is ready - function ready() { - return new Promise(resolve => this.ready ? setTimeout(resolve, 0) : on.call(this, this.elements.container, 'ready', resolve)).then(() => {}); - } - - /** - * Silence a Promise-like object. - * This is useful for avoiding non-harmful, but potentially confusing "uncaught - * play promise" rejection error messages. - * @param {object} value An object that may or may not be `Promise`-like. - */ - function silencePromise(value) { - if (is.promise(value)) { - value.then(null, () => {}); - } - } - - // ========================================================================== - // Array utils - // ========================================================================== - - - // Remove duplicates in an array - function dedupe(array) { - if (!is.array(array)) { - return array; - } - return array.filter((item, index) => array.indexOf(item) === index); - } - - // Get the closest value in an array - function closest(array, value) { - if (!is.array(array) || !array.length) { - return null; - } - return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev); - } - - // ========================================================================== - // Style utils - // ========================================================================== - - - // Check support for a CSS declaration - function supportsCSS(declaration) { - if (!window || !window.CSS) { - return false; - } - return window.CSS.supports(declaration); - } - - // Standard/common aspect ratios - const standardRatios = [[1, 1], [4, 3], [3, 4], [5, 4], [4, 5], [3, 2], [2, 3], [16, 10], [10, 16], [16, 9], [9, 16], [21, 9], [9, 21], [32, 9], [9, 32]].reduce((out, [x, y]) => ({ - ...out, - [x / y]: [x, y] - }), {}); - - // Validate an aspect ratio - function validateAspectRatio(input) { - if (!is.array(input) && (!is.string(input) || !input.includes(':'))) { - return false; - } - const ratio = is.array(input) ? input : input.split(':'); - return ratio.map(Number).every(is.number); - } - - // Reduce an aspect ratio to it's lowest form - function reduceAspectRatio(ratio) { - if (!is.array(ratio) || !ratio.every(is.number)) { - return null; - } - const [width, height] = ratio; - const getDivider = (w, h) => h === 0 ? w : getDivider(h, w % h); - const divider = getDivider(width, height); - return [width / divider, height / divider]; - } - - // Calculate an aspect ratio - function getAspectRatio(input) { - const parse = ratio => validateAspectRatio(ratio) ? ratio.split(':').map(Number) : null; - // Try provided ratio - let ratio = parse(input); - - // Get from config - if (ratio === null) { - ratio = parse(this.config.ratio); - } - - // Get from embed - if (ratio === null && !is.empty(this.embed) && is.array(this.embed.ratio)) { - ({ - ratio - } = this.embed); - } - - // Get from HTML5 video - if (ratio === null && this.isHTML5) { - const { - videoWidth, - videoHeight - } = this.media; - ratio = [videoWidth, videoHeight]; - } - return reduceAspectRatio(ratio); - } - - // Set aspect ratio for responsive container - function setAspectRatio(input) { - if (!this.isVideo) { - return {}; - } - const { - wrapper - } = this.elements; - const ratio = getAspectRatio.call(this, input); - if (!is.array(ratio)) { - return {}; - } - const [x, y] = reduceAspectRatio(ratio); - const useNative = supportsCSS(`aspect-ratio: ${x}/${y}`); - const padding = 100 / x * y; - if (useNative) { - wrapper.style.aspectRatio = `${x}/${y}`; - } else { - wrapper.style.paddingBottom = `${padding}%`; - } - - // For Vimeo we have an extra
to hide the standard controls and UI - if (this.isVimeo && !this.config.vimeo.premium && this.supported.ui) { - const height = 100 / this.media.offsetWidth * Number.parseInt(window.getComputedStyle(this.media).paddingBottom, 10); - const offset = (height - padding) / (height / 50); - if (this.fullscreen.active) { - wrapper.style.paddingBottom = null; - } else { - this.media.style.transform = `translateY(-${offset}%)`; - } - } else if (this.isHTML5) { - wrapper.classList.add(this.config.classNames.videoFixedRatio); - } - return { - padding, - ratio - }; - } - - // Round an aspect ratio to closest standard ratio - function roundAspectRatio(x, y, tolerance = 0.05) { - const ratio = x / y; - const closestRatio = closest(Object.keys(standardRatios), ratio); - - // Check match is within tolerance - if (Math.abs(closestRatio - ratio) <= tolerance) { - return standardRatios[closestRatio]; - } - - // No match - return [x, y]; - } - - // Get the size of the viewport - // https://stackoverflow.com/questions/1248081/how-to-get-the-browser-viewport-dimensions - function getViewportSize() { - const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); - const height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); - return [width, height]; - } - - // ========================================================================== - // Plyr HTML5 helpers - // ========================================================================== - - const html5 = { - getSources() { - if (!this.isHTML5) { - return []; - } - const sources = Array.from(this.media.querySelectorAll('source')); - - // Filter out unsupported sources (if type is specified) - return sources.filter(source => { - const type = source.getAttribute('type'); - if (is.empty(type)) { - return true; - } - return support.mime.call(this, type); - }); - }, - // Get quality levels - getQualityOptions() { - // Whether we're forcing all options (e.g. for streaming) - if (this.config.quality.forced) { - return this.config.quality.options; - } - - // Get sizes from elements - return html5.getSources.call(this).map(source => Number(source.getAttribute('size'))).filter(Boolean); - }, - setup() { - if (!this.isHTML5) { - return; - } - const player = this; - - // Set speed options from config - player.options.speed = player.config.speed.options; - - // Set aspect ratio if fixed - if (!is.empty(this.config.ratio)) { - setAspectRatio.call(player); - } - - // Quality - Object.defineProperty(player.media, 'quality', { - get() { - // Get sources - const sources = html5.getSources.call(player); - const source = sources.find(s => s.getAttribute('src') === player.source); - - // Return size, if match is found - return source && Number(source.getAttribute('size')); - }, - set(input) { - if (player.quality === input) { - return; - } - - // If we're using an external handler... - if (player.config.quality.forced && is.function(player.config.quality.onChange)) { - player.config.quality.onChange(input); - } else { - // Get sources - const sources = html5.getSources.call(player); - // Get first match for requested size - const source = sources.find(s => Number(s.getAttribute('size')) === input); - - // No matching source found - if (!source) { - return; - } - - // Get current state - const { - currentTime, - paused, - preload, - readyState, - playbackRate - } = player.media; - - // Set new source - player.media.src = source.getAttribute('src'); - - // Prevent loading if preload="none" and the current source isn't loaded (#1044) - if (preload !== 'none' || readyState) { - // Restore time - player.once('loadedmetadata', () => { - player.speed = playbackRate; - player.currentTime = currentTime; - - // Resume playing - if (!paused) { - silencePromise(player.play()); - } - }); - - // Load new source - player.media.load(); - } - } - - // Trigger change event - triggerEvent.call(player, player.media, 'qualitychange', false, { - quality: input - }); - } - }); - }, - // Cancel current network requests - // See https://github.com/sampotts/plyr/issues/174 - cancelRequests() { - if (!this.isHTML5) { - return; - } - - // Remove child sources - removeElement(html5.getSources.call(this)); - - // Set blank video src attribute - // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error - // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection - this.media.setAttribute('src', this.config.blankVideo); - - // Load the new empty source - // This will cancel existing requests - // See https://github.com/sampotts/plyr/issues/174 - this.media.load(); - - // Debugging - this.debug.log('Cancelled network requests'); - } - }; - - // ========================================================================== - // Browser sniffing - // Unfortunately, due to mixed support, UA sniffing is required - // ========================================================================== - - const isIE = Boolean(window.document.documentMode); - const isEdge = /Edge/.test(navigator.userAgent); - const isWebKit = 'WebkitAppearance' in document.documentElement.style && !/Edge/.test(navigator.userAgent); - // navigator.platform may be deprecated but this check is still required - const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; - const isIos = /iPad|iPhone|iPod/i.test(navigator.userAgent) && navigator.maxTouchPoints > 1; - var browser = { - isIE, - isEdge, - isWebKit, - isIPadOS, - isIos - }; - - // ========================================================================== - // String utils - // ========================================================================== - - - // Generate a random ID - function generateId(prefix) { - return `${prefix}-${Math.floor(Math.random() * 10000)}`; - } - - // Format string - function format(input, ...args) { - if (is.empty(input)) return input; - return input.toString().replace(/\{(\d+)\}/g, (_, i) => args[i].toString()); - } - - // Get percentage - function getPercentage(current, max) { - if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { - return 0; - } - return (current / max * 100).toFixed(2); - } - - // Replace all occurrences of a string in a string - function replaceAll(input = '', find = '', replace = '') { - return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString()); - } - - // Convert to title case - function toTitleCase(input = '') { - return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()); - } - - // Convert string to pascalCase - function toPascalCase(input = '') { - let string = input.toString(); - - // Convert kebab case - string = replaceAll(string, '-', ' '); - - // Convert snake case - string = replaceAll(string, '_', ' '); - - // Convert to title case - string = toTitleCase(string); - - // Convert to pascal case - return replaceAll(string, ' ', ''); - } - - // Convert string to pascalCase - function toCamelCase(input = '') { - let string = input.toString(); - - // Convert to pascal case - string = toPascalCase(string); - - // Convert first character to lowercase - return string.charAt(0).toLowerCase() + string.slice(1); - } - - // Remove HTML from a string - function stripHTML(source) { - const fragment = document.createDocumentFragment(); - const element = document.createElement('div'); - fragment.appendChild(element); - element.innerHTML = source; - return fragment.firstChild.textContent; - } - - // Like outerHTML, but also works for DocumentFragment - function getHTML(element) { - const wrapper = document.createElement('div'); - wrapper.appendChild(element); - return wrapper.innerHTML; - } - - // ========================================================================== - // Plyr internationalization - // ========================================================================== - - - // Skip i18n for abbreviations and brand names - const resources = { - pip: 'PIP', - airplay: 'AirPlay', - html5: 'HTML5', - vimeo: 'Vimeo', - youtube: 'YouTube' - }; - const i18n = { - get(key = '', config = {}) { - if (is.empty(key) || is.empty(config)) { - return ''; - } - let string = getDeep(config.i18n, key); - if (is.empty(string)) { - if (Object.keys(resources).includes(key)) { - return resources[key]; - } - return ''; - } - const replace = { - '{seektime}': config.seekTime, - '{title}': config.title - }; - Object.entries(replace).forEach(([k, v]) => { - string = replaceAll(string, k, v); - }); - return string; - } - }; - - class Storage { - constructor(player) { - _defineProperty$1(this, "get", key => { - if (!Storage.supported || !this.enabled) { - return null; - } - const store = window.localStorage.getItem(this.key); - if (is.empty(store)) return null; - const json = JSON.parse(store); - return is.string(key) && key.length ? json[key] : json; - }); - _defineProperty$1(this, "set", object => { - // Bail if we don't have localStorage support or it's disabled - if (!Storage.supported || !this.enabled) { - return; - } - - // Can only store objects - if (!is.object(object)) { - return; - } - - // Get current storage - let storage = this.get(); - - // Default to empty object - if (is.empty(storage)) { - storage = {}; - } - - // Update the working copy of the values - extend(storage, object); - - // Update storage - try { - window.localStorage.setItem(this.key, JSON.stringify(storage)); - } catch {} - }); - this.enabled = player.config.storage.enabled; - this.key = player.config.storage.key; - } - - // Check for actual support (see if we can use it) - static get supported() { - try { - if (!('localStorage' in window)) return false; - const test = '___test'; - // Try to use it (it might be disabled, e.g. user is in private mode) - // see: https://github.com/sampotts/plyr/issues/131 - window.localStorage.setItem(test, test); - window.localStorage.removeItem(test); - return true; - } catch { - return false; - } - } - } - - // ========================================================================== - // Fetch wrapper - // Using XHR to avoid issues with older browsers - // ========================================================================== - - function fetch(url, responseType = 'text', withCredentials = false) { - return new Promise((resolve, reject) => { - try { - const request = new XMLHttpRequest(); - - // Check for CORS support - if (!('withCredentials' in request)) return; - - // Set to true if needed for CORS - if (withCredentials) { - request.withCredentials = true; - } - request.addEventListener('load', () => { - if (responseType === 'text') { - try { - resolve(JSON.parse(request.responseText)); - } catch { - resolve(request.responseText); - } - } else { - resolve(request.response); - } - }); - request.addEventListener('error', () => { - throw new Error(request.status); - }); - request.open('GET', url, true); - request.responseType = responseType; - request.send(); - } catch (error) { - reject(error); - } - }); - } - - // ========================================================================== - // Sprite loader - // ========================================================================== - - - // Load an external SVG sprite - function loadSprite(url, id) { - if (!is.string(url)) { - return; - } - const prefix = 'cache'; - const hasId = is.string(id); - let isCached = false; - const exists = () => document.getElementById(id) !== null; - const update = (container, data) => { - container.innerHTML = data; - - // Check again incase of race condition - if (hasId && exists()) { - return; - } - - // Inject the SVG to the body - document.body.insertAdjacentElement('afterbegin', container); - }; - - // Only load once if ID set - if (!hasId || !exists()) { - const useStorage = Storage.supported; - // Create container - const container = document.createElement('div'); - container.setAttribute('hidden', ''); - if (hasId) { - container.setAttribute('id', id); - } - - // Check in cache - if (useStorage) { - const cached = window.localStorage.getItem(`${prefix}-${id}`); - isCached = cached !== null; - if (isCached) { - const data = JSON.parse(cached); - update(container, data.content); - } - } - - // Get the sprite - fetch(url).then(result => { - if (is.empty(result)) { - return; - } - if (useStorage) { - try { - window.localStorage.setItem(`${prefix}-${id}`, JSON.stringify({ - content: result - })); - } catch {} - } - update(container, result); - }).catch(() => {}); - } - } - - // ========================================================================== - // Time utils - // ========================================================================== - - - // Time helpers - const getHours = value => Math.trunc(value / 60 / 60 % 60, 10); - const getMinutes = value => Math.trunc(value / 60 % 60, 10); - const getSeconds = value => Math.trunc(value % 60, 10); - - // Format time to UI friendly string - function formatTime(time = 0, displayHours = false, inverted = false) { - // Bail if the value isn't a number - if (!is.number(time)) { - return formatTime(undefined, displayHours, inverted); - } - - // Format time component to add leading zero - const format = value => `0${value}`.slice(-2); - // Breakdown to hours, mins, secs - let hours = getHours(time); - const mins = getMinutes(time); - const secs = getSeconds(time); - - // Do we need to display hours? - if (displayHours || hours > 0) { - hours = `${hours}:`; - } else { - hours = ''; - } - - // Render - return `${inverted && time > 0 ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; - } - - // ========================================================================== - // Plyr controls - // TODO: This needs to be split into smaller files and cleaned up - // ========================================================================== - - - // 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 host = window.location.host ? window.location.host : window.top.location.host; - const cors = url.host !== 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
`); - } - - // Set position - tipElement.style.left = `${percent}%`; - - // Show/hide the tooltip - // If the event is a moues in/out and percentage is inside bounds - if (is.event(event) && ['mouseenter', 'mouseleave'].includes(event.type)) { - toggle(event.type === 'mouseenter'); - } - }, - // Handle time change event - timeUpdate(event) { - // Only invert if only one time element is displayed and used for both duration and currentTime - const invert = !is.element(this.elements.display.duration) && this.config.invertTime; - - // Duration - controls.updateTimeDisplay.call(this, this.elements.display.currentTime, invert ? this.duration - this.currentTime : this.currentTime, invert); - - // Ignore updates while seeking - if (event && event.type === 'timeupdate' && this.media.seeking) { - return; - } - - // Playing progress - controls.updateProgress.call(this, event); - }, - // Show the duration on metadataloaded or durationchange events - durationUpdate() { - // Bail if no UI or durationchange event triggered after playing/seek when invertTime is false - if (!this.supported.ui || !this.config.invertTime && this.currentTime) { - return; - } - - // If duration is the 2**32 (shaka), Infinity (HLS), DASH-IF (Number.MAX_SAFE_INTEGER || Number.MAX_VALUE) indicating live we hide the currentTime and progressbar. - // https://github.com/video-dev/hls.js/blob/5820d29d3c4c8a46e8b75f1e3afa3e68c1a9a2db/src/controller/buffer-controller.js#L415 - // https://github.com/google/shaka-player/blob/4d889054631f4e1cf0fbd80ddd2b71887c02e232/lib/media/streaming_engine.js#L1062 - // https://github.com/Dash-Industry-Forum/dash.js/blob/69859f51b969645b234666800d4cb596d89c602d/src/dash/models/DashManifestModel.js#L338 - if (this.duration >= 2 ** 32) { - toggleHidden(this.elements.display.currentTime, true); - toggleHidden(this.elements.progress, true); - return; - } - - // Update ARIA values - if (is.element(this.elements.inputs.seek)) { - this.elements.inputs.seek.setAttribute('aria-valuemax', this.duration); - } - - // If there's a spot to display duration - const hasDuration = is.element(this.elements.display.duration); - - // If there's only one time display, display duration there - if (!hasDuration && this.config.displayDuration && this.paused) { - controls.updateTimeDisplay.call(this, this.elements.display.currentTime, this.duration); - } - - // If there's a duration element, update content - if (hasDuration) { - controls.updateTimeDisplay.call(this, this.elements.display.duration, this.duration); - } - if (this.config.markers.enabled) { - controls.setMarkers.call(this); - } - - // Update the tooltip (if visible) - controls.updateSeekTooltip.call(this); - }, - // Hide/show a tab - toggleMenuButton(setting, toggle) { - toggleHidden(this.elements.settings.buttons[setting], !toggle); - }, - // Update the selected setting - updateSetting(setting, container, input) { - const pane = this.elements.settings.panels[setting]; - let value = null; - let list = container; - if (setting === 'captions') { - value = this.currentTrack; - } else { - value = !is.empty(input) ? input : this[setting]; - - // Get default - if (is.empty(value)) { - value = this.config[setting].default; - } - - // Unsupported value - if (!is.empty(this.options[setting]) && !this.options[setting].includes(value)) { - this.debug.warn(`Unsupported value of '${value}' for ${setting}`); - return; - } - - // Disabled value - if (!this.config[setting].options.includes(value)) { - this.debug.warn(`Disabled value of '${value}' for ${setting}`); - return; - } - } - - // Get the list if we need to - if (!is.element(list)) { - list = pane && pane.querySelector('[role="menu"]'); - } - - // If there's no list it means it's not been rendered... - if (!is.element(list)) { - return; - } - - // Update the label - const label = this.elements.settings.buttons[setting].querySelector(`.${this.config.classNames.menu.value}`); - label.innerHTML = controls.getLabel.call(this, setting, value); - - // Find the radio option and check it - const target = list && list.querySelector(`[value="${value}"]`); - if (is.element(target)) { - target.checked = true; - } - }, - // Translate a value into a nice label - getLabel(setting, value) { - switch (setting) { - case 'speed': - return value === 1 ? i18n.get('normal', this.config) : `${value}×`; - case 'quality': - if (is.number(value)) { - const label = i18n.get(`qualityLabel.${value}`, this.config); - if (!label.length) { - return `${value}p`; - } - return label; - } - return toTitleCase(value); - case 'captions': - return captions.getLabel.call(this); - default: - return null; - } - }, - // Set the quality menu - setQualityMenu(options) { - // Menu required - if (!is.element(this.elements.settings.panels.quality)) { - return; - } - const type = 'quality'; - const list = this.elements.settings.panels.quality.querySelector('[role="menu"]'); - - // Set options if passed and filter based on uniqueness and config - if (is.array(options)) { - this.options.quality = dedupe(options).filter(quality => this.config.quality.options.includes(quality)); - } - - // Toggle the pane and tab - const toggle = !is.empty(this.options.quality) && this.options.quality.length > 1; - controls.toggleMenuButton.call(this, type, toggle); - - // Empty the menu - emptyElement(list); - - // Check if we need to toggle the parent - controls.checkMenu.call(this); - - // If we're hiding, nothing more to do - if (!toggle) { - return; - } - - // Get the badge HTML for HD, 4K etc - const getBadge = quality => { - const label = i18n.get(`qualityBadge.${quality}`, this.config); - if (!label.length) { - return null; - } - return controls.createBadge.call(this, label); - }; - - // Sort options by the config and then render options - this.options.quality.sort((a, b) => { - const sorting = this.config.quality.options; - return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; - }).forEach(quality => { - controls.createMenuItem.call(this, { - value: quality, - list, - type, - title: controls.getLabel.call(this, 'quality', quality), - badge: getBadge(quality) - }); - }); - controls.updateSetting.call(this, type, list); - }, - // Set the looping options - /* setLoopMenu() { - // Menu required - if (!is.element(this.elements.settings.panels.loop)) { - return; - } - const options = ['start', 'end', 'all', 'reset']; - const list = this.elements.settings.panels.loop.querySelector('[role="menu"]'); - // Show the pane and tab - toggleHidden(this.elements.settings.buttons.loop, false); - toggleHidden(this.elements.settings.panels.loop, false); - // Toggle the pane and tab - const toggle = !is.empty(this.loop.options); - controls.toggleMenuButton.call(this, 'loop', toggle); - // Empty the menu - emptyElement(list); - options.forEach(option => { - const item = createElement('li'); - const button = createElement( - 'button', - extend(getAttributesFromSelector(this.config.selectors.buttons.loop), { - type: 'button', - class: this.config.classNames.control, - 'data-plyr-loop-action': option, - }), - i18n.get(option, this.config) - ); - if (['start', 'end'].includes(option)) { - const badge = controls.createBadge.call(this, '00:00'); - button.appendChild(badge); - } - item.appendChild(button); - list.appendChild(item); - }); - }, */ - - // Get current selected caption language - // TODO: rework this to user the getter in the API? - - // Set a list of available captions languages - setCaptionsMenu() { - // Menu required - if (!is.element(this.elements.settings.panels.captions)) { - return; - } - - // TODO: Captions or language? Currently it's mixed - const type = 'captions'; - const list = this.elements.settings.panels.captions.querySelector('[role="menu"]'); - const tracks = captions.getTracks.call(this); - const toggle = Boolean(tracks.length); - - // Toggle the pane and tab - controls.toggleMenuButton.call(this, type, toggle); - - // Empty the menu - emptyElement(list); - - // Check if we need to toggle the parent - controls.checkMenu.call(this); - - // If there's no captions, bail - if (!toggle) { - return; - } - - // Generate options data - const options = tracks.map((track, value) => ({ - value, - checked: this.captions.toggled && this.currentTrack === value, - title: captions.getLabel.call(this, track), - badge: track.language && controls.createBadge.call(this, track.language.toUpperCase()), - list, - type: 'language' - })); - - // Add the "Disabled" option to turn off captions - options.unshift({ - value: -1, - checked: !this.captions.toggled, - title: i18n.get('disabled', this.config), - list, - type: 'language' - }); - - // Generate options - options.forEach(controls.createMenuItem.bind(this)); - controls.updateSetting.call(this, type, list); - }, - // Set a list of available captions languages - setSpeedMenu() { - // Menu required - if (!is.element(this.elements.settings.panels.speed)) { - return; - } - const type = 'speed'; - const list = this.elements.settings.panels.speed.querySelector('[role="menu"]'); - - // Filter out invalid speeds - this.options.speed = this.options.speed.filter(o => o >= this.minimumSpeed && o <= this.maximumSpeed); - - // Toggle the pane and tab - const toggle = !is.empty(this.options.speed) && this.options.speed.length > 1; - controls.toggleMenuButton.call(this, type, toggle); - - // Empty the menu - emptyElement(list); - - // Check if we need to toggle the parent - controls.checkMenu.call(this); - - // If we're hiding, nothing more to do - if (!toggle) { - return; - } - - // Create items - this.options.speed.forEach(speed => { - controls.createMenuItem.call(this, { - value: speed, - list, - type, - title: controls.getLabel.call(this, 'speed', speed) - }); - }); - controls.updateSetting.call(this, type, list); - }, - // Check if we need to hide/show the settings menu - checkMenu() { - const { - buttons - } = this.elements.settings; - const visible = !is.empty(buttons) && Object.values(buttons).some(button => !button.hidden); - toggleHidden(this.elements.settings.menu, !visible); - }, - // Focus the first menu item in a given (or visible) menu - focusFirstMenuItem(pane, focusVisible = false) { - if (this.elements.settings.popup.hidden) { - return; - } - let target = pane; - if (!is.element(target)) { - target = Object.values(this.elements.settings.panels).find(p => !p.hidden); - } - const firstItem = target.querySelector('[role^="menuitem"]'); - setFocus.call(this, firstItem, focusVisible); - }, - // Show/hide menu - toggleMenu(input) { - const { - popup - } = this.elements.settings; - const button = this.elements.buttons.settings; - - // Menu and button are required - if (!is.element(popup) || !is.element(button)) { - return; - } - - // True toggle by default - const { - hidden - } = popup; - let show = hidden; - if (is.boolean(input)) { - show = input; - } else if (is.keyboardEvent(input) && input.key === 'Escape') { - show = false; - } else if (is.event(input)) { - // If Plyr is in a shadowDOM, the event target is set to the component, instead of the - // Element in the shadowDOM. The path, if available, is complete. - const target = is.function(input.composedPath) ? input.composedPath()[0] : input.target; - const isMenuItem = popup.contains(target); - - // If the click was inside the menu or if the click - // wasn't the button or menu item and we're trying to - // show the menu (a doc click shouldn't show the menu) - if (isMenuItem || !isMenuItem && input.target !== button && show) { - return; - } - } - - // Set button attributes - button.setAttribute('aria-expanded', show); - - // Show the actual popup - toggleHidden(popup, !show); - - // Add class hook - toggleClass(this.elements.container, this.config.classNames.menu.open, show); - - // Focus the first item if key interaction - if (show && is.keyboardEvent(input)) { - controls.focusFirstMenuItem.call(this, null, true); - } else if (!show && !hidden) { - // If closing, re-focus the button - setFocus.call(this, button, is.keyboardEvent(input)); - } - }, - // Get the natural size of a menu panel - getMenuSize(tab) { - const clone = tab.cloneNode(true); - clone.style.position = 'absolute'; - clone.style.opacity = 0; - clone.removeAttribute('hidden'); - - // Append to parent so we get the "real" size - tab.parentNode.appendChild(clone); - - // Get the sizes before we remove - const width = clone.scrollWidth; - const height = clone.scrollHeight; - - // Remove from the DOM - removeElement(clone); - return { - width, - height - }; - }, - // Show a panel in the menu - showMenuPanel(type = '', focusVisible = false) { - const target = this.elements.container.querySelector(`#plyr-settings-${this.id}-${type}`); - - // Nothing to show, bail - if (!is.element(target)) { - return; - } - - // Hide all other panels - const container = target.parentNode; - const current = Array.from(container.children).find(node => !node.hidden); - - // If we can do fancy animations, we'll animate the height/width - if (support.transitions && !support.reducedMotion) { - // Set the current width as a base - container.style.width = `${current.scrollWidth}px`; - container.style.height = `${current.scrollHeight}px`; - - // Get potential sizes - const size = controls.getMenuSize.call(this, target); - - // Restore auto height/width - const restore = event => { - // We're only bothered about height and width on the container - if (event.target !== container || !['width', 'height'].includes(event.propertyName)) { - return; - } - - // Revert back to auto - container.style.width = ''; - container.style.height = ''; - - // Only listen once - off.call(this, container, transitionEndEvent, restore); - }; - - // Listen for the transition finishing and restore auto height/width - on.call(this, container, transitionEndEvent, restore); - - // Set dimensions to target - container.style.width = `${size.width}px`; - container.style.height = `${size.height}px`; - } - - // Set attributes on current tab - toggleHidden(current, true); - - // Set attributes on target - toggleHidden(target, false); - - // Focus the first item - controls.focusFirstMenuItem.call(this, target, focusVisible); - }, - // Set the download URL - setDownloadUrl() { - const button = this.elements.buttons.download; - - // Bail if no button - if (!is.element(button)) { - return; - } - - // Set attribute - button.setAttribute('href', this.download); - }, - // Build the default HTML - create(data) { - const { - bindMenuItemShortcuts, - createButton, - createProgress, - createRange, - createTime, - setQualityMenu, - setSpeedMenu, - showMenuPanel - } = controls; - this.elements.controls = null; - - // Larger overlaid play button - if (is.array(this.config.controls) && this.config.controls.includes('play-large')) { - this.elements.container.appendChild(createButton.call(this, 'play-large')); - } - - // Create the container - const container = createElement('div', getAttributesFromSelector(this.config.selectors.controls.wrapper)); - this.elements.controls = container; - - // Default item attributes - const defaultAttributes = { - class: 'plyr__controls__item' - }; - - // Loop through controls in order - dedupe(is.array(this.config.controls) ? this.config.controls : []).forEach(control => { - // Restart button - if (control === 'restart') { - container.appendChild(createButton.call(this, 'restart', defaultAttributes)); - } - - // Rewind button - if (control === 'rewind') { - container.appendChild(createButton.call(this, 'rewind', defaultAttributes)); - } - - // Play/Pause button - if (control === 'play') { - container.appendChild(createButton.call(this, 'play', defaultAttributes)); - } - - // Fast forward button - if (control === 'fast-forward') { - container.appendChild(createButton.call(this, 'fast-forward', defaultAttributes)); - } - - // Progress - if (control === 'progress') { - const progressContainer = createElement('div', { - class: `${defaultAttributes.class} plyr__progress__container` - }); - const progress = createElement('div', getAttributesFromSelector(this.config.selectors.progress)); - - // Seek range slider - progress.appendChild(createRange.call(this, 'seek', { - id: `plyr-seek-${data.id}` - })); - - // Buffer progress - progress.appendChild(createProgress.call(this, 'buffer')); - - // TODO: Add loop display indicator - - // Seek tooltip - if (this.config.tooltips.seek) { - const tooltip = createElement('span', { - class: this.config.classNames.tooltip - }, '00:00'); - progress.appendChild(tooltip); - this.elements.display.seekTooltip = tooltip; - } - this.elements.progress = progress; - progressContainer.appendChild(this.elements.progress); - container.appendChild(progressContainer); - } - - // Media current time display - if (control === 'current-time') { - container.appendChild(createTime.call(this, 'currentTime', defaultAttributes)); - } - - // Media duration display - if (control === 'duration') { - container.appendChild(createTime.call(this, 'duration', defaultAttributes)); - } - - // Volume controls - if (control === 'mute' || control === 'volume') { - let { - volume - } = this.elements; - - // Create the volume container if needed - if (!is.element(volume) || !container.contains(volume)) { - volume = createElement('div', extend({}, defaultAttributes, { - class: `${defaultAttributes.class} plyr__volume`.trim() - })); - this.elements.volume = volume; - container.appendChild(volume); - } - - // Toggle mute button - if (control === 'mute') { - volume.appendChild(createButton.call(this, 'mute')); - } - - // Volume range control - // Ignored on iOS as it's handled globally - // https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html - if (control === 'volume' && !browser.isIos && !browser.isIPadOS) { - // Set the attributes - const attributes = { - max: 1, - step: 0.05, - value: this.config.volume - }; - - // Create the volume range slider - volume.appendChild(createRange.call(this, 'volume', extend(attributes, { - id: `plyr-volume-${data.id}` - }))); - } - } - - // Toggle captions button - if (control === 'captions') { - container.appendChild(createButton.call(this, 'captions', defaultAttributes)); - } - - // Settings button / menu - if (control === 'settings' && !is.empty(this.config.settings)) { - const wrapper = createElement('div', extend({}, defaultAttributes, { - class: `${defaultAttributes.class} plyr__menu`.trim(), - hidden: '' - })); - wrapper.appendChild(createButton.call(this, 'settings', { - 'aria-haspopup': true, - 'aria-controls': `plyr-settings-${data.id}`, - 'aria-expanded': false - })); - const popup = createElement('div', { - class: 'plyr__menu__container', - id: `plyr-settings-${data.id}`, - hidden: '' - }); - const inner = createElement('div'); - const home = createElement('div', { - id: `plyr-settings-${data.id}-home` - }); - - // Create the menu - const menu = createElement('div', { - role: 'menu' - }); - home.appendChild(menu); - inner.appendChild(home); - this.elements.settings.panels.home = home; - - // Build the menu items - this.config.settings.forEach(type => { - // TODO: bundle this with the createMenuItem helper and bindings - const menuItem = createElement('button', extend(getAttributesFromSelector(this.config.selectors.buttons.settings), { - 'type': 'button', - 'class': `${this.config.classNames.control} ${this.config.classNames.control}--forward`, - 'role': 'menuitem', - 'aria-haspopup': true, - 'hidden': '' - })); - - // Bind menu shortcuts for keyboard users - bindMenuItemShortcuts.call(this, menuItem, type); - - // Show menu on click - on.call(this, menuItem, 'click', () => { - showMenuPanel.call(this, type, false); - }); - const flex = createElement('span', null, i18n.get(type, this.config)); - const value = createElement('span', { - class: this.config.classNames.menu.value - }); - - // Speed contains HTML entities - value.innerHTML = data[type]; - flex.appendChild(value); - menuItem.appendChild(flex); - menu.appendChild(menuItem); - - // Build the panes - const pane = createElement('div', { - id: `plyr-settings-${data.id}-${type}`, - hidden: '' - }); - - // Back button - const backButton = createElement('button', { - type: 'button', - class: `${this.config.classNames.control} ${this.config.classNames.control}--back` - }); - - // Visible label - backButton.appendChild(createElement('span', { - 'aria-hidden': true - }, i18n.get(type, this.config))); - - // Screen reader label - backButton.appendChild(createElement('span', { - class: this.config.classNames.hidden - }, i18n.get('menuBack', this.config))); - - // Go back via keyboard - on.call(this, pane, 'keydown', event => { - if (event.key !== 'ArrowLeft') return; - - // Prevent seek - event.preventDefault(); - event.stopPropagation(); - - // Show the respective menu - showMenuPanel.call(this, 'home', true); - }, false); - - // Go back via button click - on.call(this, backButton, 'click', () => { - showMenuPanel.call(this, 'home', false); - }); - - // Add to pane - pane.appendChild(backButton); - - // Menu - pane.appendChild(createElement('div', { - role: 'menu' - })); - inner.appendChild(pane); - this.elements.settings.buttons[type] = menuItem; - this.elements.settings.panels[type] = pane; - }); - popup.appendChild(inner); - wrapper.appendChild(popup); - container.appendChild(wrapper); - this.elements.settings.popup = popup; - this.elements.settings.menu = wrapper; - } - - // Picture in picture button - if (control === 'pip' && support.pip) { - container.appendChild(createButton.call(this, 'pip', defaultAttributes)); - } - - // Airplay button - if (control === 'airplay' && support.airplay) { - container.appendChild(createButton.call(this, 'airplay', defaultAttributes)); - } - - // Download button - if (control === 'download') { - const attributes = extend({}, defaultAttributes, { - element: 'a', - href: this.download, - target: '_blank' - }); - - // Set download attribute for HTML5 only - if (this.isHTML5) { - attributes.download = ''; - } - const { - download - } = this.config.urls; - if (!is.url(download) && this.isEmbed) { - extend(attributes, { - icon: `logo-${this.provider}`, - label: this.provider - }); - } - container.appendChild(createButton.call(this, 'download', attributes)); - } - - // Toggle fullscreen button - if (control === 'fullscreen') { - container.appendChild(createButton.call(this, 'fullscreen', defaultAttributes)); - } - }); - - // Set available quality levels - if (this.isHTML5) { - setQualityMenu.call(this, html5.getQualityOptions.call(this)); - } - setSpeedMenu.call(this); - return container; - }, - // Insert controls - inject() { - // Sprite - if (this.config.loadSprite) { - const icon = controls.getIconUrl.call(this); - - // Only load external sprite using AJAX - if (icon.cors) { - loadSprite(icon.url, 'sprite-plyr'); - } - } - - // Create a unique ID - this.id = Math.floor(Math.random() * 10000); - - // Null by default - let container = null; - this.elements.controls = null; - - // Set template properties - const props = { - id: this.id, - seektime: this.config.seekTime, - title: this.config.title - }; - let update = true; - - // If function, run it and use output - if (is.function(this.config.controls)) { - this.config.controls = this.config.controls.call(this, props); - } - - // Convert falsy controls to empty array (primarily for empty strings) - if (!this.config.controls) { - this.config.controls = []; - } - if (is.element(this.config.controls) || is.string(this.config.controls)) { - // HTMLElement or Non-empty string passed as the option - container = this.config.controls; - } else { - // Create controls - container = controls.create.call(this, { - id: this.id, - seektime: this.config.seekTime, - speed: this.speed, - quality: this.quality, - captions: captions.getLabel.call(this) - // TODO: Looping - // loop: 'None', - }); - update = false; - } - - // Replace props with their value - const replace = input => { - let result = input; - Object.entries(props).forEach(([key, value]) => { - result = replaceAll(result, `{${key}}`, value); - }); - return result; - }; - - // Update markup - if (update) { - if (is.string(this.config.controls)) { - container = replace(container); - } - } - - // Controls container - let target; - - // Inject to custom location - if (is.string(this.config.selectors.controls.container)) { - target = document.querySelector(this.config.selectors.controls.container); - } - - // Inject into the container by default - if (!is.element(target)) { - target = this.elements.container; - } - - // Inject controls HTML (needs to be before captions, hence "afterbegin") - const insertMethod = is.element(container) ? 'insertAdjacentElement' : 'insertAdjacentHTML'; - target[insertMethod]('afterbegin', container); - - // Find the elements if need be - if (!is.element(this.elements.controls)) { - controls.findElements.call(this); - } - - // Add pressed property to buttons - if (!is.empty(this.elements.buttons)) { - const addProperty = button => { - const className = this.config.classNames.controlPressed; - button.setAttribute('aria-pressed', 'false'); - Object.defineProperty(button, 'pressed', { - configurable: true, - enumerable: true, - get() { - return hasClass(button, className); - }, - set(pressed = false) { - toggleClass(button, className, pressed); - button.setAttribute('aria-pressed', pressed ? 'true' : 'false'); - } - }); - }; - - // Toggle classname when pressed property is set - Object.values(this.elements.buttons).filter(Boolean).forEach(button => { - if (is.array(button) || is.nodeList(button)) { - Array.from(button).filter(Boolean).forEach(addProperty); - } else { - addProperty(button); - } - }); - } - - // Edge sometimes doesn't finish the paint so force a repaint - if (browser.isEdge) { - repaint(target); - } - - // Setup tooltips - if (this.config.tooltips.controls) { - const { - classNames, - selectors - } = this.config; - const selector = `${selectors.controls.wrapper} ${selectors.labels} .${classNames.hidden}`; - const labels = getElements.call(this, selector); - Array.from(labels).forEach(label => { - toggleClass(label, this.config.classNames.hidden, false); - toggleClass(label, this.config.classNames.tooltip, true); - }); - } - }, - // Set media metadata - setMediaMetadata() { - try { - if ('mediaSession' in navigator) { - navigator.mediaSession.metadata = new window.MediaMetadata({ - title: this.config.mediaMetadata.title, - artist: this.config.mediaMetadata.artist, - album: this.config.mediaMetadata.album, - artwork: this.config.mediaMetadata.artwork - }); - } - } catch { - // Do nothing - } - }, - // Add markers - setMarkers() { - var _this$config$markers2, _this$config$markers3; - if (!this.duration || this.elements.markers) return; - - // Get valid points - const points = (_this$config$markers2 = this.config.markers) === null || _this$config$markers2 === void 0 ? void 0 : (_this$config$markers3 = _this$config$markers2.points) === null || _this$config$markers3 === void 0 ? void 0 : _this$config$markers3.filter(({ - time - }) => time > 0 && time < this.duration); - if (!(points !== null && points !== void 0 && points.length)) return; - const containerFragment = document.createDocumentFragment(); - const pointsFragment = document.createDocumentFragment(); - let tipElement = null; - const tipVisible = `${this.config.classNames.tooltip}--visible`; - const toggleTip = show => toggleClass(tipElement, tipVisible, show); - - // Inject markers to progress container - points.forEach(point => { - const markerElement = createElement('span', { - class: this.config.classNames.marker - }, ''); - const left = `${point.time / this.duration * 100}%`; - if (tipElement) { - // Show on hover - markerElement.addEventListener('mouseenter', () => { - if (point.label) return; - tipElement.style.left = left; - tipElement.innerHTML = point.label; - toggleTip(true); - }); - - // Hide on leave - markerElement.addEventListener('mouseleave', () => { - toggleTip(false); - }); - } - markerElement.addEventListener('click', () => { - this.currentTime = point.time; - }); - markerElement.style.left = left; - pointsFragment.appendChild(markerElement); - }); - containerFragment.appendChild(pointsFragment); - - // Inject a tooltip if needed - if (!this.config.tooltips.seek) { - tipElement = createElement('span', { - class: this.config.classNames.tooltip - }, ''); - containerFragment.appendChild(tipElement); - } - this.elements.markers = { - points: pointsFragment, - tip: tipElement - }; - this.elements.progress.appendChild(containerFragment); - } - }; - - // ========================================================================== - // URL utils - // ========================================================================== - - - /** - * Parse a string to a URL object - * @param {string} input - the URL to be parsed - * @param {boolean} safe - failsafe parsing - */ - function parseUrl(input, safe = true) { - let url = input; - if (safe) { - const parser = document.createElement('a'); - parser.href = url; - url = parser.href; - } - try { - return new URL(url); - } catch { - return null; - } - } - - // Convert object to URLSearchParams - function buildUrlParams(input) { - const params = new URLSearchParams(); - if (is.object(input)) { - Object.entries(input).forEach(([key, value]) => { - params.set(key, value); - }); - } - return params; - } - - // ========================================================================== - // Plyr Captions - // TODO: Create as class - // ========================================================================== - - 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)); - this.elements.captions.setAttribute('dir', 'auto'); - 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.captions.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') || this.captions.active; - 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 - // Note: mode='hidden' forces a track to download. To ensure every track - // isn't downloaded at once, only 'showing' tracks should be reassigned - - if (track.mode === 'showing') { - 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 - if (this.elements) { - 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); - - // 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'); - } - - // Wait for the call stack to clear before setting mode='hidden' - // on the active track - forcing the browser to download it - setTimeout(() => { - if (active && this.captions.toggled) { - this.captions.currentTrackNode.mode = 'hidden'; - } - }); - }, - // 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; - } - // 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'); - } - } - }; - - // ========================================================================== - // Plyr default config - // ========================================================================== - - const defaults = { - // 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 - 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.8.3/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) - // Selector for the fullscreen container so contextual / non-player content can remain visible in fullscreen mode - // Non-ancestors of the player element will be ignored - // container: null, // defaults to the player element - }, - // Local storage - storage: { - enabled: true, - 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' - } - }, - // 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/oembed.json?url={0}' - }, - youtube: { - sdk: 'https://www.youtube.com/iframe_api', - api: 'https://noembed.com/embed?url=https://www.youtube.com/watch?v={0}' - }, - googleIMA: { - sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js' - } - }, - // 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' - }, - 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' - }, - // 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', - marker: 'plyr__progress__marker', - hidden: 'plyr__sr-only', - hideControls: 'plyr--hide-controls', - isTouch: 'plyr--is-touch', - uiSupported: 'plyr--full-ui', - noTransition: 'plyr--no-transition', - display: { - time: 'plyr__time' - }, - menu: { - value: 'plyr__menu__value', - badge: 'plyr__badge', - open: 'plyr--menu-open' - }, - captions: { - enabled: 'plyr--captions-enabled', - active: 'plyr--captions-active' - }, - fullscreen: { - enabled: 'plyr--fullscreen-enabled', - fallback: 'plyr--fullscreen-fallback' - }, - pip: { - supported: 'plyr--pip-supported', - active: 'plyr--pip-active' - }, - airplay: { - supported: 'plyr--airplay-supported', - active: 'plyr--airplay-active' - }, - previewThumbnails: { - // 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' - } - }, - // Embed attributes - attributes: { - embed: { - provider: 'data-plyr-provider', - id: 'data-plyr-embed-id', - hash: 'data-plyr-embed-hash' - } - }, - // 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: '', - withCredentials: false - }, - // Vimeo plugin - vimeo: { - byline: false, - portrait: false, - title: false, - speed: true, - transparent: false, - // Custom settings from Plyr - customControls: true, - referrerPolicy: null, - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy - // Whether the owner of the video has a Pro or Business account - // (which allows us to properly hide controls without CSS hacks, etc) - premium: false - }, - // YouTube plugin - youtube: { - 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) - // Custom settings from Plyr - customControls: true, - noCookie: false // Whether to use an alternative version of YouTube without cookies - }, - // Media Metadata - mediaMetadata: { - title: '', - artist: '', - album: '', - artwork: [] - }, - // Markers - markers: { - enabled: false, - points: [] - } - }; - - // ========================================================================== - // Plyr states - // ========================================================================== - - const pip = { - active: 'picture-in-picture', - inactive: 'inline' - }; - - // ========================================================================== - // Plyr supported types and providers - // ========================================================================== - - const providers = { - html5: 'html5', - youtube: 'youtube', - vimeo: 'vimeo' - }; - const types = { - audio: 'audio', - video: 'video' - }; - - /** - * Get provider by URL - * @param {string} url - */ - function getProviderByUrl(url) { - // 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; - } - return null; - } - - // ========================================================================== - // Console wrapper - // ========================================================================== - - function noop() {} - class Console { - constructor(enabled = false) { - this.enabled = window.console && 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 warn() { - return this.enabled ? Function.prototype.bind.call(console.warn, console) : noop; - } - get error() { - return this.enabled ? Function.prototype.bind.call(console.error, console) : noop; - } - } - - class Fullscreen { - constructor(player) { - _defineProperty$1(this, "onChange", () => { - if (!this.supported) return; - - // Update toggle button - const button = this.player.elements.buttons.fullscreen; - if (is.element(button)) { - button.pressed = this.active; - } - - // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up - const target = this.target === this.player.media ? this.target : this.player.elements.container; - // Trigger an event - triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true); - }); - _defineProperty$1(this, "toggleFallback", (toggle = false) => { - // Store or restore scroll position - if (toggle) { - var _window$scrollX, _window$scrollY; - this.scrollPosition = { - x: (_window$scrollX = window.scrollX) !== null && _window$scrollX !== void 0 ? _window$scrollX : 0, - y: (_window$scrollY = window.scrollY) !== null && _window$scrollY !== void 0 ? _window$scrollY : 0 - }; - } else { - window.scrollTo(this.scrollPosition.x, this.scrollPosition.y); - } - - // Toggle scroll - document.body.style.overflow = toggle ? 'hidden' : ''; - - // Toggle class hook - toggleClass(this.target, this.player.config.classNames.fullscreen.fallback, toggle); - - // Force full viewport on iPhone X+ - if (browser.isIos) { - let viewport = document.head.querySelector('meta[name="viewport"]'); - const property = 'viewport-fit=cover'; - - // Inject the viewport meta if required - if (!viewport) { - viewport = document.createElement('meta'); - viewport.setAttribute('name', 'viewport'); - } - - // Check if the property already exists - const hasProperty = is.string(viewport.content) && viewport.content.includes(property); - if (toggle) { - this.cleanupViewport = !hasProperty; - if (!hasProperty) viewport.content += `,${property}`; - } else if (this.cleanupViewport) { - viewport.content = viewport.content.split(',').filter(part => part.trim() !== property).join(','); - } - } - - // Toggle button and fire events - this.onChange(); - }); - // Trap focus inside container - _defineProperty$1(this, "trapFocus", event => { - // Bail if iOS/iPadOS, not active, not the tab key - if (browser.isIos || browser.isIPadOS || !this.active || event.key !== 'Tab') return; - - // Get the current focused element - const focused = document.activeElement; - const focusable = getElements.call(this.player, 'a[href], button:not(:disabled), input:not(:disabled), [tabindex]'); - const [first] = focusable; - const last = focusable[focusable.length - 1]; - if (focused === last && !event.shiftKey) { - // Move focus to first element that can be tabbed if Shift isn't used - first.focus(); - event.preventDefault(); - } else if (focused === first && event.shiftKey) { - // Move focus to last element that can be tabbed if Shift is used - last.focus(); - event.preventDefault(); - } - }); - // Update UI - _defineProperty$1(this, "update", () => { - if (this.supported) { - let mode; - if (this.forceFallback) mode = 'Fallback (forced)';else if (Fullscreen.nativeSupported) mode = 'Native';else mode = 'Fallback'; - this.player.debug.log(`${mode} fullscreen enabled`); - } else { - this.player.debug.log('Fullscreen not supported and fallback disabled'); - } - - // Add styling hook to show button - toggleClass(this.player.elements.container, this.player.config.classNames.fullscreen.enabled, this.supported); - }); - // Make an element fullscreen - _defineProperty$1(this, "enter", () => { - if (!this.supported) return; - - // iOS native fullscreen doesn't need the request step - if (browser.isIos && this.player.config.fullscreen.iosNative) { - if (this.player.isVimeo) { - this.player.embed.requestFullscreen(); - } else { - this.target.webkitEnterFullscreen(); - } - } else if (!Fullscreen.nativeSupported || this.forceFallback) { - this.toggleFallback(true); - } else if (!this.prefix) { - this.target.requestFullscreen({ - navigationUI: 'hide' - }); - } else if (!is.empty(this.prefix)) { - this.target[`${this.prefix}Request${this.property}`](); - } - }); - // Bail from fullscreen - _defineProperty$1(this, "exit", () => { - if (!this.supported) return; - - // iOS native fullscreen - if (browser.isIos && this.player.config.fullscreen.iosNative) { - if (this.player.isVimeo) { - this.player.embed.exitFullscreen(); - } else { - this.target.webkitEnterFullscreen(); - } - silencePromise(this.player.play()); - } else if (!Fullscreen.nativeSupported || this.forceFallback) { - this.toggleFallback(false); - } else if (!this.prefix) { - (document.cancelFullScreen || document.exitFullscreen).call(document); - } else if (!is.empty(this.prefix)) { - const action = this.prefix === 'moz' ? 'Cancel' : 'Exit'; - document[`${this.prefix}${action}${this.property}`](); - } - }); - // Toggle state - _defineProperty$1(this, "toggle", () => { - if (!this.active) this.enter();else this.exit(); - }); - // Keep reference to parent - this.player = player; - - // Get prefix - this.prefix = Fullscreen.prefix; - this.property = Fullscreen.property; - - // Scroll position - this.scrollPosition = { - x: 0, - y: 0 - }; - - // Force the use of 'full window/browser' rather than fullscreen - this.forceFallback = player.config.fullscreen.fallback === 'force'; - - // Get the fullscreen element - // Checks container is an ancestor, defaults to null - this.player.elements.fullscreen = player.config.fullscreen.container && closest$1(this.player.elements.container, player.config.fullscreen.container); - - // Register event listeners - // Handle event (incase user presses escape etc) - on.call(this.player, document, this.prefix === 'ms' ? 'MSFullscreenChange' : `${this.prefix}fullscreenchange`, () => { - // TODO: Filter for target?? - this.onChange(); - }); - - // Fullscreen toggle on double click - on.call(this.player, this.player.elements.container, 'dblclick', event => { - // Ignore double click in controls - if (is.element(this.player.elements.controls) && this.player.elements.controls.contains(event.target)) { - return; - } - this.player.listeners.proxy(event, this.toggle, 'fullscreen'); - }); - - // Tap focus when in fullscreen - on.call(this, this.player.elements.container, 'keydown', event => this.trapFocus(event)); - - // Update the UI - this.update(); - } - - // Determine if native supported - static get nativeSupported() { - return !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled); - } - - // If we're actually using native - get useNative() { - return Fullscreen.nativeSupported && !this.forceFallback; - } - - // Get the prefix for handlers - static get prefix() { - // No prefix - if (is.function(document.exitFullscreen)) return ''; - - // Check for fullscreen support by vendor prefix - let value = ''; - const prefixes = ['webkit', 'moz', 'ms']; - prefixes.some(pre => { - if (is.function(document[`${pre}ExitFullscreen`]) || is.function(document[`${pre}CancelFullScreen`])) { - value = pre; - return true; - } - return false; - }); - return value; - } - static get property() { - return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen'; - } - - // Determine if fullscreen is supported - get supported() { - return [ - // Fullscreen is enabled in config - this.player.config.fullscreen.enabled, - // Must be a video - this.player.isVideo, - // Either native is supported or fallback enabled - Fullscreen.nativeSupported || this.player.config.fullscreen.fallback, - // YouTube has no way to trigger fullscreen, so on devices with no native support, playsinline - // must be enabled and iosNative fullscreen must be disabled to offer the fullscreen fallback - !this.player.isYouTube || Fullscreen.nativeSupported || !browser.isIos || this.player.config.playsinline && !this.player.config.fullscreen.iosNative].every(Boolean); - } - - // Get active state - get active() { - if (!this.supported) return false; - - // Fallback using classname - if (!Fullscreen.nativeSupported || this.forceFallback) { - return hasClass(this.target, this.player.config.classNames.fullscreen.fallback); - } - const element = !this.prefix ? this.target.getRootNode().fullscreenElement : this.target.getRootNode()[`${this.prefix}${this.property}Element`]; - return element && element.shadowRoot ? element === this.target.getRootNode().host : element === this.target; - } - - // Get target element - get target() { - var _this$player$elements; - return browser.isIos && this.player.config.fullscreen.iosNative ? this.player.media : (_this$player$elements = this.player.elements.fullscreen) !== null && _this$player$elements !== void 0 ? _this$player$elements : this.player.elements.container; - } - } - - // ========================================================================== - // Load image avoiding xhr/fetch CORS issues - // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded - // By default it checks if it is at least 1px, but you can add a second argument to change this - // ========================================================================== - - function loadImage(src, minWidth = 1) { - return new Promise((resolve, reject) => { - const image = new Image(); - const handler = () => { - delete image.onload; - delete image.onerror; - (image.naturalWidth >= minWidth ? resolve : reject)(image); - }; - Object.assign(image, { - onload: handler, - onerror: handler, - src - }); - }); - } - - // ========================================================================== - // Plyr UI - // ========================================================================== - - const ui = { - addStyleHook() { - toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true); - toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui); - }, - // Toggle native HTML5 media controls - toggleNativeControls(toggle = false) { - if (toggle && this.isHTML5) { - this.media.setAttribute('controls', ''); - } else { - this.media.removeAttribute('controls'); - } - }, - // Setup the UI - build() { - // Re-attach media element listeners - // TODO: Use event bubbling? - this.listeners.media(); - - // Don't setup interface if no support - if (!this.supported.ui) { - this.debug.warn(`Basic support only for ${this.provider} ${this.type}`); - - // Restore native controls - ui.toggleNativeControls.call(this, true); - - // Bail - return; - } - - // Inject custom controls if not present - if (!is.element(this.elements.controls)) { - // Inject custom controls - controls.inject.call(this); - - // Re-attach control listeners - this.listeners.controls(); - } - - // Remove native controls - ui.toggleNativeControls.call(this); - - // Setup captions for HTML5 - if (this.isHTML5) { - captions.setup.call(this); - } - - // Reset volume - this.volume = null; - - // Reset mute state - this.muted = null; - - // Reset loop state - this.loop = null; - - // Reset quality setting - this.quality = null; - - // Reset speed - this.speed = null; - - // Reset volume display - controls.updateVolume.call(this); - - // Reset time display - controls.timeUpdate.call(this); - - // Reset duration display - controls.durationUpdate.call(this); - - // Update the UI - ui.checkPlaying.call(this); - - // Check for picture-in-picture support - toggleClass(this.elements.container, this.config.classNames.pip.supported, support.pip && this.isHTML5 && this.isVideo); - - // Check for airplay support - toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5); - - // Add touch class - toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch); - - // Ready for API calls - this.ready = true; - - // Ready event at end of execution stack - setTimeout(() => { - triggerEvent.call(this, this.media, 'ready'); - }, 0); - - // Set the title - ui.setTitle.call(this); - - // Assure the poster image is set, if the property was added before the element was created - if (this.poster) { - ui.setPoster.call(this, this.poster, false).catch(() => {}); - } - - // Manually set the duration if user has overridden it. - // The event listeners for it doesn't get called if preload is disabled (#701) - if (this.config.duration) { - controls.durationUpdate.call(this); - } - - // Media metadata - if (this.config.mediaMetadata) { - controls.setMediaMetadata.call(this); - } - }, - // Setup aria attribute for play and iframe title - setTitle() { - // Find the current text - let label = i18n.get('play', this.config); - - // If there's a media title set, use that for the label - if (is.string(this.config.title) && !is.empty(this.config.title)) { - label += `, ${this.config.title}`; - } - - // If there's a play button, set label - Array.from(this.elements.buttons.play || []).forEach(button => { - button.setAttribute('aria-label', label); - }); - - // Set iframe title - // https://github.com/sampotts/plyr/issues/124 - if (this.isEmbed) { - const iframe = getElement.call(this, 'iframe'); - if (!is.element(iframe)) { - return; - } - - // Default to media type - const title = !is.empty(this.config.title) ? this.config.title : 'video'; - const format = i18n.get('frameTitle', this.config); - iframe.setAttribute('title', format.replace('{title}', title)); - } - }, - // Toggle poster - togglePoster(enable) { - toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); - }, - // Set the poster image (async) - // Used internally for the poster setter, with the passive option forced to false - setPoster(poster, passive = true) { - // Don't override if call is passive - if (passive && this.poster) { - return Promise.reject(new Error('Poster already set')); - } - - // Set property synchronously to respect the call order - this.media.setAttribute('data-poster', poster); - - // Show the poster - this.elements.poster.removeAttribute('hidden'); - - // Wait until ui is ready - return ready.call(this) - // Load image - .then(() => loadImage(poster)).catch(error => { - // Hide poster on error unless it's been set by another call - if (poster === this.poster) { - ui.togglePoster.call(this, false); - } - // Rethrow - throw error; - }).then(() => { - // Prevent race conditions - if (poster !== this.poster) { - throw new Error('setPoster cancelled by later call to setPoster'); - } - }).then(() => { - Object.assign(this.elements.poster.style, { - backgroundImage: `url('${poster}')`, - // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) - backgroundSize: '' - }); - ui.togglePoster.call(this, true); - return poster; - }); - }, - // Check playing state - checkPlaying(event) { - // Class hooks - toggleClass(this.elements.container, this.config.classNames.playing, this.playing); - toggleClass(this.elements.container, this.config.classNames.paused, this.paused); - toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped); - - // Set state - Array.from(this.elements.buttons.play || []).forEach(target => { - Object.assign(target, { - pressed: this.playing - }); - target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config)); - }); - - // Only update controls on non timeupdate events - if (is.event(event) && event.type === 'timeupdate') { - return; - } - - // Toggle controls - ui.toggleControls.call(this); - }, - // Check if media is loading - checkLoading(event) { - this.loading = ['stalled', 'waiting'].includes(event.type); - - // Clear timer - clearTimeout(this.timers.loading); - - // Timer to prevent flicker when seeking - this.timers.loading = setTimeout(() => { - // Update progress bar loading class state - toggleClass(this.elements.container, this.config.classNames.loading, this.loading); - - // Update controls visibility - ui.toggleControls.call(this); - }, this.loading ? 250 : 0); - }, - // Toggle controls based on state and `force` argument - toggleControls(force) { - const { - controls: controlsElement - } = this.elements; - if (controlsElement && this.config.hideControls) { - // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.) - const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now(); - - // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide - this.toggleControls(Boolean(force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek)); - } - }, - // Migrate any custom properties from the media to the parent - migrateStyles() { - // Loop through values (as they are the keys when the object is spread 🤔) - Object.values({ - ...this.media.style - }) - // We're only fussed about Plyr specific properties - .filter(key => !is.empty(key) && is.string(key) && key.startsWith('--plyr')).forEach(key => { - // Set on the container - this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key)); - - // Clean up from media element - this.media.style.removeProperty(key); - }); - - // Remove attribute if empty - if (is.empty(this.media.style)) { - this.media.removeAttribute('style'); - } - } - }; - - class Listeners { - constructor(_player) { - // Device is touch enabled - _defineProperty$1(this, "firstTouch", () => { - const { - player - } = this; - const { - elements - } = player; - player.touch = true; - - // Add touch class - toggleClass(elements.container, player.config.classNames.isTouch, true); - }); - // Global window & document listeners - _defineProperty$1(this, "global", (toggle = true) => { - const { - player - } = this; - - // Keyboard shortcuts - if (player.config.keyboard.global) { - toggleListener.call(player, window, 'keydown keyup', this.handleKey, toggle, false); - } - - // Click anywhere closes menu - toggleListener.call(player, document.body, 'click', this.toggleMenu, toggle); - - // Detect touch by events - once.call(player, document.body, 'touchstart', this.firstTouch); - }); - // Container listeners - _defineProperty$1(this, "container", () => { - const { - player - } = this; - const { - config, - elements, - timers - } = player; - - // Keyboard shortcuts - if (!config.keyboard.global && config.keyboard.focused) { - on.call(player, elements.container, 'keydown keyup', this.handleKey, false); - } - - // Toggle controls on mouse events and entering fullscreen - on.call(player, elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { - const { - controls: controlsElement - } = elements; - - // Remove button states for fullscreen - if (controlsElement && event.type === 'enterfullscreen') { - controlsElement.pressed = false; - controlsElement.hover = false; - } - - // Show, then hide after a timeout unless another control event occurs - const show = ['touchstart', 'touchmove', 'mousemove'].includes(event.type); - let delay = 0; - if (show) { - ui.toggleControls.call(player, true); - // Use longer timeout for touch devices - delay = player.touch ? 3000 : 2000; - } - - // Clear timer - clearTimeout(timers.controls); - - // Set new timer to prevent flicker when seeking - timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay); - }); - - // Set a gutter for Vimeo - const setGutter = () => { - if (!player.isVimeo || player.config.vimeo.premium) { - return; - } - const target = elements.wrapper; - const { - active - } = player.fullscreen; - const [videoWidth, videoHeight] = getAspectRatio.call(player); - const useNativeAspectRatio = supportsCSS(`aspect-ratio: ${videoWidth} / ${videoHeight}`); - - // If not active, remove styles - if (!active) { - if (useNativeAspectRatio) { - target.style.width = null; - target.style.height = null; - } else { - target.style.maxWidth = null; - target.style.margin = null; - } - return; - } - - // Determine which dimension will overflow and constrain view - const [viewportWidth, viewportHeight] = getViewportSize(); - const overflow = viewportWidth / viewportHeight > videoWidth / videoHeight; - if (useNativeAspectRatio) { - target.style.width = overflow ? 'auto' : '100%'; - target.style.height = overflow ? '100%' : 'auto'; - } else { - target.style.maxWidth = overflow ? `${viewportHeight / videoHeight * videoWidth}px` : null; - target.style.margin = overflow ? '0 auto' : null; - } - }; - - // Handle resizing - const resized = () => { - clearTimeout(timers.resized); - timers.resized = setTimeout(setGutter, 50); - }; - on.call(player, elements.container, 'enterfullscreen exitfullscreen', event => { - const { - target - } = player.fullscreen; - - // Ignore events not from target - if (target !== elements.container) { - return; - } - - // If it's not an embed and no ratio specified - if (!player.isEmbed && is.empty(player.config.ratio)) { - return; - } - - // Set Vimeo gutter - setGutter(); - - // Watch for resizes - const method = event.type === 'enterfullscreen' ? on : off; - method.call(player, window, 'resize', resized); - }); - }); - // Listen for media events - _defineProperty$1(this, "media", () => { - const { - player - } = this; - const { - elements - } = player; - - // Time change on media - on.call(player, player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(player, event)); - - // Display duration - on.call(player, player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(player, event)); - - // Handle the media finishing - on.call(player, player.media, 'ended', () => { - // Show poster on end - if (player.isHTML5 && player.isVideo && player.config.resetOnEnd) { - // Restart - player.restart(); - - // Call pause otherwise IE11 will start playing the video again - player.pause(); - } - }); - - // Check for buffer progress - on.call(player, player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(player, event)); - - // Handle volume changes - on.call(player, player.media, 'volumechange', event => controls.updateVolume.call(player, event)); - - // Handle play/pause - on.call(player, player.media, 'playing play pause ended emptied timeupdate', event => ui.checkPlaying.call(player, event)); - - // Loading state - on.call(player, player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(player, event)); - - // Click video - if (player.supported.ui && player.config.clickToPlay && !player.isAudio) { - // Re-fetch the wrapper - const wrapper = getElement.call(player, `.${player.config.classNames.video}`); - - // Bail if there's no wrapper (this should never happen) - if (!is.element(wrapper)) { - return; - } - - // On click play, pause or restart - on.call(player, elements.container, 'click', event => { - const targets = [elements.container, wrapper]; - - // Ignore if click if not container or in video wrapper - if (!targets.includes(event.target) && !wrapper.contains(event.target)) { - return; - } - - // Touch devices will just show controls (if hidden) - if (player.touch && player.config.hideControls) { - return; - } - if (player.ended) { - this.proxy(event, player.restart, 'restart'); - this.proxy(event, () => { - silencePromise(player.play()); - }, 'play'); - } else { - this.proxy(event, () => { - silencePromise(player.togglePlay()); - }, 'play'); - } - }); - } - - // Disable right click - if (player.supported.ui && player.config.disableContextMenu) { - on.call(player, elements.wrapper, 'contextmenu', event => { - event.preventDefault(); - }, false); - } - - // Volume change - on.call(player, player.media, 'volumechange', () => { - // Save to storage - player.storage.set({ - volume: player.volume, - muted: player.muted - }); - }); - - // Speed change - on.call(player, player.media, 'ratechange', () => { - // Update UI - controls.updateSetting.call(player, 'speed'); - - // Save to storage - player.storage.set({ - speed: player.speed - }); - }); - - // Quality change - on.call(player, player.media, 'qualitychange', event => { - // Update UI - controls.updateSetting.call(player, 'quality', null, event.detail.quality); - }); - - // Update download link when ready and if quality changes - on.call(player, player.media, 'ready qualitychange', () => { - controls.setDownloadUrl.call(player); - }); - - // Proxy events to container - // Bubble up key events for Edge - const proxyEvents = player.config.events.concat(['keyup', 'keydown']).join(' '); - on.call(player, player.media, proxyEvents, event => { - let { - detail = {} - } = event; - - // Get error details from media - if (event.type === 'error') { - detail = player.media.error; - } - triggerEvent.call(player, elements.container, event.type, true, detail); - }); - }); - // Run default and custom handlers - _defineProperty$1(this, "proxy", (event, defaultHandler, customHandlerKey) => { - const { - player - } = this; - const customHandler = player.config.listeners[customHandlerKey]; - const hasCustomHandler = is.function(customHandler); - let returned = true; - - // Execute custom handler - if (hasCustomHandler) { - returned = customHandler.call(player, event); - } - - // Only call default handler if not prevented in custom handler - if (returned !== false && is.function(defaultHandler)) { - defaultHandler.call(player, event); - } - }); - // Trigger custom and default handlers - _defineProperty$1(this, "bind", (element, type, defaultHandler, customHandlerKey, passive = true) => { - const { - player - } = this; - const customHandler = player.config.listeners[customHandlerKey]; - const hasCustomHandler = is.function(customHandler); - on.call(player, element, type, event => this.proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler); - }); - // Listen for control events - _defineProperty$1(this, "controls", () => { - const { - player - } = this; - const { - elements - } = player; - // IE doesn't support input event, so we fallback to change - const inputEvent = browser.isIE ? 'change' : 'input'; - - // Play/pause toggle - if (elements.buttons.play) { - Array.from(elements.buttons.play).forEach(button => { - this.bind(button, 'click', () => { - silencePromise(player.togglePlay()); - }, 'play'); - }); - } - - // Pause - this.bind(elements.buttons.restart, 'click', player.restart, 'restart'); - - // Rewind - this.bind(elements.buttons.rewind, 'click', () => { - // Record seek time so we can prevent hiding controls for a few seconds after rewind - player.lastSeekTime = Date.now(); - player.rewind(); - }, 'rewind'); - - // Rewind - this.bind(elements.buttons.fastForward, 'click', () => { - // Record seek time so we can prevent hiding controls for a few seconds after fast forward - player.lastSeekTime = Date.now(); - player.forward(); - }, 'fastForward'); - - // Mute toggle - this.bind(elements.buttons.mute, 'click', () => { - player.muted = !player.muted; - }, 'mute'); - - // Captions toggle - this.bind(elements.buttons.captions, 'click', () => player.toggleCaptions()); - - // Download - this.bind(elements.buttons.download, 'click', () => { - triggerEvent.call(player, player.media, 'download'); - }, 'download'); - - // Fullscreen toggle - this.bind(elements.buttons.fullscreen, 'click', () => { - player.fullscreen.toggle(); - }, 'fullscreen'); - - // Picture-in-Picture - this.bind(elements.buttons.pip, 'click', () => { - player.pip = 'toggle'; - }, 'pip'); - - // Airplay - this.bind(elements.buttons.airplay, 'click', player.airplay, 'airplay'); - - // Settings menu - click toggle - this.bind(elements.buttons.settings, 'click', event => { - // Prevent the document click listener closing the menu - event.stopPropagation(); - event.preventDefault(); - controls.toggleMenu.call(player, event); - }, null, false); // Can't be passive as we're preventing default - - // Settings menu - keyboard toggle - // We have to bind to keyup otherwise Firefox triggers a click when a keydown event handler shifts focus - // https://bugzilla.mozilla.org/show_bug.cgi?id=1220143 - this.bind(elements.buttons.settings, 'keyup', event => { - if (![' ', 'Enter'].includes(event.key)) { - return; - } - - // Because return triggers a click anyway, all we need to do is set focus - if (event.key === 'Enter') { - controls.focusFirstMenuItem.call(player, null, true); - return; - } - - // Prevent scroll - event.preventDefault(); - - // Prevent playing video (Firefox) - event.stopPropagation(); - - // Toggle menu - controls.toggleMenu.call(player, event); - }, null, false // Can't be passive as we're preventing default - ); - - // Escape closes menu - this.bind(elements.settings.menu, 'keydown', event => { - if (event.key === 'Escape') { - controls.toggleMenu.call(player, event); - } - }); - - // Set range input alternative "value", which matches the tooltip time (#954) - this.bind(elements.inputs.seek, 'mousedown mousemove', event => { - const rect = elements.progress.getBoundingClientRect(); - const scrollLeft = event.pageX - event.clientX; - const percent = 100 / rect.width * (event.pageX - rect.left - scrollLeft); - event.currentTarget.setAttribute('seek-value', percent); - }); - - // Pause while seeking - this.bind(elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { - const seek = event.currentTarget; - const attribute = 'play-on-seeked'; - if (is.keyboardEvent(event) && !['ArrowLeft', 'ArrowRight'].includes(event.key)) { - return; - } - - // Record seek time so we can prevent hiding controls for a few seconds after seek - player.lastSeekTime = Date.now(); - - // Was playing before? - const play = seek.hasAttribute(attribute); - // Done seeking - const done = ['mouseup', 'touchend', 'keyup'].includes(event.type); - - // If we're done seeking and it was playing, resume playback - if (play && done) { - seek.removeAttribute(attribute); - silencePromise(player.play()); - } else if (!done && player.playing) { - seek.setAttribute(attribute, ''); - player.pause(); - } - }); - - // Fix range inputs on iOS - // Super weird iOS bug where after you interact with an , - // it takes over further interactions on the page. This is a hack - if (browser.isIos) { - const inputs = getElements.call(player, 'input[type="range"]'); - Array.from(inputs).forEach(input => this.bind(input, inputEvent, event => repaint(event.target))); - } - - // Seek - this.bind(elements.inputs.seek, inputEvent, event => { - const seek = event.currentTarget; - // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) - let seekTo = seek.getAttribute('seek-value'); - if (is.empty(seekTo)) { - seekTo = seek.value; - } - seek.removeAttribute('seek-value'); - player.currentTime = seekTo / seek.max * player.duration; - }, 'seek'); - - // Seek tooltip - this.bind(elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(player, event)); - - // Preview thumbnails plugin - // TODO: Really need to work on some sort of plug-in wide event bus or pub-sub for this - this.bind(elements.progress, 'mousemove touchmove', event => { - const { - previewThumbnails - } = player; - if (previewThumbnails && previewThumbnails.loaded) { - previewThumbnails.startMove(event); - } - }); - - // Hide thumbnail preview - on mouse click, mouse leave, and video play/seek. All four are required, e.g., for buffering - this.bind(elements.progress, 'mouseleave touchend click', () => { - const { - previewThumbnails - } = player; - if (previewThumbnails && previewThumbnails.loaded) { - previewThumbnails.endMove(false, true); - } - }); - - // Show scrubbing preview - this.bind(elements.progress, 'mousedown touchstart', event => { - const { - previewThumbnails - } = player; - if (previewThumbnails && previewThumbnails.loaded) { - previewThumbnails.startScrubbing(event); - } - }); - this.bind(elements.progress, 'mouseup touchend', event => { - const { - previewThumbnails - } = player; - if (previewThumbnails && previewThumbnails.loaded) { - previewThumbnails.endScrubbing(event); - } - }); - - // Polyfill for lower fill in for webkit - if (browser.isWebKit) { - Array.from(getElements.call(player, 'input[type="range"]')).forEach(element => { - this.bind(element, 'input', event => controls.updateRangeFill.call(player, event.target)); - }); - } - - // Current time invert - // Only if one time element is used for both currentTime and duration - if (player.config.toggleInvert && !is.element(elements.display.duration)) { - this.bind(elements.display.currentTime, 'click', () => { - // Do nothing if we're at the start - if (player.currentTime === 0) { - return; - } - player.config.invertTime = !player.config.invertTime; - controls.timeUpdate.call(player); - }); - } - - // Volume - this.bind(elements.inputs.volume, inputEvent, event => { - player.volume = event.target.value; - }, 'volume'); - - // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) - this.bind(elements.controls, 'mouseenter mouseleave', event => { - elements.controls.hover = !player.touch && event.type === 'mouseenter'; - }); - - // Also update controls.hover state for any non-player children of fullscreen element (as above) - if (elements.fullscreen) { - Array.from(elements.fullscreen.children).filter(c => !c.contains(elements.container)).forEach(child => { - this.bind(child, 'mouseenter mouseleave', event => { - if (elements.controls) { - elements.controls.hover = !player.touch && event.type === 'mouseenter'; - } - }); - }); - } - - // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) - this.bind(elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - elements.controls.pressed = ['mousedown', 'touchstart'].includes(event.type); - }); - - // Show controls when they receive focus (e.g., when using keyboard tab key) - this.bind(elements.controls, 'focusin', () => { - const { - config, - timers - } = player; - - // Skip transition to prevent focus from scrolling the parent element - toggleClass(elements.controls, config.classNames.noTransition, true); - - // Toggle - ui.toggleControls.call(player, true); - - // Restore transition - setTimeout(() => { - toggleClass(elements.controls, config.classNames.noTransition, false); - }, 0); - - // Delay a little more for mouse users - const delay = this.touch ? 3000 : 4000; - - // Clear timer - clearTimeout(timers.controls); - - // Hide again after delay - timers.controls = setTimeout(() => ui.toggleControls.call(player, false), delay); - }); - - // Mouse wheel for volume - this.bind(elements.inputs.volume, 'wheel', event => { - // Detect "natural" scroll - supported on OS X Safari only - // Other browsers on OS X will be inverted until support improves - const inverted = event.webkitDirectionInvertedFromDevice; - // Get delta from event. Invert if `inverted` is true - const [x, y] = [event.deltaX, -event.deltaY].map(value => inverted ? -value : value); - // Using the biggest delta, normalize to 1 or -1 (or 0 if no delta) - const direction = Math.sign(Math.abs(x) > Math.abs(y) ? x : y); - - // Change the volume by 2% - player.increaseVolume(direction / 50); - - // Don't break page scrolling at max and min - const { - volume - } = player.media; - if (direction === 1 && volume < 1 || direction === -1 && volume > 0) { - event.preventDefault(); - } - }, 'volume', false); - }); - this.player = _player; - this.lastKey = null; - this.focusTimer = null; - this.lastKeyDown = null; - this.handleKey = this.handleKey.bind(this); - this.toggleMenu = this.toggleMenu.bind(this); - this.firstTouch = this.firstTouch.bind(this); - } - - // Handle key presses - handleKey(event) { - const { - player - } = this; - const { - elements - } = player; - const { - key, - type, - altKey, - ctrlKey, - metaKey, - shiftKey - } = event; - const pressed = type === 'keydown'; - const repeat = pressed && key === this.lastKey; - - // Bail if a modifier key is set - if (altKey || ctrlKey || metaKey || shiftKey) { - return; - } - - // If the event is bubbled from the media element - // Firefox doesn't get the key for whatever reason - if (!key) { - return; - } - - // Seek by increment - const seekByIncrement = increment => { - // Divide the max duration into 10th's and times by the number value - player.currentTime = player.duration / 10 * increment; - }; - - // Handle the key on keydown - // Reset on keyup - if (pressed) { - // Check focused element - // and if the focused element is not editable (e.g. text input) - // and any that accept key input http://webaim.org/techniques/keyboard/ - const focused = document.activeElement; - if (is.element(focused)) { - const { - editable - } = player.config.selectors; - const { - seek - } = elements.inputs; - if (focused !== seek && matches(focused, editable)) { - return; - } - if (event.key === ' ' && matches(focused, 'button, [role^="menuitem"]')) { - return; - } - } - - // Which keys should we prevent default - const preventDefault = [' ', 'ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'c', 'f', 'k', 'l', 'm']; - - // If the key is found prevent default (e.g. prevent scrolling for arrows) - if (preventDefault.includes(key)) { - event.preventDefault(); - event.stopPropagation(); - } - switch (key) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - if (!repeat) { - seekByIncrement(Number.parseInt(key, 10)); - } - break; - case ' ': - case 'k': - if (!repeat) { - silencePromise(player.togglePlay()); - } - break; - case 'ArrowUp': - player.increaseVolume(0.1); - break; - case 'ArrowDown': - player.decreaseVolume(0.1); - break; - case 'm': - if (!repeat) { - player.muted = !player.muted; - } - break; - case 'ArrowRight': - player.forward(); - break; - case 'ArrowLeft': - player.rewind(); - break; - case 'f': - player.fullscreen.toggle(); - break; - case 'c': - if (!repeat) { - player.toggleCaptions(); - } - break; - case 'l': - player.loop = !player.loop; - break; - } - - // Escape is handle natively when in full screen - // So we only need to worry about non native - if (key === 'Escape' && !player.fullscreen.usingNative && player.fullscreen.active) { - player.fullscreen.toggle(); - } - - // Store last key for next cycle - this.lastKey = key; - } else { - this.lastKey = null; - } - } - - // Toggle menu - toggleMenu(event) { - controls.toggleMenu.call(this.player, event); - } - } - - function getDefaultExportFromCjs (x) { - return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; - } - - var loadjs_umd$1 = {exports: {}}; - - var loadjs_umd = loadjs_umd$1.exports; - var hasRequiredLoadjs_umd; - function requireLoadjs_umd() { - if (hasRequiredLoadjs_umd) return loadjs_umd$1.exports; - hasRequiredLoadjs_umd = 1; - (function (module, exports) { - (function (root, factory) { - { - module.exports = factory(); - } - })(loadjs_umd, function () { - /** - * Global dependencies. - * @global {Object} document - DOM - */ - - var devnull = function () {}, - bundleIdCache = {}, - bundleResultCache = {}, - bundleCallbackQueue = {}; - - /** - * Subscribe to bundle load event. - * @param {string[]} bundleIds - Bundle ids - * @param {Function} callbackFn - The callback function - */ - function subscribe(bundleIds, callbackFn) { - // listify - bundleIds = bundleIds.push ? bundleIds : [bundleIds]; - var depsNotFound = [], - i = bundleIds.length, - numWaiting = i, - fn, - bundleId, - r, - q; - - // define callback function - fn = function (bundleId, pathsNotFound) { - if (pathsNotFound.length) depsNotFound.push(bundleId); - numWaiting--; - if (!numWaiting) callbackFn(depsNotFound); - }; - - // register callback - while (i--) { - bundleId = bundleIds[i]; - - // execute callback if in result cache - r = bundleResultCache[bundleId]; - if (r) { - fn(bundleId, r); - continue; - } - - // add to callback queue - q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; - q.push(fn); - } - } - - /** - * Publish bundle load event. - * @param {string} bundleId - Bundle id - * @param {string[]} pathsNotFound - List of files not found - */ - function publish(bundleId, pathsNotFound) { - // exit if id isn't defined - if (!bundleId) return; - var q = bundleCallbackQueue[bundleId]; - - // cache result - bundleResultCache[bundleId] = pathsNotFound; - - // exit if queue is empty - if (!q) return; - - // empty callback queue - while (q.length) { - q[0](bundleId, pathsNotFound); - q.splice(0, 1); - } - } - - /** - * Execute callbacks. - * @param {Object or Function} args - The callback args - * @param {string[]} depsNotFound - List of dependencies not found - */ - function executeCallbacks(args, depsNotFound) { - // accept function as argument - if (args.call) args = { - success: args - }; - - // success and error callbacks - if (depsNotFound.length) (args.error || devnull)(depsNotFound);else (args.success || devnull)(args); - } - - /** - * Load individual file. - * @param {string} path - The file path - * @param {Function} callbackFn - The callback function - */ - function loadFile(path, callbackFn, args, numTries) { - var doc = document, - async = args.async, - maxTries = (args.numRetries || 0) + 1, - beforeCallbackFn = args.before || devnull, - pathname = path.replace(/[\?|#].*$/, ''), - pathStripped = path.replace(/^(css|img|module|nomodule)!/, ''), - isLegacyIECss, - hasModuleSupport, - e; - numTries = numTries || 0; - if (/(^css!|\.css$)/.test(pathname)) { - // css - e = doc.createElement('link'); - e.rel = 'stylesheet'; - e.href = pathStripped; - - // tag IE9+ - isLegacyIECss = 'hideFocus' in e; - - // use preload in IE Edge (to detect load errors) - if (isLegacyIECss && e.relList) { - isLegacyIECss = 0; - e.rel = 'preload'; - e.as = 'style'; - } - } else if (/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(pathname)) { - // image - e = doc.createElement('img'); - e.src = pathStripped; - } else { - // javascript - e = doc.createElement('script'); - e.src = pathStripped; - e.async = async === undefined ? true : async; - - // handle es modules - // modern browsers: - // module: add to dom with type="module" - // nomodule: call success() callback without adding to dom - // legacy browsers: - // module: call success() callback without adding to dom - // nomodule: add to dom with default type ("text/javascript") - hasModuleSupport = 'noModule' in e; - if (/^module!/.test(pathname)) { - if (!hasModuleSupport) return callbackFn(path, 'l'); - e.type = "module"; - } else if (/^nomodule!/.test(pathname) && hasModuleSupport) return callbackFn(path, 'l'); - } - e.onload = e.onerror = e.onbeforeload = function (ev) { - var result = ev.type[0]; - - // treat empty stylesheets as failures to get around lack of onerror - // support in IE9-11 - if (isLegacyIECss) { - try { - if (!e.sheet.cssText.length) result = 'e'; - } catch (x) { - // sheets objects created from load errors don't allow access to - // `cssText` (unless error is Code:18 SecurityError) - if (x.code != 18) result = 'e'; - } - } - - // handle retries in case of load failure - if (result == 'e') { - // increment counter - numTries += 1; - - // exit function and try again - if (numTries < maxTries) { - return loadFile(path, callbackFn, args, numTries); - } - } else if (e.rel == 'preload' && e.as == 'style') { - // activate preloaded stylesheets - return e.rel = 'stylesheet'; // jshint ignore:line - } - - // execute callback - callbackFn(path, result, ev.defaultPrevented); - }; - - // add to document (unless callback returns `false`) - if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); - } - - /** - * Load multiple files. - * @param {string[]} paths - The file paths - * @param {Function} callbackFn - The callback function - */ - function loadFiles(paths, callbackFn, args) { - // listify paths - paths = paths.push ? paths : [paths]; - var numWaiting = paths.length, - x = numWaiting, - pathsNotFound = [], - fn, - i; - - // define callback function - fn = function (path, result, defaultPrevented) { - // handle error - if (result == 'e') pathsNotFound.push(path); - - // handle beforeload event. If defaultPrevented then that means the load - // will be blocked (ex. Ghostery/ABP on Safari) - if (result == 'b') { - if (defaultPrevented) pathsNotFound.push(path);else return; - } - numWaiting--; - if (!numWaiting) callbackFn(pathsNotFound); - }; - - // load scripts - for (i = 0; i < x; i++) loadFile(paths[i], fn, args); - } - - /** - * Initiate script load and register bundle. - * @param {(string|string[])} paths - The file paths - * @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success - * callback or (3) object literal with success/error arguments, numRetries, - * etc. - * @param {(Function|Object)} [arg2] - The (1) success callback or (2) object - * literal with success/error arguments, numRetries, etc. - */ - function loadjs(paths, arg1, arg2) { - var bundleId, args; - - // bundleId (if string) - if (arg1 && arg1.trim) bundleId = arg1; - - // args (default is {}) - args = (bundleId ? arg2 : arg1) || {}; - - // throw error if bundle is already defined - if (bundleId) { - if (bundleId in bundleIdCache) { - throw "LoadJS"; - } else { - bundleIdCache[bundleId] = true; - } - } - function loadFn(resolve, reject) { - loadFiles(paths, function (pathsNotFound) { - // execute callbacks - executeCallbacks(args, pathsNotFound); - - // resolve Promise - if (resolve) { - executeCallbacks({ - success: resolve, - error: reject - }, pathsNotFound); - } - - // publish bundle load event - publish(bundleId, pathsNotFound); - }, args); - } - if (args.returnPromise) return new Promise(loadFn);else loadFn(); - } - - /** - * Execute callbacks when dependencies have been satisfied. - * @param {(string|string[])} deps - List of bundle ids - * @param {Object} args - success/error arguments - */ - loadjs.ready = function ready(deps, args) { - // subscribe to bundle load event - subscribe(deps, function (depsNotFound) { - // execute callbacks - executeCallbacks(args, depsNotFound); - }); - return loadjs; - }; - - /** - * Manually satisfy bundle dependencies. - * @param {string} bundleId - The bundle id - */ - loadjs.done = function done(bundleId) { - publish(bundleId, []); - }; - - /** - * Reset loadjs dependencies statuses - */ - loadjs.reset = function reset() { - bundleIdCache = {}; - bundleResultCache = {}; - bundleCallbackQueue = {}; - }; - - /** - * Determine if bundle has already been defined - * @param String} bundleId - The bundle id - */ - loadjs.isDefined = function isDefined(bundleId) { - return bundleId in bundleIdCache; - }; - - // export - return loadjs; - }); - })(loadjs_umd$1); - return loadjs_umd$1.exports; - } - - var loadjs_umdExports = requireLoadjs_umd(); - var loadjs = /*@__PURE__*/getDefaultExportFromCjs(loadjs_umdExports); - - // ========================================================================== - // Load an external script - // ========================================================================== - - function loadScript(url) { - return new Promise((resolve, reject) => { - loadjs(url, { - success: resolve, - error: reject - }); - }); - } - - // ========================================================================== - // Vimeo plugin - // ========================================================================== - - - // Parse Vimeo ID from URL - function parseId$1(url) { - if (is.empty(url)) { - return null; - } - if (is.number(Number(url))) { - return url; - } - - // eslint-disable-next-line regexp/optimal-quantifier-concatenation - const regex = /^.*(vimeo.com\/|video\/)(\d+).*/; - const match = url.match(regex); - return match ? match[2] : url; - } - - // Try to extract a hash for private videos from the URL - function parseHash(url) { - /* This regex matches a hexadecimal hash if given in any of these forms: - * - [https://player.]vimeo.com/video/{id}/{hash}[?params] - * - [https://player.]vimeo.com/video/{id}?h={hash}[¶ms] - * - [https://player.]vimeo.com/video/{id}?[params]&h={hash} - * - video/{id}/{hash} - * If matched, the hash is available in capture group 4 - */ - const regex = /^.*(vimeo.com\/|video\/)(\d+)(\?.*h=|\/)+([\d,a-f]+)/; - const found = url.match(regex); - return found && found.length === 5 ? found[4] : null; - } - - // Set playback state and trigger change (only on actual change) - function assurePlaybackState$1(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'); - } - } - const vimeo = { - 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 initial 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; - const { - premium, - referrerPolicy, - ...frameParams - } = config; - // Get the source URL or ID - let source = player.media.getAttribute('src'); - let hash = ''; - // Get from
if needed - if (is.empty(source)) { - source = player.media.getAttribute(player.config.attributes.embed.id); - // hash can also be set as attribute on the
- hash = player.media.getAttribute(player.config.attributes.embed.hash); - } else { - hash = parseHash(source); - } - const hashParam = hash ? { - h: hash - } : {}; - - // If the owner has a pro or premium account then we can hide controls etc - if (premium) { - Object.assign(frameParams, { - controls: false, - sidedock: false - }); - } - - // Get Vimeo params for the iframe - const params = buildUrlParams({ - loop: player.config.loop.active, - autoplay: player.autoplay, - muted: player.muted, - gesture: 'media', - playsinline: player.config.playsinline, - // hash has to be added to iframe-URL - ...hashParam, - ...frameParams - }); - const id = parseId$1(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('allow', ['autoplay', 'fullscreen', 'picture-in-picture', 'encrypted-media', 'accelerometer', 'gyroscope'].join('; ')); - - // Set the referrer policy if required - if (!is.empty(referrerPolicy)) { - iframe.setAttribute('referrerPolicy', referrerPolicy); - } - - // Inject the package - if (premium || !config.customControls) { - iframe.setAttribute('data-poster', player.poster); - player.media = replaceElement(iframe, player.media); - } else { - const wrapper = createElement('div', { - 'class': player.config.classNames.embedContainer, - 'data-poster': player.poster - }); - wrapper.appendChild(iframe); - player.media = replaceElement(wrapper, player.media); - } - - // Get poster image - if (!config.customControls) { - fetch(format(player.config.urls.vimeo.api, src)).then(response => { - if (is.empty(response) || !response.thumbnail_url) { - return; - } - - // Set and show poster - ui.setPoster.call(player, response.thumbnail_url).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$1.call(player, true); - return player.embed.play(); - }; - player.media.pause = () => { - assurePlaybackState$1.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'); - }); - } - }); - - // Muted - let { - muted - } = player.config; - Object.defineProperty(player.media, 'muted', { - get() { - return muted; - }, - set(input) { - const toggle = is.boolean(input) ? input : false; - player.embed.setMuted(toggle ? true : player.config.muted).then(() => { - muted = toggle; - triggerEvent.call(player, player.media, 'volumechange'); - }); - } - }); - - // 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; - }); - } - }); - - // 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 = roundAspectRatio(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$1.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$1.call(player, true); - triggerEvent.call(player, player.media, 'playing'); - }); - player.embed.on('pause', () => { - assurePlaybackState$1.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 (Number.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 - if (config.customControls) { - setTimeout(() => ui.build.call(player), 0); - } - } - }; - - // ========================================================================== - // YouTube plugin - // ========================================================================== - - - // Parse YouTube ID from URL - function parseId(url) { - if (is.empty(url)) { - return null; - } - const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; - const match = url.match(regex); - return match && match[2] ? match[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'); - } - } - function getHost(config) { - if (config.noCookie) { - return 'https://www.youtube-nocookie.com'; - } - if (window.location.protocol === 'http:') { - return 'http://www.youtube.com'; - } - - // 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); - }); - } - }, - // Get the media title - getTitle(videoId) { - const url = format(this.config.urls.youtube.api, videoId); - fetch(url).then(data => { - if (is.object(data)) { - const { - title, - height, - width - } = data; - - // Set title - this.config.title = title; - ui.setTitle.call(this); - - // Set aspect ratio - this.embed.ratio = roundAspectRatio(width, height); - } - setAspectRatio.call(this); - }).catch(() => { - // Set aspect ratio - setAspectRatio.call(this); - }); - }, - // API ready - ready() { - const player = this; - const config = player.config.youtube; - // 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); - } - - // Replace the