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.
Files changed (34) hide show
  1. package/RNBitmovinPlayer.podspec +1 -1
  2. package/android/build.gradle +1 -2
  3. package/android/src/main/java/com/bitmovin/player/reactnative/AnalyticsModule.kt +43 -11
  4. package/android/src/main/java/com/bitmovin/player/reactnative/OfflineModule.kt +279 -0
  5. package/android/src/main/java/com/bitmovin/player/reactnative/PlayerModule.kt +37 -1
  6. package/android/src/main/java/com/bitmovin/player/reactnative/RNPlayerViewPackage.kt +1 -0
  7. package/android/src/main/java/com/bitmovin/player/reactnative/converter/JsonConverter.kt +123 -35
  8. package/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineContentManagerBridge.kt +216 -0
  9. package/android/src/main/java/com/bitmovin/player/reactnative/offline/OfflineDownloadRequest.kt +7 -0
  10. package/ios/DrmModule.swift +5 -5
  11. package/ios/OfflineContentManagerBridge.swift +141 -0
  12. package/ios/OfflineModule.m +19 -0
  13. package/ios/OfflineModule.swift +465 -0
  14. package/ios/PlayerModule.m +2 -0
  15. package/ios/PlayerModule.swift +40 -7
  16. package/ios/RCTConvert+BitmovinPlayer.swift +135 -1
  17. package/ios/RNBitmovinPlayer.h +1 -0
  18. package/lib/index.d.mts +382 -2
  19. package/lib/index.d.ts +382 -2
  20. package/lib/index.js +233 -0
  21. package/lib/index.mjs +231 -0
  22. package/package.json +1 -1
  23. package/src/adaptationConfig.ts +10 -0
  24. package/src/index.ts +2 -0
  25. package/src/offline/index.ts +7 -0
  26. package/src/offline/offlineContentConfig.ts +18 -0
  27. package/src/offline/offlineContentManager.ts +216 -0
  28. package/src/offline/offlineContentManagerListener.ts +187 -0
  29. package/src/offline/offlineContentOptions.ts +29 -0
  30. package/src/offline/offlineDownloadRequest.ts +20 -0
  31. package/src/offline/offlineSourceOptions.ts +11 -0
  32. package/src/offline/offlineState.ts +22 -0
  33. package/src/player.ts +30 -0
  34. 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.config.SourceMetadata
5
- import com.bitmovin.analytics.data.CustomData
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.putInt
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?.getString("position") ?: "pre")
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 != null && format.isNotBlank()) {
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? = json?.let {
782
- val customData = CustomData()
783
- for (n in 1..30) {
784
- it.getString("customData${n}")?.let { customDataN ->
785
- customData.setProperty("customData${n}", customDataN)
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
- it.getString("experimentName")?.let { experimentName ->
789
- customData.experimentName = experimentName
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 sourceMetadata = SourceMetadata(
816
- title = it.getString("title"),
817
- videoId = it.getString("videoId"),
818
- cdnProvider = it.getString("cdnProvider"),
819
- path = it.getString("path"),
820
- isLive = it.getBoolean("isLive")
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
  }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ package com.bitmovin.player.reactnative.offline
2
+
3
+ data class OfflineDownloadRequest(
4
+ val minimumBitrate: Int?,
5
+ val audioOptionIds: List<String>?,
6
+ val textOptionIds: List<String>?
7
+ )
@@ -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 ncecessary to add a return type and a return
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 ncecessary to add a return type and a return
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 ncecessary to add a return type and a return value
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 ncecessary to add a return type and a return value
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 ncecessary to add a return type and a return value
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