blix-expo-settings 0.1.9 → 0.1.11

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.
@@ -8,12 +8,14 @@ public class ExpoSettingsModule: Module {
8
8
  private var rtmpStream: RTMPStream?
9
9
  private var currentStreamStatus: String = "stopped"
10
10
 
11
+
11
12
  public func definition() -> ModuleDefinition {
12
13
  Name("ExpoSettings")
13
14
 
14
- // View component (preview)
15
+ // Registra o view component para o admin se enxergar na live
16
+
15
17
  View(ExpoSettingsView.self) {
16
- // sem props
18
+ // não precisa colocar nada aqui se você não tiver Props
17
19
  }
18
20
 
19
21
  Events("onStreamStatus")
@@ -24,65 +26,186 @@ public class ExpoSettingsModule: Module {
24
26
 
25
27
  Function("initializePreview") { () -> Void in
26
28
  Task {
27
- self.setStatus("previewInitializing")
28
-
29
- // 0) Configura AVAudioSession
30
- self.configureAudioSession()
29
+ self.currentStreamStatus = "previewInitializing"
30
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
31
+
32
+ do {
33
+
34
+ // 0) Configura e ativa o AVAudioSession
35
+ let session = AVAudioSession.sharedInstance()
36
+ do {
37
+ try session.setCategory(.playAndRecord,
38
+ mode: .default,
39
+ options: [.defaultToSpeaker, .allowBluetooth])
40
+ try session.setActive(true)
41
+ } catch {
42
+ print("[ExpoSettings] AVAudioSession error:", error)
43
+ }
31
44
 
32
- // 1) Cria connection + stream
45
+ // 1) Conectar ao servidor RTMP, mas não publica
33
46
  let connection = RTMPConnection()
34
47
  self.rtmpConnection = connection
35
48
 
49
+ // 2) Criar RTMPStream, mas não publica pro servidor ainda
36
50
  let stream = RTMPStream(connection: connection)
37
51
  self.rtmpStream = stream
52
+ print("[ExpoSettings] RTMPStream initialized")
53
+
54
+ // 3) Configurar captura: frame rate e preset
55
+ stream.sessionPreset = .hd1280x720
56
+ stream.frameRate = 30
57
+ stream.videoOrientation = .portrait
58
+ stream.configuration { captureSession in
59
+ captureSession.automaticallyConfiguresApplicationAudioSession = true
60
+ }
38
61
 
39
- // 2) Configura stream (tudo que influencia proporção/encode)
40
- self.configureStream(stream)
41
-
42
- // 3) Attach áudio
43
- self.attachAudioIfAvailable(stream)
44
-
45
- // 4) Attach câmera frontal (portrait + mirror)
46
- await self.attachFrontCamera(stream)
62
+ // 4) Configurar áudio: anexa microfone
63
+ if let audioDevice = AVCaptureDevice.default(for: .audio) {
64
+ print("[ExpoSettings] Attaching audio device")
65
+ stream.attachAudio(audioDevice)
66
+ } else {
67
+ print("[ExpoSettings] No audio device found")
68
+ }
47
69
 
48
- // 5) Attach preview (MainActor)
49
- await self.attachPreviewIfAvailable(stream)
70
+ // 5) Configurar vídeo: anexa câmera frontal
71
+ if let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
72
+ for: .video,
73
+ position: .front) {
74
+ print("[ExpoSettings] Attaching camera device")
75
+ stream.attachCamera(camera) { videoUnit, error in
76
+ guard let unit = videoUnit else {
77
+ print("[ExpoSettings] attachCamera error:", error?.localizedDescription ?? "unknown")
78
+ return
79
+ }
80
+ unit.isVideoMirrored = true
81
+ unit.videoOrientation = .portrait
82
+ unit.preferredVideoStabilizationMode = .standard
83
+ unit.colorFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
84
+
85
+ }
86
+ if let preview = await ExpoSettingsView.current {
87
+ print("[ExpoSettings] Attaching stream to preview view")
88
+ await preview.attachStream(stream)
89
+ } else {
90
+ print("[ExpoSettings] ERROR: Preview view not found!")
91
+ }
92
+ } else {
93
+ print("[ExpoSettings] No camera device found")
94
+ }
50
95
 
51
- self.setStatus("previewReady")
96
+ //6) Definir configurações de codec
97
+ print("[ExpoSettings] Setting audio and video codecs")
98
+ var audioSettings = AudioCodecSettings()
99
+ audioSettings.bitRate = 128 * 1000
100
+ stream.audioSettings = audioSettings
101
+
102
+ let videoSettings = VideoCodecSettings(
103
+ videoSize: .init(width: 720, height: 1280),
104
+ bitRate: 4000 * 1000,
105
+ profileLevel: kVTProfileLevel_H264_Baseline_3_1 as String,
106
+ scalingMode: .trim,
107
+ bitRateMode: .average,
108
+ maxKeyFrameIntervalDuration: 2,
109
+ allowFrameReordering: nil,
110
+ isHardwareEncoderEnabled: true
111
+ )
112
+ stream.videoSettings = videoSettings
113
+ }
114
+ self.currentStreamStatus = "previewReady"
115
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
52
116
  }
53
117
  }
54
118
 
55
119
  Function("publishStream") { (url: String, streamKey: String) -> Void in
56
120
  Task {
121
+
57
122
  print("[ExpoSettings] Publishing stream to URL: \(url) with key: \(streamKey)")
58
- self.setStatus("connecting")
59
123
 
60
- // Caso não tenha initializePreview, cria do zero com config completa
61
- if self.rtmpConnection == nil || self.rtmpStream == nil {
62
- print("[ExpoSettings] WARNING: Connection or stream not initialized. Creating new ones.")
124
+ self.currentStreamStatus = "connecting"
125
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
63
126
 
127
+ // se não houve initializePreview→recria a connection
128
+ if self.rtmpConnection == nil || self.rtmpStream == nil {
129
+ print("[ExpoSettings] WARNING: Connection or stream not initialized, creating new ones")
130
+ // Create new connection
64
131
  let connection = RTMPConnection()
65
132
  self.rtmpConnection = connection
66
133
 
134
+ // Create new stream
67
135
  let stream = RTMPStream(connection: connection)
68
136
  self.rtmpStream = stream
69
137
 
70
- self.configureStream(stream)
71
- self.attachAudioIfAvailable(stream)
72
- await self.attachFrontCamera(stream)
73
- await self.attachPreviewIfAvailable(stream)
138
+ // Captura: preset antes do FPS + orientação no stream
139
+ stream.sessionPreset = .hd1280x720
140
+ stream.frameRate = 30
141
+ stream.videoOrientation = .portrait
142
+ stream.configuration { captureSession in
143
+ captureSession.automaticallyConfiguresApplicationAudioSession = true
144
+ }
145
+
146
+ // Áudio
147
+ if let audioDevice = AVCaptureDevice.default(for: .audio) {
148
+ stream.attachAudio(audioDevice)
149
+ }
150
+
151
+ if let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
152
+ for: .video,
153
+ position: .front) {
154
+ stream.attachCamera(camera) { videoUnit, error in
155
+ guard let unit = videoUnit else {
156
+ print("[ExpoSettings] attachCamera error:", error?.localizedDescription ?? "unknown")
157
+ return
158
+ }
159
+ unit.isVideoMirrored = true
160
+ unit.videoOrientation = .portrait
161
+ unit.preferredVideoStabilizationMode = .standard
162
+ unit.colorFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
163
+ }
164
+ }
165
+
166
+ // Attach preview (se existir)
167
+ if let preview = await ExpoSettingsView.current {
168
+ await preview.attachStream(stream)
169
+ } else {
170
+ print("[ExpoSettings] ERROR: Preview view not found during publish!")
171
+ }
172
+
173
+ var audioSettings = AudioCodecSettings()
174
+ audioSettings.bitRate = 128 * 1000 // 128 kbps
175
+ stream.audioSettings = audioSettings
176
+
177
+ // Vídeo
178
+ let videoSettings = VideoCodecSettings(
179
+ videoSize: .init(width: 720, height: 1280),
180
+ bitRate: 4000 * 1000, // 4 Mbps
181
+ profileLevel: kVTProfileLevel_H264_Baseline_3_1 as String,
182
+ scalingMode: .trim,
183
+ bitRateMode: .average,
184
+ maxKeyFrameIntervalDuration: 2,
185
+ allowFrameReordering: nil,
186
+ isHardwareEncoderEnabled: true
187
+ )
188
+ stream.videoSettings = videoSettings
189
+
190
+ self.currentStreamStatus = "previewReady"
191
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
192
+
193
+ connection.connect(url)
194
+ } else {
195
+ // Use existing connection
196
+ self.rtmpConnection?.connect(url)
74
197
  }
198
+ self.currentStreamStatus = "connected"
199
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
75
200
 
76
- // Conecta
77
- self.rtmpConnection?.connect(url)
78
- self.setStatus("connected")
201
+ self.currentStreamStatus = "publishing"
202
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
79
203
 
80
- // Publica
81
- self.setStatus("publishing")
82
204
  self.rtmpStream?.publish(streamKey)
83
205
  print("[ExpoSettings] Stream published successfully")
84
206
 
85
- self.setStatus("started")
207
+ self.currentStreamStatus = "started"
208
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
86
209
  }
87
210
  }
88
211
 
@@ -90,114 +213,31 @@ public class ExpoSettingsModule: Module {
90
213
  Task {
91
214
  print("[ExpoSettings] stopStream called")
92
215
 
216
+ // Primeiro pare a publicação (se estiver publicando)
93
217
  if let stream = self.rtmpStream {
94
218
  print("[ExpoSettings] Stopping stream publication")
95
219
  stream.close()
96
220
 
97
- // Libera recursos
221
+ // Desanexa a câmera e o áudio para liberar recursos
98
222
  stream.attachCamera(nil)
99
223
  stream.attachAudio(nil)
100
224
  }
101
225
 
226
+ // Depois feche a conexão RTMP
102
227
  if let connection = self.rtmpConnection {
103
228
  print("[ExpoSettings] Closing RTMP connection")
104
229
  connection.close()
105
230
  }
106
231
 
232
+ // Limpe as referências
107
233
  self.rtmpStream = nil
108
234
  self.rtmpConnection = nil
109
235
 
110
236
  print("[ExpoSettings] Stream and connection closed and resources released")
111
- self.setStatus("stopped")
112
- }
113
- }
114
- }
115
-
116
- // MARK: - Helpers
117
-
118
- private func setStatus(_ status: String) {
119
- self.currentStreamStatus = status
120
- sendEvent("onStreamStatus", ["status": status])
121
- }
122
237
 
123
- private func configureAudioSession() {
124
- let session = AVAudioSession.sharedInstance()
125
- do {
126
- try session.setCategory(.playAndRecord,
127
- mode: .default,
128
- options: [.defaultToSpeaker, .allowBluetooth])
129
- try session.setActive(true)
130
- } catch {
131
- print("[ExpoSettings] AVAudioSession error:", error)
132
- }
133
- }
134
-
135
- /// Configuração centralizada do stream (evita mismatch entre preview/publish).
136
- private func configureStream(_ stream: RTMPStream) {
137
- print("[ExpoSettings] Configuring stream...")
138
-
139
- // IMPORTANTE: preset primeiro, depois FPS (evita fallback estranho)
140
- stream.sessionPreset = .hd1280x720
141
- stream.frameRate = 30
142
-
143
- // Orientação no nível do stream (governança do pipeline)
144
- stream.videoOrientation = .portrait
145
-
146
- // Sugestão para estabilidade de scaling/mix
147
- stream.videoMixerSettings.mode = .offscreen
148
-
149
- // Opcional: deixa o capture session gerenciar áudio automaticamente
150
- stream.configuration { captureSession in
151
- captureSession.automaticallyConfiguresApplicationAudioSession = true
152
- }
153
-
154
- // Áudio
155
- var audioSettings = AudioCodecSettings()
156
- audioSettings.bitRate = 128 * 1000
157
- stream.audioSettings = audioSettings
158
-
159
- // Vídeo (vertical 9:16)
160
- // 720x1280 é um “sweet spot” pra compatibilidade e estabilidade
161
- let videoSettings = VideoCodecSettings(
162
- videoSize: .init(width: 720, height: 1280),
163
- bitRate: 4_000 * 1000,
164
- profileLevel: kVTProfileLevel_H264_Baseline_3_1 as String,
165
- scalingMode: .trim, // sem distorção (corta excesso). Alternativa: .letterbox (barras)
166
- bitRateMode: .average,
167
- maxKeyFrameIntervalDuration: 2,
168
- allowFrameReordering: nil,
169
- isHardwareEncoderEnabled: true
170
- )
171
- stream.videoSettings = videoSettings
172
-
173
- print("[ExpoSettings] Stream configured: preset=\(stream.sessionPreset.rawValue), fps=\(stream.frameRate), orientation=\(stream.videoOrientation.rawValue)")
174
- print("[ExpoSettings] Encoder target: \(Int(videoSettings.videoSize.width))x\(Int(videoSettings.videoSize.height)) scaling=\(videoSettings.scalingMode.rawValue)")
175
- }
176
-
177
- private func attachAudioIfAvailable(_ stream: RTMPStream) {
178
- if let audioDevice = AVCaptureDevice.default(for: .audio) {
179
- print("[ExpoSettings] Attaching audio device")
180
- stream.attachAudio(audioDevice)
181
- } else {
182
- print("[ExpoSettings] No audio device found")
238
+ self.currentStreamStatus = "stopped"
239
+ sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
240
+ }
183
241
  }
184
242
  }
185
-
186
- private func attachFrontCamera(_ stream: RTMPStream) async {
187
- guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
188
- for: .video,
189
- position: .front) else {
190
- print("[ExpoSettings] No front camera device found")
191
- return
192
- }
193
-
194
- print("[ExpoSettings] Attaching front camera device")
195
-
196
- stream.attachCamera(camera) { videoUnit, error in
197
- guard let unit = videoUnit else {
198
- print("[ExpoSettings] attachCamera error:", error?.localizedDescription ?? "unknown")
199
- return
200
- }
201
-
202
- unit.isVideoMirrored = true
203
- unit.videoOrientation = .po
243
+ }
@@ -3,40 +3,34 @@ import HaishinKit
3
3
  import AVFoundation
4
4
 
5
5
  public class ExpoSettingsView: ExpoView {
6
-
7
- public static weak var current: ExpoSettingsView?
8
-
9
- // A view de preview do HaishinKit
10
- private let hkView: MTHKView = {
11
- let view = MTHKView(frame: .zero)
12
- view.videoGravity = .resizeAspectFill
13
- return view
14
- }()
6
+ public static weak var current: ExpoSettingsView?
15
7
 
16
- required init(appContext: AppContext? = nil) {
17
- super.init(appContext: appContext)
18
- clipsToBounds = true
19
- addSubview(hkView)
20
- ExpoSettingsView.current = self
21
- print("[ExpoSettingsView] View initialized and set as current")
22
- }
8
+ private let hkView: MTHKView = {
9
+ let view = MTHKView(frame: .zero)
10
+ view.videoGravity = .resizeAspectFill
11
+ return view
12
+ }()
23
13
 
24
- override public func layoutSubviews() {
25
- super.layoutSubviews()
26
- hkView.frame = bounds
27
- print("[ExpoSettingsView] Layout updated, frame: \(bounds)")
28
- }
14
+ required init(appContext: AppContext? = nil) {
15
+ super.init(appContext: appContext)
16
+ clipsToBounds = true
17
+ addSubview(hkView)
18
+ ExpoSettingsView.current = self
19
+ print("[ExpoSettingsView] initialized")
20
+ }
29
21
 
30
- /// Conecta o RTMPStream a essa view (internamente já faz o attachCamera + renderização)
31
- public func attachStream(_ stream: RTMPStream) {
32
- print("[ExpoSettingsView] Attaching stream to view")
33
- hkView.attachStream(stream)
34
- }
35
-
36
- deinit {
37
- print("[ExpoSettingsView] View being deinitialized")
38
- if ExpoSettingsView.current === self {
39
- ExpoSettingsView.current = nil
40
- }
22
+ public override func layoutSubviews() {
23
+ super.layoutSubviews()
24
+ hkView.frame = bounds
25
+ }
26
+
27
+ public func attachStream(_ stream: RTMPStream) {
28
+ hkView.attachStream(stream)
29
+ }
30
+
31
+ deinit {
32
+ if ExpoSettingsView.current === self {
33
+ ExpoSettingsView.current = nil
41
34
  }
35
+ }
42
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blix-expo-settings",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "LiveStream",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",