Try an example app and debug issues

This commit is contained in:
2026-04-19 23:40:54 +02:00
parent f2332b4844
commit 9c8fdbcc53
10 changed files with 320 additions and 20 deletions
+5
View File
@@ -46,6 +46,10 @@ android {
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all" cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
@@ -113,6 +117,7 @@ android {
sourceSets { sourceSets {
main { main {
jniLibs.srcDirs = ['libs']
if (isNewArchitectureEnabled()) { if (isNewArchitectureEnabled()) {
java.srcDirs += [ java.srcDirs += [
// React Codegen files // React Codegen files
+4
View File
@@ -1,2 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature
android:name="android.software.picture_in_picture"
android:required="false" />
</manifest> </manifest>
@@ -27,6 +27,9 @@ class OmniPlayer : HybridOmniPlayerSpec() {
player.setOptionString("demuxer-max-back-bytes", "75MiB") player.setOptionString("demuxer-max-back-bytes", "75MiB")
player.setOptionString("demuxer-readahead-secs", "20") player.setOptionString("demuxer-readahead-secs", "20")
player.setOptionString("save-position-on-quit", "no")
player.setOptionString("ytdl", "no")
// seek to keyframes // seek to keyframes
player.setOptionString("hr-seek", "no") player.setOptionString("hr-seek", "no")
+296 -10
View File
@@ -1,24 +1,310 @@
import { useCallback, useMemo, useState } from "react";
import type React from "react"; import type React from "react";
import { StyleSheet, View } from "react-native"; import {
import { Omni } from "react-native-omni"; Pressable,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import {
OmniProvider,
OmniView,
useEvent,
usePlayer,
usePlayerState,
} from "react-native-omni";
const PLAYLIST = [
{
title: "Big Buck Bunny (HLS)",
uri: "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8",
},
{
title: "Sintel Trailer (MP4)",
uri: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4",
},
] as const;
function formatTime(seconds: number): string {
if (!Number.isFinite(seconds) || seconds < 0) {
return "00:00";
}
const total = Math.floor(seconds);
const mins = Math.floor(total / 60)
.toString()
.padStart(2, "0");
const secs = (total % 60).toString().padStart(2, "0");
return `${mins}:${secs}`;
}
function PlayerExample({
onPrev,
onNext,
trackLabel,
}: {
onPrev: () => void;
onNext: () => void;
trackLabel: string;
}): React.JSX.Element {
const player = usePlayer();
const status = usePlayerState("status");
const isPlaying = usePlayerState("isPlaying");
const currentTime = usePlayerState("currentTime");
const duration = usePlayerState("duration");
const playbackRate = usePlayerState("playbackRate");
const muted = usePlayerState("muted");
const volume = usePlayerState("volume");
const [logs, setLogs] = useState<string[]>([]);
const pushLog = useCallback((message: string) => {
setLogs((prev) => {
const next = [message, ...prev];
return next.slice(0, 8);
});
}, []);
const handlePrev = useCallback(() => {
pushLog("Prev selected");
onPrev();
}, [onPrev, pushLog]);
const handleNext = useCallback(() => {
pushLog("Next selected");
onNext();
}, [onNext, pushLog]);
useEvent(
"error",
useCallback(
(type, message) => {
pushLog(`Error (${type}): ${message}`);
},
[pushLog],
),
);
useEvent(
"audioFocusChange",
useCallback(
(focus) => {
pushLog(`Audio focus: ${focus}`);
},
[pushLog],
),
);
useEvent("prev", handlePrev);
useEvent("next", handleNext);
useEvent(
"end",
useCallback(() => {
pushLog("Playback ended");
handleNext();
}, [handleNext, pushLog]),
);
const togglePlayback = () => {
if (isPlaying) {
player.pause();
return;
}
player.play();
};
const toggleMute = () => {
player.muted = !muted;
};
const changeVolume = (delta: number) => {
const nextVolume = Math.max(0, Math.min(1, volume + delta));
player.volume = Number(nextVolume.toFixed(2));
};
const cyclePlaybackRate = () => {
const rates = [0.75, 1, 1.25, 1.5, 2];
const index = rates.findIndex((rate) => rate === playbackRate);
const nextIndex = (index + 1) % rates.length;
player.playbackRate = rates[nextIndex];
};
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<Text style={styles.heading}>react-native-omni</Text>
<Text style={styles.subheading}>{trackLabel}</Text>
<View style={styles.video}>
<OmniView autoplay={true} showNotification={true} />
</View>
<View style={styles.row}>
<Pressable style={styles.button} onPress={togglePlayback}>
<Text style={styles.buttonText}>{isPlaying ? "Pause" : "Play"}</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => player.seekBy(-10)}>
<Text style={styles.buttonText}>-10s</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => player.seekBy(10)}>
<Text style={styles.buttonText}>+10s</Text>
</Pressable>
</View>
<View style={styles.row}>
<Pressable style={styles.button} onPress={() => player.playPrev()}>
<Text style={styles.buttonText}>Prev</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => player.playNext()}>
<Text style={styles.buttonText}>Next</Text>
</Pressable>
<Pressable style={styles.button} onPress={toggleMute}>
<Text style={styles.buttonText}>{muted ? "Unmute" : "Mute"}</Text>
</Pressable>
</View>
<View style={styles.row}>
<Pressable style={styles.button} onPress={() => changeVolume(-0.1)}>
<Text style={styles.buttonText}>Vol -</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => changeVolume(0.1)}>
<Text style={styles.buttonText}>Vol +</Text>
</Pressable>
<Pressable style={styles.button} onPress={cyclePlaybackRate}>
<Text style={styles.buttonText}>{playbackRate.toFixed(2)}x</Text>
</Pressable>
</View>
<View style={styles.statsCard}>
<Text style={styles.statText}>Status: {status}</Text>
<Text style={styles.statText}>
Time: {formatTime(currentTime)} / {formatTime(duration)}
</Text>
<Text style={styles.statText}>Volume: {(volume * 100).toFixed(0)}%</Text>
</View>
<ScrollView style={styles.logCard} contentContainerStyle={styles.logContent}>
{logs.length === 0 ? (
<Text style={styles.logText}>Event log will appear here.</Text>
) : (
logs.map((entry, index) => (
<Text key={`${entry}-${index}`} style={styles.logText}>
{entry}
</Text>
))
)}
</ScrollView>
</View>
</SafeAreaView>
);
}
function App(): React.JSX.Element { function App(): React.JSX.Element {
const [currentIndex, setCurrentIndex] = useState(0);
const handlePrev = useCallback(() => {
setCurrentIndex((index) => (index === 0 ? PLAYLIST.length - 1 : index - 1));
}, []);
const handleNext = useCallback(() => {
setCurrentIndex((index) => (index + 1) % PLAYLIST.length);
}, []);
const source = useMemo(
() => ({
src: [
{
uri: PLAYLIST[currentIndex].uri,
headers: {},
},
],
subtitles: [],
metadata: {
title: PLAYLIST[currentIndex].title,
hasPrev: true,
hasNext: true,
},
}),
[currentIndex],
);
return ( return (
<View style={styles.container}> <OmniProvider source={source}>
<Omni isRed={true} style={styles.view} testID="omni" /> <PlayerExample
</View> onPrev={handlePrev}
onNext={handleNext}
trackLabel={PLAYLIST[currentIndex].title}
/>
</OmniProvider>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "#0b1020",
},
container: { container: {
flex: 1, flex: 1,
justifyContent: "center", paddingHorizontal: 16,
alignItems: "center", paddingVertical: 12,
gap: 12,
}, },
view: { heading: {
width: 200, fontSize: 24,
height: 200, fontWeight: "700",
color: "#e8ecff",
},
subheading: {
fontSize: 14,
color: "#a7b4df",
},
video: {
width: "100%",
aspectRatio: 16 / 9,
borderRadius: 14,
overflow: "hidden",
backgroundColor: "#000000",
},
row: {
flexDirection: "row",
gap: 10,
},
button: {
flex: 1,
backgroundColor: "#1a2442",
paddingVertical: 12,
borderRadius: 10,
alignItems: "center",
borderWidth: 1,
borderColor: "#2d3f74",
},
buttonText: {
color: "#f2f4ff",
fontSize: 14,
fontWeight: "600",
},
statsCard: {
backgroundColor: "#101833",
borderRadius: 10,
padding: 12,
gap: 4,
},
statText: {
color: "#cfd9ff",
fontSize: 13,
},
logCard: {
flex: 1,
backgroundColor: "#101833",
borderRadius: 10,
},
logContent: {
padding: 12,
gap: 6,
},
logText: {
color: "#9fb0e8",
fontSize: 12,
}, },
}); });
+1 -1
View File
@@ -6,7 +6,7 @@
"android": "react-native run-android", "android": "react-native run-android",
"ios": "react-native run-ios --simulator='iPhone 16'", "ios": "react-native run-ios --simulator='iPhone 16'",
"lint": "eslint .", "lint": "eslint .",
"start": "react-native start --reset-cache", "start": "react-native start --reset-cache --port 8082",
"test": "jest", "test": "jest",
"pod": "bundle install && bundle exec pod install --project-directory=ios" "pod": "bundle install && bundle exec pod install --project-directory=ios"
}, },
+2 -2
View File
@@ -12,13 +12,13 @@
"OmniView": { "OmniView": {
"android": { "android": {
"language": "kotlin", "language": "kotlin",
"implementationClassName": "HybridOmniView" "implementationClassName": "OmniView"
} }
}, },
"OmniPlayerFactory": { "OmniPlayerFactory": {
"android": { "android": {
"language": "kotlin", "language": "kotlin",
"implementationClassName": "HybridOmniPlayerFactory" "implementationClassName": "OmniPlayerFactory"
} }
} }
}, },
+2 -2
View File
@@ -40,7 +40,7 @@ int initialize(JavaVM* vm) {
} }
struct JHybridOmniViewSpecImpl: public jni::JavaClass<JHybridOmniViewSpecImpl, JHybridOmniViewSpec::JavaPart> { struct JHybridOmniViewSpecImpl: public jni::JavaClass<JHybridOmniViewSpecImpl, JHybridOmniViewSpec::JavaPart> {
static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/HybridOmniView;"; static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/OmniView;";
static std::shared_ptr<JHybridOmniViewSpec> create() { static std::shared_ptr<JHybridOmniViewSpec> create() {
static const auto constructorFn = javaClassStatic()->getConstructor<JHybridOmniViewSpecImpl::javaobject()>(); static const auto constructorFn = javaClassStatic()->getConstructor<JHybridOmniViewSpecImpl::javaobject()>();
jni::local_ref<JHybridOmniViewSpec::JavaPart> javaPart = javaClassStatic()->newObject(constructorFn); jni::local_ref<JHybridOmniViewSpec::JavaPart> javaPart = javaClassStatic()->newObject(constructorFn);
@@ -48,7 +48,7 @@ struct JHybridOmniViewSpecImpl: public jni::JavaClass<JHybridOmniViewSpecImpl, J
} }
}; };
struct JHybridOmniPlayerFactorySpecImpl: public jni::JavaClass<JHybridOmniPlayerFactorySpecImpl, JHybridOmniPlayerFactorySpec::JavaPart> { struct JHybridOmniPlayerFactorySpecImpl: public jni::JavaClass<JHybridOmniPlayerFactorySpecImpl, JHybridOmniPlayerFactorySpec::JavaPart> {
static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/HybridOmniPlayerFactory;"; static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/OmniPlayerFactory;";
static std::shared_ptr<JHybridOmniPlayerFactorySpec> create() { static std::shared_ptr<JHybridOmniPlayerFactorySpec> create() {
static const auto constructorFn = javaClassStatic()->getConstructor<JHybridOmniPlayerFactorySpecImpl::javaobject()>(); static const auto constructorFn = javaClassStatic()->getConstructor<JHybridOmniPlayerFactorySpecImpl::javaobject()>();
jni::local_ref<JHybridOmniPlayerFactorySpec::JavaPart> javaPart = javaClassStatic()->newObject(constructorFn); jni::local_ref<JHybridOmniPlayerFactorySpec::JavaPart> javaPart = javaClassStatic()->newObject(constructorFn);
@@ -21,7 +21,7 @@ import dev.zoriya.omni.*
*/ */
public class HybridOmniViewManager: SimpleViewManager<View>() { public class HybridOmniViewManager: SimpleViewManager<View>() {
init { init {
if (RecyclableView::class.java.isAssignableFrom(HybridOmniView::class.java)) { if (RecyclableView::class.java.isAssignableFrom(OmniView::class.java)) {
// Enable view recycling // Enable view recycling
super.setupViewRecycling() super.setupViewRecycling()
} }
@@ -32,7 +32,7 @@ public class HybridOmniViewManager: SimpleViewManager<View>() {
} }
override fun createViewInstance(reactContext: ThemedReactContext): View { override fun createViewInstance(reactContext: ThemedReactContext): View {
val hybridView = HybridOmniView(reactContext) val hybridView = OmniView(reactContext)
val view = hybridView.view val view = hybridView.view
view.setTag(associated_hybrid_view_tag, hybridView) view.setTag(associated_hybrid_view_tag, hybridView)
return view return view
@@ -74,7 +74,7 @@ public class HybridOmniViewManager: SimpleViewManager<View>() {
} }
} }
private fun getHybridView(view: View): HybridOmniView? { private fun getHybridView(view: View): OmniView? {
return view.getTag(associated_hybrid_view_tag) as? HybridOmniView return view.getTag(associated_hybrid_view_tag) as? OmniView
} }
} }
+2
View File
@@ -8,6 +8,8 @@
"react-native": "src/index", "react-native": "src/index",
"source": "src/index", "source": "src/index",
"scripts": { "scripts": {
"dev": "cd example && bun start",
"android": " cd example && bunx react-native run-android --no-packager --port 8082",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "bun run typecheck && bob build", "build": "bun run typecheck && bob build",
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js" "codegen": "nitrogen --logLevel=\"debug\" && node post-script.js"
+1 -1
View File
@@ -6,7 +6,7 @@ import type { Source } from "./types/source";
import { useLazyRef } from "./utils/lazy-ref"; import { useLazyRef } from "./utils/lazy-ref";
const ProviderFactory = NitroModules.createHybridObject<OmniPlayerFactory>( const ProviderFactory = NitroModules.createHybridObject<OmniPlayerFactory>(
"OmniProviderFactory", "OmniPlayerFactory",
); );
const PlayerCtx = createContext<OmniPlayer>(null!); const PlayerCtx = createContext<OmniPlayer>(null!);