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.
- package/README.md +238 -24
- package/RNBitmovinPlayer.podspec +3 -1
- package/android/build.gradle +7 -5
- package/android/src/main/java/com/bitmovin/player/reactnative/DrmModule.kt +4 -5
- package/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +41 -5
- package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerView.kt +273 -2
- package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewManager.kt +45 -5
- package/android/src/main/java/com/bitmovin/player/reactnative/SourceModule.kt +4 -5
- package/android/src/main/java/com/bitmovin/player/reactnative/UuidModule.kt +3 -1
- package/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +301 -7
- package/android/src/main/java/com/bitmovin/player/reactnative/extensions/ReadableArray.kt +35 -0
- package/android/src/main/java/com/bitmovin/player/reactnative/extensions/WritableMap.kt +19 -0
- package/android/src/main/java/com/bitmovin/player/reactnative/ui/RNPictureInPictureHandler.kt +191 -0
- package/ios/AudioSessionModule.m +10 -0
- package/ios/AudioSessionModule.swift +65 -0
- package/ios/Event+JSON.swift +123 -0
- package/ios/PlayerModule.m +6 -0
- package/ios/PlayerModule.swift +44 -0
- package/ios/RCTConvert+BitmovinPlayer.swift +285 -15
- package/ios/RNPlayerView+PlayerListener.swift +52 -0
- package/ios/RNPlayerView+UserInterfaceListener.swift +19 -0
- package/ios/RNPlayerView.swift +17 -0
- package/ios/RNPlayerViewManager.m +18 -1
- package/ios/RNPlayerViewManager.swift +2 -1
- package/lib/index.d.ts +577 -3
- package/lib/index.js +92 -33
- package/lib/index.mjs +75 -19
- package/package.json +1 -1
- package/src/advertising.ts +155 -0
- package/src/audioSession.ts +47 -0
- package/src/components/PlayerView/events.ts +39 -3
- package/src/components/PlayerView/index.tsx +31 -11
- package/src/events.ts +212 -0
- package/src/index.ts +2 -0
- package/src/player.ts +41 -1
- 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
|
-
|
|
37
|
-
|
|
38
|
-
playerConfig.playbackConfig.isAutoplayEnabled = playbackConfigJson.getBoolean("isAutoplayEnabled")
|
|
44
|
+
toPlaybackConfig(json.getMap("playbackConfig"))?.let {
|
|
45
|
+
playerConfig.playbackConfig = it
|
|
39
46
|
}
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
}
|
|
48
|
+
if (json.hasKey("tweaksConfig")) {
|
|
49
|
+
toTweaksConfig(json.getMap("tweaksConfig"))?.let {
|
|
50
|
+
playerConfig.tweaksConfig = it
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|