Skip to content

Commit c77e1f8

Browse files
authored
feat: source switcher (#904)
* feat: source switcher
1 parent 755e72b commit c77e1f8

19 files changed

+442
-31
lines changed

docs/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ <h3 class="mt-4">Some code examples:</h3>
7676
<li><a href="./seek-thumbs.html">Seek Thumbnails</a></li>
7777
<li><a href="./share-plugin.html">Share &amp; Download</a></li>
7878
<li><a href="./shoppable.html">Shoppable Videos</a></li>
79+
<li><a href="./source-switcher.html">Source switcher</a></li>
7980
<li><a href="./subtitles-and-captions.html">Subtitles & Captions</a></li>
8081
<li><a href="./transformations.html">Video Transformations</a></li>
8182
<li><a href="./vast-vpaid.html">VAST & VPAID Support</a></li>

docs/source-switcher.html

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Cloudinary Video Player</title>
6+
<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">
7+
8+
<!-- Bootstrap -->
9+
<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">
10+
11+
<!-- highlight.js -->
12+
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/solarized-light.min.css">
13+
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
14+
<script>hljs.initHighlightingOnLoad();</script>
15+
16+
<!--
17+
We're loading scripts & style dynamically for development/testing.
18+
Real-world usage would look like this:
19+
20+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cloudinary-video-player/dist/cld-video-player.min.css">
21+
<script src="https://cdn.jsdelivr.net/npm/cloudinary-video-player/dist/cld-video-player.min.js"></script>
22+
-->
23+
24+
<script type="text/javascript" src="./scripts.js"></script>
25+
26+
<script type="text/javascript">
27+
window.addEventListener('load', function() {
28+
cloudinary.videoPlayer('player-multiple-sources', {
29+
cloudName: 'demo',
30+
videoSources: [
31+
{
32+
publicId: 'snow_horses'
33+
},
34+
{
35+
publicId: 'dirt_bike',
36+
textTracks: {
37+
captions: {
38+
label: 'Original',
39+
default: true,
40+
},
41+
subtitles: [
42+
{
43+
label: 'English',
44+
language: 'en-US',
45+
},
46+
{
47+
label: 'Polish',
48+
language: 'pl-PL',
49+
},
50+
]
51+
},
52+
},
53+
{
54+
publicId: 'marmots',
55+
label: 'Custom video name',
56+
download: true,
57+
},
58+
],
59+
});
60+
}, false);
61+
</script>
62+
63+
</head>
64+
<body>
65+
<div class="container p-4 col-12 col-md-9 col-xl-6">
66+
<nav class="nav mb-2">
67+
<a href="./index.html">&#60;&#60; Back to examples index</a>
68+
</nav>
69+
70+
<h1>Cloudinary Video Player</h1>
71+
72+
<h3 class="mb-4">Source switcher</h3>
73+
74+
<div class="d-flex flex-column justify-content-start align-items-start">
75+
<video
76+
playsinline
77+
id="player-multiple-sources"
78+
controls
79+
autoplay
80+
class="cld-video-player cld-fluid"
81+
></video>
82+
</div>
83+
84+
<p class="mt-4">
85+
<a href="https://cloudinary.com/documentation/cloudinary_video_player">Full documentation</a>
86+
</p>
87+
88+
<h3 class="mt-4">Example Code:</h3>
89+
90+
<pre>
91+
<code class="language-html">
92+
93+
&lt;video
94+
id="player-multiple-sources"
95+
controls
96+
autoplay
97+
class="cld-video-player"
98+
width="500"&gt;
99+
&lt;/video&gt;
100+
101+
</code>
102+
</pre>
103+
104+
<pre>
105+
<code class="language-javascript">
106+
cloudinary.videoPlayer('player-multiple-sources', {
107+
cloudName: 'demo',
108+
videoSources: [
109+
{
110+
publicId: 'snow_horses'
111+
},
112+
{
113+
publicId: 'dirt_bike',
114+
textTracks: {
115+
captions: {
116+
label: 'Original',
117+
default: true,
118+
},
119+
subtitles: [
120+
{
121+
label: 'English',
122+
language: 'en-US',
123+
},
124+
{
125+
label: 'Polish',
126+
language: 'pl-PL',
127+
},
128+
]
129+
},
130+
},
131+
{
132+
publicId: 'marmots',
133+
label: 'Custom video name',
134+
download: true,
135+
},
136+
],
137+
});
138+
</code>
139+
</pre>
140+
</div>
141+
142+
</body>
143+
</html>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
},
6767
{
6868
"path": "./lib/all.js",
69-
"maxSize": "320kb"
69+
"maxSize": "325kb"
7070
}
7171
]
7272
},
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 4 additions & 0 deletions
Loading

src/components/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import JumpBackButton from './jumpButtons/jump-10-minus';
33
import LogoButton from './logoButton/logo-button';
44
import ProgressControlEventsBlocker from './progress-control-events-blocker/progress-control-events-blocker';
55
import TitleBar from './title-bar/title-bar';
6+
import SourceSwitcherButton from './source-switcher-button/source-switcher-button';
67

78
export {
89
JumpForwardButton,
910
JumpBackButton,
1011
LogoButton,
1112
ProgressControlEventsBlocker,
12-
TitleBar
13+
TitleBar,
14+
SourceSwitcherButton
1315
};
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import videojs from 'video.js';
2+
import './source-switcher-button.scss';
3+
4+
const MenuButton = videojs.getComponent('MenuButton');
5+
const MenuItem = videojs.getComponent('MenuItem');
6+
7+
class SourceSwitcherButton extends MenuButton {
8+
constructor(player, options = {}) {
9+
super(player, options);
10+
11+
this.controlText(options.tooltip || 'Sources');
12+
this._emptyLabel = options.emptyLabel || 'No sources';
13+
14+
this._items = options.items || [];
15+
this._selectedIndex = Number.isInteger(options.defaultIndex) ? options.defaultIndex : undefined;
16+
17+
const onSelected = typeof options.onSelected === 'function' ? options.onSelected : null;
18+
19+
this._onSelected = onSelected || null;
20+
this._setEnabled(Array.isArray(this._items) && this._items.length > 0);
21+
}
22+
23+
buildCSSClass() {
24+
const empty = !Array.isArray(this._items) || this._items.length === 0;
25+
return `vjs-source-switcher-button${empty ? ' vjs-source-switcher-disabled' : ''} ${super.buildCSSClass()}`;
26+
}
27+
28+
createItems() {
29+
if (!Array.isArray(this._items) || this._items.length === 0) {
30+
const empty = new MenuItem(this.player_, {
31+
label: this._emptyLabel || 'No sources',
32+
selectable: false
33+
});
34+
empty.addClass('vjs-source-switcher-empty');
35+
empty.disable();
36+
return [empty];
37+
}
38+
39+
return this._items.map(({ label, value }, index) => {
40+
const item = new MenuItem(this.player_, {
41+
label,
42+
selectable: true,
43+
selected: index === this._selectedIndex
44+
});
45+
item.value = value;
46+
item._ssIndex = index;
47+
48+
item.on('click', () => {
49+
this.menu.children().forEach((child) => {
50+
if (child instanceof MenuItem) {
51+
child.selected(child._ssIndex === index);
52+
}
53+
});
54+
55+
this._selectedIndex = index;
56+
const payload = { index, value, label };
57+
if (this._onSelected) this._onSelected(payload, this.player_);
58+
this.player_.trigger('sourceswitcher:change', payload);
59+
});
60+
61+
return item;
62+
});
63+
}
64+
65+
setItems(items) {
66+
this._items = items;
67+
this._selectedIndex = this._items.length ? 0 : undefined;
68+
69+
this._setEnabled(this._items.length > 0);
70+
this._rebuildMenu();
71+
}
72+
73+
setSelected(index) {
74+
if (
75+
!Array.isArray(this._items) ||
76+
index == null ||
77+
index < 0 ||
78+
index >= this._items.length
79+
) return;
80+
81+
this._selectedIndex = index;
82+
83+
// reflect in UI if menu exists
84+
if (this.menu) {
85+
this.menu.children().forEach((child) => {
86+
if (child instanceof MenuItem) {
87+
child.selected(child._ssIndex === index);
88+
}
89+
});
90+
}
91+
92+
const { label, value } = this._items[index];
93+
const payload = { index, value, label };
94+
if (this._onSelected) this._onSelected(payload, this.player_);
95+
this.player_.trigger('sourceswitcher:change', payload);
96+
}
97+
98+
setOnSelected(fn) {
99+
this._onSelected = typeof fn === 'function' ? fn : null;
100+
}
101+
102+
_rebuildMenu() {
103+
if (!this.menu) return;
104+
this.menu.children().slice().forEach((c) => this.menu.removeChild(c));
105+
this.createItems().forEach((i) => this.menu.addItem(i));
106+
107+
// Toggle disabled class based on emptiness
108+
const el = this.el();
109+
if (el) {
110+
const empty = this._items.length === 0;
111+
el.classList.toggle('vjs-source-switcher-disabled', empty);
112+
el.setAttribute('aria-disabled', String(empty));
113+
}
114+
}
115+
116+
_setEnabled(enabled) {
117+
const el = this.el();
118+
if (!el) return;
119+
el.classList.toggle('vjs-source-switcher-disabled', !enabled);
120+
el.setAttribute('aria-disabled', String(!enabled));
121+
}
122+
}
123+
124+
videojs.registerComponent('sourceSwitcherButton', SourceSwitcherButton);
125+
export default SourceSwitcherButton;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.vjs-control-bar .vjs-menu-button.vjs-source-switcher-button {
2+
background-image: url("../../assets/icons/source_switcher_icon_for_black_bg.svg");
3+
background-size: 25px;
4+
background-position: center;
5+
background-repeat: no-repeat;
6+
color: inherit;
7+
opacity: 0.9;
8+
9+
.cld-video-player-skin-light & {
10+
background-image: url("../../assets/icons/source_switcher_icon_for_white_bg.svg");
11+
}
12+
13+
&:hover {
14+
cursor: pointer;
15+
opacity: 1;
16+
}
17+
}

0 commit comments

Comments
 (0)