# Live TV channel previews that loop

> Tomo TV loops a five-second live TV channel preview clip on every card of its channel wall. Each channel is opened once: the first keyframe becomes the card still and the next five seconds are encoded on device to 360p at 1 Mbps. Clips are written as a temporary file and renamed only when whole. A red badge reads LIVE for 15 seconds after the picture changes, then counts down to the next grab. The player pool starts at 12 on Apple TV and lowers itself when the hardware decoder reports busy.

- ID: LAB-009
- Area: Live TV · Channel wall
- Problem: Live TV channels have no thumbnails, and providers cap how many streams a client can open at once.
- Solution: One open per channel gives a still frame and a five-second clip, encoded small on the device and looped on every card in view.
- Status: In production
- Published: 2026-10-05
- Updated: 2026-10-11
- Author: Keiver Hernandez
- URL: https://keiver.dev/lab/live-tv-channel-previews-that-loop
- Source: https://github.com/keiver/tomotv

A grid of channel names tells you nothing about what is on. Every card on Tomo TV's channel wall loops a five-second live TV channel preview clip, recorded on the device. The catch is the provider. It rations live streams, so each clip has to come out of a single open.

![The Channels screen in Tomo TV on Apple TV, a grid of channel cards that each show a frame of what is on, with the first card focused](/lab/live-tv-channel-previews-that-loop/channel-wall-tv.webp "The channel wall on Apple TV. Each card shows a still or loops a short clip recorded on the device.")

## Live TV channel preview clips come from one open

Many providers cap how many live streams one account holds at once. A still from one open and a clip from another would spend two connections on one card. The engine opens each channel once and takes both.

The first keyframe becomes the card's still, on screen while the rest is read. The next five seconds become the clip. The device encodes it to at most 360 pixels tall, 15 frames per second and 1 Mbps. When the clip lands, the card swaps its still for the loop.

@[video](lab/live-tv-channel-previews-that-loop/channel-wall-iphone.mp4 "The same wall on iPhone, every card looping its own clip."){autoplay=true, loop=true, muted=true, playsinline=true, controls=false}

The sampler reads two channels at a time, never two from the same provider host. It starts grabs at least a second apart. The moment playback takes the link, it stops and cancels the reads in flight.

```mermaid
flowchart LR
    A[Channel in view] --> B[Open to provider]
    B --> C[First keyframe: card still]
    B --> D[Next 5 s: encode clip]
    D --> E[Write as .part]
    E --> F{Whole?}
    F -- yes --> G[Rename: clip ready]
    F -- no --> H[Remove, card keeps still]
    G --> I[Card loops clip]
```

## Written whole or not at all

A clip cut short looks broken on a loop, and plenty can cut one short. Playback starts. The viewer picks another channel group. A slow source runs into the grab's deadline, 9 seconds for a first picture and 12 for a refresh.

The engine writes the clip to a `.part` file. It gives the file its final name only after the last frame is flushed and the MP4 trailer is written. A cancelled grab deletes the partial file, and the card keeps its still. A clip holding less than 60 percent of its five seconds is thrown away too. Three seconds is the shortest loop a card will ever play.

## Starting on the keyframe

A live stream is joined in the middle of a group of pictures (GOP), and those first packets cannot be decoded alone. The clip waits for the first keyframe and sets its own zero on that frame's presentation time. Every loop opens on a whole picture, with no empty gap at its head.

The player had its own flash. Hiding its layer ran Core Animation's implicit fade, which painted an empty black player over the card. Hides now run with animations off. A newer clip for the same card waits for the next loop seam and cuts in there, with the layer still visible.

## The badge says how old the picture is

A card's picture counts as fresh for 15 seconds. For that long its red badge reads LIVE. Then the word fades and the pill shrinks to a red circle holding a pale wedge.

The wedge is a countdown to that channel's next grab. That is two minutes after a picture that moved, and up to five when the channel keeps sending the same keyframe. The card in focus is read again after 30 seconds. When the wedge empties, the circle pulses until a new picture lands and LIVE comes back.

![The Channels screen in Tomo TV's Mac app, rows of seven channel cards. Some show a frame of what is on with a red badge in the corner, and the rest show only a channel logo](/lab/live-tv-channel-previews-that-loop/channel-wall-mac.webp "The channel wall in the Mac app. One card reads LIVE. The red circles are older pictures counting down to their next grab, and cards showing only a logo have no picture, so no badge.")

## The device learns its own limits

Two limits decide how many clips loop at once: the hardware decoder and the provider.

The player pool starts at 12 on Apple TV, 16 on iPhone and 48 on a Mac. When a clip fails with a decoder-busy error, the pool caps itself at the number still playing. That card keeps its still, and cards past the cap wait for a player to free up. Nobody configures this. The device finds its ceiling at the first failure.

The provider's limit is learned the same way. When an open is refused while the device already holds other connections to that provider, the number held becomes its budget.

Our first attempt at this was blunt. Three refused opens in a row meant "provider cap", and the whole sampler slept for three minutes. Three unrelated dead channels were enough to trip it, and it tripped again on the first failure after every rest. Minutes in, a wall had three clips. Now a failed channel backs off alone, two minutes doubling up to ten, and the rest of the wall keeps filling.

---

Background work on a TV app competes with the one thing on screen. Every connection to a provider takes a ranked lease first. A card grab ranks lowest: it takes a slot only when one is free, and playback revokes it the moment it needs the connection. Once no card is playing, idle players stay five seconds for the next scroll, then go. Let background work learn its limits from real failures, and make it yield every time.

See also: [channel surfing on Apple TV with AVKit](/lab/tvos-channel-surfing-avkit), [generating video thumbnails on device](/lab/generating-video-thumbnails-on-device), and [Tomo TV on the lab page](/lab/tomotv).

Drafted with Claude from Tomo TV's code and device measurements. Reviewed, edited and verified by Keiver Hernandez.

Cite as: Keiver Hernandez, "Live TV channel previews that loop", keiver.dev/lab/live-tv-channel-previews-that-loop, 2026-10-05.
