Compare commits

...
5 Commits
Author SHA1 Message Date
zoriya 6ab5743f94 fix(pip): fix wrong player source 2026-07-26 23:26:17 +02:00
zoriya 9dae350c73 fix(pip): fix black screen 2026-07-26 22:36:05 +02:00
zoriya 074b821e06 fix(pip): prevent pip without a view mounted 2026-07-26 21:07:00 +02:00
zoriya dc0627afb4 fix(source): remove array syntax 2026-07-26 20:42:47 +02:00
zoriya e565717016 fix(exoplayer): fix hotswap between players 2026-07-26 20:05:12 +02:00
13 changed files with 212 additions and 70 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ function Player() {
export default function App() { export default function App() {
const source = { const source = {
src: [{ uri: "https://example.com/stream.m3u8", headers: {} }], src: { uri: "https://example.com/stream.m3u8", headers: {} },
subtitles: [], subtitles: [],
metadata: { title: "My video", hasPrev: false, hasNext: true }, metadata: { title: "My video", hasPrev: false, hasNext: true },
}; };
@@ -8,7 +8,6 @@ import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.SurfaceHolder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
@@ -107,7 +106,10 @@ class OmniPlayer(
override var showNotification: Boolean? = false override var showNotification: Boolean? = false
set(value) { set(value) {
if (value == true) { if (value == true) {
if (notificationPlayer != null && notificationPlayer?.isPlaying == true) { val otherIsPlaying = notificationPlayer?.let { other ->
runOnMainThreadSync { other.isPlaying }
} == true
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 = localPlayer
@@ -205,16 +207,29 @@ class OmniPlayer(
.build() .build()
} }
fun setSurface(holder: SurfaceHolder?) { fun setVideoView(surfaceView: android.view.SurfaceView?) {
runOnMainThread { runOnMainThread {
if (holder == null) { if (surfaceView == null) {
localPlayer.clearVideoSurface() localPlayer.clearVideoSurface()
} else { } else {
localPlayer.setVideoSurfaceHolder(holder) localPlayer.setVideoSurfaceView(surfaceView)
} }
} }
} }
// rebuild video pipeline after pip, noop for exoplayer that recover on it's own.
fun rebuildVideoOutput() {
runOnMainThread {
(localPlayer as? VlcPlayer)?.rebuildVideoOutput()
}
}
fun updateVideoLayout(width: Int, height: Int) {
runOnMainThread {
(localPlayer as? VlcPlayer)?.updateVideoLayout(width, height)
}
}
override val hasPrev: Boolean get() = player.hasPreviousMediaItem() override val hasPrev: Boolean get() = player.hasPreviousMediaItem()
override val hasNext: Boolean get() = player.hasNextMediaItem() override val hasNext: Boolean get() = player.hasNextMediaItem()
override val status by mainThreadProperty { override val status by mainThreadProperty {
@@ -287,18 +302,11 @@ class OmniPlayer(
// audio focus only makes sense locally. not in a cast session // audio focus only makes sense locally. not in a cast session
runOnMainThread { localPlayer.setAudioAttributes(audioAttributes, handleAudioFocus) } runOnMainThread { localPlayer.setAudioAttributes(audioAttributes, handleAudioFocus) }
val firstSrc = value.src.firstOrNull() val src = value.src
if (firstSrc == null) { httpDataSourceFactory?.setDefaultRequestProperties(src.headers)
runOnMainThreadSync {
player.setMediaItem(MediaItem.EMPTY)
player.prepare()
}
return
}
httpDataSourceFactory?.setDefaultRequestProperties(firstSrc.headers)
val currentItem = buildMediaItem( val currentItem = buildMediaItem(
firstSrc, src,
value.metadata, value.metadata,
value.subtitles, value.subtitles,
value.castId, value.castId,
@@ -532,6 +540,15 @@ class OmniPlayerService : MediaSessionService() {
triggerNotificationUpdate() triggerNotificationUpdate()
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val current = OmniPlayer.notificationPlayer
if (current != null && ::mediaSession.isInitialized && mediaSession.player !== current) {
player = current
mediaSession.player = current
}
return super.onStartCommand(intent, flags, startId)
}
private fun createImmediateNotification(): Notification { private fun createImmediateNotification(): Notification {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel( val channel = NotificationChannel(
@@ -125,6 +125,7 @@ class OmniView(val context: ThemedReactContext) :
autoPip == false && curPip == this -> { autoPip == false && curPip == this -> {
activeView = WeakReference(null) activeView = WeakReference(null)
clearPictureInPictureParams()
} }
} }
@@ -139,12 +140,12 @@ class OmniView(val context: ThemedReactContext) :
} }
boundPlayer?.localPlayer?.removeListener(this) boundPlayer?.localPlayer?.removeListener(this)
boundPlayer?.setSurface(null) boundPlayer?.setVideoView(null)
boundPlayer = omniPlayer boundPlayer = omniPlayer
omniPlayer.localPlayer.addListener(this) omniPlayer.localPlayer.addListener(this)
if (surfaceReady) { if (surfaceReady) {
omniPlayer.setSurface(surfaceView.holder) omniPlayer.setVideoView(surfaceView)
} }
if (autoplay == true && !omniPlayer.isPlaying) { if (autoplay == true && !omniPlayer.isPlaying) {
@@ -156,6 +157,7 @@ class OmniView(val context: ThemedReactContext) :
restoreUiAfterPip() restoreUiAfterPip()
if (activeView.get() == this) { if (activeView.get() == this) {
activeView = WeakReference(null) activeView = WeakReference(null)
clearPictureInPictureParams()
} }
surfaceView.removeOnLayoutChangeListener(this) surfaceView.removeOnLayoutChangeListener(this)
context.removeLifecycleEventListener(this) context.removeLifecycleEventListener(this)
@@ -164,7 +166,7 @@ class OmniView(val context: ThemedReactContext) :
val omniPlayer = player as? OmniPlayer ?: return val omniPlayer = player as? OmniPlayer ?: return
boundPlayer?.localPlayer?.removeListener(this) boundPlayer?.localPlayer?.removeListener(this)
omniPlayer.setSurface(null) omniPlayer.setVideoView(null)
boundPlayer = null boundPlayer = null
} }
@@ -183,8 +185,7 @@ class OmniView(val context: ThemedReactContext) :
return return
} }
val omniPlayer = boundPlayer ?: return if (boundPlayer == null || boundPlayer?.localPlayer?.isPlaying != true) {
if (!omniPlayer.isPlaying) {
return return
} }
@@ -218,10 +219,25 @@ class OmniView(val context: ThemedReactContext) :
val autoEnterEnabled = val autoEnterEnabled =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
autoPip == true && autoPip == true &&
boundPlayer?.isPlaying == true boundPlayer?.localPlayer?.isPlaying == true
activity.setPictureInPictureParams(buildPipParams(autoEnterEnabled)) activity.setPictureInPictureParams(buildPipParams(autoEnterEnabled))
} }
private fun clearPictureInPictureParams() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return
}
val activity = context.currentActivity ?: return
if (activity.isFinishing || activity.isDestroyed) {
return
}
activity.setPictureInPictureParams(
PictureInPictureParams.Builder()
.setAutoEnterEnabled(false)
.build()
)
}
private fun buildPipParams(autoEnterEnabled: Boolean): PictureInPictureParams { private fun buildPipParams(autoEnterEnabled: Boolean): PictureInPictureParams {
val aspectRatio = if (surfaceView.width > 0 && surfaceView.height > 0) { val aspectRatio = if (surfaceView.width > 0 && surfaceView.height > 0) {
Rational(surfaceView.width, surfaceView.height) Rational(surfaceView.width, surfaceView.height)
@@ -253,7 +269,7 @@ class OmniView(val context: ThemedReactContext) :
if (context.currentActivity !== activity || autoPip != true || boundPlayer == null) { if (context.currentActivity !== activity || autoPip != true || boundPlayer == null) {
return return
} }
if (!afterEnteredPip && boundPlayer?.isPlaying != true) return if (!afterEnteredPip && boundPlayer?.localPlayer?.isPlaying != true) return
isolateUiForPip(activity) isolateUiForPip(activity)
} }
@@ -305,9 +321,22 @@ class OmniView(val context: ThemedReactContext) :
movedSurfaceToRootForPip = false movedSurfaceToRootForPip = false
} }
// A surface that is destroyed and recreated while playback runs (the PIP
// 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
// once the (PIP) window has settled at its final size — firing it mid-resize
// leaves the vout stopped — so it is debounced off surfaceChanged.
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?.setSurface(holder) boundPlayer?.setVideoView(surfaceView)
if (boundPlayer?.localPlayer?.isPlaying == true) pendingVideoRebuild = true
updatePictureInPictureParams() updatePictureInPictureParams()
} }
@@ -316,10 +345,17 @@ class OmniView(val context: ThemedReactContext) :
format: Int, format: Int,
width: Int, width: Int,
height: Int height: Int
) { } ) {
boundPlayer?.updateVideoLayout(width, height)
if (pendingVideoRebuild) {
surfaceView.removeCallbacks(rebuildRunnable)
surfaceView.postDelayed(rebuildRunnable, 200)
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
surfaceReady = false surfaceReady = false
boundPlayer?.setSurface(null) surfaceView.removeCallbacks(rebuildRunnable)
boundPlayer?.setVideoView(null)
} }
} }
@@ -101,6 +101,7 @@ class VlcPlayer(ctx: Context) :
private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY private var playlistMetadata: MediaMetadata = MediaMetadata.EMPTY
private var userInitiatedTransition: Boolean = false private var userInitiatedTransition: Boolean = false
private var cachedBufferedPosition: Long = 0L private var cachedBufferedPosition: Long = 0L
private var boundSurfaceView: SurfaceView? = null
private val availableCommands: Player.Commands = Player.Commands.Builder() private val availableCommands: Player.Commands = Player.Commands.Builder()
.add(COMMAND_PLAY_PAUSE) .add(COMMAND_PLAY_PAUSE)
@@ -795,6 +796,7 @@ class VlcPlayer(ctx: Context) :
} }
override fun clearVideoSurface() { override fun clearVideoSurface() {
boundSurfaceView = null
vlcVout.detachViews() vlcVout.detachViews()
} }
@@ -825,10 +827,39 @@ class VlcPlayer(ctx: Context) :
} }
override fun setVideoSurfaceView(surfaceView: SurfaceView?) { override fun setVideoSurfaceView(surfaceView: SurfaceView?) {
vlcVout.setVideoView(surfaceView ?: return clearVideoSurface()) if (surfaceView == null) return clearVideoSurface()
if (vlcVout.areViewsAttached() && boundSurfaceView === surfaceView) return
boundSurfaceView = surfaceView
// Hand VLC the SurfaceView (not a raw Surface) so it reads the real view
// size and keeps the video layout correct as the surface resizes (e.g. the
// PIP window shrinking/growing); see updateVideoLayout for size updates.
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() {
if (!vlcVout.areViewsAttached() || !player.isPlaying) return
player.setVideoTrackEnabled(false)
player.setVideoTrackEnabled(true)
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) {
if (!vlcVout.areViewsAttached()) return
if (width > 0 && height > 0) vlcVout.setWindowSize(width, height)
player.updateVideoSurfaces()
}
override fun clearVideoSurfaceView(surfaceView: SurfaceView?) { override fun clearVideoSurfaceView(surfaceView: SurfaceView?) {
clearVideoSurface() clearVideoSurface()
} }
+83 -7
View File
@@ -2,6 +2,7 @@ import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { import {
type AndroidBackend,
OmniProvider, OmniProvider,
OmniView, OmniView,
useEvent, useEvent,
@@ -54,10 +55,14 @@ function PlayerExample({
onPrev, onPrev,
onNext, onNext,
trackLabel, trackLabel,
backend,
onSwitchBackend,
}: { }: {
onPrev: () => void; onPrev: () => void;
onNext: () => void; onNext: () => void;
trackLabel: string; trackLabel: string;
backend: AndroidBackend;
onSwitchBackend: (backend: AndroidBackend) => void;
}): React.JSX.Element { }): React.JSX.Element {
const player = usePlayer(); const player = usePlayer();
const status = usePlayerState("status"); const status = usePlayerState("status");
@@ -176,6 +181,15 @@ function PlayerExample({
), ),
); );
const switchBackend = (target: AndroidBackend) => {
if (target === backend) return;
// Pause before tearing down: the native media-session guard refuses a
// second player while the current one is still *playing*, so this keeps
// the transition clean (we don't preserve state across a switch anyway).
player.pause();
onSwitchBackend(target);
};
const togglePlayback = () => { const togglePlayback = () => {
if (isPlaying) { if (isPlaying) {
player.pause(); player.pause();
@@ -220,6 +234,27 @@ function PlayerExample({
<Text style={styles.heading}>react-native-omni</Text> <Text style={styles.heading}>react-native-omni</Text>
<Text style={styles.subheading}>{trackLabel}</Text> <Text style={styles.subheading}>{trackLabel}</Text>
<View style={styles.row}>
<Pressable
style={[
styles.button,
backend === "vlc" && styles.selectedTrackButton,
]}
onPress={() => switchBackend("vlc")}
>
<Text style={styles.buttonText}>VLC</Text>
</Pressable>
<Pressable
style={[
styles.button,
backend === "exoplayer" && styles.selectedTrackButton,
]}
onPress={() => switchBackend("exoplayer")}
>
<Text style={styles.buttonText}>ExoPlayer</Text>
</Pressable>
</View>
<OmniView <OmniView
style={styles.video} style={styles.video}
autoplay={true} autoplay={true}
@@ -461,6 +496,27 @@ function PlayerExample({
function App(): React.JSX.Element { function App(): React.JSX.Element {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [backend, setBackend] = useState<AndroidBackend>("vlc");
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
null,
);
// Switching backend recreates the native player, so we fully unmount the
// provider first (rendering nothing) and remount it on the next tick with
// the new backend. This lets the old OmniView/player tear down before the
// new one is created, avoiding the "only one view"/"two players" guards.
const handleSwitchBackend = useCallback((next: AndroidBackend) => {
setPendingBackend(next);
}, []);
useEffect(() => {
if (pendingBackend === null) return;
const id = setTimeout(() => {
setBackend(pendingBackend);
setPendingBackend(null);
}, 300);
return () => clearTimeout(id);
}, [pendingBackend]);
const handlePrev = useCallback(() => { const handlePrev = useCallback(() => {
setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1)); setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1));
@@ -472,12 +528,10 @@ function App(): React.JSX.Element {
const source = useMemo( const source = useMemo(
() => ({ () => ({
src: [ src: {
{ uri: PLAYLIST[currentIndex].uri,
uri: PLAYLIST[currentIndex].uri, headers: {},
headers: {}, },
},
],
subtitles: [ subtitles: [
{ {
id: "kusu", id: "kusu",
@@ -505,12 +559,28 @@ function App(): React.JSX.Element {
[currentIndex], [currentIndex],
); );
// While switching, render nothing so the current player is torn down first.
if (pendingBackend !== null) {
return (
<View style={styles.switching}>
<Text style={styles.subheading}>Switching to {pendingBackend}…</Text>
</View>
);
}
return ( return (
<OmniProvider source={source} showNotification> <OmniProvider
key={backend}
source={source}
backend={{ android: backend }}
showNotification
>
<PlayerExample <PlayerExample
onPrev={handlePrev} onPrev={handlePrev}
onNext={handleNext} onNext={handleNext}
trackLabel={PLAYLIST[currentIndex].title} trackLabel={PLAYLIST[currentIndex].title}
backend={backend}
onSwitchBackend={handleSwitchBackend}
/> />
</OmniProvider> </OmniProvider>
); );
@@ -525,6 +595,12 @@ const styles = StyleSheet.create({
paddingVertical: 12, paddingVertical: 12,
gap: 12, gap: 12,
}, },
switching: {
flex: 1,
backgroundColor: "#0b1020",
alignItems: "center",
justifyContent: "center",
},
heading: { heading: {
fontSize: 24, fontSize: 24,
fontWeight: "700", fontWeight: "700",
@@ -33,11 +33,11 @@ namespace margelo::nitro::omni { struct CastOptions; }
#include <optional> #include <optional>
#include "JSource.hpp" #include "JSource.hpp"
#include "VideoSrc.hpp" #include "VideoSrc.hpp"
#include <vector>
#include "JVideoSrc.hpp" #include "JVideoSrc.hpp"
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "Subtitle.hpp" #include "Subtitle.hpp"
#include <vector>
#include "JSubtitle.hpp" #include "JSubtitle.hpp"
#include "Metadata.hpp" #include "Metadata.hpp"
#include "JMetadata.hpp" #include "JMetadata.hpp"
+1 -1
View File
@@ -35,11 +35,11 @@ namespace margelo::nitro::omni { enum class CastStatus; }
#include "Source.hpp" #include "Source.hpp"
#include "JSource.hpp" #include "JSource.hpp"
#include "VideoSrc.hpp" #include "VideoSrc.hpp"
#include <vector>
#include "JVideoSrc.hpp" #include "JVideoSrc.hpp"
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "Subtitle.hpp" #include "Subtitle.hpp"
#include <vector>
#include "JSubtitle.hpp" #include "JSubtitle.hpp"
#include "Metadata.hpp" #include "Metadata.hpp"
#include "JMetadata.hpp" #include "JMetadata.hpp"
+5 -23
View File
@@ -42,8 +42,8 @@ namespace margelo::nitro::omni {
[[nodiscard]] [[nodiscard]]
Source toCpp() const { Source toCpp() const {
static const auto clazz = javaClassStatic(); static const auto clazz = javaClassStatic();
static const auto fieldSrc = clazz->getField<jni::JArrayClass<JVideoSrc>>("src"); static const auto fieldSrc = clazz->getField<JVideoSrc>("src");
jni::local_ref<jni::JArrayClass<JVideoSrc>> src = this->getFieldValue(fieldSrc); jni::local_ref<JVideoSrc> src = this->getFieldValue(fieldSrc);
static const auto fieldStartTime = clazz->getField<jni::JDouble>("startTime"); static const auto fieldStartTime = clazz->getField<jni::JDouble>("startTime");
jni::local_ref<jni::JDouble> startTime = this->getFieldValue(fieldStartTime); jni::local_ref<jni::JDouble> startTime = this->getFieldValue(fieldStartTime);
static const auto fieldSubtitles = clazz->getField<jni::JArrayClass<JSubtitle>>("subtitles"); static const auto fieldSubtitles = clazz->getField<jni::JArrayClass<JSubtitle>>("subtitles");
@@ -59,16 +59,7 @@ namespace margelo::nitro::omni {
static const auto fieldCastData = clazz->getField<jni::JMap<jni::JString, jni::JString>>("castData"); static const auto fieldCastData = clazz->getField<jni::JMap<jni::JString, jni::JString>>("castData");
jni::local_ref<jni::JMap<jni::JString, jni::JString>> castData = this->getFieldValue(fieldCastData); jni::local_ref<jni::JMap<jni::JString, jni::JString>> castData = this->getFieldValue(fieldCastData);
return Source( return Source(
[&](auto&& __input) { src->toCpp(),
size_t __size = __input->size();
std::vector<VideoSrc> __vector;
__vector.reserve(__size);
for (size_t __i = 0; __i < __size; __i++) {
auto __element = __input->getElement(__i);
__vector.push_back(__element->toCpp());
}
return __vector;
}(src),
startTime != nullptr ? std::make_optional(startTime->value()) : std::nullopt, startTime != nullptr ? std::make_optional(startTime->value()) : std::nullopt,
[&](auto&& __input) { [&](auto&& __input) {
size_t __size = __input->size(); size_t __size = __input->size();
@@ -110,21 +101,12 @@ namespace margelo::nitro::omni {
*/ */
[[maybe_unused]] [[maybe_unused]]
static jni::local_ref<JSource::javaobject> fromCpp(const Source& value) { static jni::local_ref<JSource::javaobject> fromCpp(const Source& value) {
using JSignature = JSource(jni::alias_ref<jni::JArrayClass<JVideoSrc>>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JArrayClass<JSubtitle>>, jni::alias_ref<jni::JArrayClass<jni::JString>>, jni::alias_ref<JMetadata>, jni::alias_ref<JMixAudioMode>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JMap<jni::JString, jni::JString>>); using JSignature = JSource(jni::alias_ref<JVideoSrc>, jni::alias_ref<jni::JDouble>, jni::alias_ref<jni::JArrayClass<JSubtitle>>, jni::alias_ref<jni::JArrayClass<jni::JString>>, jni::alias_ref<JMetadata>, jni::alias_ref<JMixAudioMode>, jni::alias_ref<jni::JString>, jni::alias_ref<jni::JMap<jni::JString, jni::JString>>);
static const auto clazz = javaClassStatic(); static const auto clazz = javaClassStatic();
static const auto create = clazz->getStaticMethod<JSignature>("fromCpp"); static const auto create = clazz->getStaticMethod<JSignature>("fromCpp");
return create( return create(
clazz, clazz,
[&](auto&& __input) { JVideoSrc::fromCpp(value.src),
size_t __size = __input.size();
jni::local_ref<jni::JArrayClass<JVideoSrc>> __array = jni::JArrayClass<JVideoSrc>::newArray(__size);
for (size_t __i = 0; __i < __size; __i++) {
const auto& __element = __input[__i];
auto __elementJni = JVideoSrc::fromCpp(__element);
__array->setElement(__i, *__elementJni);
}
return __array;
}(value.src),
value.startTime.has_value() ? jni::JDouble::valueOf(value.startTime.value()) : nullptr, value.startTime.has_value() ? jni::JDouble::valueOf(value.startTime.value()) : nullptr,
[&](auto&& __input) { [&](auto&& __input) {
size_t __size = __input.size(); size_t __size = __input.size();
@@ -20,7 +20,7 @@ import java.util.Objects
data class Source( data class Source(
@DoNotStrip @DoNotStrip
@Keep @Keep
val src: Array<VideoSrc>, val src: VideoSrc,
@DoNotStrip @DoNotStrip
@Keep @Keep
val startTime: Double?, val startTime: Double?,
@@ -79,7 +79,7 @@ data class Source(
@Keep @Keep
@Suppress("unused") @Suppress("unused")
@JvmStatic @JvmStatic
private fun fromCpp(src: Array<VideoSrc>, startTime: Double?, subtitles: Array<Subtitle>, fonts: Array<String>?, metadata: Metadata?, mixAudio: MixAudioMode?, castId: String?, castData: Map<String, String>?): Source { private fun fromCpp(src: VideoSrc, startTime: Double?, subtitles: Array<Subtitle>, fonts: Array<String>?, metadata: Metadata?, mixAudio: MixAudioMode?, castId: String?, castData: Map<String, String>?): Source {
return Source(src, startTime, subtitles, fonts, metadata, mixAudio, castId, castData) return Source(src, startTime, subtitles, fonts, metadata, mixAudio, castId, castData)
} }
} }
+6 -6
View File
@@ -38,9 +38,9 @@ namespace margelo::nitro::omni { struct Metadata; }
namespace margelo::nitro::omni { enum class MixAudioMode; } namespace margelo::nitro::omni { enum class MixAudioMode; }
#include "VideoSrc.hpp" #include "VideoSrc.hpp"
#include <vector>
#include <optional> #include <optional>
#include "Subtitle.hpp" #include "Subtitle.hpp"
#include <vector>
#include <string> #include <string>
#include "Metadata.hpp" #include "Metadata.hpp"
#include "MixAudioMode.hpp" #include "MixAudioMode.hpp"
@@ -53,7 +53,7 @@ namespace margelo::nitro::omni {
*/ */
struct Source final { struct Source final {
public: public:
std::vector<VideoSrc> src SWIFT_PRIVATE; VideoSrc src SWIFT_PRIVATE;
std::optional<double> startTime SWIFT_PRIVATE; std::optional<double> startTime SWIFT_PRIVATE;
std::vector<Subtitle> subtitles SWIFT_PRIVATE; std::vector<Subtitle> subtitles SWIFT_PRIVATE;
std::optional<std::vector<std::string>> fonts SWIFT_PRIVATE; std::optional<std::vector<std::string>> fonts SWIFT_PRIVATE;
@@ -64,7 +64,7 @@ namespace margelo::nitro::omni {
public: public:
Source() = default; Source() = default;
explicit Source(std::vector<VideoSrc> src, std::optional<double> startTime, std::vector<Subtitle> subtitles, std::optional<std::vector<std::string>> fonts, std::optional<Metadata> metadata, std::optional<MixAudioMode> mixAudio, std::optional<std::string> castId, std::optional<std::unordered_map<std::string, std::string>> castData): src(src), startTime(startTime), subtitles(subtitles), fonts(fonts), metadata(metadata), mixAudio(mixAudio), castId(castId), castData(castData) {} explicit Source(VideoSrc src, std::optional<double> startTime, std::vector<Subtitle> subtitles, std::optional<std::vector<std::string>> fonts, std::optional<Metadata> metadata, std::optional<MixAudioMode> mixAudio, std::optional<std::string> castId, std::optional<std::unordered_map<std::string, std::string>> castData): src(src), startTime(startTime), subtitles(subtitles), fonts(fonts), metadata(metadata), mixAudio(mixAudio), castId(castId), castData(castData) {}
public: public:
friend bool operator==(const Source& lhs, const Source& rhs) = default; friend bool operator==(const Source& lhs, const Source& rhs) = default;
@@ -80,7 +80,7 @@ namespace margelo::nitro {
static inline margelo::nitro::omni::Source fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { static inline margelo::nitro::omni::Source fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
jsi::Object obj = arg.asObject(runtime); jsi::Object obj = arg.asObject(runtime);
return margelo::nitro::omni::Source( return margelo::nitro::omni::Source(
JSIConverter<std::vector<margelo::nitro::omni::VideoSrc>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "src"))), JSIConverter<margelo::nitro::omni::VideoSrc>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "src"))),
JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startTime"))), JSIConverter<std::optional<double>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startTime"))),
JSIConverter<std::vector<margelo::nitro::omni::Subtitle>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "subtitles"))), JSIConverter<std::vector<margelo::nitro::omni::Subtitle>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "subtitles"))),
JSIConverter<std::optional<std::vector<std::string>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "fonts"))), JSIConverter<std::optional<std::vector<std::string>>>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "fonts"))),
@@ -92,7 +92,7 @@ namespace margelo::nitro {
} }
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::Source& arg) { static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::omni::Source& arg) {
jsi::Object obj(runtime); jsi::Object obj(runtime);
obj.setProperty(runtime, PropNameIDCache::get(runtime, "src"), JSIConverter<std::vector<margelo::nitro::omni::VideoSrc>>::toJSI(runtime, arg.src)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "src"), JSIConverter<margelo::nitro::omni::VideoSrc>::toJSI(runtime, arg.src));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "startTime"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.startTime)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "startTime"), JSIConverter<std::optional<double>>::toJSI(runtime, arg.startTime));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "subtitles"), JSIConverter<std::vector<margelo::nitro::omni::Subtitle>>::toJSI(runtime, arg.subtitles)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "subtitles"), JSIConverter<std::vector<margelo::nitro::omni::Subtitle>>::toJSI(runtime, arg.subtitles));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "fonts"), JSIConverter<std::optional<std::vector<std::string>>>::toJSI(runtime, arg.fonts)); obj.setProperty(runtime, PropNameIDCache::get(runtime, "fonts"), JSIConverter<std::optional<std::vector<std::string>>>::toJSI(runtime, arg.fonts));
@@ -110,7 +110,7 @@ namespace margelo::nitro {
if (!nitro::isPlainObject(runtime, obj)) { if (!nitro::isPlainObject(runtime, obj)) {
return false; return false;
} }
if (!JSIConverter<std::vector<margelo::nitro::omni::VideoSrc>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "src")))) return false; if (!JSIConverter<margelo::nitro::omni::VideoSrc>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "src")))) return false;
if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startTime")))) return false; if (!JSIConverter<std::optional<double>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "startTime")))) return false;
if (!JSIConverter<std::vector<margelo::nitro::omni::Subtitle>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "subtitles")))) return false; if (!JSIConverter<std::vector<margelo::nitro::omni::Subtitle>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "subtitles")))) return false;
if (!JSIConverter<std::optional<std::vector<std::string>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "fonts")))) return false; if (!JSIConverter<std::optional<std::vector<std::string>>>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "fonts")))) return false;
+1 -1
View File
@@ -60,7 +60,7 @@ const PlayerInitializer = ({
useEffect(() => { useEffect(() => {
player.source = source; player.source = source;
const uri = source?.src[0]?.uri; const uri = source?.src?.uri;
if (uri !== seekedForSrc.current) { if (uri !== seekedForSrc.current) {
seekedForSrc.current = uri; seekedForSrc.current = uri;
if (source?.startTime) store.seek(source.startTime); if (source?.startTime) store.seek(source.startTime);
+1 -1
View File
@@ -1,5 +1,5 @@
export interface Source { export interface Source {
src: VideoSrc[]; src: VideoSrc;
startTime?: number; startTime?: number;
subtitles: Subtitle[]; subtitles: Subtitle[];
// fonts that can be used by ass subtitles on the web (native will use // fonts that can be used by ass subtitles on the web (native will use
+1 -1
View File
@@ -114,7 +114,7 @@ export const OmniView = ({
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLVideoElement>(undefined!); const ref = useRef<HTMLVideoElement>(undefined!);
const src = player.source?.src[0]; const src = player.source?.src;
const isHls = const isHls =
src?.mimeType?.toLowerCase().includes("mpegurl") || src?.mimeType?.toLowerCase().includes("mpegurl") ||
src?.uri.split(/[?#]/)[0]?.toLowerCase().endsWith(".m3u8") || src?.uri.split(/[?#]/)[0]?.toLowerCase().endsWith(".m3u8") ||