kasunk99-livestream-core 0.2.9 → 0.3.1
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.
|
@@ -25,6 +25,7 @@ export type HostChatMessage = {
|
|
|
25
25
|
export type UseHostSocketReturn = HostState & {
|
|
26
26
|
chatMessages: HostChatMessage[];
|
|
27
27
|
setDisplayName: (name: string) => void;
|
|
28
|
+
sendChatMessage: (text: string) => void;
|
|
28
29
|
startPreview: () => Promise<void>;
|
|
29
30
|
stopPreview: () => void;
|
|
30
31
|
startLive: () => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useHostSocket.d.ts","sourceRoot":"","sources":["../../src/hooks/useHostSocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAC;AAoB3B,MAAM,MAAM,oBAAoB,GAAG;IACjC,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG;IAC5C,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC,CAAC;AAMF,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,mBAAmB,
|
|
1
|
+
{"version":3,"file":"useHostSocket.d.ts","sourceRoot":"","sources":["../../src/hooks/useHostSocket.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,mBAAmB,CAAC;AAoB3B,MAAM,MAAM,oBAAoB,GAAG;IACjC,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG;IAC5C,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC,CAAC;AAMF,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,mBAAmB,CAupBrF"}
|
|
@@ -172,6 +172,31 @@ export function useHostSocket(options = {}) {
|
|
|
172
172
|
const onIceServers = (servers) => {
|
|
173
173
|
hostSession.iceServers = Array.isArray(servers) ? servers : [];
|
|
174
174
|
};
|
|
175
|
+
socket.on('connect', onConnect);
|
|
176
|
+
socket.on('connect_error', onConnectError);
|
|
177
|
+
socket.on('disconnect', onDisconnect);
|
|
178
|
+
socket.on('ice-servers', onIceServers);
|
|
179
|
+
return () => {
|
|
180
|
+
socket.off('connect', onConnect);
|
|
181
|
+
socket.off('connect_error', onConnectError);
|
|
182
|
+
socket.off('disconnect', onDisconnect);
|
|
183
|
+
socket.off('ice-servers', onIceServers);
|
|
184
|
+
};
|
|
185
|
+
}, [fullCleanup]);
|
|
186
|
+
// ── Chat subscription ──────────────────────────────────────────────────────
|
|
187
|
+
// Registered on a separate effect keyed to live state so it is guaranteed to
|
|
188
|
+
// be active only while the host is in the room (socket.join runs as part of
|
|
189
|
+
// startLive). Registering it in the socket setup effect is too early — the host
|
|
190
|
+
// has not joined the room yet, and reconnects after navigate-away clear the
|
|
191
|
+
// room membership, so we need to re-register on every live transition.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!state.live) {
|
|
194
|
+
setChatMessages([]);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const socket = hostSession.socket;
|
|
198
|
+
if (!socket)
|
|
199
|
+
return;
|
|
175
200
|
const onChatMessage = (msg) => {
|
|
176
201
|
if (!msg || typeof msg !== 'object')
|
|
177
202
|
return;
|
|
@@ -184,19 +209,10 @@ export function useHostSocket(options = {}) {
|
|
|
184
209
|
const id = `${String(m.peerId ?? 'p')}-${timestamp}-${text.slice(0, 6)}`;
|
|
185
210
|
setChatMessages((prev) => [...prev.slice(-99), { id, displayName, text, timestamp }]);
|
|
186
211
|
};
|
|
187
|
-
socket.on('connect', onConnect);
|
|
188
|
-
socket.on('connect_error', onConnectError);
|
|
189
|
-
socket.on('disconnect', onDisconnect);
|
|
190
|
-
socket.on('ice-servers', onIceServers);
|
|
191
212
|
socket.on('chat-message', onChatMessage);
|
|
192
|
-
return () => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
socket.off('disconnect', onDisconnect);
|
|
196
|
-
socket.off('ice-servers', onIceServers);
|
|
197
|
-
socket.off('chat-message', onChatMessage);
|
|
198
|
-
};
|
|
199
|
-
}, [fullCleanup]);
|
|
213
|
+
return () => { socket.off('chat-message', onChatMessage); };
|
|
214
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
215
|
+
}, [state.live]);
|
|
200
216
|
// ── Permissions ────────────────────────────────────────────────────────────
|
|
201
217
|
const requestCameraMicPermissions = useCallback(async () => {
|
|
202
218
|
if (Platform.OS !== 'android')
|
|
@@ -311,6 +327,20 @@ export function useHostSocket(options = {}) {
|
|
|
311
327
|
screenStreamToStop.release?.();
|
|
312
328
|
}
|
|
313
329
|
catch { /* ignore */ }
|
|
330
|
+
if (Platform.OS !== 'android') {
|
|
331
|
+
// iOS: camera was never stopped — the existing track in localStream is still live.
|
|
332
|
+
// Just switch the producer back to it; no getUserMedia call needed.
|
|
333
|
+
const existingCamTrack = hostSession.localStream?.getVideoTracks()?.[0];
|
|
334
|
+
if (existingCamTrack) {
|
|
335
|
+
patchHostState({ streamURL: getStreamURL(hostSession.localStream), captureMode: 'camera' });
|
|
336
|
+
await replaceVideoProducer(existingCamTrack);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
patchHostState({ captureMode: 'camera' });
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// Android: camera was stopped when screen share started — re-acquire it.
|
|
314
344
|
try {
|
|
315
345
|
const { isFrontCamera: front } = getHostState();
|
|
316
346
|
const newCamStream = (await mediaDevices.getUserMedia({
|
|
@@ -369,17 +399,31 @@ export function useHostSocket(options = {}) {
|
|
|
369
399
|
return;
|
|
370
400
|
}
|
|
371
401
|
hostSession.screenStream = screenStream;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
402
|
+
if (Platform.OS === 'android') {
|
|
403
|
+
// Android: stop camera hardware — MediaProjection captures the hardware
|
|
404
|
+
// display independently, so stopping the camera is safe and saves battery.
|
|
405
|
+
hostSession.localStream?.getVideoTracks()
|
|
406
|
+
?.forEach((t) => { try {
|
|
407
|
+
t.stop?.();
|
|
408
|
+
}
|
|
409
|
+
catch { /* ignore */ } });
|
|
410
|
+
// Point the RTCView preview at the screen capture stream.
|
|
411
|
+
patchHostState({ streamURL: getStreamURL(screenStream), captureMode: 'screen' });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// iOS: keep camera hardware running.
|
|
415
|
+
// RPScreenRecorder captures the app's own screen; if we stopped the camera
|
|
416
|
+
// first, the app would display black and that black frame would be captured
|
|
417
|
+
// → host and viewers both see a black screen. Keeping the camera alive
|
|
418
|
+
// means the host's RTCView still shows the camera preview while the
|
|
419
|
+
// WebRTC producer (replaced below) delivers screen content to viewers.
|
|
420
|
+
patchHostState({ captureMode: 'screen' });
|
|
421
|
+
// Update the preview URL so the RTCView keeps showing the local camera stream.
|
|
422
|
+
patchHostState({ streamURL: getStreamURL(hostSession.localStream) });
|
|
376
423
|
}
|
|
377
|
-
catch { /* ignore */ } });
|
|
378
|
-
// Point preview at the native screenStream — avoids the black-screen bug
|
|
379
|
-
// caused by copying a screen-capture track into a different MediaStream object.
|
|
380
|
-
patchHostState({ streamURL: getStreamURL(screenStream), captureMode: 'screen' });
|
|
381
424
|
await replaceVideoProducer(screenVideoTrack);
|
|
382
|
-
// Auto-revert when user dismisses screen share
|
|
425
|
+
// Auto-revert when user dismisses screen share
|
|
426
|
+
// (Android: from the notification bar; iOS: from the status-bar recording dot)
|
|
383
427
|
screenVideoTrack.addEventListener?.('ended', () => {
|
|
384
428
|
void stopScreenShare();
|
|
385
429
|
});
|
|
@@ -561,10 +605,17 @@ export function useHostSocket(options = {}) {
|
|
|
561
605
|
const setDisplayName = useCallback((name) => {
|
|
562
606
|
patchHostState({ displayName: name });
|
|
563
607
|
}, []);
|
|
608
|
+
const sendChatMessage = useCallback((text) => {
|
|
609
|
+
const trimmed = text.trim();
|
|
610
|
+
if (!trimmed || !hostSession.socket?.connected)
|
|
611
|
+
return;
|
|
612
|
+
hostSession.socket.emit('chat-message', { text: trimmed });
|
|
613
|
+
}, []);
|
|
564
614
|
return {
|
|
565
615
|
...state,
|
|
566
616
|
chatMessages,
|
|
567
617
|
setDisplayName,
|
|
618
|
+
sendChatMessage,
|
|
568
619
|
startPreview,
|
|
569
620
|
stopPreview,
|
|
570
621
|
startLive,
|
package/package.json
CHANGED