@livekit/react-native 1.4.3 → 2.0.2

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 (38) hide show
  1. package/README.md +35 -23
  2. package/android/src/main/java/com/livekit/reactnative/LiveKitReactNative.kt +4 -4
  3. package/android/src/main/java/com/livekit/reactnative/video/CustomVideoDecoderFactory.kt +67 -0
  4. package/android/src/main/java/com/livekit/reactnative/video/CustomVideoEncoderFactory.kt +74 -0
  5. package/lib/commonjs/audio/AudioManager.js +15 -6
  6. package/lib/commonjs/audio/AudioManager.js.map +1 -1
  7. package/lib/commonjs/audio/AudioSession.js +2 -0
  8. package/lib/commonjs/audio/AudioSession.js.map +1 -1
  9. package/lib/commonjs/components/VideoView.js +12 -11
  10. package/lib/commonjs/components/VideoView.js.map +1 -1
  11. package/lib/commonjs/components/ViewPortDetector.js +134 -39
  12. package/lib/commonjs/components/ViewPortDetector.js.map +1 -1
  13. package/lib/commonjs/useParticipant.js +9 -9
  14. package/lib/commonjs/useParticipant.js.map +1 -1
  15. package/lib/commonjs/useRoom.js +5 -5
  16. package/lib/commonjs/useRoom.js.map +1 -1
  17. package/lib/module/audio/AudioManager.js +15 -6
  18. package/lib/module/audio/AudioManager.js.map +1 -1
  19. package/lib/module/audio/AudioSession.js +2 -0
  20. package/lib/module/audio/AudioSession.js.map +1 -1
  21. package/lib/module/components/VideoView.js +13 -12
  22. package/lib/module/components/VideoView.js.map +1 -1
  23. package/lib/module/components/ViewPortDetector.js +134 -40
  24. package/lib/module/components/ViewPortDetector.js.map +1 -1
  25. package/lib/module/useParticipant.js +9 -9
  26. package/lib/module/useParticipant.js.map +1 -1
  27. package/lib/module/useRoom.js +5 -5
  28. package/lib/module/useRoom.js.map +1 -1
  29. package/lib/typescript/audio/AudioManager.d.ts +1 -1
  30. package/lib/typescript/audio/AudioSession.d.ts +2 -0
  31. package/lib/typescript/components/ViewPortDetector.d.ts +11 -4
  32. package/package.json +2 -2
  33. package/src/audio/AudioManager.ts +18 -8
  34. package/src/audio/AudioSession.ts +2 -0
  35. package/src/components/VideoView.tsx +20 -13
  36. package/src/components/ViewPortDetector.tsx +112 -21
  37. package/src/useParticipant.ts +15 -9
  38. package/src/useRoom.ts +5 -5
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useMemo } from 'react';
2
2
  import { Platform } from 'react-native';
3
- import { RoomEvent, type Room } from 'livekit-client';
3
+ import { RoomEvent, Room } from 'livekit-client';
4
4
  import AudioSession, {
5
5
  getDefaultAppleAudioConfigurationForMode,
6
6
  type AppleAudioConfiguration,
@@ -31,14 +31,25 @@ export function useIOSAudioManagement(
31
31
  [localTrackCount, remoteTrackCount]
32
32
  );
33
33
 
34
+ useEffect(() => {
35
+ let recalculateTrackCounts = () => {
36
+ setLocalTrackCount(getLocalAudioTrackCount(room));
37
+ setRemoteTrackCount(getRemoteAudioTrackCount(room));
38
+ };
39
+
40
+ recalculateTrackCounts();
41
+
42
+ room.on(RoomEvent.Connected, recalculateTrackCounts);
43
+
44
+ return () => {
45
+ room.off(RoomEvent.Connected, recalculateTrackCounts);
46
+ };
47
+ }, [room]);
34
48
  useEffect(() => {
35
49
  if (Platform.OS !== 'ios') {
36
50
  return () => {};
37
51
  }
38
52
 
39
- setLocalTrackCount(getLocalAudioTrackCount(room));
40
- setRemoteTrackCount(getRemoteAudioTrackCount(room));
41
-
42
53
  let onLocalPublished = () => {
43
54
  setLocalTrackCount(localTrackCount + 1);
44
55
  };
@@ -85,7 +96,6 @@ export function useIOSAudioManagement(
85
96
  let configFunc =
86
97
  onConfigureNativeAudio ?? getDefaultAppleAudioConfigurationForMode;
87
98
  let audioConfig = configFunc(trackState, preferSpeakerOutput);
88
-
89
99
  AudioSession.setAppleAudioConfiguration(audioConfig);
90
100
  }, [trackState, onConfigureNativeAudio, preferSpeakerOutput]);
91
101
  }
@@ -106,13 +116,13 @@ function computeAudioTrackState(
106
116
  }
107
117
 
108
118
  function getLocalAudioTrackCount(room: Room): number {
109
- return room.localParticipant.audioTracks.entries.length;
119
+ return room.localParticipant.audioTrackPublications.size;
110
120
  }
111
121
 
112
122
  function getRemoteAudioTrackCount(room: Room): number {
113
123
  var audioTracks = 0;
114
- room.participants.forEach((participant) => {
115
- audioTracks += participant.audioTracks.entries.length;
124
+ room.remoteParticipants.forEach((participant) => {
125
+ audioTracks += participant.audioTrackPublications.size;
116
126
  });
117
127
 
118
128
  return audioTracks;
@@ -36,6 +36,8 @@ const LivekitReactNative = NativeModules.LivekitReactNative
36
36
  *
37
37
  * See {@link AndroidAudioTypePresets} for pre-configured values.
38
38
  *
39
+ * NOTE: If `audioTypeOptions` is set, this must also be reflected in your android MainApplication setup.
40
+ *
39
41
  * ----
40
42
  * iOS
41
43
  *
@@ -9,7 +9,7 @@ import {
9
9
  VideoTrack,
10
10
  } from 'livekit-client';
11
11
  import { RTCView } from '@livekit/react-native-webrtc';
12
- import { useEffect, useState } from 'react';
12
+ import { useCallback, useEffect, useMemo, useState } from 'react';
13
13
  import { RemoteVideoTrack } from 'livekit-client';
14
14
  import ViewPortDetector from './ViewPortDetector';
15
15
 
@@ -35,6 +35,20 @@ export const VideoView = ({
35
35
  return info;
36
36
  });
37
37
 
38
+ const layoutOnChange = useCallback(
39
+ (event: LayoutChangeEvent) => elementInfo.onLayout(event),
40
+ [elementInfo]
41
+ );
42
+ const visibilityOnChange = useCallback(
43
+ (isVisible: boolean) => elementInfo.onVisibility(isVisible),
44
+ [elementInfo]
45
+ );
46
+ const shouldObserveVisibility = useMemo(() => {
47
+ return (
48
+ videoTrack instanceof RemoteVideoTrack && videoTrack.isAdaptiveStream
49
+ );
50
+ }, [videoTrack]);
51
+
38
52
  const [mediaStream, setMediaStream] = useState(videoTrack?.mediaStream);
39
53
  useEffect(() => {
40
54
  setMediaStream(videoTrack?.mediaStream);
@@ -64,22 +78,15 @@ export const VideoView = ({
64
78
  }, [videoTrack, elementInfo]);
65
79
 
66
80
  return (
67
- <View
68
- style={{ ...style, ...styles.container }}
69
- onLayout={(event) => {
70
- elementInfo.onLayout(event);
71
- }}
72
- >
81
+ <View style={{ ...style, ...styles.container }} onLayout={layoutOnChange}>
73
82
  <ViewPortDetector
74
- onChange={(isVisible: boolean) => elementInfo.onVisibility(isVisible)}
83
+ onChange={visibilityOnChange}
75
84
  style={styles.videoView}
85
+ disabled={!shouldObserveVisibility}
86
+ propKey={videoTrack}
76
87
  >
77
88
  <RTCView
78
- // eslint-disable-next-line react-native/no-inline-styles
79
- style={{
80
- flex: 1,
81
- width: '100%',
82
- }}
89
+ style={styles.videoView}
83
90
  streamURL={mediaStream?.toURL() ?? ''}
84
91
  objectFit={objectFit}
85
92
  zOrder={zOrder}
@@ -1,15 +1,61 @@
1
1
  'use strict';
2
2
 
3
3
  import React, { Component, PropsWithChildren } from 'react';
4
- import { View, ViewStyle } from 'react-native';
4
+ import {
5
+ AppState,
6
+ AppStateStatus,
7
+ NativeEventSubscription,
8
+ View,
9
+ ViewStyle,
10
+ } from 'react-native';
11
+
12
+ const DEFAULT_DELAY = 1000;
5
13
 
6
14
  export type Props = {
7
15
  disabled?: boolean;
8
16
  style?: ViewStyle;
9
17
  onChange?: (isVisible: boolean) => void;
10
18
  delay?: number;
19
+ propKey?: any;
11
20
  };
12
21
 
22
+ class TimeoutHandler {
23
+ private handlerRef: { id: any } = { id: -1 };
24
+
25
+ get handler(): any {
26
+ return this.handlerRef.id;
27
+ }
28
+ set handler(n: any) {
29
+ this.handlerRef.id = n;
30
+ }
31
+
32
+ clear() {
33
+ clearTimeout(this.handlerRef.id as any);
34
+ }
35
+ }
36
+
37
+ function setIntervalWithTimeout(
38
+ callback: (clear: () => void) => any,
39
+ intervalMs: number,
40
+ handleWrapper = new TimeoutHandler()
41
+ ): TimeoutHandler {
42
+ let cleared = false;
43
+
44
+ const timeout = () => {
45
+ handleWrapper.handler = setTimeout(() => {
46
+ callback(() => {
47
+ cleared = true;
48
+ handleWrapper.clear();
49
+ });
50
+ if (!cleared) {
51
+ timeout();
52
+ }
53
+ }, intervalMs);
54
+ };
55
+ timeout();
56
+ return handleWrapper;
57
+ }
58
+
13
59
  /**
14
60
  * Detects when this is in the viewport and visible.
15
61
  *
@@ -19,8 +65,10 @@ export default class ViewPortDetector extends Component<
19
65
  PropsWithChildren<Props>
20
66
  > {
21
67
  private lastValue: boolean | null = null;
22
- private interval: any | null = null;
68
+ private interval: TimeoutHandler | null = null;
23
69
  private view: View | null = null;
70
+ private lastAppStateActive = false;
71
+ private appStateSubscription: NativeEventSubscription | null = null;
24
72
 
25
73
  constructor(props: Props) {
26
74
  super(props);
@@ -28,43 +76,84 @@ export default class ViewPortDetector extends Component<
28
76
  }
29
77
 
30
78
  componentDidMount() {
31
- if (!this.props.disabled) {
79
+ this.lastAppStateActive = AppState.currentState === 'active';
80
+ this.appStateSubscription = AppState.addEventListener(
81
+ 'change',
82
+ this.handleAppStateChange
83
+ );
84
+ if (this.hasValidTimeout(this.props.disabled, this.props.delay)) {
32
85
  this.startWatching();
33
86
  }
34
87
  }
35
88
 
36
89
  componentWillUnmount() {
90
+ this.appStateSubscription?.remove();
91
+ this.appStateSubscription = null;
37
92
  this.stopWatching();
38
93
  }
39
94
 
95
+ hasValidTimeout = (disabled?: boolean, delay?: number): boolean => {
96
+ let disabledValue = disabled ?? false;
97
+ let delayValue = delay ?? DEFAULT_DELAY;
98
+ return (
99
+ AppState.currentState === 'active' && !disabledValue && delayValue > 0
100
+ );
101
+ };
102
+
40
103
  UNSAFE_componentWillReceiveProps(nextProps: Props) {
41
- if (nextProps.disabled) {
104
+ if (!this.hasValidTimeout(nextProps.disabled, nextProps.delay)) {
42
105
  this.stopWatching();
43
106
  } else {
44
- this.lastValue = null;
107
+ if (this.props.propKey !== nextProps.propKey) {
108
+ this.lastValue = null;
109
+ }
45
110
  this.startWatching();
46
111
  }
47
112
  }
113
+ handleAppStateChange = (nextAppState: AppStateStatus) => {
114
+ let nextAppStateActive = nextAppState === 'active';
115
+ if (this.lastAppStateActive !== nextAppStateActive) {
116
+ this.checkVisibility();
117
+ }
118
+ this.lastAppStateActive = nextAppStateActive;
48
119
 
49
- private startWatching() {
120
+ if (!this.hasValidTimeout(this.props.disabled, this.props.delay)) {
121
+ this.stopWatching();
122
+ } else {
123
+ this.startWatching();
124
+ }
125
+ };
126
+
127
+ startWatching = () => {
50
128
  if (this.interval) {
51
129
  return;
52
130
  }
53
- this.interval = setInterval(() => {
54
- if (!this.view) {
55
- return;
56
- }
57
- this.view.measure((_x, _y, width, height, _pageX, _pageY) => {
58
- this.checkInViewPort(width, height);
59
- });
60
- }, this.props.delay || 100);
61
- }
131
+ this.interval = setIntervalWithTimeout(
132
+ this.checkVisibility,
133
+ this.props.delay || DEFAULT_DELAY
134
+ );
135
+ };
62
136
 
63
- private stopWatching() {
64
- this.interval = clearInterval(this.interval);
65
- }
137
+ stopWatching = () => {
138
+ this.interval?.clear();
139
+ this.interval = null;
140
+ };
66
141
 
67
- private checkInViewPort(width?: number, height?: number) {
142
+ checkVisibility = () => {
143
+ if (!this.view) {
144
+ return;
145
+ }
146
+
147
+ if (AppState.currentState !== 'active') {
148
+ this.updateVisibility(false);
149
+ return;
150
+ }
151
+
152
+ this.view.measure((_x, _y, width, height, _pageX, _pageY) => {
153
+ this.checkInViewPort(width, height);
154
+ });
155
+ };
156
+ checkInViewPort = (width?: number, height?: number) => {
68
157
  let isVisible: boolean;
69
158
  // Not visible if any of these are missing.
70
159
  if (!width || !height) {
@@ -72,13 +161,15 @@ export default class ViewPortDetector extends Component<
72
161
  } else {
73
162
  isVisible = true;
74
163
  }
164
+ this.updateVisibility(isVisible);
165
+ };
75
166
 
167
+ updateVisibility = (isVisible: boolean) => {
76
168
  if (this.lastValue !== isVisible) {
77
169
  this.lastValue = isVisible;
78
170
  this.props.onChange?.(isVisible);
79
171
  }
80
- }
81
-
172
+ };
82
173
  render() {
83
174
  return (
84
175
  <View
@@ -34,22 +34,28 @@ export function useParticipant(participant: Participant): ParticipantState {
34
34
  );
35
35
 
36
36
  const [cameraPublication, setCameraPublication] = useState(
37
- participant.getTrack(Track.Source.Camera)
37
+ participant.getTrackPublication(Track.Source.Camera)
38
38
  );
39
39
  const [microphonePublication, setMicrophonePublication] = useState(
40
- participant.getTrack(Track.Source.Microphone)
40
+ participant.getTrackPublication(Track.Source.Microphone)
41
41
  );
42
42
  const [screenSharePublication, setScreenSharePublication] = useState(
43
- participant.getTrack(Track.Source.ScreenShare)
43
+ participant.getTrackPublication(Track.Source.ScreenShare)
44
44
  );
45
45
  useEffect(() => {
46
46
  const onPublicationsChanged = () => {
47
- setPublications(Array.from(participant.tracks.values()));
48
- setCameraPublication(participant.getTrack(Track.Source.Camera));
49
- setMicrophonePublication(participant.getTrack(Track.Source.Microphone));
50
- setScreenSharePublication(participant.getTrack(Track.Source.ScreenShare));
47
+ setPublications(Array.from(participant.trackPublications.values()));
48
+ setCameraPublication(
49
+ participant.getTrackPublication(Track.Source.Camera)
50
+ );
51
+ setMicrophonePublication(
52
+ participant.getTrackPublication(Track.Source.Microphone)
53
+ );
54
+ setScreenSharePublication(
55
+ participant.getTrackPublication(Track.Source.ScreenShare)
56
+ );
51
57
  setSubscribedTracks(
52
- Array.from(participant.tracks.values()).filter((pub) => {
58
+ Array.from(participant.trackPublications.values()).filter((pub) => {
53
59
  return pub.isSubscribed && pub.track !== undefined;
54
60
  })
55
61
  );
@@ -120,7 +126,7 @@ export function useParticipant(participant: Participant): ParticipantState {
120
126
  }, [participant]);
121
127
 
122
128
  let muted: boolean | undefined;
123
- participant.audioTracks.forEach((pub) => {
129
+ participant.audioTrackPublications.forEach((pub) => {
124
130
  muted = pub.isMuted;
125
131
  });
126
132
  if (muted === undefined) {
package/src/useRoom.ts CHANGED
@@ -32,7 +32,7 @@ export function useRoom(room: Room, options?: RoomOptions): RoomState {
32
32
 
33
33
  useEffect(() => {
34
34
  const onParticipantsChanged = () => {
35
- const remotes = Array.from(room.participants.values());
35
+ const remotes = Array.from(room.remoteParticipants.values());
36
36
  const newParticipants: Participant[] = [room.localParticipant];
37
37
  newParticipants.push(...remotes);
38
38
  sortFunc(newParticipants, room.localParticipant);
@@ -45,8 +45,8 @@ export function useRoom(room: Room, options?: RoomOptions): RoomState {
45
45
  return;
46
46
  }
47
47
  const tracks: AudioTrack[] = [];
48
- room.participants.forEach((p) => {
49
- p.audioTracks.forEach((pub) => {
48
+ room.remoteParticipants.forEach((p) => {
49
+ p.audioTrackPublications.forEach((pub) => {
50
50
  if (pub.audioTrack) {
51
51
  tracks.push(pub.audioTrack);
52
52
  }
@@ -135,8 +135,8 @@ export function sortParticipants(
135
135
  }
136
136
 
137
137
  // video on
138
- const aVideo = a.videoTracks.size > 0;
139
- const bVideo = b.videoTracks.size > 0;
138
+ const aVideo = a.videoTrackPublications.size > 0;
139
+ const bVideo = b.videoTrackPublications.size > 0;
140
140
  if (aVideo !== bVideo) {
141
141
  if (aVideo) {
142
142
  return -1;