Compare commits
13
Commits
5c37234bf6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82c4b637bb | ||
|
|
ea7eb45665 | ||
|
|
11e799207e | ||
|
|
24281cd491 | ||
|
|
c59248f7a5 | ||
|
|
a34613a64a | ||
|
|
207476efdc | ||
|
|
855c479caf | ||
|
|
b46806ada1 | ||
|
|
1e16d0e482 | ||
|
|
9d3767d68c | ||
|
|
4d2ba762f3 | ||
|
|
6d32d2d005 |
No files matched your search
@@ -49,8 +49,10 @@ const RAW_RUNTIME_STATE =
|
||||
["prettier", "npm:3.8.1"],\
|
||||
["react", "npm:19.2.4"],\
|
||||
["react-bootstrap", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:2.10.10"],\
|
||||
["react-bootstrap-icons", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:1.11.6"],\
|
||||
["react-dom", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:19.2.4"],\
|
||||
["react-router-dom", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:7.13.1"],\
|
||||
["socket.io-client", "npm:4.8.3"],\
|
||||
["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"],\
|
||||
["typescript-eslint", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:8.56.1"],\
|
||||
["vite", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:7.3.1"],\
|
||||
@@ -96,7 +98,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@babel/types", "npm:7.29.0"],\
|
||||
["@jridgewell/remapping", "npm:2.3.5"],\
|
||||
["convert-source-map", "npm:2.0.0"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["gensync", "npm:1.0.0-beta.2"],\
|
||||
["json5", "npm:2.2.3"],\
|
||||
["semver", "npm:6.3.1"]\
|
||||
@@ -312,7 +314,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@babel/template", "npm:7.28.6"],\
|
||||
["@babel/traverse", "npm:7.29.0"],\
|
||||
["@babel/types", "npm:7.29.0"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"]\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
@@ -600,7 +602,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@eslint/config-array", "npm:0.21.1"],\
|
||||
["@eslint/object-schema", "npm:2.1.7"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["minimatch", "npm:3.1.5"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -632,7 +634,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageDependencies": [\
|
||||
["@eslint/eslintrc", "npm:3.3.4"],\
|
||||
["ajv", "npm:6.14.0"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["espree", "npm:10.4.0"],\
|
||||
["globals", "npm:14.0.0"],\
|
||||
["ignore", "npm:5.3.2"],\
|
||||
@@ -1151,6 +1153,15 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@socket.io/component-emitter", [\
|
||||
["npm:3.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/@socket.io-component-emitter-npm-3.1.2-bc4133d33f-c4242bad66.zip/node_modules/@socket.io/component-emitter/",\
|
||||
"packageDependencies": [\
|
||||
["@socket.io/component-emitter", "npm:3.1.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["@swc/helpers", [\
|
||||
["npm:0.5.19", {\
|
||||
"packageLocation": "./.yarn/cache/@swc-helpers-npm-0.5.19-8b3f562b85-30317ee644.zip/node_modules/@swc/helpers/",\
|
||||
@@ -1377,7 +1388,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@typescript-eslint/types", "npm:8.56.1"],\
|
||||
["@typescript-eslint/typescript-estree", "virtual:58ccd472628fcf79c5364cbafb0fa7401eab7f9f0a8d7e57daff8d16b3df3fcd9fae4f499821557ed433fdc33c9608f2e228cdb9e43ac4ccbde485d9fe0d92aa#npm:8.56.1"],\
|
||||
["@typescript-eslint/visitor-keys", "npm:8.56.1"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["eslint", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:9.39.3"],\
|
||||
["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"]\
|
||||
],\
|
||||
@@ -1405,7 +1416,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@typescript-eslint/project-service", "virtual:08eece7f558a61e3ba42d291e347c98bcfb1ebfb15f338a48676e1eb239671d197c1afd15b648b5d4feafc689a0004679f0f549b21e3ba4e2b84a1430d054544#npm:8.56.1"],\
|
||||
["@typescript-eslint/tsconfig-utils", "virtual:08eece7f558a61e3ba42d291e347c98bcfb1ebfb15f338a48676e1eb239671d197c1afd15b648b5d4feafc689a0004679f0f549b21e3ba4e2b84a1430d054544#npm:8.56.1"],\
|
||||
["@typescript-eslint/types", "npm:8.56.1"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
@@ -1465,7 +1476,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@typescript-eslint/types", "npm:8.56.1"],\
|
||||
["@typescript-eslint/typescript-estree", "virtual:58ccd472628fcf79c5364cbafb0fa7401eab7f9f0a8d7e57daff8d16b3df3fcd9fae4f499821557ed433fdc33c9608f2e228cdb9e43ac4ccbde485d9fe0d92aa#npm:8.56.1"],\
|
||||
["@typescript-eslint/utils", "virtual:58ccd472628fcf79c5364cbafb0fa7401eab7f9f0a8d7e57daff8d16b3df3fcd9fae4f499821557ed433fdc33c9608f2e228cdb9e43ac4ccbde485d9fe0d92aa#npm:8.56.1"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["eslint", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:9.39.3"],\
|
||||
["ts-api-utils", "virtual:79e3fd711d219a7e195f7717b302bba495278e314f4cbf21fc912bccc21ee612c45d3bed436698ce744dcc117f9112db1d68749c33956122d7e1360e6bcb3f62#npm:2.4.0"],\
|
||||
["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"]\
|
||||
@@ -1505,7 +1516,7 @@ const RAW_RUNTIME_STATE =
|
||||
["@typescript-eslint/types", "npm:8.56.1"],\
|
||||
["@typescript-eslint/typescript-estree", "virtual:58ccd472628fcf79c5364cbafb0fa7401eab7f9f0a8d7e57daff8d16b3df3fcd9fae4f499821557ed433fdc33c9608f2e228cdb9e43ac4ccbde485d9fe0d92aa#npm:8.56.1"],\
|
||||
["@typescript-eslint/visitor-keys", "npm:8.56.1"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["minimatch", "npm:10.2.4"],\
|
||||
["semver", "npm:7.7.4"],\
|
||||
["tinyglobby", "npm:0.2.15"],\
|
||||
@@ -1675,8 +1686,10 @@ const RAW_RUNTIME_STATE =
|
||||
["prettier", "npm:3.8.1"],\
|
||||
["react", "npm:19.2.4"],\
|
||||
["react-bootstrap", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:2.10.10"],\
|
||||
["react-bootstrap-icons", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:1.11.6"],\
|
||||
["react-dom", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:19.2.4"],\
|
||||
["react-router-dom", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:7.13.1"],\
|
||||
["socket.io-client", "npm:4.8.3"],\
|
||||
["typescript", "patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"],\
|
||||
["typescript-eslint", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:8.56.1"],\
|
||||
["vite", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:7.3.1"],\
|
||||
@@ -1936,11 +1949,11 @@ const RAW_RUNTIME_STATE =
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3", {\
|
||||
"packageLocation": "./.yarn/__virtual__/debug-virtual-ce2b683fbc/0/cache/debug-npm-4.4.3-0105c6123a-d79136ec6c.zip/node_modules/debug/",\
|
||||
["virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3", {\
|
||||
"packageLocation": "./.yarn/__virtual__/debug-virtual-b7022d90de/0/cache/debug-npm-4.4.3-0105c6123a-d79136ec6c.zip/node_modules/debug/",\
|
||||
"packageDependencies": [\
|
||||
["@types/supports-color", null],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["ms", "npm:2.1.3"],\
|
||||
["supports-color", null]\
|
||||
],\
|
||||
@@ -1989,6 +2002,29 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["engine.io-client", [\
|
||||
["npm:6.6.6", {\
|
||||
"packageLocation": "./.yarn/cache/engine.io-client-npm-6.6.6-fd14f4b531-805995b57b.zip/node_modules/engine.io-client/",\
|
||||
"packageDependencies": [\
|
||||
["@socket.io/component-emitter", "npm:3.1.2"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["engine.io-client", "npm:6.6.6"],\
|
||||
["engine.io-parser", "npm:5.2.3"],\
|
||||
["ws", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:8.21.3"],\
|
||||
["xmlhttprequest-ssl", "npm:2.1.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["engine.io-parser", [\
|
||||
["npm:5.2.3", {\
|
||||
"packageLocation": "./.yarn/cache/engine.io-parser-npm-5.2.3-6dfa36cede-ed4900d8db.zip/node_modules/engine.io-parser/",\
|
||||
"packageDependencies": [\
|
||||
["engine.io-parser", "npm:5.2.3"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["env-paths", [\
|
||||
["npm:2.2.1", {\
|
||||
"packageLocation": "./.yarn/cache/env-paths-npm-2.2.1-7c7577428c-285325677b.zip/node_modules/env-paths/",\
|
||||
@@ -2078,7 +2114,7 @@ const RAW_RUNTIME_STATE =
|
||||
["ajv", "npm:6.14.0"],\
|
||||
["chalk", "npm:4.1.2"],\
|
||||
["cross-spawn", "npm:7.0.6"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["escape-string-regexp", "npm:4.0.0"],\
|
||||
["eslint", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:9.39.3"],\
|
||||
["eslint-scope", "npm:8.4.0"],\
|
||||
@@ -2457,7 +2493,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./.yarn/cache/http-proxy-agent-npm-7.0.2-643ed7cc33-4207b06a45.zip/node_modules/http-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
["agent-base", "npm:7.1.4"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["http-proxy-agent", "npm:7.0.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -2468,7 +2504,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-f729219bc7.zip/node_modules/https-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
["agent-base", "npm:7.1.4"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["https-proxy-agent", "npm:7.0.6"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
@@ -3225,6 +3261,29 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["react-bootstrap-icons", [\
|
||||
["npm:1.11.6", {\
|
||||
"packageLocation": "./.yarn/cache/react-bootstrap-icons-npm-1.11.6-2dbc39d6ac-4dfd75c980.zip/node_modules/react-bootstrap-icons/",\
|
||||
"packageDependencies": [\
|
||||
["react-bootstrap-icons", "npm:1.11.6"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:1.11.6", {\
|
||||
"packageLocation": "./.yarn/__virtual__/react-bootstrap-icons-virtual-ffeae68cb7/0/cache/react-bootstrap-icons-npm-1.11.6-2dbc39d6ac-4dfd75c980.zip/node_modules/react-bootstrap-icons/",\
|
||||
"packageDependencies": [\
|
||||
["@types/react", "npm:19.2.14"],\
|
||||
["prop-types", "npm:15.8.1"],\
|
||||
["react", "npm:19.2.4"],\
|
||||
["react-bootstrap-icons", "virtual:ae939b5bf1c4da31987d1d461851d6cc956667f76e9d6d3a45643e06af20bf533b9bcec6bab70ed0ca795aebf9ce43b3ed74bd20004783b74b2a0cc963cd86b8#npm:1.11.6"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/react",\
|
||||
"react"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["react-dom", [\
|
||||
["npm:19.2.4", {\
|
||||
"packageLocation": "./.yarn/cache/react-dom-npm-19.2.4-4921ae503e-f0c63f1794.zip/node_modules/react-dom/",\
|
||||
@@ -3485,6 +3544,30 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["socket.io-client", [\
|
||||
["npm:4.8.3", {\
|
||||
"packageLocation": "./.yarn/cache/socket.io-client-npm-4.8.3-6800638250-76c0d86de0.zip/node_modules/socket.io-client/",\
|
||||
"packageDependencies": [\
|
||||
["@socket.io/component-emitter", "npm:3.1.2"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["engine.io-client", "npm:6.6.6"],\
|
||||
["socket.io-client", "npm:4.8.3"],\
|
||||
["socket.io-parser", "npm:4.2.7"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["socket.io-parser", [\
|
||||
["npm:4.2.7", {\
|
||||
"packageLocation": "./.yarn/cache/socket.io-parser-npm-4.2.7-0fe17183fd-16a5579718.zip/node_modules/socket.io-parser/",\
|
||||
"packageDependencies": [\
|
||||
["@socket.io/component-emitter", "npm:3.1.2"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["socket.io-parser", "npm:4.2.7"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["socks", [\
|
||||
["npm:2.8.7", {\
|
||||
"packageLocation": "./.yarn/cache/socks-npm-2.8.7-d1d20aae19-2805a43a1c.zip/node_modules/socks/",\
|
||||
@@ -3501,7 +3584,7 @@ const RAW_RUNTIME_STATE =
|
||||
"packageLocation": "./.yarn/cache/socks-proxy-agent-npm-8.0.5-24d77a90dc-5d2c6cecba.zip/node_modules/socks-proxy-agent/",\
|
||||
"packageDependencies": [\
|
||||
["agent-base", "npm:7.1.4"],\
|
||||
["debug", "virtual:85d5d916b6a745b2f8de0d4b1704b7084bdd4f7573c1ef5d1c877f2c866045a9b29fe2bc752d1b73e531c378a0518dd3f9fa187b31427fb9bacbe34b14715dde#npm:4.4.3"],\
|
||||
["debug", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:4.4.3"],\
|
||||
["socks", "npm:2.8.7"],\
|
||||
["socks-proxy-agent", "npm:8.0.5"]\
|
||||
],\
|
||||
@@ -3862,6 +3945,41 @@ const RAW_RUNTIME_STATE =
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["ws", [\
|
||||
["npm:8.21.3", {\
|
||||
"packageLocation": "./.yarn/cache/ws-npm-8.21.3-63c47e1f60-7b28dc2863.zip/node_modules/ws/",\
|
||||
"packageDependencies": [\
|
||||
["ws", "npm:8.21.3"]\
|
||||
],\
|
||||
"linkType": "SOFT"\
|
||||
}],\
|
||||
["virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:8.21.3", {\
|
||||
"packageLocation": "./.yarn/__virtual__/ws-virtual-097af440a8/0/cache/ws-npm-8.21.3-63c47e1f60-7b28dc2863.zip/node_modules/ws/",\
|
||||
"packageDependencies": [\
|
||||
["@types/bufferutil", null],\
|
||||
["@types/utf-8-validate", null],\
|
||||
["bufferutil", null],\
|
||||
["utf-8-validate", null],\
|
||||
["ws", "virtual:fd14f4b531a5a95a85dc33ff712d35b53c356fa12f6734083978806651903eae60196105e71a7d037cf3aeffe0d9c55861ee577971d38735aba6ad2f7a34a45a#npm:8.21.3"]\
|
||||
],\
|
||||
"packagePeers": [\
|
||||
"@types/bufferutil",\
|
||||
"@types/utf-8-validate",\
|
||||
"bufferutil",\
|
||||
"utf-8-validate"\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["xmlhttprequest-ssl", [\
|
||||
["npm:2.1.2", {\
|
||||
"packageLocation": "./.yarn/cache/xmlhttprequest-ssl-npm-2.1.2-7d8e3817bc-70d6086932.zip/node_modules/xmlhttprequest-ssl/",\
|
||||
"packageDependencies": [\
|
||||
["xmlhttprequest-ssl", "npm:2.1.2"]\
|
||||
],\
|
||||
"linkType": "HARD"\
|
||||
}]\
|
||||
]],\
|
||||
["yallist", [\
|
||||
["npm:3.1.1", {\
|
||||
"packageLocation": "./.yarn/cache/yallist-npm-3.1.1-a568a556b4-c66a5c46bc.zip/node_modules/yallist/",\
|
||||
@@ -4519,7 +4637,7 @@ async function copyFileViaIndex(
|
||||
const isIndexModified = indexStat?.mtimeMs !== defaultTimeMs;
|
||||
if (isDestinationHardlinkedFromIndex) {
|
||||
if (isIndexModified && linkStrategy.autoRepair) {
|
||||
atomicBehavior = 0 /* Lock */;
|
||||
atomicBehavior = 0; /* Lock */
|
||||
indexStat = null;
|
||||
}
|
||||
}
|
||||
@@ -4739,7 +4857,7 @@ class CustomStatWatcher extends events.EventEmitter {
|
||||
fakeFs;
|
||||
path;
|
||||
bigint;
|
||||
status = "ready" /* Ready */;
|
||||
status = "ready"; /* Ready */
|
||||
changeListeners = /* @__PURE__ */ new Map();
|
||||
lastStats;
|
||||
startTimeout = null;
|
||||
@@ -4757,7 +4875,7 @@ class CustomStatWatcher extends events.EventEmitter {
|
||||
}
|
||||
start() {
|
||||
assertStatus(this.status, "ready" /* Ready */);
|
||||
this.status = "running" /* Running */;
|
||||
this.status = "running"; /* Running */
|
||||
this.startTimeout = setTimeout(() => {
|
||||
this.startTimeout = null;
|
||||
if (!this.fakeFs.existsSync(this.path)) {
|
||||
@@ -4767,7 +4885,7 @@ class CustomStatWatcher extends events.EventEmitter {
|
||||
}
|
||||
stop() {
|
||||
assertStatus(this.status, "running" /* Running */);
|
||||
this.status = "stopped" /* Stopped */;
|
||||
this.status = "stopped"; /* Stopped */
|
||||
if (this.startTimeout !== null) {
|
||||
clearTimeout(this.startTimeout);
|
||||
this.startTimeout = null;
|
||||
@@ -8234,7 +8352,7 @@ var libzipSync = { exports: {} };
|
||||
function UTF8ToString(ptr, maxBytesToRead) {
|
||||
if (!ptr) return "";
|
||||
var maxPtr = ptr + maxBytesToRead;
|
||||
for (var end = ptr; !(end >= maxPtr) && HEAPU8[end]; ) ++end;
|
||||
for (var end = ptr; !(end >= maxPtr) && HEAPU8[end];) ++end;
|
||||
return UTF8Decoder.decode(HEAPU8.subarray(ptr, end));
|
||||
}
|
||||
function stringToUTF8Array(str, heap, outIdx, maxBytesToWrite) {
|
||||
|
||||
Generated
+1
-1
@@ -319,7 +319,7 @@ async function copyFileViaIndex(
|
||||
const isIndexModified = indexStat?.mtimeMs !== defaultTimeMs;
|
||||
if (isDestinationHardlinkedFromIndex) {
|
||||
if (isIndexModified && linkStrategy.autoRepair) {
|
||||
atomicBehavior = 0 /* Lock */;
|
||||
atomicBehavior = 0; /* Lock */
|
||||
indexStat = null;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -20,8 +20,10 @@
|
||||
"prettier": "^3.8.1",
|
||||
"react": "^19.2.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-bootstrap-icons": "^1.11.6",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"yaml": "^2.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import { useState } from "react";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import { Provider as JotaiProvider } from "jotai";
|
||||
import { Home } from "./Home.tsx";
|
||||
import { Autonomy } from "./Autonomy.tsx";
|
||||
import { Video } from "./Video.tsx";
|
||||
import { Version } from "./Version.tsx";
|
||||
import "./App.css";
|
||||
|
||||
@@ -40,8 +40,8 @@ const router = createBrowserRouter([
|
||||
element: <Version />,
|
||||
},
|
||||
{
|
||||
path: "/autonomy",
|
||||
element: <Autonomy />,
|
||||
path: "/video",
|
||||
element: <Video />,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
//
|
||||
// Copyright 2026 James Pace
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
import { AppNav, Footer } from "./AppNav.tsx";
|
||||
import { Container, Row, Col, Image, Card } from "react-bootstrap";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import { WebRTCVideo } from "mediamtx-webrtc-react";
|
||||
|
||||
const statusStringQuery = async () => {
|
||||
const resp = await fetch("api/status");
|
||||
if (!resp.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
const statusStringQueryAtom = atomWithQuery(() => ({
|
||||
queryKey: ["status_string"],
|
||||
queryFn: statusStringQuery,
|
||||
refetchInterval: 500, // 0.5s
|
||||
}));
|
||||
const statusStringAtom = atom((get) => {
|
||||
const response = get(statusStringQueryAtom);
|
||||
if (response.isPending || response.isError || !response.data.status) {
|
||||
return "N/A";
|
||||
}
|
||||
return response.data.message;
|
||||
});
|
||||
|
||||
const latLongQuery = async () => {
|
||||
const resp = await fetch("api/position");
|
||||
if (!resp.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
const latLongQueryAtom = atomWithQuery(() => ({
|
||||
queryKey: ["latlong"],
|
||||
queryFn: latLongQuery,
|
||||
refetchInterval: 500, // 0.5s
|
||||
}));
|
||||
const latLongAtom = atom((get) => {
|
||||
const response = get(latLongQueryAtom);
|
||||
if (response.isPending || response.isError || !response.data.status) {
|
||||
return "N/A";
|
||||
}
|
||||
return `(${response.data.latitude}, ${response.data.longitude})`;
|
||||
});
|
||||
|
||||
export function Autonomy() {
|
||||
const statusString = useAtomValue(statusStringAtom);
|
||||
const latLong = useAtomValue(latLongAtom);
|
||||
|
||||
const imageStreamURI = "http://" + window.location.hostname + ":8889/image/whep";
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppNav />
|
||||
<Container className="vert-padded">
|
||||
<Card className="padded">
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<WebRTCVideo
|
||||
url={ imageStreamURI }
|
||||
controls
|
||||
autoPlay
|
||||
style={{ width: "100%", maxWidth: "800px" }}
|
||||
/>
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<p>Status:</p>
|
||||
<pre>{statusString}</pre>
|
||||
<p>Latitude, Longitude:</p>
|
||||
<pre>{latLong}</pre>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Container>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+63
-8
@@ -8,21 +8,76 @@
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
import { AppNav, Footer } from "./AppNav.tsx";
|
||||
import { Container, Row, Col, Image, Card } from "react-bootstrap";
|
||||
import { AppNav, Footer } from "./components/AppNav.tsx";
|
||||
import { Container, Row, Col, Card } from "react-bootstrap";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import { createQueryAtom } from "./utils/query.ts";
|
||||
import { DiagnosticsCard } from "./components/DiagnosticsCard.tsx";
|
||||
|
||||
const identityStringQueryAtom = createQueryAtom("api/identity", {
|
||||
refetchInterval: 500,
|
||||
});
|
||||
const identityStringAtom = atom((get) => {
|
||||
const response = get(identityStringQueryAtom);
|
||||
if (response.isPending || response.isError) {
|
||||
return "N/A";
|
||||
}
|
||||
return response.data.message;
|
||||
});
|
||||
|
||||
const uptimeQueryAtom = createQueryAtom("api/uptime", {
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
const uptimeAtom = atom((get) => {
|
||||
const response = get(uptimeQueryAtom);
|
||||
if (response.isPending || response.isError) {
|
||||
return "N/A";
|
||||
}
|
||||
return response.data.uptime.toFixed(1);
|
||||
});
|
||||
|
||||
function IdentityCard() {
|
||||
const identityString = useAtomValue(identityStringAtom);
|
||||
return (
|
||||
<Card className="padded h-100">
|
||||
<Card.Title>Identity</Card.Title>
|
||||
<Card.Body>
|
||||
<div style={{ whiteSpace: "pre-line" }}>{identityString}</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function UptimeCard() {
|
||||
const uptime = useAtomValue(uptimeAtom);
|
||||
return (
|
||||
<Card className="padded h-100">
|
||||
<Card.Title>Uptime</Card.Title>
|
||||
<Card.Body>
|
||||
<p>{uptime}s</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<div>
|
||||
<AppNav />
|
||||
<Container className="vert-padded">
|
||||
<Card className="padded">
|
||||
<Row>
|
||||
<p>Hello World!</p>
|
||||
</Row>
|
||||
</Card>
|
||||
<Row>
|
||||
<Col>
|
||||
<UptimeCard />
|
||||
</Col>
|
||||
<Col>
|
||||
<IdentityCard />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="vert-padded">
|
||||
<Col>
|
||||
<DiagnosticsCard />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
+42
-29
@@ -8,52 +8,65 @@
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
import { AppNav, Footer } from "./AppNav.tsx";
|
||||
import { AppNav, Footer } from "./components/AppNav.tsx";
|
||||
import { Container, Row, Col, Card } from "react-bootstrap";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
import { createQueryAtom } from "./utils/query.ts";
|
||||
import YAML from "yaml";
|
||||
|
||||
const versionQueryFn = async () => {
|
||||
const resp = await fetch("api/build_info");
|
||||
if (!resp.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
return resp.json();
|
||||
};
|
||||
|
||||
const versionQueryAtom = atomWithQuery(() => ({
|
||||
queryKey: ["version"],
|
||||
queryFn: versionQueryFn,
|
||||
}));
|
||||
|
||||
const versionAtom = atom((get) => {
|
||||
const version = get(versionQueryAtom);
|
||||
if (version.isPending) {
|
||||
const buildInfoQueryAtom = createQueryAtom("api/build_info");
|
||||
const buildInfoAtom = atom((get) => {
|
||||
const buildInfo = get(buildInfoQueryAtom);
|
||||
if (buildInfo.isPending) {
|
||||
return "Loading...";
|
||||
}
|
||||
if (version.isError) {
|
||||
if (buildInfo.isError) {
|
||||
return "Error loading!";
|
||||
}
|
||||
if (!version.data.status) {
|
||||
return "Can not find version";
|
||||
if (!buildInfo.data.status) {
|
||||
return "Can not find buildInfo";
|
||||
}
|
||||
return YAML.stringify(version.data.message);
|
||||
return YAML.stringify(buildInfo.data.message);
|
||||
});
|
||||
|
||||
const versionNumberQueryAtom = createQueryAtom("api/version_number");
|
||||
const versionNumberAtom = atom((get) => {
|
||||
const versionNumber = get(versionNumberQueryAtom);
|
||||
if (versionNumber.isPending || versionNumber.isError) {
|
||||
return "...";
|
||||
}
|
||||
if (!versionNumber.data.status) {
|
||||
return "Can not find version number.";
|
||||
}
|
||||
return versionNumber.data.version;
|
||||
});
|
||||
|
||||
export function Version() {
|
||||
let versionText = useAtomValue(versionAtom);
|
||||
let buildInfoText = useAtomValue(buildInfoAtom);
|
||||
let versionText = useAtomValue(versionNumberAtom);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppNav />
|
||||
<Container className="vert-padded">
|
||||
<Card className="padded">
|
||||
<Card.Title>project.yaml</Card.Title>
|
||||
<Card.Body>
|
||||
<pre>{versionText}</pre>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
<Row>
|
||||
<Col>
|
||||
<Card className="padded">
|
||||
<Card.Title>Version Number</Card.Title>
|
||||
<Card.Body>{versionText}</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className="vert-padded">
|
||||
<Col>
|
||||
<Card className="padded">
|
||||
<Card.Title>project.yaml</Card.Title>
|
||||
<Card.Body>
|
||||
<pre>{buildInfoText}</pre>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// Copyright 2026 James Pace
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
import { AppNav, Footer } from "./components/AppNav.tsx";
|
||||
import { Container, Row, Col, Card } from "react-bootstrap";
|
||||
import { MediaMtxPlayer } from "./external/MediaMtxPlayer.tsx";
|
||||
import { atom, useAtomValue, useAtom } from "jotai";
|
||||
import { createQueryAtom } from "./utils/query.ts";
|
||||
|
||||
const tokenQueryAtom = createQueryAtom("api/mediamtx/login", { retry: true });
|
||||
const tokenAtom = atom((get) => {
|
||||
const response = get(tokenQueryAtom);
|
||||
if (response.isPending || response.isError) {
|
||||
return null;
|
||||
}
|
||||
return response.data.token;
|
||||
});
|
||||
|
||||
const streamsQueryAtom = createQueryAtom("api/available_streams");
|
||||
const streamsAtom = atom((get) => {
|
||||
const response = get(streamsQueryAtom);
|
||||
if (response.isPending || response.isError) {
|
||||
return [];
|
||||
}
|
||||
return response.data.streams;
|
||||
});
|
||||
const videoDisplayPortAtom = atom((get) => {
|
||||
const response = get(streamsQueryAtom);
|
||||
if (response.isPending || response.isError) {
|
||||
return null;
|
||||
}
|
||||
return response.data.port;
|
||||
});
|
||||
|
||||
const selectedStreamAtom = atom("image");
|
||||
const imageStreamURIAtom = atom((get) => {
|
||||
const port = get(videoDisplayPortAtom);
|
||||
const selectedStream = get(selectedStreamAtom);
|
||||
if (port === null || selectedStream === null) {
|
||||
return null;
|
||||
}
|
||||
const imageStreamURI =
|
||||
"http://" +
|
||||
window.location.hostname +
|
||||
":" +
|
||||
port +
|
||||
"/" +
|
||||
selectedStream +
|
||||
"/whep";
|
||||
return imageStreamURI;
|
||||
});
|
||||
|
||||
export function Video() {
|
||||
const streamName = "image";
|
||||
|
||||
const imageStreamURI = useAtomValue(imageStreamURIAtom);
|
||||
|
||||
const token = useAtomValue(tokenAtom);
|
||||
|
||||
const availableStreams = useAtomValue(streamsAtom);
|
||||
|
||||
const [selectedStream, setSelectedStream] = useAtom(selectedStreamAtom);
|
||||
|
||||
const handleSelect = (event) => {
|
||||
setSelectedStream(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppNav />
|
||||
<Container className="vert-padded">
|
||||
<Card className="padded">
|
||||
<Row>
|
||||
<Col lg={8}>
|
||||
<MediaMtxPlayer url={imageStreamURI} token={token} />
|
||||
</Col>
|
||||
<Col lg={4}>
|
||||
<p>Available Streams: </p>
|
||||
{availableStreams.map((stream, index) => (
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="selected_stream"
|
||||
value={stream}
|
||||
checked={selectedStream === stream}
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
{stream}
|
||||
</div>
|
||||
))}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Container>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export function AppNav() {
|
||||
<Navbar bg="dark" variant="dark" expand="sm" className="py-0">
|
||||
<Container className="me-auto">
|
||||
<Navbar.Brand as={Link} to="/">
|
||||
J7s-Bridge
|
||||
am_i_up
|
||||
</Navbar.Brand>
|
||||
<Navbar.Toggle aria-controls="collapse-navbar-nav" />
|
||||
<Navbar.Collapse id="collapse-navbar-nav">
|
||||
@@ -24,12 +24,12 @@ export function AppNav() {
|
||||
<Nav.Link as={Link} to="/">
|
||||
Home
|
||||
</Nav.Link>
|
||||
<Nav.Link as={Link} to="/video">
|
||||
Video
|
||||
</Nav.Link>
|
||||
<Nav.Link as={Link} to="/version">
|
||||
Version
|
||||
</Nav.Link>
|
||||
<Nav.Link as={Link} to="/autonomy">
|
||||
Autonomy
|
||||
</Nav.Link>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// Copyright 2026 James Pace
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
import React, { useState } from "react";
|
||||
import { Button, ListGroup, Collapse } from "react-bootstrap";
|
||||
import {
|
||||
Dash,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleFill,
|
||||
} from "react-bootstrap-icons";
|
||||
import { Container, Row, Col, Table, Card } from "react-bootstrap";
|
||||
import { atom, useAtomValue } from "jotai";
|
||||
import { createQueryAtom } from "../utils/query.ts";
|
||||
|
||||
const diagQueryAtom = createQueryAtom("api/diagnostics", {
|
||||
refetchInterval: 1000,
|
||||
});
|
||||
|
||||
const diagAtom = atom((get) => {
|
||||
const diag = get(diagQueryAtom);
|
||||
if (diag.isPending) {
|
||||
return "Loading...";
|
||||
}
|
||||
if (diag.isError) {
|
||||
return "Error loading!";
|
||||
}
|
||||
if (!diag.data.status) {
|
||||
return "Failed to get diagnostic information.";
|
||||
}
|
||||
return diag.data.message;
|
||||
});
|
||||
|
||||
export function DiagnosticsCard() {
|
||||
const diagnostics = useAtomValue(diagAtom);
|
||||
|
||||
const diagContent = () => {
|
||||
if (typeof diagnostics === "string" || diagnostics instanceof String) {
|
||||
return <pre> {diagnostics} </pre>;
|
||||
}
|
||||
return <TreeView data={diagnostics} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="padded">
|
||||
<Card.Title>Diagnostics</Card.Title>
|
||||
<Card.Body>{diagContent()}</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeView(props: Any) {
|
||||
const data = props.data;
|
||||
return (
|
||||
<ListGroup>
|
||||
{data.children.map((node) => (
|
||||
<SingleNode key={node.id} node={node} />
|
||||
))}
|
||||
</ListGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function LevelIndicator(props: Any) {
|
||||
const level = props.level;
|
||||
if (level == "ERROR") {
|
||||
return <CircleFill color="red" />;
|
||||
}
|
||||
if (level == "WARN") {
|
||||
return <CircleFill color="orange" />;
|
||||
}
|
||||
if (level == "OK") {
|
||||
return <CircleFill color="green" />;
|
||||
}
|
||||
return <CircleFill color="grey" />;
|
||||
}
|
||||
|
||||
function TitleText(props: Any) {
|
||||
if (props.message === "") {
|
||||
return <p>{props.name}</p>;
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
{props.name}: {props.message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function ValuesTable(props: Any) {
|
||||
const values = props.values;
|
||||
if (Object.keys(values).length === 0) {
|
||||
return <div></div>;
|
||||
}
|
||||
return (
|
||||
<Table>
|
||||
<thead>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(values).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td>{key}</td>
|
||||
<td>{value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusContent(props: Any) {
|
||||
const status = props.status;
|
||||
return (
|
||||
<div>
|
||||
<TitleText name={status.name} message={status.message} />
|
||||
<p>
|
||||
<ValuesTable values={status.values} />
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DirectionIcon(props: Any) {
|
||||
let icon = <ChevronRight />;
|
||||
if (props.isOpen) {
|
||||
icon = <ChevronDown />;
|
||||
}
|
||||
if (!props.hasChildren) {
|
||||
icon = <Dash />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="link" onClick={props.onClick}>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function ChildStatuses(props: Any) {
|
||||
const isOpen = props.isOpen;
|
||||
const node = props.node;
|
||||
return (
|
||||
<Collapse in={isOpen}>
|
||||
<div>
|
||||
<ListGroup>
|
||||
{node.children.map((child) => (
|
||||
<SingleNode key={child.id} node={child} />
|
||||
))}
|
||||
</ListGroup>
|
||||
</div>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleNode(props: Any) {
|
||||
const node = props.node;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
const openFunction = () => {
|
||||
if (hasChildren) {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ListGroup.Item>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col xs={1}>
|
||||
<DirectionIcon
|
||||
isOpen={isOpen}
|
||||
hasChildren={hasChildren}
|
||||
onClick={openFunction}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={1}>
|
||||
<LevelIndicator level={node.status.level} />
|
||||
</Col>
|
||||
<Col xs={"auto"}>
|
||||
<StatusContent status={node.status} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<ChildStatuses isOpen={isOpen} node={node} />
|
||||
</Row>
|
||||
</Container>
|
||||
</ListGroup.Item>
|
||||
);
|
||||
}
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
// From Gemini.
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import "./reader.js";
|
||||
|
||||
interface MediaMTXPlayerProps {
|
||||
url: string;
|
||||
user?: string;
|
||||
pass?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export const MediaMtxPlayer: React.FC<MediaMTXPlayerProps> = ({
|
||||
url,
|
||||
user,
|
||||
pass,
|
||||
token,
|
||||
}) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let reader: any = null;
|
||||
|
||||
if (videoRef.current && url) {
|
||||
reader = new MediaMTXWebRTCReader({
|
||||
url,
|
||||
user,
|
||||
pass,
|
||||
token,
|
||||
onError: (err: any) => {
|
||||
console.error("MediaMTX WebRTC Error:", err);
|
||||
setError(err);
|
||||
},
|
||||
onTrack: (evt: RTCTrackEvent) => {
|
||||
if (videoRef.current && evt.streams[0]) {
|
||||
videoRef.current.srcObject = evt.streams[0];
|
||||
}
|
||||
},
|
||||
onDataChannel: (evt: RTCDataChannelEvent) => {
|
||||
console.log("Data channel opened:", evt.channel);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup when component unmounts or URL changes
|
||||
return () => {
|
||||
if (reader && typeof reader.close === "function") {
|
||||
reader.close();
|
||||
}
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = null;
|
||||
}
|
||||
};
|
||||
}, [url, user, pass, token]);
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", width: "100%", maxWidth: "720px" }}>
|
||||
{error && (
|
||||
<div style={{ color: "red", marginBottom: "8px" }}>
|
||||
Error getting video.
|
||||
</div>
|
||||
)}
|
||||
<video
|
||||
ref={videoRef}
|
||||
autoPlay
|
||||
playsInline
|
||||
muted
|
||||
controls
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: "720px",
|
||||
height: "auto",
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Vendored
+676
@@ -0,0 +1,676 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @callback OnError
|
||||
* @param {string} err - error.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback OnTrack
|
||||
* @param {RTCTrackEvent} evt - track event.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback OnDataChannel
|
||||
* @param {RTCDataChannelEvent} evt - data channel event.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Conf
|
||||
* @type {object}
|
||||
* @property {string} url - absolute URL of the WHEP endpoint.
|
||||
* @property {string} user - username.
|
||||
* @property {string} pass - password.
|
||||
* @property {string} token - token.
|
||||
* @property {OnError} onError - called when there's an error.
|
||||
* @property {OnTrack} onTrack - called when there's a track available.
|
||||
* @property {OnDataChannel} onDataChannel - called when there's a data channel available.
|
||||
*/
|
||||
|
||||
/** WebRTC/WHEP reader. */
|
||||
class MediaMTXWebRTCReader {
|
||||
static #RETRY_PAUSE = 2000;
|
||||
|
||||
#conf;
|
||||
#state = "getting_codecs";
|
||||
#restartTimeout = null;
|
||||
#pc = null;
|
||||
#offerData = null;
|
||||
#sessionUrl = null;
|
||||
#queuedCandidates = [];
|
||||
#nonAdvertisedCodecs;
|
||||
|
||||
/**
|
||||
* Create a MediaMTXWebRTCReader.
|
||||
* @param {Conf} conf - configuration.
|
||||
*/
|
||||
constructor(conf) {
|
||||
this.#conf = conf;
|
||||
this.#getNonAdvertisedCodecs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the reader and all its resources.
|
||||
*/
|
||||
close() {
|
||||
this.#state = "closed";
|
||||
|
||||
if (this.#pc !== null) {
|
||||
this.#pc.close();
|
||||
}
|
||||
|
||||
if (this.#restartTimeout !== null) {
|
||||
clearTimeout(this.#restartTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
static #supportsNonAdvertisedCodec(codec, fmtp) {
|
||||
return new Promise((resolve) => {
|
||||
const pc = new RTCPeerConnection({ iceServers: [] });
|
||||
const mediaType = "audio";
|
||||
let payloadType = "";
|
||||
|
||||
pc.addTransceiver(mediaType, { direction: "recvonly" });
|
||||
pc.createOffer()
|
||||
.then((offer) => {
|
||||
if (offer.sdp === undefined) {
|
||||
throw new Error("SDP not present");
|
||||
}
|
||||
if (offer.sdp.includes(` ${codec}`)) {
|
||||
// codec is advertised, there's no need to add it manually
|
||||
throw new Error("already present");
|
||||
}
|
||||
|
||||
const sections = offer.sdp.split(`m=${mediaType}`);
|
||||
|
||||
const payloadTypes = sections
|
||||
.slice(1)
|
||||
.map((s) => s.split("\r\n")[0].split(" ").slice(3))
|
||||
.reduce((prev, cur) => [...prev, ...cur], []);
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
|
||||
const lines = sections[1].split("\r\n");
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} ${codec}`);
|
||||
if (fmtp !== undefined) {
|
||||
lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} ${fmtp}`);
|
||||
}
|
||||
sections[1] = lines.join("\r\n");
|
||||
offer.sdp = sections.join(`m=${mediaType}`);
|
||||
return pc.setLocalDescription(offer);
|
||||
})
|
||||
.then(() =>
|
||||
pc.setRemoteDescription(
|
||||
new RTCSessionDescription({
|
||||
type: "answer",
|
||||
sdp:
|
||||
"v=0\r\n" +
|
||||
"o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n" +
|
||||
"s=-\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n" +
|
||||
`m=${mediaType} 9 UDP/TLS/RTP/SAVPF ${payloadType}\r\n` +
|
||||
"c=IN IP4 0.0.0.0\r\n" +
|
||||
"a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n" +
|
||||
"a=ice-ufrag:29e036dc\r\n" +
|
||||
"a=sendonly\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
`a=rtpmap:${payloadType} ${codec}\r\n` +
|
||||
(fmtp !== undefined ? `a=fmtp:${payloadType} ${fmtp}\r\n` : ""),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch(() => {
|
||||
resolve(false);
|
||||
})
|
||||
.finally(() => {
|
||||
pc.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static #unquoteCredential(v) {
|
||||
return JSON.parse(`"${v}"`);
|
||||
}
|
||||
|
||||
static #linkToIceServers(links) {
|
||||
return links !== null
|
||||
? links.split(", ").map((link) => {
|
||||
const m = link.match(
|
||||
/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i,
|
||||
);
|
||||
const ret = {
|
||||
urls: [m[1]],
|
||||
};
|
||||
|
||||
if (m[3] !== undefined) {
|
||||
ret.username = this.#unquoteCredential(m[3]);
|
||||
ret.credential = this.#unquoteCredential(m[4]);
|
||||
ret.credentialType = "password";
|
||||
}
|
||||
|
||||
return ret;
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
static #parseOffer(sdp) {
|
||||
const ret = {
|
||||
iceUfrag: "",
|
||||
icePwd: "",
|
||||
medias: [],
|
||||
};
|
||||
|
||||
for (const line of sdp.split("\r\n")) {
|
||||
if (line.startsWith("m=")) {
|
||||
ret.medias.push(line.slice("m=".length));
|
||||
} else if (ret.iceUfrag === "" && line.startsWith("a=ice-ufrag:")) {
|
||||
ret.iceUfrag = line.slice("a=ice-ufrag:".length);
|
||||
} else if (ret.icePwd === "" && line.startsWith("a=ice-pwd:")) {
|
||||
ret.icePwd = line.slice("a=ice-pwd:".length);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static #reservePayloadType(payloadTypes) {
|
||||
// everything is valid between 30 and 127, except for interval between 64 and 95
|
||||
// https://chromium.googlesource.com/external/webrtc/+/refs/heads/master/call/payload_type.h#29
|
||||
for (let i = 30; i <= 127; i++) {
|
||||
if ((i <= 63 || i >= 96) && !payloadTypes.includes(i.toString())) {
|
||||
const pl = i.toString();
|
||||
payloadTypes.push(pl);
|
||||
return pl;
|
||||
}
|
||||
}
|
||||
throw Error("unable to find a free payload type");
|
||||
}
|
||||
|
||||
static #enableStereoPcmau(payloadTypes, section) {
|
||||
const lines = section.split("\r\n");
|
||||
|
||||
let payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMU/8000/2`);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMA/8000/2`);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
static #enableMultichannelOpus(payloadTypes, section) {
|
||||
const lines = section.split("\r\n");
|
||||
|
||||
let payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=rtpmap:${payloadType} multiopus/48000/3`,
|
||||
);
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`,
|
||||
);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=rtpmap:${payloadType} multiopus/48000/4`,
|
||||
);
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`,
|
||||
);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=rtpmap:${payloadType} multiopus/48000/5`,
|
||||
);
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`,
|
||||
);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=rtpmap:${payloadType} multiopus/48000/6`,
|
||||
);
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`,
|
||||
);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=rtpmap:${payloadType} multiopus/48000/7`,
|
||||
);
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`,
|
||||
);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=rtpmap:${payloadType} multiopus/48000/8`,
|
||||
);
|
||||
lines.splice(
|
||||
lines.length - 1,
|
||||
0,
|
||||
`a=fmtp:${payloadType} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`,
|
||||
);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
static #enableL16(payloadTypes, section) {
|
||||
const lines = section.split("\r\n");
|
||||
|
||||
let payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/8000/2`);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/16000/2`);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
payloadType = this.#reservePayloadType(payloadTypes);
|
||||
lines[0] += ` ${payloadType}`;
|
||||
lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/48000/2`);
|
||||
lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
static #enableStereoOpus(section) {
|
||||
let opusPayloadFormat = "";
|
||||
const lines = section.split("\r\n");
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (
|
||||
lines[i].startsWith("a=rtpmap:") &&
|
||||
lines[i].toLowerCase().includes("opus/")
|
||||
) {
|
||||
opusPayloadFormat = lines[i].slice("a=rtpmap:".length).split(" ")[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opusPayloadFormat === "") {
|
||||
return section;
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].startsWith(`a=fmtp:${opusPayloadFormat} `)) {
|
||||
if (!lines[i].includes("stereo")) {
|
||||
lines[i] += ";stereo=1";
|
||||
}
|
||||
if (!lines[i].includes("sprop-stereo")) {
|
||||
lines[i] += ";sprop-stereo=1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\r\n");
|
||||
}
|
||||
|
||||
static #editOffer(sdp, nonAdvertisedCodecs) {
|
||||
const sections = sdp.split("m=");
|
||||
|
||||
const payloadTypes = sections
|
||||
.slice(1)
|
||||
.map((s) => s.split("\r\n")[0].split(" ").slice(3))
|
||||
.reduce((prev, cur) => [...prev, ...cur], []);
|
||||
|
||||
for (let i = 1; i < sections.length; i++) {
|
||||
if (sections[i].startsWith("audio")) {
|
||||
sections[i] = this.#enableStereoOpus(sections[i]);
|
||||
|
||||
if (nonAdvertisedCodecs.includes("pcma/8000/2")) {
|
||||
sections[i] = this.#enableStereoPcmau(payloadTypes, sections[i]);
|
||||
}
|
||||
if (nonAdvertisedCodecs.includes("multiopus/48000/6")) {
|
||||
sections[i] = this.#enableMultichannelOpus(payloadTypes, sections[i]);
|
||||
}
|
||||
if (nonAdvertisedCodecs.includes("L16/48000/2")) {
|
||||
sections[i] = this.#enableL16(payloadTypes, sections[i]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join("m=");
|
||||
}
|
||||
|
||||
static #generateSdpFragment(od, candidates) {
|
||||
const candidatesByMedia = {};
|
||||
for (const candidate of candidates) {
|
||||
const mid = candidate.sdpMLineIndex;
|
||||
if (candidatesByMedia[mid] === undefined) {
|
||||
candidatesByMedia[mid] = [];
|
||||
}
|
||||
candidatesByMedia[mid].push(candidate);
|
||||
}
|
||||
|
||||
let frag = `a=ice-ufrag:${od.iceUfrag}\r\n` + `a=ice-pwd:${od.icePwd}\r\n`;
|
||||
|
||||
let mid = 0;
|
||||
|
||||
for (const media of od.medias) {
|
||||
if (candidatesByMedia[mid] !== undefined) {
|
||||
frag += `m=${media}\r\n` + `a=mid:${mid}\r\n`;
|
||||
|
||||
for (const candidate of candidatesByMedia[mid]) {
|
||||
frag += `a=${candidate.candidate}\r\n`;
|
||||
}
|
||||
}
|
||||
mid++;
|
||||
}
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
/** @param {string} err */
|
||||
#handleError(err) {
|
||||
if (this.#state === "running") {
|
||||
if (this.#pc !== null) {
|
||||
this.#pc.close();
|
||||
this.#pc = null;
|
||||
}
|
||||
|
||||
this.#offerData = null;
|
||||
|
||||
if (this.#sessionUrl !== null) {
|
||||
fetch(this.#sessionUrl, {
|
||||
method: "DELETE",
|
||||
});
|
||||
this.#sessionUrl = null;
|
||||
}
|
||||
|
||||
this.#queuedCandidates = [];
|
||||
this.#state = "restarting";
|
||||
|
||||
this.#restartTimeout = window.setTimeout(
|
||||
() => this.#restart(),
|
||||
MediaMTXWebRTCReader.#RETRY_PAUSE,
|
||||
);
|
||||
|
||||
if (this.#conf.onError !== undefined) {
|
||||
this.#conf.onError(`${err}, retrying in some seconds`);
|
||||
}
|
||||
} else if (this.#state === "getting_codecs") {
|
||||
this.#state = "failed";
|
||||
|
||||
if (this.#conf.onError !== undefined) {
|
||||
this.#conf.onError(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#restart() {
|
||||
this.#restartTimeout = null;
|
||||
this.#state = "running";
|
||||
this.#start();
|
||||
}
|
||||
|
||||
#getNonAdvertisedCodecs() {
|
||||
Promise.all(
|
||||
[
|
||||
["pcma/8000/2"],
|
||||
[
|
||||
"multiopus/48000/6",
|
||||
"channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2",
|
||||
],
|
||||
["L16/48000/2"],
|
||||
].map((c) =>
|
||||
MediaMTXWebRTCReader.#supportsNonAdvertisedCodec(c[0], c[1]).then(
|
||||
(r) => (r ? c[0] : false),
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((c) => c.filter((e) => e !== false))
|
||||
.then((codecs) => {
|
||||
if (this.#state !== "getting_codecs") {
|
||||
throw new Error("closed");
|
||||
}
|
||||
|
||||
this.#nonAdvertisedCodecs = codecs;
|
||||
this.#state = "running";
|
||||
this.#start();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.#handleError(err.toString());
|
||||
});
|
||||
}
|
||||
|
||||
#start() {
|
||||
this.#requestICEServers()
|
||||
.then((iceServers) => this.#setupPeerConnection(iceServers))
|
||||
.then((offer) => this.#sendOffer(offer))
|
||||
.then((answer) => this.#setAnswer(answer))
|
||||
.catch((err) => {
|
||||
this.#handleError(err.toString());
|
||||
});
|
||||
}
|
||||
|
||||
#authHeader() {
|
||||
if (this.#conf.user !== undefined && this.#conf.user !== "") {
|
||||
const credentials = btoa(`${this.#conf.user}:${this.#conf.pass}`);
|
||||
return { Authorization: `Basic ${credentials}` };
|
||||
}
|
||||
if (this.#conf.token !== undefined && this.#conf.token !== "") {
|
||||
return { Authorization: `Bearer ${this.#conf.token}` };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
#requestICEServers() {
|
||||
return fetch(this.#conf.url, {
|
||||
method: "OPTIONS",
|
||||
headers: this.#authHeader(),
|
||||
}).then((res) =>
|
||||
MediaMTXWebRTCReader.#linkToIceServers(res.headers.get("Link")),
|
||||
);
|
||||
}
|
||||
|
||||
#setupPeerConnection(iceServers) {
|
||||
if (this.#state !== "running") {
|
||||
throw new Error("closed");
|
||||
}
|
||||
|
||||
this.#pc = new RTCPeerConnection({
|
||||
iceServers,
|
||||
// https://webrtc.org/getting-started/unified-plan-transition-guide
|
||||
sdpSemantics: "unified-plan",
|
||||
});
|
||||
|
||||
const direction = "recvonly";
|
||||
this.#pc.addTransceiver("video", { direction });
|
||||
this.#pc.addTransceiver("audio", { direction });
|
||||
|
||||
// using data channels requires creating a data channel locally
|
||||
this.#pc.createDataChannel("");
|
||||
|
||||
this.#pc.onicecandidate = (evt) => this.#onLocalCandidate(evt);
|
||||
this.#pc.onconnectionstatechange = () => this.#onConnectionState();
|
||||
this.#pc.ontrack = (evt) => this.#onTrack(evt);
|
||||
this.#pc.ondatachannel = (evt) => this.#onDataChannel(evt);
|
||||
|
||||
return this.#pc.createOffer().then((offer) => {
|
||||
offer.sdp = MediaMTXWebRTCReader.#editOffer(
|
||||
offer.sdp,
|
||||
this.#nonAdvertisedCodecs,
|
||||
);
|
||||
this.#offerData = MediaMTXWebRTCReader.#parseOffer(offer.sdp);
|
||||
|
||||
return this.#pc.setLocalDescription(offer).then(() => offer.sdp);
|
||||
});
|
||||
}
|
||||
|
||||
#sendOffer(offer) {
|
||||
if (this.#state !== "running") {
|
||||
throw new Error("closed");
|
||||
}
|
||||
|
||||
return fetch(this.#conf.url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...this.#authHeader(),
|
||||
"Content-Type": "application/sdp",
|
||||
},
|
||||
body: offer,
|
||||
}).then((res) => {
|
||||
switch (res.status) {
|
||||
case 201:
|
||||
break;
|
||||
case 404:
|
||||
throw new Error("stream not found");
|
||||
case 400:
|
||||
return res.json().then((e) => {
|
||||
throw new Error(e.error);
|
||||
});
|
||||
default:
|
||||
throw new Error(`bad status code ${res.status}`);
|
||||
}
|
||||
|
||||
this.#sessionUrl = new URL(
|
||||
res.headers.get("location"),
|
||||
this.#conf.url,
|
||||
).toString();
|
||||
|
||||
return res.text();
|
||||
});
|
||||
}
|
||||
|
||||
#setAnswer(answer) {
|
||||
if (this.#state !== "running") {
|
||||
throw new Error("closed");
|
||||
}
|
||||
|
||||
return this.#pc
|
||||
.setRemoteDescription(
|
||||
new RTCSessionDescription({
|
||||
type: "answer",
|
||||
sdp: answer,
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
if (this.#state !== "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#queuedCandidates.length !== 0) {
|
||||
this.#sendLocalCandidates(this.#queuedCandidates);
|
||||
this.#queuedCandidates = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#onLocalCandidate(evt) {
|
||||
if (this.#state !== "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.candidate !== null) {
|
||||
if (this.#sessionUrl === null) {
|
||||
this.#queuedCandidates.push(evt.candidate);
|
||||
} else {
|
||||
this.#sendLocalCandidates([evt.candidate]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sendLocalCandidates(candidates) {
|
||||
fetch(this.#sessionUrl, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/trickle-ice-sdpfrag",
|
||||
"If-Match": "*",
|
||||
},
|
||||
body: MediaMTXWebRTCReader.#generateSdpFragment(
|
||||
this.#offerData,
|
||||
candidates,
|
||||
),
|
||||
})
|
||||
.then((res) => {
|
||||
switch (res.status) {
|
||||
case 204:
|
||||
break;
|
||||
case 404:
|
||||
throw new Error("stream not found");
|
||||
default:
|
||||
throw new Error(`bad status code ${res.status}`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.#handleError(err.toString());
|
||||
});
|
||||
}
|
||||
|
||||
#onConnectionState() {
|
||||
if (this.#state !== "running") {
|
||||
return;
|
||||
}
|
||||
|
||||
// "closed" can arrive before "failed" and without
|
||||
// the close() method being called at all.
|
||||
// It happens when the other peer sends a termination
|
||||
// message like a DTLS CloseNotify.
|
||||
if (
|
||||
this.#pc.connectionState === "failed" ||
|
||||
this.#pc.connectionState === "closed"
|
||||
) {
|
||||
this.#handleError("peer connection closed");
|
||||
}
|
||||
}
|
||||
|
||||
#onTrack(evt) {
|
||||
if (this.#conf.onTrack !== undefined) {
|
||||
this.#conf.onTrack(evt);
|
||||
}
|
||||
}
|
||||
|
||||
#onDataChannel(evt) {
|
||||
if (this.#conf.onDataChannel !== undefined) {
|
||||
this.#conf.onDataChannel(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.MediaMTXWebRTCReader = MediaMTXWebRTCReader;
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2026 James Pace
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
// defined by the Mozilla Public License, v. 2.0.
|
||||
//
|
||||
|
||||
import { atomWithQuery } from "jotai-tanstack-query";
|
||||
|
||||
export async function fetchJson(url: string): Promise<any> {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Network response was not ok: ${resp.statusText}`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
interface CreateQueryAtomOptions {
|
||||
refetchInterval?: number;
|
||||
retry?: boolean;
|
||||
}
|
||||
|
||||
export function createQueryAtom(url: string, options?: CreateQueryAtomOptions) {
|
||||
return atomWithQuery(() => ({
|
||||
queryKey: [url],
|
||||
queryFn: () => fetchJson(url),
|
||||
...options,
|
||||
}));
|
||||
}
|
||||
@@ -843,6 +843,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@socket.io/component-emitter@npm:~3.1.0":
|
||||
version: 3.1.2
|
||||
resolution: "@socket.io/component-emitter@npm:3.1.2"
|
||||
checksum: 10c0/c4242bad66f67e6f7b712733d25b43cbb9e19a595c8701c3ad99cbeb5901555f78b095e24852f862fffb43e96f1d8552e62def885ca82ae1bb05da3668fd87d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/helpers@npm:^0.5.0":
|
||||
version: 0.5.19
|
||||
resolution: "@swc/helpers@npm:0.5.19"
|
||||
@@ -1192,8 +1199,10 @@ __metadata:
|
||||
prettier: "npm:^3.8.1"
|
||||
react: "npm:^19.2.0"
|
||||
react-bootstrap: "npm:^2.10.10"
|
||||
react-bootstrap-icons: "npm:^1.11.6"
|
||||
react-dom: "npm:^19.2.0"
|
||||
react-router-dom: "npm:^7.13.1"
|
||||
socket.io-client: "npm:^4.8.3"
|
||||
typescript: "npm:~5.9.3"
|
||||
typescript-eslint: "npm:^8.48.0"
|
||||
vite: "npm:^7.3.1"
|
||||
@@ -1404,7 +1413,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.3":
|
||||
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.3, debug@npm:~4.4.1":
|
||||
version: 4.4.3
|
||||
resolution: "debug@npm:4.4.3"
|
||||
dependencies:
|
||||
@@ -1447,6 +1456,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"engine.io-client@npm:~6.6.1":
|
||||
version: 6.6.6
|
||||
resolution: "engine.io-client@npm:6.6.6"
|
||||
dependencies:
|
||||
"@socket.io/component-emitter": "npm:~3.1.0"
|
||||
debug: "npm:~4.4.1"
|
||||
engine.io-parser: "npm:~5.2.1"
|
||||
ws: "npm:~8.21.0"
|
||||
xmlhttprequest-ssl: "npm:~2.1.1"
|
||||
checksum: 10c0/805995b57b2656f52322004b4d767851b517618b1fcecabf5968c55ce6864c692e594fe3a6ae9284504aa087a5a872b0ea1b76b876dcf4cf866907bd61e9e1dd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"engine.io-parser@npm:~5.2.1":
|
||||
version: 5.2.3
|
||||
resolution: "engine.io-parser@npm:5.2.3"
|
||||
checksum: 10c0/ed4900d8dbef470ab3839ccf3bfa79ee518ea8277c7f1f2759e8c22a48f64e687ea5e474291394d0c94f84054749fd93f3ef0acb51fa2f5f234cc9d9d8e7c536
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"env-paths@npm:^2.2.0":
|
||||
version: 2.2.1
|
||||
resolution: "env-paths@npm:2.2.1"
|
||||
@@ -2484,7 +2513,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1":
|
||||
"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
dependencies:
|
||||
@@ -2502,6 +2531,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-bootstrap-icons@npm:^1.11.6":
|
||||
version: 1.11.6
|
||||
resolution: "react-bootstrap-icons@npm:1.11.6"
|
||||
dependencies:
|
||||
prop-types: "npm:^15.7.2"
|
||||
peerDependencies:
|
||||
react: ">=16.8.6"
|
||||
checksum: 10c0/4dfd75c980a4431032e7d861d52efeb18cb141e00300ae1cf055dcfcb1fd7689991d8b467c38f1103563662bbbd61aaf247093e7c04f6f1ad0aae86feff5047d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-bootstrap@npm:^2.10.10":
|
||||
version: 2.10.10
|
||||
resolution: "react-bootstrap@npm:2.10.10"
|
||||
@@ -2778,6 +2818,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socket.io-client@npm:^4.8.3":
|
||||
version: 4.8.3
|
||||
resolution: "socket.io-client@npm:4.8.3"
|
||||
dependencies:
|
||||
"@socket.io/component-emitter": "npm:~3.1.0"
|
||||
debug: "npm:~4.4.1"
|
||||
engine.io-client: "npm:~6.6.1"
|
||||
socket.io-parser: "npm:~4.2.4"
|
||||
checksum: 10c0/76c0d86de0b636d0bf5011cf3425212c900f43dac632db6eb493816920cd035af7ddd92fea9a106b45eb347405953c0414eb3a5d465b180215e46fc8b61420b3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socket.io-parser@npm:~4.2.4":
|
||||
version: 4.2.7
|
||||
resolution: "socket.io-parser@npm:4.2.7"
|
||||
dependencies:
|
||||
"@socket.io/component-emitter": "npm:~3.1.0"
|
||||
debug: "npm:~4.4.1"
|
||||
checksum: 10c0/16a5579718b871114ca644d688987dd3cf9292010c949fb083633eefbc1d8fdaf424691d6511d746e5c10f5c88cbd8911cd0b1e807242159f40989be90842b4e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socks-proxy-agent@npm:^8.0.3":
|
||||
version: 8.0.5
|
||||
resolution: "socks-proxy-agent@npm:8.0.5"
|
||||
@@ -3078,6 +3140,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:~8.21.0":
|
||||
version: 8.21.3
|
||||
resolution: "ws@npm:8.21.3"
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ">=5.0.2"
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
checksum: 10c0/7b28dc2863ea0e2cece68d142a3eee90361021b73f750431e6d8076bb7dede5fdfdb75b3d29534b62f411261147b76b1bc80fb5cc63ab0aabd467280e85b22e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xmlhttprequest-ssl@npm:~2.1.1":
|
||||
version: 2.1.2
|
||||
resolution: "xmlhttprequest-ssl@npm:2.1.2"
|
||||
checksum: 10c0/70d60869323e823f473a238f78fd108437edbc3690ecd5859c39c83217080090a18899b272e515769c0d1f518cc64cbed6b6995b23fdd7ba13b297d530b6e631
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yallist@npm:^3.0.2":
|
||||
version: 3.1.1
|
||||
resolution: "yallist@npm:3.1.1"
|
||||
|
||||
Reference in new issue
Block a user