Merge branch 'share-desktop'
This commit is contained in:
commit
e28af6740e
6323
package-lock.json
generated
6323
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -65,7 +65,7 @@
|
|||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/ejs": "^2.6.3",
|
"@types/ejs": "^2.6.3",
|
||||||
"@types/express": "^4.17.2",
|
"@types/express": "^4.17.2",
|
||||||
"@types/jest": "^24.0.23",
|
"@types/jest": "^25.1.0",
|
||||||
"@types/js-yaml": "^3.12.1",
|
"@types/js-yaml": "^3.12.1",
|
||||||
"@types/lodash": "^4.14.148",
|
"@types/lodash": "^4.14.148",
|
||||||
"@types/node": "^12.12.8",
|
"@types/node": "^12.12.8",
|
||||||
@ -81,7 +81,6 @@
|
|||||||
"@types/uuid": "^3.4.6",
|
"@types/uuid": "^3.4.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||||
"@typescript-eslint/parser": "^2.7.0",
|
"@typescript-eslint/parser": "^2.7.0",
|
||||||
"acorn": "^7.1.0",
|
|
||||||
"babel-core": "^7.0.0-bridge.0",
|
"babel-core": "^7.0.0-bridge.0",
|
||||||
"babel-minify": "^0.5.1",
|
"babel-minify": "^0.5.1",
|
||||||
"babelify": "^10.0.0",
|
"babelify": "^10.0.0",
|
||||||
@ -91,9 +90,9 @@
|
|||||||
"eslint": "^6.6.0",
|
"eslint": "^6.6.0",
|
||||||
"eslint-plugin-import": "^2.18.2",
|
"eslint-plugin-import": "^2.18.2",
|
||||||
"eslint-plugin-react": "^7.16.0",
|
"eslint-plugin-react": "^7.16.0",
|
||||||
"jest": "^24.9.0",
|
"jest": "^25.1.0",
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"node-sass": "^4.13.0",
|
"node-sass": "4.13.1",
|
||||||
"nodemon": "^1.19.4",
|
"nodemon": "^1.19.4",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
@ -107,7 +106,7 @@
|
|||||||
"simple-peer": "^9.6.2",
|
"simple-peer": "^9.6.2",
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"ts-jest": "^24.1.0",
|
"ts-jest": "^25.1.0",
|
||||||
"ts-node": "^8.5.2",
|
"ts-node": "^8.5.2",
|
||||||
"tsify": "^4.0.1",
|
"tsify": "^4.0.1",
|
||||||
"typescript": "^3.7.2",
|
"typescript": "^3.7.2",
|
||||||
|
|||||||
Binary file not shown.
@ -7,19 +7,20 @@
|
|||||||
<font-face units-per-em="1024" ascent="870.850439882698" descent="-153.14956011730206" />
|
<font-face units-per-em="1024" ascent="870.850439882698" descent="-153.14956011730206" />
|
||||||
<missing-glyph horiz-adv-x="1024" />
|
<missing-glyph horiz-adv-x="1024" />
|
||||||
<glyph unicode=" " horiz-adv-x="0" d="" />
|
<glyph unicode=" " horiz-adv-x="0" d="" />
|
||||||
<glyph unicode="" glyph-name="mic" d="M738.721 376.857h72.070c0-146.143-116.113-266.26-256.25-286.28v-140.137h-84.082v140.137c-140.137 20.020-256.25 140.137-256.25 286.28h72.070c0-128.125 108.106-216.211 226.221-216.211s226.221 88.086 226.221 216.211zM512.5 248.732c-70.068 0-128.125 58.057-128.125 128.125v256.25c0 70.068 58.057 128.125 128.125 128.125s128.125-58.057 128.125-128.125v-256.25c0-70.068-58.057-128.125-128.125-128.125z" />
|
<glyph unicode="" glyph-name="mic" d="M739.443 370.375h72.14c0-146.286-116.227-266.52-256.5-286.56v-140.274h-84.164v140.274c-140.274 20.040-256.5 140.274-256.5 286.56h72.14c0-128.25 108.212-216.422 226.442-216.422s226.442 88.172 226.442 216.422zM513.001 242.125c-70.136 0-128.25 58.114-128.25 128.25v256.5c0 70.136 58.114 128.25 128.25 128.25s128.25-58.114 128.25-128.25v-256.5c0-70.136-58.114-128.25-128.25-128.25z" />
|
||||||
<glyph unicode="" glyph-name="mic_off" d="M182.178 719.192l714.698-714.698-54.053-54.053-178.174 178.174c-32.031-20.020-72.070-32.031-110.108-38.037v-140.137h-84.082v140.137c-140.137 20.020-256.25 140.137-256.25 286.28h72.070c0-128.125 108.106-216.211 226.221-216.211 34.033 0 68.066 8.008 98.096 22.022l-70.068 70.068c-8.008-2.002-18.018-4.004-28.027-4.004-70.068 0-128.125 58.057-128.125 128.125v32.031l-256.25 256.25zM640.626 370.851l-256.25 254.248v8.008c0 70.068 58.057 128.125 128.125 128.125s128.125-58.057 128.125-128.125v-262.256zM810.792 376.857c0-50.049-14.014-98.096-38.037-140.137l-52.051 54.053c12.012 26.025 18.018 54.053 18.018 86.084h72.070z" />
|
<glyph unicode="" glyph-name="mic_off" d="M182.356 713.045l715.397-715.397-54.106-54.106-178.348 178.348c-32.062-20.040-72.14-32.062-110.216-38.074v-140.274h-84.164v140.274c-140.274 20.040-256.5 140.274-256.5 286.56h72.14c0-128.25 108.212-216.422 226.442-216.422 34.066 0 68.133 8.016 98.192 22.044l-70.136 70.136c-8.016-2.004-18.036-4.008-28.054-4.008-70.136 0-128.25 58.114-128.25 128.25v32.062l-256.5 256.5zM641.252 364.363l-256.5 254.497v8.016c0 70.136 58.114 128.25 128.25 128.25s128.25-58.114 128.25-128.25v-262.512zM811.585 370.375c0-50.098-14.028-98.192-38.074-140.274l-52.102 54.106c12.024 26.050 18.036 54.106 18.036 86.168h72.14z" />
|
||||||
<glyph unicode="" glyph-name="videocam" d="M726.71 398.879l170.166 170.166v-468.457l-170.166 170.166v-150.147c0-24.023-20.020-42.041-44.043-42.041h-512.5c-24.023 0-42.041 18.018-42.041 42.041v428.418c0 24.023 18.018 42.041 42.041 42.041h512.5c24.023 0 44.043-18.018 44.043-42.041v-150.147z" />
|
<glyph unicode="" glyph-name="videocam" d="M727.42 392.418l170.332 170.332v-468.915l-170.332 170.332v-150.294c0-24.046-20.040-42.082-44.086-42.082h-513.001c-24.046 0-42.082 18.036-42.082 42.082v428.837c0 24.046 18.036 42.082 42.082 42.082h513.001c24.046 0 44.086-18.036 44.086-42.082v-150.294z" />
|
||||||
<glyph unicode="" glyph-name="videocam_off" d="M140.137 761.233l756.739-756.739-54.053-54.053-136.133 136.133c-6.006-4.004-16.016-8.008-24.023-8.008h-512.5c-24.023 0-42.041 18.018-42.041 42.041v428.418c0 24.023 18.018 42.041 42.041 42.041h32.031l-116.113 116.113zM896.876 569.045v-456.446l-478.467 478.467h264.258c24.023 0 44.043-18.018 44.043-42.041v-150.147z" />
|
<glyph unicode="" glyph-name="videocam_off" d="M140.274 755.127l757.479-757.479-54.106-54.106-136.266 136.266c-6.012-4.008-16.032-8.016-24.046-8.016h-513.001c-24.046 0-42.082 18.036-42.082 42.082v428.837c0 24.046 18.036 42.082 42.082 42.082h32.062l-116.227 116.227zM897.753 562.751v-456.892l-478.935 478.935h264.516c24.046 0 44.086-18.036 44.086-42.082v-150.294z" />
|
||||||
<glyph unicode="" glyph-name="call_end" d="M512.5 462.941c-68.066 0-134.131-10.010-196.192-30.029v-132.129c0-16.016-10.010-34.033-24.023-40.039-42.041-20.020-80.078-46.045-114.111-78.076-8.008-8.008-18.018-12.012-30.029-12.012s-22.022 4.004-30.029 12.012l-106.104 106.104c-8.008 8.008-12.012 18.018-12.012 30.029s4.004 22.022 12.012 30.029c130.127 124.121 306.299 200.196 500.489 200.196s370.362-76.074 500.489-200.196c8.008-8.008 12.012-18.018 12.012-30.029s-4.004-22.022-12.012-30.029l-106.104-106.104c-8.008-8.008-18.018-12.012-30.029-12.012s-22.022 4.004-30.029 12.012c-34.033 32.031-72.070 58.057-114.111 78.076-14.014 6.006-24.023 20.020-24.023 38.037v132.129c-62.061 20.020-128.125 32.031-196.192 32.031z" />
|
<glyph unicode="" glyph-name="call_end" d="M513.001 456.543c-68.133 0-134.262-10.020-196.384-30.058v-132.258c0-16.032-10.020-34.066-24.046-40.078-42.082-20.040-80.156-46.090-114.223-78.152-8.016-8.016-18.036-12.024-30.058-12.024s-22.044 4.008-30.058 12.024l-106.208 106.208c-8.016 8.016-12.024 18.036-12.024 30.058s4.008 22.044 12.024 30.058c130.254 124.242 306.598 200.392 500.978 200.392s370.724-76.148 500.978-200.392c8.016-8.016 12.024-18.036 12.024-30.058s-4.008-22.044-12.024-30.058l-106.208-106.208c-8.016-8.016-18.036-12.024-30.058-12.024s-22.044 4.008-30.058 12.024c-34.066 32.062-72.14 58.114-114.223 78.152-14.028 6.012-24.046 20.040-24.046 38.074v132.258c-62.122 20.040-128.25 32.062-196.384 32.062z" />
|
||||||
<glyph unicode="" glyph-name="arrow_forward" d="M512.5 677.151l342.334-342.334-342.334-342.334-60.059 60.059 238.233 240.235h-520.508v84.082h520.508l-238.233 240.235z" />
|
<glyph unicode="" glyph-name="arrow_forward" d="M513.001 670.962l342.669-342.669-342.669-342.669-60.118 60.118 238.466 240.47h-521.017v84.164h521.017l-238.466 240.47z" />
|
||||||
<glyph unicode="" glyph-name="fullscreen" d="M598.585 633.108h212.207v-212.207h-84.082v128.125h-128.125v84.082zM726.71 120.607v128.125h84.082v-212.207h-212.207v84.082h128.125zM214.209 420.9v212.207h212.207v-84.082h-128.125v-128.125h-84.082zM298.291 248.732v-128.125h128.125v-84.082h-212.207v212.207h84.082z" />
|
<glyph unicode="" glyph-name="fullscreen" d="M599.17 626.876h212.414v-212.414h-84.164v128.25h-128.25v84.164zM727.42 113.874v128.25h84.164v-212.414h-212.414v84.164h128.25zM214.418 414.461v212.414h212.414v-84.164h-128.25v-128.25h-84.164zM298.583 242.125v-128.25h128.25v-84.164h-212.414v212.414h84.164z" />
|
||||||
<glyph unicode="" glyph-name="fullscreen_exit" d="M682.667 504.982h128.125v-84.082h-212.207v212.207h84.082v-128.125zM598.585 36.525v212.207h212.207v-84.082h-128.125v-128.125h-84.082zM342.334 504.982v128.125h84.082v-212.207h-212.207v84.082h128.125zM214.209 164.65v84.082h212.207v-212.207h-84.082v128.125h-128.125z" />
|
<glyph unicode="" glyph-name="fullscreen_exit" d="M683.334 498.625h128.25v-84.164h-212.414v212.414h84.164v-128.25zM599.17 29.71v212.414h212.414v-84.164h-128.25v-128.25h-84.164zM342.669 498.625v128.25h84.164v-212.414h-212.414v84.164h128.25zM214.418 157.961v84.164h212.414v-212.414h-84.164v128.25h-128.25z" />
|
||||||
<glyph unicode="" glyph-name="more_vert" d="M512.5 164.65c46.045 0 86.084-40.039 86.084-86.084s-40.039-86.084-86.084-86.084-86.084 40.039-86.084 86.084 40.039 86.084 86.084 86.084zM512.5 420.9c46.045 0 86.084-40.039 86.084-86.084s-40.039-86.084-86.084-86.084-86.084 40.039-86.084 86.084 40.039 86.084 86.084 86.084zM512.5 504.982c-46.045 0-86.084 40.039-86.084 86.084s40.039 86.084 86.084 86.084 86.084-40.039 86.084-86.084-40.039-86.084-86.084-86.084z" />
|
<glyph unicode="" glyph-name="more_vert" d="M513.001 157.961c46.090 0 86.168-40.078 86.168-86.168s-40.078-86.168-86.168-86.168-86.168 40.078-86.168 86.168 40.078 86.168 86.168 86.168zM513.001 414.461c46.090 0 86.168-40.078 86.168-86.168s-40.078-86.168-86.168-86.168-86.168 40.078-86.168 86.168 40.078 86.168 86.168 86.168zM513.001 498.625c-46.090 0-86.168 40.078-86.168 86.168s40.078 86.168 86.168 86.168 86.168-40.078 86.168-86.168-40.078-86.168-86.168-86.168z" />
|
||||||
<glyph unicode="" glyph-name="sentiment_satisfied" d="M512.5 164.65c64.063 0 118.115 34.033 148.145 84.082h70.068c-34.033-88.086-118.115-148.145-218.213-148.145s-184.18 60.059-218.213 148.145h70.068c30.029-50.049 84.082-84.082 148.145-84.082zM512.5-7.518c188.184 0 342.334 154.151 342.334 342.334s-154.151 342.334-342.334 342.334-342.334-154.151-342.334-342.334 154.151-342.334 342.334-342.334zM512.5 761.233c236.231 0 426.416-190.186 426.416-426.416s-190.186-426.416-426.416-426.416-426.416 190.186-426.416 426.416 190.186 426.416 426.416 426.416zM298.291 440.92c0 36.035 28.027 64.063 64.063 64.063s64.063-28.027 64.063-64.063-28.027-64.063-64.063-64.063-64.063 28.027-64.063 64.063zM598.585 440.92c0 36.035 28.027 64.063 64.063 64.063s64.063-28.027 64.063-64.063-28.027-64.063-64.063-64.063-64.063 28.027-64.063 64.063z" />
|
<glyph unicode="" glyph-name="sentiment_satisfied" d="M513.001 157.961c64.126 0 118.23 34.066 148.29 84.164h70.136c-34.066-88.172-118.23-148.29-218.426-148.29s-184.36 60.118-218.426 148.29h70.136c30.058-50.098 84.164-84.164 148.29-84.164zM513.001-14.376c188.368 0 342.669 154.302 342.669 342.669s-154.302 342.669-342.669 342.669-342.669-154.302-342.669-342.669 154.302-342.669 342.669-342.669zM513.001 755.127c236.462 0 426.833-190.372 426.833-426.833s-190.372-426.833-426.833-426.833-426.833 190.372-426.833 426.833 190.372 426.833 426.833 426.833zM298.583 434.501c0 36.070 28.054 64.126 64.126 64.126s64.126-28.054 64.126-64.126-28.054-64.126-64.126-64.126-64.126 28.054-64.126 64.126zM599.17 434.501c0 36.070 28.054 64.126 64.126 64.126s64.126-28.054 64.126-64.126-28.054-64.126-64.126-64.126-64.126 28.054-64.126 64.126z" />
|
||||||
<glyph unicode="" glyph-name="face" d="M512.5-7.518c188.184 0 342.334 154.151 342.334 342.334 0 34.033-6.006 66.065-14.014 96.094-30.029-8.008-62.061-10.010-96.094-10.010-144.141 0-270.264 70.068-348.34 180.176-42.041-102.1-124.121-186.182-224.219-230.225-2.002-12.012-2.002-24.023-2.002-36.035 0-188.184 154.151-342.334 342.334-342.334zM512.5 761.233c236.231 0 426.416-190.186 426.416-426.416s-190.186-426.416-426.416-426.416-426.416 190.186-426.416 426.416 190.186 426.416 426.416 426.416zM640.626 344.826c30.029 0 54.053-22.022 54.053-52.051s-24.023-54.053-54.053-54.053-54.053 24.023-54.053 54.053 24.023 52.051 54.053 52.051zM384.375 344.826c30.029 0 54.053-22.022 54.053-52.051s-24.023-54.053-54.053-54.053-54.053 24.023-54.053 54.053 24.023 52.051 54.053 52.051z" />
|
<glyph unicode="" glyph-name="face" d="M513.001-14.376c188.368 0 342.669 154.302 342.669 342.669 0 34.066-6.012 66.13-14.028 96.188-30.058-8.016-62.122-10.020-96.188-10.020-144.282 0-270.528 70.136-348.681 180.352-42.082-102.2-124.242-186.364-224.438-230.45-2.004-12.024-2.004-24.046-2.004-36.070 0-188.368 154.302-342.669 342.669-342.669zM513.001 755.127c236.462 0 426.833-190.372 426.833-426.833s-190.372-426.833-426.833-426.833-426.833 190.372-426.833 426.833 190.372 426.833 426.833 426.833zM641.252 338.313c30.058 0 54.106-22.044 54.106-52.102s-24.046-54.106-54.106-54.106-54.106 24.046-54.106 54.106 24.046 52.102 54.106 52.102zM384.751 338.313c30.058 0 54.106-22.044 54.106-52.102s-24.046-54.106-54.106-54.106-54.106 24.046-54.106 54.106 24.046 52.102 54.106 52.102z" />
|
||||||
<glyph unicode="" glyph-name="question_answer" d="M726.71 334.816c0-24.023-20.020-42.041-44.043-42.041h-426.416l-170.166-172.168v598.585c0 24.023 18.018 42.041 42.041 42.041h554.542c24.023 0 44.043-18.018 44.043-42.041v-384.375zM896.876 591.066c24.023 0 42.041-18.018 42.041-42.041v-640.626l-170.166 170.166h-470.459c-24.023 0-42.041 18.018-42.041 42.041v86.084h554.542v384.375h86.084z" />
|
<glyph unicode="" glyph-name="question_answer" d="M727.42 328.293c0-24.046-20.040-42.082-44.086-42.082h-426.833l-170.332-172.336v599.17c0 24.046 18.036 42.082 42.082 42.082h555.084c24.046 0 44.086-18.036 44.086-42.082v-384.751zM897.753 584.793c24.046 0 42.082-18.036 42.082-42.082v-641.252l-170.332 170.332h-470.919c-24.046 0-42.082 18.036-42.082 42.082v86.168h555.084v384.751h86.168z" />
|
||||||
<glyph unicode="" glyph-name="room" d="M512.5 356.838c58.057 0 106.104 48.047 106.104 106.104s-48.047 106.104-106.104 106.104-106.104-48.047-106.104-106.104 48.047-106.104 106.104-106.104zM512.5 761.233c166.162 0 298.291-132.129 298.291-298.291 0-224.219-298.291-554.542-298.291-554.542s-298.291 330.323-298.291 554.542c0 166.162 132.129 298.291 298.291 298.291z" />
|
<glyph unicode="" glyph-name="room" d="M513.001 350.336c58.114 0 106.208 48.094 106.208 106.208s-48.094 106.208-106.208 106.208-106.208-48.094-106.208-106.208 48.094-106.208 106.208-106.208zM513.001 755.127c166.324 0 298.583-132.258 298.583-298.583 0-224.438-298.583-555.084-298.583-555.084s-298.583 330.646-298.583 555.084c0 166.324 132.258 298.583 298.583 298.583z" />
|
||||||
<glyph unicode="" glyph-name="schedule" d="M534.522 549.025v-224.219l192.188-114.111-32.031-54.053-224.219 136.133v256.25h64.063zM512.5-7.518c188.184 0 342.334 154.151 342.334 342.334s-154.151 342.334-342.334 342.334-342.334-154.151-342.334-342.334 154.151-342.334 342.334-342.334zM512.5 761.233c236.231 0 426.416-190.186 426.416-426.416s-190.186-426.416-426.416-426.416-426.416 190.186-426.416 426.416 190.186 426.416 426.416 426.416z" />
|
<glyph unicode="" glyph-name="schedule" d="M535.045 542.711v-224.438l192.376-114.223-32.062-54.106-224.438 136.266v256.5h64.126zM513.001-14.376c188.368 0 342.669 154.302 342.669 342.669s-154.302 342.669-342.669 342.669-342.669-154.302-342.669-342.669 154.302-342.669 342.669-342.669zM513.001 755.127c236.462 0 426.833-190.372 426.833-426.833s-190.372-426.833-426.833-426.833-426.833 190.372-426.833 426.833 190.372 426.833 426.833 426.833z" />
|
||||||
<glyph unicode="" glyph-name="file-text2" d="M917.806 666.924c-22.212 30.292-53.174 65.7-87.178 99.704s-69.412 64.964-99.704 87.178c-51.574 37.82-76.592 42.194-90.924 42.194h-496c-44.112 0-80-35.888-80-80v-864c0-44.112 35.888-80 80-80h736c44.112 0 80 35.888 80 80v624c0 14.332-4.372 39.35-42.194 90.924zM785.374 721.374c30.7-30.7 54.8-58.398 72.58-81.374h-153.954v153.946c22.984-17.78 50.678-41.878 81.374-72.572zM896-48c0-8.672-7.328-16-16-16h-736c-8.672 0-16 7.328-16 16v864c0 8.672 7.328 16 16 16 0 0 495.956 0.002 496 0v-224c0-17.672 14.326-32 32-32h224v-624zM736 64h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 192h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32zM736 320h-448c-17.672 0-32 14.326-32 32s14.328 32 32 32h448c17.674 0 32-14.326 32-32s-14.326-32-32-32z" />
|
<glyph unicode="" glyph-name="file-text2" d="M918.703 660.725c-22.234 30.322-53.226 65.764-87.263 99.801s-69.48 65.028-99.801 87.263c-51.624 37.857-76.667 42.235-91.013 42.235h-496.485c-44.155 0-80.078-35.923-80.078-80.078v-864.845c0-44.155 35.923-80.078 80.078-80.078h736.719c44.155 0 80.078 35.923 80.078 80.078v624.61c0 14.346-4.376 39.388-42.235 91.013zM786.142 715.229c30.73-30.73 54.854-58.455 72.651-81.454h-154.104v154.096c23.006-17.797 50.728-41.919 81.454-72.643zM896.876-54.897c0-8.68-7.335-16.016-16.016-16.016h-736.719c-8.68 0-16.016 7.335-16.016 16.016v864.845c0 8.68 7.335 16.016 16.016 16.016 0 0 496.441 0.002 496.485 0v-224.219c0-17.689 14.34-32.031 32.031-32.031h224.219v-624.61zM736.719 57.212h-448.438c-17.689 0-32.031 14.34-32.031 32.031s14.342 32.031 32.031 32.031h448.438c17.691 0 32.031-14.34 32.031-32.031s-14.34-32.031-32.031-32.031zM736.719 185.337h-448.438c-17.689 0-32.031 14.34-32.031 32.031s14.342 32.031 32.031 32.031h448.438c17.691 0 32.031-14.34 32.031-32.031s-14.34-32.031-32.031-32.031zM736.719 313.462h-448.438c-17.689 0-32.031 14.34-32.031 32.031s14.342 32.031 32.031 32.031h448.438c17.691 0 32.031-14.34 32.031-32.031s-14.34-32.031-32.031-32.031z" />
|
||||||
|
<glyph unicode="" glyph-name="display" d="M0 832v-640h1024v640h-1024zM960 256h-896v512h896v-512zM672 128h-320l-32-128-64-64h512l-64 64z" />
|
||||||
</font></defs></svg>
|
</font></defs></svg>
|
||||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.2 KiB |
Binary file not shown.
Binary file not shown.
@ -6,6 +6,10 @@ const Peer = jest.fn().mockImplementation(() => {
|
|||||||
(peer as any).destroy = jest.fn();
|
(peer as any).destroy = jest.fn();
|
||||||
(peer as any).signal = jest.fn();
|
(peer as any).signal = jest.fn();
|
||||||
(peer as any).send = jest.fn();
|
(peer as any).send = jest.fn();
|
||||||
|
(peer as any).addTrack = jest.fn();
|
||||||
|
(peer as any).removeTrack = jest.fn();
|
||||||
|
(peer as any).addStream = jest.fn();
|
||||||
|
(peer as any).removeStream = jest.fn();
|
||||||
(Peer as any).instances.push(peer)
|
(Peer as any).instances.push(peer)
|
||||||
return peer
|
return peer
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,6 +32,9 @@ window.navigator.mediaDevices.enumerateDevices = async () => {
|
|||||||
window.navigator.mediaDevices.getUserMedia = async () => {
|
window.navigator.mediaDevices.getUserMedia = async () => {
|
||||||
return {} as any
|
return {} as any
|
||||||
}
|
}
|
||||||
|
(window.navigator.mediaDevices as any).getDisplayMedia = async () => {
|
||||||
|
return {} as any
|
||||||
|
}
|
||||||
|
|
||||||
export const play = jest.fn()
|
export const play = jest.fn()
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,8 @@ describe('CallActions', () => {
|
|||||||
message: 'Connected to server socket',
|
message: 'Connected to server socket',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
type: constants.INIT,
|
||||||
}, {
|
}, {
|
||||||
type: constants.NOTIFY,
|
type: constants.NOTIFY,
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import socket from '../socket'
|
import socket from '../socket'
|
||||||
import { Dispatch, ThunkResult } from '../store'
|
import { ThunkResult } from '../store'
|
||||||
import { callId } from '../window'
|
import { callId } from '../window'
|
||||||
import { ClientSocket } from '../socket'
|
|
||||||
import * as NotifyActions from './NotifyActions'
|
import * as NotifyActions from './NotifyActions'
|
||||||
import * as SocketActions from './SocketActions'
|
import * as SocketActions from './SocketActions'
|
||||||
|
|
||||||
@ -20,23 +19,15 @@ const initialize = (): InitializeAction => ({
|
|||||||
|
|
||||||
export const init = (): ThunkResult<Promise<void>> =>
|
export const init = (): ThunkResult<Promise<void>> =>
|
||||||
async (dispatch, getState) => {
|
async (dispatch, getState) => {
|
||||||
const socket = await dispatch(connect())
|
return new Promise(resolve => {
|
||||||
|
|
||||||
dispatch(SocketActions.handshake({
|
|
||||||
socket,
|
|
||||||
roomName: callId,
|
|
||||||
}))
|
|
||||||
|
|
||||||
dispatch(initialize())
|
|
||||||
}
|
|
||||||
|
|
||||||
export const connect = () => (dispatch: Dispatch) => {
|
|
||||||
return new Promise<ClientSocket>(resolve => {
|
|
||||||
socket.once('connect', () => {
|
|
||||||
resolve(socket)
|
|
||||||
})
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', () => {
|
||||||
dispatch(NotifyActions.warning('Connected to server socket'))
|
dispatch(NotifyActions.warning('Connected to server socket'))
|
||||||
|
dispatch(SocketActions.handshake({
|
||||||
|
socket,
|
||||||
|
roomName: callId,
|
||||||
|
}))
|
||||||
|
dispatch(initialize())
|
||||||
|
resolve()
|
||||||
})
|
})
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
dispatch(NotifyActions.error('Server socket disconnected'))
|
dispatch(NotifyActions.error('Server socket disconnected'))
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { makeAction, AsyncAction } from '../async'
|
import { makeAction, AsyncAction } from '../async'
|
||||||
import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM } from '../constants'
|
import { MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_ENUMERATE, MEDIA_STREAM, ME, STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants'
|
||||||
import _debug from 'debug'
|
import _debug from 'debug'
|
||||||
|
import { AddStreamPayload } from './StreamActions'
|
||||||
|
|
||||||
const debug = _debug('peercalls')
|
const debug = _debug('peercalls')
|
||||||
|
|
||||||
@ -66,6 +67,11 @@ async function getUserMedia(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getDisplayMedia(): Promise<MediaStream> {
|
||||||
|
const mediaDevices = navigator.mediaDevices as any // eslint-disable-line
|
||||||
|
return mediaDevices.getDisplayMedia({video: true, audio: false})
|
||||||
|
}
|
||||||
|
|
||||||
export interface MediaVideoConstraintAction {
|
export interface MediaVideoConstraintAction {
|
||||||
type: 'MEDIA_VIDEO_CONSTRAINT_SET'
|
type: 'MEDIA_VIDEO_CONSTRAINT_SET'
|
||||||
payload: VideoConstraint
|
payload: VideoConstraint
|
||||||
@ -106,12 +112,30 @@ export const getMediaStream = makeAction(
|
|||||||
MEDIA_STREAM,
|
MEDIA_STREAM,
|
||||||
async (constraints: GetMediaConstraints) => {
|
async (constraints: GetMediaConstraints) => {
|
||||||
debug('getMediaStream', constraints)
|
debug('getMediaStream', constraints)
|
||||||
return getUserMedia(constraints)
|
const payload: AddStreamPayload = {
|
||||||
|
stream: await getUserMedia(constraints),
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
userId: ME,
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export const getDesktopStream = makeAction(
|
||||||
|
MEDIA_STREAM,
|
||||||
|
async () => {
|
||||||
|
debug('getDesktopStream')
|
||||||
|
const payload: AddStreamPayload = {
|
||||||
|
stream: await getDisplayMedia(),
|
||||||
|
type: STREAM_TYPE_DESKTOP,
|
||||||
|
userId: ME,
|
||||||
|
}
|
||||||
|
return payload
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
export type MediaEnumerateAction = AsyncAction<'MEDIA_ENUMERATE', MediaDevice[]>
|
||||||
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', MediaStream>
|
export type MediaStreamAction = AsyncAction<'MEDIA_STREAM', AddStreamPayload>
|
||||||
export type MediaPlayAction = AsyncAction<'MEDIA_PLAY', void>
|
export type MediaPlayAction = AsyncAction<'MEDIA_PLAY', void>
|
||||||
|
|
||||||
export type MediaAction =
|
export type MediaAction =
|
||||||
|
|||||||
@ -57,18 +57,31 @@ class PeerHandler {
|
|||||||
const state = getState()
|
const state = getState()
|
||||||
const peer = state.peers[user.id]
|
const peer = state.peers[user.id]
|
||||||
const localStream = state.streams[constants.ME]
|
const localStream = state.streams[constants.ME]
|
||||||
if (localStream && localStream.stream) {
|
localStream && localStream.streams.forEach(s => {
|
||||||
// If the local user pressed join call before this peer has joined the
|
// If the local user pressed join call before this peer has joined the
|
||||||
// call, now is the time to share local media stream with the peer since
|
// call, now is the time to share local media stream with the peer since
|
||||||
// we no longer automatically send the stream to the peer.
|
// we no longer automatically send the stream to the peer.
|
||||||
peer.addStream(localStream.stream)
|
s.stream.getTracks().forEach(track => {
|
||||||
}
|
peer.addTrack(track, s.stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
handleStream = (stream: MediaStream) => {
|
handleTrack = (track: MediaStreamTrack, stream: MediaStream) => {
|
||||||
const { user, dispatch } = this
|
const { user, dispatch } = this
|
||||||
debug('peer: %s, stream', user.id)
|
const userId = user.id
|
||||||
|
debug('peer: %s, track', userId)
|
||||||
|
// Listen to mute event to know when a track was removed
|
||||||
|
// https://github.com/feross/simple-peer/issues/512
|
||||||
|
track.onmute = () => {
|
||||||
|
debug('peer: %s, track muted', userId)
|
||||||
|
dispatch(StreamActions.removeTrack({
|
||||||
|
userId,
|
||||||
|
stream,
|
||||||
|
track,
|
||||||
|
}))
|
||||||
|
}
|
||||||
dispatch(StreamActions.addStream({
|
dispatch(StreamActions.addStream({
|
||||||
userId: user.id,
|
userId,
|
||||||
stream,
|
stream,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -95,10 +108,13 @@ class PeerHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
const { dispatch, user } = this
|
const { dispatch, user, getState } = this
|
||||||
debug('peer: %s, close', user.id)
|
|
||||||
dispatch(NotifyActions.error('Peer connection closed'))
|
dispatch(NotifyActions.error('Peer connection closed'))
|
||||||
dispatch(StreamActions.removeStream(user.id))
|
const state = getState()
|
||||||
|
const userStreams = state.streams[user.id]
|
||||||
|
userStreams && userStreams.streams.forEach(s => {
|
||||||
|
dispatch(StreamActions.removeStream(user.id, s.stream))
|
||||||
|
})
|
||||||
dispatch(removePeer(user.id))
|
dispatch(removePeer(user.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -156,7 +172,7 @@ export function createPeer (options: CreatePeerOptions) {
|
|||||||
peer.once(constants.PEER_EVENT_CONNECT, handler.handleConnect)
|
peer.once(constants.PEER_EVENT_CONNECT, handler.handleConnect)
|
||||||
peer.once(constants.PEER_EVENT_CLOSE, handler.handleClose)
|
peer.once(constants.PEER_EVENT_CLOSE, handler.handleClose)
|
||||||
peer.on(constants.PEER_EVENT_SIGNAL, handler.handleSignal)
|
peer.on(constants.PEER_EVENT_SIGNAL, handler.handleSignal)
|
||||||
peer.on(constants.PEER_EVENT_STREAM, handler.handleStream)
|
peer.on(constants.PEER_EVENT_TRACK, handler.handleTrack)
|
||||||
peer.on(constants.PEER_EVENT_DATA, handler.handleData)
|
peer.on(constants.PEER_EVENT_DATA, handler.handleData)
|
||||||
|
|
||||||
dispatch(addPeer({ peer, userId }))
|
dispatch(addPeer({ peer, userId }))
|
||||||
|
|||||||
@ -136,13 +136,16 @@ describe('SocketActions', () => {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
peer.emit(constants.PEER_EVENT_STREAM, stream)
|
peer.emit(constants.PEER_EVENT_TRACK, stream.getTracks()[0], stream)
|
||||||
|
|
||||||
expect(store.getState().streams).toEqual({
|
expect(store.getState().streams).toEqual({
|
||||||
b: {
|
b: {
|
||||||
userId: 'b',
|
userId: 'b',
|
||||||
stream,
|
streams: [{
|
||||||
url: jasmine.any(String),
|
stream,
|
||||||
|
type: undefined,
|
||||||
|
url: jasmine.any(String),
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -151,12 +154,18 @@ describe('SocketActions', () => {
|
|||||||
describe('close', () => {
|
describe('close', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const stream = new MediaStream()
|
const stream = new MediaStream()
|
||||||
peer.emit(constants.PEER_EVENT_STREAM, stream)
|
const track = {} as unknown as MediaStreamTrack
|
||||||
|
peer.emit(constants.PEER_EVENT_TRACK, track, stream)
|
||||||
|
// test stream with two tracks
|
||||||
|
peer.emit(constants.PEER_EVENT_TRACK, track, stream)
|
||||||
expect(store.getState().streams).toEqual({
|
expect(store.getState().streams).toEqual({
|
||||||
b: {
|
b: {
|
||||||
userId: 'b',
|
userId: 'b',
|
||||||
stream,
|
streams: [{
|
||||||
url: jasmine.any(String),
|
stream,
|
||||||
|
type: undefined,
|
||||||
|
url: jasmine.any(String),
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -88,6 +88,10 @@ export function handshake (options: HandshakeOptions) {
|
|||||||
getState,
|
getState,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// remove listeneres to make seocket reusable
|
||||||
|
socket.removeListener(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
||||||
|
socket.removeListener(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
||||||
|
|
||||||
socket.on(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
socket.on(constants.SOCKET_EVENT_SIGNAL, handler.handleSignal)
|
||||||
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
socket.on(constants.SOCKET_EVENT_USERS, handler.handleUsers)
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
|
|
||||||
|
export type StreamType = 'camera' | 'desktop'
|
||||||
|
|
||||||
export interface AddStreamPayload {
|
export interface AddStreamPayload {
|
||||||
userId: string
|
userId: string
|
||||||
|
type?: StreamType
|
||||||
stream: MediaStream
|
stream: MediaStream
|
||||||
url?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddStreamAction {
|
export interface AddStreamAction {
|
||||||
@ -18,11 +20,16 @@ export interface RemoveStreamAction {
|
|||||||
|
|
||||||
export interface RemoveStreamPayload {
|
export interface RemoveStreamPayload {
|
||||||
userId: string
|
userId: string
|
||||||
|
stream: MediaStream
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetActiveStreamPayload {
|
||||||
|
userId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetActiveStreamAction {
|
export interface SetActiveStreamAction {
|
||||||
type: 'ACTIVE_SET'
|
type: 'ACTIVE_SET'
|
||||||
payload: RemoveStreamPayload
|
payload: SetActiveStreamPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToggleActiveStreamAction {
|
export interface ToggleActiveStreamAction {
|
||||||
@ -30,6 +37,17 @@ export interface ToggleActiveStreamAction {
|
|||||||
payload: UserIdPayload
|
payload: UserIdPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RemoveStreamTrackPayload {
|
||||||
|
userId: string
|
||||||
|
stream: MediaStream
|
||||||
|
track: MediaStreamTrack
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoveStreamTrackAction {
|
||||||
|
type: 'PEER_STREAM_TRACK_REMOVE'
|
||||||
|
payload: RemoveStreamTrackPayload
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserIdPayload {
|
export interface UserIdPayload {
|
||||||
userId: string
|
userId: string
|
||||||
}
|
}
|
||||||
@ -39,9 +57,19 @@ export const addStream = (payload: AddStreamPayload): AddStreamAction => ({
|
|||||||
payload,
|
payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const removeStream = (userId: string): RemoveStreamAction => ({
|
export const removeStream = (
|
||||||
|
userId: string,
|
||||||
|
stream: MediaStream,
|
||||||
|
): RemoveStreamAction => ({
|
||||||
type: constants.STREAM_REMOVE,
|
type: constants.STREAM_REMOVE,
|
||||||
payload: { userId },
|
payload: { userId, stream },
|
||||||
|
})
|
||||||
|
|
||||||
|
export const removeTrack = (
|
||||||
|
payload: RemoveStreamTrackPayload,
|
||||||
|
): RemoveStreamTrackAction => ({
|
||||||
|
type: constants.STREAM_TRACK_REMOVE,
|
||||||
|
payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setActive = (userId: string): SetActiveStreamAction => ({
|
export const setActive = (userId: string): SetActiveStreamAction => ({
|
||||||
@ -58,4 +86,5 @@ export type StreamAction =
|
|||||||
AddStreamAction |
|
AddStreamAction |
|
||||||
RemoveStreamAction |
|
RemoveStreamAction |
|
||||||
SetActiveStreamAction |
|
SetActiveStreamAction |
|
||||||
ToggleActiveStreamAction
|
ToggleActiveStreamAction |
|
||||||
|
RemoveStreamTrackAction
|
||||||
|
|||||||
@ -14,6 +14,15 @@ export type RejectedAction<T extends string> = Action<T> & {
|
|||||||
status: 'rejected'
|
status: 'rejected'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isRejectedAction(
|
||||||
|
value: unknown,
|
||||||
|
): value is RejectedAction<string> {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const v = value as any
|
||||||
|
return !!v && 'type' in v && typeof v.type === 'string' &&
|
||||||
|
'status' in v && v.status === 'rejected'
|
||||||
|
}
|
||||||
|
|
||||||
export type AsyncAction<T extends string, P> =
|
export type AsyncAction<T extends string, P> =
|
||||||
PendingAction<T, P> |
|
PendingAction<T, P> |
|
||||||
ResolvedAction<T, P> |
|
ResolvedAction<T, P> |
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Peer from 'simple-peer'
|
|||||||
import { Message } from '../actions/ChatActions'
|
import { Message } from '../actions/ChatActions'
|
||||||
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
import { dismissNotification, Notification } from '../actions/NotifyActions'
|
||||||
import { TextMessage } from '../actions/PeerActions'
|
import { TextMessage } from '../actions/PeerActions'
|
||||||
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
import { removeStream } from '../actions/StreamActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
import { Media } from './Media'
|
import { Media } from './Media'
|
||||||
@ -13,6 +13,8 @@ import Notifications from './Notifications'
|
|||||||
import { Side } from './Side'
|
import { Side } from './Side'
|
||||||
import Toolbar from './Toolbar'
|
import Toolbar from './Toolbar'
|
||||||
import Video from './Video'
|
import Video from './Video'
|
||||||
|
import { getDesktopStream } from '../actions/MediaActions'
|
||||||
|
import { StreamsState } from '../reducers/streams'
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
active: string | null
|
active: string | null
|
||||||
@ -24,7 +26,8 @@ export interface AppProps {
|
|||||||
peers: Record<string, Peer.Instance>
|
peers: Record<string, Peer.Instance>
|
||||||
play: () => void
|
play: () => void
|
||||||
sendMessage: (message: TextMessage) => void
|
sendMessage: (message: TextMessage) => void
|
||||||
streams: Record<string, AddStreamPayload>
|
streams: StreamsState
|
||||||
|
getDesktopStream: typeof getDesktopStream
|
||||||
removeStream: typeof removeStream
|
removeStream: typeof removeStream
|
||||||
onSendFile: (file: File) => void
|
onSendFile: (file: File) => void
|
||||||
toggleActive: (userId: string) => void
|
toggleActive: (userId: string) => void
|
||||||
@ -60,7 +63,16 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
onHangup = () => {
|
onHangup = () => {
|
||||||
this.props.removeStream(constants.ME)
|
const localStreams = this.getLocalStreams()
|
||||||
|
localStreams.streams.forEach(s => {
|
||||||
|
this.props.removeStream(constants.ME, s.stream)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
getLocalStreams() {
|
||||||
|
return this.props.streams[constants.ME] || {
|
||||||
|
userId: constants.ME,
|
||||||
|
streams: [],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
const {
|
const {
|
||||||
@ -83,6 +95,8 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
'chat-visible': this.state.chatVisible,
|
'chat-visible': this.state.chatVisible,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const localStreams = this.getLocalStreams()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Side align='flex-end' left zIndex={2}>
|
<Side align='flex-end' left zIndex={2}>
|
||||||
@ -92,7 +106,16 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
onToggleChat={this.handleToggleChat}
|
onToggleChat={this.handleToggleChat}
|
||||||
onSendFile={onSendFile}
|
onSendFile={onSendFile}
|
||||||
onHangup={this.onHangup}
|
onHangup={this.onHangup}
|
||||||
stream={streams[constants.ME]}
|
stream={
|
||||||
|
localStreams.streams
|
||||||
|
.filter(s => s.type === constants.STREAM_TYPE_CAMERA)[0]
|
||||||
|
}
|
||||||
|
desktopStream={
|
||||||
|
localStreams.streams
|
||||||
|
.filter(s => s.type === constants.STREAM_TYPE_DESKTOP)[0]
|
||||||
|
}
|
||||||
|
onGetDesktopStream={this.props.getDesktopStream}
|
||||||
|
onRemoveStream={this.props.removeStream}
|
||||||
/>
|
/>
|
||||||
</Side>
|
</Side>
|
||||||
<Side className={chatVisibleClassName} top zIndex={1}>
|
<Side className={chatVisibleClassName} top zIndex={1}>
|
||||||
@ -109,33 +132,43 @@ export default class App extends React.PureComponent<AppProps, AppState> {
|
|||||||
visible={this.state.chatVisible}
|
visible={this.state.chatVisible}
|
||||||
/>
|
/>
|
||||||
<div className={classnames('videos', chatVisibleClassName)}>
|
<div className={classnames('videos', chatVisibleClassName)}>
|
||||||
{streams[constants.ME] && (
|
{localStreams.streams.map((s, i) => {
|
||||||
<Video
|
const key = localStreams.userId + '_' + i
|
||||||
videos={videos}
|
return (
|
||||||
active={active === constants.ME}
|
<Video
|
||||||
onClick={toggleActive}
|
videos={videos}
|
||||||
play={play}
|
key={key}
|
||||||
stream={streams[constants.ME]}
|
active={active === key}
|
||||||
userId={constants.ME}
|
onClick={toggleActive}
|
||||||
muted
|
play={play}
|
||||||
mirrored
|
stream={s}
|
||||||
/>
|
userId={key}
|
||||||
)}
|
muted
|
||||||
|
mirrored={s.type === 'camera'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
{
|
{
|
||||||
map(peers, (_, userId) => userId)
|
map(peers, (_, userId) => userId)
|
||||||
.filter(stream => !!stream)
|
.filter(stream => !!stream)
|
||||||
.map(userId =>
|
.map(userId => streams[userId])
|
||||||
<Video
|
.filter(userStreams => !!userStreams)
|
||||||
active={userId === active}
|
.map(userStreams => {
|
||||||
key={userId}
|
return userStreams.streams.map((s, i) => {
|
||||||
onClick={toggleActive}
|
const key = userStreams.userId + '_' + i
|
||||||
play={play}
|
return (
|
||||||
stream={streams[userId]}
|
<Video
|
||||||
userId={userId}
|
active={key === active}
|
||||||
videos={videos}
|
key={key}
|
||||||
/>,
|
onClick={toggleActive}
|
||||||
)
|
play={play}
|
||||||
|
stream={s}
|
||||||
|
userId={key}
|
||||||
|
videos={videos}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { MediaState } from '../reducers/media'
|
|||||||
import { State } from '../store'
|
import { State } from '../store'
|
||||||
import { Alerts, Alert } from './Alerts'
|
import { Alerts, Alert } from './Alerts'
|
||||||
import { info, warning, error } from '../actions/NotifyActions'
|
import { info, warning, error } from '../actions/NotifyActions'
|
||||||
import { ME } from '../constants'
|
import { ME, STREAM_TYPE_CAMERA } from '../constants'
|
||||||
|
|
||||||
export type MediaProps = MediaState & {
|
export type MediaProps = MediaState & {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
@ -21,7 +21,9 @@ export type MediaProps = MediaState & {
|
|||||||
|
|
||||||
function mapStateToProps(state: State) {
|
function mapStateToProps(state: State) {
|
||||||
const localStream = state.streams[ME]
|
const localStream = state.streams[ME]
|
||||||
const visible = !localStream
|
const hidden = !!localStream &&
|
||||||
|
localStream.streams.filter(s => s.type === STREAM_TYPE_CAMERA).length > 0
|
||||||
|
const visible = !hidden
|
||||||
return {
|
return {
|
||||||
...state.media,
|
...state.media,
|
||||||
visible,
|
visible,
|
||||||
|
|||||||
@ -2,9 +2,12 @@ jest.mock('../window')
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import Toolbar, { ToolbarProps } from './Toolbar'
|
import { getDesktopStream } from '../actions/MediaActions'
|
||||||
|
import { AddStreamPayload, removeStream } from '../actions/StreamActions'
|
||||||
|
import { STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants'
|
||||||
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
import { MediaStream } from '../window'
|
import { MediaStream } from '../window'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
import Toolbar, { ToolbarProps } from './Toolbar'
|
||||||
|
|
||||||
describe('components/Toolbar', () => {
|
describe('components/Toolbar', () => {
|
||||||
|
|
||||||
@ -15,15 +18,19 @@ describe('components/Toolbar', () => {
|
|||||||
class ToolbarWrapper extends React.PureComponent<ToolbarProps, StreamState> {
|
class ToolbarWrapper extends React.PureComponent<ToolbarProps, StreamState> {
|
||||||
state = {
|
state = {
|
||||||
stream: null,
|
stream: null,
|
||||||
|
desktopStream: null,
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return <Toolbar
|
return <Toolbar
|
||||||
chatVisible={this.props.chatVisible}
|
chatVisible={this.props.chatVisible}
|
||||||
onToggleChat={this.props.onToggleChat}
|
onToggleChat={this.props.onToggleChat}
|
||||||
onHangup={this.props.onHangup}
|
onHangup={this.props.onHangup}
|
||||||
|
onGetDesktopStream={this.props.onGetDesktopStream}
|
||||||
|
onRemoveStream={this.props.onRemoveStream}
|
||||||
onSendFile={this.props.onSendFile}
|
onSendFile={this.props.onSendFile}
|
||||||
messagesCount={this.props.messagesCount}
|
messagesCount={this.props.messagesCount}
|
||||||
stream={this.state.stream || this.props.stream}
|
stream={this.state.stream || this.props.stream}
|
||||||
|
desktopStream={this.state.desktopStream || this.props.desktopStream}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,12 +41,22 @@ describe('components/Toolbar', () => {
|
|||||||
let onToggleChat: jest.Mock<() => void>
|
let onToggleChat: jest.Mock<() => void>
|
||||||
let onSendFile: jest.Mock<(file: File) => void>
|
let onSendFile: jest.Mock<(file: File) => void>
|
||||||
let onHangup: jest.Mock<() => void>
|
let onHangup: jest.Mock<() => void>
|
||||||
|
let onGetDesktopStream: jest.MockedFunction<typeof getDesktopStream>
|
||||||
|
let onRemoveStream: jest.MockedFunction<typeof removeStream>
|
||||||
|
let desktopStream: StreamWithURL | undefined
|
||||||
async function render () {
|
async function render () {
|
||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
onToggleChat = jest.fn()
|
onToggleChat = jest.fn()
|
||||||
onSendFile = jest.fn()
|
onSendFile = jest.fn()
|
||||||
onHangup = jest.fn()
|
onHangup = jest.fn()
|
||||||
|
onGetDesktopStream = jest.fn().mockImplementation(() => Promise.resolve())
|
||||||
|
onRemoveStream = jest.fn()
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
const stream: StreamWithURL = {
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
url,
|
||||||
|
}
|
||||||
await new Promise<ToolbarWrapper>(resolve => {
|
await new Promise<ToolbarWrapper>(resolve => {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<ToolbarWrapper
|
<ToolbarWrapper
|
||||||
@ -49,7 +66,10 @@ describe('components/Toolbar', () => {
|
|||||||
onToggleChat={onToggleChat}
|
onToggleChat={onToggleChat}
|
||||||
onSendFile={onSendFile}
|
onSendFile={onSendFile}
|
||||||
messagesCount={1}
|
messagesCount={1}
|
||||||
stream={{ userId: '', stream: mediaStream, url }}
|
stream={stream}
|
||||||
|
desktopStream={desktopStream}
|
||||||
|
onGetDesktopStream={onGetDesktopStream}
|
||||||
|
onRemoveStream={onRemoveStream}
|
||||||
/>,
|
/>,
|
||||||
div,
|
div,
|
||||||
)
|
)
|
||||||
@ -136,4 +156,26 @@ describe('components/Toolbar', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('desktop sharing', () => {
|
||||||
|
it('starts desktop sharing', async () => {
|
||||||
|
const shareDesktop = node.querySelector('.stream-desktop')!
|
||||||
|
expect(shareDesktop).toBeDefined()
|
||||||
|
TestUtils.Simulate.click(shareDesktop)
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(onGetDesktopStream.mock.calls.length).toBe(1)
|
||||||
|
})
|
||||||
|
it('stops desktop sharing', async () => {
|
||||||
|
desktopStream = {
|
||||||
|
stream: new MediaStream(),
|
||||||
|
type: STREAM_TYPE_DESKTOP,
|
||||||
|
}
|
||||||
|
await render()
|
||||||
|
const shareDesktop = node.querySelector('.stream-desktop')!
|
||||||
|
expect(shareDesktop).toBeDefined()
|
||||||
|
TestUtils.Simulate.click(shareDesktop)
|
||||||
|
await Promise.resolve()
|
||||||
|
expect(onRemoveStream.mock.calls.length).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import screenfull from 'screenfull'
|
import screenfull from 'screenfull'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
import { removeStream } from '../actions/StreamActions'
|
||||||
|
import { getDesktopStream } from '../actions/MediaActions'
|
||||||
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
|
import { ME } from '../constants'
|
||||||
|
|
||||||
const hidden = {
|
const hidden = {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
@ -9,8 +12,11 @@ const hidden = {
|
|||||||
|
|
||||||
export interface ToolbarProps {
|
export interface ToolbarProps {
|
||||||
messagesCount: number
|
messagesCount: number
|
||||||
stream: AddStreamPayload
|
stream: StreamWithURL
|
||||||
|
desktopStream: StreamWithURL | undefined
|
||||||
onToggleChat: () => void
|
onToggleChat: () => void
|
||||||
|
onGetDesktopStream: typeof getDesktopStream
|
||||||
|
onRemoveStream: typeof removeStream
|
||||||
onSendFile: (file: File) => void
|
onSendFile: (file: File) => void
|
||||||
onHangup: () => void
|
onHangup: () => void
|
||||||
chatVisible: boolean
|
chatVisible: boolean
|
||||||
@ -113,6 +119,13 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
})
|
})
|
||||||
this.props.onToggleChat()
|
this.props.onToggleChat()
|
||||||
}
|
}
|
||||||
|
handleToggleShareDesktop = () => {
|
||||||
|
if (this.props.desktopStream) {
|
||||||
|
this.props.onRemoveStream(ME, this.props.desktopStream.stream)
|
||||||
|
} else {
|
||||||
|
this.props.onGetDesktopStream().catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
const { messagesCount, stream } = this.props
|
const { messagesCount, stream } = this.props
|
||||||
const unreadCount = messagesCount - this.state.readMessages
|
const unreadCount = messagesCount - this.state.readMessages
|
||||||
@ -145,6 +158,14 @@ extends React.PureComponent<ToolbarProps, ToolbarState> {
|
|||||||
title='Send File'
|
title='Send File'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ToolbarButton
|
||||||
|
className='stream-desktop'
|
||||||
|
icon='icon-display'
|
||||||
|
onClick={this.handleToggleShareDesktop}
|
||||||
|
on={!!this.props.desktopStream}
|
||||||
|
title='Share Desktop'
|
||||||
|
/>
|
||||||
|
|
||||||
{stream && (
|
{stream && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
|
|||||||
@ -2,14 +2,15 @@ jest.mock('../window')
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import TestUtils from 'react-dom/test-utils'
|
import TestUtils from 'react-dom/test-utils'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
|
||||||
import Video, { VideoProps } from './Video'
|
import Video, { VideoProps } from './Video'
|
||||||
import { MediaStream } from '../window'
|
import { MediaStream } from '../window'
|
||||||
|
import { STREAM_TYPE_CAMERA } from '../constants'
|
||||||
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
|
|
||||||
describe('components/Video', () => {
|
describe('components/Video', () => {
|
||||||
|
|
||||||
interface VideoState {
|
interface VideoState {
|
||||||
stream: null | AddStreamPayload
|
stream: null | StreamWithURL
|
||||||
}
|
}
|
||||||
|
|
||||||
const play = jest.fn()
|
const play = jest.fn()
|
||||||
@ -61,12 +62,17 @@ describe('components/Video', () => {
|
|||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
component = await new Promise<VideoWrapper>(resolve => {
|
component = await new Promise<VideoWrapper>(resolve => {
|
||||||
|
const stream: StreamWithURL = {
|
||||||
|
stream: mediaStream,
|
||||||
|
url,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
}
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<VideoWrapper
|
<VideoWrapper
|
||||||
ref={instance => resolve(instance!)}
|
ref={instance => resolve(instance!)}
|
||||||
videos={videos}
|
videos={videos}
|
||||||
active={flags.active}
|
active={flags.active}
|
||||||
stream={{ stream: mediaStream, url, userId: 'test' }}
|
stream={stream}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
play={play}
|
play={play}
|
||||||
userId="test"
|
userId="test"
|
||||||
@ -100,22 +106,38 @@ describe('components/Video', () => {
|
|||||||
it('updates src only when changed', () => {
|
it('updates src only when changed', () => {
|
||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
component.setState({
|
component.setState({
|
||||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
stream: {
|
||||||
|
url: 'test',
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(video.videoRef.current!.src).toBe('http://localhost/test')
|
expect(video.videoRef.current!.src).toBe('http://localhost/test')
|
||||||
component.setState({
|
component.setState({
|
||||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
stream: {
|
||||||
|
url: 'test',
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('updates srcObject only when changed', () => {
|
it('updates srcObject only when changed', () => {
|
||||||
video.videoRef.current!.srcObject = null
|
video.videoRef.current!.srcObject = null
|
||||||
mediaStream = new MediaStream()
|
mediaStream = new MediaStream()
|
||||||
component.setState({
|
component.setState({
|
||||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
stream: {
|
||||||
|
url: 'test',
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(video.videoRef.current!.srcObject).toBe(mediaStream)
|
expect(video.videoRef.current!.srcObject).toBe(mediaStream)
|
||||||
component.setState({
|
component.setState({
|
||||||
stream: { url: 'test', stream: mediaStream, userId: '' },
|
stream: {
|
||||||
|
url: 'test',
|
||||||
|
stream: mediaStream,
|
||||||
|
type: STREAM_TYPE_CAMERA,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import React, { ReactEventHandler } from 'react'
|
import React, { ReactEventHandler } from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import socket from '../socket'
|
import socket from '../socket'
|
||||||
import { AddStreamPayload } from '../actions/StreamActions'
|
import { StreamWithURL } from '../reducers/streams'
|
||||||
|
|
||||||
export interface VideoProps {
|
export interface VideoProps {
|
||||||
videos: Record<string, unknown>
|
videos: Record<string, unknown>
|
||||||
onClick: (userId: string) => void
|
onClick: (userId: string) => void
|
||||||
active: boolean
|
active: boolean
|
||||||
stream?: AddStreamPayload
|
stream?: StreamWithURL
|
||||||
userId: string
|
userId: string
|
||||||
muted: boolean
|
muted: boolean
|
||||||
mirrored: boolean
|
mirrored: boolean
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const PEER_EVENT_ERROR = 'error'
|
|||||||
export const PEER_EVENT_CONNECT = 'connect'
|
export const PEER_EVENT_CONNECT = 'connect'
|
||||||
export const PEER_EVENT_CLOSE = 'close'
|
export const PEER_EVENT_CLOSE = 'close'
|
||||||
export const PEER_EVENT_SIGNAL = 'signal'
|
export const PEER_EVENT_SIGNAL = 'signal'
|
||||||
export const PEER_EVENT_STREAM = 'stream'
|
export const PEER_EVENT_TRACK = 'track'
|
||||||
export const PEER_EVENT_DATA = 'data'
|
export const PEER_EVENT_DATA = 'data'
|
||||||
|
|
||||||
export const SOCKET_EVENT_SIGNAL = 'signal'
|
export const SOCKET_EVENT_SIGNAL = 'signal'
|
||||||
@ -37,3 +37,7 @@ export const SOCKET_EVENT_USERS = 'users'
|
|||||||
|
|
||||||
export const STREAM_ADD = 'PEER_STREAM_ADD'
|
export const STREAM_ADD = 'PEER_STREAM_ADD'
|
||||||
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'
|
export const STREAM_REMOVE = 'PEER_STREAM_REMOVE'
|
||||||
|
export const STREAM_TRACK_REMOVE = 'PEER_STREAM_TRACK_REMOVE'
|
||||||
|
|
||||||
|
export const STREAM_TYPE_CAMERA = 'camera'
|
||||||
|
export const STREAM_TYPE_DESKTOP = 'desktop'
|
||||||
|
|||||||
@ -79,13 +79,19 @@ describe('App', () => {
|
|||||||
state.streams = {
|
state.streams = {
|
||||||
[constants.ME]: {
|
[constants.ME]: {
|
||||||
userId: constants.ME,
|
userId: constants.ME,
|
||||||
stream: new MediaStream(),
|
streams: [{
|
||||||
url: 'blob://',
|
stream: new MediaStream(),
|
||||||
|
type: constants.STREAM_TYPE_CAMERA,
|
||||||
|
url: 'blob://',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
'other-user': {
|
'other-user': {
|
||||||
userId: 'other-user',
|
userId: 'other-user',
|
||||||
stream: new MediaStream(),
|
streams: [{
|
||||||
url: 'blob://',
|
stream: new MediaStream(),
|
||||||
|
type: undefined,
|
||||||
|
url: 'blob://',
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
state.peers = {
|
state.peers = {
|
||||||
@ -109,7 +115,7 @@ describe('App', () => {
|
|||||||
expect(dispatchSpy.mock.calls[0][0].type).toBe(constants.MEDIA_PLAY)
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(constants.MEDIA_PLAY)
|
||||||
expect(dispatchSpy.mock.calls.slice(1)).toEqual([[{
|
expect(dispatchSpy.mock.calls.slice(1)).toEqual([[{
|
||||||
type: constants.ACTIVE_TOGGLE,
|
type: constants.ACTIVE_TOGGLE,
|
||||||
payload: { userId: constants.ME },
|
payload: { userId: constants.ME + '_0' },
|
||||||
}]])
|
}]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import { init } from '../actions/CallActions'
|
import { init } from '../actions/CallActions'
|
||||||
import { play } from '../actions/MediaActions'
|
import { getDesktopStream, play } from '../actions/MediaActions'
|
||||||
import { dismissNotification } from '../actions/NotifyActions'
|
import { dismissNotification } from '../actions/NotifyActions'
|
||||||
import { sendFile, sendMessage } from '../actions/PeerActions'
|
import { sendFile, sendMessage } from '../actions/PeerActions'
|
||||||
import { toggleActive, removeStream } from '../actions/StreamActions'
|
import { toggleActive, removeStream } from '../actions/StreamActions'
|
||||||
@ -22,6 +22,7 @@ const mapDispatchToProps = {
|
|||||||
toggleActive,
|
toggleActive,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
dismissNotification,
|
dismissNotification,
|
||||||
|
getDesktopStream,
|
||||||
removeStream,
|
removeStream,
|
||||||
init,
|
init,
|
||||||
onSendFile: sendFile,
|
onSendFile: sendFile,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
jest.mock('simple-peer')
|
||||||
|
|
||||||
import * as MediaActions from '../actions/MediaActions'
|
import * as MediaActions from '../actions/MediaActions'
|
||||||
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD } from '../constants'
|
import { MEDIA_ENUMERATE, MEDIA_VIDEO_CONSTRAINT_SET, MEDIA_AUDIO_CONSTRAINT_SET, MEDIA_STREAM, ME, PEERS_DESTROY, PEER_ADD, STREAM_TYPE_CAMERA, STREAM_TYPE_DESKTOP } from '../constants'
|
||||||
import { createStore, Store } from '../store'
|
import { createStore, Store } from '../store'
|
||||||
import SimplePeer from 'simple-peer'
|
import SimplePeer from 'simple-peer'
|
||||||
|
|
||||||
@ -89,7 +91,12 @@ describe('media', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe(MEDIA_STREAM, () => {
|
describe(MEDIA_STREAM, () => {
|
||||||
const stream: MediaStream = {} as MediaStream
|
const track: MediaStreamTrack = {} as unknown as MediaStreamTrack
|
||||||
|
const stream: MediaStream = {
|
||||||
|
getTracks() {
|
||||||
|
return [track]
|
||||||
|
},
|
||||||
|
} as MediaStream
|
||||||
describe('using navigator.mediaDevices.getUserMedia', () => {
|
describe('using navigator.mediaDevices.getUserMedia', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -97,29 +104,30 @@ describe('media', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function dispatch() {
|
async function dispatch() {
|
||||||
const promise = store.dispatch(MediaActions.getMediaStream({
|
const result = await store.dispatch(MediaActions.getMediaStream({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: true,
|
video: true,
|
||||||
}))
|
}))
|
||||||
expect(await promise).toBe(stream)
|
expect(result.stream).toBe(stream)
|
||||||
|
expect(result.type).toBe(STREAM_TYPE_CAMERA)
|
||||||
|
expect(result.userId).toBe(ME)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('reducers/streams', () => {
|
describe('reducers/streams', () => {
|
||||||
it('adds the local stream to the map of videos', async () => {
|
it('adds the local stream to the map of videos', async () => {
|
||||||
expect(store.getState().streams[ME]).toBeFalsy()
|
expect(store.getState().streams[ME]).toBeFalsy()
|
||||||
await dispatch()
|
await dispatch()
|
||||||
expect(store.getState().streams[ME]).toBeTruthy()
|
const localStreams = store.getState().streams[ME]
|
||||||
expect(store.getState().streams[ME].stream).toBe(stream)
|
expect(localStreams).toBeTruthy()
|
||||||
|
expect(localStreams.streams.length).toBe(1)
|
||||||
|
expect(localStreams.streams[0].type).toBe(STREAM_TYPE_CAMERA)
|
||||||
|
expect(localStreams.streams[0].stream).toBe(stream)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('reducers/peers', () => {
|
describe('reducers/peers', () => {
|
||||||
const peer1 = new SimplePeer()
|
const peer1 = new SimplePeer()
|
||||||
peer1.addStream = jest.fn()
|
|
||||||
peer1.removeStream = jest.fn()
|
|
||||||
const peer2 = new SimplePeer()
|
const peer2 = new SimplePeer()
|
||||||
peer2.addStream = jest.fn()
|
|
||||||
peer2.removeStream = jest.fn()
|
|
||||||
const peers = [peer1, peer2]
|
const peers = [peer1, peer2]
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -148,19 +156,19 @@ describe('media', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replaces local stream on all peers', async () => {
|
it('adds local camera stream to all peers', async () => {
|
||||||
await dispatch()
|
await dispatch()
|
||||||
peers.forEach(peer => {
|
peers.forEach(peer => {
|
||||||
expect((peer.addStream as jest.Mock).mock.calls)
|
expect((peer.addTrack as jest.Mock).mock.calls)
|
||||||
.toEqual([[ stream ]])
|
.toEqual([[ track, stream ]])
|
||||||
expect((peer.removeStream as jest.Mock).mock.calls).toEqual([])
|
expect((peer.removeTrack as any).mock.calls).toEqual([])
|
||||||
})
|
})
|
||||||
await dispatch()
|
await dispatch()
|
||||||
peers.forEach(peer => {
|
peers.forEach(peer => {
|
||||||
expect((peer.addStream as jest.Mock).mock.calls)
|
expect((peer.addTrack as jest.Mock).mock.calls)
|
||||||
.toEqual([[ stream ], [ stream ]])
|
.toEqual([[ track, stream ], [ track, stream ]])
|
||||||
expect((peer.removeStream as jest.Mock).mock.calls)
|
expect((peer.removeTrack as jest.Mock).mock.calls)
|
||||||
.toEqual([[ stream ]])
|
.toEqual([[ track, stream ]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -183,10 +191,34 @@ describe('media', () => {
|
|||||||
})
|
})
|
||||||
expect(promise.type).toBe('MEDIA_STREAM')
|
expect(promise.type).toBe('MEDIA_STREAM')
|
||||||
expect(promise.status).toBe('pending')
|
expect(promise.status).toBe('pending')
|
||||||
expect(await promise).toBe(stream)
|
const result = await promise
|
||||||
|
expect(result.stream).toBe(stream)
|
||||||
|
expect(result.userId).toBe(ME)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getDesktopStream (getDisplayMedia)', () => {
|
||||||
|
const stream: MediaStream = {} as MediaStream
|
||||||
|
beforeEach(() => {
|
||||||
|
(navigator.mediaDevices as any).getDisplayMedia = async () => stream
|
||||||
|
})
|
||||||
|
async function dispatch() {
|
||||||
|
const result = await store.dispatch(MediaActions.getDesktopStream())
|
||||||
|
expect(result.stream).toBe(stream)
|
||||||
|
expect(result.type).toBe(STREAM_TYPE_DESKTOP)
|
||||||
|
expect(result.userId).toBe(ME)
|
||||||
|
}
|
||||||
|
it('adds the local stream to the map of videos', async () => {
|
||||||
|
expect(store.getState().streams[ME]).toBeFalsy()
|
||||||
|
await dispatch()
|
||||||
|
const localStreams = store.getState().streams[ME]
|
||||||
|
expect(localStreams).toBeTruthy()
|
||||||
|
expect(localStreams.streams.length).toBe(1)
|
||||||
|
expect(localStreams.streams[0].type).toBe(STREAM_TYPE_DESKTOP)
|
||||||
|
expect(localStreams.streams[0].stream).toBe(stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import { Notification, NotificationActionType } from '../actions/NotifyActions'
|
import { error, Notification, NotificationActionType } from '../actions/NotifyActions'
|
||||||
|
import { isRejectedAction } from '../async'
|
||||||
|
import { AnyAction } from 'redux'
|
||||||
|
|
||||||
export type NotificationState = Record<string, Notification>
|
export type NotificationState = Record<string, Notification>
|
||||||
|
|
||||||
@ -7,7 +9,17 @@ const defaultState: NotificationState = {}
|
|||||||
|
|
||||||
export default function notifications (
|
export default function notifications (
|
||||||
state = defaultState,
|
state = defaultState,
|
||||||
action: NotificationActionType,
|
action: AnyAction,
|
||||||
|
) {
|
||||||
|
if (isRejectedAction(action)) {
|
||||||
|
action = error('' + action.payload)
|
||||||
|
}
|
||||||
|
return handleNotifications(state, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNotifications (
|
||||||
|
state = defaultState,
|
||||||
|
action: NotificationActionType,
|
||||||
) {
|
) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case constants.NOTIFY:
|
case constants.NOTIFY:
|
||||||
|
|||||||
@ -4,16 +4,63 @@ import Peer from 'simple-peer'
|
|||||||
import { PeerAction } from '../actions/PeerActions'
|
import { PeerAction } from '../actions/PeerActions'
|
||||||
import * as constants from '../constants'
|
import * as constants from '../constants'
|
||||||
import { MediaStreamAction } from '../actions/MediaActions'
|
import { MediaStreamAction } from '../actions/MediaActions'
|
||||||
|
import { RemoveStreamAction, StreamType } from '../actions/StreamActions'
|
||||||
|
|
||||||
export type PeersState = Record<string, Peer.Instance>
|
export type PeersState = Record<string, Peer.Instance>
|
||||||
|
|
||||||
const defaultState: PeersState = {}
|
const defaultState: PeersState = {}
|
||||||
|
|
||||||
let localStream: MediaStream | undefined
|
let localStreams: Record<StreamType, MediaStream | undefined> = {
|
||||||
|
camera: undefined,
|
||||||
|
desktop: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveStream(
|
||||||
|
state: PeersState,
|
||||||
|
action: RemoveStreamAction,
|
||||||
|
): PeersState {
|
||||||
|
const stream = action.payload.stream
|
||||||
|
if (action.payload.userId === constants.ME) {
|
||||||
|
forEach(state, peer => {
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
peer.removeTrack(track, stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMediaStream(
|
||||||
|
state: PeersState,
|
||||||
|
action: MediaStreamAction,
|
||||||
|
): PeersState {
|
||||||
|
if (action.status !== 'resolved') {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
const streamType = action.payload.type
|
||||||
|
if (
|
||||||
|
action.payload.userId === constants.ME &&
|
||||||
|
streamType
|
||||||
|
) {
|
||||||
|
forEach(state, peer => {
|
||||||
|
const localStream = localStreams[streamType]
|
||||||
|
localStream && localStream.getTracks().forEach(track => {
|
||||||
|
peer.removeTrack(track, localStream)
|
||||||
|
})
|
||||||
|
const stream = action.payload.stream
|
||||||
|
stream.getTracks().forEach(track => {
|
||||||
|
peer.addTrack(track, stream)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
localStreams[streamType] = action.payload.stream
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
export default function peers(
|
export default function peers(
|
||||||
state = defaultState,
|
state = defaultState,
|
||||||
action: PeerAction | MediaStreamAction,
|
action: PeerAction | MediaStreamAction | RemoveStreamAction,
|
||||||
): PeersState {
|
): PeersState {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case constants.PEER_ADD:
|
case constants.PEER_ADD:
|
||||||
@ -24,18 +71,16 @@ export default function peers(
|
|||||||
case constants.PEER_REMOVE:
|
case constants.PEER_REMOVE:
|
||||||
return omit(state, [action.payload.userId])
|
return omit(state, [action.payload.userId])
|
||||||
case constants.PEERS_DESTROY:
|
case constants.PEERS_DESTROY:
|
||||||
localStream = undefined
|
localStreams = {
|
||||||
|
camera: undefined,
|
||||||
|
desktop: undefined,
|
||||||
|
}
|
||||||
forEach(state, peer => peer.destroy())
|
forEach(state, peer => peer.destroy())
|
||||||
return defaultState
|
return defaultState
|
||||||
|
case constants.STREAM_REMOVE:
|
||||||
|
return handleRemoveStream(state, action)
|
||||||
case constants.MEDIA_STREAM:
|
case constants.MEDIA_STREAM:
|
||||||
if (action.status === 'resolved') {
|
return handleMediaStream(state, action)
|
||||||
forEach(state, peer => {
|
|
||||||
localStream && peer.removeStream(localStream)
|
|
||||||
peer.addStream(action.payload)
|
|
||||||
})
|
|
||||||
localStream = action.payload
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,8 +35,11 @@ describe('reducers/alerts', () => {
|
|||||||
expect(store.getState().streams).toEqual({
|
expect(store.getState().streams).toEqual({
|
||||||
[userId]: {
|
[userId]: {
|
||||||
userId,
|
userId,
|
||||||
stream: stream,
|
streams: [{
|
||||||
url: jasmine.any(String),
|
stream,
|
||||||
|
url: jasmine.any(String),
|
||||||
|
type: undefined,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -47,8 +50,11 @@ describe('reducers/alerts', () => {
|
|||||||
expect(store.getState().streams).toEqual({
|
expect(store.getState().streams).toEqual({
|
||||||
[userId]: {
|
[userId]: {
|
||||||
userId,
|
userId,
|
||||||
stream: stream,
|
streams: [{
|
||||||
url: undefined,
|
stream,
|
||||||
|
type: undefined,
|
||||||
|
url: undefined,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -57,12 +63,16 @@ describe('reducers/alerts', () => {
|
|||||||
describe('removeStream', () => {
|
describe('removeStream', () => {
|
||||||
it('removes a stream', () => {
|
it('removes a stream', () => {
|
||||||
store.dispatch(StreamActions.addStream({ userId, stream }))
|
store.dispatch(StreamActions.addStream({ userId, stream }))
|
||||||
store.dispatch(StreamActions.removeStream(userId))
|
store.dispatch(StreamActions.removeStream(userId, stream))
|
||||||
expect(store.getState().streams).toEqual({})
|
expect(store.getState().streams).toEqual({})
|
||||||
})
|
})
|
||||||
it('does not fail when no stream', () => {
|
it('does not fail when no stream', () => {
|
||||||
store.dispatch(StreamActions.removeStream(userId))
|
store.dispatch(StreamActions.removeStream(userId, stream))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('removeStreamTrack', () => {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import _debug from 'debug'
|
import _debug from 'debug'
|
||||||
import omit from 'lodash/omit'
|
import omit from 'lodash/omit'
|
||||||
import { AddStreamAction, AddStreamPayload, RemoveStreamAction, StreamAction } from '../actions/StreamActions'
|
import { AddStreamAction, RemoveStreamAction, StreamAction, StreamType, RemoveStreamTrackAction } from '../actions/StreamActions'
|
||||||
import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, ME } from '../constants'
|
import { STREAM_ADD, STREAM_REMOVE, MEDIA_STREAM, STREAM_TRACK_REMOVE } from '../constants'
|
||||||
import { createObjectURL, revokeObjectURL } from '../window'
|
import { createObjectURL, revokeObjectURL } from '../window'
|
||||||
import { MediaStreamAction } from '../actions/MediaActions'
|
import { MediaStreamAction } from '../actions/MediaActions'
|
||||||
|
|
||||||
@ -17,8 +17,19 @@ function safeCreateObjectURL (stream: MediaStream) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StreamWithURL {
|
||||||
|
stream: MediaStream
|
||||||
|
type: StreamType | undefined
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserStreams {
|
||||||
|
userId: string
|
||||||
|
streams: StreamWithURL[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface StreamsState {
|
export interface StreamsState {
|
||||||
[userId: string]: AddStreamPayload
|
[userId: string]: UserStreams
|
||||||
}
|
}
|
||||||
|
|
||||||
function addStream (
|
function addStream (
|
||||||
@ -26,40 +37,87 @@ function addStream (
|
|||||||
): StreamsState {
|
): StreamsState {
|
||||||
const { userId, stream } = payload
|
const { userId, stream } = payload
|
||||||
|
|
||||||
const userStream: AddStreamPayload = {
|
const userStreams = state[userId] || {
|
||||||
userId,
|
userId,
|
||||||
|
streams: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userStreams.streams.map(s => s.stream).indexOf(stream) >= 0) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamWithURL: StreamWithURL = {
|
||||||
stream,
|
stream,
|
||||||
|
type: payload.type,
|
||||||
url: safeCreateObjectURL(stream),
|
url: safeCreateObjectURL(stream),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
[userId]: userStream,
|
[userId]: {
|
||||||
|
userId,
|
||||||
|
streams: [...userStreams.streams, streamWithURL],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeStream (
|
function removeStream (
|
||||||
state: StreamsState, payload: RemoveStreamAction['payload'],
|
state: StreamsState, payload: RemoveStreamAction['payload'],
|
||||||
): StreamsState {
|
): StreamsState {
|
||||||
const { userId } = payload
|
const { userId, stream } = payload
|
||||||
const stream = state[userId]
|
const userStreams = state[userId]
|
||||||
if (stream && stream.stream) {
|
if (!userStreams) {
|
||||||
stream.stream.getTracks().forEach(track => track.stop())
|
return state
|
||||||
}
|
}
|
||||||
if (stream && stream.url) {
|
|
||||||
revokeObjectURL(stream.url)
|
if (stream) {
|
||||||
|
const streams = userStreams.streams.filter(s => {
|
||||||
|
const found = s.stream === stream
|
||||||
|
if (found) {
|
||||||
|
stream.getTracks().forEach(track => track.stop())
|
||||||
|
s.url && revokeObjectURL(s.url)
|
||||||
|
}
|
||||||
|
return !found
|
||||||
|
})
|
||||||
|
if (streams.length > 0) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[userId]: {
|
||||||
|
userId,
|
||||||
|
streams,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
omit(state, [userId])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userStreams && userStreams.streams.forEach(s => {
|
||||||
|
s.stream.getTracks().forEach(track => track.stop())
|
||||||
|
s.url && revokeObjectURL(s.url)
|
||||||
|
})
|
||||||
return omit(state, [userId])
|
return omit(state, [userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceStream(state: StreamsState, stream: MediaStream): StreamsState {
|
function removeStreamTrack(
|
||||||
state = removeStream(state, {
|
state: StreamsState, payload: RemoveStreamTrackAction['payload'],
|
||||||
userId: ME,
|
): StreamsState {
|
||||||
})
|
const { userId, stream, track } = payload
|
||||||
return addStream(state, {
|
const userStreams = state[userId]
|
||||||
userId: ME,
|
if (!userStreams) {
|
||||||
stream,
|
return state
|
||||||
})
|
}
|
||||||
|
const index = userStreams.streams.map(s => s.stream).indexOf(stream)
|
||||||
|
if (index < 0) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
stream.removeTrack(track)
|
||||||
|
if (stream.getTracks().length === 0) {
|
||||||
|
return removeStream(state, {userId, stream})
|
||||||
|
}
|
||||||
|
// UI does not update when a stream track is removed so there is no need to
|
||||||
|
// update the state object
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function streams(
|
export default function streams(
|
||||||
@ -71,9 +129,11 @@ export default function streams(
|
|||||||
return addStream(state, action.payload)
|
return addStream(state, action.payload)
|
||||||
case STREAM_REMOVE:
|
case STREAM_REMOVE:
|
||||||
return removeStream(state, action.payload)
|
return removeStream(state, action.payload)
|
||||||
|
case STREAM_TRACK_REMOVE:
|
||||||
|
return removeStreamTrack(state, action.payload)
|
||||||
case MEDIA_STREAM:
|
case MEDIA_STREAM:
|
||||||
if (action.status === 'resolved') {
|
if (action.status === 'resolved') {
|
||||||
return replaceStream(state, action.payload)
|
return addStream(state, action.payload)
|
||||||
} else {
|
} else {
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/scss/_fonts.scss
Executable file → Normal file
13
src/scss/_fonts.scss
Executable file → Normal file
@ -1,10 +1,10 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'icons';
|
font-family: 'icons';
|
||||||
src: url('../res/fonts/icons.eot?tcgv6b');
|
src: url('../res/fonts/icons.eot?ny6drs');
|
||||||
src: url('../res/fonts/icons.eot?tcgv6b#iefix') format('embedded-opentype'),
|
src: url('../res/fonts/icons.eot?ny6drs#iefix') format('embedded-opentype'),
|
||||||
url('../res/fonts/icons.woff?tcgv6b') format('woff'),
|
url('../res/fonts/icons.ttf?ny6drs') format('truetype'),
|
||||||
url('../res/fonts/icons.ttf?tcgv6b') format('truetype'),
|
url('../res/fonts/icons.woff?ny6drs') format('woff'),
|
||||||
url('../res/fonts/icons.svg?tcgv6b#icons') format('svg');
|
url('../res/fonts/icons.svg?ny6drs#icons') format('svg');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
@ -92,3 +92,6 @@
|
|||||||
.icon-file-text2:before {
|
.icon-file-text2:before {
|
||||||
content: "\e926";
|
content: "\e926";
|
||||||
}
|
}
|
||||||
|
.icon-display:before {
|
||||||
|
content: "\e956";
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user