mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
666838057f | ||
|
|
c553c75433 | ||
|
|
0ac36e9890 | ||
|
|
466b792878 | ||
|
|
89799cd5cc | ||
|
|
d603f95720 | ||
|
|
0466e0fab4 | ||
|
|
9410846a0f | ||
|
|
22c41331f9 |
@@ -39,7 +39,7 @@ jobs:
|
||||
name: Build Android Example App
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies (bun)
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
name: Build iOS Example App
|
||||
runs-on: macOS-15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: Setup Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Bun.js
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.MediaItem.RequestMetadata
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
@@ -64,43 +63,6 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
runOnMainThread { player.release() }
|
||||
}
|
||||
|
||||
private var currentSource: Source? = null
|
||||
override var source: Source
|
||||
get() = currentSource
|
||||
?: throw IllegalStateException("source should be initialized before get")
|
||||
set(value) {
|
||||
Log.i("omni", "update source")
|
||||
currentSource = value
|
||||
val src = value.src.firstOrNull()
|
||||
if (src == null) {
|
||||
runOnMainThreadSync {
|
||||
player.setMediaItem(MediaItem.EMPTY)
|
||||
player.prepare()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val currentItem = buildMediaItem(src, value.metadata, value.subtitles)
|
||||
val mediaItems = mutableListOf<MediaItem>()
|
||||
|
||||
if (value.metadata?.hasPrev == true) {
|
||||
mediaItems.add(currentItem)
|
||||
}
|
||||
|
||||
mediaItems.add(currentItem)
|
||||
|
||||
if (value.metadata?.hasNext == true) {
|
||||
mediaItems.add(currentItem)
|
||||
}
|
||||
|
||||
runOnMainThreadSync {
|
||||
val startIndex = if (value.metadata?.hasPrev == true) 1 else 0
|
||||
val startPositionMs = (value.startTime?.coerceAtLeast(0.0) ?: 0.0) * 1000.0
|
||||
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
||||
player.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMediaItem(
|
||||
src: com.margelo.nitro.omni.VideoSrc,
|
||||
metadata: com.margelo.nitro.omni.Metadata?,
|
||||
@@ -153,8 +115,8 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
}
|
||||
}
|
||||
|
||||
override val hasPrev: Boolean get() = currentSource?.metadata?.hasPrev == true
|
||||
override val hasNext: Boolean get() = currentSource?.metadata?.hasNext == true
|
||||
override val hasPrev: Boolean get() = player.hasPreviousMediaItem()
|
||||
override val hasNext: Boolean get() = player.hasNextMediaItem()
|
||||
override val status by mainThreadProperty {
|
||||
when (player.playbackState) {
|
||||
Player.STATE_IDLE,
|
||||
@@ -207,6 +169,38 @@ class OmniPlayer : HybridOmniPlayerSpec() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSource(src: Source) {
|
||||
Log.i("omni", "update source")
|
||||
val source = src.src.firstOrNull()
|
||||
if (source == null) {
|
||||
runOnMainThreadSync {
|
||||
player.setMediaItem(MediaItem.EMPTY)
|
||||
player.prepare()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val currentItem = buildMediaItem(source, src.metadata, src.subtitles)
|
||||
val mediaItems = mutableListOf<MediaItem>()
|
||||
|
||||
if (src.metadata?.hasPrev == true) {
|
||||
mediaItems.add(currentItem)
|
||||
}
|
||||
|
||||
mediaItems.add(currentItem)
|
||||
|
||||
if (src.metadata?.hasNext == true) {
|
||||
mediaItems.add(currentItem)
|
||||
}
|
||||
|
||||
runOnMainThreadSync {
|
||||
val startIndex = if (src.metadata?.hasPrev == true) 1 else 0
|
||||
val startPositionMs = (src.startTime?.coerceAtLeast(0.0) ?: 0.0) * 1000.0
|
||||
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
||||
player.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
override fun play() {
|
||||
runOnMainThreadSync { player.play() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @format
|
||||
*/
|
||||
|
||||
import { AppRegistry } from "react-native";
|
||||
import App from "./App";
|
||||
import { name as appName } from "./app.json";
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
||||
AppRegistry.runApplication(appName, {
|
||||
rootTag: document.getElementById("root"),
|
||||
});
|
||||
+18
-1
@@ -2,6 +2,8 @@ const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
|
||||
const path = require("node:path");
|
||||
const root = path.resolve(__dirname, "..");
|
||||
|
||||
const useWeb = process.env.USE_WEB === "true" || process.env.PLATFORM === "web";
|
||||
|
||||
/**
|
||||
* Metro configuration
|
||||
* https://facebook.github.io/metro/docs/configuration
|
||||
@@ -9,7 +11,22 @@ const root = path.resolve(__dirname, "..");
|
||||
* @type {import('metro-config').MetroConfig}
|
||||
*/
|
||||
const config = {
|
||||
watchFolders: [root],
|
||||
watchFolders: [root],
|
||||
...(useWeb && {
|
||||
resolver: {
|
||||
extraNodeModules: {
|
||||
"react-native": path.resolve(__dirname, "node_modules", "react-native-web"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||
|
||||
+15
-4
@@ -7,15 +7,18 @@
|
||||
"ios": "react-native run-ios --simulator='iPhone 16'",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start --reset-cache --port 8082",
|
||||
"web": "webpack serve --mode development --open",
|
||||
"test": "jest",
|
||||
"pod": "bundle install && bundle exec pod install --project-directory=ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.84.1",
|
||||
"@react-native/new-app-screen": "0.84.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.84.1",
|
||||
"react-native-nitro-modules": "0.35.3",
|
||||
"react-native-safe-area-context": "^5.5.2",
|
||||
"react-native-nitro-modules": "0.35.3"
|
||||
"react-native-web": "^0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
@@ -29,7 +32,15 @@
|
||||
"@react-native/metro-config": "0.84.1",
|
||||
"@react-native/typescript-config": "0.84.1",
|
||||
"@types/jest": "^29.5.13",
|
||||
"babel-plugin-module-resolver": "^5.0.2"
|
||||
"babel-loader": "^10.1.1",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
"style-loader": "^4.0.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.107.2",
|
||||
"webpack-cli": "^7.0.3",
|
||||
"webpack-dev-server": "^5.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.11.0"
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>React Native Omni Example</title>
|
||||
<style>
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
const path = require("path");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
|
||||
const appDirectory = path.resolve(__dirname);
|
||||
const rootDirectory = path.resolve(__dirname, "..");
|
||||
|
||||
module.exports = {
|
||||
mode: "development",
|
||||
devtool: "eval-source-map",
|
||||
entry: path.resolve(__dirname, "index.web.js"),
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "bundle.web.js",
|
||||
publicPath: "/",
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".web.tsx", ".web.ts", ".tsx", ".ts", ".web.js", ".js"],
|
||||
alias: {
|
||||
"react-native$": "react-native-web",
|
||||
"react-native-omni": path.resolve(rootDirectory, "src"),
|
||||
react: path.resolve(appDirectory, "node_modules", "react"),
|
||||
"react-dom": path.resolve(appDirectory, "node_modules", "react-dom"),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts|tsx)$/,
|
||||
exclude: /node_modules\/(?!(react-native|@react-native|react-native-web|react-native-omni)\/).*/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
options: {
|
||||
presets: [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react",
|
||||
"@babel/preset-typescript",
|
||||
],
|
||||
plugins: [
|
||||
["@babel/plugin-transform-react-jsx", { runtime: "automatic" }],
|
||||
[
|
||||
"module-resolver",
|
||||
{
|
||||
extensions: [".js", ".ts", ".json", ".jsx", ".tsx"],
|
||||
alias: {
|
||||
"react-native-omni": path.resolve(rootDirectory, "src"),
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)$/i,
|
||||
type: "asset/resource",
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, "public/index.html"),
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
port: 3000,
|
||||
hot: true,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
};
|
||||
+19
-24
@@ -9,6 +9,12 @@
|
||||
|
||||
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Rendition; }
|
||||
// Forward declaration of `PlayerStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `VideoSrc` to properly resolve imports.
|
||||
@@ -19,23 +25,23 @@ namespace margelo::nitro::omni { struct Subtitle; }
|
||||
namespace margelo::nitro::omni { struct Metadata; }
|
||||
// Forward declaration of `MixAudioMode` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class MixAudioMode; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Rendition; }
|
||||
// Forward declaration of `PlayerStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
|
||||
#include <memory>
|
||||
#include "HybridOmniEventMapSpec.hpp"
|
||||
#include "JHybridOmniEventMapSpec.hpp"
|
||||
#include <optional>
|
||||
#include "Track.hpp"
|
||||
#include <vector>
|
||||
#include "JTrack.hpp"
|
||||
#include <string>
|
||||
#include "Rendition.hpp"
|
||||
#include "JRendition.hpp"
|
||||
#include "PlayerStatus.hpp"
|
||||
#include "JPlayerStatus.hpp"
|
||||
#include "Source.hpp"
|
||||
#include "JSource.hpp"
|
||||
#include "VideoSrc.hpp"
|
||||
#include <vector>
|
||||
#include "JVideoSrc.hpp"
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include "Subtitle.hpp"
|
||||
#include "JSubtitle.hpp"
|
||||
@@ -43,12 +49,6 @@ namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
#include "JMetadata.hpp"
|
||||
#include "MixAudioMode.hpp"
|
||||
#include "JMixAudioMode.hpp"
|
||||
#include "Track.hpp"
|
||||
#include "JTrack.hpp"
|
||||
#include "Rendition.hpp"
|
||||
#include "JRendition.hpp"
|
||||
#include "PlayerStatus.hpp"
|
||||
#include "JPlayerStatus.hpp"
|
||||
|
||||
namespace margelo::nitro::omni {
|
||||
|
||||
@@ -85,15 +85,6 @@ namespace margelo::nitro::omni {
|
||||
auto __result = method(_javaPart);
|
||||
return __result->getJHybridOmniEventMapSpec();
|
||||
}
|
||||
Source JHybridOmniPlayerSpec::getSource() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JSource>()>("getSource");
|
||||
auto __result = method(_javaPart);
|
||||
return __result->toCpp();
|
||||
}
|
||||
void JHybridOmniPlayerSpec::setSource(const Source& source) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* source */)>("setSource");
|
||||
method(_javaPart, JSource::fromCpp(source));
|
||||
}
|
||||
std::optional<bool> JHybridOmniPlayerSpec::getShowNotification() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JBoolean>()>("getShowNotification");
|
||||
auto __result = method(_javaPart);
|
||||
@@ -232,6 +223,10 @@ namespace margelo::nitro::omni {
|
||||
}
|
||||
|
||||
// Methods
|
||||
void JHybridOmniPlayerSpec::setSource(const Source& src) {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* src */)>("setSource");
|
||||
method(_javaPart, JSource::fromCpp(src));
|
||||
}
|
||||
void JHybridOmniPlayerSpec::play() {
|
||||
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("play");
|
||||
method(_javaPart);
|
||||
|
||||
+1
-2
@@ -51,8 +51,6 @@ namespace margelo::nitro::omni {
|
||||
public:
|
||||
// Properties
|
||||
std::shared_ptr<HybridOmniEventMapSpec> getEventMap() override;
|
||||
Source getSource() override;
|
||||
void setSource(const Source& source) override;
|
||||
std::optional<bool> getShowNotification() override;
|
||||
void setShowNotification(std::optional<bool> showNotification) override;
|
||||
bool getHasPrev() override;
|
||||
@@ -77,6 +75,7 @@ namespace margelo::nitro::omni {
|
||||
|
||||
public:
|
||||
// Methods
|
||||
void setSource(const Source& src) override;
|
||||
void play() override;
|
||||
void pause() override;
|
||||
void seekBy(double offset) override;
|
||||
|
||||
+4
-6
@@ -29,12 +29,6 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
|
||||
@get:Keep
|
||||
abstract val eventMap: HybridOmniEventMapSpec
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
@set:DoNotStrip
|
||||
@set:Keep
|
||||
abstract var source: Source
|
||||
|
||||
@get:DoNotStrip
|
||||
@get:Keep
|
||||
@set:DoNotStrip
|
||||
@@ -110,6 +104,10 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
|
||||
abstract val isAutoQuality: Boolean
|
||||
|
||||
// Methods
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
abstract fun setSource(src: Source): Unit
|
||||
|
||||
@DoNotStrip
|
||||
@Keep
|
||||
abstract fun play(): Unit
|
||||
|
||||
+1
-2
@@ -15,8 +15,6 @@ namespace margelo::nitro::omni {
|
||||
// load custom methods/properties
|
||||
registerHybrids(this, [](Prototype& prototype) {
|
||||
prototype.registerHybridGetter("eventMap", &HybridOmniPlayerSpec::getEventMap);
|
||||
prototype.registerHybridGetter("source", &HybridOmniPlayerSpec::getSource);
|
||||
prototype.registerHybridSetter("source", &HybridOmniPlayerSpec::setSource);
|
||||
prototype.registerHybridGetter("showNotification", &HybridOmniPlayerSpec::getShowNotification);
|
||||
prototype.registerHybridSetter("showNotification", &HybridOmniPlayerSpec::setShowNotification);
|
||||
prototype.registerHybridGetter("hasPrev", &HybridOmniPlayerSpec::getHasPrev);
|
||||
@@ -38,6 +36,7 @@ namespace margelo::nitro::omni {
|
||||
prototype.registerHybridGetter("muted", &HybridOmniPlayerSpec::getMuted);
|
||||
prototype.registerHybridSetter("muted", &HybridOmniPlayerSpec::setMuted);
|
||||
prototype.registerHybridGetter("isAutoQuality", &HybridOmniPlayerSpec::getIsAutoQuality);
|
||||
prototype.registerHybridMethod("setSource", &HybridOmniPlayerSpec::setSource);
|
||||
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
|
||||
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
|
||||
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
|
||||
|
||||
+4
-5
@@ -15,23 +15,23 @@
|
||||
|
||||
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
// Forward declaration of `Track` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Track; }
|
||||
// Forward declaration of `Rendition` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Rendition; }
|
||||
// Forward declaration of `PlayerStatus` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { enum class PlayerStatus; }
|
||||
// Forward declaration of `Source` to properly resolve imports.
|
||||
namespace margelo::nitro::omni { struct Source; }
|
||||
|
||||
#include <memory>
|
||||
#include "HybridOmniEventMapSpec.hpp"
|
||||
#include "Source.hpp"
|
||||
#include <optional>
|
||||
#include "Track.hpp"
|
||||
#include <vector>
|
||||
#include "Rendition.hpp"
|
||||
#include "PlayerStatus.hpp"
|
||||
#include "Source.hpp"
|
||||
|
||||
namespace margelo::nitro::omni {
|
||||
|
||||
@@ -61,8 +61,6 @@ namespace margelo::nitro::omni {
|
||||
public:
|
||||
// Properties
|
||||
virtual std::shared_ptr<HybridOmniEventMapSpec> getEventMap() = 0;
|
||||
virtual Source getSource() = 0;
|
||||
virtual void setSource(const Source& source) = 0;
|
||||
virtual std::optional<bool> getShowNotification() = 0;
|
||||
virtual void setShowNotification(std::optional<bool> showNotification) = 0;
|
||||
virtual bool getHasPrev() = 0;
|
||||
@@ -87,6 +85,7 @@ namespace margelo::nitro::omni {
|
||||
|
||||
public:
|
||||
// Methods
|
||||
virtual void setSource(const Source& src) = 0;
|
||||
virtual void play() = 0;
|
||||
virtual void pause() = 0;
|
||||
virtual void seekBy(double offset) = 0;
|
||||
|
||||
+2
-1
@@ -56,7 +56,8 @@
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/config-plugins": "^10.0.2"
|
||||
"@expo/config-plugins": "^10.0.2",
|
||||
"@videojs/react": "^10.0.0-beta.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.10",
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
type Selector,
|
||||
selectBuffer,
|
||||
selectError,
|
||||
selectPlayback,
|
||||
selectPlaybackRate,
|
||||
selectTextTrack,
|
||||
selectTime,
|
||||
selectVolume,
|
||||
usePlayer,
|
||||
} from "@videojs/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { OmniEvents } from "./types/events";
|
||||
import type { OmniPlayerState } from "./types/player";
|
||||
|
||||
type EventMapperConfig = {
|
||||
[Key in keyof OmniEvents]?: {
|
||||
selector: Selector<any, any>;
|
||||
handler: (cb: OmniEvents[Key], value: any, prev: any) => void;
|
||||
};
|
||||
};
|
||||
|
||||
function createEventMapper<Key extends keyof OmniEvents, Result>(
|
||||
key: Key,
|
||||
selector: Selector<any, Result>,
|
||||
handler: (
|
||||
cb: OmniEvents[Key],
|
||||
value: Result,
|
||||
prev: Result | undefined,
|
||||
) => void,
|
||||
) {
|
||||
return { [key]: { selector, handler } };
|
||||
}
|
||||
|
||||
const eventMapper: EventMapperConfig = {
|
||||
...createEventMapper("end", selectPlayback, (cb, value, prev) => {
|
||||
if (value?.ended && prev && !prev.ended) cb();
|
||||
}),
|
||||
...createEventMapper("error", selectError, (cb, value, prev) => {
|
||||
if (value?.error && value.error !== prev?.error) {
|
||||
cb(
|
||||
value.error.message ?? "Unknown error",
|
||||
value.error.message ?? "Unknown error",
|
||||
);
|
||||
}
|
||||
}),
|
||||
...createEventMapper("subtitleChange", selectTextTrack, (cb, value, prev) => {
|
||||
if (value === prev) return;
|
||||
const tracks = value?.textTrackList;
|
||||
if (tracks?.length) {
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
if (track?.mode === "showing") {
|
||||
cb({
|
||||
id: `text-${i}`,
|
||||
label: track.label,
|
||||
language: track.language,
|
||||
selected: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
cb(undefined);
|
||||
}),
|
||||
};
|
||||
|
||||
export const useEvent = <Event extends keyof OmniEvents>(
|
||||
event: Event,
|
||||
callback: OmniEvents[Event],
|
||||
) => {
|
||||
const config = eventMapper[event];
|
||||
const callbackRef = useRef(callback);
|
||||
callbackRef.current = callback;
|
||||
const prevRef = useRef<any>(undefined);
|
||||
|
||||
const value = usePlayer(config?.selector ?? (() => ({})));
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
const prev = prevRef.current;
|
||||
config.handler(callbackRef.current as any, value, prev);
|
||||
prevRef.current = value;
|
||||
}, [value, config]);
|
||||
};
|
||||
|
||||
function createMapper<Key extends keyof OmniPlayerState, State, Result>(
|
||||
key: Key,
|
||||
selector: Selector<State, Result>,
|
||||
mapper: (ret: Result) => OmniPlayerState[Key],
|
||||
): Pick<
|
||||
{
|
||||
[K in keyof OmniPlayerState]: {
|
||||
selector: Selector<State, Result>;
|
||||
mapper: (ret: Result) => OmniPlayerState[K];
|
||||
};
|
||||
},
|
||||
Key
|
||||
> {
|
||||
return { [key]: { selector, mapper } } as any;
|
||||
}
|
||||
|
||||
export const stateMapper = {
|
||||
...createMapper("status", selectPlayback, (s) => {
|
||||
if (s?.waiting) return "loading";
|
||||
if (s?.ended) return "idle";
|
||||
return "readyToPlay";
|
||||
}),
|
||||
...createMapper("isPlaying", selectPlayback, (s) => {
|
||||
if (!s) return false;
|
||||
return !s.paused;
|
||||
}),
|
||||
...createMapper("currentTime", selectTime, (s) => {
|
||||
return s?.currentTime ?? 0;
|
||||
}),
|
||||
...createMapper("buffered", selectBuffer, (s) => {
|
||||
if (!s?.buffered.length) return 0;
|
||||
const last = s.buffered[s.buffered.length - 1];
|
||||
return last?.[1] ?? 0;
|
||||
}),
|
||||
...createMapper("duration", selectTime, (s) => {
|
||||
return s?.duration ?? 0;
|
||||
}),
|
||||
...createMapper("playbackRate", selectPlaybackRate, (s) => {
|
||||
return s?.playbackRate ?? 1;
|
||||
}),
|
||||
...createMapper("volume", selectVolume, (s) => {
|
||||
return s?.volume ?? 1;
|
||||
}),
|
||||
...createMapper("muted", selectVolume, (s) => {
|
||||
return s?.muted ?? false;
|
||||
}),
|
||||
...createMapper(
|
||||
"isAutoQuality",
|
||||
() => {},
|
||||
() => {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
key: Key,
|
||||
): OmniPlayerState[Key];
|
||||
export function usePlayerState(
|
||||
key: "currentTime",
|
||||
refresh?: number,
|
||||
): OmniPlayerState["currentTime"];
|
||||
export function usePlayerState<Key extends keyof OmniPlayerState>(
|
||||
key: Key,
|
||||
_refresh?: number,
|
||||
): OmniPlayerState[Key] {
|
||||
const config = stateMapper[key];
|
||||
if (!config) throw new Error(`No mapper for ${key}`);
|
||||
const ret = usePlayer(config.selector as Selector<any, any>);
|
||||
return config.mapper(ret) as OmniPlayerState[Key];
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { VideoPlayerStore } from "@videojs/core/dom";
|
||||
import { stateMapper } from "./events.web";
|
||||
import type {
|
||||
OmniPlayer,
|
||||
PlayerStatus,
|
||||
Rendition,
|
||||
Track,
|
||||
} from "./types/player";
|
||||
import type { Source } from "./types/source";
|
||||
|
||||
export class WebOmniPlayer implements OmniPlayer {
|
||||
_store: VideoPlayerStore;
|
||||
onPrev?: () => void;
|
||||
onNext?: () => void;
|
||||
|
||||
constructor(store: VideoPlayerStore) {
|
||||
this._store = store;
|
||||
}
|
||||
|
||||
source: Source | null = null;
|
||||
|
||||
get showNotification(): boolean {
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
set showNotification(_: boolean) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
get status(): PlayerStatus {
|
||||
const state = this._store.state;
|
||||
return stateMapper.status.mapper(state);
|
||||
}
|
||||
|
||||
get isPlaying(): boolean {
|
||||
return !this._store.state.paused && !this._store.state.ended;
|
||||
}
|
||||
|
||||
get currentTime(): number {
|
||||
return this._store.state.currentTime;
|
||||
}
|
||||
|
||||
get buffered(): number {
|
||||
const buffered = this._store.state.buffered;
|
||||
if (buffered.length === 0) return 0;
|
||||
return buffered[buffered.length - 1]?.[1] ?? 0;
|
||||
}
|
||||
|
||||
get duration(): number {
|
||||
return this._store.state.duration;
|
||||
}
|
||||
|
||||
get playbackRate(): number {
|
||||
return this._store.state.playbackRate;
|
||||
}
|
||||
|
||||
set playbackRate(rate: number) {
|
||||
this._store.setPlaybackRate(rate);
|
||||
}
|
||||
|
||||
get volume(): number {
|
||||
return this._store.state.volume;
|
||||
}
|
||||
|
||||
set volume(vol: number) {
|
||||
this._store.setVolume(vol);
|
||||
}
|
||||
|
||||
get muted(): boolean {
|
||||
return this._store.state.muted;
|
||||
}
|
||||
|
||||
set muted(value: boolean) {
|
||||
if (value !== this.muted) {
|
||||
this._store.toggleMuted();
|
||||
}
|
||||
}
|
||||
|
||||
get isAutoQuality(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
play(): void {
|
||||
this._store.play();
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this._store.pause();
|
||||
}
|
||||
|
||||
seekBy(offset: number): void {
|
||||
this._store.seek(this.currentTime + offset).catch(() => {});
|
||||
}
|
||||
|
||||
playPrev(): void {
|
||||
this.onPrev?.();
|
||||
}
|
||||
|
||||
playNext(): void {
|
||||
this.onNext?.();
|
||||
}
|
||||
|
||||
get hasPrev(): boolean {
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
get hasNext(): boolean {
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
get videos(): Track[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
selectVideo(video: Track): void {}
|
||||
|
||||
get audios(): Track[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
selectAudio(audio: Track): void {}
|
||||
|
||||
get subtitles(): Track[] {
|
||||
return this._store.textTrackList
|
||||
.map((x, i) => ({
|
||||
id: i.toString(),
|
||||
kind: x.kind,
|
||||
label: x.label,
|
||||
language: x.language,
|
||||
selected: x.mode === "showing",
|
||||
}))
|
||||
.filter((x) => x.kind === "subtitles" || x.kind === "captions");
|
||||
}
|
||||
|
||||
selectSubtitle(subtitle?: Track): void {
|
||||
for (let i = 0; i < this._store.textTrackList.length; i++) {
|
||||
const track = this._store.textTrackList[i]!;
|
||||
if (track.kind !== "subtitles" && track.kind !== "captions") continue;
|
||||
track.mode =
|
||||
subtitle && i.toString() === subtitle.id ? "showing" : "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
get rendition(): Rendition[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
selectRendition(_rendition?: Rendition): void {
|
||||
// Auto quality is handled by videojs/HLS
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ export const OmniProvider = ({
|
||||
const player = useLazyRef(() => ProviderFactory.createPlayer(source));
|
||||
|
||||
useEffect(() => {
|
||||
player.source = source;
|
||||
player.setSource(source);
|
||||
}, [source]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { createPlayer } from "@videojs/react";
|
||||
import { videoFeatures } from "@videojs/react/video";
|
||||
import { createContext, type ReactNode, useContext, useEffect } from "react";
|
||||
import { WebOmniPlayer } from "./player.web";
|
||||
import type { OmniPlayer } from "./types/player";
|
||||
import type { Source } from "./types/source";
|
||||
import { useLazyRef } from "./utils/lazy-ref";
|
||||
|
||||
export const VideoPlayer = createPlayer({ features: videoFeatures });
|
||||
|
||||
const PlayerCtx = createContext<OmniPlayer>(null!);
|
||||
|
||||
export const OmniProvider = ({
|
||||
children,
|
||||
source,
|
||||
showNotification = false,
|
||||
}: {
|
||||
source: Source;
|
||||
children: ReactNode;
|
||||
showNotification?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<VideoPlayer.Provider>
|
||||
<PlayerInitializer source={source} showNotification={showNotification}>
|
||||
{children}
|
||||
</PlayerInitializer>
|
||||
</VideoPlayer.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const PlayerInitializer = ({
|
||||
children,
|
||||
source,
|
||||
showNotification,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
source: Source;
|
||||
showNotification: boolean;
|
||||
}) => {
|
||||
const store = VideoPlayer.usePlayer();
|
||||
const player = useLazyRef(() => new WebOmniPlayer(store));
|
||||
|
||||
useEffect(() => {
|
||||
player.source = source;
|
||||
}, [source]);
|
||||
|
||||
useEffect(() => {
|
||||
player.showNotification = showNotification;
|
||||
}, [showNotification]);
|
||||
|
||||
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
|
||||
};
|
||||
|
||||
export const usePlayer = () => useContext(PlayerCtx);
|
||||
@@ -56,6 +56,8 @@ export interface OmniPlayer
|
||||
extends HybridObject<{ android: "kotlin" }>,
|
||||
OmniPlayerT {
|
||||
readonly eventMap: OmniEventMap;
|
||||
|
||||
setSource(src: Source): void;
|
||||
}
|
||||
|
||||
export interface OmniPlayerFactory extends HybridObject<{ android: "kotlin" }> {
|
||||
|
||||
+1
-4
@@ -1,7 +1,4 @@
|
||||
import type { Source } from "./source";
|
||||
|
||||
export interface OmniPlayer extends OmniPlayerState {
|
||||
source: Source;
|
||||
showNotification?: boolean;
|
||||
|
||||
play(): void;
|
||||
@@ -34,7 +31,7 @@ export interface OmniPlayerState {
|
||||
// between 0 and 1
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
readonly isAutoQuality: boolean
|
||||
readonly isAutoQuality: boolean;
|
||||
}
|
||||
|
||||
export type PlayerStatus = "idle" | "loading" | "readyToPlay" | "error";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { HlsVideo } from "@videojs/react/media/hls-video";
|
||||
import { Video } from "@videojs/react/video";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useRef } from "react";
|
||||
import type { WebOmniPlayer } from "./player.web";
|
||||
import { usePlayer, VideoPlayer } from "./provider.web";
|
||||
import type { OmniViewProps } from "./types/view";
|
||||
|
||||
export const OmniView = ({
|
||||
style,
|
||||
autoplay,
|
||||
}: OmniViewProps & { style: CSSProperties }) => {
|
||||
const player = usePlayer() as WebOmniPlayer;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const uri = player.source?.src[0]?.uri;
|
||||
const Tech = uri?.endsWith("m3u8") ? HlsVideo : Video;
|
||||
|
||||
return (
|
||||
<VideoPlayer.Container ref={containerRef} style={style}>
|
||||
{uri && (
|
||||
<Tech
|
||||
src={uri}
|
||||
autoPlay={autoplay}
|
||||
playsInline
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
)}
|
||||
</VideoPlayer.Container>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -6,9 +6,9 @@
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["esnext"],
|
||||
"lib": ["esnext", "dom"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
Reference in New Issue
Block a user