mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ab5743f94 | ||
|
|
9dae350c73 | ||
|
|
074b821e06 | ||
|
|
dc0627afb4 | ||
|
|
e565717016 |
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-6
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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") ||
|
||||||
|
|||||||
Reference in New Issue
Block a user