mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a335509b4e |
+1
-1
@@ -1 +1 @@
|
|||||||
module.exports = require('./plugin/build').default;
|
module.exports = require("./plugin/build").default;
|
||||||
|
|||||||
+2
-2
@@ -88,7 +88,7 @@ function PlayerExample({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshTracks();
|
refreshTracks();
|
||||||
}, [refreshTracks, trackLabel]);
|
}, [refreshTracks]);
|
||||||
|
|
||||||
const pushLog = useCallback((message: string) => {
|
const pushLog = useCallback((message: string) => {
|
||||||
setLogs((prev) => {
|
setLogs((prev) => {
|
||||||
@@ -194,7 +194,7 @@ function PlayerExample({
|
|||||||
|
|
||||||
const cyclePlaybackRate = () => {
|
const cyclePlaybackRate = () => {
|
||||||
const rates = [0.75, 1, 1.25, 1.5, 2];
|
const rates = [0.75, 1, 1.25, 1.5, 2];
|
||||||
const index = rates.findIndex((rate) => rate === playbackRate);
|
const index = rates.indexOf(playbackRate);
|
||||||
const nextIndex = (index + 1) % rates.length;
|
const nextIndex = (index + 1) % rates.length;
|
||||||
player.playbackRate = rates[nextIndex];
|
player.playbackRate = rates[nextIndex];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ import { name as appName } from "./app.json";
|
|||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App);
|
||||||
|
|
||||||
AppRegistry.runApplication(appName, {
|
AppRegistry.runApplication(appName, {
|
||||||
rootTag: document.getElementById("root"),
|
rootTag: document.getElementById("root"),
|
||||||
});
|
});
|
||||||
|
|||||||
+20
-16
@@ -11,22 +11,26 @@ const useWeb = process.env.USE_WEB === "true" || process.env.PLATFORM === "web";
|
|||||||
* @type {import('metro-config').MetroConfig}
|
* @type {import('metro-config').MetroConfig}
|
||||||
*/
|
*/
|
||||||
const config = {
|
const config = {
|
||||||
watchFolders: [root],
|
watchFolders: [root],
|
||||||
...(useWeb && {
|
...(useWeb && {
|
||||||
resolver: {
|
resolver: {
|
||||||
extraNodeModules: {
|
extraNodeModules: {
|
||||||
"react-native": path.resolve(__dirname, "node_modules", "react-native-web"),
|
"react-native": path.resolve(
|
||||||
},
|
__dirname,
|
||||||
},
|
"node_modules",
|
||||||
}),
|
"react-native-web",
|
||||||
transformer: {
|
),
|
||||||
getTransformOptions: async () => ({
|
},
|
||||||
transform: {
|
},
|
||||||
experimentalImportSupport: false,
|
}),
|
||||||
inlineRequires: true,
|
transformer: {
|
||||||
},
|
getTransformOptions: async () => ({
|
||||||
}),
|
transform: {
|
||||||
},
|
experimentalImportSupport: false,
|
||||||
|
inlineRequires: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const path = require("path");
|
const path = require("node:path");
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
const appDirectory = path.resolve(__dirname);
|
const appDirectory = path.resolve(__dirname);
|
||||||
|
|||||||
+11
-11
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"uiViewClassName": "OmniView",
|
"uiViewClassName": "OmniView",
|
||||||
"supportsRawText": false,
|
"supportsRawText": false,
|
||||||
"bubblingEventTypes": {},
|
"bubblingEventTypes": {},
|
||||||
"directEventTypes": {},
|
"directEventTypes": {},
|
||||||
"validAttributes": {
|
"validAttributes": {
|
||||||
"player": true,
|
"player": true,
|
||||||
"autoplay": true,
|
"autoplay": true,
|
||||||
"autoPip": true,
|
"autoPip": true,
|
||||||
"subtitleAssets": true,
|
"subtitleAssets": true,
|
||||||
"hybridRef": true
|
"hybridRef": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -1,15 +1,15 @@
|
|||||||
import { createRunOncePlugin } from '@expo/config-plugins';
|
import type { ConfigPlugin } from "@expo/config-plugins";
|
||||||
import type { ConfigPlugin } from '@expo/config-plugins';
|
import { createRunOncePlugin } from "@expo/config-plugins";
|
||||||
import pkg from '../../package.json';
|
import pkg from "../../package.json";
|
||||||
import { withMediaNotifications } from './withMediaNotifications';
|
import { withMediaNotifications } from "./withMediaNotifications";
|
||||||
import { withPip } from './withPip';
|
import { withPip } from "./withPip";
|
||||||
|
|
||||||
const withOmni: ConfigPlugin = (config) => {
|
const withOmni: ConfigPlugin = (config) => {
|
||||||
let nextConfig = withPip(config);
|
let nextConfig = withPip(config);
|
||||||
nextConfig = withMediaNotifications(nextConfig);
|
nextConfig = withMediaNotifications(nextConfig);
|
||||||
return nextConfig;
|
return nextConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
|
export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
|
||||||
|
|
||||||
export { withPip, withMediaNotifications };
|
export { withMediaNotifications, withPip };
|
||||||
|
|||||||
@@ -1,68 +1,85 @@
|
|||||||
import { AndroidConfig, withAndroidManifest, type ConfigPlugin } from '@expo/config-plugins';
|
import {
|
||||||
|
AndroidConfig,
|
||||||
|
type ConfigPlugin,
|
||||||
|
withAndroidManifest,
|
||||||
|
} from "@expo/config-plugins";
|
||||||
|
|
||||||
function ensureUsesPermission(
|
function ensureUsesPermission(
|
||||||
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
permission: string
|
permission: string,
|
||||||
): void {
|
): void {
|
||||||
const usesPermissions = androidManifest.manifest['uses-permission'] ?? [];
|
const usesPermissions = androidManifest.manifest["uses-permission"] ?? [];
|
||||||
const alreadyDefined = usesPermissions.some((item) => item.$['android:name'] === permission);
|
const alreadyDefined = usesPermissions.some(
|
||||||
|
(item) => item.$["android:name"] === permission,
|
||||||
|
);
|
||||||
|
|
||||||
if (!alreadyDefined) {
|
if (!alreadyDefined) {
|
||||||
usesPermissions.push({
|
usesPermissions.push({
|
||||||
$: {
|
$: {
|
||||||
'android:name': permission,
|
"android:name": permission,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
androidManifest.manifest['uses-permission'] = usesPermissions;
|
androidManifest.manifest["uses-permission"] = usesPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchManifestForMediaNotifications(
|
function patchManifestForMediaNotifications(
|
||||||
androidManifest: AndroidConfig.Manifest.AndroidManifest
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
): AndroidConfig.Manifest.AndroidManifest {
|
): AndroidConfig.Manifest.AndroidManifest {
|
||||||
const mainApplication = AndroidConfig.Manifest.getMainApplication(androidManifest);
|
const mainApplication =
|
||||||
if (!mainApplication) {
|
AndroidConfig.Manifest.getMainApplication(androidManifest);
|
||||||
console.warn(
|
if (!mainApplication) {
|
||||||
'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.'
|
console.warn(
|
||||||
);
|
'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.',
|
||||||
return androidManifest;
|
);
|
||||||
}
|
return androidManifest;
|
||||||
|
}
|
||||||
|
|
||||||
const services = mainApplication.service ?? [];
|
const services = mainApplication.service ?? [];
|
||||||
const serviceName = 'dev.zoriya.omni.OmniPlayerService';
|
const serviceName = "dev.zoriya.omni.OmniPlayerService";
|
||||||
const hasService = services.some((service) => service.$['android:name'] === serviceName);
|
const hasService = services.some(
|
||||||
if (!hasService) {
|
(service) => service.$["android:name"] === serviceName,
|
||||||
services.push({
|
);
|
||||||
$: {
|
if (!hasService) {
|
||||||
'android:name': serviceName,
|
services.push({
|
||||||
'android:enabled': 'true',
|
$: {
|
||||||
'android:exported': 'true',
|
"android:name": serviceName,
|
||||||
'android:foregroundServiceType': 'mediaPlayback',
|
"android:enabled": "true",
|
||||||
},
|
"android:exported": "true",
|
||||||
'intent-filter': [
|
"android:foregroundServiceType": "mediaPlayback",
|
||||||
{
|
},
|
||||||
action: [
|
"intent-filter": [
|
||||||
{
|
{
|
||||||
$: {
|
action: [
|
||||||
'android:name': 'androidx.media3.session.MediaSessionService',
|
{
|
||||||
},
|
$: {
|
||||||
},
|
"android:name": "androidx.media3.session.MediaSessionService",
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
},
|
||||||
}
|
],
|
||||||
mainApplication.service = services;
|
});
|
||||||
|
}
|
||||||
|
mainApplication.service = services;
|
||||||
|
|
||||||
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE');
|
ensureUsesPermission(
|
||||||
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK');
|
androidManifest,
|
||||||
return androidManifest;
|
"android.permission.FOREGROUND_SERVICE",
|
||||||
|
);
|
||||||
|
ensureUsesPermission(
|
||||||
|
androidManifest,
|
||||||
|
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
|
||||||
|
);
|
||||||
|
return androidManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const withMediaNotifications: ConfigPlugin = (config) => {
|
export const withMediaNotifications: ConfigPlugin = (config) => {
|
||||||
return withAndroidManifest(config, (manifestConfig) => {
|
return withAndroidManifest(config, (manifestConfig) => {
|
||||||
manifestConfig.modResults = patchManifestForMediaNotifications(manifestConfig.modResults);
|
manifestConfig.modResults = patchManifestForMediaNotifications(
|
||||||
return manifestConfig;
|
manifestConfig.modResults,
|
||||||
});
|
);
|
||||||
|
return manifestConfig;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+139
-99
@@ -1,129 +1,169 @@
|
|||||||
import { AndroidConfig, withAndroidManifest, withMainActivity, type ConfigPlugin } from '@expo/config-plugins';
|
import {
|
||||||
import { addImports, appendContentsInsideDeclarationBlock } from '@expo/config-plugins/build/android/codeMod';
|
AndroidConfig,
|
||||||
|
type ConfigPlugin,
|
||||||
|
withAndroidManifest,
|
||||||
|
withMainActivity,
|
||||||
|
} from "@expo/config-plugins";
|
||||||
|
import {
|
||||||
|
addImports,
|
||||||
|
appendContentsInsideDeclarationBlock,
|
||||||
|
} from "@expo/config-plugins/build/android/codeMod";
|
||||||
|
|
||||||
const MODE_MARKER = 'OmniView.onActivityPipModeChanged(';
|
const MODE_MARKER = "OmniView.onActivityPipModeChanged(";
|
||||||
const UI_STATE_MARKER = 'OmniView.onActivityPipTransitionToPip(';
|
const UI_STATE_MARKER = "OmniView.onActivityPipTransitionToPip(";
|
||||||
|
|
||||||
function patchMainActivityForPip(mainActivity: string, language: 'java' | 'kt'): string {
|
function patchMainActivityForPip(
|
||||||
const isJava = language === 'java';
|
mainActivity: string,
|
||||||
|
language: "java" | "kt",
|
||||||
|
): string {
|
||||||
|
const isJava = language === "java";
|
||||||
|
|
||||||
if (mainActivity.includes(MODE_MARKER) && mainActivity.includes(UI_STATE_MARKER)) {
|
if (
|
||||||
return mainActivity;
|
mainActivity.includes(MODE_MARKER) &&
|
||||||
}
|
mainActivity.includes(UI_STATE_MARKER)
|
||||||
|
) {
|
||||||
|
return mainActivity;
|
||||||
|
}
|
||||||
|
|
||||||
const withRequiredImports = addImports(
|
const withRequiredImports = addImports(
|
||||||
mainActivity,
|
mainActivity,
|
||||||
['android.app.PictureInPictureUiState', 'android.os.Build', 'dev.zoriya.omni.OmniView'],
|
[
|
||||||
isJava
|
"android.app.PictureInPictureUiState",
|
||||||
);
|
"android.os.Build",
|
||||||
|
"dev.zoriya.omni.OmniView",
|
||||||
|
],
|
||||||
|
isJava,
|
||||||
|
);
|
||||||
|
|
||||||
let output = withRequiredImports;
|
let output = withRequiredImports;
|
||||||
|
|
||||||
if (!output.includes(MODE_MARKER)) {
|
if (!output.includes(MODE_MARKER)) {
|
||||||
const modeChangedBlock = isJava
|
const modeChangedBlock = isJava
|
||||||
? [
|
? [
|
||||||
'\n @Override',
|
"\n @Override",
|
||||||
' public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {',
|
" public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {",
|
||||||
' super.onPictureInPictureModeChanged(isInPictureInPictureMode);',
|
" super.onPictureInPictureModeChanged(isInPictureInPictureMode);",
|
||||||
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);',
|
" OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);",
|
||||||
' }\n',
|
" }\n",
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
'\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {',
|
"\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {",
|
||||||
' super.onPictureInPictureModeChanged(isInPictureInPictureMode)',
|
" super.onPictureInPictureModeChanged(isInPictureInPictureMode)",
|
||||||
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)',
|
" OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)",
|
||||||
' }\n',
|
" }\n",
|
||||||
];
|
];
|
||||||
|
|
||||||
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', modeChangedBlock.join('\n'));
|
output = appendContentsInsideDeclarationBlock(
|
||||||
}
|
output,
|
||||||
|
"class MainActivity",
|
||||||
|
modeChangedBlock.join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!output.includes(UI_STATE_MARKER)) {
|
if (!output.includes(UI_STATE_MARKER)) {
|
||||||
const uiStateChangedBlock = isJava
|
const uiStateChangedBlock = isJava
|
||||||
? [
|
? [
|
||||||
'\n @Override',
|
"\n @Override",
|
||||||
' public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {',
|
" public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {",
|
||||||
' super.onPictureInPictureUiStateChanged(pipState);',
|
" super.onPictureInPictureUiStateChanged(pipState);",
|
||||||
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {',
|
" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {",
|
||||||
' OmniView.onActivityPipTransitionToPip(this);',
|
" OmniView.onActivityPipTransitionToPip(this);",
|
||||||
' }',
|
" }",
|
||||||
' }\n',
|
" }\n",
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
'\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {',
|
"\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {",
|
||||||
' super.onPictureInPictureUiStateChanged(pipState)',
|
" super.onPictureInPictureUiStateChanged(pipState)",
|
||||||
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {',
|
" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {",
|
||||||
' OmniView.onActivityPipTransitionToPip(this)',
|
" OmniView.onActivityPipTransitionToPip(this)",
|
||||||
' }',
|
" }",
|
||||||
' }\n',
|
" }\n",
|
||||||
];
|
];
|
||||||
|
|
||||||
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', uiStateChangedBlock.join('\n'));
|
output = appendContentsInsideDeclarationBlock(
|
||||||
}
|
output,
|
||||||
|
"class MainActivity",
|
||||||
|
uiStateChangedBlock.join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureUsesFeature(
|
function ensureUsesFeature(
|
||||||
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
featureName: string,
|
featureName: string,
|
||||||
required: 'true' | 'false'
|
required: "true" | "false",
|
||||||
): void {
|
): void {
|
||||||
const usesFeature = androidManifest.manifest['uses-feature'] ?? [];
|
const usesFeature = androidManifest.manifest["uses-feature"] ?? [];
|
||||||
const existing = usesFeature.find((feature) => feature.$['android:name'] === featureName);
|
const existing = usesFeature.find(
|
||||||
|
(feature) => feature.$["android:name"] === featureName,
|
||||||
|
);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.$['android:required'] = required;
|
existing.$["android:required"] = required;
|
||||||
} else {
|
} else {
|
||||||
usesFeature.push({
|
usesFeature.push({
|
||||||
$: {
|
$: {
|
||||||
'android:name': featureName,
|
"android:name": featureName,
|
||||||
'android:required': required,
|
"android:required": required,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
androidManifest.manifest['uses-feature'] = usesFeature;
|
androidManifest.manifest["uses-feature"] = usesFeature;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureConfigChanges(mainActivity: AndroidConfig.Manifest.ManifestActivity): void {
|
function ensureConfigChanges(
|
||||||
const requiredChanges = ['screenSize', 'smallestScreenSize', 'screenLayout', 'orientation'];
|
mainActivity: AndroidConfig.Manifest.ManifestActivity,
|
||||||
const existing = mainActivity.$['android:configChanges'] ?? '';
|
): void {
|
||||||
const existingSet = new Set(
|
const requiredChanges = [
|
||||||
existing
|
"screenSize",
|
||||||
.split('|')
|
"smallestScreenSize",
|
||||||
.map((item) => item.trim())
|
"screenLayout",
|
||||||
.filter(Boolean)
|
"orientation",
|
||||||
);
|
];
|
||||||
|
const existing = mainActivity.$["android:configChanges"] ?? "";
|
||||||
|
const existingSet = new Set(
|
||||||
|
existing
|
||||||
|
.split("|")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
);
|
||||||
|
|
||||||
requiredChanges.forEach((item) => existingSet.add(item));
|
requiredChanges.forEach((item) => existingSet.add(item));
|
||||||
mainActivity.$['android:configChanges'] = Array.from(existingSet).join('|');
|
mainActivity.$["android:configChanges"] = Array.from(existingSet).join("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
function patchManifestForPip(
|
function patchManifestForPip(
|
||||||
androidManifest: AndroidConfig.Manifest.AndroidManifest
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
): AndroidConfig.Manifest.AndroidManifest {
|
): AndroidConfig.Manifest.AndroidManifest {
|
||||||
ensureUsesFeature(androidManifest, 'android.software.picture_in_picture', 'false');
|
ensureUsesFeature(
|
||||||
|
androidManifest,
|
||||||
|
"android.software.picture_in_picture",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
|
||||||
const mainActivity = AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest);
|
const mainActivity =
|
||||||
mainActivity.$['android:supportsPictureInPicture'] = 'true';
|
AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest);
|
||||||
mainActivity.$['android:resizeableActivity'] = 'true';
|
mainActivity.$["android:supportsPictureInPicture"] = "true";
|
||||||
ensureConfigChanges(mainActivity);
|
mainActivity.$["android:resizeableActivity"] = "true";
|
||||||
|
ensureConfigChanges(mainActivity);
|
||||||
|
|
||||||
return androidManifest;
|
return androidManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const withPip: ConfigPlugin = (config) => {
|
export const withPip: ConfigPlugin = (config) => {
|
||||||
const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
|
const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
|
||||||
activityConfig.modResults.contents = patchMainActivityForPip(
|
activityConfig.modResults.contents = patchMainActivityForPip(
|
||||||
activityConfig.modResults.contents,
|
activityConfig.modResults.contents,
|
||||||
activityConfig.modResults.language
|
activityConfig.modResults.language,
|
||||||
);
|
);
|
||||||
return activityConfig;
|
return activityConfig;
|
||||||
});
|
});
|
||||||
|
|
||||||
return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
|
return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
|
||||||
manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
|
manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
|
||||||
return manifestConfig;
|
return manifestConfig;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-12
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2019",
|
"target": "ES2019",
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -20,7 +20,10 @@ const updateViewManagerFiles = async (file) => {
|
|||||||
const viewManagerStr = await readFile(viewManagerFile, { encoding: "utf8" });
|
const viewManagerStr = await readFile(viewManagerFile, { encoding: "utf8" });
|
||||||
await writeFile(
|
await writeFile(
|
||||||
viewManagerFile,
|
viewManagerFile,
|
||||||
viewManagerStr.replace(/com\.margelo\.nitro\.omni\.\*/g, "dev.zoriya.omni.*"),
|
viewManagerStr.replace(
|
||||||
|
/com\.margelo\.nitro\.omni\.\*/g,
|
||||||
|
"dev.zoriya.omni.*",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,6 +52,9 @@ const androidWorkaround = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const str = await readFile(androidOnLoadFile, { encoding: "utf8" });
|
const str = await readFile(androidOnLoadFile, { encoding: "utf8" });
|
||||||
await writeFile(androidOnLoadFile, str.replace(/com\/margelo\/nitro\//g, "dev/zoriya/"));
|
await writeFile(
|
||||||
|
androidOnLoadFile,
|
||||||
|
str.replace(/com\/margelo\/nitro\//g, "dev/zoriya/"),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
androidWorkaround();
|
androidWorkaround();
|
||||||
|
|||||||
+22
-1
@@ -126,6 +126,27 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [event, player]);
|
}, [event, player]);
|
||||||
|
|
||||||
|
// ASS/PGS (overlay) subtitle selection is not a videojs text-track change, so
|
||||||
|
// re-fire subtitleChange when the overlay subtitle changes too — consumers
|
||||||
|
// only need useEvent("subtitleChange").
|
||||||
|
useEffect(() => {
|
||||||
|
if (event !== "subtitleChange") return;
|
||||||
|
return player.subscribeOverlaySubtitle(() => {
|
||||||
|
const sel = player.subtitles.find((s) => s.selected);
|
||||||
|
const cb = callbackRef.current as (s: unknown) => void;
|
||||||
|
cb(
|
||||||
|
sel
|
||||||
|
? {
|
||||||
|
id: sel.id,
|
||||||
|
label: sel.label,
|
||||||
|
language: sel.language,
|
||||||
|
selected: true,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [event, player]);
|
||||||
};
|
};
|
||||||
|
|
||||||
function createMapper<Key extends keyof OmniPlayerState, State, Result>(
|
function createMapper<Key extends keyof OmniPlayerState, State, Result>(
|
||||||
@@ -182,7 +203,7 @@ export const stateMapper = {
|
|||||||
if (!r) return "unavailable";
|
if (!r) return "unavailable";
|
||||||
if (r.remotePlaybackState === "connecting") return "connecting";
|
if (r.remotePlaybackState === "connecting") return "connecting";
|
||||||
if (r.remotePlaybackState === "connected") return "connected";
|
if (r.remotePlaybackState === "connected") return "connected";
|
||||||
return r.remotePlaybackAvailability
|
return r.remotePlaybackAvailability;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,26 @@ export class WebOmniPlayer implements OmniPlayer {
|
|||||||
const tracks = selectTextTrack(this._store.state);
|
const tracks = selectTextTrack(this._store.state);
|
||||||
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
|
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
|
||||||
this.setOverlaySubtitle(overlay ?? null);
|
this.setOverlaySubtitle(overlay ?? null);
|
||||||
|
|
||||||
|
if (this.castStatus === "connected") {
|
||||||
|
try {
|
||||||
|
// if the cast receiver handles overlay subtitles like us, send this message.
|
||||||
|
const ctx = (
|
||||||
|
window as unknown as {
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: cast sender SDK global
|
||||||
|
cast?: { framework?: { CastContext?: { getInstance(): any } } };
|
||||||
|
}
|
||||||
|
).cast?.framework?.CastContext?.getInstance?.();
|
||||||
|
ctx
|
||||||
|
?.getCurrentSession?.()
|
||||||
|
?.sendMessage("urn:x-cast:dev.zoriya.omni", {
|
||||||
|
subtitle: overlay?.id ?? null,
|
||||||
|
})
|
||||||
|
?.catch?.(() => {});
|
||||||
|
} catch {
|
||||||
|
// no active cast session / sender SDK not loaded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setOverlaySubtitle(sub: Subtitle | null): void {
|
private setOverlaySubtitle(sub: Subtitle | null): void {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ export interface Source {
|
|||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
metadata?: Metadata;
|
metadata?: Metadata;
|
||||||
mixAudio?: MixAudioMode;
|
mixAudio?: MixAudioMode;
|
||||||
|
// contentId of a cast request. defaults to src but can be overridden here.
|
||||||
|
castId?: string;
|
||||||
// Opaque, consumer-defined payload forwarded verbatim to the cast receiver
|
// Opaque, consumer-defined payload forwarded verbatim to the cast receiver
|
||||||
// as the load request's customData. omni does not interpret it.
|
// as the load request's customData. omni does not interpret it.
|
||||||
castData?: Record<string, string>;
|
castData?: Record<string, string>;
|
||||||
|
|||||||
+48
-33
@@ -9,6 +9,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { usePlayerState } from "./events";
|
||||||
import {
|
import {
|
||||||
getSubtitleFormat,
|
getSubtitleFormat,
|
||||||
isCustomSubtitle,
|
isCustomSubtitle,
|
||||||
@@ -58,35 +59,41 @@ const SubtitleOverlay = ({
|
|||||||
const renditions = player.rendition;
|
const renditions = player.rendition;
|
||||||
const videoWidth = Math.max(0, ...renditions.map((r) => r.width));
|
const videoWidth = Math.max(0, ...renditions.map((r) => r.width));
|
||||||
const videoHeight = Math.max(0, ...renditions.map((r) => r.height));
|
const videoHeight = Math.max(0, ...renditions.map((r) => r.height));
|
||||||
import("jassub").then(({ default: JASSUB }) => {
|
import("jassub")
|
||||||
const instance = new JASSUB({
|
.then(({ default: JASSUB }) => {
|
||||||
video: el,
|
const instance = new JASSUB({
|
||||||
subUrl: subtitle.link,
|
video: el,
|
||||||
...(videoWidth && { videoWidth }),
|
subUrl: subtitle.link,
|
||||||
...(videoHeight && { videoHeight }),
|
...(videoWidth && { videoWidth }),
|
||||||
...(fonts?.length && { fonts }),
|
...(videoHeight && { videoHeight }),
|
||||||
...(jassub?.workerUrl && { workerUrl: jassub.workerUrl }),
|
...(fonts?.length && { fonts }),
|
||||||
...(jassub?.wasmUrl && { wasmUrl: jassub.wasmUrl }),
|
...(jassub?.workerUrl && { workerUrl: jassub.workerUrl }),
|
||||||
...(jassub?.modernWasmUrl && { modernWasmUrl: jassub.modernWasmUrl }),
|
...(jassub?.wasmUrl && { wasmUrl: jassub.wasmUrl }),
|
||||||
...(jassub?.fontUrl && {
|
...(jassub?.modernWasmUrl && {
|
||||||
availableFonts: { "liberation sans": jassub.fontUrl },
|
modernWasmUrl: jassub.modernWasmUrl,
|
||||||
defaultFont: "liberation sans",
|
}),
|
||||||
}),
|
...(jassub?.fontUrl && {
|
||||||
});
|
availableFonts: { "liberation sans": jassub.fontUrl },
|
||||||
attach({ destroy: () => instance.destroy() });
|
defaultFont: "liberation sans",
|
||||||
});
|
}),
|
||||||
|
});
|
||||||
|
attach({ destroy: () => instance.destroy() });
|
||||||
|
})
|
||||||
|
.catch((e) => console.error("[omni] failed to render ass subtitle", e));
|
||||||
} else {
|
} else {
|
||||||
const pgs = assetsRef.current?.pgs;
|
const pgs = assetsRef.current?.pgs;
|
||||||
import("libpgs").then(({ PgsRenderer }) => {
|
import("libpgs")
|
||||||
const instance = new PgsRenderer({
|
.then(({ PgsRenderer }) => {
|
||||||
video: el,
|
const instance = new PgsRenderer({
|
||||||
subUrl: subtitle.link,
|
video: el,
|
||||||
workerUrl:
|
subUrl: subtitle.link,
|
||||||
pgs?.workerUrl ??
|
workerUrl:
|
||||||
new URL("libpgs/dist/libpgs.worker.js", import.meta.url).href,
|
pgs?.workerUrl ??
|
||||||
});
|
new URL("libpgs/dist/libpgs.worker.js", import.meta.url).href,
|
||||||
attach({ destroy: () => instance.dispose() });
|
});
|
||||||
});
|
attach({ destroy: () => instance.dispose() });
|
||||||
|
})
|
||||||
|
.catch((e) => console.error("[omni] failed to render pgs subtitle", e));
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -116,6 +123,7 @@ export const OmniView = ({
|
|||||||
|
|
||||||
const headersRef = useRef(src?.headers);
|
const headersRef = useRef(src?.headers);
|
||||||
headersRef.current = src?.headers;
|
headersRef.current = src?.headers;
|
||||||
|
const castId = player.source?.castId;
|
||||||
const castData = player.source?.castData;
|
const castData = player.source?.castData;
|
||||||
|
|
||||||
const config = useMemo<HlsMediaConfig>(
|
const config = useMemo<HlsMediaConfig>(
|
||||||
@@ -132,11 +140,16 @@ export const OmniView = ({
|
|||||||
googleCast: {
|
googleCast: {
|
||||||
receiver: player.castOptions?.receiverApplicationId,
|
receiver: player.castOptions?.receiverApplicationId,
|
||||||
customData: castData ?? null,
|
customData: castData ?? null,
|
||||||
|
...(castId && { src: castId }),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[player.castOptions, castData],
|
[player.castOptions, castData, castId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// While casting, the receiver renders subtitles (the player forwards the
|
||||||
|
// selection); skip rendering them locally on the idle <video>.
|
||||||
|
const castStatus = usePlayerState("castStatus");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoPlayer.Container
|
<VideoPlayer.Container
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -166,11 +179,13 @@ export const OmniView = ({
|
|||||||
))}
|
))}
|
||||||
</Tech>
|
</Tech>
|
||||||
)}
|
)}
|
||||||
<SubtitleOverlay
|
{castStatus !== "connected" && castStatus !== "connecting" && (
|
||||||
video={ref}
|
<SubtitleOverlay
|
||||||
assets={subtitleAssets}
|
video={ref}
|
||||||
fonts={player.source?.fonts}
|
assets={subtitleAssets}
|
||||||
/>
|
fonts={player.source?.fonts}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</VideoPlayer.Container>
|
</VideoPlayer.Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user