Skip to content
brandomoorePublic

About

Free, open-source Apple TV streaming viewer for Twitch, YouTube, and supported Kick simulcasts, with multi-view and native chat emotes.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Strozz logo

Strozz

Your streams, together on Apple TV — Twitch, YouTube, and Kick simulcasts, with chat and native emotes.

License: MIT Platform: tvOS Donate

Strozz brings live streams and chat together on the big screen. Watch Twitch and YouTube, follow supported creators across their YouTube and Kick simulcasts, or watch several channels in multi-view. It's built for the Apple TV remote and the tvOS focus engine — not a stretched phone app — with native 7TV, BTTV, and FFZ emotes. It's free and open source.

An initial iPhone and iPad app (iOS 18+) is also available to build from the StrozzMobile scheme. It shares the Twitch streaming, account, and chat services, with a separate touch interface rather than the TV's remote controls.

Features

The feature list below describes the Apple TV app. See iPhone and iPad for the smaller mobile feature set.

Watch

  • Chat beside the video. Live streams play with the video on the left and a chat pane on the right, so you never have to choose between watching and reading along.
  • Low latency by default. A low-latency mode closes most of the gap to the live edge, so you're not minutes behind the moment. Healthy playback that drifts behind uses a held 1.05x catch-up rate, without an automatic seek/reload or repeated speed changes as the segment buffer fluctuates.
  • Rewind live. Seek back within the live window (DVR) to catch what you missed without leaving the stream.
  • Pick your quality. Choose Auto or an explicit resolution, ordered highest-to-lowest, and Strozz remembers your choice.
  • Audio-only mode. Drop to audio with a reactive visualizer — handy for music streams, Just Chatting, or background listening.
  • Sleep timer. Set a timer or "end of stream," with a gentle "still watching?" check, a starry sleeping screen, and one press to snap back to the live edge.
  • VODs and clips. Watch past broadcasts and top clips from channel pages; VODs include synced chat replay and variable speed (0.5×–2×).
  • Multi-view. Watch several live channels at once, picked from your follows and recommendations.
  • Live captions (beta). Optional on-device captions for streams, with size, position, and styling controls.

Chat

  • Third-party emotes, built in. 7TV, BTTV, and FFZ emotes (global and channel, including animated ones) render right alongside Twitch's native, sub, and channel emotes.
  • Badges and bits. Global and channel badges plus cheermotes are shown just like they are on the web.
  • Read anonymously, or chat when signed in. Chat connects anonymously by default and auto-reconnects; sign in to send messages.
  • Replay keeps up with live recordings. After a deep rewind, chat replay checks for newly archived comments rather than stopping at the current last page.
  • Make chat yours. Adjust text and emote size, font (including OpenDyslexic), spacing, width, and layout — side, overlay, or glass.
  • Live moments surfaced. Polls, predictions, hype trains, creator goals, and incoming/outgoing raids appear as calm, display-only overlays.
  • Simulcast chat merge (experimental). When a streamer you're watching is also live on YouTube or Kick, their chats can be merged into a single pane. When YouTube chat connects, the player can show its live viewer count even if the streamer is not in the shared YouTube alias catalog.

Discover

  • Home built around your follows. See the channels you follow that are live now, plus recommendations.
  • Stable loading rows. Home and Browse use card-sized skeletons while their first results load. Existing cards stay visible during refresh, and Home's horizontal rails keep their space even when a result is empty.
  • Recommendations you control. Optional personalized picks built from on-device watch history and your followed categories — or anonymous trending when you're signed out or have it turned off.
  • Browse and search. Explore top categories and their live streams, and search channels and categories with live results.
  • Channel pages. Top clips, past broadcasts, and similar channels for every channel.
  • Top Shelf. Your live follows and recommendations surface on the tvOS home screen above the app icon.
  • YouTube, too. Connect a YouTube account to see your subscribed streamers who are live and watch YouTube-only streams; streamers live on both platforms show up as one combined card.

Make it comfortable

  • Themes. System, Dark, OLED, and Light.
  • Night Shift. An optional warm screen wash that eases in after sunset on a solar or manual schedule.
  • Tune the grid. Adjustable stream-card sizes and a stream-language filter.

Getting started

Strozz is an early, non-commercial project and isn't on the App Store. To run it you'll build it yourself from source with Xcode and your own Twitch developer client_id. See CONTRIBUTING.md for the full setup.

You'll want:

  • An Apple TV running tvOS 18 or newer (live playback and Top Shelf need real hardware).
  • A Twitch account, if you want to sign in — browsing and anonymous chat work without one.

Reporting bugs & requesting features

Found a bug or have an idea? Please open a GitHub issue. Including your Apple TV model, tvOS version, and the stream where something went wrong helps a lot. See CONTRIBUTING.md for details.

iPhone and iPad

The mobile version includes personalized Twitch recommendations, a full followed channel directory, channel profiles, past broadcasts with local resume, category browsing, search, native low-latency live playback, standard Auto and fixed qualities (including audio-only), and live chat with Twitch/7TV/BTTV/FFZ emotes. Sign in through Account > Sign in to Twitch, open the Twitch link, approve the displayed code, then return to Strozz. Browsing, playback, and reading chat also work anonymously.

Tap a chat emote on iPhone or iPad to open a larger preview with its name and, when recognized, its provider. Supported providers use higher-resolution artwork; if that image is unavailable, the sheet labels its fallback to the chat image and offers retry when loading fails. Animated previews respect both the chat animation preference and Reduce Motion. Dismiss the sheet to return to chat without stopping playback.

The rounded chat composer grows as you type. Its … button opens Chat settings when the draft is empty and becomes Send when there is text. The keyboard's Send key also sends; pasted multiline text remains supported. The composer follows the selected theme and uses an opaque surface with Reduce Transparency.

Opening a mobile stream shows its profile photo, description, category, and playback controls. After four seconds of inactivity they collapse together, leaving video and chat; tap the video to show or hide them again. Pausing, loading, VoiceOver, and open playback-control sheets keep them visible. The video surface and chat session stay mounted throughout the transition. Chat starts directly below the collapsed profile without a permanent heading; a connection status appears only while chat is reconnecting. Messages fade gently over the top 48 points of the chat viewport, without changing scroll insets or fading the composer. Reduce Transparency and Increase Contrast disable that fade.

The video controls show one horizontal readout row: a red live dot with elapsed time, then a viewer icon and compact count, without chips or backplates. The red dot appears only at the live edge. The readouts hide with the controls; the Account > Overlays duration preference still applies. Playback quality remains in the quality menu rather than repeating below the profile. Channel points appear as a compact balance and the channel's own 18-point currency icon beside the message box, eight points from the leading edge (a gift icon is used when no image is available). Tap it for the streamer's currency name, exact balance, watch streak, and any rewards errors; opening it does not spend points. Only a loaded balance is shown, never a guessed zero.

The iPhone rotation button uses a rotation glyph. The chat toggle appears only in landscape: iPhone starts video-only and can open or hide side-by-side chat; iPad offers the same toggle when its side-by-side layout is available. Portrait chat stays below the video without a misleading sideways collapse button. Drafts, in-flight sends, and the chat reading state are shared across those layouts. Chat keeps its exact-height scrollback, but only emotes intersecting the viewport animate. Offscreen animation buffers are released, and all chat animation pauses during minimization, while chat is hidden, or in the background. Incoming chat continues to collect in its bounded buffer; hidden/gesture-time view updates are deferred until chat is visible again, without reconnecting or changing playback.

Chat settings are also available from Account and when reading anonymously. Adjust mobile size presets, fonts, spacing, emote sizing and animation, badges, mention/reply/keyword highlights, and extra-delay chat sync. Existing mobile sizes remain the defaults; resetting appearance preserves saved keywords. Optional incoming YouTube and Kick chat are off until enabled. While watching a stream, edit its other channel handles or URLs and tap Apply channel targets; defaults use the Twitch handle, not automatic cross-platform account matching. Targets belong to the active stream session. Sending remains Twitch-only.

Selecting a fixed video quality retains the native engine when native playback is selected, on both TV and mobile. Explicit standard playback, Audio Only, and AirPlay retain their standard paths; a genuine unsupported-format fallback is still reported rather than disguised as native playback.

Home automatically previews one mostly visible stream at a time, nearest the middle of the screen. A small, leading-aligned heading scrolls with the feed instead of occupying a fixed navigation bar. Previews are always muted and stop when you scroll away, switch tabs, open a stream, or background the app. Lower-bandwidth preview renditions are preferred; an undecodable preview gets one Source-quality retry. Live Twitch previews on both platforms use the native low-latency engine and its guarded recovery path, rather than a separate standard-HLS player. Home defaults to For you: live followed and most-watched channels lead the feed, followed by personalized discovery from the shared recommendation engine. Watch frequency on this device ranks familiar channels; global popular streams are only the signed-out/no-history or personalization-disabled feed. Following is a flat list of every followed channel, including offline ones. Tap an offline channel to open its profile and past broadcasts; long-press any followed row for its profile or a saved broadcast's Continue option. Home, Browse, and Account remain in the bottom bar. Home also shows up to six compact live-followed shortcuts. Large stream thumbnails show a red-dot Live badge at top-right, viewer counts at bottom-left, and the muted preview indicator at bottom-right. Compact Following thumbnails combine a red live dot and viewer count in one small badge, without a separate LIVE pill. Anonymous/demo recommendations are never presented as personal follows. The category rail scrolls to the screen edges, and the feed draws behind the floating tab bar while leaving enough end-of-feed clearance to reach the last card. For you / Following pins below the status bar as you scroll; the Strozz heading and category filters scroll away.

Mobile loading placeholders use the same artwork and reserved text-line sizes as the loaded cards. Live-followed shortcuts remember the last successful live count per account (up to six) and reserve that many skeleton cards while loading. An unknown account starts with six placeholders. Loaded results show only live channels, without unused rows; no live channels produces a compact empty message. Changes in row count resize smoothly, unless Reduce Motion is enabled. Cached counts are not changed by category filters or failed requests, and this adds no requests, polling, or loading animation timers. The section is omitted once the account is confirmed signed out. Chat wraps long names and links within the pane; scroll up to read earlier messages, then tap the floating Jump to present button to resume following live chat. It disappears at the live edge without reserving an empty status row or changing the chat viewport.

Swipe down anywhere over the live video, or tap its down chevron, to shrink it into Strozz's in-app mini-player and return to the page you opened the stream from. The same video surface follows your pull smoothly; a short pull springs back, and Reduce Motion skips the resizing animation. The mini-player's pause and close controls fade away after four seconds of playback. Tap once to reveal hidden controls, then tap the video again to expand it. Dragging or pinching also reveals the controls and restarts the timer. Controls stay available while paused, loading, showing an error, or using VoiceOver. Select another stream or broadcast to replace playback. Drag the mini-player with one finger to move it, or pinch to resize it. It keeps the video's proportions and stays within the available screen area. Its placement is retained when expanding and minimizing the same stream or returning from native PiP. Quick drags coast briefly on release; the edges and resize limits have gentle resistance and settle back smoothly. A deliberate fast upward or downward flick carries the mini-player to that edge; gentle releases keep a short, bounded glide. Grab it again to stop the glide. Reduce Motion keeps movement direct, without momentum or elastic settling. Player controls use white icons without individual backplates. The expanded player keeps one even, full-frame dark gradient with gentle extra edge shading. The mini-player uses one full-width fade over roughly the top half, extending farther down on smaller cards to keep the transition gradual. It disappears with the controls. Reduce Transparency strengthens the gradients without replacing the picture with an opaque panel. Home previews stay paused while a live stream is playing.

When you leave Strozz, supported video playback switches to native iOS Picture in Picture. Returning to the app brings playback back to its previous expanded or mini-player layout; PiP's system restore control returns directly to the expanded player at the top of the page, without first landing in the mini-player. Closing native PiP stops playback. If native PiP cannot start, background playback is suspended and resumes when you return. In-app minimization still works when native PiP is unavailable, including audio-only playback.

Past broadcasts use native on-demand controls with seeking. Continue watching on Home and channel profiles resumes saved progress; finished broadcasts leave that list. Watch history and resume positions stay on this device, are separated by signed-in account, and do not import Twitch's or the Apple TV's watch history. Account provides a personalization toggle and a confirmed history/progress reset. Signed-out/demo streams never masquerade as followed channels.

Video sits above chat on a portrait iPhone; landscape iPhone shows video alone. A wide iPad window places chat beside video, while narrow multitasking windows stack them. Dedicated over-video controls provide play/pause, mute, quality, Back to live (return from paused/delayed playback), fullscreen/rotation, chat visibility, sharing the Twitch link, and Apple's AirPlay picker. Rendering still uses AVKit. Controls hide after inactivity and reappear on tap; they stay available when paused or using VoiceOver. AirPlay switches native low latency to standard playback because a receiver cannot access the app's loopback media server; keep the app open while using it. System, Dark, OLED, and Light appearances are available in Account, and chat follows Dynamic Type and Reduce Motion. App panels use opaque theme-aware surfaces. Browse shows three categories across on iPhone (two at accessibility text sizes) and an adaptive grid on iPad. Typing in Browse's search field switches to compact channel and category results with artwork and viewer counts.

TV and mobile share the stream loading view and a loading/ready/unavailable presentation contract. Until playback is prepared and video is displayable, mobile shows the channel poster, avatar, and one native loading indicator; play/pause and other transport controls are not layered over it. Close remains available, and errors replace loading with a retry action. The mobile Live badge describes the achievable playback edge, not the broadcast's absolute delivery delay. Back to live appears only while paused or measurably behind; unknown timing shows Checking live instead. The status uses the shared source-relative delay estimate and catch-up tolerances so normal segment/buffer variation does not flash an unnecessary jump button. Player stream information on TV and mobile also shows a compact clock-and-duration readout (for example, 2h 14m), using Twitch's broadcast start time rather than the viewer's watch time. Overlays > Stream Duration in the TV player controls can hide it; mobile offers the same option under Account > Overlays. It is enabled by default and saved per device. The readout updates once per minute, keeps counting while playback is paused, and is omitted when the start time is unknown. Mobile fetches this optional metadata separately so it cannot delay playback startup.

This is not full TV feature parity: VOD chat replay, clips, multiview, YouTube/Kick playback and chat merging, interactive reward redemption/polls, and advanced TV settings are not included. Playback stops in the background; Picture in Picture/background audio are not yet supported. Returning resolves fresh stream URLs instead of reviving an expired native engine. Paused/rewound positions are preserved when still available; an expired position shows an error rather than silently jumping to live. Like TV, mobile recreates both its player and AVKit rendering owner on foreground return and media-service reset, preserving mute, volume, quality, and pause/DVR intent. Both targets use the same audio-session setup and bounded date-position restoration. Audio activation failures surface an error without exhausting the native stream retry budget; interruptions honor the system's resume permission. Transient native-engine failures get up to two fresh native attempts in a rolling minute before standard fallback; unsupported formats still fail over explicitly instead of leaving playback stuck. If audio advances without decodable video, the mobile player makes one recovery attempt using the primary video rendition and displays the actual selected quality and a notice. A failed recovery shows an error instead of staying black.

Use the same Twitch client configuration described in CONTRIBUTING.md, then:

./tools/generate-project.sh
./tools/xcbuild.sh -project Strozz.xcodeproj -scheme StrozzMobile \
  -destination 'generic/platform=iOS Simulator' build
# Substitute an available iPhone or iPad simulator UUID:
./tools/xcbuild.sh -project Strozz.xcodeproj -scheme StrozzMobile \
  -destination 'platform=iOS Simulator,id=<SIMULATOR_UUID>' test

StrozzMobileTests covers playback lifecycle and layout policy; StrozzMobileUITests covers navigation and themes. Set STROZZ_MOBILE_LIVE_TESTS=1 in the scheme's Test environment to opt into bounded real-stream/frame, browse/search, quality, and rotation checks. They connect anonymously, run muted, and never send chat messages. Set STROZZ_MOBILE_LIVE_CHANNEL to a live channel for the native full-player check; the test requires native playback rather than treating a compatibility fallback as a native success.

Both platform targets use com.thatcube.Strozz in the new universal App Store Connect record. This is a separate app from the legacy com.thatcube.Twozz installation, not an in-place update. Twitch sign-in and the optional rewards connection now sync through encrypted records in your private iCloud database between devices using the same Apple Account. The existing Fastlane lanes still ship tvOS only. Adding this target does not upload or distribute an iOS build.

Contributing & development

Twitch sign-in across devices

Sign in once in the new Strozz app, then open Strozz on another iPhone, iPad, or Apple TV using the same Apple Account. The connection is fetched on launch and checked periodically while the app is running. Startup restores local state and finishes the first iCloud check before offering a new Twitch approval, so the sign-in screen cannot race and block automatic restoration. Sign in also stays out of Home, account settings, Following, rewards, and chat while that initial account check is pending; these surfaces show a neutral loading state instead of briefly suggesting that a returning viewer must sign in again. An explicit local sign-out still shows its sign-in action normally. Sign in checks for a saved connection first; there is no separate routine "Use iCloud connection" step. If iCloud is unavailable, an explicit Sign in with Twitch instead action still allows local sign-in. Different Twitch accounts on the same Apple Account require an explicit choice; Strozz does not silently overwrite one with the other.

Connect rewards requires a separate Twitch approval for the same Twitch account. On iPhone/iPad it opens Twitch's prefilled approval link directly and keeps the connection process alive when you return from the browser. Once connected, that authorization also syncs. Mobile live playback reports observed watch time and shows Twitch-provided points/streaks; Twitch remains authoritative about credit. Preview, paused and background time do not count.

Tokens are cached in device Keychain, with only the access token shared with Top Shelf. Cloud copies use CKRecord.encryptedValues in the private iCloud.com.thatcube.Strozz database; no credential fields have public-database permissions. Sign out affects only this device and suppresses automatic restoration until the viewer chooses Sign in again. Sign out everywhere requires confirmation and writes a cloud sign-out marker that the other devices observe when connected. It signs out Strozz's synced connections, not unrelated Twitch apps or browser sessions.

Twitch's device-flow refresh tokens are single-use. A conditional cloud record update reserves renewal before contacting Twitch, and a rotated pair is saved locally before publication. Other devices never take over an ambiguous renewal after an arbitrary timeout. If the renewing device loses connectivity, reopen Strozz there to finish; if it crashed before saving the new pair, Twitch approval may be necessary again. Expiry, revoked Twitch permission, or an unavailable iCloud account can also require attention; the UI reports these rather than claiming a permanent login.

Config/StrozzAccounts.ckdb is the versioned CloudKit schema. Deploy its Development schema to Production before shipping TestFlight builds. Simulator tests cover conditional-write contention, account boundaries, pending renewal and sign-out fencing. Explicit Debug-only probes verify private encrypted cross-device reads and Keychain access using synthetic data, not Twitch tokens. Viewing history and VOD progress remain device-local.

Build instructions, the Twitch auth setup, how playback is resolved, versioning, and release steps all live in CONTRIBUTING.md. Notes on the low-latency playback work are in docs/low-latency.md.

Brand assets

Strozz now has its own Apple app identity: com.thatcube.Strozz, with com.thatcube.Strozz.TopShelfExtension, App Group group.com.thatcube.Strozz, and Keychain service com.thatcube.Strozz.watch-rewards. The new universal App Store Connect record is 6819913170; the legacy com.thatcube.Twozz record (6782643545, now named Strozz Old) and its installed data remain untouched.

This is a clean replacement installation. Testers install the new TestFlight app and sign in again; local preferences, history, and saved sessions are not automatically imported. Separate storage prevents signing out of the new app from deleting the old app's credentials. Twitch-side follows, points, and streaks remain attached to the Twitch account. iCloud sign-in sync uses the new iCloud.com.thatcube.Strozz container; the old app does not participate.

The Xcode project, scheme, source module, assets, and repository use Strozz. Channel links use strozz://; the parser also recognizes legacy twozz:// and twizz:// links. With both apps installed, custom-scheme routing can be ambiguous; use the intended app's own navigation until switching fully to the replacement. Shared build/cleanup protocol identifiers also stay unchanged for interoperability. Historical Git branches and commits are not renamed or rewritten.

The diagnostics CLI defaults to the new app. To inspect a still-installed legacy build, explicitly pass --bundle com.thatcube.Twozz. Keep historical release archives and receipts associated with their original bundle ID.

Branding/strozz_logo.svg is the canonical Strozz mark. The in-app SVG and transparent splash artwork use it unchanged; the layered tvOS icons and static Top Shelf images pair it with charcoal (#1C1C1E) and a subtle purple radial glow. The background follows Plozz's smooth treatment, without grain or static.

To regenerate all catalog variants while preserving their dimensions and layers:

python3 -m pip install -r tools/requirements-brand-assets.txt
python3 tools/generate_brand_assets.py
python3 -m unittest discover -s tools/tests -p 'test_brand_assets.py'

The iOS MobileAppIcon uses the same mark and opaque charcoal treatment. Regenerate only that 1024-pixel icon with python3 tools/generate_brand_assets.py --mobile-only.

Go Live Alerts

In-app live-channel alerts are off by default, including after updating from the old opt-out behavior. Strozz asks once on Home after Twitch sign-in: keep alerts off, enable All Channels, or Choose Channels individually. You can change this later under Settings > Go Live Alerts. Review Options reopens the introduction without resetting your selections or making the automatic prompt repeat.

All Channels includes current and future Twitch follows. Turning any channel off switches to a custom selection of your current follows, with new follows off. Turning every individual switch back on does not opt into future follows; use Enable All for that. Search-based bulk actions affect only matching channels. Turning alerts off also dismisses any pending alerts and clears the queue.

These settings stay on this Apple TV and affect only Strozz's in-app alerts. Twitch's supported Get Followed Channels API does not expose notification-bell preferences, so Strozz does not sync them or change Twitch notifications on other devices.

YouTube live-source selection

YouTube playback requires a currently live broadcast, not just a playable HLS playlist or the isLiveContent flag (which remains set on archived streams). Channel lookups use the primary player response, never arbitrary video IDs from uploads or recommendations. The native player response must confirm the selected video ID and current live status before playback. If it cannot, the YouTube source stays unavailable and Twitch remains selected; an already-selected YouTube source uses the existing bounded retry and Twitch fallback notice.

Playback diagnostics

Live playback lets AVPlayer buffer before starting instead of forcing an immediate first frame. With Prefer YouTube enabled, Strozz gives source selection up to four seconds before falling back to Twitch; it does not start Twitch and then automatically interrupt it with a YouTube switch. YouTube uses an eight-second forward-buffer preference to help absorb short delivery gaps. Because a buffer preference cannot fetch video that has not aired yet, native YouTube playback targets a six-second margin behind its available live edge. It does not automatically seek away the extra headroom gained during a buffering wait. For simulcasts, three stalls within thirty seconds trigger the existing single fresh retry, with only two extra seconds of live-edge margin; persistent trouble then falls back to Twitch instead of repeatedly reloading or adding more delay. This can add initial loading time in exchange for smoother playback; it cannot eliminate upstream or network interruptions. The loading screen clears when AVPlayer starts playing, independently of the longer startup-health check, so it does not cover video that's already audible. The in-player stream title stays with the channel across source switches and playback retries; changing channels clears it before fetching the new metadata.

Closing a live player or channel page refreshes the Home rails and the originating Following, category, or search list. Returning from a category also refreshes Browse/Search, and returning from a recording refreshes its channel page. Live thumbnails receive a new image request without replacing card identities. The same return-refresh behavior covers iPhone/iPad Home, Browse, categories, and channel profiles. Stream-card identity follows the streamer, not the broadcast ID or ranking, so tvOS can retain focus through live-status updates and reordering. Return refreshes do not force focus back to the first card. TV Browse grids use the full visible screen as their lazy-loading viewport. Header and safe-area spacing live inside the scroll content, so partially visible top or bottom rows do not disappear before reaching the screen edge.

On foreground return, account synchronization and token validation finish before refreshing Home. Normal restoration has no visible account-status message; only actual account or refresh errors are shown. Concurrent account recovery waits for the current local update and reuses a token adopted from iCloud rather than racing its single-use refresh. A temporary Following failure preserves that account's previous channels and shows a retryable error; it never replaces them with Trending or marks the failed attempt as fresh. Trending is for signed-out browsing only. Late or cancelled requests cannot overwrite a newer refresh or another account's directory.

On Apple TV, selecting a stream requests exclusive playback audio. If another app interrupts that initial handoff, Strozz makes one bounded reactivation attempt; muted panes and deliberate pauses do not repeatedly claim audio. Later interruptions offer Resume playback (also available through the remote's Play/Pause button) instead of leaving an endless loading indicator.

When you return to a stream, Strozz restarts its stall-detection window rather than counting time spent in the background as a freeze. An empty buffer or expired playlist triggers a live-status check and recovery, not a "stream ended" verdict: that message requires Twitch to confirm the channel is offline. Leaving while following live also preserves that intent: returning from the background or a channel page refreshes the current source to its live position, rather than leaving playback paused at the old point. Brief trips that remain near live avoid an unnecessary reload. Deliberate pauses, rewinds, and VOD positions are preserved. "LIVE" means the source's playable live position, including its normal buffering margin, not zero broadcast/network latency.

Multiview pauses its wall when Strozz goes into the background. On return, it re-resolves each pane's live playlist and resumes all streams without changing the chosen grid/spotlight layout or audio selection.

Multiview also uses the normal native live player for every pane. Selecting a pane zooms that same player and video surface into the single-stream layout with normal chat and controls; Back returns it to the wall without reconnecting. Explicit source/quality changes and error recovery remain real playback changes. Play/Pause opens wall controls in the grid and controls playback when expanded. Other panes remain live and muted while expanded, with a lower bitrate and resolution preference. These are adaptive preferences, not re-encoding: a Source-only feed or decode-recovery override can exceed the requested thumbnail budget. The selected pane, player item, and AVKit surface are retained; a normal layout transition does not create an extra decoder.

The TV latency readout shows numeric seconds behind the available live edge (for example, 3.18s), including the normal playback cushion rather than replacing small values with "Live." It shares the viewer-count and uptime font size and weight, respects the existing Latency Readout toggle, and hides with the playback controls. Native measurements compare dates on the same source clock; raw timestamp age can include upstream delay or clock offset and remains in Diagnostics. Mobile's separate Live/Behind transport status is unchanged.

Chat's timed read pause releases its frozen snapshot when the countdown ends; collapsing chat or changing channels also resets scrolling state. The live list follows a permanent bottom anchor as its bounded message buffer rotates. While following live, it fully lays out a viewport-sized tail rather than relying on lazy row-height estimates that can leave a blank panel after emotes resize. Pausing or scrolling still exposes the full retained history. Twitch chat checks the join handshake and sends a heartbeat every thirty seconds after joining. A missing join acknowledgement, failed send, missing heartbeat reply, or server reconnect request enters the existing backoff/rejoin loop without clearing visible messages. Quiet channels do not trigger recovery just because nobody is chatting. When playback catches up to live, queued chat is retimed to the shorter video delay and its release task wakes for the earliest pending message. Foreground return also rechecks pending deadlines, so an old pre-suspension sync delay cannot hold newer chat behind a sleeping task.

While a channel is open, its 7TV emote set is rechecked every minute, including during VOD chat replay. Newly added emotes update messages already on screen. Successful provider catalogs are cached separately; failed requests are retried without discarding known emotes or caching an outage as an empty catalog. Playback diagnostics include catalog size and pending-retry state, not emote names or chat text.

Twitch rewards and polls (experimental)

In Settings > Accounts > Twitch Rewards, connect watch rewards using the same Twitch account as your normal Strozz login. This is a separate, unofficial Twitch TV device-code connection: approve it on Twitch's activation page using your phone. Strozz never asks for your password. The rewards session is cached in a device-only Keychain item, never preferences or Top Shelf, and shared through encrypted private iCloud records. Disconnecting removes the rewards connection from synced Strozz devices; it does not sign out the normal Twitch account or revoke unrelated Twitch sessions.

When connected, Strozz reports one minute only after observing a minute of advancing, visible Twitch live playback. Pauses, buffering, seeking, background time, previews, YouTube playback, and VODs do not count. In multiview, only the selected audio pane is reported; opening the full player stops reporting the underlying grid. Changing channels or player items starts a fresh measurement. It does not farm unseen channels or share announcements.

The gift button in the live Twitch player opens Polls & Rewards without leaving the video. View your channel-point balance, cast one free vote in the current poll, or redeem streamer rewards, highlighted messages, and random, chosen, or modified emote unlocks. Each redemption requires confirmation; Strozz rechecks the current price, availability, and balance before submitting. Bits purchases, paid poll votes, predictions, and sub-only-message redemptions are not supported. Rewards marked Available on Twitch cannot be redeemed from Strozz.

Collect watch bonuses is enabled with the rewards connection and can be turned off in Accounts. It claims only Twitch-provided bonuses during observed, advancing playback, using the same visibility and multiview rules above. Opening the rewards menu alone never claims a bonus or spends points. Balances and successful actions come from Twitch acknowledgements, not local estimates. If a result is unconfirmed, check Twitch before retrying.

The player controls show the watch-streak count returned by Twitch. A missing milestone is shown as awaiting Twitch, never as a locally invented streak. An accepted watch report is not proof that Twitch credited it: eligibility and streak updates remain Twitch's decision. The integration can stop working if Twitch changes its private endpoints; errors are surfaced instead of silently claiming success. Twitch TV sessions may have no scheduled expiry (expires_in: 0); Strozz still validates them on first use after launch and hourly during viewing. Expired or revoked rewards sessions require reconnecting.

Strozz keeps a bounded, local JSONL playback log in its app cache so lag reports can be examined after the fact. Logging samples playback state about every two seconds and records noteworthy state changes, stalls, access/error-log updates, seeks, and recovery actions. It is diagnostic observation only; enabling it does not change playback tuning. Samples also include chat connection/read-pause flags, message-buffer counts, time since the last IRC frame, and reconnect counts/reasons, current sync delay, and queued release/wake deadlines; they do not include chat text or chat participants. Watch-rewards diagnostics record report acknowledgements and server-returned streak counts, never access tokens or activation codes.

Pull the retained logs from the paired Apple TV and summarize the current or most recent session:

python3 tools/playback-diagnostics.py pull --device <device-id>

Select a paired Apple TV with --device or the STROZZ_DEVICE_ID environment variable. The default bundle is com.thatcube.Strozz. Every pull goes into a new UTC-stamped directory under the gitignored playback-diagnostics/ directory:

python3 tools/playback-diagnostics.py pull \
  --device <device-id> --bundle com.thatcube.Strozz
python3 tools/playback-diagnostics.py pull --device <device-id> --session <session-uuid> --json

Previously pulled data can be analyzed without Xcode or a connected device:

python3 tools/playback-diagnostics.py analyze playback-diagnostics/<timestamp>
python3 tools/playback-diagnostics.py analyze <file.jsonl> --session <session-uuid>
python3 tools/playback-diagnostics.py analyze playback-diagnostics/<timestamp> --all --json

By default, analysis uses latest-session.json, or the session containing the newest record when no manifest is available. --all reports retained sessions separately; sessions are never silently combined. A copied, incomplete final JSON line is warned about and ignored, while completed corrupt lines and unknown schema versions fail analysis. Sequence gaps, dropped telemetry, reclaimed rotation parts, bounded native access/error-log backlog skips, and sessions that are still active are marked as partial evidence.

The cache retains at most eight 4 MiB files across all sessions (about 32 MiB) and tvOS may reclaim it. It does not contain OAuth credentials, full URLs, request headers, server IP addresses, AVPlayer session IDs, SDK localized error prose, or error comments. Failures retain only structured evidence such as error domain/code, AVPlayer error-log status code, and an explicit HTTP status when AVFoundation includes one. YouTube resolution records the client/version and sanitized failure category, never visitor context or response bodies. Public channel names and viewing timestamps do appear. The tool reads locally and never uploads logs.

YouTube simulcasts use the native-HLS client shared with YouTube-only playback and captions. Startup respects AVPlayer's native buffering wait; a stall notification does not force an empty-buffer YouTube item to restart immediately. The obsolete Android VR client and silent web-manifest fallback are not used. A terminal media error (even when the item still reports ready), or 20 seconds without clock progress while playback is intended, triggers one fresh YouTube resolution. Automatic attempts are at least 10 seconds apart. If that attempt also fails, the player refreshes the Twitch source and shows a brief, non-focusable notice. It does not change the saved YouTube preference or automatically switch back during that channel visit; the source picker remains available for a deliberate retry.

Interpret summaries cautiously. Proxy timings cover playlist/master requests, not media segment transfers; AVPlayer access-log throughput is a coarse cumulative estimate, not an instantaneous network test. Low buffer, bitrate differences, dropped frames, healthy-buffer waits, decode freezes, controller interventions, and thermal state can support hypotheses but do not prove a root cause. Rates cover the retained record window; initial cumulative values in a partial tail are treated as baselines, so its counter deltas are lower bounds. recovery_completed describes the recovery task returning (load_returned, load_failed, or offline); later clock/frame progress is the health evidence. first_clock_progress confirms clock movement. Stall notifications and AVPlayer's reset-aware stall counter are reported separately from clock-classified episodes, not added together. Repeated short hiccups can fall below the four-second clock threshold, so zero classified episodes does not mean playback was uninterrupted. first_video_output_frame is currently native-Twitch-only, may be up to one watchdog interval late, and records an observed pixel buffer rather than proof that a picture was rendered on screen. A seek callback arriving after the 15-second seek_deadline_exceeded event confirms only that the target callback landed, not that a picture rendered. The proxy's last failure status, error code, and monotonic uptime remain in samples after a later success so the failure is not mistaken for the latest request.

Donate

Strozz is free and open source, and it always will be. There's no paywall, no ads, and no obligation to give anything.

If the app has been useful to you and you'd like to chip in toward its upkeep — things like the Apple Developer Program fee and time spent maintaining it — donations are welcome and genuinely appreciated. Anything is plenty, and not donating is completely fine too.

Donate via GitHub Sponsors — one-time or recurring, whatever suits you.

Credits

Strozz is an unofficial, non-commercial Twitch client. It is not affiliated with, endorsed by, or sponsored by Twitch Interactive, Inc. or Amazon. Twitch is a trademark of its owner.

Third-party emote support is provided through the public 7TV, BetterTTV, and FrankerFaceZ services, and belongs to them.

License

MIT © 2026 thatcube


More open source

Hozz        Mozz        Plozz        Strozz

Brandon Moore

About

Free, open-source Apple TV streaming viewer for Twitch, YouTube, and supported Kick simulcasts, with multi-view and native chat emotes.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages