Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub 666838057f Bump actions/checkout from 4 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-19 05:43:34 +00:00
zoriya c553c75433 Fix source/tech seleciton on web 2026-05-29 12:32:41 +02:00
zoriya 0ac36e9890 Setup react native web 2026-05-28 12:49:58 +02:00
zoriya 466b792878 Cleanup setSource 2026-05-25 18:33:44 +02:00
zoriya 89799cd5cc Cleanup web player 2026-05-24 23:38:18 +02:00
zoriya d603f95720 Cleanup view on web 2026-05-24 22:19:07 +02:00
zoriya 0466e0fab4 Cleanup useEvent on web 2026-05-24 17:09:58 +02:00
zoriya 9410846a0f Cleanup usePlayerStatus on web 2026-05-24 16:32:26 +02:00
zoriya 22c41331f9 Init videojs v10 implementation 2026-05-24 16:32:26 +02:00
24 changed files with 1293 additions and 120 deletions
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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() }
}
+690 -25
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+18
View File
@@ -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>
+69
View File
@@ -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
View File
@@ -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
View File
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+157
View File
@@ -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];
}
+154
View File
@@ -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
View File
@@ -22,7 +22,7 @@ export const OmniProvider = ({
const player = useLazyRef(() => ProviderFactory.createPlayer(source));
useEffect(() => {
player.source = source;
player.setSource(source);
}, [source]);
useEffect(() => {
+54
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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";
+31
View File
@@ -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
View File
@@ -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,