blix-expo-settings 0.1.7 → 0.1.9

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.
@@ -4,189 +4,200 @@ import AVFoundation
4
4
  import VideoToolbox
5
5
 
6
6
  public class ExpoSettingsModule: Module {
7
- private var rtmpConnection: RTMPConnection?
8
- private var rtmpStream: RTMPStream?
9
- private var currentStreamStatus: String = "stopped"
7
+ private var rtmpConnection: RTMPConnection?
8
+ private var rtmpStream: RTMPStream?
9
+ private var currentStreamStatus: String = "stopped"
10
10
 
11
-
12
- public func definition() -> ModuleDefinition {
13
- Name("ExpoSettings")
11
+ public func definition() -> ModuleDefinition {
12
+ Name("ExpoSettings")
14
13
 
15
- // Registra o view component para o admin se enxergar na live
14
+ // View component (preview)
15
+ View(ExpoSettingsView.self) {
16
+ // sem props
17
+ }
16
18
 
17
- View(ExpoSettingsView.self) {
18
- // não precisa colocar nada aqui se você não tiver Props
19
+ Events("onStreamStatus")
20
+
21
+ Function("getStreamStatus") {
22
+ return self.currentStreamStatus
23
+ }
24
+
25
+ Function("initializePreview") { () -> Void in
26
+ Task {
27
+ self.setStatus("previewInitializing")
28
+
29
+ // 0) Configura AVAudioSession
30
+ self.configureAudioSession()
31
+
32
+ // 1) Cria connection + stream
33
+ let connection = RTMPConnection()
34
+ self.rtmpConnection = connection
35
+
36
+ let stream = RTMPStream(connection: connection)
37
+ self.rtmpStream = stream
38
+
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)
47
+
48
+ // 5) Attach preview (MainActor)
49
+ await self.attachPreviewIfAvailable(stream)
50
+
51
+ self.setStatus("previewReady")
52
+ }
53
+ }
54
+
55
+ Function("publishStream") { (url: String, streamKey: String) -> Void in
56
+ Task {
57
+ print("[ExpoSettings] Publishing stream to URL: \(url) with key: \(streamKey)")
58
+ self.setStatus("connecting")
59
+
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.")
63
+
64
+ let connection = RTMPConnection()
65
+ self.rtmpConnection = connection
66
+
67
+ let stream = RTMPStream(connection: connection)
68
+ self.rtmpStream = stream
69
+
70
+ self.configureStream(stream)
71
+ self.attachAudioIfAvailable(stream)
72
+ await self.attachFrontCamera(stream)
73
+ await self.attachPreviewIfAvailable(stream)
19
74
  }
20
-
21
- Events("onStreamStatus")
22
75
 
23
- Function("getStreamStatus") {
24
- return self.currentStreamStatus
76
+ // Conecta
77
+ self.rtmpConnection?.connect(url)
78
+ self.setStatus("connected")
79
+
80
+ // Publica
81
+ self.setStatus("publishing")
82
+ self.rtmpStream?.publish(streamKey)
83
+ print("[ExpoSettings] Stream published successfully")
84
+
85
+ self.setStatus("started")
86
+ }
87
+ }
88
+
89
+ Function("stopStream") { () -> Void in
90
+ Task {
91
+ print("[ExpoSettings] stopStream called")
92
+
93
+ if let stream = self.rtmpStream {
94
+ print("[ExpoSettings] Stopping stream publication")
95
+ stream.close()
96
+
97
+ // Libera recursos
98
+ stream.attachCamera(nil)
99
+ stream.attachAudio(nil)
25
100
  }
26
101
 
27
- Function("initializePreview") { () -> Void in
28
- Task {
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
- }
44
-
45
- // 1) Conectar ao servidor RTMP, mas não publica
46
- let connection = RTMPConnection()
47
- self.rtmpConnection = connection
48
-
49
- // 2) Criar RTMPStream, mas não publica pro servidor ainda
50
- let stream = RTMPStream(connection: connection)
51
- self.rtmpStream = stream
52
- print("[ExpoSettings] RTMPStream initialized")
53
-
54
- // 3) Configurar captura: frame rate e preset
55
- stream.frameRate = 30
56
- stream.sessionPreset = .high
57
- stream.configuration { captureSession in
58
- captureSession.automaticallyConfiguresApplicationAudioSession = true
59
- }
60
-
61
- // 4) Configurar áudio: anexa microfone
62
- if let audioDevice = AVCaptureDevice.default(for: .audio) {
63
- print("[ExpoSettings] Attaching audio device")
64
- stream.attachAudio(audioDevice)
65
- } else {
66
- print("[ExpoSettings] No audio device found")
67
- }
68
-
69
- // 5) Configurar vídeo: anexa câmera frontal
70
- if let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
71
- for: .video,
72
- position: .front) {
73
- print("[ExpoSettings] Attaching camera device")
74
- stream.attachCamera(camera) { videoUnit, error in
75
- guard let unit = videoUnit else {
76
- print("[ExpoSettings] attachCamera error:", error?.localizedDescription ?? "unknown")
77
- return
78
- }
79
- unit.isVideoMirrored = true
80
- unit.preferredVideoStabilizationMode = .standard
81
- unit.colorFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
82
-
83
- }
84
- if let preview = await ExpoSettingsView.current {
85
- print("[ExpoSettings] Attaching stream to preview view")
86
- await preview.attachStream(stream)
87
- } else {
88
- print("[ExpoSettings] ERROR: Preview view not found!")
89
- }
90
- } else {
91
- print("[ExpoSettings] No camera device found")
92
- }
93
-
94
- //6) Definir configurações de codec
95
- print("[ExpoSettings] Setting audio and video codecs")
96
- var audioSettings = AudioCodecSettings()
97
- audioSettings.bitRate = 128 * 1000
98
- stream.audioSettings = audioSettings
99
-
100
- let videoSettings = VideoCodecSettings(
101
- videoSize: .init(width: 1080, height: 1920),
102
- bitRate: 4000 * 1000,
103
- profileLevel: kVTProfileLevel_H264_Baseline_4_0 as String,
104
- scalingMode: .trim,
105
- bitRateMode: .average,
106
- maxKeyFrameIntervalDuration: 2,
107
- allowFrameReordering: nil,
108
- isHardwareEncoderEnabled: true
109
- )
110
- stream.videoSettings = videoSettings
111
- }
112
- self.currentStreamStatus = "previewReady"
113
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
114
- }
115
- }
116
-
117
- Function("publishStream") { (url: String, streamKey: String) -> Void in
118
- Task {
119
-
120
- print("[ExpoSettings] Publishing stream to URL: \(url) with key: \(streamKey)")
121
-
122
- self.currentStreamStatus = "connecting"
123
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
124
-
125
- // se não houve initializePreview→recria a connection
126
- if self.rtmpConnection == nil || self.rtmpStream == nil {
127
- print("[ExpoSettings] WARNING: Connection or stream not initialized, creating new ones")
128
- // Create new connection
129
- let connection = RTMPConnection()
130
- self.rtmpConnection = connection
131
- connection.connect(url)
132
-
133
- // Create new stream
134
- let stream = RTMPStream(connection: connection)
135
- self.rtmpStream = stream
136
-
137
- // Attach to view if available
138
- if let preview = await ExpoSettingsView.current {
139
- await preview.attachStream(stream)
140
- } else {
141
- print("[ExpoSettings] ERROR: Preview view not found during publish!")
142
- }
143
- } else {
144
- // Use existing connection
145
- self.rtmpConnection?.connect(url)
146
- }
147
- self.currentStreamStatus = "connected"
148
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
149
-
150
- self.currentStreamStatus = "publishing"
151
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
152
-
153
- self.rtmpStream?.publish(streamKey)
154
- print("[ExpoSettings] Stream published successfully")
155
-
156
- self.currentStreamStatus = "started"
157
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
158
- }
159
- }
160
-
161
- Function("stopStream") { () -> Void in
162
- Task {
163
- print("[ExpoSettings] stopStream called")
164
-
165
- // Primeiro pare a publicação (se estiver publicando)
166
- if let stream = self.rtmpStream {
167
- print("[ExpoSettings] Stopping stream publication")
168
- stream.close()
169
-
170
- // Desanexa a câmera e o áudio para liberar recursos
171
- stream.attachCamera(nil)
172
- stream.attachAudio(nil)
173
- }
174
-
175
- // Depois feche a conexão RTMP
176
- if let connection = self.rtmpConnection {
177
- print("[ExpoSettings] Closing RTMP connection")
178
- connection.close()
179
- }
180
-
181
- // Limpe as referências
182
- self.rtmpStream = nil
183
- self.rtmpConnection = nil
184
-
185
- print("[ExpoSettings] Stream and connection closed and resources released")
186
-
187
- self.currentStreamStatus = "stopped"
188
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
189
- }
190
- }
102
+ if let connection = self.rtmpConnection {
103
+ print("[ExpoSettings] Closing RTMP connection")
104
+ connection.close()
191
105
  }
192
- }
106
+
107
+ self.rtmpStream = nil
108
+ self.rtmpConnection = nil
109
+
110
+ 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
+
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")
183
+ }
184
+ }
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blix-expo-settings",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "LiveStream",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",