{
    "type": "article",
    "title": "A fourth screen on the living-room TV",
    "canonical": "https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv",
    "path": "/writing/a-fourth-screen-on-the-living-room-tv",
    "url": "https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv",
    "markdown": "https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv.md",
    "json": "https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv.json",
    "summary": "Giving a Linux laptop an extra 4K screen that only exists while the TV is watching, with Sunshine and Moonlight to stream it, a virtual display driver instead of a dummy plug, and the black screens and lost layouts on the way.",
    "published": "2026-10-23T00:00:00+01:00",
    "modified": "2026-10-25T18:32:07+00:00",
    "image": {
        "url": "https://www.dealmobile.co.uk/sites/default/files/content/images/posts/a-fourth-screen-on-the-living-room-tv/hero.png",
        "alt": "The laptop’s desktop drawn to scale: the 3840 by 2160 TV screen on the left, then the laptop’s own panel, then two portrait 4K monitors."
    },
    "fields": {
        "image_dark": null
    },
    "tags": [
        {
            "name": "Linux",
            "url": "https://www.dealmobile.co.uk/writing/tag/linux",
            "json": "https://www.dealmobile.co.uk/writing/tag/linux.json"
        },
        {
            "name": "Android",
            "url": "https://www.dealmobile.co.uk/writing/tag/android",
            "json": "https://www.dealmobile.co.uk/writing/tag/android.json"
        },
        {
            "name": "TCL Google TV Tools",
            "url": "https://www.dealmobile.co.uk/writing/tag/tcl-google-tv-tools",
            "json": "https://www.dealmobile.co.uk/writing/tag/tcl-google-tv-tools.json"
        }
    ],
    "projects": [
        {
            "name": "TCL Google TV Tools",
            "url": "https://www.dealmobile.co.uk/projects/tcl-google-tv-tools",
            "json": "https://www.dealmobile.co.uk/projects/tcl-google-tv-tools.json"
        }
    ],
    "body": {
        "html": "<p>Casting a browser tab to the TV is fine for a video. It isn’t a second screen: the sound of a meeting cast from a tab stays split from the laptop, and nothing outside the browser comes along. I wanted the TV I <a href=\"/writing/decluttering-a-tcl-google-tv\">decluttered over ADB</a> to be a fourth screen for my laptop: drag a window onto it and it plays on the TV, with the TV’s sound, while the other three screens stay mine. The laptop runs Ubuntu 22.04 and GNOME on X11, with an RTX 4080, its own panel and two portrait 4K monitors behind a DisplayPort hub. The streaming is done by tools built for games. The screen itself doesn’t exist, and that turned out to be the interesting bit. This is how.</p>\n<h2 id=\"streaming-a-screen\">Streaming a screen</h2>\n<p>Sunshine runs on the PC and encodes a screen on the GPU, and Moonlight on the TV plays it back. Sunshine’s Ubuntu package installs a systemd user service, and the firewall lets only the TV in:</p>\n<pre><code class=\"language-bash\">sudo apt install ./sunshine_2026.914.233613-1+ubuntu22.04_amd64.deb\nsystemctl --user enable --now app-dev.lizardbyte.app.Sunshine\nsudo ufw allow from 192.168.1.x to any port 47984,47989,48010 proto tcp\nsudo ufw allow from 192.168.1.x to any port 47998:48010 proto udp\n</code></pre>\n<p>Two settings in <code>~/.config/sunshine/sunshine.conf</code> made it work. <code>capture = x11</code> stops a hang at start-up, where Sunshine tries NVIDIA’s fast capture, which needs a patched driver on consumer cards, and never moves on. <code>output_name</code> has to be a name such as <code>eDP-1-1</code>, not a number, because the capture code counts every output the X server lists, connected or not, while the session code matches names.</p>\n<p>That streams one of the laptop’s existing screens. A fourth screen needs a monitor the desktop can lay windows on and nobody is looking at.</p>\n<h2 id=\"a-monitor-that-isnt-there\">A monitor that isn’t there</h2>\n<p>The usual answer is a dummy HDMI plug, a few pounds’ worth of chip that tells the laptop a monitor is connected. NVIDIA’s driver can fake one too, with its <code>ConnectedMonitor</code> option, but that replaces the driver’s own detection of every display, and the two 4K monitors come and go with the hub.</p>\n<p>The third option is evdi, the virtual display driver behind DisplayLink docks. Ubuntu packages it for 22.04, with patches for the 6.8 kernel and for running without a DisplayLink device, and Secure Boot was off, so DKMS could build it without signing:</p>\n<pre><code class=\"language-bash\">sudo apt install evdi-dkms libevdi0 libevdi0-dev\necho 'options evdi initial_device_count=1' | sudo tee /etc/modprobe.d/evdi.conf\necho evdi | sudo tee /etc/modules-load.d/evdi.conf\n</code></pre>\n<p>One more file, <code>/etc/X11/xorg.conf.d/20-evdi.conf</code>, tells X11 to drive it with the modesetting driver and <code>AccelMethod none</code>, which is DisplayLink’s advice for NVIDIA machines. After a restart, <code>xrandr</code> listed a third display provider and a new output, <code>DVI-I-2-1</code>, disconnected: a port with nothing in it.</p>\n<h2 id=\"plugging-it-in\">Plugging it in</h2>\n<p>Something has to plug a monitor into that port. A monitor describes itself with an EDID, a 128-byte block listing its name and modes, so <code>make-edid.py</code> writes one by hand: 3840 by 2160 at 60 Hz with the standard 594 MHz timing, 1920 by 1080 as a second mode, and the name “TV screen”. <code>edid-decode --check</code> passes it. A C program of about 140 lines hands it to evdi:</p>\n<pre><code class=\"language-c\">evdi_connect(handle, edid, edid_len, 3840 * 2160, 3840 * 2160 * 60);\n</code></pre>\n<p>GNOME saw a new monitor appear, and turned both portrait monitors landscape.</p>\n<h2 id=\"black-screens\">Black screens</h2>\n<p>The first version of the program connected the monitor and then just waited. Within a minute every screen was black, and the kernel log said why, every 10 seconds:</p>\n<pre><code>evdi evdi.0: [drm] *ERROR* flip_done timed out\nevdi evdi.0: [drm] *ERROR* [CRTC:35:crtc-0] commit wait timed out\n</code></pre>\n<p>evdi doesn’t finish a display update until its client has collected the frame. A DisplayLink dock collects them to send down the cable. My program didn’t, so the X server waited on each update, and everything else waited on the X server. The fix is to behave like a dock and throw the frames away:</p>\n<pre><code class=\"language-c\">static void request_frames(void)\n{\n\twhile (pixels &amp;&amp; evdi_request_update(handle, BUFFER_ID))\n\t\tgrab();\n}\n</code></pre>\n<p><code>grab()</code> copies the changed areas into a buffer registered when the mode is set, and an <code>update_ready</code> event calls it again for the next frame. Sunshine doesn’t need those pixels, because it reads the screen from the X server like any other. Since then there hasn’t been a single timeout. While a session runs, it costs about 8% of one CPU core at 1080p and about 23% at 4K.</p>\n<figure class=\"shot shot--prose\"><img class=\"theme-img theme-img--light\" src=\"/sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline.png.webp?itok=-adRf1-6\" srcset=\"/sites/default/files/styles/almanac_shot_md/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline.png.webp?itok=-3DMjiz2 1200w, /sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline.png.webp?itok=-adRf1-6 2240w\" sizes=\"(width &gt;= 39em) 37rem, calc(100vw - 2rem)\" alt=\"How the fourth screen works. Inside the laptop, the X server draws the desktop across four screens. It sends display updates to the evdi screen DVI-I-2-1, where tv-screen collects each frame and throws it away. Sunshine reads the same screen’s area from the X server, encodes it as HEVC on the GPU and sends it over Wi-Fi, with the sound, to Moonlight on the TV, which decodes it at 3840 by 2160 and 60 Hz.\" width=\"2240\" height=\"1752\" loading=\"lazy\" decoding=\"async\"><img class=\"theme-img theme-img--dark\" src=\"/sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline-dark.png.webp?itok=MWDcOLtp\" srcset=\"/sites/default/files/styles/almanac_shot_md/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline-dark.png.webp?itok=DucPbq60 1200w, /sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline-dark.png.webp?itok=MWDcOLtp 2240w\" sizes=\"(width &gt;= 39em) 37rem, calc(100vw - 2rem)\" alt=\"How the fourth screen works. Inside the laptop, the X server draws the desktop across four screens. It sends display updates to the evdi screen DVI-I-2-1, where tv-screen collects each frame and throws it away. Sunshine reads the same screen’s area from the X server, encodes it as HEVC on the GPU and sends it over Wi-Fi, with the sound, to Moonlight on the TV, which decodes it at 3840 by 2160 and 60 Hz.\" width=\"2240\" height=\"1752\" loading=\"lazy\" decoding=\"async\"><figcaption>Two readers of one screen: evdi only needs its frames collected, and Sunshine takes the picture from the X server.</figcaption></figure>\n<h2 id=\"where-gnome-puts-it\">Where GNOME puts it</h2>\n<p>The landscape problem was GNOME doing its job. It remembers a layout for each set of monitors, and “panel, two 4K monitors and a TV screen” was a set it had never seen, so it laid them out from scratch. It also only saves a layout once someone clicks Keep Changes, and while the X server was stalling it lost track of which layout went with which set, so the normal three-screen one came back landscape too.</p>\n<p><code>tv-screen-layout.py</code> takes the current layout, adds the TV screen to the left of the laptop panel, and applies it over D-Bus with Mutter’s <code>ApplyMonitorsConfig</code> method. First it applies it temporarily, then persistently, so if nobody clicks, GNOME falls back to the right layout rather than its defaults. One click later, I plugged the screen in and took it out three times in a row, and both layouts came back correctly each time.</p>\n<h2 id=\"only-while-the-tv-is-watching\">Only while the TV is watching</h2>\n<p>Sunshine runs a prep command before a session and an undo command after it. <code>tv-screen-prep.sh do</code> starts the program and waits until the desktop has switched the screen on, and <code>undo</code> stops it:</p>\n<pre><code>output_name = DVI-I-2-1\nglobal_prep_cmd = [{\"do\":\".../tv-screen-prep.sh do\",\"undo\":\".../tv-screen-prep.sh undo\",\"elevated\":false}]\n</code></pre>\n<figure class=\"shot shot--prose\"><img class=\"theme-img theme-img--light\" src=\"/sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id.png.webp?itok=bOHzab9n\" srcset=\"/sites/default/files/styles/almanac_shot_md/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id.png.webp?itok=2LwssOj- 1200w, /sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id.png.webp?itok=bOHzab9n 2304w\" sizes=\"(width &gt;= 39em) 37rem, calc(100vw - 2rem)\" alt=\"Sunshine’s Display Id setting, set to DVI-I-2-1, with its help text recommending a display’s connector name, such as DP-0.\" width=\"2304\" height=\"796\" loading=\"lazy\" decoding=\"async\"><img class=\"theme-img theme-img--dark\" src=\"/sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id-dark.png.webp?itok=hNkLKJn8\" srcset=\"/sites/default/files/styles/almanac_shot_md/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id-dark.png.webp?itok=Gw0RH47w 1200w, /sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id-dark.png.webp?itok=hNkLKJn8 2304w\" sizes=\"(width &gt;= 39em) 37rem, calc(100vw - 2rem)\" alt=\"Sunshine’s Display Id setting, set to DVI-I-2-1, with its help text recommending a display’s connector name, such as DP-0.\" width=\"2304\" height=\"796\" loading=\"lazy\" decoding=\"async\"><figcaption>The same setting in Sunshine’s web interface. Its help text recommends a connector name, and on this laptop a number doesn’t work.</figcaption></figure>\n<p>The first session logged a warning, “Couldn’t get info for requested display [DVI-I-2-1], defaulting to recording entire virtual desktop”, which looked bad and wasn’t. Sunshine tests its encoders before running the prep command. The stream itself started 0.65 seconds later, after it:</p>\n<pre><code>Info: Streaming display: DVI-I-2-1 with res 3840x2160 offset by 0x373\n</code></pre>\n<p>When a session ended, though, the screen stayed plugged in. Sunshine keeps the Desktop app running after a disconnect so the client can resume, and only runs the undo when the app is closed. <code>tv-screen-follow.sh</code> follows Sunshine’s log and, on “CLIENT DISCONNECTED”, closes the app through Sunshine’s own API, <code>POST /api/apps/close</code>. Closing the app runs the undo. The next time the TV left for real, the screen came out two and a half seconds later, two of them a pause the script waits on purpose.</p>\n<p>The last surprise was on the TV. Moonlight’s resolution setting was 720p, so every frame had been scaled down to 1280 by 720 before it left the laptop. At 4K the TV now gets the screen pixel for pixel.</p>\n<h2 id=\"what-it-misses\">What it misses</h2>\n<p>Text on a 4K screen at normal scaling is drawn at the same size in pixels as on a 4K monitor, so from the sofa it’s small. That’s fine for a meeting or a video and less so for reading. GNOME on X11 can’t scale one screen on its own, so the choice is between 4K and setting the TV screen to 1080p in Settings.</p>\n<p>I haven’t measured the delay, or tried a whole meeting on it. Moonlight crashed once, seven minutes into a 720p session, with a decoder error on the TV, and I don’t know why. The 4K session since has run for more than a quarter of an hour without one. What the TV remote’s arrows do during a session hasn’t been tested either.</p>\n<p>It’s X11 only. GNOME on Wayland has virtual monitors of its own for remote desktops, which might make most of this unnecessary, but I haven’t tried it.</p>\n<p>And a screenshot of the TV is now a screenshot of the laptop. While a session runs, anything that captures the TV’s screen captures whatever is on the TV screen, messages and all.</p>\n<h2 id=\"using-it\">Using it</h2>\n<p>With the driver installed and the laptop restarted:</p>\n<pre><code class=\"language-bash\">gcc -O2 -Wall -o laptop/tv-screen laptop/tv-screen.c -levdi\npython3 -I laptop/make-edid.py laptop/tv-screen-4k.edid\nsystemctl --user link \"$PWD/laptop/tv-screen.service\"\nsystemctl --user enable --now \"$PWD/laptop/tv-screen-follow.service\"\npython3 -I laptop/tv-screen-layout.py\n</code></pre>\n<p>Then set <code>output_name</code> and <code>global_prep_cmd</code> in Sunshine, and the resolution in Moonlight. On the TV, Moonlight, the laptop, then Desktop, and the laptop gains a screen to the left of its own. The code and the full setup are on <a href=\"https://github.com/tomgould/tcl-google-tv-tools/blob/main/docs/second-screen.md\">GitHub</a>, and the <a href=\"/projects/tcl-google-tv-tools\">project page</a> lists the rest of the scripts.</p>"
    },
    "markdown_text": "---\ntitle: 'A fourth screen on the living-room TV'\ncanonical: 'https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv'\nurl: 'https://www.dealmobile.co.uk/writing/a-fourth-screen-on-the-living-room-tv'\ntype: Post\npublished: '2026-10-23'\nsummary: 'Giving a Linux laptop an extra 4K screen that only exists while the TV is watching, with Sunshine and Moonlight to stream it, a virtual display driver instead of a dummy plug, and the black screens and lost layouts on the way.'\n---\n\n# A fourth screen on the living-room TV\n\nPublished 2026-10-23\n\n![The laptop’s desktop drawn to scale: the 3840 by 2160 TV screen on the left, then the laptop’s own panel, then two portrait 4K monitors.](https://www.dealmobile.co.uk/sites/default/files/styles/almanac_shot_md/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/hero.png.webp?itok=pLcCCiam)\n\nCasting a browser tab to the TV is fine for a video. It isn’t a second screen: the sound of a meeting cast from a tab stays split from the laptop, and nothing outside the browser comes along. I wanted the TV I [decluttered over ADB](https://www.dealmobile.co.uk/writing/decluttering-a-tcl-google-tv.md) to be a fourth screen for my laptop: drag a window onto it and it plays on the TV, with the TV’s sound, while the other three screens stay mine. The laptop runs Ubuntu 22.04 and GNOME on X11, with an RTX 4080, its own panel and two portrait 4K monitors behind a DisplayPort hub. The streaming is done by tools built for games. The screen itself doesn’t exist, and that turned out to be the interesting bit. This is how.\n\n## Streaming a screen\n\nSunshine runs on the PC and encodes a screen on the GPU, and Moonlight on the TV plays it back. Sunshine’s Ubuntu package installs a systemd user service, and the firewall lets only the TV in:\n\n```bash\nsudo apt install ./sunshine_2026.914.233613-1+ubuntu22.04_amd64.deb\nsystemctl --user enable --now app-dev.lizardbyte.app.Sunshine\nsudo ufw allow from 192.168.1.x to any port 47984,47989,48010 proto tcp\nsudo ufw allow from 192.168.1.x to any port 47998:48010 proto udp\n\n```\n\nTwo settings in `~/.config/sunshine/sunshine.conf` made it work. `capture = x11` stops a hang at start-up, where Sunshine tries NVIDIA’s fast capture, which needs a patched driver on consumer cards, and never moves on. `output_name` has to be a name such as `eDP-1-1`, not a number, because the capture code counts every output the X server lists, connected or not, while the session code matches names.\n\nThat streams one of the laptop’s existing screens. A fourth screen needs a monitor the desktop can lay windows on and nobody is looking at.\n\n## A monitor that isn’t there\n\nThe usual answer is a dummy HDMI plug, a few pounds’ worth of chip that tells the laptop a monitor is connected. NVIDIA’s driver can fake one too, with its `ConnectedMonitor` option, but that replaces the driver’s own detection of every display, and the two 4K monitors come and go with the hub.\n\nThe third option is evdi, the virtual display driver behind DisplayLink docks. Ubuntu packages it for 22.04, with patches for the 6.8 kernel and for running without a DisplayLink device, and Secure Boot was off, so DKMS could build it without signing:\n\n```bash\nsudo apt install evdi-dkms libevdi0 libevdi0-dev\necho 'options evdi initial_device_count=1' | sudo tee /etc/modprobe.d/evdi.conf\necho evdi | sudo tee /etc/modules-load.d/evdi.conf\n\n```\n\nOne more file, `/etc/X11/xorg.conf.d/20-evdi.conf`, tells X11 to drive it with the modesetting driver and `AccelMethod none`, which is DisplayLink’s advice for NVIDIA machines. After a restart, `xrandr` listed a third display provider and a new output, `DVI-I-2-1`, disconnected: a port with nothing in it.\n\n## Plugging it in\n\nSomething has to plug a monitor into that port. A monitor describes itself with an EDID, a 128-byte block listing its name and modes, so `make-edid.py` writes one by hand: 3840 by 2160 at 60 Hz with the standard 594 MHz timing, 1920 by 1080 as a second mode, and the name “TV screen”. `edid-decode --check` passes it. A C program of about 140 lines hands it to evdi:\n\n```c\nevdi_connect(handle, edid, edid_len, 3840 * 2160, 3840 * 2160 * 60);\n\n```\n\nGNOME saw a new monitor appear, and turned both portrait monitors landscape.\n\n## Black screens\n\nThe first version of the program connected the monitor and then just waited. Within a minute every screen was black, and the kernel log said why, every 10 seconds:\n\n```\nevdi evdi.0: [drm] *ERROR* flip_done timed out\nevdi evdi.0: [drm] *ERROR* [CRTC:35:crtc-0] commit wait timed out\n\n```\n\nevdi doesn’t finish a display update until its client has collected the frame. A DisplayLink dock collects them to send down the cable. My program didn’t, so the X server waited on each update, and everything else waited on the X server. The fix is to behave like a dock and throw the frames away:\n\n```c\nstatic void request_frames(void)\n{\n\twhile (pixels && evdi_request_update(handle, BUFFER_ID))\n\t\tgrab();\n}\n\n```\n\n`grab()` copies the changed areas into a buffer registered when the mode is set, and an `update_ready` event calls it again for the next frame. Sunshine doesn’t need those pixels, because it reads the screen from the X server like any other. Since then there hasn’t been a single timeout. While a session runs, it costs about 8% of one CPU core at 1080p and about 23% at 4K.\n\n![How the fourth screen works. Inside the laptop, the X server draws the desktop across four screens. It sends display updates to the evdi screen DVI-I-2-1, where tv-screen collects each frame and throws it away. Sunshine reads the same screen’s area from the X server, encodes it as HEVC on the GPU and sends it over Wi-Fi, with the sound, to Moonlight on the TV, which decodes it at 3840 by 2160 and 60 Hz.](https://www.dealmobile.co.uk/sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/screen-pipeline.png.webp?itok=-adRf1-6)\n\n*Two readers of one screen: evdi only needs its frames collected, and Sunshine takes the picture from the X server.*\n\n## Where GNOME puts it\n\nThe landscape problem was GNOME doing its job. It remembers a layout for each set of monitors, and “panel, two 4K monitors and a TV screen” was a set it had never seen, so it laid them out from scratch. It also only saves a layout once someone clicks Keep Changes, and while the X server was stalling it lost track of which layout went with which set, so the normal three-screen one came back landscape too.\n\n`tv-screen-layout.py` takes the current layout, adds the TV screen to the left of the laptop panel, and applies it over D-Bus with Mutter’s `ApplyMonitorsConfig` method. First it applies it temporarily, then persistently, so if nobody clicks, GNOME falls back to the right layout rather than its defaults. One click later, I plugged the screen in and took it out three times in a row, and both layouts came back correctly each time.\n\n## Only while the TV is watching\n\nSunshine runs a prep command before a session and an undo command after it. `tv-screen-prep.sh do` starts the program and waits until the desktop has switched the screen on, and `undo` stops it:\n\n```\noutput_name = DVI-I-2-1\nglobal_prep_cmd = [{\"do\":\".../tv-screen-prep.sh do\",\"undo\":\".../tv-screen-prep.sh undo\",\"elevated\":false}]\n\n```\n\n![Sunshine’s Display Id setting, set to DVI-I-2-1, with its help text recommending a display’s connector name, such as DP-0.](https://www.dealmobile.co.uk/sites/default/files/styles/almanac_shot/public/content/images/posts/a-fourth-screen-on-the-living-room-tv/sunshine-display-id.png.webp?itok=bOHzab9n)\n\n*The same setting in Sunshine’s web interface. Its help text recommends a connector name, and on this laptop a number doesn’t work.*\n\nThe first session logged a warning, “Couldn’t get info for requested display \\[DVI-I-2-1\\], defaulting to recording entire virtual desktop”, which looked bad and wasn’t. Sunshine tests its encoders before running the prep command. The stream itself started 0.65 seconds later, after it:\n\n```\nInfo: Streaming display: DVI-I-2-1 with res 3840x2160 offset by 0x373\n\n```\n\nWhen a session ended, though, the screen stayed plugged in. Sunshine keeps the Desktop app running after a disconnect so the client can resume, and only runs the undo when the app is closed. `tv-screen-follow.sh` follows Sunshine’s log and, on “CLIENT DISCONNECTED”, closes the app through Sunshine’s own API, `POST /api/apps/close`. Closing the app runs the undo. The next time the TV left for real, the screen came out two and a half seconds later, two of them a pause the script waits on purpose.\n\nThe last surprise was on the TV. Moonlight’s resolution setting was 720p, so every frame had been scaled down to 1280 by 720 before it left the laptop. At 4K the TV now gets the screen pixel for pixel.\n\n## What it misses\n\nText on a 4K screen at normal scaling is drawn at the same size in pixels as on a 4K monitor, so from the sofa it’s small. That’s fine for a meeting or a video and less so for reading. GNOME on X11 can’t scale one screen on its own, so the choice is between 4K and setting the TV screen to 1080p in Settings.\n\nI haven’t measured the delay, or tried a whole meeting on it. Moonlight crashed once, seven minutes into a 720p session, with a decoder error on the TV, and I don’t know why. The 4K session since has run for more than a quarter of an hour without one. What the TV remote’s arrows do during a session hasn’t been tested either.\n\nIt’s X11 only. GNOME on Wayland has virtual monitors of its own for remote desktops, which might make most of this unnecessary, but I haven’t tried it.\n\nAnd a screenshot of the TV is now a screenshot of the laptop. While a session runs, anything that captures the TV’s screen captures whatever is on the TV screen, messages and all.\n\n## Using it\n\nWith the driver installed and the laptop restarted:\n\n```bash\ngcc -O2 -Wall -o laptop/tv-screen laptop/tv-screen.c -levdi\npython3 -I laptop/make-edid.py laptop/tv-screen-4k.edid\nsystemctl --user link \"$PWD/laptop/tv-screen.service\"\nsystemctl --user enable --now \"$PWD/laptop/tv-screen-follow.service\"\npython3 -I laptop/tv-screen-layout.py\n\n```\n\nThen set `output_name` and `global_prep_cmd` in Sunshine, and the resolution in Moonlight. On the TV, Moonlight, the laptop, then Desktop, and the laptop gains a screen to the left of its own. The code and the full setup are on [GitHub](https://github.com/tomgould/tcl-google-tv-tools/blob/main/docs/second-screen.md), and the [project page](https://www.dealmobile.co.uk/projects/tcl-google-tv-tools.md) lists the rest of the scripts.\n\n- **Projects:** [TCL Google TV Tools](https://www.dealmobile.co.uk/projects/tcl-google-tv-tools.md)\n- **Tags:** [Linux](https://www.dealmobile.co.uk/writing/tag/linux.md), [Android](https://www.dealmobile.co.uk/writing/tag/android.md), [TCL Google TV Tools](https://www.dealmobile.co.uk/writing/tag/tcl-google-tv-tools.md)\n\n---\n\nIndex of this site: [llms.txt](https://www.dealmobile.co.uk/llms.txt)\n"
}