kasunk99-livestream-core 0.3.6 → 0.3.8

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;AAe1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAK/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAgBF;;GAEG;AACH,eAAO,MAAM,oBAAoB,uDA4V/B,CAAC"}
1
+ {"version":3,"file":"LiveStreamViewerItem.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamViewerItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqD,MAAM,OAAO,CAAC;AAe1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAK/C,KAAK,yBAAyB,GAAG;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAgBF;;GAEG;AACH,eAAO,MAAM,oBAAoB,uDAgX/B,CAAC"}
@@ -17,7 +17,7 @@ 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, viewerCount: liveViewerCount, } = useViewerSocket(roomId);
20
+ const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
21
21
  // Prefer real-time socket count (updated on every join/leave event);
22
22
  // fall back to the HTTP-polled value from the feed for the initial render.
23
23
  const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
@@ -133,9 +133,21 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
133
133
  return next;
134
134
  });
135
135
  };
136
+ const onViewerJoined = (msg) => {
137
+ const m = msg;
138
+ const name = typeof m.displayName === 'string' ? m.displayName : 'Someone';
139
+ const timestamp = Date.now();
140
+ const id = `${roomId}-join-${String(m.peerId ?? timestamp)}-${timestamp}`;
141
+ setChatMessages((prev) => [
142
+ ...prev,
143
+ { id, displayName: '', text: `${name} joined`, timestamp },
144
+ ]);
145
+ };
136
146
  socket.on('chat-message', onChatMessage);
147
+ socket.on('viewer-joined', onViewerJoined);
137
148
  return () => {
138
149
  socket.off('chat-message', onChatMessage);
150
+ socket.off('viewer-joined', onViewerJoined);
139
151
  };
140
152
  }, [isActive, roomId, socket]);
141
153
  // Render messages in natural order (oldest -> newest).
@@ -208,13 +220,24 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
208
220
  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.hostDisplayName || stream.title || stream.roomId] }) }), _jsx(View, { style: styles.stats, children: _jsxs(Text, { style: styles.statsText, children: ["\uD83D\uDC41 ", viewerCount] }) })] }), _jsx(Animated.View, { style: [styles.chatPanel, { bottom: chatBottomAnim }], pointerEvents: "box-none", children: _jsxs(ScrollView, { scrollEnabled: false, keyboardShouldPersistTaps: "always", contentContainerStyle: 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: [
209
221
  styles.chatSendButton,
210
222
  (!joined || joining || !!error || !chatInput.trim()) && styles.chatSendButtonDisabled,
211
- ], children: _jsx(Text, { style: styles.chatSendText, children: "\u27A4" }) })] })] }) })] })] }));
223
+ ], children: _jsx(Text, { style: styles.chatSendText, children: "\u27A4" }) })] })] }) })] }), streamEnded && (_jsx(View, { style: styles.streamEndedOverlay, children: _jsx(Text, { style: styles.streamEndedText, children: "Live stream ended" }) }))] }));
212
224
  });
213
225
  const styles = StyleSheet.create({
214
226
  container: {
215
227
  flex: 1,
216
228
  backgroundColor: '#0a0a0a',
217
229
  },
230
+ streamEndedOverlay: {
231
+ ...StyleSheet.absoluteFillObject,
232
+ backgroundColor: 'rgba(0,0,0,0.82)',
233
+ alignItems: 'center',
234
+ justifyContent: 'center',
235
+ },
236
+ streamEndedText: {
237
+ color: '#ffffff',
238
+ fontSize: 20,
239
+ fontWeight: '600',
240
+ },
218
241
  rtcView: {
219
242
  ...StyleSheet.absoluteFillObject,
220
243
  backgroundColor: '#0a0a0a',
@@ -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,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,CAiqBrF"}
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,CAgrBrF"}
@@ -214,8 +214,22 @@ export function useHostSocket(options = {}) {
214
214
  const id = `${String(m.peerId ?? 'p')}-${timestamp}-${text.slice(0, 6)}`;
215
215
  setChatMessages((prev) => [...prev.slice(-99), { id, displayName, text, timestamp }]);
216
216
  };
217
+ const onViewerJoined = (msg) => {
218
+ const m = msg;
219
+ const name = typeof m.displayName === 'string' ? m.displayName : 'Someone';
220
+ const timestamp = Date.now();
221
+ const id = `join-${String(m.peerId ?? timestamp)}-${timestamp}`;
222
+ setChatMessages((prev) => [
223
+ ...prev.slice(-99),
224
+ { id, displayName: '', text: `${name} joined`, timestamp },
225
+ ]);
226
+ };
217
227
  socket.on('chat-message', onChatMessage);
218
- return () => { socket.off('chat-message', onChatMessage); };
228
+ socket.on('viewer-joined', onViewerJoined);
229
+ return () => {
230
+ socket.off('chat-message', onChatMessage);
231
+ socket.off('viewer-joined', onViewerJoined);
232
+ };
219
233
  // eslint-disable-next-line react-hooks/exhaustive-deps
220
234
  }, [state.live]);
221
235
  // ── Permissions ────────────────────────────────────────────────────────────
@@ -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
+ /** True once the host disconnects — triggers the "stream ended" overlay. */
19
+ streamEnded: boolean;
18
20
  };
19
21
  /**
20
22
  * 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;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,CAmgBpG"}
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"}
@@ -21,6 +21,7 @@ export function useViewerSocket(roomId) {
21
21
  consumeError: null,
22
22
  consumeRetryKey: 0,
23
23
  viewerCount: 0,
24
+ streamEnded: false,
24
25
  });
25
26
  const socketRef = useRef(null);
26
27
  const roomIdRef = useRef(roomId);
@@ -62,6 +63,7 @@ export function useViewerSocket(roomId) {
62
63
  consumeError: null,
63
64
  consumeRetryKey: 0,
64
65
  viewerCount: 0,
66
+ streamEnded: false,
65
67
  });
66
68
  consumeRetryCountRef.current = 0;
67
69
  }, []);
@@ -87,7 +89,7 @@ export function useViewerSocket(roomId) {
87
89
  setState((prev) => ({ ...prev, error: 'Livestream server not configured', joining: false }));
88
90
  return;
89
91
  }
90
- setState((prev) => ({ ...prev, joining: true, error: null }));
92
+ setState((prev) => ({ ...prev, joining: true, error: null, streamEnded: false }));
91
93
  const JOIN_TIMEOUT_MS = 12000;
92
94
  const joinTimeout = setTimeout(() => {
93
95
  if (roomIdRef.current !== roomId)
@@ -201,6 +203,11 @@ export function useViewerSocket(roomId) {
201
203
  setState((prev) => ({ ...prev, viewerCount: payload.viewerCount }));
202
204
  }
203
205
  });
206
+ socket.on('peer-left', (payload) => {
207
+ if (payload?.isHost) {
208
+ setState((prev) => ({ ...prev, streamEnded: true, producerList: [] }));
209
+ }
210
+ });
204
211
  socket.on('system-audio-chunk', (payload) => {
205
212
  const b64 = typeof payload === 'string' ? payload
206
213
  : typeof payload?.data === 'string'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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",