Loading apps/demo/next.config.js +20 −6 Original line number Diff line number Diff line Loading @@ -7,15 +7,26 @@ const withMDX = require("@next/mdx")({ }, }); const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", }); const basePath = process.env.BASE_PATH ?? ""; const withPwa = require("@yume-chan/next-pwa")({ dest: "public", }); function pipe(value, ...callbacks) { for (const callback of callbacks) { value = callback(value); } return value; } /** @type {import('next').NextConfig} */ module.exports = withPwa( withMDX({ module.exports = pipe( { basePath, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], reactStrictMode: false, Loading @@ -27,7 +38,8 @@ module.exports = withPwa( publicRuntimeConfig: { basePath, }, webpack(config, options) { webpack(config) { // Bundle Scrcpy Server config.module.rules.push({ resourceQuery: /url/, type: "asset/resource", Loading @@ -49,10 +61,9 @@ module.exports = withPwa( enforce: "pre", }); // config.experiments.topLevelAwait = true; return config; }, // Enable Direct Sockets API async headers() { return [ { Loading @@ -70,5 +81,8 @@ module.exports = withPwa( }, ]; }, }) }, withBundleAnalyzer, withPwa, withMDX ); apps/demo/package.json +14 −22 Original line number Diff line number Diff line Loading @@ -10,8 +10,7 @@ "lint": "next lint" }, "dependencies": { "@angular/compiler": "^15.2.6", "@fluentui/react": "^8.107.5", "@fluentui/react": "^8.108.1", "@fluentui/react-file-type-icons": "^8.8.13", "@fluentui/react-hooks": "^8.6.20", "@fluentui/react-icons": "^2.0.200", Loading @@ -19,15 +18,15 @@ "@griffel/react": "^1.5.7", "@yume-chan/adb": "workspace:^0.0.19", "@yume-chan/adb-backend-direct-sockets": "workspace:^0.0.9", "@yume-chan/adb-backend-proxy": "workspace:^0.0.9", "@yume-chan/adb-backend-proxy": "workspace:^0.0.19", "@yume-chan/adb-backend-webusb": "workspace:^0.0.19", "@yume-chan/adb-backend-ws": "workspace:^0.0.9", "@yume-chan/adb-credential-web": "workspace:^0.0.19", "@yume-chan/adb-scrcpy": "workspace:^0.0.19", "@yume-chan/android-bin": "workspace:^0.0.19", "@yume-chan/aoa": "workspace:^0.0.18", "@yume-chan/aoa": "workspace:^0.0.19", "@yume-chan/async": "^2.2.0", "@yume-chan/b-tree": "workspace:^0.0.16", "@yume-chan/b-tree": "workspace:^0.0.19", "@yume-chan/event": "workspace:^0.0.19", "@yume-chan/pcm-player": "workspace:^0.0.19", "@yume-chan/scrcpy": "workspace:^0.0.19", Loading @@ -36,35 +35,28 @@ "@yume-chan/stream-extra": "workspace:^0.0.19", "@yume-chan/stream-saver": "^2.0.6", "@yume-chan/struct": "workspace:^0.0.19", "@yume-chan/tabby-tango": "workspace:^0.0.19", "@yume-chan/undici-browser": "5.21.2-mod.9", "@yume-chan/tabby-launcher": "workspace:^1.0.197-nightly.1", "fflate": "^0.7.4", "yaml": "^2.2.1", "mobx": "^6.7.0", "mobx-react-lite": "^3.4.3", "next": "13.3.0", "tabby-core": "^1.0.197-nightly.0", "tabby-settings": "^1.0.197-nightly.0", "tabby-terminal": "^1.0.197-nightly.0", "tabby-community-color-schemes": "^1.0.197-nightly.0", "tabby-web": "^1.0.197-nightly.0", "tabby-web-container": "^1.0.197-nightly.0", "next": "13.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "rxjs": "^7.8.0", "webm-muxer": "^2.2.3" "webm-muxer": "^3.0.3" }, "devDependencies": { "@mdx-js/loader": "^2.2.1", "@mdx-js/react": "^2.2.1", "@next/mdx": "^13.2.4", "@next/bundle-analyzer": "^13.3.1", "@next/mdx": "^13.3.1", "@types/dom-webcodecs": "^0.1.6", "@types/node": "^18.15.3", "@types/react": "18.0.35", "@types/node": "^18.16.0", "@types/react": "18.0.38", "@yume-chan/next-pwa": "5.6.0-mod.2", "eslint": "^8.36.0", "eslint-config-next": "13.3.0", "prettier": "^2.8.4", "eslint": "^8.39.0", "eslint-config-next": "13.3.1", "prettier": "^2.8.8", "source-map-loader": "^4.0.1", "typescript": "^5.0.3" } Loading apps/demo/scripts/manifest.mjs +0 −24 Original line number Diff line number Diff line Loading @@ -27,27 +27,3 @@ fs.writeFileSync( ), "utf8" ); fs.writeFileSync( new URL( "../node_modules/tabby-web-container/dist/preload.mjs", import.meta.url ), "export {};\n" + fs .readFileSync( new URL( "../node_modules/tabby-web-container/dist/preload.js", import.meta.url ), "utf8" ) .replaceAll(/__webpack_require__\.p \+ "(.+)"/g, (_, match) => { return `new URL("./${match}", import.meta.url).toString()`; }) .replaceAll(/__webpack_require__/g, "__webpack_require_nested__") .replace( "var scriptUrl;", "var scriptUrl = import.meta.url.toString();" ) ); apps/demo/src/components/scrcpy/recorder.ts +6 −5 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ import { h265SearchConfiguration, } from "@yume-chan/scrcpy"; import { action, makeAutoObservable, reaction } from "mobx"; import WebMMuxer from "webm-muxer"; import { ArrayBufferTarget, Muxer as WebMMuxer } from "webm-muxer"; import { saveFile } from "../../utils"; // https://ffmpeg.org/doxygen/0.11/avc_8c-source.html#l00106 Loading Loading @@ -234,7 +234,7 @@ export class MatroskaMuxingRecorder { public videoMetadata: ScrcpyVideoStreamMetadata | undefined; public audioCodec: ScrcpyAudioCodec | undefined; private muxer: WebMMuxer | undefined; private muxer: WebMMuxer<ArrayBufferTarget> | undefined; private videoCodecDescription: Uint8Array | undefined; private configurationWritten = false; private _firstTimestamp = -1; Loading Loading @@ -351,7 +351,7 @@ export class MatroskaMuxingRecorder { this.running = true; const options: ConstructorParameters<typeof WebMMuxer>[0] = { target: "buffer", target: new ArrayBufferTarget(), type: "matroska", firstTimestampBehavior: "permissive", video: { Loading @@ -371,7 +371,7 @@ export class MatroskaMuxingRecorder { }; } this.muxer = new WebMMuxer(options); this.muxer = new WebMMuxer(options as any); if (this._packetsFromLastKeyframe.length > 0) { for (const { type, packet } of this._packetsFromLastKeyframe) { Loading @@ -389,7 +389,8 @@ export class MatroskaMuxingRecorder { return; } const buffer = this.muxer.finalize()!; this.muxer.finalize()!; const buffer = this.muxer.target.buffer; const now = new Date(); const stream = saveFile( // prettier-ignore Loading apps/demo/src/pages/tabby-frame.tsx +4 −214 Original line number Diff line number Diff line import { Adb } from "@yume-chan/adb"; import { AdbProxyBackend, AdbProxyServerInfo, } from "@yume-chan/adb-backend-proxy"; import { useEffect } from "react"; import { Subject } from "rxjs"; import Yaml from "yaml"; export class NotImplementedSocket { connect$ = new Subject<void>(); data$ = new Subject<Buffer>(); error$ = new Subject<Error>(); close$ = new Subject<Buffer>(); async connect() { this.error$.next(new Error("Socket is not implemented in Web")); } } // Usage of connector is scattered around the Tabby codebase, // but these is all methods that are required. class WebConnector { constructor() {} async loadConfig(): Promise<string> { let config; const text = localStorage.getItem("tabby-config"); if (text) { config = Yaml.parse(text); } else { config = { recoverTabs: false, web: { preventAccidentalTabClosure: false, }, terminal: { fontSize: 11, }, }; } config.providerBlacklist = [ ...(config.providerBlacklist ?? []), "settings:ProfilesSettingsTabProvider", ]; config.commandBlacklist = [ ...(config.commandBlacklist ?? []), "core:profile-selector", ]; return Yaml.stringify(config); } async saveConfig(content: string): Promise<void> { localStorage.setItem("tabby-config", content); } getAppVersion(): string { return "1.0.197-nightly.0"; } createSocket() { return new NotImplementedSocket(); } } interface TabbyWeb { registerPluginModule(packageName: string, exports: unknown): void; bootstrap(options: unknown): Promise<void>; } interface TabbyPluginModule { pluginName: string; } interface GlobalExtension { __connector__: WebConnector | undefined; module: any; exports: any; Tabby: TabbyWeb; pluginModules: TabbyPluginModule[]; } const globalExtension = globalThis as unknown as GlobalExtension; async function start() { const connector = new WebConnector(); globalExtension["__connector__"] = connector; await import("@angular/compiler"); // @ts-expect-error await import("tabby-web-container/dist/preload.mjs"); // @ts-expect-error await import("tabby-web-container/dist/bundle.js"); async function webRequire(url: string | URL) { if (typeof url === "object") { url = url.toString(); } console.log(`Loading ${url}`); const e = document.createElement("script"); globalExtension["module"] = { exports: {} }; globalExtension["exports"] = globalExtension["module"].exports; await new Promise((resolve) => { e.onload = resolve; e.src = url as string; document.head.appendChild(e); }); return globalExtension["module"].exports; } async function prefetchURL(url: string | URL) { await (await fetch(url)).text(); } const tabby = globalExtension.Tabby; const pluginInfos: { pluginName: string; packageName: string; url: URL; }[] = [ { pluginName: "core", packageName: "tabby-core", url: new URL("tabby-core/dist/index.js", import.meta.url), }, { pluginName: "settings", packageName: "tabby-settings", url: new URL("tabby-settings/dist/index.js", import.meta.url), }, { pluginName: "terminal", packageName: "tabby-terminal", url: new URL("tabby-terminal/dist/index.js", import.meta.url), }, { pluginName: "community-color-schemes", packageName: "tabby-community-color-schemes", url: new URL( "tabby-community-color-schemes/dist/index.js", import.meta.url ), }, { pluginName: "web", packageName: "tabby-web", url: new URL("tabby-web/dist/index.js", import.meta.url), }, ]; await Promise.all( pluginInfos.map((pluginInfo) => prefetchURL(pluginInfo.url)) ); const pluginModules = []; for (const info of pluginInfos) { const result = await webRequire(info.url); result.pluginName = info.pluginName; tabby.registerPluginModule(info.packageName, result); pluginModules.push(result); } const TabbyTango = await webRequire( new URL("@yume-chan/tabby-tango/dist/index.js", import.meta.url) ); TabbyTango.pluginName = "tango"; window.addEventListener("message", (e) => { if ("type" in e.data && e.data.type === "adb") { const { port, version, maxPayloadSize, banner } = e.data as AdbProxyServerInfo; const backend = new AdbProxyBackend(port); const connection = backend.connect(); TabbyTango.AdbState.value = new Adb( connection, version, maxPayloadSize, banner ); } }); window.parent.postMessage("adb", "*"); pluginModules.push(TabbyTango); globalExtension["pluginModules"] = pluginModules.map((plugin) => { if ("default" in plugin) { plugin.default.pluginName = plugin.pluginName; return plugin.default; } return plugin; }); const config = connector.loadConfig(); await tabby.bootstrap({ packageModules: pluginModules, bootstrapData: { config, executable: "web", isFirstWindow: true, windowID: 1, installedPlugins: [], userPluginsPath: "/", }, debugMode: false, connector, }); } function TabbyFrame() { useEffect(() => { // Only run at client side. start().catch((e) => { try { require("@yume-chan/tabby-launcher"); } catch (e) { console.error(e); }); } }, []); return ( Loading Loading
apps/demo/next.config.js +20 −6 Original line number Diff line number Diff line Loading @@ -7,15 +7,26 @@ const withMDX = require("@next/mdx")({ }, }); const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", }); const basePath = process.env.BASE_PATH ?? ""; const withPwa = require("@yume-chan/next-pwa")({ dest: "public", }); function pipe(value, ...callbacks) { for (const callback of callbacks) { value = callback(value); } return value; } /** @type {import('next').NextConfig} */ module.exports = withPwa( withMDX({ module.exports = pipe( { basePath, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], reactStrictMode: false, Loading @@ -27,7 +38,8 @@ module.exports = withPwa( publicRuntimeConfig: { basePath, }, webpack(config, options) { webpack(config) { // Bundle Scrcpy Server config.module.rules.push({ resourceQuery: /url/, type: "asset/resource", Loading @@ -49,10 +61,9 @@ module.exports = withPwa( enforce: "pre", }); // config.experiments.topLevelAwait = true; return config; }, // Enable Direct Sockets API async headers() { return [ { Loading @@ -70,5 +81,8 @@ module.exports = withPwa( }, ]; }, }) }, withBundleAnalyzer, withPwa, withMDX );
apps/demo/package.json +14 −22 Original line number Diff line number Diff line Loading @@ -10,8 +10,7 @@ "lint": "next lint" }, "dependencies": { "@angular/compiler": "^15.2.6", "@fluentui/react": "^8.107.5", "@fluentui/react": "^8.108.1", "@fluentui/react-file-type-icons": "^8.8.13", "@fluentui/react-hooks": "^8.6.20", "@fluentui/react-icons": "^2.0.200", Loading @@ -19,15 +18,15 @@ "@griffel/react": "^1.5.7", "@yume-chan/adb": "workspace:^0.0.19", "@yume-chan/adb-backend-direct-sockets": "workspace:^0.0.9", "@yume-chan/adb-backend-proxy": "workspace:^0.0.9", "@yume-chan/adb-backend-proxy": "workspace:^0.0.19", "@yume-chan/adb-backend-webusb": "workspace:^0.0.19", "@yume-chan/adb-backend-ws": "workspace:^0.0.9", "@yume-chan/adb-credential-web": "workspace:^0.0.19", "@yume-chan/adb-scrcpy": "workspace:^0.0.19", "@yume-chan/android-bin": "workspace:^0.0.19", "@yume-chan/aoa": "workspace:^0.0.18", "@yume-chan/aoa": "workspace:^0.0.19", "@yume-chan/async": "^2.2.0", "@yume-chan/b-tree": "workspace:^0.0.16", "@yume-chan/b-tree": "workspace:^0.0.19", "@yume-chan/event": "workspace:^0.0.19", "@yume-chan/pcm-player": "workspace:^0.0.19", "@yume-chan/scrcpy": "workspace:^0.0.19", Loading @@ -36,35 +35,28 @@ "@yume-chan/stream-extra": "workspace:^0.0.19", "@yume-chan/stream-saver": "^2.0.6", "@yume-chan/struct": "workspace:^0.0.19", "@yume-chan/tabby-tango": "workspace:^0.0.19", "@yume-chan/undici-browser": "5.21.2-mod.9", "@yume-chan/tabby-launcher": "workspace:^1.0.197-nightly.1", "fflate": "^0.7.4", "yaml": "^2.2.1", "mobx": "^6.7.0", "mobx-react-lite": "^3.4.3", "next": "13.3.0", "tabby-core": "^1.0.197-nightly.0", "tabby-settings": "^1.0.197-nightly.0", "tabby-terminal": "^1.0.197-nightly.0", "tabby-community-color-schemes": "^1.0.197-nightly.0", "tabby-web": "^1.0.197-nightly.0", "tabby-web-container": "^1.0.197-nightly.0", "next": "13.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", "rxjs": "^7.8.0", "webm-muxer": "^2.2.3" "webm-muxer": "^3.0.3" }, "devDependencies": { "@mdx-js/loader": "^2.2.1", "@mdx-js/react": "^2.2.1", "@next/mdx": "^13.2.4", "@next/bundle-analyzer": "^13.3.1", "@next/mdx": "^13.3.1", "@types/dom-webcodecs": "^0.1.6", "@types/node": "^18.15.3", "@types/react": "18.0.35", "@types/node": "^18.16.0", "@types/react": "18.0.38", "@yume-chan/next-pwa": "5.6.0-mod.2", "eslint": "^8.36.0", "eslint-config-next": "13.3.0", "prettier": "^2.8.4", "eslint": "^8.39.0", "eslint-config-next": "13.3.1", "prettier": "^2.8.8", "source-map-loader": "^4.0.1", "typescript": "^5.0.3" } Loading
apps/demo/scripts/manifest.mjs +0 −24 Original line number Diff line number Diff line Loading @@ -27,27 +27,3 @@ fs.writeFileSync( ), "utf8" ); fs.writeFileSync( new URL( "../node_modules/tabby-web-container/dist/preload.mjs", import.meta.url ), "export {};\n" + fs .readFileSync( new URL( "../node_modules/tabby-web-container/dist/preload.js", import.meta.url ), "utf8" ) .replaceAll(/__webpack_require__\.p \+ "(.+)"/g, (_, match) => { return `new URL("./${match}", import.meta.url).toString()`; }) .replaceAll(/__webpack_require__/g, "__webpack_require_nested__") .replace( "var scriptUrl;", "var scriptUrl = import.meta.url.toString();" ) );
apps/demo/src/components/scrcpy/recorder.ts +6 −5 Original line number Diff line number Diff line Loading @@ -16,7 +16,7 @@ import { h265SearchConfiguration, } from "@yume-chan/scrcpy"; import { action, makeAutoObservable, reaction } from "mobx"; import WebMMuxer from "webm-muxer"; import { ArrayBufferTarget, Muxer as WebMMuxer } from "webm-muxer"; import { saveFile } from "../../utils"; // https://ffmpeg.org/doxygen/0.11/avc_8c-source.html#l00106 Loading Loading @@ -234,7 +234,7 @@ export class MatroskaMuxingRecorder { public videoMetadata: ScrcpyVideoStreamMetadata | undefined; public audioCodec: ScrcpyAudioCodec | undefined; private muxer: WebMMuxer | undefined; private muxer: WebMMuxer<ArrayBufferTarget> | undefined; private videoCodecDescription: Uint8Array | undefined; private configurationWritten = false; private _firstTimestamp = -1; Loading Loading @@ -351,7 +351,7 @@ export class MatroskaMuxingRecorder { this.running = true; const options: ConstructorParameters<typeof WebMMuxer>[0] = { target: "buffer", target: new ArrayBufferTarget(), type: "matroska", firstTimestampBehavior: "permissive", video: { Loading @@ -371,7 +371,7 @@ export class MatroskaMuxingRecorder { }; } this.muxer = new WebMMuxer(options); this.muxer = new WebMMuxer(options as any); if (this._packetsFromLastKeyframe.length > 0) { for (const { type, packet } of this._packetsFromLastKeyframe) { Loading @@ -389,7 +389,8 @@ export class MatroskaMuxingRecorder { return; } const buffer = this.muxer.finalize()!; this.muxer.finalize()!; const buffer = this.muxer.target.buffer; const now = new Date(); const stream = saveFile( // prettier-ignore Loading
apps/demo/src/pages/tabby-frame.tsx +4 −214 Original line number Diff line number Diff line import { Adb } from "@yume-chan/adb"; import { AdbProxyBackend, AdbProxyServerInfo, } from "@yume-chan/adb-backend-proxy"; import { useEffect } from "react"; import { Subject } from "rxjs"; import Yaml from "yaml"; export class NotImplementedSocket { connect$ = new Subject<void>(); data$ = new Subject<Buffer>(); error$ = new Subject<Error>(); close$ = new Subject<Buffer>(); async connect() { this.error$.next(new Error("Socket is not implemented in Web")); } } // Usage of connector is scattered around the Tabby codebase, // but these is all methods that are required. class WebConnector { constructor() {} async loadConfig(): Promise<string> { let config; const text = localStorage.getItem("tabby-config"); if (text) { config = Yaml.parse(text); } else { config = { recoverTabs: false, web: { preventAccidentalTabClosure: false, }, terminal: { fontSize: 11, }, }; } config.providerBlacklist = [ ...(config.providerBlacklist ?? []), "settings:ProfilesSettingsTabProvider", ]; config.commandBlacklist = [ ...(config.commandBlacklist ?? []), "core:profile-selector", ]; return Yaml.stringify(config); } async saveConfig(content: string): Promise<void> { localStorage.setItem("tabby-config", content); } getAppVersion(): string { return "1.0.197-nightly.0"; } createSocket() { return new NotImplementedSocket(); } } interface TabbyWeb { registerPluginModule(packageName: string, exports: unknown): void; bootstrap(options: unknown): Promise<void>; } interface TabbyPluginModule { pluginName: string; } interface GlobalExtension { __connector__: WebConnector | undefined; module: any; exports: any; Tabby: TabbyWeb; pluginModules: TabbyPluginModule[]; } const globalExtension = globalThis as unknown as GlobalExtension; async function start() { const connector = new WebConnector(); globalExtension["__connector__"] = connector; await import("@angular/compiler"); // @ts-expect-error await import("tabby-web-container/dist/preload.mjs"); // @ts-expect-error await import("tabby-web-container/dist/bundle.js"); async function webRequire(url: string | URL) { if (typeof url === "object") { url = url.toString(); } console.log(`Loading ${url}`); const e = document.createElement("script"); globalExtension["module"] = { exports: {} }; globalExtension["exports"] = globalExtension["module"].exports; await new Promise((resolve) => { e.onload = resolve; e.src = url as string; document.head.appendChild(e); }); return globalExtension["module"].exports; } async function prefetchURL(url: string | URL) { await (await fetch(url)).text(); } const tabby = globalExtension.Tabby; const pluginInfos: { pluginName: string; packageName: string; url: URL; }[] = [ { pluginName: "core", packageName: "tabby-core", url: new URL("tabby-core/dist/index.js", import.meta.url), }, { pluginName: "settings", packageName: "tabby-settings", url: new URL("tabby-settings/dist/index.js", import.meta.url), }, { pluginName: "terminal", packageName: "tabby-terminal", url: new URL("tabby-terminal/dist/index.js", import.meta.url), }, { pluginName: "community-color-schemes", packageName: "tabby-community-color-schemes", url: new URL( "tabby-community-color-schemes/dist/index.js", import.meta.url ), }, { pluginName: "web", packageName: "tabby-web", url: new URL("tabby-web/dist/index.js", import.meta.url), }, ]; await Promise.all( pluginInfos.map((pluginInfo) => prefetchURL(pluginInfo.url)) ); const pluginModules = []; for (const info of pluginInfos) { const result = await webRequire(info.url); result.pluginName = info.pluginName; tabby.registerPluginModule(info.packageName, result); pluginModules.push(result); } const TabbyTango = await webRequire( new URL("@yume-chan/tabby-tango/dist/index.js", import.meta.url) ); TabbyTango.pluginName = "tango"; window.addEventListener("message", (e) => { if ("type" in e.data && e.data.type === "adb") { const { port, version, maxPayloadSize, banner } = e.data as AdbProxyServerInfo; const backend = new AdbProxyBackend(port); const connection = backend.connect(); TabbyTango.AdbState.value = new Adb( connection, version, maxPayloadSize, banner ); } }); window.parent.postMessage("adb", "*"); pluginModules.push(TabbyTango); globalExtension["pluginModules"] = pluginModules.map((plugin) => { if ("default" in plugin) { plugin.default.pluginName = plugin.pluginName; return plugin.default; } return plugin; }); const config = connector.loadConfig(); await tabby.bootstrap({ packageModules: pluginModules, bootstrapData: { config, executable: "web", isFirstWindow: true, windowID: 1, installedPlugins: [], userPluginsPath: "/", }, debugMode: false, connector, }); } function TabbyFrame() { useEffect(() => { // Only run at client side. start().catch((e) => { try { require("@yume-chan/tabby-launcher"); } catch (e) { console.error(e); }); } }, []); return ( Loading