|
| 1 | +# TV4 Play |
| 2 | + |
| 3 | +This is an unofficial [10-foot UI][10foot] for [TV4 Play][tv4play], a Swedish |
| 4 | +commercial broadcaster. The UI is designed specifically for jailbroken PS5 and |
| 5 | +is built along the same lines as [the SVT Play UI][svtplay]. |
| 6 | + |
| 7 | +The majority of the UI has been produced by [claude.ai][claude]. |
| 8 | + |
| 9 | +> [!NOTE] |
| 10 | +> Signing in is optional and manual — see [Signing in](#signing-in). Without an |
| 11 | +> account only content TV4 serves anonymously is listed and plays; cards that |
| 12 | +> need a subscription carry a *Premium* badge. |
| 13 | +
|
| 14 | +> [!NOTE] |
| 15 | +> Streams provided by [TV4 Play][tv4play] are generally geo-blocked for devices |
| 16 | +> without a Swedish IP address. |
| 17 | +
|
| 18 | +## Controls |
| 19 | + |
| 20 | +| Button | Browsing | Playback | |
| 21 | +|--------------|---------------------------------|---------------------------| |
| 22 | +| D-pad | Move; Left at the edge exits to the menu | Seek ±10 s / ±5 min | |
| 23 | +| Cross | Open | Play / pause | |
| 24 | +| Circle | Back | Stop | |
| 25 | +| Square | — | Cycle subtitles | |
| 26 | +| Triangle | Reload the current view | Cycle audio tracks | |
| 27 | + |
| 28 | +Moving down the menu opens each view as the cursor settles on it; Right or |
| 29 | +Cross hands the cursor over to the listing. |
| 30 | + |
| 31 | +## Installing |
| 32 | + |
| 33 | +Assuming you have [elfldr.elf][elfldr] running on your PS5, a launcher can be |
| 34 | +installed by running the install payload on your PS5. A TV4 Play icon will |
| 35 | +then appear on the Media tab on your PS5 dashboard. |
| 36 | + |
| 37 | +The launcher points at whatever `webAppUri` in `sce_sys/param.json` says, so |
| 38 | +that has to be changed to wherever you end up hosting `docs/`. The title id in |
| 39 | +`param.json` and in the `Makefile` must also be unique among the apps installed |
| 40 | +on the console — `BREW10003` is a placeholder, and it collides with nothing in |
| 41 | +this repository but nothing here knows what else you have installed either. |
| 42 | + |
| 43 | +> [!NOTE] |
| 44 | +> Once the installation is completed, the application can be launched without |
| 45 | +> having to jailbreak the console after a reboot. |
| 46 | +
|
| 47 | +`sce_sys/icon0.png` and `sce_sys/pic1.png` are plain generated placeholders so |
| 48 | +that `make` works out of the box. Replace them with artwork of your own. |
| 49 | + |
| 50 | +## How it works |
| 51 | + |
| 52 | +`docs/js/tv4.js` talks to TV4's client gateway, an ordinary GraphQL endpoint |
| 53 | +that accepts the whole query document in a POST body. That is a good deal less |
| 54 | +brittle than SVT's persisted queries, which stop answering the moment a hash is |
| 55 | +retired: nothing here breaks when the website ships a new build, as long as the |
| 56 | +schema holds. |
| 57 | + |
| 58 | +Playback goes through a separate service that trades a media id for a manifest |
| 59 | +URL. Only HLS is asked for — WebKit plays it natively on the console, and |
| 60 | +hls.js covers the case where the audio sits in renditions of its own, which |
| 61 | +WebKit loads no sound for. |
| 62 | + |
| 63 | +The query documents, the panel ids and the playback URL were all taken from the |
| 64 | +[Retrospect][retrospect] Kodi addon (`channels/channel.se/tv4se/`), which is |
| 65 | +GPLv3 like this repository. |
| 66 | + |
| 67 | +### Things that will need touching up |
| 68 | + |
| 69 | +Two kinds of value in `tv4.js` are TV4's rather than the schema's, and they are |
| 70 | +the ones that rot: |
| 71 | + |
| 72 | +- `START_ROWS` — the opaque ids of the three curated rows on the start page. |
| 73 | +- `LIVE_PAGE_IDS` — candidates for the page behind tv4play.se/live, tried in |
| 74 | + order until one answers. The winner is remembered in `localStorage`, and |
| 75 | + `check.html` reports which one it was; move a confirmed id to the front. |
| 76 | + |
| 77 | +When a row turns up empty, open tv4play.se with the browser's network tab |
| 78 | +recording, find the `Panel` or `Page` request behind the row you want, and copy |
| 79 | +the id out of the request body. |
| 80 | + |
| 81 | +`docs/check.html` runs each of the calls the UI depends on and prints what came |
| 82 | +back. Open it on the console when something stops working: it tells a browser |
| 83 | +refusing the request apart from TV4 having moved an id. |
| 84 | + |
| 85 | +### If the browser refuses the request |
| 86 | + |
| 87 | +Every call carries `Client-Name` and `Client-Version` headers, which makes it a |
| 88 | +non-simple cross-origin request, so each one is preceded by a CORS preflight. |
| 89 | +TV4's own site is served from a different origin than the gateway, so the |
| 90 | +gateway does answer preflights — but whether it answers them for *any* origin |
| 91 | +or only for TV4's own is not something this repository can promise. If |
| 92 | +`check.html` reports failures that the console log shows as CORS errors, set |
| 93 | +`PROXY` at the top of `tv4.js` to a proxy of your own and everything else keeps |
| 94 | +working unchanged. |
| 95 | + |
| 96 | +## Signing in |
| 97 | + |
| 98 | +TV4 has no sign-in this UI can drive: the website's own form sits behind bot |
| 99 | +protection, which is why the Kodi addon abandoned username and password too. |
| 100 | +What is left is the credential your ordinary browser already holds once you |
| 101 | +have signed in on tv4play.se — the **refresh token cookie** — pasted in once |
| 102 | +under *Konto*. |
| 103 | + |
| 104 | +That token is long lived and grants full access to the account. It is kept in |
| 105 | +the console's `localStorage` and is sent nowhere except to TV4's own auth |
| 106 | +service, which trades it for a short lived access token; only the latter goes |
| 107 | +out with ordinary calls. Anyone else who uses the console can watch on your |
| 108 | +account, so *Logga ut* is there for a reason. |
| 109 | + |
| 110 | +For local testing, pasting the token every time is tedious. Copy |
| 111 | +`docs/js/token.js.example` to `docs/js/token.js`, put your token in it, |
| 112 | +and it is used whenever nothing has been pasted in under *Konto*. That filename |
| 113 | +is gitignored, which is the point of it: `docs/` is what gets published, so a |
| 114 | +token in a tracked file there is one careless commit away from being public. |
| 115 | +The account screen says when a development token is the one in use, and *Logga |
| 116 | +ut* is hidden then, since there is nothing stored for it to remove — empty the |
| 117 | +file instead. |
| 118 | + |
| 119 | +### When sign-in does not take |
| 120 | + |
| 121 | +*Konto* has a **Testa inloggningen** action, and `check.html` performs the same |
| 122 | +exchange as its own line. Use one of them first: storing a token and being able |
| 123 | +to trade it for an access token are different things, and a failed exchange |
| 124 | +otherwise surfaces much later as a 401 from the playback service. |
| 125 | + |
| 126 | +Things that have gone wrong with a copied cookie value: |
| 127 | + |
| 128 | +- **Truncation.** Devtools tables abbreviate long values. Use the context menu's |
| 129 | + *Copy Value* rather than selecting the text shown in the cell. |
| 130 | +- **Percent-encoding or quotes** around the value. Both are undone |
| 131 | + automatically now, since a JWT contains neither. |
| 132 | +- **The wrong cookie.** The value must have three dot-separated parts. If the |
| 133 | + account screen cannot read an expiry date from it, it is not the token. |
| 134 | + |
| 135 | +Signing in changes two things: |
| 136 | + |
| 137 | +- **Listings get complete.** The gateway trims `allSeasonLinks` for anonymous |
| 138 | + callers, and omits page panels an account cannot reach — so season lists and |
| 139 | + category pages are thinner than the website's until you sign in. This part |
| 140 | + works regardless of anything below. |
| 141 | +- **Protected titles become *possible*.** See the next section. |
| 142 | + |
| 143 | +## DRM |
| 144 | + |
| 145 | +The console reports **PlayReady** through EME, and neither Widevine nor |
| 146 | +FairPlay. That is the reverse of what a desktop browser offers, and it is why |
| 147 | +there are two playback paths here: |
| 148 | + |
| 149 | +1. Plain HLS is asked for first. Anything unencrypted comes back that way and |
| 150 | + is played by WebKit natively, which is the path the console decodes in |
| 151 | + hardware. |
| 152 | +2. If the title turns out to be protected, and only then, the manifest is |
| 153 | + requested again as `protocol=dash&drm=playready` and handed to |
| 154 | + [Shaka Player][shaka]. The console's own PlayReady module does the |
| 155 | + decryption; the licence comes from TV4's castlabs service in the ordinary |
| 156 | + way, with the challenge as the request body and the token from the playback |
| 157 | + response in an `x-dt-auth-token` header. That is the same exchange the |
| 158 | + official player performs — nothing is worked around. |
| 159 | + |
| 160 | +Whether TV4 will actually issue a PlayReady licence to a browser client is the |
| 161 | +one thing here that could not be established without trying it: their web |
| 162 | +player asks for Widevine because it expects Chrome. If the service refuses, |
| 163 | +protected titles fail with a Shaka 6xxx error and the free catalogue is |
| 164 | +unaffected. |
| 165 | + |
| 166 | +## Not implemented |
| 167 | + |
| 168 | +- **Search.** The gateway has a `PanelSearch` operation, but typing a query on a |
| 169 | + DualSense is miserable even with the system keyboard. |
| 170 | +- **Ads.** TV4 stitches advertising into its manifests server-side. It plays as |
| 171 | + part of the stream; nothing here separates it out. |
| 172 | + |
| 173 | +## Reporting Issues |
| 174 | + |
| 175 | +If you encounter problems with this unofficial TV4 Play UI, please file a |
| 176 | +github issue. If you plan on sending pull requests which affect more than a few |
| 177 | +lines of code, please file an issue before you start to work on your changes. |
| 178 | +This will allow us to discuss the solution properly before you commit time and |
| 179 | +effort. |
| 180 | + |
| 181 | +## License |
| 182 | +[GPLv3+][gplv3] |
| 183 | + |
| 184 | +[tv4play]: https://www.tv4play.se |
| 185 | +[svtplay]: https://github.com/ps5-payload-dev/svtplay |
| 186 | +[claude]: https://claude.ai |
| 187 | +[elfldr]: https://github.com/ps5-payload-dev/elfldr |
| 188 | +[retrospect]: https://github.com/retrospect-addon/plugin.video.retrospect |
| 189 | +[gplv3]: https://www.gnu.org/licenses/gpl-3.0.html |
| 190 | +[shaka]: https://github.com/shaka-project/shaka-player |
| 191 | +[10foot]: https://en.wikipedia.org/wiki/10-foot_user_interface |
0 commit comments