kasunk99-livestream-core 0.2.3 → 0.2.5

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,7 +1,9 @@
1
1
  import React from 'react';
2
2
  /**
3
3
  * TikTok-style vertical feed: one stream per full-screen item.
4
- * When the user scrolls to a new item, that item becomes active and joins the stream.
4
+ * Height is measured from the actual container so it fills exactly to the
5
+ * navigation bar on every device — no hardcoded offsets.
6
+ * Scrolling snaps per-item (next/previous stream only).
5
7
  */
6
8
  export declare function LiveStreamFeed(): React.ReactElement;
7
9
  //# sourceMappingURL=LiveStreamFeed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LiveStreamFeed.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamFeed.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAoB7D;;;GAGG;AACH,wBAAgB,cAAc,IAAI,KAAK,CAAC,YAAY,CAkHnD"}
1
+ {"version":3,"file":"LiveStreamFeed.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamFeed.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAiB7D;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,KAAK,CAAC,YAAY,CAgInD"}
@@ -3,17 +3,23 @@ import { useCallback, useRef, useState } from 'react';
3
3
  import { Dimensions, FlatList, RefreshControl, ScrollView, StyleSheet, Text, View, } from 'react-native';
4
4
  import { LiveStreamViewerItem } from './LiveStreamViewerItem';
5
5
  import { useLiveStreams } from '../hooks/useLiveStreams';
6
- const { height: SCREEN_HEIGHT } = Dimensions.get('window');
7
- // Adjust for top header + bottom tab bar so each item fits without extra scroll.
8
- const VIEWPORT_HEIGHT = SCREEN_HEIGHT - 120;
9
6
  /**
10
7
  * TikTok-style vertical feed: one stream per full-screen item.
11
- * When the user scrolls to a new item, that item becomes active and joins the stream.
8
+ * Height is measured from the actual container so it fills exactly to the
9
+ * navigation bar on every device — no hardcoded offsets.
10
+ * Scrolling snaps per-item (next/previous stream only).
12
11
  */
13
12
  export function LiveStreamFeed() {
14
13
  const { streams, loading, error, refetch } = useLiveStreams();
15
14
  const [activeIndex, setActiveIndex] = useState(0);
16
15
  const [refreshing, setRefreshing] = useState(false);
16
+ // Initialise with a rough estimate so the first render isn't empty;
17
+ // corrected to the exact value after the container lays out.
18
+ const [viewportHeight, setViewportHeight] = useState(() => Dimensions.get('window').height - 120);
19
+ const onContainerLayout = useCallback((e) => {
20
+ const h = Math.round(e.nativeEvent.layout.height);
21
+ setViewportHeight((prev) => (prev === h ? prev : h));
22
+ }, []);
17
23
  const onRefresh = useCallback(async () => {
18
24
  setRefreshing(true);
19
25
  await refetch();
@@ -25,46 +31,42 @@ export function LiveStreamFeed() {
25
31
  }).current;
26
32
  const onViewableItemsChanged = useRef(({ viewableItems }) => {
27
33
  const first = viewableItems[0];
28
- if (first?.index != null) {
34
+ if (first?.index != null)
29
35
  setActiveIndex(first.index);
30
- }
31
36
  }).current;
32
37
  const onMomentumScrollEnd = useCallback((e) => {
33
- const offsetY = e.nativeEvent.contentOffset.y;
34
- const index = Math.round(offsetY / VIEWPORT_HEIGHT);
35
- const clamped = Math.max(0, Math.min(index, streams.length - 1));
36
- setActiveIndex(clamped);
37
- }, [streams.length]);
38
- const renderItem = useCallback(({ item, index }) => (_jsx(View, { style: styles.item, children: _jsx(LiveStreamViewerItem, { stream: item, isActive: index === activeIndex, index: index }) })), [activeIndex]);
38
+ const index = Math.round(e.nativeEvent.contentOffset.y / viewportHeight);
39
+ setActiveIndex(Math.max(0, Math.min(index, streams.length - 1)));
40
+ }, [streams.length, viewportHeight]);
41
+ const renderItem = useCallback(({ item, index }) => (_jsx(View, { style: { height: viewportHeight, width: '100%' }, children: _jsx(LiveStreamViewerItem, { stream: item, isActive: index === activeIndex, index: index }) })), [activeIndex, viewportHeight]);
39
42
  const keyExtractor = useCallback((item) => item.roomId, []);
40
43
  const getItemLayout = useCallback((_, index) => ({
41
- length: VIEWPORT_HEIGHT,
42
- offset: VIEWPORT_HEIGHT * index,
44
+ length: viewportHeight,
45
+ offset: viewportHeight * index,
43
46
  index,
44
- }), []);
47
+ }), [viewportHeight]);
45
48
  if (loading && streams.length === 0) {
46
- return (_jsx(View, { style: styles.center, children: _jsx(Text, { style: styles.message, children: "Loading streams..." }) }));
49
+ return (_jsx(View, { style: styles.flex, onLayout: onContainerLayout, children: _jsx(View, { style: [styles.center, { height: viewportHeight }], children: _jsx(Text, { style: styles.message, children: "Loading streams..." }) }) }));
47
50
  }
48
51
  if (error && streams.length === 0) {
49
- return (_jsxs(ScrollView, { contentContainerStyle: styles.center, refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: onRefresh, tintColor: "#e5e5e5" }), children: [_jsx(Text, { style: styles.error, children: error }), _jsx(Text, { style: styles.hint, children: "Check server URL and pull down to retry." })] }));
52
+ return (_jsx(View, { style: styles.flex, onLayout: onContainerLayout, children: _jsxs(ScrollView, { contentContainerStyle: [styles.center, { minHeight: viewportHeight }], refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: onRefresh, tintColor: "#e5e5e5" }), children: [_jsx(Text, { style: styles.error, children: error }), _jsx(Text, { style: styles.hint, children: "Check server URL and pull down to retry." })] }) }));
50
53
  }
51
54
  if (streams.length === 0) {
52
- return (_jsxs(ScrollView, { contentContainerStyle: styles.center, refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: onRefresh, tintColor: "#e5e5e5" }), children: [_jsx(Text, { style: styles.message, children: "No live streams right now" }), _jsx(Text, { style: styles.hint, children: "Go live or pull down to refresh." })] }));
55
+ return (_jsx(View, { style: styles.flex, onLayout: onContainerLayout, children: _jsxs(ScrollView, { contentContainerStyle: [styles.center, { minHeight: viewportHeight }], refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: onRefresh, tintColor: "#e5e5e5" }), children: [_jsx(Text, { style: styles.message, children: "No live streams right now" }), _jsx(Text, { style: styles.hint, children: "Go live or pull down to refresh." })] }) }));
53
56
  }
54
- return (_jsx(FlatList, { data: streams, renderItem: renderItem, keyExtractor: keyExtractor, getItemLayout: getItemLayout, pagingEnabled: true, nestedScrollEnabled: true, showsVerticalScrollIndicator: false, decelerationRate: "fast", snapToInterval: VIEWPORT_HEIGHT, snapToAlignment: "start", onViewableItemsChanged: onViewableItemsChanged, viewabilityConfig: viewabilityConfig, onMomentumScrollEnd: onMomentumScrollEnd, initialNumToRender: 2, maxToRenderPerBatch: 2, windowSize: 3, refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: onRefresh, tintColor: "#e5e5e5" }) }));
57
+ return (_jsx(View, { style: styles.flex, onLayout: onContainerLayout, children: _jsx(FlatList, { data: streams, renderItem: renderItem, keyExtractor: keyExtractor, getItemLayout: getItemLayout, pagingEnabled: true, showsVerticalScrollIndicator: false, decelerationRate: "fast", snapToInterval: viewportHeight, snapToAlignment: "start", onViewableItemsChanged: onViewableItemsChanged, viewabilityConfig: viewabilityConfig, onMomentumScrollEnd: onMomentumScrollEnd, initialNumToRender: 2, maxToRenderPerBatch: 2, windowSize: 3, refreshControl: _jsx(RefreshControl, { refreshing: refreshing, onRefresh: onRefresh, tintColor: "#e5e5e5" }) }) }));
55
58
  }
56
59
  const styles = StyleSheet.create({
57
- center: {
60
+ flex: {
58
61
  flex: 1,
62
+ backgroundColor: '#0a0a0a',
63
+ },
64
+ center: {
59
65
  justifyContent: 'center',
60
66
  alignItems: 'center',
61
67
  backgroundColor: '#0a0a0a',
62
68
  padding: 24,
63
69
  },
64
- item: {
65
- height: VIEWPORT_HEIGHT,
66
- width: '100%',
67
- },
68
70
  message: {
69
71
  color: '#e5e5e5',
70
72
  fontSize: 16,
@@ -17,10 +17,10 @@ catch {
17
17
  */
18
18
  export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index, }) {
19
19
  const roomId = isActive ? stream.roomId : null;
20
- const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, } = useViewerSocket(roomId);
21
- const viewerCount = roomState && typeof roomState.viewerCount === 'number'
22
- ? roomState.viewerCount
23
- : stream.viewerCount;
20
+ const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, } = useViewerSocket(roomId);
21
+ // Prefer real-time socket count (updated on every join/leave event);
22
+ // fall back to the HTTP-polled value from the feed for the initial render.
23
+ const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
24
24
  const hasVideo = producerList.some((p) => p.kind === 'video');
25
25
  const streamObj = remoteStream;
26
26
  const hasVideoTrack = streamObj &&
@@ -147,7 +147,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
147
147
  }
148
148
  });
149
149
  };
150
- return (_jsxs(View, { style: styles.container, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: styles.rtcView, objectFit: "cover", mirror: false }, `rtc-${trackCount}-${streamURL}`)) : (_jsxs(View, { style: styles.videoPlaceholder, children: [joining && _jsx(Text, { style: styles.placeholderText, children: "Joining..." }), error && _jsx(Text, { style: [styles.placeholderText, styles.errorText], children: error }), joined && !joining && !error && (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.liveBadge, children: "LIVE" }), _jsx(Text, { style: styles.placeholderText, children: stream.roomId }), hasVideo && webrtcUnavailable && (_jsxs(Text, { style: [styles.hintText, styles.warnText], children: ["Video needs a development build.", '\n', "Run: npx expo run:android"] })), hasVideo && consumeError && (_jsx(Text, { style: [styles.hintText, styles.errorText], children: consumeError })), hasVideo && !remoteStream && !webrtcUnavailable && !consumeError && (_jsx(Text, { style: styles.hintText, children: "Loading video..." })), !hasVideo && joined && producerList.length === 0 && (_jsxs(Text, { style: [styles.hintText, styles.warnText], children: ["Host has not started camera yet.", '\n', "Use a web client to stream, or wait for host to produce."] })), hasVideo && remoteStream && !RTCViewComponent && (_jsx(Text, { style: styles.hintText, children: "Video received (use dev build to see it)" })), hasVideo && remoteStream && RTCViewComponent && !hasVideoTrack && (_jsx(Text, { style: [styles.hintText, styles.warnText], children: "Audio only (no video track yet)" })), hasVideo && remoteStream && hasVideoTrack && !streamURL && (_jsx(Text, { style: [styles.hintText, styles.warnText], children: "Video track received but cannot display (need dev build with react-native-webrtc)" }))] }))] })), _jsxs(View, { style: styles.overlay, children: [_jsxs(View, { style: styles.topMeta, children: [_jsx(View, { style: styles.row, children: _jsxs(Text, { style: styles.roomId, numberOfLines: 1, children: ["LIVE \u00B7 ", stream.roomId] }) }), _jsxs(View, { style: styles.stats, children: [_jsxs(Text, { style: styles.statsText, children: ["\uD83D\uDC41 ", viewerCount] }), _jsxs(Text, { style: [styles.statsText, styles.statsTextSpacer], children: ["\u00B7 ", stream.producerCount, " track(s)"] })] })] }), _jsx(View, { style: styles.chatPanel, pointerEvents: "box-none", children: _jsx(KeyboardAvoidingView, { style: styles.chatKeyboardAvoider, behavior: Platform.OS === 'ios' ? 'padding' : 'height', keyboardVerticalOffset: 0, children: _jsxs(View, { style: styles.chatStack, children: [_jsx(View, { style: styles.chatListWrapper, children: _jsx(ScrollView, { ref: chatListRef, contentContainerStyle: styles.chatListContent, showsVerticalScrollIndicator: false, keyboardDismissMode: "on-drag", keyboardShouldPersistTaps: "always", nestedScrollEnabled: true, children: chatData.map((item) => (_jsxs(Text, { style: styles.chatLine, numberOfLines: 2, children: [_jsx(Text, { style: [styles.chatName, { color: getNameColor(item.displayName) }], children: item.displayName }), _jsxs(Text, { style: styles.chatText, children: [" ", item.text] })] }, item.id))) }) }), _jsxs(View, { style: styles.chatInputRow, children: [_jsx(TextInput, { style: styles.chatInput, value: chatInput, onChangeText: setChatInput, placeholder: joined ? 'Say something...' : 'Connecting...', placeholderTextColor: "#9ca3af", editable: joined && !joining && !error, onSubmitEditing: sendChat, returnKeyType: "send" }), _jsx(TouchableOpacity, { onPress: sendChat, disabled: !joined || joining || !!error || !chatInput.trim(), style: [
150
+ return (_jsxs(View, { style: styles.container, children: [showVideo && RTCViewComponent && streamURL ? (_jsx(RTCViewComponent, { streamURL: streamURL, stream: displayStream, style: styles.rtcView, objectFit: "contain", mirror: false }, `rtc-${trackCount}-${streamURL}`)) : (_jsxs(View, { style: styles.videoPlaceholder, children: [joining && _jsx(Text, { style: styles.placeholderText, children: "Joining..." }), error && _jsx(Text, { style: [styles.placeholderText, styles.errorText], children: error }), joined && !joining && !error && (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.liveBadge, children: "LIVE" }), _jsx(Text, { style: styles.placeholderText, children: stream.roomId }), hasVideo && webrtcUnavailable && (_jsxs(Text, { style: [styles.hintText, styles.warnText], children: ["Video needs a development build.", '\n', "Run: npx expo run:android"] })), hasVideo && consumeError && (_jsx(Text, { style: [styles.hintText, styles.errorText], children: consumeError })), hasVideo && !remoteStream && !webrtcUnavailable && !consumeError && (_jsx(Text, { style: styles.hintText, children: "Loading video..." })), !hasVideo && joined && producerList.length === 0 && (_jsxs(Text, { style: [styles.hintText, styles.warnText], children: ["Host has not started camera yet.", '\n', "Use a web client to stream, or wait for host to produce."] })), hasVideo && remoteStream && !RTCViewComponent && (_jsx(Text, { style: styles.hintText, children: "Video received (use dev build to see it)" })), hasVideo && remoteStream && RTCViewComponent && !hasVideoTrack && (_jsx(Text, { style: [styles.hintText, styles.warnText], children: "Audio only (no video track yet)" })), hasVideo && remoteStream && hasVideoTrack && !streamURL && (_jsx(Text, { style: [styles.hintText, styles.warnText], children: "Video track received but cannot display (need dev build with react-native-webrtc)" }))] }))] })), _jsxs(View, { style: styles.overlay, children: [_jsxs(View, { style: styles.topMeta, children: [_jsx(View, { style: styles.row, children: _jsxs(Text, { style: styles.roomId, numberOfLines: 1, children: ["LIVE \u00B7 ", stream.hostDisplayName || stream.title || stream.roomId] }) }), _jsxs(View, { style: styles.stats, children: [_jsxs(Text, { style: styles.statsText, children: ["\uD83D\uDC41 ", viewerCount] }), _jsxs(Text, { style: [styles.statsText, styles.statsTextSpacer], children: ["\u00B7 ", stream.producerCount, " track(s)"] })] })] }), _jsx(View, { style: styles.chatPanel, pointerEvents: "box-none", children: _jsx(KeyboardAvoidingView, { style: styles.chatKeyboardAvoider, behavior: Platform.OS === 'ios' ? 'padding' : 'height', keyboardVerticalOffset: 0, children: _jsxs(View, { style: styles.chatStack, children: [_jsx(View, { style: styles.chatListWrapper, children: _jsx(ScrollView, { ref: chatListRef, contentContainerStyle: styles.chatListContent, showsVerticalScrollIndicator: false, keyboardDismissMode: "on-drag", keyboardShouldPersistTaps: "always", nestedScrollEnabled: true, children: chatData.map((item) => (_jsxs(Text, { style: styles.chatLine, numberOfLines: 2, children: [_jsx(Text, { style: [styles.chatName, { color: getNameColor(item.displayName) }], children: item.displayName }), _jsxs(Text, { style: styles.chatText, children: [" ", item.text] })] }, item.id))) }) }), _jsxs(View, { style: styles.chatInputRow, children: [_jsx(TextInput, { style: styles.chatInput, value: chatInput, onChangeText: setChatInput, placeholder: joined ? 'Say something...' : 'Connecting...', placeholderTextColor: "#9ca3af", editable: joined && !joining && !error, onSubmitEditing: sendChat, returnKeyType: "send" }), _jsx(TouchableOpacity, { onPress: sendChat, disabled: !joined || joining || !!error || !chatInput.trim(), style: [
151
151
  styles.chatSendButton,
152
152
  (!joined || joining || !!error || !chatInput.trim()) && styles.chatSendButtonDisabled,
153
153
  ], children: _jsx(Text, { style: styles.chatSendText, children: "\u27A4" }) })] })] }) }) })] })] }));
@@ -13,6 +13,8 @@ type ViewerSocketState = {
13
13
  consumeError: string | null;
14
14
  /** Incremented to trigger one auto-retry when recv transport fails (ICE). */
15
15
  consumeRetryKey: number;
16
+ /** Real-time viewer count, updated via viewer-count-updated socket event. */
17
+ viewerCount: number;
16
18
  };
17
19
  /**
18
20
  * Connects to the livestream signaling server and joins a room as viewer.
@@ -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;CACzB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA+cpG"}
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;CACrB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,iBAAiB,GAAG;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CA2dpG"}
@@ -20,6 +20,7 @@ export function useViewerSocket(roomId) {
20
20
  webrtcUnavailable: false,
21
21
  consumeError: null,
22
22
  consumeRetryKey: 0,
23
+ viewerCount: 0,
23
24
  });
24
25
  const socketRef = useRef(null);
25
26
  const roomIdRef = useRef(roomId);
@@ -61,6 +62,7 @@ export function useViewerSocket(roomId) {
61
62
  webrtcUnavailable: false,
62
63
  consumeError: null,
63
64
  consumeRetryKey: 0,
65
+ viewerCount: 0,
64
66
  });
65
67
  consumeRetryCountRef.current = 0;
66
68
  }, []);
@@ -156,14 +158,17 @@ export function useViewerSocket(roomId) {
156
158
  if (Array.isArray(iceFromJoin) && iceFromJoin.length > 0) {
157
159
  iceServersRef.current = iceFromJoin;
158
160
  }
161
+ const roomState = res.roomState ?? null;
162
+ const initialViewerCount = typeof roomState?.viewerCount === 'number' ? roomState.viewerCount : 0;
159
163
  setState((prev) => ({
160
164
  ...prev,
161
165
  joining: false,
162
166
  joined: true,
163
167
  producerList: res.producerList ?? [],
164
- roomState: res.roomState ?? null,
168
+ roomState,
165
169
  rtpCapabilities: res.rtpCapabilities ?? null,
166
170
  error: null,
171
+ viewerCount: initialViewerCount,
167
172
  }));
168
173
  });
169
174
  });
@@ -191,6 +196,11 @@ export function useViewerSocket(roomId) {
191
196
  }
192
197
  });
193
198
  });
199
+ socket.on('viewer-count-updated', (payload) => {
200
+ if (typeof payload?.viewerCount === 'number') {
201
+ setState((prev) => ({ ...prev, viewerCount: payload.viewerCount }));
202
+ }
203
+ });
194
204
  })();
195
205
  return () => {
196
206
  clearTimeout(joinTimeout);
@@ -1 +1 @@
1
- {"version":3,"file":"livestream.service.d.ts","sourceRoot":"","sources":["../../src/services/livestream.service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAC1B;IAAE,OAAO,EAAE,cAAc,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC5C;IAAE,OAAO,EAAE,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAUnC,MAAM,MAAM,qBAAqB,GAC7B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAChC;IAAE,KAAK,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC/F;IAAE,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA+BtE;AAED,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE;IAC1C,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC9B;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyBjC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
1
+ {"version":3,"file":"livestream.service.d.ts","sourceRoot":"","sources":["../../src/services/livestream.service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,MAAM,MAAM,kBAAkB,GAC1B;IAAE,OAAO,EAAE,cAAc,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC5C;IAAE,OAAO,EAAE,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAiBnC,MAAM,MAAM,qBAAqB,GAC7B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAChC;IAAE,KAAK,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAErC,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAC/F;IAAE,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CA+BtE;AAED,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE;IAC1C,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC9B;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAyBjC;AAED,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C"}
@@ -8,7 +8,12 @@ function normalizeStreamRow(row) {
8
8
  return null;
9
9
  const viewerCount = typeof row?.viewerCount === 'number' ? row.viewerCount : typeof row?.viewer_count === 'number' ? row.viewer_count : 0;
10
10
  const producerCount = typeof row?.producerCount === 'number' ? row.producerCount : typeof row?.producer_count === 'number' ? row.producer_count : 0;
11
- return { roomId, viewerCount, producerCount };
11
+ const title = typeof row?.title === 'string' ? row.title : null;
12
+ const hostDisplayName = typeof row?.hostDisplayName === 'string' ? row.hostDisplayName :
13
+ typeof row?.host_display_name === 'string' ? row.host_display_name : null;
14
+ const hostUserId = typeof row?.hostUserId === 'string' ? row.hostUserId :
15
+ typeof row?.host_user_id === 'string' ? row.host_user_id : null;
16
+ return { roomId, viewerCount, producerCount, title, hostDisplayName, hostUserId };
12
17
  }
13
18
  export async function fetchActiveStreams() {
14
19
  const base = getBaseUrl();
package/dist/types.d.ts CHANGED
@@ -6,6 +6,9 @@ export type LiveStreamInfo = {
6
6
  roomId: string;
7
7
  viewerCount: number;
8
8
  producerCount: number;
9
+ title?: string | null;
10
+ hostDisplayName?: string | null;
11
+ hostUserId?: string | null;
9
12
  };
10
13
  export type ProducerInfo = {
11
14
  id: string;
@@ -27,6 +30,8 @@ export type RoomState = {
27
30
  chat?: unknown[];
28
31
  likes?: number;
29
32
  viewerCount?: number;
33
+ cards?: unknown[];
34
+ shares?: number;
30
35
  [key: string]: unknown;
31
36
  };
32
37
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",