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.
@@ -4,190 +4,182 @@ 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"
10
-
11
-
12
- public func definition() -> ModuleDefinition {
13
- Name("ExpoSettings")
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
- }
20
-
21
- Events("onStreamStatus")
22
-
23
- Function("getStreamStatus") {
24
- return self.currentStreamStatus
25
- }
26
-
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.videoOrientation = .portrait
81
- unit.preferredVideoStabilizationMode = .standard
82
- unit.colorFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
83
-
84
- }
85
- if let preview = await ExpoSettingsView.current {
86
- print("[ExpoSettings] Attaching stream to preview view")
87
- await preview.attachStream(stream)
88
- } else {
89
- print("[ExpoSettings] ERROR: Preview view not found!")
90
- }
91
- } else {
92
- print("[ExpoSettings] No camera device found")
93
- }
94
-
95
- //6) Definir configurações de codec
96
- print("[ExpoSettings] Setting audio and video codecs")
97
- var audioSettings = AudioCodecSettings()
98
- audioSettings.bitRate = 128 * 1000
99
- stream.audioSettings = audioSettings
100
-
101
- let videoSettings = VideoCodecSettings(
102
- videoSize: .init(width: 1080, height: 1920),
103
- bitRate: 4000 * 1000,
104
- profileLevel: kVTProfileLevel_H264_Baseline_4_0 as String,
105
- scalingMode: .letterbox,
106
- bitRateMode: .average,
107
- maxKeyFrameIntervalDuration: 2,
108
- allowFrameReordering: nil,
109
- isHardwareEncoderEnabled: true
110
- )
111
- stream.videoSettings = videoSettings
112
- }
113
- self.currentStreamStatus = "previewReady"
114
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
115
- }
116
- }
117
-
118
- Function("publishStream") { (url: String, streamKey: String) -> Void in
119
- Task {
120
-
121
- print("[ExpoSettings] Publishing stream to URL: \(url) with key: \(streamKey)")
122
-
123
- self.currentStreamStatus = "connecting"
124
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
125
-
126
- // se não houve initializePreview→recria a connection
127
- if self.rtmpConnection == nil || self.rtmpStream == nil {
128
- print("[ExpoSettings] WARNING: Connection or stream not initialized, creating new ones")
129
- // Create new connection
130
- let connection = RTMPConnection()
131
- self.rtmpConnection = connection
132
- connection.connect(url)
133
-
134
- // Create new stream
135
- let stream = RTMPStream(connection: connection)
136
- self.rtmpStream = stream
137
-
138
- // Attach to view if available
139
- if let preview = await ExpoSettingsView.current {
140
- await preview.attachStream(stream)
141
- } else {
142
- print("[ExpoSettings] ERROR: Preview view not found during publish!")
143
- }
144
- } else {
145
- // Use existing connection
146
- self.rtmpConnection?.connect(url)
147
- }
148
- self.currentStreamStatus = "connected"
149
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
150
-
151
- self.currentStreamStatus = "publishing"
152
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
153
-
154
- self.rtmpStream?.publish(streamKey)
155
- print("[ExpoSettings] Stream published successfully")
156
-
157
- self.currentStreamStatus = "started"
158
- sendEvent("onStreamStatus", ["status": self.currentStreamStatus])
159
- }
160
- }
161
-
162
- Function("stopStream") { () -> Void in
163
- Task {
164
- print("[ExpoSettings] stopStream called")
165
-
166
- // Primeiro pare a publicação (se estiver publicando)
167
- if let stream = self.rtmpStream {
168
- print("[ExpoSettings] Stopping stream publication")
169
- stream.close()
170
-
171
- // Desanexa a câmera e o áudio para liberar recursos
172
- stream.attachCamera(nil)
173
- stream.attachAudio(nil)
174
- }
175
-
176
- // Depois feche a conexão RTMP
177
- if let connection = self.rtmpConnection {
178
- print("[ExpoSettings] Closing RTMP connection")
179
- connection.close()
180
- }
181
-
182
- // Limpe as referências
183
- self.rtmpStream = nil
184
- self.rtmpConnection = nil
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
- 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.8",
3
+ "version": "0.1.10",
4
4
  "description": "LiveStream",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",