@newgameplusinc/odyssey-audio-video-sdk-dev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Odyssey Spatial Audio SDK
2
+
3
+ A comprehensive SDK for real-time spatial audio and video communication using MediaSoup, designed for immersive multi-user experiences in the Odyssey platform.
4
+
5
+ ## Purpose
6
+
7
+ This package provides a complete WebRTC-based spatial audio and video solution that:
8
+ - Manages MediaSoup connections for audio/video streaming
9
+ - Implements spatial audio using Web Audio API with HRTF
10
+ - Handles participant management with user profile data (bodyHeight, bodyShape, email, etc.)
11
+ - Provides real-time position tracking for immersive spatial experiences
12
+
13
+ ## Installation
14
+
15
+ You can install this package from npm:
16
+
17
+ ```bash
18
+ npm install @newgameplusinc/odyssey-spatial-sdk-wrapper
19
+ ```
20
+
21
+ Or install locally:
22
+
23
+ ```bash
24
+ npm install ../mediasoup-sdk-test
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### 1. Initialize the SDK
30
+
31
+ ```typescript
32
+ import { OdysseySpatialComms } from "@newgameplusinc/odyssey-spatial-sdk-wrapper";
33
+
34
+ // Initialize with your MediaSoup server URL
35
+ const sdk = new OdysseySpatialComms("https://your-mediasoup-server.com");
36
+ ```
37
+
38
+ ### 2. Join a Room with User Profile Data
39
+
40
+ ```typescript
41
+ const participant = await sdk.joinRoom({
42
+ roomId: "my-room",
43
+ userId: "user-123",
44
+ deviceId: "device-456",
45
+ position: { x: 0, y: 0, z: 0 },
46
+ direction: { x: 0, y: 0, z: 1 },
47
+ bodyHeight: "0.5", // User's avatar height from Firebase
48
+ bodyShape: "4", // User's avatar body shape from Firebase
49
+ userName: "John Doe", // User's display name
50
+ userEmail: "john@example.com" // User's email
51
+ });
52
+ ```
53
+
54
+ ### 3. Produce Audio/Video Tracks
55
+
56
+ ```typescript
57
+ // Get user media
58
+ const stream = await navigator.mediaDevices.getUserMedia({
59
+ audio: true,
60
+ video: true
61
+ });
62
+
63
+ // Produce audio track
64
+ const audioTrack = stream.getAudioTracks()[0];
65
+ await sdk.produceTrack(audioTrack);
66
+
67
+ // Produce video track
68
+ const videoTrack = stream.getVideoTracks()[0];
69
+ await sdk.produceTrack(videoTrack);
70
+ ```
71
+
72
+ ### 4. Update Position for Spatial Audio
73
+
74
+ ```typescript
75
+ sdk.updatePosition(
76
+ { x: 10, y: 0, z: 5 }, // New position
77
+ { x: 0, y: 0, z: 1 } // New direction
78
+ );
79
+ ```
80
+
81
+ ### 5. Listen to Events
82
+
83
+ ```typescript
84
+ // New participant joined
85
+ sdk.on("new-participant", (participant) => {
86
+ console.log("New participant:", participant.userName, participant.bodyHeight);
87
+ });
88
+
89
+ // Participant left
90
+ sdk.on("participant-left", (participantId) => {
91
+ console.log("Participant left:", participantId);
92
+ });
93
+
94
+ // Consumer created (receiving audio/video from remote participant)
95
+ sdk.on("consumer-created", ({ participant, track, consumer }) => {
96
+ console.log("Receiving", track.kind, "from", participant.userName);
97
+ });
98
+ ```
99
+
100
+ ## Build
101
+
102
+ To build the package, run:
103
+
104
+ ```bash
105
+ npm run build
106
+ ```
@@ -0,0 +1,14 @@
1
+ import { EventEmitter } from "events";
2
+ import { OdysseyEvent } from "./types";
3
+ /**
4
+ * A clean, standalone event bus for the SDK, completely decoupled from the socket.
5
+ * This ensures that only processed, structured SDK events are emitted to the application.
6
+ * It extends the Node.js EventEmitter to provide a standard and reliable event API.
7
+ */
8
+ export declare class EventManager extends EventEmitter {
9
+ constructor();
10
+ on(event: OdysseyEvent, listener: (...args: any[]) => void): this;
11
+ emit(event: OdysseyEvent, ...args: any[]): boolean;
12
+ off(event: OdysseyEvent, listener: (...args: any[]) => void): this;
13
+ removeAllListeners(event?: OdysseyEvent): this;
14
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EventManager = void 0;
4
+ const events_1 = require("events");
5
+ /**
6
+ * A clean, standalone event bus for the SDK, completely decoupled from the socket.
7
+ * This ensures that only processed, structured SDK events are emitted to the application.
8
+ * It extends the Node.js EventEmitter to provide a standard and reliable event API.
9
+ */
10
+ class EventManager extends events_1.EventEmitter {
11
+ constructor() {
12
+ super();
13
+ }
14
+ // Type-safe overrides for the methods we use
15
+ on(event, listener) {
16
+ return super.on(event, listener);
17
+ }
18
+ emit(event, ...args) {
19
+ return super.emit(event, ...args);
20
+ }
21
+ off(event, listener) {
22
+ return super.off(event, listener);
23
+ }
24
+ removeAllListeners(event) {
25
+ return super.removeAllListeners(event);
26
+ }
27
+ }
28
+ exports.EventManager = EventManager;
@@ -0,0 +1,33 @@
1
+ import * as mediasoupClient from "mediasoup-client";
2
+ import { types } from "mediasoup-client";
3
+ import { Socket } from "socket.io-client";
4
+ export declare class MediasoupManager {
5
+ private device;
6
+ private socket;
7
+ private sendTransport;
8
+ private recvTransport;
9
+ private producers;
10
+ private consumers;
11
+ constructor(socket: Socket);
12
+ loadDevice(routerRtpCapabilities: types.RtpCapabilities): Promise<void>;
13
+ sendDeviceRtpCapabilities(participantId: string): void;
14
+ createSendTransport(participantId: string): Promise<void>;
15
+ createRecvTransport(participantId: string): Promise<void>;
16
+ private connectSendTransport;
17
+ private connectRecvTransport;
18
+ produce(track: MediaStreamTrack): Promise<types.Producer>;
19
+ consume(data: {
20
+ consumerId: string;
21
+ producerId: string;
22
+ kind: "audio" | "video";
23
+ rtpParameters: any;
24
+ participantId: string;
25
+ }): Promise<{
26
+ consumer: types.Consumer;
27
+ track: MediaStreamTrack;
28
+ }>;
29
+ resumeConsumer(consumerId: string): Promise<void>;
30
+ private createWebRtcTransport;
31
+ close(): void;
32
+ getConsumers(): Map<string, mediasoupClient.types.Consumer<mediasoupClient.types.AppData>>;
33
+ }
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MediasoupManager = void 0;
37
+ const mediasoupClient = __importStar(require("mediasoup-client"));
38
+ class MediasoupManager {
39
+ constructor(socket) {
40
+ this.sendTransport = null;
41
+ this.recvTransport = null;
42
+ this.producers = new Map();
43
+ this.consumers = new Map();
44
+ this.socket = socket;
45
+ this.device = new mediasoupClient.Device();
46
+ }
47
+ async loadDevice(routerRtpCapabilities) {
48
+ if (this.device.loaded) {
49
+ console.warn("Device already loaded.");
50
+ return;
51
+ }
52
+ await this.device.load({ routerRtpCapabilities });
53
+ }
54
+ sendDeviceRtpCapabilities(participantId) {
55
+ this.socket.emit("device-rtp-capabilities", {
56
+ participantId,
57
+ rtpCapabilities: this.device.rtpCapabilities,
58
+ });
59
+ }
60
+ async createSendTransport(participantId) {
61
+ const params = await this.createWebRtcTransport("send", participantId);
62
+ this.sendTransport = this.device.createSendTransport(params);
63
+ this.connectSendTransport();
64
+ }
65
+ async createRecvTransport(participantId) {
66
+ const params = await this.createWebRtcTransport("recv", participantId);
67
+ this.recvTransport = this.device.createRecvTransport(params);
68
+ this.connectRecvTransport();
69
+ }
70
+ connectSendTransport() {
71
+ this.sendTransport?.on("connect", async ({ dtlsParameters }, callback, errback) => {
72
+ this.socket.emit("connect-transport", { transportId: this.sendTransport.id, dtlsParameters }, (response) => {
73
+ if (response.error)
74
+ errback(new Error(response.error));
75
+ else
76
+ callback();
77
+ });
78
+ });
79
+ this.sendTransport?.on("produce", async ({ kind, rtpParameters, appData, }, callback, errback) => {
80
+ this.socket.emit("produce", {
81
+ transportId: this.sendTransport.id,
82
+ kind,
83
+ rtpParameters,
84
+ appData,
85
+ }, (response) => {
86
+ if (response.error)
87
+ errback(new Error(response.error));
88
+ else if (response.producerId)
89
+ callback({ id: response.producerId });
90
+ });
91
+ });
92
+ }
93
+ connectRecvTransport() {
94
+ this.recvTransport?.on("connect", async ({ dtlsParameters }, callback, errback) => {
95
+ this.socket.emit("connect-transport", { transportId: this.recvTransport.id, dtlsParameters }, (response) => {
96
+ if (response.error)
97
+ errback(new Error(response.error));
98
+ else
99
+ callback();
100
+ });
101
+ });
102
+ }
103
+ async produce(track) {
104
+ if (!this.sendTransport)
105
+ throw new Error("Send transport not initialized");
106
+ const producer = await this.sendTransport.produce({ track });
107
+ this.producers.set(producer.id, producer);
108
+ return producer;
109
+ }
110
+ async consume(data) {
111
+ if (!this.recvTransport)
112
+ throw new Error("Receive transport not set up");
113
+ console.log(`📥 Creating consumer for ${data.participantId}:`, {
114
+ consumerId: data.consumerId,
115
+ producerId: data.producerId,
116
+ kind: data.kind,
117
+ });
118
+ const consumer = await this.recvTransport.consume({
119
+ id: data.consumerId,
120
+ producerId: data.producerId,
121
+ kind: data.kind,
122
+ rtpParameters: data.rtpParameters,
123
+ });
124
+ this.consumers.set(consumer.id, consumer);
125
+ console.log(`✅ Consumer created, track details:`, {
126
+ consumerId: consumer.id,
127
+ trackId: consumer.track.id,
128
+ trackKind: consumer.track.kind,
129
+ trackEnabled: consumer.track.enabled,
130
+ trackMuted: consumer.track.muted,
131
+ trackReadyState: consumer.track.readyState,
132
+ consumerPaused: consumer.paused,
133
+ consumerClosed: consumer.closed,
134
+ });
135
+ return { consumer, track: consumer.track };
136
+ }
137
+ async resumeConsumer(consumerId) {
138
+ console.log(`▶️ Resuming consumer ${consumerId}...`);
139
+ return new Promise((resolve, reject) => {
140
+ this.socket.emit("resume-consumer", { consumerId }, (response) => {
141
+ if (response.error) {
142
+ console.error(`❌ Failed to resume consumer ${consumerId}:`, response.error);
143
+ reject(new Error(response.error));
144
+ }
145
+ else {
146
+ console.log(`✅ Consumer ${consumerId} resumed successfully on server`);
147
+ resolve();
148
+ }
149
+ });
150
+ });
151
+ }
152
+ async createWebRtcTransport(direction, participantId) {
153
+ return new Promise((resolve, reject) => {
154
+ // Set up listener for transport-created event
155
+ const handleTransportCreated = (data) => {
156
+ if (data.type === direction) {
157
+ // Remove listener to prevent memory leaks
158
+ this.socket.off("transport-created", handleTransportCreated);
159
+ if (data.error) {
160
+ return reject(new Error(data.error));
161
+ }
162
+ resolve(data.params);
163
+ }
164
+ };
165
+ this.socket.on("transport-created", handleTransportCreated);
166
+ // Emit the request
167
+ this.socket.emit("create-transport", { direction, participantId });
168
+ // Add timeout to prevent hanging forever
169
+ setTimeout(() => {
170
+ this.socket.off("transport-created", handleTransportCreated);
171
+ reject(new Error(`Timeout waiting for ${direction} transport`));
172
+ }, 10000);
173
+ });
174
+ }
175
+ close() {
176
+ this.producers.forEach((p) => p.close());
177
+ this.consumers.forEach((c) => c.close());
178
+ if (this.sendTransport)
179
+ this.sendTransport.close();
180
+ if (this.recvTransport)
181
+ this.recvTransport.close();
182
+ this.producers.clear();
183
+ this.consumers.clear();
184
+ }
185
+ getConsumers() {
186
+ return this.consumers;
187
+ }
188
+ }
189
+ exports.MediasoupManager = MediasoupManager;
@@ -0,0 +1,24 @@
1
+ import { EventManager } from "./EventManager";
2
+ import { Position } from "./types";
3
+ export declare class SpatialAudioManager extends EventManager {
4
+ private audioContext;
5
+ private participantNodes;
6
+ private masterGainNode;
7
+ private monitoringIntervals;
8
+ constructor();
9
+ getAudioContext(): AudioContext;
10
+ setupSpatialAudioForParticipant(participantId: string, track: MediaStreamTrack, bypassSpatialization?: boolean): void;
11
+ private startMonitoring;
12
+ updateSpatialAudio(participantId: string, position: Position): void;
13
+ setListenerPosition(position: Position, orientation: {
14
+ forwardX: number;
15
+ forwardY: number;
16
+ forwardZ: number;
17
+ upX: number;
18
+ upY: number;
19
+ upZ: number;
20
+ }): void;
21
+ removeParticipant(participantId: string): void;
22
+ resumeAudioContext(): Promise<void>;
23
+ getAudioContextState(): AudioContextState;
24
+ }
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SpatialAudioManager = void 0;
4
+ const EventManager_1 = require("./EventManager");
5
+ class SpatialAudioManager extends EventManager_1.EventManager {
6
+ constructor() {
7
+ super();
8
+ this.participantNodes = new Map();
9
+ this.monitoringIntervals = new Map();
10
+ this.audioContext = new AudioContext();
11
+ this.masterGainNode = this.audioContext.createGain();
12
+ this.masterGainNode.gain.value = 5.0; // Set master gain to 5.0
13
+ this.masterGainNode.connect(this.audioContext.destination);
14
+ console.log(`🔊 SpatialAudioManager initialized, gain: ${this.masterGainNode.gain.value} audioContext state: ${this.audioContext.state}`);
15
+ }
16
+ getAudioContext() {
17
+ return this.audioContext;
18
+ }
19
+ setupSpatialAudioForParticipant(participantId, track, bypassSpatialization = false // Default to false
20
+ ) {
21
+ if (this.audioContext.state === "suspended") {
22
+ this.audioContext.resume();
23
+ }
24
+ const stream = new MediaStream([track]);
25
+ // 🔍 DIAGNOSTIC TEST: Try playing through raw HTML audio element
26
+ console.warn(`🧪 DIAGNOSTIC: Testing raw audio playback for ${participantId}`);
27
+ const testAudio = new Audio();
28
+ testAudio.srcObject = stream;
29
+ testAudio.volume = 1.0;
30
+ testAudio
31
+ .play()
32
+ .then(() => {
33
+ console.log(`✅ DIAGNOSTIC: Raw audio element can play track for ${participantId}`);
34
+ })
35
+ .catch((err) => {
36
+ console.error(`❌ DIAGNOSTIC: Raw audio element CANNOT play:`, err);
37
+ });
38
+ const source = this.audioContext.createMediaStreamSource(stream);
39
+ const panner = this.audioContext.createPanner();
40
+ const analyser = this.audioContext.createAnalyser();
41
+ const gain = this.audioContext.createGain();
42
+ // Configure Panner
43
+ panner.panningModel = "HRTF";
44
+ panner.distanceModel = "inverse";
45
+ panner.refDistance = 1;
46
+ panner.maxDistance = 100;
47
+ panner.rolloffFactor = 1;
48
+ panner.coneInnerAngle = 360;
49
+ panner.coneOuterAngle = 0;
50
+ panner.coneOuterGain = 0;
51
+ if (bypassSpatialization) {
52
+ console.log(`🔊 TESTING: Connecting audio directly to destination (bypassing spatial audio) for ${participantId}`);
53
+ source.connect(analyser);
54
+ analyser.connect(this.masterGainNode);
55
+ }
56
+ else {
57
+ // Standard spatialized path
58
+ source.connect(panner);
59
+ panner.connect(analyser);
60
+ analyser.connect(gain);
61
+ gain.connect(this.masterGainNode);
62
+ }
63
+ this.participantNodes.set(participantId, {
64
+ source,
65
+ panner,
66
+ analyser,
67
+ gain,
68
+ stream,
69
+ });
70
+ console.log(`🎧 Spatial audio setup complete for ${participantId}:`, {
71
+ audioContextState: this.audioContext.state,
72
+ gain: this.masterGainNode.gain.value,
73
+ trackEnabled: track.enabled,
74
+ trackMuted: track.muted,
75
+ trackReadyState: track.readyState,
76
+ isBypassed: bypassSpatialization,
77
+ });
78
+ // Start monitoring audio levels
79
+ this.startMonitoring(participantId);
80
+ }
81
+ startMonitoring(participantId) {
82
+ const nodes = this.participantNodes.get(participantId);
83
+ if (!nodes)
84
+ return;
85
+ const { analyser, stream } = nodes;
86
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
87
+ // Clear any existing interval for this participant
88
+ if (this.monitoringIntervals.has(participantId)) {
89
+ clearInterval(this.monitoringIntervals.get(participantId));
90
+ }
91
+ const interval = setInterval(() => {
92
+ analyser.getByteTimeDomainData(dataArray);
93
+ let sum = 0;
94
+ for (const amplitude of dataArray) {
95
+ sum += Math.abs(amplitude - 128);
96
+ }
97
+ const average = sum / dataArray.length;
98
+ const audioLevel = (average / 128) * 255; // Scale to 0-255
99
+ console.log(`📊 Audio level for ${participantId}: ${audioLevel.toFixed(2)} (0-255 scale)`);
100
+ if (audioLevel < 1.0) {
101
+ console.warn(`⚠️ NO AUDIO DATA detected for ${participantId}! Track may be silent or not transmitting.`);
102
+ console.info(`💡 Check: 1) Is microphone unmuted? 2) Is correct mic selected? 3) Is mic working in system settings?`);
103
+ }
104
+ // Check track status after 2 seconds
105
+ setTimeout(() => {
106
+ const track = stream.getAudioTracks()[0];
107
+ if (track) {
108
+ console.log(`🔊 Audio track status after 2s for ${participantId}:`, {
109
+ trackEnabled: track.enabled,
110
+ trackMuted: track.muted,
111
+ trackReadyState: track.readyState,
112
+ audioContextState: this.audioContext.state,
113
+ pannerPosition: {
114
+ x: nodes.panner.positionX.value,
115
+ y: nodes.panner.positionY.value,
116
+ z: nodes.panner.positionZ.value,
117
+ },
118
+ });
119
+ }
120
+ }, 2000);
121
+ }, 2000); // Log every 2 seconds
122
+ this.monitoringIntervals.set(participantId, interval);
123
+ }
124
+ updateSpatialAudio(participantId, position) {
125
+ const nodes = this.participantNodes.get(participantId);
126
+ if (nodes?.panner) {
127
+ nodes.panner.positionX.setValueAtTime(position.x, this.audioContext.currentTime);
128
+ nodes.panner.positionY.setValueAtTime(position.y, this.audioContext.currentTime);
129
+ nodes.panner.positionZ.setValueAtTime(position.z, this.audioContext.currentTime);
130
+ }
131
+ }
132
+ setListenerPosition(position, orientation) {
133
+ const { listener } = this.audioContext;
134
+ if (listener) {
135
+ // Use setPosition and setOrientation for atomic updates if available
136
+ if (listener.positionX) {
137
+ listener.positionX.setValueAtTime(position.x, this.audioContext.currentTime);
138
+ listener.positionY.setValueAtTime(position.y, this.audioContext.currentTime);
139
+ listener.positionZ.setValueAtTime(position.z, this.audioContext.currentTime);
140
+ }
141
+ if (listener.forwardX) {
142
+ listener.forwardX.setValueAtTime(orientation.forwardX, this.audioContext.currentTime);
143
+ listener.forwardY.setValueAtTime(orientation.forwardY, this.audioContext.currentTime);
144
+ listener.forwardZ.setValueAtTime(orientation.forwardZ, this.audioContext.currentTime);
145
+ listener.upX.setValueAtTime(orientation.upX, this.audioContext.currentTime);
146
+ listener.upY.setValueAtTime(orientation.upY, this.audioContext.currentTime);
147
+ listener.upZ.setValueAtTime(orientation.upZ, this.audioContext.currentTime);
148
+ }
149
+ }
150
+ }
151
+ removeParticipant(participantId) {
152
+ // Stop monitoring
153
+ if (this.monitoringIntervals.has(participantId)) {
154
+ clearInterval(this.monitoringIntervals.get(participantId));
155
+ this.monitoringIntervals.delete(participantId);
156
+ }
157
+ const nodes = this.participantNodes.get(participantId);
158
+ if (nodes) {
159
+ nodes.source.disconnect();
160
+ nodes.panner.disconnect();
161
+ nodes.analyser.disconnect();
162
+ nodes.gain.disconnect();
163
+ nodes.stream.getTracks().forEach((track) => track.stop());
164
+ this.participantNodes.delete(participantId);
165
+ console.log(`🗑️ Removed participant ${participantId} from spatial audio.`);
166
+ }
167
+ }
168
+ async resumeAudioContext() {
169
+ if (this.audioContext.state === "suspended") {
170
+ await this.audioContext.resume();
171
+ console.log("✅ Audio context has been resumed successfully.");
172
+ }
173
+ }
174
+ getAudioContextState() {
175
+ return this.audioContext.state;
176
+ }
177
+ }
178
+ exports.SpatialAudioManager = SpatialAudioManager;
@@ -0,0 +1,43 @@
1
+ import { EventManager } from "./EventManager";
2
+ import { Direction, MediaState, OdysseyEvent, Participant, Position, RoomJoinedData } from "./types";
3
+ export declare class OdysseySpatialComms extends EventManager {
4
+ private socket;
5
+ room: {
6
+ id: string;
7
+ participants: Map<string, Participant>;
8
+ } | null;
9
+ private localParticipant;
10
+ private mediasoupManager;
11
+ private spatialAudioManager;
12
+ constructor(serverUrl: string);
13
+ on(event: OdysseyEvent, listener: (...args: any[]) => void): this;
14
+ emit(event: OdysseyEvent, ...args: any[]): boolean;
15
+ joinRoom(data: {
16
+ roomId: string;
17
+ userId: string;
18
+ deviceId: string;
19
+ participantId?: string;
20
+ position: Position;
21
+ direction: Direction;
22
+ bodyHeight?: string;
23
+ bodyShape?: string;
24
+ userName?: string;
25
+ userEmail?: string;
26
+ }): Promise<Participant>;
27
+ leaveRoom(): void;
28
+ resumeAudio(): Promise<void>;
29
+ getAudioContextState(): AudioContextState;
30
+ produceTrack(track: MediaStreamTrack): Promise<any>;
31
+ updatePosition(position: Position, direction: Direction): void;
32
+ updateMediaState(mediaState: MediaState): void;
33
+ setListenerPosition(position: Position, orientation: {
34
+ forwardX: number;
35
+ forwardY: number;
36
+ forwardZ: number;
37
+ upX: number;
38
+ upY: number;
39
+ upZ: number;
40
+ }): void;
41
+ private listenForEvents;
42
+ }
43
+ export type { Direction, MediaState, OdysseyEvent, Participant, Position, RoomJoinedData, };
package/dist/index.js ADDED
@@ -0,0 +1,490 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OdysseySpatialComms = void 0;
4
+ const socket_io_client_1 = require("socket.io-client");
5
+ const EventManager_1 = require("./EventManager");
6
+ const MediasoupManager_1 = require("./MediasoupManager");
7
+ const SpatialAudioManager_1 = require("./SpatialAudioManager");
8
+ class OdysseySpatialComms extends EventManager_1.EventManager {
9
+ constructor(serverUrl) {
10
+ super(); // Initialize the EventEmitter base class
11
+ this.room = null;
12
+ this.localParticipant = null;
13
+ this.socket = (0, socket_io_client_1.io)(serverUrl, {
14
+ transports: ["websocket"],
15
+ });
16
+ this.mediasoupManager = new MediasoupManager_1.MediasoupManager(this.socket);
17
+ this.spatialAudioManager = new SpatialAudioManager_1.SpatialAudioManager();
18
+ // Set max listeners to prevent warning
19
+ this.setMaxListeners(50);
20
+ this.listenForEvents();
21
+ this.emit("connected");
22
+ }
23
+ // Override to ensure we're using the EventEmitter's on, not the socket's
24
+ on(event, listener) {
25
+ // Explicitly call EventEmitter's on method
26
+ return super.on(event, listener);
27
+ }
28
+ // Override to ensure we're using the EventEmitter's emit, not the socket's
29
+ emit(event, ...args) {
30
+ // Explicitly call EventEmitter's emit method
31
+ return super.emit(event, ...args);
32
+ }
33
+ async joinRoom(data) {
34
+ return new Promise((resolve, reject) => {
35
+ // Create a one-time listener for room-joined event
36
+ const handleRoomJoined = async (roomData) => {
37
+ try {
38
+ console.log("✅ Successfully joined room:", roomData);
39
+ console.log("👥 [SDK] Participants received from server:", roomData.participants);
40
+ console.log("🔍 [SDK] Each participant data:", roomData.participants.map((p) => ({
41
+ participantId: p.participantId,
42
+ bodyHeight: p.bodyHeight,
43
+ bodyShape: p.bodyShape,
44
+ userName: p.userName,
45
+ userEmail: p.userEmail,
46
+ })));
47
+ // Remove the listener to prevent memory leaks
48
+ this.socket.off("room-joined", handleRoomJoined);
49
+ // 1. Load Mediasoup device
50
+ await this.mediasoupManager.loadDevice(roomData.routerRtpCapabilities);
51
+ // 2. Create transports
52
+ await this.mediasoupManager.createSendTransport(roomData.participantId);
53
+ await this.mediasoupManager.createRecvTransport(roomData.participantId);
54
+ // 3. Store local participant info
55
+ const localParticipantData = roomData.participants.find((p) => p.participantId === roomData.participantId);
56
+ if (!localParticipantData) {
57
+ throw new Error("Could not find local participant in room data");
58
+ }
59
+ this.localParticipant = {
60
+ ...localParticipantData,
61
+ isLocal: true,
62
+ producers: new Map(),
63
+ consumers: new Map(),
64
+ };
65
+ // 4. Initialize room state
66
+ this.room = {
67
+ id: roomData.roomId,
68
+ participants: new Map(),
69
+ };
70
+ // 5. Process all participants from the server
71
+ for (const pData of roomData.participants) {
72
+ const participant = {
73
+ ...pData,
74
+ isLocal: pData.participantId === this.localParticipant.participantId,
75
+ producers: new Map(),
76
+ consumers: new Map(),
77
+ };
78
+ this.room.participants.set(pData.participantId, participant);
79
+ }
80
+ // 6. Note: Device RTP capabilities will be sent after producing tracks
81
+ // This ensures transports are fully connected via DTLS handshake
82
+ console.log("⏳ Transports created, will send RTP capabilities after producing tracks");
83
+ this.emit("room-joined", roomData);
84
+ resolve(this.localParticipant);
85
+ }
86
+ catch (error) {
87
+ this.socket.off("room-joined", handleRoomJoined);
88
+ reject(error);
89
+ }
90
+ };
91
+ // Register the listener
92
+ this.socket.on("room-joined", handleRoomJoined);
93
+ // Log the exact data being sent to the server
94
+ console.log("🚀 [SDK] Emitting 'join-room' event to server with data:", {
95
+ roomId: data.roomId,
96
+ userId: data.userId,
97
+ deviceId: data.deviceId,
98
+ participantId: data.participantId,
99
+ position: data.position,
100
+ direction: data.direction,
101
+ bodyHeight: data.bodyHeight,
102
+ bodyShape: data.bodyShape,
103
+ userName: data.userName,
104
+ userEmail: data.userEmail,
105
+ });
106
+ // Emit join-room request
107
+ this.socket.emit("join-room", data);
108
+ });
109
+ }
110
+ leaveRoom() {
111
+ if (!this.socket.connected)
112
+ return;
113
+ this.socket.emit("leave-room");
114
+ this.mediasoupManager.close();
115
+ this.socket.disconnect();
116
+ this.room = null;
117
+ this.localParticipant = null;
118
+ this.removeAllListeners(); // Clean up all listeners
119
+ console.log("🚪 SDK: Left room and cleaned up resources.");
120
+ }
121
+ async resumeAudio() {
122
+ await this.spatialAudioManager.resumeAudioContext();
123
+ }
124
+ getAudioContextState() {
125
+ return this.spatialAudioManager.getAudioContextState();
126
+ }
127
+ async produceTrack(track) {
128
+ const producer = await this.mediasoupManager.produce(track);
129
+ if (this.localParticipant) {
130
+ const isFirstProducer = this.localParticipant.producers.size === 0;
131
+ this.localParticipant.producers.set(producer.id, producer);
132
+ if (track.kind === "audio") {
133
+ this.localParticipant.audioTrack = track;
134
+ this.localParticipant.mediaState.audio = true;
135
+ }
136
+ else if (track.kind === "video") {
137
+ this.localParticipant.videoTrack = track;
138
+ this.localParticipant.mediaState.video = true;
139
+ }
140
+ // Send device RTP capabilities after first track is produced
141
+ // This ensures transports are fully connected via DTLS
142
+ if (isFirstProducer) {
143
+ console.log("📡 Sending device RTP capabilities after first track produced");
144
+ this.mediasoupManager.sendDeviceRtpCapabilities(this.localParticipant.participantId);
145
+ }
146
+ // Notify others of media state change
147
+ this.updateMediaState(this.localParticipant.mediaState);
148
+ }
149
+ return producer;
150
+ }
151
+ updatePosition(position, direction) {
152
+ if (this.localParticipant && this.room) {
153
+ this.localParticipant.position = position;
154
+ this.localParticipant.direction = direction;
155
+ this.socket.emit("update-position", {
156
+ participantId: this.localParticipant.participantId,
157
+ conferenceId: this.room.id,
158
+ roomId: this.room.id,
159
+ position,
160
+ direction,
161
+ });
162
+ }
163
+ }
164
+ updateMediaState(mediaState) {
165
+ if (this.localParticipant && this.room) {
166
+ this.localParticipant.mediaState = mediaState;
167
+ this.socket.emit("update-media-state", {
168
+ participantId: this.localParticipant.participantId,
169
+ conferenceId: this.room.id,
170
+ roomId: this.room.id,
171
+ mediaState,
172
+ });
173
+ }
174
+ }
175
+ setListenerPosition(position, orientation) {
176
+ this.spatialAudioManager.setListenerPosition(position, orientation);
177
+ }
178
+ listenForEvents() {
179
+ this.socket.on("new-participant", (participantData) => {
180
+ console.log("👋 SDK: new-participant event received:", {
181
+ participantId: participantData.participantId,
182
+ conferenceId: participantData.conferenceId,
183
+ roomId: participantData.roomId,
184
+ position: participantData.position,
185
+ direction: participantData.direction,
186
+ mediaState: participantData.mediaState,
187
+ timestamp: participantData.timestamp,
188
+ bodyHeight: participantData.bodyHeight,
189
+ bodyShape: participantData.bodyShape,
190
+ userName: participantData.userName,
191
+ userEmail: participantData.userEmail,
192
+ });
193
+ console.log("🔍 [SDK] New participant USER DATA:", {
194
+ bodyHeight: participantData.bodyHeight,
195
+ bodyShape: participantData.bodyShape,
196
+ userName: participantData.userName,
197
+ userEmail: participantData.userEmail,
198
+ });
199
+ if (this.room) {
200
+ const newParticipant = {
201
+ participantId: participantData.participantId,
202
+ userId: participantData.userId,
203
+ deviceId: participantData.deviceId,
204
+ position: participantData.position,
205
+ direction: participantData.direction,
206
+ mediaState: participantData.mediaState,
207
+ isLocal: false,
208
+ producers: new Map(),
209
+ consumers: new Map(),
210
+ bodyHeight: participantData.bodyHeight,
211
+ bodyShape: participantData.bodyShape,
212
+ userName: participantData.userName,
213
+ userEmail: participantData.userEmail,
214
+ };
215
+ this.room.participants.set(participantData.participantId, newParticipant);
216
+ console.log("✅ SDK: New participant added to room, total participants:", this.room.participants.size);
217
+ console.log("✅ SDK: New participant full data:", newParticipant);
218
+ this.emit("new-participant", newParticipant);
219
+ }
220
+ });
221
+ this.socket.on("participant-left", (data) => {
222
+ if (this.room) {
223
+ this.room.participants.delete(data.participantId);
224
+ this.spatialAudioManager.removeParticipant(data.participantId);
225
+ this.emit("participant-left", data.participantId);
226
+ }
227
+ });
228
+ this.socket.on("consumer-created", async (data) => {
229
+ console.log("📥 SDK: consumer-created event received:", {
230
+ consumerId: data.consumerId,
231
+ producerId: data.producerId,
232
+ kind: data.kind,
233
+ participantId: data.participantId,
234
+ conferenceId: data.conferenceId,
235
+ roomId: data.roomId,
236
+ position: data.position,
237
+ direction: data.direction,
238
+ mediaState: data.mediaState,
239
+ timestamp: data.timestamp,
240
+ bodyHeight: data.bodyHeight,
241
+ bodyShape: data.bodyShape,
242
+ userName: data.userName,
243
+ userEmail: data.userEmail,
244
+ });
245
+ const { consumer, track } = await this.mediasoupManager.consume(data);
246
+ this.socket.on("all-participants-update", (payload) => {
247
+ console.log("📦 SDK: all-participants-update event received:", payload);
248
+ if (!this.room) {
249
+ this.room = {
250
+ id: payload.roomId,
251
+ participants: new Map(),
252
+ };
253
+ }
254
+ const activeParticipantIds = new Set();
255
+ for (const snapshot of payload.participants) {
256
+ activeParticipantIds.add(snapshot.participantId);
257
+ let participant = this.room?.participants.get(snapshot.participantId);
258
+ const normalizedPosition = {
259
+ x: snapshot.position?.x ?? 0,
260
+ y: snapshot.position?.y ?? 0,
261
+ z: snapshot.position?.z ?? 0,
262
+ };
263
+ const normalizedDirection = {
264
+ x: snapshot.direction?.x ?? 0,
265
+ y: snapshot.direction?.y ?? 0,
266
+ z: snapshot.direction?.z ?? 1,
267
+ };
268
+ const normalizedMediaState = {
269
+ audio: snapshot.mediaState?.audio ?? false,
270
+ video: snapshot.mediaState?.video ?? false,
271
+ sharescreen: snapshot.mediaState?.sharescreen ?? false,
272
+ };
273
+ if (!participant) {
274
+ participant = {
275
+ participantId: snapshot.participantId,
276
+ userId: snapshot.userId,
277
+ deviceId: snapshot.deviceId,
278
+ isLocal: this.localParticipant?.participantId ===
279
+ snapshot.participantId,
280
+ audioTrack: undefined,
281
+ videoTrack: undefined,
282
+ producers: new Map(),
283
+ consumers: new Map(),
284
+ position: normalizedPosition,
285
+ direction: normalizedDirection,
286
+ mediaState: normalizedMediaState,
287
+ };
288
+ }
289
+ else {
290
+ participant.userId = snapshot.userId;
291
+ participant.deviceId = snapshot.deviceId;
292
+ participant.position = normalizedPosition;
293
+ participant.direction = normalizedDirection;
294
+ participant.mediaState = normalizedMediaState;
295
+ }
296
+ participant.bodyHeight = snapshot.bodyHeight;
297
+ participant.bodyShape = snapshot.bodyShape;
298
+ participant.userName = snapshot.userName;
299
+ participant.userEmail = snapshot.userEmail;
300
+ participant.isLocal =
301
+ this.localParticipant?.participantId === snapshot.participantId;
302
+ this.room?.participants.set(snapshot.participantId, participant);
303
+ if (participant.isLocal) {
304
+ this.localParticipant = participant;
305
+ }
306
+ }
307
+ if (this.room) {
308
+ for (const existingId of Array.from(this.room.participants.keys())) {
309
+ if (!activeParticipantIds.has(existingId)) {
310
+ this.room.participants.delete(existingId);
311
+ }
312
+ }
313
+ }
314
+ const normalizedParticipants = this.room
315
+ ? Array.from(this.room.participants.values())
316
+ : [];
317
+ this.emit("all-participants-update", normalizedParticipants);
318
+ });
319
+ // Resume the consumer to start receiving media (non-blocking)
320
+ this.mediasoupManager
321
+ .resumeConsumer(consumer.id)
322
+ .then(() => {
323
+ console.log(`▶️ SDK: Consumer resumed for ${data.participantId}, kind: ${data.kind}`);
324
+ })
325
+ .catch((err) => {
326
+ console.error(`❌ SDK: Failed to resume consumer for ${data.participantId}:`, err);
327
+ });
328
+ let participant = this.room?.participants.get(data.participantId);
329
+ // If participant doesn't exist yet, create it with the data from the event
330
+ if (!participant && this.room) {
331
+ console.log("🆕 SDK: Creating participant entry from consumer-created event with user profile data");
332
+ participant = {
333
+ participantId: data.participantId,
334
+ userId: data.participantId.split(":")[0] || data.participantId,
335
+ deviceId: data.participantId.split(":")[1] || data.participantId,
336
+ isLocal: false,
337
+ position: data.position,
338
+ direction: data.direction,
339
+ mediaState: data.mediaState,
340
+ producers: new Map(),
341
+ consumers: new Map(),
342
+ // Include user profile data from consumer-created event
343
+ bodyHeight: data.bodyHeight,
344
+ bodyShape: data.bodyShape,
345
+ userName: data.userName,
346
+ userEmail: data.userEmail,
347
+ };
348
+ this.room.participants.set(data.participantId, participant);
349
+ console.log("✅ SDK: Created participant WITH user profile data:", {
350
+ participantId: data.participantId,
351
+ bodyHeight: data.bodyHeight,
352
+ bodyShape: data.bodyShape,
353
+ userName: data.userName,
354
+ userEmail: data.userEmail,
355
+ });
356
+ }
357
+ console.log("🔍 SDK: Participant lookup result:", {
358
+ found: !!participant,
359
+ participantId: data.participantId,
360
+ totalParticipants: this.room?.participants.size,
361
+ participantIds: Array.from(this.room?.participants.keys() || []),
362
+ });
363
+ if (participant) {
364
+ // Update participant data with latest from server
365
+ participant.position = data.position;
366
+ participant.direction = data.direction;
367
+ participant.mediaState = data.mediaState;
368
+ // Update user profile data if provided (or preserve existing if not provided)
369
+ if (data.bodyHeight !== undefined) {
370
+ participant.bodyHeight = data.bodyHeight;
371
+ }
372
+ if (data.bodyShape !== undefined) {
373
+ participant.bodyShape = data.bodyShape;
374
+ }
375
+ if (data.userName !== undefined) {
376
+ participant.userName = data.userName;
377
+ }
378
+ if (data.userEmail !== undefined) {
379
+ participant.userEmail = data.userEmail;
380
+ }
381
+ participant.consumers.set(consumer.id, consumer);
382
+ if (track.kind === "audio") {
383
+ participant.audioTrack = track;
384
+ console.log("🎧 SDK: Setting up spatial audio for", participant.participantId);
385
+ // Setup spatial audio with full 3D positioning
386
+ this.spatialAudioManager.setupSpatialAudioForParticipant(participant.participantId, track, false // Enable spatial audio
387
+ );
388
+ // Update spatial audio position for this participant
389
+ this.spatialAudioManager.updateSpatialAudio(participant.participantId, data.position);
390
+ console.log("📍 SDK: Spatial audio position set:", data.position);
391
+ }
392
+ else if (track.kind === "video") {
393
+ participant.videoTrack = track;
394
+ console.log("📹 SDK: Video track set for", participant.participantId);
395
+ }
396
+ console.log("✅ SDK: Emitting consumer-created event with participant:", {
397
+ participantId: participant.participantId,
398
+ hasPosition: !!participant.position,
399
+ position: participant.position,
400
+ trackKind: track.kind,
401
+ conferenceId: data.conferenceId,
402
+ roomId: data.roomId,
403
+ });
404
+ this.emit("consumer-created", {
405
+ participant,
406
+ track,
407
+ consumer,
408
+ });
409
+ }
410
+ else {
411
+ console.error("❌ SDK: Participant not found for consumer-created event:", data.participantId);
412
+ }
413
+ });
414
+ this.socket.on("participant-position-updated", (data) => {
415
+ console.log("📍 SDK: participant-position-updated event received:", {
416
+ participantId: data.participantId,
417
+ conferenceId: data.conferenceId,
418
+ roomId: data.roomId,
419
+ position: data.position,
420
+ direction: data.direction,
421
+ mediaState: data.mediaState,
422
+ consumerIds: data.consumerIds,
423
+ timestamp: data.timestamp,
424
+ bodyHeight: data.bodyHeight,
425
+ bodyShape: data.bodyShape,
426
+ userName: data.userName,
427
+ userEmail: data.userEmail,
428
+ });
429
+ console.log("🔍 [SDK] Position update USER DATA:", {
430
+ participantId: data.participantId,
431
+ bodyHeight: data.bodyHeight,
432
+ bodyShape: data.bodyShape,
433
+ userName: data.userName,
434
+ userEmail: data.userEmail,
435
+ });
436
+ const participant = this.room?.participants.get(data.participantId);
437
+ if (participant) {
438
+ participant.position = data.position;
439
+ participant.direction = data.direction;
440
+ participant.mediaState = data.mediaState;
441
+ // Update user data if provided
442
+ if (data.bodyHeight !== undefined)
443
+ participant.bodyHeight = data.bodyHeight;
444
+ if (data.bodyShape !== undefined)
445
+ participant.bodyShape = data.bodyShape;
446
+ if (data.userName !== undefined)
447
+ participant.userName = data.userName;
448
+ if (data.userEmail !== undefined)
449
+ participant.userEmail = data.userEmail;
450
+ this.spatialAudioManager.updateSpatialAudio(data.participantId, data.position);
451
+ console.log("✅ SDK: Updated participant position and spatial audio");
452
+ this.emit("participant-position-updated", participant);
453
+ }
454
+ else {
455
+ console.warn("⚠️ SDK: Participant not found for position update:", data.participantId);
456
+ console.warn("⚠️ SDK: Available participants in room:", Array.from(this.room?.participants.keys() || []));
457
+ }
458
+ });
459
+ this.socket.on("participant-media-state-updated", (data) => {
460
+ console.log("🔄 SDK: participant-media-state-updated event received:", {
461
+ participantId: data.participantId,
462
+ conferenceId: data.conferenceId,
463
+ roomId: data.roomId,
464
+ mediaState: data.mediaState,
465
+ position: data.position,
466
+ direction: data.direction,
467
+ timestamp: data.timestamp,
468
+ });
469
+ const participant = this.room?.participants.get(data.participantId);
470
+ if (participant) {
471
+ participant.mediaState = data.mediaState;
472
+ participant.position = data.position;
473
+ participant.direction = data.direction;
474
+ console.log("✅ SDK: Updated participant media state");
475
+ this.emit("participant-media-state-updated", participant);
476
+ }
477
+ else {
478
+ console.warn("⚠️ SDK: Participant not found for media state update:", data.participantId);
479
+ }
480
+ });
481
+ this.socket.on("error", (error) => {
482
+ console.error("Socket error:", error);
483
+ this.emit("error", error);
484
+ });
485
+ this.socket.on("disconnect", () => {
486
+ this.emit("disconnected");
487
+ });
488
+ }
489
+ }
490
+ exports.OdysseySpatialComms = OdysseySpatialComms;
@@ -0,0 +1,58 @@
1
+ export interface Position {
2
+ x: number;
3
+ y: number;
4
+ z: number;
5
+ }
6
+ export interface Direction {
7
+ x: number;
8
+ y: number;
9
+ z: number;
10
+ }
11
+ export interface MediaState {
12
+ audio: boolean;
13
+ video: boolean;
14
+ sharescreen: boolean;
15
+ }
16
+ export interface Participant {
17
+ participantId: string;
18
+ userId: string;
19
+ deviceId: string;
20
+ name?: string;
21
+ avatarUrl?: string;
22
+ isLocal: boolean;
23
+ audioTrack?: MediaStreamTrack;
24
+ videoTrack?: MediaStreamTrack;
25
+ producers: Map<string, any>;
26
+ consumers: Map<string, any>;
27
+ position: Position;
28
+ direction: Direction;
29
+ mediaState: MediaState;
30
+ bodyHeight?: string;
31
+ bodyShape?: string;
32
+ userName?: string;
33
+ userEmail?: string;
34
+ }
35
+ export interface RoomJoinedData {
36
+ participants: Participant[];
37
+ routerRtpCapabilities: any;
38
+ participantId: string;
39
+ roomId: string;
40
+ iceServers: any[];
41
+ }
42
+ export interface ParticipantsSnapshotEvent {
43
+ roomId: string;
44
+ participants: Array<{
45
+ participantId: string;
46
+ userId: string;
47
+ deviceId: string;
48
+ position: Position;
49
+ direction: Direction;
50
+ bodyHeight: string;
51
+ bodyShape: string;
52
+ userName: string;
53
+ userEmail: string;
54
+ mediaState: MediaState;
55
+ }>;
56
+ timestamp: number;
57
+ }
58
+ export type OdysseyEvent = "connected" | "disconnected" | "room-joined" | "all-participants-update" | "new-participant" | "participant-left" | "producer-created" | "consumer-created" | "participant-media-state-updated" | "participant-position-updated" | "error";
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@newgameplusinc/odyssey-audio-video-sdk-dev",
3
+ "version": "1.0.0",
4
+ "description": "Odyssey Spatial Audio & Video SDK using MediaSoup for real-time communication",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "test": "jest",
11
+ "lint": "eslint src/**/*.ts",
12
+ "prepublishOnly": "npm run build",
13
+ "publish:npm": "npm publish --access public",
14
+ "version:patch": "npm version patch",
15
+ "version:minor": "npm version minor",
16
+ "version:major": "npm version major"
17
+ },
18
+ "keywords": [
19
+ "spatial-audio",
20
+ "webrtc",
21
+ "mediasoup",
22
+ "conference",
23
+ "real-time",
24
+ "audio",
25
+ "video",
26
+ "odyssey"
27
+ ],
28
+ "author": "New Game Plus Inc",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "socket.io-client": "^4.7.2",
32
+ "webrtc-adapter": "^8.2.3",
33
+ "mediasoup-client": "^3.6.90",
34
+ "events": "^3.3.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.0.0",
38
+ "typescript": "^5.0.0",
39
+ "jest": "^29.0.0",
40
+ "@types/jest": "^29.0.0"
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/New-Game-Plus-Inc/mediasoup-sdk-test.git"
48
+ },
49
+ "homepage": "https://github.com/New-Game-Plus-Inc/mediasoup-sdk-test"
50
+ }