@mux/mux-react-native-player 0.1.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.
Files changed (56) hide show
  1. package/MuxReactNativePlayer.podspec +37 -0
  2. package/README.md +134 -0
  3. package/android/build.gradle +33 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/mux/reactnativeplayer/MuxReactNativePlayerModule.kt +135 -0
  6. package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoRecords.kt +174 -0
  7. package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoView.kt +452 -0
  8. package/android/src/main/res/layout/mux_video_player_view.xml +6 -0
  9. package/assets/MuxRobot_02.gif +0 -0
  10. package/assets/MuxRobot_02@2x.gif +0 -0
  11. package/assets/MuxRobot_03.gif +0 -0
  12. package/assets/MuxRobot_03@2x.gif +0 -0
  13. package/assets/MuxRobot_04.gif +0 -0
  14. package/assets/MuxRobot_04@2x.gif +0 -0
  15. package/assets/MuxRobot_05.gif +0 -0
  16. package/assets/MuxRobot_05@2x.gif +0 -0
  17. package/build/MuxVideoControls.d.ts +21 -0
  18. package/build/MuxVideoControls.d.ts.map +1 -0
  19. package/build/MuxVideoControls.js +1032 -0
  20. package/build/MuxVideoPlayer.d.ts +59 -0
  21. package/build/MuxVideoPlayer.d.ts.map +1 -0
  22. package/build/MuxVideoPlayer.js +265 -0
  23. package/build/MuxVideoView.d.ts +39 -0
  24. package/build/MuxVideoView.d.ts.map +1 -0
  25. package/build/MuxVideoView.js +254 -0
  26. package/build/NativeMuxVideoView.d.ts +5 -0
  27. package/build/NativeMuxVideoView.d.ts.map +1 -0
  28. package/build/NativeMuxVideoView.js +4 -0
  29. package/build/index.d.ts +6 -0
  30. package/build/index.d.ts.map +1 -0
  31. package/build/index.js +3 -0
  32. package/build/normalizeSource.d.ts +7 -0
  33. package/build/normalizeSource.d.ts.map +1 -0
  34. package/build/normalizeSource.js +76 -0
  35. package/build/screenOrientation.d.ts +3 -0
  36. package/build/screenOrientation.d.ts.map +1 -0
  37. package/build/screenOrientation.js +38 -0
  38. package/build/types.d.ts +170 -0
  39. package/build/types.d.ts.map +1 -0
  40. package/build/types.js +1 -0
  41. package/expo-module.config.json +13 -0
  42. package/ios/MuxReactNativePlayerModule.swift +139 -0
  43. package/ios/MuxVideoRecords.swift +212 -0
  44. package/ios/MuxVideoView.swift +502 -0
  45. package/package.json +69 -0
  46. package/plugin/index.d.ts +11 -0
  47. package/plugin/index.js +1 -0
  48. package/plugin/withMuxReactNativePlayer.js +203 -0
  49. package/src/MuxVideoControls.tsx +1772 -0
  50. package/src/MuxVideoPlayer.ts +338 -0
  51. package/src/MuxVideoView.tsx +412 -0
  52. package/src/NativeMuxVideoView.ts +15 -0
  53. package/src/index.ts +32 -0
  54. package/src/normalizeSource.ts +101 -0
  55. package/src/screenOrientation.ts +46 -0
  56. package/src/types.ts +228 -0
@@ -0,0 +1,452 @@
1
+ package com.mux.reactnativeplayer
2
+
3
+ import android.content.Context
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import android.view.LayoutInflater
7
+ import android.view.ViewGroup
8
+ import androidx.annotation.OptIn
9
+ import androidx.media3.common.C
10
+ import androidx.media3.common.PlaybackException
11
+ import androidx.media3.common.Player
12
+ import androidx.media3.common.TrackGroup
13
+ import androidx.media3.common.TrackSelectionOverride
14
+ import androidx.media3.common.Tracks
15
+ import androidx.media3.exoplayer.DefaultLoadControl
16
+ import androidx.media3.common.util.UnstableApi
17
+ import androidx.media3.ui.AspectRatioFrameLayout
18
+ import androidx.media3.ui.PlayerView
19
+ import com.mux.player.MuxPlayer
20
+ import expo.modules.kotlin.AppContext
21
+ import expo.modules.kotlin.viewevent.EventDispatcher
22
+ import expo.modules.kotlin.views.ExpoView
23
+
24
+ @OptIn(UnstableApi::class)
25
+ class MuxVideoView(
26
+ context: Context,
27
+ appContext: AppContext,
28
+ ) : ExpoView(context, appContext) {
29
+ private val onStatusChange by EventDispatcher()
30
+ private val onPlayingChange by EventDispatcher()
31
+ private val onTimeUpdate by EventDispatcher()
32
+ private val onSourceLoad by EventDispatcher()
33
+ private val onSourceError by EventDispatcher()
34
+
35
+ private val mainHandler = Handler(Looper.getMainLooper())
36
+ private val playerView = (LayoutInflater.from(context)
37
+ .inflate(R.layout.mux_video_player_view, this, false) as PlayerView).also {
38
+ it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
39
+ it.useController = true
40
+ it.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
41
+ it.setKeepContentOnPlayerReset(true)
42
+ addView(it)
43
+ }
44
+
45
+ private var player: MuxPlayer? = null
46
+ private var sourceFingerprint: String? = null
47
+ private var currentPlaybackId: String? = null
48
+ private var didEmitSourceLoad = false
49
+ private var muted = false
50
+ private var volume = 1f
51
+ private var loop = false
52
+ private var playbackRate = 1f
53
+ private var shouldPlay = false
54
+ private var timeUpdateEventIntervalMs = 500L
55
+ private var startupBufferDurationMs = 0L
56
+
57
+ private data class TextTrackSelection(
58
+ val id: String,
59
+ val trackGroup: TrackGroup,
60
+ val trackIndex: Int,
61
+ )
62
+
63
+ private val timeUpdateRunnable = object : Runnable {
64
+ override fun run() {
65
+ sendTimeUpdate()
66
+ mainHandler.postDelayed(this, timeUpdateEventIntervalMs)
67
+ }
68
+ }
69
+
70
+ private val listener = object : Player.Listener {
71
+ override fun onPlaybackStateChanged(playbackState: Int) {
72
+ if (playbackState == Player.STATE_READY && !didEmitSourceLoad) {
73
+ didEmitSourceLoad = true
74
+ onSourceLoad(
75
+ mapOf(
76
+ "playbackId" to currentPlaybackId.orEmpty(),
77
+ "duration" to durationSeconds(),
78
+ "captionTracks" to captionTracksPayload(),
79
+ "selectedCaptionTrackId" to selectedCaptionTrackId().orEmpty(),
80
+ )
81
+ )
82
+ }
83
+ if (playbackState == Player.STATE_READY && shouldPlay && player?.isPlaying != true) {
84
+ player?.play()
85
+ }
86
+ sendStatusChange()
87
+ }
88
+
89
+ override fun onIsPlayingChanged(isPlaying: Boolean) {
90
+ onPlayingChange(mapOf("isPlaying" to isPlaying))
91
+ sendStatusChange()
92
+ }
93
+
94
+ override fun onPlayerError(error: PlaybackException) {
95
+ val message = error.localizedMessage ?: "Mux playback failed."
96
+ onSourceError(
97
+ mutableMapOf<String, Any>(
98
+ "playbackId" to currentPlaybackId.orEmpty(),
99
+ "message" to message,
100
+ "code" to error.errorCodeName
101
+ )
102
+ )
103
+ sendStatusChange("error", message)
104
+ }
105
+
106
+ override fun onTracksChanged(tracks: Tracks) {
107
+ sendStatusChange()
108
+ }
109
+ }
110
+
111
+ override fun onAttachedToWindow() {
112
+ super.onAttachedToWindow()
113
+ startTimeUpdates()
114
+ }
115
+
116
+ override fun onDetachedFromWindow() {
117
+ stopTimeUpdates()
118
+ super.onDetachedFromWindow()
119
+ }
120
+
121
+ fun setSource(source: MuxVideoSourceRecord?) {
122
+ if (source == null) {
123
+ release()
124
+ return
125
+ }
126
+
127
+ if (source.fingerprint == sourceFingerprint) {
128
+ return
129
+ }
130
+
131
+ releasePlayer()
132
+ sourceFingerprint = source.fingerprint
133
+ currentPlaybackId = source.playbackId
134
+ didEmitSourceLoad = false
135
+ sendStatusChange("loading")
136
+
137
+ val bufferForPlaybackMs = startupBufferDurationMs.coerceAtLeast(500L).toInt()
138
+ val bufferForPlaybackAfterRebufferMs = startupBufferDurationMs.coerceAtLeast(1_500L).toInt()
139
+ val minBufferMs = startupBufferDurationMs.coerceAtLeast(3_000L).toInt()
140
+ val loadControl = DefaultLoadControl.Builder()
141
+ .setBufferDurationsMs(
142
+ minBufferMs,
143
+ 12_000,
144
+ bufferForPlaybackMs,
145
+ bufferForPlaybackAfterRebufferMs,
146
+ )
147
+ .setPrioritizeTimeOverSizeThresholds(true)
148
+ .build()
149
+
150
+ val builder = MuxPlayer.Builder(context)
151
+ .enableSmartCache(true)
152
+ .applyExoConfig {
153
+ setLoadControl(loadControl)
154
+ }
155
+ .addMonitoringData(source.toCustomerData())
156
+
157
+ source.metadata?.envKey?.takeIf { it.isNotBlank() }?.let {
158
+ builder.setMuxDataEnv(it)
159
+ }
160
+
161
+ val nextPlayer = builder.build()
162
+ nextPlayer.addListener(listener)
163
+ nextPlayer.setMediaItem(source.toMediaItem())
164
+ nextPlayer.playWhenReady = shouldPlay
165
+ nextPlayer.prepare()
166
+
167
+ player = nextPlayer
168
+ playerView.player = nextPlayer
169
+ applyPlayerConfiguration()
170
+ }
171
+
172
+ fun setNativeControls(enabled: Boolean) {
173
+ playerView.useController = enabled
174
+ }
175
+
176
+ fun setContentFit(contentFit: String) {
177
+ playerView.resizeMode = when (contentFit) {
178
+ "cover" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
179
+ "fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
180
+ else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
181
+ }
182
+ }
183
+
184
+ fun setTimeUpdateEventInterval(interval: Double) {
185
+ timeUpdateEventIntervalMs = (interval.coerceAtLeast(0.1) * 1000).toLong()
186
+ stopTimeUpdates()
187
+ startTimeUpdates()
188
+ }
189
+
190
+ fun setStartupBufferDuration(duration: Double) {
191
+ startupBufferDurationMs = (duration.coerceAtLeast(0.0) * 1000).toLong()
192
+ }
193
+
194
+ fun setPlayWhenReady(playWhenReady: Boolean) {
195
+ if (shouldPlay == playWhenReady) {
196
+ return
197
+ }
198
+
199
+ if (playWhenReady) {
200
+ play()
201
+ } else {
202
+ pause()
203
+ }
204
+ }
205
+
206
+ fun setMuted(muted: Boolean) {
207
+ this.muted = muted
208
+ player?.volume = if (muted) 0f else volume
209
+ sendStatusChange()
210
+ }
211
+
212
+ fun setVolume(volume: Double) {
213
+ this.volume = volume.coerceIn(0.0, 1.0).toFloat()
214
+ if (!muted) {
215
+ player?.volume = this.volume
216
+ }
217
+ sendStatusChange()
218
+ }
219
+
220
+ fun setLoop(loop: Boolean) {
221
+ this.loop = loop
222
+ player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
223
+ sendStatusChange()
224
+ }
225
+
226
+ fun setPlaybackRate(rate: Double) {
227
+ playbackRate = rate.coerceIn(0.25, 4.0).toFloat()
228
+ player?.setPlaybackSpeed(playbackRate)
229
+ sendStatusChange()
230
+ }
231
+
232
+ fun setCaptionTrack(trackId: String?) {
233
+ val currentPlayer = player ?: return
234
+ val builder = currentPlayer.trackSelectionParameters.buildUpon()
235
+
236
+ if (trackId == null) {
237
+ currentPlayer.trackSelectionParameters = builder
238
+ .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
239
+ .build()
240
+ sendStatusChange()
241
+ return
242
+ }
243
+
244
+ val selection = findTextTrack(trackId) ?: return
245
+ currentPlayer.trackSelectionParameters = builder
246
+ .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
247
+ .setOverrideForType(
248
+ TrackSelectionOverride(selection.trackGroup, listOf(selection.trackIndex))
249
+ )
250
+ .build()
251
+ sendStatusChange()
252
+ }
253
+
254
+ fun play() {
255
+ shouldPlay = true
256
+ player?.play()
257
+ sendStatusChange()
258
+ }
259
+
260
+ fun pause() {
261
+ shouldPlay = false
262
+ player?.pause()
263
+ sendStatusChange()
264
+ }
265
+
266
+ fun replay() {
267
+ shouldPlay = true
268
+ seekTo(0.0)
269
+ play()
270
+ }
271
+
272
+ fun seekBy(seconds: Double) {
273
+ val current = (player?.currentPosition ?: 0L) / 1000.0
274
+ seekTo(current + seconds)
275
+ }
276
+
277
+ fun seekTo(seconds: Double) {
278
+ player?.seekTo((seconds.coerceAtLeast(0.0) * 1000).toLong())
279
+ sendStatusChange()
280
+ sendTimeUpdate()
281
+ }
282
+
283
+ fun release() {
284
+ releasePlayer()
285
+ sourceFingerprint = null
286
+ currentPlaybackId = null
287
+ didEmitSourceLoad = false
288
+ shouldPlay = false
289
+ sendStatusChange("idle")
290
+ }
291
+
292
+ private fun applyPlayerConfiguration() {
293
+ player?.volume = if (muted) 0f else volume
294
+ player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
295
+ player?.setPlaybackSpeed(playbackRate)
296
+ }
297
+
298
+ private fun releasePlayer() {
299
+ playerView.player = null
300
+ player?.removeListener(listener)
301
+ player?.release()
302
+ player = null
303
+ }
304
+
305
+ private fun startTimeUpdates() {
306
+ mainHandler.removeCallbacks(timeUpdateRunnable)
307
+ mainHandler.postDelayed(timeUpdateRunnable, timeUpdateEventIntervalMs)
308
+ }
309
+
310
+ private fun stopTimeUpdates() {
311
+ mainHandler.removeCallbacks(timeUpdateRunnable)
312
+ }
313
+
314
+ private fun sendTimeUpdate() {
315
+ onTimeUpdate(
316
+ mapOf(
317
+ "currentTime" to currentTimeSeconds(),
318
+ "duration" to durationSeconds(),
319
+ "bufferedPosition" to bufferedPositionSeconds()
320
+ )
321
+ )
322
+ }
323
+
324
+ private fun sendStatusChange(status: String? = null, error: String? = null) {
325
+ val payload = mutableMapOf<String, Any>(
326
+ "status" to (status ?: inferStatus()),
327
+ "currentTime" to currentTimeSeconds(),
328
+ "duration" to durationSeconds(),
329
+ "bufferedPosition" to bufferedPositionSeconds(),
330
+ "muted" to muted,
331
+ "volume" to volume.toDouble(),
332
+ "loop" to loop,
333
+ "playbackRate" to playbackRate.toDouble(),
334
+ "captionTracks" to captionTracksPayload(),
335
+ "selectedCaptionTrackId" to selectedCaptionTrackId().orEmpty(),
336
+ )
337
+
338
+ if (error != null) {
339
+ payload["error"] = error
340
+ }
341
+
342
+ onStatusChange(
343
+ payload
344
+ )
345
+ }
346
+
347
+ private fun inferStatus(): String {
348
+ val player = player ?: return "idle"
349
+ return when (player.playbackState) {
350
+ Player.STATE_BUFFERING -> "buffering"
351
+ Player.STATE_READY -> if (player.isPlaying) "playing" else "paused"
352
+ Player.STATE_ENDED -> "ended"
353
+ Player.STATE_IDLE -> if (currentPlaybackId == null) "idle" else "loading"
354
+ else -> "idle"
355
+ }
356
+ }
357
+
358
+ private fun currentTimeSeconds(): Double {
359
+ return ((player?.currentPosition ?: 0L).coerceAtLeast(0L)) / 1000.0
360
+ }
361
+
362
+ private fun durationSeconds(): Double {
363
+ val duration = player?.duration ?: C.TIME_UNSET
364
+ return if (duration == C.TIME_UNSET || duration < 0) 0.0 else duration / 1000.0
365
+ }
366
+
367
+ private fun bufferedPositionSeconds(): Double {
368
+ return ((player?.bufferedPosition ?: 0L).coerceAtLeast(0L)) / 1000.0
369
+ }
370
+
371
+ private fun captionTracksPayload(): List<Map<String, Any>> {
372
+ val currentPlayer = player ?: return emptyList()
373
+ val tracks = mutableListOf<Map<String, Any>>()
374
+ var fallbackIndex = 1
375
+
376
+ currentPlayer.currentTracks.groups.forEachIndexed { groupIndex, group ->
377
+ if (group.type != C.TRACK_TYPE_TEXT) {
378
+ return@forEachIndexed
379
+ }
380
+
381
+ for (trackIndex in 0 until group.length) {
382
+ if (!group.isTrackSupported(trackIndex)) {
383
+ continue
384
+ }
385
+
386
+ val format = group.getTrackFormat(trackIndex)
387
+ val language = format.language
388
+ val label = format.label
389
+ ?.takeIf { it.isNotBlank() }
390
+ ?: language?.takeIf { it.isNotBlank() }
391
+ ?: "Captions ${fallbackIndex++}"
392
+ val payload = mutableMapOf<String, Any>(
393
+ "id" to "$groupIndex:$trackIndex",
394
+ "label" to label,
395
+ "kind" to captionTrackKind(format.selectionFlags, format.roleFlags),
396
+ )
397
+
398
+ language?.takeIf { it.isNotBlank() }?.let {
399
+ payload["language"] = it
400
+ }
401
+
402
+ tracks.add(payload)
403
+ }
404
+ }
405
+
406
+ return tracks
407
+ }
408
+
409
+ private fun selectedCaptionTrackId(): String? {
410
+ val currentPlayer = player ?: return null
411
+
412
+ currentPlayer.currentTracks.groups.forEachIndexed { groupIndex, group ->
413
+ if (group.type != C.TRACK_TYPE_TEXT) {
414
+ return@forEachIndexed
415
+ }
416
+
417
+ for (trackIndex in 0 until group.length) {
418
+ if (group.isTrackSelected(trackIndex)) {
419
+ return "$groupIndex:$trackIndex"
420
+ }
421
+ }
422
+ }
423
+
424
+ return null
425
+ }
426
+
427
+ private fun findTextTrack(trackId: String): TextTrackSelection? {
428
+ val currentPlayer = player ?: return null
429
+
430
+ currentPlayer.currentTracks.groups.forEachIndexed { groupIndex, group ->
431
+ if (group.type != C.TRACK_TYPE_TEXT) {
432
+ return@forEachIndexed
433
+ }
434
+
435
+ for (trackIndex in 0 until group.length) {
436
+ if ("$groupIndex:$trackIndex" == trackId && group.isTrackSupported(trackIndex)) {
437
+ return TextTrackSelection(trackId, group.mediaTrackGroup, trackIndex)
438
+ }
439
+ }
440
+ }
441
+
442
+ return null
443
+ }
444
+
445
+ private fun captionTrackKind(selectionFlags: Int, roleFlags: Int): String {
446
+ return when {
447
+ selectionFlags and C.SELECTION_FLAG_FORCED != 0 -> "forced"
448
+ roleFlags and C.ROLE_FLAG_CAPTION != 0 -> "captions"
449
+ else -> "subtitles"
450
+ }
451
+ }
452
+ }
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
3
+ xmlns:app="http://schemas.android.com/apk/res-auto"
4
+ android:layout_width="match_parent"
5
+ android:layout_height="match_parent"
6
+ app:surface_type="texture_view" />
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,21 @@
1
+ import type { MuxPlayerStatus, MuxVideoChapter, MuxVideoControlsTheme, MuxVideoKeyMoment, MuxVideoRobotsConfig, MuxVideoSummary } from './types';
2
+ import { MuxVideoPlayer } from './MuxVideoPlayer';
3
+ type MuxVideoControlsProps = {
4
+ player: MuxVideoPlayer;
5
+ status: MuxPlayerStatus;
6
+ shouldPlay: boolean;
7
+ theme?: MuxVideoControlsTheme;
8
+ robots?: MuxVideoRobotsConfig;
9
+ allowsFullscreen?: boolean;
10
+ isFullscreen?: boolean;
11
+ onToggleFullscreen?: () => void;
12
+ generatedSummary?: MuxVideoSummary;
13
+ generatedChapters?: MuxVideoChapter[];
14
+ generatedKeyMoments?: MuxVideoKeyMoment[];
15
+ onGeneratedSummaryChange?: (summary: MuxVideoSummary | undefined) => void;
16
+ onGeneratedChaptersChange?: (chapters: MuxVideoChapter[] | undefined) => void;
17
+ onGeneratedKeyMomentsChange?: (keyMoments: MuxVideoKeyMoment[] | undefined) => void;
18
+ };
19
+ export declare function MuxVideoControls({ player, status, shouldPlay, theme, robots, allowsFullscreen, isFullscreen, onToggleFullscreen, generatedSummary, generatedChapters, generatedKeyMoments, onGeneratedSummaryChange, onGeneratedChaptersChange, onGeneratedKeyMomentsChange, }: MuxVideoControlsProps): import("react/jsx-runtime").JSX.Element;
20
+ export {};
21
+ //# sourceMappingURL=MuxVideoControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MuxVideoControls.d.ts","sourceRoot":"","sources":["../src/MuxVideoControls.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EACV,eAAe,EAEf,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,eAAe,EAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAKlD,KAAK,qBAAqB,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,qBAAqB,CAAC;IAC9B,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,gBAAgB,CAAC,EAAE,eAAe,CAAC;IACnC,iBAAiB,CAAC,EAAE,eAAe,EAAE,CAAC;IACtC,mBAAmB,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC1C,wBAAwB,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,KAAK,IAAI,CAAC;IAC1E,yBAAyB,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,EAAE,GAAG,SAAS,KAAK,IAAI,CAAC;IAC9E,2BAA2B,CAAC,EAAE,CAAC,UAAU,EAAE,iBAAiB,EAAE,GAAG,SAAS,KAAK,IAAI,CAAC;CACrF,CAAC;AA8BF,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,MAAM,EACN,UAAU,EACV,KAAK,EACL,MAAM,EACN,gBAAwB,EACxB,YAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,wBAAwB,EACxB,yBAAyB,EACzB,2BAA2B,GAC5B,EAAE,qBAAqB,2CAk4BvB"}