@stream-io/video-react-native-sdk 1.13.3 → 1.14.0

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.
Files changed (124) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/android/gradle.properties +1 -1
  3. package/dist/commonjs/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.js +23 -29
  4. package/dist/commonjs/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.js.map +1 -1
  5. package/dist/commonjs/components/Livestream/LivestreamControls/ViewerLivestreamControls.js +187 -29
  6. package/dist/commonjs/components/Livestream/LivestreamControls/ViewerLivestreamControls.js.map +1 -1
  7. package/dist/commonjs/components/Livestream/LivestreamLayout/LivestreamLayout.js +1 -1
  8. package/dist/commonjs/components/Livestream/LivestreamLayout/LivestreamLayout.js.map +1 -1
  9. package/dist/commonjs/components/Livestream/LivestreamPlayer/LivestreamEnded.js +111 -0
  10. package/dist/commonjs/components/Livestream/LivestreamPlayer/LivestreamEnded.js.map +1 -0
  11. package/dist/commonjs/components/Livestream/LivestreamPlayer/LivestreamPlayer.js +5 -6
  12. package/dist/commonjs/components/Livestream/LivestreamPlayer/LivestreamPlayer.js.map +1 -1
  13. package/dist/commonjs/components/Livestream/LivestreamTopView/DurationBadge.js +32 -28
  14. package/dist/commonjs/components/Livestream/LivestreamTopView/DurationBadge.js.map +1 -1
  15. package/dist/commonjs/components/Livestream/LivestreamTopView/FollowerCount.js +36 -36
  16. package/dist/commonjs/components/Livestream/LivestreamTopView/FollowerCount.js.map +1 -1
  17. package/dist/commonjs/components/Livestream/LivestreamTopView/LiveIndicator.js +21 -15
  18. package/dist/commonjs/components/Livestream/LivestreamTopView/LiveIndicator.js.map +1 -1
  19. package/dist/commonjs/components/Livestream/ViewerLivestream/ViewerLivestream.js +70 -4
  20. package/dist/commonjs/components/Livestream/ViewerLivestream/ViewerLivestream.js.map +1 -1
  21. package/dist/commonjs/components/Livestream/ViewerLivestream/ViewerLobby.js +143 -0
  22. package/dist/commonjs/components/Livestream/ViewerLivestream/ViewerLobby.js.map +1 -0
  23. package/dist/commonjs/icons/LivestreamControls.js +73 -0
  24. package/dist/commonjs/icons/LivestreamControls.js.map +1 -0
  25. package/dist/commonjs/icons/Maximize.js +52 -0
  26. package/dist/commonjs/icons/Maximize.js.map +1 -0
  27. package/dist/commonjs/icons/index.js +11 -0
  28. package/dist/commonjs/icons/index.js.map +1 -1
  29. package/dist/commonjs/index.js +12 -0
  30. package/dist/commonjs/index.js.map +1 -1
  31. package/dist/commonjs/providers/NoiseCancellation/NoiseCancellationProvider.js +75 -0
  32. package/dist/commonjs/providers/NoiseCancellation/NoiseCancellationProvider.js.map +1 -0
  33. package/dist/commonjs/providers/NoiseCancellation/index.js +17 -0
  34. package/dist/commonjs/providers/NoiseCancellation/index.js.map +1 -0
  35. package/dist/commonjs/providers/NoiseCancellation/lib.js +34 -0
  36. package/dist/commonjs/providers/NoiseCancellation/lib.js.map +1 -0
  37. package/dist/commonjs/version.js +1 -1
  38. package/dist/module/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.js +27 -33
  39. package/dist/module/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.js.map +1 -1
  40. package/dist/module/components/Livestream/LivestreamControls/ViewerLivestreamControls.js +187 -30
  41. package/dist/module/components/Livestream/LivestreamControls/ViewerLivestreamControls.js.map +1 -1
  42. package/dist/module/components/Livestream/LivestreamLayout/LivestreamLayout.js +1 -1
  43. package/dist/module/components/Livestream/LivestreamLayout/LivestreamLayout.js.map +1 -1
  44. package/dist/module/components/Livestream/LivestreamPlayer/LivestreamEnded.js +104 -0
  45. package/dist/module/components/Livestream/LivestreamPlayer/LivestreamEnded.js.map +1 -0
  46. package/dist/module/components/Livestream/LivestreamPlayer/LivestreamPlayer.js +5 -6
  47. package/dist/module/components/Livestream/LivestreamPlayer/LivestreamPlayer.js.map +1 -1
  48. package/dist/module/components/Livestream/LivestreamTopView/DurationBadge.js +33 -29
  49. package/dist/module/components/Livestream/LivestreamTopView/DurationBadge.js.map +1 -1
  50. package/dist/module/components/Livestream/LivestreamTopView/FollowerCount.js +35 -35
  51. package/dist/module/components/Livestream/LivestreamTopView/FollowerCount.js.map +1 -1
  52. package/dist/module/components/Livestream/LivestreamTopView/LiveIndicator.js +20 -14
  53. package/dist/module/components/Livestream/LivestreamTopView/LiveIndicator.js.map +1 -1
  54. package/dist/module/components/Livestream/ViewerLivestream/ViewerLivestream.js +73 -7
  55. package/dist/module/components/Livestream/ViewerLivestream/ViewerLivestream.js.map +1 -1
  56. package/dist/module/components/Livestream/ViewerLivestream/ViewerLobby.js +136 -0
  57. package/dist/module/components/Livestream/ViewerLivestream/ViewerLobby.js.map +1 -0
  58. package/dist/module/icons/LivestreamControls.js +62 -0
  59. package/dist/module/icons/LivestreamControls.js.map +1 -0
  60. package/dist/module/icons/Maximize.js +43 -0
  61. package/dist/module/icons/Maximize.js.map +1 -0
  62. package/dist/module/icons/index.js +1 -0
  63. package/dist/module/icons/index.js.map +1 -1
  64. package/dist/module/index.js +1 -0
  65. package/dist/module/index.js.map +1 -1
  66. package/dist/module/providers/NoiseCancellation/NoiseCancellationProvider.js +67 -0
  67. package/dist/module/providers/NoiseCancellation/NoiseCancellationProvider.js.map +1 -0
  68. package/dist/module/providers/NoiseCancellation/index.js +2 -0
  69. package/dist/module/providers/NoiseCancellation/index.js.map +1 -0
  70. package/dist/module/providers/NoiseCancellation/lib.js +26 -0
  71. package/dist/module/providers/NoiseCancellation/lib.js.map +1 -0
  72. package/dist/module/version.js +1 -1
  73. package/dist/typescript/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.d.ts.map +1 -1
  74. package/dist/typescript/components/Livestream/LivestreamControls/ViewerLivestreamControls.d.ts +7 -0
  75. package/dist/typescript/components/Livestream/LivestreamControls/ViewerLivestreamControls.d.ts.map +1 -1
  76. package/dist/typescript/components/Livestream/LivestreamPlayer/LivestreamEnded.d.ts +3 -0
  77. package/dist/typescript/components/Livestream/LivestreamPlayer/LivestreamEnded.d.ts.map +1 -0
  78. package/dist/typescript/components/Livestream/LivestreamPlayer/LivestreamPlayer.d.ts +13 -1
  79. package/dist/typescript/components/Livestream/LivestreamPlayer/LivestreamPlayer.d.ts.map +1 -1
  80. package/dist/typescript/components/Livestream/LivestreamTopView/DurationBadge.d.ts.map +1 -1
  81. package/dist/typescript/components/Livestream/LivestreamTopView/FollowerCount.d.ts.map +1 -1
  82. package/dist/typescript/components/Livestream/LivestreamTopView/LiveIndicator.d.ts.map +1 -1
  83. package/dist/typescript/components/Livestream/ViewerLivestream/ViewerLivestream.d.ts +9 -1
  84. package/dist/typescript/components/Livestream/ViewerLivestream/ViewerLivestream.d.ts.map +1 -1
  85. package/dist/typescript/components/Livestream/ViewerLivestream/ViewerLobby.d.ts +8 -0
  86. package/dist/typescript/components/Livestream/ViewerLivestream/ViewerLobby.d.ts.map +1 -0
  87. package/dist/typescript/icons/LivestreamControls.d.ts +12 -0
  88. package/dist/typescript/icons/LivestreamControls.d.ts.map +1 -0
  89. package/dist/typescript/icons/Maximize.d.ts +10 -0
  90. package/dist/typescript/icons/Maximize.d.ts.map +1 -0
  91. package/dist/typescript/icons/index.d.ts +1 -0
  92. package/dist/typescript/icons/index.d.ts.map +1 -1
  93. package/dist/typescript/index.d.ts +1 -0
  94. package/dist/typescript/index.d.ts.map +1 -1
  95. package/dist/typescript/providers/NoiseCancellation/NoiseCancellationProvider.d.ts +34 -0
  96. package/dist/typescript/providers/NoiseCancellation/NoiseCancellationProvider.d.ts.map +1 -0
  97. package/dist/typescript/providers/NoiseCancellation/index.d.ts +2 -0
  98. package/dist/typescript/providers/NoiseCancellation/index.d.ts.map +1 -0
  99. package/dist/typescript/providers/NoiseCancellation/lib.d.ts +8 -0
  100. package/dist/typescript/providers/NoiseCancellation/lib.d.ts.map +1 -0
  101. package/dist/typescript/version.d.ts +1 -1
  102. package/expo-config-plugin/dist/index.js +2 -0
  103. package/expo-config-plugin/dist/withAndroidPermissions.js +1 -0
  104. package/expo-config-plugin/dist/withAppDelegate.js +26 -7
  105. package/expo-config-plugin/dist/withMainApplication.js +24 -0
  106. package/package.json +10 -5
  107. package/src/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.tsx +30 -48
  108. package/src/components/Livestream/LivestreamControls/ViewerLivestreamControls.tsx +260 -43
  109. package/src/components/Livestream/LivestreamLayout/LivestreamLayout.tsx +1 -1
  110. package/src/components/Livestream/LivestreamPlayer/LivestreamEnded.tsx +130 -0
  111. package/src/components/Livestream/LivestreamPlayer/LivestreamPlayer.tsx +15 -5
  112. package/src/components/Livestream/LivestreamTopView/DurationBadge.tsx +35 -38
  113. package/src/components/Livestream/LivestreamTopView/FollowerCount.tsx +40 -47
  114. package/src/components/Livestream/LivestreamTopView/LiveIndicator.tsx +22 -14
  115. package/src/components/Livestream/ViewerLivestream/ViewerLivestream.tsx +107 -10
  116. package/src/components/Livestream/ViewerLivestream/ViewerLobby.tsx +171 -0
  117. package/src/icons/LivestreamControls.tsx +51 -0
  118. package/src/icons/Maximize.tsx +48 -0
  119. package/src/icons/index.tsx +1 -0
  120. package/src/index.ts +1 -0
  121. package/src/providers/NoiseCancellation/NoiseCancellationProvider.tsx +147 -0
  122. package/src/providers/NoiseCancellation/index.ts +1 -0
  123. package/src/providers/NoiseCancellation/lib.ts +37 -0
  124. package/src/version.ts +1 -1
@@ -0,0 +1,130 @@
1
+ import React from 'react';
2
+ import { useCall, useI18n } from '@stream-io/video-react-bindings';
3
+ import { useState, useEffect, useMemo } from 'react';
4
+ import { useTheme } from '../../../contexts';
5
+ import { ListRecordingsResponse } from '@stream-io/video-client';
6
+ import {
7
+ FlatList,
8
+ Pressable,
9
+ View,
10
+ Text,
11
+ StyleSheet,
12
+ Linking,
13
+ } from 'react-native';
14
+
15
+ export const CallEndedView = () => {
16
+ const { t } = useI18n();
17
+ const call = useCall();
18
+ const [recordingsResponse, setRecordingsResponse] = useState<
19
+ ListRecordingsResponse | undefined
20
+ >(undefined);
21
+
22
+ const styles = useStyles();
23
+
24
+ useEffect(() => {
25
+ let isCanceled = false;
26
+ const fetchRecordings = async () => {
27
+ if (recordingsResponse == null) {
28
+ try {
29
+ const callRecordingsResponse = await call?.queryRecordings();
30
+ if (!isCanceled) {
31
+ setRecordingsResponse(callRecordingsResponse);
32
+ }
33
+ } catch (error) {
34
+ console.log('Error fetching recordings:', error);
35
+ if (!isCanceled) {
36
+ setRecordingsResponse(undefined);
37
+ }
38
+ }
39
+ }
40
+ };
41
+
42
+ fetchRecordings();
43
+
44
+ return () => {
45
+ isCanceled = true;
46
+ };
47
+ }, [call, recordingsResponse]);
48
+
49
+ const openUrl = (url: string) => {
50
+ Linking.canOpenURL(url).then((supported) => {
51
+ if (supported) {
52
+ Linking.openURL(url);
53
+ } else {
54
+ console.log('Cannot open URL:', url);
55
+ }
56
+ });
57
+ };
58
+
59
+ const showRecordings =
60
+ recordingsResponse && recordingsResponse.recordings.length > 0;
61
+
62
+ return (
63
+ <View style={styles.container}>
64
+ <Text style={styles.title}>{t('The livestream has ended.')}</Text>
65
+
66
+ {showRecordings && (
67
+ <>
68
+ <Text style={styles.subtitle}>{t('Watch recordings:')}</Text>
69
+ <View style={styles.recordingsContainer}>
70
+ <FlatList
71
+ data={recordingsResponse.recordings}
72
+ keyExtractor={(item) => item.session_id}
73
+ renderItem={({ item }) => (
74
+ <Pressable
75
+ style={styles.recordingButton}
76
+ onPress={() => openUrl(item.url)}
77
+ >
78
+ <Text style={styles.recordingText}>
79
+ {item.url.substring(0, 70)}...
80
+ </Text>
81
+ </Pressable>
82
+ )}
83
+ />
84
+ </View>
85
+ </>
86
+ )}
87
+ </View>
88
+ );
89
+ };
90
+
91
+ const useStyles = () => {
92
+ const { theme } = useTheme();
93
+ return useMemo(
94
+ () =>
95
+ StyleSheet.create({
96
+ container: {
97
+ flex: 1,
98
+ backgroundColor: theme.colors.sheetPrimary,
99
+ justifyContent: 'center',
100
+ alignItems: 'center',
101
+ padding: theme.variants.spacingSizes.md,
102
+ },
103
+ title: {
104
+ fontSize: theme.variants.fontSizes.lg,
105
+ marginBottom: theme.variants.spacingSizes.md,
106
+ color: theme.colors.textPrimary,
107
+ fontWeight: 'bold',
108
+ },
109
+ subtitle: {
110
+ fontSize: theme.variants.fontSizes.md,
111
+ marginBottom: theme.variants.spacingSizes.md,
112
+ color: theme.colors.textPrimary,
113
+ fontWeight: 'bold',
114
+ },
115
+ recordingButton: {
116
+ padding: theme.variants.spacingSizes.sm,
117
+ width: '100%',
118
+ },
119
+ recordingText: {
120
+ color: theme.colors.textSecondary,
121
+ fontSize: theme.variants.fontSizes.md,
122
+ },
123
+ recordingsContainer: {
124
+ width: '100%',
125
+ alignItems: 'center',
126
+ },
127
+ }),
128
+ [theme],
129
+ );
130
+ };
@@ -21,12 +21,26 @@ export type LivestreamPlayerProps = {
21
21
  * **Default** [ViewerLivestream](https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/src/components/Livestream/ViewerLivestream/ViewerLivestream.tsx)
22
22
  */
23
23
  ViewerLivestream?: React.ComponentType<ViewerLivestreamProps>;
24
+
25
+ /**
26
+ * Determines when the viewer joins the call.
27
+ *
28
+ * `"asap"` behavior means joining the call as soon as it is possible
29
+ * (either the `join_ahead_time_seconds` setting allows it, or the user
30
+ * has a the capability to join backstage).
31
+ *
32
+ * `"live"` behavior means joining the call when it goes live.
33
+ *
34
+ * @default "asap"
35
+ */
36
+ joinBehavior?: 'asap' | 'live';
24
37
  };
25
38
 
26
39
  export const LivestreamPlayer = ({
27
40
  callType,
28
41
  callId,
29
42
  ViewerLivestream = DefaultViewerLivestream,
43
+ joinBehavior = 'asap',
30
44
  }: LivestreamPlayerProps) => {
31
45
  const client = useStreamVideoClient();
32
46
 
@@ -38,10 +52,6 @@ export const LivestreamPlayer = ({
38
52
  }
39
53
  const myCall = client.call(callType, callId);
40
54
  setCall(myCall);
41
- myCall.join().catch((e) => {
42
- const logger = getLogger(['LivestreamPlayer']);
43
- logger('error', 'Error joining call:', e);
44
- });
45
55
  return () => {
46
56
  if (myCall.state.callingState !== CallingState.LEFT) {
47
57
  myCall.leave().catch((e) => {
@@ -71,7 +81,7 @@ export const LivestreamPlayer = ({
71
81
 
72
82
  return (
73
83
  <StreamCall call={call}>
74
- <ViewerLivestream />
84
+ <ViewerLivestream joinBehavior={joinBehavior} />
75
85
  </StreamCall>
76
86
  );
77
87
  };
@@ -1,7 +1,6 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import { StyleSheet, Text, View } from 'react-native';
3
3
  import { useTheme } from '../../../contexts';
4
- import { ShieldBadge } from '../../../icons';
5
4
  import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
6
5
  import {
7
6
  type CallSessionResponse,
@@ -19,6 +18,7 @@ export type DurationBadgeProps = {
19
18
  * The HostDurationBadge component displays the duration while the live stream is active.
20
19
  */
21
20
  export const DurationBadge = ({ mode }: DurationBadgeProps) => {
21
+ const styles = useStyles();
22
22
  const { useCallSession } = useCallStateHooks();
23
23
  const session = useCallSession();
24
24
 
@@ -33,11 +33,7 @@ export const DurationBadge = ({ mode }: DurationBadgeProps) => {
33
33
 
34
34
  const call = useCall();
35
35
  const {
36
- theme: {
37
- colors,
38
- variants: { iconSizes },
39
- durationBadge,
40
- },
36
+ theme: { colors, durationBadge },
41
37
  } = useTheme();
42
38
 
43
39
  // for host
@@ -127,18 +123,7 @@ export const DurationBadge = ({ mode }: DurationBadgeProps) => {
127
123
  durationBadge.container,
128
124
  ]}
129
125
  >
130
- <View
131
- style={[
132
- styles.icon,
133
- {
134
- height: iconSizes.xs,
135
- width: iconSizes.xs,
136
- },
137
- durationBadge.icon,
138
- ]}
139
- >
140
- <ShieldBadge />
141
- </View>
126
+ <View style={[styles.dot, durationBadge.icon]} />
142
127
  <Text
143
128
  style={[
144
129
  styles.label,
@@ -152,22 +137,34 @@ export const DurationBadge = ({ mode }: DurationBadgeProps) => {
152
137
  );
153
138
  };
154
139
 
155
- const styles = StyleSheet.create({
156
- container: {
157
- paddingHorizontal: 8,
158
- paddingVertical: 4,
159
- borderRadius: 4,
160
- flexDirection: 'row',
161
- alignItems: 'center',
162
- justifyContent: 'center',
163
- },
164
- icon: {},
165
- label: {
166
- textAlign: 'center',
167
- fontSize: 13,
168
- fontWeight: '400',
169
- flexShrink: 1,
170
- includeFontPadding: false,
171
- paddingLeft: 4,
172
- },
173
- });
140
+ const useStyles = () => {
141
+ const { theme } = useTheme();
142
+ return useMemo(
143
+ () =>
144
+ StyleSheet.create({
145
+ container: {
146
+ paddingHorizontal: theme.variants.spacingSizes.sm,
147
+ paddingVertical: theme.variants.spacingSizes.sm,
148
+ borderRadius: theme.variants.borderRadiusSizes.sm,
149
+ flexDirection: 'row',
150
+ alignItems: 'center',
151
+ justifyContent: 'center',
152
+ },
153
+ dot: {
154
+ backgroundColor: theme.colors.iconWarning,
155
+ marginRight: theme.variants.spacingSizes.xs,
156
+ borderRadius: 90,
157
+ height: 10,
158
+ width: 10,
159
+ },
160
+ label: {
161
+ textAlign: 'center',
162
+ fontSize: theme.variants.fontSizes.md,
163
+ fontWeight: '600',
164
+ flexShrink: 1,
165
+ paddingLeft: theme.variants.spacingSizes.xs,
166
+ },
167
+ }),
168
+ [theme],
169
+ );
170
+ };
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { StyleSheet, Text, View } from 'react-native';
3
3
  import { useTheme } from '../../../contexts';
4
4
  import { useCallStateHooks } from '@stream-io/video-react-bindings';
@@ -13,62 +13,55 @@ export type FollowerCountProps = {};
13
13
  * The FollowerCount component that displays the number of participants while in the call.
14
14
  */
15
15
  export const FollowerCount = ({}: FollowerCountProps) => {
16
+ const styles = useStyles();
16
17
  const {
17
- theme: {
18
- colors,
19
- variants: { iconSizes },
20
- followerCount,
21
- },
18
+ theme: { followerCount },
22
19
  } = useTheme();
20
+
23
21
  const { useParticipantCount } = useCallStateHooks();
24
22
  const totalParticipants = useParticipantCount();
23
+
25
24
  return (
26
- <View
27
- style={[
28
- styles.container,
29
- { backgroundColor: colors.sheetTertiary },
30
- followerCount.container,
31
- ]}
32
- >
33
- <View
34
- style={[
35
- styles.icon,
36
- { height: iconSizes.xs, width: iconSizes.xs },
37
- followerCount.icon,
38
- ]}
39
- >
25
+ <View style={[styles.container, followerCount.container]}>
26
+ <View style={[styles.icon, followerCount.icon]}>
40
27
  <Eye />
41
28
  </View>
42
- <Text
43
- style={[
44
- styles.label,
45
- { color: colors.textPrimary },
46
- followerCount.label,
47
- ]}
48
- >
29
+ <Text style={[styles.label, followerCount.label]}>
49
30
  {totalParticipants}
50
31
  </Text>
51
32
  </View>
52
33
  );
53
34
  };
54
35
 
55
- const styles = StyleSheet.create({
56
- container: {
57
- paddingHorizontal: 8,
58
- paddingVertical: 4,
59
- borderTopRightRadius: 4,
60
- borderBottomRightRadius: 4,
61
- flexDirection: 'row',
62
- alignItems: 'center',
63
- justifyContent: 'center',
64
- },
65
- icon: {},
66
- label: {
67
- fontSize: 13,
68
- fontWeight: '400',
69
- flexShrink: 1,
70
- textAlign: 'center',
71
- includeFontPadding: false,
72
- marginLeft: 4,
73
- },
74
- });
36
+ const useStyles = () => {
37
+ const { theme } = useTheme();
38
+ return useMemo(
39
+ () =>
40
+ StyleSheet.create({
41
+ container: {
42
+ paddingHorizontal: theme.variants.spacingSizes.sm,
43
+ paddingVertical: 4,
44
+ borderTopRightRadius: 4,
45
+ borderBottomRightRadius: 4,
46
+ flexDirection: 'row',
47
+ alignItems: 'center',
48
+ justifyContent: 'center',
49
+ backgroundColor: theme.colors.sheetTertiary,
50
+ },
51
+ icon: {
52
+ height: theme.variants.iconSizes.sm,
53
+ width: theme.variants.iconSizes.sm,
54
+ },
55
+ label: {
56
+ fontSize: theme.variants.fontSizes.md,
57
+ fontWeight: '600',
58
+ flexShrink: 1,
59
+ textAlign: 'center',
60
+ includeFontPadding: false,
61
+ marginLeft: theme.variants.spacingSizes.xs,
62
+ color: theme.colors.textPrimary,
63
+ },
64
+ }),
65
+ [theme],
66
+ );
67
+ };
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { StyleSheet, Text, View } from 'react-native';
3
3
  import { useTheme } from '../../../contexts';
4
4
  import { useI18n } from '@stream-io/video-react-bindings';
@@ -12,6 +12,7 @@ export type LiveIndicatorProps = {};
12
12
  * The LiveIndicator component displays whether the live stream is live or not.
13
13
  */
14
14
  export const LiveIndicator = ({}: LiveIndicatorProps) => {
15
+ const styles = useStyles();
15
16
  const {
16
17
  theme: { colors, typefaces, liveIndicator },
17
18
  } = useTheme();
@@ -38,16 +39,23 @@ export const LiveIndicator = ({}: LiveIndicatorProps) => {
38
39
  );
39
40
  };
40
41
 
41
- const styles = StyleSheet.create({
42
- container: {
43
- paddingHorizontal: 8,
44
- paddingVertical: 4,
45
- borderTopLeftRadius: 4,
46
- borderBottomLeftRadius: 4,
47
- justifyContent: 'center',
48
- },
49
- label: {
50
- textAlign: 'center',
51
- includeFontPadding: false,
52
- },
53
- });
42
+ const useStyles = () => {
43
+ const { theme } = useTheme();
44
+ return useMemo(
45
+ () =>
46
+ StyleSheet.create({
47
+ container: {
48
+ paddingHorizontal: theme.variants.spacingSizes.sm,
49
+ paddingVertical: theme.variants.spacingSizes.sm,
50
+ borderTopLeftRadius: theme.variants.borderRadiusSizes.sm,
51
+ borderBottomLeftRadius: theme.variants.borderRadiusSizes.sm,
52
+ justifyContent: 'center',
53
+ },
54
+ label: {
55
+ textAlign: 'center',
56
+ includeFontPadding: false,
57
+ },
58
+ }),
59
+ [theme],
60
+ );
61
+ };
@@ -1,12 +1,8 @@
1
- import React, { useEffect, useMemo } from 'react';
2
-
1
+ import React, { useEffect, useMemo, useState } from 'react';
3
2
  import { StyleSheet, View } from 'react-native';
4
3
  import InCallManager from 'react-native-incall-manager';
5
4
  import { useTheme } from '../../../contexts';
6
- import {
7
- ViewerLivestreamTopView as DefaultViewerLivestreamTopView,
8
- type ViewerLivestreamTopViewProps,
9
- } from '../LivestreamTopView/ViewerLivestreamTopView';
5
+ import { type ViewerLivestreamTopViewProps } from '../LivestreamTopView/ViewerLivestreamTopView';
10
6
  import {
11
7
  ViewerLivestreamControls as DefaultViewerLivestreamControls,
12
8
  type ViewerLivestreamControlsProps,
@@ -16,12 +12,14 @@ import {
16
12
  LivestreamLayout as DefaultLivestreamLayout,
17
13
  type LivestreamLayoutProps,
18
14
  } from '../LivestreamLayout';
19
- import { useCallStateHooks } from '@stream-io/video-react-bindings';
15
+ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
20
16
  import {
21
17
  FloatingParticipantView as DefaultFloatingParticipantView,
22
18
  type FloatingParticipantViewProps,
23
19
  } from '../../Participant';
24
- import { hasVideo } from '@stream-io/video-client';
20
+ import { CallingState, hasVideo } from '@stream-io/video-client';
21
+ import { CallEndedView } from '../LivestreamPlayer/LivestreamEnded';
22
+ import { ViewerLobby } from './ViewerLobby';
25
23
 
26
24
  /**
27
25
  * Props for the ViewerLivestream component.
@@ -45,13 +43,21 @@ export type ViewerLivestreamProps = ViewerLivestreamTopViewProps &
45
43
  * Component to customize the FloatingParticipantView when screen is shared.
46
44
  */
47
45
  FloatingParticipantView?: React.ComponentType<FloatingParticipantViewProps> | null;
46
+ /**
47
+ * Determines when the viewer joins the call.
48
+ *
49
+ * `"asap"` behavior means joining the call as soon as it is possible
50
+ * (either the `join_ahead_time_seconds` setting allows it, or the user
51
+ * has a the capability to join backstage).
52
+ */
53
+ joinBehavior?: 'asap' | 'live';
48
54
  };
49
55
 
50
56
  /**
51
57
  * The ViewerLivestream component renders the UI for the Viewer's live stream.
52
58
  */
53
59
  export const ViewerLivestream = ({
54
- ViewerLivestreamTopView = DefaultViewerLivestreamTopView,
60
+ ViewerLivestreamTopView,
55
61
  ViewerLivestreamControls = DefaultViewerLivestreamControls,
56
62
  LivestreamLayout = DefaultLivestreamLayout,
57
63
  FloatingParticipantView = DefaultFloatingParticipantView,
@@ -60,12 +66,24 @@ export const ViewerLivestream = ({
60
66
  DurationBadge,
61
67
  ViewerLeaveStreamButton,
62
68
  onLeaveStreamHandler,
69
+ joinBehavior,
63
70
  }: ViewerLivestreamProps) => {
64
71
  const styles = useStyles();
72
+ const call = useCall();
65
73
  const {
66
74
  theme: { viewerLivestream },
67
75
  } = useTheme();
68
- const { useHasOngoingScreenShare, useParticipants } = useCallStateHooks();
76
+ const {
77
+ useHasOngoingScreenShare,
78
+ useParticipants,
79
+ useCallCallingState,
80
+ useCallEndedAt,
81
+ useIsCallLive,
82
+ useOwnCapabilities,
83
+ } = useCallStateHooks();
84
+ const canJoinLive = useIsCallLive();
85
+ const callingState = useCallCallingState();
86
+ const endedAt = useCallEndedAt();
69
87
  const hasOngoingScreenShare = useHasOngoingScreenShare();
70
88
  const [currentSpeaker] = useParticipants();
71
89
  const floatingParticipant =
@@ -73,6 +91,11 @@ export const ViewerLivestream = ({
73
91
  currentSpeaker &&
74
92
  hasVideo(currentSpeaker) &&
75
93
  currentSpeaker;
94
+ const [hasLeft, setHasLeft] = useState(false);
95
+
96
+ const canJoinEarly = useCanJoinEarly();
97
+ const canJoinBackstage =
98
+ useOwnCapabilities()?.includes('join-backstage') ?? false;
76
99
 
77
100
  const [topViewHeight, setTopViewHeight] = React.useState<number>();
78
101
  const [controlsHeight, setControlsHeight] = React.useState<number>();
@@ -83,6 +106,12 @@ export const ViewerLivestream = ({
83
106
  return () => InCallManager.stop();
84
107
  }, []);
85
108
 
109
+ useEffect(() => {
110
+ if (callingState === CallingState.LEFT) {
111
+ setHasLeft(true);
112
+ }
113
+ }, [callingState]);
114
+
86
115
  const topViewProps: ViewerLivestreamTopViewProps = {
87
116
  LiveIndicator,
88
117
  FollowerCount,
@@ -92,6 +121,41 @@ export const ViewerLivestream = ({
92
121
  },
93
122
  };
94
123
 
124
+ useEffect(() => {
125
+ const handleJoinCall = async () => {
126
+ try {
127
+ await call?.join();
128
+ } catch (error) {
129
+ console.error('Failed to join call', error);
130
+ }
131
+ };
132
+
133
+ const canJoinAsap = canJoinLive || canJoinEarly || canJoinBackstage;
134
+ const join = joinBehavior ?? 'asap';
135
+ const canJoin =
136
+ (join === 'asap' && canJoinAsap) || (join === 'live' && canJoinLive);
137
+
138
+ if (call && callingState === CallingState.IDLE && canJoin && !hasLeft) {
139
+ handleJoinCall();
140
+ }
141
+ }, [
142
+ canJoinLive,
143
+ call,
144
+ canJoinBackstage,
145
+ canJoinEarly,
146
+ joinBehavior,
147
+ callingState,
148
+ hasLeft,
149
+ ]);
150
+
151
+ if (endedAt != null) {
152
+ return <CallEndedView />;
153
+ }
154
+
155
+ if (!canJoinLive || callingState !== CallingState.JOINED) {
156
+ return <ViewerLobby isLive={canJoinLive} />;
157
+ }
158
+
95
159
  return (
96
160
  <View style={[styles.container, viewerLivestream.container]}>
97
161
  {ViewerLivestreamTopView && <ViewerLivestreamTopView {...topViewProps} />}
@@ -124,6 +188,39 @@ export const ViewerLivestream = ({
124
188
  );
125
189
  };
126
190
 
191
+ const useCanJoinEarly = () => {
192
+ const { useCallStartsAt, useCallSettings } = useCallStateHooks();
193
+ const startsAt = useCallStartsAt();
194
+ const settings = useCallSettings();
195
+ const joinAheadTimeSeconds = settings?.backstage.join_ahead_time_seconds;
196
+ const [canJoinEarly, setCanJoinEarly] = useState(() =>
197
+ checkCanJoinEarly(startsAt, joinAheadTimeSeconds),
198
+ );
199
+
200
+ useEffect(() => {
201
+ if (!canJoinEarly) {
202
+ const handle = setInterval(() => {
203
+ setCanJoinEarly(checkCanJoinEarly(startsAt, joinAheadTimeSeconds));
204
+ }, 1000);
205
+
206
+ return () => clearInterval(handle);
207
+ }
208
+ }, [canJoinEarly, startsAt, joinAheadTimeSeconds]);
209
+
210
+ return canJoinEarly;
211
+ };
212
+
213
+ const checkCanJoinEarly = (
214
+ startsAt: Date | undefined,
215
+ joinAheadTimeSeconds: number | undefined,
216
+ ) => {
217
+ if (!startsAt) {
218
+ return false;
219
+ }
220
+
221
+ return Date.now() >= +startsAt - (joinAheadTimeSeconds ?? 0) * 1000;
222
+ };
223
+
127
224
  const useStyles = () => {
128
225
  const { theme } = useTheme();
129
226
  return useMemo(