@phystack/hub-client 4.5.19-dev → 4.5.20-dev
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/dist/index.d.ts +22 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -378
- package/dist/index.js.map +1 -1
- package/dist/peripheral-twin.d.ts +34 -0
- package/dist/peripheral-twin.d.ts.map +1 -0
- package/dist/peripheral-twin.js +234 -0
- package/dist/peripheral-twin.js.map +1 -0
- package/dist/services/phyhub-connection.service.d.ts +1 -0
- package/dist/services/phyhub-connection.service.d.ts.map +1 -1
- package/dist/services/phyhub-connection.service.js +17 -0
- package/dist/services/phyhub-connection.service.js.map +1 -1
- package/dist/services/phyhub-direct-connection.service.d.ts +21 -0
- package/dist/services/phyhub-direct-connection.service.d.ts.map +1 -0
- package/dist/services/phyhub-direct-connection.service.js +101 -0
- package/dist/services/phyhub-direct-connection.service.js.map +1 -0
- package/dist/services/webrtc/data-channel-handler.d.ts +45 -0
- package/dist/services/webrtc/data-channel-handler.d.ts.map +1 -0
- package/dist/services/webrtc/data-channel-handler.js +260 -0
- package/dist/services/webrtc/data-channel-handler.js.map +1 -0
- package/dist/services/webrtc/index.d.ts +8 -0
- package/dist/services/webrtc/index.d.ts.map +1 -0
- package/dist/services/webrtc/index.js +18 -0
- package/dist/services/webrtc/index.js.map +1 -0
- package/dist/services/webrtc/media-stream-handler.d.ts +57 -0
- package/dist/services/webrtc/media-stream-handler.d.ts.map +1 -0
- package/dist/services/webrtc/media-stream-handler.js +383 -0
- package/dist/services/webrtc/media-stream-handler.js.map +1 -0
- package/dist/services/webrtc/peer-connection-manager.d.ts +40 -0
- package/dist/services/webrtc/peer-connection-manager.d.ts.map +1 -0
- package/dist/services/webrtc/peer-connection-manager.js +336 -0
- package/dist/services/webrtc/peer-connection-manager.js.map +1 -0
- package/dist/services/webrtc/types.d.ts +134 -0
- package/dist/services/webrtc/types.d.ts.map +1 -0
- package/dist/services/webrtc/types.js +12 -0
- package/dist/services/webrtc/types.js.map +1 -0
- package/dist/services/webrtc/webrtc-globals.d.ts +4 -0
- package/dist/services/webrtc/webrtc-globals.d.ts.map +1 -0
- package/dist/services/webrtc/webrtc-globals.js +72 -0
- package/dist/services/webrtc/webrtc-globals.js.map +1 -0
- package/dist/services/webrtc/webrtc-manager.d.ts +35 -0
- package/dist/services/webrtc/webrtc-manager.d.ts.map +1 -0
- package/dist/services/webrtc/webrtc-manager.js +274 -0
- package/dist/services/webrtc/webrtc-manager.js.map +1 -0
- package/dist/test/communication-comprehensive-test.d.ts +8 -0
- package/dist/test/communication-comprehensive-test.d.ts.map +1 -0
- package/dist/test/communication-comprehensive-test.js +356 -0
- package/dist/test/communication-comprehensive-test.js.map +1 -0
- package/dist/test/webrtc-channel-names-test.d.ts +2 -0
- package/dist/test/webrtc-channel-names-test.d.ts.map +1 -0
- package/dist/test/webrtc-channel-names-test.js +177 -0
- package/dist/test/webrtc-channel-names-test.js.map +1 -0
- package/dist/test/webrtc-comprehensive-test.d.ts +2 -0
- package/dist/test/webrtc-comprehensive-test.d.ts.map +1 -0
- package/dist/test/webrtc-comprehensive-test.js +328 -0
- package/dist/test/webrtc-comprehensive-test.js.map +1 -0
- package/dist/test/webrtc-reconnect-test.d.ts +4 -0
- package/dist/test/webrtc-reconnect-test.d.ts.map +1 -0
- package/dist/test/webrtc-reconnect-test.js +244 -0
- package/dist/test/webrtc-reconnect-test.js.map +1 -0
- package/dist/test/webrtc-test-harness.d.ts +4 -0
- package/dist/test/webrtc-test-harness.d.ts.map +1 -0
- package/dist/test/webrtc-test-harness.js +169 -0
- package/dist/test/webrtc-test-harness.js.map +1 -0
- package/dist/twin-messaging.d.ts +20 -0
- package/dist/twin-messaging.d.ts.map +1 -0
- package/dist/twin-messaging.js +94 -0
- package/dist/twin-messaging.js.map +1 -0
- package/dist/twin-registry.d.ts +9 -0
- package/dist/twin-registry.d.ts.map +1 -0
- package/dist/twin-registry.js +26 -0
- package/dist/twin-registry.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +20 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/twin.types.d.ts +62 -14
- package/dist/types/twin.types.d.ts.map +1 -1
- package/dist/types/twin.types.js +8 -1
- package/dist/types/twin.types.js.map +1 -1
- package/docs/webrtc-howto.md +398 -0
- package/docs/webrtc-test.md +330 -0
- package/package.json +3 -3
- package/scripts/webrtc-test.sh +401 -0
- package/src/index.ts +378 -568
- package/src/peripheral-twin.ts +337 -0
- package/src/services/phyhub-connection.service.ts +24 -0
- package/src/services/phyhub-direct-connection.service.ts +159 -0
- package/src/services/webrtc/data-channel-handler.ts +362 -0
- package/src/services/webrtc/index.ts +36 -0
- package/src/services/webrtc/media-stream-handler.ts +536 -0
- package/src/services/webrtc/peer-connection-manager.ts +467 -0
- package/src/services/webrtc/types.ts +273 -0
- package/src/services/webrtc/webrtc-globals.ts +108 -0
- package/src/services/webrtc/webrtc-manager.ts +490 -0
- package/src/test/communication-comprehensive-test.ts +533 -0
- package/src/test/webrtc-channel-names-test.ts +266 -0
- package/src/test/webrtc-comprehensive-test.ts +494 -0
- package/src/test/webrtc-reconnect-test.ts +345 -0
- package/src/test/webrtc-test-harness.ts +254 -0
- package/src/twin-messaging.ts +184 -0
- package/src/twin-registry.ts +39 -0
- package/src/types/index.ts +3 -0
- package/src/types/twin.types.ts +80 -14
- package/dist/services/webrtc/datachannel.d.ts +0 -10
- package/dist/services/webrtc/datachannel.d.ts.map +0 -1
- package/dist/services/webrtc/datachannel.js +0 -290
- package/dist/services/webrtc/datachannel.js.map +0 -1
- package/dist/services/webrtc/mediastream.d.ts +0 -10
- package/dist/services/webrtc/mediastream.d.ts.map +0 -1
- package/dist/services/webrtc/mediastream.js +0 -396
- package/dist/services/webrtc/mediastream.js.map +0 -1
- package/dist/services/webrtc/peer-connection-ice.d.ts +0 -32
- package/dist/services/webrtc/peer-connection-ice.d.ts.map +0 -1
- package/dist/services/webrtc/peer-connection-ice.js +0 -483
- package/dist/services/webrtc/peer-connection-ice.js.map +0 -1
- package/src/services/webrtc/datachannel.ts +0 -421
- package/src/services/webrtc/mediastream.ts +0 -602
- package/src/services/webrtc/peer-connection-ice.ts +0 -689
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MediaStreamHandler
|
|
3
|
+
*
|
|
4
|
+
* Manages MediaStream connections including:
|
|
5
|
+
* - Stream setup for sending/receiving media
|
|
6
|
+
* - Track lifecycle management (add/remove)
|
|
7
|
+
* - Frame activity monitoring for stale connection detection
|
|
8
|
+
* - Persistent stream abstraction across reconnections
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PeerConnectionManager, PeerConnectionManagerOptions } from './peer-connection-manager';
|
|
12
|
+
import {
|
|
13
|
+
TwinMessagingInterface,
|
|
14
|
+
PhygridMediaStream,
|
|
15
|
+
MediaStreamOptions,
|
|
16
|
+
PeerConnectionConfig,
|
|
17
|
+
ExtendedMediaStreamTrack,
|
|
18
|
+
DEFAULT_WEBRTC_OPTIONS,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
const FRAME_INACTIVITY_TIMEOUT = 5000; // 5 seconds without frames = stale
|
|
22
|
+
|
|
23
|
+
export interface MediaStreamHandlerOptions extends Partial<PeerConnectionManagerOptions> {
|
|
24
|
+
/** Use STUN servers. Default: true */
|
|
25
|
+
useStun?: boolean;
|
|
26
|
+
|
|
27
|
+
/** STUN server URLs */
|
|
28
|
+
stunServers?: string[];
|
|
29
|
+
|
|
30
|
+
/** Media options */
|
|
31
|
+
mediaOptions?: MediaStreamOptions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class MediaStreamHandler {
|
|
35
|
+
private targetTwinId: string;
|
|
36
|
+
private channelName: string;
|
|
37
|
+
private isInitiator: boolean;
|
|
38
|
+
private twinMessaging: TwinMessagingInterface;
|
|
39
|
+
private options: Required<Omit<MediaStreamHandlerOptions, 'mediaOptions'>> & {
|
|
40
|
+
mediaOptions: MediaStreamOptions;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
private pcManager: PeerConnectionManager | null = null;
|
|
44
|
+
private remoteStream: MediaStream | null = null;
|
|
45
|
+
private localStream: MediaStream | null = null;
|
|
46
|
+
private isClosed = false;
|
|
47
|
+
private isReceiving = false;
|
|
48
|
+
private isConnecting = false;
|
|
49
|
+
|
|
50
|
+
// Track management
|
|
51
|
+
private tracks: MediaStreamTrack[] = [];
|
|
52
|
+
private trackLastFrameTime: Map<string, number> = new Map();
|
|
53
|
+
private frameActivityInterval: NodeJS.Timeout | null = null;
|
|
54
|
+
|
|
55
|
+
// Event listeners
|
|
56
|
+
private trackListeners: Set<(track: MediaStreamTrack) => void> = new Set();
|
|
57
|
+
private frameListeners: Set<(frameData: any) => void> = new Set();
|
|
58
|
+
private closeListeners: Set<() => void> = new Set();
|
|
59
|
+
|
|
60
|
+
// Callbacks for WebRTCManager
|
|
61
|
+
private onConnectedCallback?: () => void;
|
|
62
|
+
private onDisconnectedCallback?: () => void;
|
|
63
|
+
private onErrorCallback?: (error: Error) => void;
|
|
64
|
+
private onReconnectingCallback?: (attempt: number) => void;
|
|
65
|
+
private onReconnectedCallback?: (attempt: number) => void;
|
|
66
|
+
|
|
67
|
+
constructor(
|
|
68
|
+
targetTwinId: string,
|
|
69
|
+
isInitiator: boolean,
|
|
70
|
+
twinMessaging: TwinMessagingInterface,
|
|
71
|
+
options: MediaStreamHandlerOptions = {},
|
|
72
|
+
channelName: string = 'default'
|
|
73
|
+
) {
|
|
74
|
+
this.targetTwinId = targetTwinId;
|
|
75
|
+
this.channelName = channelName;
|
|
76
|
+
this.isInitiator = isInitiator;
|
|
77
|
+
this.twinMessaging = twinMessaging;
|
|
78
|
+
this.options = {
|
|
79
|
+
useStun: options.useStun ?? DEFAULT_WEBRTC_OPTIONS.useStun,
|
|
80
|
+
stunServers: options.stunServers ?? DEFAULT_WEBRTC_OPTIONS.stunServers,
|
|
81
|
+
connectionTimeout: options.connectionTimeout ?? DEFAULT_WEBRTC_OPTIONS.connectionTimeout,
|
|
82
|
+
initialRetryDelay: options.initialRetryDelay ?? DEFAULT_WEBRTC_OPTIONS.initialRetryDelay,
|
|
83
|
+
maxRetryDelay: options.maxRetryDelay ?? DEFAULT_WEBRTC_OPTIONS.maxRetryDelay,
|
|
84
|
+
mediaOptions: {
|
|
85
|
+
direction: options.mediaOptions?.direction ?? (isInitiator ? 'recvonly' : 'sendrecv'),
|
|
86
|
+
localStream: options.mediaOptions?.localStream,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Create local stream for non-initiator if not provided
|
|
91
|
+
if (!isInitiator && !this.options.mediaOptions.localStream) {
|
|
92
|
+
this.localStream = new MediaStream();
|
|
93
|
+
this.options.mediaOptions.localStream = this.localStream;
|
|
94
|
+
} else {
|
|
95
|
+
this.localStream = this.options.mediaOptions.localStream ?? null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Set callbacks for connection events
|
|
101
|
+
*/
|
|
102
|
+
setCallbacks(callbacks: {
|
|
103
|
+
onConnected?: () => void;
|
|
104
|
+
onDisconnected?: () => void;
|
|
105
|
+
onError?: (error: Error) => void;
|
|
106
|
+
onReconnecting?: (attempt: number) => void;
|
|
107
|
+
onReconnected?: (attempt: number) => void;
|
|
108
|
+
}): void {
|
|
109
|
+
this.onConnectedCallback = callbacks.onConnected;
|
|
110
|
+
this.onDisconnectedCallback = callbacks.onDisconnected;
|
|
111
|
+
this.onErrorCallback = callbacks.onError;
|
|
112
|
+
this.onReconnectingCallback = callbacks.onReconnecting;
|
|
113
|
+
this.onReconnectedCallback = callbacks.onReconnected;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Connect and return a PhygridMediaStream
|
|
118
|
+
*/
|
|
119
|
+
async connect(): Promise<PhygridMediaStream> {
|
|
120
|
+
if (this.isClosed) {
|
|
121
|
+
throw new Error('MediaStreamHandler has been closed');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.isConnecting = true;
|
|
125
|
+
// Channel prefix includes name for unique signaling per channel
|
|
126
|
+
const channelPrefix = `media-${this.channelName}`;
|
|
127
|
+
|
|
128
|
+
const pcConfig: PeerConnectionConfig = {
|
|
129
|
+
targetTwinId: this.targetTwinId,
|
|
130
|
+
isInitiator: this.isInitiator,
|
|
131
|
+
connectionType: 'mediastream',
|
|
132
|
+
channelPrefix,
|
|
133
|
+
useStun: this.options.useStun,
|
|
134
|
+
stunServers: this.options.stunServers,
|
|
135
|
+
onConnected: () => this.handlePeerConnected(),
|
|
136
|
+
onDisconnected: () => this.handlePeerDisconnected(),
|
|
137
|
+
onError: (err) => this.handleError(err),
|
|
138
|
+
onReconnecting: (attempt) => {
|
|
139
|
+
this.isConnecting = true;
|
|
140
|
+
this.isReceiving = false;
|
|
141
|
+
this.onReconnectingCallback?.(attempt);
|
|
142
|
+
},
|
|
143
|
+
onReconnected: (attempt) => this.handleReconnected(attempt),
|
|
144
|
+
// Setup transceivers/tracks BEFORE offer is created (critical for ICE gathering)
|
|
145
|
+
onPeerConnectionCreated: (pc) => {
|
|
146
|
+
if (this.isInitiator) {
|
|
147
|
+
if (this.localStream && this.localStream.getTracks().length > 0) {
|
|
148
|
+
// Initiator with local stream: Add actual tracks before offer
|
|
149
|
+
this.localStream.getTracks().forEach((track) => {
|
|
150
|
+
pc.addTrack(track, this.localStream!);
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
// Initiator without local stream: Add transceiver for receiving
|
|
154
|
+
pc.addTransceiver('video', { direction: this.options.mediaOptions.direction });
|
|
155
|
+
}
|
|
156
|
+
} else if (this.localStream) {
|
|
157
|
+
// Responder: Add local tracks before answer is sent
|
|
158
|
+
this.localStream.getTracks().forEach((track) => {
|
|
159
|
+
pc.addTrack(track, this.localStream!);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle incoming tracks (both sides)
|
|
164
|
+
pc.ontrack = (event) => {
|
|
165
|
+
// Get the stream from the event or create one if not provided
|
|
166
|
+
// Some WebRTC implementations may not provide streams in the event
|
|
167
|
+
let stream: MediaStream;
|
|
168
|
+
if (event.streams && event.streams[0]) {
|
|
169
|
+
stream = event.streams[0];
|
|
170
|
+
} else {
|
|
171
|
+
// Create a stream if not provided (some WebRTC implementations don't include streams)
|
|
172
|
+
if (!this.remoteStream) {
|
|
173
|
+
this.remoteStream = new MediaStream();
|
|
174
|
+
}
|
|
175
|
+
stream = this.remoteStream;
|
|
176
|
+
// Add the track to our created stream
|
|
177
|
+
if (!stream.getTrackById(event.track.id)) {
|
|
178
|
+
stream.addTrack(event.track);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.remoteStream = stream;
|
|
183
|
+
this.handleRemoteStream(stream);
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
this.pcManager = new PeerConnectionManager(pcConfig, this.twinMessaging, {
|
|
189
|
+
connectionTimeout: this.options.connectionTimeout,
|
|
190
|
+
initialRetryDelay: this.options.initialRetryDelay,
|
|
191
|
+
maxRetryDelay: this.options.maxRetryDelay,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const pc = await this.pcManager.connect();
|
|
195
|
+
await this.setupMediaStream(pc);
|
|
196
|
+
|
|
197
|
+
return this.createPhygridMediaStream();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Close the media stream and clean up
|
|
202
|
+
*/
|
|
203
|
+
close(): void {
|
|
204
|
+
if (this.isClosed) return;
|
|
205
|
+
|
|
206
|
+
this.isClosed = true;
|
|
207
|
+
this.isReceiving = false;
|
|
208
|
+
|
|
209
|
+
this.stopFrameActivityMonitor();
|
|
210
|
+
|
|
211
|
+
// Stop all tracks
|
|
212
|
+
this.tracks.forEach((track) => {
|
|
213
|
+
try {
|
|
214
|
+
track.stop();
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error('[MediaStreamHandler] Error stopping track:', err);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
this.tracks = [];
|
|
220
|
+
|
|
221
|
+
if (this.remoteStream) {
|
|
222
|
+
this.remoteStream.getTracks().forEach((track) => track.stop());
|
|
223
|
+
this.remoteStream = null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (this.localStream) {
|
|
227
|
+
this.localStream.getTracks().forEach((track) => track.stop());
|
|
228
|
+
this.localStream = null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.pcManager) {
|
|
232
|
+
this.pcManager.close();
|
|
233
|
+
this.pcManager = null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.notifyCloseListeners();
|
|
237
|
+
this.trackListeners.clear();
|
|
238
|
+
this.frameListeners.clear();
|
|
239
|
+
this.closeListeners.clear();
|
|
240
|
+
this.trackLastFrameTime.clear();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ===========================================================================
|
|
244
|
+
// Private Methods
|
|
245
|
+
// ===========================================================================
|
|
246
|
+
|
|
247
|
+
private async setupMediaStream(_pc: RTCPeerConnection): Promise<void> {
|
|
248
|
+
// Transceivers and ontrack handler are set up in onPeerConnectionCreated
|
|
249
|
+
// This method now just waits for the connection to be established
|
|
250
|
+
// or returns immediately if we're not expecting incoming tracks
|
|
251
|
+
if (this.options.mediaOptions.direction === 'sendonly') {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// For recvonly/sendrecv, wait for tracks if not already received
|
|
256
|
+
if (this.remoteStream && this.remoteStream.getTracks().length > 0) {
|
|
257
|
+
return; // Already have tracks
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Wait a short time for tracks to arrive (they may come after connection)
|
|
261
|
+
return new Promise<void>((resolve) => {
|
|
262
|
+
const timeout = setTimeout(() => {
|
|
263
|
+
console.log('[MediaStreamHandler] Proceeding without waiting for remote tracks');
|
|
264
|
+
resolve();
|
|
265
|
+
}, 1000);
|
|
266
|
+
|
|
267
|
+
// If we get a track, resolve immediately
|
|
268
|
+
const checkForTracks = () => {
|
|
269
|
+
if (this.remoteStream && this.remoteStream.getTracks().length > 0) {
|
|
270
|
+
clearTimeout(timeout);
|
|
271
|
+
resolve();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Check periodically
|
|
276
|
+
const interval = setInterval(() => {
|
|
277
|
+
checkForTracks();
|
|
278
|
+
if (this.remoteStream && this.remoteStream.getTracks().length > 0) {
|
|
279
|
+
clearInterval(interval);
|
|
280
|
+
}
|
|
281
|
+
}, 100);
|
|
282
|
+
|
|
283
|
+
// Clean up interval on timeout
|
|
284
|
+
setTimeout(() => clearInterval(interval), 1000);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private handleRemoteStream(stream: MediaStream): void {
|
|
289
|
+
this.startFrameActivityMonitor();
|
|
290
|
+
|
|
291
|
+
stream.getTracks().forEach((track) => {
|
|
292
|
+
this.addTrackToCollection(track, stream);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
this.onConnectedCallback?.();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private addTrackToCollection(track: MediaStreamTrack, stream: MediaStream): void {
|
|
299
|
+
// Check for duplicate
|
|
300
|
+
const existingIndex = this.tracks.findIndex((existingTrack) => existingTrack.id === track.id);
|
|
301
|
+
if (existingIndex !== -1) {
|
|
302
|
+
this.tracks.splice(existingIndex, 1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
this.tracks.push(track);
|
|
306
|
+
this.trackLastFrameTime.set(track.id, Date.now());
|
|
307
|
+
|
|
308
|
+
// Set up frame monitoring for the track
|
|
309
|
+
this.setupTrackFrameMonitoring(track, stream);
|
|
310
|
+
|
|
311
|
+
// Notify listeners
|
|
312
|
+
this.notifyTrackListeners(track);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private setupTrackFrameMonitoring(track: MediaStreamTrack, stream: MediaStream): void {
|
|
316
|
+
const extendedTrack = track as ExtendedMediaStreamTrack;
|
|
317
|
+
|
|
318
|
+
// Try to use onFrame if available (Node.js @roamhq/wrtc)
|
|
319
|
+
if (typeof extendedTrack.onFrame === 'function') {
|
|
320
|
+
const originalOnFrame = extendedTrack.onFrame;
|
|
321
|
+
extendedTrack.onFrame = (frame: any) => {
|
|
322
|
+
this.trackLastFrameTime.set(track.id, Date.now());
|
|
323
|
+
this.isReceiving = true;
|
|
324
|
+
this.notifyFrameListeners(frame);
|
|
325
|
+
originalOnFrame.call(extendedTrack, frame);
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Handle track ended
|
|
330
|
+
track.onended = () => {
|
|
331
|
+
this.trackLastFrameTime.delete(track.id);
|
|
332
|
+
const index = this.tracks.findIndex((existingTrack) => existingTrack.id === track.id);
|
|
333
|
+
if (index !== -1) {
|
|
334
|
+
this.tracks.splice(index, 1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Check if all tracks ended
|
|
338
|
+
const allEnded = stream.getTracks().every((streamTrack) => streamTrack.readyState === 'ended');
|
|
339
|
+
if (allEnded && !this.isClosed) {
|
|
340
|
+
this.handlePeerDisconnected();
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private startFrameActivityMonitor(): void {
|
|
346
|
+
if (this.frameActivityInterval) return;
|
|
347
|
+
|
|
348
|
+
this.frameActivityInterval = setInterval(() => {
|
|
349
|
+
this.checkFrameActivity();
|
|
350
|
+
}, 1000);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private stopFrameActivityMonitor(): void {
|
|
354
|
+
if (this.frameActivityInterval) {
|
|
355
|
+
clearInterval(this.frameActivityInterval);
|
|
356
|
+
this.frameActivityInterval = null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private checkFrameActivity(): void {
|
|
361
|
+
if (this.trackLastFrameTime.size === 0) return;
|
|
362
|
+
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
let hasActiveTrack = false;
|
|
365
|
+
|
|
366
|
+
this.trackLastFrameTime.forEach((lastTime) => {
|
|
367
|
+
if (now - lastTime < FRAME_INACTIVITY_TIMEOUT) {
|
|
368
|
+
hasActiveTrack = true;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (!hasActiveTrack && this.isReceiving) {
|
|
373
|
+
console.log('[MediaStreamHandler] Frame inactivity detected, connection may be stale');
|
|
374
|
+
this.isReceiving = false;
|
|
375
|
+
|
|
376
|
+
// Trigger reconnect
|
|
377
|
+
if (!this.isClosed && this.pcManager) {
|
|
378
|
+
this.pcManager.reconnect();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private handlePeerConnected(): void {
|
|
384
|
+
this.isConnecting = false;
|
|
385
|
+
// Peer connection established - media setup will follow
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private handlePeerDisconnected(): void {
|
|
389
|
+
this.isReceiving = false;
|
|
390
|
+
this.onDisconnectedCallback?.();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private handleReconnected(attempt: number): void {
|
|
394
|
+
// Tracks are re-added via onPeerConnectionCreated when new peer connection is created
|
|
395
|
+
// Just notify callback that reconnection succeeded
|
|
396
|
+
console.log(`[MediaStreamHandler] Reconnected after ${attempt} attempts`);
|
|
397
|
+
this.onReconnectedCallback?.(attempt);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private handleError(error: Error): void {
|
|
401
|
+
console.error('[MediaStreamHandler] MediaStream error:', error);
|
|
402
|
+
this.onErrorCallback?.(error);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private notifyTrackListeners(track: MediaStreamTrack): void {
|
|
406
|
+
this.trackListeners.forEach((listener) => {
|
|
407
|
+
try {
|
|
408
|
+
listener(track);
|
|
409
|
+
} catch (err) {
|
|
410
|
+
console.error('[MediaStreamHandler] Error in track listener:', err);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private notifyFrameListeners(frameData: any): void {
|
|
416
|
+
this.frameListeners.forEach((listener) => {
|
|
417
|
+
try {
|
|
418
|
+
listener(frameData);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error('[MediaStreamHandler] Error in frame listener:', err);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private notifyCloseListeners(): void {
|
|
426
|
+
this.closeListeners.forEach((listener) => {
|
|
427
|
+
try {
|
|
428
|
+
listener();
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error('[MediaStreamHandler] Error in close listener:', err);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private createPhygridMediaStream(): PhygridMediaStream {
|
|
436
|
+
return {
|
|
437
|
+
getTracks: () => [...this.tracks],
|
|
438
|
+
|
|
439
|
+
getStream: () => this.remoteStream,
|
|
440
|
+
|
|
441
|
+
addTrack: (track: MediaStreamTrack) => {
|
|
442
|
+
if (this.isClosed) {
|
|
443
|
+
console.warn('[MediaStreamHandler] Cannot add track to closed stream');
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Add to local stream
|
|
448
|
+
if (this.localStream) {
|
|
449
|
+
const existing = this.localStream.getTrackById(track.id);
|
|
450
|
+
if (existing) {
|
|
451
|
+
this.localStream.removeTrack(existing);
|
|
452
|
+
}
|
|
453
|
+
this.localStream.addTrack(track);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Add to tracks collection
|
|
457
|
+
this.tracks.push(track);
|
|
458
|
+
|
|
459
|
+
// Add to peer connection if connected
|
|
460
|
+
const pc = this.pcManager?.getPeerConnection();
|
|
461
|
+
if (pc && pc.connectionState === 'connected' && this.localStream) {
|
|
462
|
+
const senders = pc.getSenders();
|
|
463
|
+
const existingSender = senders.find((sender) => sender.track?.id === track.id);
|
|
464
|
+
if (existingSender) {
|
|
465
|
+
existingSender.replaceTrack(track);
|
|
466
|
+
} else {
|
|
467
|
+
pc.addTrack(track, this.localStream);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
this.notifyTrackListeners(track);
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
onTrack: (callback: (track: MediaStreamTrack) => void) => {
|
|
475
|
+
this.trackListeners.add(callback);
|
|
476
|
+
|
|
477
|
+
// Notify for existing tracks
|
|
478
|
+
this.tracks.forEach((track) => {
|
|
479
|
+
try {
|
|
480
|
+
callback(track);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
console.error('[MediaStreamHandler] Error in track listener:', err);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
offTrack: (callback: (track: MediaStreamTrack) => void) => {
|
|
488
|
+
this.trackListeners.delete(callback);
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
onFrame: (callback: (frameData: any) => void) => {
|
|
492
|
+
this.frameListeners.add(callback);
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
offFrame: (callback: (frameData: any) => void) => {
|
|
496
|
+
this.frameListeners.delete(callback);
|
|
497
|
+
},
|
|
498
|
+
|
|
499
|
+
onClose: (callback: () => void) => {
|
|
500
|
+
this.closeListeners.add(callback);
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
offClose: (callback: () => void) => {
|
|
504
|
+
this.closeListeners.delete(callback);
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
close: () => {
|
|
508
|
+
this.close();
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
isReceivingFrames: () => {
|
|
512
|
+
if (this.trackLastFrameTime.size === 0) return false;
|
|
513
|
+
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
for (const lastTime of this.trackLastFrameTime.values()) {
|
|
516
|
+
if (now - lastTime < FRAME_INACTIVITY_TIMEOUT) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
getTargetTwinId: () => {
|
|
524
|
+
return this.targetTwinId;
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
getChannelName: () => {
|
|
528
|
+
return this.channelName;
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
isConnecting: () => {
|
|
532
|
+
return this.isConnecting;
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
}
|