kasunk99-livestream-core 0.3.36 → 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;AAkB1E,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,uDAsb/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 { AppState, 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,20 @@ 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]);
65
79
  // Incremented every time the app returns to foreground — forces RTCView to remount,
66
80
  // which restarts the Android SurfaceView renderer that freezes during background.
67
81
  const [videoKey, setVideoKey] = useState(0);
@@ -256,14 +270,23 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
256
270
  const canType = joined && !joining && !error;
257
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
258
272
  ? _jsx(IoniconsComponent, { name: "close", size: 20, color: "#fff" })
259
- : _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
260
283
  ? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
261
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
262
285
  ? _jsx(IoniconsComponent, { name: "send", size: 18, color: "#fff" })
263
286
  : _jsx(Text, { style: styles.sendIcon, children: "\u27A4" }) }))] })) : (
264
287
  // Pill bar — tapping opens the input bar.
265
288
  _jsxs(View, { style: styles.bottomBar, children: [_jsx(TouchableOpacity, { style: [styles.typeTouchable, hasUnsent && styles.typeTouchableFilled], onPress: () => { if (canType)
266
- 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" })] }) }))] }));
267
290
  });
268
291
  // ─────────────────────────────────────────────────────────────────────────────
269
292
  const styles = StyleSheet.create({
@@ -444,6 +467,18 @@ const styles = StyleSheet.create({
444
467
  typePlaceholderFilled: { color: '#fff' },
445
468
  bottomIconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },
446
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' },
447
482
  endedOverlay: {
448
483
  position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
449
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,CA+hBpG"}
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,6 +207,12 @@ 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: [] }));
@@ -500,8 +510,13 @@ export function useViewerSocket(roomId) {
500
510
  cancelled = true;
501
511
  };
502
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
+ }, []);
503
517
  return {
504
518
  ...state,
505
519
  socket: socketRef.current,
520
+ emitLike,
506
521
  };
507
522
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.36",
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",