mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-05 05:36:17 +00:00
Try an example app and debug issues
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
+4
-4
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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!);
|
||||||
|
|||||||
Reference in New Issue
Block a user