From 9c8fdbcc535db6d8c81d6ee739f4b669819c6eee Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 19 Apr 2026 21:15:08 +0200 Subject: [PATCH] Try an example app and debug issues --- android/build.gradle | 5 + android/src/main/AndroidManifest.xml | 4 + .../main/java/dev/zoriya/omni/OmniPlayer.kt | 3 + example/App.tsx | 306 +++++++++++++++++- example/package.json | 2 +- nitro.json | 4 +- nitrogen/generated/android/OmniOnLoad.cpp | 4 +- .../nitro/omni/views/HybridOmniViewManager.kt | 8 +- package.json | 2 + src/provider.tsx | 2 +- 10 files changed, 320 insertions(+), 20 deletions(-) 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!);