kasunk99-livestream-core 0.2.2 → 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
- * 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,
@@ -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.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" }) })] })] }) }) })] })] }));
@@ -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,mBAAmB,GAAG,SAAS,GAAG;IAC5C,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,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,CAkjBrF"}
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,mBAAmB,GAAG,SAAS,GAAG;IAC5C,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,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,CAsjBrF"}
@@ -279,12 +279,13 @@ export function useHostSocket(options = {}) {
279
279
  return; // fullCleanup already ran, bail
280
280
  const screenStreamToStop = hostSession.screenStream;
281
281
  hostSession.screenStream = null;
282
+ // release() → track.release() → mediaStreamTrackRelease → TrackPrivate.dispose()
283
+ // → stopCapture() (returns true) + dispose() → videoCapturer.dispose()
284
+ // → ScreenCapturerAndroid.dispose() → mediaProjection.stop(). This is the only
285
+ // path that properly stops the MediaProjection; t.stop() only calls stopCapture()
286
+ // which is now a no-op to avoid blocking the executor.
282
287
  try {
283
- screenStreamToStop.getTracks()
284
- .forEach((t) => { try {
285
- t.stop?.();
286
- }
287
- catch { /* ignore */ } });
288
+ screenStreamToStop.release?.();
288
289
  }
289
290
  catch { /* ignore */ }
290
291
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasunk99-livestream-core",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",