Etiqueta: stremio-gtk

  • Packaging Stremio’s GTK4 Shell: CEF Integration Adventures

    Nine days debugging one function call, and other lessons from browser-engine integration

    Introduction

    When Stremio released their new GTK4-based shell using Chromium Embedded Framework (CEF) instead of Qt5/QtWebEngine, it provided the perfect test case for my Debian packaging journey, specially after QT5 has become an End Of Life software that won’t be supported soon.

    It was the good usecase to test a big our freshly-packaged CEF library. What followed was a month of debugging that revealed the gap between «library compiles» and «library works.»

    This article documents the technical challenges of integrating CEF with a real-world application for Debian packaging.

    The Application Architecture


    Stremio-gtk is a relatively simple application in concept:

    1. Create a CEF browser window
    2. Load https://app.strem.io/shell-v4.4
    3. Provide IPC bridge for the web app to control native features
    4. Handle video playback through MPV

    In practice, each step revealed hidden complexity.

    Challenge 1: Resource File Discovery

    The Problem

    CEF processes need to locate several resource files at startup:

    • icudtl.dat – ICU internationalization data
    • v8_context_snapshot.bin – V8 JavaScript engine snapshot
    • *.pak files – Chromium resource bundles
    • locales/*.pak – Localization data

    In upstream builds, these files sit alongside the executable. In FHS-compliant installations, they’re scattered across /usr/share/cef/.

    The obvious solution is to configure paths in the Settings structure:

    let settings = Settings {
        resources_dir_path: "/usr/share/cef".into(),
        locales_dir_path: "/usr/share/cef/locales".into(),
        ..Default::default()
    };
    cef_initialize(&settings);

    This works for the browser process. Subprocesses crashed immediately.

    Root Cause

    CEF’s multi-process architecture spawns specialized subprocesses for rendering, GPU operations, and utility functions. These subprocesses call cef_execute_process() as their first action and exit without ever seeing the Settings structure.

    The subprocess entry point:

    fn main() {
        // For subprocesses, this returns immediately with exit code
        let exit_code = cef_execute_process(&args, None, None);
        if exit_code >= 0 {
            std::process::exit(exit_code);
        }
    
        // Only browser process reaches here
        let settings = Settings { ... };
        cef_initialize(&settings);
    }

    By the time Settings could be applied, subprocesses have already failed to find resources.

    Solution

    CEF searches for resources relative to libcef.so, not the application binary. Symlinks in the library directory solve the problem:

    # In /usr/lib/x86_64-linux-gnu/:
    icudtl.dat -> ../../share/cef/icudtl.dat
    v8_context_snapshot.bin -> ../../share/cef/v8_context_snapshot.bin

    For settings that subprocesses need, command-line switches must be added in the on_before_command_line_processing callback, which is called for all process types:

    impl App for StremioApp {
        fn on_before_command_line_processing(
            &self,
            _process_type: &str,
            command_line: &mut CommandLine,
        ) {
            command_line.append_switch_with_value(
                "resources-dir-path",
                "/usr/share/cef"
            );
        }
    }

    Challenge 2: GPU Process Crashes

    The Problem

    With resources loading correctly, the GPU subprocess now launched—and immediately crashed. CEF retried nine times before giving up:

    [GPU] error_code=1002
    GPU process isn't usable. Goodbye.

    No stack traces, no meaningful logs. The GPU process died before producing diagnostics.

    Investigation

    Adding --enable-logging --v=1 revealed the GPU process was failing to initialize OpenGL contexts. The error suggested EGL/GLES library issues.

    CEF’s GPU process expects ANGLE (Almost Native Graphics Layer Engine)—Google’s OpenGL ES implementation built on Vulkan and DirectX. It’s not interchangeable with Mesa’s EGL implementation despite similar APIs.

    Solution

    The CEF package must bundle its own ANGLE libraries:

    /usr/lib/x86_64-linux-gnu/cef/
    ├── libEGL.so.1        # CEF's ANGLE-based EGL
    ├── libGLESv2.so.2     # CEF's ANGLE-based GLES
    └── libvk_swiftshader.so  # Software Vulkan fallback

    The main library is configured with RPATH to find these private libraries before system Mesa.

    For systems without GPU acceleration, SwiftShader provides software rendering. Its Vulkan ICD registration required absolute paths:

    {
        "file_format_version": "1.0.0",
        "ICD": {
            "library_path": "/usr/lib/x86_64-linux-gnu/cef/libvk_swiftshader.so",
            "api_version": "1.1.0"
        }
    }

    Relative paths failed because the ICD loader doesn’t resolve paths relative to the JSON file.

    Challenge 3: The Nine-Day IPC Debugging

    The Symptom

    CEF launched, pages loaded, UI rendered—but video playback fell back to HTML5 instead of the native MPV player. The web app was treating stremio-gtk as a browser rather than a native shell.

    The Qt WebChannel Protocol

    Stremio’s web application expects to communicate with its native shell through Qt’s WebChannel protocol. The original Qt5 shell creates a transport object:

    // Qt5 shell (QML):
    WebChannel {
        id: webChannel
        registeredObjects: [transport]
    }
    
    property QtObject transport: QtObject {
        property string shellVersion: "4.4"
        property string serverAddress: "http://127.0.0.1:11470"
        property bool isFullscreen: false
    
        signal event(string event, string args)
        function send(data) { ... }
    }

    The web app accesses this through:

    new QWebChannel(qt.webChannelTransport, function(channel) {
        window.transport = channel.objects.transport;
        transport.event.connect(function(name, args) { ... });
    });

    Dead Ends

    Day 1-2: «IPC onmessage is not a function»

    The web app tried calling a callback before registration. Added message queuing:

    struct MessageQueue {
        pending: Vec<String>,
        handler: Option<Box<dyn Fn(String)>>,
    }
    
    impl MessageQueue {
        fn send(&mut self, msg: String) {
            match &self.handler {
                Some(h) => h(msg),
                None => self.pending.push(msg),
            }
        }
    
        fn set_handler(&mut self, h: Box<dyn Fn(String)>) {
            for msg in self.pending.drain(..) {
                h(msg);
            }
            self.handler = Some(h);
        }
    }

    Messages stopped being lost, but video still didn’t work.

    Day 3-4: Wrong message format

    QWebChannel expects specific array formats:

    // Wrong (what we sent):
    { type: 1, object: "transport", data: { properties: {} } }
    
    // Correct (QWebChannel format):
    { type: 1, object: "transport", data: [
        { }, // signals
        { }, // methods
        { "shellVersion": [1, "4.4"], "serverAddress": [1, "http://..."] }
    ]}

    Properties are [type, value] tuples. Fixed the serialization, but video still didn’t work.

    Day 5: Double JSON serialization

    A bug in the Rust→JavaScript bridge:

    // Bug: data was already JSON, gets serialized again
    let message = serde_json::to_string(&json!({
        "type": 3,
        "data": already_json_string  // Becomes "\"escaped\"" 
    }))?;

    Fixed, but video still didn’t work.

    Day 6: Wrong response type

    Init responses used type: 3 when QWebChannel expected type: 10:

    // Our response:
    { type: 3, id: 1, data: [...] }
    
    // Expected:
    { type: 10, id: 1, data: [...] }

    Fixed, but video still didn’t work.

    Day 7-8: User-Agent detection

    Examining the web app’s source revealed an early check:

    if (navigator.userAgent.indexOf('StremioShell') === -1) {
        // Use HTML5 player
        return;
    }
    // Use native MPV

    Added «StremioShell» to the user agent. Video still didn’t work.

    Day 9: The Missing Function Call

    Desperate, I read through the Qt5 shell’s main.qml line by line:

    function injectJS() {
        var injectedJS = "try { initShellComm() } catch(e) { console.error(e) }"
        webView.runJavaScript(injectedJS, function(err) {
            if (err) console.error("Injection failed:", err);
        });
    }
    
    Connections {
        target: webView
        onLoadingChanged: {
            if (loadRequest.status === WebEngineLoadRequest.LoadSucceededStatus) {
                injectJS();
            }
        }
    }

    The native shell must explicitly call window.initShellComm() after page load. The web app defines this function but never calls it automatically.

    One line in the preload script:

    window.addEventListener('load', () => {
        if (typeof initShellComm === 'function') {
            initShellComm();
        }
    });

    Video playback worked.

    Lesson

    Nine days of debugging IPC formats, serialization, and message types. The actual fix was one function call that the Qt5 shell made explicitly. The documentation existed—in QML code that had to be read line by line.

    Challenge 4: Native Widget Rendering

    The Problem

    After IPC worked, a minor but visible issue remained: HTML <select> dropdowns didn’t work. Clicking them did nothing.

    Root Cause

    CEF’s offscreen rendering mode (used by stremio-gtk) renders everything to a buffer via OnPaint. Native OS widgets like dropdown menus require real window handles.

    CEF provides callbacks for popup rendering:

    trait RenderHandler {
        fn on_popup_show(&self, browser: &Browser, show: bool);
        fn on_popup_size(&self, browser: &Browser, rect: &Rect);
    }

    Applications must implement these to render native widget representations themselves. Stremio-gtk hadn’t implemented them.

    Pragmatic Solution

    Rather than implementing full popup rendering, JavaScript injection replaces <select> elements with custom div-based dropdowns:

    function replaceSelect(select) {
        const wrapper = document.createElement('div');
        wrapper.className = 'custom-select-wrapper';
    
        const display = document.createElement('div');
        display.className = 'custom-select-display';
        display.textContent = select.options[select.selectedIndex]?.text || '';
    
        const dropdown = document.createElement('div');
        dropdown.className = 'custom-select-dropdown';
    
        Array.from(select.options).forEach((opt, i) => {
            const item = document.createElement('div');
            item.textContent = opt.text;
            item.onclick = () => {
                select.selectedIndex = i;
                select.dispatchEvent(new Event('change'));
                display.textContent = opt.text;
                dropdown.style.display = 'none';
            };
            dropdown.appendChild(item);
        });
    
        display.onclick = () => {
            dropdown.style.display = 
                dropdown.style.display === 'none' ? 'block' : 'none';
        };
    
        select.style.display = 'none';
        select.parentNode.insertBefore(wrapper, select);
        wrapper.appendChild(display);
        wrapper.appendChild(dropdown);
    }
    
    // Handle dynamic selects
    new MutationObserver((mutations) => {
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.tagName === 'SELECT') replaceSelect(node);
                if (node.querySelectorAll) {
                    node.querySelectorAll('select').forEach(replaceSelect);
                }
            });
        });
    }).observe(document.body, { childList: true, subtree: true });
    
    document.querySelectorAll('select').forEach(replaceSelect);

    The hidden native <select> elements sync state for AngularJS compatibility.

    The Final Patch Count

    After all debugging, stremio-gtk required 24 patches:

    CategoryCountExamples
    Build system4Cargo vendor, system paths
    CEF integration6Resource paths, ANGLE, zygote
    IPC protocol8Transport object, JSON format, initShellComm
    Runtime fixes4User agent, event signals
    UI workarounds2Select replacement, focus handling

    Testing Insights

    What Unit Tests Miss

    The CEF package passed all its tests. Stremio-gtk revealed:

    1. Resource symlinks needed in unexpected locations
    2. ANGLE is required, not optional
    3. Subprocess initialization differs from main process
    4. IPC protocol details matter for real applications

    Integration Test Value

    A demanding application like stremio-gtk (video, IPC, GPU, offscreen rendering) stress-tests features that simple «load webpage» tests never touch.

    Upstream Bug Discovery

    Several issues exist in upstream stremio-linux-shell. The Flatpak likely works by accident of bundling or timing. These fixes should flow back.

    Conclusion

    The stremio-gtk packaging revealed that CEF integration is more than linking against a library. The multi-process architecture, resource discovery, GPU requirements, and IPC protocols all require careful handling for distribution packaging.

    The nine-day IPC debugging produced one line of actual code. The other 23 patches required understanding internals that aren’t documented anywhere except the source code of working implementations.

    For packagers considering CEF applications: expect to bridge the gap between upstream assumptions and distribution requirements. Read the Qt5 implementation if one exists. Test early and test thoroughly.


    Packages at salsa.debian.org. ITP #1119815 (stremio-gtk), resolves #915400 (CEF).

  • Packaging Chromium Embedded Framework for Debian: A Technical Deep Dive

    Packaging Chromium Embedded Framework for Debian: A Technical Deep Dive

    Resolving ITP #915400 after seven years—the complete technical breakdown

    Introduction

    The Chromium Embedded Framework (CEF) has been sitting in Debian’s packaging queue since December 2018. Bug #915400 documented the need: obs-studio wanted browser sources, casparcg-server needed HTTP support, and various applications required a lighter alternative to Electron.

    Previously I packaged Stremio (QT5 based) for Debian and Wolfi, but QT5 is EOL (end of life), so I went on and decided to package the next generation of Stremio (GTK based) but this package depends on chromium-embedded-framework that did not exist en Debian.

    This article documents the technical approach that finally produced working Debian packages.

    Why CEF Is Different

    Most C/C++ projects follow a predictable pattern: download tarball, run configure, make, install. CEF breaks every assumption.

    The Upstream Build Process

    CEF’s official build uses automate-git.py, which:

    1. Clones depot_tools from Google
    2. Runs gclient sync to fetch ~1GB of Chromium sources
    3. Downloads prebuilt toolchains from Google Cloud Storage
    4. Optionally uses reclient for distributed compilation
    5. Builds both Debug and Release configurations
    6. Creates binary distribution packages

    This process assumes internet access, Google infrastructure, and a ~90GB working directory.

    Debian Requirements

    Debian builds must be:

    • Network-isolated during compilation
    • Reproducible from source
    • Using system toolchains where possible
    • Compliant with the Filesystem Hierarchy Standard

    The gap between these requirements and upstream assumptions drove most of the packaging complexity.

    Architecture: The Dual-Source Approach

    Problem: Chromium Integration

    CEF doesn’t bundle Chromium in its tarball. It expects to download it during build. Including Chromium sources in the CEF orig tarball would:

    • Create a ~1.5GB source package
    • Duplicate Debian’s existing chromium sources
    • Create maintenance burden tracking two projects

    Debian Chromium doesn’t provide a Source package that we can use as a dependency. To overcome that, we are going to create an experimental Debian package that will get the Debian Chromium Sources and will add it as a dependency in a subfolder. This approach will allow other Debian Developers to weigh in and see that this solution works and when the Debian Chromium Team eventually publishes the sources, we just need to add it as a regular dependency.


    Solution: Build Dependency Model

    The packaging treats Chromium as a build dependency rather than bundled source:

    debian/                     # Version controlled
    ├── rules
    ├── control
    ├── patches/
    │   ├── cef/               # 16 patches
    │   └── chromium/          # 42 patches
    └── ...
    
    cef/                        # CEF upstream sources (~450MB)
    
    ../chromium_143.0.7499.169.orig.tar.xz    # Build dep (~714MB)
    ../rust-toolchain.tar.xz                   # Rust stdlib (~142MB)

    The debian/rules file extracts Chromium sources into chromium_src/ before the build begins. This happens in the clean target to ensure sources exist before any build steps.

    Benefits

    1. Reuse Debian Chromium work: When the chromium team patches a vulnerability, CEF can rebase
    2. Smaller source package: Only CEF-specific sources in the orig tarball
    3. Clear separation: CEF patches vs Chromium patches are distinct

    Future: chromium-source Package

    Bug #893448 proposes a chromium-source binary package that would provide extracted Chromium sources. When resolved, CEF could simply Build-Depends: chromium-source and the manual tarball extraction disappears.

    The Patch Stack

    CEF Patches (16 total)

    Build System Decoupling

    0001-skip-gclient-revert.patch

    CEF’s gclient_hook.py reverts all files to git checkout HEAD state before building. This destroys any Debian patches applied during the build. The patch removes the revert logic.

    0002-skip-chromium-checkout.patch

    CEF expects to run git clone for Chromium. This patch skips the checkout and uses pre-extracted sources.

    0003-use-system-clang.patch

    CEF downloads LLVM toolchains from Google Cloud Storage. This patch configures the build to use Debian’s clang-19 package.

    0004-create-reclient-stub.patch

    Google’s reclient provides distributed compilation. Rather than removing all references, a stub script satisfies the build system without network access.

    0005-add-rust-toolchain-stub.patch

    Similar to reclient—a stub for the Rust toolchain downloader that delegates to system rustc.

    Path Configuration

    0010-use-debian-paths.patch
    0011-resource-paths.patch
    0012-library-output-paths.patch

    CEF assumes resources live alongside binaries. These patches configure FHS-compliant paths:

    • Libraries: /usr/lib/x86_64-linux-gnu/
    • Resources: /usr/share/cef/
    • Locale data: /usr/share/cef/locales/

    Chromium Patches (42 total)

    Network Isolation (12 patches)

    disable-gcs-downloads.patch
    skip-test-fonts-download.patch
    offline-build-config.patch
    ...

    Chromium’s build fetches resources at multiple points. Each download point needs a patch to either:

    • Use pre-packaged alternatives
    • Skip optional components
    • Error clearly rather than hang

    C++23 / libc++ Compatibility (8 patches)

    Debian sid uses libc++-19 with strict C++23 enforcement. The unique_ptr destructor now requires complete types:

    // Old code (worked in C++17/20):
    class RenderFrame;
    std::unique_ptr<RenderFrame> frame_;  // OK: RenderFrame forward-declared
    
    // C++23 libc++:
    // Error: RenderFrame must be complete for ~unique_ptr

    Patches add forward declarations and reorder includes in:

    • v8/src/heap/ – Garbage collector internals
    • media/gpu/ – Video acceleration
    • ui/gfx/ – Graphics primitives
    • components/viz/ – Compositor

    Example fix in v8/src/heap/marking-state.h:

    // Before patch:
    class HeapObject;
    std::unique_ptr<HeapObject> obj_;
    
    // After patch (add include):
    #include "src/objects/heap-object.h"
    std::unique_ptr<HeapObject> obj_;

    Compiler Updates (6 patches)

    GCC 15 and Clang 19 deprecated various constructs:

    fix-aggregate-optional-emplace.patch

    std::optional::emplace with aggregate initialization changed behavior. Affected code in IPC serialization.

    remove-deprecated-warning-flags.patch

    Several -W flags no longer exist in clang-19.

    fix-libclang-paths.patch

    Clang’s internal header paths changed between versions.

    Rust Stable (3 patches)

    Chromium uses Rust nightly features. Patches remove:

    • -Z flags (unstable options)
    • Nightly-only crate features
    • Unstable library functions

    System Libraries (8 patches)

    Patches to prefer system libraries where ABI-compatible:

    • libxcb
    • fontconfig
    • minizip
    • zstd
    • harfbuzz (partial)

    Some libraries cannot use system versions due to ABI differences (V8, Skia, ANGLE).

    Build Configuration

    GN Arguments

    The build uses GN (Generate Ninja) with extensive configuration:

    gn_args = [
        'is_official_build=true',
        'is_debug=false',
        'symbol_level=0',
    
        # Toolchain
        'clang_use_chrome_plugins=false',
        'use_lld=true',
        'use_custom_libcxx=false',  # System libc++
    
        # Disable Google services
        'use_official_google_api_keys=false',
        'enable_nacl=false',
        'enable_widevine=false',
    
        # Hardware acceleration
        'use_vaapi=true',
        'use_v4l2_codec=false',
    
        # System libraries
        'use_system_libffi=true',
        'use_system_zlib=false',  # ABI issues
        ...
    ]

    The use_custom_libcxx Decision

    CEF defaults to bundling its own libc++ (use_custom_libcxx=true). This avoids ABI compatibility issues but:

    • Duplicates system library
    • May conflict with applications using system libc++
    • Increases binary size

    After extensive testing (builds 108-140), use_custom_libcxx=false works with the C++23 compatibility patches. This is the preferred configuration for Debian integration.

    Build Resource Requirements

    ResourceRequirement
    Disk space~40GB during build
    RAM16GB minimum, 32GB+ recommended
    CPU time2-24 hours depending on hardware
    ParallelismScales well to 16+ cores

    Output Structure

    Binary Packages

    libcef138_138.0.7+chromium143.0.7499.169-1_amd64.deb
    ├── /usr/lib/x86_64-linux-gnu/
    │   ├── libcef.so.138
    │   ├── cef/
    │   │   ├── libEGL.so.1          # ANGLE
    │   │   ├── libGLESv2.so.2       # ANGLE
    │   │   └── libvk_swiftshader.so # Software Vulkan
    │   └── ...
    
    libcef-dev_138.0.7+chromium143.0.7499.169-1_amd64.deb
    ├── /usr/include/cef/
    │   ├── include/
    │   │   ├── cef_app.h
    │   │   ├── cef_browser.h
    │   │   └── ...
    │   └── ...
    ├── /usr/lib/x86_64-linux-gnu/
    │   ├── libcef.so -> libcef.so.138
    │   └── cmake/cef/
    │       └── cef-config.cmake
    
    cef-resources_138.0.7+chromium143.0.7499.169-1_all.deb
    ├── /usr/share/cef/
    │   ├── icudtl.dat              # ICU data
    │   ├── v8_context_snapshot.bin # V8 snapshot
    │   ├── chrome_100_percent.pak
    │   ├── chrome_200_percent.pak
    │   └── locales/
    │       ├── en-US.pak
    │       ├── es.pak
    │       └── ...

    ANGLE and SwiftShader

    CEF requires specific GPU abstraction libraries:

    ANGLE: OpenGL ES implementation over Vulkan/DirectX. Not interchangeable with Mesa’s EGL—the API is similar but internals differ. Installed in /usr/lib/x86_64-linux-gnu/cef/ with RPATH configuration.

    SwiftShader: Software Vulkan implementation for systems without GPU acceleration. The ICD JSON must use absolute paths:

    {
        "file_format_version": "1.0.0",
        "ICD": {
            "library_path": "/usr/lib/x86_64-linux-gnu/cef/libvk_swiftshader.so",
            "api_version": "1.1.0"
        }
    }

    Resource Path Discovery

    CEF loads resources early in initialization—before most application callbacks. The library searches relative to libcef.so, not the application binary.

    The Symlink Solution

    # In /usr/lib/x86_64-linux-gnu/:
    icudtl.dat -> ../../share/cef/icudtl.dat
    v8_context_snapshot.bin -> ../../share/cef/v8_context_snapshot.bin

    This allows subprocesses (renderer, GPU, utility) to find resources when spawned with cef_execute_process(), before any application configuration is applied.

    Testing and Validation

    Unit Tests

    CEF includes ceftests but many tests require network access or graphical display. The packaging runs a subset of offline-capable tests.

    Integration Testing

    The definitive test is building a real application. stremio-gtk exercises:

    • Offscreen rendering
    • Multiple process types
    • IPC protocols
    • GPU acceleration
    • Resource loading

    Issues discovered through stremio-gtk that passed unit tests:

    1. Resource symlinks needed in library directory
    2. ANGLE libraries required (not just preferred)
    3. SwiftShader ICD paths must be absolute
    4. Subprocess command-line switch handling

    Maintenance Considerations

    Chromium Updates

    When Debian updates Chromium, CEF should track:

    1. Obtain matching CEF branch for new Chromium version
    2. Rebase debian/patches/chromium/ onto new sources
    3. Test build and resolve new conflicts
    4. Update version numbers throughout

    Security Updates

    CEF inherits Chromium’s attack surface. Security updates to Chromium should flow to CEF promptly. The dual-source architecture helps: updating chromium_*.orig.tar.xz and rebuilding catches most issues.

    Upstream Coordination

    CEF upstream is responsive to packaging concerns. Several patches developed for Debian have been submitted upstream or informed upstream decisions.

    Conclusion

    CEF packaging requires treating a browser engine as a library—with all the complexity that implies. The dual-source architecture, extensive patch stack, and careful path configuration produce packages that integrate with Debian’s ecosystem rather than fighting it.

    The approach documented here should transfer to other distributions with similar policies. The patches are organized by purpose (build system, compatibility, paths) to aid porting.


    Packages available at salsa.debian.org/mendezr/chromium-embedded-framework. ITP #915400.

Creative Commons License
Except where otherwise noted, the content on this site is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.