kasunk99-livestream-core 0.3.7 → 0.3.9
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;
|
|
1
|
+
{"version":3,"file":"LiveStreamViewerItem.d.ts","sourceRoot":"","sources":["../../src/components/LiveStreamViewerItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqD,MAAM,OAAO,CAAC;AAgB1E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAO/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,uDA2X/B,CAAC"}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { Animated, Dimensions, Keyboard, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
3
|
+
import { ActivityIndicator, Animated, Dimensions, Keyboard, NativeModules, Platform, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from 'react-native';
|
|
4
4
|
import { useViewerSocket } from '../hooks/useViewerSocket';
|
|
5
|
-
const
|
|
5
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
6
6
|
const CHAT_NAME_COLORS = ['#f97316', '#22c55e', '#3b82f6', '#eab308', '#ec4899', '#a855f7'];
|
|
7
|
+
// iOS needs bottom safe-area padding; Android auto-adjusts via adjustResize
|
|
8
|
+
const BOTTOM_SAFE = Platform.OS === 'ios' ? 28 : 10;
|
|
7
9
|
let RTCViewComponent = null;
|
|
8
10
|
try {
|
|
9
11
|
const webrtc = require('react-native-webrtc');
|
|
10
12
|
RTCViewComponent = webrtc.RTCView;
|
|
11
13
|
}
|
|
12
14
|
catch {
|
|
13
|
-
// react-native-webrtc not available
|
|
15
|
+
// react-native-webrtc not available in Expo Go
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Single full-screen viewer cell. When isActive, joins the stream room and consumes video/audio.
|
|
17
19
|
*/
|
|
18
|
-
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index, }) {
|
|
20
|
+
export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream, isActive, index: _index, }) {
|
|
19
21
|
const roomId = isActive ? stream.roomId : null;
|
|
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.
|
|
22
|
+
const { joined, joining, error, producerList, roomState, remoteStream, remoteVideoStream, webrtcUnavailable, consumeError, socket, viewerCount: liveViewerCount, streamEnded, } = useViewerSocket(roomId);
|
|
23
23
|
const viewerCount = liveViewerCount > 0 ? liveViewerCount : stream.viewerCount;
|
|
24
24
|
const hasVideo = producerList.some((p) => p.kind === 'video');
|
|
25
25
|
const streamObj = remoteStream;
|
|
@@ -35,41 +35,35 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
35
35
|
const showVideo = RTCViewComponent && remoteStream && !!streamURL && hasVideoTrack;
|
|
36
36
|
const [chatInput, setChatInput] = useState('');
|
|
37
37
|
const [chatMessages, setChatMessages] = useState([]);
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
// animation so the panel moves in perfect sync with the keyboard.
|
|
41
|
-
// Android: windowSoftInputMode="adjustResize" shrinks the layout automatically,
|
|
42
|
-
// so no JS offset is needed (adding one would create a double gap).
|
|
43
|
-
const chatBottomAnim = useRef(new Animated.Value(16)).current;
|
|
38
|
+
// iOS keyboard animation — Android handled automatically via windowSoftInputMode=adjustResize
|
|
39
|
+
const kbOffset = useRef(new Animated.Value(0)).current;
|
|
44
40
|
useEffect(() => {
|
|
45
41
|
if (Platform.OS !== 'ios')
|
|
46
|
-
return;
|
|
42
|
+
return;
|
|
47
43
|
const show = Keyboard.addListener('keyboardWillShow', (e) => {
|
|
48
|
-
Animated.timing(
|
|
49
|
-
toValue:
|
|
50
|
-
duration: e.duration ??
|
|
44
|
+
Animated.timing(kbOffset, {
|
|
45
|
+
toValue: e.endCoordinates.height,
|
|
46
|
+
duration: e.duration ?? 260,
|
|
51
47
|
useNativeDriver: false,
|
|
52
48
|
}).start();
|
|
53
49
|
});
|
|
54
50
|
const hide = Keyboard.addListener('keyboardWillHide', (e) => {
|
|
55
|
-
Animated.timing(
|
|
56
|
-
toValue:
|
|
57
|
-
duration: e.duration ??
|
|
51
|
+
Animated.timing(kbOffset, {
|
|
52
|
+
toValue: 0,
|
|
53
|
+
duration: e.duration ?? 220,
|
|
58
54
|
useNativeDriver: false,
|
|
59
55
|
}).start();
|
|
60
56
|
});
|
|
61
|
-
return () => {
|
|
62
|
-
|
|
63
|
-
hide.remove();
|
|
64
|
-
};
|
|
65
|
-
}, [chatBottomAnim]);
|
|
57
|
+
return () => { show.remove(); hide.remove(); };
|
|
58
|
+
}, [kbOffset]);
|
|
66
59
|
const seededRoomRef = useRef(null);
|
|
67
60
|
const chatListRef = useRef(null);
|
|
68
61
|
const seededFromRoomState = useMemo(() => {
|
|
69
|
-
const chat = roomState && typeof roomState === 'object'
|
|
62
|
+
const chat = roomState && typeof roomState === 'object'
|
|
63
|
+
? roomState.chat
|
|
64
|
+
: null;
|
|
70
65
|
return Array.isArray(chat) ? chat : [];
|
|
71
66
|
}, [roomState]);
|
|
72
|
-
// Reset local chat when switching rooms.
|
|
73
67
|
useEffect(() => {
|
|
74
68
|
if (!roomId) {
|
|
75
69
|
seededRoomRef.current = null;
|
|
@@ -83,7 +77,6 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
83
77
|
setChatInput('');
|
|
84
78
|
}
|
|
85
79
|
}, [roomId]);
|
|
86
|
-
// Seed chat history (once per room) from roomState.chat if present.
|
|
87
80
|
useEffect(() => {
|
|
88
81
|
if (!roomId)
|
|
89
82
|
return;
|
|
@@ -102,18 +95,18 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
102
95
|
const msg = m;
|
|
103
96
|
const displayName = typeof msg.displayName === 'string' ? msg.displayName : 'User';
|
|
104
97
|
const text = typeof msg.text === 'string' ? msg.text : '';
|
|
105
|
-
const timestamp = typeof msg.timestamp === 'number'
|
|
98
|
+
const timestamp = typeof msg.timestamp === 'number'
|
|
99
|
+
? msg.timestamp
|
|
100
|
+
: now - (seededFromRoomState.length - i) * 1000;
|
|
106
101
|
if (!text)
|
|
107
102
|
return null;
|
|
108
103
|
const id = `${roomId}-seed-${String(msg.peerId ?? i)}-${timestamp}-${i}`;
|
|
109
104
|
return { id, displayName, text, timestamp };
|
|
110
105
|
})
|
|
111
106
|
.filter(Boolean);
|
|
112
|
-
// Keep a reasonable amount of history, scrollable from the start.
|
|
113
107
|
return normalized;
|
|
114
108
|
});
|
|
115
109
|
}, [roomId, seededFromRoomState]);
|
|
116
|
-
// Live incoming chat messages.
|
|
117
110
|
useEffect(() => {
|
|
118
111
|
if (!socket || !roomId || !isActive)
|
|
119
112
|
return;
|
|
@@ -127,11 +120,7 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
127
120
|
if (!text)
|
|
128
121
|
return;
|
|
129
122
|
const id = `${roomId}-${String(m.peerId ?? 'peer')}-${timestamp}-${text.slice(0, 8)}`;
|
|
130
|
-
setChatMessages((prev) => {
|
|
131
|
-
const next = [...prev, { id, displayName, text, timestamp }];
|
|
132
|
-
// Allow full history to be scrollable.
|
|
133
|
-
return next;
|
|
134
|
-
});
|
|
123
|
+
setChatMessages((prev) => [...prev, { id, displayName, text, timestamp }]);
|
|
135
124
|
};
|
|
136
125
|
const onViewerJoined = (msg) => {
|
|
137
126
|
const m = msg;
|
|
@@ -150,20 +139,16 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
150
139
|
socket.off('viewer-joined', onViewerJoined);
|
|
151
140
|
};
|
|
152
141
|
}, [isActive, roomId, socket]);
|
|
153
|
-
// Render messages in natural order (oldest -> newest).
|
|
154
142
|
const chatData = useMemo(() => chatMessages, [chatMessages]);
|
|
155
|
-
// Always scroll to the latest message at the bottom when chat loads/updates.
|
|
156
143
|
useEffect(() => {
|
|
157
144
|
if (!chatListRef.current || chatData.length === 0)
|
|
158
145
|
return;
|
|
159
146
|
try {
|
|
160
147
|
chatListRef.current.scrollToEnd({ animated: true });
|
|
161
148
|
}
|
|
162
|
-
catch {
|
|
163
|
-
// ignore scroll errors
|
|
164
|
-
}
|
|
149
|
+
catch { /* ignore */ }
|
|
165
150
|
}, [chatData.length]);
|
|
166
|
-
// System audio playback (Android only
|
|
151
|
+
// System audio playback (Android only)
|
|
167
152
|
useEffect(() => {
|
|
168
153
|
if (!socket || !joined || !isActive || Platform.OS !== 'android')
|
|
169
154
|
return;
|
|
@@ -198,174 +183,290 @@ export const LiveStreamViewerItem = memo(function LiveStreamViewerItem({ stream,
|
|
|
198
183
|
return CHAT_NAME_COLORS[0];
|
|
199
184
|
let hash = 0;
|
|
200
185
|
for (let i = 0; i < name.length; i += 1) {
|
|
201
|
-
// simple string hash
|
|
202
186
|
hash = (hash << 5) - hash + name.charCodeAt(i);
|
|
203
187
|
hash |= 0;
|
|
204
188
|
}
|
|
205
|
-
|
|
206
|
-
return CHAT_NAME_COLORS[idx];
|
|
189
|
+
return CHAT_NAME_COLORS[Math.abs(hash) % CHAT_NAME_COLORS.length];
|
|
207
190
|
};
|
|
208
|
-
const sendChat =
|
|
191
|
+
const sendChat = () => {
|
|
209
192
|
const text = chatInput.trim();
|
|
210
193
|
if (!text || !socket || !joined)
|
|
211
194
|
return;
|
|
212
195
|
setChatInput('');
|
|
213
196
|
socket.emit('chat-message', { text }, (res) => {
|
|
214
|
-
if (res?.error)
|
|
215
|
-
// eslint-disable-next-line no-console
|
|
197
|
+
if (res?.error)
|
|
216
198
|
console.log('[viewer] chat send error', res.error);
|
|
217
|
-
}
|
|
218
199
|
});
|
|
219
200
|
};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
], children: _jsx(Text, { style: styles.chatSendText, children: "\u27A4" }) })] })] }) })] })] }));
|
|
201
|
+
const hostLabel = stream.hostDisplayName || stream.title || 'Live';
|
|
202
|
+
const avatarLetter = hostLabel[0]?.toUpperCase() ?? 'L';
|
|
203
|
+
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(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 }))] })), _jsx(View, { style: styles.topGradient, pointerEvents: "none" }), _jsx(View, { style: styles.bottomGradient, pointerEvents: "none" }), _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.viewerChip, pointerEvents: "none", children: [_jsx(Text, { style: styles.viewerEye, children: "\uD83D\uDC41" }), _jsx(Text, { style: styles.viewerCount, children: viewerCount > 0 ? viewerCount.toLocaleString() : '—' })] })] }), _jsxs(View, { style: styles.rightColumn, pointerEvents: "box-none", children: [_jsxs(TouchableOpacity, { style: styles.actionBtn, activeOpacity: 0.75, children: [_jsx(View, { style: styles.actionCircle, children: _jsx(Text, { style: styles.actionIcon, children: "\u2665" }) }), _jsx(Text, { style: styles.actionLabel, children: "Like" })] }), _jsxs(TouchableOpacity, { style: styles.actionBtn, activeOpacity: 0.75, children: [_jsx(View, { style: styles.actionCircle, children: _jsx(Text, { style: styles.actionIcon, children: "\uD83C\uDF81" }) }), _jsx(Text, { style: styles.actionLabel, children: "Gift" })] }), _jsxs(TouchableOpacity, { style: styles.actionBtn, activeOpacity: 0.75, children: [_jsx(View, { style: styles.actionCircle, children: _jsx(Text, { style: styles.actionIcon, children: "\u2197" }) }), _jsx(Text, { style: styles.actionLabel, children: "Share" })] })] }), _jsxs(Animated.View, { style: [styles.bottomPanel, { bottom: kbOffset }], pointerEvents: "box-none", children: [_jsx(View, { style: styles.chatColumn, pointerEvents: "box-none", children: _jsx(ScrollView, { ref: chatListRef, contentContainerStyle: styles.chatContent, showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "always", keyboardDismissMode: "none", 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))) }) }), _jsxs(View, { style: styles.inputBar, children: [_jsx(TouchableOpacity, { style: styles.inputIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.inputIconGlyph, children: "\u263A" }) }), _jsx(TextInput, { style: styles.textInput, value: chatInput, onChangeText: setChatInput, placeholder: joined ? 'Say something...' : 'Connecting...', placeholderTextColor: "rgba(255,255,255,0.35)", editable: joined && !joining && !error, onSubmitEditing: sendChat, returnKeyType: "send", blurOnSubmit: false }), _jsx(TouchableOpacity, { style: styles.inputIconBtn, activeOpacity: 0.7, children: _jsx(Text, { style: styles.inputIconGlyph, children: "\uD83C\uDF39" }) }), _jsx(TouchableOpacity, { style: [styles.sendBtn, (!chatInput.trim() || !joined) && styles.sendBtnOff], onPress: sendChat, activeOpacity: 0.75, disabled: !joined || !chatInput.trim(), children: _jsx(Text, { style: styles.sendIcon, children: "\u25B6" }) })] })] }), 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" })] }) }))] }));
|
|
224
204
|
});
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
225
206
|
const styles = StyleSheet.create({
|
|
226
207
|
container: {
|
|
227
208
|
flex: 1,
|
|
228
|
-
backgroundColor: '#
|
|
209
|
+
backgroundColor: '#000',
|
|
229
210
|
},
|
|
211
|
+
// ── Video ─────────────────────────────────────────────────────────────────
|
|
230
212
|
rtcView: {
|
|
231
|
-
|
|
232
|
-
backgroundColor: '#
|
|
213
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
214
|
+
backgroundColor: '#000',
|
|
233
215
|
},
|
|
234
216
|
videoPlaceholder: {
|
|
235
|
-
|
|
217
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
236
218
|
alignItems: 'center',
|
|
237
219
|
justifyContent: 'center',
|
|
238
|
-
backgroundColor: '#
|
|
220
|
+
backgroundColor: '#0d0d0d',
|
|
221
|
+
gap: 12,
|
|
239
222
|
},
|
|
240
|
-
|
|
241
|
-
color: '#e5e5e5',
|
|
242
|
-
fontSize: 16,
|
|
243
|
-
marginVertical: 4,
|
|
244
|
-
},
|
|
245
|
-
errorText: {
|
|
223
|
+
placeholderError: {
|
|
246
224
|
color: '#ef4444',
|
|
225
|
+
fontSize: 13,
|
|
226
|
+
textAlign: 'center',
|
|
227
|
+
paddingHorizontal: 28,
|
|
247
228
|
},
|
|
248
|
-
|
|
249
|
-
color: '
|
|
250
|
-
fontSize: 14,
|
|
251
|
-
fontWeight: '700',
|
|
252
|
-
letterSpacing: 1,
|
|
253
|
-
marginBottom: 8,
|
|
254
|
-
},
|
|
255
|
-
hintText: {
|
|
256
|
-
color: '#737373',
|
|
229
|
+
placeholderHint: {
|
|
230
|
+
color: 'rgba(255,255,255,0.38)',
|
|
257
231
|
fontSize: 12,
|
|
258
|
-
marginTop: 4,
|
|
259
232
|
textAlign: 'center',
|
|
233
|
+
paddingHorizontal: 28,
|
|
260
234
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
marginTop: 8,
|
|
264
|
-
paddingHorizontal: 16,
|
|
265
|
-
},
|
|
266
|
-
overlay: {
|
|
235
|
+
// ── Gradient overlays ─────────────────────────────────────────────────────
|
|
236
|
+
topGradient: {
|
|
267
237
|
position: 'absolute',
|
|
268
238
|
top: 0,
|
|
269
239
|
left: 0,
|
|
270
240
|
right: 0,
|
|
241
|
+
height: 140,
|
|
242
|
+
backgroundColor: 'rgba(0,0,0,0.45)',
|
|
243
|
+
},
|
|
244
|
+
bottomGradient: {
|
|
245
|
+
position: 'absolute',
|
|
271
246
|
bottom: 0,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
247
|
+
left: 0,
|
|
248
|
+
right: 0,
|
|
249
|
+
height: 260,
|
|
250
|
+
backgroundColor: 'rgba(0,0,0,0.38)',
|
|
275
251
|
},
|
|
276
|
-
|
|
277
|
-
|
|
252
|
+
// ── Top bar ───────────────────────────────────────────────────────────────
|
|
253
|
+
topBar: {
|
|
254
|
+
position: 'absolute',
|
|
255
|
+
top: 46,
|
|
256
|
+
left: 14,
|
|
257
|
+
right: 14,
|
|
258
|
+
flexDirection: 'row',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
justifyContent: 'space-between',
|
|
278
261
|
},
|
|
279
|
-
|
|
262
|
+
hostRow: {
|
|
280
263
|
flexDirection: 'row',
|
|
281
264
|
alignItems: 'center',
|
|
265
|
+
gap: 8,
|
|
266
|
+
flex: 1,
|
|
267
|
+
marginRight: 8,
|
|
268
|
+
overflow: 'hidden',
|
|
269
|
+
},
|
|
270
|
+
avatar: {
|
|
271
|
+
width: 36,
|
|
272
|
+
height: 36,
|
|
273
|
+
borderRadius: 18,
|
|
274
|
+
backgroundColor: 'rgba(255,255,255,0.15)',
|
|
275
|
+
borderWidth: 1.5,
|
|
276
|
+
borderColor: 'rgba(255,255,255,0.3)',
|
|
277
|
+
alignItems: 'center',
|
|
278
|
+
justifyContent: 'center',
|
|
279
|
+
flexShrink: 0,
|
|
280
|
+
},
|
|
281
|
+
avatarLetter: {
|
|
282
|
+
color: '#fff',
|
|
283
|
+
fontSize: 15,
|
|
284
|
+
fontWeight: '700',
|
|
282
285
|
},
|
|
283
|
-
|
|
284
|
-
color: '#
|
|
286
|
+
hostName: {
|
|
287
|
+
color: '#fff',
|
|
285
288
|
fontSize: 14,
|
|
286
289
|
fontWeight: '600',
|
|
290
|
+
flex: 1,
|
|
291
|
+
},
|
|
292
|
+
livePill: {
|
|
293
|
+
backgroundColor: '#ef4444',
|
|
294
|
+
borderRadius: 6,
|
|
295
|
+
paddingHorizontal: 7,
|
|
296
|
+
paddingVertical: 3,
|
|
297
|
+
flexShrink: 0,
|
|
298
|
+
},
|
|
299
|
+
livePillText: {
|
|
300
|
+
color: '#fff',
|
|
301
|
+
fontSize: 11,
|
|
302
|
+
fontWeight: '700',
|
|
303
|
+
letterSpacing: 0.8,
|
|
287
304
|
},
|
|
288
|
-
|
|
305
|
+
viewerChip: {
|
|
289
306
|
flexDirection: 'row',
|
|
290
307
|
alignItems: 'center',
|
|
291
|
-
|
|
308
|
+
backgroundColor: 'rgba(0,0,0,0.42)',
|
|
309
|
+
borderRadius: 20,
|
|
310
|
+
paddingHorizontal: 10,
|
|
311
|
+
paddingVertical: 5,
|
|
312
|
+
gap: 5,
|
|
313
|
+
flexShrink: 0,
|
|
292
314
|
},
|
|
293
|
-
|
|
294
|
-
|
|
315
|
+
viewerEye: { fontSize: 11 },
|
|
316
|
+
viewerCount: {
|
|
317
|
+
color: 'rgba(255,255,255,0.88)',
|
|
295
318
|
fontSize: 12,
|
|
319
|
+
fontWeight: '500',
|
|
296
320
|
},
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
},
|
|
300
|
-
chatPanel: {
|
|
321
|
+
// ── Right action column ───────────────────────────────────────────────────
|
|
322
|
+
rightColumn: {
|
|
301
323
|
position: 'absolute',
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
324
|
+
right: 12,
|
|
325
|
+
bottom: 92,
|
|
326
|
+
alignItems: 'center',
|
|
327
|
+
gap: 14,
|
|
305
328
|
},
|
|
306
|
-
|
|
307
|
-
|
|
329
|
+
actionBtn: {
|
|
330
|
+
alignItems: 'center',
|
|
331
|
+
gap: 5,
|
|
308
332
|
},
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
borderRadius:
|
|
333
|
+
actionCircle: {
|
|
334
|
+
width: 48,
|
|
335
|
+
height: 48,
|
|
336
|
+
borderRadius: 24,
|
|
337
|
+
backgroundColor: 'rgba(255,255,255,0.14)',
|
|
338
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
339
|
+
borderColor: 'rgba(255,255,255,0.2)',
|
|
340
|
+
alignItems: 'center',
|
|
341
|
+
justifyContent: 'center',
|
|
342
|
+
},
|
|
343
|
+
actionIcon: {
|
|
344
|
+
fontSize: 22,
|
|
345
|
+
color: '#fff',
|
|
346
|
+
},
|
|
347
|
+
actionLabel: {
|
|
348
|
+
color: 'rgba(255,255,255,0.7)',
|
|
349
|
+
fontSize: 11,
|
|
350
|
+
fontWeight: '500',
|
|
351
|
+
},
|
|
352
|
+
// ── Bottom panel ──────────────────────────────────────────────────────────
|
|
353
|
+
bottomPanel: {
|
|
354
|
+
position: 'absolute',
|
|
355
|
+
left: 0,
|
|
356
|
+
right: 0,
|
|
313
357
|
paddingHorizontal: 12,
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
358
|
+
paddingBottom: BOTTOM_SAFE,
|
|
359
|
+
gap: 8,
|
|
360
|
+
},
|
|
361
|
+
// Chat messages
|
|
362
|
+
chatColumn: {
|
|
363
|
+
width: '66%',
|
|
364
|
+
maxHeight: Math.round(SCREEN_HEIGHT * 0.30),
|
|
365
|
+
},
|
|
366
|
+
chatContent: {
|
|
367
|
+
paddingBottom: 2,
|
|
368
|
+
},
|
|
369
|
+
chatBubble: {
|
|
321
370
|
alignSelf: 'flex-start',
|
|
322
|
-
|
|
323
|
-
|
|
371
|
+
marginBottom: 5,
|
|
372
|
+
backgroundColor: 'rgba(0,0,0,0.40)',
|
|
373
|
+
borderRadius: 14,
|
|
374
|
+
paddingHorizontal: 10,
|
|
375
|
+
paddingVertical: 5,
|
|
376
|
+
maxWidth: '100%',
|
|
324
377
|
},
|
|
325
|
-
|
|
326
|
-
|
|
378
|
+
joinBubble: {
|
|
379
|
+
alignSelf: 'flex-start',
|
|
380
|
+
marginBottom: 5,
|
|
381
|
+
backgroundColor: 'rgba(255,255,255,0.08)',
|
|
382
|
+
borderRadius: 14,
|
|
383
|
+
paddingHorizontal: 10,
|
|
384
|
+
paddingVertical: 4,
|
|
385
|
+
maxWidth: '100%',
|
|
327
386
|
},
|
|
328
387
|
chatLine: {
|
|
329
|
-
color: '#e5e5e5',
|
|
330
388
|
fontSize: 13,
|
|
331
389
|
lineHeight: 18,
|
|
332
|
-
marginBottom: 4,
|
|
333
390
|
},
|
|
334
|
-
|
|
335
|
-
color: '#fafafa',
|
|
391
|
+
chatUsername: {
|
|
336
392
|
fontWeight: '700',
|
|
337
393
|
},
|
|
338
|
-
|
|
394
|
+
chatMsg: {
|
|
339
395
|
color: '#e5e5e5',
|
|
340
396
|
fontWeight: '400',
|
|
341
397
|
},
|
|
342
|
-
|
|
343
|
-
|
|
398
|
+
joinText: {
|
|
399
|
+
color: 'rgba(255,255,255,0.48)',
|
|
400
|
+
fontSize: 12,
|
|
401
|
+
fontStyle: 'italic',
|
|
402
|
+
},
|
|
403
|
+
// Input bar
|
|
404
|
+
inputBar: {
|
|
344
405
|
flexDirection: 'row',
|
|
345
406
|
alignItems: 'center',
|
|
407
|
+
backgroundColor: 'rgba(18,18,18,0.75)',
|
|
346
408
|
borderRadius: 999,
|
|
347
|
-
|
|
348
|
-
borderWidth: 1,
|
|
409
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
349
410
|
borderColor: 'rgba(255,255,255,0.12)',
|
|
350
|
-
|
|
411
|
+
paddingHorizontal: 4,
|
|
412
|
+
paddingVertical: 4,
|
|
413
|
+
gap: 2,
|
|
414
|
+
},
|
|
415
|
+
inputIconBtn: {
|
|
416
|
+
width: 40,
|
|
417
|
+
height: 40,
|
|
418
|
+
alignItems: 'center',
|
|
419
|
+
justifyContent: 'center',
|
|
420
|
+
borderRadius: 20,
|
|
351
421
|
},
|
|
352
|
-
|
|
422
|
+
inputIconGlyph: {
|
|
423
|
+
fontSize: 21,
|
|
424
|
+
},
|
|
425
|
+
textInput: {
|
|
353
426
|
flex: 1,
|
|
354
|
-
|
|
355
|
-
paddingVertical: 10,
|
|
356
|
-
color: '#fafafa',
|
|
427
|
+
color: '#fff',
|
|
357
428
|
fontSize: 14,
|
|
429
|
+
paddingHorizontal: 6,
|
|
430
|
+
paddingVertical: Platform.OS === 'ios' ? 10 : 6,
|
|
431
|
+
minHeight: 40,
|
|
432
|
+
},
|
|
433
|
+
sendBtn: {
|
|
434
|
+
width: 40,
|
|
435
|
+
height: 40,
|
|
436
|
+
borderRadius: 20,
|
|
437
|
+
backgroundColor: '#f97316',
|
|
438
|
+
alignItems: 'center',
|
|
439
|
+
justifyContent: 'center',
|
|
358
440
|
},
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
paddingVertical: 10,
|
|
441
|
+
sendBtnOff: {
|
|
442
|
+
backgroundColor: 'rgba(255,255,255,0.1)',
|
|
362
443
|
},
|
|
363
|
-
|
|
364
|
-
|
|
444
|
+
sendIcon: {
|
|
445
|
+
color: '#fff',
|
|
446
|
+
fontSize: 15,
|
|
447
|
+
fontWeight: '700',
|
|
365
448
|
},
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
449
|
+
// ── Stream ended ──────────────────────────────────────────────────────────
|
|
450
|
+
endedOverlay: {
|
|
451
|
+
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
|
|
452
|
+
backgroundColor: 'rgba(0,0,0,0.80)',
|
|
453
|
+
alignItems: 'center',
|
|
454
|
+
justifyContent: 'center',
|
|
455
|
+
},
|
|
456
|
+
endedCard: {
|
|
457
|
+
alignItems: 'center',
|
|
458
|
+
gap: 10,
|
|
459
|
+
paddingHorizontal: 36,
|
|
460
|
+
},
|
|
461
|
+
endedTitle: {
|
|
462
|
+
color: '#fff',
|
|
463
|
+
fontSize: 22,
|
|
369
464
|
fontWeight: '700',
|
|
370
465
|
},
|
|
466
|
+
endedSub: {
|
|
467
|
+
color: 'rgba(255,255,255,0.5)',
|
|
468
|
+
fontSize: 14,
|
|
469
|
+
textAlign: 'center',
|
|
470
|
+
lineHeight: 20,
|
|
471
|
+
},
|
|
371
472
|
});
|
|
@@ -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;
|
|
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