@tiledev/sdk-apptile-live-selling 0.2.5 → 0.3.0
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/README.md +77 -11
- package/dist/engine.native.d.ts.map +1 -1
- package/dist/engine.native.js +16 -6
- package/dist/engine.native.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/react/LiveVideoTarget.d.ts +28 -2
- package/dist/react/LiveVideoTarget.d.ts.map +1 -1
- package/dist/react/LiveVideoTarget.js +37 -4
- package/dist/react/LiveVideoTarget.js.map +1 -1
- package/dist/react/index.web.d.ts.map +1 -1
- package/dist/react/index.web.js +5 -0
- package/dist/react/index.web.js.map +1 -1
- package/dist/react/useLiveSession.d.ts.map +1 -1
- package/dist/react/useLiveSession.js +148 -20
- package/dist/react/useLiveSession.js.map +1 -1
- package/dist/session.d.ts +8 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +37 -4
- package/dist/session.js.map +1 -1
- package/dist/sessionTypes.d.ts +57 -28
- package/dist/sessionTypes.d.ts.map +1 -1
- package/dist/sessionTypes.js.map +1 -1
- package/dist/streams.d.ts +6 -0
- package/dist/streams.d.ts.map +1 -1
- package/dist/streams.js +7 -6
- package/dist/streams.js.map +1 -1
- package/dist/types.d.ts +13 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -20
- package/android/build.gradle +0 -33
- package/android/src/main/AndroidManifest.xml +0 -7
- package/android/src/main/java/expo/modules/livepip/LivePipModule.kt +0 -310
- package/app.plugin.js +0 -3
- package/dist/pip.d.ts +0 -14
- package/dist/pip.d.ts.map +0 -1
- package/dist/pip.js +0 -15
- package/dist/pip.js.map +0 -1
- package/dist/pip.native.d.ts +0 -11
- package/dist/pip.native.d.ts.map +0 -1
- package/dist/pip.native.js +0 -47
- package/dist/pip.native.js.map +0 -1
- package/expo-module.config.json +0 -9
- package/ios/LivePip.podspec +0 -20
- package/ios/LivePipController.swift +0 -327
- package/ios/LivePipModule.swift +0 -35
- package/plugin/build/index.d.ts +0 -29
- package/plugin/build/index.js +0 -101
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import AVKit
|
|
2
|
-
import ZegoExpressEngine
|
|
3
|
-
|
|
4
|
-
/// Frame pipeline and PIP window for the live stream.
|
|
5
|
-
///
|
|
6
|
-
/// Why this exists at all: the React Native Zego wrapper exposes no picture-in-picture, and iOS
|
|
7
|
-
/// cannot put a live stream in a PIP window without a sample-buffer layer to hand AVKit. The
|
|
8
|
-
/// engine's own `ZegoTextureView` is not one, so the frames have to be intercepted.
|
|
9
|
-
///
|
|
10
|
-
/// The trick that keeps this small is `enableEngineRender`: the engine keeps drawing into the
|
|
11
|
-
/// texture view exactly as before *and* calls us back with each frame. Without it, custom
|
|
12
|
-
/// rendering replaces engine rendering outright and every surface in the app would have to be
|
|
13
|
-
/// rewritten around this layer.
|
|
14
|
-
final class LivePipController: NSObject {
|
|
15
|
-
static let shared = LivePipController()
|
|
16
|
-
|
|
17
|
-
/// `(eventName, payload)`. Set by the module; the controller never assumes a listener.
|
|
18
|
-
var onEvent: ((String, [String: Any]) -> Void)?
|
|
19
|
-
|
|
20
|
-
private var displayLayer: AVSampleBufferDisplayLayer?
|
|
21
|
-
private var pipController: AVPictureInPictureController?
|
|
22
|
-
private var hostView: UIView?
|
|
23
|
-
private var streamID: String?
|
|
24
|
-
private var configured = false
|
|
25
|
-
private var armed = false
|
|
26
|
-
/// Set between `restoreUserInterfaceForPictureInPictureStop` and `didStop`, which is the only
|
|
27
|
-
/// way to tell "the user expanded the window" from "the user closed it" — both end in `didStop`.
|
|
28
|
-
private var restoring = false
|
|
29
|
-
|
|
30
|
-
// MARK: - Engine wiring
|
|
31
|
-
|
|
32
|
-
/// `sharedEngine` is annotated non-null, so Swift imports it as non-optional — but it really does
|
|
33
|
-
/// return nil until `createEngine` has run, and every entry point here can be reached before
|
|
34
|
-
/// that. Laundering it through an optional restores the check the annotation removed.
|
|
35
|
-
private static func engine() -> ZegoExpressEngine? {
|
|
36
|
-
let candidate: ZegoExpressEngine? = ZegoExpressEngine.shared()
|
|
37
|
-
return candidate
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// Turns on custom rendering. Must run after the engine exists and before the first
|
|
41
|
-
/// `startPlayingStream`: the SDK only accepts this configuration while nothing is playing.
|
|
42
|
-
func configure() throws {
|
|
43
|
-
if configured { return }
|
|
44
|
-
guard let engine = Self.engine() else {
|
|
45
|
-
throw LivePipError.engineMissing
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let config = ZegoCustomVideoRenderConfig()
|
|
49
|
-
config.bufferType = .cvPixelBuffer
|
|
50
|
-
config.frameFormatSeries = .RGB
|
|
51
|
-
// The whole reason the app's existing video path survives this feature.
|
|
52
|
-
config.enableEngineRender = true
|
|
53
|
-
|
|
54
|
-
engine.enableCustomVideoRender(true, config: config)
|
|
55
|
-
engine.setCustomVideoRenderHandler(self)
|
|
56
|
-
configured = true
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/// The stream whose frames feed the PIP window. Pass nil when playback stops.
|
|
60
|
-
func setStream(_ id: String?) {
|
|
61
|
-
if streamID == id { return }
|
|
62
|
-
// Stop the old stream's callbacks before switching, or a cohost swap leaves both feeding.
|
|
63
|
-
if let previous = streamID, let engine = Self.engine() {
|
|
64
|
-
engine.enableRemoteVideoCustomVideoRender(false, streamID: previous)
|
|
65
|
-
}
|
|
66
|
-
streamID = id
|
|
67
|
-
applyFrameCallbacks()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/// Per-stream rather than global: the frame callback copies every frame out of the engine, so it
|
|
71
|
-
/// stays off until a PIP window is actually wanted.
|
|
72
|
-
private func applyFrameCallbacks() {
|
|
73
|
-
guard let id = streamID, let engine = Self.engine() else { return }
|
|
74
|
-
engine.enableRemoteVideoCustomVideoRender(armed, streamID: id)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// MARK: - PIP lifecycle
|
|
78
|
-
|
|
79
|
-
/// Arming is what makes backgrounding the app hand over to a system PIP window: iOS starts it
|
|
80
|
-
/// itself through `canStartPictureInPictureAutomaticallyFromInline`, with no JavaScript running
|
|
81
|
-
/// at that moment. Nothing else produces a seamless handoff — by the time a background
|
|
82
|
-
/// notification reaches JS the transition has already been missed.
|
|
83
|
-
func setArmed(_ next: Bool) {
|
|
84
|
-
if armed == next { return }
|
|
85
|
-
armed = next
|
|
86
|
-
applyFrameCallbacks()
|
|
87
|
-
|
|
88
|
-
onMain { [weak self] in
|
|
89
|
-
guard let self else { return }
|
|
90
|
-
if next {
|
|
91
|
-
self.buildController()
|
|
92
|
-
} else {
|
|
93
|
-
self.teardownController()
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
func start() {
|
|
99
|
-
onMain { [weak self] in
|
|
100
|
-
guard let self, let controller = self.pipController else { return }
|
|
101
|
-
if self.restoring { return }
|
|
102
|
-
if controller.isPictureInPictureActive { return }
|
|
103
|
-
controller.startPictureInPicture()
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
func stop() {
|
|
108
|
-
onMain { [weak self] in
|
|
109
|
-
guard let self, let controller = self.pipController else { return }
|
|
110
|
-
if controller.isPictureInPictureActive { controller.stopPictureInPicture() }
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private func buildController() {
|
|
115
|
-
let layer = displayLayer ?? makeDisplayLayer()
|
|
116
|
-
displayLayer = layer
|
|
117
|
-
|
|
118
|
-
if pipController == nil {
|
|
119
|
-
let source = AVPictureInPictureController.ContentSource(
|
|
120
|
-
sampleBufferDisplayLayer: layer,
|
|
121
|
-
playbackDelegate: self
|
|
122
|
-
)
|
|
123
|
-
let controller = AVPictureInPictureController(contentSource: source)
|
|
124
|
-
controller.delegate = self
|
|
125
|
-
controller.canStartPictureInPictureAutomaticallyFromInline = true
|
|
126
|
-
pipController = controller
|
|
127
|
-
} else {
|
|
128
|
-
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private func teardownController() {
|
|
133
|
-
if let controller = pipController, controller.isPictureInPictureActive {
|
|
134
|
-
controller.stopPictureInPicture()
|
|
135
|
-
}
|
|
136
|
-
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
|
137
|
-
pipController = nil
|
|
138
|
-
displayLayer?.flushAndRemoveImage()
|
|
139
|
-
displayLayer?.removeFromSuperlayer()
|
|
140
|
-
displayLayer = nil
|
|
141
|
-
hostView?.removeFromSuperview()
|
|
142
|
-
hostView = nil
|
|
143
|
-
restoring = false
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/// AVKit will not start PIP for a layer that is not in a window, so the layer gets a 1×1 host in
|
|
147
|
-
/// the corner of the key window, behind everything. It is a real, attached, on-screen layer —
|
|
148
|
-
/// just too small to notice. Rendering it into the app's actual video view instead would draw a
|
|
149
|
-
/// second copy of the stream over the engine's own.
|
|
150
|
-
private func makeDisplayLayer() -> AVSampleBufferDisplayLayer {
|
|
151
|
-
let layer = AVSampleBufferDisplayLayer()
|
|
152
|
-
layer.videoGravity = .resizeAspect
|
|
153
|
-
layer.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
154
|
-
|
|
155
|
-
let host = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
|
|
156
|
-
host.isUserInteractionEnabled = false
|
|
157
|
-
host.layer.addSublayer(layer)
|
|
158
|
-
hostView = host
|
|
159
|
-
|
|
160
|
-
if let window = Self.keyWindow() {
|
|
161
|
-
window.insertSubview(host, at: 0)
|
|
162
|
-
}
|
|
163
|
-
return layer
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private static func keyWindow() -> UIWindow? {
|
|
167
|
-
UIApplication.shared.connectedScenes
|
|
168
|
-
.compactMap { $0 as? UIWindowScene }
|
|
169
|
-
.first { $0.activationState == .foregroundActive }?
|
|
170
|
-
.windows.first { $0.isKeyWindow }
|
|
171
|
-
?? UIApplication.shared.connectedScenes
|
|
172
|
-
.compactMap { $0 as? UIWindowScene }
|
|
173
|
-
.flatMap { $0.windows }
|
|
174
|
-
.first
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private func onMain(_ work: @escaping () -> Void) {
|
|
178
|
-
if Thread.isMainThread { work() } else { DispatchQueue.main.async(execute: work) }
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// MARK: - Frames
|
|
183
|
-
|
|
184
|
-
extension LivePipController: ZegoCustomVideoRenderHandler {
|
|
185
|
-
func onRemoteVideoFrameCVPixelBuffer(
|
|
186
|
-
_ buffer: CVPixelBuffer,
|
|
187
|
-
param: ZegoVideoFrameParam,
|
|
188
|
-
streamID: String
|
|
189
|
-
) {
|
|
190
|
-
guard armed, streamID == self.streamID, let layer = displayLayer else { return }
|
|
191
|
-
guard let sample = Self.makeSampleBuffer(from: buffer) else { return }
|
|
192
|
-
|
|
193
|
-
layer.enqueue(sample)
|
|
194
|
-
// -11847 is "layer failed while detached"; it sticks until the layer is rebuilt, so a silent
|
|
195
|
-
// black PIP window is the alternative to noticing it here.
|
|
196
|
-
if layer.status == .failed {
|
|
197
|
-
layer.flush()
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private static func makeSampleBuffer(from pixelBuffer: CVPixelBuffer) -> CMSampleBuffer? {
|
|
202
|
-
var formatDescription: CMVideoFormatDescription?
|
|
203
|
-
guard
|
|
204
|
-
CMVideoFormatDescriptionCreateForImageBuffer(
|
|
205
|
-
allocator: kCFAllocatorDefault,
|
|
206
|
-
imageBuffer: pixelBuffer,
|
|
207
|
-
formatDescriptionOut: &formatDescription
|
|
208
|
-
) == noErr,
|
|
209
|
-
let format = formatDescription
|
|
210
|
-
else { return nil }
|
|
211
|
-
|
|
212
|
-
// A live stream has no timeline to seek, so the frames carry no timestamps and are shown as
|
|
213
|
-
// they arrive; `kCMSampleAttachmentKey_DisplayImmediately` is what makes that legal.
|
|
214
|
-
var timing = CMSampleTimingInfo(
|
|
215
|
-
duration: .invalid,
|
|
216
|
-
presentationTimeStamp: .invalid,
|
|
217
|
-
decodeTimeStamp: .invalid
|
|
218
|
-
)
|
|
219
|
-
var sampleBuffer: CMSampleBuffer?
|
|
220
|
-
guard
|
|
221
|
-
CMSampleBufferCreateForImageBuffer(
|
|
222
|
-
allocator: kCFAllocatorDefault,
|
|
223
|
-
imageBuffer: pixelBuffer,
|
|
224
|
-
dataReady: true,
|
|
225
|
-
makeDataReadyCallback: nil,
|
|
226
|
-
refcon: nil,
|
|
227
|
-
formatDescription: format,
|
|
228
|
-
sampleTiming: &timing,
|
|
229
|
-
sampleBufferOut: &sampleBuffer
|
|
230
|
-
) == noErr,
|
|
231
|
-
let sample = sampleBuffer
|
|
232
|
-
else { return nil }
|
|
233
|
-
|
|
234
|
-
if
|
|
235
|
-
let attachments = CMSampleBufferGetSampleAttachmentsArray(sample, createIfNecessary: true),
|
|
236
|
-
CFArrayGetCount(attachments) > 0
|
|
237
|
-
{
|
|
238
|
-
let raw = CFArrayGetValueAtIndex(attachments, 0)
|
|
239
|
-
let dictionary = unsafeBitCast(raw, to: CFMutableDictionary.self)
|
|
240
|
-
CFDictionarySetValue(
|
|
241
|
-
dictionary,
|
|
242
|
-
Unmanaged.passUnretained(kCMSampleAttachmentKey_DisplayImmediately).toOpaque(),
|
|
243
|
-
Unmanaged.passUnretained(kCFBooleanTrue).toOpaque()
|
|
244
|
-
)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return sample
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// MARK: - AVPictureInPictureControllerDelegate
|
|
252
|
-
|
|
253
|
-
extension LivePipController: AVPictureInPictureControllerDelegate {
|
|
254
|
-
func pictureInPictureControllerDidStartPictureInPicture(
|
|
255
|
-
_ pictureInPictureController: AVPictureInPictureController
|
|
256
|
-
) {
|
|
257
|
-
onEvent?("onPipStart", [:])
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
func pictureInPictureController(
|
|
261
|
-
_ pictureInPictureController: AVPictureInPictureController,
|
|
262
|
-
failedToStartPictureInPictureWithError error: Error
|
|
263
|
-
) {
|
|
264
|
-
restoring = false
|
|
265
|
-
onEvent?("onPipError", ["message": error.localizedDescription])
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
func pictureInPictureController(
|
|
269
|
-
_ pictureInPictureController: AVPictureInPictureController,
|
|
270
|
-
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (
|
|
271
|
-
Bool
|
|
272
|
-
) -> Void
|
|
273
|
-
) {
|
|
274
|
-
restoring = true
|
|
275
|
-
onEvent?("onPipRestore", [:])
|
|
276
|
-
completionHandler(true)
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
func pictureInPictureControllerDidStopPictureInPicture(
|
|
280
|
-
_ pictureInPictureController: AVPictureInPictureController
|
|
281
|
-
) {
|
|
282
|
-
let restored = restoring
|
|
283
|
-
restoring = false
|
|
284
|
-
onEvent?("onPipStop", ["restored": restored])
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// MARK: - AVPictureInPictureSampleBufferPlaybackDelegate
|
|
289
|
-
|
|
290
|
-
extension LivePipController: AVPictureInPictureSampleBufferPlaybackDelegate {
|
|
291
|
-
func pictureInPictureController(
|
|
292
|
-
_ pictureInPictureController: AVPictureInPictureController,
|
|
293
|
-
setPlaying playing: Bool
|
|
294
|
-
) {
|
|
295
|
-
// A live stream cannot pause, so the play/pause button is a no-op rather than a lie about state.
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
func pictureInPictureControllerTimeRangeForPlayback(
|
|
299
|
-
_ pictureInPictureController: AVPictureInPictureController
|
|
300
|
-
) -> CMTimeRange {
|
|
301
|
-
// Infinite: this is what makes AVKit render live controls instead of a scrubber.
|
|
302
|
-
CMTimeRange(start: .zero, duration: .positiveInfinity)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
func pictureInPictureControllerIsPlaybackPaused(
|
|
306
|
-
_ pictureInPictureController: AVPictureInPictureController
|
|
307
|
-
) -> Bool {
|
|
308
|
-
false
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
func pictureInPictureController(
|
|
312
|
-
_ pictureInPictureController: AVPictureInPictureController,
|
|
313
|
-
didTransitionToRenderSize newRenderSize: CMVideoDimensions
|
|
314
|
-
) {}
|
|
315
|
-
|
|
316
|
-
func pictureInPictureController(
|
|
317
|
-
_ pictureInPictureController: AVPictureInPictureController,
|
|
318
|
-
skipByInterval skipInterval: CMTime,
|
|
319
|
-
completion completionHandler: @escaping () -> Void
|
|
320
|
-
) {
|
|
321
|
-
completionHandler()
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
enum LivePipError: Error {
|
|
326
|
-
case engineMissing
|
|
327
|
-
}
|
package/ios/LivePipModule.swift
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
2
|
-
|
|
3
|
-
public class LivePipModule: Module {
|
|
4
|
-
public func definition() -> ModuleDefinition {
|
|
5
|
-
Name("LivePip")
|
|
6
|
-
|
|
7
|
-
Events("onPipStart", "onPipStop", "onPipRestore", "onPipError")
|
|
8
|
-
|
|
9
|
-
OnCreate {
|
|
10
|
-
LivePipController.shared.onEvent = { [weak self] name, payload in
|
|
11
|
-
self?.sendEvent(name, payload)
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
AsyncFunction("configure") {
|
|
16
|
-
try LivePipController.shared.configure()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
AsyncFunction("setStream") { (streamId: String?) in
|
|
20
|
-
LivePipController.shared.setStream(streamId)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
AsyncFunction("setArmed") { (armed: Bool) in
|
|
24
|
-
LivePipController.shared.setArmed(armed)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
AsyncFunction("start") {
|
|
28
|
-
LivePipController.shared.start()
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
AsyncFunction("stop") {
|
|
32
|
-
LivePipController.shared.stop()
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
package/plugin/build/index.d.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { type ConfigPlugin } from 'expo/config-plugins';
|
|
2
|
-
/**
|
|
3
|
-
* Everything a prebuilt project needs for live selling that a library cannot declare for itself.
|
|
4
|
-
*
|
|
5
|
-
* All four of these live in files `expo prebuild` regenerates — `android/build.gradle`,
|
|
6
|
-
* `AndroidManifest.xml`, `Info.plist` — so an edit made by hand survives exactly until the next native
|
|
7
|
-
* build. That is the worst kind of failure: it works on the machine that set it and silently stops
|
|
8
|
-
* working in CI.
|
|
9
|
-
*/
|
|
10
|
-
export interface LiveSellingPluginProps {
|
|
11
|
-
/**
|
|
12
|
-
* Whether to set up system picture-in-picture. Default true.
|
|
13
|
-
*
|
|
14
|
-
* Off still leaves the Zego repository and the permission strings, which the SDK needs regardless.
|
|
15
|
-
*/
|
|
16
|
-
pip?: boolean;
|
|
17
|
-
/** Shown verbatim by iOS when the camera is first opened. Co-hosting needs it. */
|
|
18
|
-
cameraPermission?: string;
|
|
19
|
-
microphonePermission?: string;
|
|
20
|
-
/**
|
|
21
|
-
* Add CAMERA and RECORD_AUDIO on Android. Default true.
|
|
22
|
-
*
|
|
23
|
-
* Worth keeping even on a viewer-only build: Zego links both APIs, and App Store validation rejects
|
|
24
|
-
* the upload (ITMS-90683) on the linked symbols alone.
|
|
25
|
-
*/
|
|
26
|
-
capturePermissions?: boolean;
|
|
27
|
-
}
|
|
28
|
-
declare const withLiveSelling: ConfigPlugin<LiveSellingPluginProps | void>;
|
|
29
|
-
export default withLiveSelling;
|
package/plugin/build/index.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const config_plugins_1 = require("expo/config-plugins");
|
|
4
|
-
const DEFAULT_CAMERA = 'Used to appear on camera when you join a live show as a guest.';
|
|
5
|
-
const DEFAULT_MICROPHONE = 'Used to be heard when you join a live show as a guest.';
|
|
6
|
-
/** Idempotence marker: a second prebuild must not append the block again. */
|
|
7
|
-
const ZEGO_MARKER = '// @generated by @tiledev/sdk-apptile-live-selling';
|
|
8
|
-
/**
|
|
9
|
-
* Zego's own Maven repository.
|
|
10
|
-
*
|
|
11
|
-
* `zego-express-engine-reactnative` declares `im.zego:express-video`, which lives only on
|
|
12
|
-
* `storage.zego.im/maven` — not Google, Maven Central or JitPack. The library's own build.gradle names
|
|
13
|
-
* that repo, but Gradle resolves a subproject's runtime dependencies with the *consuming* project's
|
|
14
|
-
* repositories, so `:app:debugRuntimeClasspath` searches only what the root script declares and fails
|
|
15
|
-
* at configuration time with "Could not find im.zego:express-video" on a clean checkout —
|
|
16
|
-
* unconditionally, for whoever next runs `expo run:android`.
|
|
17
|
-
*/
|
|
18
|
-
const ZEGO_REPO_BLOCK = `
|
|
19
|
-
${ZEGO_MARKER}
|
|
20
|
-
allprojects {
|
|
21
|
-
repositories {
|
|
22
|
-
maven { url "https://storage.zego.im/maven" }
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
`;
|
|
26
|
-
const withZegoMavenRepo = (config) => (0, config_plugins_1.withProjectBuildGradle)(config, (inner) => {
|
|
27
|
-
if (inner.modResults.language !== 'groovy') {
|
|
28
|
-
throw new Error(`@tiledev/sdk-apptile-live-selling expects a Groovy android/build.gradle; got ${inner.modResults.language}`);
|
|
29
|
-
}
|
|
30
|
-
if (!inner.modResults.contents.includes(ZEGO_MARKER)) {
|
|
31
|
-
inner.modResults.contents += ZEGO_REPO_BLOCK;
|
|
32
|
-
}
|
|
33
|
-
return inner;
|
|
34
|
-
});
|
|
35
|
-
/**
|
|
36
|
-
* The configuration a PIP transition changes.
|
|
37
|
-
*
|
|
38
|
-
* Anything not listed here recreates the activity instead of resizing it — which means the React host
|
|
39
|
-
* restarts on the way into the window, so the stream drops and the PIP window shows a fresh launch.
|
|
40
|
-
* `smallestScreenSize` is the one prebuild does not already write, and it is the one that matters:
|
|
41
|
-
* entering PIP is exactly a change of smallest width.
|
|
42
|
-
*/
|
|
43
|
-
const REQUIRED_CONFIG_CHANGES = [
|
|
44
|
-
'screenSize',
|
|
45
|
-
'smallestScreenSize',
|
|
46
|
-
'screenLayout',
|
|
47
|
-
'orientation',
|
|
48
|
-
];
|
|
49
|
-
/**
|
|
50
|
-
* `android:supportsPictureInPicture` is an attribute of the *activity*, and the activity belongs to
|
|
51
|
-
* the app — a library manifest cannot add it, which is why this is a plugin at all.
|
|
52
|
-
*/
|
|
53
|
-
const withLivePipAndroid = (config) => (0, config_plugins_1.withAndroidManifest)(config, (inner) => {
|
|
54
|
-
var _a;
|
|
55
|
-
const activity = config_plugins_1.AndroidConfig.Manifest.getMainActivityOrThrow(inner.modResults);
|
|
56
|
-
activity.$['android:supportsPictureInPicture'] = 'true';
|
|
57
|
-
// Merged rather than replaced: prebuild's own list carries `keyboard`, `uiMode` and friends, and
|
|
58
|
-
// dropping those would recreate the activity when the keyboard opens.
|
|
59
|
-
const declared = ((_a = activity.$['android:configChanges']) !== null && _a !== void 0 ? _a : '').split('|').filter(Boolean);
|
|
60
|
-
for (const change of REQUIRED_CONFIG_CHANGES) {
|
|
61
|
-
if (!declared.includes(change))
|
|
62
|
-
declared.push(change);
|
|
63
|
-
}
|
|
64
|
-
activity.$['android:configChanges'] = declared.join('|');
|
|
65
|
-
return inner;
|
|
66
|
-
});
|
|
67
|
-
/**
|
|
68
|
-
* `UIBackgroundModes: ["audio"]` is what keeps a system PIP window alive: without it iOS suspends the
|
|
69
|
-
* app and the window freezes on its last frame within seconds.
|
|
70
|
-
*/
|
|
71
|
-
const withLivePipIos = (config, props) => (0, config_plugins_1.withInfoPlist)(config, (inner) => {
|
|
72
|
-
var _a, _b, _c, _d;
|
|
73
|
-
const plist = inner.modResults;
|
|
74
|
-
if (props.pip !== false) {
|
|
75
|
-
const modes = new Set(Array.isArray(plist.UIBackgroundModes) ? plist.UIBackgroundModes : []);
|
|
76
|
-
modes.add('audio');
|
|
77
|
-
plist.UIBackgroundModes = Array.from(modes);
|
|
78
|
-
}
|
|
79
|
-
// Not overwritten when the app already sets its own — a host that has written brand-specific copy
|
|
80
|
-
// means it, and Apple shows these strings verbatim.
|
|
81
|
-
plist.NSCameraUsageDescription =
|
|
82
|
-
(_b = (_a = plist.NSCameraUsageDescription) !== null && _a !== void 0 ? _a : props.cameraPermission) !== null && _b !== void 0 ? _b : DEFAULT_CAMERA;
|
|
83
|
-
plist.NSMicrophoneUsageDescription =
|
|
84
|
-
(_d = (_c = plist.NSMicrophoneUsageDescription) !== null && _c !== void 0 ? _c : props.microphonePermission) !== null && _d !== void 0 ? _d : DEFAULT_MICROPHONE;
|
|
85
|
-
return inner;
|
|
86
|
-
});
|
|
87
|
-
const withCapturePermissions = (config) => config_plugins_1.AndroidConfig.Permissions.withPermissions(config, [
|
|
88
|
-
'android.permission.CAMERA',
|
|
89
|
-
'android.permission.RECORD_AUDIO',
|
|
90
|
-
]);
|
|
91
|
-
const withLiveSelling = (config, props) => {
|
|
92
|
-
const options = props !== null && props !== void 0 ? props : {};
|
|
93
|
-
let next = withZegoMavenRepo(config);
|
|
94
|
-
next = withLivePipIos(next, options);
|
|
95
|
-
if (options.pip !== false)
|
|
96
|
-
next = withLivePipAndroid(next);
|
|
97
|
-
if (options.capturePermissions !== false)
|
|
98
|
-
next = withCapturePermissions(next);
|
|
99
|
-
return next;
|
|
100
|
-
};
|
|
101
|
-
exports.default = withLiveSelling;
|