bitmovin-player-react-native 0.9.2 → 0.10.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/RNBitmovinPlayer.podspec +1 -1
- package/android/build.gradle +1 -2
- package/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt +43 -11
- package/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +279 -0
- package/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +37 -1
- package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt +1 -0
- package/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +123 -35
- package/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt +216 -0
- package/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineDownloadRequest.kt +7 -0
- package/ios/DrmModule.swift +5 -5
- package/ios/OfflineContentManagerBridge.swift +141 -0
- package/ios/OfflineModule.m +19 -0
- package/ios/OfflineModule.swift +465 -0
- package/ios/PlayerModule.m +2 -0
- package/ios/PlayerModule.swift +40 -7
- package/ios/RCTConvert+BitmovinPlayer.swift +135 -1
- package/ios/RNBitmovinPlayer.h +1 -0
- package/lib/index.d.mts +382 -2
- package/lib/index.d.ts +382 -2
- package/lib/index.js +233 -0
- package/lib/index.mjs +231 -0
- package/package.json +1 -1
- package/src/adaptationConfig.ts +10 -0
- package/src/index.ts +2 -0
- package/src/offline/index.ts +7 -0
- package/src/offline/offlineContentConfig.ts +18 -0
- package/src/offline/offlineContentManager.ts +216 -0
- package/src/offline/offlineContentManagerListener.ts +187 -0
- package/src/offline/offlineContentOptions.ts +29 -0
- package/src/offline/offlineDownloadRequest.ts +20 -0
- package/src/offline/offlineSourceOptions.ts +11 -0
- package/src/offline/offlineState.ts +22 -0
- package/src/player.ts +30 -0
- package/src/source.ts +38 -0
|
@@ -1,36 +1,52 @@
|
|
|
1
1
|
package com.bitmovin.player.reactnative.converter
|
|
2
2
|
|
|
3
3
|
import com.bitmovin.analytics.BitmovinAnalyticsConfig
|
|
4
|
-
import com.bitmovin.analytics.
|
|
5
|
-
import com.bitmovin.analytics.
|
|
4
|
+
import com.bitmovin.analytics.api.CustomData
|
|
5
|
+
import com.bitmovin.analytics.api.SourceMetadata
|
|
6
6
|
import com.bitmovin.player.api.DeviceDescription.DeviceName
|
|
7
7
|
import com.bitmovin.player.api.DeviceDescription.ModelName
|
|
8
8
|
import com.bitmovin.player.api.PlaybackConfig
|
|
9
9
|
import com.bitmovin.player.api.PlayerConfig
|
|
10
10
|
import com.bitmovin.player.api.TweaksConfig
|
|
11
|
-
import com.bitmovin.player.api.advertising
|
|
11
|
+
import com.bitmovin.player.api.advertising.Ad
|
|
12
|
+
import com.bitmovin.player.api.advertising.AdBreak
|
|
13
|
+
import com.bitmovin.player.api.advertising.AdConfig
|
|
14
|
+
import com.bitmovin.player.api.advertising.AdData
|
|
15
|
+
import com.bitmovin.player.api.advertising.AdItem
|
|
16
|
+
import com.bitmovin.player.api.advertising.AdQuartile
|
|
17
|
+
import com.bitmovin.player.api.advertising.AdSource
|
|
18
|
+
import com.bitmovin.player.api.advertising.AdSourceType
|
|
19
|
+
import com.bitmovin.player.api.advertising.AdvertisingConfig
|
|
12
20
|
import com.bitmovin.player.api.drm.WidevineConfig
|
|
13
21
|
import com.bitmovin.player.api.event.PlayerEvent
|
|
14
22
|
import com.bitmovin.player.api.event.SourceEvent
|
|
15
23
|
import com.bitmovin.player.api.event.data.SeekPosition
|
|
24
|
+
import com.bitmovin.player.api.media.AdaptationConfig
|
|
16
25
|
import com.bitmovin.player.api.media.audio.AudioTrack
|
|
17
26
|
import com.bitmovin.player.api.media.subtitle.SubtitleTrack
|
|
18
27
|
import com.bitmovin.player.api.media.thumbnail.ThumbnailTrack
|
|
19
28
|
import com.bitmovin.player.api.media.video.quality.VideoQuality
|
|
29
|
+
import com.bitmovin.player.api.offline.options.OfflineContentOptions
|
|
30
|
+
import com.bitmovin.player.api.offline.options.OfflineOptionEntry
|
|
20
31
|
import com.bitmovin.player.api.source.Source
|
|
21
32
|
import com.bitmovin.player.api.source.SourceConfig
|
|
33
|
+
import com.bitmovin.player.api.source.SourceOptions
|
|
22
34
|
import com.bitmovin.player.api.source.SourceType
|
|
35
|
+
import com.bitmovin.player.api.source.TimelineReferencePoint
|
|
23
36
|
import com.bitmovin.player.api.ui.ScalingMode
|
|
24
37
|
import com.bitmovin.player.api.ui.StyleConfig
|
|
25
38
|
import com.bitmovin.player.reactnative.extensions.getName
|
|
26
|
-
import com.bitmovin.player.reactnative.extensions.
|
|
39
|
+
import com.bitmovin.player.reactnative.extensions.getProperty
|
|
27
40
|
import com.bitmovin.player.reactnative.extensions.putDouble
|
|
41
|
+
import com.bitmovin.player.reactnative.extensions.putInt
|
|
42
|
+
import com.bitmovin.player.reactnative.extensions.setProperty
|
|
28
43
|
import com.bitmovin.player.reactnative.extensions.toList
|
|
29
44
|
import com.bitmovin.player.reactnative.extensions.toReadableArray
|
|
30
|
-
import com.bitmovin.player.reactnative.extensions.getProperty
|
|
31
|
-
import com.bitmovin.player.reactnative.extensions.setProperty
|
|
32
|
-
import com.facebook.react.bridge.*
|
|
33
45
|
import com.bitmovin.player.reactnative.extensions.toReadableMap
|
|
46
|
+
import com.facebook.react.bridge.Arguments
|
|
47
|
+
import com.facebook.react.bridge.ReadableArray
|
|
48
|
+
import com.facebook.react.bridge.ReadableMap
|
|
49
|
+
import com.facebook.react.bridge.WritableMap
|
|
34
50
|
import java.util.UUID
|
|
35
51
|
|
|
36
52
|
/**
|
|
@@ -71,9 +87,53 @@ class JsonConverter {
|
|
|
71
87
|
playerConfig.advertisingConfig = it
|
|
72
88
|
}
|
|
73
89
|
}
|
|
90
|
+
if (json.hasKey("adaptationConfig")) {
|
|
91
|
+
toAdaptationConfig(json.getMap("adaptationConfig"))?.let {
|
|
92
|
+
playerConfig.adaptationConfig = it
|
|
93
|
+
}
|
|
94
|
+
}
|
|
74
95
|
return playerConfig
|
|
75
96
|
}
|
|
76
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Converts an arbitrary `json` to `SourceOptions`.
|
|
100
|
+
* @param json JS object representing the `SourceOptions`.
|
|
101
|
+
* @return The generated `SourceOptions`.
|
|
102
|
+
*/
|
|
103
|
+
@JvmStatic
|
|
104
|
+
fun toSourceOptions(json: ReadableMap?): SourceOptions {
|
|
105
|
+
if (json == null) return SourceOptions()
|
|
106
|
+
val startOffset = if(json.hasKey("startOffset")) json.getDouble("startOffset") else null
|
|
107
|
+
val timelineReferencePoint = toTimelineReferencePoint(json.getString("startOffsetTimelineReference"))
|
|
108
|
+
return SourceOptions(startOffset = startOffset, startOffsetTimelineReference = timelineReferencePoint)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Converts an arbitrary `json` to `TimelineReferencePoint`.
|
|
113
|
+
* @param json JS string representing the `TimelineReferencePoint`.
|
|
114
|
+
* @return The generated `TimelineReferencePoint`.
|
|
115
|
+
*/
|
|
116
|
+
@JvmStatic
|
|
117
|
+
private fun toTimelineReferencePoint(json: String?): TimelineReferencePoint? = when (json) {
|
|
118
|
+
"start" -> TimelineReferencePoint.Start
|
|
119
|
+
"end" -> TimelineReferencePoint.End
|
|
120
|
+
else -> null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Converts an arbitrary `json` to `AdaptationConfig`.
|
|
125
|
+
* @param json JS object representing the `AdaptationConfig`.
|
|
126
|
+
* @return The generated `AdaptationConfig` if successful, `null` otherwise.
|
|
127
|
+
*/
|
|
128
|
+
private fun toAdaptationConfig(json: ReadableMap?): AdaptationConfig? {
|
|
129
|
+
if (json == null) return null
|
|
130
|
+
val adaptationConfig = AdaptationConfig()
|
|
131
|
+
if (json.hasKey("maxSelectableBitrate")) {
|
|
132
|
+
adaptationConfig.maxSelectableVideoBitrate = json.getInt("maxSelectableBitrate")
|
|
133
|
+
}
|
|
134
|
+
return adaptationConfig
|
|
135
|
+
}
|
|
136
|
+
|
|
77
137
|
/**
|
|
78
138
|
* Converts any JS object into a `PlaybackConfig` object.
|
|
79
139
|
* @param json JS object representing the `PlaybackConfig`.
|
|
@@ -213,7 +273,7 @@ class JsonConverter {
|
|
|
213
273
|
?.mapNotNull(::toAdSource)
|
|
214
274
|
?.toTypedArray()
|
|
215
275
|
?: return null
|
|
216
|
-
return AdItem(sources, json
|
|
276
|
+
return AdItem(sources, json.getString("position") ?: "pre")
|
|
217
277
|
}
|
|
218
278
|
|
|
219
279
|
/**
|
|
@@ -272,6 +332,9 @@ class JsonConverter {
|
|
|
272
332
|
?.toHashMap()
|
|
273
333
|
?.mapValues { entry -> entry.value as String }
|
|
274
334
|
}
|
|
335
|
+
if (json.hasKey("options")) {
|
|
336
|
+
config.options = toSourceOptions(json.getMap("options"))
|
|
337
|
+
}
|
|
275
338
|
return config
|
|
276
339
|
}
|
|
277
340
|
|
|
@@ -545,7 +608,7 @@ class JsonConverter {
|
|
|
545
608
|
false
|
|
546
609
|
}
|
|
547
610
|
val format = json.getString("format")
|
|
548
|
-
if (format
|
|
611
|
+
if (!format.isNullOrBlank()) {
|
|
549
612
|
return SubtitleTrack(
|
|
550
613
|
url = url,
|
|
551
614
|
label = label,
|
|
@@ -778,17 +841,20 @@ class JsonConverter {
|
|
|
778
841
|
* @return The produced `CustomData` or null.
|
|
779
842
|
*/
|
|
780
843
|
@JvmStatic
|
|
781
|
-
fun toAnalyticsCustomData(json: ReadableMap?): CustomData?
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
844
|
+
fun toAnalyticsCustomData(json: ReadableMap?): CustomData? {
|
|
845
|
+
if (json == null) return null
|
|
846
|
+
|
|
847
|
+
return CustomData.Builder().apply {
|
|
848
|
+
for (n in 1..30) {
|
|
849
|
+
setProperty(
|
|
850
|
+
"customData${n}",
|
|
851
|
+
json.getString("customData${n}") ?: continue
|
|
852
|
+
)
|
|
786
853
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
customData
|
|
854
|
+
json.getString("experimentName")?.let {
|
|
855
|
+
setExperimentName(it)
|
|
856
|
+
}
|
|
857
|
+
}.build()
|
|
792
858
|
}
|
|
793
859
|
|
|
794
860
|
/**
|
|
@@ -812,23 +878,15 @@ class JsonConverter {
|
|
|
812
878
|
|
|
813
879
|
@JvmStatic
|
|
814
880
|
fun toAnalyticsSourceMetadata(json: ReadableMap?): SourceMetadata? = json?.let {
|
|
815
|
-
val
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
881
|
+
val sourceCustomData = toAnalyticsCustomData(json) ?: CustomData()
|
|
882
|
+
SourceMetadata(
|
|
883
|
+
title = it.getString("title"),
|
|
884
|
+
videoId = it.getString("videoId"),
|
|
885
|
+
cdnProvider = it.getString("cdnProvider"),
|
|
886
|
+
path = it.getString("path"),
|
|
887
|
+
isLive = it.getBoolean("isLive"),
|
|
888
|
+
customData = sourceCustomData
|
|
821
889
|
)
|
|
822
|
-
|
|
823
|
-
for (n in 1..30) {
|
|
824
|
-
it.getString("customData${n}")?.let { customDataN ->
|
|
825
|
-
sourceMetadata.setProperty("customData${n}", customDataN)
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
it.getString("experimentName")?.let { experimentName ->
|
|
829
|
-
sourceMetadata.experimentName = experimentName
|
|
830
|
-
}
|
|
831
|
-
sourceMetadata
|
|
832
890
|
}
|
|
833
891
|
|
|
834
892
|
/**
|
|
@@ -848,5 +906,35 @@ class JsonConverter {
|
|
|
848
906
|
putInt("width", videoQuality.width)
|
|
849
907
|
}
|
|
850
908
|
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Converts any `OfflineOptionEntry` into its json representation.
|
|
912
|
+
* @param offlineEntry `OfflineOptionEntry` object to be converted.
|
|
913
|
+
* @return The generated json map.
|
|
914
|
+
*/
|
|
915
|
+
@JvmStatic
|
|
916
|
+
fun toJson(offlineEntry: OfflineOptionEntry): WritableMap {
|
|
917
|
+
return Arguments.createMap().apply {
|
|
918
|
+
putString("id", offlineEntry.id)
|
|
919
|
+
putString("language", offlineEntry.language)
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Converts any `OfflineContentOptions` into its json representation.
|
|
925
|
+
* @param options `OfflineContentOptions` object to be converted.
|
|
926
|
+
* @return The generated json map.
|
|
927
|
+
*/
|
|
928
|
+
@JvmStatic
|
|
929
|
+
fun toJson(options: OfflineContentOptions?): WritableMap? {
|
|
930
|
+
if (options == null) {
|
|
931
|
+
return null
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return Arguments.createMap().apply {
|
|
935
|
+
putArray("audioOptions", options.audioOptions.map { toJson(it) }.toReadableArray())
|
|
936
|
+
putArray("textOptions", options.textOptions.map { toJson(it) }.toReadableArray())
|
|
937
|
+
}
|
|
938
|
+
}
|
|
851
939
|
}
|
|
852
940
|
}
|
package/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
package com.bitmovin.player.reactnative.offline
|
|
2
|
+
|
|
3
|
+
import com.bitmovin.player.api.deficiency.ErrorEvent
|
|
4
|
+
import com.bitmovin.player.api.offline.OfflineContentManager
|
|
5
|
+
import com.bitmovin.player.api.offline.OfflineContentManagerListener
|
|
6
|
+
import com.bitmovin.player.api.offline.options.*
|
|
7
|
+
import com.bitmovin.player.api.source.SourceConfig
|
|
8
|
+
import com.bitmovin.player.reactnative.NativeId
|
|
9
|
+
import com.bitmovin.player.reactnative.converter.JsonConverter
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import com.facebook.react.bridge.WritableMap
|
|
13
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
14
|
+
|
|
15
|
+
class OfflineContentManagerBridge(
|
|
16
|
+
private val nativeId: NativeId,
|
|
17
|
+
private val context: ReactApplicationContext,
|
|
18
|
+
private val identifier: String,
|
|
19
|
+
source: SourceConfig,
|
|
20
|
+
location: String
|
|
21
|
+
) : OfflineContentManagerListener {
|
|
22
|
+
|
|
23
|
+
enum class OfflineEventType(val eventName: String) {
|
|
24
|
+
ON_COMPLETED("onCompleted"),
|
|
25
|
+
ON_ERROR("onError"),
|
|
26
|
+
ON_PROGRESS("onProgress"),
|
|
27
|
+
ON_OPTIONS_AVAILABLE("onOptionsAvailable"),
|
|
28
|
+
ON_DRM_LICENSE_UPDATED("onDrmLicenseUpdated"),
|
|
29
|
+
ON_SUSPENDED("onSuspended"),
|
|
30
|
+
ON_RESUMED("onResumed"),
|
|
31
|
+
ON_CANCELED("onCanceled")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
val offlineContentManager: OfflineContentManager = OfflineContentManager.getOfflineContentManager(
|
|
35
|
+
source, location, identifier, this, context
|
|
36
|
+
)
|
|
37
|
+
private var contentOptions: OfflineContentOptions? = null
|
|
38
|
+
|
|
39
|
+
val state: OfflineOptionEntryState
|
|
40
|
+
get() = aggregateState(contentOptions)
|
|
41
|
+
|
|
42
|
+
fun getOptions() {
|
|
43
|
+
offlineContentManager.getOptions()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Process the `OfflineDownloadRequest`.
|
|
48
|
+
* The `OfflineContentOptions` are stored in memory in this class because they can not be constructed to pass to the process call.
|
|
49
|
+
* We're copying the iOS interface for the `minimumBitrate` so that react-native will have a consistent interface.
|
|
50
|
+
* When no `minimumBitrate` is provided, the highest bitrate will be downloaded.
|
|
51
|
+
*/
|
|
52
|
+
fun process(request: OfflineDownloadRequest) {
|
|
53
|
+
if (contentOptions != null) {
|
|
54
|
+
val sortedVideoOptions = contentOptions!!.videoOptions
|
|
55
|
+
.sortedBy { option -> option.bitrate }
|
|
56
|
+
if (request.minimumBitrate == null) {
|
|
57
|
+
sortedVideoOptions.lastOrNull()
|
|
58
|
+
} else {
|
|
59
|
+
sortedVideoOptions
|
|
60
|
+
.firstOrNull { option -> option.bitrate >= request.minimumBitrate }
|
|
61
|
+
}?.apply {
|
|
62
|
+
action = OfflineOptionEntryAction.Download
|
|
63
|
+
}
|
|
64
|
+
appliesDownloadActions(request.audioOptionIds, contentOptions!!.audioOptions)
|
|
65
|
+
appliesDownloadActions(request.textOptionIds, contentOptions!!.textOptions)
|
|
66
|
+
|
|
67
|
+
offlineContentManager.process(contentOptions!!)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private fun appliesDownloadActions(
|
|
72
|
+
ids: List<String?>?,
|
|
73
|
+
potentialOptions: List<OfflineOptionEntry>
|
|
74
|
+
) {
|
|
75
|
+
if (ids.isNullOrEmpty()) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ids.forEach { idToDownload ->
|
|
80
|
+
potentialOptions.forEach { option ->
|
|
81
|
+
if (idToDownload == option.id && option.action !== OfflineOptionEntryAction.Download) {
|
|
82
|
+
option.action = OfflineOptionEntryAction.Download
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fun resume() {
|
|
89
|
+
offlineContentManager.resume()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun suspend() {
|
|
93
|
+
offlineContentManager.suspend()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fun cancelDownload() {
|
|
97
|
+
suspend()
|
|
98
|
+
deleteAll()
|
|
99
|
+
sendEvent(OfflineEventType.ON_CANCELED)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun deleteAll() {
|
|
103
|
+
offlineContentManager.deleteAll()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fun downloadLicense() {
|
|
107
|
+
offlineContentManager.downloadLicense()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fun releaseLicense() {
|
|
111
|
+
offlineContentManager.releaseLicense()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun renewOfflineLicense() {
|
|
115
|
+
offlineContentManager.renewOfflineLicense()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fun release() {
|
|
119
|
+
offlineContentManager.release()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Produces an aggregate state of the `OfflineOptionEntryState` for the `OfflineContentOptions`
|
|
124
|
+
* iOS does not have granular data access to the states of each entry, but instead provides a single aggregate status.
|
|
125
|
+
* Adding this produces a consistent interface in the react-native layer.
|
|
126
|
+
*/
|
|
127
|
+
private fun aggregateState(options: OfflineContentOptions?): OfflineOptionEntryState {
|
|
128
|
+
val allOptions = mutableListOf<OfflineOptionEntry>()
|
|
129
|
+
options?.videoOptions?.let { allOptions.addAll(it) }
|
|
130
|
+
options?.audioOptions?.let { allOptions.addAll(it) }
|
|
131
|
+
options?.textOptions?.let { allOptions.addAll(it) }
|
|
132
|
+
val trackableStates =
|
|
133
|
+
listOf(OfflineOptionEntryState.Suspended, OfflineOptionEntryState.Downloading)
|
|
134
|
+
|
|
135
|
+
var state = allOptions.firstOrNull { trackableStates.contains(it.state) }?.state
|
|
136
|
+
|
|
137
|
+
if (state == null && allOptions.any { it.state == OfflineOptionEntryState.Downloaded }) {
|
|
138
|
+
state = OfflineOptionEntryState.Downloaded
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
return state ?: OfflineOptionEntryState.NotDownloaded
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Called when a process call has completed.
|
|
147
|
+
*/
|
|
148
|
+
override fun onCompleted(source: SourceConfig?, options: OfflineContentOptions?) {
|
|
149
|
+
this.contentOptions = options
|
|
150
|
+
sendEvent(OfflineEventType.ON_COMPLETED, Arguments.createMap().apply {
|
|
151
|
+
putMap("options", JsonConverter.toJson(options))
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Called when an error occurs.
|
|
157
|
+
*/
|
|
158
|
+
override fun onError(source: SourceConfig?, event: ErrorEvent?) {
|
|
159
|
+
sendEvent(OfflineEventType.ON_ERROR, Arguments.createMap().apply {
|
|
160
|
+
event?.code?.value?.let { putInt("code", it) }
|
|
161
|
+
putString("message", event?.message)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Called when the progress for a process call changes.
|
|
167
|
+
*/
|
|
168
|
+
override fun onProgress(source: SourceConfig?, progress: Float) {
|
|
169
|
+
sendEvent(OfflineEventType.ON_PROGRESS, Arguments.createMap().apply {
|
|
170
|
+
putDouble("progress", progress.toDouble())
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Called after a getOptions or when am OfflineOptionEntry has been updated during a process call.
|
|
176
|
+
*/
|
|
177
|
+
override fun onOptionsAvailable(source: SourceConfig?, options: OfflineContentOptions?) {
|
|
178
|
+
this.contentOptions = options
|
|
179
|
+
sendEvent(OfflineEventType.ON_OPTIONS_AVAILABLE, Arguments.createMap().apply {
|
|
180
|
+
putMap("options", JsonConverter.toJson(options))
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Called when the DRM license was updated.
|
|
186
|
+
*/
|
|
187
|
+
override fun onDrmLicenseUpdated(source: SourceConfig?) {
|
|
188
|
+
sendEvent(OfflineEventType.ON_DRM_LICENSE_UPDATED)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Called when all actions have been suspended.
|
|
193
|
+
*/
|
|
194
|
+
override fun onSuspended(source: SourceConfig?) {
|
|
195
|
+
sendEvent(OfflineEventType.ON_SUSPENDED)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Called when all actions have been resumed.
|
|
200
|
+
*/
|
|
201
|
+
override fun onResumed(source: SourceConfig?) {
|
|
202
|
+
sendEvent(OfflineEventType.ON_RESUMED)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private fun sendEvent(eventType: OfflineEventType, event: WritableMap? = null) {
|
|
206
|
+
val e = event ?: Arguments.createMap()
|
|
207
|
+
e.putString("nativeId", nativeId)
|
|
208
|
+
e.putString("identifier", identifier)
|
|
209
|
+
e.putString("eventType", eventType.eventName)
|
|
210
|
+
e.putString("state", aggregateState(contentOptions).name)
|
|
211
|
+
|
|
212
|
+
context
|
|
213
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
214
|
+
.emit("BitmovinOfflineEvent", e)
|
|
215
|
+
}
|
|
216
|
+
}
|
package/ios/DrmModule.swift
CHANGED
|
@@ -104,7 +104,7 @@ class DrmModule: NSObject, RCTBridgeModule {
|
|
|
104
104
|
Note this function is **synchronous** and **blocks** the JS thread. It's important that it stays this way, otherwise we wouldn't be able to return
|
|
105
105
|
the computed JS message from inside the `fairplayConfig.prepareMessage` Swift closure.
|
|
106
106
|
|
|
107
|
-
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's
|
|
107
|
+
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's necessary to add a return type and a return
|
|
108
108
|
value (even if it's a void method like in this case) or a crash happens. So the type `Any?` and return value `nil` were used here (it could be any value).
|
|
109
109
|
*/
|
|
110
110
|
@objc(setPreparedMessage:message:)
|
|
@@ -119,7 +119,7 @@ class DrmModule: NSObject, RCTBridgeModule {
|
|
|
119
119
|
Note this function is **synchronous** and **blocks** the JS thread. It's important that it stays this way, otherwise we wouldn't be able to return
|
|
120
120
|
the computed JS message from inside the `fairplayConfig.prepareSyncMessage` Swift closure.
|
|
121
121
|
|
|
122
|
-
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's
|
|
122
|
+
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's necessary to add a return type and a return
|
|
123
123
|
value (even if it's a void method like in this case) or a crash happens. So the type `Any?` and return value `nil` were used here (it could be any value).
|
|
124
124
|
*/
|
|
125
125
|
@objc(setPreparedSyncMessage:syncMessage:)
|
|
@@ -134,7 +134,7 @@ class DrmModule: NSObject, RCTBridgeModule {
|
|
|
134
134
|
Note this function is **synchronous** and **blocks** the JS thread. It's important that it stays this way, otherwise we wouldn't be able to return
|
|
135
135
|
the computed JS message from inside the `fairplayConfig.prepareLicense` Swift closure.
|
|
136
136
|
|
|
137
|
-
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's
|
|
137
|
+
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's necessary to add a return type and a return value
|
|
138
138
|
(even if it's a void method like in this case) or a crash happens. So the type `Any?` and return value `nil` were used here (it could be any value).
|
|
139
139
|
*/
|
|
140
140
|
@objc(setPreparedLicense:license:)
|
|
@@ -149,7 +149,7 @@ class DrmModule: NSObject, RCTBridgeModule {
|
|
|
149
149
|
Note this function is **synchronous** and **blocks** the JS thread. It's important that it stays this way, otherwise we wouldn't be able to return
|
|
150
150
|
the computed JS message from inside the `fairplayConfig.prepareLicenseServerUrl` Swift closure.
|
|
151
151
|
|
|
152
|
-
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's
|
|
152
|
+
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's necessary to add a return type and a return value
|
|
153
153
|
(even if it's a void method like in this case) or a crash happens. So the type `Any?` and return value `nil` were used here (it could be any value).
|
|
154
154
|
*/
|
|
155
155
|
@objc(setPreparedLicenseServerUrl:url:)
|
|
@@ -164,7 +164,7 @@ class DrmModule: NSObject, RCTBridgeModule {
|
|
|
164
164
|
Note this function is **synchronous** and **blocks** the JS thread. It's important that it stays this way, otherwise we wouldn't be able to return
|
|
165
165
|
the computed JS message from inside the `fairplayConfig.prepareContentId` Swift closure.
|
|
166
166
|
|
|
167
|
-
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's
|
|
167
|
+
Also, since RN has some limitations regarding the definition of sync JS methods from Swift, it's necessary to add a return type and a return value
|
|
168
168
|
(even if it's a void method like in this case) or a crash happens. So the type `Any?` and return value `nil` were used here (it could be any value).
|
|
169
169
|
*/
|
|
170
170
|
@objc(setPreparedContentId:contentId:)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#if os(iOS)
|
|
2
|
+
import Foundation
|
|
3
|
+
import BitmovinPlayer
|
|
4
|
+
|
|
5
|
+
class OfflineContentManagerBridge: NSObject, OfflineContentManagerListener {
|
|
6
|
+
enum EventType: String {
|
|
7
|
+
case onCompleted = "onCompleted"
|
|
8
|
+
case onError = "onError"
|
|
9
|
+
case onProgress = "onProgress"
|
|
10
|
+
case onOptionsAvailable = "onOptionsAvailable"
|
|
11
|
+
case onDrmLicenseUpdated = "onDrmLicenseUpdated"
|
|
12
|
+
case onSuspended = "onSuspended"
|
|
13
|
+
case onResumed = "onResumed"
|
|
14
|
+
case onCanceled = "onCanceled"
|
|
15
|
+
case onDrmLicenseExpired = "onDrmLicenseExpired"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let offlineContentManager: OfflineContentManager
|
|
19
|
+
let eventEmitter: RCTEventEmitter?
|
|
20
|
+
let nativeId: NativeId
|
|
21
|
+
let identifier: String
|
|
22
|
+
var currentTrackSelection: OfflineTrackSelection? = nil
|
|
23
|
+
|
|
24
|
+
init(
|
|
25
|
+
forManager offlineContentManager: OfflineContentManager,
|
|
26
|
+
eventEmitter: RCTEventEmitter,
|
|
27
|
+
nativeId: NativeId,
|
|
28
|
+
identifier: String
|
|
29
|
+
) {
|
|
30
|
+
self.offlineContentManager = offlineContentManager
|
|
31
|
+
self.eventEmitter = eventEmitter
|
|
32
|
+
self.nativeId = nativeId
|
|
33
|
+
self.identifier = identifier
|
|
34
|
+
super.init()
|
|
35
|
+
|
|
36
|
+
offlineContentManager.add(listener: self)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func release() {
|
|
40
|
+
offlineContentManager.remove(listener: self)
|
|
41
|
+
currentTrackSelection = nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func fetchAvailableTracks() {
|
|
45
|
+
offlineContentManager.fetchAvailableTracks()
|
|
46
|
+
|
|
47
|
+
sendOfflineEvent(eventType: .onOptionsAvailable)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
Called when an error occurs.
|
|
52
|
+
*/
|
|
53
|
+
func onOfflineError(_ event: OfflineErrorEvent, offlineContentManager: OfflineContentManager) {
|
|
54
|
+
sendOfflineEvent(eventType: .onError, body: [
|
|
55
|
+
"code": event.code,
|
|
56
|
+
"message": event.message
|
|
57
|
+
])
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
Called after a getOptions or when am OfflineOptionEntry has been updated during a process call.
|
|
62
|
+
*/
|
|
63
|
+
func onAvailableTracksFetched(_ event: AvailableTracksFetchedEvent, offlineContentManager: OfflineContentManager) {
|
|
64
|
+
currentTrackSelection = event.tracks
|
|
65
|
+
|
|
66
|
+
sendOfflineEvent(eventType: .onOptionsAvailable, body: [
|
|
67
|
+
"options": RCTConvert.toJson(offlineTracks: event.tracks)
|
|
68
|
+
])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
Called when a process call has completed.
|
|
73
|
+
*/
|
|
74
|
+
func onContentDownloadFinished(_ event: ContentDownloadFinishedEvent, offlineContentManager: OfflineContentManager) {
|
|
75
|
+
sendOfflineEvent(eventType: .onCompleted, body: [
|
|
76
|
+
"options": RCTConvert.toJson(offlineTracks: currentTrackSelection)
|
|
77
|
+
])
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
Called when the progress for a process call changes.
|
|
82
|
+
*/
|
|
83
|
+
func onContentDownloadProgressChanged(_ event: ContentDownloadProgressChangedEvent, offlineContentManager: OfflineContentManager) {
|
|
84
|
+
sendOfflineEvent(eventType: .onProgress, body: [
|
|
85
|
+
"progress": event.progress
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
Called when all actions have been suspended.
|
|
91
|
+
*/
|
|
92
|
+
func onContentDownloadSuspended(_ event: ContentDownloadSuspendedEvent, offlineContentManager: OfflineContentManager) {
|
|
93
|
+
sendOfflineEvent(eventType: .onSuspended)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
Called when all actions have been resumed.
|
|
98
|
+
*/
|
|
99
|
+
func onContentDownloadResumed(_ event: ContentDownloadResumedEvent, offlineContentManager: OfflineContentManager) {
|
|
100
|
+
sendOfflineEvent(eventType: .onResumed)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
Called when the download of the media content was canceled by the user and all partially downloaded content has been deleted from disk.
|
|
105
|
+
*/
|
|
106
|
+
func onContentDownloadCanceled(_ event: ContentDownloadCanceledEvent, offlineContentManager: OfflineContentManager) {
|
|
107
|
+
sendOfflineEvent(eventType: .onCanceled)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
Called when the DRM license was renewed.
|
|
112
|
+
*/
|
|
113
|
+
func onOfflineContentLicenseRenewed(_ event: OfflineContentLicenseRenewedEvent, offlineContentManager: OfflineContentManager) {
|
|
114
|
+
sendOfflineEvent(eventType: .onDrmLicenseUpdated)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
Called on every call to OfflineContentManager.createOfflineSourceConfig(restrictedToAssetCache:) if it is DRM protected and the offline DRM license has expired.
|
|
119
|
+
*/
|
|
120
|
+
func onOfflineContentLicenseExpired(_ event: OfflineContentLicenseExpiredEvent, offlineContentManager: OfflineContentManager) {
|
|
121
|
+
sendOfflineEvent(eventType: .onDrmLicenseExpired)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func sendOfflineEvent(eventType: EventType, body: [String: Any?] = [:]) {
|
|
125
|
+
var baseEvent: [String: Any?] = [
|
|
126
|
+
"nativeId": nativeId,
|
|
127
|
+
"identifier": identifier,
|
|
128
|
+
"eventType": eventType.rawValue,
|
|
129
|
+
"state": RCTConvert.toJson(offlineState: offlineContentManager.offlineState)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
var eventBody = baseEvent.merging(body) { (current, _) in current }
|
|
133
|
+
|
|
134
|
+
do {
|
|
135
|
+
try eventEmitter?.sendEvent(withName: "BitmovinOfflineEvent", body: eventBody)
|
|
136
|
+
} catch let error as NSError {
|
|
137
|
+
print(error)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
#endif
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_REMAP_MODULE(BitmovinOfflineModule, OfflineModule, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(initWithConfig:(NSString *)nativeId config:(nullable id)config drmNativeId:(nullable NSString *)drmNativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
7
|
+
RCT_EXTERN_METHOD(getState:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
8
|
+
RCT_EXTERN_METHOD(getOptions:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
9
|
+
RCT_EXTERN_METHOD(download:(NSString *)nativeId request:(nullable id)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
RCT_EXTERN_METHOD(resume:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
RCT_EXTERN_METHOD(suspend:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
12
|
+
RCT_EXTERN_METHOD(cancelDownload:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
13
|
+
RCT_EXTERN_METHOD(usedStorage:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
14
|
+
RCT_EXTERN_METHOD(deleteAll:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
RCT_EXTERN_METHOD(downloadLicense:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
16
|
+
RCT_EXTERN_METHOD(renewOfflineLicense:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
17
|
+
RCT_EXTERN_METHOD(release:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
18
|
+
RCT_EXTERN_METHOD(releaseLicense:(NSString *)nativeId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
|
19
|
+
@end
|