mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-04 21:26:48 +00:00
Try an example app and debug issues
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
+296
-10
@@ -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<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 {
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Omni isRed={true} style={styles.view} testID="omni" />
|
||||
</View>
|
||||
<OmniProvider source={source}>
|
||||
<PlayerExample
|
||||
onPrev={handlePrev}
|
||||
onNext={handleNext}
|
||||
trackLabel={PLAYLIST[currentIndex].title}
|
||||
/>
|
||||
</OmniProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+2
-2
@@ -12,13 +12,13 @@
|
||||
"OmniView": {
|
||||
"android": {
|
||||
"language": "kotlin",
|
||||
"implementationClassName": "HybridOmniView"
|
||||
"implementationClassName": "OmniView"
|
||||
}
|
||||
},
|
||||
"OmniPlayerFactory": {
|
||||
"android": {
|
||||
"language": "kotlin",
|
||||
"implementationClassName": "HybridOmniPlayerFactory"
|
||||
"implementationClassName": "OmniPlayerFactory"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ int initialize(JavaVM* vm) {
|
||||
}
|
||||
|
||||
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 const auto constructorFn = javaClassStatic()->getConstructor<JHybridOmniViewSpecImpl::javaobject()>();
|
||||
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> {
|
||||
static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/HybridOmniPlayerFactory;";
|
||||
static constexpr auto kJavaDescriptor = "Ldev/zoriya/omni/OmniPlayerFactory;";
|
||||
static std::shared_ptr<JHybridOmniPlayerFactorySpec> create() {
|
||||
static const auto constructorFn = javaClassStatic()->getConstructor<JHybridOmniPlayerFactorySpecImpl::javaobject()>();
|
||||
jni::local_ref<JHybridOmniPlayerFactorySpec::JavaPart> javaPart = javaClassStatic()->newObject(constructorFn);
|
||||
|
||||
+4
-4
@@ -21,7 +21,7 @@ import dev.zoriya.omni.*
|
||||
*/
|
||||
public class HybridOmniViewManager: SimpleViewManager<View>() {
|
||||
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<View>() {
|
||||
}
|
||||
|
||||
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<View>() {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import type { Source } from "./types/source";
|
||||
import { useLazyRef } from "./utils/lazy-ref";
|
||||
|
||||
const ProviderFactory = NitroModules.createHybridObject<OmniPlayerFactory>(
|
||||
"OmniProviderFactory",
|
||||
"OmniPlayerFactory",
|
||||
);
|
||||
|
||||
const PlayerCtx = createContext<OmniPlayer>(null!);
|
||||
|
||||
Reference in New Issue
Block a user