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,KAAqD,MAAM,OAAO,CAAC;AAiB1E,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,uDAwa/B,CAAC"}
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))) }), 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
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" }) }), _jsx(TouchableOpacity, { style: styles.bottomIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: [styles.bottomIconGlyph, styles.heartIcon], children: "\u2665" }) })] })), 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" })] }) }))] }));
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,CA2gBpG"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.35",
3
+ "version": "0.3.37",
4
4
  "description": "Reusable livestream viewer/host module for React Native (Expo) — mediasoup + Socket.IO",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",