diff options
Diffstat (limited to 'client/src/assets')
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 | ||
3 | import * as videojs from 'video.js' | ||
4 | import { P2PMediaLoaderPluginOptions, PlayerNetworkInfo, VideoJSComponentInterface } from '../peertube-videojs-typings' | ||
5 | import { Engine, initHlsJsPlayer, initVideoJsContribHlsJsPlayer } from 'p2p-media-loader-hlsjs' | ||
6 | import { Events } from 'p2p-media-loader-core' | ||
7 | import { timeToInt } from '../utils' | ||
8 | |||
9 | // videojs-hlsjs-plugin needs videojs in window | ||
10 | window['videojs'] = videojs | ||
11 | require('@streamroot/videojs-hlsjs-plugin') | ||
12 | |||
13 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
14 | class 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 | |||
160 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
161 | export { 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 @@ | |||
1 | import { basename } from 'path' | ||
2 | import { Segment } from 'p2p-media-loader-core' | ||
3 | |||
4 | function 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 | |||
20 | export { | ||
21 | segmentUrlBuilderFactory | ||
22 | } | ||
23 | |||
24 | // --------------------------------------------------------------------------- | ||
25 | |||
26 | function 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 @@ | |||
1 | import { Segment } from 'p2p-media-loader-core' | ||
2 | import { basename } from 'path' | ||
3 | |||
4 | function 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 | |||
31 | export { | ||
32 | segmentValidatorFactory | ||
33 | } | ||
34 | |||
35 | // --------------------------------------------------------------------------- | ||
36 | |||
37 | function 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 | |||
46 | function 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 | ||
53 | function 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 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | // @ts-ignore | ||
3 | import * as videojs from 'video.js' | ||
4 | import 'videojs-hotkeys' | ||
5 | import 'videojs-dock' | ||
6 | import 'videojs-contextmenu-ui' | ||
7 | import 'videojs-contrib-quality-levels' | ||
8 | import './peertube-plugin' | ||
9 | import './videojs-components/peertube-link-button' | ||
10 | import './videojs-components/resolution-menu-button' | ||
11 | import './videojs-components/settings-menu-button' | ||
12 | import './videojs-components/p2p-info-button' | ||
13 | import './videojs-components/peertube-load-progress-bar' | ||
14 | import './videojs-components/theater-button' | ||
15 | import { P2PMediaLoaderPluginOptions, UserWatching, VideoJSCaption, VideoJSPluginOptions, videojsUntyped } from './peertube-videojs-typings' | ||
16 | import { buildVideoEmbed, buildVideoLink, copyToClipboard, getRtcConfig } from './utils' | ||
17 | import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n' | ||
18 | import { segmentValidatorFactory } from './p2p-media-loader/segment-validator' | ||
19 | import { segmentUrlBuilderFactory } from './p2p-media-loader/segment-url-builder' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.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) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | export type PlayerMode = 'webtorrent' | 'p2p-media-loader' | ||
29 | |||
30 | export type WebtorrentOptions = { | ||
31 | videoFiles: VideoFile[] | ||
32 | } | ||
33 | |||
34 | export type P2PMediaLoaderOptions = { | ||
35 | playlistUrl: string | ||
36 | segmentsSha256Url: string | ||
37 | trackerAnnounce: string[] | ||
38 | redundancyBaseUrls: string[] | ||
39 | videoFiles: VideoFile[] | ||
40 | } | ||
41 | |||
42 | export 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 | |||
74 | export type PeertubePlayerManagerOptions = { | ||
75 | common: CommonOptions, | ||
76 | webtorrent: WebtorrentOptions, | ||
77 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
78 | } | ||
79 | |||
80 | export 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 | |||
470 | export { | ||
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 @@ | |||
1 | import { VideoFile } from '../../../../shared/models/videos' | ||
2 | |||
3 | import 'videojs-hotkeys' | ||
4 | import 'videojs-dock' | ||
5 | import 'videojs-contextmenu-ui' | ||
6 | import './peertube-link-button' | ||
7 | import './resolution-menu-button' | ||
8 | import './settings-menu-button' | ||
9 | import './webtorrent-info-button' | ||
10 | import './peertube-videojs-plugin' | ||
11 | import './peertube-load-progress-bar' | ||
12 | import './theater-button' | ||
13 | import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings' | ||
14 | import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils' | ||
15 | import { 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 | ||
19 | import { Player } from 'video.js' | ||
20 | |||
21 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
22 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
23 | // Change Captions to Subtitles/CC | ||
24 | videojsUntyped.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) | ||
26 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
27 | |||
28 | function 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 | |||
144 | function 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 | |||
208 | function 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 | |||
241 | function 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 | } | ||
266 | namespace loadLocaleInVideoJS { | ||
267 | export const cache: { [ path: string ]: any } = {} | ||
268 | } | ||
269 | |||
270 | function 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 | |||
285 | export { | ||
286 | getServerTranslations, | ||
287 | loadLocaleInVideoJS, | ||
288 | getVideojsOptions, | ||
289 | addContextMenu | ||
290 | } | ||
291 | |||
292 | // ############################################################################ | ||
293 | |||
294 | function 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 | ||
3 | import * as videojs from 'video.js' | ||
4 | import './videojs-components/settings-menu-button' | ||
5 | import { | ||
6 | PeerTubePluginOptions, | ||
7 | ResolutionUpdateData, | ||
8 | UserWatching, | ||
9 | VideoJSCaption, | ||
10 | VideoJSComponentInterface, | ||
11 | videojsUntyped | ||
12 | } from './peertube-videojs-typings' | ||
13 | import { isMobile, timeToInt } from './utils' | ||
14 | import { | ||
15 | getStoredLastSubtitle, | ||
16 | getStoredMute, | ||
17 | getStoredVolume, | ||
18 | saveLastSubtitle, | ||
19 | saveMuteInStore, | ||
20 | saveVolumeInStore | ||
21 | } from './peertube-player-local-storage' | ||
22 | |||
23 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
24 | class 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 | |||
268 | videojs.registerPlugin('peertube', PeerTubePlugin) | ||
269 | export { 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 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
6 | import { PeerTubePlugin } from './peertube-videojs-plugin' | 6 | import { PeerTubePlugin } from './peertube-plugin' |
7 | import { WebTorrentPlugin } from './webtorrent/webtorrent-plugin' | ||
8 | import { P2pMediaLoaderPlugin } from './p2p-media-loader/p2p-media-loader-plugin' | ||
9 | import { PlayerMode } from './peertube-player-manager' | ||
7 | 10 | ||
8 | declare namespace videojs { | 11 | declare 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 | ||
33 | type PeertubePluginOptions = { | 38 | type 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 | |||
53 | type WebtorrentPluginOptions = { | ||
54 | playerElement: HTMLVideoElement | ||
55 | |||
56 | autoplay: boolean | ||
57 | videoDuration: number | ||
58 | |||
59 | videoFiles: VideoFile[] | ||
60 | |||
61 | startTime: number | string | ||
62 | } | ||
63 | |||
64 | type P2PMediaLoaderPluginOptions = { | ||
65 | redundancyBaseUrls: string[] | ||
66 | type: string | ||
67 | src: string | ||
68 | |||
69 | startTime: number | string | ||
70 | } | ||
71 | |||
72 | type 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 |
47 | const videojsUntyped = videojs as any | 81 | const videojsUntyped = videojs as any |
48 | 82 | ||
83 | type LoadedQualityData = { | ||
84 | qualitySwitchCallback: Function, | ||
85 | qualityData: { | ||
86 | video: { | ||
87 | id: number | ||
88 | label: string | ||
89 | selected: boolean | ||
90 | }[] | ||
91 | } | ||
92 | } | ||
93 | |||
94 | type ResolutionUpdateData = { | ||
95 | auto: boolean, | ||
96 | resolutionId: number | ||
97 | id?: number | ||
98 | } | ||
99 | |||
100 | type AutoResolutionUpdateData = { | ||
101 | possible: boolean | ||
102 | } | ||
103 | |||
104 | type 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 | |||
49 | export { | 121 | export { |
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 | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class 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 | } | ||
86 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
87 | |||
88 | MenuButton.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 | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class 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 | } | ||
65 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
66 | |||
67 | export { 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 | ||
7 | function 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 |
9 | const dictionaryBytes: Array<{max: number, type: string}> = [ | 13 | const 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 | ||
57 | function secondsToTime (seconds: number) { | 61 | function 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 | ||
128 | function 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 | ||
117 | export { | 143 | export { |
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 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { PlayerNetworkInfo, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { bytes } from './utils' | 2 | import { bytes } from '../utils' |
3 | 3 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class WebtorrentInfoButton extends Button { | 5 | class 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 | } |
102 | Button.registerComponent('WebTorrentButton', WebtorrentInfoButton) | 105 | Button.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 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { buildVideoLink } from './utils' | 2 | import { 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 |
5 | import { Player } from 'video.js' | 5 | import { 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 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | import { 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 |
4 | import { Player } from 'video.js' | 4 | import { 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 | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { LoadedQualityData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
7 | |||
8 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
9 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
10 | class 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 | } | ||
107 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
108 | |||
109 | MenuButton.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 | ||
3 | import { Player } from 'video.js' | ||
4 | |||
5 | import { AutoResolutionUpdateData, ResolutionUpdateData, VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | |||
7 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
8 | class 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 | } | ||
81 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
82 | |||
83 | export { 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 @@ | |||
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { SettingsMenuItem } from './settings-menu-item' | 8 | import { SettingsMenuItem } from './settings-menu-item' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | import { toTitleCase } from './utils' | 10 | import { toTitleCase } from '../utils' |
11 | 11 | ||
12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 13 | const 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 |
6 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | 7 | ||
8 | import { toTitleCase } from './utils' | 8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
10 | 10 | ||
11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const 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 |
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
6 | import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' | 6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' |
7 | 7 | ||
8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
9 | class TheaterButton extends Button { | 9 | class 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 | ||
30 | function renderMedia (file: any, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) { | 30 | function 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 @@ | |||
3 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | 4 | ||
5 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
6 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../../shared/models/videos/video.model' |
7 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
8 | import './settings-menu-button' | 8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
9 | import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
10 | import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' | ||
11 | import { PeertubeChunkStore } from './peertube-chunk-store' | 10 | import { PeertubeChunkStore } from './peertube-chunk-store' |
12 | import { | 11 | import { |
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 | ||
24 | const CacheChunkStore = require('cache-chunk-store') | 19 | const CacheChunkStore = require('cache-chunk-store') |
25 | 20 | ||
@@ -30,14 +25,13 @@ type PlayOptions = { | |||
30 | } | 25 | } |
31 | 26 | ||
32 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
33 | class PeerTubePlugin extends Plugin { | 28 | class 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 | ||
753 | videojs.registerPlugin('peertube', PeerTubePlugin) | 640 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) |
754 | export { PeerTubePlugin } | 641 | export { WebTorrentPlugin } |