Live video on the AT Protocol
1import {
2 ConfigPlugin,
3 withAndroidManifest,
4 withEntitlementsPlist,
5 withXcodeProject,
6} from "expo/config-plugins";
7import streamplaceReactNativeWebRTC from "../config-react-native-webrtc";
8export const withNotificationsIOS: ConfigPlugin = (config) => {
9 config = withEntitlementsPlist(config, (config) => {
10 config.modResults["aps-environment"] = "production";
11 return config;
12 });
13 return config;
14};
15
16export const withoutNotificationsIOS: ConfigPlugin = (config) => {
17 config = withEntitlementsPlist(config, (config) => {
18 delete config.modResults["aps-environment"];
19 return config;
20 });
21 return config;
22};
23
24const withAndroidProfileable = (config) => {
25 return withAndroidManifest(config, (config) => {
26 const androidManifest = config.modResults.manifest;
27 if (
28 !androidManifest.application ||
29 androidManifest.application.length === 0
30 ) {
31 throw new Error("No application found in AndroidManifest.xml");
32 }
33 const mainApplication = androidManifest.application[0];
34
35 (mainApplication as any).profileable = [
36 {
37 $: {
38 "android:shell": "true",
39 "android:enabled": "true",
40 },
41 },
42 ];
43
44 return config;
45 });
46};
47
48const withConsistentVersionNumber = (
49 config,
50 { version }: { version: string },
51) => {
52 // if (!config.ios) {
53 // config.ios = {};
54 // }
55 // if (!config.ios.infoPlist) {
56 // config.ios.infoPlist = {};
57 // }
58 config = withXcodeProject(config, (config) => {
59 for (let [k, v] of Object.entries(
60 config.modResults.hash.project.objects.XCBuildConfiguration,
61 )) {
62 const obj = v as any;
63 if (!obj.buildSettings) {
64 continue;
65 }
66 if (typeof obj.buildSettings.MARKETING_VERSION !== "undefined") {
67 obj.buildSettings.MARKETING_VERSION = version;
68 }
69 if (typeof obj.buildSettings.CURRENT_PROJECT_VERSION !== "undefined") {
70 obj.buildSettings.CURRENT_PROJECT_VERSION = version;
71 }
72 }
73 return config;
74 });
75 return config;
76};
77
78// turn a semver string into a always-increasing integer for google
79export const versionCode = (verStr: string) => {
80 const [major, minor, patch] = verStr.split(".").map((x) => parseInt(x));
81 return major * 1000 * 1000 + minor * 1000 + patch;
82};
83
84export default function () {
85 const isProd =
86 process.env["SP_PRODUCTION_RELEASE"] === "true" || !!process.env.CI;
87 const pkg = require("./package.json");
88 const name = isProd ? "Streamplace" : "Devplace";
89 let bundle = isProd ? "tv.aquareum" : "tv.aquareum.dev";
90 if (process.env["SP_BUNDLE_OVERRIDE"]) {
91 bundle = process.env["SP_BUNDLE_OVERRIDE"];
92 }
93 let appleTeamId = process.env["SP_APPLE_TEAM_ID"];
94 const scheme = process.env["SP_APP_SCHEME"] ?? bundle;
95 return {
96 expo: {
97 name: name,
98 slug: name,
99 version: pkg.version,
100 // Only rev this to the current version when native dependencies change!
101 runtimeVersion: pkg.runtimeVersion,
102 orientation: "default",
103 icon: "./assets/images/icon.png",
104 scheme: scheme,
105 userInterfaceStyle: "automatic",
106 splash: {
107 image: "./assets/images/splash.png",
108 resizeMode: "contain",
109 backgroundColor: "#ffffff",
110 },
111 assetBundlePatterns: ["**/*"],
112 ios: {
113 supportsTablet: true,
114 bundleIdentifier: bundle,
115 infoPlist: {
116 UIBackgroundModes: ["fetch", "remote-notification"],
117 LSMinimumSystemVersion: "12.0",
118 },
119 ...(appleTeamId
120 ? {
121 appleTeamId,
122 }
123 : {}),
124 ...(isProd
125 ? {
126 googleServicesFile: "./GoogleService-Info.plist",
127 entitlements: {
128 "aps-environment": "production",
129 },
130 associatedDomains: ["applinks:stream.place"],
131 }
132 : {}),
133 },
134 android: {
135 adaptiveIcon: {
136 foregroundImage: "./assets/images/adaptive-icon.png",
137 backgroundColor: "#ffffff",
138 },
139 package: bundle,
140 edgeToEdgeEnabled: true,
141 versionCode: versionCode(pkg.version),
142 intentFilters: [
143 {
144 action: "VIEW",
145 autoVerify: true,
146 data: [
147 {
148 scheme: "https",
149 host: "stream.place",
150 pathPattern: "/.*:.*",
151 },
152 ],
153 category: ["BROWSABLE", "DEFAULT"],
154 },
155 {
156 action: "VIEW",
157 autoVerify: true,
158 data: [
159 {
160 scheme: "https",
161 host: "stream.place",
162 pathPattern: "/.*\\\\..*",
163 },
164 ],
165 category: ["BROWSABLE", "DEFAULT"],
166 },
167 {
168 action: "VIEW",
169 autoVerify: true,
170 data: [
171 {
172 scheme: "https",
173 host: "stream.place",
174 path: "/",
175 },
176 ],
177 category: ["BROWSABLE", "DEFAULT"],
178 },
179 ],
180 ...(isProd
181 ? {
182 googleServicesFile: "./google-services.json",
183 permissions: [
184 "android.permission.SCHEDULE_EXACT_ALARM",
185 "android.permission.POST_NOTIFICATIONS",
186 ],
187 }
188 : {}),
189 },
190 web: {
191 bundler: "metro",
192 output: "single",
193 favicon: "./assets/images/favicon.png",
194 },
195 plugins: [
196 withAndroidProfileable,
197 "expo-video",
198 "expo-web-browser",
199 streamplaceReactNativeWebRTC,
200 [
201 "expo-video",
202 {
203 supportsBackgroundPlayback: true,
204 supportsPictureInPicture: true,
205 },
206 ],
207 ["expo-sqlite", { useSQLCipher: true }],
208 "expo-file-system",
209 [
210 "expo-font",
211 {
212 fonts: [
213 "assets/fonts/FiraCode-Bold.ttf",
214 "assets/fonts/FiraCode-Light.ttf",
215 "assets/fonts/FiraCode-Medium.ttf",
216 "assets/fonts/FiraCode-Regular.ttf",
217 "assets/fonts/FiraCode-Retina.ttf",
218 "assets/fonts/FiraSans-Black.ttf",
219 "assets/fonts/FiraSans-BlackItalic.ttf",
220 "assets/fonts/FiraSans-Bold.ttf",
221 "assets/fonts/FiraSans-BoldItalic.ttf",
222 "assets/fonts/FiraSans-ExtraBold.ttf",
223 "assets/fonts/FiraSans-ExtraBoldItalic.ttf",
224 "assets/fonts/FiraSans-ExtraLight.ttf",
225 "assets/fonts/FiraSans-ExtraLightItalic.ttf",
226 "assets/fonts/FiraSans-Italic.ttf",
227 "assets/fonts/FiraSans-Light.ttf",
228 "assets/fonts/FiraSans-LightItalic.ttf",
229 "assets/fonts/FiraSans-Medium.ttf",
230 "assets/fonts/FiraSans-MediumItalic.ttf",
231 "assets/fonts/FiraSans-Regular.ttf",
232 "assets/fonts/FiraSans-SemiBold.ttf",
233 "assets/fonts/FiraSans-SemiBoldItalic.ttf",
234 "assets/fonts/FiraSans-Thin.ttf",
235 "assets/fonts/FiraSans-ThinItalic.ttf",
236 "assets/fonts/SpaceMono-Regular.ttf",
237 ],
238 },
239 ],
240 [
241 "expo-build-properties",
242 {
243 ios: {
244 useFrameworks: "static",
245 },
246 // uncomment to test OTA updates to http://localhost:8080
247 // android: {
248 // usesCleartextTraffic: true,
249 // },
250 },
251 ],
252 [
253 "expo-asset",
254 {
255 assets: ["assets"],
256 },
257 ],
258 [withConsistentVersionNumber, { version: pkg.version }],
259 [
260 "react-native-edge-to-edge",
261 {
262 android: {
263 parentTheme: "Default",
264 enforceNavigationBarContrast: false,
265 },
266 },
267 ],
268 ...(isProd
269 ? [
270 "@react-native-firebase/app",
271 "@react-native-firebase/messaging",
272 [withNotificationsIOS, {}],
273 ]
274 : ["expo-dev-launcher", withoutNotificationsIOS]),
275 ],
276 experiments: {
277 typedRoutes: true,
278 },
279 updates: isProd
280 ? {
281 url: `https://stream.place/api/manifest`,
282 enabled: true,
283 checkAutomatically: "ON_LOAD",
284 fallbackToCacheTimeout: 30000,
285 codeSigningCertificate: "./code-signing/certs/certificate.pem",
286 codeSigningMetadata: {
287 keyid: "main",
288 alg: "rsa-v1_5-sha256",
289 },
290 }
291 : {},
292 },
293 };
294}