diff --git a/.eslintrc.json b/.eslintrc.json
index 9e1dbaa..f85ea66 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -27,7 +27,8 @@
}
},
"globals": {
- "YT": "readonly"
+ "YT": "readonly",
+ "ICE_SERVERS": "readonly"
},
"rules": {
"indent": [
@@ -53,12 +54,15 @@
"error",
"always"
],
+ "no-unused-vars": [
+ "warn"
+ ],
"no-console": [
- "error",
+ "warn",
{ "allow": ["info", "warn", "error"] }
],
"no-inline-comments": [
- "error"
+ "warn"
],
"spaced-comment": [
"error",
diff --git a/Makefile b/Makefile
index 4c4c6ba..3d1470b 100644
--- a/Makefile
+++ b/Makefile
@@ -18,10 +18,10 @@ lint:
npx eslint src/* --ext .js,.json --fix
start-client:
- php -S 0.0.0.0:8000 -t build
+ php -S 0.0.0.0:8020 -t build
start-server:
- node bin/server.js 8001
+ node bin/server.js 8021
# Publish package
publish: build
diff --git a/assets/css/components/buttons.scss b/assets/css/components/buttons.scss
index e6987ba..998bbe1 100644
--- a/assets/css/components/buttons.scss
+++ b/assets/css/components/buttons.scss
@@ -37,4 +37,8 @@
&[disabled] .icon-loader {
color: $color-primary;
}
+
+ &.active, &.active:hover {
+ color: $color-primary;
+ }
}
diff --git a/assets/css/components/user-list.scss b/assets/css/components/user-list.scss
index 0755995..8296d85 100644
--- a/assets/css/components/user-list.scss
+++ b/assets/css/components/user-list.scss
@@ -19,9 +19,17 @@
border-radius: 100%;
sup {
- font-size: 0.6em;
+ font-size: 0.5em;
position: absolute;
- top: 0;
+ top: -0.2em;
+ right: 0;
+ text-shadow: 0px 0px 10px rgba(0, 0, 0, 0.66);
+ }
+
+ sub {
+ font-size: 0.4em;
+ position: absolute;
+ bottom: -0.2em;
right: 0;
text-shadow: 0px 0px 10px rgba(0, 0, 0, 0.66);
}
@@ -34,6 +42,10 @@
color: green;
}
+ &.streaming sub {
+ color: $color-primary;
+ }
+
&.loading sup {
color: red;
}
diff --git a/assets/css/core/icons.scss b/assets/css/core/icons.scss
index da78951..7e1e5f0 100644
--- a/assets/css/core/icons.scss
+++ b/assets/css/core/icons.scss
@@ -22,6 +22,7 @@ $icons: (
"volume-low": "\e911",
"volume-medium": "\e912",
"volume-none": "\e910",
+ "stream": "\e90f",
);
@font-face {
diff --git a/assets/fonts/icons.svg b/assets/fonts/icons.svg
index bb065e0..84027b1 100644
--- a/assets/fonts/icons.svg
+++ b/assets/fonts/icons.svg
@@ -22,6 +22,7 @@
+
diff --git a/assets/fonts/icons.ttf b/assets/fonts/icons.ttf
index 4407077..f81f360 100644
Binary files a/assets/fonts/icons.ttf and b/assets/fonts/icons.ttf differ
diff --git a/assets/fonts/icons.woff b/assets/fonts/icons.woff
index 5f88f37..05cc7ce 100644
Binary files a/assets/fonts/icons.woff and b/assets/fonts/icons.woff differ
diff --git a/assets/fonts/selection.json b/assets/fonts/selection.json
index 8bf4b3c..50895a1 100644
--- a/assets/fonts/selection.json
+++ b/assets/fonts/selection.json
@@ -1 +1 @@
-{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M448 304c0-26.4 21.6-48 48-48h32c26.4 0 48 21.6 48 48v32c0 26.4-21.6 48-48 48h-32c-26.4 0-48-21.6-48-48v-32z","M640 768h-256v-64h64v-192h-64v-64h192v256h64z","M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z"],"attrs":[{},{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["info","information"],"grid":24},"attrs":[{},{},{}],"properties":{"order":1,"id":0,"prevSize":32,"code":59668,"name":"info"},"setIdx":0,"setId":4,"iconIdx":0},{"icon":{"paths":["M469.333 85.333v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 768v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM180.181 240.512l120.747 120.747c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-120.747-120.747c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM662.741 723.072l120.747 120.747c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-120.747-120.747c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM85.333 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM768 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM240.512 843.819l120.747-120.747c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-120.747 120.747c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM723.072 361.259l120.747-120.747c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-120.747 120.747c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["loader"],"grid":24},"attrs":[{}],"properties":{"order":1,"id":0,"name":"loader","prevSize":32,"code":59662},"setIdx":3,"setId":1,"iconIdx":0},{"icon":{"paths":["M960 619.148v84.852h-84.852l-107.148-107.148-107.148 107.148h-84.852v-84.852l107.148-107.148-107.148-107.148v-84.852h84.852l107.148 107.148 107.148-107.148h84.852v84.852l-107.148 107.148 107.148 107.148z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["volume-mute","volume","audio","player"],"grid":24},"attrs":[{},{}],"properties":{"order":1,"id":3,"prevSize":32,"code":59664,"name":"volume-none"},"setIdx":3,"setId":1,"iconIdx":1},{"icon":{"paths":["M549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["volume-low","volume","audio","speaker","player"],"grid":24},"attrs":[{},{}],"properties":{"order":2,"id":2,"prevSize":32,"code":59665,"name":"volume-low"},"setIdx":3,"setId":1,"iconIdx":2},{"icon":{"paths":["M719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["volume-medium","volume","audio","speaker","player"],"grid":24},"attrs":[{},{}],"properties":{"order":3,"id":1,"prevSize":32,"code":59666,"name":"volume-medium"},"setIdx":3,"setId":1,"iconIdx":3},{"icon":{"paths":["M890.040 922.040c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.746-18.744-49.136 0-67.882 87.638-87.642 135.904-204.16 135.904-328.1 0-123.938-48.266-240.458-135.904-328.098-18.744-18.746-18.744-49.138 0-67.882s49.138-18.744 67.882 0c105.77 105.772 164.022 246.4 164.022 395.98s-58.252 290.208-164.022 395.98c-9.372 9.372-21.656 14.058-33.94 14.058zM719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.568-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"width":1088,"isMulticolor":false,"isMulticolor2":false,"tags":["volume-high","volume","audio","speaker","player"],"grid":24},"attrs":[{},{}],"properties":{"order":4,"id":0,"prevSize":32,"code":59667,"name":"volume-high"},"setIdx":3,"setId":1,"iconIdx":4},{"icon":{"paths":["M742.997 281.003c-33.28-33.323-87.381-33.323-120.661 0l-110.336 110.336-110.336-110.336c-33.28-33.323-87.381-33.323-120.661 0-33.323 33.323-33.323 87.339 0 120.661l110.293 110.336-110.293 110.336c-33.323 33.323-33.323 87.339 0 120.661 16.64 16.683 38.485 25.003 60.331 25.003s43.691-8.32 60.331-25.003l110.336-110.336 110.336 110.336c16.64 16.683 38.485 25.003 60.331 25.003s43.691-8.32 60.331-25.003c33.323-33.323 33.323-87.339 0-120.661l-110.293-110.336 110.293-110.336c33.323-33.323 33.323-87.339 0-120.661z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["times"],"grid":24},"attrs":[],"properties":{"id":1,"order":39,"prevSize":32,"code":59648,"name":"cross"},"setIdx":3,"setId":1,"iconIdx":5},{"icon":{"paths":["M724.139 266.709c-41.259-22.955-93.227-8.021-116.053 33.152l-158.421 285.099-90.667-90.667c-33.323-33.323-87.339-33.323-120.661 0s-33.323 87.339 0 120.661l170.667 170.667c16.128 16.171 37.888 25.045 60.331 25.045 3.925 0 7.893-0.256 11.819-0.853 26.496-3.712 49.749-19.627 62.763-43.051l213.333-384c22.912-41.216 8.064-93.141-33.109-116.053z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["tick"],"grid":24},"attrs":[],"properties":{"id":2,"order":35,"prevSize":32,"code":59649,"name":"check"},"setIdx":3,"setId":1,"iconIdx":6},{"icon":{"paths":["M785.664 454.656c-33.323-33.323-87.339-33.323-120.661 0l-67.669 67.669v-308.992c0-47.147-38.229-85.333-85.333-85.333-47.147 0-85.333 38.187-85.333 85.333v308.992l-67.669-67.669c-33.323-33.323-87.339-33.323-120.661 0s-33.323 87.339 0 120.661l273.664 273.664 273.664-273.664c33.323-33.323 33.323-87.296 0-120.661z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-down-thick"],"grid":24},"attrs":[],"properties":{"id":23,"order":42,"prevSize":32,"code":59650,"name":"arrow-down"},"setIdx":3,"setId":1,"iconIdx":7},{"icon":{"paths":["M725.333 384c0-58.923-23.893-112.256-62.464-150.827-38.613-38.613-91.947-62.507-150.869-62.507s-112.256 23.893-150.869 62.507c-38.571 38.571-62.464 91.904-62.464 150.827s23.893 112.256 62.464 150.827c38.613 38.613 91.947 62.507 150.869 62.507s112.256-23.893 150.869-62.507c38.571-38.571 62.464-91.904 62.464-150.827z","M256 810.667c0 42.667 96 85.333 256 85.333 150.101 0 256-42.667 256-85.333 0-85.333-100.437-170.667-256-170.667-160 0-256 85.333-256 170.667z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["user"],"grid":24},"attrs":[],"properties":{"id":40,"order":26,"prevSize":32,"code":59651,"name":"user"},"setIdx":3,"setId":1,"iconIdx":8},{"icon":{"paths":["M384 597.333c58.923 0 112.256-23.893 150.869-62.507 38.571-38.571 62.464-91.904 62.464-150.827s-23.893-112.256-62.464-150.827c-38.613-38.613-91.947-62.507-150.869-62.507s-112.256 23.893-150.869 62.507c-38.571 38.571-62.464 91.904-62.464 150.827s23.893 112.256 62.464 150.827c38.613 38.613 91.947 62.507 150.869 62.507z","M384 896c150.101 0 256-42.667 256-85.333 0-85.333-100.437-170.667-256-170.667-160 0-256 85.333-256 170.667 0 42.667 96 85.333 256 85.333z","M896 512h-85.333v-85.333c0-23.595-19.072-42.667-42.667-42.667s-42.667 19.072-42.667 42.667v85.333h-85.333c-23.595 0-42.667 19.072-42.667 42.667s19.072 42.667 42.667 42.667h85.333v85.333c0 23.595 19.072 42.667 42.667 42.667s42.667-19.072 42.667-42.667v-85.333h85.333c23.595 0 42.667-19.072 42.667-42.667s-19.072-42.667-42.667-42.667z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["user-add"],"grid":24},"attrs":[],"properties":{"id":47,"order":37,"prevSize":32,"code":59652,"name":"user-add"},"setIdx":3,"setId":1,"iconIdx":9},{"icon":{"paths":["M341.333 256c-47.104 0-85.333 38.229-85.333 85.333v341.333c0 47.104 38.229 85.333 85.333 85.333s85.333-38.229 85.333-85.333v-341.333c0-47.104-38.229-85.333-85.333-85.333z","M640 256c-47.104 0-85.333 38.229-85.333 85.333v341.333c0 47.104 38.229 85.333 85.333 85.333s85.333-38.229 85.333-85.333v-341.333c0-47.104-38.229-85.333-85.333-85.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-pause"],"grid":24},"attrs":[],"properties":{"id":48,"order":36,"prevSize":32,"code":59653,"name":"pause"},"setIdx":3,"setId":1,"iconIdx":10},{"icon":{"paths":["M400.512 748.715l15.829 63.232c5.675 22.741 29.525 41.387 52.992 41.387h42.667c23.467 0 47.317-18.645 52.992-41.387l15.829-63.232c5.675-22.741 28.8-36.096 51.328-29.611l62.592 17.92c22.571 6.443 50.688-4.864 62.379-25.216l21.333-36.992c11.691-20.352 7.552-50.304-9.344-66.645l-46.848-45.269c-16.896-16.341-16.896-43.008 0.043-59.307l46.763-45.269c16.896-16.299 21.077-46.251 9.387-66.603l-21.376-36.992c-11.733-20.352-39.808-31.659-62.336-25.216l-62.592 17.92c-22.571 6.443-45.653-6.869-51.371-29.611l-15.787-63.147c-5.675-22.699-29.525-41.344-52.992-41.344h-42.667c-23.467 0-47.317 18.645-52.992 41.387l-15.787 63.147c-5.717 22.741-28.8 36.096-51.371 29.653l-62.592-17.92c-22.571-6.485-50.688 4.864-62.379 25.173l-21.333 36.992c-11.691 20.352-7.552 50.304 9.387 66.645l46.763 45.184c16.853 16.341 16.853 43.008 0 59.349l-46.848 45.269c-16.853 16.341-21.077 46.293-9.344 66.645l21.376 36.992c11.691 20.352 39.808 31.659 62.379 25.216l62.592-17.92c22.528-6.528 45.653 6.827 51.328 29.568zM490.667 448c47.104 0 85.333 38.187 85.333 85.333 0 47.104-38.229 85.333-85.333 85.333s-85.333-38.229-85.333-85.333c0-47.147 38.229-85.333 85.333-85.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["cog"],"grid":24},"attrs":[],"properties":{"id":59,"order":38,"prevSize":32,"code":59654,"name":"cog"},"setIdx":3,"setId":1,"iconIdx":11},{"icon":{"paths":["M261.163 554.667c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h62.507l-140.501 140.501c-16.683 16.683-16.683 43.648 0 60.331 8.32 8.32 19.243 12.501 30.165 12.501s21.845-4.181 30.165-12.501l145.664-145.664v72.832c0 23.552 19.072 42.667 42.667 42.667s37.504-19.115 37.504-42.667v-213.333h-208.171z","M298.667 469.333c23.552 0 42.667-19.115 42.667-42.667v-85.333h85.333c23.595 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-170.624l-0.043 170.667c0 23.552 19.072 42.667 42.667 42.667z","M725.333 554.667c-23.595 0-42.667 19.115-42.667 42.667v85.333h-85.333c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h170.667v-170.667c0-23.552-19.072-42.667-42.667-42.667z","M780.501 183.168l-140.501 140.501v-67.669c0-23.552-19.072-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v213.333h213.333c23.552 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-67.669l140.501-140.459c16.683-16.683 16.683-43.648 0-60.331s-43.648-16.725-60.331-0.043z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-minimise"],"grid":24},"attrs":[],"properties":{"id":82,"order":28,"prevSize":32,"code":59655,"name":"minimise"},"setIdx":3,"setId":1,"iconIdx":12},{"icon":{"paths":["M640 170.667c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h67.669l-140.501 140.501c-16.683 16.683-16.683 43.648 0 60.331 8.32 8.32 19.243 12.501 30.165 12.501s21.845-4.181 30.165-12.501l140.501-140.501v67.669c0 23.552 19.072 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-213.333h-213.333z","M396.501 567.168l-140.501 140.501v-67.669c0-23.552-19.072-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v213.291h42.496c0.341 0 170.837 0.043 170.837 0.043 23.552 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-67.669l140.501-140.459c16.683-16.683 16.683-43.648 0-60.331s-43.648-16.725-60.331-0.043z","M298.667 512c23.552 0 42.667-19.115 42.667-42.667v-128h128c23.595 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-213.291l-0.043 213.333c0 23.552 19.072 42.667 42.667 42.667z","M725.333 512c-23.595 0-42.667 19.115-42.667 42.667v128h-128c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h213.333v-213.333c0-23.552-19.072-42.667-42.667-42.667z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-maximise"],"grid":24},"attrs":[],"properties":{"id":83,"order":27,"prevSize":32,"code":59656,"name":"maximise"},"setIdx":3,"setId":1,"iconIdx":13},{"icon":{"paths":["M435.2 273.067c-20.821 0-39.723 8.405-53.461 21.845-101.589 98.773-253.739 246.997-253.739 246.997s152.149 148.181 253.611 246.997c13.909 13.44 32.768 21.76 53.589 21.76 42.411 0 76.8-34.347 76.8-76.757v-192-192c0-42.411-34.389-76.843-76.8-76.843z","M819.2 273.067c-20.821 0-39.723 8.405-53.461 21.845-101.589 98.773-253.739 246.997-253.739 246.997s152.149 148.181 253.611 246.997c13.909 13.44 32.768 21.76 53.589 21.76 42.411 0 76.8-34.347 76.8-76.757v-384c0-42.411-34.389-76.843-76.8-76.843z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-rewind"],"grid":24},"attrs":[],"properties":{"id":93,"order":34,"prevSize":32,"code":59657,"name":"backward"},"setIdx":3,"setId":1,"iconIdx":14},{"icon":{"paths":["M682.667 256h-341.333c-46.933 0-85.333 38.4-85.333 85.333v341.333c0 46.933 38.4 85.333 85.333 85.333h341.333c46.933 0 85.333-38.4 85.333-85.333v-341.333c0-46.933-38.4-85.333-85.333-85.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-stop"],"grid":24},"attrs":[],"properties":{"id":100,"order":29,"prevSize":32,"code":59658,"name":"stop"},"setIdx":3,"setId":1,"iconIdx":15},{"icon":{"paths":["M443.563 786.475c112.683-109.824 281.771-274.475 281.771-274.475s-169.088-164.651-281.771-274.475c-15.488-14.891-36.395-24.192-59.563-24.192-47.104 0-85.333 38.229-85.333 85.333v426.667c0 47.104 38.229 85.333 85.333 85.333 23.168 0 44.075-9.301 59.563-24.192z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-play"],"grid":24},"attrs":[],"properties":{"id":102,"order":31,"prevSize":32,"code":59659,"name":"play"},"setIdx":3,"setId":1,"iconIdx":16},{"icon":{"paths":["M642.261 294.912c-13.824-13.397-32.64-21.845-53.461-21.845-42.411 0-76.8 34.432-76.8 76.843v192 192c0 42.411 34.389 76.757 76.8 76.757 20.821 0 39.68-8.32 53.461-21.845 101.589-98.731 253.739-246.912 253.739-246.912s-152.149-148.224-253.739-246.997z","M258.261 294.912c-13.824-13.397-32.64-21.845-53.461-21.845-42.411 0-76.8 34.432-76.8 76.843v384c0 42.411 34.389 76.757 76.8 76.757 20.821 0 39.68-8.32 53.461-21.845 101.589-98.731 253.739-246.912 253.739-246.912s-152.149-148.224-253.739-246.997z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-fast-forward"],"grid":24},"attrs":[],"properties":{"id":103,"order":32,"prevSize":32,"code":59660,"name":"forward"},"setIdx":3,"setId":1,"iconIdx":17},{"icon":{"paths":["M725.333 682.667h-426.667c-47.104 0-85.333 38.187-85.333 85.333 0 47.104 38.229 85.333 85.333 85.333h426.667c47.104 0 85.333-38.229 85.333-85.333 0-47.147-38.229-85.333-85.333-85.333z","M786.475 452.437c-109.824-112.683-274.475-281.771-274.475-281.771s-164.651 169.088-274.475 281.771c-14.891 15.488-24.192 36.395-24.192 59.563 0 47.104 38.229 85.333 85.333 85.333h426.667c47.104 0 85.333-38.229 85.333-85.333 0-23.168-9.301-44.075-24.192-59.563z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-eject"],"grid":24},"attrs":[],"properties":{"id":104,"order":33,"prevSize":32,"code":59661,"name":"eject"},"setIdx":3,"setId":1,"iconIdx":18}],"height":1024,"metadata":{"name":"icons"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icons","majorVersion":1,"minorVersion":0},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false,"noie8":true,"ie7":false,"cssVars":true,"cssVarsFormat":"scss","showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215},"historySize":50,"gridSize":16,"showGrid":false}}
\ No newline at end of file
+{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M256.085 682.624c-47.232 0-85.504 38.272-85.419 85.376 0 47.104 38.229 85.376 85.419 85.291 47.061 0.085 85.333-38.144 85.248-85.291 0.085-47.232-38.187-85.461-85.248-85.376z","M256 170.667c-47.104 0-85.333 38.229-85.333 85.333s38.229 85.333 85.333 85.333c235.264 0 426.667 191.403 426.667 426.667 0 47.104 38.229 85.333 85.333 85.333s85.333-38.229 85.333-85.333c0-329.387-267.947-597.333-597.333-597.333z","M256 426.667c-47.104 0-85.333 38.229-85.333 85.333s38.229 85.333 85.333 85.333c94.080 0 170.667 76.544 170.667 170.667 0 47.104 38.229 85.333 85.333 85.333s85.333-38.229 85.333-85.333c0-188.203-153.131-341.333-341.333-341.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["rss"],"grid":24},"attrs":[],"properties":{"id":18,"order":46,"prevSize":24,"code":59663,"name":"stream"},"setIdx":0,"setId":4,"iconIdx":17},{"icon":{"paths":["M448 304c0-26.4 21.6-48 48-48h32c26.4 0 48 21.6 48 48v32c0 26.4-21.6 48-48 48h-32c-26.4 0-48-21.6-48-48v-32z","M640 768h-256v-64h64v-192h-64v-64h192v256h64z","M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 928c-229.75 0-416-186.25-416-416s186.25-416 416-416 416 186.25 416 416-186.25 416-416 416z"],"attrs":[{},{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["info","information"],"grid":24},"attrs":[{},{},{}],"properties":{"order":1,"id":0,"prevSize":24,"code":59668,"name":"info"},"setIdx":3,"setId":1,"iconIdx":0},{"icon":{"paths":["M469.333 85.333v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM469.333 768v170.667c0 23.552 19.115 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-170.667c0-23.552-19.115-42.667-42.667-42.667s-42.667 19.115-42.667 42.667zM180.181 240.512l120.747 120.747c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-120.747-120.747c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM662.741 723.072l120.747 120.747c16.683 16.683 43.691 16.683 60.331 0s16.683-43.691 0-60.331l-120.747-120.747c-16.683-16.683-43.691-16.683-60.331 0s-16.683 43.691 0 60.331zM85.333 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM768 554.667h170.667c23.552 0 42.667-19.115 42.667-42.667s-19.115-42.667-42.667-42.667h-170.667c-23.552 0-42.667 19.115-42.667 42.667s19.115 42.667 42.667 42.667zM240.512 843.819l120.747-120.747c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-120.747 120.747c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0zM723.072 361.259l120.747-120.747c16.683-16.683 16.683-43.691 0-60.331s-43.691-16.683-60.331 0l-120.747 120.747c-16.683 16.683-16.683 43.691 0 60.331s43.691 16.683 60.331 0z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["loader"],"grid":24},"attrs":[{}],"properties":{"order":1,"id":0,"name":"loader","prevSize":24,"code":59662},"setIdx":3,"setId":1,"iconIdx":1},{"icon":{"paths":["M960 619.148v84.852h-84.852l-107.148-107.148-107.148 107.148h-84.852v-84.852l107.148-107.148-107.148-107.148v-84.852h84.852l107.148 107.148 107.148-107.148h84.852v84.852l-107.148 107.148 107.148 107.148z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["volume-mute","volume","audio","player"],"grid":24},"attrs":[{},{}],"properties":{"order":1,"id":3,"prevSize":24,"code":59664,"name":"volume-none"},"setIdx":3,"setId":1,"iconIdx":2},{"icon":{"paths":["M549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["volume-low","volume","audio","speaker","player"],"grid":24},"attrs":[{},{}],"properties":{"order":2,"id":2,"prevSize":24,"code":59665,"name":"volume-low"},"setIdx":3,"setId":1,"iconIdx":3},{"icon":{"paths":["M719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.566-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["volume-medium","volume","audio","speaker","player"],"grid":24},"attrs":[{},{}],"properties":{"order":3,"id":1,"prevSize":24,"code":59666,"name":"volume-medium"},"setIdx":3,"setId":1,"iconIdx":4},{"icon":{"paths":["M890.040 922.040c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.746-18.744-49.136 0-67.882 87.638-87.642 135.904-204.16 135.904-328.1 0-123.938-48.266-240.458-135.904-328.098-18.744-18.746-18.744-49.138 0-67.882s49.138-18.744 67.882 0c105.77 105.772 164.022 246.4 164.022 395.98s-58.252 290.208-164.022 395.98c-9.372 9.372-21.656 14.058-33.94 14.058zM719.53 831.53c-12.286 0-24.566-4.686-33.942-14.056-18.744-18.744-18.744-49.136 0-67.882 131.006-131.006 131.006-344.17 0-475.176-18.744-18.746-18.744-49.138 0-67.882 18.744-18.742 49.138-18.744 67.882 0 81.594 81.59 126.53 190.074 126.53 305.466 0 115.39-44.936 223.876-126.53 305.47-9.372 9.374-21.656 14.060-33.94 14.060v0zM549.020 741.020c-12.286 0-24.568-4.686-33.942-14.058-18.746-18.746-18.746-49.134 0-67.88 81.1-81.1 81.1-213.058 0-294.156-18.746-18.746-18.746-49.138 0-67.882s49.136-18.744 67.882 0c118.53 118.53 118.53 311.392 0 429.922-9.372 9.368-21.656 14.054-33.94 14.054z","M416.006 960c-8.328 0-16.512-3.25-22.634-9.374l-246.626-246.626h-114.746c-17.672 0-32-14.326-32-32v-320c0-17.672 14.328-32 32-32h114.746l246.626-246.628c9.154-9.154 22.916-11.89 34.874-6.936 11.958 4.952 19.754 16.622 19.754 29.564v832c0 12.944-7.796 24.612-19.754 29.564-3.958 1.64-8.118 2.436-12.24 2.436z"],"attrs":[{},{}],"width":1088,"isMulticolor":false,"isMulticolor2":false,"tags":["volume-high","volume","audio","speaker","player"],"grid":24},"attrs":[{},{}],"properties":{"order":4,"id":0,"prevSize":24,"code":59667,"name":"volume-high"},"setIdx":3,"setId":1,"iconIdx":5},{"icon":{"paths":["M742.997 281.003c-33.28-33.323-87.381-33.323-120.661 0l-110.336 110.336-110.336-110.336c-33.28-33.323-87.381-33.323-120.661 0-33.323 33.323-33.323 87.339 0 120.661l110.293 110.336-110.293 110.336c-33.323 33.323-33.323 87.339 0 120.661 16.64 16.683 38.485 25.003 60.331 25.003s43.691-8.32 60.331-25.003l110.336-110.336 110.336 110.336c16.64 16.683 38.485 25.003 60.331 25.003s43.691-8.32 60.331-25.003c33.323-33.323 33.323-87.339 0-120.661l-110.293-110.336 110.293-110.336c33.323-33.323 33.323-87.339 0-120.661z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["times"],"grid":24},"attrs":[],"properties":{"id":1,"order":39,"prevSize":24,"code":59648,"name":"cross"},"setIdx":3,"setId":1,"iconIdx":6},{"icon":{"paths":["M724.139 266.709c-41.259-22.955-93.227-8.021-116.053 33.152l-158.421 285.099-90.667-90.667c-33.323-33.323-87.339-33.323-120.661 0s-33.323 87.339 0 120.661l170.667 170.667c16.128 16.171 37.888 25.045 60.331 25.045 3.925 0 7.893-0.256 11.819-0.853 26.496-3.712 49.749-19.627 62.763-43.051l213.333-384c22.912-41.216 8.064-93.141-33.109-116.053z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["tick"],"grid":24},"attrs":[],"properties":{"id":2,"order":35,"prevSize":24,"code":59649,"name":"check"},"setIdx":3,"setId":1,"iconIdx":7},{"icon":{"paths":["M785.664 454.656c-33.323-33.323-87.339-33.323-120.661 0l-67.669 67.669v-308.992c0-47.147-38.229-85.333-85.333-85.333-47.147 0-85.333 38.187-85.333 85.333v308.992l-67.669-67.669c-33.323-33.323-87.339-33.323-120.661 0s-33.323 87.339 0 120.661l273.664 273.664 273.664-273.664c33.323-33.323 33.323-87.296 0-120.661z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-down-thick"],"grid":24},"attrs":[],"properties":{"id":23,"order":42,"prevSize":24,"code":59650,"name":"arrow-down"},"setIdx":3,"setId":1,"iconIdx":8},{"icon":{"paths":["M725.333 384c0-58.923-23.893-112.256-62.464-150.827-38.613-38.613-91.947-62.507-150.869-62.507s-112.256 23.893-150.869 62.507c-38.571 38.571-62.464 91.904-62.464 150.827s23.893 112.256 62.464 150.827c38.613 38.613 91.947 62.507 150.869 62.507s112.256-23.893 150.869-62.507c38.571-38.571 62.464-91.904 62.464-150.827z","M256 810.667c0 42.667 96 85.333 256 85.333 150.101 0 256-42.667 256-85.333 0-85.333-100.437-170.667-256-170.667-160 0-256 85.333-256 170.667z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["user"],"grid":24},"attrs":[],"properties":{"id":40,"order":26,"prevSize":24,"code":59651,"name":"user"},"setIdx":3,"setId":1,"iconIdx":9},{"icon":{"paths":["M384 597.333c58.923 0 112.256-23.893 150.869-62.507 38.571-38.571 62.464-91.904 62.464-150.827s-23.893-112.256-62.464-150.827c-38.613-38.613-91.947-62.507-150.869-62.507s-112.256 23.893-150.869 62.507c-38.571 38.571-62.464 91.904-62.464 150.827s23.893 112.256 62.464 150.827c38.613 38.613 91.947 62.507 150.869 62.507z","M384 896c150.101 0 256-42.667 256-85.333 0-85.333-100.437-170.667-256-170.667-160 0-256 85.333-256 170.667 0 42.667 96 85.333 256 85.333z","M896 512h-85.333v-85.333c0-23.595-19.072-42.667-42.667-42.667s-42.667 19.072-42.667 42.667v85.333h-85.333c-23.595 0-42.667 19.072-42.667 42.667s19.072 42.667 42.667 42.667h85.333v85.333c0 23.595 19.072 42.667 42.667 42.667s42.667-19.072 42.667-42.667v-85.333h85.333c23.595 0 42.667-19.072 42.667-42.667s-19.072-42.667-42.667-42.667z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["user-add"],"grid":24},"attrs":[],"properties":{"id":47,"order":37,"prevSize":24,"code":59652,"name":"user-add"},"setIdx":3,"setId":1,"iconIdx":10},{"icon":{"paths":["M341.333 256c-47.104 0-85.333 38.229-85.333 85.333v341.333c0 47.104 38.229 85.333 85.333 85.333s85.333-38.229 85.333-85.333v-341.333c0-47.104-38.229-85.333-85.333-85.333z","M640 256c-47.104 0-85.333 38.229-85.333 85.333v341.333c0 47.104 38.229 85.333 85.333 85.333s85.333-38.229 85.333-85.333v-341.333c0-47.104-38.229-85.333-85.333-85.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-pause"],"grid":24},"attrs":[],"properties":{"id":48,"order":36,"prevSize":24,"code":59653,"name":"pause"},"setIdx":3,"setId":1,"iconIdx":11},{"icon":{"paths":["M400.512 748.715l15.829 63.232c5.675 22.741 29.525 41.387 52.992 41.387h42.667c23.467 0 47.317-18.645 52.992-41.387l15.829-63.232c5.675-22.741 28.8-36.096 51.328-29.611l62.592 17.92c22.571 6.443 50.688-4.864 62.379-25.216l21.333-36.992c11.691-20.352 7.552-50.304-9.344-66.645l-46.848-45.269c-16.896-16.341-16.896-43.008 0.043-59.307l46.763-45.269c16.896-16.299 21.077-46.251 9.387-66.603l-21.376-36.992c-11.733-20.352-39.808-31.659-62.336-25.216l-62.592 17.92c-22.571 6.443-45.653-6.869-51.371-29.611l-15.787-63.147c-5.675-22.699-29.525-41.344-52.992-41.344h-42.667c-23.467 0-47.317 18.645-52.992 41.387l-15.787 63.147c-5.717 22.741-28.8 36.096-51.371 29.653l-62.592-17.92c-22.571-6.485-50.688 4.864-62.379 25.173l-21.333 36.992c-11.691 20.352-7.552 50.304 9.387 66.645l46.763 45.184c16.853 16.341 16.853 43.008 0 59.349l-46.848 45.269c-16.853 16.341-21.077 46.293-9.344 66.645l21.376 36.992c11.691 20.352 39.808 31.659 62.379 25.216l62.592-17.92c22.528-6.528 45.653 6.827 51.328 29.568zM490.667 448c47.104 0 85.333 38.187 85.333 85.333 0 47.104-38.229 85.333-85.333 85.333s-85.333-38.229-85.333-85.333c0-47.147 38.229-85.333 85.333-85.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["cog"],"grid":24},"attrs":[],"properties":{"id":59,"order":38,"prevSize":24,"code":59654,"name":"cog"},"setIdx":3,"setId":1,"iconIdx":12},{"icon":{"paths":["M261.163 554.667c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h62.507l-140.501 140.501c-16.683 16.683-16.683 43.648 0 60.331 8.32 8.32 19.243 12.501 30.165 12.501s21.845-4.181 30.165-12.501l145.664-145.664v72.832c0 23.552 19.072 42.667 42.667 42.667s37.504-19.115 37.504-42.667v-213.333h-208.171z","M298.667 469.333c23.552 0 42.667-19.115 42.667-42.667v-85.333h85.333c23.595 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-170.624l-0.043 170.667c0 23.552 19.072 42.667 42.667 42.667z","M725.333 554.667c-23.595 0-42.667 19.115-42.667 42.667v85.333h-85.333c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h170.667v-170.667c0-23.552-19.072-42.667-42.667-42.667z","M780.501 183.168l-140.501 140.501v-67.669c0-23.552-19.072-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v213.333h213.333c23.552 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-67.669l140.501-140.459c16.683-16.683 16.683-43.648 0-60.331s-43.648-16.725-60.331-0.043z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-minimise"],"grid":24},"attrs":[],"properties":{"id":82,"order":28,"prevSize":24,"code":59655,"name":"minimise"},"setIdx":3,"setId":1,"iconIdx":13},{"icon":{"paths":["M640 170.667c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h67.669l-140.501 140.501c-16.683 16.683-16.683 43.648 0 60.331 8.32 8.32 19.243 12.501 30.165 12.501s21.845-4.181 30.165-12.501l140.501-140.501v67.669c0 23.552 19.072 42.667 42.667 42.667s42.667-19.115 42.667-42.667v-213.333h-213.333z","M396.501 567.168l-140.501 140.501v-67.669c0-23.552-19.072-42.667-42.667-42.667s-42.667 19.115-42.667 42.667v213.291h42.496c0.341 0 170.837 0.043 170.837 0.043 23.552 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-67.669l140.501-140.459c16.683-16.683 16.683-43.648 0-60.331s-43.648-16.725-60.331-0.043z","M298.667 512c23.552 0 42.667-19.115 42.667-42.667v-128h128c23.595 0 42.667-19.115 42.667-42.667s-19.072-42.667-42.667-42.667h-213.291l-0.043 213.333c0 23.552 19.072 42.667 42.667 42.667z","M725.333 512c-23.595 0-42.667 19.115-42.667 42.667v128h-128c-23.595 0-42.667 19.115-42.667 42.667s19.072 42.667 42.667 42.667h213.333v-213.333c0-23.552-19.072-42.667-42.667-42.667z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-maximise"],"grid":24},"attrs":[],"properties":{"id":83,"order":27,"prevSize":24,"code":59656,"name":"maximise"},"setIdx":3,"setId":1,"iconIdx":14},{"icon":{"paths":["M435.2 273.067c-20.821 0-39.723 8.405-53.461 21.845-101.589 98.773-253.739 246.997-253.739 246.997s152.149 148.181 253.611 246.997c13.909 13.44 32.768 21.76 53.589 21.76 42.411 0 76.8-34.347 76.8-76.757v-192-192c0-42.411-34.389-76.843-76.8-76.843z","M819.2 273.067c-20.821 0-39.723 8.405-53.461 21.845-101.589 98.773-253.739 246.997-253.739 246.997s152.149 148.181 253.611 246.997c13.909 13.44 32.768 21.76 53.589 21.76 42.411 0 76.8-34.347 76.8-76.757v-384c0-42.411-34.389-76.843-76.8-76.843z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-rewind"],"grid":24},"attrs":[],"properties":{"id":93,"order":34,"prevSize":24,"code":59657,"name":"backward"},"setIdx":3,"setId":1,"iconIdx":15},{"icon":{"paths":["M682.667 256h-341.333c-46.933 0-85.333 38.4-85.333 85.333v341.333c0 46.933 38.4 85.333 85.333 85.333h341.333c46.933 0 85.333-38.4 85.333-85.333v-341.333c0-46.933-38.4-85.333-85.333-85.333z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-stop"],"grid":24},"attrs":[],"properties":{"id":100,"order":29,"prevSize":24,"code":59658,"name":"stop"},"setIdx":3,"setId":1,"iconIdx":16},{"icon":{"paths":["M443.563 786.475c112.683-109.824 281.771-274.475 281.771-274.475s-169.088-164.651-281.771-274.475c-15.488-14.891-36.395-24.192-59.563-24.192-47.104 0-85.333 38.229-85.333 85.333v426.667c0 47.104 38.229 85.333 85.333 85.333 23.168 0 44.075-9.301 59.563-24.192z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-play"],"grid":24},"attrs":[],"properties":{"id":102,"order":31,"prevSize":24,"code":59659,"name":"play"},"setIdx":3,"setId":1,"iconIdx":17},{"icon":{"paths":["M642.261 294.912c-13.824-13.397-32.64-21.845-53.461-21.845-42.411 0-76.8 34.432-76.8 76.843v192 192c0 42.411 34.389 76.757 76.8 76.757 20.821 0 39.68-8.32 53.461-21.845 101.589-98.731 253.739-246.912 253.739-246.912s-152.149-148.224-253.739-246.997z","M258.261 294.912c-13.824-13.397-32.64-21.845-53.461-21.845-42.411 0-76.8 34.432-76.8 76.843v384c0 42.411 34.389 76.757 76.8 76.757 20.821 0 39.68-8.32 53.461-21.845 101.589-98.731 253.739-246.912 253.739-246.912s-152.149-148.224-253.739-246.997z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-fast-forward"],"grid":24},"attrs":[],"properties":{"id":103,"order":32,"prevSize":24,"code":59660,"name":"forward"},"setIdx":3,"setId":1,"iconIdx":18},{"icon":{"paths":["M725.333 682.667h-426.667c-47.104 0-85.333 38.187-85.333 85.333 0 47.104 38.229 85.333 85.333 85.333h426.667c47.104 0 85.333-38.229 85.333-85.333 0-47.147-38.229-85.333-85.333-85.333z","M786.475 452.437c-109.824-112.683-274.475-281.771-274.475-281.771s-164.651 169.088-274.475 281.771c-14.891 15.488-24.192 36.395-24.192 59.563 0 47.104 38.229 85.333 85.333 85.333h426.667c47.104 0 85.333-38.229 85.333-85.333 0-23.168-9.301-44.075-24.192-59.563z"],"attrs":[],"isMulticolor":false,"isMulticolor2":false,"tags":["media-eject"],"grid":24},"attrs":[],"properties":{"id":104,"order":33,"prevSize":24,"code":59661,"name":"eject"},"setIdx":3,"setId":1,"iconIdx":19}],"height":1024,"metadata":{"name":"icons"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icons","majorVersion":1,"minorVersion":0},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false,"noie8":true,"ie7":false,"cssVars":true,"cssVarsFormat":"scss","showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215},"historySize":50,"gridSize":16,"showGrid":false}}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index dfcea74..e108eb1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,8 @@
"redux": "^4.1.0",
"redux-logger": "^3.0.6",
"reset-css": "^5.0.1",
- "tom32i-event-emitter.js": "^2.0.5"
+ "tom32i-event-emitter.js": "^2.0.5",
+ "webrtc-adapter": "^7.7.0"
},
"devDependencies": {
"@babel/core": "^7.14.0",
@@ -5494,6 +5495,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rtcpeerconnection-shim": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
+ "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
+ "dependencies": {
+ "sdp": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=6.0.0",
+ "npm": ">=3.10.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -5578,6 +5591,11 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/sdp": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
+ "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
+ },
"node_modules/semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -6439,6 +6457,19 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/webrtc-adapter": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
+ "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
+ "dependencies": {
+ "rtcpeerconnection-shim": "^1.2.15",
+ "sdp": "^2.12.0"
+ },
+ "engines": {
+ "node": ">=6.0.0",
+ "npm": ">=3.10.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -10530,6 +10561,14 @@
"glob": "^7.1.3"
}
},
+ "rtcpeerconnection-shim": {
+ "version": "1.2.15",
+ "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
+ "integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
+ "requires": {
+ "sdp": "^2.6.0"
+ }
+ },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -10577,6 +10616,11 @@
"ajv-keywords": "^3.5.2"
}
},
+ "sdp": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
+ "integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
+ },
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@@ -11199,6 +11243,15 @@
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true
},
+ "webrtc-adapter": {
+ "version": "7.7.1",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz",
+ "integrity": "sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A==",
+ "requires": {
+ "rtcpeerconnection-shim": "^1.2.15",
+ "sdp": "^2.12.0"
+ }
+ },
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 8e310cb..b6672e1 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,8 @@
"redux": "^4.1.0",
"redux-logger": "^3.0.6",
"reset-css": "^5.0.1",
- "tom32i-event-emitter.js": "^2.0.5"
+ "tom32i-event-emitter.js": "^2.0.5",
+ "webrtc-adapter": "^7.7.0"
},
"devDependencies": {
"@babel/core": "^7.14.0",
diff --git a/src/client/components/Controls.js b/src/client/components/Controls.js
index 8328744..e95671a 100644
--- a/src/client/components/Controls.js
+++ b/src/client/components/Controls.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Button from '@client/components/Button';
import VolumeControl from '@client/components/VolumeControl';
+import StreamButton from '@client/components/StreamButton';
class Controls extends Component {
static propTypes = {
@@ -13,6 +14,12 @@ class Controls extends Component {
onStop: PropTypes.func.isRequired,
onBackward: PropTypes.func.isRequired,
onForward: PropTypes.func.isRequired,
+ toggleStream: PropTypes.func,
+ streamable: PropTypes.bool.isRequired,
+ };
+
+ static defaultProps = {
+ toggleStream: null,
};
constructor(props) {
@@ -102,7 +109,7 @@ class Controls extends Component {
}
render() {
- const { playing, loaded, onStop, onBackward, onForward } = this.props;
+ const { playing, loaded, streamable, onStop, onBackward, onForward, toggleStream } = this.props;
const playIcon = loaded ? playing ? 'icon-pause' : 'icon-play' : 'icon-loader';
const screenIcon = this.fullscreen ? 'icon-minimise' : 'icon-maximise';
@@ -110,6 +117,7 @@ class Controls extends Component {
} onClick={onStop} />
+ {streamable ? : null}
} />
@@ -129,5 +137,6 @@ export default connect(
state => ({
playing: state.player.playing,
loaded: state.player.loaded,
- })
+ streamable: state.player.source === 'file',
+ }),
)(Controls);
diff --git a/src/client/components/Player.js b/src/client/components/Player.js
index 3f74237..5b8d52e 100644
--- a/src/client/components/Player.js
+++ b/src/client/components/Player.js
@@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { get } from '@client/container';
-import { setShowtime } from '@client/store/player';
+import { setShowtime, setTimeline } from '@client/store/player';
import { Video, YoutubeVideo } from '@client/components/video';
import Controls from '@client/components/Controls';
import Timeline from '@client/components/Timeline';
@@ -13,12 +13,21 @@ class Player extends Component {
static propTypes = {
source: PropTypes.string.isRequired,
setShowtime: PropTypes.func.isRequired,
+ setTimeline: PropTypes.func.isRequired,
+ duration: PropTypes.number,
+ currentTime: PropTypes.number,
+ };
+
+ static defaultProps = {
+ duration: 0,
+ currentTime: 0,
};
constructor(props) {
super(props);
this.api = get('api');
+ this.peer = get('peer');
this.showtime = new Showtime(props.setShowtime);
this.video = null;
this.timeline = null;
@@ -34,6 +43,40 @@ class Player extends Component {
this.onBackward = this.onBackward.bind(this);
this.onForward = this.onForward.bind(this);
this.onSeek = this.onSeek.bind(this);
+ this.onStream = this.onStream.bind(this);
+ this.toggleStream = this.toggleStream.bind(this);
+ }
+
+ get duration() {
+ if (this.props.source === 'peer') {
+ return this.props.duration;
+ }
+
+ return this.video.duration;
+ }
+
+ get currentTime() {
+ if (this.props.source === 'peer') {
+ return this.props.currentTime;
+ }
+
+ return this.video.currentTime;
+ }
+
+ componentDidMount() {
+ this.peer.addEventListener('stream', this.onStream);
+ }
+
+ componentWillUnmount() {
+ this.peer.removeEventListener('stream', this.onStream);
+ }
+
+ componentDidUpdate(prevProps) {
+ const { source } = this.props;
+
+ if (source !== prevProps.source && prevProps.source === 'peer') {
+ this.peer.clear();
+ }
}
setVideo(video) {
@@ -46,12 +89,18 @@ class Player extends Component {
onTimeUpdate() {
if (this.timeline) {
- this.timeline.setTime(this.video.currentTime, this.video.duration);
+ if (this.props.source !== 'peer') {
+ this.props.setTimeline(this.video.currentTime, this.video.duration);
+ }
+
+ if (this.peer.isStreaming()) {
+ this.api.setTimeline(this.video.currentTime, this.video.duration);
+ }
}
}
onProgress() {
- if (this.timeline) {
+ if (this.timeline && this.props.source !== 'peer') {
this.timeline.setLoadedParts(this.video.buffered, this.video.duration);
}
}
@@ -63,16 +112,16 @@ class Player extends Component {
onSeek(progress) {
if (typeof progress === 'number' && !isNaN(progress)) {
- this.api.seek(progress * this.video.duration);
+ this.api.seek(progress * this.duration);
}
}
onPlay() {
- this.api.play(this.video.currentTime);
+ this.api.play(this.currentTime);
}
onPause() {
- this.api.pause(this.video.currentTime);
+ this.api.pause(this.currentTime);
}
onStop() {
@@ -80,16 +129,40 @@ class Player extends Component {
}
onBackward() {
- this.api.seek(Math.max(this.video.currentTime - 10, 0));
+ this.api.seek(Math.max(this.currentTime - 10, 0));
}
onForward() {
- this.api.seek(Math.min(this.video.currentTime + 10, this.video.duration));
+ this.api.seek(Math.min(this.currentTime + 10, this.duration));
+ }
+
+ onStream() {
+ this.video.loadStream(this.peer.spectator.stream);
+ }
+
+ toggleStream() {
+ if (!this.peer.isStreaming()) {
+ this.peer.distribute(this.video.captureStream());
+ this.api.setTimeline(this.video.currentTime, this.video.duration);
+ } else {
+ this.peer.clear();
+ this.api.stopStreaming();
+ }
+ }
+
+ getVideoComponent(source) {
+ switch (source) {
+ case 'youtube':
+ return YoutubeVideo;
+
+ default:
+ return Video;
+ }
}
render() {
const { source } = this.props;
- const VideoComponent = source === 'youtube' ? YoutubeVideo : Video;
+ const VideoComponent = this.getVideoComponent(source);
return (
@@ -117,6 +190,7 @@ class Player extends Component {
onPause={this.onPause}
onBackward={this.onBackward}
onForward={this.onForward}
+ toggleStream={source === 'file' ? this.toggleStream : undefined}
/>
@@ -127,8 +201,11 @@ class Player extends Component {
export default connect(
state => ({
source: state.player.source,
+ duration: state.player.duration,
+ currentTime: state.player.currentTime,
}),
dispatch => ({
setShowtime: showtime => dispatch(setShowtime(showtime)),
+ setTimeline: (currentTime, duration) => dispatch(setTimeline(currentTime, duration)),
})
)(Player);
diff --git a/src/client/components/Socket.js b/src/client/components/Socket.js
index 23f752f..7bf2c93 100644
--- a/src/client/components/Socket.js
+++ b/src/client/components/Socket.js
@@ -2,9 +2,8 @@ import { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { get } from '@client/container';
-import { socketOpen, socketClose, me, userAdd, userRemove, userReady } from '@client/store/room';
-import { loadVideoFromServer } from '@client/store/player';
-import { play, pause, seek, stop } from '@client/store/player';
+import { socketOpen, socketClose, me, userAdd, userRemove, userReady, userStreaming } from '@client/store/room';
+import { play, pause, seek, stop, loadVideoFromServer, setTimeline, unloadStream } from '@client/store/player';
class Socket extends Component {
static propTypes = {
@@ -23,11 +22,14 @@ class Socket extends Component {
onUserAdd: PropTypes.func.isRequired,
onUserRemove: PropTypes.func.isRequired,
onUserReady: PropTypes.func.isRequired,
+ onUserStreaming: PropTypes.func.isRequired,
onControlPlay: PropTypes.func.isRequired,
onControlPause: PropTypes.func.isRequired,
onControlSeek: PropTypes.func.isRequired,
onControlStop: PropTypes.func.isRequired,
onVideo: PropTypes.func.isRequired,
+ setTimeline: PropTypes.func.isRequired,
+ unloadStream: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -38,11 +40,17 @@ class Socket extends Component {
super(props);
this.api = get('api');
+ this.peer = get('peer');
this.onError = this.onError.bind(this);
this.onVideoFile = this.onVideoFile.bind(this);
this.onVideoUrl = this.onVideoUrl.bind(this);
this.onVideoYoutube = this.onVideoYoutube.bind(this);
+ this.onPeerOffer = this.onPeerOffer.bind(this);
+ this.onPeerAnswer = this.onPeerAnswer.bind(this);
+ this.onPeerCandidate = this.onPeerCandidate.bind(this);
+ this.onPeerTimeline = this.onPeerTimeline.bind(this);
+ this.onPeerStop = this.onPeerStop.bind(this);
}
componentDidMount() {
@@ -58,6 +66,7 @@ class Socket extends Component {
this.api.addEventListener('user:add', this.props.onUserAdd);
this.api.addEventListener('user:remove', this.props.onUserRemove);
this.api.addEventListener('user:ready', this.props.onUserReady);
+ this.api.addEventListener('user:streaming', this.props.onUserStreaming);
this.api.addEventListener('control:play', this.props.onControlPlay);
this.api.addEventListener('control:pause', this.props.onControlPause);
this.api.addEventListener('control:stop', this.props.onControlStop);
@@ -65,6 +74,11 @@ class Socket extends Component {
this.api.addEventListener('video:file', this.onVideoFile);
this.api.addEventListener('video:url', this.onVideoUrl);
this.api.addEventListener('video:youtube', this.onVideoYoutube);
+ this.api.addEventListener('peer:offer', this.onPeerOffer);
+ this.api.addEventListener('peer:answer', this.onPeerAnswer);
+ this.api.addEventListener('peer:candidate', this.onPeerCandidate);
+ this.api.addEventListener('peer:timeline', this.onPeerTimeline);
+ this.api.addEventListener('peer:stop', this.onPeerStop);
}
componentWillUnmount() {
@@ -78,6 +92,7 @@ class Socket extends Component {
this.api.removeEventListener('user:add', this.props.onUserAdd);
this.api.removeEventListener('user:remove', this.props.onUserRemove);
this.api.removeEventListener('user:ready', this.props.onUserReady);
+ this.api.removeEventListener('user:streaming', this.props.onUserStreaming);
this.api.removeEventListener('control:play', this.props.onControlPlay);
this.api.removeEventListener('control:pause', this.props.onControlPause);
this.api.removeEventListener('control:stop', this.props.onControlStop);
@@ -85,6 +100,11 @@ class Socket extends Component {
this.api.removeEventListener('video:file', this.onVideoFile);
this.api.removeEventListener('video:url', this.onVideoUrl);
this.api.removeEventListener('video:youtube', this.onVideoYoutube);
+ this.api.removeEventListener('peer:offer', this.onPeerOffer);
+ this.api.removeEventListener('peer:answer', this.onPeerAnswer);
+ this.api.removeEventListener('peer:candidate', this.onPeerCandidate);
+ this.api.removeEventListener('peer:timeline', this.onPeerTimeline);
+ this.api.removeEventListener('peer:stop', this.onPeerStop);
// Close connection
this.api.leave();
@@ -107,22 +127,45 @@ class Socket extends Component {
onVideoUrl(event) {
const { name, url } = event.detail;
-
this.props.onVideo('url', name, url);
}
onVideoYoutube(event) {
const { name, url } = event.detail;
-
this.props.onVideo('youtube', name, url);
}
onVideoFile(event) {
const { name } = event.detail;
-
this.props.onVideo('file', name);
}
+ onPeerOffer(event) {
+ const { description, sender } = event.detail;
+ this.peer.spectate(JSON.parse(description), sender);
+ this.props.onVideo('peer');
+ }
+
+ onPeerAnswer(event) {
+ const { sender, description } = event.detail;
+ this.peer.answer(sender, JSON.parse(description));
+ }
+
+ onPeerCandidate(event) {
+ const { sender, description } = event.detail;
+ this.peer.addCandidate(sender, JSON.parse(description));
+ }
+
+ onPeerTimeline(event) {
+ const { currentTime, duration } = event.detail;
+ this.props.setTimeline(currentTime, duration);
+ }
+
+ onPeerStop() {
+ this.props.unloadStream();
+ this.peer.clear();
+ }
+
/**
* On error
*
@@ -154,10 +197,13 @@ export default connect(
onUserAdd: event => dispatch(userAdd(event.detail)),
onUserRemove: event => dispatch(userRemove(event.detail)),
onUserReady: event => dispatch(userReady(event.detail)),
+ onUserStreaming: event => dispatch(userStreaming(event.detail)),
onControlPlay: event => dispatch(play(event.detail)),
onControlPause: event => dispatch(pause(event.detail)),
onControlSeek: event => dispatch(seek(event.detail)),
onControlStop: event => dispatch(stop(event.detail)),
onVideo: (source, name, url = null) => dispatch(loadVideoFromServer(source, name, url)),
+ unloadStream: () => dispatch(unloadStream()),
+ setTimeline: (currentTime, duration) => dispatch(setTimeline(currentTime, duration)),
})
)(Socket);
diff --git a/src/client/components/StreamButton.js b/src/client/components/StreamButton.js
new file mode 100644
index 0000000..0af6cb9
--- /dev/null
+++ b/src/client/components/StreamButton.js
@@ -0,0 +1,34 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import Button from '@client/components/Button';
+
+class StreamButton extends Component {
+ static propTypes = {
+ disabled: PropTypes.bool,
+ active: PropTypes.bool.isRequired,
+ onClick: PropTypes.func.isRequired,
+ };
+
+ static defaultProps = {
+ disabled: false,
+ };
+
+ render() {
+ const { active, disabled, onClick } = this.props;
+
+ return
}
+ className={active ? 'active' : ''}
+ onClick={onClick}
+ />;
+ }
+}
+
+export default connect(
+ state => ({
+ active: state.room.streaming === state.room.me,
+ }),
+)(StreamButton);
+
diff --git a/src/client/components/Timeline.js b/src/client/components/Timeline.js
index d4411ef..87b82cd 100644
--- a/src/client/components/Timeline.js
+++ b/src/client/components/Timeline.js
@@ -1,21 +1,44 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
-export default class Timeline extends Component {
+class Timeline extends Component {
static propTypes = {
onSeek: PropTypes.func.isRequired,
+ played: PropTypes.string,
+ time: PropTypes.string,
+ duration: PropTypes.string,
};
+ static defaultProps = {
+ time: '',
+ duration: '',
+ played: '',
+ };
+
+ static formatPercent(time, duration) {
+ return (time / duration * 100).toFixed(3);
+ }
+
+ static formatTime(time) {
+ const hours = Math.floor(time / 3600);
+ const minutes = Math.floor(time / 60) % 60;
+ const seconds = time % 60;
+
+ if (hours) {
+ return `${hours}:${minutes.toFixed(0).padStart(2, 0)}:${seconds.toFixed(0).padStart(2, 0)}`;
+ }
+
+ return `${minutes}:${seconds.toFixed(0).padStart(2, 0)}`;
+ }
+
constructor(props) {
super(props);
this.container = null;
this.state = {
- time: '',
- duration: '',
loaded: [],
- played: 0,
};
this.setContainer = this.setContainer.bind(this);
@@ -27,44 +50,26 @@ export default class Timeline extends Component {
this.container = container;
}
- setTime(time, duration) {
- this.setState({
- played: this.formetPercent(time, duration),
- time: this.formatTime(time),
- duration: this.formatTime(duration),
- });
- }
-
setLoadedParts(buffered, duration) {
const { length } = buffered;
+
+ // Is streaming?
+ if (duration === Infinity || length === 0) {
+ return;
+ }
+
const parts = new Array(length);
for (let i = 0; i < length; i++) {
parts[i] = [
- this.formetPercent(buffered.start(i), duration),
- this.formetPercent(buffered.end(i), duration)
+ this.constructor.formatPercent(buffered.start(i), duration),
+ this.constructor.formatPercent(buffered.end(i), duration)
];
}
this.setState({ loaded: parts });
}
- formetPercent(time, duration) {
- return (time / duration * 100).toFixed(3);
- }
-
- formatTime(time) {
- const hours = Math.floor(time / 3600);
- const minutes = Math.floor(time / 60) % 60;
- const seconds = time % 60;
-
- if (hours) {
- return `${hours}:${minutes.toFixed(0).padStart(2, 0)}:${seconds.toFixed(0).padStart(2, 0)}`;
- }
-
- return `${minutes}:${seconds.toFixed(0).padStart(2, 0)}`;
- }
-
onMouseDown(event) {
this.props.onSeek(this.getPosition(event.nativeEvent));
}
@@ -80,7 +85,8 @@ export default class Timeline extends Component {
}
render() {
- const { played, loaded, time, duration } = this.state;
+ const { played, time, duration } = this.props;
+ const { loaded } = this.state;
return (
@@ -96,3 +102,14 @@ export default class Timeline extends Component {
);
}
}
+
+export default connect(
+ state => ({
+ played: Timeline.formatPercent(state.player.currentTime, state.player.duration),
+ time: Timeline.formatTime(state.player.currentTime),
+ duration: Timeline.formatTime(state.player.duration),
+ }),
+ null,
+ null,
+ { forwardRef: true }
+)(Timeline);
diff --git a/src/client/components/User.js b/src/client/components/User.js
index fd60d25..0c30fc3 100644
--- a/src/client/components/User.js
+++ b/src/client/components/User.js
@@ -6,6 +6,7 @@ export default class User extends Component {
id: PropTypes.number.isRequired,
me: PropTypes.bool.isRequired,
ready: PropTypes.bool.isRequired,
+ streaming: PropTypes.bool.isRequired,
name: PropTypes.string,
};
@@ -14,11 +15,12 @@ export default class User extends Component {
};
render() {
- const { me, ready } = this.props;
+ const { me, ready, streaming } = this.props;
const classNames = [
'user',
ready ? 'ready' : 'loading',
me ? 'me' : '',
+ streaming ? 'streaming' : '',
];
return (
@@ -27,6 +29,7 @@ export default class User extends Component {
+ {streaming ?
: null}
);
}
diff --git a/src/client/components/UserList.js b/src/client/components/UserList.js
index b2a086c..4212684 100644
--- a/src/client/components/UserList.js
+++ b/src/client/components/UserList.js
@@ -6,6 +6,7 @@ import User from '@client/components/User';
class UserList extends Component {
static propTypes = {
me: PropTypes.number,
+ streaming: PropTypes.number,
users: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
@@ -15,6 +16,7 @@ class UserList extends Component {
static defaultProps = {
me: null,
+ streaming: null,
};
constructor(props) {
@@ -29,9 +31,14 @@ class UserList extends Component {
}
renderUser(user) {
- const { me } = this.props;
-
- return createElement(User, { key: user.id, me: me === user.id, ...user });
+ const { me, streaming } = this.props;
+
+ return createElement(User, {
+ key: user.id,
+ me: me === user.id,
+ streaming: streaming === user.id,
+ ...user
+ });
}
render() {
@@ -49,5 +56,6 @@ export default connect(
state => ({
users: state.room.users,
me: state.room.me,
+ streaming: state.room.streaming,
})
)(UserList);
diff --git a/src/client/components/video/Video.js b/src/client/components/video/Video.js
index 65e4ece..1762365 100644
--- a/src/client/components/video/Video.js
+++ b/src/client/components/video/Video.js
@@ -4,7 +4,7 @@ import Subtitles from '@client/components/Subtitles';
export default class Video extends Component {
static propTypes = {
- src: PropTypes.string.isRequired,
+ src: PropTypes.string,
playing: PropTypes.bool.isRequired,
time: PropTypes.number.isRequired,
authorized: PropTypes.bool.isRequired,
@@ -20,11 +20,14 @@ export default class Video extends Component {
onPaused: PropTypes.func.isRequired,
onEnded: PropTypes.func.isRequired,
preload: PropTypes.string,
+ autoplay: PropTypes.bool,
volume: PropTypes.number.isRequired,
};
static defaultProps = {
preload: 'auto',
+ autoplay: false,
+ src: undefined,
};
constructor(props) {
@@ -36,9 +39,11 @@ export default class Video extends Component {
this.setElement = this.setElement.bind(this);
this.play = this.play.bind(this);
this.onLoadStart = this.onLoadStart.bind(this);
+ this.onProgress = this.onProgress.bind(this);
this.onCanPlay = this.onCanPlay.bind(this);
this.onCanPlayThrough = this.onCanPlayThrough.bind(this);
this.onDurationChange = this.onDurationChange.bind(this);
+ this.onLoadedMetadata = this.onLoadedMetadata.bind(this);
this.onNotAuthorized = this.onNotAuthorized.bind(this);
this.onEnded = this.onEnded.bind(this);
this.onAuthorized = this.onAuthorized.bind(this);
@@ -67,7 +72,7 @@ export default class Video extends Component {
setElement(element) {
this.element = element;
- if (element) {
+ if (this.element) {
this.element.volume = this.props.volume;
}
}
@@ -100,11 +105,27 @@ export default class Video extends Component {
}
seek(time) {
- if (typeof time === 'number') {
+ if (typeof time === 'number' && this.element.seekable.length) {
this.element.currentTime = time;
}
}
+ captureStream() {
+ if (typeof this.element.mozCaptureStream === 'function') {
+ return this.element.mozCaptureStream();
+ }
+
+ if (typeof this.element.captureStream === 'function') {
+ return this.element.captureStream();
+ }
+
+ return null;
+ }
+
+ loadStream(stream) {
+ this.element.srcObject = stream;
+ }
+
onAuthorized() {
if (!this.props.authorized) {
this.props.setAuthorized(true);
@@ -118,11 +139,17 @@ export default class Video extends Component {
}
onLoadStart() {
- if (this.props.loaded) {
+ if (typeof this.props.src === 'undefined') {
+ this.props.setLoaded(true);
+ } else {
this.props.setLoaded(false);
}
}
+ onProgress() {
+ this.props.onProgress();
+ }
+
onCanPlay() {
if (!this.props.loaded) {
this.props.setLoaded(true);
@@ -134,12 +161,17 @@ export default class Video extends Component {
}
onCanPlayThrough() {
+ this.props.setLoaded(true);
+ }
+ onLoadedMetadata() {
}
onDurationChange() {
- this.props.setDuration(this.element.duration);
- this.props.onDurationChange();
+ if (this.element.duration !== Infinity) {
+ this.props.setDuration(this.element.duration);
+ this.props.onDurationChange();
+ }
}
onEnded() {
@@ -151,19 +183,21 @@ export default class Video extends Component {
console.error(error);
}
- render(){
- const { src, preload } = this.props;
+ render() {
+ const { src, preload, autoplay } = this.props;
return (