@stream-io/video-react-native-sdk 1.37.0 → 1.37.1-beta.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 (43) hide show
  1. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +81 -0
  2. package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +436 -0
  3. package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +17 -0
  4. package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +36 -0
  5. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +60 -0
  6. package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +31 -0
  7. package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +329 -0
  8. package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +472 -0
  9. package/dist/commonjs/hooks/index.js +11 -0
  10. package/dist/commonjs/hooks/index.js.map +1 -1
  11. package/dist/commonjs/hooks/useLoopbackRecording.js +243 -0
  12. package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -0
  13. package/dist/commonjs/utils/internal/callingx/callingx.js +2 -2
  14. package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
  15. package/dist/commonjs/version.js +1 -1
  16. package/dist/commonjs/version.js.map +1 -1
  17. package/dist/module/hooks/index.js +1 -0
  18. package/dist/module/hooks/index.js.map +1 -1
  19. package/dist/module/hooks/useLoopbackRecording.js +238 -0
  20. package/dist/module/hooks/useLoopbackRecording.js.map +1 -0
  21. package/dist/module/utils/internal/callingx/callingx.js +2 -2
  22. package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
  23. package/dist/module/version.js +1 -1
  24. package/dist/module/version.js.map +1 -1
  25. package/dist/typescript/hooks/index.d.ts +1 -0
  26. package/dist/typescript/hooks/index.d.ts.map +1 -1
  27. package/dist/typescript/hooks/useLoopbackRecording.d.ts +85 -0
  28. package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -0
  29. package/dist/typescript/version.d.ts +1 -1
  30. package/dist/typescript/version.d.ts.map +1 -1
  31. package/ios/StreamVideoReactNative-Bridging-Header.h +2 -0
  32. package/ios/StreamVideoReactNative.m +81 -0
  33. package/ios/TracksRecorder/AudioPipeline.swift +270 -0
  34. package/ios/TracksRecorder/PipelineHost.swift +56 -0
  35. package/ios/TracksRecorder/RecorderAudioRenderTap.swift +154 -0
  36. package/ios/TracksRecorder/RecorderVideoSink.swift +137 -0
  37. package/ios/TracksRecorder/TracksRecorderManager.swift +327 -0
  38. package/ios/TracksRecorder/VideoPipeline.swift +297 -0
  39. package/package.json +8 -8
  40. package/src/hooks/index.ts +1 -0
  41. package/src/hooks/useLoopbackRecording.ts +438 -0
  42. package/src/utils/internal/callingx/callingx.ts +2 -2
  43. package/src/version.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"names":["Platform","getCallingxLibIfAvailable","waitForAudioSessionActivation","CallingState","videoLoggerSystem","CallingxModule","getCallDisplayName","callMembers","participants","currentUserId","names","length","filter","member","user","id","map","name","undefined","participant","userId","Boolean","find","sort","join","getCallDisplayNameFromCall","call","state","custom","display_name","members","registerOutgoingCall","isSetup","isOutcomingCall","ringing","isCreatedByMe","logger","getLogger","debug","cid","callDisplayName","startCall","createdBy","settings","video","enabled","error","joinCallingxCall","activeCalls","isIncomingCall","startCallInCallingx","OS","isOngoingCallsEnabled","activeCallsToLeave","c","callingState","LEFT","activeCall","leave","reason","catch","e","displayIncomingCall","answerIncomingCall","endCallingxCall","isCallTracked","endCallWithReason"],"sourceRoot":"../../../../../src","sources":["utils/internal/callingx/callingx.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["Platform","getCallingxLibIfAvailable","waitForAudioSessionActivation","CallingState","videoLoggerSystem","CallingxModule","getCallDisplayName","callMembers","participants","currentUserId","names","length","filter","member","user","id","map","name","undefined","participant","userId","Boolean","find","sort","join","getCallDisplayNameFromCall","call","state","custom","display_name","members","registerOutgoingCall","isSetup","isSelfSubEnabled","isOutcomingCall","ringing","isCreatedByMe","logger","getLogger","debug","cid","callDisplayName","startCall","createdBy","settings","video","enabled","error","joinCallingxCall","activeCalls","isIncomingCall","startCallInCallingx","OS","isOngoingCallsEnabled","activeCallsToLeave","c","callingState","LEFT","activeCall","leave","reason","catch","e","displayIncomingCall","answerIncomingCall","endCallingxCall","isCallTracked","endCallWithReason"],"sourceRoot":"../../../../../src","sources":["utils/internal/callingx/callingx.ts"],"mappings":"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","ignoreList":[]}
@@ -1,2 +1,2 @@
1
- export const version = '1.37.0';
1
+ export const version = '1.37.1-beta.0';
2
2
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["version"],"sourceRoot":"../../src","sources":["version.ts"],"mappings":"AAAA,OAAO,MAAMA,OAAO,GAAG,QAAQ","ignoreList":[]}
1
+ {"version":3,"names":["version"],"sourceRoot":"../../src","sources":["version.ts"],"mappings":"AAAA,OAAO,MAAMA,OAAO,GAAG,eAAe","ignoreList":[]}
@@ -7,6 +7,7 @@ export * from './useIsInPiPMode';
7
7
  export * from './useAutoEnterPiPEffect';
8
8
  export * from './useScreenShareButton';
9
9
  export * from './useScreenShareAudioMixing';
10
+ export * from './useLoopbackRecording';
10
11
  export * from './useTrackDimensions';
11
12
  export * from './useScreenshot';
12
13
  export * from './useModeration';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,QAAQ,CAAC;AACvB,cAAc,iCAAiC,CAAC;AAChD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,QAAQ,CAAC;AACvB,cAAc,iCAAiC,CAAC;AAChD,cAAc,uCAAuC,CAAC;AACtD,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC"}
@@ -0,0 +1,85 @@
1
+ export type LoopbackRecordingState = 'idle' | 'awaiting-streams' | 'recording';
2
+ export type ResolvedStreams = {
3
+ audioTrack?: MediaStreamTrack;
4
+ videoTrack?: MediaStreamTrack;
5
+ };
6
+ export interface StartLoopbackRecordingOptions {
7
+ /**
8
+ * Whether to include the loopback video track in the recording.
9
+ * Defaults to `true`. Set to `false` for an audio-only recording.
10
+ * Audio is always recorded — there is no video-only mode.
11
+ */
12
+ includeVideo?: boolean;
13
+ /**
14
+ * Maximum recording duration in milliseconds, after which the
15
+ * recording auto-stops and finalises the file.
16
+ *
17
+ * Defaults to `10_000` (10 seconds). Clamped to
18
+ * `[5_000, 120_000]` (5 seconds — 2 minutes).
19
+ */
20
+ maxDurationMs?: number;
21
+ }
22
+ export interface UseLoopbackRecordingResult {
23
+ /**
24
+ * Start a recording. The hook waits internally for the SFU loopback
25
+ * streams to arrive on `localParticipant`, then begins recording.
26
+ *
27
+ * The returned promise resolves with the produced `file://` URI **at
28
+ * the recording's terminal moment** — whether that is the auto-stop
29
+ * timer expiring, an explicit `stopRecording` call, or a cleanup-
30
+ * driven stop on unmount/leave. Resolves with `null` if no file was
31
+ * produced (writer torn down before any buffer arrived, or
32
+ * `stopRecording` was called while still awaiting streams). Rejects
33
+ * on a fatal error, if a recording is already running, or if the
34
+ * stream-wait times out.
35
+ */
36
+ startRecording: (options?: StartLoopbackRecordingOptions) => Promise<string | null>;
37
+ /**
38
+ * Signal an early termination. While `awaiting-streams` this aborts
39
+ * the wait and the pending `startRecording` resolves with `null`.
40
+ * While `recording` this signals native finalisation and resolves
41
+ * once it completes.
42
+ */
43
+ stopRecording: () => Promise<void>;
44
+ /**
45
+ * Recursively delete every file under the SDK's recordings directory.
46
+ */
47
+ clearRecordings: () => Promise<void>;
48
+ /**
49
+ * List every `file://` URI in the SDK's recordings directory, sorted
50
+ * most-recent first. Returns an empty array if the directory doesn't
51
+ * exist yet.
52
+ */
53
+ getRecordings: () => Promise<string[]>;
54
+ /**
55
+ * Lifecycle phase of the recording, owned by the hook:
56
+ * - `'idle'`: no recording in progress.
57
+ * - `'awaiting-streams'`: `startRecording` was called but the SFU
58
+ * has not yet echoed the loopback tracks back.
59
+ * - `'recording'`: native pipeline is actively writing.
60
+ */
61
+ recordingState: LoopbackRecordingState;
62
+ /**
63
+ * The SFU loopback video stream on the local participant, when
64
+ * present. Identified by reference inequality against
65
+ * `call.camera.state.mediaStream`.
66
+ */
67
+ loopbackVideoStream?: MediaStream;
68
+ /**
69
+ * The SFU loopback audio stream on the local participant, when
70
+ * present. Identified by reference inequality against
71
+ * `call.microphone.state.mediaStream`.
72
+ */
73
+ loopbackAudioStream?: MediaStream;
74
+ }
75
+ /**
76
+ * Records the SFU loopback streams (audio + video) on the local participant
77
+ * to a local MP4 file. Designed for the `selfSubEnabled` pre-call test mode:
78
+ * the SFU echoes the caller's published tracks back through the Subscriber
79
+ * peer connection. The hook identifies the loopback streams on the local
80
+ * participant by reference inequality against
81
+ * `call.camera.state.mediaStream` / `call.microphone.state.mediaStream` —
82
+ * the canonical references to the local capture — and captures them.
83
+ */
84
+ export declare function useLoopbackRecording(): UseLoopbackRecordingResult;
85
+ //# sourceMappingURL=useLoopbackRecording.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useLoopbackRecording.d.ts","sourceRoot":"","sources":["../../../src/hooks/useLoopbackRecording.ts"],"names":[],"mappings":"AA4BA,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,kBAAkB,GAAG,WAAW,CAAC;AAE/E,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;;;;;;;;;;OAYG;IACH,cAAc,EAAE,CACd,OAAO,CAAC,EAAE,6BAA6B,KACpC,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC;;OAEG;IACH,eAAe,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC;;;;OAIG;IACH,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvC;;;;;;OAMG;IACH,cAAc,EAAE,sBAAsB,CAAC;IACvC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,WAAW,CAAC;IAClC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,WAAW,CAAC;CACnC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,0BAA0B,CAmNjE"}
@@ -1,2 +1,2 @@
1
- export declare const version = "1.37.0";
1
+ export declare const version = "1.37.1-beta.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,WAAW,CAAC"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,kBAAkB,CAAC"}
@@ -11,6 +11,8 @@
11
11
  #import <WebRTC/RTCVideoTrack.h>
12
12
  #import <WebRTC/RTCVideoRenderer.h>
13
13
  #import <WebRTC/RTCVideoFrameBuffer.h>
14
+ #import <WebRTC/RTCAudioTrack.h>
15
+ #import <WebRTC/RTCAudioRenderer.h>
14
16
  #import "WebRTCModule.h"
15
17
  #import "WebRTCModuleOptions.h"
16
18
 
@@ -20,6 +20,13 @@
20
20
  #import <stream_react_native_webrtc/stream_react_native_webrtc-Swift.h>
21
21
  #endif
22
22
 
23
+ // Import Swift-generated header for TracksRecorderManager and friends.
24
+ #if __has_include("stream_video_react_native-Swift.h")
25
+ #import "stream_video_react_native-Swift.h"
26
+ #elif __has_include(<stream_video_react_native/stream_video_react_native-Swift.h>)
27
+ #import <stream_video_react_native/stream_video_react_native-Swift.h>
28
+ #endif
29
+
23
30
  // Do not change these consts, it is what is used react-native-webrtc
24
31
  NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted";
25
32
  NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped";
@@ -685,4 +692,78 @@ RCT_EXPORT_METHOD(stopScreenShareAudioMixing:(RCTPromiseResolveBlock)resolve
685
692
  resolve(nil);
686
693
  }
687
694
 
695
+ #pragma mark - Track Recording
696
+
697
+ RCT_EXPORT_METHOD(startTrackRecording:(NSDictionary *)options
698
+ resolver:(RCTPromiseResolveBlock)resolve
699
+ rejecter:(RCTPromiseRejectBlock)reject)
700
+ {
701
+ WebRTCModule *webrtcModule = [self.bridge moduleForClass:[WebRTCModule class]];
702
+ if (!webrtcModule) {
703
+ reject(@"recording_error", @"WebRTCModule not available", nil);
704
+ return;
705
+ }
706
+
707
+ NSString *videoTrackId = options[@"videoTrackId"];
708
+ if (![videoTrackId isKindOfClass:[NSString class]]) videoTrackId = nil;
709
+
710
+ NSNumber *maxDuration = options[@"maxDurationMs"];
711
+ NSInteger maxDurationMs = ([maxDuration isKindOfClass:[NSNumber class]])
712
+ ? [maxDuration integerValue] : 5000;
713
+
714
+ NSNumber *targetW = options[@"targetWidth"];
715
+ NSInteger targetWidth = ([targetW isKindOfClass:[NSNumber class]])
716
+ ? [targetW integerValue] : 0;
717
+
718
+ NSNumber *targetH = options[@"targetHeight"];
719
+ NSInteger targetHeight = ([targetH isKindOfClass:[NSNumber class]])
720
+ ? [targetH integerValue] : 0;
721
+
722
+ [[TracksRecorderManager shared]
723
+ startRecordingWithVideoTrackId:videoTrackId
724
+ maxDurationMs:maxDurationMs
725
+ targetWidth:targetWidth
726
+ targetHeight:targetHeight
727
+ webRTCModule:webrtcModule
728
+ completion:^(NSURL * _Nullable fileURL, NSError * _Nullable err) {
729
+ if (err) {
730
+ reject(@"recording_error", err.localizedDescription, err);
731
+ } else {
732
+ resolve(fileURL ? fileURL.absoluteString : [NSNull null]);
733
+ }
734
+ }];
735
+ }
736
+
737
+ RCT_EXPORT_METHOD(stopTrackRecording:(RCTPromiseResolveBlock)resolve
738
+ rejecter:(RCTPromiseRejectBlock)reject)
739
+ {
740
+ [[TracksRecorderManager shared] stopRecordingWithCompletion:^{
741
+ resolve(nil);
742
+ }];
743
+ }
744
+
745
+ RCT_EXPORT_METHOD(clearStreamRecordings:(RCTPromiseResolveBlock)resolve
746
+ rejecter:(RCTPromiseRejectBlock)reject)
747
+ {
748
+ [[TracksRecorderManager shared] clearRecordingsDirectoryWithCompletion:^(NSError * _Nullable err) {
749
+ if (err) {
750
+ reject(@"clear_error", err.localizedDescription, err);
751
+ } else {
752
+ resolve(nil);
753
+ }
754
+ }];
755
+ }
756
+
757
+ RCT_EXPORT_METHOD(getStreamRecordings:(RCTPromiseResolveBlock)resolve
758
+ rejecter:(RCTPromiseRejectBlock)reject)
759
+ {
760
+ NSArray<NSURL *> *urls = [[TracksRecorderManager shared] listRecordings];
761
+ NSMutableArray<NSString *> *result = [NSMutableArray arrayWithCapacity:urls.count];
762
+ for (NSURL *url in urls) {
763
+ NSString *abs = url.absoluteString;
764
+ if (abs) [result addObject:abs];
765
+ }
766
+ resolve(result);
767
+ }
768
+
688
769
  @end
@@ -0,0 +1,270 @@
1
+ //
2
+ // Copyright © 2026 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVFoundation
6
+ import CoreMedia
7
+ import Foundation
8
+ import WebRTC
9
+
10
+ /// Audio pipeline owned by `TracksRecorderManager`. Encapsulates the AAC audio path:
11
+ /// - the `RecorderAudioRenderTap` installed on
12
+ /// `RTCDefaultAudioProcessingModule.renderPreProcessingDelegate`
13
+ /// (post-mix decoded audio, no per-track lookup required),
14
+ /// - the in-place speaker mute (`muteOriginal: true` on the tap; the tap
15
+ /// zero-fills the buffer after copying for recording),
16
+ /// - the AAC `AVAssetWriterInput` (writer-driven encode via
17
+ /// `outputSettings`),
18
+ /// - per-recording counters / PTS range surfaced via `logSummary` at stop.
19
+ ///
20
+ /// All state mutation runs on the host's serial queue. The tap's
21
+ /// callback runs on a WebRTC audio thread and re-dispatches onto
22
+ /// `host.queue` after copying the PCM buffer.
23
+ internal final class AudioPipeline {
24
+
25
+ private static let aacBitRate: NSNumber = NSNumber(value: 64_000)
26
+
27
+ private weak var host: PipelineHost?
28
+
29
+ private let apm: RTCDefaultAudioProcessingModule
30
+
31
+ private var renderTap: RecorderAudioRenderTap?
32
+ private var audioInput: AVAssetWriterInput?
33
+ private var inputAdded = false
34
+
35
+ // Diagnostic counters + PTS range, surfaced via [logSummary] at stop.
36
+ private var buffersReceived = 0
37
+ private var samplesAppended = 0
38
+ private var buffersDropped = 0
39
+ private var firstSamplePtsUs: Int64 = -1
40
+ private var lastSamplePtsUs: Int64 = -1
41
+
42
+ // MARK: - Init
43
+
44
+ init(host: PipelineHost, apm: RTCDefaultAudioProcessingModule) {
45
+ self.host = host
46
+ self.apm = apm
47
+ }
48
+
49
+ // MARK: - Public API
50
+
51
+ /// Install the render-tap as the APM's `renderPreProcessingDelegate`.
52
+ /// The tap copies PCM into a new buffer for recording AND zero-fills the
53
+ /// original (post-mix decoded audio) so the speaker plays silence —
54
+ /// this gives "audio in the file, silence at the speaker" without
55
+ /// disrupting the recording. The standard `track.setVolume(0)` /
56
+ /// `track.isEnabled = false` mutes apply *before* this tap and would
57
+ /// silence the recording too.
58
+ func start() {
59
+ let tap = RecorderAudioRenderTap(muteOriginal: true) { [weak self] pcmBuffer in
60
+ self?.handleAudioBuffer(pcmBuffer: pcmBuffer)
61
+ }
62
+ renderTap = tap
63
+ apm.renderPreProcessingDelegate = tap
64
+ }
65
+
66
+ /// On-queue. Clear the render-tap delegate slot — only if it still
67
+ /// points to this pipeline's tap. If another consumer has rotated in,
68
+ /// leave theirs alone.
69
+ func detachSink() {
70
+ if let tap = renderTap, apm.renderPreProcessingDelegate === tap {
71
+ apm.renderPreProcessingDelegate = nil
72
+ }
73
+ renderTap = nil
74
+ }
75
+
76
+ /// On-queue. Marks the asset-writer input as finished so the writer can
77
+ /// finalise.
78
+ func markInputAsFinished() {
79
+ audioInput?.markAsFinished()
80
+ }
81
+
82
+ func logSummary() {
83
+ let tapCalls = renderTap?.callCount ?? -1
84
+ let durationMs: Int64
85
+ if firstSamplePtsUs >= 0 && lastSamplePtsUs >= firstSamplePtsUs {
86
+ durationMs = (lastSamplePtsUs - firstSamplePtsUs) / 1000
87
+ } else {
88
+ durationMs = -1
89
+ }
90
+ NSLog(
91
+ "[TracksRecorder.Audio] summary received=%d appended=%d dropped=%d tapCalls=%d firstPtsUs=%lld lastPtsUs=%lld durationMs=%lld",
92
+ buffersReceived,
93
+ samplesAppended,
94
+ buffersDropped,
95
+ tapCalls,
96
+ firstSamplePtsUs,
97
+ lastSamplePtsUs,
98
+ durationMs
99
+ )
100
+ }
101
+
102
+ // MARK: - Tap → queue bridge
103
+
104
+ private func handleAudioBuffer(pcmBuffer: AVAudioPCMBuffer) {
105
+ // Unlike `VideoPipeline`'s `CVPixelBuffer` closure capture, an
106
+ // ARC-retained `AVAudioPCMBuffer` does *not* extend the lifetime
107
+ // of the underlying PCM samples — those live in WebRTC's
108
+ // render-buffer pool and are reused the moment this callback
109
+ // returns. A deep copy before the queue hop is mandatory.
110
+ guard let copy = AudioPipeline.deepCopyPCMBuffer(pcmBuffer) else { return }
111
+ guard let host = host else { return }
112
+
113
+ // `DispatchTime.now().uptimeNanoseconds` is the monotonic clock
114
+ // that matches `RTCVideoFrame.timeStampNs` on iOS — both reduce
115
+ // to `mach_absolute_time()` converted to nanoseconds, so the
116
+ // shared time origin works coherently across both pipelines.
117
+ let captureTimeNs = DispatchTime.now().uptimeNanoseconds
118
+ host.queue.async { [weak self] in
119
+ self?.handleAudioBufferOnQueue(pcmBuffer: copy, captureTimeNs: captureTimeNs)
120
+ }
121
+ }
122
+
123
+ private func handleAudioBufferOnQueue(pcmBuffer: AVAudioPCMBuffer, captureTimeNs: UInt64) {
124
+ guard let host = host, host.isRecording, let writer = host.assetWriter else { return }
125
+
126
+ // Lazy-create the writer's audio input on the first buffer. The
127
+ // input's settings depend on the runtime PCM format reported by
128
+ // WebRTC.
129
+ if audioInput == nil {
130
+ configureAudioInput(format: pcmBuffer.format, writer: writer)
131
+ }
132
+
133
+ let pts = presentationTime(host: host, timestampNs: captureTimeNs)
134
+
135
+ guard writer.status == .writing,
136
+ let audioInput = audioInput,
137
+ audioInput.isReadyForMoreMediaData else {
138
+ buffersDropped += 1
139
+ return
140
+ }
141
+
142
+ guard let sampleBuffer = AudioPipeline.makeSampleBuffer(from: pcmBuffer, pts: pts) else {
143
+ buffersDropped += 1
144
+ return
145
+ }
146
+
147
+ if audioInput.append(sampleBuffer) {
148
+ buffersReceived += 1
149
+ samplesAppended += 1
150
+ let ptsUs = Int64(CMTimeGetSeconds(pts) * 1_000_000)
151
+ if firstSamplePtsUs < 0 || ptsUs < firstSamplePtsUs {
152
+ firstSamplePtsUs = ptsUs
153
+ }
154
+ if ptsUs > lastSamplePtsUs {
155
+ lastSamplePtsUs = ptsUs
156
+ }
157
+ } else {
158
+ buffersDropped += 1
159
+ }
160
+ }
161
+
162
+ // MARK: - Asset writer input setup
163
+
164
+ private func configureAudioInput(format: AVAudioFormat, writer: AVAssetWriter) {
165
+ let settings: [String: Any] = [
166
+ AVFormatIDKey: NSNumber(value: kAudioFormatMPEG4AAC),
167
+ AVSampleRateKey: NSNumber(value: format.sampleRate),
168
+ AVNumberOfChannelsKey: NSNumber(value: format.channelCount),
169
+ AVEncoderBitRateKey: AudioPipeline.aacBitRate,
170
+ ]
171
+ let input = AVAssetWriterInput(mediaType: .audio, outputSettings: settings)
172
+ input.expectsMediaDataInRealTime = true
173
+
174
+ guard writer.canAdd(input) else {
175
+ NSLog("[TracksRecorder.Audio] writer cannot add audio input")
176
+ host?.onFatalError(makeRecorderError("audio_input_add_failed", code: 4))
177
+ return
178
+ }
179
+
180
+ writer.add(input)
181
+ audioInput = input
182
+ inputAdded = true
183
+ host?.onTrackAdded()
184
+ }
185
+
186
+ // MARK: - PCM → CMSampleBuffer helper
187
+
188
+ /// Converts an `AVAudioPCMBuffer` into a `CMSampleBuffer` suitable for
189
+ /// `AVAssetWriterInput.append`. Returns `nil` if any Core Media call
190
+ /// fails; the caller treats that as a dropped buffer.
191
+ private static func makeSampleBuffer(
192
+ from pcmBuffer: AVAudioPCMBuffer,
193
+ pts: CMTime
194
+ ) -> CMSampleBuffer? {
195
+ var formatDescription: CMAudioFormatDescription?
196
+ let createDescStatus = CMAudioFormatDescriptionCreate(
197
+ allocator: kCFAllocatorDefault,
198
+ asbd: pcmBuffer.format.streamDescription,
199
+ layoutSize: 0,
200
+ layout: nil,
201
+ magicCookieSize: 0,
202
+ magicCookie: nil,
203
+ extensions: nil,
204
+ formatDescriptionOut: &formatDescription
205
+ )
206
+ guard createDescStatus == noErr, let formatDesc = formatDescription else { return nil }
207
+
208
+ var sampleBuffer: CMSampleBuffer?
209
+ var timing = CMSampleTimingInfo(
210
+ duration: CMTime(value: 1, timescale: Int32(pcmBuffer.format.sampleRate)),
211
+ presentationTimeStamp: pts,
212
+ decodeTimeStamp: .invalid
213
+ )
214
+ let createStatus = CMSampleBufferCreate(
215
+ allocator: kCFAllocatorDefault,
216
+ dataBuffer: nil,
217
+ dataReady: false,
218
+ makeDataReadyCallback: nil,
219
+ refcon: nil,
220
+ formatDescription: formatDesc,
221
+ sampleCount: CMItemCount(pcmBuffer.frameLength),
222
+ sampleTimingEntryCount: 1,
223
+ sampleTimingArray: &timing,
224
+ sampleSizeEntryCount: 0,
225
+ sampleSizeArray: nil,
226
+ sampleBufferOut: &sampleBuffer
227
+ )
228
+ guard createStatus == noErr, let sb = sampleBuffer else { return nil }
229
+
230
+ let setStatus = CMSampleBufferSetDataBufferFromAudioBufferList(
231
+ sb,
232
+ blockBufferAllocator: kCFAllocatorDefault,
233
+ blockBufferMemoryAllocator: kCFAllocatorDefault,
234
+ flags: 0,
235
+ bufferList: pcmBuffer.audioBufferList
236
+ )
237
+ guard setStatus == noErr else { return nil }
238
+ return sb
239
+ }
240
+
241
+ /// Returns a deep copy of the supplied `AVAudioPCMBuffer`. WebRTC owns
242
+ /// the source buffer's backing memory only for the duration of the
243
+ /// render-tap callback; ARC retains the wrapper across the queue hop
244
+ /// but not the underlying PCM samples. Copying here lets the recorder
245
+ /// queue read the data later without racing WebRTC's render-buffer
246
+ /// reuse.
247
+ private static func deepCopyPCMBuffer(_ source: AVAudioPCMBuffer) -> AVAudioPCMBuffer? {
248
+ guard let copy = AVAudioPCMBuffer(
249
+ pcmFormat: source.format,
250
+ frameCapacity: source.frameCapacity
251
+ ) else { return nil }
252
+ copy.frameLength = source.frameLength
253
+ let frameLength = Int(source.frameLength)
254
+ let channelCount = Int(source.format.channelCount)
255
+ if let src = source.int16ChannelData, let dst = copy.int16ChannelData {
256
+ for ch in 0..<channelCount {
257
+ memcpy(dst[ch], src[ch], frameLength * MemoryLayout<Int16>.size)
258
+ }
259
+ } else if let src = source.floatChannelData, let dst = copy.floatChannelData {
260
+ for ch in 0..<channelCount {
261
+ memcpy(dst[ch], src[ch], frameLength * MemoryLayout<Float>.size)
262
+ }
263
+ } else if let src = source.int32ChannelData, let dst = copy.int32ChannelData {
264
+ for ch in 0..<channelCount {
265
+ memcpy(dst[ch], src[ch], frameLength * MemoryLayout<Int32>.size)
266
+ }
267
+ }
268
+ return copy
269
+ }
270
+ }
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright © 2026 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import AVFoundation
6
+ import CoreMedia
7
+ import Foundation
8
+
9
+ /// Internal coordination contract between `TracksRecorderManager` and its
10
+ /// per-kind pipelines (`VideoPipeline`, `AudioPipeline`). The pipelines own
11
+ /// their encoder + sink + drain logic; the host owns lifecycle, the asset
12
+ /// writer, the writer-start gate, the shared time origin, and the terminal-
13
+ /// completion barrier.
14
+ ///
15
+ /// Every method on this protocol is called from the host's serial queue —
16
+ /// pipelines must `host.queue.async { ... }` before calling back into the
17
+ /// host. The protocol is class-bound so pipelines can hold a `weak`
18
+ /// reference and avoid retain cycles.
19
+ internal protocol PipelineHost: AnyObject {
20
+ /// The recorder's serial dispatch queue.
21
+ var queue: DispatchQueue { get }
22
+
23
+ var assetWriter: AVAssetWriter? { get }
24
+
25
+ var isRecording: Bool { get }
26
+
27
+ /// Returns the recording's shared time origin in nanoseconds. The first
28
+ /// pipeline to deliver a sample seeds the origin with its timestamp;
29
+ /// subsequent calls return the established value.
30
+ func seedOriginNs(_ timestampNs: UInt64) -> UInt64
31
+
32
+ /// Pipeline has added an input to the writer. The host decrements its
33
+ /// pending-pipeline counter and starts the writer once all expected
34
+ /// pipelines have reported their input.
35
+ func onTrackAdded()
36
+
37
+ func onFatalError(_ error: NSError)
38
+ }
39
+
40
+ /// Maps an absolute monotonic timestamp (nanoseconds) to presentation time
41
+ /// relative to the recording's shared origin. The first sample from either
42
+ /// pipeline seeds the origin via `host.seedOriginNs`; later samples use
43
+ /// elapsed = timestamp − origin (clamped to 0).
44
+ internal func presentationTime(host: PipelineHost, timestampNs: UInt64) -> CMTime {
45
+ let origin = host.seedOriginNs(timestampNs)
46
+ let elapsed: Int64 = timestampNs >= origin ? Int64(timestampNs - origin) : 0
47
+ return CMTime(value: elapsed, timescale: 1_000_000_000)
48
+ }
49
+
50
+ internal func makeRecorderError(_ message: String, code: Int) -> NSError {
51
+ NSError(
52
+ domain: "io.stream.video.tracks-recorder",
53
+ code: code,
54
+ userInfo: [NSLocalizedDescriptionKey: message]
55
+ )
56
+ }