Compare commits

...

87 Commits

Author SHA1 Message Date
2782a00e7c v3.1.0-beta.2 2018-04-03 22:31:55 +10:00
91d192dd7c YouTube speed menu fix 2018-04-03 22:30:29 +10:00
b1e3abc795 v3.1.0-beta.1 2018-04-02 22:52:02 +10:00
3395e8df90 HTML5 quality selection 2018-04-02 22:40:03 +10:00
cce143a7da v3.0.11 2018-03-30 23:14:07 +11:00
d593005b32 Muted and autoplay fixes, small bug fixes 2018-03-30 23:09:17 +11:00
7be9d5d4d3 v3.0.10 2018-03-30 00:16:42 +11:00
d7141d5ed7 Controls docs, package upgrades 2018-03-30 00:11:48 +11:00
0d0ece94d3 Fix regression 2018-03-29 20:20:37 +11:00
1c06f6d06d Vimeo hotfix 2018-03-29 19:35:02 +11:00
dda8e30b92 Merge branch 'master' of github.com:sampotts/plyr 2018-03-28 22:45:18 +11:00
c4e2e24643 Bug fixes 2018-03-28 22:45:11 +11:00
e020a105a3 Update readme.md 2018-03-28 00:55:55 +11:00
2b7fe9a4f9 v3.0.6 2018-03-28 00:17:15 +11:00
951df64b7f v3.0.5 2018-03-27 23:52:26 +11:00
0976afe282 v3.0.4 2018-03-27 23:47:58 +11:00
7b1e4abda7 Controls fixes 2018-03-27 23:43:38 +11:00
0cf75eed3f Revert API method change 2018-03-27 21:15:11 +11:00
d96957d086 Allow fullscreen in iframe 2018-03-27 21:13:22 +11:00
1a032ea498 Fix for seeking issue 2018-03-27 21:10:06 +11:00
5d079da1b8 Use object.entries 2018-03-27 10:41:06 +11:00
9c1bc6ab08 Fixes for fast forward and issues with event.preventDefault() 2018-03-27 10:36:08 +11:00
3d2ba8c009 Update readme.md 2018-03-22 09:11:42 +11:00
e872ce3f77 Update readme.md 2018-03-22 09:10:50 +11:00
b77756da04 Typo 2018-03-22 01:15:10 +11:00
9b23e13ce8 v3.0.3 2018-03-22 01:13:37 +11:00
5eafe9baff Vimeo offset tweak (fixes #826) 2018-03-22 01:08:08 +11:00
c251c94131 Fix for .stop() method (fixes #819) 2018-03-22 01:02:38 +11:00
17041efc71 Check for array for speed options (fixes #252) 2018-03-22 00:33:14 +11:00
05b8e8a6e0 Restore as float (fixes #828) 2018-03-22 00:28:42 +11:00
f998b996fa Fix for Firefox fullscreen oddness (Fixes #821) 2018-03-22 00:26:01 +11:00
958b47c435 Merge branch 'master' of github.com:sampotts/plyr 2018-03-22 00:06:26 +11:00
a27248d3b6 Merge pull request #820 from saadshahd/patch-1
Fix fast-forward control
2018-03-22 00:05:24 +11:00
1b1f7be7ff Merge branch 'master' of github.com:sampotts/plyr 2018-03-22 00:04:34 +11:00
59d4a27240 Improve Sprite checking (fixes #827) 2018-03-22 00:04:28 +11:00
75e9f3c2e3 Fix fast-forward control
fast-forward control doesn't work.
2018-03-21 12:15:57 +02:00
7132eccf50 Merge pull request #822 from DanielRuf/patch/fix-options-link
fix the options link in the readme
2018-03-21 09:12:06 +11:00
e953c6398c fix the options link in the readme 2018-03-20 15:05:51 +01:00
bb7eea27e5 v3.0.2 2018-03-18 22:46:36 +11:00
595c5e95bc Fix for Safari with adblockers 2018-03-18 22:37:06 +11:00
43e6dcd41d Fix for local storage issue 2018-03-18 01:37:24 +11:00
b06c8ae43f Changelog updated 2018-03-18 01:14:18 +11:00
c7ea13c0c7 Sentry in live only 2018-03-18 01:08:05 +11:00
0f8c6e147b Added Sentry 2018-03-18 00:21:23 +11:00
e566365288 Typo 2018-03-17 23:44:40 +11:00
a06e0f5890 Updated screenshot 2018-03-17 23:40:28 +11:00
3bccc0da01 v3.0.0 2018-03-17 23:33:25 +11:00
a0173d991e Removed beta message 2018-03-17 23:31:34 +11:00
600f0eb8a3 Merge branch 'beta'
# Conflicts:
#	readme.md
2018-03-17 23:30:16 +11:00
5db73b1327 Added buffered getter 2018-03-17 23:27:40 +11:00
5cb1628cd8 Vimeo fix 2018-03-15 10:29:05 +11:00
c74b75e8e1 3.0.0-beta.20 2018-03-13 23:35:17 +11:00
0538476d6f 3.0.0-beta.19 2018-03-13 22:15:28 +11:00
5ebe18d081 Typography fix 2018-03-13 22:00:40 +11:00
207adde36d 3.0.0-beta.18 2018-03-13 21:44:18 +11:00
1b13ddaa54 Update ads 2018-03-13 21:42:01 +11:00
9981c349be Fix for null manager race condition 2018-03-11 18:23:47 +11:00
b3365a7373 Normalised event names and removed unused 2018-03-11 12:54:51 +11:00
9a0c1c830d Merge pull request #804 from friday/ads-trigger-arguments
[v3] Add optional arguments to Ads.trigger
2018-03-11 10:55:29 +11:00
ef27ba16f4 Add optional argument to Ads.trigger (currently only used for adblocker error) 2018-03-10 16:20:33 +01:00
e206edc1f6 Event listener fixes, loadScript promise, ads tweaks 2018-03-11 02:03:35 +11:00
c734bc4957 Merge branch 'beta' of github.com:sampotts/plyr into beta 2018-03-10 23:32:55 +11:00
572b8a7aca Manually merged PRs 2018-03-10 23:32:48 +11:00
eebae4a227 Merge pull request #802 from gehaktmolen/ad-hotfixes
Advertisement couldnt be loaded when creative dimensions do not align after resizing
2018-03-10 23:32:15 +11:00
e0562752ea Merge pull request #795 from frogg/patch-1
Added link that explains Webkit's autoplay blocker
2018-03-10 23:29:22 +11:00
6a2ca534d2 Removed redundant wrappers within the adsmanager promises. 2018-03-09 14:29:37 +01:00
7adc2bc6c8 Unneeded else has been removed within the play() method. 2018-03-09 13:21:19 +01:00
ba8d7831a7 Made sure play() returns a promise. 2018-03-09 12:50:57 +01:00
69ffcbad27 Ad block detection would not work when calling play() right after creating the player instance, so the adsManager now also rejects on such a case. Also made sure that calling play() will wait for the adsManager promise to resolve or otherwise return the media.play() method. 2018-03-09 11:17:24 +01:00
819f7d1080 Resizing the ad container while having it on display none will return offset width and height of 0, which will cause ads not to play when ad sizes are set within the clients DSP. Also making sure that the inner containers of the ad container are full size. The container is now hidden/ displayed using z-index. 2018-03-07 15:43:48 +01:00
409b588458 Made sure that cue points for midrolls are not displayed when the ad rule for a midroll doesn't exceed the total play time of a video. 2018-03-07 15:17:30 +01:00
e90a603d57 Removed a double this.enabled variable and updated a comment in ads.js. Also made sure the adsmanager promise also can fail, so we can use it to wait for getting the advertisement ready when someone clicks the play button. Otherwise there it can look glitchy when the actual video starts playing and the video ad plays a few seconds later because the vast tag was slow to retrieve. Also fixed a typo. 2018-03-06 17:27:59 +01:00
6f061621ad v3.0.0-beta.17 2018-03-03 23:16:15 +11:00
0300610108 Typo 2018-03-03 23:14:57 +11:00
2fba5f152c 3.0.0-beta.16 2018-03-03 23:06:54 +11:00
317b08c703 Ready event fix, YouTube play event fix, docs update 2018-03-03 23:06:12 +11:00
e6db374a72 Added link that explains Webkit's autoplay blocker 2018-02-24 16:19:55 +01:00
bfb550b8d0 Package updates 2018-02-22 23:46:52 +11:00
174234c166 v3.0.0-beta.15 2018-02-19 09:55:16 +11:00
24b4220de5 Fix IE CORS captions 2018-02-19 09:52:46 +11:00
f1895a4cce Pause button fix, polyfilled build, unminified builds 2018-02-17 19:34:15 +11:00
c2a6306d46 Merge pull request #781 from friday/gulp-unminified-js-output
Build both minified and non-minified js-bundles
2018-02-17 09:22:34 +11:00
7ac732f45b Merge branch 'beta' into gulp-unminified-js-output 2018-02-17 09:22:19 +11:00
a59dcb2f53 Gulp js build: create both minified and non-minified outputs 2018-02-06 01:57:27 +01:00
ab7f277a1b Merge pull request #769 from redxtech/add-vue-plyr-to-readme
Add vue-plyr to readme
2018-02-06 10:51:42 +11:00
ce1d5a60d6 Remove eslint-rule 'no-shadow' (common variable names should be able to exist in different scopes) 2018-02-05 23:22:20 +01:00
d5a1a7ca1c Add vue-plyr to readme 2018-01-28 23:03:05 -07:00
48 changed files with 28576 additions and 2072 deletions

View File

@ -8,6 +8,7 @@
"globals": { "Plyr": false, "jQuery": false }, "globals": { "Plyr": false, "jQuery": false },
"rules": { "rules": {
"no-const-assign": 1, "no-const-assign": 1,
"no-shadow": 0,
"no-this-before-super": 1, "no-this-before-super": 1,
"no-undef": 1, "no-undef": 1,
"no-unreachable": 1, "no-unreachable": 1,

View File

@ -1,6 +1,6 @@
{ {
"plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"], "plugins": ["stylelint-selector-bem-pattern", "stylelint-scss"],
"extends": ["stylelint-config-sass-guidelines", "stylelint-config-prettier"], "extends": ["stylelint-config-recommended", "stylelint-config-sass-guidelines", "stylelint-config-prettier"],
"rules": { "rules": {
"selector-class-pattern": null, "selector-class-pattern": null,
"selector-no-qualifying-type": [ "selector-no-qualifying-type": [
@ -9,6 +9,7 @@
"ignore": ["attribute", "class"] "ignore": ["attribute", "class"]
} }
], ],
"string-no-newline": null,
"indentation": 4, "indentation": 4,
"string-quotes": "single", "string-quotes": "single",
"max-nesting-depth": 2, "max-nesting-depth": 2,

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost/dev/plyr/demo",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -4,7 +4,8 @@
"plyr.css": "src/sass/plyr.scss" "plyr.css": "src/sass/plyr.scss"
}, },
"js": { "js": {
"plyr.js": "src/js/plyr.js" "plyr.js": "src/js/plyr.js",
"plyr.polyfilled.js": "src/js/plyr.polyfilled.js"
} }
}, },
"demo": { "demo": {

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,72 @@
# Controls # Controls
This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. This is the markup that is rendered for the Plyr controls. You can use the default controls or provide a customized version of markup based on your needs. You can pass the following to the `controls` option:
## Internationalization using default controls * `Array` of options (this builds the default controls based on your choices)
* `String` containing the desired HTML
* `Function` that will be executed and should return one of the above
## Using default controls
If you want to use the standard controls as they are, you don't need to pass any options. If you want to turn on off controls, here's the full list:
```javascript
controls: [
'play-large', // The large play button in the center
'restart', // Restart playback
'rewind', // Rewind by the seek time (default 10 seconds)
'play', // Play/pause playback
'fast-forward', // Fast forward by the seek time (default 10 seconds)
'progress', // The progress bar and scrubber for playback and buffering
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'volume', // Volume control
'captions', // Toggle captions
'settings', // Settings menu
'pip', // Picture-in-picture (currently Safari only)
'airplay', // Airplay (currently Safari only)
'fullscreen', // Toggle fullscreen
];
```
### Internationalization using default controls
You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls. You can provide an `i18n` object as one of your options when initialising the plugin which we be used when rendering the controls.
### Example #### Example
```javascript ```javascript
i18n: { i18n: {
restart: "Restart", restart: 'Restart',
rewind: "Rewind {seektime} secs", rewind: 'Rewind {seektime} secs',
play: "Play", play: 'Play',
pause: "Pause", pause: 'Pause',
forward: "Forward {seektime} secs", fastForward: 'Forward {seektime} secs',
buffered: "buffered", seek: 'Seek',
currentTime: "Current time", played: 'Played',
duration: "Duration", buffered: 'Buffered',
volume: "Volume", currentTime: 'Current time',
toggleMute: "Toggle Mute", duration: 'Duration',
toggleCaptions: "Toggle Captions", volume: 'Volume',
toggleFullscreen: "Toggle Fullscreen" mute: 'Mute',
unmute: 'Unmute',
enableCaptions: 'Enable captions',
disableCaptions: 'Disable captions',
enterFullscreen: 'Enter fullscreen',
exitFullscreen: 'Exit fullscreen',
frameTitle: 'Player for {title}',
captions: 'Captions',
settings: 'Settings',
speed: 'Speed',
quality: 'Quality',
loop: 'Loop',
start: 'Start',
end: 'End',
all: 'All',
reset: 'Reset',
disabled: 'Disabled',
advertisement: 'Ad',
} }
``` ```
@ -29,85 +74,78 @@ Note: `{seektime}` will be replaced with your configured seek time or the defaul
## Using custom HTML ## Using custom HTML
You can specify the HTML for the controls using the `html` option. You can specify the HTML as a `String` or your `Function` return for the controls using the `controls` option.
The classes and data attributes used in your template should match the `selectors` option. The classes and data attributes used in your template should match the `selectors` option if you change any.
You need to add several placeholders to your html template that are replaced when rendering: You need to add several placeholders to your HTML template that are replaced when rendering:
- `{id}` - the dynamically generated ID for the player (for form controls) * `{id}` - the dynamically generated ID for the player (for form controls)
- `{seektime}` - the seek time specified in options for fast forward and rewind * `{seektime}` - the seek time specified in options for fast forward and rewind
- `{title}` - the title of your media, if specified * `{title}` - the title of your media, if specified
You can include only the controls you need when specifying custom html. ### Limitations
* Currently the settings menus are not supported with custom controls HTML
* AirPlay and PiP buttons can be added but you will have to manage feature detection
### Example ### Example
This is an example `html` option with all controls. Here's an example of custom controls markup (this is just all default controls shown).
```javascript ```javascript
var controls = ["<div class='plyr__controls'>", const controls = `
"<button type='button' data-plyr='restart'>", <div class="plyr__controls">
"<svg><use xlink:href='#plyr-restart'></use></svg>", <button type="button" class="plyr__control" data-plyr="restart">
"<span class='plyr__sr-only'>Restart</span>", <svg role="presentation"><use xlink:href="#plyr-restart"></use></svg>
"</button>", <span class="plyr__tooltip" role="tooltip">Restart</span>
"<button type='button' data-plyr='rewind'>", </button>
"<svg><use xlink:href='#plyr-rewind'></use></svg>", <button type="button" class="plyr__control" data-plyr="rewind">
"<span class='plyr__sr-only'>Rewind {seektime} secs</span>", <svg role="presentation"><use xlink:href="#plyr-rewind"></use></svg>
"</button>", <span class="plyr__tooltip" role="tooltip">Rewind {seektime} secs</span>
"<button type='button' data-plyr='play'>", </button>
"<svg><use xlink:href='#plyr-play'></use></svg>", <button type="button" class="plyr__control" aria-pressed="false" aria-label="Play, {title}" data-plyr="play">
"<span class='plyr__sr-only'>Play</span>", <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-pause"></use></svg>
"</button>", <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-play"></use></svg>
"<button type='button' data-plyr='pause'>", <span class="label--pressed plyr__tooltip" role="tooltip">Pause</span>
"<svg><use xlink:href='#plyr-pause'></use></svg>", <span class="label--not-pressed plyr__tooltip" role="tooltip">Play</span>
"<span class='plyr__sr-only'>Pause</span>", </button>
"</button>", <button type="button" class="plyr__control" data-plyr="fast-forward">
"<button type='button' data-plyr='fast-forward'>", <svg role="presentation"><use xlink:href="#plyr-fast-forward"></use></svg>
"<svg><use xlink:href='#plyr-fast-forward'></use></svg>", <span class="plyr__tooltip" role="tooltip">Forward {seektime} secs</span>
"<span class='plyr__sr-only'>Forward {seektime} secs</span>", </button>
"</button>", <div class="plyr__progress">
"<span class='plyr__progress'>", <label for="plyr-seek-{id}" class="plyr__sr-only">Seek</label>
"<label for='seek{id}' class='plyr__sr-only'>Seek</label>", <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" id="plyr-seek-{id}">
"<input id='seek{id}' class='plyr__progress--seek' type='range' min='0' max='100' step='0.1' value='0' data-plyr='seek'>", <progress class="plyr__progress--buffer" min="0" max="100" value="0">% buffered</progress>
"<progress class='plyr__progress--played' max='100' value='0' role='presentation'></progress>", <span role="tooltip" class="plyr__tooltip">00:00</span>
"<progress class='plyr__progress--buffer' max='100' value='0'>", </div>
"<span>0</span>% buffered", <div class="plyr__time">00:00</div>
"</progress>", <button type="button" class="plyr__control" aria-pressed="false" aria-label="Mute" data-plyr="mute">
"<span class='plyr__tooltip'>00:00</span>", <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-muted"></use></svg>
"</span>", <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-volume"></use></svg>
"<span class='plyr__time'>", <span class="label--pressed plyr__tooltip" role="tooltip">Unmute</span>
"<span class='plyr__sr-only'>Current time</span>", <span class="label--not-pressed plyr__tooltip" role="tooltip">Mute</span>
"<span class='plyr__time--current'>00:00</span>", </button>
"</span>", <div class="plyr__volume">
"<span class='plyr__time'>", <label for="plyr-volume-{id}" class="plyr__sr-only">Volume</label>
"<span class='plyr__sr-only'>Duration</span>", <input data-plyr="volume" type="range" min="0" max="1" step="0.05" value="1" autocomplete="off" id="plyr-volume-{id}">
"<span class='plyr__time--duration'>00:00</span>", </div>
"</span>", <button type="button" class="plyr__control" aria-pressed="true" aria-label="Enable captions" data-plyr="captions">
"<button type='button' data-plyr='mute'>", <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-captions-on"></use></svg>
"<svg class='icon--muted'><use xlink:href='#plyr-muted'></use></svg>", <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-captions-off"></use></svg>
"<svg><use xlink:href='#plyr-volume'></use></svg>", <span class="label--pressed plyr__tooltip" role="tooltip">Disable captions</span>
"<span class='plyr__sr-only'>Toggle Mute</span>", <span class="label--not-pressed plyr__tooltip" role="tooltip">Enable captions</span>
"</button>", </button>
"<span class='plyr__volume'>", <button type="button" class="plyr__control" aria-pressed="false" aria-label="Enter fullscreen" data-plyr="fullscreen">
"<label for='volume{id}' class='plyr__sr-only'>Volume</label>", <svg class="icon--pressed" role="presentation"><use xlink:href="#plyr-exit-fullscreen"></use></svg>
"<input id='volume{id}' class='plyr__volume--input' type='range' min='0' max='10' value='5' data-plyr='volume'>", <svg class="icon--not-pressed" role="presentation"><use xlink:href="#plyr-enter-fullscreen"></use></svg>
"<progress class='plyr__volume--display' max='10' value='0' role='presentation'></progress>", <span class="label--pressed plyr__tooltip" role="tooltip">Exit fullscreen</span>
"</span>", <span class="label--not-pressed plyr__tooltip" role="tooltip">Enter fullscreen</span>
"<button type='button' data-plyr='captions'>", </button>
"<svg class='icon--captions-on'><use xlink:href='#plyr-captions-on'></use></svg>", </div>
"<svg><use xlink:href='#plyr-captions-off'></use></svg>", `;
"<span class='plyr__sr-only'>Toggle Captions</span>",
"</button>",
"<button type='button' data-plyr='fullscreen'>",
"<svg class='icon--exit-fullscreen'><use xlink:href='#plyr-exit-fullscreen'></use></svg>",
"<svg><use xlink:href='#plyr-enter-fullscreen'></use></svg>",
"<span class='plyr__sr-only'>Toggle Fullscreen</span>",
"</button>",
"</div>"].join("");
// Setup the player // Setup the player
plyr.setup('.js-player', { const player = new Plyr('#player', { controls });
html: controls
});
``` ```

2
demo/dist/demo.css vendored

File diff suppressed because one or more lines are too long

4123
demo/dist/demo.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

2
demo/dist/demo.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
demo/dist/demo.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -66,7 +66,7 @@
</p> </p>
<p>Premium video monitization from <p>Premium video monitization from
<a href="https://vi.ai/publisher-video-monetization/" target="_blank" class="no-border"> <a href="https://vi.ai/publisher-video-monetization/?aid=plyrio" target="_blank" class="no-border">
<img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi"> <img src="https://cdn.plyr.io/static/vi-logo-24x24.svg" alt="ai.vi">
<span class="sr-only">ai.vi</span> <span class="sr-only">ai.vi</span>
</a> </a>
@ -93,16 +93,18 @@
<main> <main>
<video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player"> <video controls crossorigin playsinline poster="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg" id="player">
<!-- Video files --> <!-- Video files -->
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" type="video/mp4"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" type="video/mp4" size="576">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.webm" type="video/webm"> <source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4" type="video/mp4" size="720">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4" type="video/mp4" size="1080">
<source src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4" type="video/mp4" size="1440">
<!-- Text track file --> <!-- Caption files -->
<track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt" <track kind="captions" label="English" srclang="en" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt"
default> default>
<track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt"> <track kind="captions" label="Français" srclang="fr" src="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt">
<!-- Fallback for browsers that don't support the <video> element --> <!-- Fallback for browsers that don't support the <video> element -->
<a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4" download>Download</a> <a href="https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4" download>Download</a>
</video> </video>
<ul> <ul>
@ -163,25 +165,26 @@
c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path> c-1.1,0.9-2.5,1.4-4.1,1.4c-0.3,0-0.5,0-0.8,0c1.5,0.9,3.2,1.5,5,1.5c6,0,9.3-5,9.3-9.3c0-0.1,0-0.3,0-0.4C15,4.3,15.6,3.7,16,3z"></path>
</svg> </svg>
<p>If you think Plyr's good, <p>If you think Plyr's good,
<a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&url=http%3A%2F%2Fplyr.io&via=Sam_Potts" <a href="https://twitter.com/intent/tweet?text=A+simple+HTML5+media+player+with+custom+controls+and+WebVTT+captions.&amp;url=http%3A%2F%2Fplyr.io&amp;via=Sam_Potts"
target="_blank" data-shr-network="twitter">tweet it</a> target="_blank" data-shr-network="twitter">tweet it</a>
</p> </p>
</aside> </aside>
<!-- Polyfills --> <!-- Polyfills -->
<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent"></script> <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Array.prototype.includes,CustomEvent,Object.entries,Object.values"
crossorigin="anonymous"></script>
<!-- Plyr core script --> <!-- Plyr core script -->
<script src="../dist/plyr.js"></script> <script src="../dist/plyr.js" crossorigin="anonymous"></script>
<!-- Sharing libary (https://shr.one) --> <!-- Sharing libary (https://shr.one) -->
<script src="https://cdn.shr.one/1.0.1/shr.js"></script> <script src="https://cdn.shr.one/1.0.1/shr.js" crossorigin="anonymous"></script>
<!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) --> <!-- Rangetouch to fix <input type="range"> on touch devices (see https://rangetouch.com) -->
<script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async></script> <script src="https://cdn.rangetouch.com/1.0.1/rangetouch.js" async crossorigin="anonymous"></script>
<!-- Docs script --> <!-- Docs script -->
<script src="dist/demo.js"></script> <script src="dist/demo.js" crossorigin="anonymous"></script>
</body> </body>
</html> </html>

View File

@ -4,242 +4,292 @@
// Please see readme.md in the root or github.com/sampotts/plyr // Please see readme.md in the root or github.com/sampotts/plyr
// ========================================================================== // ==========================================================================
document.addEventListener('DOMContentLoaded', () => { import Raven from 'raven-js';
if (window.shr) {
window.shr.setup({ (() => {
count: { const isLive = window.location.host === 'plyr.io';
classname: 'button__count',
}, // Raven / Sentry
}); // For demo site (https://plyr.io) only
if (isLive) {
Raven.config('https://d4ad9866ad834437a4754e23937071e4@sentry.io/305555').install();
} }
// Setup tab focus document.addEventListener('DOMContentLoaded', () => {
const tabClassName = 'tab-focus'; Raven.context(() => {
if (window.shr) {
window.shr.setup({
count: {
classname: 'button__count',
},
});
}
// Remove class on blur // Setup tab focus
document.addEventListener('focusout', event => { const tabClassName = 'tab-focus';
event.target.classList.remove(tabClassName);
});
// Add classname to tabbed elements // Remove class on blur
document.addEventListener('keydown', event => { document.addEventListener('focusout', event => {
if (event.keyCode !== 9) { event.target.classList.remove(tabClassName);
return; });
}
// Delay the adding of classname until the focus has changed // Add classname to tabbed elements
// This event fires before the focusin event document.addEventListener('keydown', event => {
window.setTimeout(() => { if (event.keyCode !== 9) {
document.activeElement.classList.add(tabClassName); return;
}, 0); }
});
// Setup the player // Delay the adding of classname until the focus has changed
const player = new Plyr('#player', { // This event fires before the focusin event
debug: true, setTimeout(() => {
title: 'View From A Blue Moon', document.activeElement.classList.add(tabClassName);
// iconUrl: '../dist/plyr.svg', }, 0);
keyboard: { });
global: true,
},
tooltips: {
controls: true,
},
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
},
});
// Expose for testing // Setup the player
window.player = player; const player = new Plyr('#player', {
debug: true,
title: 'View From A Blue Moon',
iconUrl: '../dist/plyr.svg',
keyboard: {
global: true,
},
tooltips: {
controls: true,
},
/* controls: [
'play-large',
'restart',
'rewind',
'play',
'fast-forward',
'progress',
'current-time',
'duration',
'mute',
'volume',
'captions',
'settings',
'pip',
'airplay',
'fullscreen',
], */
captions: {
active: true,
},
keys: {
google: 'AIzaSyDrNwtN3nLH_8rjCmu5Wq3ZCm4MNAVdc0c',
},
ads: {
enabled: true,
publisherId: '918848828995742',
},
});
// Setup type toggle // Expose for tinkering in the console
const buttons = document.querySelectorAll('[data-source]'); window.player = player;
const types = {
video: 'video',
audio: 'audio',
youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Toggle class on an element // Setup type toggle
function toggleClass(element, className, state) { const buttons = document.querySelectorAll('[data-source]');
if (element) { const types = {
element.classList[state ? 'add' : 'remove'](className); video: 'video',
} audio: 'audio',
} youtube: 'youtube',
vimeo: 'vimeo',
};
let currentType = window.location.hash.replace('#', '');
const historySupport = window.history && window.history.pushState;
// Set a new source // Toggle class on an element
function newSource(type, init) { function toggleClass(element, className, state) {
// Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video if (element) {
if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) { element.classList[state ? 'add' : 'remove'](className);
return; }
} }
switch (type) { // Set a new source
case types.video: function newSource(type, init) {
player.source = { // Bail if new type isn't known, it's the current type, or current type is empty (video is default) and new type is video
type: 'video', if (!(type in types) || (!init && type === currentType) || (!currentType.length && type === types.video)) {
title: 'View From A Blue Moon', return;
sources: [{ }
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.mp4',
type: 'video/mp4',
}],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
break; switch (type) {
case types.video:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4',
type: 'video/mp4',
size: 576,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-720p.mp4',
type: 'video/mp4',
size: 720,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1080p.mp4',
type: 'video/mp4',
size: 1080,
},
{
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-1440p.mp4',
type: 'video/mp4',
size: 1440,
},
],
poster: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.jpg',
tracks: [
{
kind: 'captions',
label: 'English',
srclang: 'en',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.en.vtt',
default: true,
},
{
kind: 'captions',
label: 'French',
srclang: 'fr',
src: 'https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-HD.fr.vtt',
},
],
};
case types.audio: break;
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
break; case types.audio:
player.source = {
type: 'audio',
title: 'Kishi Bashi &ndash; &ldquo;It All Began With A Burst&rdquo;',
sources: [
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.mp3',
type: 'audio/mp3',
},
{
src: 'https://cdn.plyr.io/static/demo/Kishi_Bashi_-_It_All_Began_With_a_Burst.ogg',
type: 'audio/ogg',
},
],
};
case types.youtube: break;
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
break; case types.youtube:
player.source = {
type: 'video',
title: 'View From A Blue Moon',
sources: [{
src: 'https://youtube.com/watch?v=bTqVqk7FSmY',
provider: 'youtube',
}],
};
case types.vimeo: break;
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
break; case types.vimeo:
player.source = {
type: 'video',
sources: [{
src: 'https://vimeo.com/76979871',
provider: 'vimeo',
}],
};
default: break;
break;
}
// Set the current type for next time default:
currentType = type; break;
}
// Remove active classes // Set the current type for next time
Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false)); currentType = type;
// Set active on parent // Remove active classes
toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true); Array.from(buttons).forEach(button => toggleClass(button.parentElement, 'active', false));
// Show cite // Set active on parent
Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => { toggleClass(document.querySelector(`[data-source="${type}"]`), 'active', true);
cite.setAttribute('hidden', '');
});
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
// Bind to each button // Show cite
Array.from(buttons).forEach(button => { Array.from(document.querySelectorAll('.plyr__cite')).forEach(cite => {
button.addEventListener('click', () => { cite.setAttribute('hidden', '');
const type = button.getAttribute('data-source'); });
document.querySelector(`.plyr__cite--${type}`).removeAttribute('hidden');
}
newSource(type); // Bind to each button
Array.from(buttons).forEach(button => {
button.addEventListener('click', () => {
const type = button.getAttribute('data-source');
newSource(type);
if (historySupport) {
window.history.pushState({ type }, '', `#${type}`);
}
});
});
// List for backwards/forwards
window.addEventListener('popstate', event => {
if (event.state && 'type' in event.state) {
newSource(event.state.type);
}
});
// On load
if (historySupport) { if (historySupport) {
window.history.pushState({ type }, '', `#${type}`); const video = !currentType.length;
// If there's no current type set, assume video
if (video) {
currentType = types.video;
}
// Replace current history state
if (currentType in types) {
window.history.replaceState(
{
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
} }
}); });
}); });
// List for backwards/forwards // Google analytics
window.addEventListener('popstate', event => { // For demo site (https://plyr.io) only
if (event.state && 'type' in event.state) { /* eslint-disable */
newSource(event.state.type); if (isLive) {
} (function(i, s, o, g, r, a, m) {
}); i.GoogleAnalyticsObject = r;
i[r] =
// On load i[r] ||
if (historySupport) { function() {
const video = !currentType.length; (i[r].q = i[r].q || []).push(arguments);
};
// If there's no current type set, assume video i[r].l = 1 * new Date();
if (video) { a = s.createElement(o);
currentType = types.video; m = s.getElementsByTagName(o)[0];
} a.async = 1;
a.src = g;
// Replace current history state m.parentNode.insertBefore(a, m);
if (currentType in types) { })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
window.history.replaceState( window.ga('create', 'UA-40881672-11', 'auto');
{ window.ga('send', 'pageview');
type: currentType,
},
'',
video ? '' : `#${currentType}`,
);
}
// If it's not video, load the source
if (currentType !== types.video) {
newSource(currentType, true);
}
} }
}); /* eslint-enable */
})();
// Google analytics
// For demo site (https://plyr.io) only
/* eslint-disable */
if (window.location.host === 'plyr.io') {
(function(i, s, o, g, r, a, m) {
i.GoogleAnalyticsObject = r;
i[r] =
i[r] ||
function() {
(i[r].q = i[r].q || []).push(arguments);
};
i[r].l = 1 * new Date();
a = s.createElement(o);
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
window.ga('create', 'UA-40881672-11', 'auto');
window.ga('send', 'pageview');
}
/* eslint-enable */

2
dist/plyr.css vendored

File diff suppressed because one or more lines are too long

7829
dist/plyr.js vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.js.map vendored

File diff suppressed because one or more lines are too long

2
dist/plyr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/plyr.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

12866
dist/plyr.polyfilled.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/plyr.polyfilled.js.map vendored Normal file

File diff suppressed because one or more lines are too long

2
dist/plyr.polyfilled.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/plyr.polyfilled.min.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -9,6 +9,7 @@ const path = require('path');
const gulp = require('gulp'); const gulp = require('gulp');
const gutil = require('gulp-util'); const gutil = require('gulp-util');
const concat = require('gulp-concat'); const concat = require('gulp-concat');
const filter = require('gulp-filter');
const sass = require('gulp-sass'); const sass = require('gulp-sass');
const cleancss = require('gulp-clean-css'); const cleancss = require('gulp-clean-css');
const run = require('run-sequence'); const run = require('run-sequence');
@ -24,8 +25,7 @@ const size = require('gulp-size');
const rollup = require('gulp-better-rollup'); const rollup = require('gulp-better-rollup');
const babel = require('rollup-plugin-babel'); const babel = require('rollup-plugin-babel');
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
const uglify = require('rollup-plugin-uglify'); const uglify = require('gulp-uglify-es').default;
const { minify } = require('uglify-es');
const commonjs = require('rollup-plugin-commonjs'); const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve'); const resolve = require('rollup-plugin-node-resolve');
@ -40,6 +40,8 @@ try {
// Do nothing // Do nothing
} }
const minSuffix = '.min';
// Paths // Paths
const root = __dirname; const root = __dirname;
const paths = { const paths = {
@ -68,8 +70,11 @@ const paths = {
root: path.join(root, 'demo/'), root: path.join(root, 'demo/'),
}, },
upload: [ upload: [
path.join(root, 'dist/**'), path.join(root, `dist/*${minSuffix}.*`),
path.join(root, 'demo/dist/**'), path.join(root, 'dist/*.css'),
path.join(root, 'dist/*.svg'),
path.join(root, `demo/dist/*${minSuffix}.*`),
path.join(root, 'demo/dist/*.css'),
], ],
}; };
@ -122,6 +127,7 @@ const build = {
Object.keys(files).forEach(key => { Object.keys(files).forEach(key => {
const name = `js:${key}`; const name = `js:${key}`;
tasks.js.push(name); tasks.js.push(name);
const { output } = paths[bundle];
gulp.task(name, () => gulp.task(name, () =>
gulp gulp
@ -135,15 +141,19 @@ const build = {
resolve(), resolve(),
commonjs(), commonjs(),
babel(babelrc), babel(babelrc),
uglify({}, minify),
], ],
}, },
options, options,
), ),
) )
.pipe(size(sizeOptions))
.pipe(sourcemaps.write('')) .pipe(sourcemaps.write(''))
.pipe(gulp.dest(paths[bundle].output)), .pipe(gulp.dest(output))
.pipe(filter('**/*.js'))
.pipe(uglify())
.pipe(size(sizeOptions))
.pipe(rename({ suffix: minSuffix }))
.pipe(sourcemaps.write(''))
.pipe(gulp.dest(output)),
); );
}); });
}, },
@ -266,6 +276,23 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
const semver = new RegExp(`v${regex}`, 'gi'); const semver = new RegExp(`v${regex}`, 'gi');
const localPath = new RegExp('(../)?dist', 'gi'); const localPath = new RegExp('(../)?dist', 'gi');
const versionPath = `https://${aws.cdn.domain}/${version}`; const versionPath = `https://${aws.cdn.domain}/${version}`;
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
gulp.task('version', () => {
console.log(`Updating versions to '${version}'...`);
// Replace versioned URLs in source
const files = [
'plyr.js',
'plyr.polyfilled.js',
'defaults.js',
];
gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
});
// Publish version to CDN bucket // Publish version to CDN bucket
gulp.task('cdn', () => { gulp.task('cdn', () => {
@ -277,22 +304,26 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
console.log(`Uploading '${version}' to ${aws.cdn.domain}...`); console.log(`Uploading '${version}' to ${aws.cdn.domain}...`);
// Upload to CDN // Upload to CDN
return gulp return (
.src(paths.upload) gulp
.pipe( .src(paths.upload)
size({ .pipe(
showFiles: true, rename(p => {
gzip: true, p.basename = p.basename.replace(minSuffix, ''); // eslint-disable-line
}), p.dirname = p.dirname.replace('.', version); // eslint-disable-line
) }),
.pipe( )
rename(p => { // Remove min suffix from source map URL
// eslint-disable-next-line .pipe(replace(/sourceMappingURL=([\w-?.]+)/, (match, p1) => `sourceMappingURL=${p1.replace(minSuffix, '')}`))
p.dirname = p.dirname.replace('.', version); .pipe(
}), size({
) showFiles: true,
.pipe(replace(localPath, versionPath)) gzip: true,
.pipe(s3(aws.cdn, options.cdn)); }),
)
.pipe(replace(localPath, versionPath))
.pipe(s3(aws.cdn, options.cdn))
);
}); });
// Publish to demo bucket // Publish to demo bucket
@ -304,25 +335,12 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`); console.log(`Uploading '${version}' demo to ${aws.demo.domain}...`);
const cdnpath = new RegExp(`${aws.cdn.domain}/${regex}/`, 'gi');
// Replace versioned files in readme.md // Replace versioned files in readme.md
gulp gulp
.src([`${root}/readme.md`]) .src([`${root}/readme.md`])
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`)) .pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(root)); .pipe(gulp.dest(root));
// Replace versioned URLs in source
const files = [
'plyr.js',
'defaults.js',
];
gulp
.src(files.map(file => path.join(root, `src/js/${file}`)))
.pipe(replace(semver, `v${version}`))
.pipe(replace(cdnpath, `${aws.cdn.domain}/${version}/`))
.pipe(gulp.dest(path.join(root, 'src/js/')));
// Replace local file paths with remote paths in demo HTML // Replace local file paths with remote paths in demo HTML
// e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js" // e.g. "../dist/plyr.js" to "https://cdn.plyr.io/x.x.x/plyr.js"
const index = `${paths.demo.root}index.html`; const index = `${paths.demo.root}index.html`;
@ -389,6 +407,6 @@ if (Object.keys(aws).includes('cdn') && Object.keys(aws).includes('demo')) {
// Do everything // Do everything
gulp.task('publish', () => { gulp.task('publish', () => {
run(tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo'); run('version', tasks.clean, tasks.js, tasks.sass, tasks.sprite, 'cdn', 'demo');
}); });
} }

View File

@ -1,50 +1,53 @@
{ {
"name": "plyr", "name": "plyr",
"version": "3.0.0-beta.13", "version": "3.1.0-beta.2",
"description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player", "description": "A simple, accessible and customizable HTML5, YouTube and Vimeo media player",
"homepage": "https://plyr.io", "homepage": "https://plyr.io",
"main": "./dist/plyr.js", "main": "./dist/plyr.js",
"browser": "./dist/plyr.min.js",
"sass": "./src/sass/plyr.scss", "sass": "./src/sass/plyr.scss",
"style": "./dist/plyr.css", "style": "./dist/plyr.css",
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-eslint": "^8.2.1", "babel-eslint": "^8.2.2",
"babel-plugin-external-helpers": "^6.22.0", "babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"del": "^3.0.0", "del": "^3.0.0",
"eslint": "^4.17.0", "eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0", "eslint-config-airbnb-base": "^12.1.0",
"eslint-config-prettier": "^2.9.0", "eslint-config-prettier": "^2.9.0",
"eslint-plugin-import": "^2.8.0", "eslint-plugin-import": "^2.10.0",
"git-branch": "^1.0.0", "git-branch": "^2.0.1",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-autoprefixer": "^4.1.0", "gulp-autoprefixer": "^5.0.0",
"gulp-better-rollup": "^3.0.0", "gulp-better-rollup": "^3.1.0",
"gulp-clean-css": "^3.9.2", "gulp-clean-css": "^3.9.3",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-open": "^2.1.0", "gulp-filter": "^5.1.0",
"gulp-open": "^3.0.1",
"gulp-rename": "^1.2.2", "gulp-rename": "^1.2.2",
"gulp-replace": "^0.6.1", "gulp-replace": "^0.6.1",
"gulp-s3": "^0.11.0", "gulp-s3": "^0.11.0",
"gulp-sass": "^3.1.0", "gulp-sass": "^3.2.1",
"gulp-size": "^3.0.0", "gulp-size": "^3.0.0",
"gulp-sourcemaps": "^2.6.4", "gulp-sourcemaps": "^2.6.4",
"gulp-svgmin": "^1.2.4", "gulp-svgmin": "^1.2.4",
"gulp-svgstore": "^6.1.1", "gulp-svgstore": "^6.1.1",
"gulp-uglify-es": "^1.0.1",
"gulp-util": "^3.0.8", "gulp-util": "^3.0.8",
"prettier-eslint": "^8.8.1",
"prettier-stylelint": "^0.4.2",
"rollup-plugin-babel": "^3.0.3", "rollup-plugin-babel": "^3.0.3",
"rollup-plugin-commonjs": "^8.3.0", "rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-node-resolve": "^3.0.2", "rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-uglify": "^3.0.0",
"run-sequence": "^2.2.1", "run-sequence": "^2.2.1",
"stylelint": "^8.4.0", "stylelint": "^9.2.0",
"stylelint-config-prettier": "^2.0.0", "stylelint-config-prettier": "^3.0.4",
"stylelint-config-sass-guidelines": "^4.1.0", "stylelint-config-recommended": "^2.1.0",
"stylelint-config-standard": "^18.0.0", "stylelint-config-sass-guidelines": "^5.0.0",
"stylelint-order": "^0.8.0", "stylelint-order": "^0.8.1",
"stylelint-scss": "^2.3.0", "stylelint-scss": "^3.0.0",
"stylelint-selector-bem-pattern": "^2.0.0", "stylelint-selector-bem-pattern": "^2.0.0"
"uglify-es": "^3.3.10"
}, },
"keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"], "keywords": ["HTML5 Video", "HTML5 Audio", "Media Player", "DASH", "Shaka", "WordPress", "HLS"],
"repository": { "repository": {
@ -62,5 +65,10 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Sam Potts <sam@potts.es>", "author": "Sam Potts <sam@potts.es>",
"dependencies": {} "dependencies": {
"babel-polyfill": "^6.26.0",
"custom-event-polyfill": "^0.3.0",
"loadjs": "^3.5.4",
"raven-js": "^3.24.0"
}
} }

165
readme.md
View File

@ -1,33 +1,33 @@
---
Beware: This version is currently in beta and not production-ready
---
# Plyr # Plyr
A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers. A simple, lightweight, accessible and customizable HTML5, YouTube and Vimeo media player that supports [_modern_](#browser-support) browsers.
[Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-slack) [Checkout the demo](https://plyr.io) - [Donate to support Plyr](#donate) - [Chat on Slack](https://bit.ly/plyr-chat)
[![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png)](https://plyr.io) [![Image of Plyr](https://cdn.plyr.io/static/demo/screenshot.png?v=3)](https://plyr.io)
## Features ## Features
* **Accessible** - full support for VTT captions and screen readers * **Accessible** - full support for VTT captions and screen readers
* **Lightweight** - just 18KB minified and gzipped * **[Customisable](#html)** - make the player look how you want with the markup you want
* **[Customisable](#html)** - make the player look how you want with the markup you want * **Good HTML** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no
* **Semantic** - uses the _right_ elements. `<input type="range">` for volume and `<progress>` for progress and well, `<button>`s for buttons. There's no `<span>` or `<a href="#">` button hacks
`<span>` or `<a href="#">` button hacks * **Responsive** - works with any screen size
* **Responsive** - works with any screen size * **HTML Video & Audio** - support for both formats
* **HTML Video & Audio** - support for both formats * **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback
* **[Embedded Video](#embeds)** - support for YouTube and Vimeo video playback * **[Monetization](#ads)** - make money from your videos
* **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback * **[Streaming](#streaming)** - support for hls.js, Shaka and dash.js streaming playback
* **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API * **[API](#api)** - toggle playback, volume, seeking, and more through a standardized API
* **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats * **[Events](#events)** - no messing around with Vimeo and YouTube APIs, all events are standardized across formats
* **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes * **[Fullscreen](#fullscreen)** - supports native fullscreen with fallback to "full window" modes
* **[Shortcuts](#shortcuts)** - supports keyboard shortcuts * **[Shortcuts](#shortcuts)** - supports keyboard shortcuts
* **i18n support** - support for internationalization of controls * **Picture-in-Picture** - supports Safari's picture-in-picture mode
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required * **Playsinline** - supports the `playsinline` attribute
* **SASS** - to include in your build processes * **Speed controls** - adjust speed on the fly
* **Multiple captions** - support for multiple caption tracks
* **i18n support** - support for internationalization of controls
* **No dependencies** - written in "vanilla" ES6 JavaScript, no jQuery required
* **SASS** - to include in your build processes
Oh and yes, it works with Bootstrap. Oh and yes, it works with Bootstrap.
@ -53,8 +53,7 @@ Here's a quick run through on getting up and running. There's also a [demo on Co
### HTML ### HTML
Plyr extends upon the standard HTML5 markup so that's all you need for those types. More info on advanced HTML markup can be found under Plyr extends upon the standard [HTML5 media element](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement) markup so that's all you need for those types.
[initialising](#initialising).
#### HTML5 Video #### HTML5 Video
@ -117,18 +116,19 @@ Or the `<div>` non progressively enhanced method:
### JavaScript ### JavaScript
Include the `plyr.js` script before the closing `</body>` tag and then call `plyr.setup()`. More info on `setup()` can be found under Include the `plyr.js` script before the closing `</body>` tag and then in your JS create a new instance of Plyr as below.
[initialising](#initialising).
```html ```html
<script src="path/to/plyr.js"></script> <script src="path/to/plyr.js"></script>
<script>const player = new Plyr('#player');</script> <script>const player = new Plyr('#player');</script>
``` ```
See [initialising](#initialising) for more information on advanced setups.
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the JavaScript, you can use the following:
```html ```html
<script src="https://cdn.plyr.io/3.0.0-beta.13/plyr.js"></script> <script src="https://cdn.plyr.io/3.1.0-beta.2/plyr.js"></script>
``` ```
_Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility _Note_: Be sure to read the [polyfills](#polyfills) section below about browser compatibility
@ -144,13 +144,23 @@ Include the `plyr.css` stylsheet into your `<head>`
If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following: If you want to use our CDN (provided by [Fastly](https://www.fastly.com/)) for the default CSS, you can use the following:
```html ```html
<link rel="stylesheet" href="https://cdn.plyr.io/3.0.0-beta.13/plyr.css"> <link rel="stylesheet" href="https://cdn.plyr.io/3.1.0-beta.2/plyr.css">
``` ```
### SVG Sprite ### SVG Sprite
The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For The SVG sprite is loaded automatically from our CDN (provided by [Fastly](https://www.fastly.com/)). To change this, see the [options](#options) below. For
reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.0.0-beta.13/plyr.svg`. reference, the CDN hosted SVG sprite can be found at `https://cdn.plyr.io/3.1.0-beta.2/plyr.svg`.
## Ads
Plyr has partnered up with [vi.ai](http://vi.ai/publisher-video-monetization/?aid=plyrio) to offer monetization options for your videos. Getting setup is easy:
* [Sign up for a vi.ai account](http://vi.ai/publisher-video-monetization/?aid=plyrio)
* Grab your publisher ID from the code snippet
* Enable ads in the [config options](#options) and enter your publisher ID
Any questions regarding the ads can be sent straight to vi.ai and any issues with rendering raised through GitHub issues.
## Advanced ## Advanced
@ -195,11 +205,12 @@ WebVTT captions are supported. To add a caption track, check the HTML example ab
You can specify a range of arguments for the constructor to use: You can specify a range of arguments for the constructor to use:
* A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector) * A CSS string selector that's compatible with [`querySelector`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector)
* A [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) * A [`HTMLElement`](https://developer.mozilla.org/en/docs/Web/API/HTMLElement)
* A [NodeList](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) or Array of [HTMLElement](https://developer.mozilla.org/en/docs/Web/API/HTMLElement) - * A [`NodeList]`(https://developer.mozilla.org/en-US/docs/Web/API/NodeList)
the first element will be used * A [jQuery](https://jquery.com) object
* A [jQuery](https://jquery.com) object - if multiple are passed, the first element will be used
_Note_: If a `NodeList`, `Array`, or jQuery object are passed, the first element will be used for setup.
Here's some examples Here's some examples
@ -223,7 +234,13 @@ const player = new Plyr(document.querySelectorAll('.js-player'));
The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds The NodeList, HTMLElement or string selector can be the target `<video>`, `<audio>`, or `<div>` wrapper for embeds
The second argument for the constructor is the [#options](options) object: ##### Setting up multiple players
```javascript
const players = Array.from(document.querySelectorAll('.js-player')).map(player => new Plyr(player));
```
The second argument for the constructor is the [options](#options) object:
```javascript ```javascript
const player = new Plyr('#player', { const player = new Plyr('#player', {
@ -271,14 +288,15 @@ Note the single quotes encapsulating the JSON and double quotes on the object ke
| `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. | | `toggleInvert` | Boolean | `true` | Allow users to click to toggle the above. |
| `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. | | `listeners` | Object | `null` | Allows binding of event listeners to the controls before the default handlers. See the `defaults.js` for available listeners. If your handler prevents default on the event (`event.preventDefault()`), the default handler will not fire. |
| `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). | | `captions` | Object | `{ active: false, language: window.navigator.language.split('-')[0] }` | `active`: Toggles if captions should be active by default. `language`: Sets the default language to load (if available). |
| `fullscreen` | Object | `{ enabled: true, fallback: true }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. | | `fullscreen` | Object | `{ enabled: true, fallback: true, iosNative: false }` | `enabled`: Toggles whether fullscreen should be enabled. `fallback`: Allow fallback to a full-window solution. `iosNative`: whether to use native iOS fullscreen when entering fullscreen (no custom controls) |
| `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. | | `ratio` | String | `16:9` | The aspect ratio you want to use for embedded players. |
| `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. | | `storage` | Object | `{ enabled: true, key: 'plyr' }` | `enabled`: Allow use of local storage to store user settings. `key`: The key name to use. |
| `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. | | `speed` | Object | `{ selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2] }` | `selected`: The default speed for playback. `options`: Options to display in the menu. Most browsers will refuse to play slower than 0.5. |
| `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. | | `quality` | Object | `{ default: 'default', options: ['hd2160', 'hd1440', 'hd1080', 'hd720', 'large', 'medium', 'small', 'tiny', 'default'] }` | Currently only supported by YouTube. `default` is the default quality level, determined by YouTube. `options` are the options to display. |
| `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. | | `loop` | Object | `{ active: false }` | `active`: Whether to loop the current video. If the `loop` attribute is present on a `<video>` or `<audio>` element, this will be automatically set to true This is an object to support future functionality. |
| `ads` | Object | `{ enabled: false, publisherId: '' }` | `enabled`: Whether to enable vi.ai ads. `publisherId`: Your unique vi.ai publisher ID. |
1. Vimeo only 1. Vimeo only
## API ## API
@ -333,7 +351,7 @@ player.fullscreen.enter(); // Enter fullscreen
| `supports(type)` | String | Check support for a mime type. | | `supports(type)` | String | Check support for a mime type. |
| `destroy()` | - | Destroy the instance and garbage collect any elements. | | `destroy()` | - | Destroy the instance and garbage collect any elements. |
1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing. 1. For HTML5 players, `play()` will return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) in _some_ browsers - WebKit and Mozilla [according to MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/play) at time of writing.
### Getters and Setters ### Getters and Setters
@ -359,6 +377,7 @@ player.fullscreen.active; // false;
| `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. | | `paused` | ✓ | - | Returns a boolean indicating if the current player is paused. |
| `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. | | `playing` | ✓ | - | Returns a boolean indicating if the current player is playing. |
| `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. | | `ended` | ✓ | - | Returns a boolean indicating if the current player has finished playback. |
| `buffered` | ✓ | - | Returns a float between 0 and 1 indicating how much of the media is buffered |
| `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. | | `currentTime` | ✓ | ✓ | Gets or sets the currentTime for the player. The setter accepts a float in seconds. |
| `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. | | `seeking` | ✓ | - | Returns a boolean indicating if the current player is seeking. |
| `duration` | ✓ | - | Returns the duration for the current media. | | `duration` | ✓ | - | Returns the duration for the current media. |
@ -376,8 +395,8 @@ player.fullscreen.active; // false;
| `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. | | `fullscreen.enabled` | ✓ | - | Returns a boolean indicating if the current player has fullscreen enabled. |
| `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. | | `pip` | ✓ | ✓ | Gets or sets the picture-in-picture state of the player. The setter accepts a boolean. This currently only supported on Safari 10+ on MacOS Sierra+ and iOS 10+. |
1. YouTube only. HTML5 will follow. 1. YouTube only. HTML5 will follow.
2. HTML5 only 2. HTML5 only
#### The `.source` setter #### The `.source` setter
@ -477,7 +496,7 @@ _Note:_ `src` property for YouTube and Vimeo can either be the video ID or the w
| `poster`&sup1; | String | The URL for the poster image (HTML5 video only). | | `poster`&sup1; | String | The URL for the poster image (HTML5 video only). |
| `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. | | `tracks`&sup1; | String | An array of track objects. Each element in the array is mapped directly to a track element and any keys mapped directly to HTML attributes so as in the example above, it will render as `<track kind="captions" label="English" srclang="en" src="https://cdn.selz.com/plyr/1.0/example_captions_en.vtt" default>` and similar for the French version. Booleans are converted to HTML5 value-less attributes. |
1. HTML5 only 1. HTML5 only
## Events ## Events
@ -547,8 +566,8 @@ YouTube and Vimeo are currently supported and function much like a HTML5 video.
to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the to access the API's directly. You can do so via the `embed` property of your player object - e.g. `player.embed`. You can then use the relevant methods from the
third party APIs. More info on the respective API's here: third party APIs. More info on the respective API's here:
* [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference) * [YouTube iframe API Reference](https://developers.google.com/youtube/iframe_api_reference)
* [Vimeo player.js Reference](https://github.com/vimeo/player.js) * [Vimeo player.js Reference](https://github.com/vimeo/player.js)
_Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible. _Note_: Not all API methods may work 100%. Your mileage may vary. It's better to use the Plyr API where possible.
@ -576,9 +595,9 @@ document then the shortcuts will work when any element has focus, apart from an
Because Plyr is an extension of the standard HTML5 video and audio elements, third party streaming plugins can be used with Plyr. Massive thanks to Matias Because Plyr is an extension of the standard HTML5 video and audio elements, third party streaming plugins can be used with Plyr. Massive thanks to Matias
Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's a few examples: Russitto ([@russitto](https://github.com/russitto)) for working on this. Here's a few examples:
* Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB) * Using [hls.js](https://github.com/dailymotion/hls.js) - [Demo](http://codepen.io/sampotts/pen/JKEMqB)
* Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR) * Using [Shaka](https://github.com/google/shaka-player) - [Demo](http://codepen.io/sampotts/pen/zBNpVR)
* Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN) * Using [dash.js](https://github.com/Dash-Industry-Forum/dash.js) - [Demo](http://codepen.io/sampotts/pen/BzpJXN)
## Fullscreen ## Fullscreen
@ -599,8 +618,8 @@ Plyr supports the last 2 versions of most _modern_ browsers.
| IE11 | ✓ | | IE11 | ✓ |
| IE10 | ✓&sup2; | | IE10 | ✓&sup2; |
1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide. 1. Mobile Safari on the iPhone forces the native player for `<video>` unless the `playsinline` attribute is present. Volume controls are also disabled as they are handled device wide.
2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options)) 2. Native player used (no support for `<progress>` or `<input type="range">`) but the API is supported. No native fullscreen support, fallback can be used (see [options](#options))
### Polyfills ### Polyfills
@ -616,9 +635,9 @@ const supported = Plyr.supported('video', 'html5', true);
The arguments are: The arguments are:
* Media type (`audio` or `video`) * Media type (`audio` or `video`)
* Provider (`html5`, `youtube` or `vimeo`) * Provider (`html5`, `youtube` or `vimeo`)
* Whether the player has the `playsinline` attribute (only applicable to iOS 10+) * Whether the player has the `playsinline` attribute (only applicable to iOS 10+)
### Disable support programatically ### Disable support programatically
@ -654,28 +673,28 @@ Plyr costs money to run, not only my time - I donate that for free but domains,
## Mentions ## Mentions
* [ProductHunt](https://www.producthunt.com/tech/plyr) * [ProductHunt](https://www.producthunt.com/tech/plyr)
* [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/) * [The Changelog](http://thechangelog.com/plyr-simple-html5-media-player-custom-controls-webvtt-captions/)
* [HTML5 Weekly #177](http://html5weekly.com/issues/177) * [HTML5 Weekly #177](http://html5weekly.com/issues/177)
* [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f) * [Responsive Design #149](http://us4.campaign-archive2.com/?u=559bc631fe5294fc66f5f7f89&id=451a61490f)
* [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/) * [Web Design Weekly #174](https://web-design-weekly.com/2015/02/24/web-design-weekly-174/)
* [Hacker News](https://news.ycombinator.com/item?id=9136774) * [Hacker News](https://news.ycombinator.com/item?id=9136774)
* [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04) * [Web Platform Daily](http://webplatformdaily.org/releases/2015-03-04)
* [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player) * [LayerVault Designer News](https://news.layervault.com/stories/45394-plyr--a-simple-html5-media-player)
* [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images) * [The Treehouse Show #131](https://teamtreehouse.com/library/episode-131-origami-react-responsive-hero-images)
* [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html) * [noupe.com](http://www.noupe.com/design/html5-plyr-is-a-responsive-and-accessible-video-player-94389.html)
## Used by ## Used by
* [Selz.com](https://selz.com) * [Selz.com](https://selz.com)
* [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html) * [Peugeot.fr](http://www.peugeot.fr/marque-et-technologie/technologies/peugeot-i-cockpit.html)
* [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html) * [Peugeot.de](http://www.peugeot.de/modelle/modellberater/208-3-turer/fotos-videos.html)
* [TomTom.com](http://prioritydriving.tomtom.com/) * [TomTom.com](http://prioritydriving.tomtom.com/)
* [DIGBMX](http://digbmx.com/) * [DIGBMX](http://digbmx.com/)
* [Grime Archive](https://grimearchive.com/) * [Grime Archive](https://grimearchive.com/)
* [koel - A personal music streaming server that works.](http://koel.phanan.net/) * [koel - A personal music streaming server that works.](http://koel.phanan.net/)
* [Oscar Radio](http://oscar-radio.xyz/) * [Oscar Radio](http://oscar-radio.xyz/)
* [Sparkk TV](https://www.sparkktv.com/) * [Sparkk TV](https://www.sparkktv.com/)
Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-) Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the above list. It'd be awesome to see how you're using Plyr :-)
@ -683,12 +702,12 @@ Let me know on [Twitter](https://twitter.com/sam_potts) I can add you to the abo
Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from: Credit to the PayPal HTML5 Video player from which Plyr's caption functionality was originally ported from:
* [PayPal's Accessible HTML5 Video Player](https://github.com/paypal/accessible-html5-video-player) * [PayPal's Accessible HTML5 Video Player](https://github.com/paypal/accessible-html5-video-player)
* [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw) * [An awesome guide for Plyr in Japanese!](http://syncer.jp/how-to-use-plyr-io) by [@arayutw](https://twitter.com/arayutw)
## Thanks ## Thanks
[![Fastly](https://www.fastly.com/sites/all/themes/custom/fastly2016/logo.png)](https://www.fastly.com/) [![Fastly](https://cdn.plyr.io/static/demo/fastly-logo.png)](https://www.fastly.com/)
Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services. Massive thanks to [Fastly](https://www.fastly.com/) for providing the CDN services.

View File

@ -1,5 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr Captions // Plyr Captions
// TODO: Create as class
// ========================================================================== // ==========================================================================
import support from './support'; import support from './support';
@ -45,7 +46,6 @@ const captions = {
return; return;
} }
// Inject the container // Inject the container
if (!utils.is.element(this.elements.captions)) { if (!utils.is.element(this.elements.captions)) {
this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions));
@ -56,11 +56,42 @@ const captions = {
// Set the class hook // Set the class hook
utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this))); utils.toggleClass(this.elements.container, this.config.classNames.captions.enabled, !utils.is.empty(captions.getTracks.call(this)));
// Get tracks
const tracks = captions.getTracks.call(this);
// If no caption file exists, hide container for caption text // If no caption file exists, hide container for caption text
if (utils.is.empty(captions.getTracks.call(this))) { if (utils.is.empty(tracks)) {
return; return;
} }
// Get browser info
const browser = utils.getBrowser();
// 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 href = utils.parseUrl(src);
if (href.hostname !== window.location.href.hostname && [
'http:',
'https:',
].includes(href.protocol)) {
utils
.fetch(src, 'blob')
.then(blob => {
track.setAttribute('src', window.URL.createObjectURL(blob));
})
.catch(() => {
utils.removeElement(track);
});
}
});
}
// Set language // Set language
captions.setLanguage.call(this); captions.setLanguage.call(this);

208
src/js/controls.js vendored
View File

@ -5,7 +5,9 @@
import support from './support'; import support from './support';
import utils from './utils'; import utils from './utils';
import ui from './ui'; import ui from './ui';
import i18n from './i18n';
import captions from './captions'; import captions from './captions';
import html5 from './html5';
// Sniff out the browser // Sniff out the browser
const browser = utils.getBrowser(); const browser = utils.getBrowser();
@ -50,7 +52,7 @@ const controls = {
icon, icon,
utils.extend(attributes, { utils.extend(attributes, {
role: 'presentation', role: 'presentation',
}) }),
); );
// Create the <use> to reference sprite // Create the <use> to reference sprite
@ -74,7 +76,7 @@ const controls = {
// Create hidden text label // Create hidden text label
createLabel(type, attr) { createLabel(type, attr) {
let text = this.config.i18n[type]; let text = i18n.get(type, this.config);
const attributes = Object.assign({}, attr); const attributes = Object.assign({}, attr);
switch (type) { switch (type) {
@ -115,8 +117,8 @@ const controls = {
{ {
class: this.config.classNames.menu.badge, class: this.config.classNames.menu.badge,
}, },
text text,
) ),
); );
return badge; return badge;
@ -126,7 +128,7 @@ const controls = {
createButton(buttonType, attr) { createButton(buttonType, attr) {
const button = utils.createElement('button'); const button = utils.createElement('button');
const attributes = Object.assign({}, attr); const attributes = Object.assign({}, attr);
let type = buttonType; let type = utils.toCamelCase(buttonType);
let toggle = false; let toggle = false;
let label; let label;
@ -147,7 +149,7 @@ const controls = {
} }
// Large play button // Large play button
switch (type) { switch (buttonType) {
case 'play': case 'play':
toggle = true; toggle = true;
label = 'play'; label = 'play';
@ -189,7 +191,7 @@ const controls = {
default: default:
label = type; label = type;
icon = type; icon = buttonType;
} }
// Setup toggle icon and labels // Setup toggle icon and labels
@ -204,7 +206,7 @@ const controls = {
// Add aria attributes // Add aria attributes
attributes['aria-pressed'] = false; attributes['aria-pressed'] = false;
attributes['aria-label'] = this.config.i18n[label]; attributes['aria-label'] = i18n.get(label, this.config);
} else { } else {
button.appendChild(controls.createIcon.call(this, icon)); button.appendChild(controls.createIcon.call(this, icon));
button.appendChild(controls.createLabel.call(this, label)); button.appendChild(controls.createLabel.call(this, label));
@ -238,7 +240,7 @@ const controls = {
for: attributes.id, for: attributes.id,
class: this.config.classNames.hidden, class: this.config.classNames.hidden,
}, },
this.config.i18n[type] i18n.get(type, this.config),
); );
// Seek input // Seek input
@ -254,8 +256,8 @@ const controls = {
value: 0, value: 0,
autocomplete: 'off', autocomplete: 'off',
}, },
attributes attributes,
) ),
); );
this.elements.inputs[type] = input; this.elements.inputs[type] = input;
@ -280,8 +282,8 @@ const controls = {
max: 100, max: 100,
value: 0, value: 0,
}, },
attributes attributes,
) ),
); );
// Create the label inside // Create the label inside
@ -291,11 +293,11 @@ const controls = {
let suffix = ''; let suffix = '';
switch (type) { switch (type) {
case 'played': case 'played':
suffix = this.config.i18n.played; suffix = i18n.get('played', this.config);
break; break;
case 'buffer': case 'buffer':
suffix = this.config.i18n.buffered; suffix = i18n.get('buffered', this.config);
break; break;
default: default:
@ -322,8 +324,8 @@ const controls = {
{ {
class: this.config.classNames.hidden, class: this.config.classNames.hidden,
}, },
this.config.i18n[type] i18n.get(type, this.config),
) ),
); );
container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00')); container.appendChild(utils.createElement('span', utils.getAttributesFromSelector(this.config.selectors.display[type]), '00:00'));
@ -349,7 +351,7 @@ const controls = {
value, value,
checked, checked,
class: 'plyr__sr-only', class: 'plyr__sr-only',
}) }),
); );
const faux = utils.createElement('span', { 'aria-hidden': true }); const faux = utils.createElement('span', { 'aria-hidden': true });
@ -383,6 +385,16 @@ const controls = {
const clientRect = this.elements.inputs.seek.getBoundingClientRect(); const clientRect = this.elements.inputs.seek.getBoundingClientRect();
const visible = `${this.config.classNames.tooltip}--visible`; const visible = `${this.config.classNames.tooltip}--visible`;
const toggle = toggle => {
utils.toggleClass(this.elements.display.seekTooltip, visible, toggle);
};
// Hide on touch
if (this.touch) {
toggle(false);
return;
}
// Determine percentage, if already visible // Determine percentage, if already visible
if (utils.is.event(event)) { if (utils.is.event(event)) {
percent = 100 / clientRect.width * (event.pageX - clientRect.left); percent = 100 / clientRect.width * (event.pageX - clientRect.left);
@ -411,7 +423,7 @@ const controls = {
'mouseenter', 'mouseenter',
'mouseleave', 'mouseleave',
].includes(event.type)) { ].includes(event.type)) {
utils.toggleClass(this.elements.display.seekTooltip, visible, event.type === 'mouseenter'); toggle(event.type === 'mouseenter');
} }
}, },
@ -424,21 +436,24 @@ const controls = {
utils.toggleHidden(pane, !toggle); utils.toggleHidden(pane, !toggle);
}, },
// Set the YouTube quality menu // Set the quality menu
// TODO: Support for HTML5 // TODO: Vimeo support
setQualityMenu(options) { setQualityMenu(options) {
// Menu required
if (!utils.is.element(this.elements.settings.panes.quality)) {
return;
}
const type = 'quality'; const type = 'quality';
const list = this.elements.settings.panes.quality.querySelector('ul'); const list = this.elements.settings.panes.quality.querySelector('ul');
// Set options if passed and filter based on config // Set options if passed and filter based on config
if (utils.is.array(options)) { if (utils.is.array(options)) {
this.options.quality = options.filter(quality => this.config.quality.options.includes(quality)); this.options.quality = options.filter(quality => this.config.quality.options.includes(quality));
} else {
this.options.quality = this.config.quality.options;
} }
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !utils.is.empty(this.options.quality) && this.isYouTube; const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleTab.call(this, type, toggle);
// If we're hiding, nothing more to do // If we're hiding, nothing more to do
@ -454,20 +469,18 @@ const controls = {
let label = ''; let label = '';
switch (quality) { switch (quality) {
case 'hd2160': case 2160:
label = '4K'; label = '4K';
break; break;
case 'hd1440': case 1440:
label = 'WQHD'; case 1080:
break; case 720:
case 'hd1080':
label = 'HD'; label = 'HD';
break; break;
case 'hd720': case 576:
label = 'HD'; label = 'SD';
break; break;
default: default:
@ -481,9 +494,14 @@ const controls = {
return controls.createBadge.call(this, label); return controls.createBadge.call(this, label);
}; };
this.options.quality.forEach(quality => // Sort options by the config and then render options
controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality)) this.options.quality.sort((a, b) => {
); const sorting = this.config.quality.options;
return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1;
}).forEach(quality => {
const label = controls.getLabel.call(this, 'quality', quality);
controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality));
});
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
@ -496,28 +514,10 @@ const controls = {
return value === 1 ? 'Normal' : `${value}&times;`; return value === 1 ? 'Normal' : `${value}&times;`;
case 'quality': case 'quality':
switch (value) { if (utils.is.number(value)) {
case 'hd2160': return `${value}p`;
return '2160P';
case 'hd1440':
return '1440P';
case 'hd1080':
return '1080P';
case 'hd720':
return '720P';
case 'large':
return '480P';
case 'medium':
return '360P';
case 'small':
return '240P';
case 'tiny':
return 'Tiny';
case 'default':
return 'Auto';
default:
return value;
} }
return utils.toTitleCase(value);
case 'captions': case 'captions':
return controls.getLanguage.call(this); return controls.getLanguage.call(this);
@ -528,18 +528,18 @@ const controls = {
}, },
// Update the selected setting // Update the selected setting
updateSetting(setting, container) { updateSetting(setting, container, input) {
const pane = this.elements.settings.panes[setting]; const pane = this.elements.settings.panes[setting];
let value = null; let value = null;
let list = container; let list = container;
switch (setting) { switch (setting) {
case 'captions': case 'captions':
value = this.captions.active ? this.captions.language : ''; value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config);
break; break;
default: default:
value = this[setting]; value = !utils.is.empty(input) ? input : this[setting];
// Get default // Get default
if (utils.is.empty(value)) { if (utils.is.empty(value)) {
@ -547,7 +547,7 @@ const controls = {
} }
// Unsupported value // Unsupported value
if (!this.options[setting].includes(value)) { if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) {
this.debug.warn(`Unsupported value of '${value}' for ${setting}`); this.debug.warn(`Unsupported value of '${value}' for ${setting}`);
return; return;
} }
@ -583,6 +583,11 @@ const controls = {
// Set the looping options // Set the looping options
/* setLoopMenu() { /* setLoopMenu() {
// Menu required
if (!utils.is.element(this.elements.settings.panes.loop)) {
return;
}
const options = ['start', 'end', 'all', 'reset']; const options = ['start', 'end', 'all', 'reset'];
const list = this.elements.settings.panes.loop.querySelector('ul'); const list = this.elements.settings.panes.loop.querySelector('ul');
@ -607,7 +612,7 @@ const controls = {
class: this.config.classNames.control, class: this.config.classNames.control,
'data-plyr-loop-action': option, 'data-plyr-loop-action': option,
}), }),
this.config.i18n[option] i18n.get(option, this.config)
); );
if (['start', 'end'].includes(option)) { if (['start', 'end'].includes(option)) {
@ -627,11 +632,7 @@ const controls = {
return null; return null;
} }
if (!support.textTracks || !captions.getTracks.call(this).length) { if (support.textTracks && captions.getTracks.call(this).length && this.captions.active) {
return this.config.i18n.none;
}
if (this.captions.active) {
const currentTrack = captions.getCurrentTrack.call(this); const currentTrack = captions.getCurrentTrack.call(this);
if (utils.is.track(currentTrack)) { if (utils.is.track(currentTrack)) {
@ -639,7 +640,7 @@ const controls = {
} }
} }
return this.config.i18n.disabled; return i18n.get('disabled', this.config);
}, },
// Set a list of available captions languages // Set a list of available captions languages
@ -649,14 +650,14 @@ const controls = {
const list = this.elements.settings.panes.captions.querySelector('ul'); const list = this.elements.settings.panes.captions.querySelector('ul');
// Toggle the pane and tab // Toggle the pane and tab
const hasTracks = captions.getTracks.call(this).length; const toggle = captions.getTracks.call(this).length;
controls.toggleTab.call(this, type, hasTracks); controls.toggleTab.call(this, type, toggle);
// Empty the menu // Empty the menu
utils.emptyElement(list); utils.emptyElement(list);
// If there's no captions, bail // If there's no captions, bail
if (!hasTracks) { if (!toggle) {
return; return;
} }
@ -666,10 +667,10 @@ const controls = {
label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(), label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(),
})); }));
// Add the "None" option to turn off captions // Add the "Disabled" option to turn off captions
tracks.unshift({ tracks.unshift({
language: '', language: '',
label: this.config.i18n.none, label: i18n.get('disabled', this.config),
}); });
// Generate options // Generate options
@ -681,7 +682,7 @@ const controls = {
'language', 'language',
track.label || track.language, track.label || track.language,
controls.createBadge.call(this, track.language.toUpperCase()), controls.createBadge.call(this, track.language.toUpperCase()),
track.language.toLowerCase() === this.captions.language.toLowerCase() track.language.toLowerCase() === this.captions.language.toLowerCase(),
); );
}); });
@ -689,11 +690,23 @@ const controls = {
}, },
// Set a list of available captions languages // Set a list of available captions languages
setSpeedMenu() { setSpeedMenu(options) {
// Do nothing if not selected
if (!this.config.controls.includes('settings') || !this.config.settings.includes('speed')) {
return;
}
// Menu required
if (!utils.is.element(this.elements.settings.panes.speed)) {
return;
}
const type = 'speed'; const type = 'speed';
// Set the default speeds // Set the speed options
if (!utils.is.object(this.options.speed) || !Object.keys(this.options.speed).length) { if (utils.is.array(options)) {
this.options.speed = options;
} else if (this.isHTML5 || this.isVimeo) {
this.options.speed = [ this.options.speed = [
0.5, 0.5,
0.75, 0.75,
@ -709,9 +722,12 @@ const controls = {
this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed)); this.options.speed = this.options.speed.filter(speed => this.config.speed.options.includes(speed));
// Toggle the pane and tab // Toggle the pane and tab
const toggle = !utils.is.empty(this.options.speed); const toggle = !utils.is.empty(this.options.speed) && this.options.speed.length > 1;
controls.toggleTab.call(this, type, toggle); controls.toggleTab.call(this, type, toggle);
// Check if we need to toggle the parent
controls.checkMenu.call(this);
// If we're hiding, nothing more to do // If we're hiding, nothing more to do
if (!toggle) { if (!toggle) {
return; return;
@ -728,15 +744,32 @@ const controls = {
utils.emptyElement(list); utils.emptyElement(list);
// Create items // Create items
this.options.speed.forEach(speed => controls.createMenuItem.call(this, speed, list, type, controls.getLabel.call(this, 'speed', speed))); this.options.speed.forEach(speed => {
const label = controls.getLabel.call(this, 'speed', speed);
controls.createMenuItem.call(this, speed, list, type, label);
});
controls.updateSetting.call(this, type, list); controls.updateSetting.call(this, type, list);
}, },
// Check if we need to hide/show the settings menu
checkMenu() {
const { tabs } = this.elements.settings;
const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden);
utils.toggleHidden(this.elements.settings.menu, !visible);
},
// Show/hide menu // Show/hide menu
toggleMenu(event) { toggleMenu(event) {
const { form } = this.elements.settings; const { form } = this.elements.settings;
const button = this.elements.buttons.settings; const button = this.elements.buttons.settings;
// Menu and button are required
if (!utils.is.element(form) || !utils.is.element(button)) {
return;
}
const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true'; const show = utils.is.boolean(event) ? event : utils.is.element(form) && form.getAttribute('aria-hidden') === 'true';
if (utils.is.event(event)) { if (utils.is.event(event)) {
@ -933,7 +966,7 @@ const controls = {
role: 'tooltip', role: 'tooltip',
class: this.config.classNames.tooltip, class: this.config.classNames.tooltip,
}, },
'00:00' '00:00',
); );
progress.appendChild(tooltip); progress.appendChild(tooltip);
@ -978,7 +1011,7 @@ const controls = {
'volume', 'volume',
utils.extend(attributes, { utils.extend(attributes, {
id: `plyr-volume-${data.id}`, id: `plyr-volume-${data.id}`,
}) }),
); );
volume.appendChild(range.label); volume.appendChild(range.label);
volume.appendChild(range.input); volume.appendChild(range.input);
@ -997,6 +1030,7 @@ const controls = {
if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
const menu = utils.createElement('div', { const menu = utils.createElement('div', {
class: 'plyr__menu', class: 'plyr__menu',
hidden: '',
}); });
menu.appendChild( menu.appendChild(
@ -1005,7 +1039,7 @@ const controls = {
'aria-haspopup': true, 'aria-haspopup': true,
'aria-controls': `plyr-settings-${data.id}`, 'aria-controls': `plyr-settings-${data.id}`,
'aria-expanded': false, 'aria-expanded': false,
}) }),
); );
const form = utils.createElement('form', { const form = utils.createElement('form', {
@ -1048,7 +1082,7 @@ const controls = {
'aria-controls': `plyr-settings-${data.id}-${type}`, 'aria-controls': `plyr-settings-${data.id}-${type}`,
'aria-expanded': false, 'aria-expanded': false,
}), }),
this.config.i18n[type] i18n.get(type, this.config),
); );
const value = utils.createElement('span', { const value = utils.createElement('span', {
@ -1088,7 +1122,7 @@ const controls = {
'aria-controls': `plyr-settings-${data.id}-home`, 'aria-controls': `plyr-settings-${data.id}-home`,
'aria-expanded': false, 'aria-expanded': false,
}, },
this.config.i18n[type] i18n.get(type, this.config),
); );
pane.appendChild(back); pane.appendChild(back);
@ -1131,10 +1165,12 @@ const controls = {
this.elements.controls = container; this.elements.controls = container;
if (this.config.controls.includes('settings') && this.config.settings.includes('speed')) { if (this.isHTML5) {
controls.setSpeedMenu.call(this); controls.setQualityMenu.call(this, html5.getQualityOptions.call(this));
} }
controls.setSpeedMenu.call(this);
return container; return container;
}, },
@ -1221,7 +1257,7 @@ const controls = {
this.config.selectors.labels, this.config.selectors.labels,
' .', ' .',
this.config.classNames.hidden, this.config.classNames.hidden,
].join('') ].join(''),
); );
Array.from(labels).forEach(label => { Array.from(labels).forEach(label => {

View File

@ -56,24 +56,26 @@ const defaults = {
// Sprite (for icons) // Sprite (for icons)
loadSprite: true, loadSprite: true,
iconPrefix: 'plyr', iconPrefix: 'plyr',
iconUrl: 'https://cdn.plyr.io/3.0.0-beta.13/plyr.svg', iconUrl: 'https://cdn.plyr.io/3.1.0-beta.2/plyr.svg',
// Blank video (used to prevent errors on source change) // Blank video (used to prevent errors on source change)
blankVideo: 'https://cdn.plyr.io/static/blank.mp4', blankVideo: 'https://cdn.plyr.io/static/blank.mp4',
// Quality default // Quality default
quality: { quality: {
default: 'default', default: 576,
options: [ options: [
'hd2160', 4320,
'hd1440', 2880,
'hd1080', 2160,
'hd720', 1440,
'large', 1080,
'medium', 720,
'small', 576,
'tiny', 480,
'default', 360,
240,
'default', // YouTube's "auto"
], ],
}, },
@ -132,7 +134,10 @@ const defaults = {
// Default controls // Default controls
controls: [ controls: [
'play-large', 'play-large',
// 'restart',
// 'rewind',
'play', 'play',
// 'fast-forward',
'progress', 'progress',
'current-time', 'current-time',
'mute', 'mute',
@ -155,7 +160,7 @@ const defaults = {
rewind: 'Rewind {seektime} secs', rewind: 'Rewind {seektime} secs',
play: 'Play', play: 'Play',
pause: 'Pause', pause: 'Pause',
forward: 'Forward {seektime} secs', fastForward: 'Forward {seektime} secs',
seek: 'Seek', seek: 'Seek',
played: 'Played', played: 'Played',
buffered: 'Buffered', buffered: 'Buffered',
@ -178,9 +183,8 @@ const defaults = {
end: 'End', end: 'End',
all: 'All', all: 'All',
reset: 'Reset', reset: 'Reset',
none: 'None',
disabled: 'Disabled', disabled: 'Disabled',
advertisment: 'Ad', advertisement: 'Ad',
}, },
// URLs // URLs
@ -203,7 +207,7 @@ const defaults = {
pause: null, pause: null,
restart: null, restart: null,
rewind: null, rewind: null,
forward: null, fastForward: null,
mute: null, mute: null,
volume: null, volume: null,
captions: null, captions: null,
@ -259,7 +263,7 @@ const defaults = {
// Ads // Ads
'adsloaded', 'adsloaded',
'adscontentpause', 'adscontentpause',
'adsconentresume', 'adscontentresume',
'adstarted', 'adstarted',
'adsmidpoint', 'adsmidpoint',
'adscomplete', 'adscomplete',
@ -283,7 +287,7 @@ const defaults = {
pause: '[data-plyr="pause"]', pause: '[data-plyr="pause"]',
restart: '[data-plyr="restart"]', restart: '[data-plyr="restart"]',
rewind: '[data-plyr="rewind"]', rewind: '[data-plyr="rewind"]',
forward: '[data-plyr="fast-forward"]', fastForward: '[data-plyr="fast-forward"]',
mute: '[data-plyr="mute"]', mute: '[data-plyr="mute"]',
captions: '[data-plyr="captions"]', captions: '[data-plyr="captions"]',
fullscreen: '[data-plyr="fullscreen"]', fullscreen: '[data-plyr="fullscreen"]',
@ -373,9 +377,10 @@ const defaults = {
}, },
// Advertisements plugin // Advertisements plugin
// Tag is not required as publisher is determined by vi.ai using the domain // Register for an account here: http://vi.ai/publisher-video-monetization/?aid=plyrio
ads: { ads: {
enabled: false, enabled: false,
publisherId: '',
}, },
}; };

View File

@ -1,5 +1,6 @@
// ========================================================================== // ==========================================================================
// Fullscreen wrapper // Fullscreen wrapper
// https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API#prefixing
// ========================================================================== // ==========================================================================
import utils from './utils'; import utils from './utils';
@ -54,6 +55,7 @@ class Fullscreen {
// Get prefix // Get prefix
this.prefix = Fullscreen.prefix; this.prefix = Fullscreen.prefix;
this.name = Fullscreen.name;
// Scroll position // Scroll position
this.scrollPosition = { x: 0, y: 0 }; this.scrollPosition = { x: 0, y: 0 };
@ -66,13 +68,15 @@ class Fullscreen {
}); });
// Fullscreen toggle on double click // Fullscreen toggle on double click
utils.on(this.player.elements.container, 'dblclick', () => { utils.on(this.player.elements.container, 'dblclick', event => {
// Ignore double click in controls
if (this.player.elements.controls.contains(event.target)) {
return;
}
this.toggle(); this.toggle();
}); });
// Prevent double click on controls bubbling up
utils.on(this.player.elements.controls, 'dblclick', event => event.stopPropagation());
// Update the UI // Update the UI
this.update(); this.update();
} }
@ -85,7 +89,7 @@ class Fullscreen {
// Get the prefix for handlers // Get the prefix for handlers
static get prefix() { static get prefix() {
// No prefix // No prefix
if (utils.is.function(document.cancelFullScreen)) { if (utils.is.function(document.exitFullscreen)) {
return false; return false;
} }
@ -98,12 +102,9 @@ class Fullscreen {
]; ];
prefixes.some(pre => { prefixes.some(pre => {
if (utils.is.function(document[`${pre}CancelFullScreen`])) { if (utils.is.function(document[`${pre}ExitFullscreen`]) || utils.is.function(document[`${pre}CancelFullScreen`])) {
value = pre; value = pre;
return true; return true;
} else if (utils.is.function(document.msExitFullscreen)) {
value = 'ms';
return true;
} }
return false; return false;
@ -112,11 +113,18 @@ class Fullscreen {
return value; return value;
} }
static get name() {
return this.prefix === 'moz' ? 'FullScreen' : 'Fullscreen';
}
// Determine if fullscreen is enabled // Determine if fullscreen is enabled
get enabled() { get enabled() {
const fallback = this.player.config.fullscreen.fallback && !utils.inFrame(); return (
(Fullscreen.native || this.player.config.fullscreen.fallback) &&
return (Fullscreen.native || fallback) && this.player.config.fullscreen.enabled && this.player.supported.ui && this.player.isVideo; this.player.config.fullscreen.enabled &&
this.player.supported.ui &&
this.player.isVideo
);
} }
// Get active state // Get active state
@ -130,7 +138,7 @@ class Fullscreen {
return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback); return utils.hasClass(this.target, this.player.config.classNames.fullscreen.fallback);
} }
const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}FullscreenElement`]; const element = !this.prefix ? document.fullscreenElement : document[`${this.prefix}${this.name}Element`];
return element === this.target; return element === this.target;
} }
@ -166,9 +174,9 @@ class Fullscreen {
} else if (!Fullscreen.native) { } else if (!Fullscreen.native) {
toggleFallback.call(this, true); toggleFallback.call(this, true);
} else if (!this.prefix) { } else if (!this.prefix) {
this.target.requestFullScreen(); this.target.requestFullscreen();
} else if (!utils.is.empty(this.prefix)) { } else if (!utils.is.empty(this.prefix)) {
this.target[`${this.prefix}${this.prefix === 'ms' ? 'RequestFullscreen' : 'RequestFullScreen'}`](); this.target[`${this.prefix}Request${this.name}`]();
} }
} }
@ -187,7 +195,8 @@ class Fullscreen {
} else if (!this.prefix) { } else if (!this.prefix) {
document.cancelFullScreen(); document.cancelFullScreen();
} else if (!utils.is.empty(this.prefix)) { } else if (!utils.is.empty(this.prefix)) {
document[`${this.prefix}${this.prefix === 'ms' ? 'ExitFullscreen' : 'CancelFullScreen'}`](); const action = this.prefix === 'moz' ? 'Cancel' : 'Exit';
document[`${this.prefix}${action}${this.name}`]();
} }
} }

146
src/js/html5.js Normal file
View File

@ -0,0 +1,146 @@
// ==========================================================================
// Plyr HTML5 helpers
// ==========================================================================
import support from './support';
import utils from './utils';
const html5 = {
getSources() {
if (!this.isHTML5) {
return null;
}
return this.media.querySelectorAll('source');
},
// Get quality levels
getQualityOptions() {
if (!this.isHTML5) {
return null;
}
// Get sources
const sources = html5.getSources.call(this);
if (utils.is.empty(sources)) {
return null;
}
// Get <source> with size attribute
const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size')));
// If none, bail
if (utils.is.empty(sizes)) {
return null;
}
// Reduce to unique list
return utils.dedupe(sizes.map(source => Number(source.getAttribute('size'))));
},
extend() {
if (!this.isHTML5) {
return;
}
const player = this;
// Quality
Object.defineProperty(player.media, 'quality', {
get() {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
return null;
}
const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source);
if (utils.is.empty(matches)) {
return null;
}
return Number(matches[0].getAttribute('size'));
},
set(input) {
// Get sources
const sources = html5.getSources.call(player);
if (utils.is.empty(sources)) {
return;
}
// Get matches for requested size
const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input);
// No matches for requested size
if (utils.is.empty(matches)) {
return;
}
// Get supported sources
const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type')));
// No supported sources
if (utils.is.empty(supported)) {
return;
}
// Trigger change event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input,
});
// Get current state
const { currentTime, playing } = player;
// Set new source
player.media.src = supported[0].getAttribute('src');
// Load new source
player.media.load();
// Resume playing
if (playing) {
player.play();
}
// Restore time
player.currentTime = currentTime;
// Trigger change event
utils.dispatchEvent.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
utils.removeElement(html5.getSources());
// 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');
},
};
export default html5;

31
src/js/i18n.js Normal file
View File

@ -0,0 +1,31 @@
// ==========================================================================
// Plyr internationalization
// ==========================================================================
import utils from './utils';
const i18n = {
get(key = '', config = {}) {
if (utils.is.empty(key) || utils.is.empty(config) || !Object.keys(config.i18n).includes(key)) {
return '';
}
let string = config.i18n[key];
const replace = {
'{seektime}': config.seekTime,
'{title}': config.title,
};
Object.entries(replace).forEach(([
key,
value,
]) => {
string = utils.replaceAll(string, key, value);
});
return string;
},
};
export default i18n;

View File

@ -10,258 +10,300 @@ import ui from './ui';
// Sniff out the browser // Sniff out the browser
const browser = utils.getBrowser(); const browser = utils.getBrowser();
const listeners = { class Listeners {
// Global listeners constructor(player) {
global() { this.player = player;
let last = null; this.lastKey = null;
// Get the key code for an event this.handleKey = this.handleKey.bind(this);
const getKeyCode = event => (event.keyCode ? event.keyCode : event.which); this.toggleMenu = this.toggleMenu.bind(this);
this.firstTouch = this.firstTouch.bind(this);
}
// Handle key press // Handle key presses
const handleKey = event => { handleKey(event) {
const code = getKeyCode(event); const code = event.keyCode ? event.keyCode : event.which;
const pressed = event.type === 'keydown'; const pressed = event.type === 'keydown';
const repeat = pressed && code === last; const repeat = pressed && code === this.lastKey;
// Bail if a modifier key is set // Bail if a modifier key is set
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return; return;
} }
// If the event is bubbled from the media element // If the event is bubbled from the media element
// Firefox doesn't get the keycode for whatever reason // Firefox doesn't get the keycode for whatever reason
if (!utils.is.number(code)) { if (!utils.is.number(code)) {
return; return;
} }
// Seek by the number keys // Seek by the number keys
const seekByKey = () => { const seekByKey = () => {
// Divide the max duration into 10th's and times by the number value // Divide the max duration into 10th's and times by the number value
this.currentTime = this.duration / 10 * (code - 48); this.player.currentTime = this.player.duration / 10 * (code - 48);
};
// Handle the key on keydown
// Reset on keyup
if (pressed) {
// Which keycodes should we prevent default
const preventDefault = [
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
// 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 = utils.getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.config.selectors.editable)) {
return;
}
// If the code is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(code)) {
event.preventDefault();
event.stopPropagation();
}
switch (code) {
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
// 0-9
if (!repeat) {
seekByKey();
}
break;
case 32:
case 75:
// Space and K key
if (!repeat) {
this.togglePlay();
}
break;
case 38:
// Arrow up
this.increaseVolume(0.1);
break;
case 40:
// Arrow down
this.decreaseVolume(0.1);
break;
case 77:
// M key
if (!repeat) {
this.muted = !this.muted;
}
break;
case 39:
// Arrow forward
this.forward();
break;
case 37:
// Arrow back
this.rewind();
break;
case 70:
// F key
this.fullscreen.toggle();
break;
case 67:
// C key
if (!repeat) {
this.toggleCaptions();
}
break;
case 76:
// L key
this.loop = !this.loop;
break;
/* case 73:
this.setLoop('start');
break;
case 76:
this.setLoop();
break;
case 79:
this.setLoop('end');
break; */
default:
break;
}
// Escape is handle natively when in full screen
// So we only need to worry about non native
if (!this.fullscreen.enabled && this.fullscreen.active && code === 27) {
this.fullscreen.toggle();
}
// Store last code for next cycle
last = code;
} else {
last = null;
}
}; };
// Handle the key on keydown
// Reset on keyup
if (pressed) {
// Which keycodes should we prevent default
const preventDefault = [
48,
49,
50,
51,
52,
53,
54,
56,
57,
32,
75,
38,
40,
77,
39,
37,
70,
67,
73,
76,
79,
];
// 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 = utils.getFocusElement();
if (utils.is.element(focused) && utils.matches(focused, this.player.config.selectors.editable)) {
return;
}
// If the code is found prevent default (e.g. prevent scrolling for arrows)
if (preventDefault.includes(code)) {
event.preventDefault();
event.stopPropagation();
}
switch (code) {
case 48:
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
// 0-9
if (!repeat) {
seekByKey();
}
break;
case 32:
case 75:
// Space and K key
if (!repeat) {
this.player.togglePlay();
}
break;
case 38:
// Arrow up
this.player.increaseVolume(0.1);
break;
case 40:
// Arrow down
this.player.decreaseVolume(0.1);
break;
case 77:
// M key
if (!repeat) {
this.player.muted = !this.player.muted;
}
break;
case 39:
// Arrow forward
this.player.forward();
break;
case 37:
// Arrow back
this.player.rewind();
break;
case 70:
// F key
this.player.fullscreen.toggle();
break;
case 67:
// C key
if (!repeat) {
this.player.toggleCaptions();
}
break;
case 76:
// L key
this.player.loop = !this.player.loop;
break;
/* case 73:
this.setLoop('start');
break;
case 76:
this.setLoop();
break;
case 79:
this.setLoop('end');
break; */
default:
break;
}
// Escape is handle natively when in full screen
// So we only need to worry about non native
if (!this.player.fullscreen.enabled && this.player.fullscreen.active && code === 27) {
this.player.fullscreen.toggle();
}
// Store last code for next cycle
this.lastKey = code;
} else {
this.lastKey = null;
}
}
// Toggle menu
toggleMenu(event) {
controls.toggleMenu.call(this.player, event);
}
// Device is touch enabled
firstTouch() {
this.player.touch = true;
// Add touch class
utils.toggleClass(this.player.elements.container, this.player.config.classNames.isTouch, true);
// Clean up
utils.off(document.body, 'touchstart', this.firstTouch);
}
// Global window & document listeners
global(toggle = true) {
// Keyboard shortcuts // Keyboard shortcuts
if (this.config.keyboard.global) { if (this.player.config.keyboard.global) {
utils.on(window, 'keydown keyup', handleKey, false); utils.toggleListener(window, 'keydown keyup', this.handleKey, toggle, false);
} else if (this.config.keyboard.focused) { }
utils.on(this.elements.container, 'keydown keyup', handleKey, false);
// Click anywhere closes menu
utils.toggleListener(document.body, 'click', this.toggleMenu, toggle);
// Detect touch by events
utils.on(document.body, 'touchstart', this.firstTouch);
}
// Container listeners
container() {
// Keyboard shortcuts
if (!this.player.config.keyboard.global && this.player.config.keyboard.focused) {
utils.on(this.player.elements.container, 'keydown keyup', this.handleKey, false);
} }
// Detect tab focus // Detect tab focus
// Remove class on blur/focusout // Remove class on blur/focusout
utils.on(this.elements.container, 'focusout', event => { utils.on(this.player.elements.container, 'focusout', event => {
utils.toggleClass(event.target, this.config.classNames.tabFocus, false); utils.toggleClass(event.target, this.player.config.classNames.tabFocus, false);
}); });
// Add classname to tabbed elements // Add classname to tabbed elements
utils.on(this.elements.container, 'keydown', event => { utils.on(this.player.elements.container, 'keydown', event => {
if (event.keyCode !== 9) { if (event.keyCode !== 9) {
return; return;
} }
// Delay the adding of classname until the focus has changed // Delay the adding of classname until the focus has changed
// This event fires before the focusin event // This event fires before the focusin event
window.setTimeout(() => { setTimeout(() => {
utils.toggleClass(utils.getFocusElement(), this.config.classNames.tabFocus, true); utils.toggleClass(utils.getFocusElement(), this.player.config.classNames.tabFocus, true);
}, 0); }, 0);
}); });
// Toggle controls visibility based on mouse movement // Toggle controls visibility based on mouse movement
if (this.config.hideControls) { if (this.player.config.hideControls) {
// Toggle controls on mouse events and entering fullscreen // Toggle controls on mouse events and entering fullscreen
utils.on(this.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => { utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => {
this.toggleControls(event); this.player.toggleControls(event);
}); });
} }
}, }
// Listen for media events // Listen for media events
media() { media() {
// Time change on media // Time change on media
utils.on(this.media, 'timeupdate seeking', event => ui.timeUpdate.call(this, event)); utils.on(this.player.media, 'timeupdate seeking', event => ui.timeUpdate.call(this.player, event));
// Display duration // Display duration
utils.on(this.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this, event)); utils.on(this.player.media, 'durationchange loadedmetadata', event => ui.durationUpdate.call(this.player, event));
// Check for audio tracks on load // Check for audio tracks on load
// We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point // We can't use `loadedmetadata` as it doesn't seem to have audio tracks at that point
utils.on(this.media, 'loadeddata', () => { utils.on(this.player.media, 'loadeddata', () => {
utils.toggleHidden(this.elements.volume, !this.hasAudio); utils.toggleHidden(this.player.elements.volume, !this.player.hasAudio);
utils.toggleHidden(this.elements.buttons.mute, !this.hasAudio); utils.toggleHidden(this.player.elements.buttons.mute, !this.player.hasAudio);
}); });
// Handle the media finishing // Handle the media finishing
utils.on(this.media, 'ended', () => { utils.on(this.player.media, 'ended', () => {
// Show poster on end // Show poster on end
if (this.isHTML5 && this.isVideo && this.config.showPosterOnEnd) { if (this.player.isHTML5 && this.player.isVideo && this.player.config.showPosterOnEnd) {
// Restart // Restart
this.restart(); this.player.restart();
// Re-load media // Re-load media
this.media.load(); this.player.media.load();
} }
}); });
// Check for buffer progress // Check for buffer progress
utils.on(this.media, 'progress playing', event => ui.updateProgress.call(this, event)); utils.on(this.player.media, 'progress playing', event => ui.updateProgress.call(this.player, event));
// Handle native mute // Handle volume changes
utils.on(this.media, 'volumechange', event => ui.updateVolume.call(this, event)); utils.on(this.player.media, 'volumechange', event => ui.updateVolume.call(this.player, event));
// Handle native play/pause // Handle play/pause
utils.on(this.media, 'playing play pause ended', event => ui.checkPlaying.call(this, event)); utils.on(this.player.media, 'playing play pause ended emptied', event => ui.checkPlaying.call(this.player, event));
// Loading // Loading state
utils.on(this.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this, event)); utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event));
// Check if media failed to load // Check if media failed to load
// utils.on(this.media, 'play', event => ui.checkFailed.call(this, event)); // utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event));
// If autoplay, then load advertisement if required
// TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows
utils.on(this.player.media, 'playing', () => {
// If ads are enabled, wait for them first
if (this.player.ads.enabled && !this.player.ads.initialized) {
// Wait for manager response
this.player.ads.managerPromise.then(() => this.player.ads.play()).catch(() => this.player.play());
}
});
// Click video // Click video
if (this.supported.ui && this.config.clickToPlay && !this.isAudio) { if (this.player.supported.ui && this.player.config.clickToPlay && !this.player.isAudio) {
// Re-fetch the wrapper // Re-fetch the wrapper
const wrapper = utils.getElement.call(this, `.${this.config.classNames.video}`); const wrapper = utils.getElement.call(this.player, `.${this.player.config.classNames.video}`);
// Bail if there's no wrapper (this should never happen) // Bail if there's no wrapper (this should never happen)
if (!utils.is.element(wrapper)) { if (!utils.is.element(wrapper)) {
@ -271,25 +313,25 @@ const listeners = {
// On click play, pause ore restart // On click play, pause ore restart
utils.on(wrapper, 'click', () => { utils.on(wrapper, 'click', () => {
// Touch devices will just show controls (if we're hiding controls) // Touch devices will just show controls (if we're hiding controls)
if (this.config.hideControls && support.touch && !this.paused) { if (this.player.config.hideControls && this.player.touch && !this.player.paused) {
return; return;
} }
if (this.paused) { if (this.player.paused) {
this.play(); this.player.play();
} else if (this.ended) { } else if (this.player.ended) {
this.restart(); this.player.restart();
this.play(); this.player.play();
} else { } else {
this.pause(); this.player.pause();
} }
}); });
} }
// Disable right click // Disable right click
if (this.supported.ui && this.config.disableContextMenu) { if (this.player.supported.ui && this.player.config.disableContextMenu) {
utils.on( utils.on(
this.media, this.player.media,
'contextmenu', 'contextmenu',
event => { event => {
event.preventDefault(); event.preventDefault();
@ -299,50 +341,53 @@ const listeners = {
} }
// Volume change // Volume change
utils.on(this.media, 'volumechange', () => { utils.on(this.player.media, 'volumechange', () => {
// Save to storage // Save to storage
this.storage.set({ volume: this.volume, muted: this.muted }); this.player.storage.set({ volume: this.player.volume, muted: this.player.muted });
}); });
// Speed change // Speed change
utils.on(this.media, 'ratechange', () => { utils.on(this.player.media, 'ratechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this, 'speed'); controls.updateSetting.call(this.player, 'speed');
// Save to storage // Save to storage
this.storage.set({ speed: this.speed }); this.player.storage.set({ speed: this.player.speed });
});
// Quality request
utils.on(this.player.media, 'qualityrequested', event => {
// Save to storage
this.player.storage.set({ quality: event.detail.quality });
}); });
// Quality change // Quality change
utils.on(this.media, 'qualitychange', () => { utils.on(this.player.media, 'qualitychange', event => {
// Update UI // Update UI
controls.updateSetting.call(this, 'quality'); controls.updateSetting.call(this.player, 'quality', null, event.detail.quality);
// Save to storage
this.storage.set({ quality: this.quality });
}); });
// Caption language change // Caption language change
utils.on(this.media, 'languagechange', () => { utils.on(this.player.media, 'languagechange', () => {
// Update UI // Update UI
controls.updateSetting.call(this, 'captions'); controls.updateSetting.call(this.player, 'captions');
// Save to storage // Save to storage
this.storage.set({ language: this.language }); this.player.storage.set({ language: this.player.language });
}); });
// Captions toggle // Captions toggle
utils.on(this.media, 'captionsenabled captionsdisabled', () => { utils.on(this.player.media, 'captionsenabled captionsdisabled', () => {
// Update UI // Update UI
controls.updateSetting.call(this, 'captions'); controls.updateSetting.call(this.player, 'captions');
// Save to storage // Save to storage
this.storage.set({ captions: this.captions.active }); this.player.storage.set({ captions: this.player.captions.active });
}); });
// Proxy events to container // Proxy events to container
// Bubble up key events for Edge // Bubble up key events for Edge
utils.on(this.media, this.config.events.concat([ utils.on(this.player.media, this.player.config.events.concat([
'keyup', 'keyup',
'keydown', 'keydown',
]).join(' '), event => { ]).join(' '), event => {
@ -350,229 +395,242 @@ const listeners = {
// Get error details from media // Get error details from media
if (event.type === 'error') { if (event.type === 'error') {
detail = this.media.error; detail = this.player.media.error;
} }
utils.dispatchEvent.call(this, this.elements.container, event.type, true, detail); utils.dispatchEvent.call(this.player, this.player.elements.container, event.type, true, detail);
}); });
}, }
// Listen for control events // Listen for control events
controls() { controls() {
// IE doesn't support input event, so we fallback to change // IE doesn't support input event, so we fallback to change
const inputEvent = browser.isIE ? 'change' : 'input'; const inputEvent = browser.isIE ? 'change' : 'input';
// Trigger custom and default handlers // Run default and custom handlers
const proxy = (event, handlerKey, defaultHandler) => { const proxy = (event, defaultHandler, customHandlerKey) => {
const customHandler = this.config.listeners[handlerKey]; const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
let returned = true;
// Execute custom handler // Execute custom handler
if (utils.is.function(customHandler)) { if (hasCustomHandler) {
customHandler.call(this, event); returned = customHandler.call(this.player, event);
} }
// Only call default handler if not prevented in custom handler // Only call default handler if not prevented in custom handler
if (!event.defaultPrevented && utils.is.function(defaultHandler)) { if (returned && utils.is.function(defaultHandler)) {
defaultHandler.call(this, event); defaultHandler.call(this.player, event);
} }
}; };
// Trigger custom and default handlers
const on = (element, type, defaultHandler, customHandlerKey, passive = true) => {
const customHandler = this.player.config.listeners[customHandlerKey];
const hasCustomHandler = utils.is.function(customHandler);
utils.on(element, type, event => proxy(event, defaultHandler, customHandlerKey), passive && !hasCustomHandler);
};
// Play/pause toggle // Play/pause toggle
utils.on(this.elements.buttons.play, 'click', event => on(this.player.elements.buttons.play, 'click', this.player.togglePlay, 'play');
proxy(event, 'play', () => {
this.togglePlay();
}),
);
// Pause // Pause
utils.on(this.elements.buttons.restart, 'click', event => on(this.player.elements.buttons.restart, 'click', this.player.restart, 'restart');
proxy(event, 'restart', () => {
this.restart();
}),
);
// Rewind // Rewind
utils.on(this.elements.buttons.rewind, 'click', event => on(this.player.elements.buttons.rewind, 'click', this.player.rewind, 'rewind');
proxy(event, 'rewind', () => {
this.rewind();
}),
);
// Rewind // Rewind
utils.on(this.elements.buttons.forward, 'click', event => on(this.player.elements.buttons.fastForward, 'click', this.player.forward, 'fastForward');
proxy(event, 'forward', () => {
this.forward();
}),
);
// Mute toggle // Mute toggle
utils.on(this.elements.buttons.mute, 'click', event => on(
proxy(event, 'mute', () => { this.player.elements.buttons.mute,
this.muted = !this.muted; 'click',
}), () => {
this.player.muted = !this.player.muted;
},
'mute',
); );
// Captions toggle // Captions toggle
utils.on(this.elements.buttons.captions, 'click', event => on(this.player.elements.buttons.captions, 'click', this.player.toggleCaptions);
proxy(event, 'captions', () => {
this.toggleCaptions();
}),
);
// Fullscreen toggle // Fullscreen toggle
utils.on(this.elements.buttons.fullscreen, 'click', event => on(
proxy(event, 'fullscreen', () => { this.player.elements.buttons.fullscreen,
this.fullscreen.toggle(); 'click',
}), () => {
this.player.fullscreen.toggle();
},
'fullscreen',
); );
// Picture-in-Picture // Picture-in-Picture
utils.on(this.elements.buttons.pip, 'click', event => on(
proxy(event, 'pip', () => { this.player.elements.buttons.pip,
this.pip = 'toggle'; 'click',
}), () => {
this.player.pip = 'toggle';
},
'pip',
); );
// Airplay // Airplay
utils.on(this.elements.buttons.airplay, 'click', event => on(this.player.elements.buttons.airplay, 'click', this.player.airplay, 'airplay');
proxy(event, 'airplay', () => {
this.airplay();
}),
);
// Settings menu // Settings menu
utils.on(this.elements.buttons.settings, 'click', event => { on(this.player.elements.buttons.settings, 'click', event => {
controls.toggleMenu.call(this, event); controls.toggleMenu.call(this.player, event);
});
// Click anywhere closes menu
utils.on(document.documentElement, 'click', event => {
controls.toggleMenu.call(this, event);
}); });
// Settings menu // Settings menu
utils.on(this.elements.settings.form, 'click', event => { on(this.player.elements.settings.form, 'click', event => {
event.stopPropagation(); event.stopPropagation();
// Settings menu items - use event delegation as items are added/removed // Settings menu items - use event delegation as items are added/removed
if (utils.matches(event.target, this.config.selectors.inputs.language)) { if (utils.matches(event.target, this.player.config.selectors.inputs.language)) {
proxy(event, 'language', () => { proxy(
this.language = event.target.value; event,
}); () => {
} else if (utils.matches(event.target, this.config.selectors.inputs.quality)) { this.player.language = event.target.value;
proxy(event, 'quality', () => { },
this.quality = event.target.value; 'language',
}); );
} else if (utils.matches(event.target, this.config.selectors.inputs.speed)) { } else if (utils.matches(event.target, this.player.config.selectors.inputs.quality)) {
proxy(event, 'speed', () => { proxy(
this.speed = parseFloat(event.target.value); event,
}); () => {
this.player.quality = event.target.value;
},
'quality',
);
} else if (utils.matches(event.target, this.player.config.selectors.inputs.speed)) {
proxy(
event,
() => {
this.player.speed = parseFloat(event.target.value);
},
'speed',
);
} else { } else {
controls.showTab.call(this, event); controls.showTab.call(this.player, event);
} }
}); });
// Seek // Seek
utils.on(this.elements.inputs.seek, inputEvent, event => on(
proxy(event, 'seek', () => { this.player.elements.inputs.seek,
this.currentTime = event.target.value / event.target.max * this.duration; inputEvent,
}), event => {
this.player.currentTime = event.target.value / event.target.max * this.player.duration;
},
'seek',
); );
// Current time invert // Current time invert
// Only if one time element is used for both currentTime and duration // Only if one time element is used for both currentTime and duration
if (this.config.toggleInvert && !utils.is.element(this.elements.display.duration)) { if (this.player.config.toggleInvert && !utils.is.element(this.player.elements.display.duration)) {
utils.on(this.elements.display.currentTime, 'click', () => { on(this.player.elements.display.currentTime, 'click', () => {
// Do nothing if we're at the start // Do nothing if we're at the start
if (this.currentTime === 0) { if (this.player.currentTime === 0) {
return; return;
} }
this.config.invertTime = !this.config.invertTime; this.player.config.invertTime = !this.player.config.invertTime;
ui.timeUpdate.call(this); ui.timeUpdate.call(this.player);
}); });
} }
// Volume // Volume
utils.on(this.elements.inputs.volume, inputEvent, event => on(
proxy(event, 'volume', () => { this.player.elements.inputs.volume,
this.volume = event.target.value; inputEvent,
}), event => {
this.player.volume = event.target.value;
},
'volume',
); );
// Polyfill for lower fill in <input type="range"> for webkit // Polyfill for lower fill in <input type="range"> for webkit
if (browser.isWebkit) { if (browser.isWebkit) {
utils.on(utils.getElements.call(this, 'input[type="range"]'), 'input', event => { on(utils.getElements.call(this.player, 'input[type="range"]'), 'input', event => {
controls.updateRangeFill.call(this, event.target); controls.updateRangeFill.call(this.player, event.target);
}); });
} }
// Seek tooltip // Seek tooltip
utils.on(this.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this, event)); on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event));
// Toggle controls visibility based on mouse movement // Toggle controls visibility based on mouse movement
if (this.config.hideControls) { if (this.player.config.hideControls) {
// Watch for cursor over controls so they don't hide when trying to interact // Watch for cursor over controls so they don't hide when trying to interact
utils.on(this.elements.controls, 'mouseenter mouseleave', event => { on(this.player.elements.controls, 'mouseenter mouseleave', event => {
this.elements.controls.hover = event.type === 'mouseenter'; this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter';
}); });
// Watch for cursor over controls so they don't hide when trying to interact // Watch for cursor over controls so they don't hide when trying to interact
utils.on(this.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => {
this.elements.controls.pressed = [ this.player.elements.controls.pressed = [
'mousedown', 'mousedown',
'touchstart', 'touchstart',
].includes(event.type); ].includes(event.type);
}); });
// Focus in/out on controls // Focus in/out on controls
utils.on(this.elements.controls, 'focusin focusout', event => { on(this.player.elements.controls, 'focusin focusout', event => {
this.toggleControls(event); this.player.toggleControls(event);
}); });
} }
// Mouse wheel for volume // Mouse wheel for volume
utils.on( on(
this.elements.inputs.volume, this.player.elements.inputs.volume,
'wheel', 'wheel',
event => event => {
proxy(event, 'volume', () => { // Detect "natural" scroll - suppored on OS X Safari only
// Detect "natural" scroll - suppored on OS X Safari only // Other browsers on OS X will be inverted until support improves
// Other browsers on OS X will be inverted until support improves const inverted = event.webkitDirectionInvertedFromDevice;
const inverted = event.webkitDirectionInvertedFromDevice; const step = 1 / 50;
const step = 1 / 50; let direction = 0;
let direction = 0;
// Scroll down (or up on natural) to decrease // Scroll down (or up on natural) to decrease
if (event.deltaY < 0 || event.deltaX > 0) { if (event.deltaY < 0 || event.deltaX > 0) {
if (inverted) { if (inverted) {
this.decreaseVolume(step); this.player.decreaseVolume(step);
direction = -1; direction = -1;
} else { } else {
this.increaseVolume(step); this.player.increaseVolume(step);
direction = 1; direction = 1;
}
} }
}
// Scroll up (or down on natural) to increase // Scroll up (or down on natural) to increase
if (event.deltaY > 0 || event.deltaX < 0) { if (event.deltaY > 0 || event.deltaX < 0) {
if (inverted) { if (inverted) {
this.increaseVolume(step); this.player.increaseVolume(step);
direction = 1; direction = 1;
} else { } else {
this.decreaseVolume(step); this.player.decreaseVolume(step);
direction = -1; direction = -1;
}
} }
}
// Don't break page scrolling at max and min // Don't break page scrolling at max and min
if ((direction === 1 && this.media.volume < 1) || (direction === -1 && this.media.volume > 0)) { if ((direction === 1 && this.player.media.volume < 1) || (direction === -1 && this.player.media.volume > 0)) {
event.preventDefault(); event.preventDefault();
} }
}), },
'volume',
false, false,
); );
}, }
};
export default listeners; // Reset on destroy
clear() {
this.global(false);
}
}
export default Listeners;

View File

@ -6,6 +6,7 @@ import support from './support';
import utils from './utils'; import utils from './utils';
import youtube from './plugins/youtube'; import youtube from './plugins/youtube';
import vimeo from './plugins/vimeo'; import vimeo from './plugins/vimeo';
import html5 from './html5';
import ui from './ui'; import ui from './ui';
// Sniff out the browser // Sniff out the browser
@ -46,7 +47,7 @@ const media = {
utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos); utils.toggleClass(this.elements.container, this.config.classNames.isIos, browser.isIos);
// Add touch class // Add touch class
utils.toggleClass(this.elements.container, this.config.classNames.isTouch, support.touch); utils.toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);
} }
// Inject the player wrapper // Inject the player wrapper
@ -75,32 +76,10 @@ const media = {
} }
} else if (this.isHTML5) { } else if (this.isHTML5) {
ui.setTitle.call(this); ui.setTitle.call(this);
html5.extend.call(this);
} }
}, },
// Cancel current network requests
// See https://github.com/sampotts/plyr/issues/174
cancelRequests() {
if (!this.isHTML5) {
return;
}
// Remove child sources
utils.removeElement(this.media.querySelectorAll('source'));
// 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');
},
}; };
export default media; export default media;

View File

@ -7,22 +7,7 @@
/* global google */ /* global google */
import utils from '../utils'; import utils from '../utils';
import i18n from '../i18n';
// Build the default tag URL
const getTagUrl = () => {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
AV_URL: '127.0.0.1:3000',
cb: 1,
AV_WIDTH: 640,
AV_HEIGHT: 480,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`;
};
class Ads { class Ads {
/** /**
@ -32,39 +17,10 @@ class Ads {
*/ */
constructor(player) { constructor(player) {
this.player = player; this.player = player;
this.enabled = player.config.ads.enabled; this.publisherId = player.config.ads.publisherId;
this.enabled = player.isHTML5 && player.isVideo && player.config.ads.enabled && utils.is.string(this.publisherId) && this.publisherId.length;
this.playing = false; this.playing = false;
this.initialized = false; this.initialized = false;
this.blocked = false;
this.enabled = utils.is.url(player.config.ads.tag);
// Check if a tag URL is provided.
if (!this.enabled) {
return;
}
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google)) {
utils.loadScript(
player.config.urls.googleIMA.api,
() => {
this.ready();
},
() => {
// Script failed to load or is blocked
this.blocked = true;
this.player.debug.log('Ads error: Google IMA SDK failed to load');
},
);
} else {
this.ready();
}
}
/**
* Get the ads instance ready.
*/
ready() {
this.elements = { this.elements = {
container: null, container: null,
displayContainer: null, displayContainer: null,
@ -76,32 +32,77 @@ class Ads {
this.safetyTimer = null; this.safetyTimer = null;
this.countdownTimer = null; this.countdownTimer = null;
// Set listeners on the Plyr instance // Setup a promise to resolve when the IMA manager is ready
this.listeners(); this.managerPromise = new Promise((resolve, reject) => {
// The ad is loaded and ready
this.on('loaded', resolve);
// Ads failed
this.on('error', reject);
});
this.load();
}
/**
* Load the IMA SDK
*/
load() {
if (this.enabled) {
// Check if the Google IMA3 SDK is loaded or load it ourselves
if (!utils.is.object(window.google) || !utils.is.object(window.google.ima)) {
utils
.loadScript(this.player.config.urls.googleIMA.api)
.then(() => {
this.ready();
})
.catch(() => {
// Script failed to load or is blocked
this.trigger('error', new Error('Google IMA SDK failed to load'));
});
} else {
this.ready();
}
}
}
/**
* Get the ads instance ready
*/
ready() {
// Start ticking our safety timer. If the whole advertisement // Start ticking our safety timer. If the whole advertisement
// thing doesn't resolve within our set time; we bail // thing doesn't resolve within our set time; we bail
this.startSafetyTimer(12000, 'ready()'); this.startSafetyTimer(12000, 'ready()');
// Setup a simple promise to resolve if the IMA loader is ready
this.loaderPromise = new Promise(resolve => {
this.on('ADS_LOADER_LOADED', () => resolve());
});
// Setup a promise to resolve if the IMA manager is ready
this.managerPromise = new Promise(resolve => {
this.on('ADS_MANAGER_LOADED', () => resolve());
});
// Clear the safety timer // Clear the safety timer
this.managerPromise.then(() => { this.managerPromise.then(() => {
this.clearSafetyTimer('onAdsManagerLoaded()'); this.clearSafetyTimer('onAdsManagerLoaded()');
}); });
// Set listeners on the Plyr instance
this.listeners();
// Setup the IMA SDK // Setup the IMA SDK
this.setupIMA(); this.setupIMA();
} }
// Build the default tag URL
get tagUrl() {
const params = {
AV_PUBLISHERID: '58c25bb0073ef448b1087ad6',
AV_CHANNELID: '5a0458dc28a06145e4519d21',
AV_URL: location.hostname,
cb: Date.now(),
AV_WIDTH: 640,
AV_HEIGHT: 480,
AV_CDIM2: this.publisherId,
};
const base = 'https://go.aniview.com/api/adserver6/vast/';
return `${base}?${utils.buildUrlParams(params)}`;
}
/** /**
* In order for the SDK to display ads for our video, we need to tell it where to put them, * In order for the SDK to display ads for our video, we need to tell it where to put them,
* so here we define our ad container. This div is set up to render on top of the video player. * so here we define our ad container. This div is set up to render on top of the video player.
@ -114,7 +115,6 @@ class Ads {
// Create the container for our advertisements // Create the container for our advertisements
this.elements.container = utils.createElement('div', { this.elements.container = utils.createElement('div', {
class: this.player.config.classNames.ads, class: this.player.config.classNames.ads,
hidden: '',
}); });
this.player.elements.container.appendChild(this.elements.container); this.player.elements.container.appendChild(this.elements.container);
@ -148,7 +148,7 @@ class Ads {
// Request video ads // Request video ads
const request = new google.ima.AdsRequest(); const request = new google.ima.AdsRequest();
request.adTagUrl = getTagUrl(); request.adTagUrl = this.tagUrl;
// Specify the linear and nonlinear slot sizes. This helps the SDK // Specify the linear and nonlinear slot sizes. This helps the SDK
// to select the correct creative if multiple are returned // to select the correct creative if multiple are returned
@ -161,8 +161,6 @@ class Ads {
request.forceNonLinearFullSlot = false; request.forceNonLinearFullSlot = false;
this.loader.requestAds(request); this.loader.requestAds(request);
this.handleEventListeners('ADS_LOADER_LOADED');
} catch (e) { } catch (e) {
this.onAdError(e); this.onAdError(e);
} }
@ -174,25 +172,25 @@ class Ads {
*/ */
pollCountdown(start = false) { pollCountdown(start = false) {
if (!start) { if (!start) {
window.clearInterval(this.countdownTimer); clearInterval(this.countdownTimer);
this.elements.container.removeAttribute('data-badge-text'); this.elements.container.removeAttribute('data-badge-text');
return; return;
} }
const update = () => { const update = () => {
const time = utils.formatTime(this.manager.getRemainingTime()); const time = utils.formatTime(Math.max(this.manager.getRemainingTime(), 0));
const label = `${this.player.config.i18n.advertisment} - ${time}`; const label = `${i18n.get('advertisement', this.player.config)} - ${time}`;
this.elements.container.setAttribute('data-badge-text', label); this.elements.container.setAttribute('data-badge-text', label);
}; };
this.countdownTimer = window.setInterval(update, 100); this.countdownTimer = setInterval(update, 100);
} }
/** /**
* This method is called whenever the ads are ready inside the AdDisplayContainer * This method is called whenever the ads are ready inside the AdDisplayContainer
* @param {Event} adsManagerLoadedEvent * @param {Event} adsManagerLoadedEvent
*/ */
onAdsManagerLoaded(adsManagerLoadedEvent) { onAdsManagerLoaded(event) {
// Get the ads manager // Get the ads manager
const settings = new google.ima.AdsRenderingSettings(); const settings = new google.ima.AdsRenderingSettings();
@ -202,27 +200,29 @@ class Ads {
// The SDK is polling currentTime on the contentPlayback. And needs a duration // The SDK is polling currentTime on the contentPlayback. And needs a duration
// so it can determine when to start the mid- and post-roll // so it can determine when to start the mid- and post-roll
this.manager = adsManagerLoadedEvent.getAdsManager(this.player, settings); this.manager = event.getAdsManager(this.player, settings);
// Get the cue points for any mid-rolls by filtering out the pre- and post-roll // Get the cue points for any mid-rolls by filtering out the pre- and post-roll
this.cuePoints = this.manager.getCuePoints(); this.cuePoints = this.manager.getCuePoints();
// Add advertisement cue's within the time line if available // Add advertisement cue's within the time line if available
this.cuePoints.forEach(cuePoint => { if (!utils.is.empty(this.cuePoints)) {
if (cuePoint !== 0 && cuePoint !== -1) { this.cuePoints.forEach(cuePoint => {
const seekElement = this.player.elements.progress; if (cuePoint !== 0 && cuePoint !== -1 && cuePoint < this.player.duration) {
const seekElement = this.player.elements.progress;
if (seekElement) { if (utils.is.element(seekElement)) {
const cuePercentage = 100 / this.player.duration * cuePoint; const cuePercentage = 100 / this.player.duration * cuePoint;
const cue = utils.createElement('span', { const cue = utils.createElement('span', {
class: this.player.config.classNames.cues, class: this.player.config.classNames.cues,
}); });
cue.style.left = `${cuePercentage.toString()}%`; cue.style.left = `${cuePercentage.toString()}%`;
seekElement.appendChild(cue); seekElement.appendChild(cue);
}
} }
} });
}); }
// Get skippable state // Get skippable state
// TODO: Skip button // TODO: Skip button
@ -241,7 +241,7 @@ class Ads {
}); });
// Resolve our adsManager // Resolve our adsManager
this.handleEventListeners('ADS_MANAGER_LOADED'); this.trigger('loaded');
} }
/** /**
@ -259,17 +259,18 @@ class Ads {
// Proxy event // Proxy event
const dispatchEvent = type => { const dispatchEvent = type => {
utils.dispatchEvent.call(this.player, this.player.media, `ads${type}`); const event = `ads${type.replace(/_/g, '').toLowerCase()}`;
utils.dispatchEvent.call(this.player, this.player.media, event);
}; };
switch (event.type) { switch (event.type) {
case google.ima.AdEvent.Type.LOADED: case google.ima.AdEvent.Type.LOADED:
// This is the first event sent for an ad - it is possible to determine whether the // This is the first event sent for an ad - it is possible to determine whether the
// ad is a video ad or an overlay // ad is a video ad or an overlay
this.handleEventListeners('LOADED'); this.trigger('loaded');
// Bubble event // Bubble event
dispatchEvent('loaded'); dispatchEvent(event.type);
// Start countdown // Start countdown
this.pollCountdown(true); this.pollCountdown(true);
@ -287,10 +288,9 @@ class Ads {
case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: case google.ima.AdEvent.Type.ALL_ADS_COMPLETED:
// All ads for the current videos are done. We can now request new advertisements // All ads for the current videos are done. We can now request new advertisements
// in case the video is re-played // in case the video is re-played
this.handleEventListeners('ALL_ADS_COMPLETED');
// Fire event // Fire event
dispatchEvent('allcomplete'); dispatchEvent(event.type);
// TODO: Example for what happens when a next video in a playlist would be loaded. // TODO: Example for what happens when a next video in a playlist would be loaded.
// So here we load a new video when all ads are done. // So here we load a new video when all ads are done.
@ -322,9 +322,8 @@ class Ads {
// This event indicates the ad has started - the video player can adjust the UI, // This event indicates the ad has started - the video player can adjust the UI,
// for example display a pause button and remaining time. Fired when content should // for example display a pause button and remaining time. Fired when content should
// be paused. This usually happens right before an ad is about to cover the content // be paused. This usually happens right before an ad is about to cover the content
this.handleEventListeners('CONTENT_PAUSE_REQUESTED');
dispatchEvent('contentpause'); dispatchEvent(event.type);
this.pauseContent(); this.pauseContent();
@ -335,9 +334,8 @@ class Ads {
// appropriate UI actions, such as removing the timer for remaining time detection. // appropriate UI actions, such as removing the timer for remaining time detection.
// Fired when content should be resumed. This usually happens when an ad finishes // Fired when content should be resumed. This usually happens when an ad finishes
// or collapses // or collapses
this.handleEventListeners('CONTENT_RESUME_REQUESTED');
dispatchEvent('contentresume'); dispatchEvent(event.type);
this.pollCountdown(); this.pollCountdown();
@ -346,23 +344,11 @@ class Ads {
break; break;
case google.ima.AdEvent.Type.STARTED: case google.ima.AdEvent.Type.STARTED:
dispatchEvent('started');
break;
case google.ima.AdEvent.Type.MIDPOINT: case google.ima.AdEvent.Type.MIDPOINT:
dispatchEvent('midpoint');
break;
case google.ima.AdEvent.Type.COMPLETE: case google.ima.AdEvent.Type.COMPLETE:
dispatchEvent('complete');
break;
case google.ima.AdEvent.Type.IMPRESSION: case google.ima.AdEvent.Type.IMPRESSION:
dispatchEvent('impression');
break;
case google.ima.AdEvent.Type.CLICK: case google.ima.AdEvent.Type.CLICK:
dispatchEvent('click'); dispatchEvent(event.type);
break; break;
default: default:
@ -376,7 +362,7 @@ class Ads {
*/ */
onAdError(event) { onAdError(event) {
this.cancel(); this.cancel();
this.player.debug.log('Ads error', event); this.player.debug.warn('Ads error', event);
} }
/** /**
@ -401,6 +387,10 @@ class Ads {
this.player.on('seeked', () => { this.player.on('seeked', () => {
const seekedTime = this.player.currentTime; const seekedTime = this.player.currentTime;
if (utils.is.empty(this.cuePoints)) {
return;
}
this.cuePoints.forEach((cuePoint, index) => { this.cuePoints.forEach((cuePoint, index) => {
if (time < cuePoint && cuePoint < seekedTime) { if (time < cuePoint && cuePoint < seekedTime) {
this.manager.discardAdBreak(); this.manager.discardAdBreak();
@ -412,7 +402,9 @@ class Ads {
// Listen to the resizing of the window. And resize ad accordingly // Listen to the resizing of the window. And resize ad accordingly
// TODO: eventually implement ResizeObserver // TODO: eventually implement ResizeObserver
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); if (this.manager) {
this.manager.resize(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
}
}); });
} }
@ -423,39 +415,41 @@ class Ads {
const { container } = this.player.elements; const { container } = this.player.elements;
if (!this.managerPromise) { if (!this.managerPromise) {
return; this.resumeContent();
} }
// Play the requested advertisement whenever the adsManager is ready // Play the requested advertisement whenever the adsManager is ready
this.managerPromise.then(() => { this.managerPromise
// Initialize the container. Must be done via a user action on mobile devices .then(() => {
this.elements.displayContainer.initialize(); // Initialize the container. Must be done via a user action on mobile devices
this.elements.displayContainer.initialize();
try { try {
if (!this.initialized) { if (!this.initialized) {
// Initialize the ads manager. Ad rules playlist will start at this time // Initialize the ads manager. Ad rules playlist will start at this time
this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL); this.manager.init(container.offsetWidth, container.offsetHeight, google.ima.ViewMode.NORMAL);
// Call play to start showing the ad. Single video and overlay ads will // Call play to start showing the ad. Single video and overlay ads will
// start at this time; the call will be ignored for ad rules // start at this time; the call will be ignored for ad rules
this.manager.start(); this.manager.start();
}
this.initialized = true;
} catch (adError) {
// An error may be thrown if there was a problem with the
// VAST response
this.onAdError(adError);
} }
})
this.initialized = true; .catch(() => {});
} catch (adError) {
// An error may be thrown if there was a problem with the
// VAST response
this.onAdError(adError);
}
});
} }
/** /**
* Resume our video. * Resume our video
*/ */
resumeContent() { resumeContent() {
// Hide our ad container // Hide the advertisement container
utils.toggleHidden(this.elements.container, true); this.elements.container.style.zIndex = '';
// Ad is stopped // Ad is stopped
this.playing = false; this.playing = false;
@ -470,8 +464,8 @@ class Ads {
* Pause our video * Pause our video
*/ */
pauseContent() { pauseContent() {
// Show our ad container. // Show the advertisement container
utils.toggleHidden(this.elements.container, false); this.elements.container.style.zIndex = 3;
// Ad is playing. // Ad is playing.
this.playing = true; this.playing = true;
@ -493,7 +487,7 @@ class Ads {
} }
// Tell our instance that we're done for now // Tell our instance that we're done for now
this.handleEventListeners('ERROR'); this.trigger('error');
// Re-create our adsManager // Re-create our adsManager
this.loadAds(); this.loadAds();
@ -504,30 +498,38 @@ class Ads {
*/ */
loadAds() { loadAds() {
// Tell our adsManager to go bye bye // Tell our adsManager to go bye bye
this.managerPromise.then(() => { this.managerPromise
// Destroy our adsManager .then(() => {
if (this.manager) { // Destroy our adsManager
this.manager.destroy(); if (this.manager) {
} this.manager.destroy();
}
// Re-set our adsManager promises // Re-set our adsManager promises
this.managerPromise = new Promise(resolve => { this.managerPromise = new Promise(resolve => {
this.on('ADS_MANAGER_LOADED', () => resolve()); this.on('loaded', resolve);
this.player.debug.log(this.manager); this.player.debug.log(this.manager);
}); });
// Now request some new advertisements // Now request some new advertisements
this.requestAds(); this.requestAds();
}); })
.catch(() => {});
} }
/** /**
* Handles callbacks after an ad event was invoked * Handles callbacks after an ad event was invoked
* @param {string} event - Event type * @param {string} event - Event type
*/ */
handleEventListeners(event) { trigger(event, ...args) {
if (utils.is.function(this.events[event])) { const handlers = this.events[event];
this.events[event].call(this);
if (utils.is.array(handlers)) {
handlers.forEach(handler => {
if (utils.is.function(handler)) {
handler.apply(this, args);
}
});
} }
} }
@ -538,7 +540,12 @@ class Ads {
* @return {Ads} * @return {Ads}
*/ */
on(event, callback) { on(event, callback) {
this.events[event] = callback; if (!utils.is.array(this.events[event])) {
this.events[event] = [];
}
this.events[event].push(callback);
return this; return this;
} }
@ -553,7 +560,7 @@ class Ads {
startSafetyTimer(time, from) { startSafetyTimer(time, from) {
this.player.debug.log(`Safety timer invoked from: ${from}`); this.player.debug.log(`Safety timer invoked from: ${from}`);
this.safetyTimer = window.setTimeout(() => { this.safetyTimer = setTimeout(() => {
this.cancel(); this.cancel();
this.clearSafetyTimer('startSafetyTimer()'); this.clearSafetyTimer('startSafetyTimer()');
}, time); }, time);

View File

@ -4,6 +4,7 @@
import utils from './../utils'; import utils from './../utils';
import captions from './../captions'; import captions from './../captions';
import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
const vimeo = { const vimeo = {
@ -16,9 +17,14 @@ const vimeo = {
// Load the API if not already // Load the API if not already
if (!utils.is.object(window.Vimeo)) { if (!utils.is.object(window.Vimeo)) {
utils.loadScript(this.config.urls.vimeo.api, () => { utils
vimeo.ready.call(this); .loadScript(this.config.urls.vimeo.api)
}); .then(() => {
vimeo.ready.call(this);
})
.catch(error => {
this.debug.warn('Vimeo API failed to load', error);
});
} else { } else {
vimeo.ready.call(this); vimeo.ready.call(this);
} }
@ -29,7 +35,7 @@ const vimeo = {
setAspectRatio(input) { setAspectRatio(input) {
const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':');
const padding = 100 / ratio[0] * ratio[1]; const padding = 100 / ratio[0] * ratio[1];
const height = 200; const height = 240;
const offset = (height - padding) / (height / 50); const offset = (height - padding) / (height / 50);
this.elements.wrapper.style.paddingBottom = `${padding}%`; this.elements.wrapper.style.paddingBottom = `${padding}%`;
this.media.style.transform = `translateY(-${offset}%)`; this.media.style.transform = `translateY(-${offset}%)`;
@ -96,10 +102,8 @@ const vimeo = {
}; };
player.media.stop = () => { player.media.stop = () => {
player.embed.stop().then(() => { player.pause();
player.media.paused = true; player.currentTime = 0;
player.currentTime = 0;
});
}; };
// Seeking // Seeking
@ -136,10 +140,18 @@ const vimeo = {
return speed; return speed;
}, },
set(input) { set(input) {
player.embed.setPlaybackRate(input).then(() => { player.embed
speed = input; .setPlaybackRate(input)
utils.dispatchEvent.call(player, player.media, 'ratechange'); .then(() => {
}); speed = input;
utils.dispatchEvent.call(player, player.media, 'ratechange');
})
.catch(error => {
// Hide menu item (and menu if empty)
if (error.name === 'Error') {
controls.setSpeedMenu.call(player, []);
}
});
}, },
}); });
@ -190,9 +202,15 @@ const vimeo = {
// Source // Source
let currentSrc; let currentSrc;
player.embed.getVideoUrl().then(value => { player.embed
currentSrc = value; .getVideoUrl()
}); .then(value => {
currentSrc = value;
})
.catch(error => {
this.debug.warn(error);
});
Object.defineProperty(player.media, 'currentSrc', { Object.defineProperty(player.media, 'currentSrc', {
get() { get() {
return currentSrc; return currentSrc;
@ -311,7 +329,7 @@ const vimeo = {
}); });
// Rebuild UI // Rebuild UI
window.setTimeout(() => ui.build.call(player), 0); setTimeout(() => ui.build.call(player), 0);
}, },
}; };

View File

@ -6,6 +6,64 @@ import utils from './../utils';
import controls from './../controls'; import controls from './../controls';
import ui from './../ui'; import ui from './../ui';
// Standardise YouTube quality unit
function mapQualityUnit(input) {
switch (input) {
case 'hd2160':
return 2160;
case 2160:
return 'hd2160';
case 'hd1440':
return 1440;
case 1440:
return 'hd1440';
case 'hd1080':
return 1080;
case 1080:
return 'hd1080';
case 'hd720':
return 720;
case 720:
return 'hd720';
case 'large':
return 480;
case 480:
return 'large';
case 'medium':
return 360;
case 360:
return 'medium';
case 'small':
return 240;
case 240:
return 'small';
default:
return 'default';
}
}
function mapQualityUnits(levels) {
if (utils.is.empty(levels)) {
return levels;
}
return utils.dedupe(levels.map(level => mapQualityUnit(level)));
}
const youtube = { const youtube = {
setup() { setup() {
// Add embed class for responsive // Add embed class for responsive
@ -19,7 +77,9 @@ const youtube = {
youtube.ready.call(this); youtube.ready.call(this);
} else { } else {
// Load the API // Load the API
utils.loadScript(this.config.urls.youtube.api); utils.loadScript(this.config.urls.youtube.api).catch(error => {
this.debug.warn('YouTube API failed to load', error);
});
// Setup callback for the API // Setup callback for the API
// YouTube has it's own system of course... // YouTube has it's own system of course...
@ -166,14 +226,10 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'error'); utils.dispatchEvent.call(player, player.media, 'error');
}, },
onPlaybackQualityChange(event) { onPlaybackQualityChange() {
// Get the instance utils.dispatchEvent.call(player, player.media, 'qualitychange', false, {
const instance = event.target; quality: player.media.quality,
});
// Get current quality
player.media.quality = instance.getPlaybackQuality();
utils.dispatchEvent.call(player, player.media, 'qualitychange');
}, },
onPlaybackRateChange(event) { onPlaybackRateChange(event) {
// Get the instance // Get the instance
@ -194,17 +250,14 @@ const youtube = {
// Create a faux HTML5 API using the YouTube API // Create a faux HTML5 API using the YouTube API
player.media.play = () => { player.media.play = () => {
instance.playVideo(); instance.playVideo();
player.media.paused = false;
}; };
player.media.pause = () => { player.media.pause = () => {
instance.pauseVideo(); instance.pauseVideo();
player.media.paused = true;
}; };
player.media.stop = () => { player.media.stop = () => {
instance.stopVideo(); instance.stopVideo();
player.media.paused = true;
}; };
player.media.duration = instance.getDuration(); player.media.duration = instance.getDuration();
@ -241,15 +294,18 @@ const youtube = {
// Quality // Quality
Object.defineProperty(player.media, 'quality', { Object.defineProperty(player.media, 'quality', {
get() { get() {
return instance.getPlaybackQuality(); return mapQualityUnit(instance.getPlaybackQuality());
}, },
set(input) { set(input) {
const quality = input;
// Set via API
instance.setPlaybackQuality(mapQualityUnit(quality));
// Trigger request event // Trigger request event
utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, {
quality: input, quality,
}); });
instance.setPlaybackQuality(input);
}, },
}); });
@ -306,10 +362,10 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'durationchange'); utils.dispatchEvent.call(player, player.media, 'durationchange');
// Reset timer // Reset timer
window.clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
// Setup buffering // Setup buffering
player.timers.buffering = window.setInterval(() => { player.timers.buffering = setInterval(() => {
// Get loaded % from YouTube // Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction(); player.media.buffered = instance.getVideoLoadedFraction();
@ -323,7 +379,7 @@ const youtube = {
// Bail if we're at 100% // Bail if we're at 100%
if (player.media.buffered === 1) { if (player.media.buffered === 1) {
window.clearInterval(player.timers.buffering); clearInterval(player.timers.buffering);
// Trigger event // Trigger event
utils.dispatchEvent.call(player, player.media, 'canplaythrough'); utils.dispatchEvent.call(player, player.media, 'canplaythrough');
@ -331,14 +387,14 @@ const youtube = {
}, 200); }, 200);
// Rebuild UI // Rebuild UI
window.setTimeout(() => ui.build.call(player), 50); setTimeout(() => ui.build.call(player), 50);
}, },
onStateChange(event) { onStateChange(event) {
// Get the instance // Get the instance
const instance = event.target; const instance = event.target;
// Reset timer // Reset timer
window.clearInterval(player.timers.playing); clearInterval(player.timers.playing);
// Handle events // Handle events
// -1 Unstarted // -1 Unstarted
@ -348,6 +404,16 @@ const youtube = {
// 3 Buffering // 3 Buffering
// 5 Video cued // 5 Video cued
switch (event.data) { switch (event.data) {
case -1:
// Update scrubber
utils.dispatchEvent.call(player, player.media, 'timeupdate');
// Get loaded % from YouTube
player.media.buffered = instance.getVideoLoadedFraction();
utils.dispatchEvent.call(player, player.media, 'progress');
break;
case 0: case 0:
player.media.paused = true; player.media.paused = true;
@ -378,7 +444,7 @@ const youtube = {
utils.dispatchEvent.call(player, player.media, 'playing'); utils.dispatchEvent.call(player, player.media, 'playing');
// Poll to get playback progress // Poll to get playback progress
player.timers.playing = window.setInterval(() => { player.timers.playing = setInterval(() => {
utils.dispatchEvent.call(player, player.media, 'timeupdate'); utils.dispatchEvent.call(player, player.media, 'timeupdate');
}, 50); }, 50);
@ -391,7 +457,7 @@ const youtube = {
} }
// Get quality // Get quality
controls.setQualityMenu.call(player, instance.getAvailableQualityLevels()); controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels()));
break; break;

View File

@ -1,6 +1,6 @@
// ========================================================================== // ==========================================================================
// Plyr // Plyr
// plyr.js v3.0.0-beta.13 // plyr.js v3.1.0-beta.2
// https://github.com/sampotts/plyr // https://github.com/sampotts/plyr
// License: The MIT License (MIT) // License: The MIT License (MIT)
// ========================================================================== // ==========================================================================
@ -12,12 +12,12 @@ import utils from './utils';
import Console from './console'; import Console from './console';
import Fullscreen from './fullscreen'; import Fullscreen from './fullscreen';
import Listeners from './listeners';
import Storage from './storage'; import Storage from './storage';
import Ads from './plugins/ads'; import Ads from './plugins/ads';
import captions from './captions'; import captions from './captions';
import controls from './controls'; import controls from './controls';
import listeners from './listeners';
import media from './media'; import media from './media';
import source from './source'; import source from './source';
import ui from './ui'; import ui from './ui';
@ -36,6 +36,9 @@ class Plyr {
this.loading = false; this.loading = false;
this.failed = false; this.failed = false;
// Touch device
this.touch = support.touch;
// Set the media element // Set the media element
this.media = target; this.media = target;
@ -130,7 +133,17 @@ class Plyr {
} }
// Cache original element state for .destroy() // Cache original element state for .destroy()
this.elements.original = this.media.cloneNode(true); // TODO: Investigate a better solution as I suspect this causes reported double load issues?
setTimeout(() => {
const clone = this.media.cloneNode(true);
// Prevent the clone autoplaying
if (clone.getAttribute('autoplay')) {
clone.pause();
}
this.elements.original = clone;
}, 0);
// Set media type based on tag or data attribute // Set media type based on tag or data attribute
// Supported: video, audio, vimeo, youtube // Supported: video, audio, vimeo, youtube
@ -235,6 +248,9 @@ class Plyr {
return; return;
} }
// Create listeners
this.listeners = new Listeners(this);
// Setup local storage for user settings // Setup local storage for user settings
this.storage = new Storage(this); this.storage = new Storage(this);
@ -250,9 +266,6 @@ class Plyr {
// Allow focus to be captured // Allow focus to be captured
this.elements.container.setAttribute('tabindex', 0); this.elements.container.setAttribute('tabindex', 0);
// Global listeners
listeners.global.call(this);
// Add style hook // Add style hook
ui.addStyleHook.call(this); ui.addStyleHook.call(this);
@ -272,11 +285,22 @@ class Plyr {
ui.build.call(this); ui.build.call(this);
} }
// Container listeners
this.listeners.container();
// Global listeners
this.listeners.global();
// Setup fullscreen // Setup fullscreen
this.fullscreen = new Fullscreen(this); this.fullscreen = new Fullscreen(this);
// Setup ads if provided // Setup ads if provided
this.ads = new Ads(this); this.ads = new Ads(this);
// Autoplay if required
if (this.config.autoplay) {
this.play();
}
} }
// --------------------------------------- // ---------------------------------------
@ -287,34 +311,37 @@ class Plyr {
* Types and provider helpers * Types and provider helpers
*/ */
get isHTML5() { get isHTML5() {
return this.provider === providers.html5; return Boolean(this.provider === providers.html5);
} }
get isEmbed() { get isEmbed() {
return this.isYouTube || this.isVimeo; return Boolean(this.isYouTube || this.isVimeo);
} }
get isYouTube() { get isYouTube() {
return this.provider === providers.youtube; return Boolean(this.provider === providers.youtube);
} }
get isVimeo() { get isVimeo() {
return this.provider === providers.vimeo; return Boolean(this.provider === providers.vimeo);
} }
get isVideo() { get isVideo() {
return this.type === types.video; return Boolean(this.type === types.video);
} }
get isAudio() { get isAudio() {
return this.type === types.audio; return Boolean(this.type === types.audio);
} }
/** /**
* Play the media, or play the advertisement (if they are not blocked) * Play the media, or play the advertisement (if they are not blocked)
*/ */
play() { play() {
// TODO: Always return a promise? if (!utils.is.function(this.media.play)) {
if (this.ads.enabled && !this.ads.initialized && !this.ads.blocked) {
this.ads.play();
return null; return null;
} }
// If ads are enabled, wait for them first
/* if (this.ads.enabled && !this.ads.initialized) {
return this.ads.managerPromise.then(() => this.ads.play()).catch(() => this.media.play());
} */
// Return the promise (for HTML5) // Return the promise (for HTML5)
return this.media.play(); return this.media.play();
} }
@ -323,7 +350,7 @@ class Plyr {
* Pause the media * Pause the media
*/ */
pause() { pause() {
if (!this.playing) { if (!this.playing || !utils.is.function(this.media.pause)) {
return; return;
} }
@ -334,21 +361,21 @@ class Plyr {
* Get paused state * Get paused state
*/ */
get paused() { get paused() {
return this.media.paused; return Boolean(this.media.paused);
} }
/** /**
* Get playing state * Get playing state
*/ */
get playing() { get playing() {
return !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true); return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true));
} }
/** /**
* Get ended state * Get ended state
*/ */
get ended() { get ended() {
return this.media.ended; return Boolean(this.media.ended);
} }
/** /**
@ -370,8 +397,11 @@ class Plyr {
* Stop playback * Stop playback
*/ */
stop() { stop() {
this.restart(); if (this.isHTML5) {
this.pause(); this.media.load();
} else if (utils.is.function(this.media.stop)) {
this.media.stop();
}
} }
/** /**
@ -416,7 +446,7 @@ class Plyr {
} }
// Set // Set
this.media.currentTime = targetTime.toFixed(4); this.media.currentTime = parseFloat(targetTime.toFixed(4));
// Logging // Logging
this.debug.log(`Seeking to ${this.currentTime} seconds`); this.debug.log(`Seeking to ${this.currentTime} seconds`);
@ -429,11 +459,32 @@ class Plyr {
return Number(this.media.currentTime); return Number(this.media.currentTime);
} }
/**
* Get buffered
*/
get buffered() {
const { buffered } = this.media;
// YouTube / Vimeo return a float between 0-1
if (utils.is.number(buffered)) {
return buffered;
}
// HTML5
// TODO: Handle buffered chunks of the media
// (i.e. seek to another section buffers only that section)
if (buffered && buffered.length && this.duration > 0) {
return buffered.end(0) / this.duration;
}
return 0;
}
/** /**
* Get seeking status * Get seeking status
*/ */
get seeking() { get seeking() {
return this.media.seeking; return Boolean(this.media.seeking);
} }
/** /**
@ -444,7 +495,7 @@ class Plyr {
const fauxDuration = parseInt(this.config.duration, 10); const fauxDuration = parseInt(this.config.duration, 10);
// True duration // True duration
const realDuration = Number(this.media.duration); const realDuration = this.media ? Number(this.media.duration) : 0;
// If custom duration is funky, use regular duration // If custom duration is funky, use regular duration
return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration; return !Number.isNaN(fauxDuration) ? fauxDuration : realDuration;
@ -488,8 +539,8 @@ class Plyr {
// Set the player volume // Set the player volume
this.media.volume = volume; this.media.volume = volume;
// If muted, and we're increasing volume, reset muted state // If muted, and we're increasing volume manually, reset muted state
if (this.muted && volume > 0) { if (!utils.is.empty(value) && this.muted && volume > 0) {
this.muted = false; this.muted = false;
} }
} }
@ -498,7 +549,7 @@ class Plyr {
* Get the current player volume * Get the current player volume
*/ */
get volume() { get volume() {
return this.media.volume; return Number(this.media.volume);
} }
/** /**
@ -547,7 +598,7 @@ class Plyr {
* Get current muted state * Get current muted state
*/ */
get muted() { get muted() {
return this.media.muted; return Boolean(this.media.muted);
} }
/** /**
@ -564,12 +615,16 @@ class Plyr {
} }
// Get audio tracks // Get audio tracks
return this.media.mozHasAudio || Boolean(this.media.webkitAudioDecodedByteCount) || Boolean(this.media.audioTracks && this.media.audioTracks.length); return (
Boolean(this.media.mozHasAudio) ||
Boolean(this.media.webkitAudioDecodedByteCount) ||
Boolean(this.media.audioTracks && this.media.audioTracks.length)
);
} }
/** /**
* Set playback speed * Set playback speed
* @param {decimal} speed - the speed of playback (0.5-2.0) * @param {number} speed - the speed of playback (0.5-2.0)
*/ */
set speed(input) { set speed(input) {
let speed = null; let speed = null;
@ -610,34 +665,43 @@ class Plyr {
* Get current playback speed * Get current playback speed
*/ */
get speed() { get speed() {
return this.media.playbackRate; return Number(this.media.playbackRate);
} }
/** /**
* Set playback quality * Set playback quality
* Currently YouTube only * Currently HTML5 & YouTube only
* @param {string} input - Quality level * @param {number} input - Quality level
*/ */
set quality(input) { set quality(input) {
let quality = null; let quality = null;
if (utils.is.string(input)) { if (!utils.is.empty(input)) {
quality = input; quality = Number(input);
} }
if (!utils.is.string(quality)) { if (!utils.is.number(quality) || quality === 0) {
quality = this.storage.get('quality'); quality = this.storage.get('quality');
} }
if (!utils.is.string(quality)) { if (!utils.is.number(quality)) {
quality = this.config.quality.selected; quality = this.config.quality.selected;
} }
if (!this.options.quality.includes(quality)) { if (!utils.is.number(quality)) {
this.debug.warn(`Unsupported quality option (${quality})`); quality = this.config.quality.default;
}
if (!this.options.quality.length) {
return; return;
} }
if (!this.options.quality.includes(quality)) {
const closest = utils.closest(this.options.quality, quality);
this.debug.warn(`Unsupported quality option: ${quality}, using ${closest} instead`);
quality = closest;
}
// Update config // Update config
this.config.quality.selected = quality; this.config.quality.selected = quality;
@ -710,7 +774,7 @@ class Plyr {
* Get current loop state * Get current loop state
*/ */
get loop() { get loop() {
return this.media.loop; return Boolean(this.media.loop);
} }
/** /**
@ -767,7 +831,7 @@ class Plyr {
* Get the current autoplay state * Get the current autoplay state
*/ */
get autoplay() { get autoplay() {
return this.config.autoplay; return Boolean(this.config.autoplay);
} }
/** /**
@ -917,26 +981,32 @@ class Plyr {
// Is the enter fullscreen event // Is the enter fullscreen event
isEnterFullscreen = toggle.type === 'enterfullscreen'; isEnterFullscreen = toggle.type === 'enterfullscreen';
// Whether to show controls // Events that show the controls
show = [ const showEvents = [
'mouseenter',
'mousemove',
'touchstart', 'touchstart',
'touchmove', 'touchmove',
'focusin', 'mouseenter',
].includes(toggle.type);
// Delay hiding on move events
if ([
'mousemove', 'mousemove',
'focusin',
];
// Events that delay hiding
const delayEvents = [
'touchmove', 'touchmove',
'touchend', 'touchend',
].includes(toggle.type)) { 'mousemove',
];
// Whether to show controls
show = showEvents.includes(toggle.type);
// Delay hiding on move events
if (delayEvents.includes(toggle.type)) {
delay = 2000; delay = 2000;
} }
// Delay a little more for keyboard users // Delay a little more for keyboard users
if (toggle.type === 'focusin') { if (!this.touch && toggle.type === 'focusin') {
delay = 3000; delay = 3000;
utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true); utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true);
} }
@ -946,7 +1016,7 @@ class Plyr {
} }
// Clear timer on every call // Clear timer on every call
window.clearTimeout(this.timers.controls); clearTimeout(this.timers.controls);
// If the mouse is not over the controls, set a timeout to hide them // If the mouse is not over the controls, set a timeout to hide them
if (show || this.paused || this.loading) { if (show || this.paused || this.loading) {
@ -964,7 +1034,7 @@ class Plyr {
} }
// Delay for hiding on touch // Delay for hiding on touch
if (support.touch) { if (this.touch) {
delay = 3000; delay = 3000;
} }
} }
@ -972,14 +1042,11 @@ class Plyr {
// If toggle is false or if we're playing (regardless of toggle), // If toggle is false or if we're playing (regardless of toggle),
// then set the timer to hide the controls // then set the timer to hide the controls
if (!show || this.playing) { if (!show || this.playing) {
this.timers.controls = window.setTimeout(() => { this.timers.controls = setTimeout(() => {
/* this.debug.warn({ // We need controls of course...
pressed: this.elements.controls.pressed, if (!utils.is.element(this.elements.controls)) {
hover: this.elements.controls.pressed, return;
playing: this.playing, }
paused: this.paused,
loading: this.loading,
}); */
// If the mouse is over the controls (and not entering fullscreen), bail // If the mouse is over the controls (and not entering fullscreen), bail
if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) { if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) {
@ -1032,6 +1099,10 @@ class Plyr {
* @param {boolean} soft - Whether it's a soft destroy (for source changes etc) * @param {boolean} soft - Whether it's a soft destroy (for source changes etc)
*/ */
destroy(callback, soft = false) { destroy(callback, soft = false) {
if (!this.ready) {
return;
}
const done = () => { const done = () => {
// Reset overflow (incase destroyed while in fullscreen) // Reset overflow (incase destroyed while in fullscreen)
document.body.style.overflow = ''; document.body.style.overflow = '';
@ -1060,6 +1131,9 @@ class Plyr {
callback(); callback();
} }
} else { } else {
// Unbind listeners
this.listeners.clear();
// Replace the container with the original element provided // Replace the container with the original element provided
utils.replaceElement(this.elements.original, this.elements.container); utils.replaceElement(this.elements.original, this.elements.container);
@ -1071,15 +1145,27 @@ class Plyr {
callback.call(this.elements.original); callback.call(this.elements.original);
} }
// Clear for GC // Reset state
this.elements = null; this.ready = false;
// Clear for garbage collection
setTimeout(() => {
this.elements = null;
this.media = null;
}, 200);
} }
}; };
// Stop playback
this.stop();
// Type specific stuff // Type specific stuff
switch (`${this.provider}:${this.type}`) { switch (`${this.provider}:${this.type}`) {
case 'html5:video': case 'html5:video':
case 'html5:audio': case 'html5:audio':
// Clear timeout
clearTimeout(this.timers.loading);
// Restore native video controls // Restore native video controls
ui.toggleNativeControls.call(this, true); ui.toggleNativeControls.call(this, true);
@ -1090,11 +1176,11 @@ class Plyr {
case 'youtube:video': case 'youtube:video':
// Clear timers // Clear timers
window.clearInterval(this.timers.buffering); clearInterval(this.timers.buffering);
window.clearInterval(this.timers.playing); clearInterval(this.timers.playing);
// Destroy YouTube API // Destroy YouTube API
if (this.embed !== null) { if (this.embed !== null && utils.is.function(this.embed.destroy)) {
this.embed.destroy(); this.embed.destroy();
} }
@ -1111,7 +1197,7 @@ class Plyr {
} }
// Vimeo does not always return // Vimeo does not always return
window.setTimeout(done, 200); setTimeout(done, 200);
break; break;

14
src/js/plyr.polyfilled.js Normal file
View File

@ -0,0 +1,14 @@
// ==========================================================================
// Plyr Polyfilled Build
// plyr.js v3.1.0-beta.2
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
import Plyr from './plyr';
export default Plyr;

View File

@ -4,6 +4,7 @@
import { providers } from './types'; import { providers } from './types';
import utils from './utils'; import utils from './utils';
import html5 from './html5';
import media from './media'; import media from './media';
import ui from './ui'; import ui from './ui';
import support from './support'; import support from './support';
@ -31,13 +32,14 @@ const source = {
} }
// Cancel current network requests // Cancel current network requests
media.cancelRequests.call(this); html5.cancelRequests.call(this);
// Destroy instance and re-setup // Destroy instance and re-setup
this.destroy.call( this.destroy.call(
this, this,
() => { () => {
// TODO: Reset menus here // Reset quality options
this.options.quality = [];
// Remove elements // Remove elements
utils.removeElement(this.media); utils.removeElement(this.media);

View File

@ -12,17 +12,18 @@ class Storage {
// Check for actual support (see if we can use it) // Check for actual support (see if we can use it)
static get supported() { static get supported() {
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
try { 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.setItem(test, test);
window.localStorage.removeItem(test); window.localStorage.removeItem(test);
return true; return true;
} catch (e) { } catch (e) {
return false; return false;
@ -30,9 +31,13 @@ class Storage {
} }
get(key) { get(key) {
if (!Storage.supported) {
return null;
}
const store = window.localStorage.getItem(this.key); const store = window.localStorage.getItem(this.key);
if (!Storage.supported || utils.is.empty(store)) { if (utils.is.empty(store)) {
return null; return null;
} }

View File

@ -30,13 +30,9 @@ const support = {
break; break;
case 'youtube:video': case 'youtube:video':
api = true;
ui = support.rangeInput && (!browser.isIPhone || playsInline);
break;
case 'vimeo:video': case 'vimeo:video':
api = true; api = true;
ui = support.rangeInput && !browser.isIPhone; ui = support.rangeInput && (!browser.isIPhone || playsInline);
break; break;
default: default:
@ -77,6 +73,11 @@ const support = {
return false; return false;
} }
// Check directly if codecs specified
if (type.includes('codecs=')) {
return media.canPlayType(type).replace(/no/, '');
}
// Type specific checks // Type specific checks
if (this.isVideo) { if (this.isVideo) {
switch (type) { switch (type) {
@ -147,7 +148,7 @@ const support = {
})(), })(),
// Touch // Touch
// Remember a device can be moust + touch enabled // NOTE: Remember a device can be mouse + touch enabled so we check on first touch event
touch: 'ontouchstart' in document.documentElement, touch: 'ontouchstart' in document.documentElement,
// Detect transitions support // Detect transitions support

View File

@ -5,7 +5,7 @@
import utils from './utils'; import utils from './utils';
import captions from './captions'; import captions from './captions';
import controls from './controls'; import controls from './controls';
import listeners from './listeners'; import i18n from './i18n';
const ui = { const ui = {
addStyleHook() { addStyleHook() {
@ -25,8 +25,8 @@ const ui = {
// Setup the UI // Setup the UI
build() { build() {
// Re-attach media element listeners // Re-attach media element listeners
// TODO: Use event bubbling // TODO: Use event bubbling?
listeners.media.call(this); this.listeners.media();
// Don't setup interface if no support // Don't setup interface if no support
if (!this.supported.ui) { if (!this.supported.ui) {
@ -45,7 +45,7 @@ const ui = {
controls.inject.call(this); controls.inject.call(this);
// Re-attach control listeners // Re-attach control listeners
listeners.controls.call(this); this.listeners.controls();
} }
// If there's no controls, bail // If there's no controls, bail
@ -71,8 +71,11 @@ const ui = {
// Reset loop state // Reset loop state
this.loop = null; this.loop = null;
// Reset quality options // Reset quality setting
this.options.quality = []; this.quality = null;
// Reset volume display
ui.updateVolume.call(this);
// Reset time display // Reset time display
ui.timeUpdate.call(this); ui.timeUpdate.call(this);
@ -84,7 +87,9 @@ const ui = {
this.ready = true; this.ready = true;
// Ready event at end of execution stack // Ready event at end of execution stack
utils.dispatchEvent.call(this, this.media, 'ready'); setTimeout(() => {
utils.dispatchEvent.call(this, this.media, 'ready');
}, 0);
// Set the title // Set the title
ui.setTitle.call(this); ui.setTitle.call(this);
@ -93,7 +98,7 @@ const ui = {
// Setup aria attribute for play and iframe title // Setup aria attribute for play and iframe title
setTitle() { setTitle() {
// Find the current text // Find the current text
let label = this.config.i18n.play; let label = i18n.get('play', this.config);
// If there's a media title set, use that for the label // If there's a media title set, use that for the label
if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) { if (utils.is.string(this.config.title) && !utils.is.empty(this.config.title)) {
@ -122,7 +127,7 @@ const ui = {
// Default to media type // Default to media type
const title = !utils.is.empty(this.config.title) ? this.config.title : 'video'; const title = !utils.is.empty(this.config.title) ? this.config.title : 'video';
iframe.setAttribute('title', this.config.i18n.frameTitle.replace('{title}', title)); iframe.setAttribute('title', i18n.get('frameTitle', this.config));
} }
}, },
@ -132,10 +137,8 @@ const ui = {
utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing); utils.toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused); utils.toggleClass(this.elements.container, this.config.classNames.stopped, this.paused);
// Set aria state // Set ARIA state
if (utils.is.nodeList(this.elements.buttons.play)) { utils.toggleState(this.elements.buttons.play, this.playing);
Array.from(this.elements.buttons.play).forEach(button => utils.toggleState(button, this.playing));
}
// Toggle controls // Toggle controls
this.toggleControls(!this.playing); this.toggleControls(!this.playing);
@ -256,21 +259,7 @@ const ui = {
// Check buffer status // Check buffer status
case 'playing': case 'playing':
case 'progress': case 'progress':
value = (() => { ui.setProgress.call(this, this.elements.display.buffer, this.buffered * 100);
const { buffered } = this.media;
if (buffered && buffered.length) {
// HTML5
return utils.getPercentage(buffered.end(0), this.duration);
} else if (utils.is.number(buffered)) {
// YouTube returns between 0 and 1
return buffered * 100;
}
return 0;
})();
ui.setProgress.call(this, this.elements.display.buffer, value);
break; break;

View File

@ -2,6 +2,8 @@
// Plyr utils // Plyr utils
// ========================================================================== // ==========================================================================
import loadjs from 'loadjs';
import support from './support'; import support from './support';
import { providers } from './types'; import { providers } from './types';
@ -83,7 +85,7 @@ const utils = {
// Fetch wrapper // Fetch wrapper
// Using XHR to avoid issues with older browsers // Using XHR to avoid issues with older browsers
fetch(url) { fetch(url, responseType = 'text') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const request = new XMLHttpRequest(); const request = new XMLHttpRequest();
@ -94,10 +96,14 @@ const utils = {
} }
request.addEventListener('load', () => { request.addEventListener('load', () => {
try { if (responseType === 'text') {
resolve(JSON.parse(request.responseText)); try {
} catch(e) { resolve(JSON.parse(request.responseText));
resolve(request.responseText); } catch (e) {
resolve(request.responseText);
}
} else {
resolve(request.response);
} }
}); });
@ -106,6 +112,10 @@ const utils = {
}); });
request.open('GET', url, true); request.open('GET', url, true);
// Set the required response type
request.responseType = responseType;
request.send(); request.send();
} catch (e) { } catch (e) {
reject(e); reject(e);
@ -114,55 +124,13 @@ const utils = {
}, },
// Load an external script // Load an external script
loadScript(url, callback, error) { loadScript(url) {
const current = document.querySelector(`script[src="${url}"]`); return new Promise((resolve, reject) => {
loadjs(url, {
// Check script is not already referenced, if so wait for load success: resolve,
if (current !== null) { error: reject,
current.callbacks = current.callbacks || []; });
current.callbacks.push(callback); });
return;
}
// Build the element
const element = document.createElement('script');
// Callback queue
element.callbacks = element.callbacks || [];
element.callbacks.push(callback);
// Error queue
element.errors = element.errors || [];
element.errors.push(error);
// Bind callback
if (utils.is.function(callback)) {
element.addEventListener(
'load',
event => {
element.callbacks.forEach(cb => cb.call(null, event));
element.callbacks = null;
},
false,
);
}
// Bind error handling
element.addEventListener(
'error',
event => {
element.errors.forEach(err => err.call(null, event));
element.errors = null;
},
false,
);
// Set the URL after binding callback
element.src = url;
// Inject
const first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(element, first);
}, },
// Load an external SVG sprite // Load an external SVG sprite
@ -175,7 +143,14 @@ const utils = {
const hasId = utils.is.string(id); const hasId = utils.is.string(id);
let isCached = false; let isCached = false;
function updateSprite(data) { const exists = () => document.querySelectorAll(`#${id}`).length;
function injectSprite(data) {
// Check again incase of race condition
if (hasId && exists()) {
return;
}
// Inject content // Inject content
this.innerHTML = data; this.innerHTML = data;
@ -183,8 +158,8 @@ const utils = {
document.body.insertBefore(this, document.body.childNodes[0]); document.body.insertBefore(this, document.body.childNodes[0]);
} }
// Only load once // Only load once if ID set
if (!hasId || !document.querySelectorAll(`#${id}`).length) { if (!hasId || !exists()) {
// Create container // Create container
const container = document.createElement('div'); const container = document.createElement('div');
utils.toggleHidden(container, true); utils.toggleHidden(container, true);
@ -200,7 +175,7 @@ const utils = {
if (isCached) { if (isCached) {
const data = JSON.parse(cached); const data = JSON.parse(cached);
updateSprite.call(container, data.content); injectSprite.call(container, data.content);
return; return;
} }
} }
@ -222,7 +197,7 @@ const utils = {
); );
} }
updateSprite.call(container, result); injectSprite.call(container, result);
}) })
.catch(() => {}); .catch(() => {});
} }
@ -233,15 +208,6 @@ const utils = {
return `${prefix}-${Math.floor(Math.random() * 10000)}`; return `${prefix}-${Math.floor(Math.random() * 10000)}`;
}, },
// Determine if we're in an iframe
inFrame() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
},
// Wrap an element // Wrap an element
wrap(elements, wrapper) { wrap(elements, wrapper) {
// Convert `elements` to an array, if necessary. // Convert `elements` to an array, if necessary.
@ -344,8 +310,11 @@ const utils = {
return; return;
} }
Object.keys(attributes).forEach(key => { Object.entries(attributes).forEach(([
element.setAttribute(key, attributes[key]); key,
value,
]) => {
element.setAttribute(key, value);
}); });
}, },
@ -472,7 +441,7 @@ const utils = {
pause: utils.getElement.call(this, this.config.selectors.buttons.pause), pause: utils.getElement.call(this, this.config.selectors.buttons.pause),
restart: utils.getElement.call(this, this.config.selectors.buttons.restart), restart: utils.getElement.call(this, this.config.selectors.buttons.restart),
rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind), rewind: utils.getElement.call(this, this.config.selectors.buttons.rewind),
forward: utils.getElement.call(this, this.config.selectors.buttons.forward), fastForward: utils.getElement.call(this, this.config.selectors.buttons.fastForward),
mute: utils.getElement.call(this, this.config.selectors.buttons.mute), mute: utils.getElement.call(this, this.config.selectors.buttons.mute),
pip: utils.getElement.call(this, this.config.selectors.buttons.pip), pip: utils.getElement.call(this, this.config.selectors.buttons.pip),
airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay), airplay: utils.getElement.call(this, this.config.selectors.buttons.airplay),
@ -565,9 +534,9 @@ const utils = {
}, },
// Toggle event listener // Toggle event listener
toggleListener(elements, event, callback, toggle, passive, capture) { toggleListener(elements, event, callback, toggle = false, passive = true, capture = false) {
// Bail if no elemetns, event, or callback // Bail if no elemetns, event, or callback
if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) { if (utils.is.empty(elements) || utils.is.empty(event) || !utils.is.function(callback)) {
return; return;
} }
@ -587,16 +556,16 @@ const utils = {
const events = event.split(' '); const events = event.split(' ');
// Build options // Build options
// Default to just capture boolean // Default to just the capture boolean for browsers with no passive listener support
let options = utils.is.boolean(capture) ? capture : false; let options = capture;
// If passive events listeners are supported // If passive events listeners are supported
if (support.passiveListeners) { if (support.passiveListeners) {
options = { options = {
// Whether the listener can be passive (i.e. default never prevented) // Whether the listener can be passive (i.e. default never prevented)
passive: utils.is.boolean(passive) ? passive : true, passive,
// Whether the listener is a capturing listener or not // Whether the listener is a capturing listener or not
capture: utils.is.boolean(capture) ? capture : false, capture,
}; };
} }
@ -607,25 +576,25 @@ const utils = {
}, },
// Bind event handler // Bind event handler
on(element, events, callback, passive, capture) { on(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, true, passive, capture); utils.toggleListener(element, events, callback, true, passive, capture);
}, },
// Unbind event handler // Unbind event handler
off(element, events, callback, passive, capture) { off(element, events = '', callback, passive = true, capture = false) {
utils.toggleListener(element, events, callback, false, passive, capture); utils.toggleListener(element, events, callback, false, passive, capture);
}, },
// Trigger event // Trigger event
dispatchEvent(element, type, bubbles, detail) { dispatchEvent(element, type = '', bubbles = false, detail = {}) {
// Bail if no element // Bail if no element
if (!element || !type) { if (!utils.is.element(element) || utils.is.empty(type)) {
return; return;
} }
// Create and dispatch the event // Create and dispatch the event
const event = new CustomEvent(type, { const event = new CustomEvent(type, {
bubbles: utils.is.boolean(bubbles) ? bubbles : false, bubbles,
detail: Object.assign({}, detail, { detail: Object.assign({}, detail, {
plyr: utils.is.plyr(this) ? this : null, plyr: utils.is.plyr(this) ? this : null,
}), }),
@ -638,6 +607,12 @@ const utils = {
// Toggle aria-pressed state on a toggle button // Toggle aria-pressed state on a toggle button
// http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles // http://www.ssbbartgroup.com/blog/how-not-to-misuse-aria-states-properties-and-roles
toggleState(element, input) { toggleState(element, input) {
// If multiple elements passed
if (utils.is.array(element) || utils.is.nodeList(element)) {
Array.from(element).forEach(target => utils.toggleState(target, input));
return;
}
// Bail if no target // Bail if no target
if (!utils.is.element(element)) { if (!utils.is.element(element)) {
return; return;
@ -656,6 +631,7 @@ const utils = {
if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) { if (current === 0 || max === 0 || Number.isNaN(current) || Number.isNaN(max)) {
return 0; return 0;
} }
return (current / max * 100).toFixed(2); return (current / max * 100).toFixed(2);
}, },
@ -696,6 +672,44 @@ const utils = {
return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`; return `${inverted ? '-' : ''}${hours}${format(mins)}:${format(secs)}`;
}, },
// Replace all occurances of a string in a string
replaceAll(input = '', find = '', replace = '') {
return input.replace(new RegExp(find.toString().replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1'), 'g'), replace.toString());
},
// Convert to title case
toTitleCase(input = '') {
return input.toString().replace(/\w\S*/g, text => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase());
},
// Convert string to pascalCase
toPascalCase(input = '') {
let string = input.toString();
// Convert kebab case
string = utils.replaceAll(string, '-', ' ');
// Convert snake case
string = utils.replaceAll(string, '_', ' ');
// Convert to title case
string = utils.toTitleCase(string);
// Convert to pascal case
return utils.replaceAll(string, ' ', '');
},
// Convert string to pascalCase
toCamelCase(input = '') {
let string = input.toString();
// Convert to pascal case
string = utils.toPascalCase(string);
// Convert first character to lowercase
return string.charAt(0).toLowerCase() + string.slice(1);
},
// Deep extend destination object with N more objects // Deep extend destination object with N more objects
extend(target = {}, ...sources) { extend(target = {}, ...sources) {
if (!sources.length) { if (!sources.length) {
@ -723,6 +737,24 @@ const utils = {
return utils.extend(target, ...sources); return utils.extend(target, ...sources);
}, },
// Remove duplicates in an array
dedupe(array) {
if (!utils.is.array(array)) {
return array;
}
return array.filter((item, index) => array.indexOf(item) === index);
},
// Get the closest value in an array
closest(array, value) {
if (!utils.is.array(array) || !array.length) {
return null;
}
return array.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev);
},
// Get the provider for a given URL // Get the provider for a given URL
getProviderByUrl(url) { getProviderByUrl(url) {
// YouTube // YouTube
@ -839,7 +871,7 @@ const utils = {
// Force repaint of element // Force repaint of element
repaint(element) { repaint(element) {
window.setTimeout(() => { setTimeout(() => {
utils.toggleHidden(element, true); utils.toggleHidden(element, true);
element.offsetHeight; // eslint-disable-line element.offsetHeight; // eslint-disable-line
utils.toggleHidden(element, false); utils.toggleHidden(element, false);

View File

@ -8,6 +8,7 @@
direction: ltr; direction: ltr;
font-family: $plyr-font-family; font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: $plyr-font-weight-regular; font-weight: $plyr-font-weight-regular;
line-height: $plyr-line-height; line-height: $plyr-line-height;
max-width: 100%; max-width: 100%;

View File

@ -6,7 +6,7 @@
.plyr__video-embed { .plyr__video-embed {
// Default to 16:9 ratio but this is set by JavaScript based on config // Default to 16:9 ratio but this is set by JavaScript based on config
$padding: ((100 / 16) * 9); $padding: ((100 / 16) * 9);
$height: 200; $height: 240;
$offset: to-percentage(($height - $padding) / ($height / 50)); $offset: to-percentage(($height - $padding) / ($height / 50));
height: 0; height: 0;

View File

@ -84,7 +84,6 @@
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
transition: border-color 0.2s ease;
} }
&--forward { &--forward {
@ -108,7 +107,6 @@
margin-bottom: floor($plyr-control-padding / 2); margin-bottom: floor($plyr-control-padding / 2);
padding-left: ceil($plyr-control-padding * 4); padding-left: ceil($plyr-control-padding * 4);
position: relative; position: relative;
width: calc(100% - #{$horizontal-padding}); width: calc(100% - #{$horizontal-padding});
&::after { &::after {

View File

@ -1,16 +1,27 @@
// ========================================================================== // ==========================================================================
// Advertisments // Advertisements
// ========================================================================== // ==========================================================================
.plyr__ads { .plyr__ads {
border-radius: inherit;
bottom: 0; bottom: 0;
cursor: pointer; cursor: pointer;
left: 0; left: 0;
overflow: hidden;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
z-index: 3; // Above the controls z-index: -1; // Hide it by default
// Make sure the inner container is big enough for the ad creative.
> div,
> div iframe {
height: 100%;
position: absolute;
width: 100%;
}
// The countdown label
&::after { &::after {
background: rgba($plyr-color-gunmetal, 0.8); background: rgba($plyr-color-gunmetal, 0.8);
border-radius: 2px; border-radius: 2px;

1789
yarn.lock

File diff suppressed because it is too large Load Diff