bitmovin-player-react-native 0.3.0 → 0.4.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 (36) hide show
  1. package/README.md +238 -24
  2. package/RNBitmovinPlayer.podspec +3 -1
  3. package/android/build.gradle +7 -5
  4. package/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +4 -5
  5. package/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +41 -5
  6. package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +273 -2
  7. package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +45 -5
  8. package/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +4 -5
  9. package/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt +3 -1
  10. package/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +301 -7
  11. package/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableArray.kt +35 -0
  12. package/android/src/main/java/com/bitmovin/player/reactnative/extensions/WritableMap.kt +19 -0
  13. package/android/src/main/java/com/bitmovin/player/reactnative/ui/RNPictureInPictureHandler.kt +191 -0
  14. package/ios/AudioSessionModule.m +10 -0
  15. package/ios/AudioSessionModule.swift +65 -0
  16. package/ios/Event+JSON.swift +123 -0
  17. package/ios/PlayerModule.m +6 -0
  18. package/ios/PlayerModule.swift +44 -0
  19. package/ios/RCTConvert+BitmovinPlayer.swift +285 -15
  20. package/ios/RNPlayerView+PlayerListener.swift +52 -0
  21. package/ios/RNPlayerView+UserInterfaceListener.swift +19 -0
  22. package/ios/RNPlayerView.swift +17 -0
  23. package/ios/RNPlayerViewManager.m +18 -1
  24. package/ios/RNPlayerViewManager.swift +2 -1
  25. package/lib/index.d.ts +577 -3
  26. package/lib/index.js +92 -33
  27. package/lib/index.mjs +75 -19
  28. package/package.json +1 -1
  29. package/src/advertising.ts +155 -0
  30. package/src/audioSession.ts +47 -0
  31. package/src/components/PlayerView/events.ts +39 -3
  32. package/src/components/PlayerView/index.tsx +31 -11
  33. package/src/events.ts +212 -0
  34. package/src/index.ts +2 -0
  35. package/src/player.ts +41 -1
  36. package/src/tweaksConfig.ts +153 -0
@@ -1,7 +1,11 @@
1
1
  package com.bitmovin.player.reactnative.converter
2
2
 
3
+ import com.bitmovin.player.api.DeviceDescription.DeviceName
4
+ import com.bitmovin.player.api.DeviceDescription.ModelName
3
5
  import com.bitmovin.player.api.PlaybackConfig
4
6
  import com.bitmovin.player.api.PlayerConfig
7
+ import com.bitmovin.player.api.TweaksConfig
8
+ import com.bitmovin.player.api.advertising.*
5
9
  import com.bitmovin.player.api.drm.WidevineConfig
6
10
  import com.bitmovin.player.api.event.PlayerEvent
7
11
  import com.bitmovin.player.api.event.SourceEvent
@@ -11,6 +15,10 @@ import com.bitmovin.player.api.source.Source
11
15
  import com.bitmovin.player.api.source.SourceConfig
12
16
  import com.bitmovin.player.api.source.SourceType
13
17
  import com.bitmovin.player.reactnative.extensions.getName
18
+ import com.bitmovin.player.reactnative.extensions.putInt
19
+ import com.bitmovin.player.reactnative.extensions.putDouble
20
+ import com.bitmovin.player.reactnative.extensions.toList
21
+ import com.bitmovin.player.reactnative.extensions.toReadableArray
14
22
  import com.facebook.react.bridge.*
15
23
  import java.util.UUID
16
24
 
@@ -33,20 +41,146 @@ class JsonConverter {
33
41
  PlayerConfig()
34
42
  }
35
43
  if (json.hasKey("playbackConfig")) {
36
- var playbackConfigJson = json.getMap("playbackConfig")
37
- if (playbackConfigJson?.hasKey("isAutoplayEnabled") == true) {
38
- playerConfig.playbackConfig.isAutoplayEnabled = playbackConfigJson.getBoolean("isAutoplayEnabled")
44
+ toPlaybackConfig(json.getMap("playbackConfig"))?.let {
45
+ playerConfig.playbackConfig = it
39
46
  }
40
- if(playbackConfigJson?.hasKey("isMuted") == true) {
41
- playerConfig.playbackConfig.isMuted = playbackConfigJson.getBoolean("isMuted")
47
+ }
48
+ if (json.hasKey("tweaksConfig")) {
49
+ toTweaksConfig(json.getMap("tweaksConfig"))?.let {
50
+ playerConfig.tweaksConfig = it
42
51
  }
43
- if(playbackConfigJson?.hasKey("isTimeShiftEnabled") == true) {
44
- playerConfig.playbackConfig.isTimeShiftEnabled = playbackConfigJson.getBoolean("isTimeShiftEnabled")
52
+ }
53
+ if (json.hasKey("advertisingConfig")) {
54
+ toAdvertisingConfig(json.getMap("advertisingConfig"))?.let {
55
+ playerConfig.advertisingConfig = it
45
56
  }
46
57
  }
47
58
  return playerConfig
48
59
  }
49
60
 
61
+ /**
62
+ * Converts any JS object into a `PlaybackConfig` object.
63
+ * @param json JS object representing the `PlaybackConfig`.
64
+ * @return The generated `PlaybackConfig` if successful, `null` otherwise.
65
+ */
66
+ @JvmStatic
67
+ fun toPlaybackConfig(json: ReadableMap?): PlaybackConfig? {
68
+ if (json == null) {
69
+ return null
70
+ }
71
+ val playbackConfig = PlaybackConfig()
72
+ if (json.hasKey("isAutoplayEnabled")) {
73
+ playbackConfig.isAutoplayEnabled = json.getBoolean("isAutoplayEnabled")
74
+ }
75
+ if (json.hasKey("isMuted")) {
76
+ playbackConfig.isMuted = json.getBoolean("isMuted")
77
+ }
78
+ if (json.hasKey("isTimeShiftEnabled")) {
79
+ playbackConfig.isTimeShiftEnabled = json.getBoolean("isTimeShiftEnabled")
80
+ }
81
+ return playbackConfig
82
+ }
83
+
84
+ /**
85
+ * Converts any JS object into a `TweaksConfig` object.
86
+ * @param json JS object representing the `TweaksConfig`.
87
+ * @return The generated `TweaksConfig` if successful, `null` otherwise.
88
+ */
89
+ @JvmStatic
90
+ fun toTweaksConfig(json: ReadableMap?): TweaksConfig? {
91
+ if (json == null) {
92
+ return null
93
+ }
94
+ val tweaksConfig = TweaksConfig()
95
+ if (json.hasKey("timeChangedInterval")) {
96
+ tweaksConfig.timeChangedInterval = json.getDouble("timeChangedInterval")
97
+ }
98
+ if (json.hasKey("bandwidthEstimateWeightLimit")) {
99
+ tweaksConfig.bandwidthEstimateWeightLimit = json.getInt("bandwidthEstimateWeightLimit")
100
+ }
101
+ if (json.hasKey("devicesThatRequireSurfaceWorkaround")) {
102
+ val devices = json.getMap("devicesThatRequireSurfaceWorkaround")
103
+ val deviceNames = devices?.getArray("deviceNames")
104
+ ?.toList<String>()
105
+ ?.mapNotNull { it }
106
+ ?.map { DeviceName(it) }
107
+ ?: emptyList()
108
+ val modelNames = devices?.getArray("modelNames")
109
+ ?.toList<String>()
110
+ ?.mapNotNull { it }
111
+ ?.map { ModelName(it) }
112
+ ?: emptyList()
113
+ tweaksConfig.devicesThatRequireSurfaceWorkaround = deviceNames + modelNames
114
+ }
115
+ if (json.hasKey("languagePropertyNormalization")) {
116
+ tweaksConfig.languagePropertyNormalization = json.getBoolean("languagePropertyNormalization")
117
+ }
118
+ if (json.hasKey("localDynamicDashWindowUpdateInterval")) {
119
+ tweaksConfig.localDynamicDashWindowUpdateInterval = json.getDouble("localDynamicDashWindowUpdateInterval")
120
+ }
121
+ if (json.hasKey("shouldApplyTtmlRegionWorkaround")) {
122
+ tweaksConfig.shouldApplyTtmlRegionWorkaround = json.getBoolean("shouldApplyTtmlRegionWorkaround")
123
+ }
124
+ if (json.hasKey("useDrmSessionForClearPeriods")) {
125
+ tweaksConfig.useDrmSessionForClearPeriods = json.getBoolean("useDrmSessionForClearPeriods")
126
+ }
127
+ if (json.hasKey("useDrmSessionForClearSources")) {
128
+ tweaksConfig.useDrmSessionForClearSources = json.getBoolean("useDrmSessionForClearSources")
129
+ }
130
+ if (json.hasKey("useFiletypeExtractorFallbackForHls")) {
131
+ tweaksConfig.useFiletypeExtractorFallbackForHls = json.getBoolean("useFiletypeExtractorFallbackForHls")
132
+ }
133
+ return tweaksConfig
134
+ }
135
+
136
+ /**
137
+ * Converts any JS object into an `AdvertisingConfig` object.
138
+ * @param json JS object representing the `AdvertisingConfig`.
139
+ * @return The generated `AdvertisingConfig` if successful, `null` otherwise.
140
+ */
141
+ @JvmStatic
142
+ fun toAdvertisingConfig(json: ReadableMap?): AdvertisingConfig? = json?.getArray("schedule")
143
+ ?.toList<ReadableMap>()
144
+ ?.mapNotNull(::toAdItem)
145
+ ?.let { AdvertisingConfig(it) }
146
+
147
+ /**
148
+ * Converts any JS object into an `AdItem` object.
149
+ * @param json JS object representing the `AdItem`.
150
+ * @return The generated `AdItem` if successful, `null` otherwise.
151
+ */
152
+ @JvmStatic
153
+ fun toAdItem(json: ReadableMap?): AdItem? {
154
+ val sources = json?.getArray("sources")
155
+ ?.toList<ReadableMap>()
156
+ ?.mapNotNull(::toAdSource)
157
+ ?.toTypedArray()
158
+ ?: return null
159
+ return AdItem(sources, json?.getString("position") ?: "pre")
160
+ }
161
+
162
+ /**
163
+ * Converts any JS object into an `AdSource` object.
164
+ * @param json JS object representing the `AdSource`.
165
+ * @return The generated `AdSource` if successful, `null` otherwise.
166
+ */
167
+ @JvmStatic
168
+ fun toAdSource(json: ReadableMap?): AdSource? = json?.getString("tag")?.let {
169
+ AdSource(toAdSourceType(json.getString("type")), it)
170
+ }
171
+
172
+ /**
173
+ * Converts any JS string into an `AdSourceType` enum value.
174
+ * @param json JS string representing the `AdSourceType`.
175
+ * @return The generated `AdSourceType`.
176
+ */
177
+ @JvmStatic
178
+ fun toAdSourceType(json: String?): AdSourceType = when (json) {
179
+ "ima" -> AdSourceType.Ima
180
+ "progressive" -> AdSourceType.Progressive
181
+ else -> AdSourceType.Unknown
182
+ }
183
+
50
184
  /**
51
185
  * Converts an arbitrary `json` to `SourceConfig`.
52
186
  * @param json JS object representing the `SourceConfig`.
@@ -193,6 +327,55 @@ class JsonConverter {
193
327
  json.putMap("from", fromSeekPosition(event.from))
194
328
  json.putMap("to", fromSeekPosition(event.to))
195
329
  }
330
+ if (event is PlayerEvent.PictureInPictureAvailabilityChanged) {
331
+ json.putBoolean("isPictureInPictureAvailable", event.isPictureInPictureAvailable)
332
+ }
333
+ if (event is PlayerEvent.AdBreakFinished) {
334
+ json.putMap("adBreak", fromAdBreak(event.adBreak))
335
+ }
336
+ if (event is PlayerEvent.AdBreakStarted) {
337
+ json.putMap("adBreak", fromAdBreak(event.adBreak))
338
+ }
339
+ if (event is PlayerEvent.AdClicked) {
340
+ json.putString("clickThroughUrl", event.clickThroughUrl)
341
+ }
342
+ if (event is PlayerEvent.AdError) {
343
+ json.putInt("code", event.code)
344
+ json.putString("message", event.message)
345
+ json.putMap("adConfig", fromAdConfig(event.adConfig))
346
+ json.putMap("adItem", fromAdItem(event.adItem))
347
+ }
348
+ if (event is PlayerEvent.AdFinished) {
349
+ json.putMap("ad", fromAd(event.ad))
350
+ }
351
+ if (event is PlayerEvent.AdManifestLoad) {
352
+ json.putMap("adBreak", fromAdBreak(event.adBreak))
353
+ json.putMap("adConfig", fromAdConfig(event.adConfig))
354
+ }
355
+ if (event is PlayerEvent.AdManifestLoaded) {
356
+ json.putMap("adBreak", fromAdBreak(event.adBreak))
357
+ json.putMap("adConfig", fromAdConfig(event.adConfig))
358
+ json.putDouble("downloadTime", event.downloadTime.toDouble())
359
+ }
360
+ if (event is PlayerEvent.AdQuartile) {
361
+ json.putString("quartile", fromAdQuartile(event.quartile))
362
+ }
363
+ if (event is PlayerEvent.AdScheduled) {
364
+ json.putInt("numberOfAds", event.numberOfAds)
365
+ }
366
+ if (event is PlayerEvent.AdSkipped) {
367
+ json.putMap("ad", fromAd(event.ad))
368
+ }
369
+ if (event is PlayerEvent.AdStarted) {
370
+ json.putMap("ad", fromAd(event.ad))
371
+ json.putString("clickThroughUrl", event.clickThroughUrl)
372
+ json.putString("clientType", fromAdSourceType(event.clientType))
373
+ json.putDouble("duration", event.duration)
374
+ json.putInt("indexInQueue", event.indexInQueue)
375
+ json.putString("position", event.position)
376
+ json.putDouble("skipOffset", event.skipOffset)
377
+ json.putDouble("timeOffset", event.timeOffset)
378
+ }
196
379
  return json
197
380
  }
198
381
 
@@ -301,5 +484,116 @@ class JsonConverter {
301
484
  }
302
485
  return mimeType.split("/").last()
303
486
  }
487
+
488
+ /**
489
+ * Converts any `AdBreak` object into its json representation.
490
+ * @param adBreak `AdBreak` object.
491
+ * @return The produced JS object.
492
+ */
493
+ @JvmStatic
494
+ fun fromAdBreak(adBreak: AdBreak?): WritableMap? = adBreak?.let {
495
+ Arguments.createMap().apply {
496
+ putArray("ads", it.ads.mapNotNull(::fromAd).toReadableArray())
497
+ putString("id", it.id)
498
+ putDouble("scheduleTime", it.scheduleTime)
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Converts any `Ad` object into its json representation.
504
+ * @param ad `Ad` object.
505
+ * @return The produced JS object.
506
+ */
507
+ @JvmStatic
508
+ fun fromAd(ad: Ad?): WritableMap? = ad?.let {
509
+ Arguments.createMap().apply {
510
+ putString("clickThroughUrl", it.clickThroughUrl)
511
+ putMap("data", fromAdData(it.data))
512
+ putInt("height", it.height)
513
+ putString("id", it.id)
514
+ putBoolean("isLinear", it.isLinear)
515
+ putString("mediaFileUrl", it.mediaFileUrl)
516
+ putInt("width", it.width)
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Converts any `AdData` object into its json representation.
522
+ * @param adData `AdData` object.
523
+ * @return The produced JS object.
524
+ */
525
+ @JvmStatic
526
+ fun fromAdData(adData: AdData?): WritableMap? = adData?.let {
527
+ Arguments.createMap().apply {
528
+ putInt("bitrate", it.bitrate)
529
+ putInt("maxBitrate", it.maxBitrate)
530
+ putString("mimeType", it.mimeType)
531
+ putInt("minBitrate", it.minBitrate)
532
+ }
533
+ }
534
+
535
+ /**
536
+ * Converts any `AdConfig` object into its json representation.
537
+ * @param adConfig `AdConfig` object.
538
+ * @return The produced JS object.
539
+ */
540
+ @JvmStatic
541
+ fun fromAdConfig(adConfig: AdConfig?): WritableMap? = adConfig?.let {
542
+ Arguments.createMap().apply {
543
+ putDouble("replaceContentDuration", it.replaceContentDuration)
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Converts any `AdItem` object into its json representation.
549
+ * @param adItem `AdItem` object.
550
+ * @return The produced JS object.
551
+ */
552
+ @JvmStatic
553
+ fun fromAdItem(adItem: AdItem?): WritableMap? = adItem?.let {
554
+ Arguments.createMap().apply {
555
+ putString("position", it.position)
556
+ putArray("sources", it.sources.mapNotNull(::fromAdSource).toReadableArray())
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Converts any `AdSource` object into its json representation.
562
+ * @param adSource `AdSource` object.
563
+ * @return The produced JS object.
564
+ */
565
+ @JvmStatic
566
+ fun fromAdSource(adSource: AdSource?): WritableMap? = adSource?.let {
567
+ Arguments.createMap().apply {
568
+ putString("tag", it.tag)
569
+ putString("type", fromAdSourceType(it.type))
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Converts any `AdSourceType` value into its json representation.
575
+ * @param adSourceType `AdSourceType` value.
576
+ * @return The produced JS string.
577
+ */
578
+ @JvmStatic
579
+ fun fromAdSourceType(adSourceType: AdSourceType?): String? = when (adSourceType) {
580
+ AdSourceType.Ima -> "ima"
581
+ AdSourceType.Unknown -> "unknown"
582
+ AdSourceType.Progressive -> "progressive"
583
+ else -> null
584
+ }
585
+
586
+ /**
587
+ * Converts any `AdQuartile` value into its json representation.
588
+ * @param adQuartile `AdQuartile` value.
589
+ * @return The produced JS string.
590
+ */
591
+ @JvmStatic
592
+ fun fromAdQuartile(adQuartile: AdQuartile?): String? = when (adQuartile) {
593
+ AdQuartile.FirstQuartile -> "first"
594
+ AdQuartile.MidPoint -> "mid_point"
595
+ AdQuartile.ThirdQuartile -> "third"
596
+ else -> null
597
+ }
304
598
  }
305
599
  }
@@ -0,0 +1,35 @@
1
+ package com.bitmovin.player.reactnative.extensions
2
+
3
+ import com.facebook.react.bridge.*
4
+
5
+ inline fun <reified T> ReadableArray.toList(): List<T?> = (0 until size()).map { i ->
6
+ getDynamic(i).let {
7
+ when (T::class) {
8
+ Boolean::class -> it.asBoolean() as T
9
+ String::class -> it.asString() as T
10
+ Double::class -> it.asDouble() as T
11
+ Int::class -> it.asInt() as T
12
+ ReadableArray::class -> it.asArray() as T
13
+ ReadableMap::class -> it.asMap() as T
14
+ WritableArray::class -> it.asArray() as T
15
+ WritableMap::class -> it.asMap() as T
16
+ else -> null
17
+ }
18
+ }
19
+ }
20
+
21
+ inline fun <reified T> List<T>.toReadableArray(): ReadableArray = Arguments.createArray().apply {
22
+ forEach {
23
+ when (T::class) {
24
+ Boolean::class -> pushBoolean(it as Boolean)
25
+ String::class -> pushString(it as String)
26
+ Double::class -> pushDouble(it as Double)
27
+ Int::class -> pushInt(it as Int)
28
+ ReadableArray::class -> pushArray(it as ReadableArray)
29
+ ReadableMap::class -> pushMap(it as ReadableMap)
30
+ WritableArray::class -> pushArray(it as ReadableArray)
31
+ WritableMap::class -> pushMap(it as ReadableMap)
32
+ else -> pushNull()
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,19 @@
1
+ package com.bitmovin.player.reactnative.extensions
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+
5
+ fun WritableMap.putInt(key: String, i: Int?) {
6
+ if (i == null) {
7
+ putNull(key)
8
+ } else {
9
+ putInt(key, i)
10
+ }
11
+ }
12
+
13
+ fun WritableMap.putDouble(key: String, d: Double?) {
14
+ if (d == null) {
15
+ putNull(key)
16
+ } else {
17
+ putDouble(key, d)
18
+ }
19
+ }
@@ -0,0 +1,191 @@
1
+ package com.bitmovin.player.reactnative.ui
2
+
3
+ import android.app.PictureInPictureParams
4
+ import android.content.pm.PackageManager
5
+ import android.content.res.Configuration
6
+ import android.graphics.Rect
7
+ import android.os.Build
8
+ import android.util.Rational
9
+ import androidx.annotation.RequiresApi
10
+ import androidx.appcompat.app.AppCompatActivity
11
+ import com.bitmovin.player.api.ui.PictureInPictureHandler
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+
14
+ /**
15
+ * Delegate object for `RNPictureInPictureHandler`. It delegates all view logic that needs
16
+ * to be performed during each PiP state to this object.
17
+ */
18
+ interface RNPictureInPictureDelegate {
19
+ /**
20
+ * Called whenever the handler's `isInPictureInPictureMode` changes to `true`.
21
+ */
22
+ fun onExitPictureInPicture()
23
+ /**
24
+ * Called whenever the handler's `isInPictureInPictureMode` changes to `false`.
25
+ */
26
+ fun onEnterPictureInPicture()
27
+ /**
28
+ * Called whenever the activity's PiP mode state changes with the new resources configuration.
29
+ */
30
+ fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?)
31
+ /**
32
+ * Called whenever the handler needs to compute a new `sourceRectHint` for PiP params.
33
+ * The passed rect reference is expected to be fulfilled with the PlayerView's global visible
34
+ * rect.
35
+ */
36
+ fun setSourceRectHint(sourceRectHint: Rect)
37
+ }
38
+
39
+ /**
40
+ * Custom PictureInPictureHandler` concrete implementation designed for React Native. It relies on
41
+ * React Native's application context to manage the application's PiP state. Can be subclassed in
42
+ * order to provide custom PiP capabilities.
43
+ */
44
+ open class RNPictureInPictureHandler(val context: ReactApplicationContext): PictureInPictureHandler {
45
+ /**
46
+ * PiP delegate object that contains the view logic to be performed on each PiP state change.
47
+ */
48
+ private var delegate: RNPictureInPictureDelegate? = null
49
+
50
+ /**
51
+ * Whether the user has enabled PiP support via `isPictureInPictureEnabled` playback configuration in JS.
52
+ */
53
+ var isPictureInPictureEnabled = false
54
+
55
+ /**
56
+ * Whether this view is currently in PiP mode.
57
+ */
58
+ var isInPictureInPictureMode = false
59
+
60
+ /**
61
+ * Whether the current Android version supports PiP mode.
62
+ */
63
+ private val isPictureInPictureSupported: Boolean
64
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
65
+ && context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
66
+
67
+ /**
68
+ * Whether the picture in picture feature is available and enabled.
69
+ */
70
+ override val isPictureInPictureAvailable: Boolean
71
+ get() = isPictureInPictureEnabled && isPictureInPictureSupported
72
+
73
+ /**
74
+ * Whether this view is currently in PiP mode. Required for PictureInPictureHandler interface.
75
+ */
76
+ override val isPictureInPicture: Boolean
77
+ get() = isInPictureInPictureMode
78
+
79
+ /**
80
+ * Current React activity computed property.
81
+ */
82
+ private val currentActivity: AppCompatActivity?
83
+ get() {
84
+ if (context.hasCurrentActivity()) {
85
+ return context.currentActivity as AppCompatActivity
86
+ }
87
+ return null
88
+ }
89
+
90
+ /**
91
+ * Sets the new delegate object and update the activity's PiP parameters accordingly.
92
+ */
93
+ open fun setDelegate(delegate: RNPictureInPictureDelegate?) {
94
+ this.delegate = delegate
95
+ // Update the activity's PiP params once the delegate has been set.
96
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPictureAvailable) {
97
+ applyPictureInPictureParams()
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Called whenever bitmovin's `PlayerView` needs to enter PiP mode.
103
+ */
104
+ override fun enterPictureInPicture() {
105
+ if (isPictureInPictureAvailable) {
106
+ currentActivity?.let {
107
+ it.supportActionBar?.hide()
108
+ it.enterPictureInPictureMode()
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Called whenever bitmovin's `PlayerView` needs to exit PiP mode.
115
+ */
116
+ override fun exitPictureInPicture() {
117
+ if (isPictureInPictureAvailable) {
118
+ currentActivity?.supportActionBar?.show()
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Called whenever the activity content resources have changed.
124
+ */
125
+ open fun onConfigurationChanged(newConfig: Configuration?) {
126
+ // PiP mode is supported since Android 7.0
127
+ if (isPictureInPictureAvailable) {
128
+ handlePictureInPictureModeChanges(newConfig)
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Checks whether the current activity `isInPictureInPictureMode` has changed since the last lifecycle
134
+ * configuration change.
135
+ */
136
+ @RequiresApi(Build.VERSION_CODES.N)
137
+ private fun handlePictureInPictureModeChanges(newConfig: Configuration?) = currentActivity?.let {
138
+ if (isInPictureInPictureMode != it.isInPictureInPictureMode) {
139
+ delegate?.onPictureInPictureModeChanged(it.isInPictureInPictureMode, newConfig)
140
+ if (it.isInPictureInPictureMode) {
141
+ delegate?.onEnterPictureInPicture()
142
+ } else {
143
+ delegate?.onExitPictureInPicture()
144
+ }
145
+ isInPictureInPictureMode = it.isInPictureInPictureMode
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Applies Android recommended PiP params on the current activity for smoother transitions.
151
+ *
152
+ * You can read more about the recommended settings for PiP here:
153
+ * - https://developer.android.com/develop/ui/views/picture-in-picture#smoother-transition
154
+ * - https://developer.android.com/develop/ui/views/picture-in-picture#smoother-exit
155
+ */
156
+ @RequiresApi(Build.VERSION_CODES.O)
157
+ private fun applyPictureInPictureParams() = currentActivity?.let {
158
+ // See also: https://developer.android.com/develop/ui/views/picture-in-picture#smoother-transition
159
+ val sourceRectHint = Rect()
160
+ delegate?.setSourceRectHint(sourceRectHint)
161
+ val ratio = Rational(16, 9)
162
+ val params = PictureInPictureParams.Builder()
163
+ .setAspectRatio(ratio)
164
+ .setSourceRectHint(sourceRectHint)
165
+ when {
166
+ // See also: https://developer.android.com/develop/ui/views/picture-in-picture#smoother-exit
167
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
168
+ params.setAutoEnterEnabled(true).setSeamlessResizeEnabled(true)
169
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ->
170
+ params.setExpandedAspectRatio(ratio)
171
+ }
172
+ it.setPictureInPictureParams(params.build())
173
+ }
174
+
175
+ /**
176
+ * Update source rect hint on activity's PiP params.
177
+ */
178
+ open fun updateSourceRectHint() {
179
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isPictureInPictureAvailable) {
180
+ return
181
+ }
182
+ currentActivity?.let {
183
+ val sourceRectHint = Rect()
184
+ delegate?.setSourceRectHint(sourceRectHint)
185
+ it.setPictureInPictureParams(
186
+ PictureInPictureParams.Builder()
187
+ .setSourceRectHint(sourceRectHint)
188
+ .build())
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,10 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_REMAP_MODULE(AudioSessionModule, AudioSessionModule, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(
6
+ setCategory:(NSString *)category
7
+ resolver:(RCTPromiseResolveBlock)resolve
8
+ rejecter:(RCTPromiseRejectBlock)reject)
9
+
10
+ @end
@@ -0,0 +1,65 @@
1
+ import AVFAudio
2
+
3
+ @objc(AudioSessionModule)
4
+ class AudioSessionModule: NSObject, RCTBridgeModule {
5
+ // Run this module methods on main thread.
6
+ var methodQueue: DispatchQueue! {
7
+ DispatchQueue.main
8
+ }
9
+
10
+ /// JS module name.
11
+ static func moduleName() -> String! {
12
+ "AudioSessionModule"
13
+ }
14
+
15
+ // Requires module initialization.
16
+ static func requiresMainQueueSetup() -> Bool {
17
+ true
18
+ }
19
+
20
+ /**
21
+ Sets the audio session’s category.
22
+ - Parameter category: Category string.
23
+ - Parameter resolver: JS promise resolver block.
24
+ - Parameter rejecter: JS promise rejecter block.
25
+ */
26
+ @objc func setCategory(
27
+ _ category: String,
28
+ resolver resolve: @escaping RCTPromiseResolveBlock,
29
+ rejecter reject: @escaping RCTPromiseRejectBlock
30
+ ) {
31
+ if let category = parseCategory(category) {
32
+ do {
33
+ try AVAudioSession.sharedInstance().setCategory(category)
34
+ resolve(nil)
35
+ } catch {
36
+ reject("\((error as NSError).code)", error.localizedDescription, error as NSError)
37
+ }
38
+ } else {
39
+ let error = RCTErrorWithMessage("Unknown audio session category: \(category)") as NSError
40
+ reject("\(error.code)", error.localizedDescription, error)
41
+ }
42
+ }
43
+
44
+ /**
45
+ Parse any category string to an `AVAudioSession.Category` type.
46
+ */
47
+ private func parseCategory(_ category: String) -> AVAudioSession.Category? {
48
+ switch (category) {
49
+ case "ambient":
50
+ return .ambient
51
+ case "multiRoute":
52
+ return .multiRoute
53
+ case "playAndRecord":
54
+ return .playAndRecord
55
+ case "playback":
56
+ return .playback
57
+ case "record":
58
+ return .record
59
+ case "soloAmbient":
60
+ return .soloAmbient
61
+ default:
62
+ return nil
63
+ }
64
+ }
65
+ }