feat(chromecast): send custom message for overlay subtitles

This commit is contained in:
2026-07-21 19:28:36 +02:00
parent e6a06b13a4
commit a335509b4e
15 changed files with 366 additions and 241 deletions
+9 -9
View File
@@ -1,15 +1,15 @@
import { createRunOncePlugin } from '@expo/config-plugins';
import type { ConfigPlugin } from '@expo/config-plugins';
import pkg from '../../package.json';
import { withMediaNotifications } from './withMediaNotifications';
import { withPip } from './withPip';
import type { ConfigPlugin } from "@expo/config-plugins";
import { createRunOncePlugin } from "@expo/config-plugins";
import pkg from "../../package.json";
import { withMediaNotifications } from "./withMediaNotifications";
import { withPip } from "./withPip";
const withOmni: ConfigPlugin = (config) => {
let nextConfig = withPip(config);
nextConfig = withMediaNotifications(nextConfig);
return nextConfig;
let nextConfig = withPip(config);
nextConfig = withMediaNotifications(nextConfig);
return nextConfig;
};
export default createRunOncePlugin(withOmni, pkg.name, pkg.version);
export { withPip, withMediaNotifications };
export { withMediaNotifications, withPip };
+70 -53
View File
@@ -1,68 +1,85 @@
import { AndroidConfig, withAndroidManifest, type ConfigPlugin } from '@expo/config-plugins';
import {
AndroidConfig,
type ConfigPlugin,
withAndroidManifest,
} from "@expo/config-plugins";
function ensureUsesPermission(
androidManifest: AndroidConfig.Manifest.AndroidManifest,
permission: string
androidManifest: AndroidConfig.Manifest.AndroidManifest,
permission: string,
): void {
const usesPermissions = androidManifest.manifest['uses-permission'] ?? [];
const alreadyDefined = usesPermissions.some((item) => item.$['android:name'] === permission);
const usesPermissions = androidManifest.manifest["uses-permission"] ?? [];
const alreadyDefined = usesPermissions.some(
(item) => item.$["android:name"] === permission,
);
if (!alreadyDefined) {
usesPermissions.push({
$: {
'android:name': permission,
},
});
}
if (!alreadyDefined) {
usesPermissions.push({
$: {
"android:name": permission,
},
});
}
androidManifest.manifest['uses-permission'] = usesPermissions;
androidManifest.manifest["uses-permission"] = usesPermissions;
}
function patchManifestForMediaNotifications(
androidManifest: AndroidConfig.Manifest.AndroidManifest
androidManifest: AndroidConfig.Manifest.AndroidManifest,
): AndroidConfig.Manifest.AndroidManifest {
const mainApplication = AndroidConfig.Manifest.getMainApplication(androidManifest);
if (!mainApplication) {
console.warn(
'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.'
);
return androidManifest;
}
const mainApplication =
AndroidConfig.Manifest.getMainApplication(androidManifest);
if (!mainApplication) {
console.warn(
'AndroidManifest.xml is missing a <application android:name=".MainApplication" /> element - skipping Omni media service config.',
);
return androidManifest;
}
const services = mainApplication.service ?? [];
const serviceName = 'dev.zoriya.omni.OmniPlayerService';
const hasService = services.some((service) => service.$['android:name'] === serviceName);
if (!hasService) {
services.push({
$: {
'android:name': serviceName,
'android:enabled': 'true',
'android:exported': 'true',
'android:foregroundServiceType': 'mediaPlayback',
},
'intent-filter': [
{
action: [
{
$: {
'android:name': 'androidx.media3.session.MediaSessionService',
},
},
],
},
],
});
}
mainApplication.service = services;
const services = mainApplication.service ?? [];
const serviceName = "dev.zoriya.omni.OmniPlayerService";
const hasService = services.some(
(service) => service.$["android:name"] === serviceName,
);
if (!hasService) {
services.push({
$: {
"android:name": serviceName,
"android:enabled": "true",
"android:exported": "true",
"android:foregroundServiceType": "mediaPlayback",
},
"intent-filter": [
{
action: [
{
$: {
"android:name": "androidx.media3.session.MediaSessionService",
},
},
],
},
],
});
}
mainApplication.service = services;
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE');
ensureUsesPermission(androidManifest, 'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK');
return androidManifest;
ensureUsesPermission(
androidManifest,
"android.permission.FOREGROUND_SERVICE",
);
ensureUsesPermission(
androidManifest,
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
);
return androidManifest;
}
export const withMediaNotifications: ConfigPlugin = (config) => {
return withAndroidManifest(config, (manifestConfig) => {
manifestConfig.modResults = patchManifestForMediaNotifications(manifestConfig.modResults);
return manifestConfig;
});
return withAndroidManifest(config, (manifestConfig) => {
manifestConfig.modResults = patchManifestForMediaNotifications(
manifestConfig.modResults,
);
return manifestConfig;
});
};
+139 -99
View File
@@ -1,129 +1,169 @@
import { AndroidConfig, withAndroidManifest, withMainActivity, type ConfigPlugin } from '@expo/config-plugins';
import { addImports, appendContentsInsideDeclarationBlock } from '@expo/config-plugins/build/android/codeMod';
import {
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 UI_STATE_MARKER = 'OmniView.onActivityPipTransitionToPip(';
const MODE_MARKER = "OmniView.onActivityPipModeChanged(";
const UI_STATE_MARKER = "OmniView.onActivityPipTransitionToPip(";
function patchMainActivityForPip(mainActivity: string, language: 'java' | 'kt'): string {
const isJava = language === 'java';
function patchMainActivityForPip(
mainActivity: string,
language: "java" | "kt",
): string {
const isJava = language === "java";
if (mainActivity.includes(MODE_MARKER) && mainActivity.includes(UI_STATE_MARKER)) {
return mainActivity;
}
if (
mainActivity.includes(MODE_MARKER) &&
mainActivity.includes(UI_STATE_MARKER)
) {
return mainActivity;
}
const withRequiredImports = addImports(
mainActivity,
['android.app.PictureInPictureUiState', 'android.os.Build', 'dev.zoriya.omni.OmniView'],
isJava
);
const withRequiredImports = addImports(
mainActivity,
[
"android.app.PictureInPictureUiState",
"android.os.Build",
"dev.zoriya.omni.OmniView",
],
isJava,
);
let output = withRequiredImports;
let output = withRequiredImports;
if (!output.includes(MODE_MARKER)) {
const modeChangedBlock = isJava
? [
'\n @Override',
' public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {',
' super.onPictureInPictureModeChanged(isInPictureInPictureMode);',
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);',
' }\n',
]
: [
'\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {',
' super.onPictureInPictureModeChanged(isInPictureInPictureMode)',
' OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)',
' }\n',
];
if (!output.includes(MODE_MARKER)) {
const modeChangedBlock = isJava
? [
"\n @Override",
" public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {",
" super.onPictureInPictureModeChanged(isInPictureInPictureMode);",
" OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode);",
" }\n",
]
: [
"\n override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {",
" super.onPictureInPictureModeChanged(isInPictureInPictureMode)",
" OmniView.onActivityPipModeChanged(this, isInPictureInPictureMode)",
" }\n",
];
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', modeChangedBlock.join('\n'));
}
output = appendContentsInsideDeclarationBlock(
output,
"class MainActivity",
modeChangedBlock.join("\n"),
);
}
if (!output.includes(UI_STATE_MARKER)) {
const uiStateChangedBlock = isJava
? [
'\n @Override',
' public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {',
' super.onPictureInPictureUiStateChanged(pipState);',
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {',
' OmniView.onActivityPipTransitionToPip(this);',
' }',
' }\n',
]
: [
'\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {',
' super.onPictureInPictureUiStateChanged(pipState)',
' if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {',
' OmniView.onActivityPipTransitionToPip(this)',
' }',
' }\n',
];
if (!output.includes(UI_STATE_MARKER)) {
const uiStateChangedBlock = isJava
? [
"\n @Override",
" public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) {",
" super.onPictureInPictureUiStateChanged(pipState);",
" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip()) {",
" OmniView.onActivityPipTransitionToPip(this);",
" }",
" }\n",
]
: [
"\n override fun onPictureInPictureUiStateChanged(pipState: PictureInPictureUiState) {",
" super.onPictureInPictureUiStateChanged(pipState)",
" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && pipState.isTransitioningToPip) {",
" OmniView.onActivityPipTransitionToPip(this)",
" }",
" }\n",
];
output = appendContentsInsideDeclarationBlock(output, 'class MainActivity', uiStateChangedBlock.join('\n'));
}
output = appendContentsInsideDeclarationBlock(
output,
"class MainActivity",
uiStateChangedBlock.join("\n"),
);
}
return output;
return output;
}
function ensureUsesFeature(
androidManifest: AndroidConfig.Manifest.AndroidManifest,
featureName: string,
required: 'true' | 'false'
androidManifest: AndroidConfig.Manifest.AndroidManifest,
featureName: string,
required: "true" | "false",
): void {
const usesFeature = androidManifest.manifest['uses-feature'] ?? [];
const existing = usesFeature.find((feature) => feature.$['android:name'] === featureName);
const usesFeature = androidManifest.manifest["uses-feature"] ?? [];
const existing = usesFeature.find(
(feature) => feature.$["android:name"] === featureName,
);
if (existing) {
existing.$['android:required'] = required;
} else {
usesFeature.push({
$: {
'android:name': featureName,
'android:required': required,
},
});
}
if (existing) {
existing.$["android:required"] = required;
} else {
usesFeature.push({
$: {
"android:name": featureName,
"android:required": required,
},
});
}
androidManifest.manifest['uses-feature'] = usesFeature;
androidManifest.manifest["uses-feature"] = usesFeature;
}
function ensureConfigChanges(mainActivity: AndroidConfig.Manifest.ManifestActivity): void {
const requiredChanges = ['screenSize', 'smallestScreenSize', 'screenLayout', 'orientation'];
const existing = mainActivity.$['android:configChanges'] ?? '';
const existingSet = new Set(
existing
.split('|')
.map((item) => item.trim())
.filter(Boolean)
);
function ensureConfigChanges(
mainActivity: AndroidConfig.Manifest.ManifestActivity,
): void {
const requiredChanges = [
"screenSize",
"smallestScreenSize",
"screenLayout",
"orientation",
];
const existing = mainActivity.$["android:configChanges"] ?? "";
const existingSet = new Set(
existing
.split("|")
.map((item) => item.trim())
.filter(Boolean),
);
requiredChanges.forEach((item) => existingSet.add(item));
mainActivity.$['android:configChanges'] = Array.from(existingSet).join('|');
requiredChanges.forEach((item) => existingSet.add(item));
mainActivity.$["android:configChanges"] = Array.from(existingSet).join("|");
}
function patchManifestForPip(
androidManifest: AndroidConfig.Manifest.AndroidManifest
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);
mainActivity.$['android:supportsPictureInPicture'] = 'true';
mainActivity.$['android:resizeableActivity'] = 'true';
ensureConfigChanges(mainActivity);
const mainActivity =
AndroidConfig.Manifest.getMainActivityOrThrow(androidManifest);
mainActivity.$["android:supportsPictureInPicture"] = "true";
mainActivity.$["android:resizeableActivity"] = "true";
ensureConfigChanges(mainActivity);
return androidManifest;
return androidManifest;
}
export const withPip: ConfigPlugin = (config) => {
const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
activityConfig.modResults.contents = patchMainActivityForPip(
activityConfig.modResults.contents,
activityConfig.modResults.language
);
return activityConfig;
});
const withMainActivityPatched = withMainActivity(config, (activityConfig) => {
activityConfig.modResults.contents = patchMainActivityForPip(
activityConfig.modResults.contents,
activityConfig.modResults.language,
);
return activityConfig;
});
return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
return manifestConfig;
});
return withAndroidManifest(withMainActivityPatched, (manifestConfig) => {
manifestConfig.modResults = patchManifestForPip(manifestConfig.modResults);
return manifestConfig;
});
};
+12 -12
View File
@@ -1,14 +1,14 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "CommonJS",
"declaration": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
"compilerOptions": {
"target": "ES2019",
"module": "CommonJS",
"declaration": true,
"outDir": "build",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*"]
}