From 8351d68149833310f5ef63f4d00beae8128c33a4 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Fri, 9 Oct 2026 09:19:24 +0800 Subject: [PATCH 1/2] Update Plyr assets and UI behavior Rebuilt Plyr to a newer bundle version, refreshed generated module/minified assets, and bumped default asset URLs to 3.8.5. Normalized CSS spacing and sizing to use calc(...) with variables, with small visual tweaks to captions, thumbnails, and fullscreen layout. Refactored playback/UI logic with improved volume tracking, mute toggling, caption track matching, and embed/API updates. --- .../BootstrapBlazor.Player/wwwroot/plyr.css | 2 +- .../BootstrapBlazor.Player/wwwroot/plyr.js | 14222 ++++++---------- .../wwwroot/plyr.min.js | 4 +- 3 files changed, 5424 insertions(+), 8804 deletions(-) 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