mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ce520ee5f | ||
|
|
8107bc4697 | ||
|
|
0c7c2b3848 | ||
|
|
d2ca76b4a9 | ||
|
|
77a8b2337d | ||
|
|
cebe97f168 | ||
|
|
e5615cbc0c | ||
|
|
5f2d74f321 |
@@ -15,6 +15,7 @@ import androidx.media3.common.Player.STATE_BUFFERING
|
||||
import androidx.media3.common.Player.STATE_ENDED
|
||||
import androidx.media3.common.Player.STATE_IDLE
|
||||
import androidx.media3.common.Player.STATE_READY
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.VideoSize
|
||||
import com.margelo.nitro.omni.BoolProperty
|
||||
@@ -23,6 +24,7 @@ import com.margelo.nitro.omni.HybridOmniEventMapSpec
|
||||
import com.margelo.nitro.omni.NumberProperty
|
||||
import com.margelo.nitro.omni.PlayerStatus
|
||||
import com.margelo.nitro.omni.Rendition
|
||||
import com.margelo.nitro.omni.Source
|
||||
import com.margelo.nitro.omni.Track
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
@@ -40,6 +42,7 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
|
||||
private val stateBoolListeners = mutableMapOf<BoolProperty, MutableSet<(Boolean) -> Unit>>()
|
||||
private val playerStatusListeners = mutableSetOf<(PlayerStatus) -> Unit>()
|
||||
private val castStatusListeners = mutableSetOf<(CastStatus) -> Unit>()
|
||||
private val sourceListeners = mutableSetOf<(Source?) -> Unit>()
|
||||
|
||||
private var lastMediaItemIndex = 0
|
||||
private var lastRendition: Rendition? = null
|
||||
@@ -92,6 +95,10 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
|
||||
castStatusListeners.forEach { it(status) }
|
||||
}
|
||||
|
||||
fun emitSource(source: Source?) {
|
||||
sourceListeners.forEach { it(source) }
|
||||
}
|
||||
|
||||
private fun selectedTrack(trackType: Int): Track? {
|
||||
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
||||
for (group in groups) {
|
||||
@@ -203,6 +210,16 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
|
||||
stateListeners[NumberProperty.DURATION]?.forEach {
|
||||
val duration = player.duration
|
||||
it(
|
||||
if (duration == C.TIME_UNSET) 0.0
|
||||
else (duration.toDouble() / 1000.0).coerceAtLeast(0.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
selectedTrack(TRACK_TYPE_VIDEO)?.let { track ->
|
||||
onVideoTrackChangeListeners.forEach { it(track) }
|
||||
@@ -300,6 +317,14 @@ class EventMap() : HybridOmniEventMapSpec(), Player.Listener {
|
||||
castStatusListeners.remove(cb)
|
||||
}
|
||||
|
||||
override fun addSourceListener(cb: (value: Source?) -> Unit) {
|
||||
sourceListeners.add(cb)
|
||||
}
|
||||
|
||||
override fun removeSourceListener(cb: (value: Source?) -> Unit) {
|
||||
sourceListeners.remove(cb)
|
||||
}
|
||||
|
||||
override fun addOnEndListener(cb: () -> Unit) {
|
||||
onEndListeners.add(cb)
|
||||
}
|
||||
|
||||
@@ -106,21 +106,34 @@ class OmniPlayer(
|
||||
|
||||
override var showNotification: Boolean? = false
|
||||
set(value) {
|
||||
if (value == true) {
|
||||
field = value
|
||||
syncNotificationService()
|
||||
}
|
||||
|
||||
private var serviceRunning = false
|
||||
|
||||
private fun syncNotificationService() {
|
||||
val shouldShow = showNotification == true && source != null
|
||||
when {
|
||||
shouldShow && !serviceRunning -> {
|
||||
val otherIsPlaying = notificationPlayer?.let { other ->
|
||||
runOnMainThreadSync { other.isPlaying }
|
||||
other !== localPlayer && runOnMainThreadSync { other.isPlaying }
|
||||
} == true
|
||||
if (otherIsPlaying) {
|
||||
throw Error("Two players can't display notifications at the same time.")
|
||||
}
|
||||
notificationPlayer = localPlayer
|
||||
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
||||
} else if (field == true && notificationPlayer == localPlayer) {
|
||||
serviceRunning = true
|
||||
}
|
||||
|
||||
!shouldShow && serviceRunning -> {
|
||||
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
||||
notificationPlayer = null
|
||||
if (notificationPlayer == localPlayer) notificationPlayer = null
|
||||
serviceRunning = false
|
||||
}
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
override val castStatus: CastStatus
|
||||
get() = runOnMainThreadSync { computeCastStatus() }
|
||||
@@ -266,7 +279,7 @@ class OmniPlayer(
|
||||
|
||||
override var playbackRate by mainThreadProperty(
|
||||
get = { player.playbackParameters.speed.toDouble() },
|
||||
set = { value -> player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0f)) }
|
||||
set = { value -> player.setPlaybackSpeed(value.toFloat().coerceAtLeast(0.01f)) }
|
||||
)
|
||||
|
||||
override var muted by mainThreadProperty(
|
||||
@@ -293,11 +306,12 @@ class OmniPlayer(
|
||||
override var source: Source? = null
|
||||
set(value) {
|
||||
field = value
|
||||
eventMap.emitSource(value)
|
||||
if (value == null) {
|
||||
runOnMainThreadSync {
|
||||
player.setMediaItem(MediaItem.EMPTY)
|
||||
player.prepare()
|
||||
player.clearMediaItems()
|
||||
}
|
||||
syncNotificationService()
|
||||
return
|
||||
}
|
||||
val handleAudioFocus =
|
||||
@@ -330,6 +344,7 @@ class OmniPlayer(
|
||||
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
||||
player.prepare()
|
||||
}
|
||||
syncNotificationService()
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
@@ -505,7 +520,13 @@ class OmniPlayerService : MediaSessionService() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
player = OmniPlayer.notificationPlayer ?: throw Error("No player available")
|
||||
val available = OmniPlayer.notificationPlayer
|
||||
if (available == null) {
|
||||
startForeground(1, createImmediateNotification())
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
player = available
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
}
|
||||
@@ -541,7 +562,8 @@ class OmniPlayerService : MediaSessionService() {
|
||||
player = current
|
||||
mediaSession.player = current
|
||||
}
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun createImmediateNotification(): Notification {
|
||||
@@ -582,7 +604,7 @@ class OmniPlayerService : MediaSessionService() {
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaSession.release()
|
||||
if (::mediaSession.isInitialized) mediaSession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.PictureInPictureParams
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.Rational
|
||||
import android.view.Gravity
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
@@ -11,6 +12,10 @@ import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.VideoSize
|
||||
import androidx.media3.common.text.CueGroup
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import com.facebook.react.bridge.LifecycleEventListener
|
||||
import com.facebook.react.uimanager.ThemedReactContext
|
||||
import com.margelo.nitro.omni.HybridOmniPlayerSpec
|
||||
@@ -68,6 +73,16 @@ class OmniView(val context: ThemedReactContext) :
|
||||
}
|
||||
}
|
||||
|
||||
private val contentFrame = AspectRatioFrameLayout(context).apply {
|
||||
setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT)
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
Gravity.CENTER
|
||||
)
|
||||
view.addView(this)
|
||||
}
|
||||
|
||||
private val surfaceView = SurfaceView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
@@ -75,8 +90,19 @@ class OmniView(val context: ThemedReactContext) :
|
||||
)
|
||||
holder.addCallback(this@OmniView)
|
||||
addOnLayoutChangeListener(this@OmniView)
|
||||
contentFrame.addView(this)
|
||||
}
|
||||
|
||||
private val subtitleView = SubtitleView(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setUserDefaultStyle()
|
||||
setUserDefaultTextSize()
|
||||
view.addView(this)
|
||||
}
|
||||
|
||||
private var surfaceReady = false
|
||||
private var boundPlayer: OmniPlayer? = null
|
||||
private var rootContent: ViewGroup? = null
|
||||
@@ -94,6 +120,20 @@ class OmniView(val context: ThemedReactContext) :
|
||||
context.addLifecycleEventListener(this)
|
||||
}
|
||||
|
||||
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
||||
contentFrame.setAspectRatio(
|
||||
if (videoSize.width > 0 && videoSize.height > 0) {
|
||||
videoSize.width * videoSize.pixelWidthHeightRatio / videoSize.height
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCues(cueGroup: CueGroup) {
|
||||
subtitleView.setCues(cueGroup.cues)
|
||||
}
|
||||
|
||||
override fun onLayoutChange(
|
||||
view: View?,
|
||||
left: Int,
|
||||
@@ -219,6 +259,11 @@ class OmniView(val context: ThemedReactContext) :
|
||||
return
|
||||
}
|
||||
|
||||
if (boundPlayer?.isCasting == true) {
|
||||
clearPictureInPictureParams()
|
||||
return
|
||||
}
|
||||
|
||||
val autoEnterEnabled =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
autoPip == true &&
|
||||
@@ -242,9 +287,18 @@ class OmniView(val context: ThemedReactContext) :
|
||||
)
|
||||
}
|
||||
|
||||
private fun clampedAspectRatio(width: Int, height: Int): Rational {
|
||||
val minRatio = 0.42
|
||||
val maxRatio = 2.38
|
||||
val ratio = width.toDouble() / height.toDouble()
|
||||
if (ratio in minRatio..maxRatio) return Rational(width, height)
|
||||
val clamped = ratio.coerceIn(minRatio, maxRatio)
|
||||
return Rational((clamped * 1000).toInt(), 1000)
|
||||
}
|
||||
|
||||
private fun buildPipParams(autoEnterEnabled: Boolean): PictureInPictureParams {
|
||||
val aspectRatio = if (surfaceView.width > 0 && surfaceView.height > 0) {
|
||||
Rational(surfaceView.width, surfaceView.height)
|
||||
clampedAspectRatio(surfaceView.width, surfaceView.height)
|
||||
} else {
|
||||
Rational(16, 9)
|
||||
}
|
||||
@@ -293,7 +347,7 @@ class OmniView(val context: ThemedReactContext) :
|
||||
child.visibility = View.GONE
|
||||
}
|
||||
|
||||
view.removeView(surfaceView)
|
||||
contentFrame.removeView(surfaceView)
|
||||
root.addView(
|
||||
surfaceView,
|
||||
FrameLayout.LayoutParams(
|
||||
@@ -317,7 +371,7 @@ class OmniView(val context: ThemedReactContext) :
|
||||
}
|
||||
rootContentViews = emptyList()
|
||||
|
||||
view.addView(surfaceView, FrameLayout.LayoutParams(
|
||||
contentFrame.addView(surfaceView, FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
))
|
||||
|
||||
@@ -97,9 +97,11 @@ class VlcPlayer(ctx: Context) :
|
||||
private var mediaItems: List<MediaItem> = emptyList()
|
||||
private var currentMediaItemIndex: Int = INDEX_UNSET
|
||||
private var currentTrackSelectionParameters = TrackSelectionParameters.Builder().build()
|
||||
@Volatile
|
||||
private var playerError: PlaybackException? = null
|
||||
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
|
||||
private var userInitiatedTransition: Boolean = false
|
||||
@Volatile
|
||||
private var cachedBufferedPosition: Long = 0L
|
||||
private var boundSurfaceView: SurfaceView? = null
|
||||
|
||||
@@ -205,23 +207,9 @@ class VlcPlayer(ctx: Context) :
|
||||
}
|
||||
}
|
||||
|
||||
MediaPlayer.Event.TimeChanged -> {
|
||||
notifyListeners(EVENT_POSITION_DISCONTINUITY) {
|
||||
val positionMs = getCurrentPosition()
|
||||
val position = Player.PositionInfo(
|
||||
currentMediaItemIndex,
|
||||
currentMediaItemIndex,
|
||||
getCurrentMediaItem(),
|
||||
currentMediaItemIndex,
|
||||
currentMediaItemIndex,
|
||||
positionMs,
|
||||
positionMs,
|
||||
INDEX_UNSET,
|
||||
INDEX_UNSET
|
||||
)
|
||||
it.onPositionDiscontinuity(position, position, DISCONTINUITY_REASON_INTERNAL)
|
||||
}
|
||||
}
|
||||
// progress is polled instead of pushed dozens of times per seconds.
|
||||
// this improves perf & battery life (native -> js bridge is expensive)
|
||||
MediaPlayer.Event.TimeChanged -> Unit
|
||||
|
||||
MediaPlayer.Event.ESAdded,
|
||||
MediaPlayer.Event.ESDeleted,
|
||||
@@ -467,7 +455,11 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
override fun getShuffleModeEnabled(): Boolean = false
|
||||
|
||||
override fun isLoading() = player.playerState == IMedia.State.Opening
|
||||
override fun isLoading(): Boolean {
|
||||
val state = playbackState
|
||||
if (state == STATE_IDLE || state == STATE_ENDED) return false
|
||||
return player.playerState == IMedia.State.Opening
|
||||
}
|
||||
|
||||
override fun seekTo(
|
||||
mediaItemIndex: Int,
|
||||
@@ -485,19 +477,42 @@ class VlcPlayer(ctx: Context) :
|
||||
return
|
||||
}
|
||||
|
||||
player.time = positionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L
|
||||
val from = getCurrentPosition()
|
||||
val target = positionMs.coerceAtLeast(0L).takeIf { it != TIME_UNSET } ?: 0L
|
||||
player.time = target
|
||||
// since vlc reports progress events every few ms they don't have a seek finished event.
|
||||
notifyListeners(EVENT_POSITION_DISCONTINUITY) {
|
||||
it.onPositionDiscontinuity(
|
||||
positionInfo(from),
|
||||
positionInfo(target),
|
||||
DISCONTINUITY_REASON_SEEK
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun positionInfo(positionMs: Long): Player.PositionInfo =
|
||||
Player.PositionInfo(
|
||||
currentMediaItemIndex,
|
||||
currentMediaItemIndex,
|
||||
getCurrentMediaItem(),
|
||||
currentMediaItemIndex,
|
||||
currentMediaItemIndex,
|
||||
positionMs,
|
||||
positionMs,
|
||||
INDEX_UNSET,
|
||||
INDEX_UNSET
|
||||
)
|
||||
|
||||
override fun getSeekBackIncrement(): Long = 5_000L
|
||||
|
||||
override fun getSeekForwardIncrement(): Long = 15_000L
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
player.rate = playbackParameters.speed.coerceAtLeast(0f)
|
||||
player.rate = playbackParameters.speed.coerceAtLeast(0.01f)
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters =
|
||||
PlaybackParameters(player.rate.coerceAtLeast(0f))
|
||||
PlaybackParameters(player.rate.takeIf { it > 0f } ?: 1f)
|
||||
|
||||
override fun stop() {
|
||||
player.stop()
|
||||
@@ -505,6 +520,7 @@ class VlcPlayer(ctx: Context) :
|
||||
|
||||
override fun release() {
|
||||
player.setEventListener(null)
|
||||
listeners.release()
|
||||
abandonAudioFocus()
|
||||
player.stop()
|
||||
clearVideoSurface()
|
||||
@@ -518,36 +534,52 @@ class VlcPlayer(ctx: Context) :
|
||||
val selectedAudio = player.getSelectedTrack(IMedia.Track.Type.Audio)
|
||||
val selectedSubtitle = player.getSelectedTrack(IMedia.Track.Type.Text)
|
||||
|
||||
val videoTracks = player.getTracks(IMedia.Track.Type.Video)
|
||||
if (!videoTracks.isNullOrEmpty()) {
|
||||
val videoFormats = videoTracks.map { track ->
|
||||
Format.Builder()
|
||||
.setId(track.id)
|
||||
.setLabel(track.name)
|
||||
.setLanguage(track.language)
|
||||
.setSampleMimeType("video/x-unknown")
|
||||
.build()
|
||||
player.getTracks(IMedia.Track.Type.Video).orEmpty()
|
||||
.groupBy { listOf(it.language, it.name, it.description) }
|
||||
.values.forEach { videoTracks ->
|
||||
val videoFormats = videoTracks.map { track ->
|
||||
val videoTrack = track as? VideoTrack
|
||||
Format.Builder()
|
||||
.setId(track.id)
|
||||
.setLabel(track.name)
|
||||
.setLanguage(track.language)
|
||||
.setSampleMimeType("video/x-unknown")
|
||||
.setWidth(videoTrack?.width?.takeIf { it > 0 } ?: Format.NO_VALUE)
|
||||
.setHeight(videoTrack?.height?.takeIf { it > 0 } ?: Format.NO_VALUE)
|
||||
.setFrameRate(
|
||||
videoTrack?.takeIf { it.frameRateDen > 0 }
|
||||
?.let { it.frameRateNum.toFloat() / it.frameRateDen }
|
||||
?: Format.NO_VALUE.toFloat()
|
||||
)
|
||||
.setAverageBitrate(track.bitrate.takeIf { it > 0 } ?: Format.NO_VALUE)
|
||||
.build()
|
||||
}
|
||||
val group = TrackGroup("vlc-video-${videoTracks.minOf { it.id }}", *videoFormats.toTypedArray())
|
||||
val selected = BooleanArray(videoFormats.size) { idx ->
|
||||
selectedVideo != null && selectedVideo.id == videoTracks[idx].id
|
||||
}
|
||||
val support = IntArray(videoFormats.size) { FORMAT_HANDLED }
|
||||
result.add(Group(group, videoFormats.size > 1, support, selected))
|
||||
}
|
||||
val group = TrackGroup("vlc-video", *videoFormats.toTypedArray())
|
||||
val selected = BooleanArray(videoFormats.size) { idx ->
|
||||
selectedVideo != null && selectedVideo.id == videoTracks[idx].id
|
||||
}
|
||||
val support = IntArray(videoFormats.size) { FORMAT_HANDLED }
|
||||
result.add(Group(group, videoFormats.size > 1, support, selected))
|
||||
}
|
||||
|
||||
player.getTracks(IMedia.Track.Type.Audio)?.forEach { track ->
|
||||
val format = Format.Builder()
|
||||
.setId(track.id)
|
||||
.setLabel(track.name)
|
||||
.setLanguage(track.language)
|
||||
.setSampleMimeType("audio/x-unknown")
|
||||
.build()
|
||||
val group = TrackGroup("vlc-audio-${track.id}", format)
|
||||
val selected = booleanArrayOf(selectedAudio != null && selectedAudio.id == track.id)
|
||||
val support = intArrayOf(FORMAT_HANDLED)
|
||||
result.add(Group(group, false, support, selected))
|
||||
}
|
||||
player.getTracks(IMedia.Track.Type.Audio).orEmpty()
|
||||
.groupBy { listOf(it.language, it.name, it.description) }
|
||||
.values.forEach { audioTracks ->
|
||||
val audioFormats = audioTracks.map { track ->
|
||||
Format.Builder()
|
||||
.setId(track.id)
|
||||
.setLabel(track.name)
|
||||
.setLanguage(track.language)
|
||||
.setSampleMimeType("audio/x-unknown")
|
||||
.build()
|
||||
}
|
||||
val group = TrackGroup("vlc-audio-${audioTracks.minOf { it.id }}", *audioFormats.toTypedArray())
|
||||
val selected = BooleanArray(audioFormats.size) { idx ->
|
||||
selectedAudio != null && selectedAudio.id == audioTracks[idx].id
|
||||
}
|
||||
val support = IntArray(audioFormats.size) { FORMAT_HANDLED }
|
||||
result.add(Group(group, audioFormats.size > 1, support, selected))
|
||||
}
|
||||
|
||||
player.getTracks(IMedia.Track.Type.Text)?.forEach { track ->
|
||||
val format = Format.Builder()
|
||||
@@ -689,25 +721,27 @@ class VlcPlayer(ctx: Context) :
|
||||
}
|
||||
|
||||
override fun getCurrentTimeline(): Timeline {
|
||||
if (mediaItems.isEmpty()) return Timeline.EMPTY
|
||||
// snapshot the current playlist for the returned timeline.
|
||||
// timelines are immutable
|
||||
val items = mediaItems
|
||||
val currentIndex = currentMediaItemIndex
|
||||
val currentDurationUs = duration.takeIf { it != TIME_UNSET }?.let { it * 1000L } ?: TIME_UNSET
|
||||
if (items.isEmpty()) return Timeline.EMPTY
|
||||
return object : Timeline() {
|
||||
private fun durationUsForIndex(index: Int): Long {
|
||||
if (index != currentMediaItemIndex) return TIME_UNSET
|
||||
val dur = duration
|
||||
return if (dur == TIME_UNSET) TIME_UNSET else dur * 1000L
|
||||
}
|
||||
private fun durationUsForIndex(index: Int): Long =
|
||||
if (index == currentIndex) currentDurationUs else TIME_UNSET
|
||||
|
||||
override fun getWindowCount(): Int = mediaItems.size
|
||||
override fun getWindowCount(): Int = items.size
|
||||
|
||||
override fun getWindow(
|
||||
windowIndex: Int,
|
||||
window: Window,
|
||||
defaultPositionProjectionUs: Long
|
||||
): Window {
|
||||
check(windowIndex in mediaItems.indices)
|
||||
val index = windowIndex.coerceIn(items.indices)
|
||||
return window.set(
|
||||
windowIndex,
|
||||
mediaItems[windowIndex],
|
||||
index,
|
||||
items[index],
|
||||
null,
|
||||
TIME_UNSET,
|
||||
TIME_UNSET,
|
||||
@@ -716,34 +750,33 @@ class VlcPlayer(ctx: Context) :
|
||||
false,
|
||||
null,
|
||||
0L,
|
||||
durationUsForIndex(windowIndex),
|
||||
0,
|
||||
mediaItems.size - 1,
|
||||
durationUsForIndex(index),
|
||||
index,
|
||||
index,
|
||||
0L
|
||||
)
|
||||
}
|
||||
|
||||
override fun getPeriodCount(): Int = mediaItems.size
|
||||
override fun getPeriodCount(): Int = items.size
|
||||
|
||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||
check(periodIndex in mediaItems.indices)
|
||||
return period.set(periodIndex, periodIndex, 0, durationUsForIndex(periodIndex), 0L)
|
||||
val index = periodIndex.coerceIn(items.indices)
|
||||
return period.set(index, index, index, durationUsForIndex(index), 0L)
|
||||
}
|
||||
|
||||
override fun getIndexOfPeriod(uid: Any): Int {
|
||||
return if (uid is Int && uid in mediaItems.indices) uid else INDEX_UNSET
|
||||
return if (uid is Int && uid in items.indices) uid else INDEX_UNSET
|
||||
}
|
||||
|
||||
override fun getUidOfPeriod(periodIndex: Int): Any {
|
||||
check(periodIndex in mediaItems.indices)
|
||||
return periodIndex
|
||||
return periodIndex.coerceIn(items.indices)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentPeriodIndex() = currentMediaItemIndex
|
||||
override fun getCurrentPeriodIndex() = currentMediaItemIndex.coerceAtLeast(0)
|
||||
|
||||
override fun getCurrentMediaItemIndex() = currentMediaItemIndex
|
||||
override fun getCurrentMediaItemIndex() = currentMediaItemIndex.coerceAtLeast(0)
|
||||
|
||||
override fun getDuration(): Long = player.length.takeIf { it > 0 } ?: TIME_UNSET
|
||||
|
||||
|
||||
+24
-2
@@ -57,12 +57,16 @@ function PlayerExample({
|
||||
trackLabel,
|
||||
backend,
|
||||
onSwitchBackend,
|
||||
hasSource,
|
||||
onLoad,
|
||||
}: {
|
||||
onPrev: () => void;
|
||||
onNext: () => void;
|
||||
trackLabel: string;
|
||||
backend: AndroidBackend;
|
||||
onSwitchBackend: (backend: AndroidBackend) => void;
|
||||
hasSource: boolean;
|
||||
onLoad: () => void;
|
||||
}): React.JSX.Element {
|
||||
const player = usePlayer();
|
||||
const status = usePlayerState("status");
|
||||
@@ -233,7 +237,20 @@ function PlayerExample({
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<Text style={styles.heading}>react-native-omni</Text>
|
||||
<Text style={styles.subheading}>{trackLabel}</Text>
|
||||
<Text style={styles.subheading}>
|
||||
{hasSource ? trackLabel : "No media loaded"}
|
||||
</Text>
|
||||
|
||||
<View style={styles.row}>
|
||||
<Pressable
|
||||
style={[styles.button, hasSource && styles.selectedTrackButton]}
|
||||
onPress={onLoad}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{hasSource ? "Unload media" : "Load media"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<View style={styles.row}>
|
||||
<Pressable
|
||||
@@ -497,6 +514,9 @@ function PlayerExample({
|
||||
|
||||
function App(): React.JSX.Element {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
// Start with no media loaded so we can verify the app boots (and the media
|
||||
// notification stays hidden) before any source is set.
|
||||
const [hasSource, setHasSource] = useState(false);
|
||||
const [backend, setBackend] = useState<AndroidBackend>("vlc");
|
||||
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
|
||||
null,
|
||||
@@ -574,7 +594,7 @@ function App(): React.JSX.Element {
|
||||
return (
|
||||
<OmniProvider
|
||||
key={backend}
|
||||
source={source}
|
||||
source={hasSource ? source : undefined}
|
||||
backend={{ android: backend }}
|
||||
cast={{receiverApplicationId: "D8FB0FC1"}}
|
||||
showNotification
|
||||
@@ -585,6 +605,8 @@ function App(): React.JSX.Element {
|
||||
trackLabel={PLAYLIST[currentIndex].title}
|
||||
backend={backend}
|
||||
onSwitchBackend={handleSwitchBackend}
|
||||
hasSource={hasSource}
|
||||
onLoad={() => setHasSource((v) => !v)}
|
||||
/>
|
||||
</OmniProvider>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:screenOrientation="nosensor"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
|
||||
+2
@@ -20,6 +20,7 @@
|
||||
#include "JFunc_void_bool.hpp"
|
||||
#include "JFunc_void_PlayerStatus.hpp"
|
||||
#include "JFunc_void_CastStatus.hpp"
|
||||
#include "JFunc_void_std__optional_Source_.hpp"
|
||||
#include "JFunc_void.hpp"
|
||||
#include "JFunc_void_std__string_std__string.hpp"
|
||||
#include "JFunc_void_std__string.hpp"
|
||||
@@ -67,6 +68,7 @@ void registerAllNatives() {
|
||||
margelo::nitro::omni::JFunc_void_bool_cxx::registerNatives();
|
||||
margelo::nitro::omni::JFunc_void_PlayerStatus_cxx::registerNatives();
|
||||
margelo::nitro::omni::JFunc_void_CastStatus_cxx::registerNatives();
|
||||
margelo::nitro::omni::JFunc_void_std__optional_Source__cxx::registerNatives();
|
||||
margelo::nitro::omni::JFunc_void_cxx::registerNatives();
|
||||
margelo::nitro::omni::JFunc_void_std__string_std__string_cxx::registerNatives();
|
||||
margelo::nitro::omni::JFunc_void_std__string_cxx::registerNatives();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
///
|
||||
/// JFunc_void_std__optional_Source_.hpp
|
||||
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
||||
/// https://github.com/mrousavy/nitro
|
||||
/// Copyright © Marc Rousavy @ Margelo
|
||||
///
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <fbjni/fbjni.h>
|
||||
#include <functional>
|
||||
|
||||
#include "Source.hpp"
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <NitroModules/JNICallable.hpp>
|
||||
#include "JSource.hpp"
|
||||
#include "VideoSrc.hpp"
|
||||
#include "JVideoSrc.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "Subtitle.hpp"
|
||||
#include <vector>
|
||||
#include "JSubtitle.hpp"
|
||||
#include "Metadata.hpp"
|
||||
#include "JMetadata.hpp"
|
||||
#include "MixAudioMode.hpp"
|
||||
#include "JMixAudioMode.hpp"
|
||||
|
||||
namespace margelo::nitro::omni {
|
||||
|
||||
using namespace facebook;
|
||||
|
||||
/**
|
||||
* Represents the Java/Kotlin callback `(value: Source?) -> Unit`.
|
||||
* This can be passed around between C++ and Java/Kotlin.
|
||||
*/
|
||||
struct JFunc_void_std__optional_Source_: public jni::JavaClass<JFunc_void_std__optional_Source_> {
|
||||
public:
|
||||
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_std__optional_Source_;";
|
||||
|
||||
public:
|
||||
/**
|
||||
* Invokes the function this `JFunc_void_std__optional_Source_` instance holds through JNI.
|
||||
*/
|
||||
void invoke(const std::optional<Source>& value) const {
|
||||
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* value */)>("invoke");
|
||||
method(self(), value.has_value() ? JSource::fromCpp(value.value()) : nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An implementation of Func_void_std__optional_Source_ that is backed by a C++ implementation (using `std::function<...>`)
|
||||
*/
|
||||
class JFunc_void_std__optional_Source__cxx final: public jni::HybridClass<JFunc_void_std__optional_Source__cxx, JFunc_void_std__optional_Source_> {
|
||||
public:
|
||||
static jni::local_ref<JFunc_void_std__optional_Source_::javaobject> fromCpp(const std::function<void(const std::optional<Source>& /* value */)>& func) {
|
||||
return JFunc_void_std__optional_Source__cxx::newObjectCxxArgs(func);
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* Invokes the C++ `std::function<...>` this `JFunc_void_std__optional_Source__cxx` instance holds.
|
||||
*/
|
||||
void invoke_cxx(jni::alias_ref<JSource> value) {
|
||||
_func(value != nullptr ? std::make_optional(value->toCpp()) : std::nullopt);
|
||||
}
|
||||
|
||||
public:
|
||||
[[nodiscard]]
|
||||
inline const std::function<void(const std::optional<Source>& /* value */)>& getFunction() const {
|
||||
return _func;
|
||||
}
|
||||
|
||||
public:
|
||||
static constexpr auto kJavaDescriptor = "Lcom/margelo/nitro/omni/Func_void_std__optional_Source__cxx;";
|
||||
static void registerNatives() {
|
||||
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__optional_Source__cxx::invoke_cxx)});
|
||||
}
|
||||
|
||||
private:
|
||||
explicit JFunc_void_std__optional_Source__cxx(const std::function<void(const std::optional<Source>& /* value */)>& func): _func(func) { }
|
||||
|
||||
private:
|
||||
friend HybridBase;
|
||||
std::function<void(const std::optional<Source>& /* value */)> _func;
|
||||
};
|
||||
|
||||
} // namespace margelo::nitro::omni
|
||||
+33
-2
@@ -15,6 +15,16 @@ namespace margelo::nitro::omni { enum class BoolProperty; }
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
// Forward declaration of `CastStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class CastStatus; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `VideoSrc` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct VideoSrc; }
|
||||
// Forward declaration of `Subtitle` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Subtitle; }
|
||||
// Forward declaration of `Metadata` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Metadata; }
|
||||
// Forward declaration of `MixAudioMode` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class MixAudioMode; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
@@ -34,14 +44,27 @@ namespace margelo::nitro::omni { struct Rendition; }
|
||||
#include "CastStatus.hpp"
|
||||
#include "JFunc_void_CastStatus.hpp"
|
||||
#include "JCastStatus.hpp"
|
||||
#include "JFunc_void.hpp"
|
||||
#include "Source.hpp"
|
||||
#include <optional>
|
||||
#include "JFunc_void_std__optional_Source_.hpp"
|
||||
#include "JSource.hpp"
|
||||
#include "VideoSrc.hpp"
|
||||
#include "JVideoSrc.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "Subtitle.hpp"
|
||||
#include <vector>
|
||||
#include "JSubtitle.hpp"
|
||||
#include "Metadata.hpp"
|
||||
#include "JMetadata.hpp"
|
||||
#include "MixAudioMode.hpp"
|
||||
#include "JMixAudioMode.hpp"
|
||||
#include "JFunc_void.hpp"
|
||||
#include "JFunc_void_std__string_std__string.hpp"
|
||||
#include "JFunc_void_std__string.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "JFunc_void_Track.hpp"
|
||||
#include "JTrack.hpp"
|
||||
#include <optional>
|
||||
#include "JFunc_void_std__optional_Track_.hpp"
|
||||
#include "Rendition.hpp"
|
||||
#include "JFunc_void_Rendition.hpp"
|
||||
@@ -112,6 +135,14 @@ namespace margelo::nitro::omni {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_CastStatus::javaobject> /* cb */)>("removeCastStatusListener_cxx");
|
||||
method(_javaPart, JFunc_void_CastStatus_cxx::fromCpp(cb));
|
||||
}
|
||||
void JHybridOmniEventMapSpec::addSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_std__optional_Source_::javaobject> /* cb */)>("addSourceListener_cxx");
|
||||
method(_javaPart, JFunc_void_std__optional_Source__cxx::fromCpp(cb));
|
||||
}
|
||||
void JHybridOmniEventMapSpec::removeSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void_std__optional_Source_::javaobject> /* cb */)>("removeSourceListener_cxx");
|
||||
method(_javaPart, JFunc_void_std__optional_Source__cxx::fromCpp(cb));
|
||||
}
|
||||
void JHybridOmniEventMapSpec::addOnEndListener(const std::function<void()>& cb) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JFunc_void::javaobject> /* cb */)>("addOnEndListener_cxx");
|
||||
method(_javaPart, JFunc_void_cxx::fromCpp(cb));
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace margelo::nitro::omni {
|
||||
void removePlayerStatusListener(const std::function<void(PlayerStatus /* value */)>& cb) override;
|
||||
void addCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) override;
|
||||
void removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) override;
|
||||
void addSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) override;
|
||||
void removeSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) override;
|
||||
void addOnEndListener(const std::function<void()>& cb) override;
|
||||
void removeOnEndListener(const std::function<void()>& cb) override;
|
||||
void addOnPrevListener(const std::function<void()>& cb) override;
|
||||
|
||||
+19
-19
@@ -9,6 +9,10 @@
|
||||
|
||||
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Rendition; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `VideoSrc` to properly resolve imports.
|
||||
@@ -19,10 +23,6 @@ namespace margelo::nitro::omni { struct Subtitle; }
|
||||
namespace margelo::nitro::omni { struct Metadata; }
|
||||
// Forward declaration of `MixAudioMode` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class MixAudioMode; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Rendition; }
|
||||
// Forward declaration of `PlayerStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
// Forward declaration of `CastStatus` to properly resolve imports.
|
||||
@@ -32,23 +32,23 @@ namespace margelo::nitro::omni { enum class CastStatus; }
|
||||
#include "HybridOmniEventMapSpec.hpp"
|
||||
#include "JHybridOmniEventMapSpec.hpp"
|
||||
#include <optional>
|
||||
#include "Track.hpp"
|
||||
#include <vector>
|
||||
#include "JTrack.hpp"
|
||||
#include <string>
|
||||
#include "Rendition.hpp"
|
||||
#include "JRendition.hpp"
|
||||
#include "Source.hpp"
|
||||
#include "JSource.hpp"
|
||||
#include "VideoSrc.hpp"
|
||||
#include "JVideoSrc.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include "Subtitle.hpp"
|
||||
#include <vector>
|
||||
#include "JSubtitle.hpp"
|
||||
#include "Metadata.hpp"
|
||||
#include "JMetadata.hpp"
|
||||
#include "MixAudioMode.hpp"
|
||||
#include "JMixAudioMode.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "JTrack.hpp"
|
||||
#include "Rendition.hpp"
|
||||
#include "JRendition.hpp"
|
||||
#include "PlayerStatus.hpp"
|
||||
#include "JPlayerStatus.hpp"
|
||||
#include "CastStatus.hpp"
|
||||
@@ -98,15 +98,6 @@ namespace margelo::nitro::omni {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* showNotification */)>("setShowNotification");
|
||||
method(_javaPart, showNotification.has_value() ? jni::JBoolean::valueOf(showNotification.value()) : nullptr);
|
||||
}
|
||||
std::optional<Source> JHybridOmniPlayerSpec::getSource() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JSource>()>("getSource");
|
||||
auto __result = method(_javaPart);
|
||||
return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt;
|
||||
}
|
||||
void JHybridOmniPlayerSpec::setSource(const std::optional<Source>& source) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* source */)>("setSource");
|
||||
method(_javaPart, source.has_value() ? JSource::fromCpp(source.value()) : nullptr);
|
||||
}
|
||||
bool JHybridOmniPlayerSpec::getHasPrev() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("getHasPrev");
|
||||
auto __result = method(_javaPart);
|
||||
@@ -173,6 +164,15 @@ namespace margelo::nitro::omni {
|
||||
return __vector;
|
||||
}(__result);
|
||||
}
|
||||
std::optional<Source> JHybridOmniPlayerSpec::getSource() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JSource>()>("getSource");
|
||||
auto __result = method(_javaPart);
|
||||
return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt;
|
||||
}
|
||||
void JHybridOmniPlayerSpec::setSource(const std::optional<Source>& source) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* source */)>("setSource");
|
||||
method(_javaPart, source.has_value() ? JSource::fromCpp(source.value()) : nullptr);
|
||||
}
|
||||
PlayerStatus JHybridOmniPlayerSpec::getStatus() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JPlayerStatus>()>("getStatus");
|
||||
auto __result = method(_javaPart);
|
||||
|
||||
+2
-2
@@ -53,14 +53,14 @@ namespace margelo::nitro::omni {
|
||||
std::shared_ptr<HybridOmniEventMapSpec> getEventMap() override;
|
||||
std::optional<bool> getShowNotification() override;
|
||||
void setShowNotification(std::optional<bool> showNotification) override;
|
||||
std::optional<Source> getSource() override;
|
||||
void setSource(const std::optional<Source>& source) override;
|
||||
bool getHasPrev() override;
|
||||
bool getHasNext() override;
|
||||
std::vector<Track> getVideos() override;
|
||||
std::vector<Track> getAudios() override;
|
||||
std::vector<Track> getSubtitles() override;
|
||||
std::vector<Rendition> getRendition() override;
|
||||
std::optional<Source> getSource() override;
|
||||
void setSource(const std::optional<Source>& source) override;
|
||||
PlayerStatus getStatus() override;
|
||||
bool getIsPlaying() override;
|
||||
double getCurrentTime() override;
|
||||
|
||||
Generated
+80
@@ -0,0 +1,80 @@
|
||||
///
|
||||
/// Func_void_std__optional_Source_.kt
|
||||
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
||||
/// https://github.com/mrousavy/nitro
|
||||
/// Copyright © Marc Rousavy @ Margelo
|
||||
///
|
||||
|
||||
package com.margelo.nitro.omni
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.facebook.jni.HybridData
|
||||
import com.facebook.proguard.annotations.DoNotStrip
|
||||
import dalvik.annotation.optimization.FastNative
|
||||
|
||||
|
||||
/**
|
||||
* Represents the JavaScript callback `(value: optional) => void`.
|
||||
* This can be either implemented in C++ (in which case it might be a callback coming from JS),
|
||||
* or in Kotlin/Java (in which case it is a native callback).
|
||||
*/
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
@Suppress("ClassName", "RedundantUnitReturnType")
|
||||
fun interface Func_void_std__optional_Source_: (Source?) -> Unit {
|
||||
/**
|
||||
* Call the given JS callback.
|
||||
* @throws Throwable if the JS function itself throws an error, or if the JS function/runtime has already been deleted.
|
||||
*/
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
override fun invoke(value: Source?): Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the JavaScript callback `(value: optional) => void`.
|
||||
* This is implemented in C++, via a `std::function<...>`.
|
||||
* The callback might be coming from JS.
|
||||
*/
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
@Suppress(
|
||||
"KotlinJniMissingFunction", "unused",
|
||||
"RedundantSuppression", "RedundantUnitReturnType", "FunctionName",
|
||||
"ConvertSecondaryConstructorToPrimary", "ClassName", "LocalVariableName",
|
||||
)
|
||||
class Func_void_std__optional_Source__cxx: Func_void_std__optional_Source_ {
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
private val mHybridData: HybridData
|
||||
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
private constructor(hybridData: HybridData) {
|
||||
mHybridData = hybridData
|
||||
}
|
||||
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
override fun invoke(value: Source?): Unit
|
||||
= invoke_cxx(value)
|
||||
|
||||
@FastNative
|
||||
private external fun invoke_cxx(value: Source?): Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the JavaScript callback `(value: optional) => void`.
|
||||
* This is implemented in Java/Kotlin, via a `(Source?) -> Unit`.
|
||||
* The callback is always coming from native.
|
||||
*/
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
@Suppress("ClassName", "RedundantUnitReturnType", "unused")
|
||||
class Func_void_std__optional_Source__java(private val function: (Source?) -> Unit): Func_void_std__optional_Source_ {
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
override fun invoke(value: Source?): Unit {
|
||||
return this.function(value)
|
||||
}
|
||||
}
|
||||
+18
@@ -100,6 +100,24 @@ abstract class HybridOmniEventMapSpec: HybridObject() {
|
||||
return __result
|
||||
}
|
||||
|
||||
abstract fun addSourceListener(cb: (value: Source?) -> Unit): Unit
|
||||
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
private fun addSourceListener_cxx(cb: Func_void_std__optional_Source_): Unit {
|
||||
val __result = addSourceListener(cb)
|
||||
return __result
|
||||
}
|
||||
|
||||
abstract fun removeSourceListener(cb: (value: Source?) -> Unit): Unit
|
||||
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
private fun removeSourceListener_cxx(cb: Func_void_std__optional_Source_): Unit {
|
||||
val __result = removeSourceListener(cb)
|
||||
return __result
|
||||
}
|
||||
|
||||
abstract fun addOnEndListener(cb: () -> Unit): Unit
|
||||
|
||||
@DoNotStrip
|
||||
|
||||
+6
-6
@@ -35,12 +35,6 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
|
||||
@set:Keep
|
||||
abstract var showNotification: Boolean?
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
@set:DoNotStrip
|
||||
@set:Keep
|
||||
abstract var source: Source?
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
abstract val hasPrev: Boolean
|
||||
@@ -65,6 +59,12 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
|
||||
@get:Keep
|
||||
abstract val rendition: Array<Rendition>
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
@set:DoNotStrip
|
||||
@set:Keep
|
||||
abstract var source: Source?
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
abstract val status: PlayerStatus
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace margelo::nitro::omni {
|
||||
prototype.registerHybridMethod("removePlayerStatusListener", &HybridOmniEventMapSpec::removePlayerStatusListener);
|
||||
prototype.registerHybridMethod("addCastStatusListener", &HybridOmniEventMapSpec::addCastStatusListener);
|
||||
prototype.registerHybridMethod("removeCastStatusListener", &HybridOmniEventMapSpec::removeCastStatusListener);
|
||||
prototype.registerHybridMethod("addSourceListener", &HybridOmniEventMapSpec::addSourceListener);
|
||||
prototype.registerHybridMethod("removeSourceListener", &HybridOmniEventMapSpec::removeSourceListener);
|
||||
prototype.registerHybridMethod("addOnEndListener", &HybridOmniEventMapSpec::addOnEndListener);
|
||||
prototype.registerHybridMethod("removeOnEndListener", &HybridOmniEventMapSpec::removeOnEndListener);
|
||||
prototype.registerHybridMethod("addOnPrevListener", &HybridOmniEventMapSpec::addOnPrevListener);
|
||||
|
||||
+6
-1
@@ -21,6 +21,8 @@ namespace margelo::nitro::omni { enum class BoolProperty; }
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
// Forward declaration of `CastStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class CastStatus; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
@@ -31,9 +33,10 @@ namespace margelo::nitro::omni { struct Rendition; }
|
||||
#include "BoolProperty.hpp"
|
||||
#include "PlayerStatus.hpp"
|
||||
#include "CastStatus.hpp"
|
||||
#include "Source.hpp"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include "Track.hpp"
|
||||
#include <optional>
|
||||
#include "Rendition.hpp"
|
||||
|
||||
namespace margelo::nitro::omni {
|
||||
@@ -75,6 +78,8 @@ namespace margelo::nitro::omni {
|
||||
virtual void removePlayerStatusListener(const std::function<void(PlayerStatus /* value */)>& cb) = 0;
|
||||
virtual void addCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) = 0;
|
||||
virtual void removeCastStatusListener(const std::function<void(CastStatus /* value */)>& cb) = 0;
|
||||
virtual void addSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) = 0;
|
||||
virtual void removeSourceListener(const std::function<void(const std::optional<Source>& /* value */)>& cb) = 0;
|
||||
virtual void addOnEndListener(const std::function<void()>& cb) = 0;
|
||||
virtual void removeOnEndListener(const std::function<void()>& cb) = 0;
|
||||
virtual void addOnPrevListener(const std::function<void()>& cb) = 0;
|
||||
|
||||
+2
-2
@@ -17,14 +17,14 @@ namespace margelo::nitro::omni {
|
||||
prototype.registerHybridGetter("eventMap", &HybridOmniPlayerSpec::getEventMap);
|
||||
prototype.registerHybridGetter("showNotification", &HybridOmniPlayerSpec::getShowNotification);
|
||||
prototype.registerHybridSetter("showNotification", &HybridOmniPlayerSpec::setShowNotification);
|
||||
prototype.registerHybridGetter("source", &HybridOmniPlayerSpec::getSource);
|
||||
prototype.registerHybridSetter("source", &HybridOmniPlayerSpec::setSource);
|
||||
prototype.registerHybridGetter("hasPrev", &HybridOmniPlayerSpec::getHasPrev);
|
||||
prototype.registerHybridGetter("hasNext", &HybridOmniPlayerSpec::getHasNext);
|
||||
prototype.registerHybridGetter("videos", &HybridOmniPlayerSpec::getVideos);
|
||||
prototype.registerHybridGetter("audios", &HybridOmniPlayerSpec::getAudios);
|
||||
prototype.registerHybridGetter("subtitles", &HybridOmniPlayerSpec::getSubtitles);
|
||||
prototype.registerHybridGetter("rendition", &HybridOmniPlayerSpec::getRendition);
|
||||
prototype.registerHybridGetter("source", &HybridOmniPlayerSpec::getSource);
|
||||
prototype.registerHybridSetter("source", &HybridOmniPlayerSpec::setSource);
|
||||
prototype.registerHybridGetter("status", &HybridOmniPlayerSpec::getStatus);
|
||||
prototype.registerHybridGetter("isPlaying", &HybridOmniPlayerSpec::getIsPlaying);
|
||||
prototype.registerHybridGetter("currentTime", &HybridOmniPlayerSpec::getCurrentTime);
|
||||
|
||||
+5
-5
@@ -15,12 +15,12 @@
|
||||
|
||||
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Rendition; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `PlayerStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
// Forward declaration of `CastStatus` to properly resolve imports.
|
||||
@@ -29,10 +29,10 @@ namespace margelo::nitro::omni { enum class CastStatus; }
|
||||
#include <memory>
|
||||
#include "HybridOmniEventMapSpec.hpp"
|
||||
#include <optional>
|
||||
#include "Source.hpp"
|
||||
#include "Track.hpp"
|
||||
#include <vector>
|
||||
#include "Rendition.hpp"
|
||||
#include "Source.hpp"
|
||||
#include "PlayerStatus.hpp"
|
||||
#include "CastStatus.hpp"
|
||||
|
||||
@@ -66,14 +66,14 @@ namespace margelo::nitro::omni {
|
||||
virtual std::shared_ptr<HybridOmniEventMapSpec> getEventMap() = 0;
|
||||
virtual std::optional<bool> getShowNotification() = 0;
|
||||
virtual void setShowNotification(std::optional<bool> showNotification) = 0;
|
||||
virtual std::optional<Source> getSource() = 0;
|
||||
virtual void setSource(const std::optional<Source>& source) = 0;
|
||||
virtual bool getHasPrev() = 0;
|
||||
virtual bool getHasNext() = 0;
|
||||
virtual std::vector<Track> getVideos() = 0;
|
||||
virtual std::vector<Track> getAudios() = 0;
|
||||
virtual std::vector<Track> getSubtitles() = 0;
|
||||
virtual std::vector<Rendition> getRendition() = 0;
|
||||
virtual std::optional<Source> getSource() = 0;
|
||||
virtual void setSource(const std::optional<Source>& source) = 0;
|
||||
virtual PlayerStatus getStatus() = 0;
|
||||
virtual bool getIsPlaying() = 0;
|
||||
virtual double getCurrentTime() = 0;
|
||||
|
||||
+6
-1
@@ -32,7 +32,9 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
refresh?: number,
|
||||
): OmniPlayerState[Key] {
|
||||
const player = usePlayer() as OmniPlayer;
|
||||
const [ret, setState] = useState<any>(player[key]);
|
||||
// don't call `player[key]` (native call on main thread) every re-render
|
||||
// only do it when needed (init)
|
||||
const [ret, setState] = useState<any>(() => player[key]);
|
||||
|
||||
useEffect(() => {
|
||||
const em = player.eventMap;
|
||||
@@ -55,6 +57,9 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
case "castStatus":
|
||||
em.addCastStatusListener(setState);
|
||||
return () => em.removeCastStatusListener(setState);
|
||||
case "source":
|
||||
em.addSourceListener(setState);
|
||||
return () => em.removeSourceListener(setState);
|
||||
}
|
||||
}, [player, key]);
|
||||
|
||||
|
||||
+14
-3
@@ -12,7 +12,7 @@ import {
|
||||
selectVolume,
|
||||
usePlayer as useStoreSelector,
|
||||
} from "@videojs/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useSyncExternalStore } from "react";
|
||||
import type { WebOmniPlayer } from "./player.web";
|
||||
import { usePlayer } from "./provider.web";
|
||||
import type { OmniEvents } from "./types/events";
|
||||
@@ -204,6 +204,7 @@ export const stateMapper = {
|
||||
}),
|
||||
};
|
||||
|
||||
// biome-ignore-start lint/correctness/useHookAtTopLevel: key must be const
|
||||
export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
key: Key,
|
||||
): OmniPlayerState[Key];
|
||||
@@ -215,8 +216,18 @@ export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
key: Key,
|
||||
_refresh?: number,
|
||||
): OmniPlayerState[Key] {
|
||||
const config = stateMapper[key];
|
||||
if (key === "source") {
|
||||
const player = usePlayer() as WebOmniPlayer;
|
||||
const source = useSyncExternalStore(
|
||||
player.subscribeSource,
|
||||
() => player.source,
|
||||
() => player.source,
|
||||
);
|
||||
return source as OmniPlayerState[Key];
|
||||
}
|
||||
const config = stateMapper[key as keyof Omit<OmniPlayerState, "source">];
|
||||
const ret = useStoreSelector(config?.selector as Selector<any, any>);
|
||||
if (!config) throw new Error(`No mapper for ${key}`);
|
||||
const ret = useStoreSelector(config.selector as Selector<any, any>);
|
||||
return config.mapper(ret) as OmniPlayerState[Key];
|
||||
}
|
||||
// biome-ignore-end lint/correctness/useHookAtTopLevel: key must be const
|
||||
|
||||
@@ -70,6 +70,13 @@ export class WebOmniPlayer implements OmniPlayer {
|
||||
private overlaySubtitle: Subtitle | null = null;
|
||||
private overlayListeners = new Set<() => void>();
|
||||
|
||||
private sourceListeners = new Set<() => void>();
|
||||
subscribeSource = (callback: () => void): (() => void) => {
|
||||
this.sourceListeners.add(callback);
|
||||
return () => this.sourceListeners.delete(callback);
|
||||
};
|
||||
|
||||
|
||||
get source(): Source | undefined {
|
||||
return this._source;
|
||||
}
|
||||
@@ -84,6 +91,7 @@ export class WebOmniPlayer implements OmniPlayer {
|
||||
this.setOverlaySubtitle(null);
|
||||
}
|
||||
this.updateMediaSession();
|
||||
for (const listener of this.sourceListeners) listener();
|
||||
}
|
||||
|
||||
get showNotification(): boolean {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { CastOptions, Source } from "../types/source";
|
||||
|
||||
export type NumberProperty = Exclude<
|
||||
keyof OmniPlayerState,
|
||||
"status" | "isPlaying" | "muted"
|
||||
"status" | "isPlaying" | "muted" | "source"
|
||||
>;
|
||||
export type BoolProperty = "isPlaying" | "muted" | "isAutoQuality";
|
||||
|
||||
@@ -29,6 +29,9 @@ export interface OmniEventMap extends HybridObject<{ android: "kotlin" }> {
|
||||
addCastStatusListener(cb: (value: CastStatus) => void): void;
|
||||
removeCastStatusListener(cb: (value: CastStatus) => void): void;
|
||||
|
||||
addSourceListener(cb: (value?: Source) => void): void;
|
||||
removeSourceListener(cb: (value?: Source) => void): void;
|
||||
|
||||
addOnEndListener(cb: OmniEvents["end"]): void;
|
||||
removeOnEndListener(cb: OmniEvents["end"]): void;
|
||||
|
||||
|
||||
+1
-2
@@ -3,8 +3,6 @@ import type { Source } from "./source";
|
||||
export interface OmniPlayer extends OmniPlayerState {
|
||||
showNotification?: boolean;
|
||||
|
||||
source?: Source;
|
||||
|
||||
play(): void;
|
||||
pause(): void;
|
||||
seekBy(offset: number): void;
|
||||
@@ -28,6 +26,7 @@ export interface OmniPlayer extends OmniPlayerState {
|
||||
}
|
||||
|
||||
export interface OmniPlayerState {
|
||||
source?: Source;
|
||||
readonly status: PlayerStatus;
|
||||
readonly isPlaying: boolean;
|
||||
currentTime: number;
|
||||
|
||||
+11
-11
@@ -109,18 +109,18 @@ export const OmniView = ({
|
||||
const player = usePlayer() as WebOmniPlayer;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const ref = useRef<HTMLVideoElement>(undefined!);
|
||||
const source = usePlayerState("source");
|
||||
|
||||
const src = player.source?.src;
|
||||
const isHls =
|
||||
src?.mimeType?.toLowerCase().includes("mpegurl") ||
|
||||
src?.uri.split(/[?#]/)[0]?.toLowerCase().endsWith(".m3u8") ||
|
||||
source?.src?.mimeType?.toLowerCase().includes("mpegurl") ||
|
||||
source?.src?.uri.split(/[?#]/)[0]?.toLowerCase().endsWith(".m3u8") ||
|
||||
false;
|
||||
const Tech = isHls ? HlsJsVideo : Video;
|
||||
|
||||
const headersRef = useRef(src?.headers);
|
||||
headersRef.current = src?.headers;
|
||||
const castId = player.source?.castId;
|
||||
const castData = player.source?.castData;
|
||||
const headersRef = useRef(source?.src?.headers);
|
||||
headersRef.current = source?.src?.headers;
|
||||
const castId = source?.castId;
|
||||
const castData = source?.castData;
|
||||
|
||||
const config = useMemo<HlsMediaConfig>(
|
||||
() => ({
|
||||
@@ -151,17 +151,17 @@ export const OmniView = ({
|
||||
ref={containerRef}
|
||||
style={{ position: "relative", ...style }}
|
||||
>
|
||||
{src && (
|
||||
{source?.src && (
|
||||
<Tech
|
||||
ref={ref}
|
||||
src={src.uri}
|
||||
src={source?.src.uri}
|
||||
config={config}
|
||||
autoPlay={autoplay}
|
||||
playsInline
|
||||
crossOrigin="anonymous"
|
||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
>
|
||||
{(player.source?.subtitles ?? []).map((subtitle) => (
|
||||
{(source?.subtitles ?? []).map((subtitle) => (
|
||||
<track
|
||||
key={subtitle.id}
|
||||
id={subtitle.id}
|
||||
@@ -177,7 +177,7 @@ export const OmniView = ({
|
||||
<SubtitleOverlay
|
||||
video={ref}
|
||||
assets={subtitleAssets}
|
||||
fonts={player.source?.fonts}
|
||||
fonts={source?.fonts}
|
||||
/>
|
||||
)}
|
||||
</VideoPlayer.Container>
|
||||
|
||||
Reference in New Issue
Block a user