aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets')
-rw-r--r--client/src/assets/images/default-playlist.jpgbin0 -> 2554 bytes
-rw-r--r--client/src/assets/images/global/add.html6
-rw-r--r--client/src/assets/images/global/folder.html10
-rw-r--r--client/src/assets/images/global/history.html11
-rw-r--r--client/src/assets/images/global/more-horizontal.html (renamed from client/src/assets/images/video/more.html)0
-rw-r--r--client/src/assets/images/global/more-vertical.html11
-rw-r--r--client/src/assets/images/global/play.html9
-rw-r--r--client/src/assets/images/global/playlists.html9
-rw-r--r--client/src/assets/images/global/refresh.html12
-rw-r--r--client/src/assets/images/global/server.html15
-rw-r--r--client/src/assets/images/global/sign-out.html3
-rw-r--r--client/src/assets/images/global/user.html10
-rw-r--r--client/src/assets/images/global/users.html11
-rw-r--r--client/src/assets/images/global/videos.html14
-rw-r--r--client/src/assets/images/menu/about.html (renamed from client/src/assets/images/menu/about.svg)5
-rw-r--r--client/src/assets/images/menu/administration.html (renamed from client/src/assets/images/menu/administration.svg)6
-rw-r--r--client/src/assets/images/menu/globe.html (renamed from client/src/assets/images/menu/globe.svg)6
-rw-r--r--client/src/assets/images/menu/go.html12
-rw-r--r--client/src/assets/images/menu/home.html (renamed from client/src/assets/images/menu/home.svg)6
-rw-r--r--client/src/assets/images/menu/recently-added.html (renamed from client/src/assets/images/menu/recently-added.svg)7
-rw-r--r--client/src/assets/images/menu/subscriptions.html (renamed from client/src/assets/images/menu/subscriptions.svg)15
-rw-r--r--client/src/assets/images/menu/trending.html (renamed from client/src/assets/images/menu/trending.svg)6
-rw-r--r--client/src/assets/images/video/playlist-add.html10
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts161
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-url-builder.ts28
-rw-r--r--client/src/assets/player/p2p-media-loader/segment-validator.ts63
-rw-r--r--client/src/assets/player/peertube-player-manager.ts472
-rw-r--r--client/src/assets/player/peertube-player.ts300
-rw-r--r--client/src/assets/player/peertube-plugin.ts269
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts99
-rw-r--r--client/src/assets/player/resolution-menu-button.ts88
-rw-r--r--client/src/assets/player/resolution-menu-item.ts67
-rw-r--r--client/src/assets/player/utils.ts43
-rw-r--r--client/src/assets/player/videojs-components/p2p-info-button.ts (renamed from client/src/assets/player/webtorrent-info-button.ts)25
-rw-r--r--client/src/assets/player/videojs-components/peertube-link-button.ts (renamed from client/src/assets/player/peertube-link-button.ts)4
-rw-r--r--client/src/assets/player/videojs-components/peertube-load-progress-bar.ts (renamed from client/src/assets/player/peertube-load-progress-bar.ts)4
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-button.ts109
-rw-r--r--client/src/assets/player/videojs-components/resolution-menu-item.ts83
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-button.ts (renamed from client/src/assets/player/settings-menu-button.ts)22
-rw-r--r--client/src/assets/player/videojs-components/settings-menu-item.ts (renamed from client/src/assets/player/settings-menu-item.ts)31
-rw-r--r--client/src/assets/player/videojs-components/theater-button.ts (renamed from client/src/assets/player/theater-button.ts)15
-rw-r--r--client/src/assets/player/webtorrent/peertube-chunk-store.ts (renamed from client/src/assets/player/peertube-chunk-store.ts)6
-rw-r--r--client/src/assets/player/webtorrent/video-renderer.ts (renamed from client/src/assets/player/video-renderer.ts)2
-rw-r--r--client/src/assets/player/webtorrent/webtorrent-plugin.ts (renamed from client/src/assets/player/peertube-videojs-plugin.ts)309
44 files changed, 1624 insertions, 770 deletions
diff --git a/client/src/assets/images/default-playlist.jpg b/client/src/assets/images/default-playlist.jpg
new file mode 100644
index 000000000..978fb16f2
--- /dev/null
+++ b/client/src/assets/images/default-playlist.jpg
Binary files differ
diff --git a/client/src/assets/images/global/add.html b/client/src/assets/images/global/add.html
index bfb0a52bc..34f497056 100644
--- a/client/src/assets/images/global/add.html
+++ b/client/src/assets/images/global/add.html
@@ -2,9 +2,9 @@
2 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 2 <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g transform="translate(-92.000000, -115.000000)"> 3 <g transform="translate(-92.000000, -115.000000)">
4 <g id="2" transform="translate(92.000000, 115.000000)"> 4 <g id="2" transform="translate(92.000000, 115.000000)">
5 <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> 5 <circle id="Oval-1" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle>
6 <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> 6 <rect id="Rectangle-1" fill="#000000" x="11" y="7" width="2" height="10" rx="1"></rect>
7 <rect id="Rectangle-1" fill="#ffffff" x="7" y="11" width="10" height="2" rx="1"></rect> 7 <rect id="Rectangle-1" fill="#000000" x="7" y="11" width="10" height="2" rx="1"></rect>
8 </g> 8 </g>
9 </g> 9 </g>
10 </g> 10 </g>
diff --git a/client/src/assets/images/global/folder.html b/client/src/assets/images/global/folder.html
new file mode 100644
index 000000000..8443c15c6
--- /dev/null
+++ b/client/src/assets/images/global/folder.html
@@ -0,0 +1,10 @@
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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-708.000000, -115.000000)">
4 <g id="16" transform="translate(708.000000, 115.000000)">
5 <path d="M11.5857864,17 L14.2928932,19.7071068 L14.5857864,20 L15,20 L20.009222,20 C21.1044506,20 22,19.102094 22,18.0014977 L22,6.99850233 C22,5.89626364 21.1085926,5 20.0066023,5 L3.99339768,5 C2.89217541,5 2,5.89385529 2,6.99539757 L2,15.0046024 C2,16.099013 2.89670181,17 3.99754465,17 L11.5857864,17 Z" id="Rectangle-406" stroke="#000000" stroke-width="2" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) "/>
6 <path d="M3,5 C3,4.44771525 3.4454627,4 3.99871095,4 L12.5,4 L10.5,6 L3.99594209,6 C3.44589846,6 3,5.55613518 3,5 L3,5 Z" id="Rectangle-409" fill="#000000"/>
7 </g>
8 </g>
9 </g>
10</svg>
diff --git a/client/src/assets/images/global/history.html b/client/src/assets/images/global/history.html
new file mode 100644
index 000000000..dfb70b598
--- /dev/null
+++ b/client/src/assets/images/global/history.html
@@ -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 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
3 <g id="Artboard-4" transform="translate(-620.000000, -863.000000)" stroke="#000000" stroke-width="2">
4 <g id="354" transform="translate(620.000000, 863.000000)">
5 <path d="M6.63582585,18.3637479 C8.26452234,19.9925528 10.5146102,21 13,21 L13,21 C17.9705627,21 22,16.9705627 22,12 C22,7.02943725 17.9705627,3 13,3 C8.02943725,3 4,7.02943725 4,12" id="Oval-203"/>
6 <polygon id="Path-282" fill="#000000" points="1.5 11 7.5 11 4.5 14"/>
7 <polyline id="Path-283" points="13 7 13 12 15.5 14.5"/>
8 </g>
9 </g>
10 </g>
11</svg>
diff --git a/client/src/assets/images/video/more.html b/client/src/assets/images/global/more-horizontal.html
index 39dcad10e..39dcad10e 100644
--- a/client/src/assets/images/video/more.html
+++ b/client/src/assets/images/global/more-horizontal.html
diff --git a/client/src/assets/images/global/more-vertical.html b/client/src/assets/images/global/more-vertical.html
new file mode 100644
index 000000000..9bff87a82
--- /dev/null
+++ b/client/src/assets/images/global/more-vertical.html
@@ -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 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-268.000000, -1046.000000)" fill="#000000">
4 <g id="Extras" transform="translate(48.000000, 1046.000000)">
5 <g id="more-vertical" transform="translate(220.000000, 0.000000)">
6 <path d="M10,12 C10,10.8954305 10.8877296,10 12,10 C13.1045695,10 14,10.8877296 14,12 C14,13.1045695 13.1122704,14 12,14 C10.8954305,14 10,13.1122704 10,12 Z M10,5 C10,3.8954305 10.8877296,3 12,3 C13.1045695,3 14,3.88772964 14,5 C14,6.1045695 13.1122704,7 12,7 C10.8954305,7 10,6.11227036 10,5 Z M10,19 C10,17.8954305 10.8877296,17 12,17 C13.1045695,17 14,17.8877296 14,19 C14,20.1045695 13.1122704,21 12,21 C10.8954305,21 10,20.1122704 10,19 Z" id="Combined-Shape"/>
7 </g>
8 </g>
9 </g>
10 </g>
11</svg>
diff --git a/client/src/assets/images/global/play.html b/client/src/assets/images/global/play.html
new file mode 100644
index 000000000..d00122de4
--- /dev/null
+++ b/client/src/assets/images/global/play.html
@@ -0,0 +1,9 @@
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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round">
3 <g id="Artboard-4" transform="translate(-532.000000, -115.000000)" stroke-width="2" stroke="#000000">
4 <g id="12" transform="translate(532.000000, 115.000000)">
5 <polygon id="Triangle-1" points="5 21 5 3 21 12" fill="#000000"/>
6 </g>
7 </g>
8 </g>
9</svg>
diff --git a/client/src/assets/images/global/playlists.html b/client/src/assets/images/global/playlists.html
new file mode 100644
index 000000000..21b05009a
--- /dev/null
+++ b/client/src/assets/images/global/playlists.html
@@ -0,0 +1,9 @@
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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-664.000000, -467.000000)" stroke="#000000">
4 <g id="175" transform="translate(664.000000, 467.000000)">
5 <path stoke="#000000" d="M7.5,7 C7.5,6.72427445 7.72568093,6.5 8.00684547,6.5 L19.9931545,6.5 C20.2754761,6.5 20.5,6.72240424 20.5,7 C20.5,7.27572555 20.2743191,7.5 19.9931545,7.5 L8.00684547,7.5 C7.72452386,7.5 7.5,7.27759576 7.5,7 Z M7.5,12 C7.5,11.7242745 7.72568093,11.5 8.00684547,11.5 L19.9931545,11.5 C20.2754761,11.5 20.5,11.7224042 20.5,12 C20.5,12.2757255 20.2743191,12.5 19.9931545,12.5 L8.00684547,12.5 C7.72452386,12.5 7.5,12.2775958 7.5,12 Z M7.5,17 C7.5,16.7242745 7.72568093,16.5 8.00684547,16.5 L19.9931545,16.5 C20.2754761,16.5 20.5,16.7224042 20.5,17 C20.5,17.2757255 20.2743191,17.5 19.9931545,17.5 L8.00684547,17.5 C7.72452386,17.5 7.5,17.2775958 7.5,17 Z M4,7.5 C3.72385763,7.5 3.5,7.27614237 3.5,7 C3.5,6.72385763 3.72385763,6.5 4,6.5 C4.27614237,6.5 4.5,6.72385763 4.5,7 C4.5,7.27614237 4.27614237,7.5 4,7.5 Z M4,12.5 C3.72385763,12.5 3.5,12.2761424 3.5,12 C3.5,11.7238576 3.72385763,11.5 4,11.5 C4.27614237,11.5 4.5,11.7238576 4.5,12 C4.5,12.2761424 4.27614237,12.5 4,12.5 Z M4,17.5 C3.72385763,17.5 3.5,17.2761424 3.5,17 C3.5,16.7238576 3.72385763,16.5 4,16.5 C4.27614237,16.5 4.5,16.7238576 4.5,17 C4.5,17.2761424 4.27614237,17.5 4,17.5 Z" id="Combined-Shape"/>
6 </g>
7 </g>
8 </g>
9</svg>
diff --git a/client/src/assets/images/global/refresh.html b/client/src/assets/images/global/refresh.html
new file mode 100644
index 000000000..421ab343d
--- /dev/null
+++ b/client/src/assets/images/global/refresh.html
@@ -0,0 +1,12 @@
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">
4 <g id="Artboard-4" transform="translate(-224.000000, -1046.000000)" fill="#000000">
5 <g id="Extras" transform="translate(48.000000, 1046.000000)">
6 <g id="refresh" transform="translate(176.000000, 0.000000)">
7 <path d="M20.9995201,13.0312796 L20.9999519,13.0312796 C20.9830843,17.9874565 16.960132,22 12,22 C7.02943725,22 3,17.9705627 3,13 C3,8.0398348 7.01259713,4.01686187 11.9688198,4.00005287 L11.9688198,6.00006796 C8.11716976,6.01686496 5,9.14440548 5,13 C5,16.8659932 8.13400675,20 12,20 C15.8555614,20 18.9830812,16.8828839 18.9999316,13.0312796 L19.0004799,13.0312796 C19.0001607,13.0208922 19,13.0104649 19,13 C19,12.4477153 19.4477153,12 20,12 C20.5522847,12 21,12.4477153 21,13 C21,13.0104649 20.9998393,13.0208922 20.9995201,13.0312796 Z M12,9 L12,1 L16,5 L12,9 Z" id="Combined-Shape"/>
8 </g>
9 </g>
10 </g>
11 </g>
12</svg>
diff --git a/client/src/assets/images/global/server.html b/client/src/assets/images/global/server.html
new file mode 100644
index 000000000..409026e1a
--- /dev/null
+++ b/client/src/assets/images/global/server.html
@@ -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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-796.000000, -643.000000)">
4 <g id="258" transform="translate(796.000000, 643.000000)">
5 <ellipse id="Oval-140" stroke="#000000" stroke-width="2" cx="12" cy="6" rx="9" ry="3"/>
6 <path d="M3,10.5 C3,12.1568542 7.02943725,13.5 12,13.5 L12,13.5 C16.9705627,13.5 21,12.1568542 21,10.5" id="Oval-140" stroke="#000000"/>
7 <path d="M3,14.5 C3,16.1568542 7.02943725,17.5 12,17.5 C16.9705627,17.5 21,16.1568542 21,14.5" id="Oval-140" stroke="#000000"/>
8 <path d="M3,5.98958785 L3,19 C3,20.6568542 7.02943725,22 12,22 C16.9705627,22 21,20.6568542 21,19 L21,5.98958785" id="Oval-140" stroke="#000000" stroke-width="2"/>
9 <circle id="Oval-141" fill="#000000" cx="18.5" cy="10.5" r="1"/>
10 <circle id="Oval-141" fill="#000000" cx="18.5" cy="14.5" r="1"/>
11 <circle id="Oval-141" fill="#000000" cx="18.5" cy="18.5" r="1"/>
12 </g>
13 </g>
14 </g>
15</svg>
diff --git a/client/src/assets/images/global/sign-out.html b/client/src/assets/images/global/sign-out.html
new file mode 100644
index 000000000..4e316dc8b
--- /dev/null
+++ b/client/src/assets/images/global/sign-out.html
@@ -0,0 +1,3 @@
1<svg viewBox="0 0 1536 1536" width="1536" height="1536" xmlns="http://www.w3.org/2000/svg">
2 <path fill="#000000" d="M640 1440c0 28 13 96-32 96H288c-159 0-288-129-288-288V544c0-159 129-288 288-288h320c17 0 32 15 32 32 0 28 13 96-32 96H288c-88 0-160 72-160 160v704c0 88 72 160 160 160h288c25 0 64-5 64 32zm928-544c0 17-7 33-19 45l-544 544c-12 12-28 19-45 19-35 0-64-29-64-64v-288H448c-35 0-64-29-64-64V704c0-35 29-64 64-64h448V352c0-35 29-64 64-64 17 0 33 7 45 19l544 544c12 12 19 28 19 45z"/>
3</svg>
diff --git a/client/src/assets/images/global/user.html b/client/src/assets/images/global/user.html
new file mode 100644
index 000000000..c7b9319b6
--- /dev/null
+++ b/client/src/assets/images/global/user.html
@@ -0,0 +1,10 @@
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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-532.000000, -159.000000)" stroke="#000000" stroke-width="2">
4 <g id="32" transform="translate(532.000000, 159.000000)">
5 <path d="M2,21 C2,21 1.5,16 7,16 C12.5,16 11.512498,16 17.006249,16 C22.5,16 22.0062485,21 22.0062485,21" id="Path-41" stroke-linecap="round" stroke-linejoin="round"/>
6 <circle id="Oval-40" cx="12" cy="8" r="5"/>
7 </g>
8 </g>
9 </g>
10</svg>
diff --git a/client/src/assets/images/global/users.html b/client/src/assets/images/global/users.html
new file mode 100644
index 000000000..522883785
--- /dev/null
+++ b/client/src/assets/images/global/users.html
@@ -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 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-92.000000, -863.000000)">
4 <g id="342" transform="translate(92.000000, 863.000000)">
5 <path d="M7,21 C7,21 7,17 11,17 C15,17 14.9937515,17 19,17 C23.0062485,17 23.0062485,21 23.0062485,21" id="Path-41" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
6 <path d="M8.27455269,11.9477557 C5.85692935,11.5963698 4,9.51503944 4,7 C4,4.23857625 6.23857625,2 9,2 C10.373942,2 11.6184509,2.55416948 12.5221996,3.45118158 C11.8469348,3.70680858 11.2215328,4.06387931 10.664592,4.50379553 C10.1883045,4.18555118 9.61582114,4 9,4 C7.34314575,4 6,5.34314575 6,7 C6,8.3069749 6.83577432,9.41874424 8.00202365,9.83000873 C8.00067709,9.88650926 8,9.94317556 8,10 C8,10.6759052 8.09579644,11.329436 8.27455269,11.9477557 Z M8.67363116,13 L5,13 C2.85717375,13 1.39436214,13.9752077 0.605572809,15.5527864 C0.148670182,16.4665917 -7.10542736e-14,17.3586127 -7.10542736e-14,18 C-7.10542736e-14,18.5522847 0.44771525,19 1,19 C1.55228475,19 2,18.5522847 2,18 C2,17.9269061 2.01176795,17.7621548 2.04889392,17.539399 C2.11167338,17.1627222 2.22417415,16.7877197 2.39442719,16.4472136 C2.85563786,15.5247923 3.64282625,15 5,15 L10.1010173,15 C9.51513298,14.4258795 9.02972955,13.7496048 8.67363116,13 Z" id="Combined-Shape" fill="#000000" fill-rule="nonzero"/>
7 <circle id="Oval-40" stroke="#000000" stroke-width="2" cx="15" cy="10" r="4"/>
8 </g>
9 </g>
10 </g>
11</svg>
diff --git a/client/src/assets/images/global/videos.html b/client/src/assets/images/global/videos.html
new file mode 100644
index 000000000..6e37f466f
--- /dev/null
+++ b/client/src/assets/images/global/videos.html
@@ -0,0 +1,14 @@
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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
3 <g id="Artboard-4" transform="translate(-312.000000, -511.000000)">
4 <g id="187" transform="translate(312.000000, 511.000000)">
5 <rect id="Rectangle-124" stroke="#000000" stroke-width="2" x="3" y="6" width="18" height="16" rx="1"/>
6
7 <polygon fill="#000000" id="Triangle-1" points="10 17.5 10 10.4 15.5 13.9"/>
8
9 <rect id="Rectangle-125" fill="#000000" x="4" y="3" width="16" height="1" rx="0.5"/>
10 <rect id="Rectangle-125" fill="#000000" x="5" y="1" width="14" height="1" rx="0.5"/>
11 </g>
12 </g>
13 </g>
14</svg>
diff --git a/client/src/assets/images/menu/about.svg b/client/src/assets/images/menu/about.html
index eac2932a9..bea602aac 100644
--- a/client/src/assets/images/menu/about.svg
+++ b/client/src/assets/images/menu/about.html
@@ -1,11 +1,10 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-400.000000, -247.000000)"> 4 <g id="Artboard-4" transform="translate(-400.000000, -247.000000)">
6 <g id="69" transform="translate(400.000000, 247.000000)"> 5 <g id="69" transform="translate(400.000000, 247.000000)">
7 <circle id="Oval-7" stroke="#808080" stroke-width="2" cx="12" cy="12" r="10"></circle> 6 <circle id="Oval-7" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle>
8 <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="#808080"></path> 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>
9 </g> 8 </g>
10 </g> 9 </g>
11 </g> 10 </g>
diff --git a/client/src/assets/images/menu/administration.svg b/client/src/assets/images/menu/administration.html
index b6da837d2..0dceda082 100644
--- a/client/src/assets/images/menu/administration.svg
+++ b/client/src/assets/images/menu/administration.html
@@ -1,11 +1,7 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>filter</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-444.000000, -247.000000)" fill="#808080"> 4 <g id="Artboard-4" transform="translate(-444.000000, -247.000000)" fill="#000000">
9 <g id="70" transform="translate(444.000000, 247.000000)"> 5 <g id="70" transform="translate(444.000000, 247.000000)">
10 <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> 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>
11 </g> 7 </g>
diff --git a/client/src/assets/images/menu/globe.svg b/client/src/assets/images/menu/globe.html
index a4b3db9c5..cf8331256 100644
--- a/client/src/assets/images/menu/globe.svg
+++ b/client/src/assets/images/menu/globe.html
@@ -1,11 +1,7 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>globe</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
8 <g id="Artboard-4" transform="translate(-224.000000, -687.000000)" stroke="#808080" stroke-width="2"> 4 <g id="Artboard-4" transform="translate(-224.000000, -687.000000)" stroke="#000000" stroke-width="2">
9 <g id="265" transform="translate(224.000000, 687.000000)"> 5 <g id="265" transform="translate(224.000000, 687.000000)">
10 <circle id="Oval-148" cx="12" cy="12" r="10"></circle> 6 <circle id="Oval-148" cx="12" cy="12" r="10"></circle>
11 <path d="M12,2 L12,22.006249" id="Path-199"></path> 7 <path d="M12,2 L12,22.006249" id="Path-199"></path>
diff --git a/client/src/assets/images/menu/go.html b/client/src/assets/images/menu/go.html
new file mode 100644
index 000000000..b16e794ec
--- /dev/null
+++ b/client/src/assets/images/menu/go.html
@@ -0,0 +1,12 @@
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 id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
3 <g id="Artboard-4" transform="translate(-576.000000, -1046.000000)" stroke="#000000" stroke-width="2">
4 <g id="Extras" transform="translate(48.000000, 1046.000000)">
5 <g id="up-right" transform="translate(528.000000, 0.000000)">
6 <path d="M18,6 L5,19" id="Path-58"/>
7 <polyline id="Path-59" stroke-linejoin="round" transform="translate(13.000000, 11.000000) scale(-1, -1) translate(-13.000000, -11.000000) " points="7 5 7 17 19 17"/>
8 </g>
9 </g>
10 </g>
11 </g>
12</svg>
diff --git a/client/src/assets/images/menu/home.svg b/client/src/assets/images/menu/home.html
index bb95e949a..b7b8cb755 100644
--- a/client/src/assets/images/menu/home.svg
+++ b/client/src/assets/images/menu/home.html
@@ -1,11 +1,7 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>home</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> 3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
8 <g id="Artboard-4" transform="translate(-620.000000, -159.000000)" stroke="#808080" stroke-width="2"> 4 <g id="Artboard-4" transform="translate(-620.000000, -159.000000)" stroke="#000000" stroke-width="2">
9 <g id="34" transform="translate(620.000000, 159.000000)"> 5 <g id="34" transform="translate(620.000000, 159.000000)">
10 <path d="M1,11 L12,2 C12,2 22.9999989,11.0000005 23,11" id="Path-50"></path> 6 <path d="M1,11 L12,2 C12,2 22.9999989,11.0000005 23,11" id="Path-50"></path>
11 <path d="M3,10 C3,10 3,10.4453982 3,10.9968336 L3,20.0170446 C3,20.5675806 3.43788135,21.0138782 4.00292933,21.0138781 L8.99707067,21.0138779 C9.55097324,21.0138779 10,20.5751284 10,20.0089602 L10,15.0049177 C10,14.449917 10.4433532,14 11.0093689,14 L12.9906311,14 C13.5480902,14 14,14.4387495 14,15.0049177 L14,20.0089602 C14,20.5639609 14.4378817,21.0138779 15.0029302,21.0138779 L19.9970758,21.0138781 C20.5509789,21.0138782 21.000006,20.56848 21.000006,20.0170446 L21.0000057,10" id="Path-51"></path> 7 <path d="M3,10 C3,10 3,10.4453982 3,10.9968336 L3,20.0170446 C3,20.5675806 3.43788135,21.0138782 4.00292933,21.0138781 L8.99707067,21.0138779 C9.55097324,21.0138779 10,20.5751284 10,20.0089602 L10,15.0049177 C10,14.449917 10.4433532,14 11.0093689,14 L12.9906311,14 C13.5480902,14 14,14.4387495 14,15.0049177 L14,20.0089602 C14,20.5639609 14.4378817,21.0138779 15.0029302,21.0138779 L19.9970758,21.0138781 C20.5509789,21.0138782 21.000006,20.56848 21.000006,20.0170446 L21.0000057,10" id="Path-51"></path>
diff --git a/client/src/assets/images/menu/recently-added.svg b/client/src/assets/images/menu/recently-added.html
index 6473837f8..d551bfb69 100644
--- a/client/src/assets/images/menu/recently-added.svg
+++ b/client/src/assets/images/menu/recently-added.html
@@ -1,12 +1,11 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-92.000000, -115.000000)"> 4 <g id="Artboard-4" transform="translate(-92.000000, -115.000000)">
6 <g id="2" transform="translate(92.000000, 115.000000)"> 5 <g id="2" transform="translate(92.000000, 115.000000)">
7 <circle id="Oval-1" stroke="#808080" stroke-width="2" cx="12" cy="12" r="10"></circle> 6 <circle id="Oval-1" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle>
8 <rect id="Rectangle-1" fill="#808080" x="11" y="7" width="2" height="10" rx="1"></rect> 7 <rect id="Rectangle-1" fill="#000000" x="11" y="7" width="2" height="10" rx="1"></rect>
9 <rect id="Rectangle-1" fill="#808080" x="7" y="11" width="10" height="2" rx="1"></rect> 8 <rect id="Rectangle-1" fill="#000000" x="7" y="11" width="10" height="2" rx="1"></rect>
10 </g> 9 </g>
11 </g> 10 </g>
12 </g> 11 </g>
diff --git a/client/src/assets/images/menu/subscriptions.svg b/client/src/assets/images/menu/subscriptions.html
index cd6efc54e..08322e520 100644
--- a/client/src/assets/images/menu/subscriptions.svg
+++ b/client/src/assets/images/menu/subscriptions.html
@@ -1,25 +1,22 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>podcasts</title>
5 <desc>Created with Sketch.</desc>
6 <defs> 3 <defs>
7 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1"> 4 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1">
8 <stop stop-color="#808080" offset="0%"></stop> 5 <stop stop-color="#000000" offset="0%"></stop>
9 <stop stop-color="#808080" stop-opacity="0.247310915" offset="100%"></stop> 6 <stop stop-color="#000000" stop-opacity="0.247310915" offset="100%"></stop>
10 </linearGradient> 7 </linearGradient>
11 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2"> 8 <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2">
12 <stop stop-color="#808080" offset="0%"></stop> 9 <stop stop-color="#000000" offset="0%"></stop>
13 <stop stop-color="#808080" stop-opacity="0.250707654" offset="100%"></stop> 10 <stop stop-color="#000000" stop-opacity="0.250707654" offset="100%"></stop>
14 </linearGradient> 11 </linearGradient>
15 </defs> 12 </defs>
16 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> 13 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
17 <g id="Artboard-4" transform="translate(-532.000000, -775.000000)"> 14 <g id="Artboard-4" transform="translate(-532.000000, -775.000000)">
18 <g id="312" transform="translate(532.000000, 775.000000)"> 15 <g id="312" transform="translate(532.000000, 775.000000)">
19 <circle id="Oval-169" fill="#808080" cx="12" cy="10" r="3"></circle> 16 <circle id="Oval-169" fill="#000000" cx="12" cy="10" r="3"></circle>
20 <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path> 17 <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
21 <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path> 18 <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
22 <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#808080"></path> 19 <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#000000"></path>
23 </g> 20 </g>
24 </g> 21 </g>
25 </g> 22 </g>
diff --git a/client/src/assets/images/menu/trending.svg b/client/src/assets/images/menu/trending.html
index ffc65cc04..f1ce11487 100644
--- a/client/src/assets/images/menu/trending.svg
+++ b/client/src/assets/images/menu/trending.html
@@ -1,11 +1,7 @@
1<?xml version="1.0" encoding="UTF-8"?> 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"> 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 <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
4 <title>graph</title>
5 <desc>Created with Sketch.</desc>
6 <defs></defs>
7 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> 3 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
8 <g id="Artboard-4" transform="translate(-444.000000, -203.000000)" stroke-width="2" stroke="#808080"> 4 <g id="Artboard-4" transform="translate(-444.000000, -203.000000)" stroke-width="2" stroke="#000000">
9 <g id="50" transform="translate(444.000000, 203.000000)"> 5 <g id="50" transform="translate(444.000000, 203.000000)">
10 <polyline id="Path-96" points="3 3 3 21.006249 21.0246733 21.006249"></polyline> 6 <polyline id="Path-96" points="3 3 3 21.006249 21.0246733 21.006249"></polyline>
11 <polyline id="Path-101" points="6 18 11 12 14 13 19 7"></polyline> 7 <polyline id="Path-101" points="6 18 11 12 14 13 19 7"></polyline>
diff --git a/client/src/assets/images/video/playlist-add.html b/client/src/assets/images/video/playlist-add.html
new file mode 100644
index 000000000..ada845c75
--- /dev/null
+++ b/client/src/assets/images/video/playlist-add.html
@@ -0,0 +1,10 @@
1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
2 viewBox="0 0 426.667 426.667" xml:space="preserve">
3 <g fill="#000000">
4 <rect x="0" y="64" width="256" height="42.667"/>
5 <rect x="0" y="149.333" width="256" height="42.667"/>
6 <rect x="0" y="234.667" width="170.667" height="42.667"/>
7 <polygon points="341.333,234.667 341.333,149.333 298.667,149.333 298.667,234.667 213.333,234.667 213.333,277.333
8 298.667,277.333 298.667,362.667 341.333,362.667 341.333,277.333 426.667,277.333 426.667,234.667 "/>
9 </g>
10</svg>
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
new file mode 100644
index 000000000..bbd3e008d
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -0,0 +1,161 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings'
5import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs'
6import { Events } from 'p2p-media-loader-core'
7import { timeToInt } from '../utils'
8
9// videojs-hlsjs-plugin needs videojs in window
10window['videojs'] = videojs
11require('@streamroot/videojs-hlsjs-plugin')
12
13const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
14class P2pMediaLoaderPlugin extends Plugin {
15
16 private readonly CONSTANTS = {
17 INFO_SCHEDULER: 1000 // Don't change this
18 }
19 private readonly options: P2PMediaLoaderPluginOptions
20
21 private hlsjs: any // Don't type hlsjs to not bundle the module
22 private p2pEngine: Engine
23 private statsP2PBytes = {
24 pendingDownload: [] as number[],
25 pendingUpload: [] as number[],
26 numPeers: 0,
27 totalDownload: 0,
28 totalUpload: 0
29 }
30 private statsHTTPBytes = {
31 pendingDownload: [] as number[],
32 pendingUpload: [] as number[],
33 totalDownload: 0,
34 totalUpload: 0
35 }
36 private startTime: number
37
38 private networkInfoInterval: any
39
40 constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) {
41 super(player, options)
42
43 this.options = options
44
45 if (!videojs.Html5Hlsjs) {
46 const message = 'HLS.js does not seem to be supported.'
47 console.warn(message)
48
49 player.ready(() => player.trigger('error', new Error(message)))
50 return
51 }
52
53 videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => {
54 this.hlsjs = hlsjs
55 })
56
57 initVideoJsContribHlsJsPlayer(player)
58
59 this.startTime = timeToInt(options.startTime)
60
61 player.src({
62 type: options.type,
63 src: options.src
64 })
65
66 player.one('play', () => {
67 player.addClass('vjs-has-big-play-button-clicked')
68 })
69
70 player.ready(() => this.initialize())
71 }
72
73 dispose () {
74 if (this.hlsjs) this.hlsjs.destroy()
75 if (this.p2pEngine) this.p2pEngine.destroy()
76
77 clearInterval(this.networkInfoInterval)
78 }
79
80 private initialize () {
81 initHlsJsPlayer(this.hlsjs)
82
83 const tech = this.player.tech_
84 this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine()
85
86 // Avoid using constants to not import hls.hs
87 // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37
88 this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => {
89 this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height })
90 })
91
92 this.p2pEngine.on(Events.SegmentError, (segment, err) => {
93 console.error('Segment error.', segment, err)
94 })
95
96 this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length
97
98 this.runStats()
99
100 this.hlsjs.on('hlsLevelLoaded', () => {
101 if (this.startTime) this.player.currentTime(this.startTime)
102
103 this.hlsjs.off('hlsLevelLoaded', this)
104 })
105 }
106
107 private runStats () {
108 this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => {
109 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
110
111 elem.pendingDownload.push(size)
112 elem.totalDownload += size
113 })
114
115 this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => {
116 const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes
117
118 elem.pendingUpload.push(size)
119 elem.totalUpload += size
120 })
121
122 this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++)
123 this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--)
124
125 this.networkInfoInterval = setInterval(() => {
126 const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload)
127 const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload)
128
129 const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload)
130 const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload)
131
132 this.statsP2PBytes.pendingDownload = []
133 this.statsP2PBytes.pendingUpload = []
134 this.statsHTTPBytes.pendingDownload = []
135 this.statsHTTPBytes.pendingUpload = []
136
137 return this.player.trigger('p2pInfo', {
138 http: {
139 downloadSpeed: httpDownloadSpeed,
140 uploadSpeed: httpUploadSpeed,
141 downloaded: this.statsHTTPBytes.totalDownload,
142 uploaded: this.statsHTTPBytes.totalUpload
143 },
144 p2p: {
145 downloadSpeed: p2pDownloadSpeed,
146 uploadSpeed: p2pUploadSpeed,
147 numPeers: this.statsP2PBytes.numPeers,
148 downloaded: this.statsP2PBytes.totalDownload,
149 uploaded: this.statsP2PBytes.totalUpload
150 }
151 } as PlayerNetworkInfo)
152 }, this.CONSTANTS.INFO_SCHEDULER)
153 }
154
155 private arraySum (data: number[]) {
156 return data.reduce((a: number, b: number) => a + b, 0)
157 }
158}
159
160videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin)
161export { P2pMediaLoaderPlugin }
diff --git a/client/src/assets/player/p2p-media-loader/segment-url-builder.ts b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts
new file mode 100644
index 000000000..fb990a19d
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/segment-url-builder.ts
@@ -0,0 +1,28 @@
1import { basename } from 'path'
2import { Segment } from 'p2p-media-loader-core'
3
4function segmentUrlBuilderFactory (baseUrls: string[]) {
5 return function segmentBuilder (segment: Segment) {
6 const max = baseUrls.length + 1
7 const i = getRandomInt(max)
8
9 if (i === max - 1) return segment.url
10
11 const newBaseUrl = baseUrls[i]
12 const middlePart = newBaseUrl.endsWith('/') ? '' : '/'
13
14 return newBaseUrl + middlePart + basename(segment.url)
15 }
16}
17
18// ---------------------------------------------------------------------------
19
20export {
21 segmentUrlBuilderFactory
22}
23
24// ---------------------------------------------------------------------------
25
26function getRandomInt (max: number) {
27 return Math.floor(Math.random() * Math.floor(max))
28}
diff --git a/client/src/assets/player/p2p-media-loader/segment-validator.ts b/client/src/assets/player/p2p-media-loader/segment-validator.ts
new file mode 100644
index 000000000..72c32f9e0
--- /dev/null
+++ b/client/src/assets/player/p2p-media-loader/segment-validator.ts
@@ -0,0 +1,63 @@
1import { Segment } from 'p2p-media-loader-core'
2import { basename } from 'path'
3
4function segmentValidatorFactory (segmentsSha256Url: string) {
5 const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
6 const regex = /bytes=(\d+)-(\d+)/
7
8 return async function segmentValidator (segment: Segment) {
9 const filename = basename(segment.url)
10 const captured = regex.exec(segment.range)
11
12 const range = captured[1] + '-' + captured[2]
13
14 const hashShouldBe = (await segmentsJSON)[filename][range]
15 if (hashShouldBe === undefined) {
16 throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
17 }
18
19 const calculatedSha = bufferToEx(await sha256(segment.data))
20 if (calculatedSha !== hashShouldBe) {
21 throw new Error(
22 `Hashes does not correspond for segment ${filename}/${range}` +
23 `(expected: ${hashShouldBe} instead of ${calculatedSha})`
24 )
25 }
26 }
27}
28
29// ---------------------------------------------------------------------------
30
31export {
32 segmentValidatorFactory
33}
34
35// ---------------------------------------------------------------------------
36
37function fetchSha256Segments (url: string) {
38 return fetch(url)
39 .then(res => res.json())
40 .catch(err => {
41 console.error('Cannot get sha256 segments', err)
42 return {}
43 })
44}
45
46function sha256 (data?: ArrayBuffer) {
47 if (!data) return undefined
48
49 return window.crypto.subtle.digest('SHA-256', data)
50}
51
52// Thanks: https://stackoverflow.com/a/53307879
53function bufferToEx (buffer?: ArrayBuffer) {
54 if (!buffer) return ''
55
56 let s = ''
57 const h = '0123456789abcdef'
58 const o = new Uint8Array(buffer)
59
60 o.forEach((v: any) => s += h[ v >> 4 ] + h[ v & 15 ])
61
62 return s
63}
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
new file mode 100644
index 000000000..6cdd54372
--- /dev/null
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -0,0 +1,472 @@
1import { VideoFile } from '../../../../shared/models/videos'
2// @ts-ignore
3import * as videojs from 'video.js'
4import 'videojs-hotkeys'
5import 'videojs-dock'
6import 'videojs-contextmenu-ui'
7import 'videojs-contrib-quality-levels'
8import './peertube-plugin'
9import './videojs-components/peertube-link-button'
10import './videojs-components/resolution-menu-button'
11import './videojs-components/settings-menu-button'
12import './videojs-components/p2p-info-button'
13import './videojs-components/peertube-load-progress-bar'
14import './videojs-components/theater-button'
15import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings'
16import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils'
17import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
18import { segmentValidatorFactory } from './p2p-media-loader/segment-validator'
19import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder'
20
21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
23// Change Captions to Subtitles/CC
24videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
25// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
26videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
27
28export type PlayerMode = 'webtorrent' | 'p2p-media-loader'
29
30export type WebtorrentOptions = {
31 videoFiles: VideoFile[]
32}
33
34export type P2PMediaLoaderOptions = {
35 playlistUrl: string
36 segmentsSha256Url: string
37 trackerAnnounce: string[]
38 redundancyBaseUrls: string[]
39 videoFiles: VideoFile[]
40}
41
42export type CommonOptions = {
43 playerElement: HTMLVideoElement
44 onPlayerElementChange: (element: HTMLVideoElement) => void
45
46 autoplay: boolean
47 videoDuration: number
48 enableHotkeys: boolean
49 inactivityTimeout: number
50 poster: string
51 startTime: number | string
52 stopTime: number | string
53
54 theaterMode: boolean
55 captions: boolean
56 peertubeLink: boolean
57
58 videoViewUrl: string
59 embedUrl: string
60
61 language?: string
62 controls?: boolean
63 muted?: boolean
64 loop?: boolean
65 subtitle?: string
66
67 videoCaptions: VideoJSCaption[]
68
69 userWatching?: UserWatching
70
71 serverUrl: string
72}
73
74export type PeertubePlayerManagerOptions = {
75 common: CommonOptions,
76 webtorrent: WebtorrentOptions,
77 p2pMediaLoader?: P2PMediaLoaderOptions
78}
79
80export class PeertubePlayerManager {
81
82 private static videojsLocaleCache: { [ path: string ]: any } = {}
83 private static playerElementClassName: string
84
85 static getServerTranslations (serverUrl: string, locale: string) {
86 const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
87 // It is the default locale, nothing to translate
88 if (!path) return Promise.resolve(undefined)
89
90 return fetch(path + '/server.json')
91 .then(res => res.json())
92 .catch(err => {
93 console.error('Cannot get server translations', err)
94 return undefined
95 })
96 }
97
98 static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) {
99 let p2pMediaLoader: any
100
101 this.playerElementClassName = options.common.playerElement.className
102
103 if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
104 if (mode === 'p2p-media-loader') {
105 [ p2pMediaLoader ] = await Promise.all([
106 import('p2p-media-loader-hlsjs'),
107 import('./p2p-media-loader/p2p-media-loader-plugin')
108 ])
109 }
110
111 const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader)
112
113 await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language)
114
115 const self = this
116 return new Promise(res => {
117 videojs(options.common.playerElement, videojsOptions, function (this: any) {
118 const player = this
119
120 player.tech_.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options))
121 player.one('error', () => self.maybeFallbackToWebTorrent(mode, player, options))
122
123 self.addContextMenu(mode, player, options.common.embedUrl)
124
125 return res(player)
126 })
127 })
128 }
129
130 private static async maybeFallbackToWebTorrent (currentMode: PlayerMode, player: any, options: PeertubePlayerManagerOptions) {
131 if (currentMode === 'webtorrent') return
132
133 console.log('Fallback to webtorrent.')
134
135 const newVideoElement = document.createElement('video')
136 newVideoElement.className = this.playerElementClassName
137
138 // VideoJS wraps our video element inside a div
139 let currentParentPlayerElement = options.common.playerElement.parentNode
140 // Fix on IOS, don't ask me why
141 if (!currentParentPlayerElement) currentParentPlayerElement = document.getElementById(options.common.playerElement.id).parentNode
142
143 currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement)
144
145 options.common.playerElement = newVideoElement
146 options.common.onPlayerElementChange(newVideoElement)
147
148 player.dispose()
149
150 await import('./webtorrent/webtorrent-plugin')
151
152 const mode = 'webtorrent'
153 const videojsOptions = this.getVideojsOptions(mode, options)
154
155 const self = this
156 videojs(newVideoElement, videojsOptions, function (this: any) {
157 const player = this
158
159 self.addContextMenu(mode, player, options.common.embedUrl)
160 })
161 }
162
163 private static loadLocaleInVideoJS (serverUrl: string, locale: string) {
164 const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
165 // It is the default locale, nothing to translate
166 if (!path) return Promise.resolve(undefined)
167
168 let p: Promise<any>
169
170 if (PeertubePlayerManager.videojsLocaleCache[path]) {
171 p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path])
172 } else {
173 p = fetch(path + '/player.json')
174 .then(res => res.json())
175 .then(json => {
176 PeertubePlayerManager.videojsLocaleCache[path] = json
177 return json
178 })
179 .catch(err => {
180 console.error('Cannot get player translations', err)
181 return undefined
182 })
183 }
184
185 const completeLocale = getCompleteLocale(locale)
186 return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
187 }
188
189 private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) {
190 const commonOptions = options.common
191 const webtorrentOptions = options.webtorrent
192 const p2pMediaLoaderOptions = options.p2pMediaLoader
193
194 let autoplay = options.common.autoplay
195 let html5 = {}
196
197 const plugins: VideoJSPluginOptions = {
198 peertube: {
199 mode,
200 autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
201 videoViewUrl: commonOptions.videoViewUrl,
202 videoDuration: commonOptions.videoDuration,
203 userWatching: commonOptions.userWatching,
204 subtitle: commonOptions.subtitle,
205 videoCaptions: commonOptions.videoCaptions,
206 stopTime: commonOptions.stopTime
207 }
208 }
209
210 if (mode === 'p2p-media-loader') {
211 const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
212 redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls,
213 type: 'application/x-mpegURL',
214 startTime: commonOptions.startTime,
215 src: p2pMediaLoaderOptions.playlistUrl
216 }
217
218 const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce
219 .filter(t => t.startsWith('ws'))
220
221 const p2pMediaLoaderConfig = {
222 loader: {
223 trackerAnnounce,
224 segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url),
225 rtcConfig: getRtcConfig(),
226 requiredSegmentsPriority: 5,
227 segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls)
228 },
229 segments: {
230 swarmId: p2pMediaLoaderOptions.playlistUrl
231 }
232 }
233 const streamrootHls = {
234 levelLabelHandler: (level: { height: number, width: number }) => {
235 const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height)
236
237 let label = file.resolution.label
238 if (file.fps >= 50) label += file.fps
239
240 return label
241 },
242 html5: {
243 hlsjsConfig: {
244 liveSyncDurationCount: 7,
245 loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass()
246 }
247 }
248 }
249
250 Object.assign(plugins, { p2pMediaLoader, streamrootHls })
251 html5 = streamrootHls.html5
252 }
253
254 if (mode === 'webtorrent') {
255 const webtorrent = {
256 autoplay,
257 videoDuration: commonOptions.videoDuration,
258 playerElement: commonOptions.playerElement,
259 videoFiles: webtorrentOptions.videoFiles,
260 startTime: commonOptions.startTime
261 }
262 Object.assign(plugins, { webtorrent })
263
264 // WebTorrent plugin handles autoplay, because we do some hackish stuff in there
265 autoplay = false
266 }
267
268 const videojsOptions = {
269 html5,
270
271 // We don't use text track settings for now
272 textTrackSettings: false,
273 controls: commonOptions.controls !== undefined ? commonOptions.controls : true,
274 loop: commonOptions.loop !== undefined ? commonOptions.loop : false,
275
276 muted: commonOptions.muted !== undefined
277 ? commonOptions.muted
278 : undefined, // Undefined so the player knows it has to check the local storage
279
280 poster: commonOptions.poster,
281 autoplay: autoplay === true ? 'any' : autoplay, // Use 'any' instead of true to get notifier by videojs if autoplay fails
282 inactivityTimeout: commonOptions.inactivityTimeout,
283 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
284 plugins,
285 controlBar: {
286 children: this.getControlBarChildren(mode, {
287 captions: commonOptions.captions,
288 peertubeLink: commonOptions.peertubeLink,
289 theaterMode: commonOptions.theaterMode
290 })
291 }
292 }
293
294 if (commonOptions.enableHotkeys === true) {
295 Object.assign(videojsOptions.plugins, {
296 hotkeys: {
297 enableVolumeScroll: false,
298 enableModifiersForNumbers: false,
299
300 fullscreenKey: function (event: KeyboardEvent) {
301 // fullscreen with the f key or Ctrl+Enter
302 return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
303 },
304
305 seekStep: function (event: KeyboardEvent) {
306 // mimic VLC seek behavior, and default to 5 (original value is 5).
307 if (event.ctrlKey && event.altKey) {
308 return 5 * 60
309 } else if (event.ctrlKey) {
310 return 60
311 } else if (event.altKey) {
312 return 10
313 } else {
314 return 5
315 }
316 },
317
318 customKeys: {
319 increasePlaybackRateKey: {
320 key: function (event: KeyboardEvent) {
321 return event.key === '>'
322 },
323 handler: function (player: videojs.Player) {
324 player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
325 }
326 },
327 decreasePlaybackRateKey: {
328 key: function (event: KeyboardEvent) {
329 return event.key === '<'
330 },
331 handler: function (player: videojs.Player) {
332 player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
333 }
334 },
335 frameByFrame: {
336 key: function (event: KeyboardEvent) {
337 return event.key === '.'
338 },
339 handler: function (player: videojs.Player) {
340 player.pause()
341 // Calculate movement distance (assuming 30 fps)
342 const dist = 1 / 30
343 player.currentTime(player.currentTime() + dist)
344 }
345 }
346 }
347 }
348 })
349 }
350
351 if (commonOptions.language && !isDefaultLocale(commonOptions.language)) {
352 Object.assign(videojsOptions, { language: commonOptions.language })
353 }
354
355 return videojsOptions
356 }
357
358 private static getControlBarChildren (mode: PlayerMode, options: {
359 peertubeLink: boolean
360 theaterMode: boolean,
361 captions: boolean
362 }) {
363 const settingEntries = []
364 const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar'
365
366 // Keep an order
367 settingEntries.push('playbackRateMenuButton')
368 if (options.captions === true) settingEntries.push('captionsButton')
369 settingEntries.push('resolutionMenuButton')
370
371 const children = {
372 'playToggle': {},
373 'currentTimeDisplay': {},
374 'timeDivider': {},
375 'durationDisplay': {},
376 'liveDisplay': {},
377
378 'flexibleWidthSpacer': {},
379 'progressControl': {
380 children: {
381 'seekBar': {
382 children: {
383 [loadProgressBar]: {},
384 'mouseTimeDisplay': {},
385 'playProgressBar': {}
386 }
387 }
388 }
389 },
390
391 'p2PInfoButton': {},
392
393 'muteToggle': {},
394 'volumeControl': {},
395
396 'settingsButton': {
397 setup: {
398 maxHeightOffset: 40
399 },
400 entries: settingEntries
401 }
402 }
403
404 if (options.peertubeLink === true) {
405 Object.assign(children, {
406 'peerTubeLinkButton': {}
407 })
408 }
409
410 if (options.theaterMode === true) {
411 Object.assign(children, {
412 'theaterButton': {}
413 })
414 }
415
416 Object.assign(children, {
417 'fullscreenToggle': {}
418 })
419
420 return children
421 }
422
423 private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) {
424 const content = [
425 {
426 label: player.localize('Copy the video URL'),
427 listener: function () {
428 copyToClipboard(buildVideoLink())
429 }
430 },
431 {
432 label: player.localize('Copy the video URL at the current time'),
433 listener: function () {
434 const player = this as videojs.Player
435 copyToClipboard(buildVideoLink(player.currentTime()))
436 }
437 },
438 {
439 label: player.localize('Copy embed code'),
440 listener: () => {
441 copyToClipboard(buildVideoEmbed(videoEmbedUrl))
442 }
443 }
444 ]
445
446 if (mode === 'webtorrent') {
447 content.push({
448 label: player.localize('Copy magnet URI'),
449 listener: function () {
450 const player = this as videojs.Player
451 copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri)
452 }
453 })
454 }
455
456 player.contextmenuUI({ content })
457 }
458
459 private static getLocalePath (serverUrl: string, locale: string) {
460 const completeLocale = getCompleteLocale(locale)
461
462 if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
463
464 return serverUrl + '/client/locales/' + completeLocale
465 }
466}
467
468// ############################################################################
469
470export {
471 videojs
472}
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts
deleted file mode 100644
index 2de6d7fef..000000000
--- a/client/src/assets/player/peertube-player.ts
+++ /dev/null
@@ -1,300 +0,0 @@
1import { VideoFile } from '../../../../shared/models/videos'
2
3import 'videojs-hotkeys'
4import 'videojs-dock'
5import 'videojs-contextmenu-ui'
6import './peertube-link-button'
7import './resolution-menu-button'
8import './settings-menu-button'
9import './webtorrent-info-button'
10import './peertube-videojs-plugin'
11import './peertube-load-progress-bar'
12import './theater-button'
13import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
14import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
15import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
16
17// FIXME: something weird with our path definition in tsconfig and typings
18// @ts-ignore
19import { Player } from 'video.js'
20
21// Change 'Playback Rate' to 'Speed' (smaller for our settings menu)
22videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed'
23// Change Captions to Subtitles/CC
24videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC'
25// We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know)
26videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' '
27
28function getVideojsOptions (options: {
29 autoplay: boolean
30 playerElement: HTMLVideoElement
31 videoViewUrl: string
32 videoDuration: number
33 videoFiles: VideoFile[]
34 enableHotkeys: boolean
35 inactivityTimeout: number
36 peertubeLink: boolean
37 poster: string
38 startTime: number | string
39 theaterMode: boolean
40 videoCaptions: VideoJSCaption[]
41
42 language?: string
43 controls?: boolean
44 muted?: boolean
45 loop?: boolean
46 subtitle?: string
47
48 userWatching?: UserWatching
49}) {
50 const videojsOptions = {
51 // We don't use text track settings for now
52 textTrackSettings: false,
53 controls: options.controls !== undefined ? options.controls : true,
54 loop: options.loop !== undefined ? options.loop : false,
55
56 muted: options.muted !== undefined ? options.muted : undefined, // Undefined so the player knows it has to check the local storage
57
58 poster: options.poster,
59 autoplay: false,
60 inactivityTimeout: options.inactivityTimeout,
61 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ],
62 plugins: {
63 peertube: {
64 autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
65 videoCaptions: options.videoCaptions,
66 videoFiles: options.videoFiles,
67 playerElement: options.playerElement,
68 videoViewUrl: options.videoViewUrl,
69 videoDuration: options.videoDuration,
70 startTime: options.startTime,
71 userWatching: options.userWatching,
72 subtitle: options.subtitle
73 }
74 },
75 controlBar: {
76 children: getControlBarChildren(options)
77 }
78 }
79
80 if (options.enableHotkeys === true) {
81 Object.assign(videojsOptions.plugins, {
82 hotkeys: {
83 enableVolumeScroll: false,
84 enableModifiersForNumbers: false,
85
86 fullscreenKey: function (event: KeyboardEvent) {
87 // fullscreen with the f key or Ctrl+Enter
88 return event.key === 'f' || (event.ctrlKey && event.key === 'Enter')
89 },
90
91 seekStep: function (event: KeyboardEvent) {
92 // mimic VLC seek behavior, and default to 5 (original value is 5).
93 if (event.ctrlKey && event.altKey) {
94 return 5 * 60
95 } else if (event.ctrlKey) {
96 return 60
97 } else if (event.altKey) {
98 return 10
99 } else {
100 return 5
101 }
102 },
103
104 customKeys: {
105 increasePlaybackRateKey: {
106 key: function (event: KeyboardEvent) {
107 return event.key === '>'
108 },
109 handler: function (player: Player) {
110 player.playbackRate((player.playbackRate() + 0.1).toFixed(2))
111 }
112 },
113 decreasePlaybackRateKey: {
114 key: function (event: KeyboardEvent) {
115 return event.key === '<'
116 },
117 handler: function (player: Player) {
118 player.playbackRate((player.playbackRate() - 0.1).toFixed(2))
119 }
120 },
121 frameByFrame: {
122 key: function (event: KeyboardEvent) {
123 return event.key === '.'
124 },
125 handler: function (player: Player) {
126 player.pause()
127 // Calculate movement distance (assuming 30 fps)
128 const dist = 1 / 30
129 player.currentTime(player.currentTime() + dist)
130 }
131 }
132 }
133 }
134 })
135 }
136
137 if (options.language && !isDefaultLocale(options.language)) {
138 Object.assign(videojsOptions, { language: options.language })
139 }
140
141 return videojsOptions
142}
143
144function getControlBarChildren (options: {
145 peertubeLink: boolean
146 theaterMode: boolean,
147 videoCaptions: VideoJSCaption[]
148}) {
149 const settingEntries = []
150
151 // Keep an order
152 settingEntries.push('playbackRateMenuButton')
153 if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton')
154 settingEntries.push('resolutionMenuButton')
155
156 const children = {
157 'playToggle': {},
158 'currentTimeDisplay': {},
159 'timeDivider': {},
160 'durationDisplay': {},
161 'liveDisplay': {},
162
163 'flexibleWidthSpacer': {},
164 'progressControl': {
165 children: {
166 'seekBar': {
167 children: {
168 'peerTubeLoadProgressBar': {},
169 'mouseTimeDisplay': {},
170 'playProgressBar': {}
171 }
172 }
173 }
174 },
175
176 'webTorrentButton': {},
177
178 'muteToggle': {},
179 'volumeControl': {},
180
181 'settingsButton': {
182 setup: {
183 maxHeightOffset: 40
184 },
185 entries: settingEntries
186 }
187 }
188
189 if (options.peertubeLink === true) {
190 Object.assign(children, {
191 'peerTubeLinkButton': {}
192 })
193 }
194
195 if (options.theaterMode === true) {
196 Object.assign(children, {
197 'theaterButton': {}
198 })
199 }
200
201 Object.assign(children, {
202 'fullscreenToggle': {}
203 })
204
205 return children
206}
207
208function addContextMenu (player: any, videoEmbedUrl: string) {
209 player.contextmenuUI({
210 content: [
211 {
212 label: player.localize('Copy the video URL'),
213 listener: function () {
214 copyToClipboard(buildVideoLink())
215 }
216 },
217 {
218 label: player.localize('Copy the video URL at the current time'),
219 listener: function () {
220 const player = this as Player
221 copyToClipboard(buildVideoLink(player.currentTime()))
222 }
223 },
224 {
225 label: player.localize('Copy embed code'),
226 listener: () => {
227 copyToClipboard(buildVideoEmbed(videoEmbedUrl))
228 }
229 },
230 {
231 label: player.localize('Copy magnet URI'),
232 listener: function () {
233 const player = this as Player
234 copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri)
235 }
236 }
237 ]
238 })
239}
240
241function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) {
242 const path = getLocalePath(serverUrl, locale)
243 // It is the default locale, nothing to translate
244 if (!path) return Promise.resolve(undefined)
245
246 let p: Promise<any>
247
248 if (loadLocaleInVideoJS.cache[path]) {
249 p = Promise.resolve(loadLocaleInVideoJS.cache[path])
250 } else {
251 p = fetch(path + '/player.json')
252 .then(res => res.json())
253 .then(json => {
254 loadLocaleInVideoJS.cache[path] = json
255 return json
256 })
257 .catch(err => {
258 console.error('Cannot get player translations', err)
259 return undefined
260 })
261 }
262
263 const completeLocale = getCompleteLocale(locale)
264 return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json))
265}
266namespace loadLocaleInVideoJS {
267 export const cache: { [ path: string ]: any } = {}
268}
269
270function getServerTranslations (serverUrl: string, locale: string) {
271 const path = getLocalePath(serverUrl, locale)
272 // It is the default locale, nothing to translate
273 if (!path) return Promise.resolve(undefined)
274
275 return fetch(path + '/server.json')
276 .then(res => res.json())
277 .catch(err => {
278 console.error('Cannot get server translations', err)
279 return undefined
280 })
281}
282
283// ############################################################################
284
285export {
286 getServerTranslations,
287 loadLocaleInVideoJS,
288 getVideojsOptions,
289 addContextMenu
290}
291
292// ############################################################################
293
294function getLocalePath (serverUrl: string, locale: string) {
295 const completeLocale = getCompleteLocale(locale)
296
297 if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined
298
299 return serverUrl + '/client/locales/' + completeLocale
300}
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
new file mode 100644
index 000000000..dd9408c8e
--- /dev/null
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -0,0 +1,269 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import * as videojs from 'video.js'
4import './videojs-components/settings-menu-button'
5import {
6 PeerTubePluginOptions,
7 ResolutionUpdateData,
8 UserWatching,
9 VideoJSCaption,
10 VideoJSComponentInterface,
11 videojsUntyped
12} from './peertube-videojs-typings'
13import { isMobile, timeToInt } from './utils'
14import {
15 getStoredLastSubtitle,
16 getStoredMute,
17 getStoredVolume,
18 saveLastSubtitle,
19 saveMuteInStore,
20 saveVolumeInStore
21} from './peertube-player-local-storage'
22
23const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
24class PeerTubePlugin extends Plugin {
25 private readonly videoViewUrl: string
26 private readonly videoDuration: number
27 private readonly CONSTANTS = {
28 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
29 }
30
31 private player: any
32 private videoCaptions: VideoJSCaption[]
33 private defaultSubtitle: string
34
35 private videoViewInterval: any
36 private userWatchingVideoInterval: any
37 private lastResolutionChange: ResolutionUpdateData
38
39 constructor (player: videojs.Player, options: PeerTubePluginOptions) {
40 super(player, options)
41
42 this.videoViewUrl = options.videoViewUrl
43 this.videoDuration = options.videoDuration
44 this.videoCaptions = options.videoCaptions
45
46 if (options.autoplay === true) this.player.addClass('vjs-has-autoplay')
47
48 this.player.on('autoplay-failure', () => {
49 this.player.removeClass('vjs-has-autoplay')
50 })
51
52 this.player.ready(() => {
53 const playerOptions = this.player.options_
54
55 if (options.mode === 'webtorrent') {
56 this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
57 this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d))
58 }
59
60 if (options.mode === 'p2p-media-loader') {
61 this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d))
62 }
63
64 this.player.tech_.on('loadedqualitydata', () => {
65 setTimeout(() => {
66 // Replay a resolution change, now we loaded all quality data
67 if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange)
68 }, 0)
69 })
70
71 const volume = getStoredVolume()
72 if (volume !== undefined) this.player.volume(volume)
73
74 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
75 if (muted !== undefined) this.player.muted(muted)
76
77 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
78
79 this.player.on('volumechange', () => {
80 saveVolumeInStore(this.player.volume())
81 saveMuteInStore(this.player.muted())
82 })
83
84 if (options.stopTime) {
85 const stopTime = timeToInt(options.stopTime)
86 const self = this
87
88 this.player.on('timeupdate', function onTimeUpdate () {
89 if (self.player.currentTime() > stopTime) {
90 self.player.pause()
91 self.player.trigger('stopped')
92
93 self.player.off('timeupdate', onTimeUpdate)
94 }
95 })
96 }
97
98 this.player.textTracks().on('change', () => {
99 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
100 return t.kind === 'captions' && t.mode === 'showing'
101 })
102
103 if (!showing) {
104 saveLastSubtitle('off')
105 return
106 }
107
108 saveLastSubtitle(showing.language)
109 })
110
111 this.player.on('sourcechange', () => this.initCaptions())
112
113 this.player.duration(options.videoDuration)
114
115 this.initializePlayer()
116 this.runViewAdd()
117
118 if (options.userWatching) this.runUserWatchVideo(options.userWatching)
119 })
120 }
121
122 dispose () {
123 if (this.videoViewInterval) clearInterval(this.videoViewInterval)
124 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
125 }
126
127 private initializePlayer () {
128 if (isMobile()) this.player.addClass('vjs-is-mobile')
129
130 this.initSmoothProgressBar()
131
132 this.initCaptions()
133
134 this.alterInactivity()
135 }
136
137 private runViewAdd () {
138 this.clearVideoViewInterval()
139
140 // After 30 seconds (or 3/4 of the video), add a view to the video
141 let minSecondsToView = 30
142
143 if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
144
145 let secondsViewed = 0
146 this.videoViewInterval = setInterval(() => {
147 if (this.player && !this.player.paused()) {
148 secondsViewed += 1
149
150 if (secondsViewed > minSecondsToView) {
151 this.clearVideoViewInterval()
152
153 this.addViewToVideo().catch(err => console.error(err))
154 }
155 }
156 }, 1000)
157 }
158
159 private runUserWatchVideo (options: UserWatching) {
160 let lastCurrentTime = 0
161
162 this.userWatchingVideoInterval = setInterval(() => {
163 const currentTime = Math.floor(this.player.currentTime())
164
165 if (currentTime - lastCurrentTime >= 1) {
166 lastCurrentTime = currentTime
167
168 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
169 .catch(err => console.error('Cannot notify user is watching.', err))
170 }
171 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
172 }
173
174 private clearVideoViewInterval () {
175 if (this.videoViewInterval !== undefined) {
176 clearInterval(this.videoViewInterval)
177 this.videoViewInterval = undefined
178 }
179 }
180
181 private addViewToVideo () {
182 if (!this.videoViewUrl) return Promise.resolve(undefined)
183
184 return fetch(this.videoViewUrl, { method: 'POST' })
185 }
186
187 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
188 const body = new URLSearchParams()
189 body.append('currentTime', currentTime.toString())
190
191 const headers = new Headers({ 'Authorization': authorizationHeader })
192
193 return fetch(url, { method: 'PUT', body, headers })
194 }
195
196 private handleResolutionChange (data: ResolutionUpdateData) {
197 this.lastResolutionChange = data
198
199 const qualityLevels = this.player.qualityLevels()
200
201 for (let i = 0; i < qualityLevels.length; i++) {
202 if (qualityLevels[i].height === data.resolutionId) {
203 data.id = qualityLevels[i].id
204 break
205 }
206 }
207
208 this.trigger('resolutionChange', data)
209 }
210
211 private alterInactivity () {
212 let saveInactivityTimeout: number
213
214 const disableInactivity = () => {
215 saveInactivityTimeout = this.player.options_.inactivityTimeout
216 this.player.options_.inactivityTimeout = 0
217 }
218 const enableInactivity = () => {
219 this.player.options_.inactivityTimeout = saveInactivityTimeout
220 }
221
222 const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
223
224 this.player.controlBar.on('mouseenter', () => disableInactivity())
225 settingsDialog.on('mouseenter', () => disableInactivity())
226 this.player.controlBar.on('mouseleave', () => enableInactivity())
227 settingsDialog.on('mouseleave', () => enableInactivity())
228 }
229
230 private initCaptions () {
231 for (const caption of this.videoCaptions) {
232 this.player.addRemoteTextTrack({
233 kind: 'captions',
234 label: caption.label,
235 language: caption.language,
236 id: caption.language,
237 src: caption.src,
238 default: this.defaultSubtitle === caption.language
239 }, false)
240 }
241
242 this.player.trigger('captionsChanged')
243 }
244
245 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657
246 private initSmoothProgressBar () {
247 const SeekBar = videojsUntyped.getComponent('SeekBar')
248 SeekBar.prototype.getPercent = function getPercent () {
249 // Allows for smooth scrubbing, when player can't keep up.
250 // const time = (this.player_.scrubbing()) ?
251 // this.player_.getCache().currentTime :
252 // this.player_.currentTime()
253 const time = this.player_.currentTime()
254 const percent = time / this.player_.duration()
255 return percent >= 1 ? 1 : percent
256 }
257 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) {
258 let newTime = this.calculateDistance(event) * this.player_.duration()
259 if (newTime === this.player_.duration()) {
260 newTime = newTime - 0.1
261 }
262 this.player_.currentTime(newTime)
263 this.update()
264 }
265 }
266}
267
268videojs.registerPlugin('peertube', PeerTubePlugin)
269export { PeerTubePlugin }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index 634c7fdc9..a96b0bc8c 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -3,11 +3,16 @@
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import { VideoFile } from '../../../../shared/models/videos/video.model' 5import { VideoFile } from '../../../../shared/models/videos/video.model'
6import { PeerTubePlugin } from './peertube-videojs-plugin' 6import { PeerTubePlugin } from './peertube-plugin'
7import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin'
8import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin'
9import { PlayerMode } from './peertube-player-manager'
7 10
8declare namespace videojs { 11declare namespace videojs {
9 interface Player { 12 interface Player {
10 peertube (): PeerTubePlugin 13 peertube (): PeerTubePlugin
14 webtorrent (): WebTorrentPlugin
15 p2pMediaLoader (): P2pMediaLoaderPlugin
11 } 16 }
12} 17}
13 18
@@ -30,26 +35,100 @@ type UserWatching = {
30 authorizationHeader: string 35 authorizationHeader: string
31} 36}
32 37
33type PeertubePluginOptions = { 38type PeerTubePluginOptions = {
34 videoFiles: VideoFile[] 39 mode: PlayerMode
35 playerElement: HTMLVideoElement 40
41 autoplay: boolean
36 videoViewUrl: string 42 videoViewUrl: string
37 videoDuration: number 43 videoDuration: number
38 startTime: number | string
39 autoplay: boolean,
40 videoCaptions: VideoJSCaption[]
41 44
42 subtitle?: string
43 userWatching?: UserWatching 45 userWatching?: UserWatching
46 subtitle?: string
47
48 videoCaptions: VideoJSCaption[]
49
50 stopTime: number | string
51}
52
53type WebtorrentPluginOptions = {
54 playerElement: HTMLVideoElement
55
56 autoplay: boolean
57 videoDuration: number
58
59 videoFiles: VideoFile[]
60
61 startTime: number | string
62}
63
64type P2PMediaLoaderPluginOptions = {
65 redundancyBaseUrls: string[]
66 type: string
67 src: string
68
69 startTime: number | string
70}
71
72type VideoJSPluginOptions = {
73 peertube: PeerTubePluginOptions
74
75 webtorrent?: WebtorrentPluginOptions
76
77 p2pMediaLoader?: P2PMediaLoaderPluginOptions
44} 78}
45 79
46// videojs typings don't have some method we need 80// videojs typings don't have some method we need
47const videojsUntyped = videojs as any 81const videojsUntyped = videojs as any
48 82
83type LoadedQualityData = {
84 qualitySwitchCallback: Function,
85 qualityData: {
86 video: {
87 id: number
88 label: string
89 selected: boolean
90 }[]
91 }
92}
93
94type ResolutionUpdateData = {
95 auto: boolean,
96 resolutionId: number
97 id?: number
98}
99
100type AutoResolutionUpdateData = {
101 possible: boolean
102}
103
104type PlayerNetworkInfo = {
105 http: {
106 downloadSpeed: number
107 uploadSpeed: number
108 downloaded: number
109 uploaded: number
110 }
111
112 p2p: {
113 downloadSpeed: number
114 uploadSpeed: number
115 downloaded: number
116 uploaded: number
117 numPeers: number
118 }
119}
120
49export { 121export {
122 PlayerNetworkInfo,
123 ResolutionUpdateData,
124 AutoResolutionUpdateData,
50 VideoJSComponentInterface, 125 VideoJSComponentInterface,
51 PeertubePluginOptions,
52 videojsUntyped, 126 videojsUntyped,
53 VideoJSCaption, 127 VideoJSCaption,
54 UserWatching 128 UserWatching,
129 PeerTubePluginOptions,
130 WebtorrentPluginOptions,
131 P2PMediaLoaderPluginOptions,
132 VideoJSPluginOptions,
133 LoadedQualityData
55} 134}
diff --git a/client/src/assets/player/resolution-menu-button.ts b/client/src/assets/player/resolution-menu-button.ts
deleted file mode 100644
index a3c1108ca..000000000
--- a/client/src/assets/player/resolution-menu-button.ts
+++ /dev/null
@@ -1,88 +0,0 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item'
7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
10class ResolutionMenuButton extends MenuButton {
11 label: HTMLElement
12
13 constructor (player: Player, options: any) {
14 super(player, options)
15 this.player = player
16
17 player.peertube().on('videoFileUpdate', () => this.updateLabel())
18 player.peertube().on('autoResolutionUpdate', () => this.updateLabel())
19 }
20
21 createEl () {
22 const el = super.createEl()
23
24 this.labelEl_ = videojsUntyped.dom.createEl('div', {
25 className: 'vjs-resolution-value',
26 innerHTML: this.buildLabelHTML()
27 })
28
29 el.appendChild(this.labelEl_)
30
31 return el
32 }
33
34 updateARIAAttributes () {
35 this.el().setAttribute('aria-label', 'Quality')
36 }
37
38 createMenu () {
39 const menu = new Menu(this.player_)
40 for (const videoFile of this.player_.peertube().videoFiles) {
41 let label = videoFile.resolution.label
42 if (videoFile.fps && videoFile.fps >= 50) {
43 label += videoFile.fps
44 }
45
46 menu.addChild(new ResolutionMenuItem(
47 this.player_,
48 {
49 id: videoFile.resolution.id,
50 label,
51 src: videoFile.magnetUri
52 })
53 )
54 }
55
56 menu.addChild(new ResolutionMenuItem(
57 this.player_,
58 {
59 id: -1,
60 label: this.player_.localize('Auto'),
61 src: null
62 }
63 ))
64
65 return menu
66 }
67
68 updateLabel () {
69 if (!this.labelEl_) return
70
71 this.labelEl_.innerHTML = this.buildLabelHTML()
72 }
73
74 buildCSSClass () {
75 return super.buildCSSClass() + ' vjs-resolution-button'
76 }
77
78 buildWrapperCSSClass () {
79 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
80 }
81
82 private buildLabelHTML () {
83 return this.player_.peertube().getCurrentResolutionLabel()
84 }
85}
86ResolutionMenuButton.prototype.controlText_ = 'Quality'
87
88MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
diff --git a/client/src/assets/player/resolution-menu-item.ts b/client/src/assets/player/resolution-menu-item.ts
deleted file mode 100644
index b54fd91ef..000000000
--- a/client/src/assets/player/resolution-menu-item.ts
+++ /dev/null
@@ -1,67 +0,0 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class ResolutionMenuItem extends MenuItem {
9
10 constructor (player: Player, options: any) {
11 const currentResolutionId = player.peertube().getCurrentResolutionId()
12 options.selectable = true
13 options.selected = options.id === currentResolutionId
14
15 super(player, options)
16
17 this.label = options.label
18 this.id = options.id
19
20 player.peertube().on('videoFileUpdate', () => this.updateSelection())
21 player.peertube().on('autoResolutionUpdate', () => this.updateSelection())
22 }
23
24 handleClick (event: any) {
25 if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return
26
27 super.handleClick(event)
28
29 // Auto resolution
30 if (this.id === -1) {
31 this.player_.peertube().enableAutoResolution()
32 return
33 }
34
35 this.player_.peertube().disableAutoResolution()
36 this.player_.peertube().updateResolution(this.id)
37 }
38
39 updateSelection () {
40 // Check if auto resolution is forbidden or not
41 if (this.id === -1) {
42 if (this.player_.peertube().isAutoResolutionForbidden()) {
43 this.addClass('disabled')
44 } else {
45 this.removeClass('disabled')
46 }
47 }
48
49 if (this.player_.peertube().isAutoResolutionOn()) {
50 this.selected(this.id === -1)
51 return
52 }
53
54 this.selected(this.player_.peertube().getCurrentResolutionId() === this.id)
55 }
56
57 getLabel () {
58 if (this.id === -1) {
59 return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>'
60 }
61
62 return this.label
63 }
64}
65MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
66
67export { ResolutionMenuItem }
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 8b9f34b99..366689962 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -4,6 +4,10 @@ function toTitleCase (str: string) {
4 return str.charAt(0).toUpperCase() + str.slice(1) 4 return str.charAt(0).toUpperCase() + str.slice(1)
5} 5}
6 6
7function isWebRTCDisabled () {
8 return !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false
9}
10
7// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts 11// https://github.com/danrevah/ngx-pipes/blob/master/src/pipes/math/bytes.ts
8// Don't import all Angular stuff, just copy the code with shame 12// Don't import all Angular stuff, just copy the code with shame
9const dictionaryBytes: Array<{max: number, type: string}> = [ 13const dictionaryBytes: Array<{max: number, type: string}> = [
@@ -42,7 +46,7 @@ function timeToInt (time: number | string) {
42 if (!time) return 0 46 if (!time) return 0
43 if (typeof time === 'number') return time 47 if (typeof time === 'number') return time
44 48
45 const reg = /^((\d+)h)?((\d+)m)?((\d+)s?)?$/ 49 const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
46 const matches = time.match(reg) 50 const matches = time.match(reg)
47 51
48 if (!matches) return 0 52 if (!matches) return 0
@@ -54,18 +58,27 @@ function timeToInt (time: number | string) {
54 return hours * 3600 + minutes * 60 + seconds 58 return hours * 3600 + minutes * 60 + seconds
55} 59}
56 60
57function secondsToTime (seconds: number) { 61function secondsToTime (seconds: number, full = false, symbol?: string) {
58 let time = '' 62 let time = ''
59 63
60 let hours = Math.floor(seconds / 3600) 64 const hourSymbol = (symbol || 'h')
61 if (hours >= 1) time = hours + 'h' 65 const minuteSymbol = (symbol || 'm')
66 const secondsSymbol = full ? '' : 's'
67
68 const hours = Math.floor(seconds / 3600)
69 if (hours >= 1) time = hours + hourSymbol
70 else if (full) time = '0' + hourSymbol
62 71
63 seconds %= 3600 72 seconds %= 3600
64 let minutes = Math.floor(seconds / 60) 73 const minutes = Math.floor(seconds / 60)
65 if (minutes >= 1) time += minutes + 'm' 74 if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
75 else if (minutes >= 1) time += minutes + minuteSymbol
76 else if (full) time += '00' + minuteSymbol
66 77
67 seconds %= 60 78 seconds %= 60
68 if (seconds >= 1) time += seconds + 's' 79 if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
80 else if (seconds >= 1) time += seconds + secondsSymbol
81 else if (full) time += '00'
69 82
70 return time 83 return time
71} 84}
@@ -112,11 +125,27 @@ function videoFileMinByResolution (files: VideoFile[]) {
112 return min 125 return min
113} 126}
114 127
128function getRtcConfig () {
129 return {
130 iceServers: [
131 {
132 urls: 'stun:stun.stunprotocol.org'
133 },
134 {
135 urls: 'stun:stun.framasoft.org'
136 }
137 ]
138 }
139}
140
115// --------------------------------------------------------------------------- 141// ---------------------------------------------------------------------------
116 142
117export { 143export {
144 getRtcConfig,
118 toTitleCase, 145 toTitleCase,
119 timeToInt, 146 timeToInt,
147 secondsToTime,
148 isWebRTCDisabled,
120 buildVideoLink, 149 buildVideoLink,
121 buildVideoEmbed, 150 buildVideoEmbed,
122 videoFileMaxByResolution, 151 videoFileMaxByResolution,
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts
index c3c1af951..6424787b2 100644
--- a/client/src/assets/player/webtorrent-info-button.ts
+++ b/client/src/assets/player/videojs-components/p2p-info-button.ts
@@ -1,8 +1,8 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { bytes } from './utils' 2import { bytes } from '../utils'
3 3
4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 4const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
5class WebtorrentInfoButton extends Button { 5class P2pInfoButton extends Button {
6 6
7 createEl () { 7 createEl () {
8 const div = videojsUntyped.dom.createEl('div', { 8 const div = videojsUntyped.dom.createEl('div', {
@@ -65,7 +65,7 @@ class WebtorrentInfoButton extends Button {
65 subDivHttp.appendChild(subDivHttpText) 65 subDivHttp.appendChild(subDivHttpText)
66 div.appendChild(subDivHttp) 66 div.appendChild(subDivHttp)
67 67
68 this.player_.peertube().on('torrentInfo', (event: any, data: any) => { 68 this.player_.on('p2pInfo', (event: any, data: PlayerNetworkInfo) => {
69 // We are in HTTP fallback 69 // We are in HTTP fallback
70 if (!data) { 70 if (!data) {
71 subDivHttp.className = 'vjs-peertube-displayed' 71 subDivHttp.className = 'vjs-peertube-displayed'
@@ -74,11 +74,14 @@ class WebtorrentInfoButton extends Button {
74 return 74 return
75 } 75 }
76 76
77 const downloadSpeed = bytes(data.downloadSpeed) 77 const p2pStats = data.p2p
78 const uploadSpeed = bytes(data.uploadSpeed) 78 const httpStats = data.http
79 const totalDownloaded = bytes(data.downloaded) 79
80 const totalUploaded = bytes(data.uploaded) 80 const downloadSpeed = bytes(p2pStats.downloadSpeed + httpStats.downloadSpeed)
81 const numPeers = data.numPeers 81 const uploadSpeed = bytes(p2pStats.uploadSpeed + httpStats.uploadSpeed)
82 const totalDownloaded = bytes(p2pStats.downloaded + httpStats.downloaded)
83 const totalUploaded = bytes(p2pStats.uploaded + httpStats.uploaded)
84 const numPeers = p2pStats.numPeers
82 85
83 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' + 86 subDivWebtorrent.title = this.player_.localize('Total downloaded: ') + totalDownloaded.join(' ') + '\n' +
84 this.player_.localize('Total uploaded: ' + totalUploaded.join(' ')) 87 this.player_.localize('Total uploaded: ' + totalUploaded.join(' '))
@@ -90,7 +93,7 @@ class WebtorrentInfoButton extends Button {
90 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ] 93 uploadSpeedUnit.textContent = ' ' + uploadSpeed[ 1 ]
91 94
92 peersNumber.textContent = numPeers 95 peersNumber.textContent = numPeers
93 peersText.textContent = ' ' + this.player_.localize('peers') 96 peersText.textContent = ' ' + (numPeers > 1 ? this.player_.localize('peers') : this.player_.localize('peer'))
94 97
95 subDivHttp.className = 'vjs-peertube-hidden' 98 subDivHttp.className = 'vjs-peertube-hidden'
96 subDivWebtorrent.className = 'vjs-peertube-displayed' 99 subDivWebtorrent.className = 'vjs-peertube-displayed'
@@ -99,4 +102,4 @@ class WebtorrentInfoButton extends Button {
99 return div 102 return div
100 } 103 }
101} 104}
102Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) 105Button.registerComponent('P2PInfoButton', P2pInfoButton)
diff --git a/client/src/assets/player/peertube-link-button.ts b/client/src/assets/player/videojs-components/peertube-link-button.ts
index de9a49de9..fed8ea33e 100644
--- a/client/src/assets/player/peertube-link-button.ts
+++ b/client/src/assets/player/videojs-components/peertube-link-button.ts
@@ -1,5 +1,5 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2import { buildVideoLink } from './utils' 2import { buildVideoLink } from '../utils'
3// FIXME: something weird with our path definition in tsconfig and typings 3// FIXME: something weird with our path definition in tsconfig and typings
4// @ts-ignore 4// @ts-ignore
5import { Player } from 'video.js' 5import { Player } from 'video.js'
diff --git a/client/src/assets/player/peertube-load-progress-bar.ts b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
index af276d1b2..9a0e3b550 100644
--- a/client/src/assets/player/peertube-load-progress-bar.ts
+++ b/client/src/assets/player/videojs-components/peertube-load-progress-bar.ts
@@ -1,4 +1,4 @@
1import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 1import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
2// FIXME: something weird with our path definition in tsconfig and typings 2// FIXME: something weird with our path definition in tsconfig and typings
3// @ts-ignore 3// @ts-ignore
4import { Player } from 'video.js' 4import { Player } from 'video.js'
@@ -27,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component {
27 } 27 }
28 28
29 update () { 29 update () {
30 const torrent = this.player().peertube().getTorrent() 30 const torrent = this.player().webtorrent().getTorrent()
31 if (!torrent) return 31 if (!torrent) return
32 32
33 this.el_.style.width = (torrent.progress * 100) + '%' 33 this.el_.style.width = (torrent.progress * 100) + '%'
diff --git a/client/src/assets/player/videojs-components/resolution-menu-button.ts b/client/src/assets/player/videojs-components/resolution-menu-button.ts
new file mode 100644
index 000000000..cff44de72
--- /dev/null
+++ b/client/src/assets/player/videojs-components/resolution-menu-button.ts
@@ -0,0 +1,109 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { ResolutionMenuItem } from './resolution-menu-item'
7
8const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
9const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton')
10class ResolutionMenuButton extends MenuButton {
11 label: HTMLElement
12
13 constructor (player: Player, options: any) {
14 super(player, options)
15 this.player = player
16
17 player.tech_.on('loadedqualitydata', (e: any, data: any) => this.buildQualities(data))
18
19 player.peertube().on('resolutionChange', () => setTimeout(() => this.trigger('updateLabel'), 0))
20 }
21
22 createEl () {
23 const el = super.createEl()
24
25 this.labelEl_ = videojsUntyped.dom.createEl('div', {
26 className: 'vjs-resolution-value'
27 })
28
29 el.appendChild(this.labelEl_)
30
31 return el
32 }
33
34 updateARIAAttributes () {
35 this.el().setAttribute('aria-label', 'Quality')
36 }
37
38 createMenu () {
39 return new Menu(this.player_)
40 }
41
42 buildCSSClass () {
43 return super.buildCSSClass() + ' vjs-resolution-button'
44 }
45
46 buildWrapperCSSClass () {
47 return 'vjs-resolution-control ' + super.buildWrapperCSSClass()
48 }
49
50 private addClickListener (component: any) {
51 component.on('click', () => {
52 const children = this.menu.children()
53
54 for (const child of children) {
55 if (component !== child) {
56 child.selected(false)
57 }
58 }
59 })
60 }
61
62 private buildQualities (data: LoadedQualityData) {
63 // The automatic resolution item will need other labels
64 const labels: { [ id: number ]: string } = {}
65
66 data.qualityData.video.sort((a, b) => {
67 if (a.id > b.id) return -1
68 if (a.id === b.id) return 0
69 return 1
70 })
71
72 for (const d of data.qualityData.video) {
73 // Skip auto resolution, we'll add it ourselves
74 if (d.id === -1) continue
75
76 this.menu.addChild(new ResolutionMenuItem(
77 this.player_,
78 {
79 id: d.id,
80 label: d.label,
81 selected: d.selected,
82 callback: data.qualitySwitchCallback
83 })
84 )
85
86 labels[d.id] = d.label
87 }
88
89 this.menu.addChild(new ResolutionMenuItem(
90 this.player_,
91 {
92 id: -1,
93 label: this.player_.localize('Auto'),
94 labels,
95 callback: data.qualitySwitchCallback,
96 selected: true // By default, in auto mode
97 }
98 ))
99
100 for (const m of this.menu.children()) {
101 this.addClickListener(m)
102 }
103
104 this.trigger('menuChanged')
105 }
106}
107ResolutionMenuButton.prototype.controlText_ = 'Quality'
108
109MenuButton.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
new file mode 100644
index 000000000..6c42fefd2
--- /dev/null
+++ b/client/src/assets/player/videojs-components/resolution-menu-item.ts
@@ -0,0 +1,83 @@
1// FIXME: something weird with our path definition in tsconfig and typings
2// @ts-ignore
3import { Player } from 'video.js'
4
5import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6
7const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
8class ResolutionMenuItem extends MenuItem {
9 private readonly id: number
10 private readonly label: string
11 // Only used for the automatic item
12 private readonly labels: { [id: number]: string }
13 private readonly callback: Function
14
15 private autoResolutionPossible: boolean
16 private currentResolutionLabel: string
17
18 constructor (player: Player, options: any) {
19 options.selectable = true
20
21 super(player, options)
22
23 this.autoResolutionPossible = true
24 this.currentResolutionLabel = ''
25
26 this.label = options.label
27 this.labels = options.labels
28 this.id = options.id
29 this.callback = options.callback
30
31 player.peertube().on('resolutionChange', (_: any, data: ResolutionUpdateData) => this.updateSelection(data))
32
33 // We only want to disable the "Auto" item
34 if (this.id === -1) {
35 player.peertube().on('autoResolutionChange', (_: any, data: AutoResolutionUpdateData) => this.updateAutoResolution(data))
36 }
37 }
38
39 handleClick (event: any) {
40 // Auto button disabled?
41 if (this.autoResolutionPossible === false && this.id === -1) return
42
43 super.handleClick(event)
44
45 this.callback(this.id, 'video')
46 }
47
48 updateSelection (data: ResolutionUpdateData) {
49 if (this.id === -1) {
50 this.currentResolutionLabel = this.labels[data.id]
51 }
52
53 // Automatic resolution only
54 if (data.auto === true) {
55 this.selected(this.id === -1)
56 return
57 }
58
59 this.selected(this.id === data.id)
60 }
61
62 updateAutoResolution (data: AutoResolutionUpdateData) {
63 // Check if the auto resolution is enabled or not
64 if (data.possible === false) {
65 this.addClass('disabled')
66 } else {
67 this.removeClass('disabled')
68 }
69
70 this.autoResolutionPossible = data.possible
71 }
72
73 getLabel () {
74 if (this.id === -1) {
75 return this.label + ' <small>' + this.currentResolutionLabel + '</small>'
76 }
77
78 return this.label
79 }
80}
81MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
82
83export { ResolutionMenuItem }
diff --git a/client/src/assets/player/settings-menu-button.ts b/client/src/assets/player/videojs-components/settings-menu-button.ts
index a7aefdcc3..5e09032b4 100644
--- a/client/src/assets/player/settings-menu-button.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-button.ts
@@ -6,8 +6,8 @@
6import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7 7
8import { SettingsMenuItem } from './settings-menu-item' 8import { SettingsMenuItem } from './settings-menu-item'
9import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10import { toTitleCase } from './utils' 10import { toTitleCase } from '../utils'
11 11
12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 12const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') 13const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu')
@@ -53,7 +53,7 @@ class SettingsButton extends Button {
53 53
54 onDisposeSettingsItem (event: any, name: string) { 54 onDisposeSettingsItem (event: any, name: string) {
55 if (name === undefined) { 55 if (name === undefined) {
56 let children = this.menu.children() 56 const children = this.menu.children()
57 57
58 while (children.length > 0) { 58 while (children.length > 0) {
59 children[0].dispose() 59 children[0].dispose()
@@ -62,7 +62,7 @@ class SettingsButton extends Button {
62 62
63 this.addClass('vjs-hidden') 63 this.addClass('vjs-hidden')
64 } else { 64 } else {
65 let item = this.menu.getChild(name) 65 const item = this.menu.getChild(name)
66 66
67 if (item) { 67 if (item) {
68 item.dispose() 68 item.dispose()
@@ -148,8 +148,8 @@ class SettingsButton extends Button {
148 return 148 return
149 } 149 }
150 150
151 let offset = this.options_.setup.maxHeightOffset 151 const offset = this.options_.setup.maxHeightOffset
152 let maxHeight = this.playerComponent.el_.offsetHeight - offset 152 const maxHeight = this.playerComponent.el_.offsetHeight - offset
153 153
154 if (height > maxHeight) { 154 if (height > maxHeight) {
155 height = maxHeight 155 height = maxHeight
@@ -166,7 +166,7 @@ class SettingsButton extends Button {
166 buildMenu () { 166 buildMenu () {
167 this.menu = new Menu(this.player()) 167 this.menu = new Menu(this.player())
168 this.menu.addClass('vjs-main-menu') 168 this.menu.addClass('vjs-main-menu')
169 let entries = this.options_.entries 169 const entries = this.options_.entries
170 170
171 if (entries.length === 0) { 171 if (entries.length === 0) {
172 this.addClass('vjs-hidden') 172 this.addClass('vjs-hidden')
@@ -174,7 +174,7 @@ class SettingsButton extends Button {
174 return 174 return
175 } 175 }
176 176
177 for (let entry of entries) { 177 for (const entry of entries) {
178 this.addMenuItem(entry, this.options_) 178 this.addMenuItem(entry, this.options_)
179 } 179 }
180 180
@@ -191,7 +191,7 @@ class SettingsButton extends Button {
191 } 191 }
192 192
193 options.name = toTitleCase(entry) 193 options.name = toTitleCase(entry)
194 let settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any) 194 const settingsMenuItem = new SettingsMenuItem(this.player(), options, entry, this as any)
195 195
196 this.menu.addChild(settingsMenuItem) 196 this.menu.addChild(settingsMenuItem)
197 197
@@ -204,7 +204,7 @@ class SettingsButton extends Button {
204 } 204 }
205 205
206 resetChildren () { 206 resetChildren () {
207 for (let menuChild of this.menu.children()) { 207 for (const menuChild of this.menu.children()) {
208 menuChild.reset() 208 menuChild.reset()
209 } 209 }
210 } 210 }
@@ -213,7 +213,7 @@ class SettingsButton extends Button {
213 * Hide all the sub menus 213 * Hide all the sub menus
214 */ 214 */
215 hideChildren () { 215 hideChildren () {
216 for (let menuChild of this.menu.children()) { 216 for (const menuChild of this.menu.children()) {
217 menuChild.hideSubMenu() 217 menuChild.hideSubMenu()
218 } 218 }
219 } 219 }
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts
index 2a3460ae5..78879a2ec 100644
--- a/client/src/assets/player/settings-menu-item.ts
+++ b/client/src/assets/player/videojs-components/settings-menu-item.ts
@@ -5,8 +5,8 @@
5// @ts-ignore 5// @ts-ignore
6import * as videojs from 'video.js' 6import * as videojs from 'video.js'
7 7
8import { toTitleCase } from './utils' 8import { toTitleCase } from '../utils'
9import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
10 10
11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') 11const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem')
12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') 12const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component')
@@ -167,7 +167,7 @@ class SettingsMenuItem extends MenuItem {
167 * @method PrefixedEvent 167 * @method PrefixedEvent
168 */ 168 */
169 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { 169 PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') {
170 let prefix = ['webkit', 'moz', 'MS', 'o', ''] 170 const prefix = ['webkit', 'moz', 'MS', 'o', '']
171 171
172 for (let p = 0; p < prefix.length; p++) { 172 for (let p = 0; p < prefix.length; p++) {
173 if (!prefix[p]) { 173 if (!prefix[p]) {
@@ -220,12 +220,14 @@ class SettingsMenuItem extends MenuItem {
220 } 220 }
221 221
222 build () { 222 build () {
223 const saveUpdateLabel = this.subMenu.updateLabel 223 this.subMenu.on('updateLabel', () => {
224 this.subMenu.updateLabel = () => {
225 this.update() 224 this.update()
226 225 })
227 saveUpdateLabel.call(this.subMenu) 226 this.subMenu.on('menuChanged', () => {
228 } 227 this.bindClickEvents()
228 this.setSize()
229 this.update()
230 })
229 231
230 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) 232 this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_)
231 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) 233 this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_)
@@ -233,7 +235,7 @@ class SettingsMenuItem extends MenuItem {
233 this.update() 235 this.update()
234 236
235 this.createBackButton() 237 this.createBackButton()
236 this.getSize() 238 this.setSize()
237 this.bindClickEvents() 239 this.bindClickEvents()
238 240
239 // prefixed event listeners for CSS TransitionEnd 241 // prefixed event listeners for CSS TransitionEnd
@@ -247,7 +249,7 @@ class SettingsMenuItem extends MenuItem {
247 249
248 update (event?: any) { 250 update (event?: any) {
249 let target: HTMLElement = null 251 let target: HTMLElement = null
250 let subMenu = this.subMenu.name() 252 const subMenu = this.subMenu.name()
251 253
252 if (event && event.type === 'tap') { 254 if (event && event.type === 'tap') {
253 target = event.target 255 target = event.target
@@ -262,7 +264,7 @@ class SettingsMenuItem extends MenuItem {
262 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250) 264 setTimeout(() => this.settingsSubMenuValueEl_.innerHTML = this.subMenu.labelEl_.innerHTML, 250)
263 } else { 265 } else {
264 // Loop trough the submenu items to find the selected child 266 // Loop trough the submenu items to find the selected child
265 for (let subMenuItem of this.subMenu.menu.children_) { 267 for (const subMenuItem of this.subMenu.menu.children_) {
266 if (!(subMenuItem instanceof component)) { 268 if (!(subMenuItem instanceof component)) {
267 continue 269 continue
268 } 270 }
@@ -285,7 +287,7 @@ class SettingsMenuItem extends MenuItem {
285 } 287 }
286 288
287 bindClickEvents () { 289 bindClickEvents () {
288 for (let item of this.subMenu.menu.children()) { 290 for (const item of this.subMenu.menu.children()) {
289 if (!(item instanceof component)) { 291 if (!(item instanceof component)) {
290 continue 292 continue
291 } 293 }
@@ -295,8 +297,9 @@ class SettingsMenuItem extends MenuItem {
295 297
296 // save size of submenus on first init 298 // save size of submenus on first init
297 // if number of submenu items change dynamically more logic will be needed 299 // if number of submenu items change dynamically more logic will be needed
298 getSize () { 300 setSize () {
299 this.dialog.removeClass('vjs-hidden') 301 this.dialog.removeClass('vjs-hidden')
302 videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden')
300 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) 303 this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_)
301 this.setMargin() 304 this.setMargin()
302 this.dialog.addClass('vjs-hidden') 305 this.dialog.addClass('vjs-hidden')
@@ -304,7 +307,7 @@ class SettingsMenuItem extends MenuItem {
304 } 307 }
305 308
306 setMargin () { 309 setMargin () {
307 let [width] = this.size 310 const [ width ] = this.size
308 311
309 this.settingsSubMenuEl_.style.marginRight = `-${width}px` 312 this.settingsSubMenuEl_.style.marginRight = `-${width}px`
310 } 313 }
diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts
index 4f8fede3d..bf383cf34 100644
--- a/client/src/assets/player/theater-button.ts
+++ b/client/src/assets/player/videojs-components/theater-button.ts
@@ -2,8 +2,8 @@
2// @ts-ignore 2// @ts-ignore
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 5import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings'
6import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' 6import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage'
7 7
8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') 8const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button')
9class TheaterButton extends Button { 9class TheaterButton extends Button {
@@ -16,8 +16,11 @@ class TheaterButton extends Button {
16 const enabled = getStoredTheater() 16 const enabled = getStoredTheater()
17 if (enabled === true) { 17 if (enabled === true) {
18 this.player_.addClass(TheaterButton.THEATER_MODE_CLASS) 18 this.player_.addClass(TheaterButton.THEATER_MODE_CLASS)
19
19 this.handleTheaterChange() 20 this.handleTheaterChange()
20 } 21 }
22
23 this.player_.theaterEnabled = enabled
21 } 24 }
22 25
23 buildCSSClass () { 26 buildCSSClass () {
@@ -25,13 +28,17 @@ class TheaterButton extends Button {
25 } 28 }
26 29
27 handleTheaterChange () { 30 handleTheaterChange () {
28 if (this.isTheaterEnabled()) { 31 const theaterEnabled = this.isTheaterEnabled()
32
33 if (theaterEnabled) {
29 this.controlText('Normal mode') 34 this.controlText('Normal mode')
30 } else { 35 } else {
31 this.controlText('Theater mode') 36 this.controlText('Theater mode')
32 } 37 }
33 38
34 saveTheaterInStore(this.isTheaterEnabled()) 39 saveTheaterInStore(theaterEnabled)
40
41 this.player_.trigger('theaterChange', theaterEnabled)
35 } 42 }
36 43
37 handleClick () { 44 handleClick () {
diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts
index 54cc0ea64..66762bef8 100644
--- a/client/src/assets/player/peertube-chunk-store.ts
+++ b/client/src/assets/player/webtorrent/peertube-chunk-store.ts
@@ -131,7 +131,7 @@ export class PeertubeChunkStore extends EventEmitter {
131 // Chunk in store 131 // Chunk in store
132 this.db.transaction('r', this.db.chunks, async () => { 132 this.db.transaction('r', this.db.chunks, async () => {
133 const result = await this.db.chunks.get({ id: index }) 133 const result = await this.db.chunks.get({ id: index })
134 if (result === undefined) return cb(null, new Buffer(0)) 134 if (result === undefined) return cb(null, Buffer.alloc(0))
135 135
136 const buf = result.buf 136 const buf = result.buf
137 if (!opts) return this.nextTick(cb, null, buf) 137 if (!opts) return this.nextTick(cb, null, buf)
@@ -162,13 +162,13 @@ export class PeertubeChunkStore extends EventEmitter {
162 } 162 }
163 163
164 if (this.db) { 164 if (this.db) {
165 await this.db.close() 165 this.db.close()
166 166
167 await this.dropDatabase(this.databaseName) 167 await this.dropDatabase(this.databaseName)
168 } 168 }
169 169
170 if (this.expirationDB) { 170 if (this.expirationDB) {
171 await this.expirationDB.close() 171 this.expirationDB.close()
172 this.expirationDB = null 172 this.expirationDB = null
173 } 173 }
174 174
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts
index a3415937b..4dce87112 100644
--- a/client/src/assets/player/video-renderer.ts
+++ b/client/src/assets/player/webtorrent/video-renderer.ts
@@ -29,7 +29,7 @@ function renderVideo (
29 29
30function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) { 30function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) {
31 const extension = extname(file.name).toLowerCase() 31 const extension = extname(file.name).toLowerCase()
32 let preparedElem: any = undefined 32 let preparedElem: any
33 let currentTime = 0 33 let currentTime = 0
34 let renderer: any 34 let renderer: any
35 35
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index e9fb90c61..eee3d4db9 100644
--- a/client/src/assets/player/peertube-videojs-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -3,23 +3,18 @@
3import * as videojs from 'video.js' 3import * as videojs from 'video.js'
4 4
5import * as WebTorrent from 'webtorrent' 5import * as WebTorrent from 'webtorrent'
6import { VideoFile } from '../../../../shared/models/videos/video.model' 6import { VideoFile } from '../../../../../shared/models/videos/video.model'
7import { renderVideo } from './video-renderer' 7import { renderVideo } from './video-renderer'
8import './settings-menu-button' 8import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
9import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' 9import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
10import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
11import { PeertubeChunkStore } from './peertube-chunk-store' 10import { PeertubeChunkStore } from './peertube-chunk-store'
12import { 11import {
13 getAverageBandwidthInStore, 12 getAverageBandwidthInStore,
14 getStoredLastSubtitle,
15 getStoredMute, 13 getStoredMute,
16 getStoredVolume, 14 getStoredVolume,
17 getStoredWebTorrentEnabled, 15 getStoredWebTorrentEnabled,
18 saveAverageBandwidth, 16 saveAverageBandwidth
19 saveLastSubtitle, 17} from '../peertube-player-local-storage'
20 saveMuteInStore,
21 saveVolumeInStore
22} from './peertube-player-local-storage'
23 18
24const CacheChunkStore = require('cache-chunk-store') 19const CacheChunkStore = require('cache-chunk-store')
25 20
@@ -30,14 +25,13 @@ type PlayOptions = {
30} 25}
31 26
32const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') 27const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
33class PeerTubePlugin extends Plugin { 28class WebTorrentPlugin extends Plugin {
34 private readonly playerElement: HTMLVideoElement 29 private readonly playerElement: HTMLVideoElement
35 30
36 private readonly autoplay: boolean = false 31 private readonly autoplay: boolean = false
37 private readonly startTime: number = 0 32 private readonly startTime: number = 0
38 private readonly savePlayerSrcFunction: Function 33 private readonly savePlayerSrcFunction: Function
39 private readonly videoFiles: VideoFile[] 34 private readonly videoFiles: VideoFile[]
40 private readonly videoViewUrl: string
41 private readonly videoDuration: number 35 private readonly videoDuration: number
42 private readonly CONSTANTS = { 36 private readonly CONSTANTS = {
43 INFO_SCHEDULER: 1000, // Don't change this 37 INFO_SCHEDULER: 1000, // Don't change this
@@ -45,22 +39,12 @@ class PeerTubePlugin extends Plugin {
45 AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it 39 AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
46 AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check 40 AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check
47 AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds 41 AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds
48 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth 42 BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
49 USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
50 } 43 }
51 44
52 private readonly webtorrent = new WebTorrent({ 45 private readonly webtorrent = new WebTorrent({
53 tracker: { 46 tracker: {
54 rtcConfig: { 47 rtcConfig: getRtcConfig()
55 iceServers: [
56 {
57 urls: 'stun:stun.stunprotocol.org'
58 },
59 {
60 urls: 'stun:stun.framasoft.org'
61 }
62 ]
63 }
64 }, 48 },
65 dht: false 49 dht: false
66 }) 50 })
@@ -68,46 +52,39 @@ class PeerTubePlugin extends Plugin {
68 private player: any 52 private player: any
69 private currentVideoFile: VideoFile 53 private currentVideoFile: VideoFile
70 private torrent: WebTorrent.Torrent 54 private torrent: WebTorrent.Torrent
71 private videoCaptions: VideoJSCaption[]
72 private defaultSubtitle: string
73 55
74 private renderer: any 56 private renderer: any
75 private fakeRenderer: any 57 private fakeRenderer: any
76 private destroyingFakeRenderer = false 58 private destroyingFakeRenderer = false
77 59
78 private autoResolution = true 60 private autoResolution = true
79 private forbidAutoResolution = false 61 private autoResolutionPossible = true
80 private isAutoResolutionObservation = false 62 private isAutoResolutionObservation = false
81 private playerRefusedP2P = false 63 private playerRefusedP2P = false
82 64
83 private videoViewInterval: any
84 private torrentInfoInterval: any 65 private torrentInfoInterval: any
85 private autoQualityInterval: any 66 private autoQualityInterval: any
86 private userWatchingVideoInterval: any
87 private addTorrentDelay: any 67 private addTorrentDelay: any
88 private qualityObservationTimer: any 68 private qualityObservationTimer: any
89 private runAutoQualitySchedulerTimer: any 69 private runAutoQualitySchedulerTimer: any
90 70
91 private downloadSpeeds: number[] = [] 71 private downloadSpeeds: number[] = []
92 72
93 constructor (player: videojs.Player, options: PeertubePluginOptions) { 73 constructor (player: videojs.Player, options: WebtorrentPluginOptions) {
94 super(player, options) 74 super(player, options)
95 75
76 this.startTime = timeToInt(options.startTime)
77
96 // Disable auto play on iOS 78 // Disable auto play on iOS
97 this.autoplay = options.autoplay && this.isIOS() === false 79 this.autoplay = options.autoplay && this.isIOS() === false
98 this.playerRefusedP2P = !getStoredWebTorrentEnabled() 80 this.playerRefusedP2P = !getStoredWebTorrentEnabled()
99 81
100 this.startTime = timeToInt(options.startTime)
101 this.videoFiles = options.videoFiles 82 this.videoFiles = options.videoFiles
102 this.videoViewUrl = options.videoViewUrl
103 this.videoDuration = options.videoDuration 83 this.videoDuration = options.videoDuration
104 this.videoCaptions = options.videoCaptions
105 84
106 this.savePlayerSrcFunction = this.player.src 85 this.savePlayerSrcFunction = this.player.src
107 this.playerElement = options.playerElement 86 this.playerElement = options.playerElement
108 87
109 if (this.autoplay === true) this.player.addClass('vjs-has-autoplay')
110
111 this.player.ready(() => { 88 this.player.ready(() => {
112 const playerOptions = this.player.options_ 89 const playerOptions = this.player.options_
113 90
@@ -117,33 +94,10 @@ class PeerTubePlugin extends Plugin {
117 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() 94 const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute()
118 if (muted !== undefined) this.player.muted(muted) 95 if (muted !== undefined) this.player.muted(muted)
119 96
120 this.defaultSubtitle = options.subtitle || getStoredLastSubtitle()
121
122 this.player.on('volumechange', () => {
123 saveVolumeInStore(this.player.volume())
124 saveMuteInStore(this.player.muted())
125 })
126
127 this.player.textTracks().on('change', () => {
128 const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
129 return t.kind === 'captions' && t.mode === 'showing'
130 })
131
132 if (!showing) {
133 saveLastSubtitle('off')
134 return
135 }
136
137 saveLastSubtitle(showing.language)
138 })
139
140 this.player.duration(options.videoDuration) 97 this.player.duration(options.videoDuration)
141 98
142 this.initializePlayer() 99 this.initializePlayer()
143 this.runTorrentInfoScheduler() 100 this.runTorrentInfoScheduler()
144 this.runViewAdd()
145
146 if (options.userWatching) this.runUserWatchVideo(options.userWatching)
147 101
148 this.player.one('play', () => { 102 this.player.one('play', () => {
149 // Don't run immediately scheduler, wait some seconds the TCP connections are made 103 // Don't run immediately scheduler, wait some seconds the TCP connections are made
@@ -157,12 +111,9 @@ class PeerTubePlugin extends Plugin {
157 clearTimeout(this.qualityObservationTimer) 111 clearTimeout(this.qualityObservationTimer)
158 clearTimeout(this.runAutoQualitySchedulerTimer) 112 clearTimeout(this.runAutoQualitySchedulerTimer)
159 113
160 clearInterval(this.videoViewInterval)
161 clearInterval(this.torrentInfoInterval) 114 clearInterval(this.torrentInfoInterval)
162 clearInterval(this.autoQualityInterval) 115 clearInterval(this.autoQualityInterval)
163 116
164 if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
165
166 // Don't need to destroy renderer, video player will be destroyed 117 // Don't need to destroy renderer, video player will be destroyed
167 this.flushVideoFile(this.currentVideoFile, false) 118 this.flushVideoFile(this.currentVideoFile, false)
168 119
@@ -173,13 +124,6 @@ class PeerTubePlugin extends Plugin {
173 return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 124 return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1
174 } 125 }
175 126
176 getCurrentResolutionLabel () {
177 if (!this.currentVideoFile) return ''
178
179 const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : ''
180 return this.currentVideoFile.resolution.label + fps
181 }
182
183 updateVideoFile ( 127 updateVideoFile (
184 videoFile?: VideoFile, 128 videoFile?: VideoFile,
185 options: { 129 options: {
@@ -228,7 +172,8 @@ class PeerTubePlugin extends Plugin {
228 return done() 172 return done()
229 }) 173 })
230 174
231 this.trigger('videoFileUpdate') 175 this.changeQuality()
176 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id })
232 } 177 }
233 178
234 updateResolution (resolutionId: number, delay = 0) { 179 updateResolution (resolutionId: number, delay = 0) {
@@ -262,28 +207,17 @@ class PeerTubePlugin extends Plugin {
262 } 207 }
263 } 208 }
264 209
265 isAutoResolutionOn () {
266 return this.autoResolution
267 }
268
269 enableAutoResolution () { 210 enableAutoResolution () {
270 this.autoResolution = true 211 this.autoResolution = true
271 this.trigger('autoResolutionUpdate') 212 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
272 } 213 }
273 214
274 disableAutoResolution (forbid = false) { 215 disableAutoResolution (forbid = false) {
275 if (forbid === true) this.forbidAutoResolution = true 216 if (forbid === true) this.autoResolutionPossible = false
276 217
277 this.autoResolution = false 218 this.autoResolution = false
278 this.trigger('autoResolutionUpdate') 219 this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible })
279 } 220 this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() })
280
281 isAutoResolutionForbidden () {
282 return this.forbidAutoResolution === true
283 }
284
285 getCurrentVideoFile () {
286 return this.currentVideoFile
287 } 221 }
288 222
289 getTorrent () { 223 getTorrent () {
@@ -413,7 +347,7 @@ class PeerTubePlugin extends Plugin {
413 if (!averageDownloadSpeed) averageDownloadSpeed = this.getAndSaveActualDownloadSpeed() 347 if (!averageDownloadSpeed) averageDownloadSpeed = this.getAndSaveActualDownloadSpeed()
414 348
415 // Limit resolution according to player height 349 // Limit resolution according to player height
416 const playerHeight = this.playerElement.offsetHeight as number 350 const playerHeight = this.playerElement.offsetHeight
417 351
418 // We take the first resolution just above the player height 352 // We take the first resolution just above the player height
419 // Example: player height is 530px, we want the 720p file instead of 480p 353 // Example: player height is 530px, we want the 720p file instead of 480p
@@ -462,13 +396,7 @@ class PeerTubePlugin extends Plugin {
462 } 396 }
463 397
464 private initializePlayer () { 398 private initializePlayer () {
465 if (isMobile()) this.player.addClass('vjs-is-mobile') 399 this.buildQualities()
466
467 this.initSmoothProgressBar()
468
469 this.initCaptions()
470
471 this.alterInactivity()
472 400
473 if (this.autoplay === true) { 401 if (this.autoplay === true) {
474 this.player.posterImage.hide() 402 this.player.posterImage.hide()
@@ -491,7 +419,7 @@ class PeerTubePlugin extends Plugin {
491 419
492 // Not initialized or in HTTP fallback 420 // Not initialized or in HTTP fallback
493 if (this.torrent === undefined || this.torrent === null) return 421 if (this.torrent === undefined || this.torrent === null) return
494 if (this.isAutoResolutionOn() === false) return 422 if (this.autoResolution === false) return
495 if (this.isAutoResolutionObservation === true) return 423 if (this.isAutoResolutionObservation === true) return
496 424
497 const file = this.getAppropriateFile() 425 const file = this.getAppropriateFile()
@@ -531,78 +459,27 @@ class PeerTubePlugin extends Plugin {
531 if (this.torrent === undefined) return 459 if (this.torrent === undefined) return
532 460
533 // Http fallback 461 // Http fallback
534 if (this.torrent === null) return this.trigger('torrentInfo', false) 462 if (this.torrent === null) return this.player.trigger('p2pInfo', false)
535 463
536 // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too 464 // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too
537 if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) 465 if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed)
538 466
539 return this.trigger('torrentInfo', { 467 return this.player.trigger('p2pInfo', {
540 downloadSpeed: this.torrent.downloadSpeed, 468 http: {
541 numPeers: this.torrent.numPeers, 469 downloadSpeed: 0,
542 uploadSpeed: this.torrent.uploadSpeed, 470 uploadSpeed: 0,
543 downloaded: this.torrent.downloaded, 471 downloaded: 0,
544 uploaded: this.torrent.uploaded 472 uploaded: 0
545 }) 473 },
546 }, this.CONSTANTS.INFO_SCHEDULER) 474 p2p: {
547 } 475 downloadSpeed: this.torrent.downloadSpeed,
548 476 numPeers: this.torrent.numPeers,
549 private runViewAdd () { 477 uploadSpeed: this.torrent.uploadSpeed,
550 this.clearVideoViewInterval() 478 downloaded: this.torrent.downloaded,
551 479 uploaded: this.torrent.uploaded
552 // After 30 seconds (or 3/4 of the video), add a view to the video
553 let minSecondsToView = 30
554
555 if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4
556
557 let secondsViewed = 0
558 this.videoViewInterval = setInterval(() => {
559 if (this.player && !this.player.paused()) {
560 secondsViewed += 1
561
562 if (secondsViewed > minSecondsToView) {
563 this.clearVideoViewInterval()
564
565 this.addViewToVideo().catch(err => console.error(err))
566 } 480 }
567 } 481 } as PlayerNetworkInfo)
568 }, 1000) 482 }, this.CONSTANTS.INFO_SCHEDULER)
569 }
570
571 private runUserWatchVideo (options: UserWatching) {
572 let lastCurrentTime = 0
573
574 this.userWatchingVideoInterval = setInterval(() => {
575 const currentTime = Math.floor(this.player.currentTime())
576
577 if (currentTime - lastCurrentTime >= 1) {
578 lastCurrentTime = currentTime
579
580 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
581 .catch(err => console.error('Cannot notify user is watching.', err))
582 }
583 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
584 }
585
586 private clearVideoViewInterval () {
587 if (this.videoViewInterval !== undefined) {
588 clearInterval(this.videoViewInterval)
589 this.videoViewInterval = undefined
590 }
591 }
592
593 private addViewToVideo () {
594 if (!this.videoViewUrl) return Promise.resolve(undefined)
595
596 return fetch(this.videoViewUrl, { method: 'POST' })
597 }
598
599 private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
600 const body = new URLSearchParams()
601 body.append('currentTime', currentTime.toString())
602
603 const headers = new Headers({ 'Authorization': authorizationHeader })
604
605 return fetch(url, { method: 'PUT', body, headers })
606 } 483 }
607 484
608 private fallbackToHttp (options: PlayOptions, done?: Function) { 485 private fallbackToHttp (options: PlayOptions, done?: Function) {
@@ -620,8 +497,10 @@ class PeerTubePlugin extends Plugin {
620 this.player.src = this.savePlayerSrcFunction 497 this.player.src = this.savePlayerSrcFunction
621 this.player.src(httpUrl) 498 this.player.src(httpUrl)
622 499
500 this.changeQuality()
501
623 // We changed the source, so reinit captions 502 // We changed the source, so reinit captions
624 this.initCaptions() 503 this.player.trigger('sourcechange')
625 504
626 return this.tryToPlay(err => { 505 return this.tryToPlay(err => {
627 if (err && done) return done(err) 506 if (err && done) return done(err)
@@ -649,25 +528,6 @@ class PeerTubePlugin extends Plugin {
649 return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) 528 return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)
650 } 529 }
651 530
652 private alterInactivity () {
653 let saveInactivityTimeout: number
654
655 const disableInactivity = () => {
656 saveInactivityTimeout = this.player.options_.inactivityTimeout
657 this.player.options_.inactivityTimeout = 0
658 }
659 const enableInactivity = () => {
660 this.player.options_.inactivityTimeout = saveInactivityTimeout
661 }
662
663 const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog')
664
665 this.player.controlBar.on('mouseenter', () => disableInactivity())
666 settingsDialog.on('mouseenter', () => disableInactivity())
667 this.player.controlBar.on('mouseleave', () => enableInactivity())
668 settingsDialog.on('mouseleave', () => enableInactivity())
669 }
670
671 private pickAverageVideoFile () { 531 private pickAverageVideoFile () {
672 if (this.videoFiles.length === 1) return this.videoFiles[0] 532 if (this.videoFiles.length === 1) return this.videoFiles[0]
673 533
@@ -712,43 +572,70 @@ class PeerTubePlugin extends Plugin {
712 } 572 }
713 } 573 }
714 574
715 private initCaptions () { 575 private buildQualities () {
716 for (const caption of this.videoCaptions) { 576 const qualityLevelsPayload = []
717 this.player.addRemoteTextTrack({ 577
718 kind: 'captions', 578 for (const file of this.videoFiles) {
719 label: caption.label, 579 const representation = {
720 language: caption.language, 580 id: file.resolution.id,
721 id: caption.language, 581 label: this.buildQualityLabel(file),
722 src: caption.src, 582 height: file.resolution.id,
723 default: this.defaultSubtitle === caption.language 583 _enabled: true
724 }, false) 584 }
585
586 this.player.qualityLevels().addQualityLevel(representation)
587
588 qualityLevelsPayload.push({
589 id: representation.id,
590 label: representation.label,
591 selected: false
592 })
725 } 593 }
726 594
727 this.player.trigger('captionsChanged') 595 const payload: LoadedQualityData = {
596 qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d),
597 qualityData: {
598 video: qualityLevelsPayload
599 }
600 }
601 this.player.tech_.trigger('loadedqualitydata', payload)
728 } 602 }
729 603
730 // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 604 private buildQualityLabel (file: VideoFile) {
731 private initSmoothProgressBar () { 605 let label = file.resolution.label
732 const SeekBar = videojsUntyped.getComponent('SeekBar') 606
733 SeekBar.prototype.getPercent = function getPercent () { 607 if (file.fps && file.fps >= 50) {
734 // Allows for smooth scrubbing, when player can't keep up. 608 label += file.fps
735 // const time = (this.player_.scrubbing()) ?
736 // this.player_.getCache().currentTime :
737 // this.player_.currentTime()
738 const time = this.player_.currentTime()
739 const percent = time / this.player_.duration()
740 return percent >= 1 ? 1 : percent
741 } 609 }
742 SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { 610
743 let newTime = this.calculateDistance(event) * this.player_.duration() 611 return label
744 if (newTime === this.player_.duration()) { 612 }
745 newTime = newTime - 0.1 613
746 } 614 private qualitySwitchCallback (id: number) {
747 this.player_.currentTime(newTime) 615 if (id === -1) {
748 this.update() 616 if (this.autoResolutionPossible === true) this.enableAutoResolution()
617 return
618 }
619
620 this.disableAutoResolution()
621 this.updateResolution(id)
622 }
623
624 private changeQuality () {
625 const resolutionId = this.currentVideoFile.resolution.id
626 const qualityLevels = this.player.qualityLevels()
627
628 if (resolutionId === -1) {
629 qualityLevels.selectedIndex = -1
630 return
631 }
632
633 for (let i = 0; i < qualityLevels; i++) {
634 const q = this.player.qualityLevels[i]
635 if (q.height === resolutionId) qualityLevels.selectedIndex = i
749 } 636 }
750 } 637 }
751} 638}
752 639
753videojs.registerPlugin('peertube', PeerTubePlugin) 640videojs.registerPlugin('webtorrent', WebTorrentPlugin)
754export { PeerTubePlugin } 641export { WebTorrentPlugin }