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