blix-expo-settings 0.1.8 → 0.1.10
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 +179 -187
- package/ios/ExpoSettingsView.swift +26 -32
- package/package.json +1 -1
|
@@ -4,190 +4,182 @@ import AVFoundation
|
|
|
4
4
|
import VideoToolbox
|
|
5
5
|
|
|
6
6
|
public class ExpoSettingsModule: Module {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
print("[ExpoSettings] Stream and connection closed and resources released")
|
|
187
|
-
|
|
188
|
-
self.currentStreamStatus = "stopped"
|
|
189
|
-
sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
7
|
+
private var rtmpConnection: RTMPConnection?
|
|
8
|
+
private var rtmpStream: RTMPStream?
|
|
9
|
+
private var currentStreamStatus: String = "stopped"
|
|
10
|
+
|
|
11
|
+
public func definition() -> ModuleDefinition {
|
|
12
|
+
Name("ExpoSettings")
|
|
13
|
+
|
|
14
|
+
View(ExpoSettingsView.self) {}
|
|
15
|
+
|
|
16
|
+
Events("onStreamStatus")
|
|
17
|
+
|
|
18
|
+
Function("getStreamStatus") {
|
|
19
|
+
return self.currentStreamStatus
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Function("initializePreview") { () -> Void in
|
|
23
|
+
self.setStatus("previewInitializing")
|
|
24
|
+
|
|
25
|
+
self.configureAudioSession()
|
|
26
|
+
|
|
27
|
+
let connection = RTMPConnection()
|
|
28
|
+
self.rtmpConnection = connection
|
|
29
|
+
|
|
30
|
+
let stream = RTMPStream(connection: connection)
|
|
31
|
+
self.rtmpStream = stream
|
|
32
|
+
|
|
33
|
+
self.configureStream(stream)
|
|
34
|
+
self.attachAudioIfAvailable(stream)
|
|
35
|
+
self.attachFrontCamera(stream)
|
|
36
|
+
self.attachPreviewIfAvailable(stream)
|
|
37
|
+
|
|
38
|
+
self.setStatus("previewReady")
|
|
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 {
|
|
47
|
+
let connection = RTMPConnection()
|
|
48
|
+
self.rtmpConnection = connection
|
|
49
|
+
|
|
50
|
+
let stream = RTMPStream(connection: connection)
|
|
51
|
+
self.rtmpStream = stream
|
|
52
|
+
|
|
53
|
+
self.configureStream(stream)
|
|
54
|
+
self.attachAudioIfAvailable(stream)
|
|
55
|
+
self.attachFrontCamera(stream)
|
|
56
|
+
self.attachPreviewIfAvailable(stream)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
self.rtmpConnection?.connect(url)
|
|
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)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if let connection = self.rtmpConnection {
|
|
78
|
+
connection.close()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
self.rtmpStream = nil
|
|
82
|
+
self.rtmpConnection = nil
|
|
83
|
+
|
|
84
|
+
self.setStatus("stopped")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// MARK: - Internals
|
|
89
|
+
|
|
90
|
+
private func setStatus(_ status: String) {
|
|
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
|
+
}
|
|
108
|
+
|
|
109
|
+
private func configureStream(_ stream: RTMPStream) {
|
|
110
|
+
print("[ExpoSettings] Configuring stream...")
|
|
111
|
+
|
|
112
|
+
// 1) Captura previsível (16:9)
|
|
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
|
+
}
|
|
140
|
+
|
|
141
|
+
private func attachAudioIfAvailable(_ stream: RTMPStream) {
|
|
142
|
+
if let audioDevice = AVCaptureDevice.default(for: .audio) {
|
|
143
|
+
print("[ExpoSettings] Attaching audio")
|
|
144
|
+
stream.attachAudio(audioDevice)
|
|
145
|
+
} else {
|
|
146
|
+
print("[ExpoSettings] No audio device found")
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private func attachFrontCamera(_ stream: RTMPStream) {
|
|
151
|
+
guard let camera = AVCaptureDevice.default(
|
|
152
|
+
.builtInWideAngleCamera,
|
|
153
|
+
for: .video,
|
|
154
|
+
position: .front
|
|
155
|
+
) else {
|
|
156
|
+
print("[ExpoSettings] No front camera found")
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print("[ExpoSettings] Attaching front camera")
|
|
161
|
+
|
|
162
|
+
stream.attachCamera(camera) { videoUnit, error in
|
|
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
|
+
}
|
|
174
|
+
|
|
175
|
+
private func attachPreviewIfAvailable(_ stream: RTMPStream) {
|
|
176
|
+
DispatchQueue.main.async {
|
|
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")
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
}
|