Browser git · main
Minimal browser in C (mini and big variant)
C 96.1% Markdown 3.7%git clone https://git.christianimmanuel.de/web-tools/Browser.gitwget https://git.christianimmanuel.de/web-tools/Browser/archive/Browser.tar.gzDefault search keys And stuff
HANDOVER.md | 393 +++++++--------- Makefile | 2 +- README.md | 1272 ++++++++++++++++++++++++++++++++++++++++++++++------ browser-big | Bin 0 -> 287104 bytes browser-big.c | 1365 +++++++++++++++++--------------------------------------- browser-mini | Bin 0 -> 215040 bytes browser_core.c | 412 ++++------------- browser_core.h | 11 +- 8 files changed, 1816 insertions(+), 1639 deletions(-) diff --git a/HANDOVER.md b/HANDOVER.md index ad6a5aa..2de5b08 100644 --- a/HANDOVER.md +++ b/HANDOVER.md @@ -1,223 +1,170 @@ -# Handover - -The state of browser-mini / browser-big at 4.39.0, for whoever picks it up -next. The short version: video calls work on WebKitGTK 2.54.0 with -GStreamer 1.28.7, given three upstream patches and the defaults in -browser-big. - -## Test setup - -- ThinkPad T14 Gen1 (AMD). Self-built WebKitGTK 2.54.0, GStreamer 1.28.7, - GTK 4.20.3. No sound server (`audio = alsa`). -- Site: a mediasoup-based chat (mediasoup-client, `Safari12` handler, - VP8-only router, socket.io signalling). -- Reference: the WebRTC samples, `peerconnection/pc1` for a call without a - server. - -## Upstream bugs (need patching, not configuration) - -### 1. WebKit: camera frame flood (WebKit PR 74373, open) - -`GStreamerVideoCapturer::createConverter` sets `drop-only` on `videorate` -only for GStreamer < 1.28. The capture pipeline runs with base time 0, so -buffer PTS are absolute CLOCK_MONOTONIC values, and `videorate` fills the -gap from 0 with uptime x fps copies. The result is a frozen or black camera, -400+ encoded frames per second, and a pinned CPU. It also affects 2.52. - -``` ---- a/Source/WebCore/platform/mediastream/gstreamer/GStreamerVideoCapturer.cpp -+++ b/Source/WebCore/platform/mediastream/gstreamer/GStreamerVideoCapturer.cpp -@@ -135,6 +135,10 @@ - - auto* bin = gst_bin_new(nullptr); - auto* videorate = makeGStreamerElement("videorate"_s, "videorate"_s); -+ // The capture pipeline runs with base time 0, so buffer PTS are absolute -+ // CLOCK_MONOTONIC values. Without skip-to-first, videorate fills the gap -+ // from segment start with uptime x fps duplicate frames (WebKit PR 74373). -+ g_object_set(videorate, "skip-to-first", TRUE, nullptr); - - // The workaround below doesn't seem necessary anymore in GStreamer 1.28 and beyond. - // Fixed by: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/commit/6f623af4d745efaacd0c8639b99536def4a65c78 -``` - -Check: `pc1` encodes about 90 frames per 3 s report (30 fps). - -### 2. GStreamer 1.28: glupload NULL video meta (fixed on main) - -`_dma_buf_upload_accept` in `gst-libs/gst/gl/gstglupload.c` does -`out_info->width = meta->width` without checking `meta`. It runs on a format -change with a dma-buf buffer that has no video meta, which is exactly the -site's second `getUserMedia`. The web process crashes (SIGSEGV on the -`vqueue:src` thread). 1.26 had the check. - -``` ---- a/gst-libs/gst/gl/gstglupload.c -+++ b/gst-libs/gst/gl/gstglupload.c -@@ -1695,8 +1695,10 @@ - * matches the size we use to import the dmabuf. @outcaps will remains - * display resolution as expected. - */ -- out_info->width = meta->width; -- out_info->height = meta->height; -+ if (meta) { -+ out_info->width = meta->width; -+ out_info->height = meta->height; -+ } - - /* - * When we zero-copy tiles, we need to propagate the strides, which contains -``` - -Without the patch: `WEBKIT_GST_DISABLE_GL_SINK=1`. - -### 3. WebKit: camera sizes given as a list are skipped - -`GStreamerVideoCaptureSource::generatePresets` reads each caps structure -with `gst_structure_get(..., "width", G_TYPE_INT, ..., "height", -G_TYPE_INT, ...)` and skips it when that fails. V4L2 lists some modes as a -size list in one structure. The T14 camera (`--list-cameras`): - -``` -640 x { (int)480, (int)360 } 30/1 -320 x { (int)240, (int)180 } 30/1 -``` - -These are all of its 4:3 modes, in every format (DMA_DRM, MJPEG, YUY2). -WebKit had no 4:3 preset, so `{max: 20}` gave 848x480@20 and no frame rate -gave 1280x720@10. `gst_caps_normalize` splits them into discrete -structures (checked with the local GStreamer: 640x480, 640x360, 320x240, -320x180). - -``` ---- a/Source/WebCore/platform/mediastream/gstreamer/GStreamerVideoCaptureSource.cpp -+++ b/Source/WebCore/platform/mediastream/gstreamer/GStreamerVideoCaptureSource.cpp -@@ -298,7 +298,13 @@ - void GStreamerVideoCaptureSource::generatePresets() - { - Vector<VideoPreset> presets; -+ // A V4L2 device may list several sizes in one structure, for instance -+ // width=640, height={ 480, 360 }. Split them into one structure each, -+ // or every size listed that way is skipped below as not discrete - on -+ // a typical laptop camera that is every 4:3 mode. - auto caps = m_capturer->caps(); -+ if (caps) -+ caps = adoptGRef(gst_caps_normalize(gst_caps_copy(caps.get()))); - for (unsigned i = 0; i < gst_caps_get_size(caps.get()); i++) { - GstStructure* str = gst_caps_get_structure(caps.get(), i); -``` - -Check: `--rtc-trace` shows the video track at 320x240, aspect 1.333. - -All three patches are needed again for each new WebKit or GStreamer until -upstream ships them. - -## WebKit behaviour browser-big works around (all default) - -Each item was found in the WebKit 2.52.6 / 2.54.0 sources and confirmed by -a run. - -1. **librice finds no srflx candidate.** `RiceBackend::resolveAddress` - keeps only the first DNS answer (upstream FIXME), and the STUN address is - built as `host:port` without IPv6 brackets. Measured: librice host-only, - libnice srflx in 0.11 s. Default is libnice - (`WEBKIT_GST_DISABLE_WEBRTC_NETWORK_SANDBOX=1`); `ice = rice` undoes it. -2. **Encoder fixed to the first codec of the own offer.** - `doSetLocalDescription` -> `linkOutgoingSources` -> - `configurePacketizers` links the first codec it can encode. The answer - is never consulted, and `codecPreferencesChanged` refuses once the bin - runs. When a track is added to an existing connection (renegotiation), - the source is configured at `addTransceiver` time, so only - `setCodecPreferences` switches it. - - Fix: `setCodecPreferences(VP8 first)` on transceivers with a video - sender track, plus VP8 moved first in the SDP (`sdpPreferCodecs`). - - The mediasoup probe pc (no tracks) is left alone; reordering it broke - mediasoup's device caps. -3. **No `a=ssrc` lines, and the real SSRC is unknown until connected.** - mediasoup-client reads the SSRC from `pc.localDescription` after SLD and - registers the producer under it. A mismatch drops every packet. - - Fix: inject a placeholder SSRC (`sdpAddSsrc`). After SLD, learn the - real one from `sender.getStats()` in the background (outbound-rtp, up - to 15 s). - - Rewrite `localDescription` on the pc (`hookLD`), and hold the one - WebSocket message that contains the placeholder until it can be - rewritten (`ssrc-ws`). -4. **Payload types differ between the probe and the real pc.** The answer - carries the probe's numbers (VP8 = 111); WebKit sends its own (96). - Fix: in the same held message, `codecs[0].payloadType` and the rtx - `apt` are set to what the stats say is sent (`pt-fixed`). -5. **`RTCRtpSendParameters.codecs` required.** Filled in from - `getParameters()` when a library omits it. -6. **A MediaStream player never starts while an audio track delivers - nothing.** Remote cameras without a microphone stay at readyState 0 - although frames are decoded. - - Fix: after 2.5 s at readyState 0, with live video and silent audio, the - element gets a video-only stream; the `srcObject` getter keeps - returning the page's stream. - - The original goes back on audio `unmute`. Confirmed: every such cam - played right after `video-audio-split`. -7. **`query-permission-state`** (new in 2.54) is answered from - `permissions.tsv`. Unanswered means "prompt", and sites get no device - labels. -8. **Microphone choice.** Device labels are hidden until the first grant, - so `mic_match` swaps the matching input into the stream after the first - successful `getUserMedia` (`mic-swapped`). - -9. **Camera frame rate.** `bestSupportedSizeFrameRateAndZoom` skips - every preset whose frame-rate list lacks the exact requested rate. The - site asks 320x240 with `frameRate {max: 20}`; 320x240 only runs at 30, - so even with patch 3 a 16:9 mode (848x480@20) won. - - Fix: `looseFps()` drops a soft frameRate in `getUserMedia` and - `applyConstraints` (exact and min are kept, `gum-fps-loosened`). - `cam_size_fix = no` undoes it. - - Chrome scales per track and drops frames instead. -10. **`mic_match` wins** over the site's own audio deviceId (`exact`). The - site's second request named the headset jack again. - -`fix_webrtc = no` turns off 3-6. `--no-cam-fix` injects no script at all. - -## Removed (proven not to work) - -- `--cam-exact`: WebKit answered an exact 320x240 with 424x240 instead of - failing. -- `--cam-force`: a preference that WebKit ignored. -- `--cam-scale`: the canvas track has no deviceId or label, and the site - rejected it at once. -- `--max-bitrate`, `--max-fps`: `setParameters` was accepted, and the - send rate did not change. -- `fixSize` (4.38, re-apply the size once the probe track ended): the log - showed no other track open (`waited:0`) and still 1280x720. The real - cause was patch 3. -- `--cam-share-clone`, `--cam-hold`: `MediaStreamTrack.clone()` goes black - after about 1 s on 2.52. Sharing the track itself works. - -## Open issues - -- **No keyframe after loss.** A remote stream that loses a few packets - stops decoding. FIRs are sent (`in-video.fir` climbs) but `keys` does not - move. Either the sender or server throttles keyframes, or the depayloader - waits for one it does not recognise. `rtpvp8depay2` (gst-plugins-rs) is - installed alongside `rtpvp8depay`; ranking it out changed nothing. -- `GStreamer-RTP-CRITICAL gst_rtp_header_extension_get_id` on receiving - streams: `gstrtpbasedepayload.c:646` walks a header extension list with a - NULL entry on a caps event. Harmless so far. -- The three patches above are not upstream yet. - -## Checks that pin a problem down - -- `--rtc-trace`: `sdp` (full `pt name` lists), `rtc-stats` (`out-*`, - `far-*`, `in-*` with fir/keys), `video-state`, `video-attach`, - `ssrc-real` / `ssrc-ws` / `pt-fixed`, `codec-mismatch`. -- `pc1` at 30 fps: the camera path and the WebKit patch are fine. -- A LibreWolf run of the same room separates site bugs from ours. -- gdb attached to `WebKitWebProcess` before the camera starts (see README). - -## Build notes - -- On 2.54, `WebKitPointerLockPermissionRequest` is only declared when - WebKit is built with pointer lock (`#if ENABLE(POINTER_LOCK)` in - `webkit.h.in`), so both uses are behind - `#ifdef WEBKIT_TYPE_POINTER_LOCK_PERMISSION_REQUEST`. -- `MAX`/`MIN`/`CLAMP` evaluate their argument twice. Option values are read - into a local first (`argv[++i]` inside `MAX` swallowed the next option). -- Always ship all seven files together; `make version` identifies the - build. +# browser-mini / browser-big: handover notes + +State at **4.18.0 (build 4bbf154)**. Written for a fresh chat whose job is +to clean the code up. Read this first, then the README. + +## 1. The project + +| File | Lines | What | +| --- | --- | --- | +| `browser_core.c` | ~7800 | window, popups, keys, history, downloads, search, media mode, config, watchdogs | +| `browser_core.h` | ~180 | `Win` struct, `BrowserApp` hooks, `LOG` macro | +| `browser-big.c` | ~2660 | camera / GStreamer / WebRTC layer on top of the core | +| `browser-mini.c` | 22 | fills in `BrowserApp`, calls `browser_main()` | +| `Makefile` | | `PREFIX ?= /usr`, `install`, `uninstall`, `clean`, `version` | + +- WebKitGTK 6.0 (GTK4) only. `make` builds both binaries. +- Build id = md5 of the sources (`make version`). +- The user's preferences apply: md5 build versioning reported at the end of + each reply, Makefile defaults to `/usr/bin` with `clean` and `uninstall`, + a short README branded "100% Vibecode but tested", no Ubuntu in the README, + no extra files unless asked, minimal testing. + +## 2. The user's system (verified in the logs) + +- Linux From Scratch, pkgusr package users, **no systemd**, native Wayland, + plain ALSA (no PulseAudio/PipeWire running), no desktop portal, + `XDG_RUNTIME_DIR=/tmp/xdg-n76310` (mode 042770). +- Lenovo 20UD (AMD Renoir): Mesa radeonsi, RADV Vulkan, VA-API H.264/HEVC + work; firmware complete. +- **WebKitGTK 2.52.5** (upgraded from 2.50.5 during this work), + GTK 4.20.3, GLib 2.88.3, GStreamer 1.28.6. 2.52 defaults to librice + instead of libnice for ICE. +- WebKit build flags: `USE_GSTREAMER_WEBRTC=ON` (the **only** WebRTC backend + the GTK port has; OFF means no WebRTC at all), `USE_LIBRICE=ON`, + `ENABLE_WEB_RTC=ON`, `ENABLE_MEDIA_STREAM=ON`, no bubblewrap sandbox. + The pkgusr script had `USE_GTK4=OFF`, which builds the wrong library for + these browsers; it must be `ON`. +- Camera `/dev/video1`: MJPEG up to 1280x720@30, raw YUY2/DMA_DRM 1280x720 + only @10. Plain `gst-launch v4l2src` starts instantly (60 frames in 2.3 s). +- gdb is **not** installed, and `wchan` reads 0 for every thread on this + kernel. Neither can be used for thread dumps. +- The user's wrapper `su - user -c 'cmd "$@"' -- "$@"` ate the first + argument (it becomes `$0`). Fixed by `-- browser-mini "$@"`. + +## 3. Done, and working + +**Core** +- `-h` answered after the config is read, so it prints effective values. +- Ctrl+K rows no longer load search templates as URLs. +- Popups unified: things you type into are top centre, messages top right, + the address bottom left. Non-interactive panels fade on hover. +- Quiet config: unknown keys in swov's shared file are counted, not listed. +- Camera/mic permission prompt (Enter = allow, Esc = deny), remembered per + site in `permissions.tsv`; `media = ask|allow|deny`; `--forget-perms`. +- Find-in-page no longer freezes big pages: 1000-match cap, 120 ms + debounce, one `search()` pass. +- Fatal messages survive `abort()`: a signal handler drains the log pipe; + backtraces via `-rdynamic`. +- Watchdogs: web process spinning (every 5 s; from 4.15 with timestamps, + a "calm again after Ns" line, and a GStreamer-vs-JIT hint), UI main loop + blocked, and a missing Wayland frame callback. +- `--gsk` defaults to `gl` (GTK's Vulkan renderer deadlocked on resize). +- `--paths` (4.12+) lists every file and directory written, including the + download rules, search keywords (with built-ins), start page colour and + media temp dir. +- Start page: flat colour, with a toggleable swatch palette at the top; + the choice is saved in `~/.local/share/wkview/start-bg`. +- Ctrl+P shows the whole URL, wrapped, and toggles (4.13). +- Media mode (4.16/4.17): F2 toggles it and a frame shows it. Ctrl+click + sends a video to `player` (mpv) and an image to `image_viewer` (imv); + images are downloaded with libsoup first. `--player` on the command line + starts in the mode. +- Built-in search keywords (4.18): g Google (default), d DuckDuckGo, + w Wikipedia, s SDL3 wiki via DuckDuckGo. The user's own keywords win by + name. + +**browser-big** +- Media workarounds are opt-in only (the user's rule: clean by default). +- 4.14 fix: the tracing and compat flags (`--rtc-trace`, `--web-compat`, + `--no-mic`, offer fixes) used to silently turn on camera *cloning and + holding*. On 2.52 that froze the page's camera after ~1 s. Now only + `--cam-fix`, `--cam-share`, `--fix-media` and `cam_share=yes` touch the + camera; the log's `shim` line says `repair:true|false`. +- `--list-cameras`, `--media-debug`, `--gl-info`, `--list-features`, + `--feature NAME[=on|off]` all verified useful. + +## 4. Root causes found + +| Symptom | Cause | Status | +| --- | --- | --- | +| WebRTC never connects, `createAnswer` never settles | WebKitGTK **2.50.5** bug: `create-answer` never emitted to webrtcbin (log proves `_create_answer_task` absent, zero warnings) | **fixed by 2.52.5**, pc1 sample works | +| pc1 camera freezes after 1 s on 2.52 | **our** shim cloned the track (see 4.14) | fixed | +| Zoom crash: `Trying to dispose element … video-frame-converter-gl … PAUSED`, segfaults in libc/libgstbase | WebKit 2.52 use-after-free in its GL video frame converter | avoided with `--feature WebCodecsVideo=off` | +| Zoom still crashes with WebCodecs off: camera freezes, Zoom releases it, the process dies on teardown | WebKit capture teardown bug (heap corruption, `free(): invalid pointer`) | **open** | +| 20–30 s frozen preview at 400 % CPU (`queue*:src`, `multiqueue*:src`) | `v4l2src` blocked downstream for ~28 s, then "Timestamp does not correlate with any clock". Seen in `-n` runs, where playback went to a **PulseAudio** sink with clock-skew warnings (libpulse autospawn is suspected) | **open**: the run with `audio = alsa` was never reported | +| `--cam-exact 640x480@30` ignored, 1280x720 returned with no error | WebKit 2.52 ignores exact capture constraints | WebKit bug; nothing to do | +| `screen.orientation` missing | WebKitGTK does not implement it; Zoom throws | `--web-compat` supplies it | +| GoDaddy login refused (`fp` → 429) | bot detection; changing the UA did not help | not ours; use another browser | +| TTY switch aborts every browser | GTK Wayland dispatch → WebKit abort | not ours | + +## 5. Tried, and wrong or useless (do not repeat) + +- A "stale binary" theory for the argument bug: it was the `su` wrapper. +- `--audio-alsa` aimed at the device provider: `GST_PLUGIN_FEATURE_RANK` + does not rank device providers. Retargeted at the elements. +- Requiring a `nicesrc` element: wrong. The real check builds a webrtcbin + and queries its `ice-agent`. +- "gst-debug compiled out", claimed twice: wrong both times. Use the + `GST_DISABLE_GST_DEBUG` macro, not grep or `gst_debug_is_active()`. +- `dbus-launch`: caused a 61 s hang (a bus with no portal behind it). +- The GPU driver theory: an all-software run froze the same way. +- `--fix-webrtc` does not enable tracing (misread once as a regression). +- "Switch to the libwebrtc backend": that option does not exist for GTK. +- `WebCodecsVideoEnabled`: wrong feature name; it is `WebCodecsVideo`. +- `--cam-force` (ideal) and `--cam-exact`: ignored by WebKit 2.52. +- `--no-hw-decode` and `--no-gpu`: no help with the freeze or the crash. +- `wchan` thread dumps: always 0 on this kernel. +- GStreamer's log has colour codes when redirected; strip them with + `sed -E 's/\x1b\[[0-9;]*m//g'` before grepping. + +## 6. Cleanup candidates for the next chat + +**Likely obsolete on 2.52**, so check each and remove what is dead: +- `--sdp-ssrc-fix`, `--rtc-params-fix`, `--fix-webrtc`: written against + 2.50 behaviour, before the real bug (the `createAnswer` hang) was known. +- The camera repair shim: `--cam-fix`, `--cam-share`, relax/retry/keepalive/ + hold/drop-audio, `--cam-scale*`, `--cam-force`, `--cam-exact`, + `--cam-match`, `--cam-retries`, `--cam-hold`. Cloning is actively harmful + on 2.52, and exact constraints are ignored. +- `--prewarm`, `--warm-cam`, `--media-watchdog`, `--auto-reload`, + `--load-timeout`, `--max-reloads`, `--stall-timeout`: never shown to help. +- `--no-mic`: its diagnostic question has been answered. + +**Keep:** `--rtc-trace`, `--media-debug`, `--list-cameras`, `--gst-*`, +`--web-compat` (screen.orientation), `--feature`, `--list-features`, +`--gl-info`, the watchdogs, `--paths`, the permission prompt, media mode, +search keywords, `--audio-alsa` / `audio = alsa`. + +**Code smells worth a pass:** +- `browser_core.c` is ~7800 lines in one file. Natural splits: config, + history, downloads, search, popup/omni, media mode, watchdogs, start page. +- `LOG` lines are inconsistent: only browser-big's `mlog` and the watchdog + carry timestamps. +- The shim JS is one large C string in `browser-big.c`; it could be a + separate `.js` embedded at build time. +- The README is ~1200 lines, with long debugging narratives from the 2.50 + era; trim it to what is still true on 2.52. + +## 7. Open questions to settle first + +1. pc1 **with** the config (`audio = alsa`, no `-n`): is the 20–30 s freeze + gone? If yes, the PulseAudio sink clock was the cause, and browser-big + should force ALSA or warn when `autoaudiosink` picks pulse. +2. `command -v pulseaudio`: is libpulse autospawning a daemon? +3. Zoom with `--web-compat --feature WebCodecsVideo=off`, then + `dmesg | grep -iE 'segfault|WebKitWeb'`: which library dies now? +4. arte.tv videos fail in both browsers. The decoder check and the three + test pages were proposed but never run (see the last messages: check + for `avdec_aac`/`faad`/`fdkaacdec`, `avdec_h264`/`vah264dec`, + `qtdemux`, `h264parse`, `aacparse`). +5. A gdb build would give real backtraces for the WebKit crash reports. + +## 8. Useful test pages + +- WebRTC loopback: `https://webrtc.github.io/samples/src/content/peerconnection/pc1/` +- No camera needed: `…/datachannel/basic/` and `…/capture/canvas-pc/` +- Camera only: `…/getusermedia/gum/` +- Zoom test meeting: `zoom.us/test` +- Plain MP4: `https://www.w3schools.com/html/mov_bbb.mp4` +- MSE/HLS: `https://hlsjs.video-dev.org/demo/` diff --git a/Makefile b/Makefile index 674ab6a..c9b8404 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CC ?= gcc CFLAGS ?= -O2 -Wall -Wextra -rdynamic PREFIX ?= /usr -VERSION := 4.39.0 +VERSION := 4.18.0 SOURCES := browser_core.c browser_core.h browser-mini.c browser-big.c Makefile BUILD_ID := $(shell cat $(SOURCES) 2>/dev/null | md5sum | cut -c1-7) diff --git a/README.md b/README.md index 659cc7e..d9259f3 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,432 @@ # browser-mini / browser-big -Two WebKitGTK 6.0 (GTK4) browsers sharing one core. +Two WebKitGTK 6.0 (GTK4) page viewers sharing one core. -**100% Vibecode but tested.** Builds warning-free with `-Wall -Wextra`. -Used for real video calls on WebKitGTK 2.54.0 with GStreamer 1.28.7. +**100% Vibecode but tested** — built warning-free with `-Wall -Wextra` against +GTK 4.14 / WebKitGTK 2.52.3, and run headless under Xvfb. -- **browser-mini**: a plain, fast page viewer. -- **browser-big**: the same, plus camera, microphone and WebRTC repairs and - diagnostics. - -## Files +## Layout | File | What it is | | --- | --- | -| `browser_core.c` / `.h` | Window, keys, history, downloads, profiles, config | -| `browser-mini.c` | Fills in the hooks and calls `browser_main()` | -| `browser-big.c` | Camera, GStreamer and WebRTC repairs, diagnostics | -| `patches/` | Three upstream fixes for WebKitGTK and GStreamer (see *Patches*) | -| `HANDOVER.md` | Background: each upstream bug and workaround, with evidence | +| `browser_core.h` | Window state, the `BrowserApp` hook struct, shared helpers | +| `browser_core.c` | Window, popup, toast, zoom, downloads, profiles, keys, history | +| `browser-mini.c` | 20 lines. Fills in four fields and calls `browser_main()` | +| `browser-big.c` | Camera / GStreamer / watchdogs / diagnostics on top of the core | + +browser-mini adds nothing to the core. Every hook is optional. + +## WebKit runtime features + +WebKit carries a few hundred runtime switches, and some a real site needs +are off by default. The one that matters most here is +**`AllowWebGLInWorkers`**: without it a library that renders in a worker +gets `Cannot create a canvas in this context`, falls back to the CPU, and +the result looks like a freeze rather than an error. Zoom's video pipeline +does exactly this. + +``` +browser-big zoom.us/test --feature AllowWebGLInWorkers --web-compat +browser-big --list-features webgl # what exists and its default +``` + +`--feature` is repeatable and takes `NAME=off` too. Measured effect: +`offscreenWebgl` in the `rtc-caps` line goes from `false` to `true`. + +Enabling it can expose a second problem underneath: + +``` +Failed to create a graphics context for WebGL using GBM, falling back to textures +``` + +That is WebKit failing to make a GBM-backed GL context in the web process. +`--feature UseGPUProcessForWebGL` moves WebGL to the GPU process instead, +which is the first thing to try; then `--no-dmabuf`, then `--no-gpu` to +take hardware paths out entirely. + +If none of those help, there is a better fallback than the CPU path. A +library decides whether to attempt worker-side rendering by checking for +`OffscreenCanvas`; if that exists but WebGL inside it does not, it starts +the work anyway and falls back to the CPU, which is the freeze. Removing +the API removes the attempt: + +``` +browser-big zoom.us/test --feature OffscreenCanvas=off +``` + +Zoom's virtual-background worker (`vb_worker.min.js`) is exactly this +case. With OffscreenCanvas absent it does not try, the feature is +unavailable, and the meeting runs. + +`--gst-debug` can only report anything if GStreamer was built with its +debug system, and `--media-debug` distinguishes the two things that are +easy to confuse: + +``` +gst-debug: supported; currently off, set GST_DEBUG to use it (threshold 0) +gst-debug: supported; currently ON (threshold 3) +gst-debug: COMPILED OUT of this GStreamer +``` + +`gst_debug_is_active()` is false whenever no level is set, even in a build +that fully supports debugging — reading it as "compiled out" is wrong. The +compile-time answer is the `GST_DISABLE_GST_DEBUG` macro, and in +`gstconfig.h` that name also appears inside an `#if 0` documentation +block, so grepping for it without context misleads too. + +`LIBGL_ALWAYS_SOFTWARE=1` is only a useful test if Mesa was built with a +software renderer. Without one it fails as + +``` +libEGL warning: egl: failed to create dri2 screen +Could not create surfaceless EGL display: EGL_NOT_INITIALIZED. Aborting... +web process crashed +``` + +which says the fallback is missing, not that software rendering is slow. + +## SharedArrayBuffer + +Off by default in JavaScriptCore, and a site whose media engine uses +threaded WebAssembly needs it — without it the page hangs at a spinner +rather than reporting anything. + +``` +browser-big zoom.us/test --shared-array-buffer --web-compat +``` + +In the config, so it cannot be forgotten: + +``` +shared_array_buffer = yes +web_compat = yes +``` + +`--jsc NAME=VALUE` sets any other JavaScriptCore option the same way +(`--jsc useJIT=0`, and so on). Measured: `sharedArrayBuffer` in the +`rtc-caps` line goes from `false` to `true`. The page must also be +cross-origin isolated, which `rtc-caps` reports separately. + +## Isolating a freeze that happens after a call connects + +Once ICE has completed and the devices are live, two pipelines start that +were idle before: the video encoder and the audio path. Each can be taken +out on its own, so one run says which side it is: + +``` +browser-big URL --no-mic # camera only, no audio path at all +browser-big URL --video-codecs VP8 # software VP8 instead of VA-API H.264 +browser-big URL --no-hw-decode # software decoding +browser-big URL --gst-rank vah264enc:NONE,vah264lpenc:NONE # no VA-API encoding, keep H.264 +``` + +And the decisive test for a GPU driver problem is to take the GPU out of +the media path entirely: + +``` +browser-big URL --no-dmabuf --no-hw-decode --no-va --video-codecs VP8 +``` + +Slow, but if it is *stable* where the hardware path freezes, the driver is +the fault. `dmesg | grep -iE 'amdgpu|gpu reset|ring .* timeout'` during a +freeze is the other half of that proof: a hung GPU says so there. + +The audio track here reports `sampleRate: 0`, which is not a valid rate; +a library that sizes its buffers from it can spin. `--no-mic` is the test +for that. + +## APIs WebKitGTK does not implement + +`--web-compat` supplies them. Currently `screen.orientation`: Zoom's media +code reads `screen.orientation.type` and throws before it starts, even +though its WebRTC negotiation completes fine. + +## A site that will not accept the stream + +WebKitGTK's WebRTC is GStreamer-based and its offer differs from the one a +site tested against Chrome. Two of those differences stop a publish before +it starts, and both are repaired in the offer rather than in the site: + +| Flag | | +| --- | --- | +| `--fix-webrtc` | `--sdp-ssrc-fix` and `--rtc-params-fix` together | +| `--rtc-params-fix` | supplies `RTCRtpSendParameters.codecs` when a library omits it. WebKit requires it, Firefox does not, so a library on its Firefox path throws `Member RTCRtpSendParameters.codecs is required` | +| `--rtc-trace` | also reports an `rtc-caps` line up front: user agent, every WebRTC API, sendable codecs, plus WebGL, OffscreenCanvas, WebAssembly, SharedArrayBuffer, WebCodecs and `requestVideoFrameCallback`. A conferencing client processes frames in WebAssembly and renders with WebGL, so a missing one shows up as a freeze rather than an error. Emitted even when WebRTC is absent | +| `--rtc-trace` | reports every WebRTC step — connection made, tracks attached, offer created, answer set. The last event logged is the step that failed. Included in `--media-trace` | +| `--sdp-ssrc-fix` | adds the `a=ssrc:<n> cname:<v>` lines WebKit leaves out. A site parsing the offer for the CNAME throws `CNAME value not found` and never connects | +| `--video-codecs VP8` | sets the sending codec order. WebKit may offer something this machine has no encoder for — `--media-debug` lists the encoders you have | + +The user agent comes first: a publisher library picks its code path from +it, and WebKitGTK's own string may get no path at all — in which case no +connection is ever built and nothing else matters. + +``` +browser-big room7.com --css no.css --ua-firefox --fix-webrtc --video-codecs VP8 +``` + +`--fix-webrtc` does not turn tracing on, so add `--rtc-trace` when you want +to watch it work. With `--sdp-ssrc-fix` the log also reports +`sdp-ssrc-verify`, which says whether the injected lines were still present +in `pc.localDescription` afterwards — WebKit reparses the description, and +a library reads the lines back from there, so injecting them is not the +same as them surviving. + +Check `--rtc-trace` first: the SDP rewrite only helps if an offer is ever +made, and a site that fails earlier will show no `createOffer` at all. No +`rtc-new` means the site never built a connection — it rejected the browser +before WebRTC, and the `rtc-caps` event reports what it would have looked +at: the user agent, and whether each API a publisher library needs exists. +In that case the lever is `--ua-chrome` or `--ua-firefox`, not the offer. + +Both are off unless asked for. The SDP rewrite only touches sections that +send and that have no ssrc lines already, and is idempotent. + +## A page that opens the same camera twice + +Some pages request a camera, enumerate devices with the labels that grants +them, drop the stream and request the same device again — twice. A camera +cannot be opened twice: the second open returns a track that reports +`readyState: live` and never delivers a frame, and the `<video>` sits at +`readyState 0` reporting `waiting` then `stalled` forever. + +This is a gap in WebKitGTK rather than a broken site: Chrome and Firefox +multiplex one device open into several tracks internally, so a page asking +twice just works there. Sharing does the same thing — it clones the live +track, so each caller gets an independent track over a single open, and +stopping one does not disturb the other. + +The hold expires on its own. Keeping the capture open is the point, but +keeping it open after the page has stopped every clone leaves the camera +light on with nothing watching it — so once no handed-out track is live, +the source is released after `--cam-hold` seconds (3 by default, 0 to +release at once). + +`--cam-share` hands the repeated request the stream that is already open +and changes nothing else, or permanently: + +``` +cam_share = yes +``` `--cam-fix` includes it, along with constraint +relaxing that the page did not ask for — `--cam-share` when the sharing is +all that is wanted. + +## Slow resize, or slow after regaining focus + +Measured, so it can be ruled out: the overlay positioning this browser does +on every allocation costs **11 microseconds per call, about 23 per +allocation** — 0.1% of a frame at 60Hz. It is not the cause of a stuttering +resize. + +What does dominate a resize is GTK's own renderer, and the default is not +always the best one for a given driver: + +``` +browser-mini URL --gsk cairo # software, no GPU involved +browser-mini URL --gsk gl # the older GL renderer +browser-mini URL --gsk ngl # the newer one, usually the default +browser-mini URL --gsk vulkan +``` + +In the config, so it holds: + +``` +gsk = gl +``` + +GTK 4.20 defaults to its Vulkan renderer where the driver claims support, +and a Vulkan stack that deadlocks while recreating its swapchain freezes +the whole browser the moment the window changes size — with the web +process idle and nothing in any log. The GL renderer has been GTK's +workhorse for years and loses nothing here: WebKit composites the page +itself, and GTK only draws the final surface and the overlays on top. + +Then WebKit's own compositing, which is separate: `--no-dmabuf`, +`--no-compositing`, `--no-gpu`. WebKit also throttles a page that loses +focus and has to catch up when it returns, which no flag here changes. + +## A page that logs heavily + +WebKit writes the page's console output synchronously from the web process, +and writing to a terminal costs about six times what writing to a file does +(measured: 4000 lines, 4.3 ms to a file, 26.7 ms to a pty). A chatty +application therefore makes its own web process wait. + +`--no-console` drops the page's logging and keeps ours. `-q` silences both. + +## A page that freezes with the CPU pinned + +Both browsers watch their web process. Every five seconds they sample its +CPU time and memory, and when it has been spinning at 90% or more for +fifteen seconds they print the threads that are running or blocked, with +the kernel function each is waiting in: + +``` +watch: [16:31:43.102] web process 19976 is spinning: 401% cpu, 0.2 GB resident, for 15s. +watch: 19976 R WebKitWebProces (running) +watch: 20031 R queue1:src (running) +watch: 20040 R multiqueue0:src (running) +watch: the busy threads are GStreamer's (media), not +watch: JavaScript - --no-jit will not help with this one +watch: [16:31:58.117] web process 19976 calm again after about 30s (4% cpu now) +``` + +Nothing is asked of the web process, so this works while it is frozen. +The `calm again` line closes the report, so the log shows how long the +page was frozen. Threads named `something:src` are GStreamer streaming +threads, and the hint says so instead of pointing at the JIT. + +The browser watches itself the same way. A heartbeat thread notices when +the main loop has not run for five seconds — the one freeze the main loop +cannot report — and prints this process's main thread and anything busy +or blocked, straight to stderr: + +``` +watch: THIS PROCESS (13843) main loop has not run for 5s. Its running/blocked threads: +watch: 13843 S browser-mini vk_wait_fence +``` + +A main thread waiting in a GPU, Vulkan or DRM call is GTK's renderer: +`--gsk gl`, then `--gsk cairo`. + +There is a third kind of freeze that is not one. GTK paints only when the +compositor returns a frame callback, and if one never arrives after a +resize the main loop keeps running, input keeps working, and nothing is +ever drawn again. The watch tells this case apart too: + +``` +watch: main loop is running but NO FRAME HAS BEEN PAINTED for 12s although one was requested. +watch: GTK is waiting for a frame callback the compositor never sent +``` + +That one is between GTK's Wayland backend and the compositor, not the +browser or the page. Resizing the window again usually shakes a new +callback loose. + +And a fourth: the popups draw, `load:` lines keep arriving, the frame +watch stays quiet — and the page area itself is stale. GTK is painting +and the web process is answering; what stopped is the hand-off between +them. On Wayland that is the dmabuf renderer: the web process exports a +buffer, the widget imports it, and after a resize the import goes stale. +`--no-dmabuf` takes that path out. **F6** re-presents the view without a +reload for the case at hand — the view is hidden for one loop iteration +and shown again with a fresh allocation, the page is kept. A +thread that is `R` with no wait and no GStreamer name is JavaScript or +WebAssembly running away — on a self-built WebKit, try `--no-jit` first. A thread in `D` names the +device or pipe it is stuck on. `--no-proc-watch` turns it off. + +## Find on a large page + +WebKit is asked for at most 1000 matches (the hint says `1000+` past that) +and the search runs on the pause after typing, not on every keystroke. A +one-letter search on a page of thousands of lines used to walk every match +twice and freeze the browser; now it is a few milliseconds. + +## Repeated messages + +WebKit and GLib write to stderr from several processes, and some lines +repeat dozens of times a run — "the desktop portal is unreachable" is +printed on every attempt. We cannot stop WebKit asking, so stderr is +threaded through a pipe: each distinct line is shown twice, then counted, +and the totals print at exit. + +``` +--- repeated messages, shown twice each above --- + 12 x Unable to connect to the Deskop portal: ... +``` + +Nothing is hidden and nothing is guessed at. `Ctrl+C` ends the run the same +way closing the window does, so the totals still appear. A fatal error — +`g_error`, `abort()`, a crash — drains the pipe from the signal handler +before the process dies, so its last message is printed rather than lost +in the pipe: `Aborted` on its own tells nobody anything. + +Some aborts say nothing at all. For those the handler prints the stack the +process was on, which names the layer even without symbols for every frame: + +``` +---- fatal: abort in this process, stack at the time: ---- +/lib/libc.so.6(abort+0xdf) +/lib/libEGL_mesa.so.0(+0x1a2f0) ← the driver's EGL +/lib/libgtk-4.so.1(gdk_gl_context_...) ← GTK's renderer asked for it +/lib/libglib-2.0.so.0(g_main_loop_run+0x127) +``` + +A frame in `libgtk`/`libgdk` is the toolkit, `libEGL`/`libGL`/`*_dri.so` +the driver, `libwayland-client` the compositor link, `libwebkit` the +engine. Built with `-rdynamic`, so the browser's own functions are named. + +## Is the graphics stack wired up at all? + +``` +browser-mini --gl-info +``` + +Checks the stack the way this browser will use it and exits: the DRM +device nodes and whether *this user* can open them, membership of the +`video` and `render` groups, the GL context GTK actually gets (API and +version), the renderer in use, and the environment variables that steer +all of it. A render node the user cannot open is the single most common +cause of "everything falls back to software and some of it aborts" — the +web process is where the GPU work happens, and it runs as you. It ends +with the commands for the layers below GTK: `dmesg`, `glxinfo`, +`vulkaninfo`, `vainfo`, and the firmware directory. + +## Switching virtual terminals + +One real backtrace from a VT switch, bottom-up: `wl_display_dispatch` → +GTK → `g_signal_emit` → **libwebkitgtk** → `abort`. No renderer, no +driver: a Wayland event that GTK relays as a signal, and a release +assertion in WebKit's own handler. That is a WebKitGTK bug, and `gsk` +cannot touch it. What can: + +``` +no_dmabuf = yes # the other backing-store path; different code, may not assert +``` + +and a newer WebKitGTK, which is where such assertions get fixed. To name +the function, resolve the WebKit frames if your build kept symbols: + +``` +gdb -batch -ex 'info symbol 0xf8d93d' /usr/lib/libwebkitgtk-6.0.so.4 +``` + +Switching away from the compositor's VT releases the GPU, and a renderer +holding a GL context can treat that as fatal. If the browser aborts on a +VT switch, the message above the `Aborted` line now says which layer did +it. `gsk = cairo` draws GTK's surface in software, with no GL context to +lose; WebKit composites the page in its own process regardless, so that is +the whole cost. +`--all-messages` turns the whole thing off. + +## Version + +``` +browser-mini --version # browser-mini 4.18.0 (build 4bbf154) +make version +``` + +The build id is an md5 of the sources, so two builds can be told apart +without guessing. + +`-h`, `-V` and `--paths` are answered before anything else on the command +line is looked at, so they work even next to a mistyped option. ## Build -Debian or any Linux distro: +Debian and derivatives: ``` apt install build-essential pkg-config libgtk-4-dev libwebkitgtk-6.0-dev \ libsoup-3.0-dev libgstreamer1.0-dev -make # build both -make install # into /usr/bin (PREFIX=... to change) +make +``` + +`libgstreamer1.0-dev` is only needed for browser-big. + +``` +make install # both into /usr/bin, PREFIX=... to move it make uninstall make clean -make version # 4.39.0 (build …), md5 of the sources ``` ## Usage @@ -40,193 +436,779 @@ browser-mini [URL] [options] browser-big [URL|PATH|diag] [options] ``` -No address opens the start page. No scheme means https; a path opens as a -file. `-h` lists every option and key. +With no address the window comes up on the start page (below). A local +file needs three slashes — `file:///tmp/index.html` — though a bare path +works too (`./index.html`, `/tmp/index.html`), and anything without a +scheme is tried as https. + +Run either with `-h` for the full option and key list. + +## Start page + +The program's name set large, a rule under it, the version and build id, +and the three keys worth knowing — on one flat colour. It is served +without a base URI, so it stays out of the history by itself. + +The dot at the top opens a row of swatches. Click one and it becomes the +default from then on, remembered in `~/.local/share/wkview/start-bg`. +Pure white and pure black are both there; between them are soft flat +tones. The text follows the colour, dark on a light background and light +on a dark one, so every swatch reads. + +``` +start_bg = f3f0e9 # the default, until you click a swatch +``` + +The file belongs to you rather than to a profile: every profile and both +browsers share it. Delete it to go back to `start_bg`. + +## Media mode + +`F2` toggles it. While it is on, a frame in the accent colour runs round +the page, and `Ctrl+click` sends media out instead of opening it: + +| Ctrl+click on | Goes to | +| --- | --- | +| a YouTube link, or a `.mp4` `.webm` `.mkv` `.m3u8` … link | `player` | +| an image, or a link to a `.jpg` `.png` `.webp` … | `image_viewer` | +| anything else | opened as usual | + +Set the programs once in the config: + +``` +player = mpv +image_viewer = imv +``` + +imv opens files, not addresses, so an image is downloaded first (with the +page as referer) into `$XDG_RUNTIME_DIR/wkview-media/`, and the viewer gets +the file. Files there are removed after an hour. + +| Option | Effect | +| --- | --- | +| `--player CMD` | the video player; given on the command line it also starts in media mode | +| `--image-viewer CMD` | the image viewer | +| `--media`, `media_mode = yes` | start in media mode | +| `player_match = youtube.com/watch, vimeo.com/` | which links count as video (default: YouTube videos, shorts, live) | + +It follows `--mod`: with `--mod alt` it is `Alt+click`. If a program +cannot start, a message says why and the click works as it normally would. + +## Where everything lives + +``` +browser-mini --paths +``` + +Prints every directory and file either browser writes, with what is on +disk now. It follows `--profile` and `--private` wherever they sit on the +line. + +| Path | What | +| --- | --- | +| `~/.config/swov/config` | Shared palette, read first | +| `~/.config/browser-mini/config` | Our settings, on top of it | +| `~/.local/share/wkview/<profile>/cookies.sqlite` | **Cookies — this is your logins** | +| `~/.local/share/wkview/<profile>/history.tsv` | Addresses visited | +| `~/.local/share/wkview/<profile>/searches.tsv` | Search keywords (`Ctrl+K`), per profile | +| `~/.local/share/wkview/<profile>/permissions.tsv` | Camera / microphone answers per site | +| `~/.local/share/wkview/<profile>/` | Also WebKit's own storage: local storage, IndexedDB, service workers | +| `~/.cache/wkview/<profile>/` | Cache, safe to delete | +| `~/.local/share/wkview/download-dirs.tsv` | Per-site download rules | +| `~/.local/share/wkview/start-bg` | Start page background | + +The last two are yours, not a profile's: every profile and both browsers +share them. Nothing else is written. + +Downloads are two separate things and `--paths` keeps them apart: the +directory a file **lands in** (`--download-dir`, or the XDG download +directory), and `download-dirs.tsv`, the one file holding every per-site +**rule** that overrides it. The report prints the rules themselves, so you +can see which site goes where without opening the file. + +Search keywords get the same treatment: `--paths` lists each keyword with +its URL and marks the default. They are per profile, so another +`--profile` shows a different list. + +`--clear-data` wipes the profile and cache directories, `--forget-perms` +drops `permissions.tsv` alone, `--private` skips all of it and keeps the +session in memory. + +Because `--paths` walks the directories instead of guessing, it also shows +WebKit's own storage under the profile, whatever the release names it. ## Keys | Key | Action | | --- | --- | -| `Ctrl+O`, `Ctrl+L` | Address (history below it) | -| `Ctrl+H` | History | -| `Ctrl+J` / `Ctrl+Shift+J` | Back / forward | -| `Ctrl+F` | Find | -| `Ctrl+S` | Download directory for page, site or everything | -| `Ctrl+K` | Search keywords | -| `Ctrl+D` | Recent downloads | -| `Ctrl+P` / `Ctrl+Y` | Show / copy the URL | -| `Ctrl+G` / `Ctrl+Shift+G` | Top / bottom | +| `Ctrl+O`, `Ctrl+L` | Type an address | +| `Ctrl+J` | Back | +| `Ctrl+Shift+J` | Forward | +| `Ctrl+H` | The same popup, opened on the history | +| `Ctrl+F` | Find in page. `Enter`/`Down`/`Ctrl+N` next, `Shift+Enter`/`Up`/`Ctrl+Shift+N` previous, `Esc` out | +| `Ctrl+S` | Download directory: this page, this site, or everything | +| `Ctrl+K` | Add a search keyword | +| `F1`, `Ctrl+/` | The key list, on screen | +| `Ctrl+G` | Scroll to top | +| `Ctrl+Shift+G` | Scroll to bottom | | `Ctrl+R`, `F5` | Reload | +| `Ctrl+D` | Recent downloads | +| `Ctrl+P` | Show the whole current URL, wrapped; again to hide | +| `F2` | Media mode on/off | +| `Ctrl+Y` | Copy URL, and show what was copied | | `Ctrl+plus/minus/0` | Zoom | -| `F1` | Key list | -| `F2` | Media mode (`Ctrl+click` sends videos to `player`, images to `image_viewer`) | -| `F6` | Redraw without reloading | | `F12` | Developer tools | -`--mod alt|super|meta` moves the modifier off Ctrl. browser-big adds -`Ctrl+Shift+M` (diagnostics page), `Ctrl+Shift+T` (web process threads), -`Ctrl+Shift+V` (video element state), `Ctrl+Shift+C` / `X` (warm / release -the camera). +Use `--mod alt|super|meta` to move the modifier off Ctrl. Nothing else is +intercepted, so copy/paste keeps working inside the page. -## Features +browser-big adds `Ctrl+Shift+M` (diagnostics page), `Ctrl+Shift+C` (warm the +camera), `Ctrl+Shift+X` (release it), `Ctrl+Shift+V` (dump video state). -- **History** survives restarts; back and forward continue into it. - `--private` keeps nothing. -- **Downloads**: `Ctrl+S` sets a directory per page, site or everything; - the narrowest rule wins. -- **Search keywords**: `g` (default), `d`, `w`, `s`. `w tree` searches - Wikipedia. Add one with `Ctrl+K`: `k https://example.org/?q={}`. -- **Lock**: `--lock-page` or `--lock-site` refuses other addresses. -- **Popups**: `--allow-popups` lets pages open windows without a click - (Zoom needs it). -- **Errors** show top right in red for 10 s, and on stderr. +## History -## Paths +Stored at: -`browser-mini --paths` prints them all. +``` +~/.local/share/wkview/<profile>/history.tsv +``` -| Path | What | -| --- | --- | -| `~/.config/swov/config` | Shared palette, read first | -| `~/.config/browser-{mini,big}/config` | Settings | -| `~/.local/share/wkview/<profile>/` | Cookies, history, search keywords, permissions | -| `~/.local/share/wkview/download-dirs.tsv` | Download rules (all profiles) | -| `~/.cache/wkview/<profile>/` | Cache, safe to delete | +One tab separated line per page: timestamp, URL, title. Only successful +loads of `http`, `https` and `file` are written, so every line is a link +that worked. Failed loads, error pages and internal pages are skipped. +Trimmed to the last 2000 lines at startup. -`--clear-data` wipes the profile. `--forget-permissions` drops camera and -microphone answers. +`--private` turns it off. The file is plain text — grep it, edit it, delete it. -## Config +One popup does both jobs, because they are the same job: an input line with +the matching history under it. + +- `Ctrl+O` types an address. The history is listed straight away — eight + lines by default, `list_rows` in a config file — and narrows as you type, + so the two are one thing: pick a line, or press `Enter` and what you typed + is loaded. +- `Ctrl+H` opens the same popup on the history. + +Every panel — address, history, find, download directory, key list — is the +same width: `popup_width`, 550px by default, which is about fifty +characters of the mono font — enough for most addresses without the panel +taking over the window. It shrinks to fit a small window rather than +clipping, and the history list takes up to about two thirds of the window's +height. + +``` +popup_width = 900 # in swov's config, or the browser's +``` + +`Tab` and `Down` walk forward through the matches, `Shift+Tab` and `Up` +back, the wheel scrolls, a click opens. Whatever is selected is written into +the input, so `Enter` always loads exactly what you can read. `Esc`, +`Ctrl+H` or a click outside closes it. + +Anything the popup can save shows a **Save** and a **Cancel** button with +the keys named on them (`Save (Enter)`, `Cancel (Esc)`) — Enter is never the +only way in. They are text, not boxes: Cancel sits at the left in `subtext`, +Save at the right in the accent and bold, and either underlines and +brightens on hover. When the directory popup asks about a clash the same two +buttons become `Drop that rule` and `Keep both`, so both answers are on +screen rather than implied. + +While a page loads, its address appears top left and becomes the page title +as soon as one arrives, with a thin accent line across the very top showing +progress. -`key = value`, `#` comments, colours as `RRGGBB[AA]`. The command line wins. -Every key is also an option: `--hl=ff8800`. `-c PATH` reads one file only, -`-n` reads none. +`Ctrl+A` selects all — in a popup's input, and in the page. WebKitGTK maps +that key to move-to-start-of-line, an Emacs habit that surprises anyone +typing into a web text field, so it is turned into WebKit's own select-all +command. `select_all = no` gives the key back to the page. + +`Esc` closes whatever is on screen — popup, key list, download list or a +message — and reaches the page only when nothing of ours is up. + +Back and forward walk WebKit's session list first, which keeps scroll +position and form state. Once that runs out they continue into the stored +history, so back still works on the first page after a restart. + +## Download directories + +Downloads land in `--download-dir` unless a rule says otherwise. `Ctrl+S` +sets one: type a directory, and the select decides how far it reaches — +this address only, everything on the site, or everything. The narrower rule +wins, so one page can go somewhere its site does not. + +``` +~/.local/share/wkview/download-dirs.tsv key <TAB> directory [<TAB> overwrite] +``` + +The file belongs to you, not to a profile: every profile and both browsers +read the same rules, since which directory a site's files go in has nothing +to do with which cookie jar is in use. Rules written by an older build are +carried over from the profile on first run. + +The key is a whole address or a bare host. The `Ctrl+S` popup lists everything +stored, so a directory can be picked with the arrows or the mouse instead +of typed, and the rule under the cursor dropped with `Delete` or `Ctrl+X`. The scopes are independent and the +narrower one always wins: a rule on `a.de/b` keeps its own directory when +`a.de` gets one. Setting a rule only ever writes the key for the scope you +picked. A rule set on a page also covers +the files that page hands out, which usually live on another address — a +CDN, or just a different path — so a rule on a download page catches what +it serves. + +Ticking **always replace existing files** turns the question off for +whatever that rule covers: files take the name they ask for. It follows the +same select, so it can apply to one page, a whole site, or everything — +`always_overwrite` in a config file does the last one permanently. + +If something else already points at the directory you set, you are asked +first: `Enter` drops the older rule, `Esc` lets both use it. Sharing a +directory is usually meant, but not always. + +Setting the scope to **everything** lasts for that run only — put +`download_dir` in a config file to make it permanent. + +Two browsers saving into one directory do not collide either: a name is +claimed by creating the file, so the second one moves to the next number +rather than landing on top of the first. Tested with two instances pulling +the same file into the same directory at once. + +Rules are merged on save, not overwritten: a second browser on the same +profile re-reads the file and keeps what the first one wrote, so two +instances cannot trample each other's rules. The same goes for search +keywords. + +A file whose name is already taken is downloaded beside the old one and the +question is asked in the download panel itself — no second popup. `Enter` +puts it in the old one's place, `Esc` keeps both, and the transfer only +reads as done once the file is where it is going to stay. A directory that does not exist +is created group writable (`0770`). The panel appears the moment the server answers, and reads `starting` until +the first byte lands. It normally fades out under the pointer so the page +can be read through it, but a download that has just appeared outranks that +for a few seconds — otherwise one starting while the pointer happened to +rest there would arrive invisibly. The panel shows the +destination in small text on the right of its header, so it is clear where +a file is going before it lands. + +## A config that needs no flags + +Everything a conferencing site has needed so far, as settled preferences: + +``` +# ~/.config/browser-big/config +audio = alsa # plain-ALSA machine, no sound server +web_compat = yes # screen.orientation for Zoom's media engine +feature = OffscreenCanvas=off # no worker-side WebGL attempt, so no CPU fallback +``` -## Patches +Two things that used to be here are now the default, because they are safe +on every machine and match what other browsers do: GTK's **GL renderer** +(`gsk = auto` hands the choice back to GTK) and **SharedArrayBuffer** +(`shared_array_buffer = no` to disable; WebKit still hands it only to +cross-origin-isolated pages). The three above stay in the config because +they are specific to the machine or the site. -Video calls need three upstream fixes. They are real bugs in current -releases, not configuration, and none is upstream yet. +`audio` and `web_compat` are browser-big's; browser-mini notes them as +unknown and carries on. The other three apply to both. -| File | Applies to | Without it | +## A test ladder that isolates each layer + +The WebRTC samples at `webrtc.github.io/samples` are the reference +implementation: unobfuscated, one thing per page, and they print what +they got. Each rung adds one layer, so the first one that fails names it. + +| Page | Tests | Flags to try if it fails | | --- | --- | --- | -| `webkit-videorate-skip-to-first.patch` | WebKitGTK 2.52 / 2.54 | Own camera freezes or goes black, hundreds of frames per second, CPU pinned (WebKit PR 74373) | -| `webkit-caps-normalize.patch` | WebKitGTK 2.54 | Camera modes listed as `640 x {480, 360}` are ignored. Many laptop cameras get no 4:3 mode, so 16:9 with black bars | -| `gst-glupload-null-meta.patch` | GStreamer 1.28 (gst-plugins-base) | Web process crashes on a site's second camera request | +| `src/content/getusermedia/gum/` | camera only, no audio, no WebRTC | `--rtc-trace` to see it | +| `src/content/getusermedia/audio/` | microphone only | `--audio-alsa` | +| `src/content/getusermedia/resolution/` | asks for QVGA, VGA, HD by size, some `exact` | `--cam-scale` | +| `src/content/getusermedia/record/` | MediaRecorder from the camera | — | +| `src/content/peerconnection/pc1/` | **a full WebRTC call inside one page**, no server | `--fix-webrtc`, `--video-codecs VP8` | +| `src/content/peerconnection/constraints/` | codec and bitrate negotiation | `--video-codecs` | -Apply in the source tree, then rebuild and reinstall that package: +`pc1` is the important one: two peer connections in one page, offer, +answer, ICE, media flowing — every part of the WebRTC stack with no site +logic in the way. If it works, the browser's WebRTC is sound and anything a +real site does differently is the site's own code. ``` -cd webkitgtk-2.54.0 -patch -p1 < /path/to/patches/webkit-videorate-skip-to-first.patch -patch -p1 < /path/to/patches/webkit-caps-normalize.patch +browser-big https://webrtc.github.io/samples/src/content/getusermedia/gum/ --rtc-trace +browser-big https://webrtc.github.io/samples/src/content/peerconnection/pc1/ --rtc-trace +``` -cd gstreamer-1.28.x/subprojects/gst-plugins-base # or the plain gst-plugins-base tarball -patch -p1 < /path/to/patches/gst-glupload-null-meta.patch +## Camera and microphone permission + +A page asking for the camera or microphone is **asked about**, the way a +browser does: a panel at the top names the site and what it wants, `Enter` +allows, `Esc` denies, and the answer is remembered per site in +`<profile>/permissions.tsv`. Device labels for `enumerateDevices()` are +revealed only once a site has been granted a device, which is also what +other browsers do. + +``` +--forget-permissions forget every remembered answer, so sites ask again +--allow-media grant every page without asking (a kiosk, a script) +--deny-media deny every page +media = ask|allow|deny the same, in the config ``` -Add `--dry-run` first to check. Without the GStreamer patch, -`WEBKIT_GST_DISABLE_GL_SINK=1` avoids the crash at some CPU cost. +Until 4.0.0 every request was granted silently. That was a shortcut, not a +policy, and it is gone. -Check that they work: +## When the camera is not picked up -- Videorate: the WebRTC sample `pc1` (below) encodes about 90 frames per - 3 s report, not hundreds. -- Caps: `--rtc-trace` shows a 4:3 camera track (`aspectRatio 1.333`) on a - site that asks for one. +browser-big probes the capture devices itself with GStreamer and logs what +it found. If that count is non-zero but the page still says access was +denied, the devices are fine — the web process could not reach them. It +gets at them through the desktop portal, and the portal needs a working +D-Bus. -## Video calls +The usual cause is a missing machine id, which makes D-Bus unable to start +at all: -browser-big repairs these WebKit problems by default: +A machine id is a D-Bus requirement, not a systemd one. Either tool works, +and neither needs an init system: -| Repair | Why | -| --- | --- | -| libnice ICE | librice finds no public address, so nothing connects past the LAN. `ice = rice` undoes it | -| VP8 first | WebKit encodes the first codec of its own offer and ignores the answer. `video_codecs = native` undoes it | -| Real SSRC and payload type | WebKit writes no `a=ssrc` lines. mediasoup sites get the values WebKit really sends | -| `RTCRtpSendParameters.codecs` | WebKit requires it; some libraries omit it | -| Camera without sound | A remote stream with silent audio is shown video-only, since WebKit's player waits for audio | -| Permission state | Answered from remembered permissions, so sites see device labels | -| Camera frame rate | WebKit only takes modes with the exact requested frame rate. A soft limit is dropped. `cam_size_fix = no` undoes it | +``` +sudo dbus-uuidgen --ensure=/etc/machine-id # any distro with dbus +sudo ln -sf /etc/machine-id /var/lib/dbus/machine-id +``` -`--no-fix-webrtc` turns off the SSRC, parameters and silent-audio repairs. -`--no-cam-fix` injects nothing. +Without dbus's tools at all, the file is just 32 hex characters: -**Permissions.** A request asks at the top of the window: `Enter` allows, -`Esc` denies, remembered per site. `media = ask|allow|deny` in the config. +``` +head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' | sudo tee /etc/machine-id +``` -**Per machine:** +Then a session bus. With no systemd user session, start one per run, or +better, start the compositor inside one so every app shares it: ``` -# ~/.config/browser-big/config -audio = alsa # no sound server -mic_match = ThinkPad # mic whose label contains this text -feature = OffscreenCanvas=off # Zoom -web_compat = yes # Zoom: screen.orientation +dbus-run-session -- browser-big URL # per run +dbus-run-session -- sway # whole session ``` -`mic_match` / `cam_match` pick a device by label. `mic_match` overrides the -microphone a site asks for. `arecord -l` lists microphones. +The portal itself is a separate package: `xdg-desktop-portal` plus a +backend — `xdg-desktop-portal-wlr` for sway, or `-gtk`. -## When a call does not work +browser-big checks the result rather than trusting the flag. A sandboxed +web process is placed in its own mount namespace by `bwrap`, so it compares +namespaces and says which it is: ``` -browser-big URL --rtc-trace 2>&1 | tee /tmp/bb.log +sandbox: web process 312, parent bwrap, mount namespace differs from ours -> sandboxed +sandbox: web process 370, parent browser-big, mount namespace same as ours -> NOT sandboxed ``` -| Line | Means | -| --- | --- | -| `sdp` | Each description, codecs as `payload-type name` | -| `ssrc-real`, `ssrc-ws`, `pt-fixed` | The server got the real SSRC and payload type | -| `rtc-stats` | Every 3 s: `out-*` sent, `far-*` server report, `in-*` received | -| `video-state` | Each `<video>`: size, frames shown | -| `video-audio-split` | A silent-audio stream shown video-only | -| `codec-mismatch` | We encode something the far end did not agree to | +Portal warnings can keep appearing either way — WebKit asks the portal for +settings and other things too, so they are not evidence of sandboxing. + +Or skip the portal entirely. `--no-sandbox` lets the web process open the +capture devices directly, which is the pragmatic answer on a machine with +no portal at all. It gives up the sandbox and prints a line saying so on +every run. + +To rule the sandbox out instead: + +``` +WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 browser-big URL +``` + +`--media-trace` logs every `getUserMedia` call, the constraints it asked +for and how it ended, **without changing any of them** — relaxing, caching, +retrying and the audio fallback are all off. That is the first thing to +reach for when a site claims access was denied while the devices look fine: +it shows which call failed and what it wanted. + +If permission is granted and no device ever reports capturing, browser-big +says so after a few seconds, with what to try next. That works on a plain +run — it watches the capture state rather than needing the `--cam-fix` +shim. + +browser-big prints this advice itself the first time a getUserMedia call +fails, and with `--cam-fix` it stops relaxing constraints once it sees the +requested kind has no device at all — loosening a constraint cannot conjure +a camera. -- `out-video.enc` climbing, no `far-video`: the server drops our stream. -- `in-video.dec` climbing, `video-state.frames` at 0: decoded, never shown. -- `in-video.fir` climbing, `keys` still: no keyframe arrives. +**Finding the microphone.** `--prewarm` now separates real microphones +from PulseAudio monitors — a monitor records what is being played, so +`getUserMedia` cannot use it, and a log line listing only +`Monitor of Dummy Output` means no microphone was found. What WebKit sees +is exactly what this prints: -**Without a server:** -`browser-big https://webrtc.github.io/samples/src/content/peerconnection/pc1/ --rtc-trace`. -If that works, WebKit's WebRTC is fine and the problem is site-specific. +``` +gst-device-monitor-1.0 Audio/Source +arecord -l # does ALSA see a capture device? +pactl info # is a Pulse server running, and did it get the card? +``` + +If ALSA has a capture device but GStreamer lists none, a half-working Pulse +server is usually in the way. -**Camera:** `browser-big --list-cameras` shows every mode each camera -offers. Check `ls -l /dev/video*` and membership in group `video`. +`--audio-alsa` ranks the pulse *elements* out, so playback and recording go +through ALSA. That fixes browser audio on a machine whose Pulse server came +up with a dummy sink. -**Crash:** attach gdb before starting the camera: +It does **not** change enumeration. `GST_PLUGIN_FEATURE_RANK` is honoured +for elements but silently ignored for device providers — measured, not +assumed — so which devices exist is still decided by the providers that are +installed. A microphone therefore needs `alsadeviceprovider`: ``` -gdb -p $(pgrep -nx WebKitWebProcess) -batch \ - -ex 'handle SIGUSR1 SIGUSR2 SIGPIPE SIG34 SIG35 nostop noprint pass' \ - -ex continue -ex 'thread apply all bt 30' > /tmp/bt.txt 2>&1 +gst-inspect-1.0 alsadeviceprovider # part of gst-plugins-base's alsa plugin +gst-device-monitor-1.0 Audio/Source # what WebKit will see ``` -## browser-big options +`--prewarm` reports whether that provider is present. `--gst-rank SPEC` +sets the variable by hand. + +To have it every run, put it in browser-big's config rather than typing the +flag: + +``` +audio = alsa # or pulse, or auto (the default) +``` + +It is deliberately **not** the built-in default. On a machine where +PipeWire or PulseAudio is healthy, ranking `pulsesink` out makes WebKit +open the card through `alsasink` directly, which without a dmix setup takes +it exclusively and silences everything else. Whether that is right is a +property of the machine, so it belongs in that machine's config. +`--audio-pulse` undoes it for one run. + +**Seeing what the camera offers.** `--list-cameras` prints every format, +size and frame rate each device supports, with the aspect ratio worked out +for each, then exits. It runs before the window is created, so it works over +ssh and from a script: + +``` +browser-big --list-cameras -| Option | | +Integrated Camera: Integrated C /dev/video0 + media type format width height framerate aspect + image/jpeg - 1280 x 720 30/1 16:9 + video/x-raw YUY2 640 x 480 { 30/1, 15/1 } 4:3 + video/x-raw YUY2 1280 x 720 10/1 16:9 +``` + +The listing ends with the exact flag to pin one of them: + +``` +browser-big URL --cam-force 640x480@30 +``` + +That asks for 640x480 at 30fps whatever the page requests, and sets the +aspect ratio to match. It implies `--cam-fix`. + +**Wrong shape.** `--cam-fix` used to delete the page's `aspectRatio` while +relaxing, and drop width and height entirely at level 2 — so a site asking +for 4:3 could be handed 16:9. It now keeps the aspect ratio as an ideal, and +derives one from the requested width and height when it gives up on the +resolution. To insist regardless of what the page asks: + +``` +browser-big URL --cam-force 640x480@30 # 4:3 +``` + +It implies `--cam-fix`, and sets width, height and `aspectRatio` as ideals +so the request cannot become over-constrained. + +**`--cam-scale` is usually the answer.** WebKit does not scale a capture: +asked for 320x240 it picks the nearest native camera mode — 848x480, say — +and hands that over. Chrome scales. `--cam-scale` does the same, drawing +the camera into a canvas of the requested size and capturing that, so the +track really does report the size the page asked for: + +``` +browser-big URL --cam-scale +→ cam-scale-mode frame-callback fps 15 +→ cam-scaled from 1280x720 to 320x240 fps 15 +``` + +The scaling runs on the page's own thread, so it draws only when a frame +actually arrives (`requestVideoFrameCallback`) and the rate is capped at 15 +by default — a timer-driven 30fps redraw was enough to freeze a conference +app that was already software-rendering. `--cam-scale-fps N` moves the cap. + +A bare `width: 320` in a page's constraints is only a *preference*, and +WebKit may answer with a different size — 848x480 for a request of 320x240, +in one real case. A site that publishes at the size it asked for then has a +stream it cannot use. `--cam-exact 320x240@20` demands that size instead of +preferring it, and fails outright if the camera cannot deliver it, which is +at least an honest answer. + +**No microphone.** A site asking for camera *and* microphone fails outright +on a machine with no audio input, even though the camera is fine. With +`--cam-fix`, browser-big retries once for video alone rather than handing +the site nothing. `--no-cam-drop-audio` turns that off, since the site did +not ask for it. + +## When video or WebRTC does not work + +WebKit plays media and talks WebRTC through GStreamer, so a missing plugin +looks like a broken site. browser-big checks at startup and says nothing +unless something needed is absent: + +``` +gstreamer: these are missing, and pages will fail without them: + webrtcbin WebRTC (gst-plugins-bad) + avdec_h264 H.264 video (gst-libav) +``` + +WebRTC needs more than one plugin, which is why "install gst-plugins-bad" +often isn't enough: + +| Element | From | Needs | +| --- | --- | --- | +| `webrtcbin` | gst-plugins-bad | **libnice** at build time | +| `x264enc` **or** `openh264enc` | gst-plugins-ugly / -bad | H.264 **encoding**, for publishing | +| `nicesrc` / `nicesink` | libnice itself | built with GStreamer support | +| `srtpenc` | gst-plugins-bad | libsrtp2 | +| `dtlssrtpenc` | gst-plugins-bad | OpenSSL | +| `rtpbin` | gst-plugins-good | — | + +Two more come from `gst-plugins-rs` — `audiornnoise` (noise suppression) +and `rtpgccbwe` (RTP bandwidth estimation). Neither is required; WebKit +complains about each at the moment it wants it, which reads like a fault, +so browser-big lists them once up front as optional. + +`libgstwebrtc.so` links `libnice.so` directly, so if libnice is absent when +gst-plugins-bad is configured, meson drops the `webrtc` feature and the +build still succeeds — silently. Verify each with +`gst-inspect-1.0 <element>`, or just start browser-big and read its report. + +## browser-big: fix and debug flags + +Nothing below is on unless asked for. A default run injects no JavaScript, +probes no devices, starts no watchdogs and reloads nothing. + +**Look at what is happening** + +| Flag | | +| --- | --- | +| `--list-cameras` | every format, size, rate and aspect each camera offers, then exit | +| `--prewarm` | what GStreamer can enumerate from here, cameras and microphones counted | +| `--media-trace` | every `getUserMedia` call, the frame that made it, its constraints and outcome, nothing altered | +| `--media-watchdog` | reports a `<video>` whose playback has stopped advancing, and tries to recover it | +| `--media-debug` | the above plus capture state, process table and GStreamer environment | +| `--gst-debug SPEC` | GStreamer's own logging, e.g. `v4l2*:6,webrtc*:5` | + +`Ctrl+Shift+T` lists every thread of the web process with its state (`R` +running, `S` sleeping, `D` blocked in the kernel) and the kernel function +it is waiting in. It reads `/proc` and does not ask the web process +anything, so it works precisely when the page is frozen and the log has +gone quiet — a thread in `D` in `snd_pcm_*` is the sound card, one in a +pipe write is stderr, one spinning `R` is JavaScript or WebAssembly. + +`Ctrl+Shift+V` dumps the state of every `<video>` on the page — readyState, +paused, currentTime, dimensions — which answers "is it stalled or is it +empty" directly. `Ctrl+Shift+M` opens the built-in diagnostics page. + +**Change what is happening** + +| Flag | | | --- | --- | -| `--rtc-trace` | Every WebRTC step, stats, video elements | -| `--media-trace` | Every `getUserMedia` call and result | -| `--media-debug` | Verbose media, permission and capture logs | -| `--list-cameras` | Camera modes, then exit | -| `--gst-debug SPEC` | GStreamer logging, e.g. `webrtc*:5` | -| `--gst-rank SPEC` | Append to `GST_PLUGIN_FEATURE_RANK` | -| `--cam-share` | A repeated request gets the camera already open | -| `--no-mic` | Camera only | -| `--audio-alsa` | ALSA instead of PulseAudio | -| `--no-hw-decode`, `--no-va` | Software decoding / no VA-API | +| `--cam-force WxH@FPS` | pin size, rate and aspect whatever the page asks | +| `--cam-share` | hand a repeated request the stream already open, nothing else | +| `--cam-fix` | relax tight constraints, retry looser, hold the stream | +| `--fix-media` | `--cam-fix` plus prewarm, media watchdog and auto-reload | +| `--audio-alsa` | play and record through ALSA rather than PulseAudio | +| `--no-sandbox` | let the web process reach devices without the portal | +| `--no-gpu`, `--no-dmabuf`, `--no-compositing`, `--no-jit` | rendering and JIT fallbacks | + +## browser-big's media workarounds are opt-in + +By default browser-big is plain WebKitGTK — no shim over `getUserMedia`, no +device probe, no watchdogs, no reloads of its own. The fixes exist for sites +that misbehave and are reached for deliberately: + +| Flag | What it does | +| --- | --- | +| `--fix-media` | all four below | +| `--cam-fix` | relax tight `getUserMedia` constraints, retry looser, hold the stream | +| `--prewarm` | probe capture devices at startup | +| `--media-watchdog` | notice a stalled `<video>` and recover it | +| `--auto-reload` | reload a page that never commits | +| `--warm-cam` | open and release the camera before the first page | + +`--no-cam-relax` and `--no-cam-keepalive` trim `--cam-fix` when it is on. + +Only the camera flags (`--cam-fix`, `--cam-share`, `--fix-media`, +`cam_share = yes`) change the camera. `--rtc-trace`, `--web-compat`, +`--no-mic` and the offer fixes use the same injected script but pass the +page's tracks through untouched. The log's `shim` line says `repair:true` +when a camera repair is on. + +Before 4.14.0 they did not: any of those flags also cloned and held the +capture track. On WebKitGTK 2.52 the page then gets a clone that freezes +after about a second. + +## Search keywords + +Four are built in, and `g` is the default: + +| Key | Searches | +| --- | --- | +| `g` | Google | +| `d` | DuckDuckGo | +| `w` | Wikipedia | +| `s` | the SDL3 wiki (through DuckDuckGo; the wiki has no search address) | + +`w tree` searches Wikipedia; a bare `tree` goes to the default. A line that +looks like an address (a scheme, a path or a dotted host) is opened, not +searched. A bare `s` with nothing after it is an address too. + +`Ctrl+K` lists the keywords and marks the default. Type +`k https://example.org/?q={}` to add one; `{}` is where the words go, +percent encoded. Tick the box to make it the default; `Delete` or `Ctrl+X` +drops the one under the cursor. `--paths` lists them all. + +Keywords live in `searches.tsv` next to the profile. One there, or in a +config file, replaces a built-in of the same name: + +``` +search_s = https://wiki.libsdl.org/SDL3/{} +search_default = d +``` + +A deleted built-in comes back on the next start; replace it instead. -## When a page renders badly +## When a page renders blank or zero sized -Try one at a time; the first that helps names the layer: +If the log says `web process crashed` or JavaScriptCore prints +`received NeedDebuggerBreak trap`, the page never had a chance — the web +process died, which is a WebKit or driver problem rather than anything the +browser did. Narrow it in this order: ``` -browser-mini URL --no-jit # JavaScript JIT -browser-mini URL --no-dmabuf # WebKit's dmabuf renderer +browser-mini URL --no-jit # JavaScriptCore's JIT +browser-mini URL --no-gpu # accelerated compositing +WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 browser-mini URL # the sandbox +``` + +The first one that helps names the culprit. A mismatched WebKit and +JavaScriptCore after a partial upgrade is the usual cause of a JIT trap. + +Two other causes were found in the loading bar and fixed in 2.3.3. It reported a +negative minimum width, which makes GTK abandon the allocation pass and +leaves the web view unallocated — a zero-sized viewport and a white window +you can scroll forever. It also changed size on every progress tick, which +re-allocated the overlay and so re-laid-out the page dozens of times a +second: black and white flicker in time with the page's own activity. The +bar is drawn with cairo now, at a constant size, so a change repaints and +nothing more. `--no-load-bar` turns it off. + +Otherwise it is usually + +Usually a broken GL or compositing stack rather than the page. In devtools +a `body` of `0px x 0px` means the viewport itself has no size, so nothing +the page's CSS says can help. Try these in order, both browsers: + +``` +browser-mini URL --no-dmabuf browser-mini URL --no-compositing browser-mini URL --no-gpu -browser-mini URL --gsk cairo # GTK renderer (default gl) ``` -`--feature NAME[=on|off]` sets a WebKit runtime feature, `--list-features` -lists them. `--gl-info` checks the graphics stack. +`--no-hw-decode` is the same idea for video. Any of them can go in a config +file once you know which one it was. + +## Options worth knowing + +| Option | Effect | +| --- | --- | +| `--profile NAME` | Named profile, persists cookies | +| `--private` | Ephemeral session, no history | +| `--clear-data` | Wipe the profile before starting | +| `--paths` | Print every directory and file written, then exit | +| `--player CMD` | start in media mode (`F2`), videos go to CMD | +| `--enable-middle-click-paste` | Let middle clicks reach the page (swallowed by default) | +| `--app-id ID` | Wayland `app_id` / X11 `WM_CLASS`, for sway rules | +| `--css FILE` | Inject a user stylesheet | +| `--ua-chrome`, `--ua-firefox`, … | Preset user agents | + +## Where things appear + +One rule, so there is nothing to learn: + +| | | +| --- | --- | +| things you type into | top, centred — the popup and the key list | +| things that just tell you something | top right — messages and downloads | +| where you are going | bottom left — the address being opened | + +Corners touching an edge stay square. Panels you can't interact with fade +out while the pointer is over them, so you can read what's underneath; +interactive ones don't. + +## Look + +Every overlay — the popup, toast, downloads — is drawn from +swov's palette and geometry, so the two programs read as one set. Defaults +match swov value for value: `tile` panels at `radius 14` with a `border 3` +outline, `text` on top, `hint` for headers, `hl` (the orange) for the caret, +the download bar and, at a third of its weight, for selections, and `find_hl` +(a pale blue) for the match `Ctrl+F` is sitting on, `urgent` for failures. + +Nothing is hardcoded. `ui_css_install()` builds the stylesheet from the +theme, so changing `hl` in a config file moves every accent at once. + +Panels hug the window edge rather than curve away from it: only the corners +facing into the page are rounded, and the border on the touching side is +dropped. The toast and the download list sit flush in the top right with one +rounded corner; the popup hangs off the top edge, centered, with two; the +address label sits in the top left with one; the key list floats clear in +the middle with four. + +Every overlay is placed by hand from the window's real size, so a narrow or +short window shrinks them instead of clipping them. Tested down to 240x180. + +## Config + +Same format and the same key names as swov: `key = value`, `#` or `;` +comments, colours as `RRGGBB` or `RRGGBBAA`. + +Files are read in this order, later wins, command line on top of both: + +``` +${XDG_CONFIG_HOME:-~/.config}/swov/config shared palette +${XDG_CONFIG_HOME:-~/.config}/browser-mini/config our own settings +``` + +So the palette lives once in swov's file and drives all three programs. +swov's file is full of keys for swov, so unknown keys there are counted and +ignored quietly — one line, not forty. An unknown key in the browser's own +file, or on the command line, is named, because there it means a typo. + +| Option | Effect | +| --- | --- | +| `-c PATH` | Read this file instead of the chain | +| `-n` | Ignore the config files | +| `-s KEY=VAL` | Set one key. `--KEY=VAL` and bare `KEY=VAL` also work | + +Every key is also a command line option: + +``` +browser-mini https://example.com --hl=ff8800 -s radius=6 ui_scale=1.2 +``` + +**Look keys** (shared with swov): `bg tile tile_sel tile_hover card +card_hover text subtext dim accent hl hltext hint urgent outline find_hl +start_bg radius border pad gap win_gap ui_scale font font_mono label_px +title_px hint_px` + +**Browser keys**: `title app_id zoom mod clip_cmd player image_viewer media_mode player_match download_dir profile +private no_media page_title middle_click_paste css user_agent quiet` + +`font` is empty by default, meaning the GTK theme font. `font_mono` is +browser-only and covers URLs and file names, where fixed width earns its +place. + +## Notes + +- Profile directories keep the historic name `wkview`. Renaming it would + orphan existing cookie jars. +- Middle-click paste is off by default: the press is claimed in the capture + phase, since WebKitGTK has no setting for it. `--enable-middle-click-paste` + puts it back. +- Downloads appear the moment one starts, in aligned columns: name on the + left, percentage / size / ETA right aligned, the bar underneath. diff --git a/browser-big b/browser-big new file mode 100755 index 0000000..48d7839 Binary files /dev/null and b/browser-big differ diff --git a/browser-big.c b/browser-big.c index 937e034..90147cf 100644 --- a/browser-big.c +++ b/browser-big.c @@ -42,7 +42,7 @@ static gboolean g_capture_started; /* something actually began capturing */ static guint g_capture_watch; /* camera / media behaviour */ -static gboolean g_cam_fix = TRUE; /* install the shim (--no-cam-fix: nothing) */ +static gboolean g_cam_fix; /* install the getUserMedia shim */ /* * The shim carries more than camera repairs: --rtc-trace, --web-compat, * --no-mic and the offer fixes all ride in it. Installing it must not @@ -58,17 +58,18 @@ static int g_cam_retries = 3; static int g_cam_max_w = 1280; static int g_cam_max_h = 720; static int g_cam_max_fps = 30; +static int g_cam_force_w, g_cam_force_h, g_cam_force_fps; +static gboolean g_cam_force_exact; static gboolean g_no_mic; /* --no-mic diagnostic */ static gboolean g_web_compat; /* fill in APIs WebKit lacks */ -static gboolean g_rtc_params_fix = TRUE; /* fill in the required codecs */ +static gboolean g_cam_scale; +static int g_cam_scale_fps = 15; /* ceiling for the scaled output */ /* give the page the size it asked */ +static gboolean g_rtc_params_fix; /* fill in the required codecs */ static gboolean g_rtc_trace; /* report each WebRTC step */ -static gboolean g_sdp_ssrc_fix = TRUE; /* a=ssrc lines, real SSRC and pt */ -static gboolean g_silent_split = TRUE; /* play video while its audio is silent */ -static gboolean g_fps_loose = TRUE; /* cam_size_fix: let the size pick the mode */ +static gboolean g_sdp_ssrc_fix; /* add the missing a=ssrc cname */ static char *g_video_codecs; /* preferred order, e.g. "VP8" */ -static gboolean g_codecs_set; /* given explicitly; unset = VP8 */ +static int g_cam_hold_ms = 3000; /* how long a shared capture lingers */ static char *g_cam_match; /* prefer this camera label */ -static char *g_mic_match; /* prefer this microphone label */ static gboolean g_prewarm; /* probe capture devices at startup */ static gboolean g_warm_cam; /* open the camera before page one */ static int g_warm_timeout = 10; @@ -88,8 +89,6 @@ static gboolean g_first_load_done; static gboolean g_no_webrtc, g_no_mediastream; static const char *g_gst_debug, *g_gst_dbgfile, *g_webkit_dbg; static char *g_gst_rank; /* GST_PLUGIN_FEATURE_RANK, accumulated */ -static gboolean g_audio_alsa; /* audio = alsa / --audio-alsa */ -static gboolean g_ice_libnice = TRUE; /* ice = rice / --ice-rice to undo */ static void gst_rank_add (const char *spec) @@ -124,8 +123,6 @@ typedef struct { static void load_diag_page (WebKitWebView *view); static gboolean g_list_cameras; static void cam_list (void); -static void set_video_codecs (const char *v); -static void set_fix_webrtc (gboolean on); /* Wall-clock stamp, so the UI process, the web process and GStreamer logs * can be lined up against each other. */ @@ -158,53 +155,6 @@ mlog (const char *fmt, ...) g_free (msg); } -/* - * GStreamer 1.28 starts the device providers asynchronously: - * gst_device_monitor_start() returns before any provider has probed, and - * GST_MESSAGE_DEVICE_MONITOR_STARTED is posted on the monitor's bus once - * the initial device list exists. Asking for the devices before that - * message yields an empty or partial list. WebKit waits for it, so we do - * too - with a deadline, since we are a diagnostic and must not hang. - */ -#if !GST_CHECK_VERSION (1, 28, 0) -#define GST_MESSAGE_DEVICE_MONITOR_STARTED ((GstMessageType) (GST_MESSAGE_EXTENDED + 9)) -#endif - -static gboolean -device_monitor_wait_started (GstDeviceMonitor *mon, guint timeout_ms) -{ - guint maj = 0, min = 0, mic = 0, nano = 0; - gst_version (&maj, &min, &mic, &nano); - if (maj < 1 || (maj == 1 && min < 28)) - return TRUE; /* older: providers started synchronously */ - - GstBus *bus = gst_device_monitor_get_bus (mon); - gint64 deadline = g_get_monotonic_time () + (gint64) timeout_ms * 1000; - gboolean started = FALSE; - - for (;;) { - gint64 left = deadline - g_get_monotonic_time (); - if (left <= 0) - break; - GstMessage *m = gst_bus_timed_pop_filtered (bus, (GstClockTime) left * 1000, - GST_MESSAGE_EXTENDED); - if (!m) - break; - if (GST_MESSAGE_TYPE (m) == GST_MESSAGE_DEVICE_MONITOR_STARTED) { - const GstStructure *st = gst_message_get_structure (m); - gboolean ok = TRUE; - if (st) - gst_structure_get_boolean (st, "success", &ok); - started = ok; - gst_message_unref (m); - break; - } - gst_message_unref (m); - } - gst_object_unref (bus); - return started; -} - static gboolean is_diag_request (const char *s) { @@ -247,16 +197,13 @@ parse_cam_max (const char *spec) * 2. retries with progressively looser constraints instead of handing the * page a NotReadableError it will usually turn into a permanent failure. * - * 3. keeps the acquired tracks and hands a repeated request the same live - * track, counting the hand-outs so stop() only closes the device when - * the last user lets go (--cam-share). + * 3. keeps the acquired tracks and hands the page *clones*. The page can + * stop its clone (leave a call, switch view) without closing the device, + * so the next getUserMedia() resolves in milliseconds instead of seconds. * * 4. watches <video>/<audio> for playback that has stopped advancing and * tries play() -> seek -> load() -> reload before giving up. * - * 5. carries the WebRTC repairs (SDP, SSRC, payload type, codec order, - * silent-audio split) and the --rtc-trace instrumentation. - * * Configuration arrives as window.__bbCfg, built in main(). */ static const char *SHIM_JS = @@ -315,8 +262,8 @@ static const char *SHIM_JS = "if(!C.camfix||!md||!md.getUserMedia){post({ev:'shim',camfix:false});return;}" "var origGUM=md.getUserMedia.bind(md);" "var origEnum=md.enumerateDevices?md.enumerateDevices.bind(md):null;" -"var cachedV=null,cachedA=null,pinned=null,pinnedA=null,pinTried=false;" -"var lastDevices='';" +"var cachedV=null,cachedA=null,pinned=null,pinTried=false;" +"var served=[],lastServe=0,lastDevices='';" "function now(){return (window.performance&&performance.now)?performance.now():Date.now();}" "function safe(o){try{return JSON.parse(JSON.stringify(o));}catch(e){return String(o);}}" "function num(v){if(v==null)return null;if(typeof v==='number')return v;" @@ -351,37 +298,9 @@ static const char *SHIM_JS = " if(level>=2&&o.audio&&typeof o.audio==='object')o.audio=true;" " return o;" "}" +/* --cam-force: the size and shape are ours, whatever the page asked. */ /* --no-mic: the page gets no microphone, whatever it asked. A diagnostic: * if a freeze disappears, the audio path is the cause. */ -/* - * WebKit only considers camera modes whose frame rate list contains the - * requested rate exactly (RealtimeVideoCaptureSource::presetSupportsFrameRate). - * A page asking frameRate {max: 20} therefore skips a 320x240 mode that - * only runs at 30 and lands on the one 16:9 mode that lists 20 - - * letterboxed instead of the 4:3 it asked for. Chrome takes the native mode - * and drops frames. A soft limit (ideal or max) is left out here so the - * size decides; exact and min are kept. (The 4:3 modes also need the WebKit - * caps-normalize patch, see HANDOVER.md.) - */ -"function looseFps(c){" -" if(!C.fpsLoose||!c||!c.video||typeof c.video!=='object'||c.video.frameRate==null)return c;" -" var f=c.video.frameRate;" -" if(typeof f==='object'&&(f.exact!=null||f.min!=null))return c;" -" var o={},nv={},k;" -" for(k in c)o[k]=c[k];" -" for(k in c.video)if(k!=='frameRate')nv[k]=c.video[k];" -" o.video=nv;" -" post({ev:'gum-fps-loosened',was:f});" -" return o;" -"}" -"if(C.fpsLoose&&window.MediaStreamTrack&&MediaStreamTrack.prototype.applyConstraints){(function(){" -" var oAC=MediaStreamTrack.prototype.applyConstraints;" -" MediaStreamTrack.prototype.applyConstraints=function(c){" -" if(this.kind==='video'&&c&&typeof c==='object'&&c.frameRate!=null)" -" c=looseFps({video:c}).video;" -" return oAC.call(this,c);" -" };" -"})();}" "function withoutMic(c){" " if(!C.noMic||!c||!c.audio)return c;" " var o={},k;for(k in c)o[k]=c[k];" @@ -390,43 +309,38 @@ static const char *SHIM_JS = " post({ev:'gum-nomic'});" " return o;" "}" -"function pinKind(o,kind,id){" -" if(!id||!o[kind])return;" -" var v=(o[kind]===true||typeof o[kind]!=='object')?{}:o[kind];" -" var nv={},k;for(k in v)nv[k]=v[k];" -" if(kind==='audio'&&C.micMatch)nv.deviceId={exact:id};" -" else if(!nv.deviceId)nv.deviceId={ideal:id};" -" o[kind]=nv;" +"function withForce(c){" +" if(!C.forceW||!c||!c.video)return c;" +" var o={},k;for(k in c)o[k]=c[k];" +" var v=(o.video===true||typeof o.video!=='object')?{}:o.video;" +" var nv={};for(k in v)nv[k]=v[k];" +" var k2=C.forceExact?'exact':'ideal',w={},h={},ar={},fr={};" +" w[k2]=C.forceW;h[k2]=C.forceH;ar[k2]=C.forceW/C.forceH;" +" nv.width=w;nv.height=h;nv.aspectRatio=ar;" +" if(C.forceFps){fr[k2]=C.forceFps;nv.frameRate=fr;}" +" o.video=nv;return o;" "}" "function withPin(c){" -" if((!pinned&&!pinnedA)||!c)return c;" +" if(!pinned||!c||!c.video)return c;" " var o={},k;for(k in c)o[k]=c[k];" -" pinKind(o,'video',pinned);pinKind(o,'audio',pinnedA);" -" return o;" +" var v=(o.video===true||typeof o.video!=='object')?{}:o.video;" +" var nv={};for(k in v)nv[k]=v[k];" +" if(!nv.deviceId)nv.deviceId={ideal:pinned};" +" o.video=nv;return o;" "}" "function pinDevice(){" -" if(pinTried||(!C.match&&!C.micMatch)||!origEnum)return Promise.resolve(null);" +" if(pinTried||!C.match||!origEnum)return Promise.resolve(null);" +" pinTried=true;" " return origEnum().then(function(l){" -/* Before the first grant in a document the labels are empty, so there is - * nothing to match yet: try again once a stream has been handed out. */ -" if(!l.some(function(d){return d.label;}))return null;" -" pinTried=true;" -" function find(kind,m){m=m.toLowerCase();" -" for(var i=0;i<l.length;i++)" -" if(l[i].kind===kind&&(l[i].label||'').toLowerCase().indexOf(m)>=0)return l[i].deviceId;" -" return null;}" -" if(C.match){pinned=find('videoinput',C.match);" -" post({ev:'cam-pin',match:C.match,found:!!pinned});}" -" if(C.micMatch){pinnedA=find('audioinput',C.micMatch);" -" post({ev:'mic-pin',match:C.micMatch,found:!!pinnedA," -" have:l.filter(function(d){return d.kind==='audioinput';}).map(function(d){return d.label;})});}" -" return pinned;" +" var m=C.match.toLowerCase();" +" for(var i=0;i<l.length;i++)" +" if(l[i].kind==='videoinput'&&(l[i].label||'').toLowerCase().indexOf(m)>=0){pinned=l[i].deviceId;break;}" +" post({ev:'cam-pin',match:C.match,found:!!pinned});return pinned;" " }).catch(function(){return null;});" "}" "function dumpTrack(t){" " var s={};try{s=t.getSettings?t.getSettings():{};}catch(e){}" -" post({ev:'track',kind:t.kind,id:t.id,label:t.label," -" readyState:t.readyState,muted:!!t.muted,settings:s});" +" post({ev:'track',kind:t.kind,label:t.label,readyState:t.readyState,settings:s});" "}" "function wants(c,k){return !!(c&&c[k]);}" "function deviceOk(track,c){" @@ -435,54 +349,138 @@ static const char *SHIM_JS = " if(typeof want!=='string')return true;" " try{return track.getSettings().deviceId===want;}catch(e){return true;}" "}" -/* - * Handing the page the same track it already has, rather than a clone. - * A clone is an independent track over one capture in the specification; - * on WebKitGTK 2.52 it goes black about a second after it is handed over - * and the encoder never sees a frame. The track itself does not. - * - * The one thing a clone bought was an independent lifetime, so the page - * dropping its first stream did not close the capture. That is kept by - * counting the hand-outs and swallowing stop() until the last one. - */ -"var shareRefs=0,realStop=null;" -"function shareTrack(t){" -" if(!t)return t;" -" if(!realStop){" -" realStop=t.stop.bind(t);" -" t.stop=function(){" -" shareRefs--;" -" if(shareRefs>0){post({ev:'cam-share-hold',refs:shareRefs});return;}" -" post({ev:'cam-share-stop'});realStop();" -" };" -" }" -" shareRefs++;" -" return t;" -"}" "function serve(c){" " if(!C.cache)return null;" " var out=new MediaStream(),ok=true;" " if(wants(c,'video')){" " if(cachedV&&cachedV.readyState==='live'&&deviceOk(cachedV,c)){" -" out.addTrack(shareTrack(cachedV));" +" var v=cachedV.clone();" +/* honour what the page asked for unless relaxing was requested */ +" if(typeof c.video==='object')try{" +" v.applyConstraints(C.relax?relaxVideo(c.video,1):c.video).catch(function(){});" +" }catch(e){}" +" out.addTrack(v);" " }else ok=false;" " }" " if(wants(c,'audio')){" -" if(cachedA&&cachedA.readyState==='live')" -" out.addTrack(cachedA);" -" else ok=false;" +" if(cachedA&&cachedA.readyState==='live')out.addTrack(cachedA.clone());else ok=false;" " }" " return (ok&&out.getTracks().length)?note(out):null;" "}" -"function note(st){return st;}" +/* Remember the clones handed to the page, so we can tell when they have + * all finished and the held source is no longer doing anybody any good. */ +"function note(st){" +" lastServe=now();" +" st.getTracks().forEach(function(t){served.push(t);});" +" return st;" +"}" +"function anyLive(){" +" served=served.filter(function(t){return t.readyState==='live';});" +" return served.length>0;" +"}" "function keep(st){" " if(!C.cache)return st;" " var v=st.getVideoTracks()[0],a=st.getAudioTracks()[0];" " if(v)cachedV=v;if(a)cachedA=a;" -" if(v)shareTrack(v);" -" return note(st);" +" var out=new MediaStream();" +" if(v)out.addTrack(v.clone());" +" if(a)out.addTrack(a.clone());" +" return out.getTracks().length?note(out):st;" "}" /* Which requested kind the browser has no device for, if any. */ +/* + * WebKit does not scale a capture: asked for 320x240 it picks the nearest + * native camera mode and hands that over instead, so a site that needs + * the size it asked for gets a stream it rejects. Chrome scales. This does + * the same, by drawing the camera into a canvas of the requested size and + * capturing that. + */ +"function scaleStream(st,c){" +" if(!C.scale||!st){return Promise.resolve(st);}" +" var wv=(c&&typeof c.video==='object')?c.video:null;" +" var wantW=wv?num(wv.width):null,wantH=wv?num(wv.height):null," +" wantF=wv?num(wv.frameRate):null;" +" var vt=st.getVideoTracks()[0];" +" if(!wantW||!wantH||!vt){" +" post({ev:'cam-scale-skip',why:!vt?'no video track':'no size asked'});" +" return Promise.resolve(st);}" +" var s={};try{s=vt.getSettings?vt.getSettings():{};}catch(e){}" +" if(s.width===wantW&&s.height===wantH){" +" post({ev:'cam-scale-skip',why:'already the right size'});" +" return Promise.resolve(st);}" +" try{" +" var v=document.createElement('video');" +" v.muted=true;v.defaultMuted=true;v.autoplay=true;v.playsInline=true;" +" v.setAttribute('playsinline','');" +/* It has to be in the document, and not display:none, or some engines + * never produce frames to draw from. Parked off-screen instead. */ +" v.style.cssText='position:fixed;left:-10000px;top:0;width:4px;height:4px;" +"opacity:0;pointer-events:none';" +" (document.body||document.documentElement).appendChild(v);" +" v.srcObject=new MediaStream([vt]);" +" var cv=document.createElement('canvas');" +" cv.width=wantW;cv.height=wantH;" +" var ctx=cv.getContext('2d');" +" if(!cv.captureStream){" +" post({ev:'cam-scale-error',why:'no captureStream'});" +" return Promise.resolve(st);}" +" var fps=Math.min(wantF||C.scaleFps||15,C.scaleFps||15);" +/* play() is fired and not awaited: if it never settles the page's own + * getUserMedia would hang forever, which is worse than a late first frame. */ +" try{var p=v.play();if(p&&p.catch)p.catch(function(){});}catch(e){}" +" return new Promise(function(resolve){" +" var done=false;" +" function go(){" +" if(done)return;done=true;" +/* + * Drawing on a timer burns CPU whether or not a new frame arrived, and + * this runs on the page's own main thread - enough to freeze a heavy + * conference app that is already software-rendering. requestVideoFrameCallback + * fires once per actual frame, which is both cheaper and in step. + */ +" var stopped=false;" +" function draw(){" +" try{if(v.videoWidth)ctx.drawImage(v,0,0,cv.width,cv.height);}catch(e){}" +" }" +" var timer=null;" +" if(v.requestVideoFrameCallback){" +" var last=0,minGap=1000/fps;" +" (function loop(now){" +" if(stopped)return;" +" if(!last||now-last>=minGap-2){last=now||0;draw();}" +" try{v.requestVideoFrameCallback(loop);}catch(e){stopped=true;}" +" })(0);" +" post({ev:'cam-scale-mode',mode:'frame-callback',fps:fps});" +" }else{" +" timer=setInterval(draw,Math.max(33,Math.round(1000/fps)));" +" post({ev:'cam-scale-mode',mode:'timer',fps:fps});" +" }" +" var out,nt;" +" try{out=cv.captureStream(fps);nt=out.getVideoTracks()[0];}catch(e){}" +" if(!nt){clearInterval(timer);" +" post({ev:'cam-scale-error',why:'captureStream gave no track'});" +" resolve(st);return;}" +" var oStop=nt.stop.bind(nt);" +" nt.stop=function(){stopped=true;if(timer)clearInterval(timer);" +" try{vt.stop();}catch(e){}" +" try{v.pause();v.srcObject=null;}catch(e){}" +" try{v.remove();}catch(e){}" +" oStop();};" +" var ms=new MediaStream();" +" ms.addTrack(nt);" +" st.getAudioTracks().forEach(function(a){ms.addTrack(a);});" +" ms.__bbScale={v:v,cv:cv,src:vt,timer:timer};" +" post({ev:'cam-scaled',from:{w:s.width||0,h:s.height||0}," +" to:{w:cv.width,h:cv.height},fps:fps});" +" resolve(ms);" +" }" +" v.addEventListener('loadedmetadata',go);" +" v.addEventListener('playing',go);" +" setTimeout(go,400);" /* never leave the page waiting */ +" });" +" }catch(e){post({ev:'cam-scale-error',why:String((e&&e.message)||e)});" +" return Promise.resolve(st);}" +"}" "function countKinds(c){" " if(!origEnum)return Promise.resolve(null);" " return origEnum().then(function(l){" @@ -501,47 +499,23 @@ static const char *SHIM_JS = " return n==='NotReadableError'||n==='AbortError'||n==='OverconstrainedError'||" " n==='TimeoutError'||n==='NotFoundError'||n==='TypeError'||!n;" "}" -"function fixMic(st,c){" -" if(!C.micMatch||!wants(c,'audio'))return Promise.resolve(st);" -" var asked=c.audio&&typeof c.audio==='object'&&c.audio.deviceId;" -" var at=st.getAudioTracks()[0];" -" if(asked||!at)return Promise.resolve(st);" -" return pinDevice().then(function(){" -" var have='';try{have=at.getSettings().deviceId;}catch(e){}" -" if(!pinnedA||have===pinnedA)return st;" -" return origGUM({audio:{deviceId:{exact:pinnedA}}}).then(function(s2){" -" var nt=s2.getAudioTracks()[0];" -" if(!nt)return st;" -" st.removeTrack(at);try{at.stop();}catch(e){}" -" st.addTrack(nt);" -" post({ev:'mic-swapped',from:at.label,to:nt.label});" -" return st;" -" },function(e){post({ev:'mic-swap-failed',name:e&&e.name});return st;});" -" });" -"}" "md.getUserMedia=function(c){" " var t0=now();" " post({ev:'gum-call',frame:location.href.slice(0,80),constraints:safe(c)});" " var hit=serve(c);" -" if(hit){" -" var ht=hit.getVideoTracks()[0];" -" post({ev:'gum-cache-hit',track:ht?ht.id:null," -" same:!!(ht&&cachedV&&ht===cachedV)});" -" return Promise.resolve(hit);}" +" if(hit){post({ev:'gum-cache-hit'});return scaleStream(hit,c);}" " return pinDevice().then(function(){" " var start=C.relax?1:0,levels=[],l;" " for(l=start;l<=3&&levels.length<=C.retries;l++)levels.push(l);" " if(!levels.length)levels=[start];" " var i=0,audioDropped=false;" " function attempt(){" -" var cc=looseFps(withoutMic(withPin(relax(c,levels[i]))));" +" var cc=withoutMic(withForce(withPin(relax(c,levels[i]))));" " post({ev:'gum-try',level:levels[i],constraints:safe(cc)});" " return origGUM(cc).then(function(st){" " post({ev:'gum-ok',ms:Math.round(now()-t0),level:levels[i]});" -" return fixMic(st,c).then(function(st){" -" st.getTracks().forEach(dumpTrack);" -" return keep(st);" -" });" +" st.getTracks().forEach(dumpTrack);" +" return scaleStream(keep(st),c);" " }).catch(function(err){" " post({ev:'gum-error',name:err&&err.name,message:err&&err.message," " constraint:err&&err.constraint,level:levels[i],ms:Math.round(now()-t0)});" @@ -582,16 +556,25 @@ static const char *SHIM_JS = " return l;});};" "window.__bbWarm=function(){" " return md.getUserMedia({video:true}).then(function(s){" -" s.getTracks().forEach(function(t){t.stop();});" /* drops our share only */ +" s.getTracks().forEach(function(t){t.stop();});" /* stops the clone only */ " post({ev:'warm-ok'});return true;" " }).catch(function(e){post({ev:'warm-error',name:e&&e.name,message:e&&e.message});return false;});" "};" "window.__bbRelease=function(){" -" shareRefs=0;" -" if(realStop)try{realStop();}catch(e){}" " [cachedV,cachedA].forEach(function(t){if(t)try{t.stop();}catch(e){}});" " cachedV=cachedA=null;post({ev:'released'});return true;" "};" +/* + * Holding the capture open is the whole point of sharing, but holding it + * after the page has stopped every clone leaves the camera light on with + * nothing watching. So the hold expires once no handed-out track is live. + */ +"if(C.cache&&C.holdMs>0)setInterval(function(){" +" if(!cachedV&&!cachedA)return;" +" if(anyLive()){lastServe=now();return;}" +" if(now()-lastServe<C.holdMs)return;" +" window.__bbRelease();post({ev:'cam-hold-expired'});" +"},1000);" /* * WebKitGTK's WebRTC is GStreamer-based and its offers differ from the * ones a site tested against Chrome. Two of those differences stop a @@ -622,85 +605,34 @@ static const char *SHIM_JS = " if(changed)post({ev:'sdp-ssrc-added'});" " return parts.join('');" "}" -/* - * What was actually negotiated, from the SDP itself: per media section - * its direction, any bandwidth ceiling the far end asked for, and the - * codecs in the order they were offered or accepted. When a stream is - * sent and the far end shows nothing, this says whether the two sides - * even agreed on a codec, and whether a limit was requested that we are - * ignoring. - */ -"function sdpSummary(sdp){" -" if(!sdp)return null;" -" var out=[],cur=null;" -" sdp.split(/\\r?\\n/).forEach(function(l){" -" var m=/^m=(\\w+)\\s+\\d+\\s+\\S+\\s+(.*)$/.exec(l);" -" if(m){cur={kind:m[1],pts:m[2].split(/\\s+/),dir:'',bw:'',names:{}};out.push(cur);return;}" -" if(!cur)return;" -" if(/^a=(sendrecv|sendonly|recvonly|inactive)/.test(l))cur.dir=l.slice(2);" -" else if(/^b=/.test(l))cur.bw=l.slice(2);" -" var r=/^a=rtpmap:(\\d+)\\s+([^/]+)/.exec(l);" -" if(r)cur.names[r[1]]=r[2];" -" });" -" return out.map(function(s){" -" return {kind:s.kind,dir:s.dir,bw:s.bw," -" codecs:s.pts.map(function(p){return p+' '+(s.names[p]||'?');})};" -" });" -"}" -"function firstVideo(sdp){" -" var s=sdpSummary(sdp)||[],i,j;" -" for(i=0;i<s.length;i++){if(s[i].kind!=='video')continue;" -" for(j=0;j<s[i].codecs.length;j++){" -" var n=s[i].codecs[j].split(' ')[1]||'';" -" if(!/^(rtx|red|ulpfec|flexfec-03|\\?)$/i.test(n))return n.toUpperCase();}}" -" return null;" -"}" -"function postSdp(which,sdp){" -" try{var sum=sdpSummary(sdp);if(sum)post({ev:'sdp',which:which,m:sum});}catch(e){}" -"}" "function codecPrefs(pc){" " if(!C.codecs||!C.codecs.length)return;" " try{" " var caps=window.RTCRtpSender&&RTCRtpSender.getCapabilities?" " RTCRtpSender.getCapabilities('video'):null;" " if(!caps||!caps.codecs)return;" -" var want=C.codecs.map(function(w){return w.toLowerCase().replace(/^video\\//,'');});" -" var front=[],rest=[];" +" var want=C.codecs,groups=[],rest=[],i,j;" +" for(i=0;i<want.length;i++)groups.push([]);" " caps.codecs.forEach(function(c){" -" var m=(c.mimeType||'').toLowerCase().replace(/^video\\//,'');" -" (want.indexOf(m)>=0?front:rest).push(c);});" -" front.sort(function(a,b){" -" return want.indexOf(a.mimeType.toLowerCase().replace(/^video\\//,''))-" -" want.indexOf(b.mimeType.toLowerCase().replace(/^video\\//,''));});" -" var flat=front.concat(rest);" -" if(!front.length)return;" +" var m=(c.mimeType||'').toLowerCase(),hit=-1;" +" for(j=0;j<want.length;j++){" +" var w=want[j].toLowerCase();" +" if(m===w||m==='video/'+w)hit=j;" +" }" +" if(hit>=0)groups[hit].push(c);else rest.push(c);" +" });" +" var flat=[];" +" groups.forEach(function(g){g.forEach(function(c){flat.push(c);});});" +" rest.forEach(function(c){flat.push(c);});" +" if(!flat.length)return;" " pc.getTransceivers().forEach(function(t){" -" if(t.setCodecPreferences&&t.sender&&t.sender.track&&t.sender.track.kind==='video')" +" if(t.setCodecPreferences&&t.sender&&t.sender.track&&" +" t.sender.track.kind==='video')" " try{t.setCodecPreferences(flat);}catch(e){}" " });" +" post({ev:'codec-pref',order:flat.slice(0,4).map(function(c){return c.mimeType;})});" " }catch(e){}" "}" -"function sdpPreferCodecs(sdp,want){" -" if(!sdp||!want||!want.length)return sdp;" -" var parts=sdp.split(/(?=\\r?\\nm=)/),changed=false,order=null;" -" for(var i=0;i<parts.length;i++){" -" var sec=parts[i],m=/(^|\\n)(m=video [^\\r\\n]*? )([0-9 ]+)(\\r?\\n|$)/.exec(sec);" -" if(!m||/a=(recvonly|inactive)/.test(sec))continue;" -" var pts=m[3].trim().split(/\\s+/),names={},re=/a=rtpmap:(\\d+) ([^\\/\\r\\n]+)/g,r;" -" while((r=re.exec(sec)))names[r[1]]=r[2].toLowerCase();" -" var front=[];" -" want.forEach(function(w){w=w.toLowerCase().replace(/^video\\//,'');" -" pts.forEach(function(pt){if(names[pt]===w&&front.indexOf(pt)<0)front.push(pt);});});" -" if(!front.length)continue;" -" var rest=pts.filter(function(pt){return front.indexOf(pt)<0;});" -" var np=front.concat(rest).join(' ');" -" if(np===pts.join(' '))continue;" -" parts[i]=sec.replace(m[0],m[1]+m[2]+np+m[4]);" -" changed=true;order=front.map(function(pt){return pt+' '+names[pt];});" -" }" -" if(changed)post({ev:'codec-pref',order:order});" -" return parts.join('');" -"}" /* * Where does a publish actually stop? The offer rewriting only helps if an * offer is ever made, so every step before it is reported too: the @@ -767,115 +699,13 @@ static const char *SHIM_JS = "if(C.rtcTrace){(function(){" " var O=window.RTCPeerConnection;" " if(!O){post({ev:'rtc-missing'});return;}" -" var pcs=[];" -/* - * Whether anything is actually going out. A connected ICE pair proves a - * path exists, not that the encoder ever produced a frame - and those two - * failures look identical from the outside: the other end shows a - * spinner. framesEncoded stays 0 when the camera track delivers nothing, - * bytesSent grows when it does. - */ -" var vlast=new WeakMap(),vids=[];" -" setInterval(function(){" -" var l=document.querySelectorAll('video');" -" for(var i=0;i<l.length;i++){var v=l[i];" -" if(!v.srcObject)continue;" -" if(vids.indexOf(v)<0)vids.push(v);" -" var q=null;try{q=v.getVideoPlaybackQuality?v.getVideoPlaybackQuality():null;}catch(e){}" -" var r=v.getBoundingClientRect(),st=getComputedStyle(v),vt=[];" -" try{vt=v.srcObject.getVideoTracks();}catch(e){}" -" var o={ev:'video-state',v:vids.indexOf(v),id:v.id||v.className||''," -" ready:v.readyState,paused:v.paused,w:v.videoWidth,h:v.videoHeight," -" ct:Math.round(v.currentTime||0),frames:q?q.totalVideoFrames:-1," -" dropped:q?q.droppedVideoFrames:-1," -" box:Math.round(r.width)+'x'+Math.round(r.height)," -" shown:st.display!=='none'&&st.visibility!=='hidden'&&+st.opacity>0," -" track:vt.length?(vt[0].id.slice(0,8)+' '+vt[0].readyState+(vt[0].muted?'/muted':'')):'none'};" -" var k=JSON.stringify(o);" -" if(vlast.get(v)===k)continue;" -" vlast.set(v,k);post(o);" -" }" -" },3000);" -" try{var SD=Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype,'srcObject');" -" if(SD&&SD.set)Object.defineProperty(HTMLMediaElement.prototype,'srcObject',{configurable:true," -" get:SD.get,set:function(v){" -" var vt=[];try{vt=v&&v.getVideoTracks?v.getVideoTracks():[];}catch(e){}" -" if(this.tagName==='VIDEO')post({ev:'video-attach',id:this.id||this.className||''," -" track:vt.length?vt[0].id.slice(0,8):(v?'no-video':'null')});" -" return SD.set.call(this,v);}});}catch(e){}" -" try{var HP=HTMLMediaElement.prototype,oPlay=HP.play;" -" HP.play=function(){var v=this,p=oPlay.apply(v,arguments);" -" if(p&&p.catch)p.catch(function(e){" -" post({ev:'play-rejected',name:e&&e.name,message:String((e&&e.message)||e)," -" srcObject:!!v.srcObject,muted:v.muted});});" -" return p;};}catch(e){}" -" var last={};" -" setInterval(function(){" -" pcs.forEach(function(pc,i){" -" if(!pc.getStats||pc.signalingState==='closed')return;" -" pc.getStats().then(function(r){" -" if(!r||!r.forEach)return;" -" var o={ev:'rtc-stats',pc:i},any=false,codecs={};" -/* The codec stats have to be collected first: an outbound-rtp only - * carries a codecId pointing at one of them. What is actually being - * encoded is the whole question when the far end shows nothing. */ -" r.forEach(function(s){" -" if(s.type==='codec')codecs[s.id]={mime:s.mimeType,pt:s.payloadType};" -" });" -" r.forEach(function(s){" -" var k=s.kind||s.mediaType||'?';" -" if(s.type==='outbound-rtp'){any=true;" -" var c=codecs[s.codecId]||{};" -" o['out-'+k]={ssrc:s.ssrc||0,bytes:s.bytesSent||0,enc:s.framesEncoded||0," -" sent:s.framesSent||0,pkts:s.packetsSent||0," -" w:s.frameWidth||0,h:s.frameHeight||0," -" codec:c.mime||null,pt:c.pt," -" pli:s.pliCount||0,nack:s.nackCount||0,fir:s.firCount||0," -" keys:s.keyFramesEncoded||0," -" limited:s.qualityLimitationReason||null," -" target:s.targetBitrate||0};}" -" else if(s.type==='inbound-rtp'){any=true;" -" o['in-'+k]={ssrc:s.ssrc||0,bytes:s.bytesReceived||0,dec:s.framesDecoded||0," -" keys:s.keyFramesDecoded||0,w:s.frameWidth||0,h:s.frameHeight||0," -" pkts:s.packetsReceived||0,lost:s.packetsLost||0," -" disc:s.packetsDiscarded||0," -" pli:s.pliCount||0,fir:s.firCount||0,nack:s.nackCount||0};}" -/* The far end's own receiver report, sent back over RTCP. Its presence - * proves our packets are arriving there; lost and rtt say in what state. */ -" else if(s.type==='remote-inbound-rtp'){any=true;" -" o['far-'+k]={lost:s.packetsLost,frac:s.fractionLost," -" jitter:s.jitter,rtt:s.roundTripTime};}" -" else if(s.type==='candidate-pair'&&(s.nominated||s.state==='succeeded')){" -" o.pair={state:s.state,sent:s.bytesSent||0,recv:s.bytesReceived||0};}" -" });" -" try{pc.getSenders().forEach(function(sn){" -" if(!sn.track)return;any=true;" -" var g={};try{g=sn.track.getSettings?sn.track.getSettings():{};}catch(e){}" -" o['track-'+sn.track.kind]={state:sn.track.readyState,muted:!!sn.track.muted," -" w:g.width||0,h:g.height||0};" -" });}catch(e){}" -" try{pc.getReceivers().forEach(function(rv){" -" if(!rv.track)return;any=true;" -" o['rtrack-'+rv.track.kind]={id:rv.track.id.slice(0,8),state:rv.track.readyState,muted:!!rv.track.muted};" -" });}catch(e){}" -" if(!any)return;" -" var key=JSON.stringify(o);" -" if(key===last[i])return;" /* nothing moved, do not repeat */ -" last[i]=key;post(o);" -" }).catch(function(e){" -" if(!pc.__bbStatsErr){pc.__bbStatsErr=1;" -" post({ev:'rtc-stats-error',pc:i,message:String((e&&e.message)||e)});}" -" });" -" });" -" },3000);" " function W(){" " var pc=new O(arguments[0],arguments[1]);" -" pcs.push(pc);var id=pcs.length-1;" -" post({ev:'rtc-new',pc:id});" +" post({ev:'rtc-new'});" " ['icegatheringstatechange','iceconnectionstatechange'," " 'connectionstatechange','signalingstatechange'].forEach(function(e){" " try{pc.addEventListener(e,function(){" -" post({ev:'rtc-state',pc:id,on:e,ice:pc.iceConnectionState," +" post({ev:'rtc-state',on:e,ice:pc.iceConnectionState," " conn:pc.connectionState,sig:pc.signalingState});});}catch(x){}" " });" " return pc;" @@ -885,60 +715,17 @@ static const char *SHIM_JS = " window.RTCPeerConnection=W;" " if(window.webkitRTCPeerConnection)window.webkitRTCPeerConnection=W;" " ['addTrack','addTransceiver','addStream','setRemoteDescription'," -" 'createOffer','createAnswer','setLocalDescription','addIceCandidate'," -" 'setConfiguration','getStats','close'].forEach(function(m){" +" 'createOffer','createAnswer','setLocalDescription'].forEach(function(m){" " var f=O.prototype[m];" " if(!f)return;" " O.prototype[m]=function(){" " var a0=arguments[0];" " var d=(m==='setRemoteDescription'&&a0&&a0.type)?a0.type:" " (a0&&a0.kind)?a0.kind:undefined;" -" if(m!=='getStats')post({ev:'rtc-call',fn:m,arg:d});" -" var r;" -" try{r=f.apply(this,arguments);}" -" catch(e){post({ev:'rtc-error',fn:m,when:'threw'," -" name:e&&e.name,message:String((e&&e.message)||e)});throw e;}" -/* A handler here does not take the rejection away from the page: it - * still gets the promise it was given, with its own handlers. */ -" if(r&&r.then)r.then(undefined,function(e){" -" post({ev:'rtc-error',fn:m,when:'rejected'," -" name:e&&e.name,message:String((e&&e.message)||e)});});" -" return r;" +" post({ev:'rtc-call',fn:m,arg:d});" +" return f.apply(this,arguments);" " };" " });" -/* - * Where a publisher sets its bitrate. WebKit requires - * RTCRtpSendParameters.codecs and Firefox does not, so a library on its - * Firefox path throws here - after the offer is made and before the - * local description is set, which looks like the site simply giving up. - * Traced whether or not --rtc-params-fix is repairing it. - */ -" var SND=window.RTCRtpSender;" -" if(SND&&SND.prototype&&SND.prototype.setParameters){" -" var oSP2=SND.prototype.setParameters;" -" SND.prototype.setParameters=function(p){" -" post({ev:'rtc-call',fn:'setParameters'," -" arg:(p&&p.encodings&&p.encodings.length)?('encodings:'+p.encodings.length):undefined," -" keys:p?Object.keys(p):[]});" -" var r;" -" try{r=oSP2.apply(this,arguments);}" -" catch(e){post({ev:'rtc-error',fn:'setParameters',when:'threw'," -" name:e&&e.name,message:String((e&&e.message)||e)});throw e;}" -" if(r&&r.then)r.then(undefined,function(e){" -" post({ev:'rtc-error',fn:'setParameters',when:'rejected'," -" name:e&&e.name,message:String((e&&e.message)||e)});});" -" return r;" -" };" -" }" -/* And what the page itself fails on, which is otherwise invisible. */ -" try{" -" window.addEventListener('error',function(e){" -" post({ev:'page-error',message:String((e&&e.message)||e)," -" src:((e&&e.filename)||'').slice(-60),line:(e&&e.lineno)||0});});" -" window.addEventListener('unhandledrejection',function(e){" -" var r=e&&e.reason;" -" post({ev:'page-reject',name:r&&r.name,message:String((r&&r.message)||r)});});" -" }catch(e){}" /* * A publisher library decides up front whether it supports this browser, * from the user agent and a handful of API checks. When it decides not to, @@ -976,151 +763,17 @@ static const char *SHIM_JS = " };" " post({ev:'params-fix-on'});" "})();}" -"if(C.ssrcFix||C.rtcTrace||(C.codecs&&C.codecs.length)){(function(){" +"if(C.ssrcFix||(C.codecs&&C.codecs.length)){(function(){" " var P=window.RTCPeerConnection;" " if(!P){post({ev:'rtc-missing'});return;}" " var cname=rtcCname();" " var oCO=P.prototype.createOffer,oCA=P.prototype.createAnswer," " oSLD=P.prototype.setLocalDescription;" " function patch(d){" -" if(!d||!d.sdp)return d;" -" var s=d.sdp;" -" if(C.codecs&&C.codecs.length)s=sdpPreferCodecs(s,C.codecs);" -" if(C.ssrcFix)s=sdpAddSsrc(s,cname);" +" if(!C.ssrcFix||!d||!d.sdp)return d;" +" var s=sdpAddSsrc(d.sdp,cname);" " return s===d.sdp?d:{type:d.type,sdp:s};" " }" -" function fakesIn(sdp,map){" -" var want=[],vals={},k;" -" for(k in map)vals[map[k]]=1;" -" (sdp||'').split(/(?=\\r?\\nm=)/).forEach(function(sec){" -" var m=new RegExp('a=ssrc:(\\\\d+) cname:'+cname).exec(sec)," -" mid=/a=mid:(\\S+)/.exec(sec),kd=/m=(audio|video)/.exec(sec);" -" if(m&&kd&&!map[m[1]]&&!vals[m[1]])" -" want.push({fake:m[1],mid:mid?mid[1]:null,kind:kd[1]});" -" });" -" return want;" -" }" -" function hookLD(pc){" -" if(pc.__bbLD)return;pc.__bbLD=true;" -" ['localDescription','currentLocalDescription','pendingLocalDescription'].forEach(function(n){" -" var d=Object.getOwnPropertyDescriptor(P.prototype,n);" -" if(!d||!d.get)return;" -" try{Object.defineProperty(pc,n,{configurable:true,get:function(){" -" var v=d.get.call(pc);if(!v||!v.sdp)return v;" -" var x=v.sdp,k,map=pc.__bbMap||{};" -" for(k in map)x=x.split('a=ssrc:'+k+' ').join('a=ssrc:'+map[k]+' ');" -" return x===v.sdp?v:new RTCSessionDescription({type:v.type,sdp:x});" -" }});}catch(e){}" -" });" -" }" -" var SS={map:{},pending:{},info:{}};" -" function ssrcRe(f){return new RegExp('(^|[^0-9])'+f+'(?![0-9])','g');}" -" function realSsrc(pc){" -" var map=pc.__bbMap||(pc.__bbMap={});" -" var want=fakesIn(pc.localDescription&&pc.localDescription.sdp,map);" -" hookLD(pc);" -" if(!want.length)return;" -" want.forEach(function(w){SS.pending[w.fake]=1;});" -" var t0=Date.now();" -" function look(){" -" var trs=[];try{trs=pc.getTransceivers();}catch(e){}" -" Promise.all(want.map(function(w){" -" if(map[w.fake])return null;" -" var tr=trs.filter(function(t){return w.mid!==null&&t.mid===w.mid;})[0];" -" var q;try{q=(tr&&tr.sender&&tr.sender.getStats)?tr.sender.getStats():pc.getStats();}" -" catch(e){q=pc.getStats();}" -" return Promise.resolve(q).then(function(r){" -" var hit=null,cid=null,cod={};" -" r.forEach(function(st){if(st.type==='codec')cod[st.id]=st;});" -" r.forEach(function(st){" -" if(st.type!=='outbound-rtp'||!st.ssrc||hit)return;" -" if(st.mid&&w.mid&&st.mid!==w.mid)return;" -" var sk=st.kind||st.mediaType;" -" if(sk&&sk!==w.kind)return;" -" hit=st.ssrc;cid=st.codecId;" -" });" -" if(hit){var cc=cod[cid]||{};" -" SS.info[String(hit)]={pt:cc.payloadType,mime:cc.mimeType||null};}" -" if(hit){map[w.fake]=String(hit);SS.map[w.fake]=String(hit);delete SS.pending[w.fake];" -" post({ev:'ssrc-real',kind:w.kind,mid:w.mid,fake:+w.fake,real:+hit,ms:Date.now()-t0});}" -" },function(){});" -" })).then(function(){" -" var left=want.filter(function(w){return !map[w.fake];});" -" if(!left.length)return;" -" if(Date.now()-t0<15000){setTimeout(look,150);return;}" -" left.forEach(function(w){delete SS.pending[w.fake];" -" post({ev:'ssrc-real-missing',kind:w.kind,mid:w.mid,fake:+w.fake});});" -" });" -" }" -" look();" -" }" -" function ssrcRewrite(d){" -" if(typeof d!=='string')return {d:d,n:0};" -" var n=0,f;" -" for(f in SS.map){var re=ssrcRe(f);" -" if(re.test(d)){n++;d=d.replace(ssrcRe(f),'$1'+SS.map[f]);}}" -" return {d:d,n:n};" -" }" -" function ptFix(d){" -" if(typeof d!=='string')return {d:d,n:0};" -" var i=d.indexOf('['),j=d.indexOf('{'),k=i<0?j:(j<0?i:Math.min(i,j));" -" if(k<0)return {d:d,n:0};" -" var body;try{body=JSON.parse(d.slice(k));}catch(e){return {d:d,n:0};}" -" var n=0;" -" (function walk(o,depth){" -" if(!o||typeof o!=='object'||depth>12)return;" -" if(Array.isArray(o.codecs)&&Array.isArray(o.encodings)){" -" var info=null;" -" o.encodings.forEach(function(e){if(e&&SS.info[String(e.ssrc)])info=SS.info[String(e.ssrc)];});" -" var media=o.codecs.filter(function(c){" -" return c&&c.mimeType&&!/\\/(rtx|red|ulpfec|flexfec)/i.test(c.mimeType);});" -" var c=media[0];" -" if(info&&c&&info.pt!=null){" -" if(info.mime&&c.mimeType.toLowerCase()!==info.mime.toLowerCase())" -" post({ev:'codec-mismatch',sending:info.mime,agreed:c.mimeType});" -" else if(c.payloadType!==info.pt){" -" var old=c.payloadType;c.payloadType=info.pt;" -" o.codecs.forEach(function(x){" -" if(x&&x.parameters&&x.parameters.apt===old)x.parameters.apt=info.pt;});" -" n++;post({ev:'pt-fixed',mime:c.mimeType,from:old,to:info.pt});" -" }" -" }" -" }" -" for(var key in o)if(Object.prototype.hasOwnProperty.call(o,key))walk(o[key],depth+1);" -" })(body,0);" -" return n?{d:d.slice(0,k)+JSON.stringify(body),n:n}:{d:d,n:0};" -" }" -" function ssrcPending(d){" -" if(typeof d!=='string')return false;" -" for(var f in SS.pending)if(ssrcRe(f).test(d))return true;" -" return false;" -" }" -" if(C.ssrcFix&&window.WebSocket){(function(){" -" var WS=window.WebSocket.prototype,oSend=WS.send;" -" function flush(ws,t0,timedOut){" -" var q=ws.__bbQ||[],n=0;ws.__bbQ=null;" -" q.forEach(function(d){var r=ssrcRewrite(d);n+=r.n;r=ptFix(r.d);try{oSend.call(ws,r.d);}catch(e){}});" -" post({ev:'ssrc-ws',held:q.length,rewritten:n,ms:Date.now()-t0,timeout:!!timedOut});" -" }" -" WS.send=function(d){" -" var ws=this;" -" if(ws.__bbQ){ws.__bbQ.push(d);return;}" -" if(ssrcPending(d)){" -" ws.__bbQ=[d];var t0=Date.now();" -" (function wait(){" -" if(!ws.__bbQ)return;" -" var still=ws.__bbQ.some(ssrcPending);" -" if(!still)flush(ws,t0,false);" -" else if(Date.now()-t0>8000)flush(ws,t0,true);" -" else setTimeout(wait,100);" -" })();" -" return;" -" }" -" var r=ssrcRewrite(d);" -" if(r.n){post({ev:'ssrc-ws',held:0,rewritten:r.n,ms:0,timeout:false});r=ptFix(r.d);}" -" return oSend.call(ws,r.d);" -" };" -" })();}" " P.prototype.createOffer=function(){" " var pc=this,a=arguments;" " codecPrefs(pc);" @@ -1140,62 +793,16 @@ static const char *SHIM_JS = " P.prototype.setLocalDescription=function(d){" " var pc=this;" " return Promise.resolve(oSLD.call(pc,patch(d))).then(function(r){" -" var sdp=(pc.localDescription&&pc.localDescription.sdp)||'';" " if(C.ssrcFix)try{" +" var sdp=(pc.localDescription&&pc.localDescription.sdp)||'';" " var n=(sdp.match(/a=ssrc:\\d+ cname:/g)||[]).length;" " post({ev:'sdp-ssrc-verify',cnameLines:n,kept:n>0});" " }catch(e){}" -" postSdp('local-'+((pc.localDescription&&pc.localDescription.type)||'?'),sdp);" -" if(C.ssrcFix)try{realSsrc(pc);}catch(e){}" -" return r;" -" });" -" };" -" var oSRD=P.prototype.setRemoteDescription;" -" P.prototype.setRemoteDescription=function(d){" -" var pc=this;" -" return Promise.resolve(oSRD.apply(pc,arguments)).then(function(r){" -" postSdp('remote-'+((d&&d.type)||'?'),(d&&d.sdp)||" -" ((pc.remoteDescription&&pc.remoteDescription.sdp)||''));" -" try{if(d&&d.type==='answer'){" -" var mine=firstVideo(pc.localDescription&&pc.localDescription.sdp)," -" theirs=firstVideo(d.sdp);" -" if(mine&&theirs&&mine!==theirs)" -" post({ev:'codec-mismatch',sending:mine,agreed:theirs});" -" }}catch(e){}" " return r;" " });" " };" " post({ev:'rtc-wrapped',ssrcFix:!!C.ssrcFix,codecs:C.codecs||[]});" "})();}" -"if(C.silentSplit){(function(){" -" var D=Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype,'srcObject');" -" if(!D||!D.set||!D.get||!window.MediaStream)return;" -" Object.defineProperty(HTMLMediaElement.prototype,'srcObject',{configurable:true," -" get:function(){return this.__bbSplit?this.__bbOrig:D.get.call(this);}," -" set:function(v){this.__bbSplit=false;this.__bbOrig=v;this.__bbAt=Date.now();" -" return D.set.call(this,v);}});" -" function rejoin(el,why){" -" if(!el.__bbSplit)return;" -" el.__bbSplit=false;D.set.call(el,el.__bbOrig);" -" try{el.play().catch(function(){});}catch(e){}" -" post({ev:'video-audio-rejoin',id:el.id||'',why:why});" -" }" -" setInterval(function(){" -" var l=document.querySelectorAll('video');" -" for(var i=0;i<l.length;i++){var el=l[i],st=el.__bbOrig;" -" if(el.__bbSplit||!st||!st.getAudioTracks||el.readyState>0)continue;" -" if(Date.now()-(el.__bbAt||0)<2500)continue;" -" var vt=st.getVideoTracks(),at=st.getAudioTracks();" -" if(!vt.length||!at.length||vt[0].muted||vt[0].readyState!=='live')continue;" -" if(!at.every(function(t){return t.muted;}))continue;" -" el.__bbSplit=true;" -" D.set.call(el,new MediaStream(vt));" -" try{el.play().catch(function(){});}catch(e){}" -" post({ev:'video-audio-split',id:el.id||'',audio:at.length});" -" at.forEach(function(t){t.addEventListener('unmute',function(){rejoin(el,'audio-unmuted');},{once:true});});" -" }" -" },1000);" -"})();}" "window.__bbHeld=function(){" " return !!(cachedV&&cachedV.readyState==='live')||!!(cachedA&&cachedA.readyState==='live');" "};" @@ -1441,8 +1048,7 @@ dump_gstreamer_env (void) dump_env ("GST_DEBUG"); dump_env ("GST_DEBUG_FILE"); dump_env ("WEBKIT_DEBUG"); - dump_env ("GST_PLUGIN_FEATURE_RANK"); - dump_env ("WEBKIT_GST_DISABLE_WEBRTC_NETWORK_SANDBOX"); + dump_env ("WEBKIT_GST_ENABLE_HW_DECODERS"); dump_env ("WEBKIT_DISABLE_DMABUF_RENDERER"); dump_env ("WEBKIT_DISABLE_COMPOSITING_MODE"); @@ -1515,8 +1121,6 @@ capture_prewarm (gpointer u) gst_object_unref (mon); return G_SOURCE_REMOVE; } - if (!device_monitor_wait_started (mon, 5000)) - mlog ("prewarm: device providers did not finish starting in 5 s, list may be short"); GList *devs = gst_device_monitor_get_devices (mon); int n = 0; @@ -1755,24 +1359,6 @@ on_web_process_terminated (WebKitWebView *view, /* ------------------------------------------------- script message routing */ -/* "sending":"H264" -> a new "H264" (short codec names, no escapes) */ -static char * -json_str (const char *json, const char *key) -{ - char *needle = g_strdup_printf ("\"%s\":\"", key); - const char *p = json ? strstr (json, needle) : NULL; - char *v = NULL; - - if (p) { - p += strlen (needle); - const char *e = strchr (p, '"'); - if (e && e - p < 64) - v = g_strndup (p, e - p); - } - g_free (needle); - return v; -} - static gboolean ev_is (const char *json, const char *ev) { @@ -1803,23 +1389,14 @@ on_script_message (WebKitUserContentManager *ucm, JSCValue *value, gpointer u) ev_is (s, "gum-ok") || ev_is (s, "gum-error") || ev_is (s, "gum-cache-hit") || ev_is (s, "gum-drop-audio") || ev_is (s, "sdp-ssrc-added") || ev_is (s, "codec-pref") || - ev_is (s, "codec-mismatch") || ev_is (s, "ssrc-real") || - ev_is (s, "ssrc-real-missing") || ev_is (s, "ssrc-ws") || - ev_is (s, "rtc-stats-error") || ev_is (s, "video-state") || - ev_is (s, "play-rejected") || ev_is (s, "pt-fixed") || - ev_is (s, "mic-pin") || ev_is (s, "cam-pin") || - ev_is (s, "video-attach") || ev_is (s, "mic-swapped") || - ev_is (s, "mic-swap-failed") || ev_is (s, "video-audio-split") || - ev_is (s, "video-audio-rejoin") || ev_is (s, "gum-fps-loosened") || ev_is (s, "rtc-wrapped") || ev_is (s, "rtc-missing") || ev_is (s, "rtc-new") || ev_is (s, "rtc-call") || - ev_is (s, "rtc-stats") || ev_is (s, "rtc-error") || - ev_is (s, "sdp") || - ev_is (s, "page-error") || ev_is (s, "page-reject") || ev_is (s, "rtc-state") || ev_is (s, "rtc-trace-on") || ev_is (s, "rtc-caps") || ev_is (s, "send-params-fixed") || ev_is (s, "params-fix-on") || ev_is (s, "sdp-ssrc-verify") || - ev_is (s, "compat-orientation") || ev_is (s, "gum-nomic") || + ev_is (s, "cam-scaled") || ev_is (s, "cam-scale-error") || + ev_is (s, "cam-scale-skip")|| ev_is (s, "compat-orientation") || + ev_is (s, "cam-scale-mode")|| ev_is (s, "gum-nomic") || ev_is (s, "compat-error") || ev_is (s, "media-stall")|| ev_is (s, "media-reload") || ev_is (s, "media-dump") || ev_is (s, "warm-ok") || @@ -1833,40 +1410,6 @@ on_script_message (WebKitUserContentManager *ucm, JSCValue *value, gpointer u) * process could not reach them, which on a desktop means the portal, * and the portal needs a working D-Bus. */ - /* - * WebKit picks the video encoder when the local description is set: - * the first codec of its own offer that it can encode - * (linkOutgoingSources -> configurePacketizers). The answer comes - * later and is not consulted again - codecPreferencesChanged refuses - * once the pipeline runs. So an answer that settles on another codec - * gets packets it cannot decode: the far end shows a spinner while - * our side reports megabytes sent. - */ - if (ev_is (s, "codec-mismatch")) { - char *mine = json_str (s, "sending"), *theirs = json_str (s, "agreed"); - mlog ("webrtc: the far end agreed to %s, but WebKit encodes %s - the", - theirs ? theirs : "?", mine ? mine : "?"); - mlog ("webrtc: first codec of its own offer - and does not switch."); - mlog ("webrtc: The far end cannot decode that. --video-codecs %s", - theirs ? theirs : "VP8"); - mlog ("webrtc: puts %s first in the offer, so both sides match.", - theirs ? theirs : "VP8"); - g_free (mine); g_free (theirs); - } - - /* - * WebKit writes no a=ssrc lines, so --sdp-ssrc-fix adds one for sites - * that read the SSRC out of the SDP (mediasoup). A made-up number - * would be announced to the server while WebKit sends another, and - * the server drops every packet. So the made-up one is swapped for - * the one WebKit really uses, read from its own stats. - */ - if (ev_is (s, "ssrc-real-missing")) { - mlog ("webrtc: could not learn the real SSRC within 15s; the site was"); - mlog ("webrtc: told a made-up one, and a server that trusts it (mediasoup)"); - mlog ("webrtc: will drop this stream."); - } - if (ev_is (s, "gum-drop-audio")) mlog ("camera: no microphone here, retrying for video alone"); @@ -1883,11 +1426,11 @@ on_script_message (WebKitUserContentManager *ucm, JSCValue *value, gpointer u) else mlog ("camera: --prewarm will say whether the devices are visible here"); - mlog ("camera: WebKit lists cameras with GStreamer's device monitor, so"); - mlog ("camera: v4l2deviceprovider (gst-plugins-good) must be installed and"); - mlog ("camera: /dev/video* readable by this user (group video)."); - mlog ("camera: only a PipeWire >= 0.3.64 provider makes it ask the camera"); - mlog ("camera: portal instead; a bubblewrap sandbox needs --no-sandbox."); + mlog ("camera: the web process reaches devices through the desktop portal;"); + mlog ("camera: with no portal, --no-sandbox lets it open them directly."); + mlog ("camera: a portal needs D-Bus, which needs a machine id:"); + mlog ("camera: dbus-uuidgen --ensure=/etc/machine-id (no systemd needed)"); + mlog ("camera: dbus-run-session -- browser-big URL"); } if (ev_is (s, "warm-page-done")) { @@ -1972,15 +1515,11 @@ static char * build_shim (void) { char *match = js_quote (g_cam_match); - char *micmatch = js_quote (g_mic_match); /* ["VP8","VP9"] from a comma separated list */ GString *cj = g_string_new ("["); - /* VP8 first by default: WebKit keeps encoding the first codec of its - * own offer, and VP8 is what SFUs answer. "native" leaves the order. */ - const char *vc = g_codecs_set ? g_video_codecs : "VP8"; - if (vc) { - char **v = g_strsplit (vc, ",", -1); + if (g_video_codecs) { + char **v = g_strsplit (g_video_codecs, ",", -1); for (int i = 0; v[i]; i++) { char *t = g_strstrip (g_strdup (v[i])); if (*t) @@ -1993,32 +1532,33 @@ build_shim (void) char *codecs_js = g_string_free (cj, FALSE); char *cfg = g_strdup_printf ( "window.__bbCfg={camfix:%s,relax:%s,cache:%s,dropAudio:%s,retries:%d," - "maxW:%d,maxH:%d,maxFps:%d," - "ssrcFix:%s,codecs:%s,rtcTrace:%s,paramsFix:%s,silentSplit:%s," - "compat:%s,noMic:%s,fpsLoose:%s," - "match:%s,micMatch:%s,watchdog:%s,stall:%d,debug:%s};", + "maxW:%d,maxH:%d,maxFps:%d,forceW:%d,forceH:%d,forceFps:%d,holdMs:%d," + "forceExact:%s,ssrcFix:%s,codecs:%s,rtcTrace:%s,paramsFix:%s,scale:%s," + "compat:%s,scaleFps:%d,noMic:%s," + "match:%s,watchdog:%s,stall:%d,debug:%s};", g_cam_fix ? "true" : "false", (g_cam_repair && g_cam_relax) ? "true" : "false", (g_cam_repair && g_cam_keepalive) ? "true" : "false", (g_cam_repair && g_cam_drop_audio) ? "true" : "false", g_cam_repair ? g_cam_retries : 0, g_cam_max_w, g_cam_max_h, g_cam_max_fps, + g_cam_force_w, g_cam_force_h, g_cam_force_fps, g_cam_hold_ms, + g_cam_force_exact ? "true" : "false", g_sdp_ssrc_fix ? "true" : "false", codecs_js, g_rtc_trace ? "true" : "false", g_rtc_params_fix ? "true" : "false", - g_silent_split ? "true" : "false", + g_cam_scale ? "true" : "false", g_web_compat ? "true" : "false", + g_cam_scale_fps, g_no_mic ? "true" : "false", - g_fps_loose ? "true" : "false", - match, micmatch, + match, g_media_watchdog ? "true" : "false", g_stall_timeout, g_shim_debug ? "true" : "false"); char *js = g_strconcat (cfg, SHIM_JS, NULL); g_free (match); - g_free (micmatch); g_free (codecs_js); g_free (cfg); return js; @@ -2112,11 +1652,11 @@ capture_check (gpointer u) mlog ("capture: permission was granted but nothing started capturing."); mlog ("capture: run --list-cameras to see the devices this build can reach;"); - mlog ("capture: an empty list means GStreamer cannot see them from here -"); - mlog ("capture: --prewarm lists cameras and microphones as WebKit sees them"); - mlog ("capture: --gst-debug webkitcapture*:5 WebKit's own capture log"); - mlog ("capture: a camera but no microphone means no alsa/pulse device provider"); - mlog ("capture: found a capture device (gst-device-monitor-1.0 Audio/Source)."); + mlog ("capture: an empty list means the web process cannot get at them -"); + mlog ("capture: --no-sandbox opens them without the desktop portal"); + mlog ("capture: --prewarm says what GStreamer can see from here"); + mlog ("capture: a camera but no microphone is usually a sound server with"); + mlog ("capture: no card; audio = alsa in the config bypasses it."); return G_SOURCE_REMOVE; } @@ -2215,8 +1755,10 @@ big_usage_options (GString *s) { g_string_append_printf (s, "camera and video:\n" -" Off unless asked for: no device probe, no watchdogs, no reloads of\n" -" its own, and the page's camera constraints pass through untouched.\n" +" Nothing in this section is on unless you ask for it. By default the\n" +" browser is plain WebKitGTK: no shim over getUserMedia, no device probe,\n" +" no watchdogs, no reloads of its own. Reach for these when a site\n" +" misbehaves, not before.\n" "\n" " --fix-media --cam-fix, --prewarm, --media-watchdog and\n" " --auto-reload together\n" @@ -2249,22 +1791,33 @@ big_usage_options (GString *s) " --no-cam-drop-audio with --cam-fix, fail instead of handing a site\n" " video only when the machine has no microphone\n" " --cam-max WxH@FPS cap relaxed constraints (default: %dx%d@%d)\n" -" --no-cam-size-fix pass a page's soft frameRate limit on to WebKit. By\n" -" default it is dropped: WebKit only picks camera\n" -" modes that list that exact rate, so {max: 20} turns\n" -" a 4:3 request into a letterboxed 16:9 mode\n" -" (cam_size_fix = no)\n" " --no-mic give pages the camera but never the microphone. A\n" " diagnostic: if a freeze goes away, the audio path\n" " is the cause (the audio track here reports\n" " sampleRate 0, which is not a valid rate)\n" -" --web-compat turn on WebKit's ScreenOrientationAPI feature, which\n" -" is built but off on GTK, and if a page still sees no\n" -" screen.orientation, supply a static one. Zoom's media\n" -" code reads screen.orientation.type and throws without it\n" +" --web-compat supply APIs WebKitGTK does not implement, currently\n" +" screen.orientation. Zoom's media code reads\n" +" screen.orientation.type and throws without it\n" +" --cam-scale deliver the size the page asked for by scaling the\n" +" camera through a canvas. WebKit picks the nearest\n" +" native mode instead of scaling, so a site that\n" +" needs 320x240 is handed 848x480 and refuses it.\n" +" Scaling happens on the page's own thread, so the\n" +" rate is capped (default 15)\n" +" --cam-scale-fps N raise or lower that cap\n" +" --cam-exact WxH@FPS demand this size exactly, whatever the page asked.\n" +" A bare width in a page's constraints is only a\n" +" preference and WebKit may answer with another size;\n" +" this refuses anything else. Fails outright if the\n" +" camera cannot do it, which is the honest answer\n" +" --cam-force WxH@FPS ask for this size and aspect ratio whatever the page\n" +" requested, e.g. 640x480@30 for 4:3. Implies\n" +" --cam-fix. Use when a site negotiates a shape the\n" +" camera answers badly\n" " --cam-match TEXT prefer the camera whose label contains TEXT\n" -" --mic-match TEXT prefer the microphone whose label contains TEXT, for\n" -" pages that just take the first one (mic_match)\n" +" --cam-hold SEC how long a shared capture is held after the page\n" +" has stopped using it (default: 3). The camera light\n" +" stays on for this long; 0 releases at once\n" " --cam-retries N retries with looser constraints (default: %d)\n" "\n" "video:\n" @@ -2275,16 +1828,11 @@ big_usage_options (GString *s) " default: %d)\n" " --max-reloads N auto-reloads per URL (default: %d)\n" -" --ice-rice use WebKit's own librice ICE agent instead of the\n" -" default libnice one. librice finds no public address\n" -" here (a DNS/IPv6 FIXME in its STUN code), so calls\n" -" over the internet fail. ice = rice in the config\n" " --no-webrtc disable WebRTC only\n" " --no-mediastream disable MediaStream / getUserMedia only\n" "\n" -"webrtc (the repairs below are on by default):\n" -" --no-fix-webrtc turn off --sdp-ssrc-fix, --rtc-params-fix and the\n" -" silent-audio split at once (fix_webrtc = no)\n" +"webrtc:\n" +" --fix-webrtc --sdp-ssrc-fix and --rtc-params-fix together\n" " --rtc-params-fix supply RTCRtpSendParameters.codecs when a library\n" " omits it. WebKit requires it and Firefox does not,\n" " so a library on its Firefox path throws\n" @@ -2293,12 +1841,12 @@ big_usage_options (GString *s) " made, tracks attached, offer created, answer set -\n" " so the last one logged is the step that failed.\n" " Included in --media-trace\n" -" --sdp-ssrc-fix add the a=ssrc lines WebKit leaves out, then tell the\n" -" server the SSRC and payload type WebKit really sends\n" -" (mediasoup drops the stream otherwise)\n" -" --video-codecs LIST sending codec order (default: VP8). WebKit keeps\n" -" encoding the first codec of its own offer, whatever\n" -" the far end answers. native keeps WebKit's order\n" +" --sdp-ssrc-fix add the a=ssrc cname lines WebKit leaves out, which\n" +" a site parsing the offer needs - without them it\n" +" throws \"CNAME value not found\" and never connects\n" +" --video-codecs LIST preferred codec order for sending, e.g. VP8 or\n" +" VP8,VP9. Use it when WebKit offers something this\n" +" machine has no encoder for\n" "\n" "diagnostics:\n" " --media-trace log every getUserMedia call, the constraints it\n" @@ -2309,11 +1857,11 @@ big_usage_options (GString *s) " (a missing GStreamer plugin is always reported,\n" " since pages fail in confusing ways without one)\n" " --audio-pulse undo audio=alsa from a config file for this run\n" -" --audio-alsa play through ALSA rather than PulseAudio, by\n" -" ranking pulsesink out. Fixes browser audio when\n" -" the Pulse server has a dummy sink. Microphones are\n" -" a separate matter: GStreamer ignores ranks for\n" -" device providers, so a microphone still\n" +" --audio-alsa play and record through ALSA rather than PulseAudio,\n" +" by ranking the pulse elements out. Fixes browser\n" +" audio when the Pulse server has a dummy sink.\n" +" Enumeration is a separate matter: device provider\n" +" ranks cannot be overridden, so a microphone still\n" " needs alsadeviceprovider installed - check with\n" " gst-inspect-1.0 alsadeviceprovider\n" " Put audio = alsa in a config file to have this\n" @@ -2388,8 +1936,7 @@ big_parse_arg (int argc, char **argv, int *i) if (!strcmp (a, "--warm-timeout")) { NEXT ("--warm-timeout"); - const char *v = argv[++(*i)]; - g_warm_timeout = MAX (1, atoi (v)); + g_warm_timeout = MAX (1, atoi (argv[++(*i)])); return TRUE; } if (!strcmp (a, "--no-mic")) { @@ -2402,7 +1949,53 @@ big_parse_arg (int argc, char **argv, int *i) g_web_compat = TRUE; g_cam_fix = TRUE; /* it rides in the same shim */ g_cam_relax = FALSE; - feature_request ("ScreenOrientationAPI"); /* WebKit has it, off on GTK */ + return TRUE; + } + if (!strcmp (a, "--cam-scale-fps")) { + NEXT ("--cam-scale-fps"); + g_cam_scale_fps = CLAMP (atoi (argv[++(*i)]), 1, 60); + g_cam_scale = g_cam_fix = TRUE; + g_cam_relax = FALSE; + return TRUE; + } + if (!strcmp (a, "--cam-scale")) { + g_cam_scale = TRUE; + g_cam_fix = TRUE; + g_cam_relax = FALSE; /* the page's size is the point */ + return TRUE; + } + if (!strcmp (a, "--cam-exact")) { + /* + * exact, not ideal. A bare width/height in a page's constraints is + * only a preference and WebKit is free to answer with another + * size - which it does, so a site that publishes at the size it + * asked for gets a stream it cannot use. + */ + NEXT ("--cam-exact"); + int sw = g_cam_max_w, sh = g_cam_max_h, sf = g_cam_max_fps; + if (!parse_cam_max (argv[++(*i)])) { + g_printerr ("%s: bad --cam-exact %s (want WxH@FPS)\n", argv[0], argv[*i]); + exit (1); + } + g_cam_force_w = g_cam_max_w; g_cam_force_h = g_cam_max_h; + g_cam_force_fps = g_cam_max_fps; + g_cam_max_w = sw; g_cam_max_h = sh; g_cam_max_fps = sf; + g_cam_force_exact = TRUE; + g_cam_fix = TRUE; + g_cam_relax = FALSE; /* relaxing would undo the point */ + return TRUE; + } + if (!strcmp (a, "--cam-force")) { + NEXT ("--cam-force"); + int sw = g_cam_max_w, sh = g_cam_max_h, sf = g_cam_max_fps; + if (!parse_cam_max (argv[++(*i)])) { + g_printerr ("%s: bad --cam-force %s (want WxH@FPS)\n", argv[0], argv[*i]); + exit (1); + } + g_cam_force_w = g_cam_max_w; g_cam_force_h = g_cam_max_h; + g_cam_force_fps = g_cam_max_fps; + g_cam_max_w = sw; g_cam_max_h = sh; g_cam_max_fps = sf; + g_cam_fix = TRUE; /* it is applied by the shim */ return TRUE; } if (!strcmp (a, "--cam-max")) { @@ -2417,20 +2010,16 @@ big_parse_arg (int argc, char **argv, int *i) NEXT ("--cam-match"); g_free (g_cam_match); g_cam_match = g_strdup (argv[++(*i)]); - g_cam_fix = TRUE; return TRUE; } - if (!strcmp (a, "--mic-match")) { - NEXT ("--mic-match"); - g_free (g_mic_match); - g_mic_match = g_strdup (argv[++(*i)]); - g_cam_fix = TRUE; /* it rides in the same shim */ + if (!strcmp (a, "--cam-hold")) { + NEXT ("--cam-hold"); + g_cam_hold_ms = MAX (0, atoi (argv[++(*i)])) * 1000; return TRUE; } if (!strcmp (a, "--cam-retries")) { NEXT ("--cam-retries"); - const char *v = argv[++(*i)]; - g_cam_retries = CLAMP (atoi (v), 0, 3); + g_cam_retries = CLAMP (atoi (argv[++(*i)]), 0, 3); return TRUE; } @@ -2442,20 +2031,17 @@ big_parse_arg (int argc, char **argv, int *i) if (!strcmp (a, "--stall-timeout")) { NEXT ("--stall-timeout"); - const char *v = argv[++(*i)]; - g_stall_timeout = MAX (2, atoi (v)); + g_stall_timeout = MAX (2, atoi (argv[++(*i)])); return TRUE; } if (!strcmp (a, "--load-timeout")) { NEXT ("--load-timeout"); - const char *v = argv[++(*i)]; - g_load_timeout = MAX (0, atoi (v)); + g_load_timeout = MAX (0, atoi (argv[++(*i)])); return TRUE; } if (!strcmp (a, "--max-reloads")) { NEXT ("--max-reloads"); - const char *v = argv[++(*i)]; - g_max_reloads = MAX (0, atoi (v)); + g_max_reloads = MAX (0, atoi (argv[++(*i)])); return TRUE; } @@ -2466,14 +2052,11 @@ big_parse_arg (int argc, char **argv, int *i) g_cam_relax = FALSE; return TRUE; } - if (!strcmp (a, "--fix-webrtc")) { /* the default; kept for old scripts */ - set_fix_webrtc (TRUE); - g_cam_fix = TRUE; - return TRUE; - } - if (!strcmp (a, "--no-fix-webrtc")) { set_fix_webrtc (FALSE); return TRUE; } - if (!strcmp (a, "--no-cam-size-fix") || !strcmp (a, "--no-framerate-fix")) { - g_fps_loose = FALSE; + if (!strcmp (a, "--fix-webrtc")) { /* both offer repairs at once */ + g_rtc_params_fix = TRUE; + g_sdp_ssrc_fix = TRUE; + g_cam_fix = TRUE; + g_cam_relax = FALSE; return TRUE; } if (!strcmp (a, "--rtc-trace")) { @@ -2491,7 +2074,8 @@ big_parse_arg (int argc, char **argv, int *i) } if (!strcmp (a, "--video-codecs")) { NEXT ("--video-codecs"); - set_video_codecs (argv[++(*i)]); + g_free (g_video_codecs); + g_video_codecs = g_strdup (argv[++(*i)]); g_cam_fix = TRUE; g_cam_relax = FALSE; return TRUE; @@ -2519,11 +2103,21 @@ big_parse_arg (int argc, char **argv, int *i) return TRUE; } - if (!strcmp (a, "--audio-pulse")) { g_audio_alsa = FALSE; return TRUE; } - if (!strcmp (a, "--ice-libnice")) { g_ice_libnice = TRUE; return TRUE; } - if (!strcmp (a, "--ice-rice")) { g_ice_libnice = FALSE; return TRUE; } + if (!strcmp (a, "--audio-pulse")) { g_clear_pointer (&g_gst_rank, g_free); return TRUE; } if (!strcmp (a, "--audio-alsa")) { - g_audio_alsa = TRUE; + /* + * Rank PulseAudio's elements out so autoaudiosink/autoaudiosrc + * pick ALSA. This fixes playback on a machine whose Pulse server + * came up with a dummy sink. + * + * It does NOT change which devices are enumerated: + * GST_PLUGIN_FEATURE_RANK is honoured for elements but ignored for + * device providers, so the microphone still depends on + * alsadeviceprovider being installed. Checked, not assumed. + */ + gst_rank_add ("pulsesink:NONE,pulsesrc:NONE," + "alsasink:PRIMARY,alsasrc:PRIMARY," + "pulsedeviceprovider:NONE,alsadeviceprovider:PRIMARY"); return TRUE; } if (!strcmp (a, "--no-va")) { @@ -2589,26 +2183,6 @@ big_parse_arg (int argc, char **argv, int *i) * setting for a machine with no working sound server, and that is a * property of the machine, so it belongs in that machine's config. */ -/* --video-codecs / video_codecs: a list, or native for WebKit's own order */ -static void -set_video_codecs (const char *v) -{ - g_free (g_video_codecs); - g_video_codecs = NULL; - g_codecs_set = TRUE; - if (v && *v && g_ascii_strcasecmp (v, "native") && g_ascii_strcasecmp (v, "none")) - g_video_codecs = g_strdup (v); -} - -/* fix_webrtc = no / --no-fix-webrtc: every call repair off at once */ -static void -set_fix_webrtc (gboolean on) -{ - g_sdp_ssrc_fix = on; - g_rtc_params_fix = on; - g_silent_split = on; -} - /* the core keeps its own truthy(); this is browser-big's copy */ static gboolean truthy_value (const char *v) @@ -2623,50 +2197,17 @@ big_cfg_set (const char *key, const char *value) /* * cam_share = yes * - * A page that opens the same camera twice gets the live track a - * second time instead of a second capture, with stop() counted, which - * is what the other engines do internally. + * WebKitGTK does not multiplex one camera across two getUserMedia + * calls the way Chrome and Firefox do, so a page that asks twice gets + * a second track that never delivers a frame. Sharing hands the + * repeated request a clone of the live track over the single open, + * which is what the other engines do internally. */ if (!g_ascii_strcasecmp (key, "web_compat")) { if (truthy_value (value)) { g_web_compat = TRUE; g_cam_fix = TRUE; g_cam_relax = FALSE; - feature_request ("ScreenOrientationAPI"); - } - return TRUE; - } - - /* the sending codec order: VP8 first matches what most SFUs answer */ - if (!g_ascii_strcasecmp (key, "video_codecs")) { - set_video_codecs (value); - g_cam_fix = TRUE; /* it rides in the same shim */ - g_cam_relax = FALSE; - return TRUE; - } - - if (!g_ascii_strcasecmp (key, "cam_match") || !g_ascii_strcasecmp (key, "mic_match")) { - char **dst = g_ascii_tolower (key[0]) == 'c' ? &g_cam_match : &g_mic_match; - g_free (*dst); - *dst = g_strdup (value); - g_cam_fix = TRUE; - return TRUE; - } - - if (!g_ascii_strcasecmp (key, "sdp_ssrc_fix")) { - if (truthy_value (value)) { - g_sdp_ssrc_fix = TRUE; - g_cam_fix = TRUE; /* it rides in the same shim */ - g_cam_relax = FALSE; - } - return TRUE; - } - - if (!g_ascii_strcasecmp (key, "rtc_params_fix")) { - if (truthy_value (value)) { - g_rtc_params_fix = TRUE; - g_cam_fix = TRUE; - g_cam_relax = FALSE; } return TRUE; } @@ -2682,33 +2223,16 @@ big_cfg_set (const char *key, const char *value) return TRUE; } - if (!g_ascii_strcasecmp (key, "cam_size_fix") || !g_ascii_strcasecmp (key, "framerate_fix")) { - g_fps_loose = truthy_value (value); - return TRUE; - } - - if (!g_ascii_strcasecmp (key, "fix_webrtc")) { - set_fix_webrtc (truthy_value (value)); - return TRUE; - } - - if (!g_ascii_strcasecmp (key, "ice")) { - if (!g_ascii_strcasecmp (value, "libnice")) g_ice_libnice = TRUE; - else if (!g_ascii_strcasecmp (value, "rice") || - !g_ascii_strcasecmp (value, "librice") || - !g_ascii_strcasecmp (value, "auto")) g_ice_libnice = FALSE; - else g_printerr ("config: ice must be rice or libnice\n"); - return TRUE; - } - if (g_ascii_strcasecmp (key, "audio") != 0) return FALSE; if (!g_ascii_strcasecmp (value, "alsa")) { - g_audio_alsa = TRUE; + gst_rank_add ("pulsesink:NONE,pulsesrc:NONE," + "alsasink:PRIMARY,alsasrc:PRIMARY," + "pulsedeviceprovider:NONE,alsadeviceprovider:PRIMARY"); } else if (!g_ascii_strcasecmp (value, "pulse") || !g_ascii_strcasecmp (value, "auto")) { - g_audio_alsa = FALSE; + g_clear_pointer (&g_gst_rank, g_free); } else { g_printerr ("config: audio must be alsa, pulse or auto\n"); } @@ -2733,31 +2257,9 @@ big_pre_gtk (void) g_setenv ("GST_DEBUG", s, TRUE); g_free (s); } - /* - * audio = alsa. WebKit plays through autoaudiosink, which takes the - * highest ranked sink, so ranking pulsesink out is all it takes. - * Capture is different: WebKit opens the GstDevice the device monitor - * found (gst_device_create_element), so the pulsesrc/alsasrc ranks do - * not matter there, and GStreamer applies GST_PLUGIN_FEATURE_RANK to - * element factories only - device provider entries are ignored - * (gstpluginfeature.c, GStreamer 1.28). - */ - if (g_audio_alsa) - gst_rank_add ("pulsesink:NONE"); - if (g_gst_rank) - gst_rank_env_add (g_gst_rank); /* append: core and user may have set it */ - if (g_getenv ("GST_PLUGIN_FEATURE_RANK")) - mlog ("gstreamer: GST_PLUGIN_FEATURE_RANK=%s", g_getenv ("GST_PLUGIN_FEATURE_RANK")); - /* - * Which ICE agent WebKit hands webrtcbin (GStreamerMediaEndpoint.cpp, - * 2.52+): by default its own librice agent, whose sockets, STUN, TURN - * and DNS lookups live in the network process. With this variable set - * WebKit creates a plain webrtcbin, which uses libnice inside the web - * process - the older, well-trodden path. Both need libgstnice. - */ - if (g_ice_libnice) { - g_setenv ("WEBKIT_GST_DISABLE_WEBRTC_NETWORK_SANDBOX", "1", TRUE); - mlog ("webrtc: ICE through libnice in the web process (ice = rice to undo)"); + if (g_gst_rank) { + g_setenv ("GST_PLUGIN_FEATURE_RANK", g_gst_rank, TRUE); + mlog ("gstreamer: GST_PLUGIN_FEATURE_RANK=%s", g_gst_rank); } if (g_gst_dbgfile) g_setenv ("GST_DEBUG_FILE", g_gst_dbgfile, TRUE); if (g_webkit_dbg) g_setenv ("WEBKIT_DEBUG", g_webkit_dbg, TRUE); @@ -2768,127 +2270,93 @@ big_pre_gtk (void) * plugin looks like a broken site rather than a missing package. Silent * when everything needed is present. */ -static gboolean -have_element (const char *name) -{ - GstElementFactory *f = gst_element_factory_find (name); - if (f) gst_object_unref (f); - return f != NULL; -} - -static gboolean -have_any_element (const char *const *names) -{ - for (int i = 0; names[i]; i++) - if (have_element (names[i])) - return TRUE; - return FALSE; -} - -static gboolean -have_provider (const char *name) -{ - GstDeviceProviderFactory *f = gst_device_provider_factory_find (name); - if (f) gst_object_unref (f); - return f != NULL; -} - static void codec_check (void) { /* - * What the sources demand, not a guess: - * - * webrtcbin (gst-plugins-bad 1.28, gstwebrtcbin.c) refuses to leave - * NULL unless nicesrc/nicesink and dtlsenc/dtlsdec are in the - * registry - whichever ICE agent it was given, so WebKit's librice - * agent does not lift the libnice requirement - and creates no data - * channel without sctpenc/sctpdec. rtpbin is looked up by name in - * WebKit's GStreamerMediaEndpoint.cpp. - * - * Capture (GStreamerVideoCapturer.cpp): source -> decodebin3 -> - * videoconvert -> videoscale -> videorate -> capsfilter. Cameras come - * from the device monitor, i.e. v4l2deviceprovider, unless a PipeWire - * provider and a camera portal exist; microphones from alsa/pulse - * device providers. + * webrtcbin alone is not enough: a call also needs ICE from libnice, + * SRTP, DTLS and the RTP manager. Naming each one separately turns + * "WebRTC does not work" into a shopping list. */ static const struct { const char *element; const char *what; } need[] = { - { "webrtcbin", "WebRTC gst-plugins-bad (webrtc plugin)" }, - { "nicesrc", "ICE libnice's GStreamer plugin (libgstnice)" }, - { "nicesink", "ICE libnice's GStreamer plugin (libgstnice)" }, - { "dtlsenc", "DTLS gst-plugins-bad, needs OpenSSL" }, - { "dtlsdec", "DTLS gst-plugins-bad, needs OpenSSL" }, - { "dtlssrtpenc", "DTLS-SRTP gst-plugins-bad" }, - { "dtlssrtpdec", "DTLS-SRTP gst-plugins-bad" }, + { "webrtcbin", "WebRTC gst-plugins-bad, built with libnice" }, { "srtpenc", "SRTP gst-plugins-bad, needs libsrtp2" }, - { "srtpdec", "SRTP gst-plugins-bad, needs libsrtp2" }, - { "sctpenc", "data channels gst-plugins-bad (sctp plugin)" }, - { "sctpdec", "data channels gst-plugins-bad (sctp plugin)" }, - { "rtpbin", "RTP gst-plugins-good (rtpmanager)" }, - { "rtpfunnel", "RTP gst-plugins-good (rtpmanager)" }, - { "rtpopuspay", "Opus over RTP gst-plugins-good (rtp)" }, - { "rtpopusdepay","Opus over RTP gst-plugins-good (rtp)" }, - { "rtpvp8pay", "VP8 over RTP gst-plugins-good (rtp)" }, - { "rtpvp8depay", "VP8 over RTP gst-plugins-good (rtp)" }, - { "rtph264pay", "H.264 over RTP gst-plugins-good (rtp)" }, - { "rtph264depay","H.264 over RTP gst-plugins-good (rtp)" }, - { "opusenc", "Opus audio gst-plugins-base, needs libopus" }, - { "opusdec", "Opus audio gst-plugins-base, needs libopus" }, - { "vp8enc", "VP8 video gst-plugins-good, needs libvpx" }, + { "dtlssrtpenc", "DTLS gst-plugins-bad, needs OpenSSL" }, + { "rtpbin", "RTP gst-plugins-good" }, { "vp8dec", "VP8 video gst-plugins-good, needs libvpx" }, - { "audioconvert","audio gst-plugins-base" }, - { "audioresample","audio gst-plugins-base" }, - { "audiomixer", "audio gst-plugins-base" }, - { "videoconvert","capture gst-plugins-base" }, - { "videoscale", "capture gst-plugins-base" }, - { "videorate", "capture gst-plugins-base" }, - { "decodebin3", "capture gst-plugins-base (playback)" }, - { "autoaudiosink","playback gst-plugins-good (autodetect)" }, + { "opusdec", "Opus audio gst-plugins-base, needs libopus" }, + { "avdec_h264", "H.264 video gst-libav" }, + { "avdec_aac", "AAC audio gst-libav" }, }; GString *missing = g_string_new (NULL); - for (gsize i = 0; i < G_N_ELEMENTS (need); i++) - if (!have_element (need[i].element)) - g_string_append_printf (missing, " %-13s %s\n", need[i].element, need[i].what); - - /* One of each will do. The H.264 encoders are the ones WebKit's own - * webkitvideoencoder knows how to drive (VideoEncoderPrivateGStreamer.cpp). */ - static const char *const h264enc[] = { "x264enc", "openh264enc", "vah264enc", - "vah264lpenc", "omxh264enc", NULL }; - static const char *const h264dec[] = { "avdec_h264", "vah264dec", "openh264dec", - "v4l2slh264dec", "v4l2h264dec", NULL }; - static const char *const jpegdec[] = { "jpegdec", "avdec_mjpeg", "vajpegdec", NULL }; - static const char *const aacdec[] = { "avdec_aac", "faad", "fdkaacdec", NULL }; - - if (!have_any_element (h264enc)) - g_string_append (missing, " H.264 enc x264enc (gst-plugins-ugly), openh264enc (-bad)" - " or vah264enc (-bad, VA-API)\n"); - if (!have_any_element (h264dec)) - g_string_append (missing, " H.264 dec avdec_h264 (gst-libav) or vah264dec (-bad, VA-API)\n"); - if (!have_any_element (aacdec)) - g_string_append (missing, " AAC dec avdec_aac (gst-libav) - MP4 sites, not WebRTC\n"); - - if (!have_provider ("v4l2deviceprovider")) - g_string_append (missing, " v4l2deviceprovider cameras: gst-plugins-good (video4linux2)\n"); - if (!have_provider ("alsadeviceprovider") && !have_provider ("pulsedeviceprovider") && - !have_provider ("pipewiredeviceprovider")) - g_string_append (missing, " alsadeviceprovider microphones: gst-plugins-base (alsa)\n"); + for (gsize i = 0; i < G_N_ELEMENTS (need); i++) { + GstElementFactory *f = gst_element_factory_find (need[i].element); + if (f) { + gst_object_unref (f); + continue; + } + g_string_append_printf (missing, " %-12s %s\n", need[i].element, need[i].what); + } + + /* Publishing to most platforms needs H.264 out, not just in. Either + * encoder serves, so neither belongs in the list above. */ + GstElementFactory *x264 = gst_element_factory_find ("x264enc"); + GstElementFactory *oh = gst_element_factory_find ("openh264enc"); + + if (!x264 && !oh) + g_string_append (missing, + " x264enc H.264 encoding gst-plugins-ugly," + " or openh264enc from gst-plugins-bad\n"); + if (x264) gst_object_unref (x264); + if (oh) gst_object_unref (oh); + + /* + * webrtcbin existing is not the same as it working. Without an ICE + * agent it gathers no candidates and createAnswer() never settles - + * a call that hangs with no error at all. Worth building one once. + */ + GstElement *wrb = gst_element_factory_make ("webrtcbin", NULL); + if (!wrb) { + g_string_append (missing, " webrtcbin could not be created at all\n"); + } else { + GObject *agent = NULL; + g_object_get (wrb, "ice-agent", &agent, NULL); + if (!agent) + g_string_append (missing, + " ICE agent webrtcbin has none: no candidates will be\n" + " gathered and createAnswer() never returns.\n" + " libnice with GStreamer support is what provides it\n"); + else + g_object_unref (agent); + gst_object_unref (wrb); + } if (missing->len) { mlog ("gstreamer: these are missing, and pages will fail without them:"); g_printerr ("%s", missing->str); } - /* Not fatal, but WebKit complains about each at the moment it wants it. */ + /* + * Not fatal, but WebKit complains about each one at the moment it + * needs it, which reads like a fault. Listed once, up front, marked + * for what they are. + */ + static const struct { const char *element; const char *what; } nice_to_have[] = { + { "audiornnoise", "noise suppression gst-plugins-rs" }, + { "rtpgccbwe", "RTP bandwidth est. gst-plugins-rs" }, + }; GString *optional = g_string_new (NULL); - if (!have_element ("audiornnoise")) - g_string_append (optional, " audiornnoise noise suppression gst-plugins-rs\n"); - if (!have_element ("rtpgccbwe")) - g_string_append (optional, " rtpgccbwe bandwidth estimation gst-plugins-rs\n"); - if (!have_any_element (jpegdec)) - g_string_append (optional, " jpegdec MJPEG cameras gst-plugins-good (jpeg)\n"); - if (!have_element ("vp9enc") || !have_element ("vp9dec")) - g_string_append (optional, " vp9enc/dec VP9 video gst-plugins-good, libvpx\n"); + + for (gsize i = 0; i < G_N_ELEMENTS (nice_to_have); i++) { + GstElementFactory *f = gst_element_factory_find (nice_to_have[i].element); + if (f) { + gst_object_unref (f); + continue; + } + g_string_append_printf (optional, " %-12s %s\n", + nice_to_have[i].element, nice_to_have[i].what); + } if (optional->len) { mlog ("gstreamer: these are optional - pages work without them:"); @@ -2964,8 +2432,6 @@ cam_list (void) gst_object_unref (mon); return; } - if (!device_monitor_wait_started (mon, 5000)) - g_printerr ("cameras: device providers did not finish starting in 5 s\n"); GList *devs = gst_device_monitor_get_devices (mon); @@ -2991,6 +2457,10 @@ cam_list (void) g_free (name); } + if (devs) + g_print ("to ask for one of these whatever the page requests:\n" + " browser-big URL --cam-force 640x480@30\n"); + g_list_free_full (devs, gst_object_unref); gst_device_monitor_stop (mon); gst_object_unref (mon); @@ -3155,7 +2625,6 @@ big_cleanup (void) { g_free (g_shim_js); g_free (g_cam_match); - g_free (g_mic_match); } /* ----------------------------------------------------------------- main */ diff --git a/browser-mini b/browser-mini new file mode 100755 index 0000000..8db16ad Binary files /dev/null and b/browser-mini differ diff --git a/browser_core.c b/browser_core.c index 4d65b91..71bb4fe 100644 --- a/browser_core.c +++ b/browser_core.c @@ -108,7 +108,6 @@ static char *g_data_dir; /* profile data dir, NULL when private */ static char *g_profile; /* NULL -> "default" */ static gboolean g_clear_data; static char *g_user_agent; -static gboolean g_allow_popups; /* --allow-popups: window.open without a click */ static gboolean g_follow_page_title = TRUE; /* see --title / --page-title */ static char *g_clip_cmd; /* --clip-cmd, NULL -> wl-copy */ static gboolean g_no_middle_paste = TRUE; /* --enable-middle-click-paste */ @@ -143,12 +142,6 @@ static int g_real_stderr = -1; static int g_noise_fd = -1; /* read end, for the final drain */ static GHashTable *g_noise; /* line -> occurrences */ static gboolean g_no_sandbox; - -/* --lock-page / --lock-site: this window shows one address, or one site, - * and nothing else. */ -typedef enum { LOCK_OFF, LOCK_PAGE, LOCK_SITE } LockMode; -static LockMode g_lock_mode; -static char *g_lock_uri; /* the address as it was opened */ static gboolean g_want_page_title; static char *g_css_owned; /* when --css came from the config */ @@ -198,7 +191,6 @@ static gboolean on_decide_policy (WebKitWebView *view, WebKitPolicyDecision *d WebKitPolicyDecisionType type, gpointer u); static gboolean on_permission (WebKitWebView *view, WebKitPermissionRequest *req, gpointer u); -static Win *win_for_view (WebKitWebView *view); static GtkWidget *on_create (WebKitWebView *view, WebKitNavigationAction *act, gpointer u); static void on_ready_to_show (WebKitWebView *view, gpointer u); @@ -267,31 +259,20 @@ elide (const char *s, int max_chars) /* ------------------------------------------------------------- helpers */ -/* - * GST_PLUGIN_FEATURE_RANK is one variable that several options feed, and - * the user may have set it too. Append, never replace: GStreamer applies - * the entries in order, so a later one for the same element wins. - */ void -gst_rank_env_add (const char *spec) +settings_set_bool_if_exists (WebKitSettings *s, const char *prop, gboolean value) { - const char *had = g_getenv ("GST_PLUGIN_FEATURE_RANK"); - char *v = (had && *had) ? g_strconcat (had, ",", spec, NULL) : g_strdup (spec); - - g_setenv ("GST_PLUGIN_FEATURE_RANK", v, TRUE); - g_free (v); + if (g_object_class_find_property (G_OBJECT_GET_CLASS (s), prop)) + g_object_set (G_OBJECT (s), prop, value, NULL); + else + LOG ("note: WebKitSettings property not supported: %s\n", prop); } -/* A front-end asking for a WebKit runtime feature, same as --feature. */ void -feature_request (const char *spec) +object_set_string_if_exists (GObject *o, const char *prop, const char *value) { - if (!g_features) - g_features = g_ptr_array_new_with_free_func (g_free); - for (guint i = 0; i < g_features->len; i++) - if (!g_strcmp0 (g_ptr_array_index (g_features, i), spec)) - return; - g_ptr_array_add (g_features, g_strdup (spec)); + if (g_object_class_find_property (G_OBJECT_GET_CLASS (o), prop)) + g_object_set (o, prop, value, NULL); } /* "example.com" -> "https://example.com", "./page.html" -> "file:///...". */ @@ -690,49 +671,9 @@ dl_dir_for_uri (const char *uri) return (g_download_dir && *g_download_dir) ? g_download_dir : "."; } -/* - * --lock-page / --lock-site. The address bar, a link, a redirect, a - * script and a popup all end up in decide-policy, so one check there - * covers every way to leave the page. A fragment is the same page. - */ -static gboolean -nav_allowed (const char *uri) -{ - if (g_lock_mode == LOCK_OFF || !g_lock_uri || !uri || !*uri) - return TRUE; - - if (g_lock_mode == LOCK_SITE) { - char *a = uri_host (uri); - char *b = uri_host (g_lock_uri); - - /* A file:// address has no host and so has no site either. Rather - * than letting every local file through, or none, the lock falls - * back to the single page. */ - if (a && b && *a && *b) { - gboolean ok = !g_ascii_strcasecmp (a, b); - g_free (a); - g_free (b); - return ok; - } - g_free (a); - g_free (b); - } - - gsize n = strcspn (uri, "#"); - gsize m = strcspn (g_lock_uri, "#"); - return n == m && !strncmp (uri, g_lock_uri, n); -} - -/* - * Which scope the popup opens on. Not the same as which rule answers for - * the address right now. - * A page with a rule of its own offers to change that rule. A page that - * is only covered by its site's rule offers a rule of its own instead: - * somebody setting a directory while looking at a subpage means that - * subpage, and the narrower rule wins without disturbing the site's. - */ +/* Which rule is answering for this address right now. */ static DlScope -dl_scope_default_for_uri (const char *uri) +dl_scope_for_uri (const char *uri) { if (uri && g_hash_table_contains (g_dlrules, uri)) return DL_SCOPE_PAGE; @@ -741,7 +682,7 @@ dl_scope_default_for_uri (const char *uri) gboolean site = host && g_hash_table_contains (g_dlrules, host); g_free (host); - return site ? DL_SCOPE_PAGE : DL_SCOPE_ALL; + return site ? DL_SCOPE_SITE : DL_SCOPE_ALL; } /* Downloads are often shared with other users or a daemon, so the target @@ -867,6 +808,7 @@ dl_dir_set (const char *uri, const char *dir, DlScope scope) g_free (g_download_dir); g_download_dir = g_strdup (dir); + object_set_string_if_exists (G_OBJECT (g_session), "downloads-directory", g_download_dir); } /* ------------------------------------------------------- search keywords */ @@ -2078,18 +2020,10 @@ cfg_set (const char *k, const char *v) if (key_is (k, "download_dir")) { set_str (&g_download_dir, v); return TRUE; } if (key_is (k, "profile")) { set_str (&g_profile, v); return TRUE; } if (key_is (k, "user_agent")) { set_str (&g_user_agent, v); return TRUE; } - if (key_is (k, "popups")) { g_allow_popups = !g_ascii_strcasecmp (v, "allow") || truthy (v); return TRUE; } if (key_is (k, "css")) { set_str (&g_css_owned, v); g_css_path = g_css_owned; return TRUE; } if (key_is (k, "zoom")) { g_zoom = CLAMP (g_ascii_strtod (v, NULL), ZOOM_MIN, ZOOM_MAX); return TRUE; } if (key_is (k, "private")) { g_private = truthy (v); return TRUE; } if (key_is (k, "no_media")) { g_deny_media = truthy (v); return TRUE; } - if (key_is (k, "lock")) { - if (!g_ascii_strcasecmp (v, "page")) g_lock_mode = LOCK_PAGE; - else if (!g_ascii_strcasecmp (v, "site")) g_lock_mode = LOCK_SITE; - else if (!truthy (v)) g_lock_mode = LOCK_OFF; - else g_printerr ("config: lock must be page, site or no\n"); - return TRUE; - } if (key_is (k, "quiet")) { g_quiet = truthy (v); return TRUE; } if (key_is (k, "page_title")) { g_want_page_title = truthy (v); return TRUE; } if (key_is (k, "select_all")) { g_fix_select_all = truthy (v); return TRUE; } @@ -2353,8 +2287,7 @@ usage (const char *argv0, gboolean to_stdout) " --no-gpu hardware acceleration policy NEVER\n" " --no-dmabuf WEBKIT_DISABLE_DMABUF_RENDERER=1\n" " --no-compositing WEBKIT_DISABLE_COMPOSITING_MODE=1\n" -" --no-hw-decode rank the hardware video decoders out\n" -" (GST_PLUGIN_FEATURE_RANK), so software decodes\n" +" --no-hw-decode WEBKIT_GST_ENABLE_HW_DECODERS=0\n" " the four to reach for when a machine comes back up\n" " and pages render blank or zero sized\n" " --enable-middle-click-paste\n" @@ -2399,9 +2332,6 @@ usage (const char *argv0, gboolean to_stdout) " what is on disk now, then exit. Follows --profile\n" " and --private wherever they sit on the line\n" " --user-agent UA set an explicit user agent string\n" -" --allow-popups let pages open windows without a click (Zoom,\n" -" meeting and login flows that open a tab after a\n" -" server round trip). popups = allow in a config file\n" " --ua-chrome | --ua-win-chrome | --ua-win-edge | --ua-firefox | --ua-safari\n" "\n" "history:\n" @@ -2433,9 +2363,8 @@ usage (const char *argv0, gboolean to_stdout) " font font_mono label_px title_px hint_px\n" " ours: title app_id zoom mod clip_cmd download_dir profile private\n" " no_media page_title middle_click_paste select_all load_bar\n" -" lock (page, site or no: stay on the address given)\n" " css\n" -" user_agent quiet popups (allow: see --allow-popups)\n" +" user_agent quiet\n" " always_overwrite search_default (which keyword needs no prefix)\n" " gsk (GTK's renderer: gl, cairo, vulkan, ngl)\n" " feature = Name[=on|off] (a WebKit runtime feature; repeatable)\n" @@ -2470,13 +2399,6 @@ usage (const char *argv0, gboolean to_stdout) " sampled every 5s and, when it spins at 90%%+ cpu for\n" " 15s, its running and blocked threads are printed -\n" " the thing to read when a page freezes\n" -" --lock-page show the address given and nothing else: a link, a\n" -" redirect, a script or the address bar leading\n" -" anywhere else is refused, and no second window is\n" -" opened. A fragment counts as the same page\n" -" --lock-site the same, but anything on that host is allowed.\n" -" Use it when a login or a payment step leaves the\n" -" page. lock = page|site in a config file\n" " --no-console do not print the page's own console output, while\n" " keeping ours. A page that logs heavily makes the\n" " web process wait on every message\n" @@ -2752,10 +2674,6 @@ ui_css_install (void) "}", c_text, ui_font, t->label_px * s); - /* an error toast: the same panel, the failure colour, and wrapped */ - g_string_append_printf (css, - "label.br-toast.br-toast-error { color: %s; }", c_urgent); - /* URLs and file names: fixed width, so they line up and elide sanely */ g_string_append_printf (css, "entry.br-omni-entry, entry.br-omni-entry > text," @@ -3312,21 +3230,6 @@ on_decide_policy (WebKitWebView *view, webkit_policy_decision_ignore (decision); return TRUE; } - - WebKitURIRequest *req = act ? webkit_navigation_action_get_request (act) : NULL; - const char *to = req ? webkit_uri_request_get_uri (req) : NULL; - - if (!nav_allowed (to)) { - Win *w = win_for_view (view); - char *msg = g_strdup_printf ("locked to %s", - g_lock_mode == LOCK_SITE ? "this site" : "this page"); - LOG ("lock: refused %s\n", to ? to : "(no address)"); - if (w) - toast_show (w, msg, 3); - g_free (msg); - webkit_policy_decision_ignore (decision); - return TRUE; - } return FALSE; } @@ -3361,20 +3264,40 @@ on_decide_policy (WebKitWebView *view, /* ----------------------------------------------------------- permissions */ +/* + * Which permission-request types reach the public headers moves between + * WebKitGTK releases: 2.54.0 drops WebKitPointerLockPermissionRequest.h + * from webkit.h by accident, and a missing WEBKIT_IS_* macro is a build + * failure rather than a type that is merely absent. So the type is matched + * by name at runtime. A name nothing has registered cannot be the type of + * a live request, so a type this build does not have answers FALSE. + */ +static gboolean +perm_is (WebKitPermissionRequest *req, const char *type_name) +{ + GType t = g_type_from_name (type_name); + return t != 0 && G_TYPE_CHECK_INSTANCE_TYPE (req, t); +} + +static const struct { const char *type, *name; } PERM_TYPES[] = { + { "WebKitUserMediaPermissionRequest", "user-media" }, + { "WebKitDeviceInfoPermissionRequest", "device-info" }, + { "WebKitPointerLockPermissionRequest", "pointer-lock" }, + { "WebKitGeolocationPermissionRequest", "geolocation" }, + { "WebKitNotificationPermissionRequest", "notification" }, + { "WebKitClipboardPermissionRequest", "clipboard" }, + { "WebKitMediaKeySystemPermissionRequest", "media-key-system" }, + { "WebKitWebsiteDataAccessPermissionRequest", "website-data-access" }, +}; + static const char * perm_type_name (WebKitPermissionRequest *req) { - if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST (req)) return "user-media"; - /* 2.54 only declares it when WebKit was built with pointer lock */ -#ifdef WEBKIT_TYPE_POINTER_LOCK_PERMISSION_REQUEST - if (WEBKIT_IS_POINTER_LOCK_PERMISSION_REQUEST (req)) return "pointer-lock"; -#endif - if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST (req)) return "geolocation"; - if (WEBKIT_IS_NOTIFICATION_PERMISSION_REQUEST (req)) return "notification"; - if (WEBKIT_IS_CLIPBOARD_PERMISSION_REQUEST (req)) return "clipboard"; - if (WEBKIT_IS_MEDIA_KEY_SYSTEM_PERMISSION_REQUEST (req)) return "media-key-system"; - if (WEBKIT_IS_WEBSITE_DATA_ACCESS_PERMISSION_REQUEST (req)) return "website-data-access"; - return "other"; + for (gsize i = 0; i < G_N_ELEMENTS (PERM_TYPES); i++) + if (perm_is (req, PERM_TYPES[i].type)) + return PERM_TYPES[i].name; + + return G_OBJECT_TYPE_NAME (req); /* new to this WebKit: name it anyway */ } static Win * @@ -3445,9 +3368,7 @@ media_spawn (Win *w, const char *cmd, const char *arg, const char *shown) int argc = 0; if (!g_shell_parse_argv (cmd, &argc, &args, &err)) { - char *msg = g_strdup_printf ("media: cannot parse \"%s\": %s", cmd, err->message); - toast_error (w, msg); - g_free (msg); + g_printerr ("media: cannot parse \"%s\": %s\n", cmd, err->message); g_clear_error (&err); return FALSE; } @@ -3462,14 +3383,13 @@ media_spawn (Win *w, const char *cmd, const char *arg, const char *shown) G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, &err); char *msg = ok ? g_strdup_printf ("%s %s", args[0], shown) - : g_strdup_printf ("media: cannot start %s: %s", args[0], err->message); - if (ok) { + : g_strdup_printf ("%s: %s", args[0], err->message); + if (ok) LOG ("media: %s %s\n", cmd, arg); - if (w) - toast_show (w, msg, 2); - } else { - toast_error (w, msg); - } + else + g_printerr ("media: %s: %s\n", cmd, err->message); + if (w) + toast_show (w, msg, ok ? 2 : URL_TOAST_SECONDS); g_free (msg); g_clear_error (&err); @@ -3539,9 +3459,12 @@ on_image_fetched (GObject *src, GAsyncResult *res, gpointer u) char *why = err ? g_strdup (err->message) : g_strdup_printf ("HTTP %u, %" G_GSIZE_FORMAT " bytes", code, body ? g_bytes_get_size (body) : 0); - char *t = g_strdup_printf ("media: image not fetched: %s\n%s", why, f->uri); - toast_error (w, t); - g_free (t); + g_printerr ("media: image %s: %s\n", f->uri, why); + if (w) { + char *t = g_strdup_printf ("image not fetched: %s", why); + toast_show (w, t, URL_TOAST_SECONDS); + g_free (t); + } g_free (why); } else { char *dir = media_tmp_dir (); @@ -3552,17 +3475,12 @@ on_image_fetched (GObject *src, GAsyncResult *res, gpointer u) const guint8 *p = g_bytes_get_data (body, &n); gboolean wrote = write (fd, p, n) == (ssize_t) n; close (fd); - if (wrote) { + if (wrote) media_spawn (w, g_image_viewer, tmpl, f->uri); - } else { - char *t = g_strdup_printf ("media: cannot write %s", tmpl); - toast_error (w, t); - g_free (t); - } + else + g_printerr ("media: cannot write %s\n", tmpl); } else { - char *t = g_strdup_printf ("media: cannot create a file in %s", dir); - toast_error (w, t); - g_free (t); + g_printerr ("media: cannot create a file in %s\n", dir); } g_free (tmpl); g_free (dir); @@ -3858,17 +3776,29 @@ on_permission (WebKitWebView *view, WebKitPermissionRequest *req, gpointer u) return TRUE; } - /* Device labels for enumerateDevices() are not decided here: WebKit - * 2.54 no longer emits WebKitDeviceInfoPermissionRequest and asks - * query-permission-state instead, see on_query_permission(). */ + /* + * Device labels for enumerateDevices(). A browser reveals them once + * the site has been granted a device, and not before; the same rule + * applies here, read from what was remembered. + */ + if (WEBKIT_IS_DEVICE_INFO_PERMISSION_REQUEST (req)) { + const char *had = perm_remembered (host); + gboolean allow = g_media_policy == MEDIA_ALLOW || + (g_media_policy == MEDIA_ASK && had && !strcmp (had, "allow")); + LOG ("perm: %s -> %s\n", type, allow ? "allow" : "deny"); + if (allow) + webkit_permission_request_allow (req); + else + webkit_permission_request_deny (req); + g_free (host); + return TRUE; + } -#ifdef WEBKIT_TYPE_POINTER_LOCK_PERMISSION_REQUEST - if (WEBKIT_IS_POINTER_LOCK_PERMISSION_REQUEST (req)) { + if (perm_is (req, "WebKitPointerLockPermissionRequest")) { webkit_permission_request_allow (req); g_free (host); return TRUE; } -#endif LOG ("perm: %s -> deny\n", type); webkit_permission_request_deny (req); @@ -3876,45 +3806,6 @@ on_permission (WebKitWebView *view, WebKitPermissionRequest *req, gpointer u) return TRUE; } -/* - * navigator.permissions.query({name:'camera'|'microphone'}), and WebKit's - * own question before enumerateDevices() and getUserMedia(): is this site - * already allowed? Unanswered, WebKit's documented default is "prompt", - * so a remembered site would never be told it is allowed and device - * labels would stay hidden. Answered from the same per-site memory as the - * prompt, keyed by the top-level origin's host. - */ -static gboolean -on_query_permission (WebKitWebView *view, WebKitPermissionStateQuery *q, gpointer u) -{ - (void) view; (void) u; - const char *name = webkit_permission_state_query_get_name (q); - - if (g_strcmp0 (name, "camera") != 0 && g_strcmp0 (name, "microphone") != 0) - return FALSE; /* WebKit answers "prompt" */ - - WebKitSecurityOrigin *o = webkit_permission_state_query_get_security_origin (q); - const char *host = o ? webkit_security_origin_get_host (o) : NULL; - WebKitPermissionState st; - - if (g_media_policy == MEDIA_ALLOW) - st = WEBKIT_PERMISSION_STATE_GRANTED; - else if (g_media_policy == MEDIA_DENY) - st = WEBKIT_PERMISSION_STATE_DENIED; - else { - const char *had = perm_remembered (host); - if (!had) - return FALSE; - st = !strcmp (had, "allow") ? WEBKIT_PERMISSION_STATE_GRANTED - : WEBKIT_PERMISSION_STATE_DENIED; - } - - LOG ("perm: query %s for %s -> %s\n", name, host ? host : "?", - st == WEBKIT_PERMISSION_STATE_GRANTED ? "granted" : "denied"); - webkit_permission_state_query_finish (q, st); - return TRUE; -} - /* ------------------------------------------------------------ inspector */ static void @@ -4787,12 +4678,11 @@ omni_setup_scope (Win *w, OmniMode mode, const char *uri) GtkStringList *list = gtk_string_list_new (items); gtk_drop_down_set_model (GTK_DROP_DOWN (w->omniscope), G_LIST_MODEL (list)); - DlScope now = dl_scope_default_for_uri (uri); - - gtk_drop_down_set_selected (GTK_DROP_DOWN (w->omniscope), now); + gtk_drop_down_set_selected (GTK_DROP_DOWN (w->omniscope), dl_scope_for_uri (uri)); gtk_widget_set_visible (w->omniscope, TRUE); /* the same select decides how far this reaches */ + DlScope now = dl_scope_for_uri (uri); char *key = now == DL_SCOPE_PAGE ? g_strdup (uri) : now == DL_SCOPE_SITE ? uri_host (uri) : NULL; @@ -4821,17 +4711,6 @@ omni_show (Win *w, OmniMode mode) w->omni_needle = g_strdup (""); w->omni_mode = mode; - /* The download-directory clash question hides the input and turns the - * hint into its question. Whatever way it was left - answered, Esc, a - * click outside - the next popup starts from the ordinary layout, and - * an unanswered question is dropped rather than answered later. */ - w->dl_conflict = FALSE; - g_clear_pointer (&w->dl_pending_dir, g_free); - gtk_widget_set_visible (w->omnientry, TRUE); - gtk_label_set_wrap (GTK_LABEL (w->omnihint), FALSE); - gtk_widget_set_hexpand (w->omnihint, FALSE); - gtk_widget_set_halign (w->omnihint, GTK_ALIGN_END); - w->omni_setting = TRUE; gtk_editable_set_text (GTK_EDITABLE (w->omnientry), mode == OMNI_URL ? (uri ? uri : "") @@ -5127,10 +5006,7 @@ omni_apply_dldir (Win *w) : g_strdup_printf ("%u rules already go there. Enter drops them, Esc keeps all.", users->len); gtk_label_set_text (GTK_LABEL (w->omnihint), ask); - gtk_label_set_wrap (GTK_LABEL (w->omnihint), TRUE); - gtk_label_set_xalign (GTK_LABEL (w->omnihint), 0.0); - gtk_widget_set_hexpand (w->omnihint, TRUE); - gtk_widget_set_halign (w->omnihint, GTK_ALIGN_FILL); + gtk_widget_set_halign (w->omnihint, GTK_ALIGN_START); gtk_widget_set_visible (w->omnihint, TRUE); gtk_widget_set_visible (w->omnientry, FALSE); gtk_widget_set_visible (w->omniscope, FALSE); @@ -5257,8 +5133,6 @@ toast_single_line (Win *w) GtkLabel *l = GTK_LABEL (w->toast); gtk_widget_remove_css_class (w->toast, "br-toast-url"); - gtk_widget_remove_css_class (w->toast, "br-toast-error"); - w->error_until_us = 0; gtk_label_set_wrap (l, FALSE); gtk_label_set_lines (l, -1); gtk_label_set_ellipsize (l, PANGO_ELLIPSIZE_MIDDLE); @@ -5276,41 +5150,6 @@ toast_show (Win *w, const char *text, guint seconds) g_source_remove (w->toast_id); w->toast_id = g_timeout_add_seconds (seconds, toast_timeout, w); } - -/* - * Something the user asked for did not happen. A message that is cut in - * the middle, gone in four seconds or faded under the pointer is no - * message at all, so this one is wrapped whole, in the failure colour, - * stays up for ERROR_TOAST_SECONDS, does not fade, and always goes to - * stderr as well - -q or not. - */ -#define ERROR_TOAST_SECONDS 10 - -void -toast_error (Win *w, const char *text) -{ - g_printerr ("error: %s\n", text); - if (!w) - return; - - GtkLabel *l = GTK_LABEL (w->toast); - - toast_single_line (w); - gtk_widget_add_css_class (w->toast, "br-toast-error"); - gtk_label_set_ellipsize (l, PANGO_ELLIPSIZE_NONE); - gtk_label_set_wrap (l, TRUE); - gtk_label_set_wrap_mode (l, PANGO_WRAP_WORD_CHAR); - gtk_label_set_max_width_chars (l, 60); - gtk_label_set_xalign (l, 0.0); - gtk_label_set_text (l, text); - gtk_widget_set_opacity (w->topright, 1.0); - gtk_widget_set_visible (w->toast, TRUE); - w->error_until_us = g_get_monotonic_time () + (gint64) ERROR_TOAST_SECONDS * G_USEC_PER_SEC; - - if (w->toast_id) - g_source_remove (w->toast_id); - w->toast_id = g_timeout_add_seconds (ERROR_TOAST_SECONDS, toast_timeout, w); -} /* * <mod>+P: the whole address. It is wrapped at any character, in the mono * font, and as wide as the window allows, so nothing is cut out. Only a @@ -5717,9 +5556,7 @@ overlay_hover_update (Win *w, double x, double y) g_get_monotonic_time () - w->dl_reveal_us < (gint64) DL_REVEAL_MS * 1000; - gboolean keep_err = w->error_until_us && g_get_monotonic_time () < w->error_until_us; - - fade_if_under (w, w->topright, x, y, keep_dl || keep_err); + fade_if_under (w, w->topright, x, y, keep_dl); fade_if_under (w, w->urltoast, x, y, FALSE); } @@ -6357,16 +6194,8 @@ static void media_mode_toggle (Win *w) { if ((!g_player || !*g_player) && (!g_image_viewer || !*g_image_viewer)) { - char *cfg = g_build_filename (g_get_user_config_dir (), g_app->default_app_id, - "config", NULL); - char *msg = g_strdup_printf ("F2 media mode: no player or image viewer is set.\n" - "Add to %s:\n" - " player = mpv\n" - " image_viewer = imv\n" - "or start with --player mpv", cfg); - toast_error (w, msg); - g_free (msg); - g_free (cfg); + toast_show (w, "media mode needs player = mpv and/or image_viewer = imv " + "in the config", URL_TOAST_SECONDS); return; } @@ -7238,7 +7067,6 @@ view_wire (WebKitWebView *view) * session, not on the web view - it is wired up once in browser_main(). */ g_signal_connect (view, "decide-policy", G_CALLBACK (on_decide_policy), NULL); g_signal_connect (view, "permission-request", G_CALLBACK (on_permission), NULL); - g_signal_connect (view, "query-permission-state", G_CALLBACK (on_query_permission), NULL); g_signal_connect (view, "create", G_CALLBACK (on_create), NULL); g_signal_connect (view, "load-changed", G_CALLBACK (on_load_core), NULL); g_signal_connect (view, "load-failed", G_CALLBACK (on_load_failed_core), NULL); @@ -7289,12 +7117,6 @@ on_create (WebKitWebView *view, WebKitNavigationAction *act, gpointer u) { (void) act; (void) u; - /* A second window would be a way around the lock, so there is none. */ - if (g_lock_mode != LOCK_OFF) { - LOG ("lock: refused a new window\n"); - return NULL; - } - /* popups (SSO, OAuth, "open in new window" players) share the session */ WebKitWebView *nv = view_new (view); g_signal_connect (nv, "ready-to-show", G_CALLBACK (on_ready_to_show), NULL); @@ -7358,6 +7180,13 @@ setup_settings (void) /* best effort at behaving like a mainstream browser */ webkit_settings_set_enable_site_specific_quirks (g_settings, TRUE); + /* Cloudflare/Turnstile sometimes correlates missing GPU features with bots */ + settings_set_bool_if_exists (g_settings, "enable-webgl", TRUE); + settings_set_bool_if_exists (g_settings, "enable-accelerated-2d-canvas", TRUE); + + /* lets a player pick a codec the build actually has, instead of + * negotiating one it cannot decode and then stalling */ + settings_set_bool_if_exists (g_settings, "enable-media-capabilities", TRUE); if (g_user_agent && *g_user_agent) { webkit_settings_set_user_agent (g_settings, g_user_agent); @@ -7369,12 +7198,6 @@ setup_settings (void) * --no-devtools */ webkit_settings_set_enable_developer_extras (g_settings, !g_no_devtools); webkit_settings_set_enable_media_stream (g_settings, TRUE); - /* Off, WebKit returns null from a window.open that no click started - * - and a click stops counting once the page has waited on a fetch. - * Zoom opens its meeting tab exactly that way. */ - webkit_settings_set_javascript_can_open_windows_automatically (g_settings, g_allow_popups); - if (g_allow_popups) - LOG ("popups: pages may open windows without a click\n"); webkit_settings_set_enable_webrtc (g_settings, TRUE); webkit_settings_set_enable_mediasource (g_settings, TRUE); webkit_settings_set_enable_encrypted_media (g_settings, TRUE); @@ -7687,8 +7510,7 @@ browser_main (int argc, char **argv, const BrowserApp *app) cfg_set ("player_match", argv[++i]); } else if (!strcmp (a, "--zoom")) { NEED_ARG ("--zoom"); - const char *zv = argv[++i]; - g_zoom = CLAMP (g_ascii_strtod (zv, NULL), ZOOM_MIN, ZOOM_MAX); + g_zoom = CLAMP (g_ascii_strtod (argv[++i], NULL), ZOOM_MIN, ZOOM_MAX); } else if (!strcmp (a, "--css")) { NEED_ARG ("--css"); g_css_path = argv[++i]; @@ -7729,10 +7551,6 @@ browser_main (int argc, char **argv, const BrowserApp *app) g_gsk_renderer = argv[++i]; } else if (!strcmp (a, "--no-proc-watch")) { g_proc_watch = FALSE; - } else if (!strcmp (a, "--lock-page")) { - g_lock_mode = LOCK_PAGE; - } else if (!strcmp (a, "--lock-site")) { - g_lock_mode = LOCK_SITE; } else if (!strcmp (a, "--no-console")) { g_no_console = TRUE; } else if (!strcmp (a, "--all-messages")) { @@ -7763,8 +7581,6 @@ browser_main (int argc, char **argv, const BrowserApp *app) g_profile = g_strdup (argv[++i]); } else if (!strcmp (a, "--clear-data")) { g_clear_data = TRUE; - } else if (!strcmp (a, "--allow-popups")) { - g_allow_popups = TRUE; } else if (!strcmp (a, "--user-agent")) { NEED_ARG ("--user-agent"); g_free (g_user_agent); @@ -7805,13 +7621,6 @@ browser_main (int argc, char **argv, const BrowserApp *app) } #undef NEED_ARG - /* Locking needs something to lock to, and saying so before the - * window is built is cheaper than after it. */ - if (g_lock_mode != LOCK_OFF && !url_arg) { - g_printerr ("%s: --lock-page/--lock-site needs an address to stay on\n", argv[0]); - return 1; - } - /* The page <title> drives the window title by default. An explicit * --title pins it instead, unless --page-title is also given - then * --title is just the fallback for pages with no title of their own. */ @@ -7894,17 +7703,7 @@ browser_main (int argc, char **argv, const BrowserApp *app) } if (g_no_dmabuf) g_setenv ("WEBKIT_DISABLE_DMABUF_RENDERER", "1", TRUE); if (g_no_compositing) g_setenv ("WEBKIT_DISABLE_COMPOSITING_MODE", "1", TRUE); - /* WebKitGTK 2.54 has no switch of its own for this (the old - * WEBKIT_GST_ENABLE_HW_DECODERS is gone). GStreamer's documented way - * is the feature rank: a decoder at NONE is never autoplugged. */ - if (g_no_hw_decode) - gst_rank_env_add ("vah264dec:NONE,vah265dec:NONE,vavp8dec:NONE,vavp9dec:NONE," - "vaav1dec:NONE,vampeg2dec:NONE,vajpegdec:NONE," - "vaapih264dec:NONE,vaapih265dec:NONE,vaapivp8dec:NONE," - "vaapivp9dec:NONE,vaapiav1dec:NONE,vaapidecodebin:NONE," - "v4l2slh264dec:NONE,v4l2slh265dec:NONE,v4l2slvp8dec:NONE," - "v4l2slvp9dec:NONE,v4l2slav1dec:NONE,v4l2h264dec:NONE," - "v4l2h265dec:NONE,v4l2vp8dec:NONE,v4l2vp9dec:NONE"); + if (g_no_hw_decode) g_setenv ("WEBKIT_GST_ENABLE_HW_DECODERS", "0", TRUE); if (app->pre_gtk) app->pre_gtk (); @@ -7932,6 +7731,7 @@ browser_main (int argc, char **argv, const BrowserApp *app) setup_session (); g_signal_connect (g_session, "download-started", G_CALLBACK (on_session_download_started), NULL); + object_set_string_if_exists (G_OBJECT (g_session), "downloads-directory", g_download_dir); history_setup (g_data_dir); dlrules_setup (g_data_dir); /* app wide; the profile is only migrated from */ @@ -7972,31 +7772,16 @@ browser_main (int argc, char **argv, const BrowserApp *app) gtk_window_present (GTK_WINDOW (win)); } else { /* the front-end may claim the address, e.g. browser-big's "diag" */ - char *url = normalize_uri (url_arg); - - /* The lock is measured against the address as given, before the - * front-end gets a chance to serve something of its own. */ - if (g_lock_mode != LOCK_OFF) { - if (!url) { - usage_short (argv[0]); - noise_drain (); - return 1; - } - g_lock_uri = g_strdup (url); - LOG ("lock: %s only (%s)\n", - g_lock_mode == LOCK_SITE ? "this site" : "this page", g_lock_uri); - } - if (!(app->load_uri && app->load_uri (view, url_arg))) { + char *url = normalize_uri (url_arg); if (!url) { usage_short (argv[0]); - noise_drain (); return 1; } LOG ("load: %s\n", url); webkit_web_view_load_uri (view, url); + g_free (url); } - g_free (url); gtk_window_present (GTK_WINDOW (win)); } @@ -8018,7 +7803,6 @@ browser_main (int argc, char **argv, const BrowserApp *app) g_free (g_profile); g_free (g_user_agent); g_free (g_css_owned); - g_free (g_lock_uri); g_free (g_theme.font); g_free (g_theme.font_mono); return 0; diff --git a/browser_core.h b/browser_core.h index 8c9cc5d..93299b4 100644 --- a/browser_core.h +++ b/browser_core.h @@ -61,7 +61,6 @@ typedef struct { guint dl_history_id; gboolean primary; gint64 dl_reveal_us; /* a download just appeared; do not fade */ - gint64 error_until_us; /* an error toast is up; do not fade */ /* stored-history walk, see history.c section in browser_core.c */ gboolean hist_walk; /* past the end of the session list */ @@ -105,10 +104,7 @@ typedef struct { void (*usage_options) (GString *out); /* extra option sections */ void (*usage_keys) (GString *out); /* extra key bindings */ - /* Return TRUE if argv[*i] was consumed; advance *i over its value. - * Read that value into a local first: MAX/MIN/CLAMP are macros that - * evaluate their arguments more than once, so MAX (0, atoi (argv[++(*i)])) - * advances *i twice and swallows the next option's value. */ + /* Return TRUE if argv[*i] was consumed; advance *i over its value. */ gboolean (*parse_arg) (int argc, char **argv, int *i); void (*pre_gtk) (void); /* env, before gtk_init() */ @@ -172,11 +168,10 @@ extern const char *g_mod_name; Win *win_of (WebKitWebView *view); void toast_show (Win *w, const char *text, guint seconds); -void toast_error (Win *w, const char *text); /* red, wrapped, 10 s, also stderr */ void css_reload (void); void view_eval (WebKitWebView *view, const char *js); char *normalize_uri (const char *in); -void gst_rank_env_add (const char *spec); /* append to GST_PLUGIN_FEATURE_RANK */ -void feature_request (const char *spec); /* --feature NAME[=on|off], from a front-end */ +void settings_set_bool_if_exists (WebKitSettings *s, const char *prop, gboolean value); +void object_set_string_if_exists (GObject *o, const char *prop, const char *value); #endif /* BROWSER_CORE_H */