@stream-io/video-react-native-sdk 1.29.4 → 1.30.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 (73) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/commonjs/components/Call/CallContent/CallContent.js +13 -7
  3. package/dist/commonjs/components/Call/CallContent/CallContent.js.map +1 -1
  4. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js +50 -14
  5. package/dist/commonjs/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  6. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js +27 -0
  7. package/dist/commonjs/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  8. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  9. package/dist/commonjs/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  10. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js +12 -9
  11. package/dist/commonjs/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  12. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js +19 -4
  13. package/dist/commonjs/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  14. package/dist/commonjs/utils/hooks/index.js +0 -11
  15. package/dist/commonjs/utils/hooks/index.js.map +1 -1
  16. package/dist/commonjs/version.js +1 -1
  17. package/dist/module/components/Call/CallContent/CallContent.js +10 -4
  18. package/dist/module/components/Call/CallContent/CallContent.js.map +1 -1
  19. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js +52 -16
  20. package/dist/module/components/Call/CallContent/RTCViewPipIOS.js.map +1 -1
  21. package/dist/module/components/Call/CallContent/RTCViewPipNative.js +27 -0
  22. package/dist/module/components/Call/CallContent/RTCViewPipNative.js.map +1 -1
  23. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js +19 -10
  24. package/dist/module/components/Call/CallLayout/CallParticipantsGrid.js.map +1 -1
  25. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js +15 -12
  26. package/dist/module/components/Call/CallLayout/CallParticipantsSpotlight.js.map +1 -1
  27. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js +20 -5
  28. package/dist/module/components/Call/CallParticipantsList/CallParticipantsList.js.map +1 -1
  29. package/dist/module/utils/hooks/index.js +0 -1
  30. package/dist/module/utils/hooks/index.js.map +1 -1
  31. package/dist/module/version.js +1 -1
  32. package/dist/typescript/components/Call/CallContent/CallContent.d.ts.map +1 -1
  33. package/dist/typescript/components/Call/CallContent/RTCViewPipIOS.d.ts.map +1 -1
  34. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts +18 -0
  35. package/dist/typescript/components/Call/CallContent/RTCViewPipNative.d.ts.map +1 -1
  36. package/dist/typescript/components/Call/CallLayout/CallParticipantsGrid.d.ts.map +1 -1
  37. package/dist/typescript/components/Call/CallLayout/CallParticipantsSpotlight.d.ts.map +1 -1
  38. package/dist/typescript/components/Call/CallParticipantsList/CallParticipantsList.d.ts.map +1 -1
  39. package/dist/typescript/utils/hooks/index.d.ts +0 -1
  40. package/dist/typescript/utils/hooks/index.d.ts.map +1 -1
  41. package/dist/typescript/version.d.ts +1 -1
  42. package/ios/PictureInPicture/PictureInPictureAvatarView.swift +273 -0
  43. package/ios/PictureInPicture/PictureInPictureConnectionQualityIndicator.swift +162 -0
  44. package/ios/PictureInPicture/PictureInPictureContent.swift +173 -0
  45. package/ios/PictureInPicture/PictureInPictureContentState.swift +123 -0
  46. package/ios/PictureInPicture/PictureInPictureDelegateProxy.swift +89 -0
  47. package/ios/PictureInPicture/PictureInPictureEnforcedStopAdapter.swift +166 -0
  48. package/ios/PictureInPicture/PictureInPictureLogger.swift +16 -0
  49. package/ios/PictureInPicture/PictureInPictureParticipantOverlayView.swift +217 -0
  50. package/ios/PictureInPicture/PictureInPictureReconnectionView.swift +193 -0
  51. package/ios/PictureInPicture/StreamAVPictureInPictureVideoCallViewController.swift +125 -7
  52. package/ios/PictureInPicture/StreamPictureInPictureController.swift +237 -63
  53. package/ios/PictureInPicture/StreamPictureInPictureControllerProtocol.swift +30 -0
  54. package/ios/PictureInPicture/StreamPictureInPictureVideoRenderer.swift +384 -12
  55. package/ios/RTCViewPip.swift +187 -21
  56. package/ios/RTCViewPipManager.mm +9 -0
  57. package/ios/RTCViewPipManager.swift +3 -3
  58. package/package.json +3 -3
  59. package/src/components/Call/CallContent/CallContent.tsx +16 -8
  60. package/src/components/Call/CallContent/RTCViewPipIOS.tsx +81 -15
  61. package/src/components/Call/CallContent/RTCViewPipNative.tsx +36 -0
  62. package/src/components/Call/CallLayout/CallParticipantsGrid.tsx +28 -14
  63. package/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +19 -10
  64. package/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +20 -5
  65. package/src/utils/hooks/index.ts +0 -1
  66. package/src/version.ts +1 -1
  67. package/dist/commonjs/utils/hooks/useDebouncedValue.js +0 -24
  68. package/dist/commonjs/utils/hooks/useDebouncedValue.js.map +0 -1
  69. package/dist/module/utils/hooks/useDebouncedValue.js +0 -19
  70. package/dist/module/utils/hooks/useDebouncedValue.js.map +0 -1
  71. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts +0 -8
  72. package/dist/typescript/utils/hooks/useDebouncedValue.d.ts.map +0 -1
  73. package/src/utils/hooks/useDebouncedValue.ts +0 -21
@@ -1 +1 @@
1
- {"version":3,"file":"RTCViewPipNative.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallContent/RTCViewPipNative.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,cAAc,CAAA;KAAE,KAAK,IAAI,CAAC;CAChE,CAAC;AAKF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,QAMlD;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,QAaf;AAED;2CAC2C;AAC3C,eAAO,MAAM,gBAAgB,yFAmB5B,CAAC"}
1
+ {"version":3,"file":"RTCViewPipNative.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallContent/RTCViewPipNative.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAY1B,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,WAAW,EAAE,cAAc,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/D,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,yEAAyE;IACzE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uEAAuE;IACvE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAKF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,QAMlD;AAED,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,QAaf;AAED;2CAC2C;AAC3C,eAAO,MAAM,gBAAgB,yFAqC5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CallParticipantsGrid.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallLayout/CallParticipantsGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,EAEL,KAAK,kCAAkC,EACxC,MAAM,8CAA8C,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAIvE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,6BAA6B,GACnE,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,GAAG,sBAAsB,CAAC,GACrE,IAAI,CAAC,kCAAkC,EAAE,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IACvE;;OAEG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,6NAYlC,yBAAyB,sBAkF3B,CAAC"}
1
+ {"version":3,"file":"CallParticipantsGrid.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallLayout/CallParticipantsGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,EAEL,KAAK,kCAAkC,EACxC,MAAM,8CAA8C,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAIvE;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,6BAA6B,GACnE,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,GAAG,sBAAsB,CAAC,GACrE,IAAI,CAAC,kCAAkC,EAAE,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IACvE;;OAEG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,6NAYlC,yBAAyB,sBAgG3B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CallParticipantsSpotlight.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallLayout/CallParticipantsSpotlight.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AASvC,OAAO,EAEL,KAAK,kCAAkC,EACxC,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,6BAA6B,GACxE,IAAI,CACF,gBAAgB,EAChB,oBAAoB,GAAG,sBAAsB,GAAG,oBAAoB,CACrE,GACD,IAAI,CAAC,kCAAkC,EAAE,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IACvE;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,yBAAyB,GAAI,2NAYvC,8BAA8B,sBAwGhC,CAAC"}
1
+ {"version":3,"file":"CallParticipantsSpotlight.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallLayout/CallParticipantsSpotlight.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAS5D,OAAO,EAEL,KAAK,kCAAkC,EACxC,MAAM,8CAA8C,CAAC;AACtD,OAAO,EAEL,KAAK,6BAA6B,EACnC,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG,6BAA6B,GACxE,IAAI,CACF,gBAAgB,EAChB,oBAAoB,GAAG,sBAAsB,GAAG,oBAAoB,CACrE,GACD,IAAI,CAAC,kCAAkC,EAAE,iBAAiB,GAAG,QAAQ,CAAC,GAAG;IACvE;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,yBAAyB,GAAI,2NAYvC,8BAA8B,sBAiHhC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"CallParticipantsList.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallParticipantsList/CallParticipantsList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAMN,MAAM,OAAO,CAAC;AAQf,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EAC1B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAYvD,MAAM,MAAM,kCAAkC,GAC5C,6BAA6B,GAAG;IAC9B;;OAEG;IACH,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC;IACnE;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEJ;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,kCAAkC,GACxE,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,GAAG;IAC7C;;OAEG;IACH,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACvC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,4NAalC,yBAAyB,sBA+K3B,CAAC"}
1
+ {"version":3,"file":"CallParticipantsList.d.ts","sourceRoot":"","sources":["../../../../../src/components/Call/CallParticipantsList/CallParticipantsList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAMN,MAAM,OAAO,CAAC;AAQf,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,oBAAoB,EAC1B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAYvD,MAAM,MAAM,kCAAkC,GAC5C,6BAA6B,GAAG;IAC9B;;OAEG;IACH,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC;IACnE;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEJ;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,kCAAkC,GACxE,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,CAAC,GAAG;IAC7C;;OAEG;IACH,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACvC;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,4NAalC,yBAAyB,sBA8L3B,CAAC"}
@@ -1,3 +1,2 @@
1
- export * from './useDebouncedValue';
2
1
  export * from './usePrevious';
3
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/utils/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/utils/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const version = "1.29.4";
1
+ export declare const version = "1.30.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1,273 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import UIKit
6
+
7
+ /// A view that displays an avatar placeholder when video is disabled in PiP mode.
8
+ /// Shows either a loaded image from URL, initials, or a default person icon.
9
+ final class PictureInPictureAvatarView: UIView {
10
+
11
+ // MARK: - Properties
12
+
13
+ /// The participant's name, used to generate initials
14
+ var participantName: String? {
15
+ didSet {
16
+ PictureInPictureLogger.log("AvatarView.participantName didSet: '\(participantName ?? "nil")'")
17
+ updateInitials()
18
+ }
19
+ }
20
+
21
+ /// The URL string for the participant's profile image
22
+ var imageURL: String? {
23
+ didSet {
24
+ loadImage()
25
+ }
26
+ }
27
+
28
+ /// Whether video is enabled - when true, the avatar should be hidden (alpha = 0)
29
+ /// Note: We use alpha instead of isHidden to match upstream SwiftUI behavior.
30
+ /// Using isHidden can cause layout issues because iOS may skip layoutSubviews for hidden views.
31
+ var isVideoEnabled: Bool = true {
32
+ didSet {
33
+ updateVisibility()
34
+ // When becoming visible (video disabled), refresh content to ensure initials are shown
35
+ // This is needed when the same avatarView instance is reused across PiP sessions
36
+ if !isVideoEnabled {
37
+ PictureInPictureLogger.log("AvatarView isVideoEnabled=false, refreshing content")
38
+ updateInitials()
39
+ }
40
+ }
41
+ }
42
+
43
+ // MARK: - Private Properties
44
+
45
+ private let containerView: UIView = {
46
+ let view = UIView()
47
+ view.translatesAutoresizingMaskIntoConstraints = false
48
+ view.backgroundColor = UIColor(red: 0.12, green: 0.13, blue: 0.15, alpha: 1.0) // Dark background
49
+ return view
50
+ }()
51
+
52
+ private let avatarContainerView: UIView = {
53
+ let view = UIView()
54
+ view.translatesAutoresizingMaskIntoConstraints = false
55
+ view.backgroundColor = UIColor(red: 0.0, green: 0.47, blue: 1.0, alpha: 1.0) // Stream blue
56
+ view.clipsToBounds = true
57
+ return view
58
+ }()
59
+
60
+ private let initialsLabel: UILabel = {
61
+ let label = UILabel()
62
+ label.translatesAutoresizingMaskIntoConstraints = false
63
+ label.textColor = .white
64
+ label.textAlignment = .center
65
+ label.font = UIFont.systemFont(ofSize: 32, weight: .semibold)
66
+ label.adjustsFontSizeToFitWidth = true
67
+ label.minimumScaleFactor = 0.5
68
+ return label
69
+ }()
70
+
71
+ private let imageView: UIImageView = {
72
+ let imageView = UIImageView()
73
+ imageView.translatesAutoresizingMaskIntoConstraints = false
74
+ imageView.contentMode = .scaleAspectFill
75
+ imageView.clipsToBounds = true
76
+ imageView.isHidden = true
77
+ return imageView
78
+ }()
79
+
80
+ private let placeholderImageView: UIImageView = {
81
+ let imageView = UIImageView()
82
+ imageView.translatesAutoresizingMaskIntoConstraints = false
83
+ imageView.contentMode = .scaleAspectFit
84
+ imageView.tintColor = .white
85
+ // Use SF Symbol for person icon
86
+ if let personImage = UIImage(systemName: "person.fill") {
87
+ imageView.image = personImage
88
+ }
89
+ imageView.isHidden = true
90
+ return imageView
91
+ }()
92
+
93
+ private var currentImageLoadTask: URLSessionDataTask?
94
+ private var avatarSizeConstraints: [NSLayoutConstraint] = []
95
+
96
+ // MARK: - Lifecycle
97
+
98
+ override init(frame: CGRect) {
99
+ super.init(frame: frame)
100
+ setUp()
101
+ }
102
+
103
+ required init?(coder: NSCoder) {
104
+ fatalError("init(coder:) has not been implemented")
105
+ }
106
+
107
+ override func layoutSubviews() {
108
+ super.layoutSubviews()
109
+ PictureInPictureLogger.log("AvatarView layoutSubviews: bounds=\(bounds), isHidden=\(isHidden)")
110
+ updateAvatarSize()
111
+ }
112
+
113
+ // MARK: - Private Helpers
114
+
115
+ private func setUp() {
116
+ addSubview(containerView)
117
+ containerView.addSubview(avatarContainerView)
118
+ avatarContainerView.addSubview(initialsLabel)
119
+ avatarContainerView.addSubview(imageView)
120
+ avatarContainerView.addSubview(placeholderImageView)
121
+
122
+ NSLayoutConstraint.activate([
123
+ containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
124
+ containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
125
+ containerView.topAnchor.constraint(equalTo: topAnchor),
126
+ containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
127
+
128
+ avatarContainerView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
129
+ avatarContainerView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
130
+
131
+ initialsLabel.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor, constant: 4),
132
+ initialsLabel.trailingAnchor.constraint(equalTo: avatarContainerView.trailingAnchor, constant: -4),
133
+ initialsLabel.topAnchor.constraint(equalTo: avatarContainerView.topAnchor, constant: 4),
134
+ initialsLabel.bottomAnchor.constraint(equalTo: avatarContainerView.bottomAnchor, constant: -4),
135
+
136
+ imageView.leadingAnchor.constraint(equalTo: avatarContainerView.leadingAnchor),
137
+ imageView.trailingAnchor.constraint(equalTo: avatarContainerView.trailingAnchor),
138
+ imageView.topAnchor.constraint(equalTo: avatarContainerView.topAnchor),
139
+ imageView.bottomAnchor.constraint(equalTo: avatarContainerView.bottomAnchor),
140
+
141
+ placeholderImageView.centerXAnchor.constraint(equalTo: avatarContainerView.centerXAnchor),
142
+ placeholderImageView.centerYAnchor.constraint(equalTo: avatarContainerView.centerYAnchor),
143
+ placeholderImageView.widthAnchor.constraint(equalTo: avatarContainerView.widthAnchor, multiplier: 0.5),
144
+ placeholderImageView.heightAnchor.constraint(equalTo: avatarContainerView.heightAnchor, multiplier: 0.5)
145
+ ])
146
+
147
+ updateAvatarSize()
148
+ updateVisibility()
149
+ // Ensure initial content state is correct (show placeholder when no name/image)
150
+ updateInitials()
151
+ }
152
+
153
+ private func updateAvatarSize() {
154
+ // Remove old constraints
155
+ NSLayoutConstraint.deactivate(avatarSizeConstraints)
156
+
157
+ // Avatar size should be about 40% of the smaller dimension
158
+ let minDimension = min(bounds.width, bounds.height)
159
+ let avatarSize = max(minDimension * 0.4, 60) // Minimum 60pt
160
+
161
+ PictureInPictureLogger.log("AvatarView updateAvatarSize: bounds=\(bounds), minDimension=\(minDimension), avatarSize=\(avatarSize)")
162
+
163
+ avatarSizeConstraints = [
164
+ avatarContainerView.widthAnchor.constraint(equalToConstant: avatarSize),
165
+ avatarContainerView.heightAnchor.constraint(equalToConstant: avatarSize)
166
+ ]
167
+ NSLayoutConstraint.activate(avatarSizeConstraints)
168
+
169
+ // Force immediate layout to apply the new constraints
170
+ // This is needed because constraints set during layoutSubviews
171
+ // won't be resolved until the next layout pass otherwise
172
+ containerView.setNeedsLayout()
173
+ containerView.layoutIfNeeded()
174
+
175
+ // Update corner radius after layout is complete
176
+ avatarContainerView.layer.cornerRadius = avatarContainerView.bounds.width / 2
177
+
178
+ PictureInPictureLogger.log("AvatarView updateAvatarSize FINAL: avatarContainer.frame=\(avatarContainerView.frame)")
179
+ }
180
+
181
+ private func updateVisibility() {
182
+ // Hide avatar when video is enabled using alpha (not isHidden)
183
+ // Using alpha instead of isHidden ensures layoutSubviews is always called,
184
+ // which is critical for proper constraint-based layout. This matches
185
+ // upstream SwiftUI's opacity-based visibility switching.
186
+ let newAlpha: CGFloat = isVideoEnabled ? 0 : 1
187
+ PictureInPictureLogger.log("AvatarView updateVisibility: isVideoEnabled=\(isVideoEnabled), setting alpha=\(newAlpha)")
188
+ alpha = newAlpha
189
+
190
+ // Force layout update when becoming visible to ensure proper sizing
191
+ if !isVideoEnabled {
192
+ PictureInPictureLogger.log("AvatarView updateVisibility: becoming visible, forcing layout")
193
+ setNeedsLayout()
194
+ layoutIfNeeded()
195
+ }
196
+ }
197
+
198
+ private func updateInitials() {
199
+ guard let name = participantName, !name.isEmpty else {
200
+ PictureInPictureLogger.log("AvatarView updateInitials: no name, showing placeholder. avatarContainer.frame=\(avatarContainerView.frame)")
201
+ initialsLabel.text = nil
202
+ initialsLabel.isHidden = true
203
+ // Show placeholder when there's no image loaded
204
+ placeholderImageView.isHidden = imageView.image != nil
205
+ return
206
+ }
207
+
208
+ let initials = generateInitials(from: name)
209
+ PictureInPictureLogger.log("AvatarView updateInitials: name=\(name), initials=\(initials), imageView.image=\(imageView.image != nil ? "loaded" : "nil"), avatarContainer.frame=\(avatarContainerView.frame)")
210
+ initialsLabel.text = initials
211
+ initialsLabel.isHidden = imageView.image != nil
212
+ placeholderImageView.isHidden = true
213
+ }
214
+
215
+ private func generateInitials(from name: String) -> String {
216
+ let components = name.split(separator: " ")
217
+ if components.count >= 2 {
218
+ let first = components[0].prefix(1)
219
+ let last = components[1].prefix(1)
220
+ return "\(first)\(last)".uppercased()
221
+ } else if let first = components.first {
222
+ return String(first.prefix(2)).uppercased()
223
+ }
224
+ return ""
225
+ }
226
+
227
+ private func loadImage() {
228
+ // Cancel any existing task
229
+ currentImageLoadTask?.cancel()
230
+ currentImageLoadTask = nil
231
+
232
+ guard let urlString = imageURL, !urlString.isEmpty, let url = URL(string: urlString) else {
233
+ imageView.image = nil
234
+ imageView.isHidden = true
235
+ updateInitials()
236
+ return
237
+ }
238
+
239
+ let requestURLString = urlString
240
+
241
+ // Load image asynchronously
242
+ var requestTask: URLSessionDataTask?
243
+ let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
244
+ DispatchQueue.main.async { [weak self] in
245
+ guard let self = self else { return }
246
+ guard let requestTask else { return }
247
+ guard self.currentImageLoadTask === requestTask else { return }
248
+ defer { self.currentImageLoadTask = nil }
249
+
250
+ // Ignore stale/cancelled responses so only the latest request can mutate UI.
251
+ if let nsError = error as NSError?, nsError.code == NSURLErrorCancelled {
252
+ return
253
+ }
254
+ guard self.imageURL == requestURLString else { return }
255
+
256
+ guard error == nil, let data = data, let image = UIImage(data: data) else {
257
+ self.imageView.image = nil
258
+ self.imageView.isHidden = true
259
+ self.updateInitials()
260
+ return
261
+ }
262
+
263
+ self.imageView.image = image
264
+ self.imageView.isHidden = false
265
+ self.initialsLabel.isHidden = true
266
+ self.placeholderImageView.isHidden = true
267
+ }
268
+ }
269
+ requestTask = task
270
+ currentImageLoadTask = task
271
+ task.resume()
272
+ }
273
+ }
@@ -0,0 +1,162 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+
5
+ import UIKit
6
+
7
+ /// A view representing a connection quality indicator for Picture-in-Picture.
8
+ /// Displays three vertical bars that indicate connection quality levels:
9
+ /// - Excellent: All 3 bars green
10
+ /// - Good: 2 bars green, 1 bar gray
11
+ /// - Poor: 1 bar red, 2 bars gray
12
+ /// - Unknown: All bars hidden
13
+ /// This aligns with upstream stream-video-swift ConnectionQualityIndicator.
14
+ final class PictureInPictureConnectionQualityIndicator: UIView {
15
+
16
+ // MARK: - Connection Quality Enum
17
+
18
+ /// Connection quality levels matching the stream-video-swift/video-client enum
19
+ enum ConnectionQuality: Int {
20
+ case unspecified = 0 // Unknown
21
+ case poor = 1
22
+ case good = 2
23
+ case excellent = 3
24
+ }
25
+
26
+ // MARK: - Properties
27
+
28
+ /// The current connection quality level
29
+ var connectionQuality: ConnectionQuality = .unspecified {
30
+ didSet {
31
+ updateIndicator()
32
+ }
33
+ }
34
+
35
+ /// Size of the indicator view
36
+ private let indicatorSize: CGFloat = 24
37
+
38
+ /// Width of each bar
39
+ private let barWidth: CGFloat = 3
40
+
41
+ /// Spacing between bars
42
+ private let barSpacing: CGFloat = 2
43
+
44
+ // MARK: - Colors
45
+
46
+ private let goodColor = UIColor(red: 0.2, green: 0.8, blue: 0.4, alpha: 1.0) // Green
47
+ private let badColor = UIColor(red: 0.9, green: 0.3, blue: 0.3, alpha: 1.0) // Red
48
+ private let inactiveColor = UIColor.white.withAlphaComponent(0.5)
49
+
50
+ // MARK: - UI Components
51
+
52
+ /// Background container with rounded corner
53
+ private lazy var containerView: UIView = {
54
+ let view = UIView()
55
+ view.translatesAutoresizingMaskIntoConstraints = false
56
+ view.backgroundColor = UIColor.black.withAlphaComponent(0.6)
57
+ // Apply rounded corner only to top-left
58
+ view.layer.cornerRadius = 8
59
+ view.layer.maskedCorners = [.layerMinXMinYCorner] // top-left only
60
+ return view
61
+ }()
62
+
63
+ /// Stack view containing the three bars
64
+ private lazy var barsStackView: UIStackView = {
65
+ let stack = UIStackView()
66
+ stack.translatesAutoresizingMaskIntoConstraints = false
67
+ stack.axis = .horizontal
68
+ stack.alignment = .bottom
69
+ stack.spacing = barSpacing
70
+ stack.distribution = .equalSpacing
71
+ return stack
72
+ }()
73
+
74
+ /// First (shortest) bar
75
+ private lazy var bar1: UIView = {
76
+ createBar(height: barWidth * 2)
77
+ }()
78
+
79
+ /// Second (medium) bar
80
+ private lazy var bar2: UIView = {
81
+ createBar(height: barWidth * 3)
82
+ }()
83
+
84
+ /// Third (tallest) bar
85
+ private lazy var bar3: UIView = {
86
+ createBar(height: barWidth * 4)
87
+ }()
88
+
89
+ // MARK: - Initialization
90
+
91
+ override init(frame: CGRect) {
92
+ super.init(frame: frame)
93
+ setUp()
94
+ }
95
+
96
+ required init?(coder: NSCoder) {
97
+ fatalError("init(coder:) has not been implemented")
98
+ }
99
+
100
+ // MARK: - Private Methods
101
+
102
+ private func setUp() {
103
+ isUserInteractionEnabled = false
104
+ isHidden = true // Hidden by default (unknown quality)
105
+
106
+ addSubview(containerView)
107
+ containerView.addSubview(barsStackView)
108
+
109
+ barsStackView.addArrangedSubview(bar1)
110
+ barsStackView.addArrangedSubview(bar2)
111
+ barsStackView.addArrangedSubview(bar3)
112
+
113
+ NSLayoutConstraint.activate([
114
+ containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
115
+ containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
116
+ containerView.widthAnchor.constraint(equalToConstant: indicatorSize),
117
+ containerView.heightAnchor.constraint(equalToConstant: indicatorSize),
118
+
119
+ barsStackView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
120
+ barsStackView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor)
121
+ ])
122
+
123
+ updateIndicator()
124
+ }
125
+
126
+ private func createBar(height: CGFloat) -> UIView {
127
+ let bar = UIView()
128
+ bar.translatesAutoresizingMaskIntoConstraints = false
129
+ bar.backgroundColor = inactiveColor
130
+ bar.layer.cornerRadius = 1
131
+ bar.layer.masksToBounds = true
132
+
133
+ NSLayoutConstraint.activate([
134
+ bar.widthAnchor.constraint(equalToConstant: barWidth),
135
+ bar.heightAnchor.constraint(equalToConstant: height)
136
+ ])
137
+
138
+ return bar
139
+ }
140
+
141
+ private func updateIndicator() {
142
+ switch connectionQuality {
143
+ case .excellent:
144
+ isHidden = false
145
+ bar1.backgroundColor = goodColor
146
+ bar2.backgroundColor = goodColor
147
+ bar3.backgroundColor = goodColor
148
+ case .good:
149
+ isHidden = false
150
+ bar1.backgroundColor = goodColor
151
+ bar2.backgroundColor = goodColor
152
+ bar3.backgroundColor = inactiveColor
153
+ case .poor:
154
+ isHidden = false
155
+ bar1.backgroundColor = badColor
156
+ bar2.backgroundColor = inactiveColor
157
+ bar3.backgroundColor = inactiveColor
158
+ case .unspecified:
159
+ isHidden = true
160
+ }
161
+ }
162
+ }
@@ -0,0 +1,173 @@
1
+ //
2
+ // Copyright © 2024 Stream.io Inc. All rights reserved.
3
+ //
4
+ // Adapted from stream-video-swift for React Native SDK
5
+ // Original: https://github.com/GetStream/stream-video-swift/blob/develop/Sources/StreamVideoSwiftUI/Utils/PictureInPicture/PictureInPictureContent.swift
6
+ //
7
+
8
+ import Foundation
9
+
10
+ /// Represents the content state for the Picture-in-Picture window.
11
+ ///
12
+ /// This enum defines the different states that the PiP window can display:
13
+ /// - `inactive`: No content is being shown (PiP is not active)
14
+ /// - `video`: Live video from a participant (camera or screen share)
15
+ /// - `avatar`: Participant avatar placeholder (when video is disabled)
16
+ /// - `screenSharing`: Screen share content with indicator overlay
17
+ /// - `reconnecting`: Connection recovery indicator
18
+ ///
19
+ /// The React Native SDK receives content state from the JavaScript layer through
20
+ /// the bridge, unlike the upstream Swift SDK which observes call state internally.
21
+ enum PictureInPictureContent: Equatable, CustomStringConvertible {
22
+ /// No content - PiP is inactive or transitioning
23
+ case inactive
24
+
25
+ /// Video content from a participant
26
+ /// - Parameters:
27
+ /// - track: The WebRTC video track to render
28
+ /// - participantName: The participant's display name (for fallback)
29
+ /// - participantImageURL: URL to participant's profile image (for fallback)
30
+ case video(track: RTCVideoTrack?, participantName: String?, participantImageURL: String?)
31
+
32
+ /// Screen sharing content
33
+ /// - Parameters:
34
+ /// - track: The WebRTC video track containing screen share
35
+ /// - participantName: Name of the participant sharing their screen
36
+ case screenSharing(track: RTCVideoTrack?, participantName: String?)
37
+
38
+ /// Avatar placeholder shown when video is disabled
39
+ /// - Parameters:
40
+ /// - participantName: The participant's display name (for initials)
41
+ /// - participantImageURL: URL to participant's profile image
42
+ case avatar(participantName: String?, participantImageURL: String?)
43
+
44
+ /// Connection recovery indicator
45
+ case reconnecting
46
+
47
+ // MARK: - CustomStringConvertible
48
+
49
+ var description: String {
50
+ switch self {
51
+ case .inactive:
52
+ return ".inactive"
53
+ case let .video(track, name, _):
54
+ return ".video(track:\(track?.trackId ?? "nil"), name:\(name ?? "-"))"
55
+ case let .screenSharing(track, name):
56
+ return ".screenSharing(track:\(track?.trackId ?? "nil"), name:\(name ?? "-"))"
57
+ case let .avatar(name, _):
58
+ return ".avatar(name:\(name ?? "-"))"
59
+ case .reconnecting:
60
+ return ".reconnecting"
61
+ }
62
+ }
63
+
64
+ // MARK: - Equatable
65
+
66
+ static func == (lhs: PictureInPictureContent, rhs: PictureInPictureContent) -> Bool {
67
+ switch (lhs, rhs) {
68
+ case (.inactive, .inactive):
69
+ return true
70
+ case let (.video(lhsTrack, lhsName, lhsImage), .video(rhsTrack, rhsName, rhsImage)):
71
+ return isSameTrackInstance(lhsTrack, rhsTrack)
72
+ && lhsName == rhsName
73
+ && lhsImage == rhsImage
74
+ case let (.screenSharing(lhsTrack, lhsName), .screenSharing(rhsTrack, rhsName)):
75
+ return isSameTrackInstance(lhsTrack, rhsTrack)
76
+ && lhsName == rhsName
77
+ case let (.avatar(lhsName, lhsImage), .avatar(rhsName, rhsImage)):
78
+ return lhsName == rhsName
79
+ && lhsImage == rhsImage
80
+ case (.reconnecting, .reconnecting):
81
+ return true
82
+ default:
83
+ return false
84
+ }
85
+ }
86
+
87
+ /// Track identity must be reference-based so reconnect-created tracks
88
+ /// with reused `trackId` still propagate through content updates.
89
+ private static func isSameTrackInstance(_ lhs: RTCVideoTrack?, _ rhs: RTCVideoTrack?) -> Bool {
90
+ switch (lhs, rhs) {
91
+ case (nil, nil):
92
+ return true
93
+ case let (lhsTrack?, rhsTrack?):
94
+ return lhsTrack === rhsTrack
95
+ default:
96
+ return false
97
+ }
98
+ }
99
+
100
+ // MARK: - Convenience Properties
101
+
102
+ /// Returns the video track if this content has one, nil otherwise
103
+ var track: RTCVideoTrack? {
104
+ switch self {
105
+ case let .video(track, _, _):
106
+ return track
107
+ case let .screenSharing(track, _):
108
+ return track
109
+ case .inactive, .avatar, .reconnecting:
110
+ return nil
111
+ }
112
+ }
113
+
114
+ /// Returns the participant name if available
115
+ var participantName: String? {
116
+ switch self {
117
+ case let .video(_, name, _):
118
+ return name
119
+ case let .screenSharing(_, name):
120
+ return name
121
+ case let .avatar(name, _):
122
+ return name
123
+ case .inactive, .reconnecting:
124
+ return nil
125
+ }
126
+ }
127
+
128
+ /// Returns the participant image URL if available
129
+ var participantImageURL: String? {
130
+ switch self {
131
+ case let .video(_, _, imageURL):
132
+ return imageURL
133
+ case let .avatar(_, imageURL):
134
+ return imageURL
135
+ case .inactive, .screenSharing, .reconnecting:
136
+ return nil
137
+ }
138
+ }
139
+
140
+ /// Whether this content represents an active video stream
141
+ var hasActiveVideo: Bool {
142
+ switch self {
143
+ case .video, .screenSharing:
144
+ return true
145
+ case .inactive, .avatar, .reconnecting:
146
+ return false
147
+ }
148
+ }
149
+
150
+ /// Whether this content is screen sharing
151
+ var isScreenSharing: Bool {
152
+ if case .screenSharing = self {
153
+ return true
154
+ }
155
+ return false
156
+ }
157
+
158
+ /// Whether this content shows an avatar
159
+ var isShowingAvatar: Bool {
160
+ if case .avatar = self {
161
+ return true
162
+ }
163
+ return false
164
+ }
165
+
166
+ /// Whether this content shows the reconnection view
167
+ var isReconnecting: Bool {
168
+ if case .reconnecting = self {
169
+ return true
170
+ }
171
+ return false
172
+ }
173
+ }