diff options
Diffstat (limited to 'client/src/assets')
58 files changed, 1663 insertions, 976 deletions
diff --git a/client/src/assets/images/global/add.svg b/client/src/assets/images/global/add.html index 42b269c43..bfb0a52bc 100644 --- a/client/src/assets/images/global/add.svg +++ b/client/src/assets/images/global/add.html | |||
@@ -1,8 +1,6 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-92.000000, -115.000000)"> |
5 | <g id="Artboard-4" transform="translate(-92.000000, -115.000000)"> | ||
6 | <g id="2" transform="translate(92.000000, 115.000000)"> | 4 | <g id="2" transform="translate(92.000000, 115.000000)"> |
7 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> | 5 | <circle id="Oval-1" stroke="#ffffff" stroke-width="2" cx="12" cy="12" r="10"></circle> |
8 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> | 6 | <rect id="Rectangle-1" fill="#ffffff" x="11" y="7" width="2" height="10" rx="1"></rect> |
diff --git a/client/src/assets/images/video/alert.svg b/client/src/assets/images/global/alert.html index 5b43534ad..7c8c02074 100644 --- a/client/src/assets/images/video/alert.svg +++ b/client/src/assets/images/global/alert.html | |||
@@ -1,11 +1,6 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>alert</title> | 3 | <g transform="translate(-48.000000, -467.000000)"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -467.000000)"> | ||
9 | <g id="161" transform="translate(48.000000, 467.000000)"> | 4 | <g id="161" transform="translate(48.000000, 467.000000)"> |
10 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> | 5 | <path d="M12.8715755,3.50973876 L12,1.96027114 L11.1284245,3.50973876 L2.12842446,19.5097388 L1.29015252,21 L3,21 L21,21 L22.7098475,21 L21.8715755,19.5097388 L12.8715755,3.50973876 Z" id="Triangle-2" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> |
11 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#000000"></path> | 6 | <path d="M12,17.75 C12.6903559,17.75 13.25,17.1903559 13.25,16.5 C13.25,15.8096441 12.6903559,15.25 12,15.25 C11.3096441,15.25 10.75,15.8096441 10.75,16.5 C10.75,17.1903559 11.3096441,17.75 12,17.75 Z" id="Oval-8" fill="#000000"></path> |
diff --git a/client/src/assets/images/global/circle-tick.html b/client/src/assets/images/global/circle-tick.html new file mode 100644 index 000000000..2327de6be --- /dev/null +++ b/client/src/assets/images/global/circle-tick.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-400.000000, -1134.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
5 | <g id="yes" transform="translate(352.000000, 88.000000)"> | ||
6 | <circle id="Oval-1" cx="12" cy="12" r="10"/> | ||
7 | <polyline id="Path-288" stroke-linecap="round" stroke-linejoin="round" points="8.5 12.5 10.5 14.5 15.5 9.5"/> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </g> | ||
12 | </svg> | ||
diff --git a/client/src/assets/images/global/cloud-download.html b/client/src/assets/images/global/cloud-download.html new file mode 100644 index 000000000..b2634fd1f --- /dev/null +++ b/client/src/assets/images/global/cloud-download.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
3 | <g transform="translate(-356.000000, -775.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="308" transform="translate(356.000000, 775.000000)"> | ||
5 | <path d="M8,17 L5,17 L5,17 C2.790861,17 1,15.209139 1,13 C1,10.790861 2.790861,9 5,9 C5.35840468,9 5.70579988,9.04713713 6.03632437,9.13555013 C6.01233106,8.92702603 6,8.71495305 6,8.5 C6,5.46243388 8.46243388,3 11.5,3 C14.0673313,3 16.2238156,4.7590449 16.8299648,7.1376465 C17.2052921,7.04765874 17.5970804,7 18,7 C20.7614237,7 23,9.23857625 23,12 C23,14.7614237 20.7614237,17 18,17 L16,17" id="Combined-Shape" stroke-linejoin="round"></path> | ||
6 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
7 | <polyline id="Path-59" stroke-linejoin="round" points="15 20 12 23 9 20"></polyline> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/cloud-error.html b/client/src/assets/images/global/cloud-error.html new file mode 100644 index 000000000..1a3483805 --- /dev/null +++ b/client/src/assets/images/global/cloud-error.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
3 | <g transform="translate(-400.000000, -775.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="309" transform="translate(400.000000, 775.000000)"> | ||
5 | <path d="M7,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L17,18" id="Combined-Shape"></path> | ||
6 | <path d="M9,21 L15,15" id="Path-238"></path> | ||
7 | <path d="M9,21 L15,15" id="Path-238" transform="translate(12.000000, 18.000000) scale(-1, 1) translate(-12.000000, -18.000000) "></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/cog.html b/client/src/assets/images/global/cog.html new file mode 100644 index 000000000..b74a180e7 --- /dev/null +++ b/client/src/assets/images/global/cog.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linejoin="round"> | ||
3 | <g transform="translate(-796.000000, -159.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="38" transform="translate(796.000000, 159.000000)"> | ||
5 | <path d="M7.20852293,4.3800958 C8.05442158,3.84706631 8.99528987,3.45099725 10,3.22301642 L10,1.99980749 C10,1.44762906 10.4433532,1 11.0093689,1 L12.9906311,1 C13.5480902,1 14,1.44371665 14,1.99980749 L14,3.22301642 C15.0047101,3.45099725 15.9455784,3.84706631 16.7914771,4.3800958 L17.6569904,3.5145825 C18.0474395,3.12413339 18.6774591,3.12110988 19.0776926,3.52134344 L20.4786566,4.92230738 C20.8728396,5.31649045 20.8786331,5.94979402 20.4854175,6.34300963 L19.6199042,7.20852293 C20.1529337,8.05442158 20.5490027,8.99528987 20.7769836,10 L22.0001925,10 C22.5523709,10 23,10.4433532 23,11.0093689 L23,12.9906311 C23,13.5480902 22.5562834,14 22.0001925,14 L20.7769836,14 C20.5490027,15.0047101 20.1529337,15.9455784 19.6199042,16.7914771 L20.4854175,17.6569904 C20.8758666,18.0474395 20.8788901,18.6774591 20.4786566,19.0776926 L19.0776926,20.4786566 C18.6835095,20.8728396 18.050206,20.8786331 17.6569904,20.4854175 L16.7914771,19.6199042 C15.9455784,20.1529337 15.0047101,20.5490027 14,20.7769836 L14,22.0001925 C14,22.5523709 13.5566468,23 12.9906311,23 L11.0093689,23 C10.4519098,23 10,22.5562834 10,22.0001925 L10,20.7769836 C8.99528987,20.5490027 8.05442158,20.1529337 7.20852293,19.6199042 L6.34300963,20.4854175 C5.95256051,20.8758666 5.32254093,20.8788901 4.92230738,20.4786566 L3.52134344,19.0776926 C3.12716036,18.6835095 3.12136689,18.050206 3.5145825,17.6569904 L4.3800958,16.7914771 C3.84706631,15.9455784 3.45099725,15.0047101 3.22301642,14 L1.99980749,14 C1.44762906,14 1,13.5566468 1,12.9906311 L1,11.0093689 C1,10.4519098 1.44371665,10 1.99980749,10 L3.22301642,10 C3.45099725,8.99528987 3.84706631,8.05442158 4.3800958,7.20852293 L3.5145825,6.34300963 C3.12413339,5.95256051 3.12110988,5.32254093 3.52134344,4.92230738 L4.92230738,3.52134344 C5.31649045,3.12716036 5.94979402,3.12136689 6.34300963,3.5145825 L7.20852293,4.3800958 Z M12,16 C14.209139,16 16,14.209139 16,12 C16,9.790861 14.209139,8 12,8 C9.790861,8 8,9.790861 8,12 C8,14.209139 9.790861,16 12,16 Z" id="Combined-Shape"/> | ||
6 | </g> | ||
7 | </g> | ||
8 | </g> | ||
9 | </svg> | ||
diff --git a/client/src/assets/images/global/cross.svg b/client/src/assets/images/global/cross.html index d47a75996..962578487 100644 --- a/client/src/assets/images/global/cross.svg +++ b/client/src/assets/images/global/cross.html | |||
@@ -1,8 +1,6 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-312.000000, -115.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-312.000000, -115.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="7" transform="translate(312.000000, 115.000000)"> | 4 | <g id="7" transform="translate(312.000000, 115.000000)"> |
7 | <path d="M19,5 L5,19" id="Path-14"></path> | 5 | <path d="M19,5 L5,19" id="Path-14"></path> |
8 | <path d="M19,5 L5,19" id="Path-14" transform="translate(12.000000, 12.000000) scale(-1, 1) translate(-12.000000, -12.000000) "></path> | 6 | <path d="M19,5 L5,19" id="Path-14" transform="translate(12.000000, 12.000000) scale(-1, 1) translate(-12.000000, -12.000000) "></path> |
diff --git a/client/src/assets/images/global/delete-black.svg b/client/src/assets/images/global/delete-black.svg deleted file mode 100644 index 04ddc23aa..000000000 --- a/client/src/assets/images/global/delete-black.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#000" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-grey.svg b/client/src/assets/images/global/delete-grey.svg deleted file mode 100644 index 67e9e2ce7..000000000 --- a/client/src/assets/images/global/delete-grey.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | ||
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#585858" stroke-width="2"></path> | ||
8 | <rect id="Rectangle-424" fill="#585858" x="2" y="4" width="20" height="2" rx="1"></rect> | ||
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#585858"></path> | ||
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#585858" stroke-width="2" stroke-linejoin="round"></path> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/global/delete-white.svg b/client/src/assets/images/global/delete.html index 9c52de557..a0d9a0cac 100644 --- a/client/src/assets/images/global/delete-white.svg +++ b/client/src/assets/images/global/delete.html | |||
@@ -1,13 +1,11 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-224.000000, -159.000000)"> |
5 | <g id="Artboard-4" transform="translate(-224.000000, -159.000000)"> | ||
6 | <g id="25" transform="translate(224.000000, 159.000000)"> | 4 | <g id="25" transform="translate(224.000000, 159.000000)"> |
7 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#ffffff" stroke-width="2"></path> | 5 | <path d="M5,7 L5,20.0081158 C5,21.1082031 5.89706013,22 7.00585866,22 L16.9941413,22 C18.1019465,22 19,21.1066027 19,20.0081158 L19,7" id="Path-296" stroke="#000000" stroke-width="2"></path> |
8 | <rect id="Rectangle-424" fill="#ffffff" x="2" y="4" width="20" height="2" rx="1"></rect> | 6 | <rect id="Rectangle-424" fill="#000000" x="2" y="4" width="20" height="2" rx="1"></rect> |
9 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#ffffff"></path> | 7 | <path d="M9,10.9970301 C9,10.4463856 9.44386482,10 10,10 C10.5522847,10 11,10.4530363 11,10.9970301 L11,17.0029699 C11,17.5536144 10.5561352,18 10,18 C9.44771525,18 9,17.5469637 9,17.0029699 L9,10.9970301 Z M13,10.9970301 C13,10.4463856 13.4438648,10 14,10 C14.5522847,10 15,10.4530363 15,10.9970301 L15,17.0029699 C15,17.5536144 14.5561352,18 14,18 C13.4477153,18 13,17.5469637 13,17.0029699 L13,10.9970301 Z" id="Combined-Shape" fill="#000000"></path> |
10 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#ffffff" stroke-width="2" stroke-linejoin="round"></path> | 8 | <path d="M9,5 L9,2.99895656 C9,2.44724809 9.45097518,2 9.99077797,2 L14.009222,2 C14.5564136,2 15,2.44266033 15,2.99895656 L15,5" id="Path-33" stroke="#000000" stroke-width="2" stroke-linejoin="round"></path> |
11 | </g> | 9 | </g> |
12 | </g> | 10 | </g> |
13 | </g> | 11 | </g> |
diff --git a/client/src/assets/images/video/download-black.svg b/client/src/assets/images/global/download.html index 501836746..259506f31 100644 --- a/client/src/assets/images/video/download-black.svg +++ b/client/src/assets/images/global/download.html | |||
@@ -1,11 +1,6 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>download</title> | 3 | <g transform="translate(-180.000000, -291.000000)" stroke="#000000" stroke-width="2"> |
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"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | 4 | <g id="84" transform="translate(180.000000, 291.000000)"> |
10 | <path d="M12,3 L12,15" id="Path-58"></path> | 5 | <path d="M12,3 L12,15" id="Path-58"></path> |
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | 6 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> |
diff --git a/client/src/assets/images/global/edit-black.svg b/client/src/assets/images/global/edit-black.svg deleted file mode 100644 index 0176b0f37..000000000 --- a/client/src/assets/images/global/edit-black.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>edit</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"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | ||
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | ||
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/global/edit-grey.svg b/client/src/assets/images/global/edit.html index 23ece68f1..f04183c2d 100644 --- a/client/src/assets/images/global/edit-grey.svg +++ b/client/src/assets/images/global/edit.html | |||
@@ -1,11 +1,6 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>edit</title> | 3 | <g transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="41" transform="translate(48.000000, 203.000000)"> | 4 | <g id="41" transform="translate(48.000000, 203.000000)"> |
10 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> | 5 | <path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path> |
11 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> | 6 | <path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path> |
diff --git a/client/src/assets/images/global/help.svg b/client/src/assets/images/global/help.html index 48252febe..80cd40321 100644 --- a/client/src/assets/images/global/help.svg +++ b/client/src/assets/images/global/help.html | |||
@@ -1,12 +1,10 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-400.000000, -247.000000)"> |
5 | <g id="Artboard-4" transform="translate(-400.000000, -247.000000)"> | ||
6 | <g id="69" transform="translate(400.000000, 247.000000)"> | 4 | <g id="69" transform="translate(400.000000, 247.000000)"> |
7 | <circle id="Oval-7" stroke="#333333" stroke-width="2" cx="12" cy="12" r="10"></circle> | 5 | <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="#333333"></path> | 6 | <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> | 7 | </g> |
10 | </g> | 8 | </g> |
11 | </g> | 9 | </g> |
12 | </svg> \ No newline at end of file | 10 | </svg> |
diff --git a/client/src/assets/images/global/im-with-her.svg b/client/src/assets/images/global/im-with-her.html index 31d4754fd..de2c62e96 100644 --- a/client/src/assets/images/global/im-with-her.svg +++ b/client/src/assets/images/global/im-with-her.html | |||
@@ -1,15 +1,10 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <title>im-with-her</title> | 3 | <g transform="translate(-708.000000, -467.000000)"> |
5 | <desc>Created with Sketch.</desc> | ||
6 | <defs></defs> | ||
7 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -467.000000)"> | ||
9 | <g id="176" transform="translate(708.000000, 467.000000)"> | 4 | <g id="176" transform="translate(708.000000, 467.000000)"> |
10 | <path d="M8,9 L8,3.99339768 C8,3.44494629 7.55641359,3 7.00922203,3 L2.99077797,3 C2.45097518,3 2,3.44475929 2,3.99339768 L2,20.0066023 C2,20.5550537 2.44358641,21 2.99077797,21 L7.00922203,21 C7.54902482,21 8,20.5552407 8,20.0066023 L8,15 L14,15 L14,20.0066023 C14,20.5550537 14.4435864,21 14.990778,21 L19.009222,21 C19.5490248,21 20,20.5564587 20,20.0093228 L20,15.0006104 L23,12 L20,8.99267578 L20,4.00303919 C20,3.45042467 19.5564136,3 19.009222,3 L14.990778,3 C14.4509752,3 14,3.44475929 14,3.99339768 L14,9 L8,9 Z" id="Combined-Shape" fill="#333333" opacity="0.5"></path> | 5 | <path d="M8,9 L8,3.99339768 C8,3.44494629 7.55641359,3 7.00922203,3 L2.99077797,3 C2.45097518,3 2,3.44475929 2,3.99339768 L2,20.0066023 C2,20.5550537 2.44358641,21 2.99077797,21 L7.00922203,21 C7.54902482,21 8,20.5552407 8,20.0066023 L8,15 L14,15 L14,20.0066023 C14,20.5550537 14.4435864,21 14.990778,21 L19.009222,21 C19.5490248,21 20,20.5564587 20,20.0093228 L20,15.0006104 L23,12 L20,8.99267578 L20,4.00303919 C20,3.45042467 19.5564136,3 19.009222,3 L14.990778,3 C14.4509752,3 14,3.44475929 14,3.99339768 L14,9 L8,9 Z" id="Combined-Shape" fill="#000000" opacity="0.5"></path> |
11 | <path d="M2,9 L14,9 L14,3.99077797 C14,3.44358641 14.3203148,3.32031476 14.7062149,3.7062149 L23,12 L14.7062149,20.2937851 C14.3161832,20.6838168 14,20.5490248 14,20.009222 L14,15 L2,15 L2,9 Z" id="Rectangle-121" fill-opacity="0.5" fill="#000000"></path> | 6 | <path d="M2,9 L14,9 L14,3.99077797 C14,3.44358641 14.3203148,3.32031476 14.7062149,3.7062149 L23,12 L14.7062149,20.2937851 C14.3161832,20.6838168 14,20.5490248 14,20.009222 L14,15 L2,15 L2,9 Z" id="Rectangle-121" fill-opacity="0.5" fill="#000000"></path> |
12 | </g> | 7 | </g> |
13 | </g> | 8 | </g> |
14 | </g> | 9 | </g> |
15 | </svg> \ No newline at end of file | 10 | </svg> |
diff --git a/client/src/assets/images/global/no.html b/client/src/assets/images/global/no.html new file mode 100644 index 000000000..bb7b28514 --- /dev/null +++ b/client/src/assets/images/global/no.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-312.000000, -863.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="347" transform="translate(312.000000, 863.000000)"> | ||
5 | <circle id="Oval-196" cx="12" cy="12" r="9"></circle> | ||
6 | <path d="M18,18 L6,6" id="Path-275"></path> | ||
7 | </g> | ||
8 | </g> | ||
9 | </g> | ||
10 | </svg> | ||
diff --git a/client/src/assets/images/global/sparkle.html b/client/src/assets/images/global/sparkle.html new file mode 100644 index 000000000..3b29fefb9 --- /dev/null +++ b/client/src/assets/images/global/sparkle.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"> | ||
3 | <g transform="translate(-488.000000, -731.000000)" stroke="#000000" stroke-width="2"> | ||
4 | <g id="291" transform="translate(488.000000, 731.000000)"> | ||
5 | <path d="M10,9 C8.5,7.5 8,3 8,3 C8,3 7.5,7.5 6,9 C4.5,10.5 2,11 2,11 C2,11 4.5,11.5 6,13 C7.5,14.5 8,19 8,19 C8,19 8.5,14.5 10,13 C11.5,11.5 14,11 14,11 C14,11 11.5,10.5 10,9 Z" id="Combined-Shape"></path> | ||
6 | <path d="M19.6666667,4.75 C18.7916667,3.8125 18.5,1 18.5,1 C18.5,1 18.2083333,3.8125 17.3333333,4.75 C16.4583333,5.6875 15,6 15,6 C15,6 16.4583333,6.3125 17.3333333,7.25 C18.2083333,8.1875 18.5,11 18.5,11 C18.5,11 18.7916667,8.1875 19.6666667,7.25 C20.5416667,6.3125 22,6 22,6 C22,6 20.5416667,5.6875 19.6666667,4.75 Z" id="Combined-Shape"></path> | ||
7 | <path d="M17,17 C16.25,16.25 16,14 16,14 C16,14 15.75,16.25 15,17 C14.25,17.75 13,18 13,18 C13,18 14.25,18.25 15,19 C15.75,19.75 16,22 16,22 C16,22 16.25,19.75 17,19 C17.75,18.25 19,18 19,18 C19,18 17.75,17.75 17,17 Z" id="Combined-Shape"></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/syndication.svg b/client/src/assets/images/global/syndication.html index cb74cf81b..e6c88a4db 100644 --- a/client/src/assets/images/global/syndication.svg +++ b/client/src/assets/images/global/syndication.html | |||
@@ -1,10 +1,8 @@ | |||
1 | <?xml version="1.0" encoding="iso-8859-1"?> | ||
2 | <!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | ||
3 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | 1 | <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" |
4 | viewBox="0 0 559.372 559.372" style="enable-background:new 0 0 559.372 559.372;" xml:space="preserve"> | 2 | viewBox="0 0 559.372 559.372" style="enable-background:new 0 0 559.372 559.372;" xml:space="preserve"> |
5 | <g> | 3 | <g> |
6 | <g> | 4 | <g> |
7 | <path style="fill:#010002;" d="M53.244,0.002c46.512,0,91.29,6.018,134.334,18.054s83.334,29.07,120.869,51.102 | 5 | <path fill="#000000" d="M53.244,0.002c46.512,0,91.29,6.018,134.334,18.054s83.334,29.07,120.869,51.102 |
8 | c37.537,22.032,71.707,48.45,102.514,79.254c30.803,30.804,57.221,64.974,79.254,102.51 | 6 | c37.537,22.032,71.707,48.45,102.514,79.254c30.803,30.804,57.221,64.974,79.254,102.51 |
9 | c22.029,37.539,39.063,77.828,51.102,120.873c12.037,43.043,18.055,87.818,18.055,134.334c0,14.688-5.201,27.23-15.605,37.637 | 7 | c22.029,37.539,39.063,77.828,51.102,120.873c12.037,43.043,18.055,87.818,18.055,134.334c0,14.688-5.201,27.23-15.605,37.637 |
10 | c-10.404,10.407-22.949,15.604-37.637,15.604c-14.689,0-27.234-5.199-37.641-15.604c-10.402-10.404-15.604-22.949-15.604-37.637 | 8 | c-10.404,10.407-22.949,15.604-37.637,15.604c-14.689,0-27.234-5.199-37.641-15.604c-10.402-10.404-15.604-22.949-15.604-37.637 |
diff --git a/client/src/assets/images/global/tick.svg b/client/src/assets/images/global/tick.html index 230caa111..4784b4807 100644 --- a/client/src/assets/images/global/tick.svg +++ b/client/src/assets/images/global/tick.html | |||
@@ -1,8 +1,6 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-356.000000, -115.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-356.000000, -115.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="8" transform="translate(356.000000, 115.000000)"> | 4 | <g id="8" transform="translate(356.000000, 115.000000)"> |
7 | <path d="M21,6 L9,18" id="Path-14"></path> | 5 | <path d="M21,6 L9,18" id="Path-14"></path> |
8 | <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> | 6 | <path d="M9,13 L4,18" id="Path-14" transform="translate(6.500000, 15.500000) scale(-1, 1) translate(-6.500000, -15.500000) "></path> |
diff --git a/client/src/assets/images/global/undo.html b/client/src/assets/images/global/undo.html new file mode 100644 index 000000000..228245c86 --- /dev/null +++ b/client/src/assets/images/global/undo.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-180.000000, -115.000000)" fill="#000000"> | ||
4 | <g id="4" transform="translate(180.000000, 115.000000)"> | ||
5 | <path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "/> | ||
6 | </g> | ||
7 | </g> | ||
8 | </g> | ||
9 | </svg> | ||
diff --git a/client/src/assets/images/global/undo.svg b/client/src/assets/images/global/undo.svg deleted file mode 100644 index f1cca03f7..000000000 --- a/client/src/assets/images/global/undo.svg +++ /dev/null | |||
@@ -1,11 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
5 | <g id="Artboard-4" transform="translate(-180.000000, -115.000000)" fill="#000"> | ||
6 | <g id="4" transform="translate(180.000000, 115.000000)"> | ||
7 | <path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "></path> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/user-add.html b/client/src/assets/images/global/user-add.html new file mode 100644 index 000000000..57df23c74 --- /dev/null +++ b/client/src/assets/images/global/user-add.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 stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | ||
3 | <g transform="translate(-136.000000, -863.000000)"> | ||
4 | <g id="343" transform="translate(136.000000, 863.000000)"> | ||
5 | <path d="M14.2571621,15 L7,15 C4.20063223,15 2.390348,16.1679253 1.5255785,18.0896353 C1.07423388,19.0926234 0.949016905,20.1108713 0.995546634,20.9698816 C0.998604759,21.0263393 1.0014872,21.0632937 1.00496281,21.0995037 C1.0599172,21.6490476 1.54995985,22.0499916 2.09950372,21.9950372 C2.64904758,21.9400828 3.04999158,21.4500401 2.99503719,20.9004963 C2.99555422,20.9071205 2.99399879,20.8871791 2.99261905,20.8617069 C2.96185588,20.2937714 3.05021139,19.575276 3.34942151,18.9103647 C3.890902,17.7070747 4.98686778,17 7,17 L12.0070975,17 L13.2070325,17 C13.4170071,16.2576107 13.7789623,15.5790321 14.2571621,15 Z" id="Path-41" fill="#000000" fill-rule="nonzero"></path> | ||
6 | <path d="M19,18 L19,16.4976988 C19,16.2228273 18.7680664,16 18.5,16 C18.2238576,16 18,16.2148438 18,16.4976988 L18,18 L16.4976988,18 C16.2148438,18 16,18.2238576 16,18.5 C16,18.7680664 16.2228273,19 16.4976988,19 L18,19 L18,20.5023012 C18,20.7771727 18.2319336,21 18.5,21 C18.7761424,21 19,20.7851562 19,20.5023012 L19,19 L20.5023012,19 C20.7851562,19 21,18.7761424 21,18.5 C21,18.2319336 20.7771727,18 20.5023012,18 L19,18 Z M18.5,23 C16.0147186,23 14,20.9852814 14,18.5 C14,16.0147186 16.0147186,14 18.5,14 C20.9852814,14 23,16.0147186 23,18.5 C23,20.9852814 20.9852814,23 18.5,23 Z" id="Combined-Shape" fill="#000000"></path> | ||
7 | <circle id="Oval-40" stroke="#000000" stroke-width="2" cx="12" cy="8" r="5"></circle> | ||
8 | </g> | ||
9 | </g> | ||
10 | </g> | ||
11 | </svg> | ||
diff --git a/client/src/assets/images/global/validate.svg b/client/src/assets/images/global/validate.html index 5c7ee9d14..520624ff6 100644 --- a/client/src/assets/images/global/validate.svg +++ b/client/src/assets/images/global/validate.html | |||
@@ -1,8 +1,6 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-400.000000, -1134.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-400.000000, -1134.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | 4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> |
7 | <g id="yes" transform="translate(352.000000, 88.000000)"> | 5 | <g id="yes" transform="translate(352.000000, 88.000000)"> |
8 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> | 6 | <circle id="Oval-1" cx="12" cy="12" r="10"></circle> |
diff --git a/client/src/assets/images/video/blacklist.svg b/client/src/assets/images/video/blacklist.svg deleted file mode 100644 index 431c73816..000000000 --- a/client/src/assets/images/video/blacklist.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>no</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"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -863.000000)" stroke="#000000" stroke-width="2"> | ||
9 | <g id="347" transform="translate(312.000000, 863.000000)"> | ||
10 | <circle id="Oval-196" cx="12" cy="12" r="9"></circle> | ||
11 | <path d="M18,18 L6,6" id="Path-275"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-white.svg b/client/src/assets/images/video/dislike-white.svg deleted file mode 100644 index cfc6eaa1f..000000000 --- a/client/src/assets/images/video/dislike-white.svg +++ /dev/null | |||
@@ -1,14 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <defs></defs> | ||
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | ||
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#ffffff" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | ||
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | ||
9 | <path d="M4,4.5 C4,4.5 3,7 3,10 C3,13 4,15.5 4,15.5" id="Path-189" transform="translate(3.500000, 10.000000) scale(1, -1) translate(-3.500000, -10.000000) "></path> | ||
10 | </g> | ||
11 | </g> | ||
12 | </g> | ||
13 | </g> | ||
14 | </svg> | ||
diff --git a/client/src/assets/images/video/dislike-grey.svg b/client/src/assets/images/video/dislike.html index 56a7908fb..acde951e2 100644 --- a/client/src/assets/images/video/dislike-grey.svg +++ b/client/src/assets/images/video/dislike.html | |||
@@ -1,8 +1,6 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> | 3 | <g transform="translate(-752.000000, -1090.000000)" stroke="#000000" stroke-width="2"> |
5 | <g id="Artboard-4" transform="translate(-752.000000, -1090.000000)" stroke="#585858" stroke-width="2"> | ||
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | 4 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> |
7 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> | 5 | <g id="thumbs-down" transform="translate(704.000000, 44.000000)"> |
8 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> | 6 | <path d="M6,16 C6,18.5 6.5,21 8,21 L16.9938335,21 C17.5495239,21 18.1819788,20.5956028 18.4072817,20.0949295 L20.8562951,14.6526776 C21.7640882,12.6353595 20.7154925,11 18.5092545,11 L15.5,11 C15.5,11 18.5,5 15,5 C12.5,5 11.5,11 8,11 C6.5,11 6,13.5 6,16 Z" id="Path-188" stroke-linejoin="round" transform="translate(13.591488, 13.000000) scale(1, -1) translate(-13.591488, -13.000000) "></path> |
diff --git a/client/src/assets/images/video/download-grey.svg b/client/src/assets/images/video/download-grey.svg deleted file mode 100644 index 5b0cca5ef..000000000 --- a/client/src/assets/images/video/download-grey.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</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"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/download-white.svg b/client/src/assets/images/video/download-white.svg deleted file mode 100644 index 0e66e06e8..000000000 --- a/client/src/assets/images/video/download-white.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>download</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"> | ||
8 | <g id="Artboard-4" transform="translate(-180.000000, -291.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="84" transform="translate(180.000000, 291.000000)"> | ||
10 | <path d="M12,3 L12,15" id="Path-58"></path> | ||
11 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 14.000000) rotate(-270.000000) translate(-12.000000, -14.000000) " points="9 8 15 14 9 20"></polyline> | ||
12 | <path d="M3,18 L3,20.0590859 C3,20.6127331 3.44494889,21.0615528 3.99340349,21.0615528 L20.0067018,21.0615528 C20.5553434,21.0615528 21.0001052,20.6098102 21.0001051,20.0590859 L21.0001049,18" id="Path-12" stroke-linejoin="round"></path> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/images/video/heart.svg b/client/src/assets/images/video/heart.html index 5d64aee0f..618f64f10 100644 --- a/client/src/assets/images/video/heart.svg +++ b/client/src/assets/images/video/heart.html | |||
@@ -1,9 +1,7 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#000000"> |
5 | <g id="Artboard-4" transform="translate(-48.000000, -1046.000000)" fill-rule="nonzero" fill="#585858"> | 4 | <g transform="translate(48.000000, 1046.000000)"> |
6 | <g id="Extras" transform="translate(48.000000, 1046.000000)"> | ||
7 | <g id="heart"> | 5 | <g id="heart"> |
8 | <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path> | 6 | <path d="M12.0174466,21 L20.9041801,11.3556763 C22.6291961,9.13778099 22.2795957,5.90145416 20.1233257,4.12713796 C17.9670557,2.35282175 14.8206518,2.71241362 13.0956358,4.93030888 L12.0174465,6.5 L10.9043642,4.93030888 C9.17934824,2.71241362 6.0329443,2.35282175 3.87667432,4.12713796 C1.72040435,5.90145416 1.37080391,9.13778099 3.09581989,11.3556763 L12.0174466,21 Z"></path> |
9 | </g> | 7 | </g> |
diff --git a/client/src/assets/images/video/like-white.svg b/client/src/assets/images/video/like-white.svg deleted file mode 100644 index 88e5f6a9a..000000000 --- a/client/src/assets/images/video/like-white.svg +++ /dev/null | |||
@@ -1,15 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>thumbs-up</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"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#ffffff" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | ||
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | ||
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | ||
12 | </g> | ||
13 | </g> | ||
14 | </g> | ||
15 | </svg> | ||
diff --git a/client/src/assets/images/video/like-grey.svg b/client/src/assets/images/video/like.html index 5ef6c7b31..d0e71763b 100644 --- a/client/src/assets/images/video/like-grey.svg +++ b/client/src/assets/images/video/like.html | |||
@@ -1,11 +1,6 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>thumbs-up</title> | 3 | <g transform="translate(-708.000000, -643.000000)" stroke="#000000" stroke-width="2"> |
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"> | ||
8 | <g id="Artboard-4" transform="translate(-708.000000, -643.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="256" transform="translate(708.000000, 643.000000)"> | 4 | <g id="256" transform="translate(708.000000, 643.000000)"> |
10 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> | 5 | <path d="M6,14 C6,16.5 6.5,19 8,19 L16.9938335,19 C17.5495239,19 18.1819788,18.5956028 18.4072817,18.0949295 L20.8562951,12.6526776 C21.7640882,10.6353595 20.7154925,9 18.5092545,9 L15.5,9 C15.5,9 18.5,3 15,3 C12.5,3 11.5,9 8,9 C6.5,9 6,11.5 6,14 Z" id="Path-188" stroke-linejoin="round"></path> |
11 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> | 6 | <path d="M4,8.5 C4,8.5 3,11 3,14 C3,17 4,19.5 4,19.5" id="Path-189"></path> |
diff --git a/client/src/assets/images/video/more.svg b/client/src/assets/images/video/more.html index dea392136..39dcad10e 100644 --- a/client/src/assets/images/video/more.svg +++ b/client/src/assets/images/video/more.html | |||
@@ -1,8 +1,6 @@ | |||
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"> | 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"> |
3 | <defs></defs> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
4 | <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> | 3 | <g transform="translate(-444.000000, -115.000000)" fill="#000000"> |
5 | <g id="Artboard-4" transform="translate(-444.000000, -115.000000)" fill="#585858"> | ||
6 | <g id="10" transform="translate(444.000000, 115.000000)"> | 4 | <g id="10" transform="translate(444.000000, 115.000000)"> |
7 | <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 M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> | 5 | <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 M17,12 C17,10.8954305 17.8877296,10 19,10 C20.1045695,10 21,10.8877296 21,12 C21,13.1045695 20.1122704,14 19,14 C17.8954305,14 17,13.1122704 17,12 Z M3,12 C3,10.8954305 3.88772964,10 5,10 C6.1045695,10 7,10.8877296 7,12 C7,13.1045695 6.11227036,14 5,14 C3.8954305,14 3,13.1122704 3,12 Z" id="Combined-Shape"></path> |
8 | </g> | 6 | </g> |
diff --git a/client/src/assets/images/video/share.svg b/client/src/assets/images/video/share.html index da0f43e81..7759b37af 100644 --- a/client/src/assets/images/video/share.svg +++ b/client/src/assets/images/video/share.html | |||
@@ -1,11 +1,6 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>share</title> | 3 | <g transform="translate(-312.000000, -203.000000)" stroke="#000000" stroke-width="2"> |
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"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -203.000000)" stroke="#585858" stroke-width="2"> | ||
9 | <g id="47" transform="translate(312.000000, 203.000000)"> | 4 | <g id="47" transform="translate(312.000000, 203.000000)"> |
10 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> | 5 | <path d="M20,15 L20,18.0026083 C20,19.1057373 19.1073772,20 18.0049107,20 L5.99508929,20 C4.8932319,20 4,19.1073772 4,18.0049107 L4,5.99508929 C4,4.8932319 4.89585781,4 5.9973917,4 L9,4" id="Rectangle-460"></path> |
11 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> | 6 | <polyline id="Path-93" stroke-linejoin="round" points="13 4 20.0207973 4 20.0207973 11.0191059"></polyline> |
diff --git a/client/src/assets/images/header/upload.svg b/client/src/assets/images/video/upload.html index 2b07caf76..3bc0d3a8a 100644 --- a/client/src/assets/images/header/upload.svg +++ b/client/src/assets/images/video/upload.html | |||
@@ -1,11 +1,6 @@ | |||
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"> | 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"> |
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | 2 | <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round"> |
4 | <title>cloud-upload</title> | 3 | <g transform="translate(-312.000000, -775.000000)" stroke="#000000" stroke-width="2"> |
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"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#fff" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | 4 | <g id="307" transform="translate(312.000000, 775.000000)"> |
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | 5 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> |
11 | <path d="M12,13 L12,21" id="Path-58"></path> | 6 | <path d="M12,13 L12,21" id="Path-58"></path> |
diff --git a/client/src/assets/images/video/upload.svg b/client/src/assets/images/video/upload.svg deleted file mode 100644 index c5b7cb443..000000000 --- a/client/src/assets/images/video/upload.svg +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | <?xml version="1.0" encoding="UTF-8"?> | ||
2 | <svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
3 | <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch --> | ||
4 | <title>cloud-upload</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"> | ||
8 | <g id="Artboard-4" transform="translate(-312.000000, -775.000000)" stroke="#C6C6C6" stroke-width="2"> | ||
9 | <g id="307" transform="translate(312.000000, 775.000000)"> | ||
10 | <path d="M8,18 L5,18 L5,18 C2.790861,18 1,16.209139 1,14 C1,11.790861 2.790861,10 5,10 C5.35840468,10 5.70579988,10.0471371 6.03632437,10.1355501 C6.01233106,9.92702603 6,9.71495305 6,9.5 C6,6.46243388 8.46243388,4 11.5,4 C14.0673313,4 16.2238156,5.7590449 16.8299648,8.1376465 C17.2052921,8.04765874 17.5970804,8 18,8 C20.7614237,8 23,10.2385763 23,13 C23,15.7614237 20.7614237,18 18,18 L16,18" id="Combined-Shape" stroke-linejoin="round"></path> | ||
11 | <path d="M12,13 L12,21" id="Path-58"></path> | ||
12 | <polyline id="Path-59" stroke-linejoin="round" transform="translate(12.000000, 12.500000) scale(1, -1) translate(-12.000000, -12.500000) " points="15 11 12 14 9 11"></polyline> | ||
13 | </g> | ||
14 | </g> | ||
15 | </g> | ||
16 | </svg> | ||
diff --git a/client/src/assets/player/images/tick.svg b/client/src/assets/player/images/tick-white.svg index d329e6bfb..d329e6bfb 100644 --- a/client/src/assets/player/images/tick.svg +++ b/client/src/assets/player/images/tick-white.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..022a9c16f --- /dev/null +++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts | |||
@@ -0,0 +1,143 @@ | |||
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 | |||
8 | // videojs-hlsjs-plugin needs videojs in window | ||
9 | window['videojs'] = videojs | ||
10 | require('@streamroot/videojs-hlsjs-plugin') | ||
11 | |||
12 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | ||
13 | class P2pMediaLoaderPlugin extends Plugin { | ||
14 | |||
15 | private readonly CONSTANTS = { | ||
16 | INFO_SCHEDULER: 1000 // Don't change this | ||
17 | } | ||
18 | private readonly options: P2PMediaLoaderPluginOptions | ||
19 | |||
20 | private hlsjs: any // Don't type hlsjs to not bundle the module | ||
21 | private p2pEngine: Engine | ||
22 | private statsP2PBytes = { | ||
23 | pendingDownload: [] as number[], | ||
24 | pendingUpload: [] as number[], | ||
25 | numPeers: 0, | ||
26 | totalDownload: 0, | ||
27 | totalUpload: 0 | ||
28 | } | ||
29 | private statsHTTPBytes = { | ||
30 | pendingDownload: [] as number[], | ||
31 | pendingUpload: [] as number[], | ||
32 | totalDownload: 0, | ||
33 | totalUpload: 0 | ||
34 | } | ||
35 | |||
36 | private networkInfoInterval: any | ||
37 | |||
38 | constructor (player: videojs.Player, options: P2PMediaLoaderPluginOptions) { | ||
39 | super(player, options) | ||
40 | |||
41 | this.options = options | ||
42 | |||
43 | videojs.Html5Hlsjs.addHook('beforeinitialize', (videojsPlayer: any, hlsjs: any) => { | ||
44 | this.hlsjs = hlsjs | ||
45 | }) | ||
46 | |||
47 | initVideoJsContribHlsJsPlayer(player) | ||
48 | |||
49 | player.src({ | ||
50 | type: options.type, | ||
51 | src: options.src | ||
52 | }) | ||
53 | |||
54 | player.on('play', () => { | ||
55 | player.addClass('vjs-has-big-play-button-clicked') | ||
56 | }) | ||
57 | |||
58 | player.ready(() => this.initialize()) | ||
59 | } | ||
60 | |||
61 | dispose () { | ||
62 | if (this.hlsjs) this.hlsjs.destroy() | ||
63 | if (this.p2pEngine) this.p2pEngine.destroy() | ||
64 | |||
65 | clearInterval(this.networkInfoInterval) | ||
66 | } | ||
67 | |||
68 | private initialize () { | ||
69 | initHlsJsPlayer(this.hlsjs) | ||
70 | |||
71 | const tech = this.player.tech_ | ||
72 | this.p2pEngine = tech.options_.hlsjsConfig.loader.getEngine() | ||
73 | |||
74 | // Avoid using constants to not import hls.hs | ||
75 | // https://github.com/video-dev/hls.js/blob/master/src/events.js#L37 | ||
76 | this.hlsjs.on('hlsLevelSwitching', (_: any, data: any) => { | ||
77 | this.trigger('resolutionChange', { auto: this.hlsjs.autoLevelEnabled, resolutionId: data.height }) | ||
78 | }) | ||
79 | |||
80 | this.p2pEngine.on(Events.SegmentError, (segment, err) => { | ||
81 | console.error('Segment error.', segment, err) | ||
82 | }) | ||
83 | |||
84 | this.statsP2PBytes.numPeers = 1 + this.options.redundancyBaseUrls.length | ||
85 | |||
86 | this.runStats() | ||
87 | } | ||
88 | |||
89 | private runStats () { | ||
90 | this.p2pEngine.on(Events.PieceBytesDownloaded, (method: string, size: number) => { | ||
91 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
92 | |||
93 | elem.pendingDownload.push(size) | ||
94 | elem.totalDownload += size | ||
95 | }) | ||
96 | |||
97 | this.p2pEngine.on(Events.PieceBytesUploaded, (method: string, size: number) => { | ||
98 | const elem = method === 'p2p' ? this.statsP2PBytes : this.statsHTTPBytes | ||
99 | |||
100 | elem.pendingUpload.push(size) | ||
101 | elem.totalUpload += size | ||
102 | }) | ||
103 | |||
104 | this.p2pEngine.on(Events.PeerConnect, () => this.statsP2PBytes.numPeers++) | ||
105 | this.p2pEngine.on(Events.PeerClose, () => this.statsP2PBytes.numPeers--) | ||
106 | |||
107 | this.networkInfoInterval = setInterval(() => { | ||
108 | const p2pDownloadSpeed = this.arraySum(this.statsP2PBytes.pendingDownload) | ||
109 | const p2pUploadSpeed = this.arraySum(this.statsP2PBytes.pendingUpload) | ||
110 | |||
111 | const httpDownloadSpeed = this.arraySum(this.statsHTTPBytes.pendingDownload) | ||
112 | const httpUploadSpeed = this.arraySum(this.statsHTTPBytes.pendingUpload) | ||
113 | |||
114 | this.statsP2PBytes.pendingDownload = [] | ||
115 | this.statsP2PBytes.pendingUpload = [] | ||
116 | this.statsHTTPBytes.pendingDownload = [] | ||
117 | this.statsHTTPBytes.pendingUpload = [] | ||
118 | |||
119 | return this.player.trigger('p2pInfo', { | ||
120 | http: { | ||
121 | downloadSpeed: httpDownloadSpeed, | ||
122 | uploadSpeed: httpUploadSpeed, | ||
123 | downloaded: this.statsHTTPBytes.totalDownload, | ||
124 | uploaded: this.statsHTTPBytes.totalUpload | ||
125 | }, | ||
126 | p2p: { | ||
127 | downloadSpeed: p2pDownloadSpeed, | ||
128 | uploadSpeed: p2pUploadSpeed, | ||
129 | numPeers: this.statsP2PBytes.numPeers, | ||
130 | downloaded: this.statsP2PBytes.totalDownload, | ||
131 | uploaded: this.statsP2PBytes.totalUpload | ||
132 | } | ||
133 | } as PlayerNetworkInfo) | ||
134 | }, this.CONSTANTS.INFO_SCHEDULER) | ||
135 | } | ||
136 | |||
137 | private arraySum (data: number[]) { | ||
138 | return data.reduce((a: number, b: number) => a + b, 0) | ||
139 | } | ||
140 | } | ||
141 | |||
142 | videojs.registerPlugin('p2pMediaLoader', P2pMediaLoaderPlugin) | ||
143 | 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..32e7ce4f2 --- /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 | let newBaseUrl = baseUrls[i] | ||
12 | let 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-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts index dac54c5a4..059fca308 100644 --- a/client/src/assets/player/peertube-player-local-storage.ts +++ b/client/src/assets/player/peertube-player-local-storage.ts | |||
@@ -10,6 +10,14 @@ function getStoredVolume () { | |||
10 | return undefined | 10 | return undefined |
11 | } | 11 | } |
12 | 12 | ||
13 | function getStoredWebTorrentEnabled (): boolean { | ||
14 | const value = getLocalStorage('webtorrent_enabled') | ||
15 | if (value !== null && value !== undefined) return value === 'true' | ||
16 | |||
17 | // By default webtorrent is enabled | ||
18 | return true | ||
19 | } | ||
20 | |||
13 | function getStoredMute () { | 21 | function getStoredMute () { |
14 | const value = getLocalStorage('mute') | 22 | const value = getLocalStorage('mute') |
15 | if (value !== null && value !== undefined) return value === 'true' | 23 | if (value !== null && value !== undefined) return value === 'true' |
@@ -52,17 +60,28 @@ function getAverageBandwidthInStore () { | |||
52 | return undefined | 60 | return undefined |
53 | } | 61 | } |
54 | 62 | ||
63 | function saveLastSubtitle (language: string) { | ||
64 | return setLocalStorage('last-subtitle', language) | ||
65 | } | ||
66 | |||
67 | function getStoredLastSubtitle () { | ||
68 | return getLocalStorage('last-subtitle') | ||
69 | } | ||
70 | |||
55 | // --------------------------------------------------------------------------- | 71 | // --------------------------------------------------------------------------- |
56 | 72 | ||
57 | export { | 73 | export { |
58 | getStoredVolume, | 74 | getStoredVolume, |
75 | getStoredWebTorrentEnabled, | ||
59 | getStoredMute, | 76 | getStoredMute, |
60 | getStoredTheater, | 77 | getStoredTheater, |
61 | saveVolumeInStore, | 78 | saveVolumeInStore, |
62 | saveMuteInStore, | 79 | saveMuteInStore, |
63 | saveTheaterInStore, | 80 | saveTheaterInStore, |
64 | saveAverageBandwidth, | 81 | saveAverageBandwidth, |
65 | getAverageBandwidthInStore | 82 | getAverageBandwidthInStore, |
83 | saveLastSubtitle, | ||
84 | getStoredLastSubtitle | ||
66 | } | 85 | } |
67 | 86 | ||
68 | // --------------------------------------------------------------------------- | 87 | // --------------------------------------------------------------------------- |
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..0ba9bcb11 --- /dev/null +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -0,0 +1,466 @@ | |||
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 | |||
53 | theaterMode: boolean | ||
54 | captions: boolean | ||
55 | peertubeLink: boolean | ||
56 | |||
57 | videoViewUrl: string | ||
58 | embedUrl: string | ||
59 | |||
60 | language?: string | ||
61 | controls?: boolean | ||
62 | muted?: boolean | ||
63 | loop?: boolean | ||
64 | subtitle?: string | ||
65 | |||
66 | videoCaptions: VideoJSCaption[] | ||
67 | |||
68 | userWatching?: UserWatching | ||
69 | |||
70 | serverUrl: string | ||
71 | } | ||
72 | |||
73 | export type PeertubePlayerManagerOptions = { | ||
74 | common: CommonOptions, | ||
75 | webtorrent: WebtorrentOptions, | ||
76 | p2pMediaLoader?: P2PMediaLoaderOptions | ||
77 | } | ||
78 | |||
79 | export class PeertubePlayerManager { | ||
80 | |||
81 | private static videojsLocaleCache: { [ path: string ]: any } = {} | ||
82 | private static playerElementClassName: string | ||
83 | |||
84 | static getServerTranslations (serverUrl: string, locale: string) { | ||
85 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
86 | // It is the default locale, nothing to translate | ||
87 | if (!path) return Promise.resolve(undefined) | ||
88 | |||
89 | return fetch(path + '/server.json') | ||
90 | .then(res => res.json()) | ||
91 | .catch(err => { | ||
92 | console.error('Cannot get server translations', err) | ||
93 | return undefined | ||
94 | }) | ||
95 | } | ||
96 | |||
97 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | ||
98 | let p2pMediaLoader: any | ||
99 | |||
100 | this.playerElementClassName = options.common.playerElement.className | ||
101 | |||
102 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') | ||
103 | if (mode === 'p2p-media-loader') { | ||
104 | [ p2pMediaLoader ] = await Promise.all([ | ||
105 | import('p2p-media-loader-hlsjs'), | ||
106 | import('./p2p-media-loader/p2p-media-loader-plugin') | ||
107 | ]) | ||
108 | } | ||
109 | |||
110 | const videojsOptions = this.getVideojsOptions(mode, options, p2pMediaLoader) | ||
111 | |||
112 | await this.loadLocaleInVideoJS(options.common.serverUrl, options.common.language) | ||
113 | |||
114 | const self = this | ||
115 | return new Promise(res => { | ||
116 | videojs(options.common.playerElement, videojsOptions, function (this: any) { | ||
117 | const player = this | ||
118 | |||
119 | player.tech_.on('error', () => { | ||
120 | // Fallback to webtorrent? | ||
121 | if (mode === 'p2p-media-loader') { | ||
122 | self.fallbackToWebTorrent(player, options) | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
127 | |||
128 | return res(player) | ||
129 | }) | ||
130 | }) | ||
131 | } | ||
132 | |||
133 | private static async fallbackToWebTorrent (player: any, options: PeertubePlayerManagerOptions) { | ||
134 | const newVideoElement = document.createElement('video') | ||
135 | newVideoElement.className = this.playerElementClassName | ||
136 | |||
137 | // VideoJS wraps our video element inside a div | ||
138 | const currentParentPlayerElement = options.common.playerElement.parentNode | ||
139 | currentParentPlayerElement.parentNode.insertBefore(newVideoElement, currentParentPlayerElement) | ||
140 | |||
141 | options.common.playerElement = newVideoElement | ||
142 | options.common.onPlayerElementChange(newVideoElement) | ||
143 | |||
144 | player.dispose() | ||
145 | |||
146 | await import('./webtorrent/webtorrent-plugin') | ||
147 | |||
148 | const mode = 'webtorrent' | ||
149 | const videojsOptions = this.getVideojsOptions(mode, options) | ||
150 | |||
151 | const self = this | ||
152 | videojs(newVideoElement, videojsOptions, function (this: any) { | ||
153 | const player = this | ||
154 | |||
155 | self.addContextMenu(mode, player, options.common.embedUrl) | ||
156 | }) | ||
157 | } | ||
158 | |||
159 | private static loadLocaleInVideoJS (serverUrl: string, locale: string) { | ||
160 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | ||
161 | // It is the default locale, nothing to translate | ||
162 | if (!path) return Promise.resolve(undefined) | ||
163 | |||
164 | let p: Promise<any> | ||
165 | |||
166 | if (PeertubePlayerManager.videojsLocaleCache[path]) { | ||
167 | p = Promise.resolve(PeertubePlayerManager.videojsLocaleCache[path]) | ||
168 | } else { | ||
169 | p = fetch(path + '/player.json') | ||
170 | .then(res => res.json()) | ||
171 | .then(json => { | ||
172 | PeertubePlayerManager.videojsLocaleCache[path] = json | ||
173 | return json | ||
174 | }) | ||
175 | .catch(err => { | ||
176 | console.error('Cannot get player translations', err) | ||
177 | return undefined | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | const completeLocale = getCompleteLocale(locale) | ||
182 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
183 | } | ||
184 | |||
185 | private static getVideojsOptions (mode: PlayerMode, options: PeertubePlayerManagerOptions, p2pMediaLoaderModule?: any) { | ||
186 | const commonOptions = options.common | ||
187 | const webtorrentOptions = options.webtorrent | ||
188 | const p2pMediaLoaderOptions = options.p2pMediaLoader | ||
189 | |||
190 | let autoplay = options.common.autoplay | ||
191 | let html5 = {} | ||
192 | |||
193 | const plugins: VideoJSPluginOptions = { | ||
194 | peertube: { | ||
195 | mode, | ||
196 | autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
197 | videoViewUrl: commonOptions.videoViewUrl, | ||
198 | videoDuration: commonOptions.videoDuration, | ||
199 | startTime: commonOptions.startTime, | ||
200 | userWatching: commonOptions.userWatching, | ||
201 | subtitle: commonOptions.subtitle, | ||
202 | videoCaptions: commonOptions.videoCaptions | ||
203 | } | ||
204 | } | ||
205 | |||
206 | if (mode === 'p2p-media-loader') { | ||
207 | const p2pMediaLoader: P2PMediaLoaderPluginOptions = { | ||
208 | redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls, | ||
209 | type: 'application/x-mpegURL', | ||
210 | src: p2pMediaLoaderOptions.playlistUrl | ||
211 | } | ||
212 | |||
213 | const trackerAnnounce = p2pMediaLoaderOptions.trackerAnnounce | ||
214 | .filter(t => t.startsWith('ws')) | ||
215 | |||
216 | const p2pMediaLoaderConfig = { | ||
217 | loader: { | ||
218 | trackerAnnounce, | ||
219 | segmentValidator: segmentValidatorFactory(options.p2pMediaLoader.segmentsSha256Url), | ||
220 | rtcConfig: getRtcConfig(), | ||
221 | requiredSegmentsPriority: 5, | ||
222 | segmentUrlBuilder: segmentUrlBuilderFactory(options.p2pMediaLoader.redundancyBaseUrls) | ||
223 | }, | ||
224 | segments: { | ||
225 | swarmId: p2pMediaLoaderOptions.playlistUrl | ||
226 | } | ||
227 | } | ||
228 | const streamrootHls = { | ||
229 | levelLabelHandler: (level: { height: number, width: number }) => { | ||
230 | const file = p2pMediaLoaderOptions.videoFiles.find(f => f.resolution.id === level.height) | ||
231 | |||
232 | let label = file.resolution.label | ||
233 | if (file.fps >= 50) label += file.fps | ||
234 | |||
235 | return label | ||
236 | }, | ||
237 | html5: { | ||
238 | hlsjsConfig: { | ||
239 | liveSyncDurationCount: 7, | ||
240 | loader: new p2pMediaLoaderModule.Engine(p2pMediaLoaderConfig).createLoaderClass() | ||
241 | } | ||
242 | } | ||
243 | } | ||
244 | |||
245 | Object.assign(plugins, { p2pMediaLoader, streamrootHls }) | ||
246 | html5 = streamrootHls.html5 | ||
247 | } | ||
248 | |||
249 | if (mode === 'webtorrent') { | ||
250 | const webtorrent = { | ||
251 | autoplay, | ||
252 | videoDuration: commonOptions.videoDuration, | ||
253 | playerElement: commonOptions.playerElement, | ||
254 | videoFiles: webtorrentOptions.videoFiles | ||
255 | } | ||
256 | Object.assign(plugins, { webtorrent }) | ||
257 | |||
258 | // WebTorrent plugin handles autoplay, because we do some hackish stuff in there | ||
259 | autoplay = false | ||
260 | } | ||
261 | |||
262 | const videojsOptions = { | ||
263 | html5, | ||
264 | |||
265 | // We don't use text track settings for now | ||
266 | textTrackSettings: false, | ||
267 | controls: commonOptions.controls !== undefined ? commonOptions.controls : true, | ||
268 | loop: commonOptions.loop !== undefined ? commonOptions.loop : false, | ||
269 | |||
270 | muted: commonOptions.muted !== undefined | ||
271 | ? commonOptions.muted | ||
272 | : undefined, // Undefined so the player knows it has to check the local storage | ||
273 | |||
274 | poster: commonOptions.poster, | ||
275 | autoplay: autoplay === true ? 'any' : autoplay, // Use 'any' instead of true to get notifier by videojs if autoplay fails | ||
276 | inactivityTimeout: commonOptions.inactivityTimeout, | ||
277 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
278 | plugins, | ||
279 | controlBar: { | ||
280 | children: this.getControlBarChildren(mode, { | ||
281 | captions: commonOptions.captions, | ||
282 | peertubeLink: commonOptions.peertubeLink, | ||
283 | theaterMode: commonOptions.theaterMode | ||
284 | }) | ||
285 | } | ||
286 | } | ||
287 | |||
288 | if (commonOptions.enableHotkeys === true) { | ||
289 | Object.assign(videojsOptions.plugins, { | ||
290 | hotkeys: { | ||
291 | enableVolumeScroll: false, | ||
292 | enableModifiersForNumbers: false, | ||
293 | |||
294 | fullscreenKey: function (event: KeyboardEvent) { | ||
295 | // fullscreen with the f key or Ctrl+Enter | ||
296 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
297 | }, | ||
298 | |||
299 | seekStep: function (event: KeyboardEvent) { | ||
300 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
301 | if (event.ctrlKey && event.altKey) { | ||
302 | return 5 * 60 | ||
303 | } else if (event.ctrlKey) { | ||
304 | return 60 | ||
305 | } else if (event.altKey) { | ||
306 | return 10 | ||
307 | } else { | ||
308 | return 5 | ||
309 | } | ||
310 | }, | ||
311 | |||
312 | customKeys: { | ||
313 | increasePlaybackRateKey: { | ||
314 | key: function (event: KeyboardEvent) { | ||
315 | return event.key === '>' | ||
316 | }, | ||
317 | handler: function (player: videojs.Player) { | ||
318 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
319 | } | ||
320 | }, | ||
321 | decreasePlaybackRateKey: { | ||
322 | key: function (event: KeyboardEvent) { | ||
323 | return event.key === '<' | ||
324 | }, | ||
325 | handler: function (player: videojs.Player) { | ||
326 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
327 | } | ||
328 | }, | ||
329 | frameByFrame: { | ||
330 | key: function (event: KeyboardEvent) { | ||
331 | return event.key === '.' | ||
332 | }, | ||
333 | handler: function (player: videojs.Player) { | ||
334 | player.pause() | ||
335 | // Calculate movement distance (assuming 30 fps) | ||
336 | const dist = 1 / 30 | ||
337 | player.currentTime(player.currentTime() + dist) | ||
338 | } | ||
339 | } | ||
340 | } | ||
341 | } | ||
342 | }) | ||
343 | } | ||
344 | |||
345 | if (commonOptions.language && !isDefaultLocale(commonOptions.language)) { | ||
346 | Object.assign(videojsOptions, { language: commonOptions.language }) | ||
347 | } | ||
348 | |||
349 | return videojsOptions | ||
350 | } | ||
351 | |||
352 | private static getControlBarChildren (mode: PlayerMode, options: { | ||
353 | peertubeLink: boolean | ||
354 | theaterMode: boolean, | ||
355 | captions: boolean | ||
356 | }) { | ||
357 | const settingEntries = [] | ||
358 | const loadProgressBar = mode === 'webtorrent' ? 'peerTubeLoadProgressBar' : 'loadProgressBar' | ||
359 | |||
360 | // Keep an order | ||
361 | settingEntries.push('playbackRateMenuButton') | ||
362 | if (options.captions === true) settingEntries.push('captionsButton') | ||
363 | settingEntries.push('resolutionMenuButton') | ||
364 | |||
365 | const children = { | ||
366 | 'playToggle': {}, | ||
367 | 'currentTimeDisplay': {}, | ||
368 | 'timeDivider': {}, | ||
369 | 'durationDisplay': {}, | ||
370 | 'liveDisplay': {}, | ||
371 | |||
372 | 'flexibleWidthSpacer': {}, | ||
373 | 'progressControl': { | ||
374 | children: { | ||
375 | 'seekBar': { | ||
376 | children: { | ||
377 | [loadProgressBar]: {}, | ||
378 | 'mouseTimeDisplay': {}, | ||
379 | 'playProgressBar': {} | ||
380 | } | ||
381 | } | ||
382 | } | ||
383 | }, | ||
384 | |||
385 | 'p2PInfoButton': {}, | ||
386 | |||
387 | 'muteToggle': {}, | ||
388 | 'volumeControl': {}, | ||
389 | |||
390 | 'settingsButton': { | ||
391 | setup: { | ||
392 | maxHeightOffset: 40 | ||
393 | }, | ||
394 | entries: settingEntries | ||
395 | } | ||
396 | } | ||
397 | |||
398 | if (options.peertubeLink === true) { | ||
399 | Object.assign(children, { | ||
400 | 'peerTubeLinkButton': {} | ||
401 | }) | ||
402 | } | ||
403 | |||
404 | if (options.theaterMode === true) { | ||
405 | Object.assign(children, { | ||
406 | 'theaterButton': {} | ||
407 | }) | ||
408 | } | ||
409 | |||
410 | Object.assign(children, { | ||
411 | 'fullscreenToggle': {} | ||
412 | }) | ||
413 | |||
414 | return children | ||
415 | } | ||
416 | |||
417 | private static addContextMenu (mode: PlayerMode, player: any, videoEmbedUrl: string) { | ||
418 | const content = [ | ||
419 | { | ||
420 | label: player.localize('Copy the video URL'), | ||
421 | listener: function () { | ||
422 | copyToClipboard(buildVideoLink()) | ||
423 | } | ||
424 | }, | ||
425 | { | ||
426 | label: player.localize('Copy the video URL at the current time'), | ||
427 | listener: function () { | ||
428 | const player = this as videojs.Player | ||
429 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
430 | } | ||
431 | }, | ||
432 | { | ||
433 | label: player.localize('Copy embed code'), | ||
434 | listener: () => { | ||
435 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
436 | } | ||
437 | } | ||
438 | ] | ||
439 | |||
440 | if (mode === 'webtorrent') { | ||
441 | content.push({ | ||
442 | label: player.localize('Copy magnet URI'), | ||
443 | listener: function () { | ||
444 | const player = this as videojs.Player | ||
445 | copyToClipboard(player.webtorrent().getCurrentVideoFile().magnetUri) | ||
446 | } | ||
447 | }) | ||
448 | } | ||
449 | |||
450 | player.contextmenuUI({ content }) | ||
451 | } | ||
452 | |||
453 | private static getLocalePath (serverUrl: string, locale: string) { | ||
454 | const completeLocale = getCompleteLocale(locale) | ||
455 | |||
456 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
457 | |||
458 | return serverUrl + '/client/locales/' + completeLocale | ||
459 | } | ||
460 | } | ||
461 | |||
462 | // ############################################################################ | ||
463 | |||
464 | export { | ||
465 | videojs | ||
466 | } | ||
diff --git a/client/src/assets/player/peertube-player.ts b/client/src/assets/player/peertube-player.ts deleted file mode 100644 index 792662b6c..000000000 --- a/client/src/assets/player/peertube-player.ts +++ /dev/null | |||
@@ -1,284 +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 | // Change 'Playback Rate' to 'Speed' (smaller for our settings menu) | ||
18 | videojsUntyped.getComponent('PlaybackRateMenuButton').prototype.controlText_ = 'Speed' | ||
19 | // Change Captions to Subtitles/CC | ||
20 | videojsUntyped.getComponent('CaptionsButton').prototype.controlText_ = 'Subtitles/CC' | ||
21 | // We just want to display 'Off' instead of 'captions off', keep a space so the variable == true (hacky I know) | ||
22 | videojsUntyped.getComponent('CaptionsButton').prototype.label_ = ' ' | ||
23 | |||
24 | function getVideojsOptions (options: { | ||
25 | autoplay: boolean, | ||
26 | playerElement: HTMLVideoElement, | ||
27 | videoViewUrl: string, | ||
28 | videoDuration: number, | ||
29 | videoFiles: VideoFile[], | ||
30 | enableHotkeys: boolean, | ||
31 | inactivityTimeout: number, | ||
32 | peertubeLink: boolean, | ||
33 | poster: string, | ||
34 | startTime: number | string | ||
35 | theaterMode: boolean, | ||
36 | videoCaptions: VideoJSCaption[], | ||
37 | |||
38 | language?: string, | ||
39 | controls?: boolean, | ||
40 | muted?: boolean, | ||
41 | loop?: boolean | ||
42 | |||
43 | userWatching?: UserWatching | ||
44 | }) { | ||
45 | const videojsOptions = { | ||
46 | // We don't use text track settings for now | ||
47 | textTrackSettings: false, | ||
48 | controls: options.controls !== undefined ? options.controls : true, | ||
49 | muted: options.controls !== undefined ? options.muted : false, | ||
50 | loop: options.loop !== undefined ? options.loop : false, | ||
51 | poster: options.poster, | ||
52 | autoplay: false, | ||
53 | inactivityTimeout: options.inactivityTimeout, | ||
54 | playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], | ||
55 | plugins: { | ||
56 | peertube: { | ||
57 | autoplay: options.autoplay, // Use peertube plugin autoplay because we get the file by webtorrent | ||
58 | videoCaptions: options.videoCaptions, | ||
59 | videoFiles: options.videoFiles, | ||
60 | playerElement: options.playerElement, | ||
61 | videoViewUrl: options.videoViewUrl, | ||
62 | videoDuration: options.videoDuration, | ||
63 | startTime: options.startTime, | ||
64 | userWatching: options.userWatching | ||
65 | } | ||
66 | }, | ||
67 | controlBar: { | ||
68 | children: getControlBarChildren(options) | ||
69 | } | ||
70 | } | ||
71 | |||
72 | if (options.enableHotkeys === true) { | ||
73 | Object.assign(videojsOptions.plugins, { | ||
74 | hotkeys: { | ||
75 | enableVolumeScroll: false, | ||
76 | enableModifiersForNumbers: false, | ||
77 | |||
78 | fullscreenKey: function (event) { | ||
79 | // fullscreen with the f key or Ctrl+Enter | ||
80 | return event.key === 'f' || (event.ctrlKey && event.key === 'Enter') | ||
81 | }, | ||
82 | |||
83 | seekStep: function (event) { | ||
84 | // mimic VLC seek behavior, and default to 5 (original value is 5). | ||
85 | if (event.ctrlKey && event.altKey) { | ||
86 | return 5 * 60 | ||
87 | } else if (event.ctrlKey) { | ||
88 | return 60 | ||
89 | } else if (event.altKey) { | ||
90 | return 10 | ||
91 | } else { | ||
92 | return 5 | ||
93 | } | ||
94 | }, | ||
95 | |||
96 | customKeys: { | ||
97 | increasePlaybackRateKey: { | ||
98 | key: function (event) { | ||
99 | return event.key === '>' | ||
100 | }, | ||
101 | handler: function (player) { | ||
102 | player.playbackRate((player.playbackRate() + 0.1).toFixed(2)) | ||
103 | } | ||
104 | }, | ||
105 | decreasePlaybackRateKey: { | ||
106 | key: function (event) { | ||
107 | return event.key === '<' | ||
108 | }, | ||
109 | handler: function (player) { | ||
110 | player.playbackRate((player.playbackRate() - 0.1).toFixed(2)) | ||
111 | } | ||
112 | }, | ||
113 | frameByFrame: { | ||
114 | key: function (event) { | ||
115 | return event.key === '.' | ||
116 | }, | ||
117 | handler: function (player, options, event) { | ||
118 | player.pause() | ||
119 | // Calculate movement distance (assuming 30 fps) | ||
120 | const dist = 1 / 30 | ||
121 | player.currentTime(player.currentTime() + dist) | ||
122 | } | ||
123 | } | ||
124 | } | ||
125 | } | ||
126 | }) | ||
127 | } | ||
128 | |||
129 | if (options.language && !isDefaultLocale(options.language)) { | ||
130 | Object.assign(videojsOptions, { language: options.language }) | ||
131 | } | ||
132 | |||
133 | return videojsOptions | ||
134 | } | ||
135 | |||
136 | function getControlBarChildren (options: { | ||
137 | peertubeLink: boolean | ||
138 | theaterMode: boolean, | ||
139 | videoCaptions: VideoJSCaption[] | ||
140 | }) { | ||
141 | const settingEntries = [] | ||
142 | |||
143 | // Keep an order | ||
144 | settingEntries.push('playbackRateMenuButton') | ||
145 | if (options.videoCaptions.length !== 0) settingEntries.push('captionsButton') | ||
146 | settingEntries.push('resolutionMenuButton') | ||
147 | |||
148 | const children = { | ||
149 | 'playToggle': {}, | ||
150 | 'currentTimeDisplay': {}, | ||
151 | 'timeDivider': {}, | ||
152 | 'durationDisplay': {}, | ||
153 | 'liveDisplay': {}, | ||
154 | |||
155 | 'flexibleWidthSpacer': {}, | ||
156 | 'progressControl': { | ||
157 | children: { | ||
158 | 'seekBar': { | ||
159 | children: { | ||
160 | 'peerTubeLoadProgressBar': {}, | ||
161 | 'mouseTimeDisplay': {}, | ||
162 | 'playProgressBar': {} | ||
163 | } | ||
164 | } | ||
165 | } | ||
166 | }, | ||
167 | |||
168 | 'webTorrentButton': {}, | ||
169 | |||
170 | 'muteToggle': {}, | ||
171 | 'volumeControl': {}, | ||
172 | |||
173 | 'settingsButton': { | ||
174 | setup: { | ||
175 | maxHeightOffset: 40 | ||
176 | }, | ||
177 | entries: settingEntries | ||
178 | } | ||
179 | } | ||
180 | |||
181 | if (options.peertubeLink === true) { | ||
182 | Object.assign(children, { | ||
183 | 'peerTubeLinkButton': {} | ||
184 | }) | ||
185 | } | ||
186 | |||
187 | if (options.theaterMode === true) { | ||
188 | Object.assign(children, { | ||
189 | 'theaterButton': {} | ||
190 | }) | ||
191 | } | ||
192 | |||
193 | Object.assign(children, { | ||
194 | 'fullscreenToggle': {} | ||
195 | }) | ||
196 | |||
197 | return children | ||
198 | } | ||
199 | |||
200 | function addContextMenu (player: any, videoEmbedUrl: string) { | ||
201 | player.contextmenuUI({ | ||
202 | content: [ | ||
203 | { | ||
204 | label: player.localize('Copy the video URL'), | ||
205 | listener: function () { | ||
206 | copyToClipboard(buildVideoLink()) | ||
207 | } | ||
208 | }, | ||
209 | { | ||
210 | label: player.localize('Copy the video URL at the current time'), | ||
211 | listener: function () { | ||
212 | const player = this | ||
213 | copyToClipboard(buildVideoLink(player.currentTime())) | ||
214 | } | ||
215 | }, | ||
216 | { | ||
217 | label: player.localize('Copy embed code'), | ||
218 | listener: () => { | ||
219 | copyToClipboard(buildVideoEmbed(videoEmbedUrl)) | ||
220 | } | ||
221 | }, | ||
222 | { | ||
223 | label: player.localize('Copy magnet URI'), | ||
224 | listener: function () { | ||
225 | const player = this | ||
226 | copyToClipboard(player.peertube().getCurrentVideoFile().magnetUri) | ||
227 | } | ||
228 | } | ||
229 | ] | ||
230 | }) | ||
231 | } | ||
232 | |||
233 | function loadLocaleInVideoJS (serverUrl: string, videojs: any, locale: string) { | ||
234 | const path = getLocalePath(serverUrl, locale) | ||
235 | // It is the default locale, nothing to translate | ||
236 | if (!path) return Promise.resolve(undefined) | ||
237 | |||
238 | let p: Promise<any> | ||
239 | |||
240 | if (loadLocaleInVideoJS.cache[path]) { | ||
241 | p = Promise.resolve(loadLocaleInVideoJS.cache[path]) | ||
242 | } else { | ||
243 | p = fetch(path + '/player.json') | ||
244 | .then(res => res.json()) | ||
245 | .then(json => { | ||
246 | loadLocaleInVideoJS.cache[path] = json | ||
247 | return json | ||
248 | }) | ||
249 | } | ||
250 | |||
251 | const completeLocale = getCompleteLocale(locale) | ||
252 | return p.then(json => videojs.addLanguage(getShortLocale(completeLocale), json)) | ||
253 | } | ||
254 | namespace loadLocaleInVideoJS { | ||
255 | export const cache: { [ path: string ]: any } = {} | ||
256 | } | ||
257 | |||
258 | function getServerTranslations (serverUrl: string, locale: string) { | ||
259 | const path = getLocalePath(serverUrl, locale) | ||
260 | // It is the default locale, nothing to translate | ||
261 | if (!path) return Promise.resolve(undefined) | ||
262 | |||
263 | return fetch(path + '/server.json') | ||
264 | .then(res => res.json()) | ||
265 | } | ||
266 | |||
267 | // ############################################################################ | ||
268 | |||
269 | export { | ||
270 | getServerTranslations, | ||
271 | loadLocaleInVideoJS, | ||
272 | getVideojsOptions, | ||
273 | addContextMenu | ||
274 | } | ||
275 | |||
276 | // ############################################################################ | ||
277 | |||
278 | function getLocalePath (serverUrl: string, locale: string) { | ||
279 | const completeLocale = getCompleteLocale(locale) | ||
280 | |||
281 | if (!is18nLocale(completeLocale) || isDefaultLocale(completeLocale)) return undefined | ||
282 | |||
283 | return serverUrl + '/client/locales/' + completeLocale | ||
284 | } | ||
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts new file mode 100644 index 000000000..7ea4a06d4 --- /dev/null +++ b/client/src/assets/player/peertube-plugin.ts | |||
@@ -0,0 +1,262 @@ | |||
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 autoplay: boolean = false | ||
26 | private readonly startTime: number = 0 | ||
27 | private readonly videoViewUrl: string | ||
28 | private readonly videoDuration: number | ||
29 | private readonly CONSTANTS = { | ||
30 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
31 | } | ||
32 | |||
33 | private player: any | ||
34 | private videoCaptions: VideoJSCaption[] | ||
35 | private defaultSubtitle: string | ||
36 | |||
37 | private videoViewInterval: any | ||
38 | private userWatchingVideoInterval: any | ||
39 | private qualityObservationTimer: any | ||
40 | private lastResolutionChange: ResolutionUpdateData | ||
41 | |||
42 | constructor (player: videojs.Player, options: PeerTubePluginOptions) { | ||
43 | super(player, options) | ||
44 | |||
45 | this.startTime = timeToInt(options.startTime) | ||
46 | this.videoViewUrl = options.videoViewUrl | ||
47 | this.videoDuration = options.videoDuration | ||
48 | this.videoCaptions = options.videoCaptions | ||
49 | |||
50 | if (options.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
51 | |||
52 | this.player.on('autoplay-failure', () => { | ||
53 | this.player.removeClass('vjs-has-autoplay') | ||
54 | }) | ||
55 | |||
56 | this.player.ready(() => { | ||
57 | const playerOptions = this.player.options_ | ||
58 | |||
59 | if (options.mode === 'webtorrent') { | ||
60 | this.player.webtorrent().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | ||
61 | this.player.webtorrent().on('autoResolutionChange', (_: any, d: any) => this.trigger('autoResolutionChange', d)) | ||
62 | } | ||
63 | |||
64 | if (options.mode === 'p2p-media-loader') { | ||
65 | this.player.p2pMediaLoader().on('resolutionChange', (_: any, d: any) => this.handleResolutionChange(d)) | ||
66 | } | ||
67 | |||
68 | this.player.tech_.on('loadedqualitydata', () => { | ||
69 | setTimeout(() => { | ||
70 | // Replay a resolution change, now we loaded all quality data | ||
71 | if (this.lastResolutionChange) this.handleResolutionChange(this.lastResolutionChange) | ||
72 | }, 0) | ||
73 | }) | ||
74 | |||
75 | const volume = getStoredVolume() | ||
76 | if (volume !== undefined) this.player.volume(volume) | ||
77 | |||
78 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
79 | if (muted !== undefined) this.player.muted(muted) | ||
80 | |||
81 | this.defaultSubtitle = options.subtitle || getStoredLastSubtitle() | ||
82 | |||
83 | this.player.on('volumechange', () => { | ||
84 | saveVolumeInStore(this.player.volume()) | ||
85 | saveMuteInStore(this.player.muted()) | ||
86 | }) | ||
87 | |||
88 | this.player.textTracks().on('change', () => { | ||
89 | const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => { | ||
90 | return t.kind === 'captions' && t.mode === 'showing' | ||
91 | }) | ||
92 | |||
93 | if (!showing) { | ||
94 | saveLastSubtitle('off') | ||
95 | return | ||
96 | } | ||
97 | |||
98 | saveLastSubtitle(showing.language) | ||
99 | }) | ||
100 | |||
101 | this.player.on('sourcechange', () => this.initCaptions()) | ||
102 | |||
103 | this.player.duration(options.videoDuration) | ||
104 | |||
105 | this.initializePlayer() | ||
106 | this.runViewAdd() | ||
107 | |||
108 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | dispose () { | ||
113 | clearTimeout(this.qualityObservationTimer) | ||
114 | |||
115 | clearInterval(this.videoViewInterval) | ||
116 | |||
117 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
118 | } | ||
119 | |||
120 | private initializePlayer () { | ||
121 | if (isMobile()) this.player.addClass('vjs-is-mobile') | ||
122 | |||
123 | this.initSmoothProgressBar() | ||
124 | |||
125 | this.initCaptions() | ||
126 | |||
127 | this.alterInactivity() | ||
128 | } | ||
129 | |||
130 | private runViewAdd () { | ||
131 | this.clearVideoViewInterval() | ||
132 | |||
133 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
134 | let minSecondsToView = 30 | ||
135 | |||
136 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
137 | |||
138 | let secondsViewed = 0 | ||
139 | this.videoViewInterval = setInterval(() => { | ||
140 | if (this.player && !this.player.paused()) { | ||
141 | secondsViewed += 1 | ||
142 | |||
143 | if (secondsViewed > minSecondsToView) { | ||
144 | this.clearVideoViewInterval() | ||
145 | |||
146 | this.addViewToVideo().catch(err => console.error(err)) | ||
147 | } | ||
148 | } | ||
149 | }, 1000) | ||
150 | } | ||
151 | |||
152 | private runUserWatchVideo (options: UserWatching) { | ||
153 | let lastCurrentTime = 0 | ||
154 | |||
155 | this.userWatchingVideoInterval = setInterval(() => { | ||
156 | const currentTime = Math.floor(this.player.currentTime()) | ||
157 | |||
158 | if (currentTime - lastCurrentTime >= 1) { | ||
159 | lastCurrentTime = currentTime | ||
160 | |||
161 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
162 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
163 | } | ||
164 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
165 | } | ||
166 | |||
167 | private clearVideoViewInterval () { | ||
168 | if (this.videoViewInterval !== undefined) { | ||
169 | clearInterval(this.videoViewInterval) | ||
170 | this.videoViewInterval = undefined | ||
171 | } | ||
172 | } | ||
173 | |||
174 | private addViewToVideo () { | ||
175 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
176 | |||
177 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
178 | } | ||
179 | |||
180 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | ||
181 | const body = new URLSearchParams() | ||
182 | body.append('currentTime', currentTime.toString()) | ||
183 | |||
184 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
185 | |||
186 | return fetch(url, { method: 'PUT', body, headers }) | ||
187 | } | ||
188 | |||
189 | private handleResolutionChange (data: ResolutionUpdateData) { | ||
190 | this.lastResolutionChange = data | ||
191 | |||
192 | const qualityLevels = this.player.qualityLevels() | ||
193 | |||
194 | for (let i = 0; i < qualityLevels.length; i++) { | ||
195 | if (qualityLevels[i].height === data.resolutionId) { | ||
196 | data.id = qualityLevels[i].id | ||
197 | break | ||
198 | } | ||
199 | } | ||
200 | |||
201 | this.trigger('resolutionChange', data) | ||
202 | } | ||
203 | |||
204 | private alterInactivity () { | ||
205 | let saveInactivityTimeout: number | ||
206 | |||
207 | const disableInactivity = () => { | ||
208 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
209 | this.player.options_.inactivityTimeout = 0 | ||
210 | } | ||
211 | const enableInactivity = () => { | ||
212 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
213 | } | ||
214 | |||
215 | const settingsDialog = this.player.children_.find((c: any) => c.name_ === 'SettingsDialog') | ||
216 | |||
217 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
218 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
219 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
220 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
221 | } | ||
222 | |||
223 | private initCaptions () { | ||
224 | for (const caption of this.videoCaptions) { | ||
225 | this.player.addRemoteTextTrack({ | ||
226 | kind: 'captions', | ||
227 | label: caption.label, | ||
228 | language: caption.language, | ||
229 | id: caption.language, | ||
230 | src: caption.src, | ||
231 | default: this.defaultSubtitle === caption.language | ||
232 | }, false) | ||
233 | } | ||
234 | |||
235 | this.player.trigger('captionsChanged') | ||
236 | } | ||
237 | |||
238 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | ||
239 | private initSmoothProgressBar () { | ||
240 | const SeekBar = videojsUntyped.getComponent('SeekBar') | ||
241 | SeekBar.prototype.getPercent = function getPercent () { | ||
242 | // Allows for smooth scrubbing, when player can't keep up. | ||
243 | // const time = (this.player_.scrubbing()) ? | ||
244 | // this.player_.getCache().currentTime : | ||
245 | // this.player_.currentTime() | ||
246 | const time = this.player_.currentTime() | ||
247 | const percent = time / this.player_.duration() | ||
248 | return percent >= 1 ? 1 : percent | ||
249 | } | ||
250 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event: any) { | ||
251 | let newTime = this.calculateDistance(event) * this.player_.duration() | ||
252 | if (newTime === this.player_.duration()) { | ||
253 | newTime = newTime - 0.1 | ||
254 | } | ||
255 | this.player_.currentTime(newTime) | ||
256 | this.update() | ||
257 | } | ||
258 | } | ||
259 | } | ||
260 | |||
261 | videojs.registerPlugin('peertube', PeerTubePlugin) | ||
262 | export { PeerTubePlugin } | ||
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts index b117007af..79a5a6c4d 100644 --- a/client/src/assets/player/peertube-videojs-typings.ts +++ b/client/src/assets/player/peertube-videojs-typings.ts | |||
@@ -1,19 +1,27 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
1 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | |||
2 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 5 | import { VideoFile } from '../../../../shared/models/videos/video.model' |
3 | 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' | ||
4 | 10 | ||
5 | declare namespace videojs { | 11 | declare namespace videojs { |
6 | interface Player { | 12 | interface Player { |
7 | peertube (): PeerTubePlugin | 13 | peertube (): PeerTubePlugin |
14 | webtorrent (): WebTorrentPlugin | ||
15 | p2pMediaLoader (): P2pMediaLoaderPlugin | ||
8 | } | 16 | } |
9 | } | 17 | } |
10 | 18 | ||
11 | interface VideoJSComponentInterface { | 19 | interface VideoJSComponentInterface { |
12 | _player: videojs.Player | 20 | _player: videojs.Player |
13 | 21 | ||
14 | new (player: videojs.Player, options?: any) | 22 | new (player: videojs.Player, options?: any): any |
15 | 23 | ||
16 | registerComponent (name: string, obj: any) | 24 | registerComponent (name: string, obj: any): any |
17 | } | 25 | } |
18 | 26 | ||
19 | type VideoJSCaption = { | 27 | type VideoJSCaption = { |
@@ -27,25 +35,95 @@ type UserWatching = { | |||
27 | authorizationHeader: string | 35 | authorizationHeader: string |
28 | } | 36 | } |
29 | 37 | ||
30 | type PeertubePluginOptions = { | 38 | type PeerTubePluginOptions = { |
31 | videoFiles: VideoFile[] | 39 | mode: PlayerMode |
32 | playerElement: HTMLVideoElement | 40 | |
41 | autoplay: boolean | ||
33 | videoViewUrl: string | 42 | videoViewUrl: string |
34 | videoDuration: number | 43 | videoDuration: number |
35 | startTime: number | string | 44 | startTime: number | string |
36 | autoplay: boolean, | ||
37 | videoCaptions: VideoJSCaption[] | ||
38 | 45 | ||
39 | userWatching?: UserWatching | 46 | userWatching?: UserWatching |
47 | subtitle?: string | ||
48 | |||
49 | videoCaptions: VideoJSCaption[] | ||
50 | } | ||
51 | |||
52 | type WebtorrentPluginOptions = { | ||
53 | playerElement: HTMLVideoElement | ||
54 | |||
55 | autoplay: boolean | ||
56 | videoDuration: number | ||
57 | |||
58 | videoFiles: VideoFile[] | ||
59 | } | ||
60 | |||
61 | type P2PMediaLoaderPluginOptions = { | ||
62 | redundancyBaseUrls: string[] | ||
63 | type: string | ||
64 | src: string | ||
65 | } | ||
66 | |||
67 | type VideoJSPluginOptions = { | ||
68 | peertube: PeerTubePluginOptions | ||
69 | |||
70 | webtorrent?: WebtorrentPluginOptions | ||
71 | |||
72 | p2pMediaLoader?: P2PMediaLoaderPluginOptions | ||
40 | } | 73 | } |
41 | 74 | ||
42 | // videojs typings don't have some method we need | 75 | // videojs typings don't have some method we need |
43 | const videojsUntyped = videojs as any | 76 | const videojsUntyped = videojs as any |
44 | 77 | ||
78 | type LoadedQualityData = { | ||
79 | qualitySwitchCallback: Function, | ||
80 | qualityData: { | ||
81 | video: { | ||
82 | id: number | ||
83 | label: string | ||
84 | selected: boolean | ||
85 | }[] | ||
86 | } | ||
87 | } | ||
88 | |||
89 | type ResolutionUpdateData = { | ||
90 | auto: boolean, | ||
91 | resolutionId: number | ||
92 | id?: number | ||
93 | } | ||
94 | |||
95 | type AutoResolutionUpdateData = { | ||
96 | possible: boolean | ||
97 | } | ||
98 | |||
99 | type PlayerNetworkInfo = { | ||
100 | http: { | ||
101 | downloadSpeed: number | ||
102 | uploadSpeed: number | ||
103 | downloaded: number | ||
104 | uploaded: number | ||
105 | } | ||
106 | |||
107 | p2p: { | ||
108 | downloadSpeed: number | ||
109 | uploadSpeed: number | ||
110 | downloaded: number | ||
111 | uploaded: number | ||
112 | numPeers: number | ||
113 | } | ||
114 | } | ||
115 | |||
45 | export { | 116 | export { |
117 | PlayerNetworkInfo, | ||
118 | ResolutionUpdateData, | ||
119 | AutoResolutionUpdateData, | ||
46 | VideoJSComponentInterface, | 120 | VideoJSComponentInterface, |
47 | PeertubePluginOptions, | ||
48 | videojsUntyped, | 121 | videojsUntyped, |
49 | VideoJSCaption, | 122 | VideoJSCaption, |
50 | UserWatching | 123 | UserWatching, |
124 | PeerTubePluginOptions, | ||
125 | WebtorrentPluginOptions, | ||
126 | P2PMediaLoaderPluginOptions, | ||
127 | VideoJSPluginOptions, | ||
128 | LoadedQualityData | ||
51 | } | 129 | } |
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 d53a24151..000000000 --- a/client/src/assets/player/resolution-menu-button.ts +++ /dev/null | |||
@@ -1,85 +0,0 @@ | |||
1 | import * as videojs from 'video.js' | ||
2 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
3 | import { ResolutionMenuItem } from './resolution-menu-item' | ||
4 | |||
5 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | ||
6 | const MenuButton: VideoJSComponentInterface = videojsUntyped.getComponent('MenuButton') | ||
7 | class ResolutionMenuButton extends MenuButton { | ||
8 | label: HTMLElement | ||
9 | |||
10 | constructor (player: videojs.Player, options) { | ||
11 | super(player, options) | ||
12 | this.player = player | ||
13 | |||
14 | player.peertube().on('videoFileUpdate', () => this.updateLabel()) | ||
15 | player.peertube().on('autoResolutionUpdate', () => this.updateLabel()) | ||
16 | } | ||
17 | |||
18 | createEl () { | ||
19 | const el = super.createEl() | ||
20 | |||
21 | this.labelEl_ = videojsUntyped.dom.createEl('div', { | ||
22 | className: 'vjs-resolution-value', | ||
23 | innerHTML: this.buildLabelHTML() | ||
24 | }) | ||
25 | |||
26 | el.appendChild(this.labelEl_) | ||
27 | |||
28 | return el | ||
29 | } | ||
30 | |||
31 | updateARIAAttributes () { | ||
32 | this.el().setAttribute('aria-label', 'Quality') | ||
33 | } | ||
34 | |||
35 | createMenu () { | ||
36 | const menu = new Menu(this.player_) | ||
37 | for (const videoFile of this.player_.peertube().videoFiles) { | ||
38 | let label = videoFile.resolution.label | ||
39 | if (videoFile.fps && videoFile.fps >= 50) { | ||
40 | label += videoFile.fps | ||
41 | } | ||
42 | |||
43 | menu.addChild(new ResolutionMenuItem( | ||
44 | this.player_, | ||
45 | { | ||
46 | id: videoFile.resolution.id, | ||
47 | label, | ||
48 | src: videoFile.magnetUri | ||
49 | }) | ||
50 | ) | ||
51 | } | ||
52 | |||
53 | menu.addChild(new ResolutionMenuItem( | ||
54 | this.player_, | ||
55 | { | ||
56 | id: -1, | ||
57 | label: this.player_.localize('Auto'), | ||
58 | src: null | ||
59 | } | ||
60 | )) | ||
61 | |||
62 | return menu | ||
63 | } | ||
64 | |||
65 | updateLabel () { | ||
66 | if (!this.labelEl_) return | ||
67 | |||
68 | this.labelEl_.innerHTML = this.buildLabelHTML() | ||
69 | } | ||
70 | |||
71 | buildCSSClass () { | ||
72 | return super.buildCSSClass() + ' vjs-resolution-button' | ||
73 | } | ||
74 | |||
75 | buildWrapperCSSClass () { | ||
76 | return 'vjs-resolution-control ' + super.buildWrapperCSSClass() | ||
77 | } | ||
78 | |||
79 | private buildLabelHTML () { | ||
80 | return this.player_.peertube().getCurrentResolutionLabel() | ||
81 | } | ||
82 | } | ||
83 | ResolutionMenuButton.prototype.controlText_ = 'Quality' | ||
84 | |||
85 | 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 0ab0f53b5..000000000 --- a/client/src/assets/player/resolution-menu-item.ts +++ /dev/null | |||
@@ -1,64 +0,0 @@ | |||
1 | import * as videojs from 'video.js' | ||
2 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | ||
3 | |||
4 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | ||
5 | class ResolutionMenuItem extends MenuItem { | ||
6 | |||
7 | constructor (player: videojs.Player, options) { | ||
8 | const currentResolutionId = player.peertube().getCurrentResolutionId() | ||
9 | options.selectable = true | ||
10 | options.selected = options.id === currentResolutionId | ||
11 | |||
12 | super(player, options) | ||
13 | |||
14 | this.label = options.label | ||
15 | this.id = options.id | ||
16 | |||
17 | player.peertube().on('videoFileUpdate', () => this.updateSelection()) | ||
18 | player.peertube().on('autoResolutionUpdate', () => this.updateSelection()) | ||
19 | } | ||
20 | |||
21 | handleClick (event) { | ||
22 | if (this.id === -1 && this.player_.peertube().isAutoResolutionForbidden()) return | ||
23 | |||
24 | super.handleClick(event) | ||
25 | |||
26 | // Auto resolution | ||
27 | if (this.id === -1) { | ||
28 | this.player_.peertube().enableAutoResolution() | ||
29 | return | ||
30 | } | ||
31 | |||
32 | this.player_.peertube().disableAutoResolution() | ||
33 | this.player_.peertube().updateResolution(this.id) | ||
34 | } | ||
35 | |||
36 | updateSelection () { | ||
37 | // Check if auto resolution is forbidden or not | ||
38 | if (this.id === -1) { | ||
39 | if (this.player_.peertube().isAutoResolutionForbidden()) { | ||
40 | this.addClass('disabled') | ||
41 | } else { | ||
42 | this.removeClass('disabled') | ||
43 | } | ||
44 | } | ||
45 | |||
46 | if (this.player_.peertube().isAutoResolutionOn()) { | ||
47 | this.selected(this.id === -1) | ||
48 | return | ||
49 | } | ||
50 | |||
51 | this.selected(this.player_.peertube().getCurrentResolutionId() === this.id) | ||
52 | } | ||
53 | |||
54 | getLabel () { | ||
55 | if (this.id === -1) { | ||
56 | return this.label + ' <small>' + this.player_.peertube().getCurrentResolutionLabel() + '</small>' | ||
57 | } | ||
58 | |||
59 | return this.label | ||
60 | } | ||
61 | } | ||
62 | MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem) | ||
63 | |||
64 | export { ResolutionMenuItem } | ||
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts index cf4f60f55..8d87567c2 100644 --- a/client/src/assets/player/utils.ts +++ b/client/src/assets/player/utils.ts | |||
@@ -12,7 +12,7 @@ const dictionaryBytes: Array<{max: number, type: string}> = [ | |||
12 | { max: 1073741824, type: 'MB' }, | 12 | { max: 1073741824, type: 'MB' }, |
13 | { max: 1.0995116e12, type: 'GB' } | 13 | { max: 1.0995116e12, type: 'GB' } |
14 | ] | 14 | ] |
15 | function bytes (value) { | 15 | function bytes (value: number) { |
16 | const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1] | 16 | const format = dictionaryBytes.find(d => value < d.max) || dictionaryBytes[dictionaryBytes.length - 1] |
17 | const calc = Math.floor(value / (format.max / 1024)).toString() | 17 | const calc = Math.floor(value / (format.max / 1024)).toString() |
18 | 18 | ||
@@ -39,6 +39,7 @@ function buildVideoLink (time?: number, url?: string) { | |||
39 | } | 39 | } |
40 | 40 | ||
41 | function timeToInt (time: number | string) { | 41 | function timeToInt (time: number | string) { |
42 | if (!time) return 0 | ||
42 | if (typeof time === 'number') return time | 43 | if (typeof time === 'number') return time |
43 | 44 | ||
44 | const reg = /^((\d+)h)?((\d+)m)?((\d+)s?)?$/ | 45 | const reg = /^((\d+)h)?((\d+)m)?((\d+)s?)?$/ |
@@ -111,9 +112,23 @@ function videoFileMinByResolution (files: VideoFile[]) { | |||
111 | return min | 112 | return min |
112 | } | 113 | } |
113 | 114 | ||
115 | function getRtcConfig () { | ||
116 | return { | ||
117 | iceServers: [ | ||
118 | { | ||
119 | urls: 'stun:stun.stunprotocol.org' | ||
120 | }, | ||
121 | { | ||
122 | urls: 'stun:stun.framasoft.org' | ||
123 | } | ||
124 | ] | ||
125 | } | ||
126 | } | ||
127 | |||
114 | // --------------------------------------------------------------------------- | 128 | // --------------------------------------------------------------------------- |
115 | 129 | ||
116 | export { | 130 | export { |
131 | getRtcConfig, | ||
117 | toTitleCase, | 132 | toTitleCase, |
118 | timeToInt, | 133 | timeToInt, |
119 | buildVideoLink, | 134 | buildVideoLink, |
diff --git a/client/src/assets/player/webtorrent-info-button.ts b/client/src/assets/player/videojs-components/p2p-info-button.ts index deef253ce..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, data) => { | 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 715207bc0..fed8ea33e 100644 --- a/client/src/assets/player/peertube-link-button.ts +++ b/client/src/assets/player/videojs-components/peertube-link-button.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | import * as videojs from 'video.js' | 1 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
2 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 2 | import { buildVideoLink } from '../utils' |
3 | import { buildVideoLink } from './utils' | 3 | // FIXME: something weird with our path definition in tsconfig and typings |
4 | // @ts-ignore | ||
5 | import { Player } from 'video.js' | ||
4 | 6 | ||
5 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 7 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
6 | class PeerTubeLinkButton extends Button { | 8 | class PeerTubeLinkButton extends Button { |
7 | 9 | ||
8 | constructor (player: videojs.Player, options) { | 10 | constructor (player: Player, options: any) { |
9 | super(player, options) | 11 | super(player, options) |
10 | } | 12 | } |
11 | 13 | ||
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 aedc641e4..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,10 +1,13 @@ | |||
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 | ||
3 | // @ts-ignore | ||
4 | import { Player } from 'video.js' | ||
2 | 5 | ||
3 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 6 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
4 | 7 | ||
5 | class PeerTubeLoadProgressBar extends Component { | 8 | class PeerTubeLoadProgressBar extends Component { |
6 | 9 | ||
7 | constructor (player, options) { | 10 | constructor (player: Player, options: any) { |
8 | super(player, options) | 11 | super(player, options) |
9 | this.partEls_ = [] | 12 | this.partEls_ = [] |
10 | this.on(player, 'progress', this.update) | 13 | this.on(player, 'progress', this.update) |
@@ -24,7 +27,7 @@ class PeerTubeLoadProgressBar extends Component { | |||
24 | } | 27 | } |
25 | 28 | ||
26 | update () { | 29 | update () { |
27 | const torrent = this.player().peertube().getTorrent() | 30 | const torrent = this.player().webtorrent().getTorrent() |
28 | if (!torrent) return | 31 | if (!torrent) return |
29 | 32 | ||
30 | 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..abcc16411 --- /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 | let 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 b51c52506..14cb8ba43 100644 --- a/client/src/assets/player/settings-menu-button.ts +++ b/client/src/assets/player/videojs-components/settings-menu-button.ts | |||
@@ -1,17 +1,20 @@ | |||
1 | // Author: Yanko Shterev | 1 | // Author: Yanko Shterev |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | 2 | // Thanks https://github.com/yshterev/videojs-settings-menu |
3 | 3 | ||
4 | // FIXME: something weird with our path definition in tsconfig and typings | ||
5 | // @ts-ignore | ||
4 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
7 | |||
5 | import { SettingsMenuItem } from './settings-menu-item' | 8 | import { SettingsMenuItem } from './settings-menu-item' |
6 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' |
7 | import { toTitleCase } from './utils' | 10 | import { toTitleCase } from '../utils' |
8 | 11 | ||
9 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 12 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
10 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') | 13 | const Menu: VideoJSComponentInterface = videojsUntyped.getComponent('Menu') |
11 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 14 | const Component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
12 | 15 | ||
13 | class SettingsButton extends Button { | 16 | class SettingsButton extends Button { |
14 | constructor (player: videojs.Player, options) { | 17 | constructor (player: videojs.Player, options: any) { |
15 | super(player, options) | 18 | super(player, options) |
16 | 19 | ||
17 | this.playerComponent = player | 20 | this.playerComponent = player |
@@ -48,7 +51,7 @@ class SettingsButton extends Button { | |||
48 | } | 51 | } |
49 | } | 52 | } |
50 | 53 | ||
51 | onDisposeSettingsItem (event, name: string) { | 54 | onDisposeSettingsItem (event: any, name: string) { |
52 | if (name === undefined) { | 55 | if (name === undefined) { |
53 | let children = this.menu.children() | 56 | let children = this.menu.children() |
54 | 57 | ||
@@ -74,7 +77,7 @@ class SettingsButton extends Button { | |||
74 | } | 77 | } |
75 | } | 78 | } |
76 | 79 | ||
77 | onAddSettingsItem (event, data) { | 80 | onAddSettingsItem (event: any, data: any) { |
78 | const [ entry, options ] = data | 81 | const [ entry, options ] = data |
79 | 82 | ||
80 | this.addMenuItem(entry, options) | 83 | this.addMenuItem(entry, options) |
@@ -120,7 +123,7 @@ class SettingsButton extends Button { | |||
120 | this.resetChildren() | 123 | this.resetChildren() |
121 | } | 124 | } |
122 | 125 | ||
123 | getComponentSize (element) { | 126 | getComponentSize (element: any) { |
124 | let width: number = null | 127 | let width: number = null |
125 | let height: number = null | 128 | let height: number = null |
126 | 129 | ||
@@ -178,8 +181,8 @@ class SettingsButton extends Button { | |||
178 | this.panelChild.addChild(this.menu) | 181 | this.panelChild.addChild(this.menu) |
179 | } | 182 | } |
180 | 183 | ||
181 | addMenuItem (entry, options) { | 184 | addMenuItem (entry: any, options: any) { |
182 | const openSubMenu = function () { | 185 | const openSubMenu = function (this: any) { |
183 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { | 186 | if (videojsUntyped.dom.hasClass(this.el_, 'open')) { |
184 | videojsUntyped.dom.removeClass(this.el_, 'open') | 187 | videojsUntyped.dom.removeClass(this.el_, 'open') |
185 | } else { | 188 | } else { |
@@ -218,7 +221,7 @@ class SettingsButton extends Button { | |||
218 | } | 221 | } |
219 | 222 | ||
220 | class SettingsPanel extends Component { | 223 | class SettingsPanel extends Component { |
221 | constructor (player: videojs.Player, options) { | 224 | constructor (player: videojs.Player, options: any) { |
222 | super(player, options) | 225 | super(player, options) |
223 | } | 226 | } |
224 | 227 | ||
@@ -232,7 +235,7 @@ class SettingsPanel extends Component { | |||
232 | } | 235 | } |
233 | 236 | ||
234 | class SettingsPanelChild extends Component { | 237 | class SettingsPanelChild extends Component { |
235 | constructor (player: videojs.Player, options) { | 238 | constructor (player: videojs.Player, options: any) { |
236 | super(player, options) | 239 | super(player, options) |
237 | } | 240 | } |
238 | 241 | ||
@@ -246,7 +249,7 @@ class SettingsPanelChild extends Component { | |||
246 | } | 249 | } |
247 | 250 | ||
248 | class SettingsDialog extends Component { | 251 | class SettingsDialog extends Component { |
249 | constructor (player: videojs.Player, options) { | 252 | constructor (player: videojs.Player, options: any) { |
250 | super(player, options) | 253 | super(player, options) |
251 | this.hide() | 254 | this.hide() |
252 | } | 255 | } |
diff --git a/client/src/assets/player/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 665ce6fc2..f14959f9c 100644 --- a/client/src/assets/player/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -1,16 +1,19 @@ | |||
1 | // Author: Yanko Shterev | 1 | // Author: Yanko Shterev |
2 | // Thanks https://github.com/yshterev/videojs-settings-menu | 2 | // Thanks https://github.com/yshterev/videojs-settings-menu |
3 | 3 | ||
4 | // FIXME: something weird with our path definition in tsconfig and typings | ||
5 | // @ts-ignore | ||
4 | import * as videojs from 'video.js' | 6 | import * as videojs from 'video.js' |
5 | import { toTitleCase } from './utils' | 7 | |
6 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 8 | import { toTitleCase } from '../utils' |
9 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
7 | 10 | ||
8 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') | 11 | const MenuItem: VideoJSComponentInterface = videojsUntyped.getComponent('MenuItem') |
9 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') | 12 | const component: VideoJSComponentInterface = videojsUntyped.getComponent('Component') |
10 | 13 | ||
11 | class SettingsMenuItem extends MenuItem { | 14 | class SettingsMenuItem extends MenuItem { |
12 | 15 | ||
13 | constructor (player: videojs.Player, options, entry: string, menuButton: VideoJSComponentInterface) { | 16 | constructor (player: videojs.Player, options: any, entry: string, menuButton: VideoJSComponentInterface) { |
14 | super(player, options) | 17 | super(player, options) |
15 | 18 | ||
16 | this.settingsButton = menuButton | 19 | this.settingsButton = menuButton |
@@ -45,6 +48,19 @@ class SettingsMenuItem extends MenuItem { | |||
45 | // Update on rate change | 48 | // Update on rate change |
46 | player.on('ratechange', this.submenuClickHandler) | 49 | player.on('ratechange', this.submenuClickHandler) |
47 | 50 | ||
51 | if (subMenuName === 'CaptionsButton') { | ||
52 | // Hack to regenerate captions on HTTP fallback | ||
53 | player.on('captionsChanged', () => { | ||
54 | setTimeout(() => { | ||
55 | this.settingsSubMenuEl_.innerHTML = '' | ||
56 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | ||
57 | this.update() | ||
58 | this.bindClickEvents() | ||
59 | |||
60 | }, 0) | ||
61 | }) | ||
62 | } | ||
63 | |||
48 | this.reset() | 64 | this.reset() |
49 | }, 0) | 65 | }, 0) |
50 | }) | 66 | }) |
@@ -55,7 +71,7 @@ class SettingsMenuItem extends MenuItem { | |||
55 | this.transitionEndHandler = this.onTransitionEnd.bind(this) | 71 | this.transitionEndHandler = this.onTransitionEnd.bind(this) |
56 | } | 72 | } |
57 | 73 | ||
58 | onSubmenuClick (event) { | 74 | onSubmenuClick (event: any) { |
59 | let target = null | 75 | let target = null |
60 | 76 | ||
61 | if (event.type === 'tap') { | 77 | if (event.type === 'tap') { |
@@ -150,7 +166,7 @@ class SettingsMenuItem extends MenuItem { | |||
150 | * | 166 | * |
151 | * @method PrefixedEvent | 167 | * @method PrefixedEvent |
152 | */ | 168 | */ |
153 | PrefixedEvent (element, type, callback, action = 'addEvent') { | 169 | PrefixedEvent (element: any, type: any, callback: any, action = 'addEvent') { |
154 | let prefix = ['webkit', 'moz', 'MS', 'o', ''] | 170 | let prefix = ['webkit', 'moz', 'MS', 'o', ''] |
155 | 171 | ||
156 | for (let p = 0; p < prefix.length; p++) { | 172 | for (let p = 0; p < prefix.length; p++) { |
@@ -166,7 +182,7 @@ class SettingsMenuItem extends MenuItem { | |||
166 | } | 182 | } |
167 | } | 183 | } |
168 | 184 | ||
169 | onTransitionEnd (event) { | 185 | onTransitionEnd (event: any) { |
170 | if (event.propertyName !== 'margin-right') { | 186 | if (event.propertyName !== 'margin-right') { |
171 | return | 187 | return |
172 | } | 188 | } |
@@ -204,12 +220,14 @@ class SettingsMenuItem extends MenuItem { | |||
204 | } | 220 | } |
205 | 221 | ||
206 | build () { | 222 | build () { |
207 | const saveUpdateLabel = this.subMenu.updateLabel | 223 | this.subMenu.on('updateLabel', () => { |
208 | this.subMenu.updateLabel = () => { | ||
209 | this.update() | 224 | this.update() |
210 | 225 | }) | |
211 | saveUpdateLabel.call(this.subMenu) | 226 | this.subMenu.on('menuChanged', () => { |
212 | } | 227 | this.bindClickEvents() |
228 | this.setSize() | ||
229 | this.update() | ||
230 | }) | ||
213 | 231 | ||
214 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) | 232 | this.settingsSubMenuTitleEl_.innerHTML = this.player_.localize(this.subMenu.controlText_) |
215 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) | 233 | this.settingsSubMenuEl_.appendChild(this.subMenu.menu.el_) |
@@ -217,7 +235,7 @@ class SettingsMenuItem extends MenuItem { | |||
217 | this.update() | 235 | this.update() |
218 | 236 | ||
219 | this.createBackButton() | 237 | this.createBackButton() |
220 | this.getSize() | 238 | this.setSize() |
221 | this.bindClickEvents() | 239 | this.bindClickEvents() |
222 | 240 | ||
223 | // prefixed event listeners for CSS TransitionEnd | 241 | // prefixed event listeners for CSS TransitionEnd |
@@ -229,8 +247,8 @@ class SettingsMenuItem extends MenuItem { | |||
229 | ) | 247 | ) |
230 | } | 248 | } |
231 | 249 | ||
232 | update (event?: Event) { | 250 | update (event?: any) { |
233 | let target = null | 251 | let target: HTMLElement = null |
234 | let subMenu = this.subMenu.name() | 252 | let subMenu = this.subMenu.name() |
235 | 253 | ||
236 | if (event && event.type === 'tap') { | 254 | if (event && event.type === 'tap') { |
@@ -279,8 +297,9 @@ class SettingsMenuItem extends MenuItem { | |||
279 | 297 | ||
280 | // save size of submenus on first init | 298 | // save size of submenus on first init |
281 | // 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 |
282 | getSize () { | 300 | setSize () { |
283 | this.dialog.removeClass('vjs-hidden') | 301 | this.dialog.removeClass('vjs-hidden') |
302 | videojsUntyped.dom.removeClass(this.settingsSubMenuEl_, 'vjs-hidden') | ||
284 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) | 303 | this.size = this.settingsButton.getComponentSize(this.settingsSubMenuEl_) |
285 | this.setMargin() | 304 | this.setMargin() |
286 | this.dialog.addClass('vjs-hidden') | 305 | this.dialog.addClass('vjs-hidden') |
diff --git a/client/src/assets/player/theater-button.ts b/client/src/assets/player/videojs-components/theater-button.ts index 5cf0b6425..1e11a9546 100644 --- a/client/src/assets/player/theater-button.ts +++ b/client/src/assets/player/videojs-components/theater-button.ts | |||
@@ -1,12 +1,16 @@ | |||
1 | import { VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 1 | // FIXME: something weird with our path definition in tsconfig and typings |
2 | import { saveTheaterInStore, getStoredTheater } from './peertube-player-local-storage' | 2 | // @ts-ignore |
3 | import * as videojs from 'video.js' | ||
4 | |||
5 | import { VideoJSComponentInterface, videojsUntyped } from '../peertube-videojs-typings' | ||
6 | import { saveTheaterInStore, getStoredTheater } from '../peertube-player-local-storage' | ||
3 | 7 | ||
4 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') | 8 | const Button: VideoJSComponentInterface = videojsUntyped.getComponent('Button') |
5 | class TheaterButton extends Button { | 9 | class TheaterButton extends Button { |
6 | 10 | ||
7 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' | 11 | private static readonly THEATER_MODE_CLASS = 'vjs-theater-enabled' |
8 | 12 | ||
9 | constructor (player, options) { | 13 | constructor (player: videojs.Player, options: any) { |
10 | super(player, options) | 14 | super(player, options) |
11 | 15 | ||
12 | const enabled = getStoredTheater() | 16 | const enabled = getStoredTheater() |
diff --git a/client/src/assets/player/peertube-chunk-store.ts b/client/src/assets/player/webtorrent/peertube-chunk-store.ts index 767e46821..54cc0ea64 100644 --- a/client/src/assets/player/peertube-chunk-store.ts +++ b/client/src/assets/player/webtorrent/peertube-chunk-store.ts | |||
@@ -40,15 +40,15 @@ export class PeertubeChunkStore extends EventEmitter { | |||
40 | // If the store is full | 40 | // If the store is full |
41 | private memoryChunks: { [ id: number ]: Buffer | true } = {} | 41 | private memoryChunks: { [ id: number ]: Buffer | true } = {} |
42 | private databaseName: string | 42 | private databaseName: string |
43 | private putBulkTimeout | 43 | private putBulkTimeout: any |
44 | private cleanerInterval | 44 | private cleanerInterval: any |
45 | private db: ChunkDatabase | 45 | private db: ChunkDatabase |
46 | private expirationDB: ExpirationDatabase | 46 | private expirationDB: ExpirationDatabase |
47 | private readonly length: number | 47 | private readonly length: number |
48 | private readonly lastChunkLength: number | 48 | private readonly lastChunkLength: number |
49 | private readonly lastChunkIndex: number | 49 | private readonly lastChunkIndex: number |
50 | 50 | ||
51 | constructor (chunkLength: number, opts) { | 51 | constructor (chunkLength: number, opts: any) { |
52 | super() | 52 | super() |
53 | 53 | ||
54 | this.databaseName = 'webtorrent-chunks-' | 54 | this.databaseName = 'webtorrent-chunks-' |
@@ -76,7 +76,7 @@ export class PeertubeChunkStore extends EventEmitter { | |||
76 | this.runCleaner() | 76 | this.runCleaner() |
77 | } | 77 | } |
78 | 78 | ||
79 | put (index: number, buf: Buffer, cb: Function) { | 79 | put (index: number, buf: Buffer, cb: (err?: Error) => void) { |
80 | const isLastChunk = (index === this.lastChunkIndex) | 80 | const isLastChunk = (index === this.lastChunkIndex) |
81 | if (isLastChunk && buf.length !== this.lastChunkLength) { | 81 | if (isLastChunk && buf.length !== this.lastChunkLength) { |
82 | return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength)) | 82 | return this.nextTick(cb, new Error('Last chunk length must be ' + this.lastChunkLength)) |
@@ -113,13 +113,13 @@ export class PeertubeChunkStore extends EventEmitter { | |||
113 | }, PeertubeChunkStore.BUFFERING_PUT_MS) | 113 | }, PeertubeChunkStore.BUFFERING_PUT_MS) |
114 | } | 114 | } |
115 | 115 | ||
116 | get (index: number, opts, cb) { | 116 | get (index: number, opts: any, cb: (err?: Error, buf?: Buffer) => void): void { |
117 | if (typeof opts === 'function') return this.get(index, null, opts) | 117 | if (typeof opts === 'function') return this.get(index, null, opts) |
118 | 118 | ||
119 | // IndexDB could be slow, use our memory index first | 119 | // IndexDB could be slow, use our memory index first |
120 | const memoryChunk = this.memoryChunks[index] | 120 | const memoryChunk = this.memoryChunks[index] |
121 | if (memoryChunk === undefined) { | 121 | if (memoryChunk === undefined) { |
122 | const err = new Error('Chunk not found') | 122 | const err = new Error('Chunk not found') as any |
123 | err['notFound'] = true | 123 | err['notFound'] = true |
124 | 124 | ||
125 | return process.nextTick(() => cb(err)) | 125 | return process.nextTick(() => cb(err)) |
@@ -146,11 +146,11 @@ export class PeertubeChunkStore extends EventEmitter { | |||
146 | }) | 146 | }) |
147 | } | 147 | } |
148 | 148 | ||
149 | close (db) { | 149 | close (cb: (err?: Error) => void) { |
150 | return this.destroy(db) | 150 | return this.destroy(cb) |
151 | } | 151 | } |
152 | 152 | ||
153 | async destroy (cb) { | 153 | async destroy (cb: (err?: Error) => void) { |
154 | try { | 154 | try { |
155 | if (this.pendingPut) { | 155 | if (this.pendingPut) { |
156 | clearTimeout(this.putBulkTimeout) | 156 | clearTimeout(this.putBulkTimeout) |
@@ -225,7 +225,7 @@ export class PeertubeChunkStore extends EventEmitter { | |||
225 | } | 225 | } |
226 | } | 226 | } |
227 | 227 | ||
228 | private nextTick (cb, err, val?) { | 228 | private nextTick <T> (cb: (err?: Error, val?: T) => void, err: Error, val?: T) { |
229 | process.nextTick(() => cb(err, val), undefined) | 229 | process.nextTick(() => cb(err, val), undefined) |
230 | } | 230 | } |
231 | } | 231 | } |
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/webtorrent/video-renderer.ts index 2cb05a448..a3415937b 100644 --- a/client/src/assets/player/video-renderer.ts +++ b/client/src/assets/player/webtorrent/video-renderer.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | // Thanks: https://github.com/feross/render-media | 1 | // Thanks: https://github.com/feross/render-media |
2 | // TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed | 2 | // TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed |
3 | 3 | ||
4 | import * as MediaElementWrapper from 'mediasource' | 4 | const MediaElementWrapper = require('mediasource') |
5 | import { extname } from 'path' | 5 | import { extname } from 'path' |
6 | import * as videostream from 'videostream' | 6 | const videostream = require('videostream') |
7 | 7 | ||
8 | const VIDEOSTREAM_EXTS = [ | 8 | const VIDEOSTREAM_EXTS = [ |
9 | '.m4a', | 9 | '.m4a', |
@@ -17,7 +17,7 @@ type RenderMediaOptions = { | |||
17 | } | 17 | } |
18 | 18 | ||
19 | function renderVideo ( | 19 | function renderVideo ( |
20 | file, | 20 | file: any, |
21 | elem: HTMLVideoElement, | 21 | elem: HTMLVideoElement, |
22 | opts: RenderMediaOptions, | 22 | opts: RenderMediaOptions, |
23 | callback: (err: Error, renderer: any) => void | 23 | callback: (err: Error, renderer: any) => void |
@@ -27,11 +27,11 @@ function renderVideo ( | |||
27 | return renderMedia(file, elem, opts, callback) | 27 | return renderMedia(file, elem, opts, callback) |
28 | } | 28 | } |
29 | 29 | ||
30 | function renderMedia (file, 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 = undefined | 32 | let preparedElem: any = undefined |
33 | let currentTime = 0 | 33 | let currentTime = 0 |
34 | let renderer | 34 | let renderer: any |
35 | 35 | ||
36 | try { | 36 | try { |
37 | if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) { | 37 | if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) { |
@@ -45,7 +45,7 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca | |||
45 | 45 | ||
46 | function useVideostream () { | 46 | function useVideostream () { |
47 | prepareElem() | 47 | prepareElem() |
48 | preparedElem.addEventListener('error', function onError (err) { | 48 | preparedElem.addEventListener('error', function onError (err: Error) { |
49 | preparedElem.removeEventListener('error', onError) | 49 | preparedElem.removeEventListener('error', onError) |
50 | 50 | ||
51 | return callback(err) | 51 | return callback(err) |
@@ -58,7 +58,7 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca | |||
58 | const codecs = getCodec(file.name, useVP9) | 58 | const codecs = getCodec(file.name, useVP9) |
59 | 59 | ||
60 | prepareElem() | 60 | prepareElem() |
61 | preparedElem.addEventListener('error', function onError (err) { | 61 | preparedElem.addEventListener('error', function onError (err: Error) { |
62 | preparedElem.removeEventListener('error', onError) | 62 | preparedElem.removeEventListener('error', onError) |
63 | 63 | ||
64 | // Try with vp9 before returning an error | 64 | // Try with vp9 before returning an error |
@@ -102,7 +102,7 @@ function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, ca | |||
102 | } | 102 | } |
103 | } | 103 | } |
104 | 104 | ||
105 | function validateFile (file) { | 105 | function validateFile (file: any) { |
106 | if (file == null) { | 106 | if (file == null) { |
107 | throw new Error('file cannot be null or undefined') | 107 | throw new Error('file cannot be null or undefined') |
108 | } | 108 | } |
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts index 2330f476f..c69bf31fa 100644 --- a/client/src/assets/player/peertube-videojs-plugin.ts +++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts | |||
@@ -1,30 +1,37 @@ | |||
1 | // FIXME: something weird with our path definition in tsconfig and typings | ||
2 | // @ts-ignore | ||
1 | import * as videojs from 'video.js' | 3 | import * as videojs from 'video.js' |
4 | |||
2 | import * as WebTorrent from 'webtorrent' | 5 | import * as WebTorrent from 'webtorrent' |
3 | import { VideoFile } from '../../../../shared/models/videos/video.model' | 6 | import { VideoFile } from '../../../../../shared/models/videos/video.model' |
4 | import { renderVideo } from './video-renderer' | 7 | import { renderVideo } from './video-renderer' |
5 | import './settings-menu-button' | 8 | import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings' |
6 | import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings' | 9 | import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils' |
7 | import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils' | ||
8 | import * as CacheChunkStore from 'cache-chunk-store' | ||
9 | import { PeertubeChunkStore } from './peertube-chunk-store' | 10 | import { PeertubeChunkStore } from './peertube-chunk-store' |
10 | import { | 11 | import { |
11 | getAverageBandwidthInStore, | 12 | getAverageBandwidthInStore, |
12 | getStoredMute, | 13 | getStoredMute, |
13 | getStoredVolume, | 14 | getStoredVolume, |
14 | saveAverageBandwidth, | 15 | getStoredWebTorrentEnabled, |
15 | saveMuteInStore, | 16 | saveAverageBandwidth |
16 | saveVolumeInStore | 17 | } from '../peertube-player-local-storage' |
17 | } from './peertube-player-local-storage' | 18 | |
19 | const CacheChunkStore = require('cache-chunk-store') | ||
20 | |||
21 | type PlayOptions = { | ||
22 | forcePlay?: boolean, | ||
23 | seek?: number, | ||
24 | delay?: number | ||
25 | } | ||
18 | 26 | ||
19 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') | 27 | const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin') |
20 | class PeerTubePlugin extends Plugin { | 28 | class WebTorrentPlugin extends Plugin { |
21 | private readonly playerElement: HTMLVideoElement | 29 | private readonly playerElement: HTMLVideoElement |
22 | 30 | ||
23 | private readonly autoplay: boolean = false | 31 | private readonly autoplay: boolean = false |
24 | private readonly startTime: number = 0 | 32 | private readonly startTime: number = 0 |
25 | private readonly savePlayerSrcFunction: Function | 33 | private readonly savePlayerSrcFunction: Function |
26 | private readonly videoFiles: VideoFile[] | 34 | private readonly videoFiles: VideoFile[] |
27 | private readonly videoViewUrl: string | ||
28 | private readonly videoDuration: number | 35 | private readonly videoDuration: number |
29 | private readonly CONSTANTS = { | 36 | private readonly CONSTANTS = { |
30 | INFO_SCHEDULER: 1000, // Don't change this | 37 | INFO_SCHEDULER: 1000, // Don't change this |
@@ -32,22 +39,12 @@ class PeerTubePlugin extends Plugin { | |||
32 | 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 |
33 | 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 |
34 | 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 |
35 | 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 |
36 | USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video | ||
37 | } | 43 | } |
38 | 44 | ||
39 | private readonly webtorrent = new WebTorrent({ | 45 | private readonly webtorrent = new WebTorrent({ |
40 | tracker: { | 46 | tracker: { |
41 | rtcConfig: { | 47 | rtcConfig: getRtcConfig() |
42 | iceServers: [ | ||
43 | { | ||
44 | urls: 'stun:stun.stunprotocol.org' | ||
45 | }, | ||
46 | { | ||
47 | urls: 'stun:stun.framasoft.org' | ||
48 | } | ||
49 | ] | ||
50 | } | ||
51 | }, | 48 | }, |
52 | dht: false | 49 | dht: false |
53 | }) | 50 | }) |
@@ -55,65 +52,56 @@ class PeerTubePlugin extends Plugin { | |||
55 | private player: any | 52 | private player: any |
56 | private currentVideoFile: VideoFile | 53 | private currentVideoFile: VideoFile |
57 | private torrent: WebTorrent.Torrent | 54 | private torrent: WebTorrent.Torrent |
58 | private videoCaptions: VideoJSCaption[] | ||
59 | 55 | ||
60 | private renderer | 56 | private renderer: any |
61 | private fakeRenderer | 57 | private fakeRenderer: any |
62 | private destoyingFakeRenderer = false | 58 | private destroyingFakeRenderer = false |
63 | 59 | ||
64 | private autoResolution = true | 60 | private autoResolution = true |
65 | private forbidAutoResolution = false | 61 | private autoResolutionPossible = true |
66 | private isAutoResolutionObservation = false | 62 | private isAutoResolutionObservation = false |
63 | private playerRefusedP2P = false | ||
67 | 64 | ||
68 | private videoViewInterval | 65 | private torrentInfoInterval: any |
69 | private torrentInfoInterval | 66 | private autoQualityInterval: any |
70 | private autoQualityInterval | 67 | private addTorrentDelay: any |
71 | private userWatchingVideoInterval | 68 | private qualityObservationTimer: any |
72 | private addTorrentDelay | 69 | private runAutoQualitySchedulerTimer: any |
73 | private qualityObservationTimer | ||
74 | private runAutoQualitySchedulerTimer | ||
75 | 70 | ||
76 | private downloadSpeeds: number[] = [] | 71 | private downloadSpeeds: number[] = [] |
77 | 72 | ||
78 | constructor (player: videojs.Player, options: PeertubePluginOptions) { | 73 | constructor (player: videojs.Player, options: WebtorrentPluginOptions) { |
79 | super(player, options) | 74 | super(player, options) |
80 | 75 | ||
81 | // Disable auto play on iOS | 76 | // Disable auto play on iOS |
82 | this.autoplay = options.autoplay && this.isIOS() === false | 77 | this.autoplay = options.autoplay && this.isIOS() === false |
78 | this.playerRefusedP2P = !getStoredWebTorrentEnabled() | ||
83 | 79 | ||
84 | this.startTime = timeToInt(options.startTime) | ||
85 | this.videoFiles = options.videoFiles | 80 | this.videoFiles = options.videoFiles |
86 | this.videoViewUrl = options.videoViewUrl | ||
87 | this.videoDuration = options.videoDuration | 81 | this.videoDuration = options.videoDuration |
88 | this.videoCaptions = options.videoCaptions | ||
89 | 82 | ||
90 | this.savePlayerSrcFunction = this.player.src | 83 | this.savePlayerSrcFunction = this.player.src |
91 | this.playerElement = options.playerElement | 84 | this.playerElement = options.playerElement |
92 | 85 | ||
93 | if (this.autoplay === true) this.player.addClass('vjs-has-autoplay') | ||
94 | |||
95 | this.player.ready(() => { | 86 | this.player.ready(() => { |
87 | const playerOptions = this.player.options_ | ||
88 | |||
96 | const volume = getStoredVolume() | 89 | const volume = getStoredVolume() |
97 | if (volume !== undefined) this.player.volume(volume) | 90 | if (volume !== undefined) this.player.volume(volume) |
98 | const muted = getStoredMute() | 91 | |
92 | const muted = playerOptions.muted !== undefined ? playerOptions.muted : getStoredMute() | ||
99 | if (muted !== undefined) this.player.muted(muted) | 93 | if (muted !== undefined) this.player.muted(muted) |
100 | 94 | ||
95 | this.player.duration(options.videoDuration) | ||
96 | |||
101 | this.initializePlayer() | 97 | this.initializePlayer() |
102 | this.runTorrentInfoScheduler() | 98 | this.runTorrentInfoScheduler() |
103 | this.runViewAdd() | ||
104 | |||
105 | if (options.userWatching) this.runUserWatchVideo(options.userWatching) | ||
106 | 99 | ||
107 | this.player.one('play', () => { | 100 | this.player.one('play', () => { |
108 | // Don't run immediately scheduler, wait some seconds the TCP connections are made | 101 | // Don't run immediately scheduler, wait some seconds the TCP connections are made |
109 | this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER) | 102 | this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER) |
110 | }) | 103 | }) |
111 | }) | 104 | }) |
112 | |||
113 | this.player.on('volumechange', () => { | ||
114 | saveVolumeInStore(this.player.volume()) | ||
115 | saveMuteInStore(this.player.muted()) | ||
116 | }) | ||
117 | } | 105 | } |
118 | 106 | ||
119 | dispose () { | 107 | dispose () { |
@@ -121,12 +109,9 @@ class PeerTubePlugin extends Plugin { | |||
121 | clearTimeout(this.qualityObservationTimer) | 109 | clearTimeout(this.qualityObservationTimer) |
122 | clearTimeout(this.runAutoQualitySchedulerTimer) | 110 | clearTimeout(this.runAutoQualitySchedulerTimer) |
123 | 111 | ||
124 | clearInterval(this.videoViewInterval) | ||
125 | clearInterval(this.torrentInfoInterval) | 112 | clearInterval(this.torrentInfoInterval) |
126 | clearInterval(this.autoQualityInterval) | 113 | clearInterval(this.autoQualityInterval) |
127 | 114 | ||
128 | if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval) | ||
129 | |||
130 | // Don't need to destroy renderer, video player will be destroyed | 115 | // Don't need to destroy renderer, video player will be destroyed |
131 | this.flushVideoFile(this.currentVideoFile, false) | 116 | this.flushVideoFile(this.currentVideoFile, false) |
132 | 117 | ||
@@ -137,13 +122,6 @@ class PeerTubePlugin extends Plugin { | |||
137 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 | 122 | return this.currentVideoFile ? this.currentVideoFile.resolution.id : -1 |
138 | } | 123 | } |
139 | 124 | ||
140 | getCurrentResolutionLabel () { | ||
141 | if (!this.currentVideoFile) return '' | ||
142 | |||
143 | const fps = this.currentVideoFile.fps >= 50 ? this.currentVideoFile.fps : '' | ||
144 | return this.currentVideoFile.resolution.label + fps | ||
145 | } | ||
146 | |||
147 | updateVideoFile ( | 125 | updateVideoFile ( |
148 | videoFile?: VideoFile, | 126 | videoFile?: VideoFile, |
149 | options: { | 127 | options: { |
@@ -178,12 +156,22 @@ class PeerTubePlugin extends Plugin { | |||
178 | const previousVideoFile = this.currentVideoFile | 156 | const previousVideoFile = this.currentVideoFile |
179 | this.currentVideoFile = videoFile | 157 | this.currentVideoFile = videoFile |
180 | 158 | ||
159 | // Don't try on iOS that does not support MediaSource | ||
160 | // Or don't use P2P if webtorrent is disabled | ||
161 | if (this.isIOS() || this.playerRefusedP2P) { | ||
162 | return this.fallbackToHttp(options, () => { | ||
163 | this.player.playbackRate(oldPlaybackRate) | ||
164 | return done() | ||
165 | }) | ||
166 | } | ||
167 | |||
181 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => { | 168 | this.addTorrent(this.currentVideoFile.magnetUri, previousVideoFile, options, () => { |
182 | this.player.playbackRate(oldPlaybackRate) | 169 | this.player.playbackRate(oldPlaybackRate) |
183 | return done() | 170 | return done() |
184 | }) | 171 | }) |
185 | 172 | ||
186 | this.trigger('videoFileUpdate') | 173 | this.changeQuality() |
174 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.currentVideoFile.resolution.id }) | ||
187 | } | 175 | } |
188 | 176 | ||
189 | updateResolution (resolutionId: number, delay = 0) { | 177 | updateResolution (resolutionId: number, delay = 0) { |
@@ -217,28 +205,17 @@ class PeerTubePlugin extends Plugin { | |||
217 | } | 205 | } |
218 | } | 206 | } |
219 | 207 | ||
220 | isAutoResolutionOn () { | ||
221 | return this.autoResolution | ||
222 | } | ||
223 | |||
224 | enableAutoResolution () { | 208 | enableAutoResolution () { |
225 | this.autoResolution = true | 209 | this.autoResolution = true |
226 | this.trigger('autoResolutionUpdate') | 210 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
227 | } | 211 | } |
228 | 212 | ||
229 | disableAutoResolution (forbid = false) { | 213 | disableAutoResolution (forbid = false) { |
230 | if (forbid === true) this.forbidAutoResolution = true | 214 | if (forbid === true) this.autoResolutionPossible = false |
231 | 215 | ||
232 | this.autoResolution = false | 216 | this.autoResolution = false |
233 | this.trigger('autoResolutionUpdate') | 217 | this.trigger('autoResolutionChange', { possible: this.autoResolutionPossible }) |
234 | } | 218 | this.trigger('resolutionChange', { auto: this.autoResolution, resolutionId: this.getCurrentResolutionId() }) |
235 | |||
236 | isAutoResolutionForbidden () { | ||
237 | return this.forbidAutoResolution === true | ||
238 | } | ||
239 | |||
240 | getCurrentVideoFile () { | ||
241 | return this.currentVideoFile | ||
242 | } | 219 | } |
243 | 220 | ||
244 | getTorrent () { | 221 | getTorrent () { |
@@ -248,18 +225,14 @@ class PeerTubePlugin extends Plugin { | |||
248 | private addTorrent ( | 225 | private addTorrent ( |
249 | magnetOrTorrentUrl: string, | 226 | magnetOrTorrentUrl: string, |
250 | previousVideoFile: VideoFile, | 227 | previousVideoFile: VideoFile, |
251 | options: { | 228 | options: PlayOptions, |
252 | forcePlay?: boolean, | ||
253 | seek?: number, | ||
254 | delay?: number | ||
255 | }, | ||
256 | done: Function | 229 | done: Function |
257 | ) { | 230 | ) { |
258 | console.log('Adding ' + magnetOrTorrentUrl + '.') | 231 | console.log('Adding ' + magnetOrTorrentUrl + '.') |
259 | 232 | ||
260 | const oldTorrent = this.torrent | 233 | const oldTorrent = this.torrent |
261 | const torrentOptions = { | 234 | const torrentOptions = { |
262 | store: (chunkLength, storeOpts) => new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { | 235 | store: (chunkLength: number, storeOpts: any) => new CacheChunkStore(new PeertubeChunkStore(chunkLength, storeOpts), { |
263 | max: 100 | 236 | max: 100 |
264 | }) | 237 | }) |
265 | } | 238 | } |
@@ -284,11 +257,14 @@ class PeerTubePlugin extends Plugin { | |||
284 | 257 | ||
285 | this.flushVideoFile(previousVideoFile) | 258 | this.flushVideoFile(previousVideoFile) |
286 | 259 | ||
260 | // Update progress bar (just for the UI), do not wait rendering | ||
261 | if (options.seek) this.player.currentTime(options.seek) | ||
262 | |||
287 | const renderVideoOptions = { autoplay: false, controls: true } | 263 | const renderVideoOptions = { autoplay: false, controls: true } |
288 | renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => { | 264 | renderVideo(torrent.files[ 0 ], this.playerElement, renderVideoOptions, (err, renderer) => { |
289 | this.renderer = renderer | 265 | this.renderer = renderer |
290 | 266 | ||
291 | if (err) return this.fallbackToHttp(done) | 267 | if (err) return this.fallbackToHttp(options, done) |
292 | 268 | ||
293 | return this.tryToPlay(err => { | 269 | return this.tryToPlay(err => { |
294 | if (err) return done(err) | 270 | if (err) return done(err) |
@@ -296,13 +272,13 @@ class PeerTubePlugin extends Plugin { | |||
296 | if (options.seek) this.seek(options.seek) | 272 | if (options.seek) this.seek(options.seek) |
297 | if (options.forcePlay === false && paused === true) this.player.pause() | 273 | if (options.forcePlay === false && paused === true) this.player.pause() |
298 | 274 | ||
299 | return done(err) | 275 | return done() |
300 | }) | 276 | }) |
301 | }) | 277 | }) |
302 | }, options.delay || 0) | 278 | }, options.delay || 0) |
303 | }) | 279 | }) |
304 | 280 | ||
305 | this.torrent.on('error', err => console.error(err)) | 281 | this.torrent.on('error', (err: any) => console.error(err)) |
306 | 282 | ||
307 | this.torrent.on('warning', (err: any) => { | 283 | this.torrent.on('warning', (err: any) => { |
308 | // We don't support HTTP tracker but we don't care -> we use the web socket tracker | 284 | // We don't support HTTP tracker but we don't care -> we use the web socket tracker |
@@ -330,13 +306,13 @@ class PeerTubePlugin extends Plugin { | |||
330 | }) | 306 | }) |
331 | } | 307 | } |
332 | 308 | ||
333 | private tryToPlay (done?: Function) { | 309 | private tryToPlay (done?: (err?: Error) => void) { |
334 | if (!done) done = function () { /* empty */ } | 310 | if (!done) done = function () { /* empty */ } |
335 | 311 | ||
336 | const playPromise = this.player.play() | 312 | const playPromise = this.player.play() |
337 | if (playPromise !== undefined) { | 313 | if (playPromise !== undefined) { |
338 | return playPromise.then(done) | 314 | return playPromise.then(done) |
339 | .catch(err => { | 315 | .catch((err: Error) => { |
340 | if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { | 316 | if (err.message.indexOf('The play() request was interrupted by a call to pause()') !== -1) { |
341 | return | 317 | return |
342 | } | 318 | } |
@@ -418,13 +394,7 @@ class PeerTubePlugin extends Plugin { | |||
418 | } | 394 | } |
419 | 395 | ||
420 | private initializePlayer () { | 396 | private initializePlayer () { |
421 | if (isMobile()) this.player.addClass('vjs-is-mobile') | 397 | this.buildQualities() |
422 | |||
423 | this.initSmoothProgressBar() | ||
424 | |||
425 | this.initCaptions() | ||
426 | |||
427 | this.alterInactivity() | ||
428 | 398 | ||
429 | if (this.autoplay === true) { | 399 | if (this.autoplay === true) { |
430 | this.player.posterImage.hide() | 400 | this.player.posterImage.hide() |
@@ -432,12 +402,6 @@ class PeerTubePlugin extends Plugin { | |||
432 | return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) | 402 | return this.updateVideoFile(undefined, { forcePlay: true, seek: this.startTime }) |
433 | } | 403 | } |
434 | 404 | ||
435 | // Don't try on iOS that does not support MediaSource | ||
436 | if (this.isIOS()) { | ||
437 | this.currentVideoFile = this.pickAverageVideoFile() | ||
438 | return this.fallbackToHttp(undefined, false) | ||
439 | } | ||
440 | |||
441 | // Proxy first play | 405 | // Proxy first play |
442 | const oldPlay = this.player.play.bind(this.player) | 406 | const oldPlay = this.player.play.bind(this.player) |
443 | this.player.play = () => { | 407 | this.player.play = () => { |
@@ -453,7 +417,7 @@ class PeerTubePlugin extends Plugin { | |||
453 | 417 | ||
454 | // Not initialized or in HTTP fallback | 418 | // Not initialized or in HTTP fallback |
455 | if (this.torrent === undefined || this.torrent === null) return | 419 | if (this.torrent === undefined || this.torrent === null) return |
456 | if (this.isAutoResolutionOn() === false) return | 420 | if (this.autoResolution === false) return |
457 | if (this.isAutoResolutionObservation === true) return | 421 | if (this.isAutoResolutionObservation === true) return |
458 | 422 | ||
459 | const file = this.getAppropriateFile() | 423 | const file = this.getAppropriateFile() |
@@ -493,81 +457,32 @@ class PeerTubePlugin extends Plugin { | |||
493 | if (this.torrent === undefined) return | 457 | if (this.torrent === undefined) return |
494 | 458 | ||
495 | // Http fallback | 459 | // Http fallback |
496 | if (this.torrent === null) return this.trigger('torrentInfo', false) | 460 | if (this.torrent === null) return this.player.trigger('p2pInfo', false) |
497 | 461 | ||
498 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too | 462 | // this.webtorrent.downloadSpeed because we need to take into account the potential old torrent too |
499 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) | 463 | if (this.webtorrent.downloadSpeed !== 0) this.downloadSpeeds.push(this.webtorrent.downloadSpeed) |
500 | 464 | ||
501 | return this.trigger('torrentInfo', { | 465 | return this.player.trigger('p2pInfo', { |
502 | downloadSpeed: this.torrent.downloadSpeed, | 466 | http: { |
503 | numPeers: this.torrent.numPeers, | 467 | downloadSpeed: 0, |
504 | uploadSpeed: this.torrent.uploadSpeed, | 468 | uploadSpeed: 0, |
505 | downloaded: this.torrent.downloaded, | 469 | downloaded: 0, |
506 | uploaded: this.torrent.uploaded | 470 | uploaded: 0 |
507 | }) | 471 | }, |
508 | }, this.CONSTANTS.INFO_SCHEDULER) | 472 | p2p: { |
509 | } | 473 | downloadSpeed: this.torrent.downloadSpeed, |
510 | 474 | numPeers: this.torrent.numPeers, | |
511 | private runViewAdd () { | 475 | uploadSpeed: this.torrent.uploadSpeed, |
512 | this.clearVideoViewInterval() | 476 | downloaded: this.torrent.downloaded, |
513 | 477 | uploaded: this.torrent.uploaded | |
514 | // After 30 seconds (or 3/4 of the video), add a view to the video | ||
515 | let minSecondsToView = 30 | ||
516 | |||
517 | if (this.videoDuration < minSecondsToView) minSecondsToView = (this.videoDuration * 3) / 4 | ||
518 | |||
519 | let secondsViewed = 0 | ||
520 | this.videoViewInterval = setInterval(() => { | ||
521 | if (this.player && !this.player.paused()) { | ||
522 | secondsViewed += 1 | ||
523 | |||
524 | if (secondsViewed > minSecondsToView) { | ||
525 | this.clearVideoViewInterval() | ||
526 | |||
527 | this.addViewToVideo().catch(err => console.error(err)) | ||
528 | } | 478 | } |
529 | } | 479 | } as PlayerNetworkInfo) |
530 | }, 1000) | 480 | }, this.CONSTANTS.INFO_SCHEDULER) |
531 | } | ||
532 | |||
533 | private runUserWatchVideo (options: UserWatching) { | ||
534 | let lastCurrentTime = 0 | ||
535 | |||
536 | this.userWatchingVideoInterval = setInterval(() => { | ||
537 | const currentTime = Math.floor(this.player.currentTime()) | ||
538 | |||
539 | if (currentTime - lastCurrentTime >= 1) { | ||
540 | lastCurrentTime = currentTime | ||
541 | |||
542 | this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) | ||
543 | .catch(err => console.error('Cannot notify user is watching.', err)) | ||
544 | } | ||
545 | }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) | ||
546 | } | ||
547 | |||
548 | private clearVideoViewInterval () { | ||
549 | if (this.videoViewInterval !== undefined) { | ||
550 | clearInterval(this.videoViewInterval) | ||
551 | this.videoViewInterval = undefined | ||
552 | } | ||
553 | } | ||
554 | |||
555 | private addViewToVideo () { | ||
556 | if (!this.videoViewUrl) return Promise.resolve(undefined) | ||
557 | |||
558 | return fetch(this.videoViewUrl, { method: 'POST' }) | ||
559 | } | 481 | } |
560 | 482 | ||
561 | private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) { | 483 | private fallbackToHttp (options: PlayOptions, done?: Function) { |
562 | const body = new URLSearchParams() | 484 | const paused = this.player.paused() |
563 | body.append('currentTime', currentTime.toString()) | ||
564 | |||
565 | const headers = new Headers({ 'Authorization': authorizationHeader }) | ||
566 | 485 | ||
567 | return fetch(url, { method: 'PUT', body, headers }) | ||
568 | } | ||
569 | |||
570 | private fallbackToHttp (done?: Function, play = true) { | ||
571 | this.disableAutoResolution(true) | 486 | this.disableAutoResolution(true) |
572 | 487 | ||
573 | this.flushVideoFile(this.currentVideoFile, true) | 488 | this.flushVideoFile(this.currentVideoFile, true) |
@@ -579,9 +494,20 @@ class PeerTubePlugin extends Plugin { | |||
579 | const httpUrl = this.currentVideoFile.fileUrl | 494 | const httpUrl = this.currentVideoFile.fileUrl |
580 | this.player.src = this.savePlayerSrcFunction | 495 | this.player.src = this.savePlayerSrcFunction |
581 | this.player.src(httpUrl) | 496 | this.player.src(httpUrl) |
582 | if (play) this.tryToPlay() | ||
583 | 497 | ||
584 | if (done) return done() | 498 | this.changeQuality() |
499 | |||
500 | // We changed the source, so reinit captions | ||
501 | this.player.trigger('sourcechange') | ||
502 | |||
503 | return this.tryToPlay(err => { | ||
504 | if (err && done) return done(err) | ||
505 | |||
506 | if (options.seek) this.seek(options.seek) | ||
507 | if (options.forcePlay === false && paused === true) this.player.pause() | ||
508 | |||
509 | if (done) return done() | ||
510 | }) | ||
585 | } | 511 | } |
586 | 512 | ||
587 | private handleError (err: Error | string) { | 513 | private handleError (err: Error | string) { |
@@ -600,25 +526,6 @@ class PeerTubePlugin extends Plugin { | |||
600 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) | 526 | return !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform) |
601 | } | 527 | } |
602 | 528 | ||
603 | private alterInactivity () { | ||
604 | let saveInactivityTimeout: number | ||
605 | |||
606 | const disableInactivity = () => { | ||
607 | saveInactivityTimeout = this.player.options_.inactivityTimeout | ||
608 | this.player.options_.inactivityTimeout = 0 | ||
609 | } | ||
610 | const enableInactivity = () => { | ||
611 | this.player.options_.inactivityTimeout = saveInactivityTimeout | ||
612 | } | ||
613 | |||
614 | const settingsDialog = this.player.children_.find(c => c.name_ === 'SettingsDialog') | ||
615 | |||
616 | this.player.controlBar.on('mouseenter', () => disableInactivity()) | ||
617 | settingsDialog.on('mouseenter', () => disableInactivity()) | ||
618 | this.player.controlBar.on('mouseleave', () => enableInactivity()) | ||
619 | settingsDialog.on('mouseleave', () => enableInactivity()) | ||
620 | } | ||
621 | |||
622 | private pickAverageVideoFile () { | 529 | private pickAverageVideoFile () { |
623 | if (this.videoFiles.length === 1) return this.videoFiles[0] | 530 | if (this.videoFiles.length === 1) return this.videoFiles[0] |
624 | 531 | ||
@@ -632,14 +539,14 @@ class PeerTubePlugin extends Plugin { | |||
632 | } | 539 | } |
633 | 540 | ||
634 | private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) { | 541 | private renderFileInFakeElement (file: WebTorrent.TorrentFile, delay: number) { |
635 | this.destoyingFakeRenderer = false | 542 | this.destroyingFakeRenderer = false |
636 | 543 | ||
637 | const fakeVideoElem = document.createElement('video') | 544 | const fakeVideoElem = document.createElement('video') |
638 | renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => { | 545 | renderVideo(file, fakeVideoElem, { autoplay: false, controls: false }, (err, renderer) => { |
639 | this.fakeRenderer = renderer | 546 | this.fakeRenderer = renderer |
640 | 547 | ||
641 | // The renderer returns an error when we destroy it, so skip them | 548 | // The renderer returns an error when we destroy it, so skip them |
642 | if (this.destoyingFakeRenderer === false && err) { | 549 | if (this.destroyingFakeRenderer === false && err) { |
643 | console.error('Cannot render new torrent in fake video element.', err) | 550 | console.error('Cannot render new torrent in fake video element.', err) |
644 | } | 551 | } |
645 | 552 | ||
@@ -650,7 +557,7 @@ class PeerTubePlugin extends Plugin { | |||
650 | 557 | ||
651 | private destroyFakeRenderer () { | 558 | private destroyFakeRenderer () { |
652 | if (this.fakeRenderer) { | 559 | if (this.fakeRenderer) { |
653 | this.destoyingFakeRenderer = true | 560 | this.destroyingFakeRenderer = true |
654 | 561 | ||
655 | if (this.fakeRenderer.destroy) { | 562 | if (this.fakeRenderer.destroy) { |
656 | try { | 563 | try { |
@@ -663,40 +570,70 @@ class PeerTubePlugin extends Plugin { | |||
663 | } | 570 | } |
664 | } | 571 | } |
665 | 572 | ||
666 | private initCaptions () { | 573 | private buildQualities () { |
667 | for (const caption of this.videoCaptions) { | 574 | const qualityLevelsPayload = [] |
668 | this.player.addRemoteTextTrack({ | 575 | |
669 | kind: 'captions', | 576 | for (const file of this.videoFiles) { |
670 | label: caption.label, | 577 | const representation = { |
671 | language: caption.language, | 578 | id: file.resolution.id, |
672 | id: caption.language, | 579 | label: this.buildQualityLabel(file), |
673 | src: caption.src | 580 | height: file.resolution.id, |
674 | }, false) | 581 | _enabled: true |
582 | } | ||
583 | |||
584 | this.player.qualityLevels().addQualityLevel(representation) | ||
585 | |||
586 | qualityLevelsPayload.push({ | ||
587 | id: representation.id, | ||
588 | label: representation.label, | ||
589 | selected: false | ||
590 | }) | ||
591 | } | ||
592 | |||
593 | const payload: LoadedQualityData = { | ||
594 | qualitySwitchCallback: (d: any) => this.qualitySwitchCallback(d), | ||
595 | qualityData: { | ||
596 | video: qualityLevelsPayload | ||
597 | } | ||
675 | } | 598 | } |
599 | this.player.tech_.trigger('loadedqualitydata', payload) | ||
676 | } | 600 | } |
677 | 601 | ||
678 | // Thanks: https://github.com/videojs/video.js/issues/4460#issuecomment-312861657 | 602 | private buildQualityLabel (file: VideoFile) { |
679 | private initSmoothProgressBar () { | 603 | let label = file.resolution.label |
680 | const SeekBar = videojsUntyped.getComponent('SeekBar') | 604 | |
681 | SeekBar.prototype.getPercent = function getPercent () { | 605 | if (file.fps && file.fps >= 50) { |
682 | // Allows for smooth scrubbing, when player can't keep up. | 606 | label += file.fps |
683 | // const time = (this.player_.scrubbing()) ? | ||
684 | // this.player_.getCache().currentTime : | ||
685 | // this.player_.currentTime() | ||
686 | const time = this.player_.currentTime() | ||
687 | const percent = time / this.player_.duration() | ||
688 | return percent >= 1 ? 1 : percent | ||
689 | } | 607 | } |
690 | SeekBar.prototype.handleMouseMove = function handleMouseMove (event) { | 608 | |
691 | let newTime = this.calculateDistance(event) * this.player_.duration() | 609 | return label |
692 | if (newTime === this.player_.duration()) { | 610 | } |
693 | newTime = newTime - 0.1 | 611 | |
694 | } | 612 | private qualitySwitchCallback (id: number) { |
695 | this.player_.currentTime(newTime) | 613 | if (id === -1) { |
696 | this.update() | 614 | if (this.autoResolutionPossible === true) this.enableAutoResolution() |
615 | return | ||
616 | } | ||
617 | |||
618 | this.disableAutoResolution() | ||
619 | this.updateResolution(id) | ||
620 | } | ||
621 | |||
622 | private changeQuality () { | ||
623 | const resolutionId = this.currentVideoFile.resolution.id | ||
624 | const qualityLevels = this.player.qualityLevels() | ||
625 | |||
626 | if (resolutionId === -1) { | ||
627 | qualityLevels.selectedIndex = -1 | ||
628 | return | ||
629 | } | ||
630 | |||
631 | for (let i = 0; i < qualityLevels; i++) { | ||
632 | const q = this.player.qualityLevels[i] | ||
633 | if (q.height === resolutionId) qualityLevels.selectedIndex = i | ||
697 | } | 634 | } |
698 | } | 635 | } |
699 | } | 636 | } |
700 | 637 | ||
701 | videojs.registerPlugin('peertube', PeerTubePlugin) | 638 | videojs.registerPlugin('webtorrent', WebTorrentPlugin) |
702 | export { PeerTubePlugin } | 639 | export { WebTorrentPlugin } |