Chorus2 for Samsung Tizen TV

May 21, 2026 · View on GitHub

Build Chorus2 Tizen Latest release License: GPL-2.0

Wraps the Chorus2 Kodi web interface in a Samsung Tizen .wgt and swaps the HTML5 <video> element for Tizen AVPlay, so video playback gets hardware decoding (HEVC, AC3, etc.) on the TV's native pipeline instead of being limited by the WebView.

The app talks JSON-RPC to a Kodi instance on your LAN and gives you a native-feeling TV client for browsing and controlling your library.

This is not a port of Kodi. It's a remote/streaming client that packages Chorus2 as a TV app. See What works below for the capability ceiling.


Screenshots

chorus2-tizen-login chorus2-tizen-default chorus2-tizen-browse chorus2-tizen-avplay

Install

  1. Download the latest .wgt from the Releases page. Each push to the tizen branch produces a signed prerelease build.

  2. Enable Developer Mode on your Samsung TV (Samsung's instructions) and put your dev-machine's IP into the TV's "Host PC IP" field.

  3. Sideload the .wgt. Either:

    • via Tizen Studio's Device Manager → Permit to install → Drag-drop the file, or
    • from the command line:
      sdb connect <tv-ip>
      sdb install Chorus2-Tizen.wgt
      
  4. Launch the app on your TV. On first run you'll see a setup screen.

  5. Enter your Kodi server details — host/IP, port (default 8080), username, password — and press Connect. The app pings Kodi with the entered credentials and only proceeds if it gets a valid pong response, so bad config is caught up-front rather than stalling the UI.


What works

  • Browse your Kodi library — movies, TV shows, music, artists, albums
  • Play direct files from the library (music + video) — video goes through Tizen AVPlay for hardware-accelerated decoding
  • Remote-control a running Kodi instance (Chorus2's "Kodi" mode)
  • Settings, search, now-playing
  • TV remote:
    • Arrow keys + OK navigate (manual spatial navigation — Tizen 5 doesn't ship one)
    • Play / Pause / Track-prev / Track-next map to Chorus2's player controls
    • Back exits the current view; from root, exits the app
  • Authentication via HTTP Basic — credentials are stored in localStorage and injected into XHR/fetch/WebSocket and the AVPlay streaming URL automatically
  • Cross-origin images — a Service Worker intercepts <img> and CSS url() loads to Kodi and adds the auth header, since those don't go through the XHR/fetch patches

What doesn't work

These are structural limits of Kodi's architecture or the JSON-RPC API — they aren't fixable from a remote client. Use a different solution if you need any of them:

  • Kodi addons (plugin:// URIs) — resolved server-side inside Kodi's Python interpreter; no playable stream URL is ever exposed via JSON-RPC
  • InputStream Adaptive (DASH/HLS+DRM addons) — same reason
  • PVR / Live TV backends
  • DRM-protected content
  • DLNA / casting to other devices

If you need addons or PVR, look at running Jellyfin + the jellyfin-tizen client instead.


Requirements

  • Samsung TV with Tizen 4.0 or later (roughly 2018 and newer)
  • A Kodi v17+ server reachable on the same network as the TV
  • Kodi's web interface enabled, with:
    • Allow control of Kodi via HTTP → on
    • Allow remote control from applications on other systems → on
    • HTTP Basic credentials configured (username + password)

Build from source

The whole build runs through tizen/build.sh. We do not rebuild Chorus2 itself from CoffeeScript source — we patch the prebuilt dist/ that upstream ships, then run tizen build-web + tizen package.

git clone https://github.com/PatrickSt1991/chorus2-tizen.git
cd chorus2-tizen

# Smoke-test the prepare pipeline without installing Tizen Studio:
bash tizen/build.sh --dry-run

# Full build (needs Tizen Studio CLI on PATH or in $TIZEN_BIN):
TIZEN_BIN=~/tizen-studio/tools/ide/bin/tizen \
TIZEN_PROFILE=Chorus2 \
  bash tizen/build.sh
# → release/Chorus2-Tizen.wgt

build.sh flags:

FlagWhat it does
(no flag)Full pipeline: prepare + tizen build-web + tizen package
--dry-runJust the prepare steps (copy + sed-inject + icon resize). No Tizen CLI needed.
--no-packagePrepare + tizen build-web, but skip the interactive tizen package. CI use.

CI

.github/workflows/build-tizen.yml installs Tizen Studio 5.5 on ubuntu-latest, creates a self-signed Chorus2 cert + security profile, runs build.sh --no-package, drives tizen package through an expect-script (the CLI prompts for cert passwords interactively), then uploads the signed .wgt as both a workflow artifact and a GitHub release. Every push to tizen produces a fresh build.


Debug

If something misbehaves on the TV, the app can stream every console.log, error, click and XHR response to a dev-machine terminal over WebSocket. The catch: there's no DevTools window on the TV, so this is how we look behind the curtain.

1. Run the listener on a machine the TV can reach. The script is at tools/debug-server.py in this repo (and attached to every Release). Stdlib-only — no pip install. From any shell:

python3 tools/debug-server.py            # default port 9999
python3 tools/debug-server.py 9099       # custom port
python3 tools/debug-server.py 9999 -q    # no colour, plain text

On Windows + WSL, prefer running it from PowerShell rather than WSL — PowerShell binds to the host IP the TV can see directly; WSL2 has its own internal network and needs port-forwarding. If Windows Firewall prompts you, allow the connection on Private networks.

2. Point the TV at it. On the app's first-launch setup screen (or after a Reset), fill in the Debug host field as <your-pc-ip>:9999 — e.g. 192.168.1.20:9999. Press Connect.

Leave the field blank to disable. The app makes one WebSocket connection on launch; if the listener isn't running there's no harm done (the app retries silently and otherwise behaves normally).

3. Watch the terminal. You'll see colour-coded events:

14:35:46  click           DIV.mdi play   "Play"   @(852,144)
14:35:46  net.xhr         POST .../Playlist.Insert  -> 200
14:35:47  localplay.intercept  {"kind":"playlist", "pid":1, "pos":1}
14:35:47  localplay.resolved   {"file":"…jellyfin2samsung.mp4"}
14:35:47  localplay.navigate   videoPlayer.html?src=…
14:35:48  [avplay] prepareAsync success READY
14:35:48  [avplay] play() called, state PLAYING

If you hit a bug, the exact event sequence makes it possible to reproduce locally and fix without a lot of guess-and-check.


How it works

The strategy is "patch upstream's prebuilt dist/" rather than rebuilding Chorus2 from source:

chorus2-tizen/
├── dist/                            # upstream Chorus2 — DO NOT EDIT
├── src/                             # upstream Chorus2 source — DO NOT EDIT
├── tizen/
│   ├── wrapper/
│   │   ├── config.xml               # Tizen app manifest
│   │   ├── icon.png                 # app icon (resized to 117×117 at build time)
│   │   └── videoPlayer.html         # AVPlay-driven replacement for dist's
│   ├── extras/
│   │   ├── tizen-bootstrap.js       # patches: config + URL/auth/WebSocket
│   │   ├── tizen-sw.js              # image-auth Service Worker
│   │   ├── tizen.css                # AVPlay surface + TV focus styles
│   │   └── avplayVideoPlayer.js     # AVPlay reference (from jellyfin-tizen)
│   └── build.sh                     # prepare + tizen build-web + tizen package
└── .github/workflows/
    └── build-tizen.yml              # CI: produces a signed .wgt per push

At build time, build.sh copies dist/* into a build directory, layers our wrapper and extras on top (overwriting config.xml, videoPlayer.html, and the index entry point), sed-injects our bootstrap into <head>, strips Chorus2's own <script> tag, then runs the Tizen CLI to web-build and package.

tizen-bootstrap.js is the integration point. It:

  • Shows a first-launch setup screen and pings Kodi with the entered creds before saving anything
  • Patches XMLHttpRequest, fetch, and WebSocket so Chorus2's relative URLs land on the configured Kodi host with HTTP Basic auth
  • Registers a Service Worker (tizen-sw.js) that catches <img> and CSS url() loads (which bypass XHR/fetch) and adds the auth header
  • Registers Tizen media keys + implements spatial navigation for the arrow keys (Tizen 5 doesn't provide one)
  • Dynamically loads js/kodi-webinterface.js only after config is verified

videoPlayer.html (our replacement for the upstream video.js one) drives webapis.avplay directly: opensetListenerSET_MODE_4KprepareAsyncsetDisplayMethod(LETTER_BOX)play. Basic Auth is embedded into the AVPlay URL as http://user:pass@host:port/... because AVPlay's open() only takes a URL.


Branches

  • tizen — default branch. Everything in this README lives here.
  • master — upstream Chorus2 tracking branch, kept clean so we can pull updates from xbmc/chorus2 cleanly.

Acknowledgments

  • Chorus2 by Jeremy Graham and the Kodi contributors — the web interface this app wraps. GPL-2.0.
  • jellyfin-tizen-avplay — the AVPlay shim was originally written there. GPL-2.0.
  • The Tizen Web Application docs and AVPlay API reference at docs.tizen.org.

License

GPL-2.0, inherited from Chorus2. See LICENSE.