blix-expo-settings 0.1.10 → 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.
- package/ios/ExpoSettingsModule.swift +201 -143
- package/package.json +1 -1
|
@@ -8,10 +8,15 @@ 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
|
-
|
|
15
|
+
// Registra o view component para o admin se enxergar na live
|
|
16
|
+
|
|
17
|
+
View(ExpoSettingsView.self) {
|
|
18
|
+
// não precisa colocar nada aqui se você não tiver Props
|
|
19
|
+
}
|
|
15
20
|
|
|
16
21
|
Events("onStreamStatus")
|
|
17
22
|
|
|
@@ -20,165 +25,218 @@ public class ExpoSettingsModule: Module {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
Function("initializePreview") { () -> Void in
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Function("publishStream") { (url: String, streamKey: String) -> Void in
|
|
42
|
-
self.setStatus("connecting")
|
|
43
|
-
print("[ExpoSettings] Publishing to: \(url) key: \(streamKey)")
|
|
44
|
-
|
|
45
|
-
// Se não existe stream/connection, cria e aplica TODA a config
|
|
46
|
-
if self.rtmpConnection == nil || self.rtmpStream == nil {
|
|
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
|
|
47
46
|
let connection = RTMPConnection()
|
|
48
47
|
self.rtmpConnection = connection
|
|
49
48
|
|
|
49
|
+
// 2) Criar RTMPStream, mas não publica pro servidor ainda
|
|
50
50
|
let stream = RTMPStream(connection: connection)
|
|
51
51
|
self.rtmpStream = stream
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
61
|
+
|
|
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
|
+
}
|
|
69
|
+
|
|
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
|
+
}
|
|
95
|
+
|
|
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
|
|
57
113
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
self.setStatus("connected")
|
|
61
|
-
|
|
62
|
-
self.setStatus("publishing")
|
|
63
|
-
self.rtmpStream?.publish(streamKey)
|
|
64
|
-
|
|
65
|
-
self.setStatus("started")
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
Function("stopStream") { () -> Void in
|
|
69
|
-
print("[ExpoSettings] stopStream called")
|
|
70
|
-
|
|
71
|
-
if let stream = self.rtmpStream {
|
|
72
|
-
stream.close()
|
|
73
|
-
stream.attachCamera(nil)
|
|
74
|
-
stream.attachAudio(nil)
|
|
114
|
+
self.currentStreamStatus = "previewReady"
|
|
115
|
+
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
75
116
|
}
|
|
117
|
+
}
|
|
76
118
|
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
Function("publishStream") { (url: String, streamKey: String) -> Void in
|
|
120
|
+
Task {
|
|
121
|
+
|
|
122
|
+
print("[ExpoSettings] Publishing stream to URL: \(url) with key: \(streamKey)")
|
|
123
|
+
|
|
124
|
+
self.currentStreamStatus = "connecting"
|
|
125
|
+
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
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
|
|
131
|
+
let connection = RTMPConnection()
|
|
132
|
+
self.rtmpConnection = connection
|
|
133
|
+
|
|
134
|
+
// Create new stream
|
|
135
|
+
let stream = RTMPStream(connection: connection)
|
|
136
|
+
self.rtmpStream = stream
|
|
137
|
+
|
|
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)
|
|
197
|
+
}
|
|
198
|
+
self.currentStreamStatus = "connected"
|
|
199
|
+
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
200
|
+
|
|
201
|
+
self.currentStreamStatus = "publishing"
|
|
202
|
+
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
203
|
+
|
|
204
|
+
self.rtmpStream?.publish(streamKey)
|
|
205
|
+
print("[ExpoSettings] Stream published successfully")
|
|
206
|
+
|
|
207
|
+
self.currentStreamStatus = "started"
|
|
208
|
+
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
79
209
|
}
|
|
80
|
-
|
|
81
|
-
self.rtmpStream = nil
|
|
82
|
-
self.rtmpConnection = nil
|
|
83
|
-
|
|
84
|
-
self.setStatus("stopped")
|
|
85
210
|
}
|
|
86
|
-
}
|
|
87
211
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.currentStreamStatus = status
|
|
92
|
-
sendEvent("onStreamStatus", ["status": status])
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
private func configureAudioSession() {
|
|
96
|
-
let session = AVAudioSession.sharedInstance()
|
|
97
|
-
do {
|
|
98
|
-
try session.setCategory(
|
|
99
|
-
.playAndRecord,
|
|
100
|
-
mode: .default,
|
|
101
|
-
options: [.defaultToSpeaker, .allowBluetooth]
|
|
102
|
-
)
|
|
103
|
-
try session.setActive(true)
|
|
104
|
-
} catch {
|
|
105
|
-
print("[ExpoSettings] AVAudioSession error:", error)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
212
|
+
Function("stopStream") { () -> Void in
|
|
213
|
+
Task {
|
|
214
|
+
print("[ExpoSettings] stopStream called")
|
|
108
215
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
stream.sessionPreset = .hd1280x720
|
|
114
|
-
stream.frameRate = 30
|
|
115
|
-
|
|
116
|
-
// 2) Orientação no nível do stream (governa pipeline)
|
|
117
|
-
stream.videoOrientation = .portrait
|
|
118
|
-
|
|
119
|
-
// 3) Áudio
|
|
120
|
-
var audioSettings = AudioCodecSettings()
|
|
121
|
-
audioSettings.bitRate = 128 * 1000
|
|
122
|
-
stream.audioSettings = audioSettings
|
|
123
|
-
|
|
124
|
-
// 4) Vídeo (9:16). Use 720x1280 para estabilidade
|
|
125
|
-
let videoSettings = VideoCodecSettings(
|
|
126
|
-
videoSize: .init(width: 720, height: 1280),
|
|
127
|
-
bitRate: 4_000 * 1000,
|
|
128
|
-
profileLevel: kVTProfileLevel_H264_Baseline_3_1 as String,
|
|
129
|
-
scalingMode: .trim, // sem distorção (corta). Alternativa: .letterbox (barras)
|
|
130
|
-
bitRateMode: .average,
|
|
131
|
-
maxKeyFrameIntervalDuration: 2,
|
|
132
|
-
allowFrameReordering: nil,
|
|
133
|
-
isHardwareEncoderEnabled: true
|
|
134
|
-
)
|
|
135
|
-
stream.videoSettings = videoSettings
|
|
136
|
-
|
|
137
|
-
print("[ExpoSettings] Stream configured preset=\(stream.sessionPreset.rawValue) fps=\(stream.frameRate)")
|
|
138
|
-
print("[ExpoSettings] Target=\(Int(videoSettings.videoSize.width))x\(Int(videoSettings.videoSize.height)) orientation=portrait")
|
|
139
|
-
}
|
|
216
|
+
// Primeiro pare a publicação (se estiver publicando)
|
|
217
|
+
if let stream = self.rtmpStream {
|
|
218
|
+
print("[ExpoSettings] Stopping stream publication")
|
|
219
|
+
stream.close()
|
|
140
220
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
} else {
|
|
146
|
-
print("[ExpoSettings] No audio device found")
|
|
147
|
-
}
|
|
148
|
-
}
|
|
221
|
+
// Desanexa a câmera e o áudio para liberar recursos
|
|
222
|
+
stream.attachCamera(nil)
|
|
223
|
+
stream.attachAudio(nil)
|
|
224
|
+
}
|
|
149
225
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
) else {
|
|
156
|
-
print("[ExpoSettings] No front camera found")
|
|
157
|
-
return
|
|
158
|
-
}
|
|
226
|
+
// Depois feche a conexão RTMP
|
|
227
|
+
if let connection = self.rtmpConnection {
|
|
228
|
+
print("[ExpoSettings] Closing RTMP connection")
|
|
229
|
+
connection.close()
|
|
230
|
+
}
|
|
159
231
|
|
|
160
|
-
|
|
232
|
+
// Limpe as referências
|
|
233
|
+
self.rtmpStream = nil
|
|
234
|
+
self.rtmpConnection = nil
|
|
161
235
|
|
|
162
|
-
|
|
163
|
-
guard let unit = videoUnit else {
|
|
164
|
-
print("[ExpoSettings] attachCamera error:", error?.localizedDescription ?? "unknown")
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
unit.isVideoMirrored = true
|
|
169
|
-
unit.videoOrientation = .portrait // <-- GARANTA que está .portrait (não .po)
|
|
170
|
-
unit.preferredVideoStabilizationMode = .standard
|
|
171
|
-
unit.colorFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
|
|
172
|
-
}
|
|
173
|
-
}
|
|
236
|
+
print("[ExpoSettings] Stream and connection closed and resources released")
|
|
174
237
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if let preview = ExpoSettingsView.current {
|
|
178
|
-
print("[ExpoSettings] Attaching stream to preview")
|
|
179
|
-
preview.attachStream(stream)
|
|
180
|
-
} else {
|
|
181
|
-
print("[ExpoSettings] Preview not available yet")
|
|
238
|
+
self.currentStreamStatus = "stopped"
|
|
239
|
+
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
182
240
|
}
|
|
183
241
|
}
|
|
184
242
|
}
|