Skip to content

Commit 4946137

Browse files
committed
add android method
1 parent cd58865 commit 4946137

File tree

11 files changed

+275
-154
lines changed

11 files changed

+275
-154
lines changed

packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import com.streamvideo.reactnative.util.CallAliveServiceChecker
2323
import com.streamvideo.reactnative.util.PiPHelper
2424
import com.streamvideo.reactnative.util.RingtoneUtil
2525
import com.streamvideo.reactnative.util.YuvFrame
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.Job
29+
import kotlinx.coroutines.launch
2630
import org.webrtc.VideoSink
2731
import org.webrtc.VideoTrack
2832
import java.io.ByteArrayOutputStream
@@ -246,9 +250,14 @@ class StreamVideoReactNativeModule(reactContext: ReactApplicationContext) :
246250
try {
247251
val track = getVideoTrackForStreamURL(streamURL)
248252
var screenshotSink: VideoSink? = null
249-
screenshotSink = VideoSink { videoFrame -> // Remove the sink before processing the frame
253+
screenshotSink = VideoSink { videoFrame -> // Remove the sink before asap
250254
// to avoid processing multiple frames.
251-
track.removeSink(screenshotSink)
255+
CoroutineScope(Dispatchers.IO).launch {
256+
// This has to be launched asynchronously - removing the sink on the
257+
// same thread as the videoframe is delivered will lead to a deadlock
258+
// (needs investigation why)
259+
track.removeSink(screenshotSink)
260+
}
252261

253262
videoFrame.retain()
254263
val bitmap = YuvFrame.bitmapFromVideoFrame(videoFrame)

packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx

+1-12
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from './VideoRenderer';
2727
import { useTheme } from '../../../contexts/ThemeContext';
2828
import type { CallContentProps } from '../../Call';
29-
import { useSnapshot } from '../../../contexts/SnapshotContext';
29+
import { useSnapshot } from '../../../contexts/internal/SnapshotContext';
3030

3131
export type ParticipantViewComponentProps = {
3232
/**
@@ -122,16 +122,6 @@ export const ParticipantView = ({
122122
participantView.highlightedContainer,
123123
];
124124

125-
const viewRef = useRef(null);
126-
const snapshot = useSnapshot();
127-
128-
// Register this view with the snapshot provider
129-
React.useEffect(() => {
130-
if (snapshot && viewRef.current) {
131-
snapshot.register(participant, viewRef);
132-
}
133-
}, [participant, snapshot]);
134-
135125
return (
136126
<View
137127
style={[styles.container, style, speakerStyle]}
@@ -140,7 +130,6 @@ export const ParticipantView = ({
140130
? `participant-${userId}-is-speaking`
141131
: `participant-${userId}-is-not-speaking`
142132
}
143-
ref={viewRef}
144133
>
145134
{ParticipantReaction && (
146135
<ParticipantReaction

packages/react-native-sdk/src/components/Participant/ParticipantView/VideoRenderer.tsx

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useRef } from 'react';
2-
import { StyleSheet, View } from 'react-native';
2+
import { Platform, StyleSheet, View } from 'react-native';
33
import type { MediaStream } from '@stream-io/react-native-webrtc';
44
import { RTCView } from '@stream-io/react-native-webrtc';
55
import type { ParticipantViewProps } from './ParticipantView';
@@ -15,6 +15,7 @@ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
1515
import { ParticipantVideoFallback as DefaultParticipantVideoFallback } from './ParticipantVideoFallback';
1616
import { useTheme } from '../../../contexts/ThemeContext';
1717
import { useTrackDimensions } from '../../../hooks/useTrackDimensions';
18+
import { useSnapshot } from '../../../contexts/internal/SnapshotContext';
1819

1920
const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
2021
VideoTrackType,
@@ -61,6 +62,10 @@ export const VideoRenderer = ({
6162
const pendingVideoLayoutRef = useRef<SfuModels.VideoDimension>();
6263
const subscribedVideoLayoutRef = useRef<SfuModels.VideoDimension>();
6364
const { direction } = useCameraState();
65+
const viewRef = useRef(null);
66+
const { register: registerSnapshot, deregister: deregisterSnapshot } =
67+
useSnapshot();
68+
6469
const videoDimensions = useTrackDimensions(participant, trackType);
6570
const {
6671
isLocalParticipant,
@@ -86,6 +91,29 @@ export const VideoRenderer = ({
8691
isPublishingVideoTrack &&
8792
isParticipantVideoEnabled(participant.sessionId);
8893

94+
// Register this view with the snapshot provider
95+
React.useEffect(() => {
96+
if (
97+
Platform.OS === 'ios' &&
98+
registerSnapshot &&
99+
viewRef.current &&
100+
canShowVideo
101+
) {
102+
registerSnapshot(participant, trackType, viewRef);
103+
} else {
104+
deregisterSnapshot(participant, trackType);
105+
}
106+
return () => {
107+
deregisterSnapshot(participant, trackType);
108+
};
109+
}, [
110+
participant,
111+
trackType,
112+
registerSnapshot,
113+
canShowVideo,
114+
deregisterSnapshot,
115+
]);
116+
89117
const mirror =
90118
isLocalParticipant && !isScreenSharing && direction === 'front';
91119

@@ -256,6 +284,7 @@ export const VideoRenderer = ({
256284
<View
257285
onLayout={onLayout}
258286
style={[styles.container, videoRenderer.container]}
287+
ref={viewRef}
259288
>
260289
{canShowVideo && videoStreamToRender ? (
261290
<RTCView

packages/react-native-sdk/src/contexts/SnapshotContext.tsx

-92
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from './StreamVideoContext';
22
export * from './ThemeContext';
33
export * from './BackgroundFilters';
4-
export * from './SnapshotContext';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import {
2+
StreamVideoParticipant,
3+
type VideoTrackType,
4+
getLogger,
5+
} from '@stream-io/video-client';
6+
import React, { createContext, useContext, useRef, RefObject } from 'react';
7+
import { NativeModules, findNodeHandle, Platform } from 'react-native';
8+
9+
const { StreamVideoReactNative } = NativeModules;
10+
11+
type SnapshotContextType = {
12+
register: (
13+
participant: StreamVideoParticipant,
14+
videoTrackType: VideoTrackType,
15+
ref: RefObject<any>,
16+
) => void;
17+
deregister: (
18+
participant: StreamVideoParticipant,
19+
videoTrackType: VideoTrackType,
20+
) => void;
21+
take: (
22+
participant: StreamVideoParticipant,
23+
videoTrackType: VideoTrackType,
24+
) => Promise<string | null>;
25+
};
26+
27+
// Create the context with a default undefined value
28+
const SnapshotContext = createContext<SnapshotContextType | undefined>(
29+
undefined,
30+
);
31+
32+
// Reference map type
33+
type RefMap = Map<string, RefObject<any>>;
34+
35+
export const SnapshotProvider = ({ children }: React.PropsWithChildren<{}>) => {
36+
// Use a ref to store the map of participant IDs to their view refs
37+
const participantRefs = useRef<RefMap>(new Map());
38+
39+
// Register a participant's RTCView ref
40+
const register = (
41+
participant: StreamVideoParticipant,
42+
videoTrackType: VideoTrackType,
43+
ref: RefObject<any>,
44+
) => {
45+
if (ref && participant.userId) {
46+
participantRefs.current.set(
47+
`${participant.userId}-${videoTrackType}`,
48+
ref,
49+
);
50+
}
51+
};
52+
53+
const deregister = (
54+
participant: StreamVideoParticipant,
55+
videoTrackType: VideoTrackType,
56+
) => {
57+
if (participant.userId) {
58+
participantRefs.current.delete(`${participant.userId}-${videoTrackType}`);
59+
}
60+
};
61+
62+
// Take a snapshot of a specific participant's view
63+
const take = async (
64+
participant: StreamVideoParticipant,
65+
videoTrackType: VideoTrackType,
66+
): Promise<string | null> => {
67+
try {
68+
if (Platform.OS !== 'ios') {
69+
throw new Error('SnapshotProvider is only supported on iOS');
70+
}
71+
72+
if (!participant?.userId) {
73+
getLogger(['SnapshotProvider'])(
74+
'error',
75+
'Cannot take snapshot: Invalid participant',
76+
);
77+
return null;
78+
}
79+
80+
const ref = participantRefs.current.get(
81+
`${participant.userId}-${videoTrackType}`,
82+
);
83+
if (!ref || !ref.current) {
84+
getLogger(['SnapshotProvider'])(
85+
'error',
86+
'Cannot take snapshot: No registered view for this participant',
87+
);
88+
return null;
89+
}
90+
91+
// Get the native handle for the view
92+
const tag = findNodeHandle(ref.current);
93+
if (!tag) {
94+
getLogger(['SnapshotProvider'])(
95+
'error',
96+
'Cannot take snapshot: Cannot get native handle for view',
97+
);
98+
return null;
99+
}
100+
101+
// Take the snapshot using our native module
102+
const base64Image = await StreamVideoReactNative.captureRef(tag, {
103+
// format: 'jpg',
104+
// quality: 0.8,
105+
});
106+
107+
return base64Image;
108+
} catch (error) {
109+
getLogger(['SnapshotProvider'])(
110+
'error',
111+
'Error taking participant snapshot:',
112+
error,
113+
);
114+
return null;
115+
}
116+
};
117+
118+
const value = {
119+
register,
120+
deregister,
121+
take,
122+
};
123+
124+
return (
125+
<SnapshotContext.Provider value={value}>
126+
{children}
127+
</SnapshotContext.Provider>
128+
);
129+
};
130+
131+
export const useSnapshot = (): SnapshotContextType => {
132+
const context = useContext(SnapshotContext);
133+
if (!context) {
134+
throw new Error('useSnapshot must be used within a SnapshotProvider');
135+
}
136+
return context;
137+
};

packages/react-native-sdk/src/hooks/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './useAutoEnterPiPEffect';
88
export * from './useApplyDefaultMediaStreamSettings';
99
export * from './useScreenShareButton';
1010
export * from './useTrackDimensions';
11+
export * from './useScreenshot';

0 commit comments

Comments
 (0)