capiumbrowser 1.2.0__tar.gz → 1.2.1__tar.gz

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.
Files changed (45) hide show
  1. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/MANIFEST.in +3 -0
  2. {capiumbrowser-1.2.0/capiumbrowser.egg-info → capiumbrowser-1.2.1}/PKG-INFO +44 -6
  3. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/README.md +43 -5
  4. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/_version.py +1 -1
  5. capiumbrowser-1.2.1/capiumbrowser/android-phones.json +99 -0
  6. capiumbrowser-1.2.1/capiumbrowser/browser-brands.json +47 -0
  7. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/browser.py +81 -12
  8. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/browser_async.py +71 -7
  9. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/channels.json +7 -7
  10. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/config.py +27 -4
  11. capiumbrowser-1.2.1/capiumbrowser/desktop.py +81 -0
  12. capiumbrowser-1.2.1/capiumbrowser/ios-phones.json +19 -0
  13. capiumbrowser-1.2.1/capiumbrowser/mobile.py +113 -0
  14. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1/capiumbrowser.egg-info}/PKG-INFO +44 -6
  15. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser.egg-info/SOURCES.txt +8 -0
  16. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/pyproject.toml +2 -2
  17. capiumbrowser-1.2.1/tests/test_desktop_brands.py +62 -0
  18. capiumbrowser-1.2.1/tests/test_ios_profiles.py +48 -0
  19. capiumbrowser-1.2.1/tests/test_mobile_profiles.py +157 -0
  20. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/LICENSE +0 -0
  21. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/__init__.py +0 -0
  22. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/__main__.py +0 -0
  23. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/_driver.py +0 -0
  24. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/errors.py +0 -0
  25. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/human/__init__.py +0 -0
  26. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/human/behavior.py +0 -0
  27. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/licensing/__init__.py +0 -0
  28. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/licensing/client.py +0 -0
  29. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/licensing/download.py +0 -0
  30. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/licensing/readiness.py +0 -0
  31. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/network/__init__.py +0 -0
  32. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser/network/proxy.py +0 -0
  33. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser.egg-info/dependency_links.txt +0 -0
  34. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser.egg-info/entry_points.txt +0 -0
  35. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser.egg-info/requires.txt +0 -0
  36. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/capiumbrowser.egg-info/top_level.txt +0 -0
  37. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/setup.cfg +0 -0
  38. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_channels_manifest.py +0 -0
  39. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_config_args.py +0 -0
  40. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_download_contract.py +0 -0
  41. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_geoip_flags.py +0 -0
  42. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_launch_viewport.py +0 -0
  43. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_license_readiness.py +0 -0
  44. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_proxy.py +0 -0
  45. {capiumbrowser-1.2.0 → capiumbrowser-1.2.1}/tests/test_version_tables_consistent.py +0 -0
@@ -1,5 +1,8 @@
1
1
  include README.md
2
2
  include LICENSE
3
3
  include capiumbrowser/channels.json
4
+ include capiumbrowser/android-phones.json
5
+ include capiumbrowser/ios-phones.json
6
+ include capiumbrowser/browser-brands.json
4
7
  global-exclude __pycache__/*
5
8
  global-exclude *.py[cod]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capiumbrowser
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Drop-in stealth browser SDK on top of Playwright (Capium Chromium).
5
5
  Author: Capzy
6
6
  License: Capzy capiumbrowser SDK License
@@ -128,6 +128,7 @@ browser.close() # also stops the Playwright driver — no leake
128
128
  - [Install](#install) · [Why Capium](#why-capium) · [Licensing & tiers](#licensing)
129
129
  - [What a persona reads](#what-a-persona-reads) · [Comparison](#comparison) · [How it works](#how-it-works)
130
130
  - [API](#api) · [Parameters](#parameters) · [Personas & seeds](#personas--seeds) · [Fingerprint switches](#fingerprint-switches)
131
+ - [Browser and mobile profiles](#browser-and-mobile-profiles)
131
132
  - [Human behavior](#human-behavior) · [Proxies & geo](#proxies--geo) · [Fonts](#fonts)
132
133
  - [Framework integrations](#framework-integrations) · [Deployment](#deployment) · [Platforms](#platforms)
133
134
  - [CLI](#cli--environment) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Security](#security) · [License](#license)
@@ -395,7 +396,9 @@ and `launch_persistent_context` forward everything to `launch`.
395
396
  | Parameter | Type | Default | Description |
396
397
  |---|---|---|---|
397
398
  | `seed` | `int` \| `None` \| `"off"` | `None` | Persona seed → a complete coherent device. `None` = fresh random seed each launch. The range selects the OS family (see [Personas & seeds](#personas--seeds)). `"off"`/`"false"`/`"none"` = **passthrough** (host's real values; debugging). |
398
- | `platform` | `str` | `"windows"` | Spoofed OS: `"windows"` \| `"macos"` \| `"linux"`. Sets `--fingerprint-platform` and picks the seed range if you didn't. |
399
+ | `platform` | `str` | `"windows"` | OS profile: `"windows"` \| `"macos"` \| `"linux"` \| `"android"` \| `"ios"`. Mobile profiles are opt-in. |
400
+ | `browser_brand` | `str` | `"chrome"` | Desktop profile: `chrome`, `edge`, `opera`, `vivaldi`. |
401
+ | `mobile_device` | `str` | seed/default | Phone catalog ID; requires `android` or `ios`. |
399
402
  | `headless` | `bool` | `False` | Headed is recommended (reads cleaner). On a server, run headed under Xvfb. |
400
403
  | `stealth_args` | `bool` | `True` | Apply the default coherent fingerprint flags. Set `False` to build the whole command line yourself via `args`. |
401
404
  | `args` | `list[str]` \| `None` | `None` | Extra raw Chrome flags appended verbatim (see [Fingerprint switches](#fingerprint-switches)). |
@@ -496,6 +499,42 @@ Full runnable version (personas, persistent profiles, a fleet, proxy+geo, full c
496
499
 
497
500
  ---
498
501
 
502
+ ## Browser and mobile profiles
503
+
504
+ CapiumBrowser 1.2.1 supports desktop Chrome, Edge, Opera and Vivaldi identity
505
+ profiles, plus opt-in Android and iPhone profiles on Windows x64, macOS arm64
506
+ and Linux x64. Chrome remains the default.
507
+
508
+ | Profile | Python | Node.js |
509
+ | --- | --- | --- |
510
+ | Desktop browser | `browser_brand="edge"` (also `chrome`, `opera`, `vivaldi`) | `browserBrand: 'edge'` |
511
+ | Android phone | `platform="android", mobile_device="pixel-9"` | `platform: 'android', mobileDevice: 'pixel-9'` |
512
+ | iPhone 16 | `platform="ios", mobile_device="iphone-16"` | `platform: 'ios', mobileDevice: 'iphone-16'` |
513
+
514
+ Android includes Pixel 7/8/9 families and Samsung Galaxy A55. Omit the Android
515
+ device to retain the original seed selection; iOS defaults to iPhone 16. Mobile
516
+ profiles set the mobile layout and identity while leaving desktop defaults intact.
517
+ These are experimental desktop emulation profiles: they do not install Android
518
+ or iOS, reproduce WebKit, or certify physical-phone fingerprints. Android Chrome
519
+ and Chrome iOS are the supported mobile browser profiles; arbitrary app/WebView
520
+ UAs are outside these presets.
521
+
522
+ Desktop profiles use verified browser/Chromium version pairs: Edge 155/155,
523
+ Opera 137/153 and Vivaldi 8.2/152. Vivaldi follows its usual Chrome branding.
524
+ Capium's engine remains Chromium 155; vendor-specific features are not emulated.
525
+ A version catalog is not an older browser engine. Native compatibility checks
526
+ reject binaries that do not implement the selected profile.
527
+
528
+ ```python
529
+ from capiumbrowser import launch_context
530
+
531
+ browser, context, page = launch_context(
532
+ seed=200123, platform="ios", mobile_device="iphone-16")
533
+ # Android: platform="android", mobile_device="pixel-9"
534
+ # Desktop Edge: platform="windows", browser_brand="edge"
535
+ browser.close()
536
+ ```
537
+
499
538
  ## Fingerprint switches
500
539
 
501
540
  The SDK sets the essentials from `seed`/`platform`. Everything below is available for precise control —
@@ -725,13 +764,12 @@ launches **headed**, which reads cleaner than headless.
725
764
  | Linux arm64 | `linux-arm64` | Declared in the manifest; not yet published or covered by the three-host audit |
726
765
 
727
766
  The SDK auto-detects your host and downloads the matching tag; macOS Intel and Windows ARM are **not**
728
- published and raise a clear error. The **1.2.0** manifest selects **Chromium 155.0.8059.39, revision 1** on the three
729
- supported hosts. This release is prepared as a draft; downloads become available when the browser
730
- artifacts are published. Linux arm64 remains unpublished.
767
+ published and raise a clear error. The **1.2.1** manifest selects **Chromium 155.0.8059.39, revision 2** on the three
768
+ supported hosts. Browser downloads are available with a valid license. Linux arm64 remains unpublished.
731
769
 
732
770
  ---
733
771
 
734
- The 1.2.0 launch APIs wait for the packaged browser's licensing decision before
772
+ The launch APIs wait for the packaged browser's licensing decision before
735
773
  returning a handle. Invalid keys raise a typed error during launch.
736
774
 
737
775
  ## CLI & environment
@@ -65,6 +65,7 @@ browser.close() # also stops the Playwright driver — no leake
65
65
  - [Install](#install) · [Why Capium](#why-capium) · [Licensing & tiers](#licensing)
66
66
  - [What a persona reads](#what-a-persona-reads) · [Comparison](#comparison) · [How it works](#how-it-works)
67
67
  - [API](#api) · [Parameters](#parameters) · [Personas & seeds](#personas--seeds) · [Fingerprint switches](#fingerprint-switches)
68
+ - [Browser and mobile profiles](#browser-and-mobile-profiles)
68
69
  - [Human behavior](#human-behavior) · [Proxies & geo](#proxies--geo) · [Fonts](#fonts)
69
70
  - [Framework integrations](#framework-integrations) · [Deployment](#deployment) · [Platforms](#platforms)
70
71
  - [CLI](#cli--environment) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Security](#security) · [License](#license)
@@ -332,7 +333,9 @@ and `launch_persistent_context` forward everything to `launch`.
332
333
  | Parameter | Type | Default | Description |
333
334
  |---|---|---|---|
334
335
  | `seed` | `int` \| `None` \| `"off"` | `None` | Persona seed → a complete coherent device. `None` = fresh random seed each launch. The range selects the OS family (see [Personas & seeds](#personas--seeds)). `"off"`/`"false"`/`"none"` = **passthrough** (host's real values; debugging). |
335
- | `platform` | `str` | `"windows"` | Spoofed OS: `"windows"` \| `"macos"` \| `"linux"`. Sets `--fingerprint-platform` and picks the seed range if you didn't. |
336
+ | `platform` | `str` | `"windows"` | OS profile: `"windows"` \| `"macos"` \| `"linux"` \| `"android"` \| `"ios"`. Mobile profiles are opt-in. |
337
+ | `browser_brand` | `str` | `"chrome"` | Desktop profile: `chrome`, `edge`, `opera`, `vivaldi`. |
338
+ | `mobile_device` | `str` | seed/default | Phone catalog ID; requires `android` or `ios`. |
336
339
  | `headless` | `bool` | `False` | Headed is recommended (reads cleaner). On a server, run headed under Xvfb. |
337
340
  | `stealth_args` | `bool` | `True` | Apply the default coherent fingerprint flags. Set `False` to build the whole command line yourself via `args`. |
338
341
  | `args` | `list[str]` \| `None` | `None` | Extra raw Chrome flags appended verbatim (see [Fingerprint switches](#fingerprint-switches)). |
@@ -433,6 +436,42 @@ Full runnable version (personas, persistent profiles, a fleet, proxy+geo, full c
433
436
 
434
437
  ---
435
438
 
439
+ ## Browser and mobile profiles
440
+
441
+ CapiumBrowser 1.2.1 supports desktop Chrome, Edge, Opera and Vivaldi identity
442
+ profiles, plus opt-in Android and iPhone profiles on Windows x64, macOS arm64
443
+ and Linux x64. Chrome remains the default.
444
+
445
+ | Profile | Python | Node.js |
446
+ | --- | --- | --- |
447
+ | Desktop browser | `browser_brand="edge"` (also `chrome`, `opera`, `vivaldi`) | `browserBrand: 'edge'` |
448
+ | Android phone | `platform="android", mobile_device="pixel-9"` | `platform: 'android', mobileDevice: 'pixel-9'` |
449
+ | iPhone 16 | `platform="ios", mobile_device="iphone-16"` | `platform: 'ios', mobileDevice: 'iphone-16'` |
450
+
451
+ Android includes Pixel 7/8/9 families and Samsung Galaxy A55. Omit the Android
452
+ device to retain the original seed selection; iOS defaults to iPhone 16. Mobile
453
+ profiles set the mobile layout and identity while leaving desktop defaults intact.
454
+ These are experimental desktop emulation profiles: they do not install Android
455
+ or iOS, reproduce WebKit, or certify physical-phone fingerprints. Android Chrome
456
+ and Chrome iOS are the supported mobile browser profiles; arbitrary app/WebView
457
+ UAs are outside these presets.
458
+
459
+ Desktop profiles use verified browser/Chromium version pairs: Edge 155/155,
460
+ Opera 137/153 and Vivaldi 8.2/152. Vivaldi follows its usual Chrome branding.
461
+ Capium's engine remains Chromium 155; vendor-specific features are not emulated.
462
+ A version catalog is not an older browser engine. Native compatibility checks
463
+ reject binaries that do not implement the selected profile.
464
+
465
+ ```python
466
+ from capiumbrowser import launch_context
467
+
468
+ browser, context, page = launch_context(
469
+ seed=200123, platform="ios", mobile_device="iphone-16")
470
+ # Android: platform="android", mobile_device="pixel-9"
471
+ # Desktop Edge: platform="windows", browser_brand="edge"
472
+ browser.close()
473
+ ```
474
+
436
475
  ## Fingerprint switches
437
476
 
438
477
  The SDK sets the essentials from `seed`/`platform`. Everything below is available for precise control —
@@ -662,13 +701,12 @@ launches **headed**, which reads cleaner than headless.
662
701
  | Linux arm64 | `linux-arm64` | Declared in the manifest; not yet published or covered by the three-host audit |
663
702
 
664
703
  The SDK auto-detects your host and downloads the matching tag; macOS Intel and Windows ARM are **not**
665
- published and raise a clear error. The **1.2.0** manifest selects **Chromium 155.0.8059.39, revision 1** on the three
666
- supported hosts. This release is prepared as a draft; downloads become available when the browser
667
- artifacts are published. Linux arm64 remains unpublished.
704
+ published and raise a clear error. The **1.2.1** manifest selects **Chromium 155.0.8059.39, revision 2** on the three
705
+ supported hosts. Browser downloads are available with a valid license. Linux arm64 remains unpublished.
668
706
 
669
707
  ---
670
708
 
671
- The 1.2.0 launch APIs wait for the packaged browser's licensing decision before
709
+ The launch APIs wait for the packaged browser's licensing decision before
672
710
  returning a handle. Invalid keys raise a typed error during launch.
673
711
 
674
712
  ## CLI & environment
@@ -17,7 +17,7 @@ import json
17
17
  import os
18
18
  import platform as _platform
19
19
 
20
- __version__ = "1.2.0"
20
+ __version__ = "1.2.1"
21
21
 
22
22
  # Used only if channels.json can't be read (corrupt/removed install). ensure_binary() raises a
23
23
  # clear error long before the "unsupported platform" case matters; this keeps lookups total.
@@ -0,0 +1,99 @@
1
+ {
2
+ "schema_version": 2,
3
+ "minimum_release_year": 2022,
4
+ "browser": "chrome",
5
+ "status": "experimental-emulation",
6
+ "gpu_policy": "host",
7
+ "hardware_policy": "host",
8
+ "physical_device_validation": "pending",
9
+ "default_profile_ids": [
10
+ "pixel-7",
11
+ "pixel-8",
12
+ "pixel-8-pro",
13
+ "pixel-8a",
14
+ "pixel-9"
15
+ ],
16
+ "sources": [
17
+ "https://support.google.com/pixelphone/answer/7158570?hl=en",
18
+ "https://news.samsung.com/global/samsung-galaxy-a55-5g-and-galaxy-a35-5g-awesome-innovations-and-security-engineered-for-everyone",
19
+ "https://www.samsung.com/ie/business/smartphones/galaxy-a/galaxy-a55-5g-awesome-navy-256gb-sm-a556bzkceub/",
20
+ "https://chromium.googlesource.com/devtools/devtools-frontend/+/cd7bfad9bba968ac23ba67e538f9e1d499991d25/front_end/models/emulation/EmulatedDevices.ts"
21
+ ],
22
+ "profiles": [
23
+ {
24
+ "id": "pixel-7",
25
+ "model": "Pixel 7",
26
+ "release_year": 2022,
27
+ "platform_version": "13",
28
+ "width": 412,
29
+ "height": 915,
30
+ "dpr": 2.625
31
+ },
32
+ {
33
+ "id": "pixel-8",
34
+ "model": "Pixel 8",
35
+ "release_year": 2023,
36
+ "platform_version": "14",
37
+ "width": 412,
38
+ "height": 915,
39
+ "dpr": 2.625
40
+ },
41
+ {
42
+ "id": "pixel-8-pro",
43
+ "model": "Pixel 8 Pro",
44
+ "release_year": 2023,
45
+ "platform_version": "14",
46
+ "width": 448,
47
+ "height": 997,
48
+ "dpr": 3.0
49
+ },
50
+ {
51
+ "id": "pixel-8a",
52
+ "model": "Pixel 8a",
53
+ "release_year": 2024,
54
+ "platform_version": "14",
55
+ "width": 412,
56
+ "height": 915,
57
+ "dpr": 2.625
58
+ },
59
+ {
60
+ "id": "pixel-9",
61
+ "model": "Pixel 9",
62
+ "release_year": 2024,
63
+ "platform_version": "14",
64
+ "width": 412,
65
+ "height": 924,
66
+ "dpr": 2.625
67
+ },
68
+ {
69
+ "id": "samsung-galaxy-a55",
70
+ "model": "SM-A556B",
71
+ "display_name": "Samsung Galaxy A55",
72
+ "release_year": 2024,
73
+ "platform_version": "14",
74
+ "width": 360,
75
+ "height": 800,
76
+ "dpr": 2.25,
77
+ "geometry_status": "devtools-emulation-only",
78
+ "geometry_note": "These Chromium preset dimensions/DPR do not directly map to the published 1080x2340 panel; physical Chrome capture required."
79
+ },
80
+ {
81
+ "id": "pixel-9-pro",
82
+ "model": "Pixel 9 Pro",
83
+ "release_year": 2024,
84
+ "platform_version": "14",
85
+ "width": 427,
86
+ "height": 952,
87
+ "dpr": 3.0
88
+ },
89
+ {
90
+ "id": "pixel-9-pro-xl",
91
+ "model": "Pixel 9 Pro XL",
92
+ "release_year": 2024,
93
+ "platform_version": "14",
94
+ "width": 448,
95
+ "height": 997,
96
+ "dpr": 3.0
97
+ }
98
+ ]
99
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "schema_version": 1,
3
+ "verified_on": "2026-10-10",
4
+ "engine_version": "155.0.8059.39",
5
+ "scope": "Desktop UA and Client Hints profiles; does not install vendor engines or features.",
6
+ "profiles": [
7
+ {
8
+ "brand": "edge",
9
+ "product_version": "155.0.4283.45",
10
+ "chromium_version": "155.0.8059.40",
11
+ "hint_brand": "Microsoft Edge",
12
+ "ua_suffix": " Edg/",
13
+ "ua_version": "155.0.0.0",
14
+ "sources": [
15
+ "https://edgeupdates.microsoft.com/api/products?view=enterprise"
16
+ ],
17
+ "measurement": "Stock Edge 155 Windows and Linux owned-origin captures"
18
+ },
19
+ {
20
+ "brand": "opera",
21
+ "product_version": "137.0.6036.39",
22
+ "chromium_version": "153.0.8010.55",
23
+ "hint_brand": "Opera",
24
+ "ua_suffix": " OPR/",
25
+ "ua_version": "137.0.0.0",
26
+ "sources": [
27
+ "https://blogs.opera.com/desktop/changelog-for-137/",
28
+ "https://blogs.opera.com/desktop/2026/10/opera-137-stable/"
29
+ ],
30
+ "measurement": "Stock Opera 137 Linux owned-origin capture"
31
+ },
32
+ {
33
+ "brand": "vivaldi",
34
+ "product_version": "8.2.4133.84",
35
+ "chromium_version": "152.0.7977.160",
36
+ "hint_brand": "Google Chrome",
37
+ "ua_suffix": "",
38
+ "ua_version": "",
39
+ "sources": [
40
+ "https://vivaldi.com/blog/minor-update-nine-8-2/",
41
+ "https://vivaldi.com/blog/desktop/",
42
+ "https://help.vivaldi.com/developers/web/vivaldi-user-agent-and-client-hints-user-agent/"
43
+ ],
44
+ "measurement": "Stock Vivaldi 8.2 Linux owned-origin capture; default branding only, partner-site exceptions excluded"
45
+ }
46
+ ]
47
+ }
@@ -117,13 +117,13 @@ def _has_screen_override(extra):
117
117
 
118
118
 
119
119
  def _build_args(seed, platform, stealth_args, timezone, locale, extension_paths, extra,
120
- headless=False):
120
+ headless=False, mobile_device=None, browser_brand=None):
121
121
  args = []
122
122
  if stealth_args:
123
- args += config.get_default_stealth_args(seed, platform)
123
+ args += config.get_default_stealth_args(seed, platform, mobile_device=mobile_device, browser_brand=browser_brand)
124
124
  # Keep the engine's seeded screen. Use initial window bounds only when
125
125
  # the caller has not provided screen/window flags.
126
- if headless and not _has_screen_override(extra):
126
+ if headless and platform not in ("android", "ios") and not _has_screen_override(extra):
127
127
  args += config.get_headless_default_args()
128
128
  if timezone:
129
129
  args.append("--timezone=%s" % timezone) # capium in-binary tz spoof
@@ -209,10 +209,13 @@ def _wrap_close(obj, pw):
209
209
  return obj
210
210
 
211
211
 
212
- def _apply_viewport_defaults(browser, asynchronous=False):
212
+ def _apply_viewport_defaults(browser, asynchronous=False, mobile_seed=None, mobile_device=None, mobile_platform="android"):
213
213
  """Match the other launch APIs: use the window unless the caller chooses a viewport."""
214
214
  def decorate(original):
215
215
  def options(kwargs):
216
+ if mobile_seed is not None:
217
+ from .mobile import context_options
218
+ return context_options(mobile_seed, kwargs, mobile_device, mobile_platform)
216
219
  if 'viewport' not in kwargs and 'no_viewport' not in kwargs:
217
220
  kwargs = dict(kwargs, no_viewport=True)
218
221
  return kwargs
@@ -234,13 +237,29 @@ def _apply_viewport_defaults(browser, asynchronous=False):
234
237
  return browser
235
238
 
236
239
 
240
+ def _require_mobile_engine(context, mobile_device=None, seed=0, platform="android"):
241
+ """Refuse incompatible binaries before a mobile case can visit a site."""
242
+ page = context.new_page()
243
+ try:
244
+ from .mobile import mobile_profile, engine_probe
245
+ expected = mobile_profile(seed, mobile_device, platform)
246
+ page.route("**/*", lambda route: route.fulfill(status=200, content_type="text/html", body="<!doctype html>"))
247
+ page.goto("https://capium-mobile-preview.invalid/", wait_until="domcontentloaded")
248
+ supported = page.evaluate(engine_probe(platform), expected)
249
+ if not supported:
250
+ raise ValueError("This binary does not support the requested mobile profile/device; use Capium 1.2.1 revision 2 or later")
251
+ finally:
252
+ page.close()
253
+
254
+
237
255
  def launch(seed=None, platform="windows", headless=False, proxy=None, geoip=None, args=None,
238
256
  stealth_args=True, timezone=None, locale=None, extension_paths=None, binary=None,
239
257
  license_key=None, license_server=None, license_through_proxy=False,
240
- license_preflight=True, driver=None, **kwargs):
258
+ license_preflight=True, driver=None, mobile_device=None, browser_brand=None, **kwargs):
241
259
  """Launch a Capium Browser (non-persistent; Playwright manages a temp profile).
242
260
 
243
261
  seed/platform : identity. stealth_args=False drops the default fingerprint flags.
262
+ mobile_device : Android/iPhone catalog ID; requires the matching native release.
244
263
  proxy : "dataimpulse" | "http://user:pass@host:port" | "socks5://host:port" | dict.
245
264
  geoip : geo coherence, resolved inside the binary. None (default) = OFF: no lookup
246
265
  runs, even with a proxy (a proxy alone no longer triggers it, so proxied
@@ -263,6 +282,10 @@ def launch(seed=None, platform="windows", headless=False, proxy=None, geoip=None
263
282
  kwargs : forwarded to Playwright's chromium.launch (e.g. slow_mo=...).
264
283
  Returns a Browser whose .close() also stops Playwright.
265
284
  """
285
+ from .mobile import validate_launch
286
+ from . import desktop
287
+ desktop.validate_launch(browser_brand, platform, binary, args, stealth_args)
288
+ validate_launch(platform, binary, args, stealth_args, mobile_device)
266
289
  if license_preflight:
267
290
  _license.preflight(license_key, license_server)
268
291
  key, _ = _license.effective(license_key, license_server)
@@ -272,7 +295,7 @@ def launch(seed=None, platform="windows", headless=False, proxy=None, geoip=None
272
295
  display = os.environ.get("DISPLAY")
273
296
  proxy_kwargs, proxy_args = _proxy_and_geo_args(proxy, binpath, geoip, display)
274
297
  launch_args = proxy_args + _build_args(
275
- seed, platform, stealth_args, timezone, locale, extension_paths, args, headless)
298
+ seed, platform, stealth_args, timezone, locale, extension_paths, args, headless, mobile_device, browser_brand)
276
299
  if license_through_proxy:
277
300
  launch_args.append("--license-through-proxy")
278
301
  env = _license.child_env(license_key, license_server)
@@ -304,7 +327,30 @@ def launch(seed=None, platform="windows", headless=False, proxy=None, geoip=None
304
327
  raise
305
328
  _clear_status_file(status_path)
306
329
  browser._capium_seed = seed
307
- return _wrap_close(_apply_viewport_defaults(browser), pw)
330
+ browser._capium_mobile = platform in ("android", "ios") and stealth_args
331
+ browser = _wrap_close(_apply_viewport_defaults(
332
+ browser, mobile_seed=seed if browser._capium_mobile else None, mobile_device=mobile_device, mobile_platform=platform), pw)
333
+ if browser._capium_mobile:
334
+ try:
335
+ context = browser.new_context()
336
+ try:
337
+ _require_mobile_engine(context, mobile_device, seed, platform)
338
+ finally:
339
+ context.close()
340
+ except Exception:
341
+ browser.close()
342
+ raise
343
+ if desktop.normalize_brand(browser_brand) != "chrome":
344
+ try:
345
+ context = browser.new_context()
346
+ try:
347
+ desktop.require_engine(context, browser_brand)
348
+ finally:
349
+ context.close()
350
+ except Exception:
351
+ browser.close()
352
+ raise
353
+ return browser
308
354
 
309
355
 
310
356
  def launch_persistent_context(user_data_dir, seed=None, platform="windows", headless=False,
@@ -312,11 +358,15 @@ def launch_persistent_context(user_data_dir, seed=None, platform="windows", head
312
358
  timezone=None, locale=None, extension_paths=None, binary=None,
313
359
  license_key=None, license_server=None,
314
360
  license_through_proxy=False,
315
- license_preflight=True, driver=None, **kwargs):
361
+ license_preflight=True, driver=None, mobile_device=None, browser_brand=None, **kwargs):
316
362
  """Launch a persistent context (cookies/localStorage/state persist in user_data_dir).
317
363
 
318
364
  Same options as launch(); returns a BrowserContext whose .close() also stops Playwright.
319
365
  """
366
+ from .mobile import context_options, validate_launch
367
+ from . import desktop
368
+ desktop.validate_launch(browser_brand, platform, binary, args, stealth_args)
369
+ validate_launch(platform, binary, args, stealth_args, mobile_device)
320
370
  if license_preflight:
321
371
  _license.preflight(license_key, license_server)
322
372
  key, _ = _license.effective(license_key, license_server)
@@ -326,14 +376,16 @@ def launch_persistent_context(user_data_dir, seed=None, platform="windows", head
326
376
  display = os.environ.get("DISPLAY")
327
377
  proxy_kwargs, proxy_args = _proxy_and_geo_args(proxy, binpath, geoip, display)
328
378
  launch_args = proxy_args + _build_args(
329
- seed, platform, stealth_args, timezone, locale, extension_paths, args, headless)
379
+ seed, platform, stealth_args, timezone, locale, extension_paths, args, headless, mobile_device, browser_brand)
330
380
  if license_through_proxy:
331
381
  launch_args.append("--license-through-proxy")
332
382
  env = _license.child_env(license_key, license_server)
333
383
  status_path = _new_status_file(env)
334
384
  # Default to the page filling the window (see launch_context) unless the caller
335
385
  # pinned a viewport themselves.
336
- if "viewport" not in kwargs and "no_viewport" not in kwargs:
386
+ if platform in ("android", "ios") and stealth_args:
387
+ kwargs = context_options(seed, kwargs, mobile_device, platform)
388
+ elif "viewport" not in kwargs and "no_viewport" not in kwargs:
337
389
  kwargs["no_viewport"] = True
338
390
  # Strip Playwright's automation + forced-software-WebGL defaults;
339
391
  # the caller can override by passing ignore_default_args explicitly.
@@ -363,6 +415,22 @@ def launch_persistent_context(user_data_dir, seed=None, platform="windows", head
363
415
  raise
364
416
  _clear_status_file(status_path)
365
417
  ctx._capium_seed = seed
418
+ if desktop.normalize_brand(browser_brand) != "chrome":
419
+ ctx = _wrap_close(ctx, pw)
420
+ try:
421
+ desktop.require_engine(ctx, browser_brand)
422
+ except Exception:
423
+ ctx.close()
424
+ raise
425
+ return ctx
426
+ if platform in ("android", "ios") and stealth_args:
427
+ ctx = _wrap_close(ctx, pw)
428
+ try:
429
+ _require_mobile_engine(ctx, mobile_device, seed, platform)
430
+ except Exception:
431
+ ctx.close()
432
+ raise
433
+ return ctx
366
434
  # Headed persistent launches fit the window to the selected screen. Headless
367
435
  # keeps its startup bounds (800x600 by default) independent of the seeded screen;
368
436
  # callers can choose larger window bounds explicitly.
@@ -383,12 +451,13 @@ def launch_context(seed=None, platform="windows", headless=False, url=None,
383
451
  # no_viewport: let the page fill the real window (fit_window sizes the window to
384
452
  # the persona's screen). A fixed 1280x720 emulated viewport would leave the page
385
453
  # smaller than the window AND mismatch window.screen — an incoherent fingerprint.
386
- context = browser.contexts[0] if browser.contexts else browser.new_context(no_viewport=True)
454
+ context = browser.contexts[0] if browser.contexts else browser.new_context(
455
+ **({} if getattr(browser, "_capium_mobile", False) else {"no_viewport": True}))
387
456
  if humanize:
388
457
  from . import human as _human
389
458
  _human.humanize(context, preset=human_preset)
390
459
  page = context.pages[0] if context.pages else context.new_page()
391
- if not headless:
460
+ if not headless and not getattr(browser, "_capium_mobile", False):
392
461
  fit_window(browser, context, page) # modest window <= spoofed screen
393
462
  if url:
394
463
  page.goto(url, wait_until="domcontentloaded", timeout=60000)
@@ -72,13 +72,17 @@ async def launch_async(seed=None, platform="windows", headless=False, proxy=None
72
72
  args=None, stealth_args=True, timezone=None, locale=None,
73
73
  extension_paths=None, binary=None, license_key=None,
74
74
  license_server=None, license_through_proxy=False,
75
- license_preflight=True, driver=None, **kwargs):
75
+ license_preflight=True, driver=None, mobile_device=None, browser_brand=None, **kwargs):
76
76
  """Async launch -> Browser (await browser.close() also stops Playwright).
77
77
 
78
78
  driver: "playwright" (default) or the installed "patchright" fork.
79
79
  CAPIUM_DRIVER=patchright also works. Evaluation-world defaults and API
80
80
  compatibility depend on the installed version. See capiumbrowser._driver.
81
81
  """
82
+ from .mobile import validate_launch
83
+ from . import desktop
84
+ desktop.validate_launch(browser_brand, platform, binary, args, stealth_args)
85
+ validate_launch(platform, binary, args, stealth_args, mobile_device)
82
86
  await _license_preflight_async(license_key, license_server, license_preflight)
83
87
  key, _ = _license.effective(license_key, license_server)
84
88
  binpath = _resolve_binary(binary, key)
@@ -87,7 +91,7 @@ async def launch_async(seed=None, platform="windows", headless=False, proxy=None
87
91
  display = os.environ.get("DISPLAY")
88
92
  proxy_kwargs, proxy_args = await _proxy_and_geo_args_async(proxy, binpath, geoip, display)
89
93
  launch_args = proxy_args + _build_args(
90
- seed, platform, stealth_args, timezone, locale, extension_paths, args, headless)
94
+ seed, platform, stealth_args, timezone, locale, extension_paths, args, headless, mobile_device, browser_brand)
91
95
  if license_through_proxy:
92
96
  launch_args.append("--license-through-proxy")
93
97
  env = _license.child_env(license_key, license_server)
@@ -119,7 +123,44 @@ async def launch_async(seed=None, platform="windows", headless=False, proxy=None
119
123
  raise
120
124
  _clear_status_file(status_path)
121
125
  browser._capium_seed = seed
122
- return _wrap_close_async(_apply_viewport_defaults(browser, asynchronous=True), pw)
126
+ browser._capium_mobile = platform in ("android", "ios") and stealth_args
127
+ browser = _wrap_close_async(_apply_viewport_defaults(browser, asynchronous=True,
128
+ mobile_seed=seed if browser._capium_mobile else None, mobile_device=mobile_device, mobile_platform=platform), pw)
129
+ if browser._capium_mobile:
130
+ try:
131
+ context = await browser.new_context()
132
+ try:
133
+ await _require_mobile_engine_async(context, mobile_device, seed, platform)
134
+ finally:
135
+ await context.close()
136
+ except Exception:
137
+ await browser.close()
138
+ raise
139
+ if desktop.normalize_brand(browser_brand) != "chrome":
140
+ try:
141
+ context = await browser.new_context()
142
+ try:
143
+ await desktop.require_engine_async(context, browser_brand)
144
+ finally:
145
+ await context.close()
146
+ except Exception:
147
+ await browser.close()
148
+ raise
149
+ return browser
150
+
151
+
152
+ async def _require_mobile_engine_async(context, mobile_device=None, seed=0, platform="android"):
153
+ page = await context.new_page()
154
+ try:
155
+ from .mobile import mobile_profile, engine_probe
156
+ expected = mobile_profile(seed, mobile_device, platform)
157
+ await page.route("**/*", lambda route: route.fulfill(status=200, content_type="text/html", body="<!doctype html>"))
158
+ await page.goto("https://capium-mobile-preview.invalid/", wait_until="domcontentloaded")
159
+ supported = await page.evaluate(engine_probe(platform), expected)
160
+ if not supported:
161
+ raise ValueError("This binary does not support the requested mobile profile/device; use Capium 1.2.1 revision 2 or later")
162
+ finally:
163
+ await page.close()
123
164
 
124
165
 
125
166
  async def launch_persistent_context_async(user_data_dir, seed=None, platform="windows",
@@ -127,9 +168,13 @@ async def launch_persistent_context_async(user_data_dir, seed=None, platform="wi
127
168
  stealth_args=True, timezone=None, locale=None,
128
169
  extension_paths=None, binary=None, license_key=None,
129
170
  license_server=None, license_through_proxy=False,
130
- license_preflight=True, driver=None,
171
+ license_preflight=True, driver=None, mobile_device=None, browser_brand=None,
131
172
  **kwargs):
132
173
  """Async persistent context -> BrowserContext."""
174
+ from .mobile import context_options, validate_launch
175
+ from . import desktop
176
+ desktop.validate_launch(browser_brand, platform, binary, args, stealth_args)
177
+ validate_launch(platform, binary, args, stealth_args, mobile_device)
133
178
  await _license_preflight_async(license_key, license_server, license_preflight)
134
179
  key, _ = _license.effective(license_key, license_server)
135
180
  binpath = _resolve_binary(binary, key)
@@ -138,12 +183,14 @@ async def launch_persistent_context_async(user_data_dir, seed=None, platform="wi
138
183
  display = os.environ.get("DISPLAY")
139
184
  proxy_kwargs, proxy_args = await _proxy_and_geo_args_async(proxy, binpath, geoip, display)
140
185
  launch_args = proxy_args + _build_args(
141
- seed, platform, stealth_args, timezone, locale, extension_paths, args, headless)
186
+ seed, platform, stealth_args, timezone, locale, extension_paths, args, headless, mobile_device, browser_brand)
142
187
  if license_through_proxy:
143
188
  launch_args.append("--license-through-proxy")
144
189
  env = _license.child_env(license_key, license_server)
145
190
  status_path = _new_status_file(env)
146
- if "viewport" not in kwargs and "no_viewport" not in kwargs:
191
+ if platform in ("android", "ios") and stealth_args:
192
+ kwargs = context_options(seed, kwargs, mobile_device, platform)
193
+ elif "viewport" not in kwargs and "no_viewport" not in kwargs:
147
194
  kwargs["no_viewport"] = True
148
195
  # Strip Playwright's automation + forced-software-WebGL defaults;
149
196
  # the caller can override by passing ignore_default_args explicitly.
@@ -173,6 +220,22 @@ async def launch_persistent_context_async(user_data_dir, seed=None, platform="wi
173
220
  raise
174
221
  _clear_status_file(status_path)
175
222
  ctx._capium_seed = seed
223
+ if desktop.normalize_brand(browser_brand) != "chrome":
224
+ ctx = _wrap_close_async(ctx, pw)
225
+ try:
226
+ await desktop.require_engine_async(ctx, browser_brand)
227
+ except Exception:
228
+ await ctx.close()
229
+ raise
230
+ return ctx
231
+ if platform in ("android", "ios") and stealth_args:
232
+ ctx = _wrap_close_async(ctx, pw)
233
+ try:
234
+ await _require_mobile_engine_async(ctx, mobile_device, seed, platform)
235
+ except Exception:
236
+ await ctx.close()
237
+ raise
238
+ return ctx
176
239
  return _wrap_close_async(ctx, pw)
177
240
 
178
241
 
@@ -182,7 +245,8 @@ async def launch_context_async(seed=None, platform="windows", headless=False, ur
182
245
  browser = await launch_async(seed=seed, platform=platform, headless=headless, **kwargs)
183
246
  # no_viewport: page fills the real window (see browser.launch_context).
184
247
  context = (browser.contexts[0] if browser.contexts
185
- else await browser.new_context(no_viewport=True))
248
+ else await browser.new_context(
249
+ **({} if getattr(browser, "_capium_mobile", False) else {"no_viewport": True})))
186
250
  if humanize:
187
251
  from . import human as _human
188
252
  _human.humanize(context, preset=human_preset)