@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.
- package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +81 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/AudioPipeline.kt +436 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/EncoderConstants.kt +17 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/PipelineHost.kt +36 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderPlaybackSamplesSink.kt +60 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/RecorderVideoSink.kt +31 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/TracksRecorderManager.kt +329 -0
- package/android/src/main/java/com/streamvideo/reactnative/recorder/VideoPipeline.kt +472 -0
- package/dist/commonjs/hooks/index.js +11 -0
- package/dist/commonjs/hooks/index.js.map +1 -1
- package/dist/commonjs/hooks/useLoopbackRecording.js +243 -0
- package/dist/commonjs/hooks/useLoopbackRecording.js.map +1 -0
- package/dist/commonjs/utils/internal/callingx/callingx.js +2 -2
- package/dist/commonjs/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/commonjs/version.js.map +1 -1
- package/dist/module/hooks/index.js +1 -0
- package/dist/module/hooks/index.js.map +1 -1
- package/dist/module/hooks/useLoopbackRecording.js +238 -0
- package/dist/module/hooks/useLoopbackRecording.js.map +1 -0
- package/dist/module/utils/internal/callingx/callingx.js +2 -2
- package/dist/module/utils/internal/callingx/callingx.js.map +1 -1
- package/dist/module/version.js +1 -1
- package/dist/module/version.js.map +1 -1
- package/dist/typescript/hooks/index.d.ts +1 -0
- package/dist/typescript/hooks/index.d.ts.map +1 -1
- package/dist/typescript/hooks/useLoopbackRecording.d.ts +85 -0
- package/dist/typescript/hooks/useLoopbackRecording.d.ts.map +1 -0
- package/dist/typescript/version.d.ts +1 -1
- package/dist/typescript/version.d.ts.map +1 -1
- package/ios/StreamVideoReactNative-Bridging-Header.h +2 -0
- package/ios/StreamVideoReactNative.m +81 -0
- package/ios/TracksRecorder/AudioPipeline.swift +270 -0
- package/ios/TracksRecorder/PipelineHost.swift +56 -0
- package/ios/TracksRecorder/RecorderAudioRenderTap.swift +154 -0
- package/ios/TracksRecorder/RecorderVideoSink.swift +137 -0
- package/ios/TracksRecorder/TracksRecorderManager.swift +327 -0
- package/ios/TracksRecorder/VideoPipeline.swift +297 -0
- package/package.json +8 -8
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useLoopbackRecording.ts +438 -0
- package/src/utils/internal/callingx/callingx.ts +2 -2
- 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":"AAAA;AACA;AACA;AACA;AACA,SAASA,QAAQ,QAAQ,cAAc;AAEvC,SAASC,yBAAyB,QAAQ,0BAA0B;AACpE,SAASC,6BAA6B,QAAQ,uBAAuB;AAMrE,SAASC,YAAY,EAAEC,iBAAiB,QAAQ,yBAAyB;AAEzE,MAAMC,cAAc,GAAGJ,yBAAyB,CAAC,CAAC;;AAElD;AACA;AACA;AACA,OAAO,SAASK,kBAAkBA,CAChCC,WAAyC,EACzCC,YAAkD,EAClDC,aAAiC,EACzB;EACR,IAAI,CAACF,WAAW,IAAI,CAACC,YAAY,IAAI,CAACC,aAAa,EAAE;IACnD,OAAO,MAAM;EACf;EAEA,IAAIC,KAAe,GAAG,EAAE;EAExB,IAAIH,WAAW,CAACI,MAAM,GAAG,CAAC,EAAE;IAC1B;IACAD,KAAK,GAAGH,WAAW,CAChBK,MAAM,CAAEC,MAAM,IAAKA,MAAM,CAACC,IAAI,CAACC,EAAE,KAAKN,aAAa,CAAC,CACpDO,GAAG,CAAEH,MAAM,IAAKA,MAAM,CAACC,IAAI,CAACG,IAAI,CAAC,CACjCL,MAAM,CAAEK,IAAI,IAAqBA,IAAI,KAAKC,SAAS,CAAC;EACzD,CAAC,MAAM,IAAIV,YAAY,CAACG,MAAM,GAAG,CAAC,EAAE;IAClC;IACAD,KAAK,GAAGF,YAAY,CACjBI,MAAM,CAAEO,WAAW,IAAKA,WAAW,CAACC,MAAM,KAAKX,aAAa,CAAC,CAC7DO,GAAG,CAAEG,WAAW,IAAKA,WAAW,CAACF,IAAI,CAAC,CACtCL,MAAM,CAACS,OAAO,CAAC;EACpB;;EAEA;EACA,IAAIX,KAAK,CAACC,MAAM,KAAK,CAAC,EAAE;IACtBD,KAAK,GAAG,CACNF,YAAY,CAACc,IAAI,CAAEH,WAAW,IAAKA,WAAW,CAACC,MAAM,KAAKX,aAAa,CAAC,EACpEQ,IAAI,IAAI,MAAM,CACnB;EACH;EAEA,OAAOP,KAAK,CAACa,IAAI,CAAC,CAAC,CAACC,IAAI,CAAC,IAAI,CAAC;AAChC;AAEA,SAASC,0BAA0BA,CAACC,IAAU,EAAU;EACtD,OACEA,IAAI,CAACC,KAAK,CAACC,MAAM,EAAEC,YAAY,IAC/BvB,kBAAkB,CAChBoB,IAAI,CAACC,KAAK,CAACG,OAAO,EAClBJ,IAAI,CAACC,KAAK,CAACnB,YAAY,EACvBkB,IAAI,CAACjB,aACP,CAAC;AAEL;AAEA,OAAO,eAAesB,oBAAoBA,CAACL,IAAU,EAAE;EACrD,IAAI,CAACrB,cAAc,IAAI,CAACA,cAAc,CAAC2B,OAAO,EAAE;
|
|
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":[]}
|
package/dist/module/version.js
CHANGED
|
@@ -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,
|
|
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,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,kBAAkB,CAAC"}
|
|
@@ -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
|
+
}
|