Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Casting support #262

Merged
merged 88 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
7e801f5
feat(casting): define public API for player API, events and remoteCon…
rolandkakonyi Sep 18, 2023
a4971c1
feat(casting): add RemoteControlConfig deserializer
strangesource Sep 19, 2023
089a40d
feat(casting): add changelog entry
rolandkakonyi Sep 19, 2023
5b8405b
feat(casting): add platform mentions to player API
rolandkakonyi Sep 19, 2023
4b56c08
feat(casting): add documentation about regular Android platback event…
rolandkakonyi Sep 19, 2023
82b056b
feat(casting): fix optionality in CastPayload
rolandkakonyi Sep 19, 2023
56ee8d3
feat(casting): fix casting event names
rolandkakonyi Sep 19, 2023
47a8071
feat(casting): fix API documentation
rolandkakonyi Sep 19, 2023
1d77a80
feat(casting): improve API documentation
rolandkakonyi Sep 19, 2023
d75187d
Merge pull request #239 from bitmovin/casting-api
rolandkakonyi Sep 19, 2023
5562685
feat(casting): add player module android casting API
strangesource Sep 19, 2023
c347115
feat(casting): define BitmovinCastManager public API
rolandkakonyi Sep 19, 2023
a313fd9
feat(casting): add android cast event serialization
strangesource Sep 19, 2023
4d76812
Merge branch 'casting' into casting-android/add-remote-control-config…
strangesource Sep 19, 2023
116ea19
Merge pull request #243 from bitmovin/casting-android/add-android-cas…
strangesource Sep 19, 2023
141ff1a
Merge pull request #244 from bitmovin/casting-android/add-remote-cont…
strangesource Sep 19, 2023
3ee1aef
feat(casting): fix API documentation
rolandkakonyi Sep 19, 2023
305d371
Merge pull request #241 from bitmovin/casting-android/add-android-api…
strangesource Sep 19, 2023
bcbaf4d
feat(casting): remove sendMetadata from BitmovinCastManager
rolandkakonyi Sep 19, 2023
f78b614
Merge pull request #242 from bitmovin/casting-bitmovincastmanager-api
rolandkakonyi Sep 19, 2023
21ac37a
feat(casting): implement casting APIs on Player for iOS
rolandkakonyi Sep 19, 2023
861c5c1
feat(casting): add API for listening to casting events on the RN side
rolandkakonyi Sep 19, 2023
7db16c6
Merge pull request #246 from bitmovin/casting-player-events-api
rolandkakonyi Sep 19, 2023
c9ef4ad
Merge pull request #245 from bitmovin/casting-ios/extend-player-api
rolandkakonyi Sep 19, 2023
34b5648
feat(casting): implement RemoteControlConfig parsing
rolandkakonyi Sep 19, 2023
8306d41
Merge pull request #247 from bitmovin/casting-ios/implement-config-se…
rolandkakonyi Sep 19, 2023
379252d
feat(casting): change customReceiverConfig to String to String map in…
rolandkakonyi Sep 19, 2023
85fafcf
feat(casting): introduce BitmovinCastManagerModule for iOS
rolandkakonyi Sep 19, 2023
b35894d
Merge branch 'casting' into casting-ios/implement-bitmovincastmanager
rolandkakonyi Sep 19, 2023
cda908a
feat(casting): add missing API docs
rolandkakonyi Sep 19, 2023
b729dc6
Merge branch 'fix-non-drm-playback-on-iOS' into casting
rolandkakonyi Sep 19, 2023
001f9f4
Merge branch 'casting' into casting-ios/implement-bitmovincastmanager
rolandkakonyi Sep 19, 2023
4315ea9
feat(casting): add note about calling initialize only when Cast SDK i…
rolandkakonyi Sep 19, 2023
733af0e
chore(casting): add missing native id
strangesource Sep 20, 2023
57381f0
feat(casting): remove unnecessary filter
rolandkakonyi Sep 20, 2023
c955a0b
Merge pull request #249 from bitmovin/casting-ios/implement-bitmovinc…
rolandkakonyi Sep 20, 2023
c5fe1d7
feat(casting): implement player events for casting
rolandkakonyi Sep 20, 2023
e522841
feat(casting): fix documentation
rolandkakonyi Sep 20, 2023
efdb661
Merge pull request #251 from bitmovin/add-missing-nativeId
strangesource Sep 20, 2023
e9f81bc
Merge pull request #248 from bitmovin/casting-update-remotecontrolcon…
rolandkakonyi Sep 20, 2023
865a993
feat(casting): rename initializeCasting to initializeCastManager for …
rolandkakonyi Sep 20, 2023
f684068
Merge pull request #253 from bitmovin/casting-rename-initializeCasting
rolandkakonyi Sep 20, 2023
e0b142e
feat(casting): implement Android BitmovinCastManagerModule and add ad…
strangesource Sep 20, 2023
4f0e67a
feat(casting): disable casting on other screens
rolandkakonyi Sep 20, 2023
64998be
feat(casting): use promise
strangesource Sep 20, 2023
7b41d9b
Merge pull request #254 from bitmovin/casting-android/add-cast-manager
strangesource Sep 20, 2023
bf7b5a8
Merge pull request #252 from bitmovin/casting-ios/implement-events
rolandkakonyi Sep 20, 2023
5ab3abc
feat(casting): add sample screen for casting
rolandkakonyi Sep 20, 2023
00a4603
feat(casting): add Google Cast SDK to example app for iOS
rolandkakonyi Sep 20, 2023
7d7648b
feat(casting): add play-services-cast-framework dependency and option…
strangesource Sep 20, 2023
93412c7
feat(casting): add mediarouter dependency to sample app
strangesource Sep 20, 2023
0141bfd
Merge pull request #255 from bitmovin/casting-add-sample-screen
rolandkakonyi Sep 20, 2023
94feff2
feat(casting): attach cast events to Android player
strangesource Sep 21, 2023
432b8fe
feat(casting): move dependencies to a more sensible location
strangesource Sep 21, 2023
ec4d23b
feat(casting): add expanded controller activity to manifest
strangesource Sep 21, 2023
d328d42
feat(casting): eagerly initialize cast context
strangesource Sep 21, 2023
23f8834
feat(casting): call `updateContext` on Android
strangesource Sep 21, 2023
98d40e5
feat(casting): add explaining comment to manifest and fix formatting
strangesource Sep 21, 2023
9153e04
Merge pull request #256 from bitmovin/casting-android/attach-cast-events
strangesource Sep 21, 2023
07519d9
Merge pull request #257 from bitmovin/casting-android/initialize-cast…
strangesource Sep 21, 2023
b348c58
docs(casting): add casting sample to example readme
strangesource Sep 21, 2023
1b35dd7
feat(casting): enable casting different source on iOS
rolandkakonyi Sep 21, 2023
2c57d12
feat(casting): cast DASH source from iOS
rolandkakonyi Sep 21, 2023
69da4fa
feat(casting): fix android parameter names
rolandkakonyi Sep 21, 2023
6a5e33e
Merge pull request #258 from bitmovin/update-readme-with-casting
strangesource Sep 21, 2023
6ec6c59
Merge branch 'casting' into casting-ios/cast-specific-source
rolandkakonyi Sep 21, 2023
45c33ac
feat(casting): improve swift code style
rolandkakonyi Sep 21, 2023
7cda7eb
feat(casting): add missing documentation
rolandkakonyi Sep 21, 2023
0669011
feat(casting): improve swift code style
rolandkakonyi Sep 21, 2023
ca1d83c
feat(casting): refactor prepareSource wrapper
rolandkakonyi Sep 21, 2023
b3e5461
feat(casting): enable DRM playback for casting
rolandkakonyi Sep 21, 2023
d4ecac6
feat(casting): update documentation
rolandkakonyi Sep 21, 2023
8fe2a9a
feat(casting): improve code style
rolandkakonyi Sep 21, 2023
f2c2c1b
feat(casting): add missing documentation
rolandkakonyi Sep 21, 2023
a015608
Merge branch 'casting-ios/cast-specific-source' into casting-ios/wide…
rolandkakonyi Sep 21, 2023
dfda37f
feat(casting): improve API docs
rolandkakonyi Sep 22, 2023
852d26f
feat(casting): rename SourceRemotePlaybackConfig to SourceRemoteContr…
rolandkakonyi Sep 22, 2023
ddff297
feat(casting): fix property name on source for remote control config
rolandkakonyi Sep 22, 2023
85c58e7
feat(casting): fix indentation
rolandkakonyi Sep 22, 2023
ee6f953
feat(casting): fix optionality
rolandkakonyi Sep 22, 2023
4993c8f
Merge branch 'casting-ios/cast-specific-source' into casting-ios/wide…
rolandkakonyi Sep 22, 2023
454fbba
feat(casting): improve DRM handling
rolandkakonyi Sep 22, 2023
2fe285f
feat(casting): mention that sourceRemoteControlConfig is not supporte…
rolandkakonyi Sep 22, 2023
da16514
feat(casting): fix documentation
rolandkakonyi Sep 22, 2023
49370d0
Merge pull request #259 from bitmovin/casting-ios/cast-specific-source
rolandkakonyi Sep 22, 2023
4fa39ac
Merge pull request #261 from bitmovin/casting-ios/widevine-drm
rolandkakonyi Sep 22, 2023
65cee12
feat(casting): fix casting sample screen
rolandkakonyi Sep 22, 2023
07943df
feat(casting): fix tvOS build
rolandkakonyi Sep 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- `DefaultMetadata` for configuration of the bundled analytics collector
- `Player.analytics` to access the `AnalyticsApi` and interact with the bundled analytics collector
- `SourceConfig.analyticsSourceMetadata` for extended configuration of the bundled analytics collector
- Google Cast SDK support for Android and iOS

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.bitmovin.player.reactnative

import com.bitmovin.player.casting.BitmovinCastManager
import com.bitmovin.player.reactnative.converter.JsonConverter
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.UIManagerModule

private const val MODULE_NAME = "BitmovinCastManagerModule"

@ReactModule(name = MODULE_NAME)
class BitmovinCastManagerModule(
private val context: ReactApplicationContext,
) : ReactContextBaseJavaModule(context) {
override fun getName() = MODULE_NAME

/**
* Returns whether the [BitmovinCastManager] is initialized.
*/
@ReactMethod
fun isInitialized(promise: Promise) = uiManager?.addUIBlock {
promise.resolve(BitmovinCastManager.isInitialized())
}

/**
* Initializes the [BitmovinCastManager] with the given options.
*/
@ReactMethod
fun initializeCastManager(options: ReadableMap?, promise: Promise) {
val castOptions = JsonConverter.toCastOptions(options)
uiManager?.addUIBlock {
BitmovinCastManager.initialize(
castOptions?.applicationId,
castOptions?.messageNamespace
)
promise.resolve(null)
}
}

/**
* Sends a message to the receiver.
*/
@ReactMethod
fun sendMessage(message: String, messageNamespace: String?, promise: Promise) {
uiManager?.addUIBlock {
BitmovinCastManager.getInstance().sendMessage(message, messageNamespace)
promise.resolve(null)
}
}

/**
* Updates the context of the [BitmovinCastManager] to the current activity.
*/
@ReactMethod
fun updateContext(promise: Promise) {
uiManager?.addUIBlock {
BitmovinCastManager.getInstance().updateContext(currentActivity)
promise.resolve(null)
}
}

private val uiManager: UIManagerModule?
get() = context.getNativeModule(UIManagerModule::class.java)
}

/**
* Represents configuration options for the [BitmovinCastManager].
*/
data class BitmovinCastManagerOptions(
val applicationId: String? = null,
val messageNamespace: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import com.bitmovin.analytics.api.DefaultMetadata
import com.bitmovin.player.api.Player
import com.bitmovin.player.api.analytics.create
import com.bitmovin.player.api.event.PlayerEvent
import com.bitmovin.player.reactnative.converter.JsonConverter
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
Expand Down Expand Up @@ -496,6 +497,48 @@ class PlayerModule(private val context: ReactApplicationContext) : ReactContextB
}
}

/**
* Initiates casting the current video to a cast-compatible remote device. The user has to choose to which device it
* should be sent.
*/
@ReactMethod
fun castVideo(nativeId: NativeId) {
uiManager()?.addUIBlock {
players[nativeId]?.castVideo()
}
}

/**
* Stops casting the current video. Has no effect if [isCasting] is false.
*/
@ReactMethod
fun castStop(nativeId: NativeId) {
uiManager()?.addUIBlock {
players[nativeId]?.castStop()
}
}

/**
* Whether casting to a cast-compatible remote device is available. [PlayerEvent.CastAvailable] signals when
* casting becomes available.
*/
@ReactMethod
fun isCastAvailable(nativeId: NativeId, promise: Promise) {
uiManager()?.addUIBlock {
promise.resolve(players[nativeId]?.isCastAvailable)
}
}

/**
* Whether video is currently being casted to a remote device and not played locally.
*/
@ReactMethod
fun isCasting(nativeId: NativeId, promise: Promise) {
uiManager()?.addUIBlock {
promise.resolve(players[nativeId]?.isCasting)
}
}

/**
* Helper function that returns the initialized `UIManager` instance.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ private val EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING = mapOf(
PlayerEvent.AdSkipped::class to "adSkipped",
PlayerEvent.AdStarted::class to "adStarted",
PlayerEvent.VideoPlaybackQualityChanged::class to "videoPlaybackQualityChanged",
PlayerEvent.CastStart::class to "castStart",
@Suppress("DEPRECATION")
PlayerEvent.CastPlaybackFinished::class to "castPlaybackFinished",
@Suppress("DEPRECATION")
PlayerEvent.CastPaused::class to "castPaused",
@Suppress("DEPRECATION")
PlayerEvent.CastPlaying::class to "castPlaying",
PlayerEvent.CastStarted::class to "castStarted",
PlayerEvent.CastAvailable::class to "castAvailable",
PlayerEvent.CastStopped::class to "castStopped",
PlayerEvent.CastWaitingForDevice::class to "castWaitingForDevice",
PlayerEvent.CastTimeUpdated::class to "castTimeUpdated",
)

private val EVENT_CLASS_TO_REACT_NATIVE_NAME_MAPPING_UI = mapOf<KClass<out Event>, String>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ class RNPlayerViewManager(private val context: ReactApplicationContext) : Simple
"fullscreenDisabled" to "onFullscreenDisabled",
"fullscreenEnter" to "onFullscreenEnter",
"fullscreenExit" to "onFullscreenExit",
"castStart" to "onCastStart",
"castPlaybackFinished" to "onCastPlaybackFinished",
"castPaused" to "onCastPaused",
"castPlaying" to "onCastPlaying",
"castStarted" to "onCastStarted",
"castAvailable" to "onCastAvailable",
"castStopped" to "onCastStopped",
"castWaitingForDevice" to "onCastWaitingForDevice",
"castTimeUpdated" to "onCastTimeUpdated",
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class RNPlayerViewPackage : ReactPackage {
PlayerAnalyticsModule(reactContext),
RNPlayerViewManager(reactContext),
FullscreenHandlerModule(reactContext),
CustomMessageHandlerModule(reactContext)
CustomMessageHandlerModule(reactContext),
BitmovinCastManagerModule(reactContext),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,15 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB
* @param nativeId ID to be associated with the `Source` instance.
* @param drmNativeId ID of the DRM config to use.
* @param config `SourceConfig` object received from JS.
* @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. Not supported on Android.
* @param analyticsSourceMetadata `SourceMetadata` object received from JS.
*/
@ReactMethod
fun initWithAnalyticsConfig(
nativeId: NativeId,
drmNativeId: NativeId?,
config: ReadableMap?,
sourceRemoteControlConfig: ReadableMap?,
analyticsSourceMetadata: ReadableMap?
) {
uiManager()?.addUIBlock {
Expand All @@ -69,12 +71,14 @@ class SourceModule(private val context: ReactApplicationContext) : ReactContextB
* @param nativeId ID to be associated with the `Source` instance.
* @param drmNativeId ID of the DRM config to use.
* @param config `SourceConfig` object received from JS.
* @param sourceRemoteControlConfig `SourceRemoteControlConfig` object received from JS. Not supported on Android.
*/
@ReactMethod
fun initWithConfig(
nativeId: NativeId,
drmNativeId: NativeId?,
config: ReadableMap?,
sourceRemoteControlConfig: ReadableMap?
) {
uiManager()?.addUIBlock {
initializeSource(nativeId, drmNativeId, config) { sourceConfig ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import com.bitmovin.player.api.advertising.AdQuartile
import com.bitmovin.player.api.advertising.AdSource
import com.bitmovin.player.api.advertising.AdSourceType
import com.bitmovin.player.api.advertising.AdvertisingConfig
import com.bitmovin.player.api.casting.RemoteControlConfig
import com.bitmovin.player.api.drm.WidevineConfig
import com.bitmovin.player.api.event.PlayerEvent
import com.bitmovin.player.api.event.SourceEvent
import com.bitmovin.player.api.event.data.CastPayload
import com.bitmovin.player.api.event.data.SeekPosition
import com.bitmovin.player.api.media.AdaptationConfig
import com.bitmovin.player.api.media.audio.AudioTrack
Expand All @@ -37,8 +39,10 @@ import com.bitmovin.player.api.source.SourceType
import com.bitmovin.player.api.source.TimelineReferencePoint
import com.bitmovin.player.api.ui.ScalingMode
import com.bitmovin.player.api.ui.StyleConfig
import com.bitmovin.player.reactnative.BitmovinCastManagerOptions
import com.bitmovin.player.reactnative.extensions.getBooleanOrNull
import com.bitmovin.player.reactnative.extensions.getName
import com.bitmovin.player.reactnative.extensions.getOrDefault
import com.bitmovin.player.reactnative.extensions.getProperty
import com.bitmovin.player.reactnative.extensions.putBoolean
import com.bitmovin.player.reactnative.extensions.putDouble
Expand Down Expand Up @@ -96,9 +100,67 @@ class JsonConverter {
playerConfig.adaptationConfig = it
}
}
if (json.hasKey("remoteControlConfig")) {
toRemoteControlConfig(json.getMap("remoteControlConfig"))?.let {
playerConfig.remoteControlConfig = it
}
}
return playerConfig
}

/**
* Converts an arbitrary [ReadableMap] to a [RemoteControlConfig].
*
* @param json JS object representing the [RemoteControlConfig].
* @return The generated [RemoteControlConfig].
*/
private fun toRemoteControlConfig(json: ReadableMap?): RemoteControlConfig? {
if (json == null) return null
val defaultRemoteControlConfig = RemoteControlConfig()

val receiverStylesheetUrl = json.getOrDefault(
"receiverStylesheetUrl",
defaultRemoteControlConfig.receiverStylesheetUrl
)

var customReceiverConfig = defaultRemoteControlConfig.customReceiverConfig
if (json.hasKey("customReceiverConfig")) {
customReceiverConfig = json.getMap("customReceiverConfig")
?.toHashMap()
?.mapValues { entry -> entry.value as String } ?: emptyMap()
}

val isCastEnabled = json.getOrDefault(
"isCastEnabled",
defaultRemoteControlConfig.isCastEnabled
)

val sendManifestRequestsWithCredentials = json.getOrDefault(
"sendManifestRequestsWithCredentials",
defaultRemoteControlConfig.sendManifestRequestsWithCredentials
)

val sendSegmentRequestsWithCredentials = json.getOrDefault(
"sendSegmentRequestsWithCredentials",
defaultRemoteControlConfig.sendSegmentRequestsWithCredentials
)

val sendDrmLicenseRequestsWithCredentials = json.getOrDefault(
"sendDrmLicenseRequestsWithCredentials",
defaultRemoteControlConfig.sendDrmLicenseRequestsWithCredentials
)


return RemoteControlConfig(
receiverStylesheetUrl = receiverStylesheetUrl,
customReceiverConfig = customReceiverConfig,
isCastEnabled = isCastEnabled,
sendManifestRequestsWithCredentials = sendManifestRequestsWithCredentials,
sendSegmentRequestsWithCredentials = sendSegmentRequestsWithCredentials,
sendDrmLicenseRequestsWithCredentials = sendDrmLicenseRequestsWithCredentials,
)
}

/**
* Converts an arbitrary `json` to `SourceOptions`.
* @param json JS object representing the `SourceOptions`.
Expand All @@ -123,7 +185,7 @@ class JsonConverter {
"end" -> TimelineReferencePoint.End
else -> null
}

/**
* Converts an arbitrary `json` to `AdaptationConfig`.
* @param json JS object representing the `AdaptationConfig`.
Expand Down Expand Up @@ -564,13 +626,34 @@ class JsonConverter {
json.putMap("oldVideoQuality", fromVideoQuality(event.oldVideoQuality))
}

is PlayerEvent.CastWaitingForDevice -> {
json.putMap("castPayload", fromCastPayload(event.castPayload))
}

is PlayerEvent.CastStarted -> {
json.putString("deviceName", event.deviceName)
}

else -> {
// Event is not supported yet or does not have any additional data
}
}
return json
}

/**
* Converts an arbitrary `json` into [BitmovinCastManagerOptions].
* @param json JS object representing the [BitmovinCastManagerOptions].
* @return The generated [BitmovinCastManagerOptions] if successful, `null` otherwise.
*/
fun toCastOptions(json: ReadableMap?): BitmovinCastManagerOptions? {
if (json == null) return null
return BitmovinCastManagerOptions(
json.getOrDefault("applicationId", null),
json.getOrDefault("messageNamespace", null)
)
}

/**
* Converts an arbitrary `json` to `WidevineConfig`.
* @param json JS object representing the `WidevineConfig`.
Expand Down Expand Up @@ -1008,3 +1091,12 @@ class JsonConverter {
}
}
}

/**
* Converts a [CastPayload] object into its JS representation.
*/
private fun fromCastPayload(castPayload: CastPayload) = Arguments.createMap().apply {
putDouble("currentTime", castPayload.currentTime)
putString("deviceName", castPayload.deviceName)
putString("type", castPayload.type)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,21 @@ import com.facebook.react.bridge.ReadableMap
fun ReadableMap.getBooleanOrNull(
key: String
): Boolean? = takeIf { hasKey(key) }?.getBoolean(key)

/**
* Reads the [Boolean] value from the given [ReadableMap] if the [key] is present.
* Returns the [default] value otherwise.
*/
fun ReadableMap.getOrDefault(
key: String,
default: Boolean,
) = if (hasKey(key)) getBoolean(key) else default

/**
* Reads the [String] value from the given [ReadableMap] if the [key] is present.
* Returns the [default] value otherwise.
*/
fun ReadableMap.getOrDefault(
key: String,
default: String?,
) = if (hasKey(key)) getString(key) else default
1 change: 1 addition & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inside the [`src/screens/`](https://github.com/bitmovin/bitmovin-player-react-na
- [Basic Analytics](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/BasicAnalytics.tsx)
- [Basic Offline Playback](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/OfflinePlayback.tsx) (iOS and Android only)
- [System UI](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/SystemUi.tsx) (iOS and tvOS only)
- [Casting](https://github.com/bitmovin/bitmovin-player-react-native/blob/development/example/src/screens/Casting.tsx) (iOS and Android only)

### Custom asset playback

Expand Down
Loading