@stream-io/video-react-native-sdk 1.37.1-beta.0 → 1.38.1

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 (84) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +0 -81
  3. package/dist/commonjs/hooks/index.js +0 -11
  4. package/dist/commonjs/hooks/index.js.map +1 -1
  5. package/dist/commonjs/modules/call-manager/CallManager.js +13 -0
  6. package/dist/commonjs/modules/call-manager/CallManager.js.map +1 -1
  7. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js +37 -0
  8. package/dist/commonjs/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  9. package/dist/commonjs/providers/StreamCall/index.js +2 -1
  10. package/dist/commonjs/providers/StreamCall/index.js.map +1 -1
  11. package/dist/commonjs/utils/internal/callingx/callingx.js +2 -2
  12. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  13. package/dist/commonjs/utils/internal/registerSDKGlobals.js +16 -0
  14. package/dist/commonjs/utils/internal/registerSDKGlobals.js.map +1 -1
  15. package/dist/commonjs/utils/push/internal/ios.js +5 -0
  16. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  17. package/dist/commonjs/version.js +1 -1
  18. package/dist/commonjs/version.js.map +1 -1
  19. package/dist/module/hooks/index.js +0 -1
  20. package/dist/module/hooks/index.js.map +1 -1
  21. package/dist/module/modules/call-manager/CallManager.js +13 -0
  22. package/dist/module/modules/call-manager/CallManager.js.map +1 -1
  23. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js +30 -0
  24. package/dist/module/providers/StreamCall/AudioInterruptionTracer.js.map +1 -0
  25. package/dist/module/providers/StreamCall/index.js +2 -1
  26. package/dist/module/providers/StreamCall/index.js.map +1 -1
  27. package/dist/module/utils/internal/callingx/callingx.js +2 -2
  28. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  29. package/dist/module/utils/internal/registerSDKGlobals.js +17 -1
  30. package/dist/module/utils/internal/registerSDKGlobals.js.map +1 -1
  31. package/dist/module/utils/push/internal/ios.js +5 -0
  32. package/dist/module/utils/push/internal/ios.js.map +1 -1
  33. package/dist/module/version.js +1 -1
  34. package/dist/module/version.js.map +1 -1
  35. package/dist/typescript/hooks/index.d.ts +0 -1
  36. package/dist/typescript/hooks/index.d.ts.map +1 -1
  37. package/dist/typescript/modules/call-manager/CallManager.d.ts +8 -1
  38. package/dist/typescript/modules/call-manager/CallManager.d.ts.map +1 -1
  39. package/dist/typescript/modules/call-manager/types.d.ts +6 -0
  40. package/dist/typescript/modules/call-manager/types.d.ts.map +1 -1
  41. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts +5 -0
  42. package/dist/typescript/providers/StreamCall/AudioInterruptionTracer.d.ts.map +1 -0
  43. package/dist/typescript/providers/StreamCall/index.d.ts.map +1 -1
  44. package/dist/typescript/utils/internal/registerSDKGlobals.d.ts.map +1 -1
  45. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  46. package/dist/typescript/version.d.ts +1 -1
  47. package/dist/typescript/version.d.ts.map +1 -1
  48. package/ios/PictureInPicture/PictureInPictureAvatarView.swift +3 -1
  49. package/ios/PictureInPicture/StreamBufferTransformer.swift +13 -4
  50. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +79 -71
  51. package/ios/PictureInPicture/StreamRTCYUVBuffer.swift +20 -16
  52. package/ios/StreamInCallManager.swift +256 -81
  53. package/ios/StreamVideoReactNative-Bridging-Header.h +0 -2
  54. package/ios/StreamVideoReactNative.m +0 -81
  55. package/package.json +11 -11
  56. package/src/hooks/index.ts +0 -1
  57. package/src/modules/call-manager/CallManager.ts +25 -1
  58. package/src/modules/call-manager/types.ts +7 -0
  59. package/src/providers/StreamCall/AudioInterruptionTracer.tsx +51 -0
  60. package/src/providers/StreamCall/index.tsx +2 -0
  61. package/src/utils/internal/callingx/callingx.ts +2 -2
  62. package/src/utils/internal/registerSDKGlobals.ts +23 -1
  63. package/src/utils/push/internal/ios.ts +5 -0
  64. package/src/version.ts +1 -1
  65. package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +0 -436
  66. package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +0 -17
  67. package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +0 -36
  68. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +0 -60
  69. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +0 -31
  70. package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +0 -329
  71. package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +0 -472
  72. package/dist/commonjs/hooks/useLoopbackRecording.js +0 -243
  73. package/dist/commonjs/hooks/useLoopbackRecording.js.map +0 -1
  74. package/dist/module/hooks/useLoopbackRecording.js +0 -238
  75. package/dist/module/hooks/useLoopbackRecording.js.map +0 -1
  76. package/dist/typescript/hooks/useLoopbackRecording.d.ts +0 -85
  77. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +0 -1
  78. package/ios/TracksRecorder/AudioPipeline.swift +0 -270
  79. package/ios/TracksRecorder/PipelineHost.swift +0 -56
  80. package/ios/TracksRecorder/RecorderAudioRenderTap.swift +0 -154
  81. package/ios/TracksRecorder/RecorderVideoSink.swift +0 -137
  82. package/ios/TracksRecorder/TracksRecorderManager.swift +0 -327
  83. package/ios/TracksRecorder/VideoPipeline.swift +0 -297
  84. package/src/hooks/useLoopbackRecording.ts +0 -438
@@ -1,31 +0,0 @@
1
- package com.streamvideo.reactnative.recorder
2
-
3
- import org.webrtc.VideoFrame
4
- import org.webrtc.VideoSink
5
-
6
- /**
7
- * Per-track [VideoSink] for [TracksRecorderManager]. Stays dumb —
8
- * retains each incoming frame and forwards it; the manager handles
9
- * queue hops, I420 → NV12 conversion, and the encoder feed.
10
- *
11
- * `onFrame` is invoked from a WebRTC delivery thread. **Never** call
12
- * `removeSink` from inside `onFrame` — Android WebRTC has a known
13
- * deadlock there. The manager removes from its handler thread.
14
- */
15
- internal class RecorderVideoSink(
16
- private val handler: (VideoFrame) -> Unit,
17
- ) : VideoSink {
18
-
19
- override fun onFrame(frame: VideoFrame) {
20
- // Retain so the buffer outlives this delivery-thread call.
21
- // The manager releases after the encoder consumes it.
22
- frame.retain()
23
- try {
24
- handler.invoke(frame)
25
- } catch (t: Throwable) {
26
- // Balance the retain when the manager rejects the frame.
27
- frame.release()
28
- throw t
29
- }
30
- }
31
- }
@@ -1,329 +0,0 @@
1
- package com.streamvideo.reactnative.recorder
2
-
3
- import android.content.Context
4
- import android.media.MediaMuxer
5
- import android.os.Handler
6
- import android.os.HandlerThread
7
- import android.os.Looper
8
- import android.util.Log
9
- import com.oney.WebRTCModule.WebRTCModule
10
- import java.io.File
11
- import org.webrtc.VideoTrack
12
-
13
- /**
14
- * Orchestrator for the React Native track recorder. Owns the [MediaMuxer], the recording lifecycle,
15
- * the muxer-start gate, and the terminal-completion barrier. Delegates the encoder + sink + drain
16
- * work to [VideoPipeline] and [AudioPipeline] respectively (composed via [PipelineHost]).
17
- *
18
- * The public surface is wrapped by `StreamVideoReactNativeModule.kt`'s `startTrackRecording` /
19
- * `stopTrackRecording` / `clearStreamRecordings` / `getStreamRecordings` methods, which in turn are
20
- * the bridge contract used by the JS `useLoopbackRecording` hook. Knows nothing about loopback or
21
- * any specific recording use case — it's a generic encode-and-mux orchestrator.
22
- *
23
- * Both video and audio are optional. Audio is always requested by the current bridge contract;
24
- * video is requested whenever the caller passes a `videoTrackId` that resolves to a [VideoTrack].
25
- * The [MediaMuxer] only starts once **all** active pipelines have reported their track (gated by
26
- * [maybeStartMuxer] on [pendingPipelines]).
27
- *
28
- * Threading: a dedicated [HandlerThread] serialises every state mutation (start, stop, encoder
29
- * feed, muxer writes) so the rest of the file is lock-free. Pipelines accept buffers on WebRTC
30
- * delivery threads and post to this handler before touching any state.
31
- *
32
- * Completion semantics: `startRecording` is the **lifecycle promise** — it fires once at the
33
- * recording's terminal moment with the produced file (or an error). `stopRecording` is a void sync
34
- * point that resolves after native finalisation, so callers can `await stopTrackRecording(); await
35
- * getStreamRecordings()` without racing the disk flush. Same shape as iOS.
36
- */
37
- class TracksRecorderManager private constructor() : PipelineHost {
38
-
39
- companion object {
40
- @JvmField val shared = TracksRecorderManager()
41
-
42
- private const val TAG = "TracksRecorder"
43
- private const val RECORDINGS_DIR_NAME = "StreamRecordings"
44
- }
45
-
46
- private val thread = HandlerThread("io.stream.video.tracks-recorder").apply { start() }
47
- private val timerHandler = Handler(Looper.getMainLooper())
48
-
49
- override val handler = Handler(thread.looper)
50
- override var muxer: MediaMuxer? = null
51
- private set
52
- override var muxerStarted = false
53
- private set
54
-
55
- private var videoPipeline: VideoPipeline? = null
56
- private var audioPipeline: AudioPipeline? = null
57
-
58
- private var outputFile: File? = null
59
-
60
- private var recordingCompletion: ((File?, Throwable?) -> Unit)? = null
61
- private var isCompleted = false
62
- private var isRecording = false
63
-
64
- private var pendingPipelines = 0
65
- private var recordingStartHostTimeNs: Long? = null
66
- private var autoStopRunnable: Runnable? = null
67
-
68
- fun recordingsDirectory(context: Context): File {
69
- val dir = File(context.cacheDir, RECORDINGS_DIR_NAME)
70
- if (!dir.exists()) dir.mkdirs()
71
- return dir
72
- }
73
-
74
- fun startRecording(
75
- context: Context,
76
- webRTCModule: WebRTCModule,
77
- videoTrackId: String?,
78
- maxDurationMs: Long,
79
- targetWidth: Int = 0,
80
- targetHeight: Int = 0,
81
- completion: (File?, Throwable?) -> Unit,
82
- ) {
83
- handler.post {
84
- if (isRecording) {
85
- completion(null, RecordingError("recording_in_progress"))
86
- return@post
87
- }
88
-
89
- val resolvedVideoTrack =
90
- videoTrackId?.let { webRTCModule.getTrackById(it) } as? VideoTrack
91
-
92
- val dir = recordingsDirectory(context)
93
- val outFile = File(dir, "recording_${System.currentTimeMillis()}.mp4")
94
- val muxerInstance: MediaMuxer =
95
- try {
96
- MediaMuxer(
97
- outFile.absolutePath,
98
- MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
99
- )
100
- } catch (t: Throwable) {
101
- completion(null, t)
102
- return@post
103
- }
104
-
105
- resetTransientState()
106
-
107
- this.muxer = muxerInstance
108
- this.outputFile = outFile
109
- this.recordingCompletion = completion
110
- this.isRecording = true
111
-
112
- if (resolvedVideoTrack != null) {
113
- val pipeline = VideoPipeline(
114
- host = this,
115
- videoTrack = resolvedVideoTrack,
116
- targetWidth = targetWidth,
117
- targetHeight = targetHeight,
118
- )
119
- videoPipeline = pipeline
120
- pendingPipelines++
121
- pipeline.start()
122
- }
123
-
124
- val audio = AudioPipeline(host = this, webRTCModule = webRTCModule)
125
- audioPipeline = audio
126
- pendingPipelines++
127
- audio.start()
128
-
129
- if (maxDurationMs > 0) {
130
- val runnable = Runnable { stopRecording { /* fire-and-forget */} }
131
- autoStopRunnable = runnable
132
- timerHandler.postDelayed(runnable, maxDurationMs)
133
- }
134
-
135
- Log.i(
136
- TAG,
137
- "recording started video=${resolvedVideoTrack != null} audio=true → ${outFile.absolutePath}",
138
- )
139
- }
140
- }
141
-
142
- fun stopRecording(completion: () -> Unit) {
143
- // Detach sinks synchronously off the recorder handler so no
144
- // new buffers can be enqueued while the backlog drains. The
145
- // audio pipeline also restores speaker volume here so the
146
- // mute lifts immediately.
147
- videoPipeline?.detachSink()
148
- audioPipeline?.detachSink()
149
- autoStopRunnable?.let { timerHandler.removeCallbacks(it) }
150
- autoStopRunnable = null
151
-
152
- handler.post {
153
- if (!isRecording) {
154
- completion()
155
- return@post
156
- }
157
-
158
- val video = videoPipeline
159
- val audio = audioPipeline
160
- val muxerInstance = muxer
161
- val resolved = outputFile
162
-
163
- if (!muxerStarted || muxerInstance == null) {
164
- Log.w(TAG, "stopRecording: muxer never started — discarding empty recording")
165
- video?.logSummary()
166
- audio?.logSummary()
167
- video?.stopAndRelease()
168
- audio?.stopAndRelease()
169
- try {
170
- muxerInstance?.release()
171
- } catch (_: Throwable) {}
172
- // Best-effort: delete the empty file so getStreamRecordings()
173
- // doesn't surface an unplayable 0-byte mp4.
174
- resolved?.delete()
175
- fireTerminalCompletion(null, null)
176
- cleanupAfterStop()
177
- completion()
178
- return@post
179
- }
180
-
181
- // Skip the EOS drain when EOS can't be queued — waiting on
182
- // a marker that will never arrive would hang the handler.
183
- if (video != null) {
184
- try {
185
- val queued = video.signalEndOfStream(muxerInstance)
186
- if (queued) video.drainAfterEoS(muxerInstance)
187
- } catch (t: Throwable) {
188
- Log.e(TAG, "stopRecording: video drain failed", t)
189
- }
190
- }
191
- if (audio != null) {
192
- try {
193
- val queued = audio.signalEndOfStream(muxerInstance)
194
- if (queued) audio.drainAfterEoS(muxerInstance)
195
- } catch (t: Throwable) {
196
- Log.e(TAG, "stopRecording: audio drain failed", t)
197
- }
198
- }
199
-
200
- video?.stopAndRelease()
201
- audio?.stopAndRelease()
202
-
203
- var finalResolved: File? = resolved
204
- try {
205
- muxerInstance.stop()
206
- } catch (t: Throwable) {
207
- Log.e(TAG, "muxer.stop() threw — likely no usable samples", t)
208
- finalResolved = null
209
- resolved?.delete()
210
- }
211
- try {
212
- muxerInstance.release()
213
- } catch (t: Throwable) {
214
- Log.w(TAG, "muxer.release() threw", t)
215
- }
216
-
217
- video?.logSummary()
218
- audio?.logSummary()
219
- Log.i(
220
- TAG,
221
- "recording finalised → ${finalResolved?.absolutePath ?: "(no file produced)"}",
222
- )
223
-
224
- fireTerminalCompletion(finalResolved, null)
225
- cleanupAfterStop()
226
- completion()
227
- }
228
- }
229
-
230
- fun clearRecordingsDirectory(context: Context, completion: (Throwable?) -> Unit) {
231
- handler.post {
232
- try {
233
- val dir = recordingsDirectory(context)
234
- dir.listFiles()?.forEach { it.deleteRecursively() }
235
- completion(null)
236
- } catch (t: Throwable) {
237
- completion(t)
238
- }
239
- }
240
- }
241
-
242
- fun listRecordings(context: Context): List<File> {
243
- val dir = File(context.cacheDir, RECORDINGS_DIR_NAME)
244
- if (!dir.isDirectory) return emptyList()
245
- return dir.listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList()
246
- }
247
-
248
- override fun seedOriginNs(timestampNs: Long): Long {
249
- val existing = recordingStartHostTimeNs
250
- if (existing != null) return existing
251
- recordingStartHostTimeNs = timestampNs
252
- return timestampNs
253
- }
254
-
255
- override fun onTrackAdded() {
256
- pendingPipelines = (pendingPipelines - 1).coerceAtLeast(0)
257
- muxer?.let { maybeStartMuxer(it) }
258
- }
259
-
260
- override fun onFatalError(error: Throwable) {
261
- fireTerminalCompletion(null, error)
262
- cleanupAfterFailure()
263
- }
264
-
265
- /**
266
- * Starts the muxer once every active pipeline has added its track. Calling `start()` before all
267
- * `addTrack` calls makes subsequent `addTrack` throw "Muxer is not initialized", so the gate is
268
- * load-bearing.
269
- */
270
- private fun maybeStartMuxer(muxerInstance: MediaMuxer) {
271
- if (muxerStarted) return
272
- if (pendingPipelines > 0) return
273
-
274
- try {
275
- muxerInstance.start()
276
- muxerStarted = true
277
- } catch (t: Throwable) {
278
- Log.e(TAG, "muxer.start() threw", t)
279
- fireTerminalCompletion(null, t)
280
- cleanupAfterFailure()
281
- }
282
- }
283
-
284
- private fun fireTerminalCompletion(file: File?, error: Throwable?) {
285
- if (isCompleted) return
286
- isCompleted = true
287
-
288
- val cb = recordingCompletion
289
- recordingCompletion = null
290
- cb?.invoke(file, error)
291
- }
292
-
293
- /**
294
- * Resets every transient field to its initial value. Single source of truth for "the manager is
295
- * between recordings". Does NOT release native resources — the caller must stop/release
296
- * encoders and the muxer before invoking this.
297
- */
298
- private fun resetTransientState() {
299
- muxer = null
300
- muxerStarted = false
301
- videoPipeline = null
302
- audioPipeline = null
303
- outputFile = null
304
- recordingCompletion = null
305
- isCompleted = false
306
- isRecording = false
307
- pendingPipelines = 0
308
- recordingStartHostTimeNs = null
309
- autoStopRunnable?.let { timerHandler.removeCallbacks(it) }
310
- autoStopRunnable = null
311
- }
312
-
313
- private fun cleanupAfterFailure() {
314
- videoPipeline?.detachSink()
315
- audioPipeline?.detachSink()
316
- videoPipeline?.stopAndRelease()
317
- audioPipeline?.stopAndRelease()
318
- try {
319
- muxer?.release()
320
- } catch (t: Throwable) {
321
- Log.w(TAG, "failed to release muxer", t)
322
- }
323
- resetTransientState()
324
- }
325
-
326
- private fun cleanupAfterStop() {
327
- resetTransientState()
328
- }
329
- }