mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6d544591 | ||
|
|
456f903f97 | ||
|
|
af3e96de78 | ||
|
|
fe51950bcf | ||
|
|
cd4ad50604 | ||
|
|
7578fe944e | ||
|
|
1789d00fdc | ||
|
|
49f08137f0 | ||
|
|
153b492e0a |
+9
-10
@@ -5,7 +5,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:8.8.0"
|
classpath "com.android.tools.build:gradle:9.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,16 +143,15 @@ dependencies {
|
|||||||
// Add a dependency on NitroModules
|
// Add a dependency on NitroModules
|
||||||
implementation project(":react-native-nitro-modules")
|
implementation project(":react-native-nitro-modules")
|
||||||
|
|
||||||
implementation("org.videolan.android:libvlc-all:4.0.0-eap24")
|
implementation("org.videolan.android:libvlc-all:4.0.0-eap29")
|
||||||
implementation("androidx.media3:media3-common:1.4.1")
|
implementation("androidx.media3:media3-common:1.11.0")
|
||||||
implementation("androidx.media3:media3-session:1.4.1")
|
implementation("androidx.media3:media3-session:1.11.0")
|
||||||
implementation("androidx.media3:media3-ui:1.4.1")
|
implementation("androidx.media3:media3-ui:1.11.0")
|
||||||
implementation 'androidx.media3:media3-exoplayer:1.10.0'
|
implementation 'androidx.media3:media3-exoplayer:1.11.0'
|
||||||
implementation("androidx.media3:media3-exoplayer-hls:1.10.0")
|
implementation("androidx.media3:media3-exoplayer-hls:1.11.0")
|
||||||
|
implementation("androidx.media3:media3-cast:1.11.0")
|
||||||
implementation("androidx.media3:media3-cast:1.10.0")
|
|
||||||
implementation("com.google.android.gms:play-services-cast-framework:22.3.1")
|
implementation("com.google.android.gms:play-services-cast-framework:22.3.1")
|
||||||
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
implementation("androidx.mediarouter:mediarouter:1.8.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isNewArchitectureEnabled()) {
|
if (isNewArchitectureEnabled()) {
|
||||||
|
|||||||
@@ -5,12 +5,9 @@ import androidx.media3.common.C
|
|||||||
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
import androidx.media3.common.C.TRACK_TYPE_AUDIO
|
||||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||||
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
import androidx.media3.common.C.TRACK_TYPE_VIDEO
|
||||||
import androidx.media3.common.MediaItem
|
|
||||||
import androidx.media3.common.PlaybackException
|
import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.PlaybackParameters
|
import androidx.media3.common.PlaybackParameters
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
|
|
||||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_SEEK
|
|
||||||
import androidx.media3.common.Player.STATE_BUFFERING
|
import androidx.media3.common.Player.STATE_BUFFERING
|
||||||
import androidx.media3.common.Player.STATE_ENDED
|
import androidx.media3.common.Player.STATE_ENDED
|
||||||
import androidx.media3.common.Player.STATE_IDLE
|
import androidx.media3.common.Player.STATE_IDLE
|
||||||
@@ -59,7 +56,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
|||||||
private val castStatusListeners = mutableSetOf<(CastStatus) -> Unit>()
|
private val castStatusListeners = mutableSetOf<(CastStatus) -> Unit>()
|
||||||
private val sourceListeners = mutableSetOf<(Source?) -> Unit>()
|
private val sourceListeners = mutableSetOf<(Source?) -> Unit>()
|
||||||
|
|
||||||
private var lastMediaItemIndex = 0
|
|
||||||
private var lastRendition: Rendition? = null
|
private var lastRendition: Rendition? = null
|
||||||
private var lastRenditions: Array<Rendition>? = null
|
private var lastRenditions: Array<Rendition>? = null
|
||||||
private var lastIsAutoQuality: Boolean? = null
|
private var lastIsAutoQuality: Boolean? = null
|
||||||
@@ -73,7 +69,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
|||||||
_player?.removeListener(this)
|
_player?.removeListener(this)
|
||||||
_player = value
|
_player = value
|
||||||
value.addListener(this)
|
value.addListener(this)
|
||||||
lastMediaItemIndex = value.currentMediaItemIndex
|
|
||||||
lastRendition = null
|
lastRendition = null
|
||||||
lastRenditions = null
|
lastRenditions = null
|
||||||
lastIsAutoQuality = null
|
lastIsAutoQuality = null
|
||||||
@@ -116,6 +111,14 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
|||||||
sourceListeners.forEach { it(source) }
|
sourceListeners.forEach { it(source) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun emitPrev() {
|
||||||
|
onPrevListeners.forEach { it() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitNext() {
|
||||||
|
onNextListeners.forEach { it() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun selectedTrack(trackType: Int): Track? {
|
private fun selectedTrack(trackType: Int): Track? {
|
||||||
val list = when (trackType) {
|
val list = when (trackType) {
|
||||||
TRACK_TYPE_VIDEO -> tracks.videos
|
TRACK_TYPE_VIDEO -> tracks.videos
|
||||||
@@ -278,20 +281,6 @@ class EventMap(private val tracks: TrackProvider) : HybridOmniEventMapSpec(), Pl
|
|||||||
playerStatusListeners.forEach { it(PlayerStatus.ERROR) }
|
playerStatusListeners.forEach { it(PlayerStatus.ERROR) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
||||||
if (reason != MEDIA_ITEM_TRANSITION_REASON_AUTO && reason != MEDIA_ITEM_TRANSITION_REASON_SEEK) {
|
|
||||||
lastMediaItemIndex = player.currentMediaItemIndex
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val newIndex = player.currentMediaItemIndex
|
|
||||||
if (newIndex < lastMediaItemIndex) {
|
|
||||||
onPrevListeners.forEach { it() }
|
|
||||||
} else if (newIndex > lastMediaItemIndex) {
|
|
||||||
onNextListeners.forEach { it() }
|
|
||||||
}
|
|
||||||
lastMediaItemIndex = newIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPositionDiscontinuity(
|
override fun onPositionDiscontinuity(
|
||||||
oldPosition: Player.PositionInfo,
|
oldPosition: Player.PositionInfo,
|
||||||
newPosition: Player.PositionInfo,
|
newPosition: Player.PositionInfo,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package dev.zoriya.omni
|
||||||
|
|
||||||
|
import androidx.annotation.OptIn
|
||||||
|
import androidx.media3.common.ForwardingSimpleBasePlayer
|
||||||
|
import androidx.media3.common.Player
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import com.google.common.util.concurrent.Futures
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
class NavigationPlayer(
|
||||||
|
player: Player,
|
||||||
|
private val hasPrev: () -> Boolean,
|
||||||
|
private val hasNext: () -> Boolean,
|
||||||
|
private val onPrev: () -> Unit,
|
||||||
|
private val onNext: () -> Unit,
|
||||||
|
) : ForwardingSimpleBasePlayer(player) {
|
||||||
|
override fun getState(): State {
|
||||||
|
val state = super.getState()
|
||||||
|
return state.buildUpon()
|
||||||
|
.setAvailableCommands(
|
||||||
|
state.availableCommands.buildUpon()
|
||||||
|
.addIf(Player.COMMAND_SEEK_TO_PREVIOUS, hasPrev())
|
||||||
|
.addIf(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, hasPrev())
|
||||||
|
.addIf(Player.COMMAND_SEEK_TO_NEXT, hasNext())
|
||||||
|
.addIf(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, hasNext())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSeek(
|
||||||
|
mediaItemIndex: Int,
|
||||||
|
positionMs: Long,
|
||||||
|
seekCommand: Int,
|
||||||
|
): ListenableFuture<*> = when (seekCommand) {
|
||||||
|
Player.COMMAND_SEEK_TO_PREVIOUS,
|
||||||
|
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM -> {
|
||||||
|
onPrev()
|
||||||
|
Futures.immediateVoidFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
Player.COMMAND_SEEK_TO_NEXT,
|
||||||
|
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM -> {
|
||||||
|
onNext()
|
||||||
|
Futures.immediateVoidFuture()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.handleSeek(mediaItemIndex, positionMs, seekCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package dev.zoriya.omni
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.google.android.gms.cast.CastMediaControlIntent
|
import com.google.android.gms.cast.CastMediaControlIntent
|
||||||
import com.google.android.gms.cast.framework.CastOptions
|
import com.google.android.gms.cast.framework.CastOptions
|
||||||
|
import com.google.android.gms.cast.framework.media.CastMediaOptions
|
||||||
import com.google.android.gms.cast.framework.OptionsProvider
|
import com.google.android.gms.cast.framework.OptionsProvider
|
||||||
import com.google.android.gms.cast.framework.SessionProvider
|
import com.google.android.gms.cast.framework.SessionProvider
|
||||||
|
|
||||||
@@ -11,9 +12,15 @@ class OmniCastOptionsProvider : OptionsProvider {
|
|||||||
val appId = OmniPlayer.receiverApplicationId
|
val appId = OmniPlayer.receiverApplicationId
|
||||||
?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
|
?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
|
||||||
|
|
||||||
|
val mediaOptions = CastMediaOptions.Builder()
|
||||||
|
.setMediaSessionEnabled(false)
|
||||||
|
.setNotificationOptions(null)
|
||||||
|
.build()
|
||||||
|
|
||||||
return CastOptions.Builder()
|
return CastOptions.Builder()
|
||||||
.setReceiverApplicationId(appId)
|
.setReceiverApplicationId(appId)
|
||||||
.setShowSystemOutputSwitcherOnCastIconClick(true)
|
.setShowSystemOutputSwitcherOnCastIconClick(true)
|
||||||
|
.setCastMediaOptions(mediaOptions)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,13 +60,18 @@ class OmniCastTrackSelector : CastTrackSelector() {
|
|||||||
val overrideMatch = indices.firstOrNull { i ->
|
val overrideMatch = indices.firstOrNull { i ->
|
||||||
overrides.keys.any { it.id == groups[i].id }
|
overrides.keys.any { it.id == groups[i].id }
|
||||||
}
|
}
|
||||||
val match = indices.firstOrNull { i ->
|
val matchesLabel = { i: Int ->
|
||||||
val name: String? = tracks[i].name
|
val name: String? = tracks[i].name
|
||||||
val language: String? = tracks[i].language
|
labels.isNotEmpty() && name != null && name in labels
|
||||||
(labels.isNotEmpty() && name != null && name in labels) ||
|
|
||||||
(languages.isNotEmpty() && language != null &&
|
|
||||||
Util.normalizeLanguageCode(language) in languages)
|
|
||||||
}
|
}
|
||||||
|
val matchesLanguage = { i: Int ->
|
||||||
|
val language: String? = tracks[i].language
|
||||||
|
languages.isNotEmpty() && language != null &&
|
||||||
|
Util.normalizeLanguageCode(language) in languages
|
||||||
|
}
|
||||||
|
val match = indices.firstOrNull { matchesLabel(it) && matchesLanguage(it) }
|
||||||
|
?: indices.firstOrNull { matchesLabel(it) }
|
||||||
|
?: indices.firstOrNull { matchesLanguage(it) }
|
||||||
val chosen = when {
|
val chosen = when {
|
||||||
overrideMatch != null -> overrideMatch
|
overrideMatch != null -> overrideMatch
|
||||||
match != null -> match
|
match != null -> match
|
||||||
|
|||||||
@@ -24,8 +24,15 @@ class OmniMediaItemConverter : MediaItemConverter {
|
|||||||
builder.setMediaMetadata(
|
builder.setMediaMetadata(
|
||||||
androidx.media3.common.MediaMetadata.Builder()
|
androidx.media3.common.MediaMetadata.Builder()
|
||||||
.setTitle(md.getString(CastMediaMetadata.KEY_TITLE))
|
.setTitle(md.getString(CastMediaMetadata.KEY_TITLE))
|
||||||
.setArtist(md.getString(CastMediaMetadata.KEY_ARTIST))
|
.setArtist(
|
||||||
.setAlbumTitle(md.getString(CastMediaMetadata.KEY_ALBUM_TITLE))
|
md.getString(CastMediaMetadata.KEY_ARTIST)
|
||||||
|
?: md.getString(CastMediaMetadata.KEY_SUBTITLE)
|
||||||
|
)
|
||||||
|
.setAlbumTitle(
|
||||||
|
md.getString(CastMediaMetadata.KEY_ALBUM_TITLE)
|
||||||
|
?: md.getString(CastMediaMetadata.KEY_STUDIO)
|
||||||
|
)
|
||||||
|
.setArtworkUri(md.images.firstOrNull()?.url)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package dev.zoriya.omni
|
package dev.zoriya.omni
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Looper
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -13,11 +15,16 @@ import androidx.media3.common.AudioAttributes
|
|||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.MimeTypes
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.datasource.DefaultDataSource
|
import androidx.media3.datasource.DefaultDataSource
|
||||||
import androidx.media3.datasource.DefaultHttpDataSource
|
import androidx.media3.datasource.DefaultHttpDataSource
|
||||||
|
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
|
import androidx.media3.exoplayer.Renderer
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||||
|
import androidx.media3.exoplayer.text.TextOutput
|
||||||
|
import androidx.media3.exoplayer.text.TextRenderer
|
||||||
import com.margelo.nitro.NitroModules
|
import com.margelo.nitro.NitroModules
|
||||||
import com.margelo.nitro.omni.AndroidBackend
|
import com.margelo.nitro.omni.AndroidBackend
|
||||||
import com.margelo.nitro.omni.CastStatus
|
import com.margelo.nitro.omni.CastStatus
|
||||||
@@ -62,9 +69,26 @@ class OmniPlayer(
|
|||||||
AndroidBackend.EXOPLAYER -> {
|
AndroidBackend.EXOPLAYER -> {
|
||||||
val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
|
val http = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
|
||||||
httpDataSourceFactory = http
|
httpDataSourceFactory = http
|
||||||
ExoPlayer.Builder(ctx)
|
// karaoke subs make exoplayer crash without this...
|
||||||
|
val renderersFactory = object : DefaultRenderersFactory(ctx) {
|
||||||
|
override fun buildTextRenderers(
|
||||||
|
context: Context,
|
||||||
|
output: TextOutput,
|
||||||
|
outputLooper: Looper,
|
||||||
|
extensionRendererMode: Int,
|
||||||
|
out: ArrayList<Renderer>,
|
||||||
|
) {
|
||||||
|
out.add(
|
||||||
|
TextRenderer(output, outputLooper).apply {
|
||||||
|
experimentalSetLegacyDecodingEnabled(true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExoPlayer.Builder(ctx, renderersFactory)
|
||||||
.setMediaSourceFactory(
|
.setMediaSourceFactory(
|
||||||
DefaultMediaSourceFactory(DefaultDataSource.Factory(ctx, http))
|
DefaultMediaSourceFactory(DefaultDataSource.Factory(ctx, http))
|
||||||
|
.experimentalParseSubtitlesDuringExtraction(false)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
@@ -75,8 +99,27 @@ class OmniPlayer(
|
|||||||
override val eventMap = EventMap(this)
|
override val eventMap = EventMap(this)
|
||||||
|
|
||||||
private var castContext: CastContext? = null
|
private var castContext: CastContext? = null
|
||||||
private val castStateListener =
|
private val castStateListener = CastStateListener {
|
||||||
CastStateListener { eventMap.emitCastStatus(computeCastStatus()) }
|
eventMap.emitCastStatus(computeCastStatus())
|
||||||
|
listenToReceiver()
|
||||||
|
}
|
||||||
|
|
||||||
|
// receivers have no queue to skip in (a queue would make them play things on
|
||||||
|
// their own), so they ask us to do it when something else - google home, the
|
||||||
|
// assistant, ... - requests a prev/next. same path as our own buttons.
|
||||||
|
private fun listenToReceiver() {
|
||||||
|
val session = castContext?.sessionManager?.currentCastSession ?: return
|
||||||
|
try {
|
||||||
|
session.setMessageReceivedCallbacks(OMNI_NAMESPACE) { _, _, message ->
|
||||||
|
when (JSONObject(message).optString("action")) {
|
||||||
|
"prev" -> runOnMainThread { eventMap.emitPrev() }
|
||||||
|
"next" -> runOnMainThread { eventMap.emitNext() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
android.util.Log.w("OmniPlayer", "could not listen to the receiver", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val player: Player = runOnMainThreadSync {
|
val player: Player = runOnMainThreadSync {
|
||||||
castOptions?.receiverApplicationId?.let { receiverApplicationId = it }
|
castOptions?.receiverApplicationId?.let { receiverApplicationId = it }
|
||||||
@@ -100,7 +143,13 @@ class OmniPlayer(
|
|||||||
.setRemotePlayer(remote)
|
.setRemotePlayer(remote)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
active
|
NavigationPlayer(
|
||||||
|
active,
|
||||||
|
hasPrev = { source?.metadata?.hasPrev == true },
|
||||||
|
hasNext = { source?.metadata?.hasNext == true },
|
||||||
|
onPrev = { eventMap.emitPrev() },
|
||||||
|
onNext = { eventMap.emitNext() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override var showNotification: Boolean? = false
|
override var showNotification: Boolean? = false
|
||||||
@@ -116,19 +165,19 @@ class OmniPlayer(
|
|||||||
when {
|
when {
|
||||||
shouldShow && !serviceRunning -> {
|
shouldShow && !serviceRunning -> {
|
||||||
val otherIsPlaying = notificationPlayer?.let { other ->
|
val otherIsPlaying = notificationPlayer?.let { other ->
|
||||||
other !== localPlayer && runOnMainThreadSync { other.isPlaying }
|
other !== player && runOnMainThreadSync { other.isPlaying }
|
||||||
} == true
|
} == true
|
||||||
if (otherIsPlaying) {
|
if (otherIsPlaying) {
|
||||||
throw Error("Two players can't display notifications at the same time.")
|
throw Error("Two players can't display notifications at the same time.")
|
||||||
}
|
}
|
||||||
notificationPlayer = localPlayer
|
notificationPlayer = player
|
||||||
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
||||||
serviceRunning = true
|
serviceRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
!shouldShow && serviceRunning -> {
|
!shouldShow && serviceRunning -> {
|
||||||
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
||||||
if (notificationPlayer == localPlayer) notificationPlayer = null
|
if (notificationPlayer == player) notificationPlayer = null
|
||||||
serviceRunning = false
|
serviceRunning = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +236,19 @@ class OmniPlayer(
|
|||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inferSubtitleMimeType(link: String): String? {
|
||||||
|
val ext = link.toUri().lastPathSegment?.substringAfterLast('.', "")?.lowercase()
|
||||||
|
return when (ext) {
|
||||||
|
"vtt", "webvtt" -> MimeTypes.TEXT_VTT
|
||||||
|
"srt" -> MimeTypes.APPLICATION_SUBRIP
|
||||||
|
"ass", "ssa" -> MimeTypes.TEXT_SSA
|
||||||
|
"ttml", "dfxp", "xml" -> MimeTypes.APPLICATION_TTML
|
||||||
|
"sup" -> MimeTypes.APPLICATION_PGS
|
||||||
|
"sub" -> MimeTypes.APPLICATION_VOBSUB
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildMediaItem(
|
private fun buildMediaItem(
|
||||||
src: com.margelo.nitro.omni.VideoSrc,
|
src: com.margelo.nitro.omni.VideoSrc,
|
||||||
metadata: com.margelo.nitro.omni.Metadata?,
|
metadata: com.margelo.nitro.omni.Metadata?,
|
||||||
@@ -212,7 +274,10 @@ class OmniPlayer(
|
|||||||
.setId(subtitle.id)
|
.setId(subtitle.id)
|
||||||
.setLanguage(subtitle.language)
|
.setLanguage(subtitle.language)
|
||||||
.setLabel(subtitle.label)
|
.setLabel(subtitle.label)
|
||||||
.setMimeType(subtitle.mimeType)
|
// exoplayer requires a mime type to pick a subtitle parser.
|
||||||
|
// fall back to inferring it from the link's extension so
|
||||||
|
// consumers don't have to provide one.
|
||||||
|
.setMimeType(subtitle.mimeType ?: inferSubtitleMimeType(subtitle.link))
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.setRequestMetadata(
|
.setRequestMetadata(
|
||||||
@@ -258,8 +323,8 @@ class OmniPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val hasPrev: Boolean get() = player.hasPreviousMediaItem()
|
override val hasPrev: Boolean get() = source?.metadata?.hasPrev == true
|
||||||
override val hasNext: Boolean get() = player.hasNextMediaItem()
|
override val hasNext: Boolean get() = source?.metadata?.hasNext == true
|
||||||
override val status by mainThreadProperty {
|
override val status by mainThreadProperty {
|
||||||
when (player.playbackState) {
|
when (player.playbackState) {
|
||||||
Player.STATE_IDLE,
|
Player.STATE_IDLE,
|
||||||
@@ -377,15 +442,10 @@ class OmniPlayer(
|
|||||||
value.castId,
|
value.castId,
|
||||||
value.castData,
|
value.castData,
|
||||||
)
|
)
|
||||||
val mediaItems = mutableListOf<MediaItem>()
|
|
||||||
if (value.metadata?.hasPrev == true) mediaItems.add(currentItem)
|
|
||||||
mediaItems.add(currentItem)
|
|
||||||
if (value.metadata?.hasNext == true) mediaItems.add(currentItem)
|
|
||||||
|
|
||||||
runOnMainThreadSync {
|
runOnMainThreadSync {
|
||||||
val startIndex = if (value.metadata?.hasPrev == true) 1 else 0
|
|
||||||
val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0
|
val startPositionMs = ((value.startTime ?: 0.0).coerceAtLeast(0.0)) * 1000.0
|
||||||
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
player.setMediaItems(listOf(currentItem), 0, startPositionMs.toLong())
|
||||||
player.prepare()
|
player.prepare()
|
||||||
}
|
}
|
||||||
syncNotificationService()
|
syncNotificationService()
|
||||||
@@ -407,11 +467,11 @@ class OmniPlayer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun playPrev() {
|
override fun playPrev() {
|
||||||
runOnMainThreadSync { player.seekToPreviousMediaItem() }
|
runOnMainThreadSync { eventMap.emitPrev() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun playNext() {
|
override fun playNext() {
|
||||||
runOnMainThreadSync { player.seekToNextMediaItem() }
|
runOnMainThreadSync { eventMap.emitNext() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun selectVideo(video: Track) {
|
override fun selectVideo(video: Track) {
|
||||||
@@ -441,15 +501,15 @@ class OmniPlayer(
|
|||||||
}?.mediaTrackGroup
|
}?.mediaTrackGroup
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
params.setOverrideForType(TrackSelectionOverride(group, 0))
|
params.setOverrideForType(TrackSelectionOverride(group, 0))
|
||||||
|
}
|
||||||
|
// also set preferred flags for cast selection
|
||||||
|
val labels = track.label?.let { arrayOf(it) } ?: emptyArray()
|
||||||
|
if (type == C.TRACK_TYPE_AUDIO) {
|
||||||
|
params.setPreferredAudioLanguage(track.language)
|
||||||
|
.setPreferredAudioLabels(*labels)
|
||||||
} else {
|
} else {
|
||||||
val labels = track.label?.let { arrayOf(it) } ?: emptyArray()
|
params.setPreferredTextLanguage(track.language)
|
||||||
if (type == C.TRACK_TYPE_AUDIO) {
|
.setPreferredTextLabels(*labels)
|
||||||
params.setPreferredAudioLanguage(track.language)
|
|
||||||
.setPreferredAudioLabels(*labels)
|
|
||||||
} else {
|
|
||||||
params.setPreferredTextLanguage(track.language)
|
|
||||||
.setPreferredTextLabels(*labels)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
player.trackSelectionParameters = params.build()
|
player.trackSelectionParameters = params.build()
|
||||||
@@ -523,6 +583,10 @@ class OmniPlayer(
|
|||||||
const val CAST_ID_EXTRA = "dev.zoriya.omni.castId"
|
const val CAST_ID_EXTRA = "dev.zoriya.omni.castId"
|
||||||
const val CAST_DATA_EXTRA = "dev.zoriya.omni.castData"
|
const val CAST_DATA_EXTRA = "dev.zoriya.omni.castData"
|
||||||
|
|
||||||
|
// channel a receiver uses to ask us to play the prev/next media.
|
||||||
|
// it must declare it too (`customNamespaces` in the caf options).
|
||||||
|
const val OMNI_NAMESPACE = "urn:x-cast:dev.zoriya.omni"
|
||||||
|
|
||||||
// Receiver application id passed at runtime via OmniProvider's `cast`
|
// Receiver application id passed at runtime via OmniProvider's `cast`
|
||||||
// prop; read by OmniCastOptionsProvider when the Cast SDK initializes.
|
// prop; read by OmniCastOptionsProvider when the Cast SDK initializes.
|
||||||
var receiverApplicationId: String? = null
|
var receiverApplicationId: String? = null
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
if (isPlaying) scheduleVideoRebuild()
|
||||||
updatePictureInPictureParams()
|
updatePictureInPictureParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,22 +378,16 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
movedSurfaceToRootForPip = false
|
movedSurfaceToRootForPip = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// A surface that is destroyed and recreated while playback runs (the PIP
|
private val rebuildRunnable = Runnable { boundPlayer?.rebuildVideoOutput() }
|
||||||
// reparent) makes VLC drop its video pipeline; it won't rebuild it for the
|
|
||||||
// new surface on its own, so we must ask it to. The rebuild only succeeds
|
private fun scheduleVideoRebuild() {
|
||||||
// once the (PIP) window has settled at its final size — firing it mid-resize
|
surfaceView.removeCallbacks(rebuildRunnable)
|
||||||
// leaves the vout stopped — so it is debounced off surfaceChanged.
|
surfaceView.postDelayed(rebuildRunnable, 200)
|
||||||
private var pendingVideoRebuild = false
|
|
||||||
private val rebuildRunnable = Runnable {
|
|
||||||
if (!pendingVideoRebuild) return@Runnable
|
|
||||||
pendingVideoRebuild = false
|
|
||||||
boundPlayer?.rebuildVideoOutput()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
surfaceReady = true
|
surfaceReady = true
|
||||||
boundPlayer?.setVideoView(surfaceView)
|
boundPlayer?.setVideoView(surfaceView)
|
||||||
if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true
|
|
||||||
updatePictureInPictureParams()
|
updatePictureInPictureParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,10 +398,7 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
height: Int
|
height: Int
|
||||||
) {
|
) {
|
||||||
boundPlayer?.updateVideoLayout(width, height)
|
boundPlayer?.updateVideoLayout(width, height)
|
||||||
if (pendingVideoRebuild) {
|
scheduleVideoRebuild()
|
||||||
surfaceView.removeCallbacks(rebuildRunnable)
|
|
||||||
surfaceView.postDelayed(rebuildRunnable, 200)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import org.videolan.libvlc.MediaPlayer
|
|||||||
import org.videolan.libvlc.interfaces.IMedia
|
import org.videolan.libvlc.interfaces.IMedia
|
||||||
import org.videolan.libvlc.interfaces.IMedia.VideoTrack
|
import org.videolan.libvlc.interfaces.IMedia.VideoTrack
|
||||||
import org.videolan.libvlc.interfaces.IVLCVout
|
import org.videolan.libvlc.interfaces.IVLCVout
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
class VlcPlayer(ctx: Context) :
|
class VlcPlayer(ctx: Context) :
|
||||||
@@ -106,6 +107,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
@Volatile
|
@Volatile
|
||||||
private var cachedBufferedPosition: Long = 0L
|
private var cachedBufferedPosition: Long = 0L
|
||||||
private var boundSurfaceView: SurfaceView? = null
|
private var boundSurfaceView: SurfaceView? = null
|
||||||
|
private var videoOutputStale = false
|
||||||
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
|
private var lastVideoSize: VideoSize = VideoSize.UNKNOWN
|
||||||
|
|
||||||
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
private val availableCommands: Player.Commands = Player.Commands.Builder()
|
||||||
@@ -347,6 +349,7 @@ class VlcPlayer(ctx: Context) :
|
|||||||
playerError = null
|
playerError = null
|
||||||
|
|
||||||
player.stop()
|
player.stop()
|
||||||
|
videoOutputStale = false
|
||||||
|
|
||||||
mediaItems.getOrNull(targetIndex)?.let { item ->
|
mediaItems.getOrNull(targetIndex)?.let { item ->
|
||||||
val uri = item.localConfiguration?.uri?.toString()
|
val uri = item.localConfiguration?.uri?.toString()
|
||||||
@@ -387,6 +390,20 @@ class VlcPlayer(ctx: Context) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vlc keys each slave's tracks by the md5 of its uri ("<md5(uri)>/spu/<n>")
|
||||||
|
private fun subtitleSlaveForTrackId(trackId: String): MediaItem.SubtitleConfiguration? {
|
||||||
|
val hash = trackId.substringBefore("/spu/", "")
|
||||||
|
if (hash.isEmpty()) return null
|
||||||
|
return mediaItems.getOrNull(currentMediaItemIndex)
|
||||||
|
?.localConfiguration?.subtitleConfigurations
|
||||||
|
?.firstOrNull { md5(it.uri.toString()) == hash }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun md5(value: String): String =
|
||||||
|
MessageDigest.getInstance("MD5")
|
||||||
|
.digest(value.toByteArray())
|
||||||
|
.joinToString("") { "%02x".format(it.toInt() and 0xFF) }
|
||||||
|
|
||||||
// vlc doesn't allow arbitrary headers :c
|
// vlc doesn't allow arbitrary headers :c
|
||||||
private fun applyRequestHeaders(media: Media, extras: Bundle?) {
|
private fun applyRequestHeaders(media: Media, extras: Bundle?) {
|
||||||
if (extras == null) return
|
if (extras == null) return
|
||||||
@@ -599,10 +616,12 @@ class VlcPlayer(ctx: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.getTracks(IMedia.Track.Type.Text)?.forEach { track ->
|
player.getTracks(IMedia.Track.Type.Text)?.forEach { track ->
|
||||||
|
// external subs don't have label/language, match them back to surface them
|
||||||
|
val slave = subtitleSlaveForTrackId(track.id)
|
||||||
val format = Format.Builder()
|
val format = Format.Builder()
|
||||||
.setId(track.id)
|
.setId(track.id)
|
||||||
.setLabel(track.name)
|
.setLabel(slave?.label ?: track.name)
|
||||||
.setLanguage(track.language)
|
.setLanguage(slave?.language ?: track.language)
|
||||||
.setSampleMimeType("text/x-unknown")
|
.setSampleMimeType("text/x-unknown")
|
||||||
.build()
|
.build()
|
||||||
val group = TrackGroup("vlc-sub-${track.id}", format)
|
val group = TrackGroup("vlc-sub-${track.id}", format)
|
||||||
@@ -880,30 +899,22 @@ class VlcPlayer(ctx: Context) :
|
|||||||
if (surfaceView == null) return clearVideoSurface()
|
if (surfaceView == null) return clearVideoSurface()
|
||||||
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
|
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
|
||||||
boundSurfaceView = surfaceView
|
boundSurfaceView = surfaceView
|
||||||
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view
|
videoOutputStale = (
|
||||||
// size and keeps the video layout correct as the surface resizes (e.g. the
|
player.playerState == IMedia.State.Playing ||
|
||||||
// PIP window shrinking/growing); see updateVideoLayout for size updates.
|
player.playerState == IMedia.State.Paused
|
||||||
|
)
|
||||||
vlcVout.setVideoView(surfaceView)
|
vlcVout.setVideoView(surfaceView)
|
||||||
vlcVout.attachViews()
|
vlcVout.attachViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* VLC tears down its video decoder + output whenever the output Surface is
|
|
||||||
* destroyed (e.g. the SurfaceView being reparented for PIP) and does not
|
|
||||||
* rebuild them when a new Surface arrives — the picture stays black. Toggling
|
|
||||||
* the video track forces VLC to spin up a fresh decoder/vout against the
|
|
||||||
* currently-attached Surface. Safe no-op when nothing is attached/playing.
|
|
||||||
*/
|
|
||||||
fun rebuildVideoOutput() {
|
fun rebuildVideoOutput() {
|
||||||
if (!vlcVout.areViewsAttached() || !player.isPlaying) return
|
if (!videoOutputStale || !player.isPlaying || !vlcVout.areViewsAttached()) return
|
||||||
|
videoOutputStale = false
|
||||||
player.setVideoTrackEnabled(false)
|
player.setVideoTrackEnabled(false)
|
||||||
player.setVideoTrackEnabled(true)
|
player.setVideoTrackEnabled(true)
|
||||||
player.updateVideoSurfaces()
|
player.updateVideoSurfaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recompute the video layout for the current surface size. Must run whenever
|
|
||||||
* the surface resizes (VLC latches a stale geometry otherwise -> the picture
|
|
||||||
* renders at the wrong size, anchored in a corner). */
|
|
||||||
fun updateVideoLayout(width: Int, height: Int) {
|
fun updateVideoLayout(width: Int, height: Int) {
|
||||||
if (!vlcVout.areViewsAttached()) return
|
if (!vlcVout.areViewsAttached()) return
|
||||||
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
|
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
|
||||||
|
|||||||
@@ -6,7 +6,12 @@
|
|||||||
"name": "react-native-omni",
|
"name": "react-native-omni",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config-plugins": "^57.0.3",
|
"@expo/config-plugins": "^57.0.3",
|
||||||
"@videojs/react": "10.0.0-beta.25",
|
"@videojs/core": "github:zoriya/v10#build26-core",
|
||||||
|
"@videojs/media": "github:zoriya/v10#build26-media",
|
||||||
|
"@videojs/react": "github:zoriya/v10#build26-react",
|
||||||
|
"@videojs/spf": "github:zoriya/v10#build26-spf",
|
||||||
|
"@videojs/store": "github:zoriya/v10#build26-store",
|
||||||
|
"@videojs/utils": "github:zoriya/v10#build26-utils",
|
||||||
"jassub": "^2.5.7",
|
"jassub": "^2.5.7",
|
||||||
"libpgs": "^0.8.1",
|
"libpgs": "^0.8.1",
|
||||||
},
|
},
|
||||||
@@ -723,15 +728,17 @@
|
|||||||
|
|
||||||
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
||||||
|
|
||||||
"@videojs/core": ["@videojs/core@10.0.0-beta.25", "", { "dependencies": { "@videojs/spf": "10.0.0-beta.25", "@videojs/store": "10.0.0-beta.25", "@videojs/utils": "10.0.0-beta.25", "@vimeo/player": "^2.30.3", "dashjs": "^5.2.0", "hls.js": "^1.6.7", "mux-embed": "^5.17.10" } }, "sha512-Yxd6c+u7LrYl6aKzZRyR5PjDqUt1lUbgFwIhg6pMzI8bJszIGjLS7TDzXdALpEFJpM1Z08dbqCEzCGaMj7Eu6A=="],
|
"@videojs/core": ["@videojs/core@github:zoriya/v10#02a4769", { "peerDependencies": { "@videojs/media": "*", "@videojs/store": "*", "@videojs/utils": "*" } }, "zoriya-v10-02a4769", "sha512-eq79BQJ+e2Lx6k9tCW68vBolcJD3OynwgE7Y8pmI9gb+RWQAYJFLEY8B576ueKHhcgOOT/taerI/Wg3eqougSg=="],
|
||||||
|
|
||||||
"@videojs/react": ["@videojs/react@10.0.0-beta.25", "", { "dependencies": { "@videojs/core": "10.0.0-beta.25", "@videojs/spf": "10.0.0-beta.25", "@videojs/store": "10.0.0-beta.25", "@videojs/utils": "10.0.0-beta.25" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-MUG8NFntK3u4OTArwqG4utZ11HU1Ku5kzzTfTA2BMG5shNj7KWqPz0BMR2Nnv6hwEvcxpm9vwZxk+AwkmoB5jA=="],
|
"@videojs/media": ["@videojs/media@github:zoriya/v10#46507f9", { "dependencies": { "@vimeo/player": "^2.30.3", "dashjs": "^5.2.0", "hls.js": "^1.6.7", "mux-embed": "^5.17.10" }, "peerDependencies": { "@videojs/spf": "*", "@videojs/utils": "*" } }, "zoriya-v10-46507f9", "sha512-1f7jIP5KPGKo4NeyG+Q22eS7KhvYZam8yvG985Y9edCp7wFcrVcraEQwKJrESaa/GDNgeKIeEg05MZ+l+ZHftQ=="],
|
||||||
|
|
||||||
"@videojs/spf": ["@videojs/spf@10.0.0-beta.25", "", { "dependencies": { "@videojs/utils": "10.0.0-beta.25", "signal-polyfill": "^0.2.2" } }, "sha512-SETOzCPbSxuzCoX+bF2u7/Z80AA/Xwpqpy5ki//yslIyPxWkMT0GFydXdoZ/bJpNsBqGmDbDxEBDo1uMjnM4mQ=="],
|
"@videojs/react": ["@videojs/react@github:zoriya/v10#784a383", { "peerDependencies": { "@videojs/core": "*", "@videojs/media": "*", "@videojs/spf": "*", "@videojs/store": "*", "@videojs/utils": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "zoriya-v10-784a383", "sha512-wLuXUjLfhLNZAVSxCsTmpidN8DZPy6Us788vXDSRJKh/a6WDTCWV2op26EK3eCgeC1P8Hwc5s5cOEhqx2fGMRQ=="],
|
||||||
|
|
||||||
"@videojs/store": ["@videojs/store@10.0.0-beta.25", "", { "dependencies": { "@videojs/utils": "10.0.0-beta.25" }, "peerDependencies": { "@videojs/element": "10.0.0-beta.25", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@videojs/element", "react"] }, "sha512-XvPC0+MQ6AZ/oKGpSzmjOM56aYESMpPsTg5zddBDXhcqeAOUxpEsXDqhU1rsm751tU4ZavGxHENx70lBOa5dUA=="],
|
"@videojs/spf": ["@videojs/spf@github:zoriya/v10#8920135", { "dependencies": { "signal-polyfill": "^0.2.2" }, "peerDependencies": { "@videojs/utils": "*" } }, "zoriya-v10-8920135", "sha512-jGXo5tyPpIk96XWksTKbWpi7POHzdt4KtvqDZ9+SDa+MOqLV3TcgM8soo6KO2jMnkv8d8APqaz+E27qfDf16kg=="],
|
||||||
|
|
||||||
"@videojs/utils": ["@videojs/utils@10.0.0-beta.25", "", {}, "sha512-tq/848qmzgTncopeYE5iiYs1Pfzj4OEdSe7zE2H6xoOB0PUMlnPOThA3aybHJ8Lh9bmw+wTb6QJ0NZanIzt2Zg=="],
|
"@videojs/store": ["@videojs/store@github:zoriya/v10#3f9bddd", { "peerDependencies": { "@videojs/element": "*", "@videojs/utils": "*", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@videojs/element", "react"] }, "zoriya-v10-3f9bddd", "sha512-QjSGJ6EO6uoij41F7KgMKrNi5XjvXfkODOrn17wcFa6+Ig1+ftnckkNZsJ44bARdJ1XUkuj3wNyGidYMrmKRmQ=="],
|
||||||
|
|
||||||
|
"@videojs/utils": ["@videojs/utils@github:zoriya/v10#9fed1b6", {}, "zoriya-v10-9fed1b6", "sha512-ZgbMw3v41aEru0056VVwy0UtiZk8zq6Tm6ddRAJtm5VXz2nEf36CoejFFvmGICW5cQ2WWhvMQmtqCSr+HmYGIw=="],
|
||||||
|
|
||||||
"@vimeo/player": ["@vimeo/player@2.30.4", "", { "dependencies": { "native-promise-only": "0.8.1", "weakmap-polyfill": "2.0.4" } }, "sha512-M8m1UAhJSb+KCWuXDLWHViwj+3YY/0ogwFquRfMHs9e9LYjXT9iB7n+sOCKwUusbiXuU2HKmXx+FEGHtYZfUSg=="],
|
"@vimeo/player": ["@vimeo/player@2.30.4", "", { "dependencies": { "native-promise-only": "0.8.1", "weakmap-polyfill": "2.0.4" } }, "sha512-M8m1UAhJSb+KCWuXDLWHViwj+3YY/0ogwFquRfMHs9e9LYjXT9iB7n+sOCKwUusbiXuU2HKmXx+FEGHtYZfUSg=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
+6
-1
@@ -63,7 +63,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config-plugins": "^57.0.3",
|
"@expo/config-plugins": "^57.0.3",
|
||||||
"@videojs/react": "10.0.0-beta.25",
|
"@videojs/core": "github:zoriya/v10#build26-core",
|
||||||
|
"@videojs/media": "github:zoriya/v10#build26-media",
|
||||||
|
"@videojs/react": "github:zoriya/v10#build26-react",
|
||||||
|
"@videojs/spf": "github:zoriya/v10#build26-spf",
|
||||||
|
"@videojs/store": "github:zoriya/v10#build26-store",
|
||||||
|
"@videojs/utils": "github:zoriya/v10#build26-utils",
|
||||||
"jassub": "^2.5.7",
|
"jassub": "^2.5.7",
|
||||||
"libpgs": "^0.8.1"
|
"libpgs": "^0.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export class WebOmniPlayer implements OmniPlayer {
|
|||||||
this._store.state.toggleRemotePlayback();
|
this._store.state.toggleRemotePlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
castOptions: CastOptions | null = null;
|
|
||||||
_source: Source | undefined = undefined;
|
_source: Source | undefined = undefined;
|
||||||
private _showNotification = false;
|
private _showNotification = false;
|
||||||
|
|
||||||
|
|||||||
+19
-5
@@ -1,4 +1,5 @@
|
|||||||
import { createPlayer } from "@videojs/react";
|
import { createPlayer } from "@videojs/react";
|
||||||
|
import { GoogleCast } from "@videojs/react/media/google-cast";
|
||||||
import { videoFeatures } from "@videojs/react/video";
|
import { videoFeatures } from "@videojs/react/video";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { usePlayerState } from "./events";
|
||||||
import { WebOmniPlayer } from "./player.web";
|
import { WebOmniPlayer } from "./player.web";
|
||||||
import type { OmniPlayer, PlayerBackend } from "./types/player";
|
import type { OmniPlayer, PlayerBackend } from "./types/player";
|
||||||
import type { CastOptions, Source } from "./types/source";
|
import type { CastOptions, Source } from "./types/source";
|
||||||
@@ -67,15 +69,27 @@ const PlayerInitializer = ({
|
|||||||
}
|
}
|
||||||
}, [source, store]);
|
}, [source, store]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
player.castOptions = cast ?? null;
|
|
||||||
}, [cast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
player.showNotification = showNotification;
|
player.showNotification = showNotification;
|
||||||
}, [showNotification]);
|
}, [showNotification]);
|
||||||
|
|
||||||
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
|
return (
|
||||||
|
<PlayerCtx.Provider value={player}>
|
||||||
|
<CastBridge cast={cast} />
|
||||||
|
{children}
|
||||||
|
</PlayerCtx.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CastBridge = ({ cast }: { cast?: CastOptions | null }) => {
|
||||||
|
const source = usePlayerState("source");
|
||||||
|
return (
|
||||||
|
<GoogleCast
|
||||||
|
receiver={cast?.receiverApplicationId}
|
||||||
|
src={source?.castId}
|
||||||
|
customData={source?.castData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePlayer = () => useContext(PlayerCtx);
|
export const usePlayer = () => useContext(PlayerCtx);
|
||||||
|
|||||||
+2
-10
@@ -1,4 +1,4 @@
|
|||||||
import type { HlsMediaConfig } from "@videojs/core/dom/media/hls-js";
|
import type { HlsMediaConfig } from "@videojs/media/dom/hls-js";
|
||||||
import { HlsJsVideo } from "@videojs/react/media/hlsjs-video";
|
import { HlsJsVideo } from "@videojs/react/media/hlsjs-video";
|
||||||
import { Video } from "@videojs/react/video";
|
import { Video } from "@videojs/react/video";
|
||||||
import {
|
import {
|
||||||
@@ -106,7 +106,6 @@ export const OmniView = ({
|
|||||||
autoplay,
|
autoplay,
|
||||||
subtitleAssets,
|
subtitleAssets,
|
||||||
}: OmniViewProps & { style: CSSProperties }) => {
|
}: OmniViewProps & { style: CSSProperties }) => {
|
||||||
const player = usePlayer() as WebOmniPlayer;
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const ref = useRef<HTMLVideoElement>(undefined!);
|
const ref = useRef<HTMLVideoElement>(undefined!);
|
||||||
const source = usePlayerState("source");
|
const source = usePlayerState("source");
|
||||||
@@ -119,8 +118,6 @@ export const OmniView = ({
|
|||||||
|
|
||||||
const headersRef = useRef(source?.src?.headers);
|
const headersRef = useRef(source?.src?.headers);
|
||||||
headersRef.current = source?.src?.headers;
|
headersRef.current = source?.src?.headers;
|
||||||
const castId = source?.castId;
|
|
||||||
const castData = source?.castData;
|
|
||||||
|
|
||||||
const config = useMemo<HlsMediaConfig>(
|
const config = useMemo<HlsMediaConfig>(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -133,13 +130,8 @@ export const OmniView = ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
googleCast: {
|
|
||||||
receiver: player.castOptions?.receiverApplicationId,
|
|
||||||
customData: castData ?? null,
|
|
||||||
...(castId && { src: castId }),
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
[player.castOptions, castData, castId],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// While casting, the receiver renders subtitles (the player forwards the
|
// While casting, the receiver renders subtitles (the player forwards the
|
||||||
|
|||||||
Reference in New Issue
Block a user