@sanohiro/casty 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/browser.js CHANGED
@@ -12,9 +12,11 @@ const DEFAULT_CHROME_VERSION = '146.0.7680.80';
12
12
  // Platform-specific identity (must match real OS to avoid TLS/TCP fingerprint mismatch)
13
13
  const PLATFORM = process.platform === 'darwin'
14
14
  ? { ua: 'Macintosh; Intel Mac OS X 10_15_7', nav: 'MacIntel',
15
+ uaPlatform: 'macOS', uaPlatformVersion: '15.0.0',
15
16
  glVendor: 'Google Inc. (Apple)', glRenderer: 'ANGLE (Apple, Apple M1, OpenGL 4.1)' }
16
17
  : { ua: `X11; Linux ${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}`,
17
18
  nav: `Linux ${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}`,
19
+ uaPlatform: 'Linux', uaPlatformVersion: '',
18
20
  glVendor: 'Google Inc. (Intel)', glRenderer: 'ANGLE (Intel, Mesa Intel UHD Graphics, OpenGL 4.5)' };
19
21
 
20
22
  const buildUserAgent = (v) =>
@@ -31,7 +33,7 @@ const CAPTURE_STUCK_RESET = 5000; // Reset stuck capturing flag (ms)
31
33
 
32
34
  // Build stealth script with locale-dependent language settings
33
35
  // lang: primary language (e.g. "ja", "en-US")
34
- function buildStealthScript(lang) {
36
+ function buildStealthScript(lang, { fakeMedia = false } = {}) {
35
37
  // Build Accept-Language style list: primary, then en-US/en fallbacks
36
38
  const languages = [lang];
37
39
  if (lang !== 'en-US' && lang !== 'en') {
@@ -91,6 +93,9 @@ navigator.permissions.query = (params) => {
91
93
  if (params.name === 'notifications') {
92
94
  return Promise.resolve({ state: Notification.permission });
93
95
  }
96
+ ${fakeMedia ? ` if (params.name === 'camera' || params.name === 'microphone') {
97
+ return Promise.resolve({ state: 'granted' });
98
+ }` : ''}
94
99
  return origQuery(params);
95
100
  };
96
101
 
@@ -109,6 +114,43 @@ if (!navigator.connection) {
109
114
  get: () => ({ effectiveType: '4g', rtt: 50, downlink: 10, saveData: false }),
110
115
  });
111
116
  }
117
+
118
+ ${fakeMedia ? `
119
+ // getUserMedia emulation (headless-shell lacks media capture)
120
+ if (navigator.mediaDevices) {
121
+ const fakeStream = (constraints) => {
122
+ const tracks = [];
123
+ if (constraints.video) {
124
+ const w = constraints.video.width?.ideal || constraints.video.width?.max || 640;
125
+ const h = constraints.video.height?.ideal || constraints.video.height?.max || 480;
126
+ const canvas = document.createElement('canvas');
127
+ canvas.width = w;
128
+ canvas.height = h;
129
+ const ctx = canvas.getContext('2d');
130
+ const draw = () => { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, w, h); requestAnimationFrame(draw); };
131
+ draw();
132
+ const stream = canvas.captureStream(30);
133
+ tracks.push(...stream.getVideoTracks());
134
+ }
135
+ if (constraints.audio) {
136
+ const actx = new AudioContext();
137
+ const gain = actx.createGain();
138
+ gain.gain.value = 0;
139
+ gain.connect(actx.destination);
140
+ const dest = actx.createMediaStreamDestination();
141
+ gain.connect(dest);
142
+ tracks.push(...dest.stream.getAudioTracks());
143
+ }
144
+ return Promise.resolve(new MediaStream(tracks));
145
+ };
146
+ navigator.mediaDevices.getUserMedia = fakeStream;
147
+ navigator.mediaDevices.enumerateDevices = () => Promise.resolve([
148
+ { deviceId: 'default', kind: 'audioinput', label: 'Default Microphone', groupId: 'default' },
149
+ { deviceId: 'default', kind: 'videoinput', label: 'Default Camera', groupId: 'default' },
150
+ { deviceId: 'default', kind: 'audiooutput', label: 'Default Speaker', groupId: 'default' },
151
+ ]);
152
+ }
153
+ ` : ''}
112
154
  `;
113
155
  }
114
156
 
@@ -143,7 +185,7 @@ export async function setupPage({ port, wsUrl }, { width, height, zoom = 1 } = {
143
185
 
144
186
  // Detect Chrome version from CDP endpoint
145
187
  const versionInfo = await fetchJson(`${baseUrl}/json/version`);
146
- const chromeVersion = versionInfo?.Browser?.replace(/^Chrome\//, '') || DEFAULT_CHROME_VERSION;
188
+ const chromeVersion = versionInfo?.Browser?.replace(/^.*\//, '') || DEFAULT_CHROME_VERSION;
147
189
  const userAgent = buildUserAgent(chromeVersion);
148
190
 
149
191
  // Try /json/new (single HTTP request, fastest)
@@ -178,8 +220,27 @@ export async function setupPage({ port, wsUrl }, { width, height, zoom = 1 } = {
178
220
  client.send('Network.setUserAgentOverride', {
179
221
  userAgent: userAgent, platform: PLATFORM.nav,
180
222
  acceptLanguage: buildAcceptLanguage(lang),
223
+ userAgentMetadata: {
224
+ brands: [
225
+ { brand: 'Chromium', version: chromeVersion.split('.')[0] },
226
+ { brand: 'Google Chrome', version: chromeVersion.split('.')[0] },
227
+ { brand: 'Not-A.Brand', version: '99' },
228
+ ],
229
+ fullVersionList: [
230
+ { brand: 'Chromium', version: chromeVersion },
231
+ { brand: 'Google Chrome', version: chromeVersion },
232
+ { brand: 'Not-A.Brand', version: '99.0.0.0' },
233
+ ],
234
+ platform: PLATFORM.uaPlatform,
235
+ platformVersion: PLATFORM.uaPlatformVersion,
236
+ architecture: process.arch === 'arm64' ? 'arm' : 'x86',
237
+ bitness: '64',
238
+ model: '',
239
+ mobile: false,
240
+ wow64: false,
241
+ },
181
242
  }),
182
- client.send('Page.addScriptToEvaluateOnNewDocument', { source: buildStealthScript(lang) }),
243
+ client.send('Page.addScriptToEvaluateOnNewDocument', { source: buildStealthScript(lang, { fakeMedia: config.fakeMedia }) }),
183
244
  client.send('Browser.setDownloadBehavior', {
184
245
  behavior: 'allowAndName', downloadPath: join(homedir(), 'Downloads'),
185
246
  eventsEnabled: true,
package/lib/config.js CHANGED
@@ -32,6 +32,7 @@ const DEFAULTS = {
32
32
  searchUrl: 'https://www.google.com/search?q=',
33
33
  transport: 'auto', // 'auto' | 'file' | 'inline'
34
34
  format: 'auto', // 'auto' | 'png' | 'jpeg'
35
+ fakeMedia: false, // Emulate camera/mic for WebRTC (headless-shell lacks getUserMedia)
35
36
  language: detectLocale(), // System locale (override: "en-US", "ja", etc.)
36
37
  };
37
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanohiro/casty",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "TTY web browser using raw CDP and Kitty graphics protocol",
5
5
  "main": "bin/casty.js",
6
6
  "bin": {