Browser git · main
Minimal browser in C (mini and big variant)
C 95.8% Markdown 3.9%git clone https://git.christianimmanuel.de/web-tools/Browser.gitwget https://git.christianimmanuel.de/web-tools/Browser/archive/Browser.tar.gzTry to let Fable 5.1 cleanup And stuff maybe
HANDOVER.md | 170 ++++++++++++++++++++++++++++++++++++++++ Makefile | 2 +- README.md | 158 ++++++++++++++++++------------------- browser-big.c | 242 +++++++++++++++++++++++++++++++++++++++++++++++++++------ browser_core.c | 224 ++++++++++++++-------------------------------------- browser_core.h | 6 +- 6 files changed, 527 insertions(+), 275 deletions(-) diff --git a/HANDOVER.md b/HANDOVER.md new file mode 100644 index 0000000..2de5b08 --- /dev/null +++ b/HANDOVER.md @@ -0,0 +1,170 @@ +# 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 5000b7d..c9b8404 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CC ?= gcc CFLAGS ?= -O2 -Wall -Wextra -rdynamic PREFIX ?= /usr -VERSION := 4.19.1 +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 eac1b1a..d9259f3 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,7 @@ Two WebKitGTK 6.0 (GTK4) page viewers sharing one core. **100% Vibecode but tested** — built warning-free with `-Wall -Wextra` against -the WebKitGTK 2.54.0 and 2.52.6 headers, linked and run headless under Xvfb -on 2.52.6. Every WebKit call, signal and property was checked against the -2.54.0 sources (`Source/WebKit/UIProcess/API/glib`) and the GStreamer 1.28 -sources; what they contradicted was removed. +GTK 4.14 / WebKitGTK 2.52.3, and run headless under Xvfb. ## Layout @@ -107,10 +104,8 @@ 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`. With the option on, -JavaScriptCore exposes the constructor to every page, isolated or not -(`JSGlobalObject.cpp`); `rtc-caps` still reports `crossOriginIsolated` -because some libraries check it themselves. +`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 @@ -140,14 +135,11 @@ 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 has but keeps off +## APIs WebKitGTK does not implement -`screen.orientation` is implemented, but the `ScreenOrientationAPI` runtime -feature defaults to off on GTK (`WebPreferencesDefaultValues.cpp`), so -Zoom's media code reads `screen.orientation.type` and throws. `--web-compat` -turns the real feature on and, only if a page still sees nothing, supplies a -static object. `--feature ScreenOrientationAPI` on its own does the first -half. +`--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 @@ -163,11 +155,6 @@ it starts, and both are repaired in the offer rather than in the site: | `--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 | -| `--ice-libnice` | give webrtcbin its stock libnice ICE agent in the web process. Since 2.52 WebKit hands it its own **librice** agent whose sockets, STUN, TURN and DNS lookups live in the network process (`GStreamerMediaEndpoint.cpp`, `RiceBackend.cpp`); 2.54 reworked that backend and it still carries FIXMEs (TURN allocations are UDP only, only the first DNS answer is used, the UDP port range is ignored). This is the switch between the two implementations; `ice = libnice` in the config | - -`--ice-libnice` is the first thing to try when `pc1` works, the call -connects on the LAN, and it does not connect over the internet: everything -else is the same and only the ICE implementation changes. 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 @@ -414,7 +401,7 @@ the whole cost. ## Version ``` -browser-mini --version # browser-mini 4.19.0 (build …) +browser-mini --version # browser-mini 4.18.0 (build 4bbf154) make version ``` @@ -709,9 +696,8 @@ 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 # ScreenOrientationAPI on, for Zoom's media engine +web_compat = yes # screen.orientation for Zoom's media engine feature = OffscreenCanvas=off # no worker-side WebGL attempt, so no CPU fallback -# ice = libnice # if calls connect on the LAN but not over the internet ``` Two things that used to be here are now the default, because they are safe @@ -733,11 +719,10 @@ they got. Each rung adds one layer, so the first one that fails names it. | Page | Tests | Flags to try if it fails | | --- | --- | --- | | `src/content/getusermedia/gum/` | camera only, no audio, no WebRTC | `--rtc-trace` to see it | -| `src/content/getusermedia/audio/` | microphone only | `--prewarm` to see the providers | +| `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, no ICE servers | `--video-codecs VP8` | -| any real call to another machine | STUN, TURN, DNS — the part `pc1` never exercises | `--ice-libnice`, `--rtc-trace` | +| `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` | `pc1` is the important one: two peer connections in one page, offer, @@ -755,12 +740,9 @@ browser-big https://webrtc.github.io/samples/src/content/peerconnection/pc1/ --r 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`. WebKit 2.54 no longer asks about device labels -with a separate request: before `enumerateDevices()` and `getUserMedia()` -it emits `query-permission-state` for `camera` and `microphone`, and an -unanswered query counts as *prompt*. Both browsers answer it from the same -remembered file, so a site that was allowed once gets its labels, and -`navigator.permissions.query()` tells the page the truth. +`<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 @@ -774,33 +756,63 @@ policy, and it is gone. ## When the camera is not picked up -WebKit 2.54 finds cameras the way `gst-device-monitor-1.0 Video/Source` -does: with GStreamer's device monitor, which on a plain Linux box means -`v4l2deviceprovider` from gst-plugins-good and readable `/dev/video*` -nodes. The desktop portal is asked only when a PipeWire ≥ 0.3.64 device -provider is installed (`PipeWireCaptureDeviceManager.cpp`); without one the -portal, D-Bus and the machine id play no part. Microphones come from -`alsadeviceprovider` or `pulsedeviceprovider` the same way. +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. + +The usual cause is a missing machine id, which makes D-Bus unable to start +at all: -So the checks are: +A machine id is a D-Bus requirement, not a systemd one. Either tool works, +and neither needs an init system: ``` -browser-big --list-cameras # what the device monitor sees -gst-device-monitor-1.0 Video/Source # the same, from GStreamer itself -gst-inspect-1.0 v4l2deviceprovider alsadeviceprovider -ls -l /dev/video* ; id # readable? group video? +sudo dbus-uuidgen --ensure=/etc/machine-id # any distro with dbus +sudo ln -sf /etc/machine-id /var/lib/dbus/machine-id ``` -A bubblewrap-sandboxed build (`ENABLE_BUBBLEWRAP_SANDBOX`) binds -`/dev/video*` into the sandbox by itself (`BubblewrapLauncher.cpp`); -`--no-sandbox` (`WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1`) is for the -case where that still fails. browser-big reports which it got: +Without dbus's tools at all, the file is just 32 hex characters: + +``` +head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' | sudo tee /etc/machine-id +``` + +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: + +``` +dbus-run-session -- browser-big URL # per run +dbus-run-session -- sway # whole session +``` + +The portal itself is a separate package: `xdg-desktop-portal` plus a +backend — `xdg-desktop-portal-wlr` for sway, or `-gtk`. + +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: ``` 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 ``` +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 @@ -832,16 +844,14 @@ pactl info # is a Pulse server running, and did it get the ca If ALSA has a capture device but GStreamer lists none, a half-working Pulse server is usually in the way. -`--audio-alsa` ranks `pulsesink` out, so `autoaudiosink` — which is what -WebKit plays through — picks `alsasink`. That fixes browser audio on a -machine whose Pulse server came up with a dummy sink. +`--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. -It does **not** change capture. WebKit opens the microphone the device -monitor found (`gst_device_create_element`, `GStreamerCapturer.cpp`), so -source element ranks never come into it, and `GST_PLUGIN_FEATURE_RANK` is -applied to element factories only — `gstpluginfeature.c` looks the name up -as `GST_TYPE_ELEMENT_FACTORY`, so a device provider entry is silently -ignored. A microphone therefore needs `alsadeviceprovider`: +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`: ``` gst-inspect-1.0 alsadeviceprovider # part of gst-plugins-base's alsa plugin @@ -849,9 +859,7 @@ gst-device-monitor-1.0 Audio/Source # what WebKit will see ``` `--prewarm` reports whether that provider is present. `--gst-rank SPEC` -appends to the variable by hand. GStreamer 1.28 starts device providers -asynchronously; `--list-cameras` and `--prewarm` wait for the monitor's -*started* message (up to 5 s) the way WebKit does, so the list is complete. +sets the variable by hand. To have it every run, put it in browser-big's config rather than typing the flag: @@ -947,23 +955,16 @@ gstreamer: these are missing, and pages will fail without them: ``` WebRTC needs more than one plugin, which is why "install gst-plugins-bad" -often isn't enough. The list is what `gstwebrtcbin.c` (1.28) and WebKit's -`GStreamerMediaEndpoint.cpp` look up by name: +often isn't enough: | Element | From | Needs | | --- | --- | --- | | `webrtcbin` | gst-plugins-bad | **libnice** at build time | -| `nicesrc` / `nicesink` | libnice's own GStreamer plugin | webrtcbin refuses to leave NULL without them — **also with WebKit's librice agent** | -| `dtlsenc` / `dtlsdec`, `dtlssrtpenc` / `dtlssrtpdec` | gst-plugins-bad | OpenSSL | -| `srtpenc` / `srtpdec` | gst-plugins-bad | libsrtp2 | -| `sctpenc` / `sctpdec` | gst-plugins-bad | data channels; most conferencing sites open one | -| `rtpbin`, `rtpfunnel`, `rtp*pay` / `rtp*depay` | gst-plugins-good | — | -| `opusenc` / `opusdec` | gst-plugins-base | libopus | -| `vp8enc` / `vp8dec` | gst-plugins-good | libvpx | -| `x264enc`, `openh264enc` **or** `vah264enc` | -ugly / -bad / -bad (VA-API) | H.264 **encoding**; the three WebKit's own encoder wraps | -| `avdec_h264` **or** `vah264dec` | gst-libav / -bad | H.264 decoding | -| `videoconvert`, `videoscale`, `videorate`, `decodebin3` | gst-plugins-base | the capture pipeline (`GStreamerVideoCapturer.cpp`) | -| `jpegdec` | gst-plugins-good | an MJPEG camera, through `decodebin3` | +| `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 @@ -1010,8 +1011,7 @@ empty" directly. `Ctrl+Shift+M` opens the built-in diagnostics page. | `--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 through ALSA rather than PulseAudio | -| `--ice-libnice` | the stock libnice ICE agent instead of WebKit's librice one | +| `--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 | @@ -1109,11 +1109,8 @@ browser-mini URL --no-compositing browser-mini URL --no-gpu ``` -`--no-hw-decode` is the same idea for video: WebKitGTK 2.54 has no switch -of its own any more, so it ranks the VA-API, VAAPI and V4L2 decoders to -`NONE` through `GST_PLUGIN_FEATURE_RANK`, which WebKit's registry scanner -honours (it lists decoders from `GST_RANK_MARGINAL` up). Any of them can go -in a config file once you know which one it was. +`--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 @@ -1137,7 +1134,6 @@ 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 | -| something you asked for that failed | top right in `urgent` red, whole and wrapped, 10 s, not faded by the pointer, and on stderr | | where you are going | bottom left — the address being opened | Corners touching an edge stay square. Panels you can't interact with fade diff --git a/browser-big.c b/browser-big.c index 22eaa41..90147cf 100644 --- a/browser-big.c +++ b/browser-big.c @@ -43,6 +43,14 @@ static guint g_capture_watch; /* camera / media behaviour */ 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 + * change how the camera behaves, so the repairs below - relaxing, + * retrying, dropping audio, and above all cloning and holding the capture + * track - only apply when one of the camera flags asked for them. + */ +static gboolean g_cam_repair; /* --cam-fix, --cam-share, ... */ static gboolean g_cam_relax = TRUE; /* loosen the page's constraints */ static gboolean g_cam_keepalive = TRUE; /* reuse and hold the capture stream */ static gboolean g_cam_drop_audio = TRUE; /* video-only rather than nothing */ @@ -52,6 +60,7 @@ 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_cam_scale; static int g_cam_scale_fps = 15; /* ceiling for the scaled output */ /* give the page the size it asked */ @@ -79,7 +88,26 @@ static gboolean g_first_load_done; /* rendering / debugging switches that only touch the environment */ static gboolean g_no_webrtc, g_no_mediastream; static const char *g_gst_debug, *g_gst_dbgfile, *g_webkit_dbg; -static const char *g_gst_rank; /* GST_PLUGIN_FEATURE_RANK */ +static char *g_gst_rank; /* GST_PLUGIN_FEATURE_RANK, accumulated */ + +static void +gst_rank_add (const char *spec) +{ + if (!g_gst_rank) { + g_gst_rank = g_strdup (spec); + return; + } + char *joined = g_strconcat (g_gst_rank, ",", spec, NULL); + g_free (g_gst_rank); + g_gst_rank = joined; +} + +/* every VA-API element GStreamer offers, so none of them gets picked */ +#define VA_RANK_OFF \ + "vah264enc:NONE,vah264lpenc:NONE,vah265enc:NONE,vah265lpenc:NONE," \ + "vavp9enc:NONE,vaav1enc:NONE,vah264dec:NONE,vah265dec:NONE," \ + "vavp8dec:NONE,vavp9dec:NONE,vaav1dec:NONE,vajpegdec:NONE," \ + "vapostproc:NONE,vadeinterlace:NONE,vacompositor:NONE" static int g_gst_level = -1; /* Per-window watchdog bookkeeping, hung off Win.ext. */ @@ -271,6 +299,16 @@ static const char *SHIM_JS = " 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. */ +"function withoutMic(c){" +" if(!C.noMic||!c||!c.audio)return c;" +" var o={},k;for(k in c)o[k]=c[k];" +" o.audio=false;" +" if(!o.video){post({ev:'gum-nomic-refused'});return c;}" +" post({ev:'gum-nomic'});" +" return o;" +"}" "function withForce(c){" " if(!C.forceW||!c||!c.video)return c;" " var o={},k;for(k in c)o[k]=c[k];" @@ -472,7 +510,7 @@ static const char *SHIM_JS = " if(!levels.length)levels=[start];" " var i=0,audioDropped=false;" " function attempt(){" -" var cc=withForce(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]});" @@ -768,7 +806,7 @@ static const char *SHIM_JS = "window.__bbHeld=function(){" " return !!(cachedV&&cachedV.readyState==='live')||!!(cachedA&&cachedA.readyState==='live');" "};" -"post({ev:'shim',camfix:true,url:location.href});" +"post({ev:'shim',camfix:true,repair:!!(C.cache||C.relax||C.retries),url:location.href});" "})();"; /* Dumps the state of every media element; used by <mod>+Shift+V. */ @@ -1014,6 +1052,29 @@ dump_gstreamer_env (void) dump_env ("WEBKIT_DISABLE_DMABUF_RENDERER"); dump_env ("WEBKIT_DISABLE_COMPOSITING_MODE"); + /* + * Whether GStreamer can report anything at all. Built with + * -Dgst-debug=disabled these are compile-time constants, GST_DEBUG is + * ignored, GST_DEBUG_FILE is never even created - and every attempt + * to diagnose a media problem returns nothing, which looks like the + * problem being silent rather than the tooling being absent. + */ + /* + * Two different things, easily confused: whether the debug system was + * compiled in at all (a macro in gstconfig.h), and whether a level is + * currently set (gst_debug_is_active(), which is false whenever + * GST_DEBUG is unset, even in a build that supports it). + */ +#ifdef GST_DISABLE_GST_DEBUG + mlog ("gst-debug: COMPILED OUT of this GStreamer - GST_DEBUG can never\n" + "gst-debug: report anything. Rebuild gstreamer core with its\n" + "gst-debug: gst_debug option on."); +#else + mlog ("gst-debug: supported; currently %s (threshold %d)", + gst_debug_is_active () ? "ON" : "off, set GST_DEBUG to use it", + (int) gst_debug_get_default_threshold ()); +#endif + mlog ("---- capture / WebRTC element check ----"); static const char *els[] = { "v4l2src", "pipewiresrc", "videoconvert", "videoscale", "videorate", @@ -1335,7 +1396,7 @@ on_script_message (WebKitUserContentManager *ucm, JSCValue *value, gpointer u) ev_is (s, "params-fix-on") || ev_is (s, "sdp-ssrc-verify") || 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, "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") || @@ -1473,13 +1534,13 @@ build_shim (void) "window.__bbCfg={camfix:%s,relax:%s,cache:%s,dropAudio:%s,retries:%d," "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," + "compat:%s,scaleFps:%d,noMic:%s," "match:%s,watchdog:%s,stall:%d,debug:%s};", g_cam_fix ? "true" : "false", - g_cam_relax ? "true" : "false", - g_cam_keepalive ? "true" : "false", - g_cam_drop_audio ? "true" : "false", - g_cam_retries, + (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", @@ -1490,6 +1551,7 @@ build_shim (void) g_cam_scale ? "true" : "false", g_web_compat ? "true" : "false", g_cam_scale_fps, + g_no_mic ? "true" : "false", match, g_media_watchdog ? "true" : "false", g_stall_timeout, @@ -1610,6 +1672,82 @@ big_media_asked (gboolean audio, gboolean video) g_capture_watch = g_timeout_add_seconds (5, capture_check, NULL); } +/* + * Where is the web process? When a page freezes and the log shows nothing, + * the answer is in what its threads are doing: a thread in D state is + * blocked in the kernel (a device write, a pipe), and wchan names the + * function it is waiting in. Needs no gdb and no privileges. + */ +static void +threads_dump (void) +{ + GDir *d = g_dir_open ("/proc", 0, NULL); + pid_t self = getpid (); + GHashTable *parent = g_hash_table_new (g_direct_hash, g_direct_equal); + const char *name; + guint shown = 0; + + if (!d) + return; + + /* first pass: parent map, so only our own web processes count */ + GPtrArray *webs = g_ptr_array_new (); + while ((name = g_dir_read_name (d))) { + if (!g_ascii_isdigit (name[0])) continue; + pid_t pid = atoi (name), ppid = 0; char comm[64]; + if (!proc_ppid_comm (pid, &ppid, comm, sizeof comm)) continue; + g_hash_table_insert (parent, GINT_TO_POINTER (pid), GINT_TO_POINTER (ppid)); + if (strstr (comm, "WebKitWebProc") || strstr (comm, "WebKitGPUProc")) + g_ptr_array_add (webs, GINT_TO_POINTER (pid)); + } + g_dir_close (d); + + for (guint i = 0; i < webs->len; i++) { + pid_t pid = GPOINTER_TO_INT (g_ptr_array_index (webs, i)); + if (!is_descendant (parent, pid, self)) + continue; + + char *tdir = g_strdup_printf ("/proc/%d/task", (int) pid); + GDir *td = g_dir_open (tdir, 0, NULL); + mlog ("threads: process %d", (int) pid); + mlog ("threads: %-6s %-2s %-18s %s", "tid", "st", "name", "waiting in"); + + const char *tn; + while (td && (tn = g_dir_read_name (td))) { + char *p, *comm = NULL, *wchan = NULL, *stat = NULL; + char state = '?'; + + p = g_strdup_printf ("%s/%s/comm", tdir, tn); + g_file_get_contents (p, &comm, NULL, NULL); g_free (p); + p = g_strdup_printf ("%s/%s/wchan", tdir, tn); + g_file_get_contents (p, &wchan, NULL, NULL); g_free (p); + p = g_strdup_printf ("%s/%s/stat", tdir, tn); + if (g_file_get_contents (p, &stat, NULL, NULL)) { + char *rp = strrchr (stat, ')'); /* comm may hold spaces */ + if (rp && rp[1] == ' ' && rp[2]) state = rp[2]; + } + g_free (p); + + if (comm) g_strchomp (comm); + if (wchan) g_strchomp (wchan); + + /* R running, S sleeping, D blocked in the kernel */ + mlog ("threads: %-6s %-2c %-18s %s", tn, state, + comm ? comm : "?", (wchan && *wchan && strcmp (wchan, "0")) ? wchan : "-"); + shown++; + g_free (comm); g_free (wchan); g_free (stat); + } + if (td) g_dir_close (td); + g_free (tdir); + } + + if (!shown) + mlog ("threads: no web process found under this browser"); + + g_ptr_array_free (webs, TRUE); + g_hash_table_destroy (parent); +} + /* ---------------------------------------------------------------- hooks */ static void @@ -1631,7 +1769,10 @@ big_usage_options (GString *s) " in a config file makes it permanent\n" " --cam-fix relax over-tight getUserMedia constraints, retry\n" " with looser ones, and hold the capture stream so a\n" -" second call is instant\n" +" second call is instant\n"" Only these camera flags change the camera. The\n" +" tracing and compat flags share the same script\n" +" but hand the page its tracks untouched; the log's\n" +" shim line says repair:true when anything is on\n" " --list-cameras print every format, size and frame rate each camera\n" " offers, with the aspect ratio of each, then exit.\n" " Use it to check a site is asking for a shape the\n" @@ -1650,6 +1791,10 @@ 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-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 supply APIs WebKitGTK does not implement, currently\n" " screen.orientation. Zoom's media code reads\n" " screen.orientation.type and throws without it\n" @@ -1721,7 +1866,11 @@ big_usage_options (GString *s) " gst-inspect-1.0 alsadeviceprovider\n" " Put audio = alsa in a config file to have this\n" " every run, on a machine with no sound server\n" -" --gst-rank SPEC set GST_PLUGIN_FEATURE_RANK yourself, e.g.\n" +" --no-va rank every VA-API element out: no hardware video\n" +" encoding or decoding. With --no-dmabuf and\n" +" --no-hw-decode this is an all-software media path,\n" +" which is the test for a GPU driver problem\n" +" --gst-rank SPEC add to GST_PLUGIN_FEATURE_RANK yourself, e.g.\n" " 'pulsedeviceprovider:NONE'\n" " --gst-debug SPEC set GST_DEBUG (e.g. 'v4l2*:6,webrtc*:5')\n" " --gst-debug-level N set GST_DEBUG to a global level\n" @@ -1757,7 +1906,7 @@ big_parse_arg (int argc, char **argv, int *i) /* ---- camera ---- */ if (!strcmp (a, "--list-cameras")) { g_list_cameras = TRUE; return TRUE; } if (!strcmp (a, "--warm-cam")) { g_warm_cam = TRUE; return TRUE; } - if (!strcmp (a, "--cam-fix")) { g_cam_fix = TRUE; return TRUE; } + if (!strcmp (a, "--cam-fix")) { g_cam_fix = g_cam_repair = TRUE; return TRUE; } if (!strcmp (a, "--cam-share")) { /* * Hand a repeated request the stream that is already open, and @@ -1766,6 +1915,7 @@ big_parse_arg (int argc, char **argv, int *i) * produces a frame, and the page waits for it forever. */ g_cam_fix = TRUE; + g_cam_repair = TRUE; g_cam_relax = FALSE; g_cam_keepalive = TRUE; g_cam_retries = 0; @@ -1775,7 +1925,7 @@ big_parse_arg (int argc, char **argv, int *i) if (!strcmp (a, "--media-watchdog")) { g_media_watchdog = TRUE; return TRUE; } if (!strcmp (a, "--auto-reload")) { g_auto_reload = TRUE; return TRUE; } if (!strcmp (a, "--fix-media")) { /* everything at once */ - g_cam_fix = g_prewarm = g_media_watchdog = g_auto_reload = TRUE; + g_cam_fix = g_cam_repair = g_prewarm = g_media_watchdog = g_auto_reload = TRUE; return TRUE; } if (!strcmp (a, "--no-prewarm")) { g_prewarm = FALSE; return TRUE; } @@ -1789,6 +1939,12 @@ big_parse_arg (int argc, char **argv, int *i) g_warm_timeout = MAX (1, atoi (argv[++(*i)])); return TRUE; } + if (!strcmp (a, "--no-mic")) { + g_no_mic = TRUE; + g_cam_fix = TRUE; + g_cam_relax = FALSE; + return TRUE; + } if (!strcmp (a, "--web-compat")) { g_web_compat = TRUE; g_cam_fix = TRUE; /* it rides in the same shim */ @@ -1947,7 +2103,7 @@ big_parse_arg (int argc, char **argv, int *i) return TRUE; } - if (!strcmp (a, "--audio-pulse")) { g_gst_rank = NULL; return TRUE; } + if (!strcmp (a, "--audio-pulse")) { g_clear_pointer (&g_gst_rank, g_free); return TRUE; } if (!strcmp (a, "--audio-alsa")) { /* * Rank PulseAudio's elements out so autoaudiosink/autoaudiosrc @@ -1959,14 +2115,18 @@ big_parse_arg (int argc, char **argv, int *i) * device providers, so the microphone still depends on * alsadeviceprovider being installed. Checked, not assumed. */ - g_gst_rank = "pulsesink:NONE,pulsesrc:NONE," - "alsasink:PRIMARY,alsasrc:PRIMARY," - "pulsedeviceprovider:NONE,alsadeviceprovider:PRIMARY"; + gst_rank_add ("pulsesink:NONE,pulsesrc:NONE," + "alsasink:PRIMARY,alsasrc:PRIMARY," + "pulsedeviceprovider:NONE,alsadeviceprovider:PRIMARY"); + return TRUE; + } + if (!strcmp (a, "--no-va")) { + gst_rank_add (VA_RANK_OFF); return TRUE; } if (!strcmp (a, "--gst-rank")) { NEXT ("--gst-rank"); - g_gst_rank = argv[++(*i)]; + gst_rank_add (argv[++(*i)]); return TRUE; } @@ -2043,9 +2203,19 @@ big_cfg_set (const char *key, const char *value) * 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; + } + return TRUE; + } + if (!g_ascii_strcasecmp (key, "cam_share")) { if (truthy_value (value)) { g_cam_fix = TRUE; + g_cam_repair = TRUE; g_cam_relax = FALSE; g_cam_keepalive = TRUE; g_cam_retries = 0; @@ -2057,12 +2227,12 @@ big_cfg_set (const char *key, const char *value) return FALSE; if (!g_ascii_strcasecmp (value, "alsa")) { - g_gst_rank = "pulsesink:NONE,pulsesrc:NONE," - "alsasink:PRIMARY,alsasrc:PRIMARY," - "pulsedeviceprovider:NONE,alsadeviceprovider:PRIMARY"; + 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_gst_rank = NULL; + g_clear_pointer (&g_gst_rank, g_free); } else { g_printerr ("config: audio must be alsa, pulse or auto\n"); } @@ -2110,7 +2280,6 @@ codec_check (void) */ static const struct { const char *element; const char *what; } need[] = { { "webrtcbin", "WebRTC gst-plugins-bad, built with libnice" }, - { "nicesrc", "ICE libnice, built with GStreamer support" }, { "srtpenc", "SRTP gst-plugins-bad, needs libsrtp2" }, { "dtlssrtpenc", "DTLS gst-plugins-bad, needs OpenSSL" }, { "rtpbin", "RTP gst-plugins-good" }, @@ -2142,6 +2311,27 @@ codec_check (void) 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); @@ -2387,6 +2577,12 @@ big_key (Win *w, guint key, gboolean shift) toast_show (w, "media state -> stderr", 2); view_eval (w->view, MEDIA_DUMP_JS); return TRUE; + case GDK_KEY_t: + /* Deliberately not view_eval: a frozen web process cannot answer, + * and this reads /proc, which does not need it to. */ + toast_show (w, "web process threads -> stderr", 2); + threads_dump (); + return TRUE; default: return FALSE; } diff --git a/browser_core.c b/browser_core.c index b38ab5b..906dde1 100644 --- a/browser_core.c +++ b/browser_core.c @@ -259,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:///...". */ @@ -819,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 */ @@ -2297,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" @@ -2685,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," @@ -3283,6 +3268,7 @@ static const char * perm_type_name (WebKitPermissionRequest *req) { if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST (req)) return "user-media"; + if (WEBKIT_IS_DEVICE_INFO_PERMISSION_REQUEST (req)) return "device-info"; if (WEBKIT_IS_POINTER_LOCK_PERMISSION_REQUEST (req)) return "pointer-lock"; if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST (req)) return "geolocation"; if (WEBKIT_IS_NOTIFICATION_PERMISSION_REQUEST (req)) return "notification"; @@ -3360,9 +3346,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; } @@ -3377,14 +3361,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); @@ -3454,9 +3437,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 (); @@ -3467,17 +3453,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); @@ -3773,9 +3754,23 @@ 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; + } if (WEBKIT_IS_POINTER_LOCK_PERMISSION_REQUEST (req)) { webkit_permission_request_allow (req); @@ -3789,45 +3784,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 @@ -4733,17 +4689,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 : "") @@ -5039,10 +4984,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); @@ -5169,8 +5111,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); @@ -5188,41 +5128,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 @@ -5629,9 +5534,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); } @@ -6269,16 +6172,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; } @@ -7150,7 +7045,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); @@ -7264,6 +7158,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); @@ -7780,17 +7681,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 (); @@ -7818,6 +7709,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 */ diff --git a/browser_core.h b/browser_core.h index 61584ab..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 */ @@ -169,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 */