@stream-io/video-react-native-sdk 1.20.16 → 1.21.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 (60) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/com/streamvideo/reactnative/StreamVideoReactNativeModule.kt +127 -0
  4. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer.js +1 -1
  5. package/dist/commonjs/components/Participant/ParticipantView/VideoRenderer.js.map +1 -1
  6. package/dist/commonjs/providers/BusyTonePlayer.js +54 -0
  7. package/dist/commonjs/providers/BusyTonePlayer.js.map +1 -0
  8. package/dist/commonjs/providers/StreamVideo.js +3 -2
  9. package/dist/commonjs/providers/StreamVideo.js.map +1 -1
  10. package/dist/commonjs/utils/StreamVideoRN/index.js +16 -0
  11. package/dist/commonjs/utils/StreamVideoRN/index.js.map +1 -1
  12. package/dist/commonjs/utils/push/android.js +2 -0
  13. package/dist/commonjs/utils/push/android.js.map +1 -1
  14. package/dist/commonjs/utils/push/internal/ios.js +5 -0
  15. package/dist/commonjs/utils/push/internal/ios.js.map +1 -1
  16. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js +2 -2
  17. package/dist/commonjs/utils/push/setupIosVoipPushEvents.js.map +1 -1
  18. package/dist/commonjs/version.js +1 -1
  19. package/dist/commonjs/version.js.map +1 -1
  20. package/dist/module/components/Participant/ParticipantView/VideoRenderer.js +1 -1
  21. package/dist/module/components/Participant/ParticipantView/VideoRenderer.js.map +1 -1
  22. package/dist/module/providers/BusyTonePlayer.js +48 -0
  23. package/dist/module/providers/BusyTonePlayer.js.map +1 -0
  24. package/dist/module/providers/StreamVideo.js +3 -2
  25. package/dist/module/providers/StreamVideo.js.map +1 -1
  26. package/dist/module/utils/StreamVideoRN/index.js +16 -0
  27. package/dist/module/utils/StreamVideoRN/index.js.map +1 -1
  28. package/dist/module/utils/push/android.js +2 -0
  29. package/dist/module/utils/push/android.js.map +1 -1
  30. package/dist/module/utils/push/internal/ios.js +5 -0
  31. package/dist/module/utils/push/internal/ios.js.map +1 -1
  32. package/dist/module/utils/push/setupIosVoipPushEvents.js +2 -2
  33. package/dist/module/utils/push/setupIosVoipPushEvents.js.map +1 -1
  34. package/dist/module/version.js +1 -1
  35. package/dist/module/version.js.map +1 -1
  36. package/dist/typescript/components/Participant/ParticipantView/VideoRenderer.d.ts.map +1 -1
  37. package/dist/typescript/providers/BusyTonePlayer.d.ts +6 -0
  38. package/dist/typescript/providers/BusyTonePlayer.d.ts.map +1 -0
  39. package/dist/typescript/providers/StreamVideo.d.ts.map +1 -1
  40. package/dist/typescript/utils/StreamVideoRN/index.d.ts +9 -0
  41. package/dist/typescript/utils/StreamVideoRN/index.d.ts.map +1 -1
  42. package/dist/typescript/utils/StreamVideoRN/types.d.ts.map +1 -1
  43. package/dist/typescript/utils/push/android.d.ts.map +1 -1
  44. package/dist/typescript/utils/push/internal/ios.d.ts.map +1 -1
  45. package/dist/typescript/version.d.ts +1 -1
  46. package/dist/typescript/version.d.ts.map +1 -1
  47. package/expo-config-plugin/dist/withAppDelegate.js +20 -0
  48. package/ios/StreamVideoReactNative.h +4 -0
  49. package/ios/StreamVideoReactNative.m +276 -0
  50. package/ios/StreamVideoReactNative.xcodeproj/project.pbxproj +29 -1
  51. package/package.json +5 -5
  52. package/src/components/Participant/ParticipantView/VideoRenderer.tsx +1 -6
  53. package/src/providers/BusyTonePlayer.tsx +71 -0
  54. package/src/providers/StreamVideo.tsx +4 -4
  55. package/src/utils/StreamVideoRN/index.ts +16 -0
  56. package/src/utils/StreamVideoRN/types.ts +1 -0
  57. package/src/utils/push/android.ts +3 -0
  58. package/src/utils/push/internal/ios.ts +8 -0
  59. package/src/utils/push/setupIosVoipPushEvents.ts +2 -2
  60. package/src/version.ts +1 -1
@@ -3,9 +3,12 @@
3
3
  #import <React/RCTUIManager.h>
4
4
  #import <React/RCTUIManagerUtils.h>
5
5
  #import <UIKit/UIKit.h>
6
+ #import <CallKit/CallKit.h>
6
7
  #import "StreamVideoReactNative.h"
7
8
  #import "WebRTCModule.h"
8
9
  #import "WebRTCModuleOptions.h"
10
+ #import <AVFoundation/AVFoundation.h>
11
+ #import <AudioToolbox/AudioToolbox.h>
9
12
 
10
13
  // Do not change these consts, it is what is used react-native-webrtc
11
14
  NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted";
@@ -15,6 +18,8 @@ static NSMutableDictionary *_incomingCallUUIDsByCallID = nil;
15
18
  static NSMutableDictionary *_incomingCallCidsByUUID = nil;
16
19
  static dispatch_queue_t _dictionaryQueue = nil;
17
20
 
21
+ static BOOL _shouldRejectCallWhenBusy = NO;
22
+
18
23
  void broadcastNotificationCallback(CFNotificationCenterRef center,
19
24
  void *observer,
20
25
  CFStringRef name,
@@ -25,10 +30,14 @@ void broadcastNotificationCallback(CFNotificationCenterRef center,
25
30
  [this screenShareEventReceived: eventName];
26
31
  }
27
32
 
33
+ @interface StreamVideoReactNative () <AVAudioPlayerDelegate>
34
+ @end
35
+
28
36
  @implementation StreamVideoReactNative
29
37
  {
30
38
  bool hasListeners;
31
39
  CFNotificationCenterRef _notificationCenter;
40
+ AVAudioPlayer *_busyTonePlayer; // Instance variable
32
41
  }
33
42
 
34
43
  // necessary for addUIBlock usage https://github.com/facebook/react-native/issues/50800#issuecomment-2823327307
@@ -77,6 +86,13 @@ RCT_EXPORT_METHOD(currentThermalState:(RCTPromiseResolveBlock)resolve rejecter:(
77
86
  }
78
87
 
79
88
  -(void)dealloc {
89
+ if (_busyTonePlayer) {
90
+ if (_busyTonePlayer.isPlaying) {
91
+ [_busyTonePlayer stop];
92
+ }
93
+ _busyTonePlayer = nil;
94
+ [self removeAudioInterruptionHandling];
95
+ }
80
96
  [self clearObserver];
81
97
  }
82
98
 
@@ -356,4 +372,264 @@ RCT_EXPORT_METHOD(getBatteryState:(RCTPromiseResolveBlock)resolve
356
372
  ];
357
373
  }
358
374
 
375
+ +(BOOL)shouldRejectCallWhenBusy {
376
+ return _shouldRejectCallWhenBusy;
377
+ }
378
+
379
+ RCT_EXPORT_METHOD(setShouldRejectCallWhenBusy:(BOOL)shouldReject) {
380
+ _shouldRejectCallWhenBusy = shouldReject;
381
+ #ifdef DEBUG
382
+ NSLog(@"setShouldRejectCallWhenBusy: %@", shouldReject ? @"YES" : @"NO");
383
+ #endif
384
+ }
385
+
386
+ + (BOOL)hasAnyActiveCall
387
+ {
388
+ CXCallObserver *callObserver = [[CXCallObserver alloc] init];
389
+
390
+ for(CXCall *call in callObserver.calls){
391
+ if(call.hasConnected){
392
+ NSLog(@"[RNCallKeep] Found active call with UUID: %@", call.UUID);
393
+ return YES;
394
+ }
395
+ }
396
+ return NO;
397
+ }
398
+
399
+
400
+ RCT_EXPORT_METHOD(playBusyTone:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
401
+ {
402
+ dispatch_async(dispatch_get_main_queue(), ^{
403
+ [self stopBusyTone]; // Stop any existing playback first
404
+
405
+ // Configure audio session
406
+ NSError *sessionError = nil;
407
+ AVAudioSession *session = [AVAudioSession sharedInstance];
408
+ [session setCategory:AVAudioSessionCategoryPlayback
409
+ mode:AVAudioSessionModeDefault
410
+ options:AVAudioSessionCategoryOptionMixWithOthers
411
+ error:&sessionError];
412
+
413
+ if (sessionError) {
414
+ NSString *errorMsg = [NSString stringWithFormat:@"Failed to set audio session category: %@", sessionError.localizedDescription];
415
+ NSLog(@"%@", errorMsg);
416
+ reject(@"AUDIO_SESSION_ERROR", errorMsg, sessionError);
417
+ return;
418
+ }
419
+
420
+ [session setActive:YES error:&sessionError];
421
+ if (sessionError) {
422
+ NSString *errorMsg = [NSString stringWithFormat:@"Failed to activate audio session: %@", sessionError.localizedDescription];
423
+ NSLog(@"%@", errorMsg);
424
+ reject(@"AUDIO_SESSION_ERROR", errorMsg, sessionError);
425
+ return;
426
+ }
427
+
428
+ // Generate busy tone data
429
+ NSData *busyToneData = [self generateBusyToneData];
430
+ if (!busyToneData || busyToneData.length == 0) {
431
+ NSString *errorMsg = @"Failed to generate busy tone data";
432
+ NSLog(@"%@", errorMsg);
433
+ reject(@"AUDIO_GENERATION_ERROR", errorMsg, nil);
434
+ return;
435
+ }
436
+
437
+ // Create audio player
438
+ NSError *playerError = nil;
439
+ self->_busyTonePlayer = [[AVAudioPlayer alloc] initWithData:busyToneData error:&playerError];
440
+
441
+ if (playerError || !self->_busyTonePlayer) {
442
+ NSString *errorMsg = [NSString stringWithFormat:@"Failed to create audio player: %@", playerError.localizedDescription];
443
+ NSLog(@"%@", errorMsg);
444
+ reject(@"AUDIO_PLAYER_ERROR", errorMsg, playerError);
445
+ return;
446
+ }
447
+
448
+ // Configure player
449
+ self->_busyTonePlayer.delegate = self;
450
+ self->_busyTonePlayer.numberOfLoops = -1; // Loop indefinitely
451
+ self->_busyTonePlayer.volume = 0.5; // Set reasonable volume
452
+
453
+ // Setup audio interruption handling
454
+ [self setupAudioInterruptionHandling];
455
+
456
+ // Prepare and play
457
+ BOOL prepared = [self->_busyTonePlayer prepareToPlay];
458
+ if (!prepared) {
459
+ NSString *errorMsg = @"Failed to prepare audio player";
460
+ NSLog(@"%@", errorMsg);
461
+ reject(@"AUDIO_PLAYER_ERROR", errorMsg, nil);
462
+ return;
463
+ }
464
+
465
+ BOOL playing = [self->_busyTonePlayer play];
466
+ if (playing) {
467
+ resolve(@YES);
468
+ } else {
469
+ NSString *errorMsg = @"Failed to start audio playback";
470
+ NSLog(@"%@", errorMsg);
471
+ reject(@"AUDIO_PLAYBACK_ERROR", errorMsg, nil);
472
+ }
473
+ });
474
+ }
475
+
476
+ - (NSData *)generateBusyToneData {
477
+ // Generate 1 second of busy tone pattern: 0.5s 480Hz tone, 0.5s silence
478
+ const int sampleRate = 44100; // Standard sample rate for better quality
479
+ const float duration = 1.0; // 1 second total
480
+ const float beepDuration = 0.5; // 0.5 seconds beep
481
+ const float frequency = 480.0; // 480 Hz busy tone frequency
482
+
483
+ int totalSamples = (int)(duration * sampleRate);
484
+
485
+ // Create PCM data buffer with proper size calculation
486
+ NSMutableData *audioData = [NSMutableData dataWithLength:44 + totalSamples * 2]; // WAV header + 16-bit samples
487
+ uint8_t *bytes = (uint8_t *)[audioData mutableBytes];
488
+
489
+ // Helper function to write little-endian values
490
+ void (^writeLittleEndian32)(uint8_t *, uint32_t) = ^(uint8_t *dest, uint32_t value) {
491
+ dest[0] = value & 0xFF;
492
+ dest[1] = (value >> 8) & 0xFF;
493
+ dest[2] = (value >> 16) & 0xFF;
494
+ dest[3] = (value >> 24) & 0xFF;
495
+ };
496
+
497
+ void (^writeLittleEndian16)(uint8_t *, uint16_t) = ^(uint8_t *dest, uint16_t value) {
498
+ dest[0] = value & 0xFF;
499
+ dest[1] = (value >> 8) & 0xFF;
500
+ };
501
+
502
+ // Write WAV header with proper endianness
503
+ memcpy(bytes, "RIFF", 4);
504
+ writeLittleEndian32(bytes + 4, 36 + totalSamples * 2);
505
+ memcpy(bytes + 8, "WAVE", 4);
506
+ memcpy(bytes + 12, "fmt ", 4);
507
+ writeLittleEndian32(bytes + 16, 16); // PCM format chunk size
508
+ writeLittleEndian16(bytes + 20, 1); // PCM format
509
+ writeLittleEndian16(bytes + 22, 1); // Mono
510
+ writeLittleEndian32(bytes + 24, sampleRate);
511
+ writeLittleEndian32(bytes + 28, sampleRate * 2); // Bytes per second
512
+ writeLittleEndian16(bytes + 32, 2); // Bytes per sample
513
+ writeLittleEndian16(bytes + 34, 16); // Bits per sample
514
+ memcpy(bytes + 36, "data", 4);
515
+ writeLittleEndian32(bytes + 40, totalSamples * 2);
516
+
517
+ // Generate audio samples
518
+ int16_t *samples = (int16_t *)(bytes + 44);
519
+ for (int i = 0; i < totalSamples; i++) {
520
+ float t = (float)i / sampleRate;
521
+ float cycleTime = fmod(t, duration); // Time within current cycle
522
+
523
+ if (cycleTime < beepDuration) {
524
+ // Generate 480Hz sine wave with proper amplitude
525
+ float sineWave = sinf(2.0f * M_PI * frequency * t);
526
+ float amplitude = 0.4f * sineWave; // Increased amplitude for better audibility
527
+ samples[i] = CFSwapInt16HostToLittle((int16_t)(amplitude * 32767.0f));
528
+ } else {
529
+ // Silence period
530
+ samples[i] = 0;
531
+ }
532
+ }
533
+
534
+ return audioData;
535
+ }
536
+
537
+ RCT_EXPORT_METHOD(stopBusyTone:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
538
+ {
539
+ dispatch_async(dispatch_get_main_queue(), ^{
540
+ [self stopBusyTone];
541
+ resolve(@YES);
542
+ });
543
+ }
544
+
545
+ - (void)stopBusyTone {
546
+ if (_busyTonePlayer) {
547
+ if (_busyTonePlayer.isPlaying) {
548
+ [_busyTonePlayer stop];
549
+ }
550
+
551
+ _busyTonePlayer = nil;
552
+
553
+ // Remove audio interruption observers
554
+ [self removeAudioInterruptionHandling];
555
+
556
+ // Only deactivate audio session if there are no active calls
557
+ // This prevents interfering with ongoing WebRTC audio sessions
558
+ BOOL hasActiveCall = [StreamVideoReactNative hasAnyActiveCall];
559
+ if (!hasActiveCall) {
560
+ NSError *error = nil;
561
+ [[AVAudioSession sharedInstance] setActive:NO
562
+ withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
563
+ error:&error];
564
+ if (error) {
565
+ NSLog(@"Error deactivating audio session: %@", error.localizedDescription);
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ #pragma mark - AVAudioPlayerDelegate
572
+
573
+ - (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {
574
+ // Audio finished - this shouldn't happen with infinite loops
575
+ // Check if this is our player and handle cleanup if needed
576
+ if (player == _busyTonePlayer) {
577
+ _busyTonePlayer = nil;
578
+ }
579
+ }
580
+
581
+ - (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {
582
+ if (player == _busyTonePlayer) {
583
+ _busyTonePlayer = nil;
584
+ }
585
+ }
586
+
587
+ // Note: audioPlayerBeginInterruption and audioPlayerEndInterruption are deprecated
588
+ // Audio interruptions are now handled via AVAudioSession notifications
589
+ // These are registered in setupAudioInterruptionHandling method
590
+
591
+ #pragma mark - Audio Interruption Handling
592
+
593
+ - (void)setupAudioInterruptionHandling {
594
+ [[NSNotificationCenter defaultCenter] addObserver:self
595
+ selector:@selector(audioSessionInterrupted:)
596
+ name:AVAudioSessionInterruptionNotification
597
+ object:[AVAudioSession sharedInstance]];
598
+ }
599
+
600
+ - (void)removeAudioInterruptionHandling {
601
+ [[NSNotificationCenter defaultCenter] removeObserver:self
602
+ name:AVAudioSessionInterruptionNotification
603
+ object:[AVAudioSession sharedInstance]];
604
+ }
605
+
606
+ - (void)audioSessionInterrupted:(NSNotification *)notification {
607
+ AVAudioSessionInterruptionType interruptionType = [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
608
+
609
+ switch (interruptionType) {
610
+ case AVAudioSessionInterruptionTypeBegan:
611
+ if (_busyTonePlayer && _busyTonePlayer.isPlaying) {
612
+ [_busyTonePlayer pause];
613
+ }
614
+ break;
615
+
616
+ case AVAudioSessionInterruptionTypeEnded: {
617
+ AVAudioSessionInterruptionOptions options = [notification.userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
618
+
619
+ if (options & AVAudioSessionInterruptionOptionShouldResume) {
620
+ // Reactivate audio session
621
+ NSError *error = nil;
622
+ [[AVAudioSession sharedInstance] setActive:YES error:&error];
623
+
624
+ if (!error && _busyTonePlayer) {
625
+ [_busyTonePlayer play];
626
+ } else if (error) {
627
+ NSLog(@"Failed to reactivate audio session after interruption: %@", error.localizedDescription);
628
+ }
629
+ }
630
+ break;
631
+ }
632
+ }
633
+ }
634
+
359
635
  @end
@@ -3,7 +3,7 @@
3
3
  archiveVersion = 1;
4
4
  classes = {
5
5
  };
6
- objectVersion = 46;
6
+ objectVersion = 70;
7
7
  objects = {
8
8
 
9
9
  /* Begin PBXCopyFilesBuildPhase section */
@@ -42,6 +42,21 @@
42
42
  F4FF95D5245B92E700C19C63 /* StreamVideoReactNative-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "StreamVideoReactNative-Bridging-Header.h"; sourceTree = "<group>"; };
43
43
  /* End PBXFileReference section */
44
44
 
45
+ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
46
+ 05DBB1DF2E6B3F0600309346 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
47
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
48
+ membershipExceptions = (
49
+ busy.mp3,
50
+ );
51
+ target = 58B511DA1A9E6C8500147676 /* StreamVideoReactNative */;
52
+ };
53
+ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
54
+
55
+ /* Begin PBXFileSystemSynchronizedRootGroup section */
56
+ 05DBB1DA2E6B3ECE00309346 /* New Folder */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = "New Folder"; sourceTree = "<group>"; };
57
+ 05DBB1DB2E6B3EEC00309346 /* Resources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (05DBB1DF2E6B3F0600309346 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Resources; sourceTree = "<group>"; };
58
+ /* End PBXFileSystemSynchronizedRootGroup section */
59
+
45
60
  /* Begin PBXFrameworksBuildPhase section */
46
61
  58B511D81A9E6C8500147676 /* Frameworks */ = {
47
62
  isa = PBXFrameworksBuildPhase;
@@ -64,6 +79,8 @@
64
79
  58B511D21A9E6C8500147676 = {
65
80
  isa = PBXGroup;
66
81
  children = (
82
+ 05DBB1DB2E6B3EEC00309346 /* Resources */,
83
+ 05DBB1DA2E6B3ECE00309346 /* New Folder */,
67
84
  DDCF7DCC2C90359D00420842 /* PictureInPicture */,
68
85
  DDCF7DCA2C90359D00420842 /* RTCViewPip.swift */,
69
86
  DDCF7DC92C90359D00420842 /* RTCViewPipManager.mm */,
@@ -114,6 +131,7 @@
114
131
  58B511D71A9E6C8500147676 /* Sources */,
115
132
  58B511D81A9E6C8500147676 /* Frameworks */,
116
133
  58B511D91A9E6C8500147676 /* CopyFiles */,
134
+ 05DBB1DD2E6B3F0100309346 /* Resources */,
117
135
  );
118
136
  buildRules = (
119
137
  );
@@ -156,6 +174,16 @@
156
174
  };
157
175
  /* End PBXProject section */
158
176
 
177
+ /* Begin PBXResourcesBuildPhase section */
178
+ 05DBB1DD2E6B3F0100309346 /* Resources */ = {
179
+ isa = PBXResourcesBuildPhase;
180
+ buildActionMask = 2147483647;
181
+ files = (
182
+ );
183
+ runOnlyForDeploymentPostprocessing = 0;
184
+ };
185
+ /* End PBXResourcesBuildPhase section */
186
+
159
187
  /* Begin PBXSourcesBuildPhase section */
160
188
  58B511D71A9E6C8500147676 /* Sources */ = {
161
189
  isa = PBXSourcesBuildPhase;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-react-native-sdk",
3
- "version": "1.20.16",
3
+ "version": "1.21.0",
4
4
  "description": "Stream Video SDK for React Native",
5
5
  "author": "https://getstream.io",
6
6
  "homepage": "https://getstream.io/video/docs/react-native/",
@@ -45,8 +45,8 @@
45
45
  "!**/.*"
46
46
  ],
47
47
  "dependencies": {
48
- "@stream-io/video-client": "1.31.0",
49
- "@stream-io/video-react-bindings": "1.8.3",
48
+ "@stream-io/video-client": "1.32.0",
49
+ "@stream-io/video-react-bindings": "1.8.4",
50
50
  "intl-pluralrules": "2.0.1",
51
51
  "lodash.merge": "^4.6.2",
52
52
  "react-native-url-polyfill": "1.3.0",
@@ -125,9 +125,9 @@
125
125
  "@react-native-firebase/app": "^22.1.0",
126
126
  "@react-native-firebase/messaging": "^22.1.0",
127
127
  "@react-native/babel-preset": "^0.79.2",
128
- "@stream-io/noise-cancellation-react-native": "^0.2.4",
128
+ "@stream-io/noise-cancellation-react-native": "^0.3.0",
129
129
  "@stream-io/react-native-webrtc": "125.4.3",
130
- "@stream-io/video-filters-react-native": "^0.6.3",
130
+ "@stream-io/video-filters-react-native": "^0.7.0",
131
131
  "@testing-library/jest-native": "^5.4.3",
132
132
  "@testing-library/react-native": "13.2.0",
133
133
  "@tsconfig/node14": "14.1.3",
@@ -104,12 +104,7 @@ export const VideoRenderer = ({
104
104
  isParticipantVideoEnabled(participant.sessionId);
105
105
 
106
106
  useEffect(() => {
107
- if (
108
- Platform.OS === 'ios' &&
109
- registerIosScreenshot &&
110
- viewRef.current &&
111
- canShowVideo
112
- ) {
107
+ if (Platform.OS === 'ios' && registerIosScreenshot && canShowVideo) {
113
108
  registerIosScreenshot(participant, trackType, viewRef);
114
109
  return () => {
115
110
  deregisterIosScreenshot(participant, trackType);
@@ -0,0 +1,71 @@
1
+ import { useEffect } from 'react';
2
+ import { useStreamVideoClient } from '@stream-io/video-react-bindings';
3
+ import { StreamVideoRN } from '../utils';
4
+ import { getLogger } from '@stream-io/video-client';
5
+
6
+ const BUSY_TONE_DURATION_IN_MS = 1500;
7
+
8
+ /**
9
+ * This is a renderless component to play a busy tone for call rejection when the callee is busy for the user's outgoing call.
10
+ */
11
+ const BusyTonePlayer = () => {
12
+ const client = useStreamVideoClient();
13
+
14
+ useEffect(() => {
15
+ if (!client) {
16
+ return;
17
+ }
18
+
19
+ const unsubscribe = client.on('call.rejected', async (event) => {
20
+ const isCallCreatedByMe =
21
+ event.call.created_by.id === client.state.connectedUser?.id;
22
+ const isCalleeBusy = isCallCreatedByMe && event.reason === 'busy';
23
+
24
+ let busyToneTimeout: ReturnType<typeof setTimeout> | undefined;
25
+
26
+ const logger = getLogger(['RejectCallWhenBusy']);
27
+
28
+ if (isCalleeBusy) {
29
+ if (busyToneTimeout) {
30
+ clearTimeout(busyToneTimeout);
31
+ busyToneTimeout = undefined;
32
+ }
33
+ logger(
34
+ 'info',
35
+ `Playing busy tone for call rejection for call cid: ${event.call.cid}`,
36
+ );
37
+
38
+ StreamVideoRN.playBusyTone()
39
+ .then(() => {
40
+ busyToneTimeout = setTimeout(() => {
41
+ StreamVideoRN.stopBusyTone()
42
+ .then(() => {
43
+ logger(
44
+ 'info',
45
+ `Stopped busy tone for call rejection for call cid: ${event.call.cid}`,
46
+ );
47
+ })
48
+ .catch((error) =>
49
+ logger('error', 'stopBusyTone failed:', error),
50
+ );
51
+ busyToneTimeout = undefined;
52
+ }, BUSY_TONE_DURATION_IN_MS);
53
+ })
54
+ .catch((error) => {
55
+ logger('error', 'playBusyTone failed:', error);
56
+ });
57
+ }
58
+ return () => {
59
+ StreamVideoRN.stopBusyTone().catch((err) =>
60
+ logger('error', 'stopBusyTone on cleanup failed:', err),
61
+ );
62
+ clearTimeout(busyToneTimeout);
63
+ unsubscribe();
64
+ };
65
+ });
66
+ }, [client]);
67
+
68
+ return null;
69
+ };
70
+
71
+ export default BusyTonePlayer;
@@ -10,6 +10,7 @@ import { translations } from '../translations';
10
10
  import { type DeepPartial, StreamTheme } from '../contexts/ThemeContext';
11
11
  import { type Theme } from '../theme/theme';
12
12
  import { ScreenshotIosContextProvider } from '../contexts/internal/ScreenshotIosContext';
13
+ import BusyTonePlayer from './BusyTonePlayer';
13
14
 
14
15
  /**
15
16
  *
@@ -59,11 +60,10 @@ export const StreamVideo = (
59
60
  translationsOverrides={translationsOverrides}
60
61
  i18nInstance={i18nInstance}
61
62
  >
63
+ <PushRegister />
64
+ <BusyTonePlayer />
62
65
  <StreamTheme style={style}>
63
- <ScreenshotIosContextProvider>
64
- <PushRegister />
65
- {children}
66
- </ScreenshotIosContextProvider>
66
+ <ScreenshotIosContextProvider>{children}</ScreenshotIosContextProvider>
67
67
  </StreamTheme>
68
68
  </StreamVideoProvider>
69
69
  );
@@ -5,6 +5,7 @@ import newNotificationCallbacks, {
5
5
  } from '../internal/newNotificationCallbacks';
6
6
  import { setupIosCallKeepEvents } from '../push/setupIosCallKeepEvents';
7
7
  import { setupIosVoipPushEvents } from '../push/setupIosVoipPushEvents';
8
+ import { NativeModules } from 'react-native';
8
9
 
9
10
  // Utility type for deep partial
10
11
  type DeepPartial<T> = {
@@ -62,6 +63,7 @@ const DEFAULT_STREAM_VIDEO_CONFIG: StreamVideoConfig = {
62
63
 
63
64
  export class StreamVideoRN {
64
65
  private static config = DEFAULT_STREAM_VIDEO_CONFIG;
66
+ private static busyToneTimeout: NodeJS.Timeout | null = null;
65
67
 
66
68
  /**
67
69
  * Update the global config for StreamVideoRN except for push config.
@@ -164,4 +166,18 @@ export class StreamVideoRN {
164
166
  newNotificationCallbacks.current?.filter((cb) => cb !== callback);
165
167
  };
166
168
  }
169
+
170
+ /**
171
+ * Play native busy tone for call rejection
172
+ */
173
+ static async playBusyTone() {
174
+ return NativeModules.StreamVideoReactNative?.playBusyTone();
175
+ }
176
+
177
+ /**
178
+ * Stop native busy tone
179
+ */
180
+ static async stopBusyTone() {
181
+ return NativeModules.StreamVideoReactNative?.stopBusyTone();
182
+ }
167
183
  }
@@ -24,6 +24,7 @@ export type StreamVideoConfig = {
24
24
  * @internal
25
25
  */
26
26
  publishOptions?: ClientPublishOptions;
27
+
27
28
  ios: {
28
29
  /**
29
30
  * The name for the alias of push provider used for iOS
@@ -169,6 +169,9 @@ export const firebaseDataHandler = async (
169
169
  const created_by_id = data.created_by_id as string;
170
170
  const receiver_id = data.receiver_id as string;
171
171
 
172
+ const video_client = await pushConfig.createStreamVideoClient();
173
+ await video_client?.onRingingCall(call_cid);
174
+
172
175
  const shouldCallBeClosed = (callToCheck: Call) => {
173
176
  const { mustEndCall } = shouldCallBeEnded(
174
177
  callToCheck,
@@ -46,6 +46,7 @@ export const onVoipNotificationReceived = async (
46
46
  }
47
47
  const logger = getLogger(['setupIosVoipPushEvents']);
48
48
  const client = await pushConfig.createStreamVideoClient();
49
+
49
50
  if (!client) {
50
51
  logger(
51
52
  'debug',
@@ -53,6 +54,13 @@ export const onVoipNotificationReceived = async (
53
54
  );
54
55
  return;
55
56
  }
57
+ const shouldRejectCallWhenBusy = client['rejectCallWhenBusy'] ?? false;
58
+ if (shouldRejectCallWhenBusy) {
59
+ // inform the iOS native module that we should reject call when busy
60
+ NativeModules.StreamVideoReactNative.setShouldRejectCallWhenBusy(
61
+ shouldRejectCallWhenBusy,
62
+ );
63
+ }
56
64
  const callFromPush = await client.onRingingCall(call_cid);
57
65
  let uuid = '';
58
66
  try {
@@ -13,11 +13,11 @@ export function setupIosVoipPushEvents(
13
13
  return;
14
14
  }
15
15
  const logger = getLogger(['setupIosVoipPushEvents']);
16
- if (!pushConfig.android.incomingCallChannel) {
16
+ if (!pushConfig.ios.pushProviderName) {
17
17
  // TODO: remove this check and find a better way once we have telecom integration for android
18
18
  logger(
19
19
  'debug',
20
- 'android incomingCallChannel is not defined, so skipping the setupIosVoipPushEvents',
20
+ 'ios pushProviderName is not defined, so skipping the setupIosVoipPushEvents',
21
21
  );
22
22
  return;
23
23
  }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '1.20.16';
1
+ export const version = '1.21.0';