aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets')
-rw-r--r--client/src/assets/images/global/exit-fullscreen.svg16
-rw-r--r--client/src/assets/images/global/fullscreen.svg17
-rw-r--r--client/src/assets/images/global/npm.svg6
-rw-r--r--client/src/assets/images/global/video-lang.svg15
-rw-r--r--client/src/assets/images/menu/about.svg11
-rw-r--r--client/src/assets/images/menu/administration.svg10
-rw-r--r--client/src/assets/images/menu/eye-closed.svg17
-rw-r--r--client/src/assets/images/menu/eye.svg15
-rw-r--r--client/src/assets/images/menu/language.pngbin10937 -> 0 bytes
-rw-r--r--client/src/assets/images/menu/language.svg10
-rw-r--r--client/src/assets/images/menu/moonsun.svg1
-rw-r--r--client/src/assets/images/menu/p2p.svg11
-rw-r--r--client/src/assets/player/bezels/bezels-plugin.ts86
-rw-r--r--client/src/assets/player/bezels/pause-bezel.ts72
-rw-r--r--client/src/assets/player/p2p-media-loader/hls-plugin.ts628
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts36
-rw-r--r--client/src/assets/player/peertube-player-manager.ts73
-rw-r--r--client/src/assets/player/peertube-plugin.ts22
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts82
-rw-r--r--client/src/assets/player/upnext/end-card.ts155
-rw-r--r--client/src/assets/player/upnext/upnext-plugin.ts155
-rw-r--r--client/src/assets/player/utils.ts3
-rw-r--r--client/src/assets/player/videojs-components/next-video-button.ts29
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts48
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts20
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts17
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts32
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts36
-rw-r--r--client/src/assets/player/videojs-components/settings-dialog.ts37
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts191
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts159
-rw-r--r--client/src/assets/player/videojs-components/settings-panel-child.ts22
-rw-r--r--client/src/assets/player/videojs-components/settings-panel.ts22
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts20
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts40
35 files changed, 1465 insertions, 649 deletions
diff --git a/client/src/assets/images/global/exit-fullscreen.svg b/client/src/assets/images/global/exit-fullscreen.svg
new file mode 100644
index 000000000..ba01f583c
--- /dev/null
+++ b/client/src/assets/images/global/exit-fullscreen.svg
@@ -0,0 +1,16 @@
1<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <defs/>
3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
4 <g id="Artboard-4" transform="translate(-400.000000, -1046.000000)" stroke="#333333" stroke-width="2">
5 <g id="Extras" transform="translate(48.000000, 1046.000000)">
6 <g id="exit-fullscreen" transform="translate(352.000000, 0.000000)">
7 <rect id="Rectangle-433" x="6" y="8" width="12" height="8"/>
8 <polyline id="Path-42" stroke-linecap="round" transform="translate(21.500000, 5.500000) scale(-1, -1) translate(-21.500000, -5.500000) " points="23 7 23 4 20 4"/>
9 <polyline id="Path-42" stroke-linecap="round" transform="translate(2.500000, 18.500000) scale(-1, -1) translate(-2.500000, -18.500000) " points="4 20 1 20 1 17"/>
10 <polyline id="Path-42" stroke-linecap="round" transform="translate(21.500000, 18.500000) scale(-1, 1) translate(-21.500000, -18.500000) " points="23 20 23 17 20 17"/>
11 <polyline id="Path-42" stroke-linecap="round" transform="translate(2.500000, 5.500000) scale(-1, 1) translate(-2.500000, -5.500000) " points="4 7 1 7 1 4"/>
12 </g>
13 </g>
14 </g>
15 </g>
16</svg>
diff --git a/client/src/assets/images/global/fullscreen.svg b/client/src/assets/images/global/fullscreen.svg
new file mode 100644
index 000000000..4a9d67864
--- /dev/null
+++ b/client/src/assets/images/global/fullscreen.svg
@@ -0,0 +1,17 @@
1<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
3 <title>fullscreen</title>
4 <desc>Created with Sketch.</desc>
5 <defs/>
6 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
7 <g id="Artboard-4" transform="translate(-576.000000, -159.000000)" stroke="#333333" stroke-width="2">
8 <g id="33" transform="translate(576.000000, 159.000000)">
9 <rect id="Rectangle-433" x="1" y="4" width="22" height="16" rx="1"/>
10 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" points="20 10 20 7 17 7"/>
11 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" points="7 17 4 17 4 14"/>
12 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" transform="translate(18.500000, 15.500000) scale(1, -1) translate(-18.500000, -15.500000) " points="20 17 20 14 17 14"/>
13 <polyline id="Path-42" stroke-linecap="round" stroke-linejoin="round" transform="translate(5.500000, 8.500000) scale(1, -1) translate(-5.500000, -8.500000) " points="7 10 4 10 4 7"/>
14 </g>
15 </g>
16 </g>
17</svg>
diff --git a/client/src/assets/images/global/npm.svg b/client/src/assets/images/global/npm.svg
new file mode 100644
index 000000000..ec8f41243
--- /dev/null
+++ b/client/src/assets/images/global/npm.svg
@@ -0,0 +1,6 @@
1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);">
2 <path fill="#00000" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/>
3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/>
4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/>
5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/>
6</svg>
diff --git a/client/src/assets/images/global/video-lang.svg b/client/src/assets/images/global/video-lang.svg
new file mode 100644
index 000000000..8d7b6a016
--- /dev/null
+++ b/client/src/assets/images/global/video-lang.svg
@@ -0,0 +1,15 @@
1<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" style="transform:scale(1.1);">
2 <g class="layer" style="transform: scale(.9);">
3 <g fill="none" fill-rule="evenodd">
4 <g transform="translate(-884.000000, -863.000000)">
5 <g transform="translate(884.000000, 863.000000)">
6 <path d="m22.78031,7.45167c0,0 -0.21495,-1.56763 -0.87461,-2.25797c-0.83658,-0.90605 -1.7743,-0.91055 -2.20433,-0.96357c-3.07858,-0.23013 -7.69661,-0.23013 -7.69661,-0.23013l-0.00956,0c0,0 -4.61792,0 -7.69661,0.23013c-0.43005,0.05302 -1.36743,0.05752 -2.20431,0.96357c-0.65962,0.69034 -0.87427,2.25797 -0.87427,2.25797c0,0 -0.22001,1.84092 -0.22001,3.68182l0,1.72585c0,1.84087 0.22001,3.68177 0.22001,3.68177c0,0 0.21465,1.56766 0.87427,2.25799c0.83688,0.90608 1.93618,0.87741 2.42581,0.97238c1.76002,0.17451 7.47991,0.22852 7.47991,0.22852c0,0 4.62279,-0.00719 7.70137,-0.2373c0.43003,-0.05305 1.36775,-0.05752 2.20433,-0.9636c0.65966,-0.69033 0.87461,-2.25799 0.87461,-2.25799c0,0 0.21969,-1.8409 0.21969,-3.68177l0,-1.72585c0,-1.8409 -0.21969,-3.68182 -0.21969,-3.68182l0,0z" fill="#ffffff" stroke="#000000" stroke-width="2"/>
7 </g>
8 </g>
9 </g>
10 <g>
11 <path d="m9.639451,16.289861a0.758829,0.758829 0 0 1 -0.537251,-1.296079l3.226539,-3.226539l-2.689289,0a0.758829,0.758829 0 0 1 0,-1.517657l4.521101,0a0.758829,0.758829 0 0 1 0.537251,1.296079l-4.522619,4.521101a0.758829,0.758829 0 0 1 -0.535733,0.223096z" fill="#000000" stroke="#000000" stroke-width="0"/>
12 <path d="m13.029897,9.507451l-2.208191,0a0.758829,0.758829 0 1 1 0,-1.517657l2.21578,0a0.758829,0.758829 0 0 1 0,1.517657l-0.007588,0z" fill="#000000" stroke="#000000" stroke-width="0"/>
13 </g>
14 </g>
15</svg>
diff --git a/client/src/assets/images/menu/about.svg b/client/src/assets/images/menu/about.svg
deleted file mode 100644
index bea602aac..000000000
--- a/client/src/assets/images/menu/about.svg
+++ /dev/null
@@ -1,11 +0,0 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
4 <g id="Artboard-4" transform="translate(-400.000000, -247.000000)">
5 <g id="69" transform="translate(400.000000, 247.000000)">
6 <circle id="Oval-7" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle>
7 <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#000000"></path>
8 </g>
9 </g>
10 </g>
11</svg>
diff --git a/client/src/assets/images/menu/administration.svg b/client/src/assets/images/menu/administration.svg
deleted file mode 100644
index 0dceda082..000000000
--- a/client/src/assets/images/menu/administration.svg
+++ /dev/null
@@ -1,10 +0,0 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
4 <g id="Artboard-4" transform="translate(-444.000000, -247.000000)" fill="#000000">
5 <g id="70" transform="translate(444.000000, 247.000000)">
6 <path d="M8.82929429,17 L20.0066023,17 C20.5552407,17 21,17.4438648 21,18 C21,18.5522847 20.5550537,19 20.0066023,19 L8.82929429,19 C8.41745788,20.1651924 7.30621883,21 6,21 C4.34314575,21 3,19.6568542 3,18 C3,16.3431458 4.34314575,15 6,15 C7.30621883,15 8.41745788,15.8348076 8.82929429,17 Z M9.17070571,13 L3.99339768,13 C3.44475929,13 3,12.5561352 3,12 C3,11.4477153 3.44494629,11 3.99339768,11 L9.17070571,11 C9.58254212,9.83480763 10.6937812,9 12,9 C13.3062188,9 14.4174579,9.83480763 14.8292943,11 L20.0066023,11 C20.5552407,11 21,11.4438648 21,12 C21,12.5522847 20.5550537,13 20.0066023,13 L14.8292943,13 C14.4174579,14.1651924 13.3062188,15 12,15 C10.6937812,15 9.58254212,14.1651924 9.17070571,13 Z M15.1659641,6.98648118 C15.1124525,6.99537358 15.05751,7 15.0014977,7 L3.99850233,7 C3.44704472,7 3,6.55613518 3,6 C3,5.44771525 3.44748943,5 3.99850233,5 L15.0014977,5 C15.0575314,5 15.1124871,5.00458274 15.1660053,5.01340035 C15.5740343,3.84121344 16.6887792,3 18,3 C19.6568542,3 21,4.34314575 21,6 C21,7.65685425 19.6568542,9 18,9 C16.688735,9 15.5739592,8.15872988 15.1659641,6.98648118 Z M18,7 C18.5522847,7 19,6.55228475 19,6 C19,5.44771525 18.5522847,5 18,5 C17.4477153,5 17,5.44771525 17,6 C17,6.55228475 17.4477153,7 18,7 Z M12,13 C12.5522847,13 13,12.5522847 13,12 C13,11.4477153 12.5522847,11 12,11 C11.4477153,11 11,11.4477153 11,12 C11,12.5522847 11.4477153,13 12,13 Z M6,19 C6.55228475,19 7,18.5522847 7,18 C7,17.4477153 6.55228475,17 6,17 C5.44771525,17 5,17.4477153 5,18 C5,18.5522847 5.44771525,19 6,19 Z" id="Combined-Shape"></path>
7 </g>
8 </g>
9 </g>
10</svg>
diff --git a/client/src/assets/images/menu/eye-closed.svg b/client/src/assets/images/menu/eye-closed.svg
new file mode 100644
index 000000000..5c441e719
--- /dev/null
+++ b/client/src/assets/images/menu/eye-closed.svg
@@ -0,0 +1,17 @@
1<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <defs/>
3 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
4 <g transform="translate(-796.000000, -1046.000000)" stroke="#000000" stroke-width="2">
5 <g transform="translate(48.000000, 1046.000000)">
6 <g transform="translate(760.000000, 12.000000) scale(1, -1) translate(-760.000000, -12.000000) translate(748.000000, 0.000000)">
7 <path d="M2,14 C2,14 5,7 12,7 C19,7 22,14 22,14" id="Path-80" stroke-linejoin="round"/>
8 <path d="M12,7 L12,5"/>
9 <path d="M18,8.5 L19,7"/>
10 <path d="M21,12 L22.5,11"/>
11 <path d="M1.5,12 L3,11" transform="translate(2.250000, 11.500000) scale(1, -1) translate(-2.250000, -11.500000) "/>
12 <path d="M5,8.5 L6,7" transform="translate(5.500000, 7.750000) scale(-1, 1) translate(-5.500000, -7.750000) "/>
13 </g>
14 </g>
15 </g>
16 </g>
17</svg> \ No newline at end of file
diff --git a/client/src/assets/images/menu/eye.svg b/client/src/assets/images/menu/eye.svg
new file mode 100644
index 000000000..d1c3941f1
--- /dev/null
+++ b/client/src/assets/images/menu/eye.svg
@@ -0,0 +1,15 @@
1<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g transform="translate(-268.000000, -203.000000)" stroke="#000000" stroke-width="2">
4 <g transform="translate(268.000000, 203.000000)">
5 <path d="M2,12 C2,12 5,5 12,5 C19,5 22,12 22,12 C22,12 19,19 12,19 C5,19 2,12 2,12 Z" stroke-linejoin="round"/>
6 <circle id="Oval-50" cx="12" cy="12" r="3"/>
7 <path d="M12,5 L12,3" stroke-linecap="round"/>
8 <path d="M18,6.5 L19,5" stroke-linecap="round"/>
9 <path d="M21,10 L22.5,9" stroke-linecap="round"/>
10 <path d="M1.5,10 L3,9" stroke-linecap="round" transform="translate(2.250000, 9.500000) scale(1, -1) translate(-2.250000, -9.500000) "/>
11 <path d="M5,6.5 L6,5" stroke-linecap="round" transform="translate(5.500000, 5.750000) scale(-1, 1) translate(-5.500000, -5.750000) "/>
12 </g>
13 </g>
14 </g>
15</svg> \ No newline at end of file
diff --git a/client/src/assets/images/menu/language.png b/client/src/assets/images/menu/language.png
deleted file mode 100644
index 60e6fec00..000000000
--- a/client/src/assets/images/menu/language.png
+++ /dev/null
Binary files differ
diff --git a/client/src/assets/images/menu/language.svg b/client/src/assets/images/menu/language.svg
new file mode 100644
index 000000000..0ac754c87
--- /dev/null
+++ b/client/src/assets/images/menu/language.svg
@@ -0,0 +1,10 @@
1<!-- by Aaron Jin - free for commercial use -->
2<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" style="transform:scale(1.2)">
3 <path stroke="#000000" fill="#000000" stroke-width="3" d="M92.63,155H42.09a17.8,17.8,0,0,1-17.78-17.78V29.31a5,5,0,0,1,5-5h88.32a5,5,0,0,1,4.9,6L97.53,151A5,5,0,0,1,92.63,155ZM34.31,34.31V137.22A7.79,7.79,0,0,0,42.09,145H88.56L111.49,34.31Z"/>
4 <path stroke="#000000" fill="#000000" stroke-width="3" d="M170.69,175.69H75a5,5,0,0,1-4.9-6L74.39,149a5,5,0,0,1,9.8,2l-3,14.67h84.55V62.78A7.79,7.79,0,0,0,157.91,55H113.35a5,5,0,0,1,0-10h44.56a17.8,17.8,0,0,1,17.78,17.78V170.69A5,5,0,0,1,170.69,175.69Z"/>
5 <path stroke="#000000" fill="#000000" stroke-width="3" d="M50,92h0a5,5,0,0,1-5-5l0-24.49a17.49,17.49,0,0,1,35,0V87a5,5,0,0,1-10,0V62.49a7.49,7.49,0,0,0-15,0L55,87A5,5,0,0,1,50,92Z"/>
6 <path stroke="#000000" fill="#000000" stroke-width="3" d="M75,76H50a5,5,0,0,1,0-10H75a5,5,0,0,1,0,10Z"/>
7 <path stroke="#000000" fill="#000000" stroke-width="3" d="M120.21,155a5,5,0,0,1-3.54-8.54l21.26-21.26H120.21a5,5,0,0,1,0-10H150a5,5,0,0,1,3.54,8.54l-29.8,29.79A5,5,0,0,1,120.21,155Z"/>
8 <path stroke="#000000" fill="#000000" stroke-width="3" d="M150,155a5,5,0,0,1-3.54-1.47l-14.89-14.89a5,5,0,0,1,7.07-7.07l14.9,14.89A5,5,0,0,1,150,155Z"/>
9 <path stroke="#000000" fill="#000000" stroke-width="3" d="M142.55,110.31H128a5,5,0,1,1,0-10h14.6a5,5,0,0,1,0,10Z"/>
10</svg> \ No newline at end of file
diff --git a/client/src/assets/images/menu/moonsun.svg b/client/src/assets/images/menu/moonsun.svg
deleted file mode 100644
index fe2a96396..000000000
--- a/client/src/assets/images/menu/moonsun.svg
+++ /dev/null
@@ -1 +0,0 @@
1<svg height="300px" width="300px" fill="#fff" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 100 100" x="0px" y="0px"><title>Artboard 633</title><circle cx="50" cy="6" r="4"/><circle cx="50" cy="94" r="4"/><circle cx="6" cy="50" r="4"/><circle cx="94" cy="50" r="4"/><circle cx="18" cy="18" r="4"/><circle cx="82" cy="82" r="4"/><circle cx="18" cy="82" r="4"/><circle cx="82" cy="18" r="4"/><path d="M82,50A32,32,0,1,0,50,82,32,32,0,0,0,82,50ZM50,26a23.67,23.67,0,0,1,5.87.76c4.36,9.93.57,19-4.66,24.29s-14.4,9.24-24.45,4.83A23.75,23.75,0,0,1,26,50,24,24,0,0,1,50,26Zm0,48a23.94,23.94,0,0,1-18.26-8.47,29.38,29.38,0,0,0,3.74.26,30.07,30.07,0,0,0,21.41-9.11,29.82,29.82,0,0,0,8.61-25A24,24,0,0,1,50,74Z"/></svg> \ No newline at end of file
diff --git a/client/src/assets/images/menu/p2p.svg b/client/src/assets/images/menu/p2p.svg
new file mode 100644
index 000000000..744643010
--- /dev/null
+++ b/client/src/assets/images/menu/p2p.svg
@@ -0,0 +1,11 @@
1<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
2 <defs/>
3 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
4 <g transform="translate(-752.000000, -423.000000)" fill="#000000">
5 <g transform="translate(752.000000, 423.000000)">
6 <path d="M19.1632285,17.9958742 C20.7455119,17.9132011 22,16.5984601 22,14.9914698 L22,7.0085302 C22,5.35043647 20.6598453,4 19.0049107,4 L4.99508929,4 C3.33899222,4 2,5.34829734 2,7.0085302 L2,14.9914698 C2,16.5963573 3.25552676,17.9130154 4.83678095,17.9958629 L6.5,16 L4.99508929,16 C4.4481604,16 4,15.5484013 4,14.9914698 L4,7.0085302 C4,6.4497782 4.44667411,6 4.99508929,6 L19.0049107,6 C19.5518396,6 20,6.45159872 20,7.0085302 L20,14.9914698 C20,15.5502218 19.5533259,16 19.0049107,16 L17.5,16 L19.1632285,17.9958742 Z" fill-rule="nonzero"/>
7 <polygon stroke="#000000" stroke-width="2" stroke-linejoin="round" points="12 14 17 20 7 20"/>
8 </g>
9 </g>
10 </g>
11</svg> \ No newline at end of file
diff --git a/client/src/assets/player/bezels/bezels-plugin.ts b/client/src/assets/player/bezels/bezels-plugin.ts
index c2c251961..ca88bc1f9 100644
--- a/client/src/assets/player/bezels/bezels-plugin.ts
+++ b/client/src/assets/player/bezels/bezels-plugin.ts
@@ -1,85 +1,12 @@
1// @ts-ignore 1import videojs from 'video.js'
2import * as videojs from 'video.js' 2import './pause-bezel'
3import { VideoJSComponentInterface } from '../peertube-videojs-typings'
4 3
5function getPauseBezel () { 4const Plugin = videojs.getPlugin('plugin')
6 return `
7 <div class="vjs-bezels-pause">
8 <div class="vjs-bezel" role="status" aria-label="Pause">
9 <div class="vjs-bezel-icon">
10 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
11 <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use>
12 <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path>
13 </svg>
14 </div>
15 </div>
16 </div>
17 `
18}
19
20function getPlayBezel () {
21 return `
22 <div class="vjs-bezels-play">
23 <div class="vjs-bezel" role="status" aria-label="Play">
24 <div class="vjs-bezel-icon">
25 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
26 <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use>
27 <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path>
28 </svg>
29 </div>
30 </div>
31 </div>
32 `
33}
34
35// @ts-ignore-start
36const Component = videojs.getComponent('Component')
37class PauseBezel extends Component {
38 options_: any
39 container: HTMLBodyElement
40
41 constructor (player: videojs.Player, options: any) {
42 super(player, options)
43 this.options_ = options
44
45 player.on('pause', (_: any) => {
46 if (player.seeking() || player.ended()) return
47 this.container.innerHTML = getPauseBezel()
48 this.showBezel()
49 })
50
51 player.on('play', (_: any) => {
52 if (player.seeking()) return
53 this.container.innerHTML = getPlayBezel()
54 this.showBezel()
55 })
56 }
57 5
58 createEl () {
59 const container = super.createEl('div', {
60 className: 'vjs-bezels-content'
61 })
62 this.container = container
63 container.style.display = 'none'
64
65 return container
66 }
67
68 showBezel () {
69 this.container.style.display = 'inherit'
70 setTimeout(() => {
71 this.container.style.display = 'none'
72 }, 500) // matching the animation duration
73 }
74}
75// @ts-ignore-end
76
77videojs.registerComponent('PauseBezel', PauseBezel)
78
79const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
80class BezelsPlugin extends Plugin { 6class BezelsPlugin extends Plugin {
81 constructor (player: videojs.Player, options: any = {}) { 7
82 super(player, options) 8 constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
9 super(player)
83 10
84 this.player.ready(() => { 11 this.player.ready(() => {
85 player.addClass('vjs-bezels') 12 player.addClass('vjs-bezels')
@@ -90,4 +17,5 @@ class BezelsPlugin extends Plugin {
90} 17}
91 18
92videojs.registerPlugin('bezels', BezelsPlugin) 19videojs.registerPlugin('bezels', BezelsPlugin)
20
93export { BezelsPlugin } 21export { BezelsPlugin }
diff --git a/client/src/assets/player/bezels/pause-bezel.ts b/client/src/assets/player/bezels/pause-bezel.ts
new file mode 100644
index 000000000..886574380
--- /dev/null
+++ b/client/src/assets/player/bezels/pause-bezel.ts
@@ -0,0 +1,72 @@
1import videojs from 'video.js'
2
3function getPauseBezel () {
4 return `
5 <div class="vjs-bezels-pause">
6 <div class="vjs-bezel" role="status" aria-label="Pause">
7 <div class="vjs-bezel-icon">
8 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
9 <use class="vjs-svg-shadow" xlink:href="#vjs-id-1"></use>
10 <path class="vjs-svg-fill" d="M 12,26 16,26 16,10 12,10 z M 21,26 25,26 25,10 21,10 z" id="vjs-id-1"></path>
11 </svg>
12 </div>
13 </div>
14 </div>
15 `
16}
17
18function getPlayBezel () {
19 return `
20 <div class="vjs-bezels-play">
21 <div class="vjs-bezel" role="status" aria-label="Play">
22 <div class="vjs-bezel-icon">
23 <svg height="100%" version="1.1" viewBox="0 0 36 36" width="100%">
24 <use class="vjs-svg-shadow" xlink:href="#vjs-id-2"></use>
25 <path class="vjs-svg-fill" d="M 12,26 18.5,22 18.5,14 12,10 z M 18.5,22 25,18 25,18 18.5,14 z" id="ytp-id-2"></path>
26 </svg>
27 </div>
28 </div>
29 </div>
30 `
31}
32
33const Component = videojs.getComponent('Component')
34class PauseBezel extends Component {
35 container: HTMLDivElement
36
37 constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
38 super(player, options)
39
40 player.on('pause', (_: any) => {
41 if (player.seeking() || player.ended()) return
42 this.container.innerHTML = getPauseBezel()
43 this.showBezel()
44 })
45
46 player.on('play', (_: any) => {
47 if (player.seeking()) return
48 this.container.innerHTML = getPlayBezel()
49 this.showBezel()
50 })
51 }
52
53 createEl () {
54 this.container = super.createEl('div', {
55 className: 'vjs-bezels-content'
56 }) as HTMLDivElement
57
58 this.container.style.display = 'none'
59
60 return this.container
61 }
62
63 showBezel () {
64 this.container.style.display = 'inherit'
65
66 setTimeout(() => {
67 this.container.style.display = 'none'
68 }, 500) // matching the animation duration
69 }
70}
71
72videojs.registerComponent('PauseBezel', PauseBezel)
diff --git a/client/src/assets/player/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
new file mode 100644
index 000000000..9e2ac1aa4
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/hls-plugin.ts
@@ -0,0 +1,628 @@
1// Thanks https://github.com/streamroot/videojs-hlsjs-plugin
2// We duplicated this plugin to choose the hls.js version we want, because streamroot only provide a bundled file
3
4import * as Hlsjs from 'hls.js/dist/hls.light.js'
5import videojs from 'video.js'
6import { HlsjsConfigHandlerOptions, QualityLevelRepresentation, QualityLevels, VideoJSTechHLS } from '../peertube-videojs-typings'
7
8type ErrorCounts = {
9 [ type: string ]: number
10}
11
12type Metadata = {
13 levels: Hlsjs.Level[]
14}
15
16type CustomAudioTrack = AudioTrack & { name?: string, lang?: string }
17
18const registerSourceHandler = function (vjs: typeof videojs) {
19 if (!Hlsjs.isSupported()) {
20 console.warn('Hls.js is not supported in this browser!')
21 return
22 }
23
24 const html5 = vjs.getTech('Html5')
25
26 if (!html5) {
27 console.error('Not supported version if video.js')
28 return
29 }
30
31 // FIXME: typings
32 (html5 as any).registerSourceHandler({
33 canHandleSource: function (source: videojs.Tech.SourceObject) {
34 const hlsTypeRE = /^application\/x-mpegURL|application\/vnd\.apple\.mpegurl$/i
35 const hlsExtRE = /\.m3u8/i
36
37 if (hlsTypeRE.test(source.type)) return 'probably'
38 if (hlsExtRE.test(source.src)) return 'maybe'
39
40 return ''
41 },
42
43 handleSource: function (source: videojs.Tech.SourceObject, tech: VideoJSTechHLS) {
44 if (tech.hlsProvider) {
45 tech.hlsProvider.dispose()
46 }
47
48 tech.hlsProvider = new Html5Hlsjs(vjs, source, tech)
49
50 return tech.hlsProvider
51 }
52 }, 0);
53
54 // FIXME: typings
55 (vjs as any).Html5Hlsjs = Html5Hlsjs
56}
57
58function hlsjsConfigHandler (this: videojs.Player, options: HlsjsConfigHandlerOptions) {
59 const player = this
60
61 if (!options) return
62
63 if (!player.srOptions_) {
64 player.srOptions_ = {}
65 }
66
67 if (!player.srOptions_.hlsjsConfig) {
68 player.srOptions_.hlsjsConfig = options.hlsjsConfig
69 }
70
71 if (!player.srOptions_.captionConfig) {
72 player.srOptions_.captionConfig = options.captionConfig
73 }
74
75 if (options.levelLabelHandler && !player.srOptions_.levelLabelHandler) {
76 player.srOptions_.levelLabelHandler = options.levelLabelHandler
77 }
78}
79
80const registerConfigPlugin = function (vjs: typeof videojs) {
81 // Used in Brightcove since we don't pass options directly there
82 const registerVjsPlugin = vjs.registerPlugin || vjs.plugin
83 registerVjsPlugin('hlsjs', hlsjsConfigHandler)
84}
85
86class Html5Hlsjs {
87 private static readonly hooks: { [id: string]: Function[] } = {}
88
89 private readonly videoElement: HTMLVideoElement
90 private readonly errorCounts: ErrorCounts = {}
91 private readonly player: videojs.Player
92 private readonly tech: videojs.Tech
93 private readonly source: videojs.Tech.SourceObject
94 private readonly vjs: typeof videojs
95
96 private hls: Hlsjs & { manualLevel?: number, audioTrack?: any, audioTracks?: CustomAudioTrack[] } // FIXME: typings
97 private hlsjsConfig: Partial<Hlsjs.Config & { cueHandler: any }> = null
98
99 private _duration: number = null
100 private metadata: Metadata = null
101 private isLive: boolean = null
102 private dvrDuration: number = null
103 private edgeMargin: number = null
104
105 private handlers: { [ id in 'play' | 'addtrack' | 'playing' | 'textTracksChange' | 'audioTracksChange' ]: EventListener } = {
106 play: null,
107 addtrack: null,
108 playing: null,
109 textTracksChange: null,
110 audioTracksChange: null
111 }
112
113 private uiTextTrackHandled = false
114
115 constructor (vjs: typeof videojs, source: videojs.Tech.SourceObject, tech: videojs.Tech) {
116 this.vjs = vjs
117 this.source = source
118
119 this.tech = tech;
120 (this.tech as any).name_ = 'Hlsjs'
121
122 this.videoElement = tech.el() as HTMLVideoElement
123 this.player = vjs((tech.options_ as any).playerId)
124
125 this.videoElement.addEventListener('error', event => {
126 let errorTxt: string
127 const mediaError = (event.currentTarget as HTMLVideoElement).error
128
129 switch (mediaError.code) {
130 case mediaError.MEDIA_ERR_ABORTED:
131 errorTxt = 'You aborted the video playback'
132 break
133 case mediaError.MEDIA_ERR_DECODE:
134 errorTxt = 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support'
135 this._handleMediaError(mediaError)
136 break
137 case mediaError.MEDIA_ERR_NETWORK:
138 errorTxt = 'A network error caused the video download to fail part-way'
139 break
140 case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
141 errorTxt = 'The video could not be loaded, either because the server or network failed or because the format is not supported'
142 break
143
144 default:
145 errorTxt = mediaError.message
146 }
147
148 console.error('MEDIA_ERROR: ', errorTxt)
149 })
150
151 this.initialize()
152 }
153
154 duration () {
155 return this._duration || this.videoElement.duration || 0
156 }
157
158 seekable () {
159 if (this.hls.media) {
160 if (!this.isLive) {
161 return this.vjs.createTimeRanges(0, this.hls.media.duration)
162 }
163
164 // Video.js doesn't seem to like floating point timeranges
165 const startTime = Math.round(this.hls.media.duration - this.dvrDuration)
166 const endTime = Math.round(this.hls.media.duration - this.edgeMargin)
167
168 return this.vjs.createTimeRanges(startTime, endTime)
169 }
170
171 return this.vjs.createTimeRanges()
172 }
173
174 // See comment for `initialize` method.
175 dispose () {
176 this.videoElement.removeEventListener('play', this.handlers.play)
177 this.videoElement.textTracks.removeEventListener('addtrack', this.handlers.addtrack)
178 this.videoElement.removeEventListener('playing', this.handlers.playing)
179
180 this.player.textTracks().removeEventListener('change', this.handlers.textTracksChange)
181 this.uiTextTrackHandled = false
182
183 this.player.audioTracks().removeEventListener('change', this.handlers.audioTracksChange)
184
185 this.hls.destroy()
186 }
187
188 static addHook (type: string, callback: Function) {
189 Html5Hlsjs.hooks[ type ] = this.hooks[ type ] || []
190 Html5Hlsjs.hooks[ type ].push(callback)
191 }
192
193 static removeHook (type: string, callback: Function) {
194 if (Html5Hlsjs.hooks[ type ] === undefined) return false
195
196 const index = Html5Hlsjs.hooks[ type ].indexOf(callback)
197 if (index === -1) return false
198
199 Html5Hlsjs.hooks[ type ].splice(index, 1)
200
201 return true
202 }
203
204 private _executeHooksFor (type: string) {
205 if (Html5Hlsjs.hooks[ type ] === undefined) {
206 return
207 }
208
209 // ES3 and IE < 9
210 for (let i = 0; i < Html5Hlsjs.hooks[ type ].length; i++) {
211 Html5Hlsjs.hooks[ type ][ i ](this.player, this.hls)
212 }
213 }
214
215 private _handleMediaError (error: any) {
216 if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] === 1) {
217 console.info('trying to recover media error')
218 this.hls.recoverMediaError()
219 return
220 }
221
222 if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] === 2) {
223 console.info('2nd try to recover media error (by swapping audio codec')
224 this.hls.swapAudioCodec()
225 this.hls.recoverMediaError()
226 return
227 }
228
229 if (this.errorCounts[ Hlsjs.ErrorTypes.MEDIA_ERROR ] > 2) {
230 console.info('bubbling media error up to VIDEOJS')
231 this.tech.error = () => error
232 this.tech.trigger('error')
233 return
234 }
235 }
236
237 private _onError (_event: any, data: Hlsjs.errorData) {
238 const error: { message: string, code?: number } = {
239 message: `HLS.js error: ${data.type} - fatal: ${data.fatal} - ${data.details}`
240 }
241 console.error(error.message)
242
243 // increment/set error count
244 if (this.errorCounts[ data.type ]) this.errorCounts[ data.type ] += 1
245 else this.errorCounts[ data.type ] = 1
246
247 // Implement simple error handling based on hls.js documentation
248 // https://github.com/dailymotion/hls.js/blob/master/API.md#fifth-step-error-handling
249 if (data.fatal) {
250 switch (data.type) {
251 case Hlsjs.ErrorTypes.NETWORK_ERROR:
252 console.info('bubbling network error up to VIDEOJS')
253 error.code = 2
254 this.tech.error = () => error as any
255 this.tech.trigger('error')
256 break
257
258 case Hlsjs.ErrorTypes.MEDIA_ERROR:
259 error.code = 3
260 this._handleMediaError(error)
261 break
262
263 default:
264 // cannot recover
265 this.hls.destroy()
266 console.info('bubbling error up to VIDEOJS')
267 this.tech.error = () => error as any
268 this.tech.trigger('error')
269 break
270 }
271 }
272 }
273
274 private switchQuality (qualityId: number) {
275 this.hls.nextLevel = qualityId
276 }
277
278 private _levelLabel (level: Hlsjs.Level) {
279 if (this.player.srOptions_.levelLabelHandler) {
280 return this.player.srOptions_.levelLabelHandler(level)
281 }
282
283 if (level.height) return level.height + 'p'
284 if (level.width) return Math.round(level.width * 9 / 16) + 'p'
285 if (level.bitrate) return (level.bitrate / 1000) + 'kbps'
286
287 return 0
288 }
289
290 private _relayQualityChange (qualityLevels: QualityLevels) {
291 // Determine if it is "Auto" (all tracks enabled)
292 let isAuto = true
293
294 for (let i = 0; i < qualityLevels.length; i++) {
295 if (!qualityLevels[ i ]._enabled) {
296 isAuto = false
297 break
298 }
299 }
300
301 // Interact with ME
302 if (isAuto) {
303 this.hls.currentLevel = -1
304 return
305 }
306
307 // Find ID of highest enabled track
308 let selectedTrack: number
309
310 for (selectedTrack = qualityLevels.length - 1; selectedTrack >= 0; selectedTrack--) {
311 if (qualityLevels[ selectedTrack ]._enabled) {
312 break
313 }
314 }
315
316 this.hls.currentLevel = selectedTrack
317 }
318
319 private _handleQualityLevels () {
320 if (!this.metadata) return
321
322 const qualityLevels = this.player.qualityLevels && this.player.qualityLevels()
323 if (!qualityLevels) return
324
325 for (let i = 0; i < this.metadata.levels.length; i++) {
326 const details = this.metadata.levels[ i ]
327 const representation: QualityLevelRepresentation = {
328 id: i,
329 width: details.width,
330 height: details.height,
331 bandwidth: details.bitrate,
332 bitrate: details.bitrate,
333 _enabled: true
334 }
335
336 const self = this
337 representation.enabled = function (this: QualityLevels, level: number, toggle?: boolean) {
338 // Brightcove switcher works TextTracks-style (enable tracks that it wants to ABR on)
339 if (typeof toggle === 'boolean') {
340 this[ level ]._enabled = toggle
341 self._relayQualityChange(this)
342 }
343
344 return this[ level ]._enabled
345 }
346
347 qualityLevels.addQualityLevel(representation)
348 }
349 }
350
351 private _notifyVideoQualities () {
352 if (!this.metadata) return
353 const cleanTracklist = []
354
355 if (this.metadata.levels.length > 1) {
356 const autoLevel = {
357 id: -1,
358 label: 'auto',
359 selected: this.hls.manualLevel === -1
360 }
361 cleanTracklist.push(autoLevel)
362 }
363
364 this.metadata.levels.forEach((level, index) => {
365 // Don't write in level (shared reference with Hls.js)
366 const quality = {
367 id: index,
368 selected: index === this.hls.manualLevel,
369 label: this._levelLabel(level)
370 }
371
372 cleanTracklist.push(quality)
373 })
374
375 const payload = {
376 qualityData: { video: cleanTracklist },
377 qualitySwitchCallback: this.switchQuality.bind(this)
378 }
379
380 this.tech.trigger('loadedqualitydata', payload)
381
382 // Self-de-register so we don't raise the payload multiple times
383 this.videoElement.removeEventListener('playing', this.handlers.playing)
384 }
385
386 private _updateSelectedAudioTrack () {
387 const playerAudioTracks = this.tech.audioTracks()
388 for (let j = 0; j < playerAudioTracks.length; j++) {
389 // FIXME: typings
390 if ((playerAudioTracks[ j ] as any).enabled) {
391 this.hls.audioTrack = j
392 break
393 }
394 }
395 }
396
397 private _onAudioTracks () {
398 const hlsAudioTracks = this.hls.audioTracks
399 const playerAudioTracks = this.tech.audioTracks()
400
401 if (hlsAudioTracks.length > 1 && playerAudioTracks.length === 0) {
402 // Add Hls.js audio tracks if not added yet
403 for (let i = 0; i < hlsAudioTracks.length; i++) {
404 playerAudioTracks.addTrack(new this.vjs.AudioTrack({
405 id: i.toString(),
406 kind: 'alternative',
407 label: hlsAudioTracks[ i ].name || hlsAudioTracks[ i ].lang,
408 language: hlsAudioTracks[ i ].lang,
409 enabled: i === this.hls.audioTrack
410 }))
411 }
412
413 // Handle audio track change event
414 this.handlers.audioTracksChange = this._updateSelectedAudioTrack.bind(this)
415 playerAudioTracks.addEventListener('change', this.handlers.audioTracksChange)
416 }
417 }
418
419 private _getTextTrackLabel (textTrack: TextTrack) {
420 // Label here is readable label and is optional (used in the UI so if it is there it should be different)
421 return textTrack.label ? textTrack.label : textTrack.language
422 }
423
424 private _isSameTextTrack (track1: TextTrack, track2: TextTrack) {
425 return this._getTextTrackLabel(track1) === this._getTextTrackLabel(track2)
426 && track1.kind === track2.kind
427 }
428
429 private _updateSelectedTextTrack () {
430 const playerTextTracks = this.player.textTracks()
431 let activeTrack: TextTrack = null
432
433 for (let j = 0; j < playerTextTracks.length; j++) {
434 if (playerTextTracks[ j ].mode === 'showing') {
435 activeTrack = playerTextTracks[ j ]
436 break
437 }
438 }
439
440 const hlsjsTracks = this.videoElement.textTracks
441 for (let k = 0; k < hlsjsTracks.length; k++) {
442 if (hlsjsTracks[ k ].kind === 'subtitles' || hlsjsTracks[ k ].kind === 'captions') {
443 hlsjsTracks[ k ].mode = activeTrack && this._isSameTextTrack(hlsjsTracks[ k ], activeTrack)
444 ? 'showing'
445 : 'disabled'
446 }
447 }
448 }
449
450 private _startLoad () {
451 this.hls.startLoad(-1)
452 this.videoElement.removeEventListener('play', this.handlers.play)
453 }
454
455 private _oneLevelObjClone (obj: object) {
456 const result = {}
457 const objKeys = Object.keys(obj)
458 for (let i = 0; i < objKeys.length; i++) {
459 result[ objKeys[ i ] ] = obj[ objKeys[ i ] ]
460 }
461
462 return result
463 }
464
465 private _filterDisplayableTextTracks (textTracks: TextTrackList) {
466 const displayableTracks = []
467
468 // Filter out tracks that is displayable (captions or subtitles)
469 for (let idx = 0; idx < textTracks.length; idx++) {
470 if (textTracks[ idx ].kind === 'subtitles' || textTracks[ idx ].kind === 'captions') {
471 displayableTracks.push(textTracks[ idx ])
472 }
473 }
474
475 return displayableTracks
476 }
477
478 private _updateTextTrackList () {
479 const displayableTracks = this._filterDisplayableTextTracks(this.videoElement.textTracks)
480 const playerTextTracks = this.player.textTracks()
481
482 // Add stubs to make the caption switcher shows up
483 // Adding the Hls.js text track in will make us have double captions
484 for (let idx = 0; idx < displayableTracks.length; idx++) {
485 let isAdded = false
486
487 for (let jdx = 0; jdx < playerTextTracks.length; jdx++) {
488 if (this._isSameTextTrack(displayableTracks[ idx ], playerTextTracks[ jdx ])) {
489 isAdded = true
490 break
491 }
492 }
493
494 if (!isAdded) {
495 const hlsjsTextTrack = displayableTracks[ idx ]
496 this.player.addRemoteTextTrack({
497 kind: hlsjsTextTrack.kind as videojs.TextTrack.Kind,
498 label: this._getTextTrackLabel(hlsjsTextTrack),
499 language: hlsjsTextTrack.language,
500 srclang: hlsjsTextTrack.language
501 }, false)
502 }
503 }
504
505 // Handle UI switching
506 this._updateSelectedTextTrack()
507
508 if (!this.uiTextTrackHandled) {
509 this.handlers.textTracksChange = this._updateSelectedTextTrack.bind(this)
510 playerTextTracks.addEventListener('change', this.handlers.textTracksChange)
511
512 this.uiTextTrackHandled = true
513 }
514 }
515
516 private _onMetaData (_event: any, data: Hlsjs.manifestLoadedData) {
517 // This could arrive before 'loadedqualitydata' handlers is registered, remember it so we can raise it later
518 this.metadata = data as any
519 this._handleQualityLevels()
520 }
521
522 private _createCueHandler (captionConfig: any) {
523 return {
524 newCue: (track: any, startTime: number, endTime: number, captionScreen: { rows: any[] }) => {
525 let row: any
526 let cue: VTTCue
527 let text: string
528 const VTTCue = (window as any).VTTCue || (window as any).TextTrackCue
529
530 for (let r = 0; r < captionScreen.rows.length; r++) {
531 row = captionScreen.rows[ r ]
532 text = ''
533
534 if (!row.isEmpty()) {
535 for (let c = 0; c < row.chars.length; c++) {
536 text += row.chars[ c ].ucharj
537 }
538
539 cue = new VTTCue(startTime, endTime, text.trim())
540
541 // typeof null === 'object'
542 if (captionConfig != null && typeof captionConfig === 'object') {
543 // Copy client overridden property into the cue object
544 const configKeys = Object.keys(captionConfig)
545
546 for (let k = 0; k < configKeys.length; k++) {
547 cue[ configKeys[ k ] ] = captionConfig[ configKeys[ k ] ]
548 }
549 }
550 track.addCue(cue)
551 if (endTime === startTime) track.addCue(new VTTCue(endTime + 5, ''))
552 }
553 }
554 }
555 }
556 }
557
558 private _initHlsjs () {
559 const techOptions = this.tech.options_ as HlsjsConfigHandlerOptions
560 const srOptions_ = this.player.srOptions_
561
562 const hlsjsConfigRef = srOptions_ && srOptions_.hlsjsConfig || techOptions.hlsjsConfig
563 // Hls.js will write to the reference thus change the object for later streams
564 this.hlsjsConfig = hlsjsConfigRef ? this._oneLevelObjClone(hlsjsConfigRef) : {}
565
566 if ([ '', 'auto' ].includes(this.videoElement.preload) && !this.videoElement.autoplay && this.hlsjsConfig.autoStartLoad === undefined) {
567 this.hlsjsConfig.autoStartLoad = false
568 }
569
570 const captionConfig = srOptions_ && srOptions_.captionConfig || techOptions.captionConfig
571 if (captionConfig) {
572 this.hlsjsConfig.cueHandler = this._createCueHandler(captionConfig)
573 }
574
575 // If the user explicitly sets autoStartLoad to false, we're not going to enter the if block above
576 // That's why we have a separate if block here to set the 'play' listener
577 if (this.hlsjsConfig.autoStartLoad === false) {
578 this.handlers.play = this._startLoad.bind(this)
579 this.videoElement.addEventListener('play', this.handlers.play)
580 }
581
582 // _notifyVideoQualities sometimes runs before the quality picker event handler is registered -> no video switcher
583 this.handlers.playing = this._notifyVideoQualities.bind(this)
584 this.videoElement.addEventListener('playing', this.handlers.playing)
585
586 this.hls = new Hlsjs(this.hlsjsConfig)
587
588 this._executeHooksFor('beforeinitialize')
589
590 this.hls.on(Hlsjs.Events.ERROR, (event, data) => this._onError(event, data))
591 this.hls.on(Hlsjs.Events.AUDIO_TRACKS_UPDATED, () => this._onAudioTracks())
592 this.hls.on(Hlsjs.Events.MANIFEST_PARSED, (event, data) => this._onMetaData(event, data as any)) // FIXME: typings
593 this.hls.on(Hlsjs.Events.LEVEL_LOADED, (event, data) => {
594 // The DVR plugin will auto seek to "live edge" on start up
595 if (this.hlsjsConfig.liveSyncDuration) {
596 this.edgeMargin = this.hlsjsConfig.liveSyncDuration
597 } else if (this.hlsjsConfig.liveSyncDurationCount) {
598 this.edgeMargin = this.hlsjsConfig.liveSyncDurationCount * data.details.targetduration
599 }
600
601 this.isLive = data.details.live
602 this.dvrDuration = data.details.totalduration
603 this._duration = this.isLive ? Infinity : data.details.totalduration
604 })
605 this.hls.once(Hlsjs.Events.FRAG_LOADED, () => {
606 // Emit custom 'loadedmetadata' event for parity with `videojs-contrib-hls`
607 // Ref: https://github.com/videojs/videojs-contrib-hls#loadedmetadata
608 this.tech.trigger('loadedmetadata')
609 })
610
611 this.hls.attachMedia(this.videoElement)
612
613 this.handlers.addtrack = this._updateTextTrackList.bind(this)
614 this.videoElement.textTracks.addEventListener('addtrack', this.handlers.addtrack)
615
616 this.hls.loadSource(this.source.src)
617 }
618
619 private initialize () {
620 this._initHlsjs()
621 }
622}
623
624export {
625 Html5Hlsjs,
626 registerSourceHandler,
627 registerConfigPlugin
628}
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
index c3f863f72..46c6bbaf2 100644
--- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -1,16 +1,15 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore 2import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo } from '../peertube-videojs-typings'
3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
5import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' 3import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
6import { Events, Segment } from 'p2p-media-loader-core' 4import { Events, Segment } from 'p2p-media-loader-core'
7import { timeToInt } from '../utils' 5import { timeToInt } from '../utils'
6import { registerConfigPlugin, registerSourceHandler } from './hls-plugin'
7import * as Hlsjs from 'hls.js/dist/hls.light.js'
8 8
9// videojs-hlsjs-plugin needs videojs in window 9registerConfigPlugin(videojs)
10window['videojs'] = videojs 10registerSourceHandler(videojs)
11require('@streamroot/videojs-hlsjs-plugin')
12 11
13const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 12const Plugin = videojs.getPlugin('plugin')
14class P2pMediaLoaderPlugin extends Plugin { 13class P2pMediaLoaderPlugin extends Plugin {
15 14
16 private readonly CONSTANTS = { 15 private readonly CONSTANTS = {
@@ -18,7 +17,7 @@ class P2pMediaLoaderPlugin extends Plugin {
18 } 17 }
19 private readonly options: P2PMediaLoaderPluginOptions 18 private readonly options: P2PMediaLoaderPluginOptions
20 19
21 private hlsjs: any // Don't type hlsjs to not bundle the module 20 private hlsjs: Hlsjs
22 private p2pEngine: Engine 21 private p2pEngine: Engine
23 private statsP2PBytes = { 22 private statsP2PBytes = {
24 pendingDownload: [] as number[], 23 pendingDownload: [] as number[],
@@ -37,12 +36,13 @@ class P2pMediaLoaderPlugin extends Plugin {
37 36
38 private networkInfoInterval: any 37 private networkInfoInterval: any
39 38
40 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { 39 constructor (player: videojs.Player, options?: P2PMediaLoaderPluginOptions) {
41 super(player, options) 40 super(player)
42 41
43 this.options = options 42 this.options = options
44 43
45 if (!videojs.Html5Hlsjs) { 44 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
45 if (!(videojs as any).Html5Hlsjs) {
46 const message = 'HLS.js does not seem to be supported.' 46 const message = 'HLS.js does not seem to be supported.'
47 console.warn(message) 47 console.warn(message)
48 48
@@ -50,7 +50,8 @@ class P2pMediaLoaderPlugin extends Plugin {
50 return 50 return
51 } 51 }
52 52
53 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { 53 // FIXME: typings https://github.com/Microsoft/TypeScript/issues/14080
54 (videojs as any).Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
54 this.hlsjs = hlsjs 55 this.hlsjs = hlsjs
55 }) 56 })
56 57
@@ -84,12 +85,11 @@ class P2pMediaLoaderPlugin extends Plugin {
84 private initialize () { 85 private initialize () {
85 initHlsJsPlayer(this.hlsjs) 86 initHlsJsPlayer(this.hlsjs)
86 87
87 const tech = this.player.tech_ 88 // FIXME: typings
88 this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() 89 const options = this.player.tech(true).options_ as any
90 this.p2pEngine = options.hlsjsConfig.loader.getEngine()
89 91
90 // Avoid using constants to not import hls.hs 92 this.hlsjs.on(Hlsjs.Events.LEVEL_SWITCHING, (_: any, data: any) => {
91 // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37
92 this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => {
93 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) 93 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
94 }) 94 })
95 95
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index d9e02cd7d..12e460f03 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -1,21 +1,26 @@
1import { VideoFile } from '../../../../shared/models/videos' 1import { VideoFile } from '../../../../shared/models/videos'
2// @ts-ignore 2import videojs from 'video.js'
3import * as videojs from 'video.js' 3import 'videojs-hotkeys/videojs.hotkeys'
4import 'videojs-hotkeys'
5import 'videojs-dock' 4import 'videojs-dock'
6import 'videojs-contextmenu-ui' 5import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels' 6import 'videojs-contrib-quality-levels'
7import './upnext/end-card'
8import './upnext/upnext-plugin' 8import './upnext/upnext-plugin'
9import './bezels/bezels-plugin' 9import './bezels/bezels-plugin'
10import './peertube-plugin' 10import './peertube-plugin'
11import './videojs-components/next-video-button' 11import './videojs-components/next-video-button'
12import './videojs-components/p2p-info-button'
12import './videojs-components/peertube-link-button' 13import './videojs-components/peertube-link-button'
14import './videojs-components/peertube-load-progress-bar'
13import './videojs-components/resolution-menu-button' 15import './videojs-components/resolution-menu-button'
16import './videojs-components/resolution-menu-item'
17import './videojs-components/settings-dialog'
14import './videojs-components/settings-menu-button' 18import './videojs-components/settings-menu-button'
15import './videojs-components/p2p-info-button' 19import './videojs-components/settings-menu-item'
16import './videojs-components/peertube-load-progress-bar' 20import './videojs-components/settings-panel'
21import './videojs-components/settings-panel-child'
17import './videojs-components/theater-button' 22import './videojs-components/theater-button'
18import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' 23import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions } from './peertube-videojs-typings'
19import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' 24import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils'
20import { isDefaultLocale } from '../../../../shared/models/i18n/i18n' 25import { isDefaultLocale } from '../../../../shared/models/i18n/i18n'
21import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' 26import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
@@ -25,11 +30,13 @@ import { getStoredP2PEnabled } from './peertube-player-local-storage'
25import { TranslationsManager } from './translations-manager' 30import { TranslationsManager } from './translations-manager'
26 31
27// Change 'Playback Rate' to 'Speed' (smaller for our settings menu) 32// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
28videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' 33(videojs.getComponent('PlaybackRateMenuButton') as any).prototype.controlText_ = 'Speed'
34
35const CaptionsButton = videojs.getComponent('CaptionsButton') as any
29// Change Captions to Subtitles/CC 36// Change Captions to Subtitles/CC
30videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' 37CaptionsButton.prototype.controlText_ = 'Subtitles/CC'
31// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) 38// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
32videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' 39CaptionsButton.prototype.label_ = ' '
33 40
34export type PlayerMode = 'webtorrent' | 'p2p-media-loader' 41export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
35 42
@@ -92,9 +99,9 @@ export type PeertubePlayerManagerOptions = {
92 99
93export class PeertubePlayerManager { 100export class PeertubePlayerManager {
94 private static playerElementClassName: string 101 private static playerElementClassName: string
95 private static onPlayerChange: (player: any) => void 102 private static onPlayerChange: (player: videojs.Player) => void
96 103
97 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) { 104 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: videojs.Player) => void) {
98 let p2pMediaLoader: any 105 let p2pMediaLoader: any
99 106
100 this.onPlayerChange = onPlayerChange 107 this.onPlayerChange = onPlayerChange
@@ -114,12 +121,12 @@ export class PeertubePlayerManager {
114 121
115 const self = this 122 const self = this
116 return new Promise(res => { 123 return new Promise(res => {
117 videojs(options.common.playerElement, videojsOptions, function (this: any) { 124 videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) {
118 const player = this 125 const player = this
119 126
120 let alreadyFallback = false 127 let alreadyFallback = false
121 128
122 player.tech_.one('error', () => { 129 player.tech(true).one('error', () => {
123 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options) 130 if (!alreadyFallback) self.maybeFallbackToWebTorrent(mode, player, options)
124 alreadyFallback = true 131 alreadyFallback = true
125 }) 132 })
@@ -164,7 +171,7 @@ export class PeertubePlayerManager {
164 const videojsOptions = this.getVideojsOptions(mode, options) 171 const videojsOptions = this.getVideojsOptions(mode, options)
165 172
166 const self = this 173 const self = this
167 videojs(newVideoElement, videojsOptions, function (this: any) { 174 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
168 const player = this 175 const player = this
169 176
170 self.addContextMenu(mode, player, options.common.embedUrl) 177 self.addContextMenu(mode, player, options.common.embedUrl)
@@ -173,7 +180,11 @@ export class PeertubePlayerManager {
173 }) 180 })
174 } 181 }
175 182
176 private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { 183 private static getVideojsOptions (
184 mode: PlayerMode,
185 options: PeertubePlayerManagerOptions,
186 p2pMediaLoaderModule?: any
187 ): videojs.PlayerOptions {
177 const commonOptions = options.common 188 const commonOptions = options.common
178 189
179 let autoplay = commonOptions.autoplay 190 let autoplay = commonOptions.autoplay
@@ -197,9 +208,9 @@ export class PeertubePlayerManager {
197 } 208 }
198 209
199 if (mode === 'p2p-media-loader') { 210 if (mode === 'p2p-media-loader') {
200 const { streamrootHls } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule) 211 const { hlsjs } = PeertubePlayerManager.addP2PMediaLoaderOptions(plugins, options, p2pMediaLoaderModule)
201 212
202 html5 = streamrootHls.html5 213 html5 = hlsjs.html5
203 } 214 }
204 215
205 if (mode === 'webtorrent') { 216 if (mode === 'webtorrent') {
@@ -213,7 +224,7 @@ export class PeertubePlayerManager {
213 html5, 224 html5,
214 225
215 // We don't use text track settings for now 226 // We don't use text track settings for now
216 textTrackSettings: false, 227 textTrackSettings: false as any, // FIXME: typings
217 controls: commonOptions.controls !== undefined ? commonOptions.controls : true, 228 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
218 loop: commonOptions.loop !== undefined ? commonOptions.loop : false, 229 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
219 230
@@ -222,7 +233,7 @@ export class PeertubePlayerManager {
222 : undefined, // Undefined so the player knows it has to check the local storage 233 : undefined, // Undefined so the player knows it has to check the local storage
223 234
224 autoplay: autoplay === true 235 autoplay: autoplay === true
225 ? 'any' // Use 'any' instead of true to get notifier by videojs if autoplay fails 236 ? 'play' // Use 'any' instead of true to get notifier by videojs if autoplay fails
226 : autoplay, 237 : autoplay,
227 238
228 poster: commonOptions.poster, 239 poster: commonOptions.poster,
@@ -237,7 +248,7 @@ export class PeertubePlayerManager {
237 peertubeLink: commonOptions.peertubeLink, 248 peertubeLink: commonOptions.peertubeLink,
238 theaterButton: commonOptions.theaterButton, 249 theaterButton: commonOptions.theaterButton,
239 nextVideo: commonOptions.nextVideo 250 nextVideo: commonOptions.nextVideo
240 }) 251 }) as any // FIXME: typings
241 } 252 }
242 } 253 }
243 254
@@ -289,7 +300,7 @@ export class PeertubePlayerManager {
289 swarmId: p2pMediaLoaderOptions.playlistUrl 300 swarmId: p2pMediaLoaderOptions.playlistUrl
290 } 301 }
291 } 302 }
292 const streamrootHls = { 303 const hlsjs = {
293 levelLabelHandler: (level: { height: number, width: number }) => { 304 levelLabelHandler: (level: { height: number, width: number }) => {
294 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) 305 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height)
295 306
@@ -308,7 +319,7 @@ export class PeertubePlayerManager {
308 } 319 }
309 } 320 }
310 321
311 const toAssign = { p2pMediaLoader, streamrootHls } 322 const toAssign = { p2pMediaLoader, hlsjs }
312 Object.assign(plugins, toAssign) 323 Object.assign(plugins, toAssign)
313 324
314 return toAssign 325 return toAssign
@@ -406,7 +417,7 @@ export class PeertubePlayerManager {
406 return children 417 return children
407 } 418 }
408 419
409 private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { 420 private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string) {
410 const content = [ 421 const content = [
411 { 422 {
412 label: player.localize('Copy the video URL'), 423 label: player.localize('Copy the video URL'),
@@ -416,9 +427,8 @@ export class PeertubePlayerManager {
416 }, 427 },
417 { 428 {
418 label: player.localize('Copy the video URL at the current time'), 429 label: player.localize('Copy the video URL at the current time'),
419 listener: function () { 430 listener: function (this: videojs.Player) {
420 const player = this as videojs.Player 431 copyToClipboard(buildVideoLink({ startTime: this.currentTime() }))
421 copyToClipboard(buildVideoLink({ startTime: player.currentTime() }))
422 } 432 }
423 }, 433 },
424 { 434 {
@@ -432,9 +442,8 @@ export class PeertubePlayerManager {
432 if (mode === 'webtorrent') { 442 if (mode === 'webtorrent') {
433 content.push({ 443 content.push({
434 label: player.localize('Copy magnet URI'), 444 label: player.localize('Copy magnet URI'),
435 listener: function () { 445 listener: function (this: videojs.Player) {
436 const player = this as videojs.Player 446 copyToClipboard(this.webtorrent().getCurrentVideoFile().magnetUri)
437 copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri)
438 } 447 }
439 }) 448 })
440 } 449 }
@@ -472,7 +481,8 @@ export class PeertubePlayerManager {
472 return event.key === '>' 481 return event.key === '>'
473 }, 482 },
474 handler: function (player: videojs.Player) { 483 handler: function (player: videojs.Player) {
475 player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) 484 const newValue = Math.min(player.playbackRate() + 0.1, 5)
485 player.playbackRate(parseFloat(newValue.toFixed(2)))
476 } 486 }
477 }, 487 },
478 decreasePlaybackRateKey: { 488 decreasePlaybackRateKey: {
@@ -480,7 +490,8 @@ export class PeertubePlayerManager {
480 return event.key === '<' 490 return event.key === '<'
481 }, 491 },
482 handler: function (player: videojs.Player) { 492 handler: function (player: videojs.Player) {
483 player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) 493 const newValue = Math.max(player.playbackRate() - 0.1, 0.10)
494 player.playbackRate(parseFloat(newValue.toFixed(2)))
484 } 495 }
485 }, 496 },
486 frameByFrame: { 497 frameByFrame: {
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 9824c43b5..5085f7f86 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -1,14 +1,10 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4import './videojs-components/settings-menu-button' 2import './videojs-components/settings-menu-button'
5import { 3import {
6 PeerTubePluginOptions, 4 PeerTubePluginOptions,
7 ResolutionUpdateData, 5 ResolutionUpdateData,
8 UserWatching, 6 UserWatching,
9 VideoJSCaption, 7 VideoJSCaption
10 VideoJSComponentInterface,
11 videojsUntyped
12} from './peertube-videojs-typings' 8} from './peertube-videojs-typings'
13import { isMobile, timeToInt } from './utils' 9import { isMobile, timeToInt } from './utils'
14import { 10import {
@@ -20,7 +16,8 @@ import {
20 saveVolumeInStore 16 saveVolumeInStore
21} from './peertube-player-local-storage' 17} from './peertube-player-local-storage'
22 18
23const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 19const Plugin = videojs.getPlugin('plugin')
20
24class PeerTubePlugin extends Plugin { 21class PeerTubePlugin extends Plugin {
25 private readonly videoViewUrl: string 22 private readonly videoViewUrl: string
26 private readonly videoDuration: number 23 private readonly videoDuration: number
@@ -28,7 +25,6 @@ class PeerTubePlugin extends Plugin {
28 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video 25 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
29 } 26 }
30 27
31 private player: any
32 private videoCaptions: VideoJSCaption[] 28 private videoCaptions: VideoJSCaption[]
33 private defaultSubtitle: string 29 private defaultSubtitle: string
34 30
@@ -40,8 +36,8 @@ class PeerTubePlugin extends Plugin {
40 private mouseInControlBar = false 36 private mouseInControlBar = false
41 private readonly savedInactivityTimeout: number 37 private readonly savedInactivityTimeout: number
42 38
43 constructor (player: videojs.Player, options: PeerTubePluginOptions) { 39 constructor (player: videojs.Player, options?: PeerTubePluginOptions) {
44 super(player, options) 40 super(player)
45 41
46 this.videoViewUrl = options.videoViewUrl 42 this.videoViewUrl = options.videoViewUrl
47 this.videoDuration = options.videoDuration 43 this.videoDuration = options.videoDuration
@@ -67,7 +63,7 @@ class PeerTubePlugin extends Plugin {
67 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) 63 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
68 } 64 }
69 65
70 this.player.tech_.on('loadedqualitydata', () => { 66 this.player.tech(true).on('loadedqualitydata', () => {
71 setTimeout(() => { 67 setTimeout(() => {
72 // Replay a resolution change, now we loaded all quality data 68 // Replay a resolution change, now we loaded all quality data
73 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) 69 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
@@ -102,7 +98,7 @@ class PeerTubePlugin extends Plugin {
102 } 98 }
103 99
104 this.player.textTracks().on('change', () => { 100 this.player.textTracks().on('change', () => {
105 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { 101 const showing = this.player.textTracks().tracks_.find(t => {
106 return t.kind === 'captions' && t.mode === 'showing' 102 return t.kind === 'captions' && t.mode === 'showing'
107 }) 103 })
108 104
@@ -262,7 +258,7 @@ class PeerTubePlugin extends Plugin {
262 258
263 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 259 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
264 private initSmoothProgressBar () { 260 private initSmoothProgressBar () {
265 const SeekBar = videojsUntyped.getComponent('SeekBar') 261 const SeekBar = videojs.getComponent('SeekBar') as any
266 SeekBar.prototype.getPercent = function getPercent () { 262 SeekBar.prototype.getPercent = function getPercent () {
267 // Allows for smooth scrubbing, when player can't keep up. 263 // Allows for smooth scrubbing, when player can't keep up.
268 // const time = (this.player_.scrubbing()) ? 264 // const time = (this.player_.scrubbing()) ?
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index aad4dbb4f..cb7d6f6b4 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -1,28 +1,81 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { PeerTubePlugin } from './peertube-plugin' 1import { PeerTubePlugin } from './peertube-plugin'
6import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' 2import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
7import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' 3import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
8import { PlayerMode } from './peertube-player-manager' 4import { PlayerMode } from './peertube-player-manager'
9import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager' 5import { RedundancyUrlManager } from './p2p-media-loader/redundancy-url-manager'
10import { VideoFile } from '@shared/models' 6import { VideoFile } from '@shared/models'
7import videojs from 'video.js'
8import { Config, Level } from 'hls.js'
9
10declare module 'video.js' {
11
12 export interface VideoJsPlayer {
13 srOptions_: HlsjsConfigHandlerOptions
14
15 theaterEnabled: boolean
16
17 // FIXME: add it to upstream typings
18 posterImage: {
19 show (): void
20 hide (): void
21 }
22
23 handleTechSeeked_ (): void
24
25 // Plugins
11 26
12declare namespace videojs {
13 interface Player {
14 peertube (): PeerTubePlugin 27 peertube (): PeerTubePlugin
28
15 webtorrent (): WebTorrentPlugin 29 webtorrent (): WebTorrentPlugin
30
16 p2pMediaLoader (): P2pMediaLoaderPlugin 31 p2pMediaLoader (): P2pMediaLoaderPlugin
32
33 contextmenuUI (options: any): any
34
35 bezels (): void
36
37 qualityLevels (): QualityLevels
38
39 textTracks (): TextTrackList & {
40 on: Function
41 tracks_: (TextTrack & { id: string, label: string, src: string })[]
42 }
43
44 audioTracks (): AudioTrackList
45
46 dock (options: { title: string, description: string }): void
17 } 47 }
18} 48}
19 49
20interface VideoJSComponentInterface { 50export interface VideoJSTechHLS extends videojs.Tech {
21 _player: videojs.Player 51 hlsProvider: any // FIXME: typings
52}
53
54export interface HlsjsConfigHandlerOptions {
55 hlsjsConfig?: Config & { cueHandler: any }// FIXME: typings
56 captionConfig?: any // FIXME: typings
57
58 levelLabelHandler?: (level: Level) => string
59}
60
61type QualityLevelRepresentation = {
62 id: number
63 height: number
64
65 label?: string
66 width?: number
67 bandwidth?: number
68 bitrate?: number
22 69
23 new (player: videojs.Player, options?: any): any 70 enabled?: Function
71 _enabled: boolean
72}
73
74type QualityLevels = QualityLevelRepresentation[] & {
75 selectedIndex: number
76 selectedIndex_: number
24 77
25 registerComponent (name: string, obj: any): any 78 addQualityLevel (representation: QualityLevelRepresentation): void
26} 79}
27 80
28type VideoJSCaption = { 81type VideoJSCaption = {
@@ -78,9 +131,6 @@ type VideoJSPluginOptions = {
78 p2pMediaLoader?: P2PMediaLoaderPluginOptions 131 p2pMediaLoader?: P2PMediaLoaderPluginOptions
79} 132}
80 133
81// videojs typings don't have some method we need
82const videojsUntyped = videojs as any
83
84type LoadedQualityData = { 134type LoadedQualityData = {
85 qualitySwitchCallback: Function, 135 qualitySwitchCallback: Function,
86 qualityData: { 136 qualityData: {
@@ -123,13 +173,13 @@ export {
123 PlayerNetworkInfo, 173 PlayerNetworkInfo,
124 ResolutionUpdateData, 174 ResolutionUpdateData,
125 AutoResolutionUpdateData, 175 AutoResolutionUpdateData,
126 VideoJSComponentInterface,
127 videojsUntyped,
128 VideoJSCaption, 176 VideoJSCaption,
129 UserWatching, 177 UserWatching,
130 PeerTubePluginOptions, 178 PeerTubePluginOptions,
131 WebtorrentPluginOptions, 179 WebtorrentPluginOptions,
132 P2PMediaLoaderPluginOptions, 180 P2PMediaLoaderPluginOptions,
133 VideoJSPluginOptions, 181 VideoJSPluginOptions,
134 LoadedQualityData 182 LoadedQualityData,
183 QualityLevelRepresentation,
184 QualityLevels
135} 185}
diff --git a/client/src/assets/player/upnext/end-card.ts b/client/src/assets/player/upnext/end-card.ts
new file mode 100644
index 000000000..8fabfc3fd
--- /dev/null
+++ b/client/src/assets/player/upnext/end-card.ts
@@ -0,0 +1,155 @@
1import videojs from 'video.js'
2
3function getMainTemplate (options: any) {
4 return `
5 <div class="vjs-upnext-top">
6 <span class="vjs-upnext-headtext">${options.headText}</span>
7 <div class="vjs-upnext-title"></div>
8 </div>
9 <div class="vjs-upnext-autoplay-icon">
10 <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%">
11 <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle>
12 <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle>
13 <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg>
14 </div>
15 <span class="vjs-upnext-bottom">
16 <span class="vjs-upnext-cancel">
17 <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button>
18 </span>
19 <span class="vjs-upnext-suspended">${options.suspendedText}</span>
20 </span>
21 `
22}
23
24export interface EndCardOptions extends videojs.ComponentOptions {
25 next: Function,
26 getTitle: () => string
27 timeout: number
28 cancelText: string
29 headText: string
30 suspendedText: string
31 condition: () => boolean
32 suspended: () => boolean
33}
34
35const Component = videojs.getComponent('Component')
36class EndCard extends Component {
37 options_: EndCardOptions
38
39 dashOffsetTotal = 586
40 dashOffsetStart = 293
41 interval = 50
42 upNextEvents = new videojs.EventTarget()
43 ticks = 0
44 totalTicks: number
45
46 container: HTMLDivElement
47 title: HTMLElement
48 autoplayRing: HTMLElement
49 cancelButton: HTMLElement
50 suspendedMessage: HTMLElement
51 nextButton: HTMLElement
52
53 constructor (player: videojs.Player, options: EndCardOptions) {
54 super(player, options)
55
56 this.totalTicks = this.options_.timeout / this.interval
57
58 player.on('ended', (_: any) => {
59 if (!this.options_.condition()) return
60
61 player.addClass('vjs-upnext--showing')
62 this.showCard((canceled: boolean) => {
63 player.removeClass('vjs-upnext--showing')
64 this.container.style.display = 'none'
65 if (!canceled) {
66 this.options_.next()
67 }
68 })
69 })
70
71 player.on('playing', () => {
72 this.upNextEvents.trigger('playing')
73 })
74 }
75
76 createEl () {
77 const container = super.createEl('div', {
78 className: 'vjs-upnext-content',
79 innerHTML: getMainTemplate(this.options_)
80 }) as HTMLDivElement
81
82 this.container = container
83 container.style.display = 'none'
84
85 this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0] as HTMLElement
86 this.title = container.getElementsByClassName('vjs-upnext-title')[0] as HTMLElement
87 this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0] as HTMLElement
88 this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0] as HTMLElement
89 this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0] as HTMLElement
90
91 this.cancelButton.onclick = () => {
92 this.upNextEvents.trigger('cancel')
93 }
94
95 this.nextButton.onclick = () => {
96 this.upNextEvents.trigger('next')
97 }
98
99 return container
100 }
101
102 showCard (cb: Function) {
103 let timeout: any
104
105 this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart)
106 this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart)
107
108 this.title.innerHTML = this.options_.getTitle()
109
110 this.upNextEvents.one('cancel', () => {
111 clearTimeout(timeout)
112 cb(true)
113 })
114
115 this.upNextEvents.one('playing', () => {
116 clearTimeout(timeout)
117 cb(true)
118 })
119
120 this.upNextEvents.one('next', () => {
121 clearTimeout(timeout)
122 cb(false)
123 })
124
125 const goToPercent = (percent: number) => {
126 const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100)
127 this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset)
128 }
129
130 const tick = () => {
131 goToPercent((this.ticks++) * 100 / this.totalTicks)
132 }
133
134 const update = () => {
135 if (this.options_.suspended()) {
136 this.suspendedMessage.innerText = this.options_.suspendedText
137 goToPercent(0)
138 this.ticks = 0
139 timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
140 } else if (this.ticks >= this.totalTicks) {
141 clearTimeout(timeout)
142 cb(false)
143 } else {
144 this.suspendedMessage.innerText = ''
145 tick()
146 timeout = setTimeout(update.bind(this), this.interval)
147 }
148 }
149
150 this.container.style.display = 'block'
151 timeout = setTimeout(update.bind(this), this.interval)
152 }
153}
154
155videojs.registerComponent('EndCard', EndCard)
diff --git a/client/src/assets/player/upnext/upnext-plugin.ts b/client/src/assets/player/upnext/upnext-plugin.ts
index a3747b25f..db969024d 100644
--- a/client/src/assets/player/upnext/upnext-plugin.ts
+++ b/client/src/assets/player/upnext/upnext-plugin.ts
@@ -1,154 +1,11 @@
1// @ts-ignore 1import videojs from 'video.js'
2import * as videojs from 'video.js' 2import { EndCardOptions } from './end-card'
3import { VideoJSComponentInterface } from '../peertube-videojs-typings'
4 3
5function getMainTemplate (options: any) { 4const Plugin = videojs.getPlugin('plugin')
6 return `
7 <div class="vjs-upnext-top">
8 <span class="vjs-upnext-headtext">${options.headText}</span>
9 <div class="vjs-upnext-title"></div>
10 </div>
11 <div class="vjs-upnext-autoplay-icon">
12 <svg height="100%" version="1.1" viewbox="0 0 98 98" width="100%">
13 <circle class="vjs-upnext-svg-autoplay-circle" cx="49" cy="49" fill="#000" fill-opacity="0.8" r="48"></circle>
14 <circle class="vjs-upnext-svg-autoplay-ring" cx="-49" cy="49" fill-opacity="0" r="46.5" stroke="#FFFFFF" stroke-width="4" transform="rotate(-90)"></circle>
15 <polygon class="vjs-upnext-svg-autoplay-triangle" fill="#fff" points="32,27 72,49 32,71"></polygon></svg>
16 </div>
17 <span class="vjs-upnext-bottom">
18 <span class="vjs-upnext-cancel">
19 <button class="vjs-upnext-cancel-button" tabindex="0" aria-label="Cancel autoplay">${options.cancelText}</button>
20 </span>
21 <span class="vjs-upnext-suspended">${options.suspendedText}</span>
22 </span>
23 `
24}
25
26// @ts-ignore-start
27const Component = videojs.getComponent('Component')
28class EndCard extends Component {
29 options_: any
30 dashOffsetTotal = 586
31 dashOffsetStart = 293
32 interval = 50
33 upNextEvents = new videojs.EventTarget()
34 ticks = 0
35 totalTicks: number
36
37 container: HTMLElement
38 title: HTMLElement
39 autoplayRing: HTMLElement
40 cancelButton: HTMLElement
41 suspendedMessage: HTMLElement
42 nextButton: HTMLElement
43
44 constructor (player: videojs.Player, options: any) {
45 super(player, options)
46
47 this.totalTicks = this.options_.timeout / this.interval
48
49 player.on('ended', (_: any) => {
50 if (!this.options_.condition()) return
51
52 player.addClass('vjs-upnext--showing')
53 this.showCard((canceled: boolean) => {
54 player.removeClass('vjs-upnext--showing')
55 this.container.style.display = 'none'
56 if (!canceled) {
57 this.options_.next()
58 }
59 })
60 })
61
62 player.on('playing', () => {
63 this.upNextEvents.trigger('playing')
64 })
65 }
66
67 createEl () {
68 const container = super.createEl('div', {
69 className: 'vjs-upnext-content',
70 innerHTML: getMainTemplate(this.options_)
71 })
72
73 this.container = container
74 container.style.display = 'none'
75
76 this.autoplayRing = container.getElementsByClassName('vjs-upnext-svg-autoplay-ring')[0]
77 this.title = container.getElementsByClassName('vjs-upnext-title')[0]
78 this.cancelButton = container.getElementsByClassName('vjs-upnext-cancel-button')[0]
79 this.suspendedMessage = container.getElementsByClassName('vjs-upnext-suspended')[0]
80 this.nextButton = container.getElementsByClassName('vjs-upnext-autoplay-icon')[0]
81
82 this.cancelButton.onclick = () => {
83 this.upNextEvents.trigger('cancel')
84 }
85
86 this.nextButton.onclick = () => {
87 this.upNextEvents.trigger('next')
88 }
89
90 return container
91 }
92 5
93 showCard (cb: Function) {
94 let timeout: any
95
96 this.autoplayRing.setAttribute('stroke-dasharray', '' + this.dashOffsetStart)
97 this.autoplayRing.setAttribute('stroke-dashoffset', '' + -this.dashOffsetStart)
98
99 this.title.innerHTML = this.options_.getTitle()
100
101 this.upNextEvents.one('cancel', () => {
102 clearTimeout(timeout)
103 cb(true)
104 })
105
106 this.upNextEvents.one('playing', () => {
107 clearTimeout(timeout)
108 cb(true)
109 })
110
111 this.upNextEvents.one('next', () => {
112 clearTimeout(timeout)
113 cb(false)
114 })
115
116 const goToPercent = (percent: number) => {
117 const newOffset = Math.max(-this.dashOffsetTotal, - this.dashOffsetStart - percent * this.dashOffsetTotal / 2 / 100)
118 this.autoplayRing.setAttribute('stroke-dashoffset', '' + newOffset)
119 }
120
121 const tick = () => {
122 goToPercent((this.ticks++) * 100 / this.totalTicks)
123 }
124
125 const update = () => {
126 if (this.options_.suspended()) {
127 this.suspendedMessage.innerText = this.options_.suspendedText
128 goToPercent(0)
129 this.ticks = 0
130 timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
131 } else if (this.ticks >= this.totalTicks) {
132 clearTimeout(timeout)
133 cb(false)
134 } else {
135 this.suspendedMessage.innerText = ''
136 tick()
137 timeout = setTimeout(update.bind(this), this.interval)
138 }
139 }
140
141 this.container.style.display = 'block'
142 timeout = setTimeout(update.bind(this), this.interval)
143 }
144}
145// @ts-ignore-end
146
147videojs.registerComponent('EndCard', EndCard)
148
149const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
150class UpNextPlugin extends Plugin { 6class UpNextPlugin extends Plugin {
151 constructor (player: videojs.Player, options: any = {}) { 7
8 constructor (player: videojs.Player, options: Partial<EndCardOptions> = {}) {
152 const settings = { 9 const settings = {
153 next: options.next, 10 next: options.next,
154 getTitle: options.getTitle, 11 getTitle: options.getTitle,
@@ -160,7 +17,7 @@ class UpNextPlugin extends Plugin {
160 suspended: options.suspended 17 suspended: options.suspended
161 } 18 }
162 19
163 super(player, settings) 20 super(player)
164 21
165 this.player.ready(() => { 22 this.player.ready(() => {
166 player.addClass('vjs-upnext') 23 player.addClass('vjs-upnext')
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 38f2482eb..fa902e1f1 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -51,8 +51,9 @@ function buildVideoLink (options: {
51 : window.location.origin + window.location.pathname.replace('/embed/', '/watch/') 51 : window.location.origin + window.location.pathname.replace('/embed/', '/watch/')
52 52
53 const params = new URLSearchParams(window.location.search) 53 const params = new URLSearchParams(window.location.search)
54 // Remove this unused parameter when we are on a playlist page 54 // Remove these unused parameters when we are on a playlist page
55 params.delete('videoId') 55 params.delete('videoId')
56 params.delete('resume')
56 57
57 if (options.startTime) { 58 if (options.startTime) {
58 const startTimeInt = Math.floor(options.startTime) 59 const startTimeInt = Math.floor(options.startTime)
diff --git a/client/src/assets/player/videojs-components/next-video-button.ts b/client/src/assets/player/videojs-components/next-video-button.ts
index bf5c1aba4..22b32f06b 100644
--- a/client/src/assets/player/videojs-components/next-video-button.ts
+++ b/client/src/assets/player/videojs-components/next-video-button.ts
@@ -1,21 +1,25 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 1import videojs from 'video.js'
2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore
4import { Player } from 'video.js'
5 2
6const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 3const Button = videojs.getComponent('Button')
4
5export interface NextVideoButtonOptions extends videojs.ComponentOptions {
6 handler: Function
7}
7 8
8class NextVideoButton extends Button { 9class NextVideoButton extends Button {
10 private readonly nextVideoButtonOptions: NextVideoButtonOptions
9 11
10 constructor (player: Player, options: any) { 12 constructor (player: videojs.Player, options?: NextVideoButtonOptions) {
11 super(player, options) 13 super(player, options)
14
15 this.nextVideoButtonOptions = options
12 } 16 }
13 17
14 createEl () { 18 createEl () {
15 const button = videojsUntyped.dom.createEl('button', { 19 const button = videojs.dom.createEl('button', {
16 className: 'vjs-next-video' 20 className: 'vjs-next-video'
17 }) 21 }) as HTMLButtonElement
18 const nextIcon = videojsUntyped.dom.createEl('span', { 22 const nextIcon = videojs.dom.createEl('span', {
19 className: 'icon icon-next' 23 className: 'icon icon-next'
20 }) 24 })
21 button.appendChild(nextIcon) 25 button.appendChild(nextIcon)
@@ -26,11 +30,8 @@ class NextVideoButton extends Button {
26 } 30 }
27 31
28 handleClick () { 32 handleClick () {
29 this.options_.handler() 33 this.nextVideoButtonOptions.handler()
30 } 34 }
31
32} 35}
33 36
34NextVideoButton.prototype.controlText_ = 'Next video' 37videojs.registerComponent('NextVideoButton', NextVideoButton)
35
36NextVideoButton.registerComponent('NextVideoButton', NextVideoButton)
diff --git a/client/src/assets/player/videojs-components/p2p-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts
index 6424787b2..db6806fed 100644
--- a/client/src/assets/player/videojs-components/p2p-info-button.ts
+++ b/client/src/assets/player/videojs-components/p2p-info-button.ts
@@ -1,63 +1,64 @@
1import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 1import { PlayerNetworkInfo } from '../peertube-videojs-typings'
2import videojs from 'video.js'
2import { bytes } from '../utils' 3import { bytes } from '../utils'
3 4
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 5const Button = videojs.getComponent('Button')
5class P2pInfoButton extends Button { 6class P2pInfoButton extends Button {
6 7
7 createEl () { 8 createEl () {
8 const div = videojsUntyped.dom.createEl('div', { 9 const div = videojs.dom.createEl('div', {
9 className: 'vjs-peertube' 10 className: 'vjs-peertube'
10 }) 11 })
11 const subDivWebtorrent = videojsUntyped.dom.createEl('div', { 12 const subDivWebtorrent = videojs.dom.createEl('div', {
12 className: 'vjs-peertube-hidden' // Hide the stats before we get the info 13 className: 'vjs-peertube-hidden' // Hide the stats before we get the info
13 }) 14 }) as HTMLDivElement
14 div.appendChild(subDivWebtorrent) 15 div.appendChild(subDivWebtorrent)
15 16
16 const downloadIcon = videojsUntyped.dom.createEl('span', { 17 const downloadIcon = videojs.dom.createEl('span', {
17 className: 'icon icon-download' 18 className: 'icon icon-download'
18 }) 19 })
19 subDivWebtorrent.appendChild(downloadIcon) 20 subDivWebtorrent.appendChild(downloadIcon)
20 21
21 const downloadSpeedText = videojsUntyped.dom.createEl('span', { 22 const downloadSpeedText = videojs.dom.createEl('span', {
22 className: 'download-speed-text' 23 className: 'download-speed-text'
23 }) 24 })
24 const downloadSpeedNumber = videojsUntyped.dom.createEl('span', { 25 const downloadSpeedNumber = videojs.dom.createEl('span', {
25 className: 'download-speed-number' 26 className: 'download-speed-number'
26 }) 27 })
27 const downloadSpeedUnit = videojsUntyped.dom.createEl('span') 28 const downloadSpeedUnit = videojs.dom.createEl('span')
28 downloadSpeedText.appendChild(downloadSpeedNumber) 29 downloadSpeedText.appendChild(downloadSpeedNumber)
29 downloadSpeedText.appendChild(downloadSpeedUnit) 30 downloadSpeedText.appendChild(downloadSpeedUnit)
30 subDivWebtorrent.appendChild(downloadSpeedText) 31 subDivWebtorrent.appendChild(downloadSpeedText)
31 32
32 const uploadIcon = videojsUntyped.dom.createEl('span', { 33 const uploadIcon = videojs.dom.createEl('span', {
33 className: 'icon icon-upload' 34 className: 'icon icon-upload'
34 }) 35 })
35 subDivWebtorrent.appendChild(uploadIcon) 36 subDivWebtorrent.appendChild(uploadIcon)
36 37
37 const uploadSpeedText = videojsUntyped.dom.createEl('span', { 38 const uploadSpeedText = videojs.dom.createEl('span', {
38 className: 'upload-speed-text' 39 className: 'upload-speed-text'
39 }) 40 })
40 const uploadSpeedNumber = videojsUntyped.dom.createEl('span', { 41 const uploadSpeedNumber = videojs.dom.createEl('span', {
41 className: 'upload-speed-number' 42 className: 'upload-speed-number'
42 }) 43 })
43 const uploadSpeedUnit = videojsUntyped.dom.createEl('span') 44 const uploadSpeedUnit = videojs.dom.createEl('span')
44 uploadSpeedText.appendChild(uploadSpeedNumber) 45 uploadSpeedText.appendChild(uploadSpeedNumber)
45 uploadSpeedText.appendChild(uploadSpeedUnit) 46 uploadSpeedText.appendChild(uploadSpeedUnit)
46 subDivWebtorrent.appendChild(uploadSpeedText) 47 subDivWebtorrent.appendChild(uploadSpeedText)
47 48
48 const peersText = videojsUntyped.dom.createEl('span', { 49 const peersText = videojs.dom.createEl('span', {
49 className: 'peers-text' 50 className: 'peers-text'
50 }) 51 })
51 const peersNumber = videojsUntyped.dom.createEl('span', { 52 const peersNumber = videojs.dom.createEl('span', {
52 className: 'peers-number' 53 className: 'peers-number'
53 }) 54 })
54 subDivWebtorrent.appendChild(peersNumber) 55 subDivWebtorrent.appendChild(peersNumber)
55 subDivWebtorrent.appendChild(peersText) 56 subDivWebtorrent.appendChild(peersText)
56 57
57 const subDivHttp = videojsUntyped.dom.createEl('div', { 58 const subDivHttp = videojs.dom.createEl('div', {
58 className: 'vjs-peertube-hidden' 59 className: 'vjs-peertube-hidden'
59 }) 60 })
60 const subDivHttpText = videojsUntyped.dom.createEl('span', { 61 const subDivHttpText = videojs.dom.createEl('span', {
61 className: 'http-fallback', 62 className: 'http-fallback',
62 textContent: 'HTTP' 63 textContent: 'HTTP'
63 }) 64 })
@@ -83,8 +84,8 @@ class P2pInfoButton extends Button {
83 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded) 84 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
84 const numPeers = p2pStats.numPeers 85 const numPeers = p2pStats.numPeers
85 86
86 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + 87 subDivWebtorrent.title = this.player().localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
87 this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) 88 this.player().localize('Total uploaded: ' + totalUploaded.join(' '))
88 89
89 downloadSpeedNumber.textContent = downloadSpeed[ 0 ] 90 downloadSpeedNumber.textContent = downloadSpeed[ 0 ]
90 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ] 91 downloadSpeedUnit.textContent = ' ' + downloadSpeed[ 1 ]
@@ -92,14 +93,15 @@ class P2pInfoButton extends Button {
92 uploadSpeedNumber.textContent = uploadSpeed[ 0 ] 93 uploadSpeedNumber.textContent = uploadSpeed[ 0 ]
93 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] 94 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
94 95
95 peersNumber.textContent = numPeers 96 peersNumber.textContent = numPeers.toString()
96 peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer')) 97 peersText.textContent = ' ' + (numPeers > 1 ? this.player().localize('peers') : this.player_.localize('peer'))
97 98
98 subDivHttp.className = 'vjs-peertube-hidden' 99 subDivHttp.className = 'vjs-peertube-hidden'
99 subDivWebtorrent.className = 'vjs-peertube-displayed' 100 subDivWebtorrent.className = 'vjs-peertube-displayed'
100 }) 101 })
101 102
102 return div 103 return div as HTMLButtonElement
103 } 104 }
104} 105}
105Button.registerComponent('P2PInfoButton', P2pInfoButton) 106
107videojs.registerComponent('P2PInfoButton', P2pInfoButton)
diff --git a/client/src/assets/player/videojs-components/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts
index 4d0ea37f5..63e92eb77 100644
--- a/client/src/assets/player/videojs-components/peertube-link-button.ts
+++ b/client/src/assets/player/videojs-components/peertube-link-button.ts
@@ -1,13 +1,10 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { buildVideoLink } from '../utils' 1import { buildVideoLink } from '../utils'
3// FIXME: something weird with our path definition in tsconfig and typings 2import videojs from 'video.js'
4// @ts-ignore
5import { Player } from 'video.js'
6 3
7const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button = videojs.getComponent('Button')
8class PeerTubeLinkButton extends Button { 5class PeerTubeLinkButton extends Button {
9 6
10 constructor (player: Player, options: any) { 7 constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
11 super(player, options) 8 super(player, options)
12 } 9 }
13 10
@@ -20,21 +17,22 @@ class PeerTubeLinkButton extends Button {
20 } 17 }
21 18
22 handleClick () { 19 handleClick () {
23 this.player_.pause() 20 this.player().pause()
24 } 21 }
25 22
26 private buildElement () { 23 private buildElement () {
27 const el = videojsUntyped.dom.createEl('a', { 24 const el = videojs.dom.createEl('a', {
28 href: buildVideoLink(), 25 href: buildVideoLink(),
29 innerHTML: 'PeerTube', 26 innerHTML: 'PeerTube',
30 title: this.player_.localize('Go to the video page'), 27 title: this.player().localize('Go to the video page'),
31 className: 'vjs-peertube-link', 28 className: 'vjs-peertube-link',
32 target: '_blank' 29 target: '_blank'
33 }) 30 })
34 31
35 el.addEventListener('mouseenter', () => this.updateHref()) 32 el.addEventListener('mouseenter', () => this.updateHref())
36 33
37 return el 34 return el as HTMLButtonElement
38 } 35 }
39} 36}
40Button.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton) 37
38videojs.registerComponent('PeerTubeLinkButton', PeerTubeLinkButton)
diff --git a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
index b594fc1c5..7869b56ce 100644
--- a/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
+++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
@@ -1,16 +1,12 @@
1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 1import videojs from 'video.js'
2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore
4import { Player } from 'video.js'
5 2
6const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 3const Component = videojs.getComponent('Component')
7 4
8class PeerTubeLoadProgressBar extends Component { 5class PeerTubeLoadProgressBar extends Component {
9 partEls_: any[]
10 6
11 constructor (player: Player, options: any) { 7 constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
12 super(player, options) 8 super(player, options)
13 this.partEls_ = [] 9
14 this.on(player, 'progress', this.update) 10 this.on(player, 'progress', this.update)
15 } 11 }
16 12
@@ -22,8 +18,6 @@ class PeerTubeLoadProgressBar extends Component {
22 } 18 }
23 19
24 dispose () { 20 dispose () {
25 this.partEls_ = null
26
27 super.dispose() 21 super.dispose()
28 } 22 }
29 23
@@ -31,7 +25,8 @@ class PeerTubeLoadProgressBar extends Component {
31 const torrent = this.player().webtorrent().getTorrent() 25 const torrent = this.player().webtorrent().getTorrent()
32 if (!torrent) return 26 if (!torrent) return
33 27
34 this.el_.style.width = (torrent.progress * 100) + '%' 28 // FIXME: typings
29 (this.el() as HTMLElement).style.width = (torrent.progress * 100) + '%'
35 } 30 }
36 31
37} 32}
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
index 2de3ece19..98e7f56fc 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-button.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -1,22 +1,19 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore
3import { Player } from 'video.js'
4 2
5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 3import { LoadedQualityData } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item' 4import { ResolutionMenuItem } from './resolution-menu-item'
7 5
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 6const Menu = videojs.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') 7const MenuButton = videojs.getComponent('MenuButton')
10class ResolutionMenuButton extends MenuButton { 8class ResolutionMenuButton extends MenuButton {
11 label: HTMLElement 9 labelEl_: HTMLElement
12 labelEl_: any
13 player: Player
14 10
15 constructor (player: Player, options: any) { 11 constructor (player: videojs.Player, options?: videojs.MenuButtonOptions) {
16 super(player, options) 12 super(player, options)
17 this.player = player
18 13
19 player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data)) 14 this.controlText('Quality')
15
16 player.tech(true).on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
20 17
21 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0)) 18 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0))
22 } 19 }
@@ -24,9 +21,9 @@ class ResolutionMenuButton extends MenuButton {
24 createEl () { 21 createEl () {
25 const el = super.createEl() 22 const el = super.createEl()
26 23
27 this.labelEl_ = videojsUntyped.dom.createEl('div', { 24 this.labelEl_ = videojs.dom.createEl('div', {
28 className: 'vjs-resolution-value' 25 className: 'vjs-resolution-value'
29 }) 26 }) as HTMLElement
30 27
31 el.appendChild(this.labelEl_) 28 el.appendChild(this.labelEl_)
32 29
@@ -55,7 +52,7 @@ class ResolutionMenuButton extends MenuButton {
55 52
56 for (const child of children) { 53 for (const child of children) {
57 if (component !== child) { 54 if (component !== child) {
58 child.selected(false) 55 (child as videojs.MenuItem).selected(false)
59 } 56 }
60 } 57 }
61 }) 58 })
@@ -76,7 +73,7 @@ class ResolutionMenuButton extends MenuButton {
76 if (d.id === -1) continue 73 if (d.id === -1) continue
77 74
78 const label = d.label === '0p' 75 const label = d.label === '0p'
79 ? this.player.localize('Audio-only') 76 ? this.player().localize('Audio-only')
80 : d.label 77 : d.label
81 78
82 this.menu.addChild(new ResolutionMenuItem( 79 this.menu.addChild(new ResolutionMenuItem(
@@ -110,6 +107,5 @@ class ResolutionMenuButton extends MenuButton {
110 this.trigger('menuChanged') 107 this.trigger('menuChanged')
111 } 108 }
112} 109}
113ResolutionMenuButton.prototype.controlText_ = 'Quality'
114 110
115MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton) 111videojs.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/videojs-components/resolution-menu-item.ts b/client/src/assets/player/videojs-components/resolution-menu-item.ts
index 6c42fefd2..73ad47d2b 100644
--- a/client/src/assets/player/videojs-components/resolution-menu-item.ts
+++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts
@@ -1,12 +1,16 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore 2import { AutoResolutionUpdateData, ResolutionUpdateData } from '../peertube-videojs-typings'
3import { Player } from 'video.js'
4 3
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 4const MenuItem = videojs.getComponent('MenuItem')
5
6export interface ResolutionMenuItemOptions extends videojs.MenuItemOptions {
7 labels?: { [id: number]: string }
8 id: number
9 callback: Function
10}
6 11
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class ResolutionMenuItem extends MenuItem { 12class ResolutionMenuItem extends MenuItem {
9 private readonly id: number 13 private readonly resolutionId: number
10 private readonly label: string 14 private readonly label: string
11 // Only used for the automatic item 15 // Only used for the automatic item
12 private readonly labels: { [id: number]: string } 16 private readonly labels: { [id: number]: string }
@@ -15,7 +19,7 @@ class ResolutionMenuItem extends MenuItem {
15 private autoResolutionPossible: boolean 19 private autoResolutionPossible: boolean
16 private currentResolutionLabel: string 20 private currentResolutionLabel: string
17 21
18 constructor (player: Player, options: any) { 22 constructor (player: videojs.Player, options?: ResolutionMenuItemOptions) {
19 options.selectable = true 23 options.selectable = true
20 24
21 super(player, options) 25 super(player, options)
@@ -23,40 +27,40 @@ class ResolutionMenuItem extends MenuItem {
23 this.autoResolutionPossible = true 27 this.autoResolutionPossible = true
24 this.currentResolutionLabel = '' 28 this.currentResolutionLabel = ''
25 29
30 this.resolutionId = options.id
26 this.label = options.label 31 this.label = options.label
27 this.labels = options.labels 32 this.labels = options.labels
28 this.id = options.id
29 this.callback = options.callback 33 this.callback = options.callback
30 34
31 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data)) 35 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
32 36
33 // We only want to disable the "Auto" item 37 // We only want to disable the "Auto" item
34 if (this.id === -1) { 38 if (this.resolutionId === -1) {
35 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data)) 39 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
36 } 40 }
37 } 41 }
38 42
39 handleClick (event: any) { 43 handleClick (event: any) {
40 // Auto button disabled? 44 // Auto button disabled?
41 if (this.autoResolutionPossible === false && this.id === -1) return 45 if (this.autoResolutionPossible === false && this.resolutionId === -1) return
42 46
43 super.handleClick(event) 47 super.handleClick(event)
44 48
45 this.callback(this.id, 'video') 49 this.callback(this.resolutionId, 'video')
46 } 50 }
47 51
48 updateSelection (data: ResolutionUpdateData) { 52 updateSelection (data: ResolutionUpdateData) {
49 if (this.id === -1) { 53 if (this.resolutionId === -1) {
50 this.currentResolutionLabel = this.labels[data.id] 54 this.currentResolutionLabel = this.labels[data.id]
51 } 55 }
52 56
53 // Automatic resolution only 57 // Automatic resolution only
54 if (data.auto === true) { 58 if (data.auto === true) {
55 this.selected(this.id === -1) 59 this.selected(this.resolutionId === -1)
56 return 60 return
57 } 61 }
58 62
59 this.selected(this.id === data.id) 63 this.selected(this.resolutionId === data.id)
60 } 64 }
61 65
62 updateAutoResolution (data: AutoResolutionUpdateData) { 66 updateAutoResolution (data: AutoResolutionUpdateData) {
@@ -71,13 +75,13 @@ class ResolutionMenuItem extends MenuItem {
71 } 75 }
72 76
73 getLabel () { 77 getLabel () {
74 if (this.id === -1) { 78 if (this.resolutionId === -1) {
75 return this.label + ' <small>' + this.currentResolutionLabel + '</small>' 79 return this.label + ' <small>' + this.currentResolutionLabel + '</small>'
76 } 80 }
77 81
78 return this.label 82 return this.label
79 } 83 }
80} 84}
81MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) 85videojs.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
82 86
83export { ResolutionMenuItem } 87export { ResolutionMenuItem }
diff --git a/client/src/assets/player/videojs-components/settings-dialog.ts b/client/src/assets/player/videojs-components/settings-dialog.ts
new file mode 100644
index 000000000..41911e7e8
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-dialog.ts
@@ -0,0 +1,37 @@
1import videojs from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsDialog extends Component {
6 constructor (player: videojs.Player) {
7 super(player)
8
9 this.hide()
10 }
11
12 /**
13 * Create the component's DOM element
14 *
15 * @return {Element}
16 * @method createEl
17 */
18 createEl () {
19 const uniqueId = this.id()
20 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
21 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
22
23 return super.createEl('div', {
24 className: 'vjs-settings-dialog vjs-modal-overlay',
25 innerHTML: '',
26 tabIndex: -1
27 }, {
28 'role': 'dialog',
29 'aria-labelledby': dialogLabelId,
30 'aria-describedby': dialogDescriptionId
31 })
32 }
33}
34
35Component.registerComponent('SettingsDialog', SettingsDialog)
36
37export { SettingsDialog }
diff --git a/client/src/assets/player/videojs-components/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
index b700f4be6..011323267 100644
--- a/client/src/assets/player/videojs-components/settings-menu-button.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -1,43 +1,52 @@
1// Author: Yanko Shterev 1// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
6import * as videojs from 'video.js'
7
8import { SettingsMenuItem } from './settings-menu-item' 2import { SettingsMenuItem } from './settings-menu-item'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10import { toTitleCase } from '../utils' 3import { toTitleCase } from '../utils'
4import videojs from 'video.js'
5
6import { SettingsDialog } from './settings-dialog'
7import { SettingsPanel } from './settings-panel'
8import { SettingsPanelChild } from './settings-panel-child'
11 9
12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 10const Button = videojs.getComponent('Button')
13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 11const Menu = videojs.getComponent('Menu')
14const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 12const Component = videojs.getComponent('Component')
13
14export interface SettingsButtonOptions extends videojs.ComponentOptions {
15 entries: any[]
16 setup?: {
17 maxHeightOffset: number
18 }
19}
15 20
16class SettingsButton extends Button { 21class SettingsButton extends Button {
17 playerComponent = videojs.Player 22 dialog: SettingsDialog
18 dialog: any 23 dialogEl: HTMLElement
19 dialogEl: any 24 menu: videojs.Menu
20 menu: any 25 panel: SettingsPanel
21 panel: any 26 panelChild: SettingsPanelChild
22 panelChild: any 27
23 28 addSettingsItemHandler: typeof SettingsButton.prototype.onAddSettingsItem
24 addSettingsItemHandler: Function 29 disposeSettingsItemHandler: typeof SettingsButton.prototype.onDisposeSettingsItem
25 disposeSettingsItemHandler: Function 30 playerClickHandler: typeof SettingsButton.prototype.onPlayerClick
26 playerClickHandler: Function 31 userInactiveHandler: typeof SettingsButton.prototype.onUserInactive
27 userInactiveHandler: Function 32
28 33 private settingsButtonOptions: SettingsButtonOptions
29 constructor (player: videojs.Player, options: any) { 34
35 constructor (player: videojs.Player, options?: SettingsButtonOptions) {
30 super(player, options) 36 super(player, options)
31 37
32 this.playerComponent = player 38 this.settingsButtonOptions = options
33 this.dialog = this.playerComponent.addChild('settingsDialog') 39
34 this.dialogEl = this.dialog.el_ 40 this.controlText('Settings')
41
42 this.dialog = this.player().addChild('settingsDialog')
43 this.dialogEl = this.dialog.el() as HTMLElement
35 this.menu = null 44 this.menu = null
36 this.panel = this.dialog.addChild('settingsPanel') 45 this.panel = this.dialog.addChild('settingsPanel')
37 this.panelChild = this.panel.addChild('settingsPanelChild') 46 this.panelChild = this.panel.addChild('settingsPanelChild')
38 47
39 this.addClass('vjs-settings') 48 this.addClass('vjs-settings')
40 this.el_.setAttribute('aria-label', 'Settings Button') 49 this.el().setAttribute('aria-label', 'Settings Button')
41 50
42 // Event handlers 51 // Event handlers
43 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this) 52 this.addSettingsItemHandler = this.onAddSettingsItem.bind(this)
@@ -84,7 +93,7 @@ class SettingsButton extends Button {
84 93
85 this.hideDialog() 94 this.hideDialog()
86 95
87 if (this.options_.entries.length === 0) { 96 if (this.settingsButtonOptions.entries.length === 0) {
88 this.addClass('vjs-hidden') 97 this.addClass('vjs-hidden')
89 } 98 }
90 } 99 }
@@ -103,10 +112,10 @@ class SettingsButton extends Button {
103 } 112 }
104 113
105 bindEvents () { 114 bindEvents () {
106 this.playerComponent.on('click', this.playerClickHandler) 115 this.player().on('click', this.playerClickHandler)
107 this.playerComponent.on('addsettingsitem', this.addSettingsItemHandler) 116 this.player().on('addsettingsitem', this.addSettingsItemHandler)
108 this.playerComponent.on('disposesettingsitem', this.disposeSettingsItemHandler) 117 this.player().on('disposesettingsitem', this.disposeSettingsItemHandler)
109 this.playerComponent.on('userinactive', this.userInactiveHandler) 118 this.player().on('userinactive', this.userInactiveHandler)
110 } 119 }
111 120
112 buildCSSClass () { 121 buildCSSClass () {
@@ -122,9 +131,9 @@ class SettingsButton extends Button {
122 } 131 }
123 132
124 showDialog () { 133 showDialog () {
125 this.player_.peertube().onMenuOpen() 134 this.player().peertube().onMenuOpen();
126 135
127 this.menu.el_.style.opacity = '1' 136 (this.menu.el() as HTMLElement).style.opacity = '1'
128 this.dialog.show() 137 this.dialog.show()
129 138
130 this.setDialogSize(this.getComponentSize(this.menu)) 139 this.setDialogSize(this.getComponentSize(this.menu))
@@ -134,23 +143,24 @@ class SettingsButton extends Button {
134 this.player_.peertube().onMenuClosed() 143 this.player_.peertube().onMenuClosed()
135 144
136 this.dialog.hide() 145 this.dialog.hide()
137 this.setDialogSize(this.getComponentSize(this.menu)) 146 this.setDialogSize(this.getComponentSize(this.menu));
138 this.menu.el_.style.opacity = '1' 147 (this.menu.el() as HTMLElement).style.opacity = '1'
139 this.resetChildren() 148 this.resetChildren()
140 } 149 }
141 150
142 getComponentSize (element: any) { 151 getComponentSize (element: videojs.Component | HTMLElement) {
143 let width: number = null 152 let width: number = null
144 let height: number = null 153 let height: number = null
145 154
146 // Could be component or just DOM element 155 // Could be component or just DOM element
147 if (element instanceof Component) { 156 if (element instanceof Component) {
148 width = element.el_.offsetWidth 157 const el = element.el() as HTMLElement
149 height = element.el_.offsetHeight 158
159 width = el.offsetWidth
160 height = el.offsetHeight;
150 161
151 // keep width/height as properties for direct use 162 (element as any).width = width;
152 element.width = width 163 (element as any).height = height
153 element.height = height
154 } else { 164 } else {
155 width = element.offsetWidth 165 width = element.offsetWidth
156 height = element.offsetHeight 166 height = element.offsetHeight
@@ -164,15 +174,17 @@ class SettingsButton extends Button {
164 return 174 return
165 } 175 }
166 176
167 const offset = this.options_.setup.maxHeightOffset 177 const offset = this.settingsButtonOptions.setup.maxHeightOffset
168 const maxHeight = this.playerComponent.el_.offsetHeight - offset 178 const maxHeight = (this.player().el() as HTMLElement).offsetHeight - offset // FIXME: typings
179
180 const panelEl = this.panel.el() as HTMLElement
169 181
170 if (height > maxHeight) { 182 if (height > maxHeight) {
171 height = maxHeight 183 height = maxHeight
172 width += 17 184 width += 17
173 this.panel.el_.style.maxHeight = `${height}px` 185 panelEl.style.maxHeight = `${height}px`
174 } else if (this.panel.el_.style.maxHeight !== '') { 186 } else if (panelEl.style.maxHeight !== '') {
175 this.panel.el_.style.maxHeight = '' 187 panelEl.style.maxHeight = ''
176 } 188 }
177 189
178 this.dialogEl.style.width = `${width}px` 190 this.dialogEl.style.width = `${width}px`
@@ -182,7 +194,7 @@ class SettingsButton extends Button {
182 buildMenu () { 194 buildMenu () {
183 this.menu = new Menu(this.player()) 195 this.menu = new Menu(this.player())
184 this.menu.addClass('vjs-main-menu') 196 this.menu.addClass('vjs-main-menu')
185 const entries = this.options_.entries 197 const entries = this.settingsButtonOptions.entries
186 198
187 if (entries.length === 0) { 199 if (entries.length === 0) {
188 this.addClass('vjs-hidden') 200 this.addClass('vjs-hidden')
@@ -191,7 +203,7 @@ class SettingsButton extends Button {
191 } 203 }
192 204
193 for (const entry of entries) { 205 for (const entry of entries) {
194 this.addMenuItem(entry, this.options_) 206 this.addMenuItem(entry, this.settingsButtonOptions)
195 } 207 }
196 208
197 this.panelChild.addChild(this.menu) 209 this.panelChild.addChild(this.menu)
@@ -199,15 +211,17 @@ class SettingsButton extends Button {
199 211
200 addMenuItem (entry: any, options: any) { 212 addMenuItem (entry: any, options: any) {
201 const openSubMenu = function (this: any) { 213 const openSubMenu = function (this: any) {
202 if (videojsUntyped.dom.hasClass(this.el_, 'open')) { 214 if (videojs.dom.hasClass(this.el_, 'open')) {
203 videojsUntyped.dom.removeClass(this.el_, 'open') 215 videojs.dom.removeClass(this.el_, 'open')
204 } else { 216 } else {
205 videojsUntyped.dom.addClass(this.el_, 'open') 217 videojs.dom.addClass(this.el_, 'open')
206 } 218 }
207 } 219 }
208 220
209 options.name = toTitleCase(entry) 221 options.name = toTitleCase(entry)
210 const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) 222
223 const newOptions = Object.assign({}, options, { entry, menuButton: this })
224 const settingsMenuItem = new SettingsMenuItem(this.player(), newOptions)
211 225
212 this.menu.addChild(settingsMenuItem) 226 this.menu.addChild(settingsMenuItem)
213 227
@@ -221,7 +235,7 @@ class SettingsButton extends Button {
221 235
222 resetChildren () { 236 resetChildren () {
223 for (const menuChild of this.menu.children()) { 237 for (const menuChild of this.menu.children()) {
224 menuChild.reset() 238 (menuChild as SettingsMenuItem).reset()
225 } 239 }
226 } 240 }
227 241
@@ -230,75 +244,12 @@ class SettingsButton extends Button {
230 */ 244 */
231 hideChildren () { 245 hideChildren () {
232 for (const menuChild of this.menu.children()) { 246 for (const menuChild of this.menu.children()) {
233 menuChild.hideSubMenu() 247 (menuChild as SettingsMenuItem).hideSubMenu()
234 } 248 }
235 } 249 }
236 250
237} 251}
238 252
239class SettingsPanel extends Component {
240 constructor (player: videojs.Player, options: any) {
241 super(player, options)
242 }
243
244 createEl () {
245 return super.createEl('div', {
246 className: 'vjs-settings-panel',
247 innerHTML: '',
248 tabIndex: -1
249 })
250 }
251}
252
253class SettingsPanelChild extends Component {
254 constructor (player: videojs.Player, options: any) {
255 super(player, options)
256 }
257
258 createEl () {
259 return super.createEl('div', {
260 className: 'vjs-settings-panel-child',
261 innerHTML: '',
262 tabIndex: -1
263 })
264 }
265}
266
267class SettingsDialog extends Component {
268 constructor (player: videojs.Player, options: any) {
269 super(player, options)
270 this.hide()
271 }
272
273 /**
274 * Create the component's DOM element
275 *
276 * @return {Element}
277 * @method createEl
278 */
279 createEl () {
280 const uniqueId = this.id_
281 const dialogLabelId = 'TTsettingsDialogLabel-' + uniqueId
282 const dialogDescriptionId = 'TTsettingsDialogDescription-' + uniqueId
283
284 return super.createEl('div', {
285 className: 'vjs-settings-dialog vjs-modal-overlay',
286 innerHTML: '',
287 tabIndex: -1
288 }, {
289 'role': 'dialog',
290 'aria-labelledby': dialogLabelId,
291 'aria-describedby': dialogDescriptionId
292 })
293 }
294
295}
296
297SettingsButton.prototype.controlText_ = 'Settings'
298
299Component.registerComponent('SettingsButton', SettingsButton) 253Component.registerComponent('SettingsButton', SettingsButton)
300Component.registerComponent('SettingsDialog', SettingsDialog)
301Component.registerComponent('SettingsPanel', SettingsPanel)
302Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
303 254
304export { SettingsButton, SettingsDialog, SettingsPanel, SettingsPanelChild } 255export { SettingsButton }
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
index 84d394c0e..f1342f179 100644
--- a/client/src/assets/player/videojs-components/settings-menu-item.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -1,57 +1,63 @@
1// Author: Yanko Shterev 1// Thanks to Yanko Shterev: https://github.com/yshterev/videojs-settings-menu
2// Thanks https://github.com/yshterev/videojs-settings-menu
3
4// FIXME: something weird with our path definition in tsconfig and typings
5// @ts-ignore
6import * as videojs from 'video.js'
7
8import { toTitleCase } from '../utils' 2import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' 3import videojs from 'video.js'
10 4import { SettingsButton } from './settings-menu-button'
11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') 5import { SettingsDialog } from './settings-dialog'
12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 6import { SettingsPanel } from './settings-panel'
7import { SettingsPanelChild } from './settings-panel-child'
8
9const MenuItem = videojs.getComponent('MenuItem')
10const component = videojs.getComponent('Component')
11
12export interface SettingsMenuItemOptions extends videojs.MenuItemOptions {
13 entry: string
14 menuButton: SettingsButton
15}
13 16
14class SettingsMenuItem extends MenuItem { 17class SettingsMenuItem extends MenuItem {
15 settingsButton: any 18 settingsButton: SettingsButton
16 dialog: any 19 dialog: SettingsDialog
17 mainMenu: any 20 mainMenu: videojs.Menu
18 panel: any 21 panel: SettingsPanel
19 panelChild: any 22 panelChild: SettingsPanelChild
20 panelChildEl: any 23 panelChildEl: HTMLElement
21 size: any 24 size: number[]
22 menuToLoad: string 25 menuToLoad: string
23 subMenu: any 26 subMenu: SettingsButton
24 27
25 submenuClickHandler: Function 28 submenuClickHandler: typeof SettingsMenuItem.prototype.onSubmenuClick
26 transitionEndHandler: Function 29 transitionEndHandler: typeof SettingsMenuItem.prototype.onTransitionEnd
27 30
28 settingsSubMenuTitleEl_: any 31 settingsSubMenuTitleEl_: HTMLElement
29 settingsSubMenuValueEl_: any 32 settingsSubMenuValueEl_: HTMLElement
30 settingsSubMenuEl_: any 33 settingsSubMenuEl_: HTMLElement
31 34
32 constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { 35 constructor (player: videojs.Player, options?: SettingsMenuItemOptions) {
33 super(player, options) 36 super(player, options)
34 37
35 this.settingsButton = menuButton 38 this.settingsButton = options.menuButton
36 this.dialog = this.settingsButton.dialog 39 this.dialog = this.settingsButton.dialog
37 this.mainMenu = this.settingsButton.menu 40 this.mainMenu = this.settingsButton.menu
38 this.panel = this.dialog.getChild('settingsPanel') 41 this.panel = this.dialog.getChild('settingsPanel')
39 this.panelChild = this.panel.getChild('settingsPanelChild') 42 this.panelChild = this.panel.getChild('settingsPanelChild')
40 this.panelChildEl = this.panelChild.el_ 43 this.panelChildEl = this.panelChild.el() as HTMLElement
41 44
42 this.size = null 45 this.size = null
43 46
44 // keep state of what menu type is loading next 47 // keep state of what menu type is loading next
45 this.menuToLoad = 'mainmenu' 48 this.menuToLoad = 'mainmenu'
46 49
47 const subMenuName = toTitleCase(entry) 50 const subMenuName = toTitleCase(options.entry)
48 const SubMenuComponent = videojsUntyped.getComponent(subMenuName) 51 const SubMenuComponent = videojs.getComponent(subMenuName)
49 52
50 if (!SubMenuComponent) { 53 if (!SubMenuComponent) {
51 throw new Error(`Component ${subMenuName} does not exist`) 54 throw new Error(`Component ${subMenuName} does not exist`)
52 } 55 }
53 this.subMenu = new SubMenuComponent(this.player(), options, menuButton, this) 56
54 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[0] 57 const newOptions = Object.assign({}, options, { entry: options.menuButton, menuButton: this })
58
59 this.subMenu = new SubMenuComponent(this.player(), newOptions) as any // FIXME: typings
60 const subMenuClass = this.subMenu.buildCSSClass().split(' ')[ 0 ]
55 this.settingsSubMenuEl_.className += ' ' + subMenuClass 61 this.settingsSubMenuEl_.className += ' ' + subMenuClass
56 62
57 this.eventHandlers() 63 this.eventHandlers()
@@ -72,7 +78,7 @@ class SettingsMenuItem extends MenuItem {
72 player.on('captionsChanged', () => { 78 player.on('captionsChanged', () => {
73 setTimeout(() => { 79 setTimeout(() => {
74 this.settingsSubMenuEl_.innerHTML = '' 80 this.settingsSubMenuEl_.innerHTML = ''
75 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) 81 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
76 this.update() 82 this.update()
77 this.bindClickEvents() 83 this.bindClickEvents()
78 }, 0) 84 }, 0)
@@ -119,27 +125,27 @@ class SettingsMenuItem extends MenuItem {
119 * @method createEl 125 * @method createEl
120 */ 126 */
121 createEl () { 127 createEl () {
122 const el = videojsUntyped.dom.createEl('li', { 128 const el = videojs.dom.createEl('li', {
123 className: 'vjs-menu-item' 129 className: 'vjs-menu-item'
124 }) 130 })
125 131
126 this.settingsSubMenuTitleEl_ = videojsUntyped.dom.createEl('div', { 132 this.settingsSubMenuTitleEl_ = videojs.dom.createEl('div', {
127 className: 'vjs-settings-sub-menu-title' 133 className: 'vjs-settings-sub-menu-title'
128 }) 134 }) as HTMLElement
129 135
130 el.appendChild(this.settingsSubMenuTitleEl_) 136 el.appendChild(this.settingsSubMenuTitleEl_)
131 137
132 this.settingsSubMenuValueEl_ = videojsUntyped.dom.createEl('div', { 138 this.settingsSubMenuValueEl_ = videojs.dom.createEl('div', {
133 className: 'vjs-settings-sub-menu-value' 139 className: 'vjs-settings-sub-menu-value'
134 }) 140 }) as HTMLElement
135 141
136 el.appendChild(this.settingsSubMenuValueEl_) 142 el.appendChild(this.settingsSubMenuValueEl_)
137 143
138 this.settingsSubMenuEl_ = videojsUntyped.dom.createEl('div', { 144 this.settingsSubMenuEl_ = videojs.dom.createEl('div', {
139 className: 'vjs-settings-sub-menu' 145 className: 'vjs-settings-sub-menu'
140 }) 146 }) as HTMLElement
141 147
142 return el 148 return el as HTMLLIElement
143 } 149 }
144 150
145 /** 151 /**
@@ -147,17 +153,17 @@ class SettingsMenuItem extends MenuItem {
147 * 153 *
148 * @method handleClick 154 * @method handleClick
149 */ 155 */
150 handleClick () { 156 handleClick (event: videojs.EventTarget.Event) {
151 this.menuToLoad = 'submenu' 157 this.menuToLoad = 'submenu'
152 // Remove open class to ensure only the open submenu gets this class 158 // Remove open class to ensure only the open submenu gets this class
153 videojsUntyped.dom.removeClass(this.el_, 'open') 159 videojs.dom.removeClass(this.el(), 'open')
154 160
155 super.handleClick() 161 super.handleClick(event);
156 162
157 this.mainMenu.el_.style.opacity = '0' 163 (this.mainMenu.el() as HTMLElement).style.opacity = '0'
158 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element 164 // Whether to add or remove vjs-hidden class on the settingsSubMenuEl element
159 if (videojsUntyped.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) { 165 if (videojs.dom.hasClass(this.settingsSubMenuEl_, 'vjs-hidden')) {
160 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') 166 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
161 167
162 // animation not played without timeout 168 // animation not played without timeout
163 setTimeout(() => { 169 setTimeout(() => {
@@ -167,7 +173,7 @@ class SettingsMenuItem extends MenuItem {
167 173
168 this.settingsButton.setDialogSize(this.size) 174 this.settingsButton.setDialogSize(this.size)
169 } else { 175 } else {
170 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 176 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
171 } 177 }
172 } 178 }
173 179
@@ -178,9 +184,9 @@ class SettingsMenuItem extends MenuItem {
178 */ 184 */
179 createBackButton () { 185 createBackButton () {
180 const button = this.subMenu.menu.addChild('MenuItem', {}, 0) 186 const button = this.subMenu.menu.addChild('MenuItem', {}, 0)
181 button.name_ = 'BackButton' 187
182 button.addClass('vjs-back-button') 188 button.addClass('vjs-back-button');
183 button.el_.innerHTML = this.player_.localize(this.subMenu.controlText_) 189 (button.el() as HTMLElement).innerHTML = this.player().localize(this.subMenu.controlText())
184 } 190 }
185 191
186 /** 192 /**
@@ -189,17 +195,17 @@ class SettingsMenuItem extends MenuItem {
189 * @method PrefixedEvent 195 * @method PrefixedEvent
190 */ 196 */
191 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { 197 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
192 const prefix = ['webkit', 'moz', 'MS', 'o', ''] 198 const prefix = [ 'webkit', 'moz', 'MS', 'o', '' ]
193 199
194 for (let p = 0; p < prefix.length; p++) { 200 for (let p = 0; p < prefix.length; p++) {
195 if (!prefix[p]) { 201 if (!prefix[ p ]) {
196 type = type.toLowerCase() 202 type = type.toLowerCase()
197 } 203 }
198 204
199 if (action === 'addEvent') { 205 if (action === 'addEvent') {
200 element.addEventListener(prefix[p] + type, callback, false) 206 element.addEventListener(prefix[ p ] + type, callback, false)
201 } else if (action === 'removeEvent') { 207 } else if (action === 'removeEvent') {
202 element.removeEventListener(prefix[p] + type, callback, false) 208 element.removeEventListener(prefix[ p ] + type, callback, false)
203 } 209 }
204 } 210 }
205 } 211 }
@@ -211,7 +217,7 @@ class SettingsMenuItem extends MenuItem {
211 217
212 if (this.menuToLoad === 'mainmenu') { 218 if (this.menuToLoad === 'mainmenu') {
213 // hide submenu 219 // hide submenu
214 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 220 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
215 221
216 // reset opacity to 0 222 // reset opacity to 0
217 this.settingsSubMenuEl_.style.opacity = '0' 223 this.settingsSubMenuEl_.style.opacity = '0'
@@ -219,25 +225,27 @@ class SettingsMenuItem extends MenuItem {
219 } 225 }
220 226
221 reset () { 227 reset () {
222 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 228 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
223 this.settingsSubMenuEl_.style.opacity = '0' 229 this.settingsSubMenuEl_.style.opacity = '0'
224 this.setMargin() 230 this.setMargin()
225 } 231 }
226 232
227 loadMainMenu () { 233 loadMainMenu () {
234 const mainMenuEl = this.mainMenu.el() as HTMLElement
228 this.menuToLoad = 'mainmenu' 235 this.menuToLoad = 'mainmenu'
229 this.mainMenu.show() 236 this.mainMenu.show()
230 this.mainMenu.el_.style.opacity = '0' 237 mainMenuEl.style.opacity = '0'
231 238
232 // back button will always take you to main menu, so set dialog sizes 239 // back button will always take you to main menu, so set dialog sizes
233 this.settingsButton.setDialogSize([this.mainMenu.width, this.mainMenu.height]) 240 const mainMenuAny = this.mainMenu as any
241 this.settingsButton.setDialogSize([ mainMenuAny.width, mainMenuAny.height ])
234 242
235 // animation not triggered without timeout (some async stuff ?!?) 243 // animation not triggered without timeout (some async stuff ?!?)
236 setTimeout(() => { 244 setTimeout(() => {
237 // animate margin and opacity before hiding the submenu 245 // animate margin and opacity before hiding the submenu
238 // this triggers CSS Transition event 246 // this triggers CSS Transition event
239 this.setMargin() 247 this.setMargin()
240 this.mainMenu.el_.style.opacity = '1' 248 mainMenuEl.style.opacity = '1'
241 }, 0) 249 }, 0)
242 } 250 }
243 251
@@ -251,8 +259,8 @@ class SettingsMenuItem extends MenuItem {
251 this.update() 259 this.update()
252 }) 260 })
253 261
254 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) 262 this.settingsSubMenuTitleEl_.innerHTML = this.player().localize(this.subMenu.controlText())
255 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) 263 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el())
256 this.panelChildEl.appendChild(this.settingsSubMenuEl_) 264 this.panelChildEl.appendChild(this.settingsSubMenuEl_)
257 this.update() 265 this.update()
258 266
@@ -283,7 +291,8 @@ class SettingsMenuItem extends MenuItem {
283 // or sets options_['selected'] on the selected playback rate. 291 // or sets options_['selected'] on the selected playback rate.
284 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton 292 // Thus we get the submenu value based on the labelEl of playbackRateMenuButton
285 if (subMenu === 'PlaybackRateMenuButton') { 293 if (subMenu === 'PlaybackRateMenuButton') {
286 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) 294 const html = (this.subMenu as any).labelEl_.innerHTML
295 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = html, 250)
287 } else { 296 } else {
288 // Loop trough the submenu items to find the selected child 297 // Loop trough the submenu items to find the selected child
289 for (const subMenuItem of this.subMenu.menu.children_) { 298 for (const subMenuItem of this.subMenu.menu.children_) {
@@ -292,13 +301,15 @@ class SettingsMenuItem extends MenuItem {
292 } 301 }
293 302
294 if (subMenuItem.hasClass('vjs-selected')) { 303 if (subMenuItem.hasClass('vjs-selected')) {
304 const subMenuItemUntyped = subMenuItem as any
305
295 // Prefer to use the function 306 // Prefer to use the function
296 if (typeof subMenuItem.getLabel === 'function') { 307 if (typeof subMenuItemUntyped.getLabel === 'function') {
297 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.getLabel() 308 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.getLabel()
298 break 309 break
299 } 310 }
300 311
301 this.settingsSubMenuValueEl_.innerHTML = subMenuItem.options_.label 312 this.settingsSubMenuValueEl_.innerHTML = subMenuItemUntyped.options_.label
302 } 313 }
303 } 314 }
304 } 315 }
@@ -313,7 +324,7 @@ class SettingsMenuItem extends MenuItem {
313 if (!(item instanceof component)) { 324 if (!(item instanceof component)) {
314 continue 325 continue
315 } 326 }
316 item.on(['tap', 'click'], this.submenuClickHandler) 327 item.on([ 'tap', 'click' ], this.submenuClickHandler)
317 } 328 }
318 } 329 }
319 330
@@ -321,11 +332,11 @@ class SettingsMenuItem extends MenuItem {
321 // if number of submenu items change dynamically more logic will be needed 332 // if number of submenu items change dynamically more logic will be needed
322 setSize () { 333 setSize () {
323 this.dialog.removeClass('vjs-hidden') 334 this.dialog.removeClass('vjs-hidden')
324 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') 335 videojs.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
325 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) 336 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
326 this.setMargin() 337 this.setMargin()
327 this.dialog.addClass('vjs-hidden') 338 this.dialog.addClass('vjs-hidden')
328 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 339 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
329 } 340 }
330 341
331 setMargin () { 342 setMargin () {
@@ -341,19 +352,19 @@ class SettingsMenuItem extends MenuItem {
341 */ 352 */
342 hideSubMenu () { 353 hideSubMenu () {
343 // after removing settings item this.el_ === null 354 // after removing settings item this.el_ === null
344 if (!this.el_) { 355 if (!this.el()) {
345 return 356 return
346 } 357 }
347 358
348 if (videojsUntyped.dom.hasClass(this.el_, 'open')) { 359 if (videojs.dom.hasClass(this.el(), 'open')) {
349 videojsUntyped.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden') 360 videojs.dom.addClass(this.settingsSubMenuEl_, 'vjs-hidden')
350 videojsUntyped.dom.removeClass(this.el_, 'open') 361 videojs.dom.removeClass(this.el(), 'open')
351 } 362 }
352 } 363 }
353 364
354} 365}
355 366
356SettingsMenuItem.prototype.contentElType = 'button' 367(SettingsMenuItem as any).prototype.contentElType = 'button'
357videojsUntyped.registerComponent('SettingsMenuItem', SettingsMenuItem) 368videojs.registerComponent('SettingsMenuItem', SettingsMenuItem)
358 369
359export { SettingsMenuItem } 370export { SettingsMenuItem }
diff --git a/client/src/assets/player/videojs-components/settings-panel-child.ts b/client/src/assets/player/videojs-components/settings-panel-child.ts
new file mode 100644
index 000000000..d1582412c
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-panel-child.ts
@@ -0,0 +1,22 @@
1import videojs from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsPanelChild extends Component {
6
7 constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
8 super(player, options)
9 }
10
11 createEl () {
12 return super.createEl('div', {
13 className: 'vjs-settings-panel-child',
14 innerHTML: '',
15 tabIndex: -1
16 })
17 }
18}
19
20Component.registerComponent('SettingsPanelChild', SettingsPanelChild)
21
22export { SettingsPanelChild }
diff --git a/client/src/assets/player/videojs-components/settings-panel.ts b/client/src/assets/player/videojs-components/settings-panel.ts
new file mode 100644
index 000000000..1ad8bb1fc
--- /dev/null
+++ b/client/src/assets/player/videojs-components/settings-panel.ts
@@ -0,0 +1,22 @@
1import videojs from 'video.js'
2
3const Component = videojs.getComponent('Component')
4
5class SettingsPanel extends Component {
6
7 constructor (player: videojs.Player, options?: videojs.ComponentOptions) {
8 super(player, options)
9 }
10
11 createEl () {
12 return super.createEl('div', {
13 className: 'vjs-settings-panel',
14 innerHTML: '',
15 tabIndex: -1
16 })
17 }
18}
19
20Component.registerComponent('SettingsPanel', SettingsPanel)
21
22export { SettingsPanel }
diff --git a/client/src/assets/player/videojs-components/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts
index bf383cf34..f862ee224 100644
--- a/client/src/assets/player/videojs-components/theater-button.ts
+++ b/client/src/assets/player/videojs-components/theater-button.ts
@@ -1,26 +1,24 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' 2import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
7 3
8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button = videojs.getComponent('Button')
9class TheaterButton extends Button { 5class TheaterButton extends Button {
10 6
11 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' 7 private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled'
12 8
13 constructor (player: videojs.Player, options: any) { 9 constructor (player: videojs.Player, options: videojs.ComponentOptions) {
14 super(player, options) 10 super(player, options)
15 11
16 const enabled = getStoredTheater() 12 const enabled = getStoredTheater()
17 if (enabled === true) { 13 if (enabled === true) {
18 this.player_.addClass(TheaterButton.THEATER_MODE_CLASS) 14 this.player().addClass(TheaterButton.THEATER_MODE_CLASS)
19 15
20 this.handleTheaterChange() 16 this.handleTheaterChange()
21 } 17 }
22 18
23 this.player_.theaterEnabled = enabled 19 this.controlText('Theater mode')
20
21 this.player().theaterEnabled = enabled
24 } 22 }
25 23
26 buildCSSClass () { 24 buildCSSClass () {
@@ -52,6 +50,4 @@ class TheaterButton extends Button {
52 } 50 }
53} 51}
54 52
55TheaterButton.prototype.controlText_ = 'Theater mode' 53videojs.registerComponent('TheaterButton', TheaterButton)
56
57TheaterButton.registerComponent('TheaterButton', TheaterButton)
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index 35cf85c99..cb3deacc6 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -1,17 +1,14 @@
1// FIXME: something weird with our path definition in tsconfig and typings 1import videojs from 'video.js'
2// @ts-ignore
3import * as videojs from 'video.js'
4
5import * as WebTorrent from 'webtorrent' 2import * as WebTorrent from 'webtorrent'
6import { renderVideo } from './video-renderer' 3import { renderVideo } from './video-renderer'
7import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' 4import { LoadedQualityData, PlayerNetworkInfo, WebtorrentPluginOptions } from '../peertube-videojs-typings'
8import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' 5import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
9import { PeertubeChunkStore } from './peertube-chunk-store' 6import { PeertubeChunkStore } from './peertube-chunk-store'
10import { 7import {
11 getAverageBandwidthInStore, 8 getAverageBandwidthInStore,
12 getStoredMute, 9 getStoredMute,
13 getStoredVolume,
14 getStoredP2PEnabled, 10 getStoredP2PEnabled,
11 getStoredVolume,
15 saveAverageBandwidth 12 saveAverageBandwidth
16} from '../peertube-player-local-storage' 13} from '../peertube-player-local-storage'
17import { VideoFile } from '@shared/models' 14import { VideoFile } from '@shared/models'
@@ -24,14 +21,16 @@ type PlayOptions = {
24 delay?: number 21 delay?: number
25} 22}
26 23
27const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 24const Plugin = videojs.getPlugin('plugin')
25
28class WebTorrentPlugin extends Plugin { 26class WebTorrentPlugin extends Plugin {
27 readonly videoFiles: VideoFile[]
28
29 private readonly playerElement: HTMLVideoElement 29 private readonly playerElement: HTMLVideoElement
30 30
31 private readonly autoplay: boolean = false 31 private readonly autoplay: boolean = false
32 private readonly startTime: number = 0 32 private readonly startTime: number = 0
33 private readonly savePlayerSrcFunction: Function 33 private readonly savePlayerSrcFunction: videojs.Player['src']
34 private readonly videoFiles: VideoFile[]
35 private readonly videoDuration: number 34 private readonly videoDuration: number
36 private readonly CONSTANTS = { 35 private readonly CONSTANTS = {
37 INFO_SCHEDULER: 1000, // Don't change this 36 INFO_SCHEDULER: 1000, // Don't change this
@@ -49,7 +48,6 @@ class WebTorrentPlugin extends Plugin {
49 dht: false 48 dht: false
50 }) 49 })
51 50
52 private player: any
53 private currentVideoFile: VideoFile 51 private currentVideoFile: VideoFile
54 private torrent: WebTorrent.Torrent 52 private torrent: WebTorrent.Torrent
55 53
@@ -70,8 +68,8 @@ class WebTorrentPlugin extends Plugin {
70 68
71 private downloadSpeeds: number[] = [] 69 private downloadSpeeds: number[] = []
72 70
73 constructor (player: videojs.Player, options: WebtorrentPluginOptions) { 71 constructor (player: videojs.Player, options?: WebtorrentPluginOptions) {
74 super(player, options) 72 super(player)
75 73
76 this.startTime = timeToInt(options.startTime) 74 this.startTime = timeToInt(options.startTime)
77 75
@@ -147,12 +145,12 @@ class WebTorrentPlugin extends Plugin {
147 } 145 }
148 146
149 // Do not display error to user because we will have multiple fallback 147 // Do not display error to user because we will have multiple fallback
150 this.disableErrorDisplay() 148 this.disableErrorDisplay();
151 149
152 // Hack to "simulate" src link in video.js >= 6 150 // Hack to "simulate" src link in video.js >= 6
153 // Without this, we can't play the video after pausing it 151 // Without this, we can't play the video after pausing it
154 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633 152 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
155 this.player.src = () => true 153 (this.player as any).src = () => true
156 const oldPlaybackRate = this.player.playbackRate() 154 const oldPlaybackRate = this.player.playbackRate()
157 155
158 const previousVideoFile = this.currentVideoFile 156 const previousVideoFile = this.currentVideoFile
@@ -333,7 +331,7 @@ class WebTorrentPlugin extends Plugin {
333 331
334 const playPromise = this.player.play() 332 const playPromise = this.player.play()
335 if (playPromise !== undefined) { 333 if (playPromise !== undefined) {
336 return playPromise.then(done) 334 return playPromise.then(() => done())
337 .catch((err: Error) => { 335 .catch((err: Error) => {
338 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { 336 if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) {
339 return 337 return
@@ -426,8 +424,8 @@ class WebTorrentPlugin extends Plugin {
426 } 424 }
427 425
428 // Proxy first play 426 // Proxy first play
429 const oldPlay = this.player.play.bind(this.player) 427 const oldPlay = this.player.play.bind(this.player);
430 this.player.play = () => { 428 (this.player as any).play = () => {
431 this.player.addClass('vjs-has-big-play-button-clicked') 429 this.player.addClass('vjs-has-big-play-button-clicked')
432 this.player.play = oldPlay 430 this.player.play = oldPlay
433 431
@@ -619,7 +617,7 @@ class WebTorrentPlugin extends Plugin {
619 video: qualityLevelsPayload 617 video: qualityLevelsPayload
620 } 618 }
621 } 619 }
622 this.player.tech_.trigger('loadedqualitydata', payload) 620 this.player.tech(true).trigger('loadedqualitydata', payload)
623 } 621 }
624 622
625 private buildQualityLabel (file: VideoFile) { 623 private buildQualityLabel (file: VideoFile) {
@@ -651,9 +649,9 @@ class WebTorrentPlugin extends Plugin {
651 return 649 return
652 } 650 }
653 651
654 for (let i = 0; i < qualityLevels; i++) { 652 for (let i = 0; i < qualityLevels.length; i++) {
655 const q = this.player.qualityLevels[i] 653 const q = qualityLevels[i]
656 if (q.height === resolutionId) qualityLevels.selectedIndex = i 654 if (q.height === resolutionId) qualityLevels.selectedIndex_ = i
657 } 655 }
658 } 656 }
659} 657}