From 1bf7d866d97b791d226dc9b8c23de0357bf478b4 Mon Sep 17 00:00:00 2001 From: TK11235 Date: Tue, 14 Nov 2023 23:42:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E3=81=97=E3=81=84SkyWay?= =?UTF-8?q?=E3=81=AB=E3=82=88=E3=82=8B=E9=80=9A=E4=BF=A1=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=AF=E3=83=A9=E3=82=B9=E7=BE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **概要** `Network`クラス内でオブジェクト化している`SkyWayConnection`クラスの実装を 現在の"src\app\class\core\system\network\skyway\skyway-connection.ts"から 新規実装した"src\app\class\core\system\network\skyway2023\skyway-connection.ts"に 切り替えることで新しいSkyWayを使用した通信処理が可能になる. **制約** - 新しいSkyWayのChannelの概念の都合からユドナリウムのプライベート接続をオミットしている. - 現状のコードはバックエンドのモック実装を含んでおりセキュリティ的に問題があるため本番環境で使用してはならない.(詳細は下記) **シークレットキーの取り扱いについて** 新しいSkyWayはJWTによる権限管理を行っており、JWTの署名に使用するシークレットキーはクライアントに秘匿される必要がある. しかし、現状のコードではバックエンド処理(`SkyWayBackend`)がモックとなっておりシークレットキーが秘匿されないため危険である. 安全に運用するためにはJWTを発行するWeb APIなどの準備が別途必要. --- package-lock.json | 438 +++++++++++++++++- package.json | 1 + .../network/skyway2023/skyway-backend.ts | 93 ++++ .../network/skyway2023/skyway-connection.ts | 382 +++++++++++++++ .../skyway2023/skyway-data-stream-list.ts | 85 ++++ .../network/skyway2023/skyway-data-stream.ts | 324 +++++++++++++ .../network/skyway2023/skyway-facade.ts | 423 +++++++++++++++++ tsconfig.json | 1 + 8 files changed, 1736 insertions(+), 11 deletions(-) create mode 100644 src/app/class/core/system/network/skyway2023/skyway-backend.ts create mode 100644 src/app/class/core/system/network/skyway2023/skyway-connection.ts create mode 100644 src/app/class/core/system/network/skyway2023/skyway-data-stream-list.ts create mode 100644 src/app/class/core/system/network/skyway2023/skyway-data-stream.ts create mode 100644 src/app/class/core/system/network/skyway2023/skyway-facade.ts diff --git a/package-lock.json b/package-lock.json index 93bf014dd..fec127b46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@angular/platform-browser": "^15.2.8", "@angular/platform-browser-dynamic": "^15.2.8", "@angular/router": "^15.2.8", + "@skyway-sdk/core": "^1.4.1", "base-x": "^4.0.0", "bcdice": "^4.2.0", "crypto-js": "^4.1.1", @@ -3042,6 +3043,136 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@skyway-sdk/common": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/common/-/common-1.4.1.tgz", + "integrity": "sha512-4Eddi5EbIzUjI33BqT19dAX+TssZSQL8E1OrB+uh7dygsYDj3Mme0bejnQm+LesfJYqWWNBcu2DBQGJZ/AHLbQ==", + "dependencies": { + "axios": "^0.23.0" + } + }, + "node_modules/@skyway-sdk/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/core/-/core-1.4.1.tgz", + "integrity": "sha512-nTx9HLKQJb8SbromrBR5uJ+aD4P4Y9cCAKAgsDqcPEaqk/8ybNgncW9Bj9ARRsF7NUkX9v6QOzkWbYF5eOoR5Q==", + "dependencies": { + "@skyway-sdk/rtc-api-client": "^1.4.1", + "@skyway-sdk/signaling-client": "^1.0.1", + "bowser": "^2.11.0", + "deepmerge": "^4.2.2", + "sdp-transform": "^2.14.1", + "uuid": "^9.0.0" + } + }, + "node_modules/@skyway-sdk/core/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@skyway-sdk/model": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@skyway-sdk/model/-/model-1.0.0.tgz", + "integrity": "sha512-7I3n5yMnf9vZCWpRwH1/lxXSdVA6xw/hQ864leUkYJkAhKDvYPVcgD+2qWJyh7tXSLqUNGzvsBhphuZ747OZiA==" + }, + "node_modules/@skyway-sdk/rtc-api-client": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/rtc-api-client/-/rtc-api-client-1.4.1.tgz", + "integrity": "sha512-RVOTjLK8QvEELewMOZT+ee3H5kTq6ttKt6r5b6VdMKn3WREaKiQ62lNtGZq9hnSNB05hoiYBD+2nogz72QSVEw==", + "dependencies": { + "@skyway-sdk/rtc-rpc-api-client": "^1.4.1", + "@skyway-sdk/token": "^1.4.1", + "deepmerge": "^4.2.2", + "uuid": "^9.0.0" + } + }, + "node_modules/@skyway-sdk/rtc-api-client/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@skyway-sdk/rtc-rpc-api-client": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/rtc-rpc-api-client/-/rtc-rpc-api-client-1.4.1.tgz", + "integrity": "sha512-XXVXj8oIFkTKMSrKTwqvJUoakdbpaqZLtKYFn9qm9YoJJtOCVP9P344vK4k4Y+F+/aVzLKEavHurulIhXVHKSA==", + "dependencies": { + "@skyway-sdk/common": "^1.4.1", + "@skyway-sdk/model": "^1.0.0", + "isomorphic-ws": "^4.0.1", + "uuid": "^9.0.0" + } + }, + "node_modules/@skyway-sdk/rtc-rpc-api-client/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@skyway-sdk/signaling-client": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/signaling-client/-/signaling-client-1.0.1.tgz", + "integrity": "sha512-Ulm4G7TsjsLKhMEnffDjZB4HHBitm44b4e3NILeCs9XMq8+8iqt5fXjiDK0WjEwt8zfIr4PTkCTy8LRzj5q8uQ==", + "dependencies": { + "isomorphic-fetch": "^3.0.0", + "isomorphic-ws": "^4.0.1", + "uuid": "^9.0.0", + "ws": "^7.5.2" + } + }, + "node_modules/@skyway-sdk/signaling-client/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@skyway-sdk/signaling-client/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@skyway-sdk/token": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/token/-/token-1.4.1.tgz", + "integrity": "sha512-lA7qijBLc+AAzRbS2HyAISTzF1vevjC5eWgHHCKFmuLQ57SeLTnWrTG3uxn1ZiPRuHvKU853nCBoWtD0NdPBHQ==", + "dependencies": { + "@skyway-sdk/common": "^1.4.1", + "jsrsasign": "^10.6.0", + "jwt-decode": "3.1.2", + "uuid": "^9.0.0" + } + }, + "node_modules/@skyway-sdk/token/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -3810,6 +3941,14 @@ "postcss": "^8.1.0" } }, + "node_modules/axios": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz", + "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==", + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, "node_modules/babel-loader": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", @@ -4046,6 +4185,11 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -4886,6 +5030,14 @@ } } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -5102,7 +5254,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -5112,7 +5263,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -5763,7 +5913,6 @@ "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true, "funding": [ { "type": "individual", @@ -6747,6 +6896,23 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -6973,6 +7139,14 @@ "node >= 0.2.0" ] }, + "node_modules/jsrsasign": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.8.6.tgz", + "integrity": "sha512-bQmbVtsfbgaKBTWCKiDCPlUPbdlRIK/FzSwT3BzIgZl/cU6TqXu6pZJsCI/dJVrZ9Gir5GC4woqw9shH/v7MBw==", + "funding": { + "url": "https://github.com/kjur/jsrsasign#donations" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -6989,6 +7163,11 @@ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "node_modules/karma": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", @@ -8221,6 +8400,25 @@ "dev": true, "optional": true }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -9770,7 +9968,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sass": { "version": "1.58.1", @@ -9853,6 +10051,14 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", + "bin": { + "sdp-verify": "checker.js" + } + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -10866,6 +11072,11 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -11224,6 +11435,11 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "node_modules/webpack": { "version": "5.76.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", @@ -11487,6 +11703,20 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -11574,7 +11804,6 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, "engines": { "node": ">=10.0.0" }, @@ -13663,6 +13892,117 @@ "integrity": "sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==", "dev": true }, + "@skyway-sdk/common": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/common/-/common-1.4.1.tgz", + "integrity": "sha512-4Eddi5EbIzUjI33BqT19dAX+TssZSQL8E1OrB+uh7dygsYDj3Mme0bejnQm+LesfJYqWWNBcu2DBQGJZ/AHLbQ==", + "requires": { + "axios": "^0.23.0" + } + }, + "@skyway-sdk/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/core/-/core-1.4.1.tgz", + "integrity": "sha512-nTx9HLKQJb8SbromrBR5uJ+aD4P4Y9cCAKAgsDqcPEaqk/8ybNgncW9Bj9ARRsF7NUkX9v6QOzkWbYF5eOoR5Q==", + "requires": { + "@skyway-sdk/rtc-api-client": "^1.4.1", + "@skyway-sdk/signaling-client": "^1.0.1", + "bowser": "^2.11.0", + "deepmerge": "^4.2.2", + "sdp-transform": "^2.14.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, + "@skyway-sdk/model": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@skyway-sdk/model/-/model-1.0.0.tgz", + "integrity": "sha512-7I3n5yMnf9vZCWpRwH1/lxXSdVA6xw/hQ864leUkYJkAhKDvYPVcgD+2qWJyh7tXSLqUNGzvsBhphuZ747OZiA==" + }, + "@skyway-sdk/rtc-api-client": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/rtc-api-client/-/rtc-api-client-1.4.1.tgz", + "integrity": "sha512-RVOTjLK8QvEELewMOZT+ee3H5kTq6ttKt6r5b6VdMKn3WREaKiQ62lNtGZq9hnSNB05hoiYBD+2nogz72QSVEw==", + "requires": { + "@skyway-sdk/rtc-rpc-api-client": "^1.4.1", + "@skyway-sdk/token": "^1.4.1", + "deepmerge": "^4.2.2", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, + "@skyway-sdk/rtc-rpc-api-client": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/rtc-rpc-api-client/-/rtc-rpc-api-client-1.4.1.tgz", + "integrity": "sha512-XXVXj8oIFkTKMSrKTwqvJUoakdbpaqZLtKYFn9qm9YoJJtOCVP9P344vK4k4Y+F+/aVzLKEavHurulIhXVHKSA==", + "requires": { + "@skyway-sdk/common": "^1.4.1", + "@skyway-sdk/model": "^1.0.0", + "isomorphic-ws": "^4.0.1", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, + "@skyway-sdk/signaling-client": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/signaling-client/-/signaling-client-1.0.1.tgz", + "integrity": "sha512-Ulm4G7TsjsLKhMEnffDjZB4HHBitm44b4e3NILeCs9XMq8+8iqt5fXjiDK0WjEwt8zfIr4PTkCTy8LRzj5q8uQ==", + "requires": { + "isomorphic-fetch": "^3.0.0", + "isomorphic-ws": "^4.0.1", + "uuid": "^9.0.0", + "ws": "^7.5.2" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } + } + }, + "@skyway-sdk/token": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@skyway-sdk/token/-/token-1.4.1.tgz", + "integrity": "sha512-lA7qijBLc+AAzRbS2HyAISTzF1vevjC5eWgHHCKFmuLQ57SeLTnWrTG3uxn1ZiPRuHvKU853nCBoWtD0NdPBHQ==", + "requires": { + "@skyway-sdk/common": "^1.4.1", + "jsrsasign": "^10.6.0", + "jwt-decode": "3.1.2", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -14335,6 +14675,14 @@ "postcss-value-parser": "^4.2.0" } }, + "axios": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.23.0.tgz", + "integrity": "sha512-NmvAE4i0YAv5cKq8zlDoPd1VLKAqX5oLuZKs8xkJa4qi6RGn0uhCYFjWtHHC9EM/MwOwYWOs53W+V0aqEXq1sg==", + "requires": { + "follow-redirects": "^1.14.4" + } + }, "babel-loader": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", @@ -14525,6 +14873,11 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -15141,6 +15494,11 @@ "ms": "2.1.2" } }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, "default-gateway": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", @@ -15308,7 +15666,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "requires": { "iconv-lite": "^0.6.2" @@ -15318,7 +15675,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -15836,8 +16192,7 @@ "follow-redirects": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "dev": true + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" }, "forwarded": { "version": "0.2.0", @@ -16556,6 +16911,21 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "isomorphic-ws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", + "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", + "requires": {} + }, "istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -16733,6 +17103,11 @@ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", "dev": true }, + "jsrsasign": { + "version": "10.8.6", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-10.8.6.tgz", + "integrity": "sha512-bQmbVtsfbgaKBTWCKiDCPlUPbdlRIK/FzSwT3BzIgZl/cU6TqXu6pZJsCI/dJVrZ9Gir5GC4woqw9shH/v7MBw==" + }, "jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -16751,6 +17126,11 @@ } } }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, "karma": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.1.tgz", @@ -17712,6 +18092,14 @@ "dev": true, "optional": true }, + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -18858,7 +19246,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "sass": { "version": "1.58.1", @@ -18900,6 +19288,11 @@ "ajv-keywords": "^5.0.0" } }, + "sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -19703,6 +20096,11 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -19959,6 +20357,11 @@ "defaults": "^1.0.3" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, "webpack": { "version": "5.76.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", @@ -20136,6 +20539,20 @@ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, + "whatwg-fetch": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", + "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -20207,7 +20624,6 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "dev": true, "requires": {} }, "y18n": { diff --git a/package.json b/package.json index 072ec089e..23dbce5de 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@angular/platform-browser": "^15.2.8", "@angular/platform-browser-dynamic": "^15.2.8", "@angular/router": "^15.2.8", + "@skyway-sdk/core": "^1.4.1", "base-x": "^4.0.0", "bcdice": "^4.2.0", "crypto-js": "^4.1.1", diff --git a/src/app/class/core/system/network/skyway2023/skyway-backend.ts b/src/app/class/core/system/network/skyway2023/skyway-backend.ts new file mode 100644 index 000000000..5ff3c957f --- /dev/null +++ b/src/app/class/core/system/network/skyway2023/skyway-backend.ts @@ -0,0 +1,93 @@ +import { ChannelScope, nowInSec, SkyWayAuthToken, uuidV4 } from '@skyway-sdk/core'; + +export namespace SkyWayBackend { + export async function createSkyWayAuthToken(appId: string, channelName: string, peerId: string): Promise { + return createSkyWayAuthTokenMock(appId, channelName, peerId); + } +} + +/** + * SkyWayAuthTokenを生成するモック実装. + * + * **シークレットキーはフロントエンドでは秘匿されている必要があります. この実装を本番環境で運用しないでください.** + * + * サーバを構築せずにフロントエンドでSkyWayAuthTokenを生成した場合、 + * シークレットキーをエンドユーザが取得できるため、誰でも任意のChannelやRoomを生成して参加できる等のセキュリティ上の問題が発生します. + * + * @param appId アプリケーションID + * @param channelName 接続するチャンネルの名称 + * @param peerId PeerId + * @returns JWT + */ +async function createSkyWayAuthTokenMock(appId: string, channelName: string, peerId: string): Promise { + // モック実装のため、アプリケーションIDとシークレットキーは固定値 + // 本番環境ではシークレットキーをサーバなどに置いて秘匿する + const _appId = ''; + const _secret = ''; + + const _lobbySize = 4; + + let lobbyChannels: ChannelScope[] = []; + for (let index = 0; index < _lobbySize; index++) { + lobbyChannels.push({ + name: `udonarium-lobby-${index}`, + actions: ['read', 'create'], + members: [ + { + name: peerId, + actions: ['write'], + publication: { + actions: [], + }, + subscription: { + actions: [], + }, + }, + ], + }); + } + + let roomChannels: ChannelScope[] = []; + roomChannels.push({ + name: channelName, + actions: ['read', 'create'], + members: [ + { + name: peerId, + actions: ['write'], + publication: { + actions: ['write'], + }, + subscription: { + actions: ['write'], + }, + }, + { + name: '*', + actions: ['signal'], + publication: { + actions: [], + }, + subscription: { + actions: [], + }, + }, + ], + }); + + let token = new SkyWayAuthToken({ + jti: uuidV4(), + iat: nowInSec(), + exp: nowInSec() + 60 * 60 * 24, + scope: { + app: { + id: _appId, + turn: false, + actions: ['read'], + channels: lobbyChannels.concat(roomChannels), + }, + }, + }).encode(_secret); + + return token; +} diff --git a/src/app/class/core/system/network/skyway2023/skyway-connection.ts b/src/app/class/core/system/network/skyway2023/skyway-connection.ts new file mode 100644 index 000000000..daea24524 --- /dev/null +++ b/src/app/class/core/system/network/skyway2023/skyway-connection.ts @@ -0,0 +1,382 @@ +import { ArrayUtil } from '../../util/array-util'; +import { compressAsync, decompressAsync } from '../../util/compress'; +import { MessagePack } from '../../util/message-pack'; +import { setZeroTimeout } from '../../util/zero-timeout'; +import { Connection, ConnectionCallback } from "../connection"; +import { IPeerContext, PeerContext } from "../peer-context"; +import { IRoomInfo, RoomInfo } from "../room-info"; +import { SkyWayDataStream } from './skyway-data-stream'; +import { SkyWayDataStreamList } from './skyway-data-stream-list'; +import { SkyWayFacade } from './skyway-facade'; + +type PeerId = string; + +interface DataContainer { + data: Uint8Array; + users?: string[]; + ttl: number; + isCompressed?: boolean; +} + +export class SkyWayConnection implements Connection { + private get userIds(): string[] { return this.peers.map(peer => peer.userId).filter(userId => 0 < userId.length).concat([this.peer.userId]); } + + get peerId(): string { return this.peer.peerId; } + get peerIds(): string[] { return this.streams.peerIds; } + + get peer(): PeerContext { return this.skyWay.peer; } + get peers(): PeerContext[] { return this.streams.peers; } + + readonly callback: ConnectionCallback = new ConnectionCallback(); + bandwidthUsage: number = 0; + + private appId = ''; + private readonly skyWay: SkyWayFacade = new SkyWayFacade(); + private readonly streams: SkyWayDataStreamList = new SkyWayDataStreamList(); + + private listAllPeersCache: PeerId[] = []; + private httpRequestInterval: number = performance.now() + 500; + + private outboundQueue: Promise = Promise.resolve(); + private inboundQueue: Promise = Promise.resolve(); + + private readonly trustedPeerIds: Set = new Set(); + + open(userId?: string) + open(userId: string, roomId: string, roomName: string, password: string) + open(...args: any[]) { + console.log('open', args); + let peer: PeerContext; + if (args.length === 0) { + peer = PeerContext.create(PeerContext.generateId()); + } else if (args.length === 1) { + peer = PeerContext.create(args[0]); + } else { + peer = PeerContext.create(args[0], args[1], args[2], args[3]); + } + this.trustedPeerIds.clear(); + this.openSkyWay(peer); + } + + close() { + this.disconnectAll(); + this.skyWay.close(); + } + + connect(peer: IPeerContext): boolean { + if (!this.peer.isRoom) { + console.warn('connect() is Fail. ルーム接続のみ可能'); + let errorType = 'udonarium-unsupported'; + let errorMessage = '現在のユドナリウムでSkyWay(2023)を使用する場合、プライベート接続は利用できません。ルーム接続機能を利用してください。'; + if (this.callback.onError) this.callback.onError(this.peer, errorType, errorMessage, {}); + return false; + } + + if (!this.shouldConnect(peer.peerId)) return false; + + console.log(`connect() ${peer.peerId}`); + this.subscribe(peer); + return true; + } + + private shouldConnect(peerId: string): boolean { + if (!this.skyWay.isOpen) { + console.log('connect() is Fail. IDが割り振られるまで待てや'); + return false; + } + + if (this.peerId === peerId) { + console.log('connect() is Fail. ' + peerId + ' is me.'); + return false; + } + + if (this.peerIds.includes(peerId)) { + console.log('connect() is Fail. <' + peerId + '> is already connecting.'); + return false; + } + + if (!this.peer.verifyPeer(peerId)) { + console.log('connect() is Fail. <' + peerId + '> is not valid.'); + return false; + } + + if (peerId && peerId.length && peerId !== this.peerId) return true; + return false; + } + + disconnect(peer: IPeerContext): boolean { + let stream = this.streams.find(peer.peerId) + if (!stream) return false; + this.unsubscribe(stream.peer); + return true; + } + + disconnectAll() { + for (let peer of this.peers) { + this.disconnect(peer); + } + } + + send(data: any, sendTo?: string) { + if (this.peers.length < 1) return; + //console.log('send', data); + let container: DataContainer = { + data: MessagePack.encode(data), + ttl: 1 + } + + let byteLength = container.data.byteLength; + this.bandwidthUsage += byteLength; + this.outboundQueue = this.outboundQueue.then(() => new Promise((resolve, reject) => { + setZeroTimeout(async () => { + if (1 * 1024 < container.data.byteLength && Array.isArray(data) && 1 < data.length) { + let compressed = await compressAsync(container.data); + if (compressed.byteLength < container.data.byteLength) { + container.data = compressed; + container.isCompressed = true; + } + } + if (sendTo) { + this.sendUnicast(container, sendTo); + } else { + this.sendBroadcast(container); + } + this.bandwidthUsage -= byteLength; + return resolve(); + }); + })); + } + + private sendUnicast(container: DataContainer, sendTo: string) { + container.ttl = 0; + let stream = this.streams.find(sendTo); + if (stream && stream.open) stream.send(container); + } + + private sendBroadcast(container: DataContainer) { + for (let stream of this.streams) { + if (stream.open) stream.send(container); + } + } + + setApiKey(key: string) { + console.warn('Method not implemented. set hard code value.'); + this.skyWay.appId = this.appId; + } + + async listAllPeers(): Promise { + let now = performance.now(); + if (now < this.httpRequestInterval) { + console.warn('httpRequestInterval... ' + (this.httpRequestInterval - now)); + } else { + this.httpRequestInterval = now + 10000; + this.listAllPeersCache = await this.skyWay.listAllPeers(); + } + + return this.listAllPeersCache; + } + + async listAllRooms(): Promise { + let allPeerIds = await this.listAllPeers(); + return RoomInfo.listFrom(allPeerIds); + } + + private async openSkyWay(peer: IPeerContext) { + if (this.skyWay.context) { + console.warn('It is already opened.'); + await this.skyWay.close(); + } + + this.skyWay.onOpen = peer => { + console.log('skyWay onOpen', peer); + console.log('My peer Context', this.peer); + if (this.callback.onOpen) this.callback.onOpen(this.peer); + }; + + this.skyWay.onClose = peer => { + console.log('skyWay onClose', peer); + if (this.peer.isOpen) this.close(); + if (this.callback.onClose) this.callback.onClose(this.peer); + }; + + this.skyWay.onFatalError = (peer, errorType, errorMessage, errorObject) => { + console.error('skyWay onFatalError', errorObject); + if (this.peer.isOpen) { + this.close(); + if (this.callback.onClose) this.callback.onClose(this.peer); + } + if (this.callback.onError) this.callback.onError(this.peer, errorType, errorMessage, errorObject); + }; + + this.skyWay.onConnectionStateChanged = (peer, state) => { + console.log(`publication onConnectionStateChanged ${peer.peerId} -> ${state}`); + let stream = this.streams.find(peer.peerId); + if (!stream) return; + switch (state) { + case 'new': + break; + case 'connecting': + stream.peer.isOpen = false; + break; + case 'connected': + if (this.skyWay.isConnectedDataStream(stream.member)) { + console.log(`onConnect ${stream.peer.peerId}`); + stream.refresh(); + this.trustedPeerIds.add(stream.peer.peerId); + this.notifyUserList(); + if (this.callback.onConnect) this.callback.onConnect(stream.peer); + } + break; + case 'reconnecting': + stream.peer.isOpen = false; + break; + case 'disconnected': + this.unsubscribe(stream.peer); + break; + } + } + + this.skyWay.onSubscribed = (peer, subscription) => { + console.log(`publication onSubscribed ${peer.peerId}`); + + let validPeerId = this.peer.verifyPeer(peer.peerId); + if (!validPeerId) { + subscription.cancel(); + console.log('connection is close. <' + peer.peerId + '> is not valid.'); + return; + } + console.log('connection is subscribed. <' + peer.peerId + '> is valid.'); + + this.subscribe(peer); + } + + this.skyWay.onUnsubscribed = (peer, subscription) => { + console.log(`publication onUnsubscribed ${peer.peerId}`); + let stream = this.streams.find(peer.peerId); + if (stream == null) return; + + this.unsubscribe(stream.peer); + } + + this.skyWay.onDataStreamPublished = (peer, publication) => { + let stream = this.streams.find(peer.peerId); + if (stream == null || stream.open) return; + stream.subscribe(); + //this.connect(peer); + } + + this.skyWay.onRoomRestore = (peer) => { + for (let peerId of this.trustedPeerIds) { + let peer = PeerContext.parse(peerId); + this.unsubscribe(peer); + this.connect(peer); + } + } + + await this.skyWay.open(peer); + return; + } + + private async subscribe(peer: IPeerContext) { + if (this.streams.find(peer.peerId)) { + console.log(`${peer.peerId} is already subscribed`); + return; + } + let stream = new SkyWayDataStream(this.skyWay, peer); + + stream.on('data', data => { + this.onData(stream, data); + }); + stream.on('open', () => { + if (this.skyWay.isConnectedDataStream(stream.member)) { + console.log(`onConnect ${stream.peer.peerId}`); + stream.refresh(); + this.trustedPeerIds.add(stream.peer.peerId); + this.notifyUserList(); + if (this.callback.onConnect) this.callback.onConnect(stream.peer); + } + }); + stream.on('close', () => { + this.unsubscribe(stream.peer); + }); + stream.on('error', () => { + this.unsubscribe(stream.peer); + }); + stream.on('stats', () => { + if (stream.peer.session.health < 0.2) { + this.unsubscribe(stream.peer); + } + }); + + this.streams.add(stream); + await stream.subscribe(); + return; + } + + private async unsubscribe(peer: IPeerContext) { + let closed = this.streams.find(peer.peerId); + if (!closed) return; + this.streams.remove(closed); + await closed.unsubscribe(); + if (closed && this.callback.onDisconnect) this.callback.onDisconnect(closed.peer); + } + + private onData(stream: SkyWayDataStream, container: DataContainer) { + if (container.users && 0 < container.users.length) this.onUpdateUserIds(stream, container.users); + //if (0 < container.ttl) this.onRelay(conn, container); + if (!this.callback.onData) return; + let byteLength = container.data.byteLength; + this.bandwidthUsage += byteLength; + this.inboundQueue = this.inboundQueue.then(() => new Promise((resolve, reject) => { + setZeroTimeout(async () => { + if (!this.callback.onData) return; + let data = container.isCompressed ? await decompressAsync(container.data) : container.data; + this.callback.onData(stream.peer, MessagePack.decode(data)); + this.bandwidthUsage -= byteLength; + return resolve(); + }); + })); + } + + private onUpdateUserIds(stream: SkyWayDataStream, userIds: string[]) { + let needsNotifyUserList = false; + userIds.forEach(userId => { + let peer = this.makeFriendPeer(userId); + let stream = this.streams.find(peer.peerId); + if (stream && stream.peer.userId !== userId) { + stream.peer.userId = userId; + needsNotifyUserList = true; + } + }); + + let diff = ArrayUtil.diff(this.userIds, userIds); + let unknownUserIds = diff.diff2; + + if (unknownUserIds.length) { + for (let userId of unknownUserIds) { + let peer = this.makeFriendPeer(userId); + if (this.connect(peer)) { + console.log('auto connect to unknown Peer <' + peer.peerId + '>'); + } + } + } + if (needsNotifyUserList) this.notifyUserList(); + } + + private notifyUserList() { + this.streams.refresh(); + if (this.streams.length < 1) return; + let container: DataContainer = { + data: MessagePack.encode([]), + users: this.userIds, + ttl: 1 + } + this.sendBroadcast(container); + } + + private makeFriendPeer(userId: string): PeerContext { + return this.peer.isRoom + ? PeerContext.create(userId, this.peer.roomId, this.peer.roomName, this.peer.password) + : PeerContext.create(userId); + } +} diff --git a/src/app/class/core/system/network/skyway2023/skyway-data-stream-list.ts b/src/app/class/core/system/network/skyway2023/skyway-data-stream-list.ts new file mode 100644 index 000000000..36791aad8 --- /dev/null +++ b/src/app/class/core/system/network/skyway2023/skyway-data-stream-list.ts @@ -0,0 +1,85 @@ +import { PeerContext } from "../peer-context"; +import { SkyWayDataStream } from "./skyway-data-stream"; + +export class SkyWayDataStreamList implements Iterable { + private streams: SkyWayDataStream[] = []; + get length(): number { return this.streams.length; } + + [Symbol.iterator]() { + let streams = this.streams.concat(); + let index = 0; + return { + next(): IteratorResult { + return { value: streams[index++], done: streams.length + 1 <= index }; + } + }; + } + + private needsRefreshPeers = false; + private _peers: PeerContext[] = []; + get peers(): PeerContext[] { + if (this.needsRefreshPeers) { + this.needsRefreshPeers = false; + this._peers = this.streams.map(stream => stream.peer); + this._peers.sort((a, b) => { + if (a.peerId > b.peerId) return 1; + if (a.peerId < b.peerId) return -1; + return 0; + }); + } + return this._peers; + } + + private needsRefreshPeerIds = false; + private _peerIds: string[] = []; + get peerIds(): string[] { + if (this.needsRefreshPeerIds) { + this.needsRefreshPeerIds = false; + let peerIds: string[] = []; + for (let stream of this.streams) { + if (stream.open) peerIds.push(stream.peer.peerId); + } + peerIds.sort((a, b) => { + if (a > b) return 1; + if (a < b) return -1; + return 0; + }); + this._peerIds = peerIds; + } + return this._peerIds + } + + add(stream: SkyWayDataStream): SkyWayDataStream { + let existStream = this.find(stream.peer.peerId); + if (existStream) { + console.log('add() is Fail. ' + stream.peer.peerId + ' is already connecting.', existStream); + return existStream; + } + //stream.subscribe(); + this.streams.push(stream); + this.refresh(); + console.log(' Peer:' + stream.peer.peerId + ' length:' + this.streams.length); + return stream; + } + + remove(stream: SkyWayDataStream): SkyWayDataStream { + //stream.unsubscribe(); + let index = this.streams.indexOf(stream); + if (0 <= index) { + console.log(stream.peer.peerId + ' is えんいー' + 'index:' + index + ' length:' + this.streams.length); + this.streams.splice(index, 1); + this.refresh(); + } + console.log(' Peer:' + stream.peer.peerId + ' length:' + this.streams.length); + return 0 <= index ? stream : null; + } + + find(peerId: string): SkyWayDataStream { + return this.streams.find(stream => stream.peer.peerId === peerId); + } + + refresh() { + this.needsRefreshPeers = true; + this.needsRefreshPeerIds = true; + } +} diff --git a/src/app/class/core/system/network/skyway2023/skyway-data-stream.ts b/src/app/class/core/system/network/skyway2023/skyway-data-stream.ts new file mode 100644 index 000000000..784ae9a0b --- /dev/null +++ b/src/app/class/core/system/network/skyway2023/skyway-data-stream.ts @@ -0,0 +1,324 @@ +import { P2PConnection, RemoteDataStream, RemoteMember, Subscription } from "@skyway-sdk/core"; +import { EventEmitter } from "events"; +import { MessagePack } from "../../util/message-pack"; +import { UUID } from "../../util/uuid"; +import { setZeroTimeout } from "../../util/zero-timeout"; +import { IPeerContext, PeerContext } from "../peer-context"; +import { PeerSessionGrade } from "../peer-session-state"; +import { CandidateType, WebRTCStats } from "../webrtc/webrtc-stats"; +import { WebRTCConnection, WebRTCStatsMonitor } from "../webrtc/webrtc-stats-monitor"; +import { SkyWayFacade } from "./skyway-facade"; + +interface Ping { + from: string; + ping: number; +}; + +interface DataChank { + id: string; + data: Uint8Array; + index: number; + total: number; +}; + +interface ReceivedChank { + id: string; + chanks: Uint8Array[]; + length: number; + byteLength: number; +}; + +export class SkyWayDataStream extends EventEmitter implements WebRTCConnection { + readonly peer: PeerContext; + + private chunkSize = 15.5 * 1024; + private receivedMap: Map = new Map(); + + private stats: WebRTCStats; + + get open(): boolean { return this.peer.isOpen; } + get member(): RemoteMember { return this.skyWay.room?.members.find(member => member.name === this.peer.peerId); } + + private subscription: Subscription; + private senderDataChannel: RTCDataChannel; + private receiverDataChannel: RTCDataChannel; + + private isBuffering = false; + private buffer: Set = new Set(); + + private _timestamp: number = performance.now(); + get timestamp(): number { return this._timestamp; } + private set timestamp(timestamp: number) { this._timestamp = timestamp }; + + private _ping: number = 0; + get ping(): number { return this._ping; } + private set ping(ping: number) { this._ping = ping }; + + private _candidateType: CandidateType = CandidateType.UNKNOWN; + get candidateType(): CandidateType { return this._candidateType; } + private set candidateType(candidateType: CandidateType) { this._candidateType = candidateType }; + + constructor(readonly skyWay: SkyWayFacade, peer: IPeerContext) { + super(); + + this.peer = PeerContext.parse(peer.peerId); + this.peer.userId = peer.userId; + this.peer.password = peer.password; + } + + async subscribe() { + if (!this.shouldSubscribe()) return; + + let member = this.member; + let publication = member.publications.find(publication => publication.metadata === 'udonarium-data-stream'); + + console.log(`subscription ready ${publication.id}`); + let { subscription, stream } = await this.skyWay.roomPerson.subscribe(publication.id); + + subscription.onCanceled.add(() => { + console.log(`subscription onCanceled ${member.name}`); + this.subscription = null; + }); + + subscription.onStreamAttached.add(() => { + console.log(`subscription onStreamAttached ${member.name}`); + }); + + subscription.onConnectionStateChanged.add(state => { + console.log(`subscription onConnectionStateChanged ${member.name} -> ${state}`); + this.refresh(); + switch (state) { + case 'new': + break; + case 'connecting': + break; + case 'connected': + this.emit('open'); + break; + case 'reconnecting': + break; + case 'disconnected': + this.emit('close'); + break; + } + }); + + this.subscription = subscription; + this.refresh(); + this.emit('open'); + } + + private shouldSubscribe(): boolean { + if (!this.skyWay.roomPerson) { + console.log('roomPerson is null'); + return false; + } + + let member = this.member; + if (!member) { + console.log(`connect member is not found`); + return false; + } + + console.log(`member.publications ${member.name}`, member.publications); + let publication = member.publications.find(publication => publication.metadata === 'udonarium-data-stream'); + if (!publication) { + console.log(`'udonarium-data-stream' is not found`); + return false; + } + + if (this.skyWay.roomPerson.subscriptions.find(subscription => subscription.publication.id === publication.id)) { + console.log(`'udonarium-data-stream' is already subscribed.`); + return false; + } + return true; + } + + async unsubscribe() { + await this.subscription?.cancel(); + this.subscription = null; + this.peer.isOpen = false; + this.stopMonitoring(); + this.refresh(); + } + + refresh() { + let member = this.member; + let dataStream = this.subscription?.stream; + + dataStream?.onData.removeAllListeners(); + dataStream?.onData.add(data => { + if (!(data instanceof ArrayBuffer)) { + console.log(`data is not ArrayBuffer`); + return; + } + this.onData(data); + }); + + let connection = (member as any)?._getConnection(this.skyWay.roomPerson?.id) as P2PConnection; + + this.senderDataChannel = connection?.sender.datachannels[this.skyWay.publication?.id]; + this.receiverDataChannel = dataStream?._datachannel; + + this.peer.isOpen = this.skyWay.isConnectedDataStream(member); + + let peerConnection = this.getPeerConnection(); + this.stats = peerConnection ? new WebRTCStats(peerConnection) : null; + + if (this.peer.isOpen) { + this.startMonitoring(); + if (!this.isBuffering) this.sendBuffer(); + } else { + this.stopMonitoring(); + } + } + + send(data: any) { + let encodedData: Uint8Array = MessagePack.encode(data); + + let total = Math.ceil(encodedData.byteLength / this.chunkSize); + if (total <= 1) { + this.bufferedSend(encodedData); + return; + } + + let id = UUID.generateUuid(); + + let sliceData: Uint8Array = null; + let chank: DataChank = null; + for (let sliceIndex = 0; sliceIndex < total; sliceIndex++) { + sliceData = encodedData.slice(sliceIndex * this.chunkSize, (sliceIndex + 1) * this.chunkSize); + chank = { id: id, data: sliceData, index: sliceIndex, total: total }; + this.bufferedSend(MessagePack.encode(chank)); + } + } + + private bufferedSend(data: Uint8Array) { + this.buffer.add(data); + if (!this.isBuffering) this.sendBuffer(); + } + + private sendBuffer = () => { + if (!this.senderDataChannel || this.senderDataChannel.readyState !== 'open') { + this.isBuffering = false; + return; + } + for (let data of this.buffer) { + try { + this.senderDataChannel.send(data); + this.buffer.delete(data); + } catch (err) { + console.error(err); + } + break; + } + this.isBuffering = 0 < this.buffer.size; + if (this.isBuffering) setZeroTimeout(this.sendBuffer); + } + + getPeerConnection(): RTCPeerConnection { + return this.subscription?.stream?._getRTCPeerConnection(); + } + + private startMonitoring() { + WebRTCStatsMonitor.add(this); + } + + private stopMonitoring() { + WebRTCStatsMonitor.remove(this); + } + + async updateStatsAsync() { + if (this.stats == null) return; + this.sendPing(); + await this.stats.updateAsync(); + this.candidateType = this.stats.candidateType; + + let deltaTime = performance.now() - this.timestamp; + let healthRate = deltaTime <= 10000 ? 1 : 5000 / ((deltaTime - 10000) + 5000); + let ping = healthRate < 1 ? deltaTime : this.ping; + let pingRate = 500 / (ping + 500); + + this.peer.session.health = healthRate; + this.peer.session.ping = ping; + this.peer.session.speed = pingRate * healthRate; + + switch (this.candidateType) { + case CandidateType.HOST: + this.peer.session.grade = PeerSessionGrade.HIGH; + break; + case CandidateType.SRFLX: + case CandidateType.PRFLX: + this.peer.session.grade = PeerSessionGrade.MIDDLE; + break; + case CandidateType.RELAY: + this.peer.session.grade = PeerSessionGrade.LOW; + break; + default: + this.peer.session.grade = PeerSessionGrade.UNSPECIFIED; + break; + } + this.peer.session.description = this.candidateType; + + this.emit('stats', this.stats); + } + + sendPing() { + let encodedData: Uint8Array = MessagePack.encode({ from: this.peer.peerId, ping: performance.now() }); + this.bufferedSend(encodedData); + } + + private receivePing(ping: Ping) { + if (ping.from === this.peer.peerId) { + let now = performance.now(); + let rtt = now - ping.ping; + this.ping = rtt <= this.ping ? (this.ping * 0.5) + (rtt * 0.5) : rtt; + } else { + let encodedData = MessagePack.encode(ping); + this.bufferedSend(encodedData); + } + } + + private onData(data: ArrayBuffer) { + this.timestamp = performance.now(); + let decoded: unknown = MessagePack.decode(new Uint8Array(data)); + + let ping: Ping = decoded as Ping; + if (ping.ping != null) { + this.receivePing(ping); + return; + } + + let chank: DataChank = decoded as DataChank; + if (chank.id == null) { + this.emit('data', decoded); + return; + } + + let received = this.receivedMap.get(chank.id); + if (received == null) { + received = { id: chank.id, chanks: new Array(chank.total), length: 0, byteLength: 0 }; + this.receivedMap.set(chank.id, received); + } + + if (received.chanks[chank.index] != null) return; + + received.length++; + received.byteLength += chank.data.byteLength; + received.chanks[chank.index] = chank.data; + + if (received.length < chank.total) return; + this.receivedMap.delete(chank.id); + + let uint8Array = new Uint8Array(received.byteLength); + + let pos = 0; + for (let c of received.chanks) { + uint8Array.set(c, pos); + pos += c.byteLength; + } + + let decodedChank = MessagePack.decode(uint8Array); + this.emit('data', decodedChank); + } +} diff --git a/src/app/class/core/system/network/skyway2023/skyway-facade.ts b/src/app/class/core/system/network/skyway2023/skyway-facade.ts new file mode 100644 index 000000000..d63dd3154 --- /dev/null +++ b/src/app/class/core/system/network/skyway2023/skyway-facade.ts @@ -0,0 +1,423 @@ +import { + Channel, + LocalDataStream, + LocalPerson, + Logger, + Publication, + RemoteMember, + SkyWayChannel, + SkyWayContext, + SkyWayError, + SkyWayStreamFactory, + Subscription, + TransportConnectionState +} from "@skyway-sdk/core"; +import { CryptoUtil } from '../../util/crypto-util'; +import { IPeerContext, PeerContext } from "../peer-context"; +import { SkyWayBackend } from './skyway-backend'; + +export class SkyWayFacade { + appId = ''; + context: SkyWayContext; + private lobby: Channel; + private lobbyPerson: LocalPerson; + room: Channel; + roomPerson: LocalPerson; + + publication: Publication; + + peer: PeerContext = PeerContext.parse('???'); + get isOpen(): boolean { return this.peer.isOpen }; + private isDestroyed = false; + + onOpen: (peer: IPeerContext) => void; + onClose: (peer: IPeerContext) => void; + onFatalError: (peer: IPeerContext, errorType: string, errorMessage: string, errorObject: any) => void; + onConnectionStateChanged: (peer: IPeerContext, state: TransportConnectionState) => void; + onSubscribed: (peer: IPeerContext, subscription: Subscription) => void; + onUnsubscribed: (peer: IPeerContext, subscription: Subscription) => void; + onDataStreamPublished: (peer: IPeerContext, publication: Publication) => void; + onRoomRestore: (peer: IPeerContext) => void; + + async open(peer: IPeerContext) { + if (this.isOpen) await this.close(); + try { + console.log('SkyWayFacade open...'); + this.peer = PeerContext.parse(peer.peerId); + this.peer.userId = peer.userId; + this.peer.password = peer.password; + this.isDestroyed = false; + + await this.createContext(); + await this.joinRoom(); + await this.joinLobby(); + + this.peer.isOpen = true; + console.log('SkyWayFacade open ok'); + + if (this.onOpen) this.onOpen(this.peer); + } catch (err) { + console.error(err); + if (this.onFatalError) this.onFatalError(this.peer, err.name, err.message, err); + } + } + + async close() { + try { + console.log('SkyWayFacade close...'); + this.peer = PeerContext.parse('???'); + this.isDestroyed = true; + + await this.leaveLobby(); + await this.leaveRoom(); + await this.disposeContext(); + console.log('SkyWayFacade close ok'); + } catch (err) { + console.error(err); + } + } + + private async createContext() { + await this.disposeContext(); + if (this.isDestroyed) return; + + let channelName = CryptoUtil.sha256Base64Url(this.peer.roomId + this.peer.roomName + this.peer.password); + let context = await SkyWayContext.Create(await SkyWayBackend.createSkyWayAuthToken(this.appId, channelName, this.peer.peerId)); + context.onTokenUpdateReminder.add(async () => { + console.log(`skyWay onTokenUpdateReminder ${new Date().toISOString()}`); + context.updateAuthToken(await SkyWayBackend.createSkyWayAuthToken(this.appId, channelName, this.peer.peerId)); + }); + + context.onTokenExpired.add(() => { + console.error('skyWay onTokenExpired'); + if (this.isOpen) { + this.close(); + if (this.onClose) this.onClose(this.peer); + } + let message = 'SkyWayの認証トークンの有効期限が切れました。' + if (this.onFatalError) this.onFatalError(this.peer, 'token-expired', message, new Error(message)); + }); + + context.onFatalError.add(err => { + console.error('skyWay onFatalError', err); + if (this.isOpen) { + this.close(); + if (this.onClose) this.onClose(this.peer); + } + if (this.onFatalError) this.onFatalError(this.peer, err.name, err.message, err); + }); + + this.context = context; + } + + private async joinLobby() { + await this.joinLobbyChannel(); + await this.joinLobbyPerson(); + } + + private async joinLobbyChannel() { + await this.leaveLobbyChannel(); + if (this.isDestroyed || !this.peer.isRoom || !this.context || this.context?.disposed) return; + + let lobbys: Channel[] = []; + for (let lobbyName of this.getLobbyNames()) { + let lobby = await SkyWayChannel.FindOrCreate(this.context, { + name: lobbyName, + }); + console.log(`FindOrCreate<${lobbyName}>`); + lobbys.push(lobby); + if (lobby.members.length < 300) break; + } + + let min = 9999; + let joinLobby: Channel = null; + lobbys.forEach(lobby => { + if (min <= lobby.members.length) return; + min = lobby.members.length; + joinLobby = lobby; + }); + + lobbys.forEach(lobby => { + if (lobby !== joinLobby) lobby.dispose(); + }); + + joinLobby.onMemberJoined.add(event => { + console.log(`lobby<${joinLobby.name}> onMemberJoined: ${event.member.name}`); + }); + + joinLobby.onMemberLeft.add(event => { + console.log(`lobby<${joinLobby.name}> onMemberLeft: ${event.member.name}`); + }); + + joinLobby.onMemberListChanged.add(() => { + console.log(`lobby<${joinLobby.name}> onMemberListChanged`); + }); + + joinLobby.onClosed.add(() => { + console.log(`lobby<${joinLobby.name}> onClosed`); + this.joinLobby(); + }); + + this.lobby = joinLobby; + } + + private async joinLobbyPerson() { + await this.leaveLobbyPerson(); + if (this.isDestroyed || !this.peer.isRoom || !this.context || this.context?.disposed || this.lobby == null) return; + + let lobbyPerson = await this.lobby.join({ + name: this.peer.peerId, + }); + + console.log(`lobbyPerson join <${this.lobby.name}>`); + lobbyPerson.onLeft.add(() => { + console.log(`lobbyPerson onClosed`); + }); + + lobbyPerson.onFatalError.add(err => { + console.error('lobbyPerson onFatalError', err); + }); + + this.lobbyPerson = lobbyPerson; + } + + private async joinRoom() { + await this.joinRoomChannel(); + await this.joinRoomPerson(); + await this.createRoomDataStream(); + } + + private async joinRoomChannel() { + await this.leaveRoomChannel(); + if (this.isDestroyed || !this.peer.isRoom || !this.context || this.context?.disposed) return; + + let roomName = CryptoUtil.sha256Base64Url(this.peer.roomId + this.peer.roomName + this.peer.password); + console.log(`roomName: ${roomName}`); + + let room = await SkyWayChannel.FindOrCreate(this.context, { + name: roomName, + }); + console.log(`FindOrCreate<${roomName}>`); + + room.onMemberJoined.add(event => { + console.log(`room<${room.name}> onMemberJoined: ${event.member.name}`); + }); + + room.onMemberLeft.add(event => { + console.log(`room<${room.name}> onMemberLeft: ${event.member.name}`); + }); + + room.onMemberListChanged.add(() => { + console.log(`room<${room.name}> onMemberListChanged`); + }); + + room.onStreamPublished.add(event => { + if (event.publication.contentType === 'data' + && event.publication.metadata === 'udonarium-data-stream' + && 0 < event.publication.publisher.name?.length) { + console.log(`room<${room.name}> onStreamPublished: ${event.publication.publisher.name} <${event.publication.metadata}>`); + let peer = PeerContext.parse(event.publication.publisher.name); + if (this.onDataStreamPublished && peer.peerId !== this.peer.peerId) this.onDataStreamPublished(peer, event.publication); + } + }); + + room.onClosed.add(async () => { + console.log(`room<${room.name}> onClosed`); + await this.joinRoom(); + console.log(`room<${room.name}> onRoomRestore`); + if (this.onRoomRestore) this.onRoomRestore(this.peer); + }); + + this.room = room; + } + + private async joinRoomPerson() { + await this.leaveRoomPerson(); + if (this.isDestroyed || !this.peer.isRoom || !this.context || this.context?.disposed || this.room == null) return; + + let roomPerson = await this.room.join({ + name: this.peer.peerId + }); + + console.log(`roomPerson join <${this.room.name}>`); + roomPerson.onLeft.add(() => { + console.log(`roomPerson onClosed`); + }); + + roomPerson.onFatalError.add(err => { + console.error('roomPerson onFatalError', err); + if (this.isOpen) { + this.close(); + if (this.onClose) this.onClose(this.peer); + } + if (this.onFatalError) this.onFatalError(this.peer, err.name, err.message, err); + }); + + this.roomPerson = roomPerson; + } + + private async createRoomDataStream() { + if (this.isDestroyed || !this.peer.isRoom || !this.context || this.context?.disposed || this.roomPerson == null) return; + let dataStream = await SkyWayStreamFactory.createDataStream(); + let publication = await this.roomPerson.publish(dataStream, { metadata: 'udonarium-data-stream' }); + + publication.onConnectionStateChanged.add(event => { + console.log(`publication onConnectionStateChanged ${event.remoteMember.name} -> ${event.state}`); + let peerId = event.remoteMember.name; + if (peerId == null) return; + + let peer = PeerContext.parse(peerId); + if (this.onConnectionStateChanged) this.onConnectionStateChanged(peer, event.state); + }); + + publication.onSubscribed.add(event => { + console.log(`publication onSubscribed ${event.subscription.subscriber.name}`); + let peerId = event.subscription.subscriber.name; + if (peerId == null) { + event.subscription.cancel(); + return; + } + + let peer = PeerContext.parse(event.subscription.subscriber.name); + if (this.onSubscribed) this.onSubscribed(peer, event.subscription); + }); + + publication.onUnsubscribed.add(event => { + console.log(`publication onUnsubscribed ${event.subscription.subscriber.name}`); + let peerId = event.subscription.subscriber.name; + if (peerId == null) return; + + let peer = PeerContext.parse(event.subscription.subscriber.name); + if (this.onUnsubscribed) this.onUnsubscribed(peer, event.subscription); + }); + + publication.onCanceled.add(() => { + console.log(`publication onCanceled`); + }); + + this.publication = publication; + } + + private async disposeContext() { + let context = this.context; + this.context = null; + if (!context) return; + console.log('disposeContext'); + context.dispose(); + } + + private async leaveLobby() { + await this.leaveLobbyPerson(); + await this.leaveLobbyChannel(); + } + + private async leaveLobbyChannel() { + let lobby = this.lobby; + this.lobby = null; + + if (!lobby) return; + console.log('leaveLobbyChannel'); + lobby.dispose(); + } + + private async leaveLobbyPerson() { + let lobbyPerson = this.lobbyPerson; + this.lobbyPerson = null; + + if (!lobbyPerson || lobbyPerson.state === 'left') return; + console.log('leaveLobbyPerson'); + lobbyPerson.onLeft.removeAllListeners(); + lobbyPerson.onFatalError.removeAllListeners(); + await lobbyPerson.leave(); + } + + private async leaveRoom() { + await this.closeRoomDataStream(); + await this.leaveRoomPerson(); + await this.leaveRoomChannel(); + } + + private async leaveRoomChannel() { + let room = this.room; + this.room = null; + + if (!room) return; + console.log('leaveRoomChannel'); + room.onMemberJoined.removeAllListeners(); + room.onMemberLeft.removeAllListeners(); + room.onMemberListChanged.removeAllListeners(); + room.onStreamPublished.removeAllListeners(); + room.onClosed.removeAllListeners(); + room.dispose(); + } + + private async leaveRoomPerson() { + let roomPerson = this.roomPerson; + this.roomPerson = null; + + if (!roomPerson || roomPerson.state === 'left') return; + console.log('leaveRoomPerson'); + roomPerson.onLeft.removeAllListeners(); + roomPerson.onFatalError.removeAllListeners(); + await roomPerson.leave(); + } + + private async closeRoomDataStream() { + let publication = this.publication; + this.publication = null; + + if (!publication) return; + await publication.cancel(); + } + + async listAllPeers(): Promise { + if (this.isDestroyed || !this.isOpen) return []; + + let lobbys: Channel[] = []; + for (let lobbyName of this.getLobbyNames()) { + let level = Logger.level; + Logger.level = 'disable'; + try { + let lobby = this.lobby?.name === lobbyName ? this.lobby : await SkyWayChannel.Find(this.context, { name: lobbyName }); + lobbys.push(lobby); + } catch (error) { + if (error instanceof SkyWayError) { + console.log(`${error.name} ${error.message}`); + } else { + console.error(error); + } + } + Logger.level = level; + } + + let allPeerIds = lobbys.flatMap(lobby => lobby.members.map(member => member.name ?? '???')); + + lobbys.forEach(lobby => { + if (lobby.name !== this.lobby?.name) lobby.dispose(); + }); + return allPeerIds; + } + + private getLobbyNames(): string[] { + let lobbyNames: string[] = []; + for (let channel of this.context?.authToken.scope.app.channels ?? []) { + let lobbyName = channel.name ?? ''; + if (lobbyName.startsWith('udonarium-lobby-')) lobbyNames.push(lobbyName); + } + return lobbyNames; + } + + isConnectedDataStream(remote: RemoteMember, local: LocalPerson = this.roomPerson): boolean { + if (!remote || !local) return false; + let isReadyForSend = local.publications.find(publication => + publication.metadata === 'udonarium-data-stream' && publication.getConnectionState(remote.id) === 'connected') != null; + + let isReadyForReceive = local.subscriptions.find(subscription => + subscription.publication.metadata === 'udonarium-data-stream' + && subscription.publication.publisher.name === remote.name + && (subscription.stream?._getConnectionState() === 'connected' || (subscription.stream as any)?._datachannel?.readyState === 'open')) != null; + + console.log('isConnected', isReadyForSend, isReadyForReceive); + return isReadyForReceive && isReadyForSend; + } +} diff --git a/tsconfig.json b/tsconfig.json index 7c2dfea5a..cfd21d3da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, + "allowSyntheticDefaultImports": true, "lib": [ "ES2022", "dom"