@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 +106 -0
- package/dist/EventManager.d.ts +14 -0
- package/dist/EventManager.js +28 -0
- package/dist/MediasoupManager.d.ts +33 -0
- package/dist/MediasoupManager.js +189 -0
- package/dist/SpatialAudioManager.d.ts +24 -0
- package/dist/SpatialAudioManager.js +178 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +490 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +2 -0
- package/package.json +50 -0
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;
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|