kasunk99-livestream-core 0.3.35 → 0.3.37
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LiveStreamViewerItem.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamViewerItem.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"LiveStreamViewerItem.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamViewerItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkE,MAAM,OAAO,CAAC;AAmBvF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAM/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,CAAC;AAmEF,eAAO,MAAM,oBAAoB,uDAme/B,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator, Keyboard, KeyboardAvoidingView as RNKeyboardAvoidingView, NativeModules, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, AppState, ActivityIndicator, Keyboard, KeyboardAvoidingView as RNKeyboardAvoidingView, NativeModules, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
4
4
|
import { useViewerSocket } from '../hooks/useViewerSocket';
|
|
5
5
|
const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899', '#a855f7'];
|
|
6
6
|
const BOTTOM_SAFE = Platform.OS === 'ios' ? 28 : 10;
|
|
@@ -43,7 +43,7 @@ try {
|
|
|
43
43
|
catch { }
|
|
44
44
|
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, onLeave, onJoinStatusChange, }) {
|
|
45
45
|
const roomId = isActive ? stream.roomId : null;
|
|
46
|
-
const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
|
|
46
|
+
const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, likeCount, streamEnded, emitLike, } = useViewerSocket(roomId);
|
|
47
47
|
const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
|
|
48
48
|
const hasVideo = producerList.some((p) => p.kind === 'video');
|
|
49
49
|
const streamObj = remoteStream;
|
|
@@ -62,6 +62,33 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
62
62
|
const [chatMessages, setChatMessages] = useState([]);
|
|
63
63
|
// isTyping controls whether we show the TextInput bar or the pill bar
|
|
64
64
|
const [isTyping, setIsTyping] = useState(false);
|
|
65
|
+
const [floatingHearts, setFloatingHearts] = useState([]);
|
|
66
|
+
const heartIdRef = useRef(0);
|
|
67
|
+
const handleLike = useCallback(() => {
|
|
68
|
+
if (!joined)
|
|
69
|
+
return;
|
|
70
|
+
emitLike();
|
|
71
|
+
const id = heartIdRef.current++;
|
|
72
|
+
const anim = new Animated.Value(0);
|
|
73
|
+
const dx = (Math.random() - 0.5) * 36;
|
|
74
|
+
setFloatingHearts((prev) => [...prev, { id, anim, dx }]);
|
|
75
|
+
Animated.timing(anim, { toValue: 1, duration: 900, useNativeDriver: true }).start(() => {
|
|
76
|
+
setFloatingHearts((prev) => prev.filter((h) => h.id !== id));
|
|
77
|
+
});
|
|
78
|
+
}, [joined, emitLike]);
|
|
79
|
+
// Incremented every time the app returns to foreground — forces RTCView to remount,
|
|
80
|
+
// which restarts the Android SurfaceView renderer that freezes during background.
|
|
81
|
+
const [videoKey, setVideoKey] = useState(0);
|
|
82
|
+
const prevAppStateRef = useRef(AppState.currentState);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const sub = AppState.addEventListener('change', (next) => {
|
|
85
|
+
if (prevAppStateRef.current !== 'active' && next === 'active') {
|
|
86
|
+
setVideoKey((k) => k + 1);
|
|
87
|
+
}
|
|
88
|
+
prevAppStateRef.current = next;
|
|
89
|
+
});
|
|
90
|
+
return () => sub.remove();
|
|
91
|
+
}, []);
|
|
65
92
|
const seededRoomRef = useRef(null);
|
|
66
93
|
const chatListRef = useRef(null);
|
|
67
94
|
const textInputRef = useRef(null);
|
|
@@ -241,16 +268,25 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
241
268
|
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
242
269
|
const hasUnsent = chatInput.trim().length > 0;
|
|
243
270
|
const canType = joined && !joining && !error;
|
|
244
|
-
return (_jsxs(KeyboardAvoidingView, { style: styles.container, behavior: "padding", automaticOffset: true, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: StyleSheet.absoluteFillObject, objectFit: "cover", mirror: false, pointerEvents: "none" }, `rtc-${trackCount}-${streamURL}`)) : (_jsxs(View, { style: [StyleSheet.absoluteFillObject, styles.videoPlaceholder], children: [joining && _jsx(ActivityIndicator, { size: "large", color: "rgba(255,255,255,0.6)" }), !joining && error && _jsx(Text, { style: styles.placeholderError, children: error }), joined && !joining && !error && hasVideo && !remoteStream && (_jsx(ActivityIndicator, { size: "large", color: "rgba(255,255,255,0.45)" })), joined && !joining && !error && hasVideo && webrtcUnavailable && (_jsx(Text, { style: styles.placeholderHint, children: "Video needs a development build" })), joined && !joining && !error && hasVideo && consumeError && (_jsx(Text, { style: styles.placeholderError, children: consumeError }))] })), LinearGradient ? (_jsx(LinearGradient, { colors: ['rgba(0,0,0,0.70)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.78)'], locations: [0, 0.28, 0.48, 1], style: StyleSheet.absoluteFillObject, pointerEvents: "none" })) : null, _jsxs(View, { style: styles.topBar, pointerEvents: "box-none", children: [_jsxs(View, { style: styles.hostRow, pointerEvents: "none", children: [_jsx(View, { style: styles.avatar, children: _jsx(Text, { style: styles.avatarLetter, children: avatarLetter }) }), _jsx(Text, { style: styles.hostName, numberOfLines: 1, children: hostLabel }), _jsx(View, { style: styles.livePill, children: _jsx(Text, { style: styles.livePillText, children: "LIVE" }) })] }), _jsxs(View, { style: styles.topRight, pointerEvents: "box-none", children: [_jsxs(View, { style: styles.viewerChip, pointerEvents: "none", children: [_jsx(Text, { style: styles.viewerEye, children: "\uD83D\uDC41" }), _jsx(Text, { style: styles.viewerCount, children: viewerCount > 0 ? viewerCount.toLocaleString() : '—' })] }), onLeave ? (_jsx(TouchableOpacity, { style: styles.closeBtn, onPress: onLeave, hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, activeOpacity: 0.7, children: IoniconsComponent
|
|
271
|
+
return (_jsxs(KeyboardAvoidingView, { style: styles.container, behavior: "padding", automaticOffset: true, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: StyleSheet.absoluteFillObject, objectFit: "cover", mirror: false, pointerEvents: "none" }, `rtc-${trackCount}-${streamURL}-${videoKey}`)) : (_jsxs(View, { style: [StyleSheet.absoluteFillObject, styles.videoPlaceholder], children: [joining && _jsx(ActivityIndicator, { size: "large", color: "rgba(255,255,255,0.6)" }), !joining && error && _jsx(Text, { style: styles.placeholderError, children: error }), joined && !joining && !error && hasVideo && !remoteStream && (_jsx(ActivityIndicator, { size: "large", color: "rgba(255,255,255,0.45)" })), joined && !joining && !error && hasVideo && webrtcUnavailable && (_jsx(Text, { style: styles.placeholderHint, children: "Video needs a development build" })), joined && !joining && !error && hasVideo && consumeError && (_jsx(Text, { style: styles.placeholderError, children: consumeError }))] })), LinearGradient ? (_jsx(LinearGradient, { colors: ['rgba(0,0,0,0.70)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.0)', 'rgba(0,0,0,0.78)'], locations: [0, 0.28, 0.48, 1], style: StyleSheet.absoluteFillObject, pointerEvents: "none" })) : null, _jsxs(View, { style: styles.topBar, pointerEvents: "box-none", children: [_jsxs(View, { style: styles.hostRow, pointerEvents: "none", children: [_jsx(View, { style: styles.avatar, children: _jsx(Text, { style: styles.avatarLetter, children: avatarLetter }) }), _jsx(Text, { style: styles.hostName, numberOfLines: 1, children: hostLabel }), _jsx(View, { style: styles.livePill, children: _jsx(Text, { style: styles.livePillText, children: "LIVE" }) })] }), _jsxs(View, { style: styles.topRight, pointerEvents: "box-none", children: [_jsxs(View, { style: styles.viewerChip, pointerEvents: "none", children: [_jsx(Text, { style: styles.viewerEye, children: "\uD83D\uDC41" }), _jsx(Text, { style: styles.viewerCount, children: viewerCount > 0 ? viewerCount.toLocaleString() : '—' })] }), onLeave ? (_jsx(TouchableOpacity, { style: styles.closeBtn, onPress: onLeave, hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, activeOpacity: 0.7, children: IoniconsComponent
|
|
245
272
|
? _jsx(IoniconsComponent, { name: "close", size: 20, color: "#fff" })
|
|
246
|
-
: _jsx(Text, { style: { color: '#fff', fontSize: 18, fontWeight: '700' }, children: "\u00D7" }) })) : null] })] }), _jsx(View, { style: styles.spacer, pointerEvents: "none" }), _jsx(ScrollView, { ref: chatListRef, style: styles.chatList, contentContainerStyle: styles.chatContent, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "always", children: chatData.map((item) => item.displayName ? (_jsx(View, { style: styles.chatBubble, children: _jsxs(Text, { style: styles.chatLine, numberOfLines: 3, children: [_jsxs(Text, { style: [styles.chatUsername, { color: getNameColor(item.displayName) }], children: [item.displayName, ' '] }), _jsx(Text, { style: styles.chatMsg, children: item.text })] }) }, item.id)) : (_jsx(View, { style: styles.joinBubble, children: _jsx(Text, { style: styles.joinText, children: item.text }) }, item.id))) }),
|
|
273
|
+
: _jsx(Text, { style: { color: '#fff', fontSize: 18, fontWeight: '700' }, children: "\u00D7" }) })) : null] })] }), _jsx(View, { style: styles.spacer, pointerEvents: "none" }), _jsx(ScrollView, { ref: chatListRef, style: styles.chatList, contentContainerStyle: styles.chatContent, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "always", children: chatData.map((item) => item.displayName ? (_jsx(View, { style: styles.chatBubble, children: _jsxs(Text, { style: styles.chatLine, numberOfLines: 3, children: [_jsxs(Text, { style: [styles.chatUsername, { color: getNameColor(item.displayName) }], children: [item.displayName, ' '] }), _jsx(Text, { style: styles.chatMsg, children: item.text })] }) }, item.id)) : (_jsx(View, { style: styles.joinBubble, children: _jsx(Text, { style: styles.joinText, children: item.text }) }, item.id))) }), _jsx(View, { style: styles.heartsContainer, pointerEvents: "none", children: floatingHearts.map(({ id, anim, dx }) => (_jsx(Animated.Text, { style: [
|
|
274
|
+
styles.floatingHeart,
|
|
275
|
+
{
|
|
276
|
+
transform: [
|
|
277
|
+
{ translateX: dx },
|
|
278
|
+
{ translateY: anim.interpolate({ inputRange: [0, 1], outputRange: [0, -150] }) },
|
|
279
|
+
],
|
|
280
|
+
opacity: anim.interpolate({ inputRange: [0, 0.65, 1], outputRange: [1, 0.85, 0] }),
|
|
281
|
+
},
|
|
282
|
+
], children: "\u2665" }, id))) }), isTyping ? (_jsxs(View, { style: styles.inputBar, children: [_jsx(TextInput, { ref: textInputRef, style: styles.textInput, value: chatInput, onChangeText: (text) => { chatInputRef.current = text; setChatInput(text); }, placeholder: "Say something...", placeholderTextColor: "rgba(255,255,255,0.40)", editable: canType, onSubmitEditing: sendChat, returnKeyType: "send", submitBehavior: "submit", autoFocus: true }), RNGHTouchable ? (_jsx(RNGHTouchable, { onPress: sendChat, activeOpacity: 0.75, style: [styles.sendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], hitSlop: { top: 8, right: 8, bottom: 8, left: 8 }, children: IoniconsComponent
|
|
247
283
|
? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
|
|
248
284
|
: _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) })) : (_jsx(Pressable, { style: [styles.sendBtn, (!hasUnsent || !joined) && styles.sendBtnOff], onPressIn: sendChat, hitSlop: { top: 8, right: 8, bottom: 8, left: 8 }, children: IoniconsComponent
|
|
249
285
|
? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
|
|
250
286
|
: _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) }))] })) : (
|
|
251
287
|
// Pill bar — tapping opens the input bar.
|
|
252
288
|
_jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (canType)
|
|
253
|
-
setIsTyping(true); }, activeOpacity: 0.7, children: _jsx(Text, { style: [styles.typePlaceholder, hasUnsent && styles.typePlaceholderFilled], numberOfLines: 1, children: hasUnsent ? chatInput : (joined && !joining ? 'Say something...' : 'Connecting...') }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\u263A" }) }),
|
|
289
|
+
setIsTyping(true); }, activeOpacity: 0.7, children: _jsx(Text, { style: [styles.typePlaceholder, hasUnsent && styles.typePlaceholderFilled], numberOfLines: 1, children: hasUnsent ? chatInput : (joined && !joining ? 'Say something...' : 'Connecting...') }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.bottomIconGlyph, children: "\u263A" }) }), _jsxs(TouchableOpacity, { style: styles.likeBtn, onPress: handleLike, activeOpacity: 0.75, children: [_jsx(Text, { style: [styles.bottomIconGlyph, styles.heartIcon], children: "\u2665" }), likeCount > 0 && (_jsx(Text, { style: styles.likeCountText, children: likeCount >= 1000 ? `${(likeCount / 1000).toFixed(1)}K` : String(likeCount) }))] })] })), streamEnded && (_jsx(View, { style: styles.endedOverlay, children: _jsxs(View, { style: styles.endedCard, children: [_jsx(Text, { style: styles.endedTitle, children: "Stream ended" }), _jsx(Text, { style: styles.endedSub, children: "The host has ended this live stream" })] }) }))] }));
|
|
254
290
|
});
|
|
255
291
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
256
292
|
const styles = StyleSheet.create({
|
|
@@ -431,6 +467,18 @@ const styles = StyleSheet.create({
|
|
|
431
467
|
typePlaceholderFilled: { color: '#fff' },
|
|
432
468
|
bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
|
|
433
469
|
bottomIconGlyph: { fontSize: 22 },
|
|
470
|
+
likeBtn: { alignItems: 'center', justifyContent: 'center', minWidth: 40 },
|
|
471
|
+
likeCountText: { color: 'rgba(255,255,255,0.85)', fontSize: 10, fontWeight: '600', marginTop: 1 },
|
|
472
|
+
heartsContainer: {
|
|
473
|
+
position: 'absolute',
|
|
474
|
+
right: 12,
|
|
475
|
+
bottom: 56 + BOTTOM_SAFE,
|
|
476
|
+
width: 48,
|
|
477
|
+
height: 160,
|
|
478
|
+
alignItems: 'center',
|
|
479
|
+
justifyContent: 'flex-end',
|
|
480
|
+
},
|
|
481
|
+
floatingHeart: { position: 'absolute', bottom: 0, color: '#ef4444', fontSize: 26, fontWeight: '700' },
|
|
434
482
|
endedOverlay: {
|
|
435
483
|
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
436
484
|
backgroundColor: 'rgba(0,0,0,0.80)',
|
|
@@ -15,6 +15,8 @@ type ViewerSocketState = {
|
|
|
15
15
|
consumeRetryKey: number;
|
|
16
16
|
/** Real-time viewer count, updated via viewer-count-updated socket event. */
|
|
17
17
|
viewerCount: number;
|
|
18
|
+
/** Unique-user like count, updated via like-count-updated socket event. */
|
|
19
|
+
likeCount: number;
|
|
18
20
|
/** True once the host disconnects — triggers the "stream ended" overlay. */
|
|
19
21
|
streamEnded: boolean;
|
|
20
22
|
};
|
|
@@ -24,6 +26,7 @@ type ViewerSocketState = {
|
|
|
24
26
|
*/
|
|
25
27
|
export declare function useViewerSocket(roomId: string | null): ViewerSocketState & {
|
|
26
28
|
socket: Socket | null;
|
|
29
|
+
emitLike: () => void;
|
|
27
30
|
};
|
|
28
31
|
export {};
|
|
29
32
|
//# sourceMappingURL=useViewerSocket.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useViewerSocket.d.ts","sourceRoot":"","sources":["../../src/hooks/useViewerSocket.ts"],"names":[],"mappings":"AACA,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"useViewerSocket.d.ts","sourceRoot":"","sources":["../../src/hooks/useViewerSocket.ts"],"names":[],"mappings":"AACA,OAAO,EAAM,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7D,KAAK,iBAAiB,GAAG;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,YAAY,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7B,iBAAiB,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAAE,CAijB1H"}
|
|
@@ -21,6 +21,7 @@ export function useViewerSocket(roomId) {
|
|
|
21
21
|
consumeError: null,
|
|
22
22
|
consumeRetryKey: 0,
|
|
23
23
|
viewerCount: 0,
|
|
24
|
+
likeCount: 0,
|
|
24
25
|
streamEnded: false,
|
|
25
26
|
});
|
|
26
27
|
const socketRef = useRef(null);
|
|
@@ -63,6 +64,7 @@ export function useViewerSocket(roomId) {
|
|
|
63
64
|
consumeError: null,
|
|
64
65
|
consumeRetryKey: 0,
|
|
65
66
|
viewerCount: 0,
|
|
67
|
+
likeCount: 0,
|
|
66
68
|
streamEnded: false,
|
|
67
69
|
});
|
|
68
70
|
consumeRetryCountRef.current = 0;
|
|
@@ -162,6 +164,7 @@ export function useViewerSocket(roomId) {
|
|
|
162
164
|
}
|
|
163
165
|
const roomState = res.roomState ?? null;
|
|
164
166
|
const initialViewerCount = typeof roomState?.viewerCount === 'number' ? roomState.viewerCount : 0;
|
|
167
|
+
const initialLikeCount = typeof roomState?.likeCount === 'number' ? roomState.likeCount : 0;
|
|
165
168
|
setState((prev) => ({
|
|
166
169
|
...prev,
|
|
167
170
|
joining: false,
|
|
@@ -171,6 +174,7 @@ export function useViewerSocket(roomId) {
|
|
|
171
174
|
rtpCapabilities: res.rtpCapabilities ?? null,
|
|
172
175
|
error: null,
|
|
173
176
|
viewerCount: initialViewerCount,
|
|
177
|
+
likeCount: initialLikeCount,
|
|
174
178
|
}));
|
|
175
179
|
});
|
|
176
180
|
});
|
|
@@ -203,11 +207,37 @@ export function useViewerSocket(roomId) {
|
|
|
203
207
|
setState((prev) => ({ ...prev, viewerCount: payload.viewerCount }));
|
|
204
208
|
}
|
|
205
209
|
});
|
|
210
|
+
// Like count: server deduplicates by user identity and broadcasts the unique count.
|
|
211
|
+
socket.on('like-count-updated', (payload) => {
|
|
212
|
+
if (typeof payload?.likeCount === 'number') {
|
|
213
|
+
setState((prev) => ({ ...prev, likeCount: payload.likeCount }));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
206
216
|
socket.on('peer-left', (payload) => {
|
|
207
217
|
if (payload?.isHost) {
|
|
208
218
|
setState((prev) => ({ ...prev, streamEnded: true, producerList: [] }));
|
|
209
219
|
}
|
|
210
220
|
});
|
|
221
|
+
// Socket dropped (Android background network throttling, server restart, etc.).
|
|
222
|
+
// Clear the dead transport and remote stream so the consume flow reruns automatically
|
|
223
|
+
// when socket.io reconnects and fires 'connect' → 'join-room' again.
|
|
224
|
+
socket.on('disconnect', () => {
|
|
225
|
+
const old = transportRef.current;
|
|
226
|
+
try {
|
|
227
|
+
old?.close?.();
|
|
228
|
+
}
|
|
229
|
+
catch { /* ignore */ }
|
|
230
|
+
transportRef.current = null;
|
|
231
|
+
deviceRef.current = null;
|
|
232
|
+
consumedProducerIdsRef.current.clear();
|
|
233
|
+
consumeInProgressRef.current = false;
|
|
234
|
+
setState((prev) => ({
|
|
235
|
+
...prev,
|
|
236
|
+
joined: false,
|
|
237
|
+
remoteStream: null,
|
|
238
|
+
remoteVideoStream: null,
|
|
239
|
+
}));
|
|
240
|
+
});
|
|
211
241
|
socket.on('system-audio-chunk', (payload) => {
|
|
212
242
|
const b64 = typeof payload === 'string' ? payload
|
|
213
243
|
: typeof payload?.data === 'string'
|
|
@@ -348,6 +378,8 @@ export function useViewerSocket(roomId) {
|
|
|
348
378
|
consumeInProgressRef.current = false;
|
|
349
379
|
setState((prev) => ({
|
|
350
380
|
...prev,
|
|
381
|
+
remoteStream: null,
|
|
382
|
+
remoteVideoStream: null,
|
|
351
383
|
consumeError: null,
|
|
352
384
|
consumeRetryKey: prev.consumeRetryKey + 1,
|
|
353
385
|
}));
|
|
@@ -478,8 +510,13 @@ export function useViewerSocket(roomId) {
|
|
|
478
510
|
cancelled = true;
|
|
479
511
|
};
|
|
480
512
|
}, [roomId, state.remoteStream, state.producerList]);
|
|
513
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
514
|
+
const emitLike = useCallback(() => {
|
|
515
|
+
socketRef.current?.emit('like');
|
|
516
|
+
}, []);
|
|
481
517
|
return {
|
|
482
518
|
...state,
|
|
483
519
|
socket: socketRef.current,
|
|
520
|
+
emitLike,
|
|
484
521
|
};
|
|
485
522
|
}
|
package/package.json
CHANGED