mirror of
https://github.com/zoriya/react-native-video.git
synced 2025-12-05 23:06:14 +00:00
Compare commits
15 Commits
0492198dff
...
52a22aa10e
| Author | SHA1 | Date | |
|---|---|---|---|
| 52a22aa10e | |||
| 771196dd0f | |||
| 82ff34fa9c | |||
| b2cf9d63f1 | |||
| 937d34d73e | |||
| a092578cab | |||
| d6803b2b4c | |||
| 6703499e60 | |||
| ecf5849f2c | |||
| 8ad923750b | |||
| 796c0edfa0 | |||
| 57039bb564 | |||
|
|
9b74665fb4 | ||
|
|
1671c63dab | ||
|
|
02044de0e9 |
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
5
biome.json
Normal file
5
biome.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"formatter": {
|
||||
"useEditorconfig": true
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
"allowUnusedLabels": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["ESNext"],
|
||||
"lib": ["ESNext", "dom"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
@@ -33,4 +33,4 @@
|
||||
"target": "ESNext",
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc",
|
||||
"start": "react-native start --client-logs"
|
||||
"start": "react-native start --client-logs",
|
||||
"bundle-install": "bundle install",
|
||||
"pods": "cd ios && pod install && cd .."
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-community/slider": "^4.5.6",
|
||||
|
||||
@@ -146,6 +146,7 @@ const VideoDemo = () => {
|
||||
player.playWhenInactive = settings.playWhenInactive;
|
||||
player.mixAudioMode = settings.mixAudioMode;
|
||||
player.ignoreSilentSwitchMode = settings.ignoreSilentSwitchMode;
|
||||
player.showNotificationControls = settings.showNotificationControls;
|
||||
}, [settings, player]);
|
||||
|
||||
const handleSeek = (val: number) => {
|
||||
@@ -315,6 +316,13 @@ const VideoDemo = () => {
|
||||
value={settings.playWhenInactive}
|
||||
onValueChange={(value) => updateSetting('playWhenInactive', value)}
|
||||
/>
|
||||
<SwitchControl
|
||||
label="Notification Controls"
|
||||
value={settings.showNotificationControls}
|
||||
onValueChange={(value) =>
|
||||
updateSetting('showNotificationControls', value)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface VideoSettings {
|
||||
ignoreSilentSwitchMode: IgnoreSilentSwitchMode;
|
||||
playInBackground: boolean;
|
||||
playWhenInactive: boolean;
|
||||
showNotificationControls: boolean;
|
||||
}
|
||||
|
||||
export const defaultSettings: VideoSettings = {
|
||||
@@ -32,4 +33,5 @@ export const defaultSettings: VideoSettings = {
|
||||
ignoreSilentSwitchMode: 'auto',
|
||||
playInBackground: true,
|
||||
playWhenInactive: false,
|
||||
showNotificationControls: true,
|
||||
};
|
||||
|
||||
@@ -103,5 +103,14 @@ export const getVideoSource = (type: VideoType): VideoConfig => {
|
||||
type: 'vtt',
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
title: 'Big Buck Bunny',
|
||||
artist: 'Blender Foundation',
|
||||
imageUri:
|
||||
'https://peach.blender.org/wp-content/uploads/title_anouncement.jpg',
|
||||
subtitle: 'By the Blender Institute',
|
||||
description:
|
||||
'Big Buck Bunny is a short computer-animated comedy film by the Blender Institute, part of the Blender Foundation. It was made using Blender, a free and open-source 3D creation suite.',
|
||||
},
|
||||
} as VideoConfig;
|
||||
};
|
||||
|
||||
11
package.json
11
package.json
@@ -64,7 +64,8 @@
|
||||
"release": true
|
||||
},
|
||||
"hooks": {
|
||||
"before:release": "bun run --cwd packages/react-native-video build"
|
||||
"before:release": "bun run --cwd packages/react-native-video build && bun run --cwd packages/drm-plugin build",
|
||||
"before:git": "bun install && bun example bundle-install && bun example pods && git add bun.lock && git add example/ios/Podfile.lock"
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
@@ -73,6 +74,10 @@
|
||||
"file": "packages/react-native-video/package.json",
|
||||
"path": "version"
|
||||
},
|
||||
{
|
||||
"file": "packages/drm-plugin/package.json",
|
||||
"path": "version"
|
||||
},
|
||||
{
|
||||
"file": "example/package.json",
|
||||
"path": "version"
|
||||
@@ -91,6 +96,10 @@
|
||||
"type": "fix",
|
||||
"section": "Bug Fixes 🐛"
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"section": "Code Refactoring 🛠"
|
||||
},
|
||||
{
|
||||
"type": "chore(deps)",
|
||||
"section": "Dependency Upgrades 📦"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@twg/react-native-video-drm",
|
||||
"version": "0.1.0",
|
||||
"version": "7.0.0-alpha.5",
|
||||
"description": "DRM plugin for react-native-video",
|
||||
"main": "./lib/module/index.js",
|
||||
"types": "./lib/typescript/src/index.d.ts",
|
||||
@@ -41,7 +41,7 @@
|
||||
"prepare": "bun run build",
|
||||
"build": "bob build",
|
||||
"specs": "nitrogen",
|
||||
"release": "release-it --only-version"
|
||||
"release": "release-it --preRelease alpha --npm.tag=next"
|
||||
},
|
||||
"keywords": [
|
||||
"react-native",
|
||||
|
||||
@@ -120,7 +120,7 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags "-O2 -frtti -fexceptions -Wall -fstack-protector-all"
|
||||
arguments "-DANDROID_STL=c++_shared"
|
||||
arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
|
||||
abiFilters (*reactNativeArchitectures())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
RNVideo_kotlinVersion=1.9.24
|
||||
RNVideo_minKotlinVersion=1.8.0
|
||||
RNVideo_minSdkVersion=23
|
||||
RNVideo_targetSdkVersion=34
|
||||
RNVideo_compileSdkVersion=34
|
||||
RNVideo_ndkversion=26.1.10909125
|
||||
RNVideo_minSdkVersion=24
|
||||
RNVideo_targetSdkVersion=35
|
||||
RNVideo_compileSdkVersion=35
|
||||
RNVideo_ndkversion=27.1.12297006
|
||||
|
||||
RNVideo_useExoplayerDash=true
|
||||
RNVideo_useExoplayerHls=true
|
||||
|
||||
@@ -42,7 +42,11 @@ fun VideoPlaybackService.Companion.stopService(
|
||||
serviceConnection: VideoPlaybackServiceConnection
|
||||
) {
|
||||
try {
|
||||
NitroModules.applicationContext?.currentActivity?.unbindService(serviceConnection)
|
||||
// Unregister the player first; this might stop the service if no players remain
|
||||
serviceConnection.unregisterPlayer(player)
|
||||
// Ask service (if still connected) to stop when idle
|
||||
try { serviceConnection.serviceBinder?.service?.stopIfNoPlayers() } catch (_: Exception) {}
|
||||
// Then unbind
|
||||
NitroModules.applicationContext?.currentActivity?.unbindService(serviceConnection)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import androidx.annotation.OptIn
|
||||
import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.margelo.nitro.video.BufferConfig
|
||||
import com.margelo.nitro.video.CustomVideoMetadata
|
||||
import com.margelo.nitro.video.HybridVideoPlayerSource
|
||||
import com.margelo.nitro.video.LivePlaybackParams
|
||||
import com.margelo.nitro.video.NativeDrmParams
|
||||
@@ -39,6 +41,10 @@ fun createMediaItemFromVideoConfig(
|
||||
mediaItemBuilder.setLiveConfiguration(getLiveConfiguration(livePlaybackParams))
|
||||
}
|
||||
|
||||
source.config.metadata?.let { metadata ->
|
||||
mediaItemBuilder.setMediaMetadata(getCustomMetadata(metadata))
|
||||
}
|
||||
|
||||
return PluginsRegistry.shared.overrideMediaItemBuilder(
|
||||
source,
|
||||
mediaItemBuilder
|
||||
@@ -122,3 +128,14 @@ fun getLiveConfiguration(
|
||||
|
||||
return liveConfiguration.build()
|
||||
}
|
||||
|
||||
fun getCustomMetadata(metadata: CustomVideoMetadata): MediaMetadata {
|
||||
return MediaMetadata.Builder()
|
||||
.setDisplayTitle(metadata.title)
|
||||
.setTitle(metadata.title)
|
||||
.setSubtitle(metadata.subtitle)
|
||||
.setDescription(metadata.description)
|
||||
.setArtist(metadata.artist)
|
||||
.setArtworkUri(metadata.imageUri?.toUri())
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@ fun buildExternalSubtitlesMediaSource(context: Context, source: HybridVideoPlaye
|
||||
.setUri(source.uri.toUri())
|
||||
.setSubtitleConfigurations(getSubtitlesConfiguration(source.config))
|
||||
|
||||
source.config.metadata?.let { metadata ->
|
||||
mediaItemBuilderWithSubtitles.setMediaMetadata(getCustomMetadata(metadata))
|
||||
}
|
||||
|
||||
val mediaItemBuilder = PluginsRegistry.shared.overrideMediaItemBuilder(
|
||||
source,
|
||||
mediaItemBuilderWithSubtitles
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.twg.video.core.services.playback
|
||||
|
||||
import android.content.Context
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.google.common.collect.ImmutableList
|
||||
import androidx.core.os.bundleOf
|
||||
import android.os.Bundle
|
||||
import com.margelo.nitro.NitroModules
|
||||
import com.twg.video.core.LibraryError
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class CustomMediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) {
|
||||
|
||||
init {
|
||||
setSmallIcon(androidx.media3.session.R.drawable.media3_notification_small_icon)
|
||||
}
|
||||
|
||||
fun getContext(): Context {
|
||||
return NitroModules.applicationContext ?: run {
|
||||
throw LibraryError.ApplicationContextNotFound
|
||||
}
|
||||
}
|
||||
|
||||
override fun getNotificationContentTitle(metadata: MediaMetadata): CharSequence? {
|
||||
return metadata.title
|
||||
?: metadata.displayTitle
|
||||
?: metadata.subtitle
|
||||
?: metadata.description
|
||||
?: "${getAppName()} is playing"
|
||||
}
|
||||
|
||||
override fun getNotificationContentText(metadata: MediaMetadata): CharSequence? {
|
||||
return metadata.artist
|
||||
?: metadata.subtitle
|
||||
?: metadata.description
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEEK_INTERVAL_MS = 10000L
|
||||
private const val TAG = "CustomMediaNotificationProvider"
|
||||
|
||||
enum class COMMAND(val stringValue: String) {
|
||||
NONE("NONE"),
|
||||
SEEK_FORWARD("COMMAND_SEEK_FORWARD"),
|
||||
SEEK_BACKWARD("COMMAND_SEEK_BACKWARD"),
|
||||
TOGGLE_PLAY("COMMAND_TOGGLE_PLAY"),
|
||||
PLAY("COMMAND_PLAY"),
|
||||
PAUSE("COMMAND_PAUSE")
|
||||
}
|
||||
|
||||
fun commandFromString(value: String): COMMAND =
|
||||
when (value) {
|
||||
COMMAND.SEEK_FORWARD.stringValue -> COMMAND.SEEK_FORWARD
|
||||
COMMAND.SEEK_BACKWARD.stringValue -> COMMAND.SEEK_BACKWARD
|
||||
COMMAND.TOGGLE_PLAY.stringValue -> COMMAND.TOGGLE_PLAY
|
||||
COMMAND.PLAY.stringValue -> COMMAND.PLAY
|
||||
COMMAND.PAUSE.stringValue -> COMMAND.PAUSE
|
||||
else -> COMMAND.NONE
|
||||
}
|
||||
|
||||
fun handleCommand(command: COMMAND, session: MediaSession) {
|
||||
// TODO: get somehow ControlsConfig here - for now hardcoded 10000ms
|
||||
|
||||
when (command) {
|
||||
COMMAND.SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition - SEEK_INTERVAL_MS)
|
||||
COMMAND.SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + SEEK_INTERVAL_MS)
|
||||
COMMAND.TOGGLE_PLAY -> handleCommand(if (session.player.isPlaying) COMMAND.PAUSE else COMMAND.PLAY, session)
|
||||
COMMAND.PLAY -> session.player.play()
|
||||
COMMAND.PAUSE -> session.player.pause()
|
||||
else -> Log.w(TAG, "Received COMMAND.NONE - was there an error?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppName(): String {
|
||||
return try {
|
||||
val context = getContext()
|
||||
val pm = context.packageManager
|
||||
val label = pm.getApplicationLabel(context.applicationInfo)
|
||||
label.toString()
|
||||
} catch (e: Exception) {
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMediaButtons(
|
||||
session: MediaSession,
|
||||
playerCommands: Player.Commands,
|
||||
mediaButtonPreferences: ImmutableList<CommandButton>,
|
||||
showPauseButton: Boolean
|
||||
): ImmutableList<CommandButton> {
|
||||
val rewind = CommandButton.Builder()
|
||||
.setDisplayName("Rewind")
|
||||
.setSessionCommand(androidx.media3.session.SessionCommand(
|
||||
COMMAND.SEEK_BACKWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
))
|
||||
.setIconResId(androidx.media3.session.R.drawable.media3_icon_skip_back_10)
|
||||
.setExtras(bundleOf(COMMAND_KEY_COMPACT_VIEW_INDEX to 0))
|
||||
.build()
|
||||
|
||||
val toggle = CommandButton.Builder()
|
||||
.setDisplayName(if (showPauseButton) "Pause" else "Play")
|
||||
.setSessionCommand(androidx.media3.session.SessionCommand(
|
||||
COMMAND.TOGGLE_PLAY.stringValue,
|
||||
Bundle.EMPTY
|
||||
))
|
||||
.setIconResId(
|
||||
if (showPauseButton) androidx.media3.session.R.drawable.media3_icon_pause
|
||||
else androidx.media3.session.R.drawable.media3_icon_play
|
||||
)
|
||||
.setExtras(bundleOf(COMMAND_KEY_COMPACT_VIEW_INDEX to 1))
|
||||
.build()
|
||||
|
||||
val forward = CommandButton.Builder()
|
||||
.setDisplayName("Forward")
|
||||
.setSessionCommand(androidx.media3.session.SessionCommand(
|
||||
COMMAND.SEEK_FORWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
))
|
||||
.setIconResId(androidx.media3.session.R.drawable.media3_icon_skip_forward_10)
|
||||
.setExtras(bundleOf(COMMAND_KEY_COMPACT_VIEW_INDEX to 2))
|
||||
.build()
|
||||
|
||||
return ImmutableList.of(rewind, toggle, forward)
|
||||
}
|
||||
|
||||
override fun addNotificationActions(
|
||||
mediaSession: MediaSession,
|
||||
mediaButtons: ImmutableList<CommandButton>,
|
||||
builder: androidx.core.app.NotificationCompat.Builder,
|
||||
actionFactory: androidx.media3.session.MediaNotification.ActionFactory
|
||||
): IntArray {
|
||||
// Use default behavior to add actions from our custom buttons and return compact indices
|
||||
val compact = super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory)
|
||||
return if (compact.isEmpty()) intArrayOf(0, 1, 2) else compact
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,41 @@ import android.os.Bundle
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.collect.ImmutableList
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class VideoPlaybackCallback : MediaSession.Callback {
|
||||
// For Android 13+
|
||||
private fun buildCustomButtons(): ImmutableList<CommandButton> {
|
||||
val rewind = CommandButton.Builder()
|
||||
.setDisplayName("Rewind")
|
||||
.setSessionCommand(
|
||||
SessionCommand(
|
||||
CustomMediaNotificationProvider.Companion.COMMAND.SEEK_BACKWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
)
|
||||
.setIconResId(androidx.media3.session.R.drawable.media3_icon_skip_back_10)
|
||||
.build()
|
||||
|
||||
val forward = CommandButton.Builder()
|
||||
.setDisplayName("Forward")
|
||||
.setSessionCommand(
|
||||
SessionCommand(
|
||||
CustomMediaNotificationProvider.Companion.COMMAND.SEEK_FORWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
)
|
||||
.setIconResId(androidx.media3.session.R.drawable.media3_icon_skip_forward_10)
|
||||
.build()
|
||||
|
||||
return ImmutableList.of(rewind, forward)
|
||||
}
|
||||
|
||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||
try {
|
||||
@@ -23,36 +51,46 @@ class VideoPlaybackCallback : MediaSession.Callback {
|
||||
).setAvailableSessionCommands(
|
||||
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||
.add(
|
||||
SessionCommand(
|
||||
VideoPlaybackService.Companion.COMMAND.SEEK_FORWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
SessionCommand(
|
||||
CustomMediaNotificationProvider.Companion.COMMAND.SEEK_FORWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
)
|
||||
.add(
|
||||
SessionCommand(
|
||||
VideoPlaybackService.Companion.COMMAND.SEEK_BACKWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
CustomMediaNotificationProvider.Companion.COMMAND.SEEK_BACKWARD.stringValue,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
.add(
|
||||
SessionCommand(
|
||||
CustomMediaNotificationProvider.Companion.COMMAND.TOGGLE_PLAY.stringValue,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
).build()
|
||||
).build()
|
||||
} catch (e: Exception) {
|
||||
return MediaSession.ConnectionResult.reject()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||
session.setCustomLayout(buildCustomButtons())
|
||||
super.onPostConnect(session, controller)
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
VideoPlaybackService.Companion.handleCommand(
|
||||
VideoPlaybackService.Companion.commandFromString(
|
||||
customCommand.customAction
|
||||
), session
|
||||
CustomMediaNotificationProvider.Companion.handleCommand(
|
||||
CustomMediaNotificationProvider.Companion.commandFromString(
|
||||
customCommand.customAction
|
||||
), session
|
||||
)
|
||||
|
||||
return super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
package com.twg.video.core.services.playback
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.media3.common.util.BitmapLoader
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.SimpleBitmapLoader
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import androidx.media3.session.MediaStyleNotificationHelper
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.ui.R
|
||||
import com.margelo.nitro.NitroModules
|
||||
import com.margelo.nitro.video.HybridVideoPlayer
|
||||
import okhttp3.internal.immutableListOf
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class VideoPlaybackServiceBinder(val service: VideoPlaybackService): Binder()
|
||||
|
||||
@@ -32,26 +24,12 @@ class VideoPlaybackServiceBinder(val service: VideoPlaybackService): Binder()
|
||||
class VideoPlaybackService : MediaSessionService() {
|
||||
private var mediaSessionsList = mutableMapOf<HybridVideoPlayer, MediaSession>()
|
||||
private var binder = VideoPlaybackServiceBinder(this)
|
||||
private var sourceActivity: Class<Activity>? = null
|
||||
private var placeholderCanceled = false
|
||||
private var sourceActivity: Class<Activity>? = null // retained for future deep-links; currently unused
|
||||
|
||||
// Controls for Android 13+ - see buildNotification function
|
||||
private val commandSeekForward = SessionCommand(COMMAND.SEEK_FORWARD.stringValue, Bundle.EMPTY)
|
||||
private val commandSeekBackward = SessionCommand(COMMAND.SEEK_BACKWARD.stringValue, Bundle.EMPTY)
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private val seekForwardBtn = CommandButton.Builder()
|
||||
.setDisplayName("forward")
|
||||
.setSessionCommand(commandSeekForward)
|
||||
.setIconResId(R.drawable.exo_notification_fastforward)
|
||||
.build()
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private val seekBackwardBtn = CommandButton.Builder()
|
||||
.setDisplayName("backward")
|
||||
.setSessionCommand(commandSeekBackward)
|
||||
.setIconResId(R.drawable.exo_notification_rewind)
|
||||
.build()
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
setMediaNotificationProvider(CustomMediaNotificationProvider(this))
|
||||
}
|
||||
|
||||
// Player Registry
|
||||
fun registerPlayer(player: HybridVideoPlayer, from: Class<Activity>) {
|
||||
@@ -60,27 +38,54 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
}
|
||||
sourceActivity = from
|
||||
|
||||
val mediaSession = MediaSession.Builder(this, player.player)
|
||||
val builder = MediaSession.Builder(this, player.player)
|
||||
.setId("RNVideoPlaybackService_" + player.hashCode())
|
||||
.setCallback(VideoPlaybackCallback())
|
||||
.setCustomLayout(immutableListOf(seekBackwardBtn, seekForwardBtn))
|
||||
.build()
|
||||
|
||||
// Ensure tapping the notification opens the app via sessionActivity
|
||||
try {
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
if (launchIntent != null) {
|
||||
launchIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
val contentIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
builder.setSessionActivity(contentIntent)
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
|
||||
|
||||
val mediaSession = builder.build()
|
||||
|
||||
mediaSessionsList[player] = mediaSession
|
||||
addSession(mediaSession)
|
||||
|
||||
// Manually trigger initial notification creation for the registered player
|
||||
// This ensures the player notification appears immediately, even if not playing
|
||||
onUpdateNotification(mediaSession, true)
|
||||
}
|
||||
|
||||
fun unregisterPlayer(player: HybridVideoPlayer) {
|
||||
hidePlayerNotification(player.player)
|
||||
val session = mediaSessionsList.remove(player)
|
||||
session?.release()
|
||||
if (mediaSessionsList.isEmpty()) {
|
||||
cleanup()
|
||||
stopSelf()
|
||||
stopIfNoPlayers()
|
||||
}
|
||||
|
||||
fun updatePlayerPreferences(player: HybridVideoPlayer) {
|
||||
val session = mediaSessionsList[player]
|
||||
if (session == null) {
|
||||
// If not registered but now needs it, register
|
||||
if (player.playInBackground || player.showNotificationControls) {
|
||||
val activity = try { NitroModules.applicationContext?.currentActivity } catch (_: Exception) { null }
|
||||
if (activity != null) registerPlayer(player, activity.javaClass)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If no longer needs registration, unregister and possibly stop service
|
||||
if (!player.playInBackground && !player.showNotificationControls) {
|
||||
unregisterPlayer(player)
|
||||
stopIfNoPlayers()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,235 +98,41 @@ class VideoPlaybackService : MediaSessionService() {
|
||||
return binder
|
||||
}
|
||||
|
||||
override fun onUpdateNotification(session: MediaSession, startInForegroundRequired: Boolean) {
|
||||
val notification = buildNotification(session)
|
||||
val notificationId = session.player.hashCode()
|
||||
val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
// Always cancel the placeholder notification once we have a real player notification
|
||||
if (!placeholderCanceled) {
|
||||
notificationManager.cancel(PLACEHOLDER_NOTIFICATION_ID)
|
||||
placeholderCanceled = true
|
||||
}
|
||||
|
||||
if (startInForegroundRequired) {
|
||||
startForeground(notificationId, notification)
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANEL_ID,
|
||||
NOTIFICATION_CHANEL_ID,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (session.player.currentMediaItem == null) {
|
||||
notificationManager.cancel(notificationId)
|
||||
return
|
||||
}
|
||||
|
||||
notificationManager.notify(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
stopForegroundSafely()
|
||||
cleanup()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
stopForegroundSafely()
|
||||
cleanup()
|
||||
val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.deleteNotificationChannel(NOTIFICATION_CHANEL_ID)
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun buildNotification(session: MediaSession): Notification {
|
||||
val returnToPlayer = Intent(this, sourceActivity ?: this.javaClass).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
|
||||
/*
|
||||
* On Android 13+ controls are automatically handled via media session
|
||||
* On Android 12 and bellow we need to add controls manually
|
||||
*/
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
.setStyle(MediaStyleNotificationHelper.MediaStyle(session))
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
.build()
|
||||
} else {
|
||||
val playerId = session.player.hashCode()
|
||||
|
||||
// Action for COMMAND.SEEK_BACKWARD
|
||||
val seekBackwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
|
||||
putExtra("PLAYER_ID", playerId)
|
||||
putExtra("ACTION", COMMAND.SEEK_BACKWARD.stringValue)
|
||||
}
|
||||
val seekBackwardPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
playerId * 10,
|
||||
seekBackwardIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// ACTION FOR COMMAND.TOGGLE_PLAY
|
||||
val togglePlayIntent = Intent(this, VideoPlaybackService::class.java).apply {
|
||||
putExtra("PLAYER_ID", playerId)
|
||||
putExtra("ACTION", COMMAND.TOGGLE_PLAY.stringValue)
|
||||
}
|
||||
val togglePlayPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
playerId * 10 + 1,
|
||||
togglePlayIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// ACTION FOR COMMAND.SEEK_FORWARD
|
||||
val seekForwardIntent = Intent(this, VideoPlaybackService::class.java).apply {
|
||||
putExtra("PLAYER_ID", playerId)
|
||||
putExtra("ACTION", COMMAND.SEEK_FORWARD.stringValue)
|
||||
}
|
||||
val seekForwardPendingIntent = PendingIntent.getService(
|
||||
this,
|
||||
playerId * 10 + 2,
|
||||
seekForwardIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
// Show controls on lock screen even when user hides sensitive content.
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
// Add media control buttons that invoke intents in your media service
|
||||
.addAction(androidx.media3.session.R.drawable.media3_icon_rewind, "Seek Backward", seekBackwardPendingIntent) // #0
|
||||
.addAction(
|
||||
if (session.player.isPlaying) {
|
||||
androidx.media3.session.R.drawable.media3_icon_pause
|
||||
} else {
|
||||
androidx.media3.session.R.drawable.media3_icon_play
|
||||
},
|
||||
"Toggle Play",
|
||||
togglePlayPendingIntent
|
||||
) // #1
|
||||
.addAction(androidx.media3.session.R.drawable.media3_icon_fast_forward, "Seek Forward", seekForwardPendingIntent) // #2
|
||||
// Apply the media style template
|
||||
.setStyle(MediaStyleNotificationHelper.MediaStyle(session).setShowActionsInCompactView(0, 1, 2))
|
||||
.setContentTitle(session.player.mediaMetadata.title)
|
||||
.setContentText(session.player.mediaMetadata.description)
|
||||
.setContentIntent(PendingIntent.getActivity(this, 0, returnToPlayer, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
|
||||
.setLargeIcon(session.player.mediaMetadata.artworkUri?.let { session.bitmapLoader.loadBitmap(it).get() })
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hidePlayerNotification(player: ExoPlayer) {
|
||||
val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(player.hashCode())
|
||||
}
|
||||
|
||||
private fun hideAllNotifications() {
|
||||
val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancelAll()
|
||||
private fun stopForegroundSafely() {
|
||||
try {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
hideAllNotifications()
|
||||
stopForegroundSafely()
|
||||
stopSelf()
|
||||
mediaSessionsList.forEach { (_, session) ->
|
||||
session.release()
|
||||
}
|
||||
mediaSessionsList.clear()
|
||||
placeholderCanceled = false
|
||||
}
|
||||
|
||||
private fun createPlaceholderNotification(): Notification {
|
||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANEL_ID,
|
||||
NOTIFICATION_CHANEL_ID,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
)
|
||||
// Stop the service if there are no active media sessions (no players need it)
|
||||
fun stopIfNoPlayers() {
|
||||
if (mediaSessionsList.isEmpty()) {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
return NotificationCompat.Builder(this, NOTIFICATION_CHANEL_ID)
|
||||
.setSmallIcon(androidx.media3.session.R.drawable.media3_icon_circular_play)
|
||||
.setContentTitle("Media playback")
|
||||
.setContentText("Preparing playback")
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !placeholderCanceled) {
|
||||
startForeground(PLACEHOLDER_NOTIFICATION_ID, createPlaceholderNotification())
|
||||
}
|
||||
|
||||
intent?.let {
|
||||
val playerId = it.getIntExtra("PLAYER_ID", -1)
|
||||
val actionCommand = it.getStringExtra("ACTION")
|
||||
|
||||
if (playerId < 0) {
|
||||
Log.w(TAG, "Received Command without playerId")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
if (actionCommand == null) {
|
||||
Log.w(TAG, "Received Command without action command")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
val session = mediaSessionsList.values.find { s -> s.player.hashCode() == playerId } ?: return super.onStartCommand(intent, flags, startId)
|
||||
|
||||
handleCommand(commandFromString(actionCommand), session)
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEEK_INTERVAL_MS = 10000L
|
||||
private const val TAG = "VideoPlaybackService"
|
||||
private const val PLACEHOLDER_NOTIFICATION_ID = 9999
|
||||
|
||||
const val NOTIFICATION_CHANEL_ID = "RNVIDEO_SESSION_NOTIFICATION"
|
||||
const val VIDEO_PLAYBACK_SERVICE_INTERFACE = SERVICE_INTERFACE
|
||||
|
||||
enum class COMMAND(val stringValue: String) {
|
||||
NONE("NONE"),
|
||||
SEEK_FORWARD("COMMAND_SEEK_FORWARD"),
|
||||
SEEK_BACKWARD("COMMAND_SEEK_BACKWARD"),
|
||||
TOGGLE_PLAY("COMMAND_TOGGLE_PLAY"),
|
||||
PLAY("COMMAND_PLAY"),
|
||||
PAUSE("COMMAND_PAUSE")
|
||||
}
|
||||
|
||||
fun commandFromString(value: String): COMMAND =
|
||||
when (value) {
|
||||
COMMAND.SEEK_FORWARD.stringValue -> COMMAND.SEEK_FORWARD
|
||||
COMMAND.SEEK_BACKWARD.stringValue -> COMMAND.SEEK_BACKWARD
|
||||
COMMAND.TOGGLE_PLAY.stringValue -> COMMAND.TOGGLE_PLAY
|
||||
COMMAND.PLAY.stringValue -> COMMAND.PLAY
|
||||
COMMAND.PAUSE.stringValue -> COMMAND.PAUSE
|
||||
else -> COMMAND.NONE
|
||||
}
|
||||
fun handleCommand(command: COMMAND, session: MediaSession) {
|
||||
// TODO: get somehow ControlsConfig here - for now hardcoded 10000ms
|
||||
|
||||
when (command) {
|
||||
COMMAND.SEEK_BACKWARD -> session.player.seekTo(session.player.contentPosition - SEEK_INTERVAL_MS)
|
||||
COMMAND.SEEK_FORWARD -> session.player.seekTo(session.player.contentPosition + SEEK_INTERVAL_MS)
|
||||
COMMAND.TOGGLE_PLAY -> handleCommand(if (session.player.isPlaying) COMMAND.PAUSE else COMMAND.PLAY, session)
|
||||
COMMAND.PLAY -> session.player.play()
|
||||
COMMAND.PAUSE -> session.player.pause()
|
||||
else -> Log.w(TAG, "Received COMMAND.NONE - was there an error?")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ class VideoPlaybackServiceConnection (private val player: WeakReference<HybridVi
|
||||
}
|
||||
|
||||
serviceBinder = binder as? VideoPlaybackServiceBinder
|
||||
serviceBinder?.service?.registerPlayer(player, activity.javaClass)
|
||||
// Only register when the player actually needs background service/notification
|
||||
if (player.playInBackground || player.showNotificationControls) {
|
||||
serviceBinder?.service?.registerPlayer(player, activity.javaClass)
|
||||
}
|
||||
} catch (err: Exception) {
|
||||
Log.e("VideoPlaybackServiceConnection", "Could not bind to playback service", err)
|
||||
}
|
||||
|
||||
@@ -102,6 +102,23 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
|
||||
field = value
|
||||
}
|
||||
|
||||
override var showNotificationControls: Boolean = false
|
||||
set(value) {
|
||||
val wasRunning = (field || playInBackground)
|
||||
val shouldRun = (value || playInBackground)
|
||||
|
||||
if (shouldRun && !wasRunning) {
|
||||
VideoPlaybackService.startService(context, videoPlaybackServiceConnection)
|
||||
}
|
||||
if (!shouldRun && wasRunning) {
|
||||
VideoPlaybackService.stopService(this, videoPlaybackServiceConnection)
|
||||
}
|
||||
|
||||
field = value
|
||||
// Inform service to refresh notification/session layout
|
||||
try { videoPlaybackServiceConnection.serviceBinder?.service?.updatePlayerPreferences(this) } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
// Player Properties
|
||||
override var currentTime: Double by mainThreadProperty(
|
||||
get = { player.currentPosition.toDouble() / 1000.0 },
|
||||
@@ -172,16 +189,18 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
|
||||
|
||||
override var playInBackground: Boolean = false
|
||||
set(value) {
|
||||
// playback in background was disabled and is now enabled
|
||||
if (value == true && field == false) {
|
||||
val shouldRun = (value || showNotificationControls)
|
||||
val wasRunning = (field || showNotificationControls)
|
||||
|
||||
if (shouldRun && !wasRunning) {
|
||||
VideoPlaybackService.startService(context, videoPlaybackServiceConnection)
|
||||
field = true
|
||||
}
|
||||
// playback in background was enabled and is now disabled
|
||||
else if (field == true) {
|
||||
if (!shouldRun && wasRunning) {
|
||||
VideoPlaybackService.stopService(this, videoPlaybackServiceConnection)
|
||||
field = false
|
||||
}
|
||||
field = value
|
||||
// Update preferences to refresh notifications/registration
|
||||
try { videoPlaybackServiceConnection.serviceBinder?.service?.updatePlayerPreferences(this) } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
override var playWhenInactive: Boolean = false
|
||||
@@ -323,7 +342,7 @@ class HybridVideoPlayer() : HybridVideoPlayerSpec() {
|
||||
}
|
||||
|
||||
private fun release() {
|
||||
if (playInBackground) {
|
||||
if (playInBackground || showNotificationControls) {
|
||||
VideoPlaybackService.stopService(this, videoPlaybackServiceConnection)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,16 @@ import com.facebook.proguard.annotations.DoNotStrip
|
||||
@DoNotStrip
|
||||
class HybridVideoPlayerSourceFactory: HybridVideoPlayerSourceFactorySpec() {
|
||||
override fun fromUri(uri: String): HybridVideoPlayerSourceSpec {
|
||||
val config = NativeVideoConfig(uri, null, null, null, null, true)
|
||||
val config = NativeVideoConfig(
|
||||
uri = uri,
|
||||
externalSubtitles = null,
|
||||
drm = null,
|
||||
headers = null,
|
||||
bufferConfig = null,
|
||||
metadata = null,
|
||||
initializeOnCreation = true
|
||||
)
|
||||
|
||||
return HybridVideoPlayerSource(config)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
class NowPlayingInfoCenterManager {
|
||||
static let shared = NowPlayingInfoCenterManager()
|
||||
|
||||
private let SEEK_INTERVAL_SECONDS: Double = 10
|
||||
|
||||
private weak var currentPlayer: AVPlayer?
|
||||
private var players = NSHashTable<AVPlayer>.weakObjects()
|
||||
|
||||
private var observers: [Int: NSKeyValueObservation] = [:]
|
||||
private var playbackObserver: Any?
|
||||
|
||||
private var playTarget: Any?
|
||||
private var pauseTarget: Any?
|
||||
private var skipForwardTarget: Any?
|
||||
private var skipBackwardTarget: Any?
|
||||
private var playbackPositionTarget: Any?
|
||||
private var seekTarget: Any?
|
||||
private var togglePlayPauseTarget: Any?
|
||||
|
||||
private let remoteCommandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
var receivingRemoteControlEvents = false {
|
||||
didSet {
|
||||
if receivingRemoteControlEvents {
|
||||
DispatchQueue.main.async {
|
||||
VideoManager.shared.setRemoteControlEventsActive(true)
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||
VideoManager.shared.setRemoteControlEventsActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
func registerPlayer(player: AVPlayer) {
|
||||
if players.contains(player) {
|
||||
return
|
||||
}
|
||||
|
||||
if receivingRemoteControlEvents == false {
|
||||
receivingRemoteControlEvents = true
|
||||
}
|
||||
|
||||
if let oldObserver = observers[player.hashValue] {
|
||||
oldObserver.invalidate()
|
||||
}
|
||||
|
||||
observers[player.hashValue] = observePlayers(player: player)
|
||||
players.add(player)
|
||||
|
||||
if currentPlayer == nil {
|
||||
setCurrentPlayer(player: player)
|
||||
}
|
||||
}
|
||||
|
||||
func removePlayer(player: AVPlayer) {
|
||||
if !players.contains(player) {
|
||||
return
|
||||
}
|
||||
|
||||
if let observer = observers[player.hashValue] {
|
||||
observer.invalidate()
|
||||
}
|
||||
|
||||
observers.removeValue(forKey: player.hashValue)
|
||||
players.remove(player)
|
||||
|
||||
if currentPlayer == player {
|
||||
currentPlayer = nil
|
||||
updateNowPlayingInfo()
|
||||
}
|
||||
|
||||
if players.allObjects.isEmpty {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
public func cleanup() {
|
||||
observers.removeAll()
|
||||
players.removeAllObjects()
|
||||
|
||||
if let playbackObserver {
|
||||
currentPlayer?.removeTimeObserver(playbackObserver)
|
||||
}
|
||||
|
||||
invalidateCommandTargets()
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
receivingRemoteControlEvents = false
|
||||
}
|
||||
|
||||
private func setCurrentPlayer(player: AVPlayer) {
|
||||
if player == currentPlayer {
|
||||
return
|
||||
}
|
||||
|
||||
if let playbackObserver {
|
||||
currentPlayer?.removeTimeObserver(playbackObserver)
|
||||
}
|
||||
|
||||
currentPlayer = player
|
||||
registerCommandTargets()
|
||||
|
||||
updateNowPlayingInfo()
|
||||
playbackObserver = player.addPeriodicTimeObserver(
|
||||
forInterval: CMTime(value: 1, timescale: 4),
|
||||
queue: .global(),
|
||||
using: { [weak self] _ in
|
||||
self?.updateNowPlayingInfo()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func registerCommandTargets() {
|
||||
invalidateCommandTargets()
|
||||
|
||||
playTarget = remoteCommandCenter.playCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if player.rate == 0 {
|
||||
player.play()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
pauseTarget = remoteCommandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if player.rate != 0 {
|
||||
player.pause()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
skipBackwardTarget = remoteCommandCenter.skipBackwardCommand.addTarget {
|
||||
[weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
let newTime =
|
||||
player.currentTime()
|
||||
- CMTime(seconds: self.SEEK_INTERVAL_SECONDS, preferredTimescale: .max)
|
||||
player.seek(to: newTime)
|
||||
return .success
|
||||
}
|
||||
|
||||
skipForwardTarget = remoteCommandCenter.skipForwardCommand.addTarget {
|
||||
[weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
let newTime =
|
||||
player.currentTime()
|
||||
+ CMTime(seconds: self.SEEK_INTERVAL_SECONDS, preferredTimescale: .max)
|
||||
player.seek(to: newTime)
|
||||
return .success
|
||||
}
|
||||
|
||||
playbackPositionTarget = remoteCommandCenter.changePlaybackPositionCommand
|
||||
.addTarget { [weak self] event in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent {
|
||||
player.seek(
|
||||
to: CMTime(seconds: event.positionTime, preferredTimescale: .max)
|
||||
)
|
||||
return .success
|
||||
}
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
// Handler for togglePlayPauseCommand, sent by Apple's Earpods wired headphones
|
||||
togglePlayPauseTarget = remoteCommandCenter.togglePlayPauseCommand.addTarget
|
||||
{ [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if player.rate == 0 {
|
||||
player.play()
|
||||
} else {
|
||||
player.pause()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidateCommandTargets() {
|
||||
remoteCommandCenter.playCommand.removeTarget(playTarget)
|
||||
remoteCommandCenter.pauseCommand.removeTarget(pauseTarget)
|
||||
remoteCommandCenter.skipForwardCommand.removeTarget(skipForwardTarget)
|
||||
remoteCommandCenter.skipBackwardCommand.removeTarget(skipBackwardTarget)
|
||||
remoteCommandCenter.changePlaybackPositionCommand.removeTarget(
|
||||
playbackPositionTarget
|
||||
)
|
||||
remoteCommandCenter.togglePlayPauseCommand.removeTarget(
|
||||
togglePlayPauseTarget
|
||||
)
|
||||
}
|
||||
|
||||
public func updateNowPlayingInfo() {
|
||||
guard let player = currentPlayer, let currentItem = player.currentItem
|
||||
else {
|
||||
invalidateCommandTargets()
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
return
|
||||
}
|
||||
|
||||
// commonMetadata is metadata from asset, externalMetadata is custom metadata set by user
|
||||
// externalMetadata should override commonMetadata to allow override metadata from source
|
||||
// When the metadata has the tag "iTunSMPB" or "iTunNORM" then the metadata is not converted correctly and comes [nil, nil, ...]
|
||||
// This leads to a crash of the app
|
||||
let metadata: [AVMetadataItem] = {
|
||||
|
||||
let common = processMetadataItems(currentItem.asset.commonMetadata)
|
||||
let external = processMetadataItems(currentItem.externalMetadata)
|
||||
|
||||
return Array(common.merging(external) { _, new in new }.values)
|
||||
}()
|
||||
|
||||
let titleItem =
|
||||
AVMetadataItem.metadataItems(
|
||||
from: metadata,
|
||||
filteredByIdentifier: .commonIdentifierTitle
|
||||
).first?.stringValue ?? ""
|
||||
|
||||
let artistItem =
|
||||
AVMetadataItem.metadataItems(
|
||||
from: metadata,
|
||||
filteredByIdentifier: .commonIdentifierArtist
|
||||
).first?.stringValue ?? ""
|
||||
|
||||
// I have some issue with this - setting artworkItem when it not set dont return nil but also is crashing application
|
||||
// this is very hacky workaround for it
|
||||
let imgData = AVMetadataItem.metadataItems(
|
||||
from: metadata,
|
||||
filteredByIdentifier: .commonIdentifierArtwork
|
||||
).first?.dataValue
|
||||
let image = imgData.flatMap { UIImage(data: $0) } ?? UIImage()
|
||||
let artworkItem = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
||||
|
||||
let newNowPlayingInfo: [String: Any] = [
|
||||
MPMediaItemPropertyTitle: titleItem,
|
||||
MPMediaItemPropertyArtist: artistItem,
|
||||
MPMediaItemPropertyArtwork: artworkItem,
|
||||
MPMediaItemPropertyPlaybackDuration: currentItem.duration.seconds,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentItem.currentTime()
|
||||
.seconds.rounded(),
|
||||
MPNowPlayingInfoPropertyPlaybackRate: player.rate,
|
||||
MPNowPlayingInfoPropertyIsLiveStream: CMTIME_IS_INDEFINITE(
|
||||
currentItem.asset.duration
|
||||
),
|
||||
]
|
||||
let currentNowPlayingInfo =
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:]
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo =
|
||||
currentNowPlayingInfo.merging(newNowPlayingInfo) { _, new in new }
|
||||
}
|
||||
|
||||
private func findNewCurrentPlayer() {
|
||||
if let newPlayer = players.allObjects.first(where: {
|
||||
$0.rate != 0
|
||||
}) {
|
||||
setCurrentPlayer(player: newPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
// We will observe players rate to find last active player that info will be displayed
|
||||
private func observePlayers(player: AVPlayer) -> NSKeyValueObservation {
|
||||
return player.observe(\.rate) { [weak self] player, change in
|
||||
guard let self else { return }
|
||||
|
||||
let rate = change.newValue
|
||||
|
||||
// case where there is new player that is not paused
|
||||
// In this case event is triggered by non currentPlayer
|
||||
if rate != 0 && self.currentPlayer != player {
|
||||
self.setCurrentPlayer(player: player)
|
||||
return
|
||||
}
|
||||
|
||||
// case where currentPlayer was paused
|
||||
// In this case event is triggered by currentPlayer
|
||||
if rate == 0 && self.currentPlayer == player {
|
||||
self.findNewCurrentPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func processMetadataItems(_ items: [AVMetadataItem]) -> [String:
|
||||
AVMetadataItem]
|
||||
{
|
||||
var result = [String: AVMetadataItem]()
|
||||
|
||||
for item in items {
|
||||
if let id = item.identifier?.rawValue, !id.isEmpty, result[id] == nil {
|
||||
result[id] = item
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,16 @@ class VideoManager {
|
||||
updateAudioSessionConfiguration()
|
||||
}
|
||||
|
||||
// MARK: - Remote Control Events
|
||||
func setRemoteControlEventsActive(_ active: Bool) {
|
||||
if isAudioSessionManagementDisabled || remoteControlEventsActive == active {
|
||||
return
|
||||
}
|
||||
|
||||
remoteControlEventsActive = active
|
||||
requestAudioSessionUpdate()
|
||||
}
|
||||
|
||||
// MARK: - Audio Session Management
|
||||
private func activateAudioSession() {
|
||||
if isAudioSessionActive {
|
||||
@@ -133,7 +143,11 @@ class VideoManager {
|
||||
player.mixAudioMode == .donotmix
|
||||
}
|
||||
|
||||
if isAnyPlayerPlaying || anyPlayerNeedsNotMixWithOthers {
|
||||
let anyPlayerNeedsNotificationControls = players.allObjects.contains { player in
|
||||
player.showNotificationControls
|
||||
}
|
||||
|
||||
if isAnyPlayerPlaying || anyPlayerNeedsNotMixWithOthers || anyPlayerNeedsNotificationControls || remoteControlEventsActive {
|
||||
activateAudioSession()
|
||||
} else {
|
||||
deactivateAudioSession()
|
||||
@@ -162,6 +176,10 @@ class VideoManager {
|
||||
player.playInBackground
|
||||
}
|
||||
|
||||
let anyPlayerNeedsNotificationControls = players.allObjects.contains { player in
|
||||
player.showNotificationControls
|
||||
}
|
||||
|
||||
if isAudioSessionManagementDisabled {
|
||||
return
|
||||
}
|
||||
@@ -172,7 +190,7 @@ class VideoManager {
|
||||
earpiece: false, // TODO: Pass actual value after we add prop
|
||||
pip: anyViewNeedsPictureInPicture,
|
||||
backgroundPlayback: anyPlayerNeedsBackgroundPlayback,
|
||||
notificationControls: false // TODO: Pass actual value after we add prop
|
||||
notificationControls: anyPlayerNeedsNotificationControls
|
||||
)
|
||||
|
||||
let audioMixingMode = determineAudioMixingMode()
|
||||
|
||||
@@ -22,6 +22,7 @@ extension HybridVideoPlayer: VideoPlayerObserverDelegate {
|
||||
|
||||
func onRateChanged(rate: Float) {
|
||||
eventEmitter.onPlaybackRateChange(Double(rate))
|
||||
NowPlayingInfoCenterManager.shared.updateNowPlayingInfo()
|
||||
updateAndEmitPlaybackState()
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,16 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
|
||||
return player.rate != 0
|
||||
}
|
||||
|
||||
var showNotificationControls: Bool = false {
|
||||
didSet {
|
||||
if showNotificationControls {
|
||||
NowPlayingInfoCenterManager.shared.registerPlayer(player: player)
|
||||
} else {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initialize() throws -> Promise<Void> {
|
||||
return Promise.async { [weak self] in
|
||||
guard let self else {
|
||||
@@ -174,6 +184,7 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
|
||||
}
|
||||
|
||||
func release() {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
self.player.replaceCurrentItem(with: nil)
|
||||
self.playerItem = nil
|
||||
|
||||
@@ -270,6 +281,7 @@ class HybridVideoPlayer: HybridVideoPlayerSpec, NativeVideoPlayerSpec {
|
||||
self.source = source
|
||||
self.playerItem = try await self.initializePlayerItem()
|
||||
self.player.replaceCurrentItem(with: self.playerItem)
|
||||
NowPlayingInfoCenterManager.shared.updateNowPlayingInfo()
|
||||
promise.resolve(withResult: ())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,22 @@
|
||||
import Foundation
|
||||
|
||||
class HybridVideoPlayerSourceFactory: HybridVideoPlayerSourceFactorySpec {
|
||||
func fromVideoConfig(config: NativeVideoConfig) throws -> any HybridVideoPlayerSourceSpec {
|
||||
func fromVideoConfig(config: NativeVideoConfig) throws
|
||||
-> any HybridVideoPlayerSourceSpec
|
||||
{
|
||||
return try HybridVideoPlayerSource(config: config)
|
||||
}
|
||||
|
||||
|
||||
func fromUri(uri: String) throws -> HybridVideoPlayerSourceSpec {
|
||||
let config = NativeVideoConfig(uri: uri, externalSubtitles: nil, drm: nil, headers: nil, bufferConfig: nil, initializeOnCreation: true)
|
||||
let config = NativeVideoConfig(
|
||||
uri: uri,
|
||||
externalSubtitles: nil,
|
||||
drm: nil,
|
||||
headers: nil,
|
||||
bufferConfig: nil,
|
||||
metadata: nil,
|
||||
initializeOnCreation: true
|
||||
)
|
||||
return try HybridVideoPlayerSource(config: config)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,9 @@ import AVKit
|
||||
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
controller.view.backgroundColor = .clear
|
||||
|
||||
// We manage this manually in NowPlayingInfoCenterManager
|
||||
controller.updatesNowPlayingInfoCenter = false
|
||||
|
||||
if #available(iOS 16.0, *) {
|
||||
if let initialSpeed = controller.speeds.first(where: { $0.rate == player.rate }) {
|
||||
controller.selectSpeed(initialSpeed)
|
||||
|
||||
70
packages/react-native-video/nitrogen/generated/android/c++/JCustomVideoMetadata.hpp
generated
Normal file
70
packages/react-native-video/nitrogen/generated/android/c++/JCustomVideoMetadata.hpp
generated
Normal file
@@ -0,0 +1,70 @@
|
||||
///
|
||||
/// JCustomVideoMetadata.hpp
|
||||
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
||||
/// https://github.com/mrousavy/nitro
|
||||
/// Copyright © 2025 Marc Rousavy @ Margelo
|
||||
///
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <fbjni/fbjni.h>
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace margelo::nitro::video {
|
||||
|
||||
using namespace facebook;
|
||||
|
||||
/**
|
||||
* The C++ JNI bridge between the C++ struct "CustomVideoMetadata" and the the Kotlin data class "CustomVideoMetadata".
|
||||
*/
|
||||
struct JCustomVideoMetadata final: public jni::JavaClass<JCustomVideoMetadata> {
|
||||
public:
|
||||
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/video/CustomVideoMetadata;";
|
||||
|
||||
public:
|
||||
/**
|
||||
* Convert this Java/Kotlin-based struct to the C++ struct CustomVideoMetadata by copying all values to C++.
|
||||
*/
|
||||
[[maybe_unused]]
|
||||
[[nodiscard]]
|
||||
CustomVideoMetadata toCpp() const {
|
||||
static const auto clazz = javaClassStatic();
|
||||
static const auto fieldTitle = clazz->getField<jni::JString>("title");
|
||||
jni::local_ref<jni::JString> title = this->getFieldValue(fieldTitle);
|
||||
static const auto fieldSubtitle = clazz->getField<jni::JString>("subtitle");
|
||||
jni::local_ref<jni::JString> subtitle = this->getFieldValue(fieldSubtitle);
|
||||
static const auto fieldDescription = clazz->getField<jni::JString>("description");
|
||||
jni::local_ref<jni::JString> description = this->getFieldValue(fieldDescription);
|
||||
static const auto fieldArtist = clazz->getField<jni::JString>("artist");
|
||||
jni::local_ref<jni::JString> artist = this->getFieldValue(fieldArtist);
|
||||
static const auto fieldImageUri = clazz->getField<jni::JString>("imageUri");
|
||||
jni::local_ref<jni::JString> imageUri = this->getFieldValue(fieldImageUri);
|
||||
return CustomVideoMetadata(
|
||||
title != nullptr ? std::make_optional(title->toStdString()) : std::nullopt,
|
||||
subtitle != nullptr ? std::make_optional(subtitle->toStdString()) : std::nullopt,
|
||||
description != nullptr ? std::make_optional(description->toStdString()) : std::nullopt,
|
||||
artist != nullptr ? std::make_optional(artist->toStdString()) : std::nullopt,
|
||||
imageUri != nullptr ? std::make_optional(imageUri->toStdString()) : std::nullopt
|
||||
);
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Create a Java/Kotlin-based struct by copying all values from the given C++ struct to Java.
|
||||
*/
|
||||
[[maybe_unused]]
|
||||
static jni::local_ref<JCustomVideoMetadata::javaobject> fromCpp(const CustomVideoMetadata& value) {
|
||||
return newInstance(
|
||||
value.title.has_value() ? jni::make_jstring(value.title.value()) : nullptr,
|
||||
value.subtitle.has_value() ? jni::make_jstring(value.subtitle.value()) : nullptr,
|
||||
value.description.has_value() ? jni::make_jstring(value.description.value()) : nullptr,
|
||||
value.artist.has_value() ? jni::make_jstring(value.artist.value()) : nullptr,
|
||||
value.imageUri.has_value() ? jni::make_jstring(value.imageUri.value()) : nullptr
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace margelo::nitro::video
|
||||
@@ -25,6 +25,8 @@ namespace margelo::nitro::video { struct BufferConfig; }
|
||||
namespace margelo::nitro::video { struct LivePlaybackParams; }
|
||||
// Forward declaration of `Resolution` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct Resolution; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
|
||||
#include <memory>
|
||||
#include "HybridVideoPlayerSourceSpec.hpp"
|
||||
@@ -53,6 +55,8 @@ namespace margelo::nitro::video { struct Resolution; }
|
||||
#include "JLivePlaybackParams.hpp"
|
||||
#include "Resolution.hpp"
|
||||
#include "JResolution.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
#include "JCustomVideoMetadata.hpp"
|
||||
|
||||
namespace margelo::nitro::video {
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace margelo::nitro::video { struct BufferConfig; }
|
||||
namespace margelo::nitro::video { struct LivePlaybackParams; }
|
||||
// Forward declaration of `Resolution` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct Resolution; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
// Forward declaration of `VideoInformation` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct VideoInformation; }
|
||||
// Forward declaration of `VideoOrientation` to properly resolve imports.
|
||||
@@ -52,6 +54,8 @@ namespace margelo::nitro::video { enum class VideoOrientation; }
|
||||
#include "JLivePlaybackParams.hpp"
|
||||
#include "Resolution.hpp"
|
||||
#include "JResolution.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
#include "JCustomVideoMetadata.hpp"
|
||||
#include "VideoInformation.hpp"
|
||||
#include "JVideoInformation.hpp"
|
||||
#include "VideoOrientation.hpp"
|
||||
|
||||
@@ -72,6 +72,15 @@ namespace margelo::nitro::video {
|
||||
auto __result = method(_javaPart);
|
||||
return __result->cthis()->shared_cast<JHybridVideoPlayerEventEmitterSpec>();
|
||||
}
|
||||
bool JHybridVideoPlayerSpec::getShowNotificationControls() {
|
||||
static const auto method = javaClassStatic()->getMethod<jboolean()>("getShowNotificationControls");
|
||||
auto __result = method(_javaPart);
|
||||
return static_cast<bool>(__result);
|
||||
}
|
||||
void JHybridVideoPlayerSpec::setShowNotificationControls(bool showNotificationControls) {
|
||||
static const auto method = javaClassStatic()->getMethod<void(jboolean /* showNotificationControls */)>("setShowNotificationControls");
|
||||
method(_javaPart, showNotificationControls);
|
||||
}
|
||||
VideoPlayerStatus JHybridVideoPlayerSpec::getStatus() {
|
||||
static const auto method = javaClassStatic()->getMethod<jni::local_ref<JVideoPlayerStatus>()>("getStatus");
|
||||
auto __result = method(_javaPart);
|
||||
|
||||
@@ -51,6 +51,8 @@ namespace margelo::nitro::video {
|
||||
// Properties
|
||||
std::shared_ptr<HybridVideoPlayerSourceSpec> getSource() override;
|
||||
std::shared_ptr<HybridVideoPlayerEventEmitterSpec> getEventEmitter() override;
|
||||
bool getShowNotificationControls() override;
|
||||
void setShowNotificationControls(bool showNotificationControls) override;
|
||||
VideoPlayerStatus getStatus() override;
|
||||
double getDuration() override;
|
||||
double getVolume() override;
|
||||
|
||||
@@ -11,7 +11,9 @@
|
||||
#include "NativeVideoConfig.hpp"
|
||||
|
||||
#include "BufferConfig.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
#include "JBufferConfig.hpp"
|
||||
#include "JCustomVideoMetadata.hpp"
|
||||
#include "JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__string_____OnGetLicensePayload.hpp"
|
||||
#include "JLivePlaybackParams.hpp"
|
||||
#include "JNativeDrmParams.hpp"
|
||||
@@ -62,6 +64,8 @@ namespace margelo::nitro::video {
|
||||
jni::local_ref<jni::JMap<jni::JString, jni::JString>> headers = this->getFieldValue(fieldHeaders);
|
||||
static const auto fieldBufferConfig = clazz->getField<JBufferConfig>("bufferConfig");
|
||||
jni::local_ref<JBufferConfig> bufferConfig = this->getFieldValue(fieldBufferConfig);
|
||||
static const auto fieldMetadata = clazz->getField<JCustomVideoMetadata>("metadata");
|
||||
jni::local_ref<JCustomVideoMetadata> metadata = this->getFieldValue(fieldMetadata);
|
||||
static const auto fieldInitializeOnCreation = clazz->getField<jni::JBoolean>("initializeOnCreation");
|
||||
jni::local_ref<jni::JBoolean> initializeOnCreation = this->getFieldValue(fieldInitializeOnCreation);
|
||||
return NativeVideoConfig(
|
||||
@@ -86,6 +90,7 @@ namespace margelo::nitro::video {
|
||||
return __map;
|
||||
}()) : std::nullopt,
|
||||
bufferConfig != nullptr ? std::make_optional(bufferConfig->toCpp()) : std::nullopt,
|
||||
metadata != nullptr ? std::make_optional(metadata->toCpp()) : std::nullopt,
|
||||
initializeOnCreation != nullptr ? std::make_optional(static_cast<bool>(initializeOnCreation->value())) : std::nullopt
|
||||
);
|
||||
}
|
||||
@@ -116,6 +121,7 @@ namespace margelo::nitro::video {
|
||||
return __map;
|
||||
}() : nullptr,
|
||||
value.bufferConfig.has_value() ? JBufferConfig::fromCpp(value.bufferConfig.value()) : nullptr,
|
||||
value.metadata.has_value() ? JCustomVideoMetadata::fromCpp(value.metadata.value()) : nullptr,
|
||||
value.initializeOnCreation.has_value() ? jni::JBoolean::valueOf(value.initializeOnCreation.value()) : nullptr
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
///
|
||||
/// CustomVideoMetadata.kt
|
||||
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
||||
/// https://github.com/mrousavy/nitro
|
||||
/// Copyright © 2025 Marc Rousavy @ Margelo
|
||||
///
|
||||
|
||||
package com.margelo.nitro.video
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.facebook.proguard.annotations.DoNotStrip
|
||||
import com.margelo.nitro.core.*
|
||||
|
||||
|
||||
/**
|
||||
* Represents the JavaScript object/struct "CustomVideoMetadata".
|
||||
*/
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
data class CustomVideoMetadata
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
constructor(
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val title: String?,
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val subtitle: String?,
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val description: String?,
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val artist: String?,
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val imageUri: String?
|
||||
) {
|
||||
/* main constructor */
|
||||
}
|
||||
@@ -45,6 +45,12 @@ abstract class HybridVideoPlayerSpec: HybridObject() {
|
||||
@get:Keep
|
||||
abstract val eventEmitter: HybridVideoPlayerEventEmitterSpec
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
@set:DoNotStrip
|
||||
@set:Keep
|
||||
abstract var showNotificationControls: Boolean
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
abstract val status: VideoPlayerStatus
|
||||
|
||||
@@ -38,6 +38,9 @@ data class NativeVideoConfig
|
||||
val bufferConfig: BufferConfig?,
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val metadata: CustomVideoMetadata?,
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
val initializeOnCreation: Boolean?
|
||||
) {
|
||||
/* main constructor */
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
namespace margelo::nitro::video { struct BandwidthData; }
|
||||
// Forward declaration of `BufferConfig` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct BufferConfig; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
// Forward declaration of `HybridVideoPlayerEventEmitterSpec` to properly resolve imports.
|
||||
namespace margelo::nitro::video { class HybridVideoPlayerEventEmitterSpec; }
|
||||
// Forward declaration of `HybridVideoPlayerFactorySpec` to properly resolve imports.
|
||||
@@ -82,6 +84,7 @@ namespace ReactNativeVideo { class HybridVideoViewViewManagerSpec_cxx; }
|
||||
// Include C++ defined types
|
||||
#include "BandwidthData.hpp"
|
||||
#include "BufferConfig.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
#include "HybridVideoPlayerEventEmitterSpec.hpp"
|
||||
#include "HybridVideoPlayerFactorySpec.hpp"
|
||||
#include "HybridVideoPlayerSourceFactorySpec.hpp"
|
||||
@@ -872,6 +875,15 @@ namespace margelo::nitro::video::bridge::swift {
|
||||
return *optional;
|
||||
}
|
||||
|
||||
// pragma MARK: std::optional<CustomVideoMetadata>
|
||||
/**
|
||||
* Specialized version of `std::optional<CustomVideoMetadata>`.
|
||||
*/
|
||||
using std__optional_CustomVideoMetadata_ = std::optional<CustomVideoMetadata>;
|
||||
inline std::optional<CustomVideoMetadata> create_std__optional_CustomVideoMetadata_(const CustomVideoMetadata& value) {
|
||||
return std::optional<CustomVideoMetadata>(value);
|
||||
}
|
||||
|
||||
// pragma MARK: std::shared_ptr<Promise<VideoInformation>>
|
||||
/**
|
||||
* Specialized version of `std::shared_ptr<Promise<VideoInformation>>`.
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
namespace margelo::nitro::video { struct BandwidthData; }
|
||||
// Forward declaration of `BufferConfig` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct BufferConfig; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
// Forward declaration of `HybridVideoPlayerEventEmitterSpec` to properly resolve imports.
|
||||
namespace margelo::nitro::video { class HybridVideoPlayerEventEmitterSpec; }
|
||||
// Forward declaration of `HybridVideoPlayerFactorySpec` to properly resolve imports.
|
||||
@@ -76,6 +78,7 @@ namespace margelo::nitro::video { struct onVolumeChangeData; }
|
||||
// Include C++ defined types
|
||||
#include "BandwidthData.hpp"
|
||||
#include "BufferConfig.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
#include "HybridVideoPlayerEventEmitterSpec.hpp"
|
||||
#include "HybridVideoPlayerFactorySpec.hpp"
|
||||
#include "HybridVideoPlayerSourceFactorySpec.hpp"
|
||||
|
||||
@@ -30,6 +30,8 @@ namespace margelo::nitro::video { struct BufferConfig; }
|
||||
namespace margelo::nitro::video { struct LivePlaybackParams; }
|
||||
// Forward declaration of `Resolution` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct Resolution; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
|
||||
#include <memory>
|
||||
#include "HybridVideoPlayerSourceSpec.hpp"
|
||||
@@ -47,6 +49,7 @@ namespace margelo::nitro::video { struct Resolution; }
|
||||
#include "BufferConfig.hpp"
|
||||
#include "LivePlaybackParams.hpp"
|
||||
#include "Resolution.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
|
||||
#include "ReactNativeVideo-Swift-Cxx-Umbrella.hpp"
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace margelo::nitro::video { struct BufferConfig; }
|
||||
namespace margelo::nitro::video { struct LivePlaybackParams; }
|
||||
// Forward declaration of `Resolution` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct Resolution; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
// Forward declaration of `VideoInformation` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct VideoInformation; }
|
||||
// Forward declaration of `VideoOrientation` to properly resolve imports.
|
||||
@@ -47,6 +49,7 @@ namespace margelo::nitro::video { enum class VideoOrientation; }
|
||||
#include "BufferConfig.hpp"
|
||||
#include "LivePlaybackParams.hpp"
|
||||
#include "Resolution.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
#include "VideoInformation.hpp"
|
||||
#include "VideoOrientation.hpp"
|
||||
|
||||
|
||||
@@ -82,6 +82,12 @@ namespace margelo::nitro::video {
|
||||
auto __result = _swiftPart.getEventEmitter();
|
||||
return __result;
|
||||
}
|
||||
inline bool getShowNotificationControls() noexcept override {
|
||||
return _swiftPart.getShowNotificationControls();
|
||||
}
|
||||
inline void setShowNotificationControls(bool showNotificationControls) noexcept override {
|
||||
_swiftPart.setShowNotificationControls(std::forward<decltype(showNotificationControls)>(showNotificationControls));
|
||||
}
|
||||
inline VideoPlayerStatus getStatus() noexcept override {
|
||||
auto __result = _swiftPart.getStatus();
|
||||
return static_cast<VideoPlayerStatus>(__result);
|
||||
|
||||
169
packages/react-native-video/nitrogen/generated/ios/swift/CustomVideoMetadata.swift
generated
Normal file
169
packages/react-native-video/nitrogen/generated/ios/swift/CustomVideoMetadata.swift
generated
Normal file
@@ -0,0 +1,169 @@
|
||||
///
|
||||
/// CustomVideoMetadata.swift
|
||||
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
||||
/// https://github.com/mrousavy/nitro
|
||||
/// Copyright © 2025 Marc Rousavy @ Margelo
|
||||
///
|
||||
|
||||
import NitroModules
|
||||
|
||||
/**
|
||||
* Represents an instance of `CustomVideoMetadata`, backed by a C++ struct.
|
||||
*/
|
||||
public typealias CustomVideoMetadata = margelo.nitro.video.CustomVideoMetadata
|
||||
|
||||
public extension CustomVideoMetadata {
|
||||
private typealias bridge = margelo.nitro.video.bridge.swift
|
||||
|
||||
/**
|
||||
* Create a new instance of `CustomVideoMetadata`.
|
||||
*/
|
||||
init(title: String?, subtitle: String?, description: String?, artist: String?, imageUri: String?) {
|
||||
self.init({ () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = title {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}(), { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = subtitle {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}(), { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = description {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}(), { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = artist {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}(), { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = imageUri {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}())
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
@inline(__always)
|
||||
get {
|
||||
return { () -> String? in
|
||||
if let __unwrapped = self.__title.value {
|
||||
return String(__unwrapped)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__title = { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = newValue {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var subtitle: String? {
|
||||
@inline(__always)
|
||||
get {
|
||||
return { () -> String? in
|
||||
if let __unwrapped = self.__subtitle.value {
|
||||
return String(__unwrapped)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__subtitle = { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = newValue {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var description: String? {
|
||||
@inline(__always)
|
||||
get {
|
||||
return { () -> String? in
|
||||
if let __unwrapped = self.__description.value {
|
||||
return String(__unwrapped)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__description = { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = newValue {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var artist: String? {
|
||||
@inline(__always)
|
||||
get {
|
||||
return { () -> String? in
|
||||
if let __unwrapped = self.__artist.value {
|
||||
return String(__unwrapped)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__artist = { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = newValue {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var imageUri: String? {
|
||||
@inline(__always)
|
||||
get {
|
||||
return { () -> String? in
|
||||
if let __unwrapped = self.__imageUri.value {
|
||||
return String(__unwrapped)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__imageUri = { () -> bridge.std__optional_std__string_ in
|
||||
if let __unwrappedValue = newValue {
|
||||
return bridge.create_std__optional_std__string_(std.string(__unwrappedValue))
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public protocol HybridVideoPlayerSpec_protocol: HybridObject {
|
||||
// Properties
|
||||
var source: (any HybridVideoPlayerSourceSpec) { get }
|
||||
var eventEmitter: (any HybridVideoPlayerEventEmitterSpec) { get }
|
||||
var showNotificationControls: Bool { get set }
|
||||
var status: VideoPlayerStatus { get }
|
||||
var duration: Double { get }
|
||||
var volume: Double { get set }
|
||||
|
||||
@@ -126,6 +126,17 @@ open class HybridVideoPlayerSpec_cxx {
|
||||
}
|
||||
}
|
||||
|
||||
public final var showNotificationControls: Bool {
|
||||
@inline(__always)
|
||||
get {
|
||||
return self.__implementation.showNotificationControls
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__implementation.showNotificationControls = newValue
|
||||
}
|
||||
}
|
||||
|
||||
public final var status: Int32 {
|
||||
@inline(__always)
|
||||
get {
|
||||
|
||||
@@ -18,7 +18,7 @@ public extension NativeVideoConfig {
|
||||
/**
|
||||
* Create a new instance of `NativeVideoConfig`.
|
||||
*/
|
||||
init(uri: String, externalSubtitles: [NativeExternalSubtitle]?, drm: NativeDrmParams?, headers: Dictionary<String, String>?, bufferConfig: BufferConfig?, initializeOnCreation: Bool?) {
|
||||
init(uri: String, externalSubtitles: [NativeExternalSubtitle]?, drm: NativeDrmParams?, headers: Dictionary<String, String>?, bufferConfig: BufferConfig?, metadata: CustomVideoMetadata?, initializeOnCreation: Bool?) {
|
||||
self.init(std.string(uri), { () -> bridge.std__optional_std__vector_NativeExternalSubtitle__ in
|
||||
if let __unwrappedValue = externalSubtitles {
|
||||
return bridge.create_std__optional_std__vector_NativeExternalSubtitle__(__unwrappedValue.withUnsafeBufferPointer { __pointer -> bridge.std__vector_NativeExternalSubtitle_ in
|
||||
@@ -51,6 +51,12 @@ public extension NativeVideoConfig {
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}(), { () -> bridge.std__optional_CustomVideoMetadata_ in
|
||||
if let __unwrappedValue = metadata {
|
||||
return bridge.create_std__optional_CustomVideoMetadata_(__unwrappedValue)
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}(), { () -> bridge.std__optional_bool_ in
|
||||
if let __unwrappedValue = initializeOnCreation {
|
||||
return bridge.create_std__optional_bool_(__unwrappedValue)
|
||||
@@ -173,6 +179,29 @@ public extension NativeVideoConfig {
|
||||
}
|
||||
}
|
||||
|
||||
var metadata: CustomVideoMetadata? {
|
||||
@inline(__always)
|
||||
get {
|
||||
return { () -> CustomVideoMetadata? in
|
||||
if let __unwrapped = self.__metadata.value {
|
||||
return __unwrapped
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
@inline(__always)
|
||||
set {
|
||||
self.__metadata = { () -> bridge.std__optional_CustomVideoMetadata_ in
|
||||
if let __unwrappedValue = newValue {
|
||||
return bridge.create_std__optional_CustomVideoMetadata_(__unwrappedValue)
|
||||
} else {
|
||||
return .init()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
var initializeOnCreation: Bool? {
|
||||
@inline(__always)
|
||||
get {
|
||||
|
||||
84
packages/react-native-video/nitrogen/generated/shared/c++/CustomVideoMetadata.hpp
generated
Normal file
84
packages/react-native-video/nitrogen/generated/shared/c++/CustomVideoMetadata.hpp
generated
Normal file
@@ -0,0 +1,84 @@
|
||||
///
|
||||
/// CustomVideoMetadata.hpp
|
||||
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
||||
/// https://github.com/mrousavy/nitro
|
||||
/// Copyright © 2025 Marc Rousavy @ Margelo
|
||||
///
|
||||
|
||||
#pragma once
|
||||
|
||||
#if __has_include(<NitroModules/JSIConverter.hpp>)
|
||||
#include <NitroModules/JSIConverter.hpp>
|
||||
#else
|
||||
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
||||
#endif
|
||||
#if __has_include(<NitroModules/NitroDefines.hpp>)
|
||||
#include <NitroModules/NitroDefines.hpp>
|
||||
#else
|
||||
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
namespace margelo::nitro::video {
|
||||
|
||||
/**
|
||||
* A struct which can be represented as a JavaScript object (CustomVideoMetadata).
|
||||
*/
|
||||
struct CustomVideoMetadata {
|
||||
public:
|
||||
std::optional<std::string> title SWIFT_PRIVATE;
|
||||
std::optional<std::string> subtitle SWIFT_PRIVATE;
|
||||
std::optional<std::string> description SWIFT_PRIVATE;
|
||||
std::optional<std::string> artist SWIFT_PRIVATE;
|
||||
std::optional<std::string> imageUri SWIFT_PRIVATE;
|
||||
|
||||
public:
|
||||
CustomVideoMetadata() = default;
|
||||
explicit CustomVideoMetadata(std::optional<std::string> title, std::optional<std::string> subtitle, std::optional<std::string> description, std::optional<std::string> artist, std::optional<std::string> imageUri): title(title), subtitle(subtitle), description(description), artist(artist), imageUri(imageUri) {}
|
||||
};
|
||||
|
||||
} // namespace margelo::nitro::video
|
||||
|
||||
namespace margelo::nitro {
|
||||
|
||||
// C++ CustomVideoMetadata <> JS CustomVideoMetadata (object)
|
||||
template <>
|
||||
struct JSIConverter<margelo::nitro::video::CustomVideoMetadata> final {
|
||||
static inline margelo::nitro::video::CustomVideoMetadata fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
|
||||
jsi::Object obj = arg.asObject(runtime);
|
||||
return margelo::nitro::video::CustomVideoMetadata(
|
||||
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, "title")),
|
||||
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, "subtitle")),
|
||||
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, "description")),
|
||||
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, "artist")),
|
||||
JSIConverter<std::optional<std::string>>::fromJSI(runtime, obj.getProperty(runtime, "imageUri"))
|
||||
);
|
||||
}
|
||||
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::video::CustomVideoMetadata& arg) {
|
||||
jsi::Object obj(runtime);
|
||||
obj.setProperty(runtime, "title", JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.title));
|
||||
obj.setProperty(runtime, "subtitle", JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.subtitle));
|
||||
obj.setProperty(runtime, "description", JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.description));
|
||||
obj.setProperty(runtime, "artist", JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.artist));
|
||||
obj.setProperty(runtime, "imageUri", JSIConverter<std::optional<std::string>>::toJSI(runtime, arg.imageUri));
|
||||
return obj;
|
||||
}
|
||||
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
|
||||
if (!value.isObject()) {
|
||||
return false;
|
||||
}
|
||||
jsi::Object obj = value.getObject(runtime);
|
||||
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, "title"))) return false;
|
||||
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, "subtitle"))) return false;
|
||||
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, "description"))) return false;
|
||||
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, "artist"))) return false;
|
||||
if (!JSIConverter<std::optional<std::string>>::canConvert(runtime, obj.getProperty(runtime, "imageUri"))) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace margelo::nitro
|
||||
@@ -16,6 +16,8 @@ namespace margelo::nitro::video {
|
||||
registerHybrids(this, [](Prototype& prototype) {
|
||||
prototype.registerHybridGetter("source", &HybridVideoPlayerSpec::getSource);
|
||||
prototype.registerHybridGetter("eventEmitter", &HybridVideoPlayerSpec::getEventEmitter);
|
||||
prototype.registerHybridGetter("showNotificationControls", &HybridVideoPlayerSpec::getShowNotificationControls);
|
||||
prototype.registerHybridSetter("showNotificationControls", &HybridVideoPlayerSpec::setShowNotificationControls);
|
||||
prototype.registerHybridGetter("status", &HybridVideoPlayerSpec::getStatus);
|
||||
prototype.registerHybridGetter("duration", &HybridVideoPlayerSpec::getDuration);
|
||||
prototype.registerHybridGetter("volume", &HybridVideoPlayerSpec::getVolume);
|
||||
|
||||
@@ -66,6 +66,8 @@ namespace margelo::nitro::video {
|
||||
// Properties
|
||||
virtual std::shared_ptr<HybridVideoPlayerSourceSpec> getSource() = 0;
|
||||
virtual std::shared_ptr<HybridVideoPlayerEventEmitterSpec> getEventEmitter() = 0;
|
||||
virtual bool getShowNotificationControls() = 0;
|
||||
virtual void setShowNotificationControls(bool showNotificationControls) = 0;
|
||||
virtual VideoPlayerStatus getStatus() = 0;
|
||||
virtual double getDuration() = 0;
|
||||
virtual double getVolume() = 0;
|
||||
|
||||
@@ -24,6 +24,8 @@ namespace margelo::nitro::video { struct NativeExternalSubtitle; }
|
||||
namespace margelo::nitro::video { struct NativeDrmParams; }
|
||||
// Forward declaration of `BufferConfig` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct BufferConfig; }
|
||||
// Forward declaration of `CustomVideoMetadata` to properly resolve imports.
|
||||
namespace margelo::nitro::video { struct CustomVideoMetadata; }
|
||||
|
||||
#include <string>
|
||||
#include "NativeExternalSubtitle.hpp"
|
||||
@@ -32,6 +34,7 @@ namespace margelo::nitro::video { struct BufferConfig; }
|
||||
#include "NativeDrmParams.hpp"
|
||||
#include <unordered_map>
|
||||
#include "BufferConfig.hpp"
|
||||
#include "CustomVideoMetadata.hpp"
|
||||
|
||||
namespace margelo::nitro::video {
|
||||
|
||||
@@ -45,11 +48,12 @@ namespace margelo::nitro::video {
|
||||
std::optional<NativeDrmParams> drm SWIFT_PRIVATE;
|
||||
std::optional<std::unordered_map<std::string, std::string>> headers SWIFT_PRIVATE;
|
||||
std::optional<BufferConfig> bufferConfig SWIFT_PRIVATE;
|
||||
std::optional<CustomVideoMetadata> metadata SWIFT_PRIVATE;
|
||||
std::optional<bool> initializeOnCreation SWIFT_PRIVATE;
|
||||
|
||||
public:
|
||||
NativeVideoConfig() = default;
|
||||
explicit NativeVideoConfig(std::string uri, std::optional<std::vector<NativeExternalSubtitle>> externalSubtitles, std::optional<NativeDrmParams> drm, std::optional<std::unordered_map<std::string, std::string>> headers, std::optional<BufferConfig> bufferConfig, std::optional<bool> initializeOnCreation): uri(uri), externalSubtitles(externalSubtitles), drm(drm), headers(headers), bufferConfig(bufferConfig), initializeOnCreation(initializeOnCreation) {}
|
||||
explicit NativeVideoConfig(std::string uri, std::optional<std::vector<NativeExternalSubtitle>> externalSubtitles, std::optional<NativeDrmParams> drm, std::optional<std::unordered_map<std::string, std::string>> headers, std::optional<BufferConfig> bufferConfig, std::optional<CustomVideoMetadata> metadata, std::optional<bool> initializeOnCreation): uri(uri), externalSubtitles(externalSubtitles), drm(drm), headers(headers), bufferConfig(bufferConfig), metadata(metadata), initializeOnCreation(initializeOnCreation) {}
|
||||
};
|
||||
|
||||
} // namespace margelo::nitro::video
|
||||
@@ -67,6 +71,7 @@ namespace margelo::nitro {
|
||||
JSIConverter<std::optional<margelo::nitro::video::NativeDrmParams>>::fromJSI(runtime, obj.getProperty(runtime, "drm")),
|
||||
JSIConverter<std::optional<std::unordered_map<std::string, std::string>>>::fromJSI(runtime, obj.getProperty(runtime, "headers")),
|
||||
JSIConverter<std::optional<margelo::nitro::video::BufferConfig>>::fromJSI(runtime, obj.getProperty(runtime, "bufferConfig")),
|
||||
JSIConverter<std::optional<margelo::nitro::video::CustomVideoMetadata>>::fromJSI(runtime, obj.getProperty(runtime, "metadata")),
|
||||
JSIConverter<std::optional<bool>>::fromJSI(runtime, obj.getProperty(runtime, "initializeOnCreation"))
|
||||
);
|
||||
}
|
||||
@@ -77,6 +82,7 @@ namespace margelo::nitro {
|
||||
obj.setProperty(runtime, "drm", JSIConverter<std::optional<margelo::nitro::video::NativeDrmParams>>::toJSI(runtime, arg.drm));
|
||||
obj.setProperty(runtime, "headers", JSIConverter<std::optional<std::unordered_map<std::string, std::string>>>::toJSI(runtime, arg.headers));
|
||||
obj.setProperty(runtime, "bufferConfig", JSIConverter<std::optional<margelo::nitro::video::BufferConfig>>::toJSI(runtime, arg.bufferConfig));
|
||||
obj.setProperty(runtime, "metadata", JSIConverter<std::optional<margelo::nitro::video::CustomVideoMetadata>>::toJSI(runtime, arg.metadata));
|
||||
obj.setProperty(runtime, "initializeOnCreation", JSIConverter<std::optional<bool>>::toJSI(runtime, arg.initializeOnCreation));
|
||||
return obj;
|
||||
}
|
||||
@@ -90,6 +96,7 @@ namespace margelo::nitro {
|
||||
if (!JSIConverter<std::optional<margelo::nitro::video::NativeDrmParams>>::canConvert(runtime, obj.getProperty(runtime, "drm"))) return false;
|
||||
if (!JSIConverter<std::optional<std::unordered_map<std::string, std::string>>>::canConvert(runtime, obj.getProperty(runtime, "headers"))) return false;
|
||||
if (!JSIConverter<std::optional<margelo::nitro::video::BufferConfig>>::canConvert(runtime, obj.getProperty(runtime, "bufferConfig"))) return false;
|
||||
if (!JSIConverter<std::optional<margelo::nitro::video::CustomVideoMetadata>>::canConvert(runtime, obj.getProperty(runtime, "metadata"))) return false;
|
||||
if (!JSIConverter<std::optional<bool>>::canConvert(runtime, obj.getProperty(runtime, "initializeOnCreation"))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/config-plugins": "^10.0.2",
|
||||
"@react-native/eslint-config": "^0.77.0",
|
||||
"@types/react": "^18.2.44",
|
||||
"del-cli": "^5.1.0",
|
||||
"eslint": "^8.51.0",
|
||||
@@ -84,7 +85,7 @@
|
||||
"prettier": "^3.0.3",
|
||||
"react": "18.3.1",
|
||||
"react-native": "^0.77.0",
|
||||
"@react-native/eslint-config": "^0.77.0",
|
||||
"shaka-player": "^4.15.9",
|
||||
"react-native-builder-bob": "^0.40.0",
|
||||
"react-native-nitro-modules": "^0.29.0",
|
||||
"typescript": "^5.2.2"
|
||||
@@ -149,5 +150,11 @@
|
||||
"type": "view-legacy",
|
||||
"languages": "kotlin-swift",
|
||||
"version": "0.41.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"shaka-player": "^4.15.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/react-native-web": "^0.19.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Platform } from 'react-native';
|
||||
import { NitroModules } from 'react-native-nitro-modules';
|
||||
import { type VideoPlayer as VideoPlayerImpl } from '../spec/nitro/VideoPlayer.nitro';
|
||||
import type { VideoPlayer as VideoPlayerImpl } from '../spec/nitro/VideoPlayer.nitro';
|
||||
import type { VideoPlayerSource } from '../spec/nitro/VideoPlayerSource.nitro';
|
||||
import type { IgnoreSilentSwitchMode } from './types/IgnoreSilentSwitchMode';
|
||||
import type { MixAudioMode } from './types/MixAudioMode';
|
||||
@@ -185,6 +185,14 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
return this.player.isPlaying;
|
||||
}
|
||||
|
||||
get showNotificationControls(): boolean {
|
||||
return this.player.showNotificationControls;
|
||||
}
|
||||
|
||||
set showNotificationControls(value: boolean) {
|
||||
this.player.showNotificationControls = value;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.wrapPromise(this.player.initialize());
|
||||
|
||||
|
||||
295
packages/react-native-video/src/core/VideoPlayer.web.ts
Normal file
295
packages/react-native-video/src/core/VideoPlayer.web.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import shaka from "shaka-player";
|
||||
import type { VideoPlayerSource } from "../spec/nitro/VideoPlayerSource.nitro";
|
||||
import type { IgnoreSilentSwitchMode } from "./types/IgnoreSilentSwitchMode";
|
||||
import type { MixAudioMode } from "./types/MixAudioMode";
|
||||
import type { TextTrack } from "./types/TextTrack";
|
||||
import type { NoAutocomplete } from "./types/Utils";
|
||||
import type { VideoConfig, VideoSource } from "./types/VideoConfig";
|
||||
import {
|
||||
tryParseNativeVideoError,
|
||||
VideoRuntimeError,
|
||||
} from "./types/VideoError";
|
||||
import type { VideoPlayerBase } from "./types/VideoPlayerBase";
|
||||
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
|
||||
import { VideoPlayerEvents } from "./VideoPlayerEvents";
|
||||
import { WebEventEmiter } from "./WebEventEmiter";
|
||||
|
||||
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
|
||||
protected player = new shaka.Player();
|
||||
protected video: HTMLVideoElement;
|
||||
protected headers: Record<string, string> = {};
|
||||
|
||||
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
|
||||
const video = document.createElement("video");
|
||||
super(new WebEventEmiter(video));
|
||||
this.video = video;
|
||||
this.player.attach(this.video);
|
||||
this.player.getNetworkingEngine()!.registerRequestFilter((_type, request) => {
|
||||
request.headers = this.headers;
|
||||
});
|
||||
this.replaceSourceAsync(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up player's native resources and releases native state.
|
||||
* After calling this method, the player is no longer usable.
|
||||
* @internal
|
||||
*/
|
||||
__destroy() {
|
||||
this.player.destroy();
|
||||
}
|
||||
|
||||
__getNativeRef() {
|
||||
return this.video;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles parsing native errors to VideoRuntimeError and calling onError if provided
|
||||
* @internal
|
||||
*/
|
||||
private throwError(error: unknown) {
|
||||
const parsedError = tryParseNativeVideoError(error);
|
||||
|
||||
if (
|
||||
parsedError instanceof VideoRuntimeError &&
|
||||
this.triggerEvent("onError", parsedError)
|
||||
) {
|
||||
// We don't throw errors if onError is provided
|
||||
return;
|
||||
}
|
||||
|
||||
throw parsedError;
|
||||
}
|
||||
|
||||
// Source
|
||||
get source(): VideoPlayerSource {
|
||||
// TODO: properly implement this
|
||||
return {
|
||||
uri: this.player.getAssetUri()!,
|
||||
config: {},
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Status
|
||||
get status(): VideoPlayerStatus {
|
||||
if (this.video.error) return "error";
|
||||
if (this.video.readyState === HTMLMediaElement.HAVE_NOTHING) return "idle";
|
||||
if (
|
||||
this.video.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
|
||||
this.video.readyState === HTMLMediaElement.HAVE_FUTURE_DATA
|
||||
)
|
||||
return "readyToPlay";
|
||||
return "loading";
|
||||
}
|
||||
|
||||
// Duration
|
||||
get duration(): number {
|
||||
return this.video.duration;
|
||||
}
|
||||
|
||||
// Volume
|
||||
get volume(): number {
|
||||
return this.video.volume;
|
||||
}
|
||||
|
||||
set volume(value: number) {
|
||||
this.video.volume = value;
|
||||
}
|
||||
|
||||
// Current Time
|
||||
get currentTime(): number {
|
||||
return this.video.currentTime;
|
||||
}
|
||||
|
||||
set currentTime(value: number) {
|
||||
this.video.currentTime = value;
|
||||
}
|
||||
|
||||
// Muted
|
||||
get muted(): boolean {
|
||||
return this.video.muted;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
this.video.muted = value;
|
||||
}
|
||||
|
||||
// Loop
|
||||
get loop(): boolean {
|
||||
return this.video.loop;
|
||||
}
|
||||
|
||||
set loop(value: boolean) {
|
||||
this.video.loop = value;
|
||||
}
|
||||
|
||||
// Rate
|
||||
get rate(): number {
|
||||
return this.video.playbackRate;
|
||||
}
|
||||
|
||||
set rate(value: number) {
|
||||
this.video.playbackRate = value;
|
||||
}
|
||||
|
||||
// Mix Audio Mode
|
||||
get mixAudioMode(): MixAudioMode {
|
||||
return "auto";
|
||||
}
|
||||
|
||||
set mixAudioMode(_: MixAudioMode) {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
"mixAudioMode is not supported on this platform, it wont have any effect",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore Silent Switch Mode
|
||||
get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode {
|
||||
return "auto";
|
||||
}
|
||||
|
||||
set ignoreSilentSwitchMode(_: IgnoreSilentSwitchMode) {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
"ignoreSilentSwitchMode is not supported on this platform, it wont have any effect",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Play In Background
|
||||
get playInBackground(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
set playInBackground(_: boolean) {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
"playInBackground is not supported on this platform, it wont have any effect",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Play When Inactive
|
||||
get playWhenInactive(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
set playWhenInactive(_: boolean) {
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
"playWhenInactive is not supported on this platform, it wont have any effect",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Is Playing
|
||||
get isPlaying(): boolean {
|
||||
return this.status === "readyToPlay" && !this.video.paused;
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// noop on web
|
||||
}
|
||||
|
||||
async preload(): Promise<void> {
|
||||
// we start loading when initializing the source.
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the player's native resources and releases native state.
|
||||
* After calling this method, the player is no longer usable.
|
||||
* Accessing any properties or methods of the player after calling this method will throw an error.
|
||||
* If you want to clean player resource use `replaceSourceAsync` with `null` instead.
|
||||
*/
|
||||
release(): void {
|
||||
this.__destroy();
|
||||
}
|
||||
|
||||
play(): void {
|
||||
try {
|
||||
this.video.play();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
try {
|
||||
this.video.pause();
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
seekBy(time: number): void {
|
||||
try {
|
||||
this.video.currentTime += time;
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
seekTo(time: number): void {
|
||||
try {
|
||||
this.video.currentTime = time;
|
||||
} catch (error) {
|
||||
this.throwError(error);
|
||||
}
|
||||
}
|
||||
|
||||
async replaceSourceAsync(
|
||||
source:
|
||||
| VideoSource
|
||||
| VideoConfig
|
||||
| NoAutocomplete<VideoPlayerSource>
|
||||
| null,
|
||||
): Promise<void> {
|
||||
const src =
|
||||
typeof source === "object" && source && "uri" in source
|
||||
? source.uri
|
||||
: source;
|
||||
if (typeof src === "number") {
|
||||
console.error("A source uri must be a string. Numbers are only supported on native.");
|
||||
return;
|
||||
}
|
||||
// TODO: handle start time
|
||||
this.player.load(src)
|
||||
if (typeof source !== "object") return;
|
||||
|
||||
this.headers = source?.headers ?? {};
|
||||
// this.player.configure({
|
||||
// drm: undefined,
|
||||
// streaming: {
|
||||
// bufferingGoal: source?.bufferConfig?.maxBufferMs,
|
||||
// },
|
||||
// } satisfies Partial<shaka.extern.PlayerConfiguration>);
|
||||
}
|
||||
|
||||
// Text Track Management
|
||||
getAvailableTextTracks(): TextTrack[] {
|
||||
return this.player.getTextTracks().map(x => ({
|
||||
id: x.id.toString(),
|
||||
label: x.label ?? "",
|
||||
language: x.language,
|
||||
selected: x.active,
|
||||
}));
|
||||
}
|
||||
|
||||
selectTextTrack(textTrack: TextTrack | null): void {
|
||||
this.player.setTextTrackVisibility(textTrack !== null)
|
||||
if (!textTrack) return;
|
||||
const track = this.player
|
||||
.getTextTracks()
|
||||
.find((x) => x.id === Number(textTrack.id));
|
||||
if (track) this.player.selectTextTrack(track);
|
||||
}
|
||||
|
||||
// Selected Text Track
|
||||
get selectedTrack(): TextTrack | undefined {
|
||||
return this.getAvailableTextTracks().find(x => x.selected);
|
||||
}
|
||||
}
|
||||
|
||||
export { VideoPlayer };
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { VideoPlayerEventEmitter } from '../spec/nitro/VideoPlayerEventEmitter.nitro';
|
||||
import {
|
||||
ALL_PLAYER_EVENTS,
|
||||
type VideoPlayerEvents as NativePlayerEvents,
|
||||
type AllPlayerEvents as PlayerEvents,
|
||||
} from './types/Events';
|
||||
} from "./types/Events";
|
||||
|
||||
export class VideoPlayerEvents {
|
||||
protected eventEmitter: VideoPlayerEventEmitter;
|
||||
protected eventEmitter: NativePlayerEvents;
|
||||
protected eventListeners: Partial<
|
||||
Record<keyof PlayerEvents, Set<(...params: any[]) => void>>
|
||||
> = {};
|
||||
@@ -13,9 +13,9 @@ export class VideoPlayerEvents {
|
||||
protected readonly supportedEvents: (keyof PlayerEvents)[] =
|
||||
ALL_PLAYER_EVENTS;
|
||||
|
||||
constructor(eventEmitter: VideoPlayerEventEmitter) {
|
||||
constructor(eventEmitter: NativePlayerEvents) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
for (let event of this.supportedEvents) {
|
||||
for (const event of this.supportedEvents) {
|
||||
// @ts-expect-error we narrow the type of the event
|
||||
this.eventEmitter[event] = this.triggerEvent.bind(this, event);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export class VideoPlayerEvents {
|
||||
...params: Parameters<PlayerEvents[Event]>
|
||||
): boolean {
|
||||
if (!this.eventListeners[event]?.size) return false;
|
||||
for (let fn of this.eventListeners[event]) {
|
||||
for (const fn of this.eventListeners[event]) {
|
||||
fn(...params);
|
||||
}
|
||||
return true;
|
||||
@@ -34,7 +34,7 @@ export class VideoPlayerEvents {
|
||||
|
||||
addEventListener<Event extends keyof PlayerEvents>(
|
||||
event: Event,
|
||||
callback: PlayerEvents[Event]
|
||||
callback: PlayerEvents[Event],
|
||||
) {
|
||||
this.eventListeners[event] ??= new Set<PlayerEvents[Event]>();
|
||||
this.eventListeners[event].add(callback);
|
||||
@@ -42,7 +42,7 @@ export class VideoPlayerEvents {
|
||||
|
||||
removeEventListener<Event extends keyof PlayerEvents>(
|
||||
event: Event,
|
||||
callback: PlayerEvents[Event]
|
||||
callback: PlayerEvents[Event],
|
||||
) {
|
||||
this.eventListeners[event]?.delete(callback);
|
||||
}
|
||||
|
||||
198
packages/react-native-video/src/core/WebEventEmiter.ts
Normal file
198
packages/react-native-video/src/core/WebEventEmiter.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import type {
|
||||
BandwidthData,
|
||||
onLoadData,
|
||||
onLoadStartData,
|
||||
onPlaybackStateChangeData,
|
||||
onProgressData,
|
||||
onVolumeChangeData,
|
||||
AllPlayerEvents as PlayerEvents,
|
||||
TimedMetadata,
|
||||
} from "./types/Events";
|
||||
import type { TextTrack } from "./types/TextTrack";
|
||||
import type { VideoRuntimeError } from "./types/VideoError";
|
||||
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
|
||||
|
||||
export class WebEventEmiter implements PlayerEvents {
|
||||
private _isBuferring = false;
|
||||
|
||||
constructor(private video: HTMLVideoElement) {
|
||||
// TODO: add `onBandwithUpdate`
|
||||
|
||||
// on buffer
|
||||
this.video.addEventListener("canplay", this._onCanPlay);
|
||||
this.video.addEventListener("waiting", this._onWaiting);
|
||||
|
||||
// on end
|
||||
this.video.addEventListener("ended", this._onEnded);
|
||||
|
||||
// on load
|
||||
this.video.addEventListener("durationchange", this._onDurationChange);
|
||||
|
||||
// on load start
|
||||
this.video.addEventListener("loadstart", this._onLoadStart);
|
||||
|
||||
// on playback state change
|
||||
this.video.addEventListener("play", this._onPlay);
|
||||
this.video.addEventListener("pause", this._onPause);
|
||||
|
||||
// on playback rate change
|
||||
this.video.addEventListener("ratechange", this._onRateChange);
|
||||
|
||||
// on progress
|
||||
this.video.addEventListener("timeupdate", this._onTimeUpdate);
|
||||
|
||||
// on ready to play
|
||||
this.video.addEventListener("loadeddata", this._onLoadedData);
|
||||
|
||||
// on seek
|
||||
this.video.addEventListener("seeked", this._onSeeked);
|
||||
|
||||
// on volume change
|
||||
this.video.addEventListener("volumechange", this._onVolumeChange);
|
||||
|
||||
// on status change
|
||||
this.video.addEventListener("error", this._onError);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.video.removeEventListener("canplay", this._onCanPlay);
|
||||
this.video.removeEventListener("waiting", this._onWaiting);
|
||||
|
||||
this.video.removeEventListener("ended", this._onEnded);
|
||||
|
||||
this.video.removeEventListener("durationchange", this._onDurationChange);
|
||||
|
||||
this.video.removeEventListener("play", this._onPlay);
|
||||
this.video.removeEventListener("pause", this._onPause);
|
||||
|
||||
this.video.removeEventListener("ratechange", this._onRateChange);
|
||||
|
||||
this.video.removeEventListener("timeupdate", this._onTimeUpdate);
|
||||
|
||||
this.video.removeEventListener("loadeddata", this._onLoadedData);
|
||||
|
||||
this.video.removeEventListener("seeked", this._onSeeked);
|
||||
|
||||
this.video.removeEventListener("volumechange", this._onVolumeChange);
|
||||
|
||||
this.video.removeEventListener("error", this._onError);
|
||||
}
|
||||
|
||||
_onTimeUpdate() {
|
||||
this.onProgress({
|
||||
currentTime: this.video.currentTime,
|
||||
bufferDuration: this.video.buffered.length
|
||||
? this.video.buffered.end(this.video.buffered.length - 1)
|
||||
: 0,
|
||||
});
|
||||
}
|
||||
|
||||
_onCanPlay() {
|
||||
this._isBuferring = false;
|
||||
this.onBuffer(false);
|
||||
this.onStatusChange("readyToPlay");
|
||||
}
|
||||
_onWaiting() {
|
||||
this._isBuferring = true;
|
||||
this.onBuffer(true);
|
||||
this.onStatusChange("loading");
|
||||
}
|
||||
|
||||
_onDurationChange() {
|
||||
this.onLoad({
|
||||
currentTime: this.video.currentTime,
|
||||
duration: this.video.duration,
|
||||
width: this.video.width,
|
||||
height: this.video.height,
|
||||
orientation: "unknown",
|
||||
});
|
||||
}
|
||||
|
||||
_onEnded() {
|
||||
this.onEnd();
|
||||
this.onStatusChange("idle");
|
||||
}
|
||||
|
||||
_onLoadStart() {
|
||||
this.onLoadStart({
|
||||
sourceType: "network",
|
||||
source: {
|
||||
uri: this.video.currentSrc,
|
||||
config: {
|
||||
uri: this.video.currentSrc,
|
||||
externalSubtitles: [],
|
||||
},
|
||||
getAssetInformationAsync: async () => {
|
||||
return {
|
||||
duration: BigInt(this.video.duration),
|
||||
height: this.video.height,
|
||||
width: this.video.width,
|
||||
orientation: "unknown",
|
||||
bitrate: NaN,
|
||||
fileSize: BigInt(NaN),
|
||||
isHDR: false,
|
||||
isLive: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_onPlay() {
|
||||
this.onPlaybackStateChange({
|
||||
isPlaying: true,
|
||||
isBuffering: this._isBuferring,
|
||||
});
|
||||
}
|
||||
|
||||
_onPause() {
|
||||
this.onPlaybackStateChange({
|
||||
isPlaying: false,
|
||||
isBuffering: this._isBuferring,
|
||||
});
|
||||
}
|
||||
|
||||
_onRateChange() {
|
||||
this.onPlaybackRateChange(this.video.playbackRate);
|
||||
}
|
||||
|
||||
_onLoadedData() {
|
||||
this.onReadyToDisplay();
|
||||
}
|
||||
|
||||
_onSeeked() {
|
||||
this.onSeek(this.video.currentTime);
|
||||
}
|
||||
|
||||
_onVolumeChange() {
|
||||
this.onVolumeChange({ muted: this.video.muted, volume: this.video.volume });
|
||||
}
|
||||
|
||||
_onError() {
|
||||
this.onStatusChange("error");
|
||||
}
|
||||
|
||||
NOOP = () => {};
|
||||
|
||||
onError: (error: VideoRuntimeError) => void = this.NOOP;
|
||||
onAudioBecomingNoisy: () => void = this.NOOP;
|
||||
onAudioFocusChange: (hasAudioFocus: boolean) => void = this.NOOP;
|
||||
onBandwidthUpdate: (data: BandwidthData) => void = this.NOOP;
|
||||
onBuffer: (buffering: boolean) => void = this.NOOP;
|
||||
onControlsVisibleChange: (visible: boolean) => void = this.NOOP;
|
||||
onEnd: () => void = this.NOOP;
|
||||
onExternalPlaybackChange: (externalPlaybackActive: boolean) => void =
|
||||
this.NOOP;
|
||||
onLoad: (data: onLoadData) => void = this.NOOP;
|
||||
onLoadStart: (data: onLoadStartData) => void = this.NOOP;
|
||||
onPlaybackStateChange: (data: onPlaybackStateChangeData) => void = this.NOOP;
|
||||
onPlaybackRateChange: (rate: number) => void = this.NOOP;
|
||||
onProgress: (data: onProgressData) => void = this.NOOP;
|
||||
onReadyToDisplay: () => void = this.NOOP;
|
||||
onSeek: (seekTime: number) => void = this.NOOP;
|
||||
onTimedMetadata: (metadata: TimedMetadata) => void = this.NOOP;
|
||||
onTextTrackDataChanged: (texts: string[]) => void = this.NOOP;
|
||||
onTrackChange: (track: TextTrack | null) => void = this.NOOP;
|
||||
onVolumeChange: (data: onVolumeChangeData) => void = this.NOOP;
|
||||
onStatusChange: (status: VideoPlayerStatus) => void = this.NOOP;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { VideoPlayer } from '../VideoPlayer';
|
||||
import { type AllPlayerEvents } from '../types/Events';
|
||||
import type { AllPlayerEvents } from '../types/Events';
|
||||
import type { VideoPlayer } from '../VideoPlayer';
|
||||
|
||||
/**
|
||||
* Attaches an event listener to a `VideoPlayer` instance for a specified event.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
|
||||
import type { NoAutocomplete } from '../types/Utils';
|
||||
import type { VideoConfig, VideoSource } from '../types/VideoConfig';
|
||||
import { isVideoPlayerSource } from '../utils/sourceFactory';
|
||||
import { isVideoPlayerSource } from '../utils/sourceUtils';
|
||||
import { VideoPlayer } from '../VideoPlayer';
|
||||
import { useManagedInstance } from './useManagedInstance';
|
||||
|
||||
|
||||
7
packages/react-native-video/src/core/shaka.d.ts
vendored
Normal file
7
packages/react-native-video/src/core/shaka.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
declare module 'shaka-player' {
|
||||
export = shaka;
|
||||
}
|
||||
|
||||
declare module 'shaka-player/dist/shaka-player.compiled' {
|
||||
export = shaka;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
|
||||
import type { TextTrack } from './TextTrack';
|
||||
import type { VideoRuntimeError } from './VideoError';
|
||||
import type { VideoOrientation } from './VideoOrientation';
|
||||
import type { VideoPlayerSourceBase } from './VideoPlayerSourceBase';
|
||||
import type { VideoPlayerStatus } from './VideoPlayerStatus';
|
||||
|
||||
export interface VideoPlayerEvents {
|
||||
@@ -28,6 +28,7 @@ export interface VideoPlayerEvents {
|
||||
/**
|
||||
* Called when the video view's controls visibility changes.
|
||||
* @param visible Whether the video view's controls are visible.
|
||||
* @platform Android, Ios
|
||||
*/
|
||||
onControlsVisibleChange: (visible: boolean) => void;
|
||||
/**
|
||||
@@ -72,15 +73,18 @@ export interface VideoPlayerEvents {
|
||||
onSeek: (seekTime: number) => void;
|
||||
/**
|
||||
* Called when player receives timed metadata.
|
||||
* @platform Android, Ios
|
||||
*/
|
||||
onTimedMetadata: (metadata: TimedMetadata) => void;
|
||||
/**
|
||||
* Called when the text track (currently displayed subtitle) data changes.
|
||||
* @platform Android, Ios
|
||||
*/
|
||||
onTextTrackDataChanged: (texts: string[]) => void;
|
||||
/**
|
||||
* Called when the selected text track changes.
|
||||
* @param track - The newly selected text track, or null if no track is selected
|
||||
* @platform Android, Ios
|
||||
*/
|
||||
onTrackChange: (track: TextTrack | null) => void;
|
||||
/**
|
||||
@@ -178,7 +182,7 @@ export interface onLoadStartData {
|
||||
/**
|
||||
* The source of the video.
|
||||
*/
|
||||
source: VideoPlayerSource;
|
||||
source: VideoPlayerSourceBase;
|
||||
}
|
||||
|
||||
export interface onPlaybackStateChangeData {
|
||||
|
||||
@@ -26,6 +26,11 @@ export type VideoConfig = {
|
||||
* The player buffer configuration.
|
||||
*/
|
||||
bufferConfig?: BufferConfig;
|
||||
/**
|
||||
* The custom metadata to be associated with the video.
|
||||
* This metadata can be used by the native player to show information about the video.
|
||||
*/
|
||||
metadata?: CustomVideoMetadata;
|
||||
/**
|
||||
* The external subtitles to be used.
|
||||
* @note on iOS, only WebVTT (.vtt) subtitles are supported (for HLS streams and MP4 files).
|
||||
@@ -135,3 +140,11 @@ interface NativeExternalSubtitle {
|
||||
interface NativeDrmParams extends DrmParams {
|
||||
type?: string;
|
||||
}
|
||||
|
||||
interface CustomVideoMetadata {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
artist?: string;
|
||||
imageUri?: string;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import type {
|
||||
} from '../../spec/nitro/VideoPlayer.nitro';
|
||||
import type { VideoPlayerSource } from '../../spec/nitro/VideoPlayerSource.nitro';
|
||||
import type { VideoConfig, VideoSource } from '../types/VideoConfig';
|
||||
import { createSource, isVideoPlayerSource } from './sourceFactory';
|
||||
import { createSource } from './sourceFactory';
|
||||
import { tryParseNativeVideoError } from '../types/VideoError';
|
||||
import { isVideoPlayerSource } from './sourceUtils';
|
||||
|
||||
const VideoPlayerFactory =
|
||||
NitroModules.createHybridObject<VideoPlayerFactory>('VideoPlayerFactory');
|
||||
|
||||
@@ -12,21 +12,13 @@ import type {
|
||||
VideoSource,
|
||||
} from '../types/VideoConfig';
|
||||
import { tryParseNativeVideoError } from '../types/VideoError';
|
||||
import { isVideoPlayerSource } from './sourceUtils';
|
||||
|
||||
const VideoPlayerSourceFactory =
|
||||
NitroModules.createHybridObject<VideoPlayerSourceFactory>(
|
||||
'VideoPlayerSourceFactory'
|
||||
);
|
||||
|
||||
export const isVideoPlayerSource = (obj: any): obj is VideoPlayerSource => {
|
||||
return (
|
||||
obj && // obj is not null
|
||||
typeof obj === 'object' && // obj is an object
|
||||
'name' in obj && // obj has a name property
|
||||
obj.name === 'VideoPlayerSource' // obj.name is 'VideoPlayerSource'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a `VideoPlayerSource` instance from a URI (string).
|
||||
*
|
||||
|
||||
11
packages/react-native-video/src/core/utils/sourceUtils.ts
Normal file
11
packages/react-native-video/src/core/utils/sourceUtils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { VideoPlayerSource } from "../../spec/nitro/VideoPlayerSource.nitro";
|
||||
|
||||
export const isVideoPlayerSource = (obj: any): obj is VideoPlayerSource => {
|
||||
return (
|
||||
obj && // obj is not null
|
||||
typeof obj === 'object' && // obj is an object
|
||||
'name' in obj && // obj has a name property
|
||||
obj.name === 'VideoPlayerSource' // obj.name is 'VideoPlayerSource'
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,87 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import type { ViewProps, ViewStyle } from 'react-native';
|
||||
import type { ViewStyle } from 'react-native';
|
||||
import { NitroModules } from 'react-native-nitro-modules';
|
||||
import type {
|
||||
SurfaceType,
|
||||
VideoViewViewManager,
|
||||
VideoViewViewManagerFactory,
|
||||
} from '../../spec/nitro/VideoViewViewManager.nitro';
|
||||
import type { VideoViewEvents } from '../types/Events';
|
||||
import type { ResizeMode } from '../types/ResizeMode';
|
||||
import { tryParseNativeVideoError, VideoError } from '../types/VideoError';
|
||||
import type { VideoPlayer } from '../VideoPlayer';
|
||||
import { NativeVideoView } from './NativeVideoView';
|
||||
|
||||
export interface VideoViewProps extends Partial<VideoViewEvents>, ViewProps {
|
||||
/**
|
||||
* The player to play the video - {@link VideoPlayer}
|
||||
*/
|
||||
player: VideoPlayer;
|
||||
/**
|
||||
* The style of the video view - {@link ViewStyle}
|
||||
*/
|
||||
style?: ViewStyle;
|
||||
/**
|
||||
* Whether to show the controls. Defaults to false.
|
||||
*/
|
||||
controls?: boolean;
|
||||
/**
|
||||
* Whether to enable & show the picture in picture button in native controls. Defaults to false.
|
||||
*/
|
||||
pictureInPicture?: boolean;
|
||||
/**
|
||||
* Whether to automatically enter picture in picture mode when the video is playing. Defaults to false.
|
||||
*/
|
||||
autoEnterPictureInPicture?: boolean;
|
||||
/**
|
||||
* How the video should be resized to fit the view. Defaults to 'none'.
|
||||
* - 'contain': Scale the video uniformly (maintain aspect ratio) so that it fits entirely within the view
|
||||
* - 'cover': Scale the video uniformly (maintain aspect ratio) so that it fills the entire view (may crop)
|
||||
* - 'stretch': Scale the video to fill the entire view without maintaining aspect ratio
|
||||
* - 'none': Do not resize the video
|
||||
*/
|
||||
resizeMode?: ResizeMode;
|
||||
/**
|
||||
* Whether to keep the screen awake while the video view is mounted. Defaults to true.
|
||||
*/
|
||||
keepScreenAwake?: boolean;
|
||||
|
||||
/**
|
||||
* The type of underlying native view. Defaults to 'surface'.
|
||||
* - 'surface': Uses a SurfaceView on Android. More performant, but cannot be animated or transformed.
|
||||
* - 'texture': Uses a TextureView on Android. Less performant, but can be animated and transformed.
|
||||
*
|
||||
* Only applicable on Android
|
||||
*
|
||||
* @default 'surface'
|
||||
* @platform android
|
||||
*/
|
||||
surfaceType?: SurfaceType;
|
||||
}
|
||||
|
||||
export interface VideoViewRef {
|
||||
/**
|
||||
* Enter fullscreen mode
|
||||
*/
|
||||
enterFullscreen: () => void;
|
||||
/**
|
||||
* Exit fullscreen mode
|
||||
*/
|
||||
exitFullscreen: () => void;
|
||||
/**
|
||||
* Enter picture in picture mode
|
||||
*/
|
||||
enterPictureInPicture: () => void;
|
||||
/**
|
||||
* Exit picture in picture mode
|
||||
*/
|
||||
exitPictureInPicture: () => void;
|
||||
/**
|
||||
* Check if picture in picture mode is supported
|
||||
* @returns true if picture in picture mode is supported, false otherwise
|
||||
*/
|
||||
canEnterPictureInPicture: () => boolean;
|
||||
}
|
||||
import type { VideoViewProps, VideoViewRef } from './ViewViewProps';
|
||||
|
||||
let nitroIdCounter = 1;
|
||||
const VideoViewViewManagerFactory =
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
} from "react";
|
||||
import { View, type ViewStyle } from "react-native";
|
||||
import type { VideoPlayer } from "../VideoPlayer.web";
|
||||
import type { VideoViewProps, VideoViewRef } from "./ViewViewProps";
|
||||
|
||||
/**
|
||||
* VideoView is a component that allows you to display a video from a {@link VideoPlayer}.
|
||||
*
|
||||
* @param player - The player to play the video - {@link VideoPlayer}
|
||||
* @param controls - Whether to show the controls. Defaults to false.
|
||||
* @param style - The style of the video view - {@link ViewStyle}
|
||||
* @param pictureInPicture - Whether to show the picture in picture button. Defaults to false.
|
||||
* @param autoEnterPictureInPicture - Whether to automatically enter picture in picture mode
|
||||
* when the video is playing. Defaults to false.
|
||||
* @param resizeMode - How the video should be resized to fit the view. Defaults to 'none'.
|
||||
*/
|
||||
const VideoView = forwardRef<VideoViewRef, VideoViewProps>(
|
||||
(
|
||||
{
|
||||
player: nPlayer,
|
||||
controls = false,
|
||||
resizeMode = "none",
|
||||
// auto pip is unsupported
|
||||
pictureInPicture = false,
|
||||
autoEnterPictureInPicture = false,
|
||||
keepScreenAwake = true,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const player = nPlayer as unknown as VideoPlayer;
|
||||
const vRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const videoElement = player.__getNativeRef();
|
||||
vRef.current?.appendChild(videoElement);
|
||||
return () => {
|
||||
vRef.current?.removeChild(videoElement);
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
enterFullscreen: () => {
|
||||
player.__getNativeRef().requestFullscreen({ navigationUI: "hide" });
|
||||
},
|
||||
exitFullscreen: () => {
|
||||
document.exitFullscreen();
|
||||
},
|
||||
enterPictureInPicture: () => {
|
||||
player.__getNativeRef().requestPictureInPicture();
|
||||
},
|
||||
exitPictureInPicture: () => {
|
||||
document.exitPictureInPicture();
|
||||
},
|
||||
canEnterPictureInPicture: () => document.pictureInPictureEnabled,
|
||||
}),
|
||||
[player],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
player.__getNativeRef().controls = controls;
|
||||
}, [player, controls]);
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<div
|
||||
ref={vRef}
|
||||
style={{ objectFit: resizeMode === "stretch" ? "fill" : resizeMode }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
VideoView.displayName = "VideoView";
|
||||
|
||||
export default memo(VideoView);
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { ViewProps, ViewStyle } from "react-native";
|
||||
import type { SurfaceType } from "../../spec/nitro/VideoViewViewManager.nitro";
|
||||
import type { VideoViewEvents } from "../types/Events";
|
||||
import type { ResizeMode } from "../types/ResizeMode";
|
||||
import type { VideoPlayer } from "../VideoPlayer";
|
||||
|
||||
export interface VideoViewProps extends Partial<VideoViewEvents>, ViewProps {
|
||||
/**
|
||||
* The player to play the video - {@link VideoPlayer}
|
||||
*/
|
||||
player: VideoPlayer;
|
||||
/**
|
||||
* The style of the video view - {@link ViewStyle}
|
||||
*/
|
||||
style?: ViewStyle;
|
||||
/**
|
||||
* Whether to show the controls. Defaults to false.
|
||||
*/
|
||||
controls?: boolean;
|
||||
/**
|
||||
* Whether to enable & show the picture in picture button in native controls. Defaults to false.
|
||||
*/
|
||||
pictureInPicture?: boolean;
|
||||
/**
|
||||
* Whether to automatically enter picture in picture mode when the video is playing. Defaults to false.
|
||||
*/
|
||||
autoEnterPictureInPicture?: boolean;
|
||||
/**
|
||||
* How the video should be resized to fit the view. Defaults to 'none'.
|
||||
* - 'contain': Scale the video uniformly (maintain aspect ratio) so that it fits entirely within the view
|
||||
* - 'cover': Scale the video uniformly (maintain aspect ratio) so that it fills the entire view (may crop)
|
||||
* - 'stretch': Scale the video to fill the entire view without maintaining aspect ratio
|
||||
* - 'none': Do not resize the video
|
||||
*/
|
||||
resizeMode?: ResizeMode;
|
||||
/**
|
||||
* Whether to keep the screen awake while the video view is mounted. Defaults to true.
|
||||
*/
|
||||
keepScreenAwake?: boolean;
|
||||
|
||||
/**
|
||||
* The type of underlying native view. Defaults to 'surface'.
|
||||
* - 'surface': Uses a SurfaceView on Android. More performant, but cannot be animated or transformed.
|
||||
* - 'texture': Uses a TextureView on Android. Less performant, but can be animated and transformed.
|
||||
*
|
||||
* Only applicable on Android
|
||||
*
|
||||
* @default 'surface'
|
||||
* @platform android
|
||||
*/
|
||||
surfaceType?: SurfaceType;
|
||||
}
|
||||
|
||||
export interface VideoViewRef {
|
||||
/**
|
||||
* Enter fullscreen mode
|
||||
*/
|
||||
enterFullscreen: () => void;
|
||||
/**
|
||||
* Exit fullscreen mode
|
||||
*/
|
||||
exitFullscreen: () => void;
|
||||
/**
|
||||
* Enter picture in picture mode
|
||||
*/
|
||||
enterPictureInPicture: () => void;
|
||||
/**
|
||||
* Exit picture in picture mode
|
||||
*/
|
||||
exitPictureInPicture: () => void;
|
||||
/**
|
||||
* Check if picture in picture mode is supported
|
||||
* @returns true if picture in picture mode is supported, false otherwise
|
||||
*/
|
||||
canEnterPictureInPicture: () => boolean;
|
||||
}
|
||||
@@ -1,26 +1,27 @@
|
||||
export { useEvent } from './core/hooks/useEvent';
|
||||
export { useVideoPlayer } from './core/hooks/useVideoPlayer';
|
||||
export * from './core/types/Events';
|
||||
export type { IgnoreSilentSwitchMode } from './core/types/IgnoreSilentSwitchMode';
|
||||
export type { MixAudioMode } from './core/types/MixAudioMode';
|
||||
export type { ResizeMode } from './core/types/ResizeMode';
|
||||
export type { TextTrack } from './core/types/TextTrack';
|
||||
export type { VideoConfig, VideoSource } from './core/types/VideoConfig';
|
||||
export {
|
||||
type LibraryError,
|
||||
type PlayerError,
|
||||
type SourceError,
|
||||
type UnknownError,
|
||||
type VideoComponentError,
|
||||
type VideoError,
|
||||
type VideoErrorCode,
|
||||
type VideoRuntimeError,
|
||||
type VideoViewError,
|
||||
} from './core/types/VideoError';
|
||||
export type { VideoPlayerStatus } from './core/types/VideoPlayerStatus';
|
||||
export {
|
||||
default as VideoView,
|
||||
type VideoViewProps,
|
||||
type VideoViewRef,
|
||||
} from './core/video-view/VideoView';
|
||||
export { VideoPlayer } from './core/VideoPlayer';
|
||||
export { useEvent } from "./core/hooks/useEvent";
|
||||
export { useVideoPlayer } from "./core/hooks/useVideoPlayer";
|
||||
export type * from "./core/types/Events";
|
||||
export type { IgnoreSilentSwitchMode } from "./core/types/IgnoreSilentSwitchMode";
|
||||
export type { MixAudioMode } from "./core/types/MixAudioMode";
|
||||
export type { ResizeMode } from "./core/types/ResizeMode";
|
||||
export type { TextTrack } from "./core/types/TextTrack";
|
||||
export type { VideoConfig, VideoSource } from "./core/types/VideoConfig";
|
||||
export type {
|
||||
LibraryError,
|
||||
PlayerError,
|
||||
SourceError,
|
||||
UnknownError,
|
||||
VideoComponentError,
|
||||
VideoError,
|
||||
VideoErrorCode,
|
||||
VideoRuntimeError,
|
||||
VideoViewError,
|
||||
} from "./core/types/VideoError";
|
||||
export type { VideoPlayerStatus } from "./core/types/VideoPlayerStatus";
|
||||
|
||||
export { VideoPlayer } from "./core/VideoPlayer";
|
||||
export { default as VideoView } from "./core/video-view/VideoView";
|
||||
export type {
|
||||
VideoViewProps,
|
||||
VideoViewRef,
|
||||
} from "./core/video-view/ViewViewProps";
|
||||
|
||||
@@ -13,6 +13,16 @@ export interface VideoPlayer
|
||||
// Holder of the video player events.
|
||||
readonly eventEmitter: VideoPlayerEventEmitter;
|
||||
|
||||
/**
|
||||
* Show playback controls in the notifications area
|
||||
*
|
||||
* @note on Android, this can be overridden by {@linkcode VideoPlayer.playInBackground}, as Android requires
|
||||
* a foreground service to show notifications while the app is in the background.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
showNotificationControls: boolean;
|
||||
|
||||
replaceSourceAsync(source: VideoPlayerSource | null): Promise<void>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,14 +18,9 @@ cd ../..
|
||||
|
||||
echo "[DRM Plugin] Publishing drm plugin"
|
||||
|
||||
read -p "[DRM Plugin] Do you want to release the DRM plugin? (y/n): " confirm
|
||||
if [[ $confirm == "y" || $confirm == "Y" ]]; then
|
||||
cd packages/drm-plugin
|
||||
bun run release $@
|
||||
cd ../..
|
||||
else
|
||||
echo "[DRM Plugin] Skipping DRM plugin release."
|
||||
fi
|
||||
cd packages/drm-plugin
|
||||
bun run release $@
|
||||
cd ../..
|
||||
|
||||
echo "[React Native Video] Making Github Release"
|
||||
bun run release:github $@
|
||||
|
||||
Reference in New Issue
Block a user