-
Notifications
You must be signed in to change notification settings - Fork 25
feat: source switcher #904
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a777c0b
fc10b97
ae3b9ff
2870912
e668c78
a16feae
58b4533
14ca155
4be8406
bed17c9
4468c21
3f9c560
b254d5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <title>Cloudinary Video Player</title> | ||
| <link href="https://res.cloudinary.com/cloudinary-marketing/image/upload/f_auto,q_auto/c_scale,w_32/v1597183771/creative_staging/cloudinary_internal/Website/Brand%20Updates/Favicon/cloudinary_web_favicon_192x192.png" rel="icon" type="image/png"> | ||
|
|
||
| <!-- Bootstrap --> | ||
| <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> | ||
|
|
||
| <!-- highlight.js --> | ||
| <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/solarized-light.min.css"> | ||
| <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script> | ||
| <script>hljs.initHighlightingOnLoad();</script> | ||
|
|
||
| <!-- | ||
| We're loading scripts & style dynamically for development/testing. | ||
| Real-world usage would look like this: | ||
|
|
||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cloudinary-video-player/dist/cld-video-player.min.css"> | ||
| <script src="https://cdn.jsdelivr.net/npm/cloudinary-video-player/dist/cld-video-player.min.js"></script> | ||
| --> | ||
|
|
||
| <script type="text/javascript" src="./scripts.js"></script> | ||
|
|
||
| <script type="text/javascript"> | ||
| window.addEventListener('load', function() { | ||
| cloudinary.videoPlayer('player-multiple-sources', { | ||
| cloudName: 'demo', | ||
| videoSources: [ | ||
| { | ||
| publicId: 'snow_horses' | ||
| }, | ||
| { | ||
| publicId: 'dirt_bike', | ||
| textTracks: { | ||
| captions: { | ||
| label: 'Original', | ||
| default: true, | ||
| }, | ||
| subtitles: [ | ||
| { | ||
| label: 'English', | ||
| language: 'en-US', | ||
| }, | ||
| { | ||
| label: 'Polish', | ||
| language: 'pl-PL', | ||
| }, | ||
| ] | ||
| }, | ||
| }, | ||
| { | ||
| publicId: 'marmots', | ||
| label: 'Custom video name', | ||
| download: true, | ||
| }, | ||
| ], | ||
| }); | ||
| }, false); | ||
| </script> | ||
|
|
||
| </head> | ||
| <body> | ||
| <div class="container p-4 col-12 col-md-9 col-xl-6"> | ||
| <nav class="nav mb-2"> | ||
| <a href="./index.html"><< Back to examples index</a> | ||
| </nav> | ||
|
|
||
| <h1>Cloudinary Video Player</h1> | ||
|
|
||
| <h3 class="mb-4">Source switcher</h3> | ||
|
|
||
| <div class="d-flex flex-column justify-content-start align-items-start"> | ||
| <video | ||
| playsinline | ||
| id="player-multiple-sources" | ||
| controls | ||
| autoplay | ||
| class="cld-video-player cld-fluid" | ||
| ></video> | ||
| </div> | ||
|
|
||
| <p class="mt-4"> | ||
| <a href="https://cloudinary.com/documentation/cloudinary_video_player">Full documentation</a> | ||
| </p> | ||
|
|
||
| <h3 class="mt-4">Example Code:</h3> | ||
|
|
||
| <pre> | ||
| <code class="language-html"> | ||
|
|
||
| <video | ||
| id="player-multiple-sources" | ||
| controls | ||
| autoplay | ||
| class="cld-video-player" | ||
| width="500"> | ||
| </video> | ||
|
|
||
| </code> | ||
| </pre> | ||
|
|
||
| <pre> | ||
| <code class="language-javascript"> | ||
| cloudinary.videoPlayer('player-multiple-sources', { | ||
| cloudName: 'demo', | ||
| videoSources: [ | ||
| { | ||
| publicId: 'snow_horses' | ||
| }, | ||
| { | ||
| publicId: 'dirt_bike', | ||
| textTracks: { | ||
| captions: { | ||
| label: 'Original', | ||
| default: true, | ||
| }, | ||
| subtitles: [ | ||
| { | ||
| label: 'English', | ||
| language: 'en-US', | ||
| }, | ||
| { | ||
| label: 'Polish', | ||
| language: 'pl-PL', | ||
| }, | ||
| ] | ||
| }, | ||
| }, | ||
| { | ||
| publicId: 'marmots', | ||
| label: 'Custom video name', | ||
| download: true, | ||
| }, | ||
| ], | ||
| }); | ||
| </code> | ||
| </pre> | ||
| </div> | ||
|
|
||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -66,7 +66,7 @@ | |
| }, | ||
| { | ||
| "path": "./lib/all.js", | ||
| "maxSize": "320kb" | ||
| "maxSize": "325kb" | ||
| } | ||
| ] | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import videojs from 'video.js'; | ||
| import './source-switcher-button.scss'; | ||
|
|
||
| const MenuButton = videojs.getComponent('MenuButton'); | ||
| const MenuItem = videojs.getComponent('MenuItem'); | ||
|
|
||
| class SourceSwitcherButton extends MenuButton { | ||
| constructor(player, options = {}) { | ||
| super(player, options); | ||
|
|
||
| this.controlText(options.tooltip || 'Sources'); | ||
| this._emptyLabel = options.emptyLabel || 'No sources'; | ||
|
|
||
| this._items = options.items || []; | ||
| this._selectedIndex = Number.isInteger(options.defaultIndex) ? options.defaultIndex : undefined; | ||
|
|
||
| const onSelected = typeof options.onSelected === 'function' ? options.onSelected : null; | ||
|
|
||
| this._onSelected = onSelected || null; | ||
| this._setEnabled(Array.isArray(this._items) && this._items.length > 0); | ||
| } | ||
|
|
||
| buildCSSClass() { | ||
| const empty = !Array.isArray(this._items) || this._items.length === 0; | ||
| return `vjs-source-switcher-button${empty ? ' vjs-source-switcher-disabled' : ''} ${super.buildCSSClass()}`; | ||
| } | ||
|
|
||
| createItems() { | ||
| if (!Array.isArray(this._items) || this._items.length === 0) { | ||
| const empty = new MenuItem(this.player_, { | ||
| label: this._emptyLabel || 'No sources', | ||
| selectable: false | ||
| }); | ||
| empty.addClass('vjs-source-switcher-empty'); | ||
| empty.disable(); | ||
| return [empty]; | ||
|
Comment on lines
+30
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we have an "empty" state?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. initially this component is mounted without any source, just to avoid complex implementation and just after that it gets list of sources provided as player params (so its the part of player, it should never be visible but just in case empty state is added rather than nothing) |
||
| } | ||
|
|
||
| return this._items.map(({ label, value }, index) => { | ||
| const item = new MenuItem(this.player_, { | ||
| label, | ||
| selectable: true, | ||
| selected: index === this._selectedIndex | ||
| }); | ||
| item.value = value; | ||
| item._ssIndex = index; | ||
|
|
||
| item.on('click', () => { | ||
| this.menu.children().forEach((child) => { | ||
| if (child instanceof MenuItem) { | ||
| child.selected(child._ssIndex === index); | ||
| } | ||
| }); | ||
|
|
||
| this._selectedIndex = index; | ||
| const payload = { index, value, label }; | ||
| if (this._onSelected) this._onSelected(payload, this.player_); | ||
| this.player_.trigger('sourceswitcher:change', payload); | ||
| }); | ||
|
|
||
| return item; | ||
| }); | ||
| } | ||
|
|
||
| setItems(items) { | ||
| this._items = items; | ||
| this._selectedIndex = this._items.length ? 0 : undefined; | ||
|
|
||
| this._setEnabled(this._items.length > 0); | ||
| this._rebuildMenu(); | ||
| } | ||
|
|
||
| setSelected(index) { | ||
| if ( | ||
| !Array.isArray(this._items) || | ||
| index == null || | ||
| index < 0 || | ||
| index >= this._items.length | ||
| ) return; | ||
|
|
||
| this._selectedIndex = index; | ||
|
|
||
| // reflect in UI if menu exists | ||
| if (this.menu) { | ||
| this.menu.children().forEach((child) => { | ||
| if (child instanceof MenuItem) { | ||
| child.selected(child._ssIndex === index); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| const { label, value } = this._items[index]; | ||
| const payload = { index, value, label }; | ||
| if (this._onSelected) this._onSelected(payload, this.player_); | ||
| this.player_.trigger('sourceswitcher:change', payload); | ||
| } | ||
|
|
||
| setOnSelected(fn) { | ||
| this._onSelected = typeof fn === 'function' ? fn : null; | ||
| } | ||
|
|
||
| _rebuildMenu() { | ||
| if (!this.menu) return; | ||
| this.menu.children().slice().forEach((c) => this.menu.removeChild(c)); | ||
| this.createItems().forEach((i) => this.menu.addItem(i)); | ||
|
|
||
| // Toggle disabled class based on emptiness | ||
| const el = this.el(); | ||
| if (el) { | ||
| const empty = this._items.length === 0; | ||
| el.classList.toggle('vjs-source-switcher-disabled', empty); | ||
| el.setAttribute('aria-disabled', String(empty)); | ||
| } | ||
| } | ||
|
|
||
| _setEnabled(enabled) { | ||
| const el = this.el(); | ||
| if (!el) return; | ||
| el.classList.toggle('vjs-source-switcher-disabled', !enabled); | ||
| el.setAttribute('aria-disabled', String(!enabled)); | ||
| } | ||
| } | ||
|
|
||
| videojs.registerComponent('sourceSwitcherButton', SourceSwitcherButton); | ||
| export default SourceSwitcherButton; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| .vjs-control-bar .vjs-menu-button.vjs-source-switcher-button { | ||
| background-image: url("../../assets/icons/source_switcher_icon_for_black_bg.svg"); | ||
| background-size: 25px; | ||
| background-position: center; | ||
| background-repeat: no-repeat; | ||
| color: inherit; | ||
| opacity: 0.9; | ||
|
|
||
| .cld-video-player-skin-light & { | ||
| background-image: url("../../assets/icons/source_switcher_icon_for_white_bg.svg"); | ||
| } | ||
|
|
||
| &:hover { | ||
| cursor: pointer; | ||
| opacity: 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also add an ESM example page? I'm ok with not having everything doubled, just that this is what we did with other examples
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we dont have yet E2E test for this page (just mocked for now) but I believe ESM should be added only if there is a reason for that (otherwise we are doubling everything), for now I will keep just one page but we need to decide about some rules here (will schedule meeting for us after gathering)