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.
- package/ios/ExpoSettingsModule.swift +189 -178
- package/package.json +1 -1
|
@@ -4,189 +4,200 @@ import AVFoundation
|
|
|
4
4
|
import VideoToolbox
|
|
5
5
|
|
|
6
6
|
public class ExpoSettingsModule: Module {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
private var rtmpConnection: RTMPConnection?
|
|
8
|
+
private var rtmpStream: RTMPStream?
|
|
9
|
+
private var currentStreamStatus: String = "stopped"
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Name("ExpoSettings")
|
|
11
|
+
public func definition() -> ModuleDefinition {
|
|
12
|
+
Name("ExpoSettings")
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
// View component (preview)
|
|
15
|
+
View(ExpoSettingsView.self) {
|
|
16
|
+
// sem props
|
|
17
|
+
}
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|