kasunk99-livestream-core 0.2.3 → 0.2.4
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
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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:
|
|
42
|
-
offset:
|
|
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,
|
|
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
|
-
|
|
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,
|
|
@@ -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: "
|
|
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.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" }) })] })] }) }) })] })] }));
|
package/package.json
CHANGED