diff --git a/android/build.gradle b/android/build.gradle
index dcb46f7..39e0d5f 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -46,6 +46,10 @@ android {
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
+ ndk {
+ abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
+ }
+
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
@@ -113,6 +117,7 @@ android {
sourceSets {
main {
+ jniLibs.srcDirs = ['libs']
if (isNewArchitectureEnabled()) {
java.srcDirs += [
// React Codegen files
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index a2f47b6..99bd5c6 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,2 +1,6 @@
+
+
diff --git a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
index 20be282..d8e0d55 100644
--- a/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
+++ b/android/src/main/java/dev/zoriya/omni/OmniPlayer.kt
@@ -27,6 +27,9 @@ class OmniPlayer : HybridOmniPlayerSpec() {
player.setOptionString("demuxer-max-back-bytes", "75MiB")
player.setOptionString("demuxer-readahead-secs", "20")
+ player.setOptionString("save-position-on-quit", "no")
+ player.setOptionString("ytdl", "no")
+
// seek to keyframes
player.setOptionString("hr-seek", "no")
diff --git a/example/App.tsx b/example/App.tsx
index dcf881b..f3eab58 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -1,24 +1,310 @@
+import { useCallback, useMemo, useState } from "react";
import type React from "react";
-import { StyleSheet, View } from "react-native";
-import { Omni } from "react-native-omni";
+import {
+ 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([]);
+
+ 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 (
+
+
+ react-native-omni
+ {trackLabel}
+
+
+
+
+
+
+
+ {isPlaying ? "Pause" : "Play"}
+
+ player.seekBy(-10)}>
+ -10s
+
+ player.seekBy(10)}>
+ +10s
+
+
+
+
+ player.playPrev()}>
+ Prev
+
+ player.playNext()}>
+ Next
+
+
+ {muted ? "Unmute" : "Mute"}
+
+
+
+
+ changeVolume(-0.1)}>
+ Vol -
+
+ changeVolume(0.1)}>
+ Vol +
+
+
+ {playbackRate.toFixed(2)}x
+
+
+
+
+ Status: {status}
+
+ Time: {formatTime(currentTime)} / {formatTime(duration)}
+
+ Volume: {(volume * 100).toFixed(0)}%
+
+
+
+ {logs.length === 0 ? (
+ Event log will appear here.
+ ) : (
+ logs.map((entry, index) => (
+
+ {entry}
+
+ ))
+ )}
+
+
+
+ );
+}
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 (
-
-
-
+
+
+
);
}
const styles = StyleSheet.create({
+ safeArea: {
+ flex: 1,
+ backgroundColor: "#0b1020",
+ },
container: {
flex: 1,
- justifyContent: "center",
- alignItems: "center",
+ paddingHorizontal: 16,
+ paddingVertical: 12,
+ gap: 12,
},
- view: {
- width: 200,
- height: 200,
+ heading: {
+ fontSize: 24,
+ 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,
},
});
diff --git a/example/package.json b/example/package.json
index bdd64a2..a71d133 100644
--- a/example/package.json
+++ b/example/package.json
@@ -6,7 +6,7 @@
"android": "react-native run-android",
"ios": "react-native run-ios --simulator='iPhone 16'",
"lint": "eslint .",
- "start": "react-native start --reset-cache",
+ "start": "react-native start --reset-cache --port 8082",
"test": "jest",
"pod": "bundle install && bundle exec pod install --project-directory=ios"
},
diff --git a/nitro.json b/nitro.json
index 0121b24..1c823d2 100644
--- a/nitro.json
+++ b/nitro.json
@@ -12,13 +12,13 @@
"OmniView": {
"android": {
"language": "kotlin",
- "implementationClassName": "HybridOmniView"
+ "implementationClassName": "OmniView"
}
},
"OmniPlayerFactory": {
"android": {
"language": "kotlin",
- "implementationClassName": "HybridOmniPlayerFactory"
+ "implementationClassName": "OmniPlayerFactory"
}
}
},
diff --git a/nitrogen/generated/android/OmniOnLoad.cpp b/nitrogen/generated/android/OmniOnLoad.cpp
index 21d3a7f..3c696cb 100644
--- a/nitrogen/generated/android/OmniOnLoad.cpp
+++ b/nitrogen/generated/android/OmniOnLoad.cpp
@@ -40,7 +40,7 @@ int initialize(JavaVM* vm) {
}
struct JHybridOmniViewSpecImpl: public jni::JavaClass {
- static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/HybridOmniView;";
+ static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/OmniView;";
static std::shared_ptr create() {
static const auto constructorFn = javaClassStatic()->getConstructor();
jni::local_ref javaPart = javaClassStatic()->newObject(constructorFn);
@@ -48,7 +48,7 @@ struct JHybridOmniViewSpecImpl: public jni::JavaClass {
- static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/HybridOmniPlayerFactory;";
+ static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/OmniPlayerFactory;";
static std::shared_ptr create() {
static const auto constructorFn = javaClassStatic()->getConstructor();
jni::local_ref javaPart = javaClassStatic()->newObject(constructorFn);
diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/views/HybridOmniViewManager.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/views/HybridOmniViewManager.kt
index ac20c1c..c179b45 100644
--- a/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/views/HybridOmniViewManager.kt
+++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/omni/views/HybridOmniViewManager.kt
@@ -21,7 +21,7 @@ import dev.zoriya.omni.*
*/
public class HybridOmniViewManager: SimpleViewManager() {
init {
- if (RecyclableView::class.java.isAssignableFrom(HybridOmniView::class.java)) {
+ if (RecyclableView::class.java.isAssignableFrom(OmniView::class.java)) {
// Enable view recycling
super.setupViewRecycling()
}
@@ -32,7 +32,7 @@ public class HybridOmniViewManager: SimpleViewManager() {
}
override fun createViewInstance(reactContext: ThemedReactContext): View {
- val hybridView = HybridOmniView(reactContext)
+ val hybridView = OmniView(reactContext)
val view = hybridView.view
view.setTag(associated_hybrid_view_tag, hybridView)
return view
@@ -74,7 +74,7 @@ public class HybridOmniViewManager: SimpleViewManager() {
}
}
- private fun getHybridView(view: View): HybridOmniView? {
- return view.getTag(associated_hybrid_view_tag) as? HybridOmniView
+ private fun getHybridView(view: View): OmniView? {
+ return view.getTag(associated_hybrid_view_tag) as? OmniView
}
}
diff --git a/package.json b/package.json
index 400d328..495f444 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,8 @@
"react-native": "src/index",
"source": "src/index",
"scripts": {
+ "dev": "cd example && bun start",
+ "android": " cd example && bunx react-native run-android --no-packager --port 8082",
"typecheck": "tsc --noEmit",
"build": "bun run typecheck && bob build",
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js"
diff --git a/src/provider.tsx b/src/provider.tsx
index 2376308..33ccb82 100755
--- a/src/provider.tsx
+++ b/src/provider.tsx
@@ -6,7 +6,7 @@ import type { Source } from "./types/source";
import { useLazyRef } from "./utils/lazy-ref";
const ProviderFactory = NitroModules.createHybridObject(
- "OmniProviderFactory",
+ "OmniPlayerFactory",
);
const PlayerCtx = createContext(null!);