Skip to content

Commit a6276a9

Browse files
committed
initial commit
0 parents  commit a6276a9

15 files changed

Lines changed: 5142 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
paths-ignore:
6+
- "**/*.md"
7+
- '**/*.txt'
8+
pull_request:
9+
paths-ignore:
10+
- "**/*.md"
11+
- '**/*.txt'
12+
workflow_dispatch:
13+
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
19+
- name: Checkout SDK
20+
uses: actions/checkout@v3
21+
with:
22+
repository: ps5-payload-dev/sdk
23+
path: sdk
24+
25+
- name: Setup SDK
26+
working-directory: sdk
27+
run: |
28+
sudo apt update
29+
sudo apt install clang-18 lld-18
30+
make DESTDIR=${{ runner.tool_cache }}/ps5-payload-sdk clean install
31+
echo PS5_PAYLOAD_SDK=${{ runner.tool_cache }}/ps5-payload-sdk >> $GITHUB_ENV
32+
33+
- name: Checkout Payload
34+
uses: actions/checkout@v3
35+
36+
- name: Build Payload
37+
run: |
38+
make -f Makefile clean all
39+
40+
- name: Upload Payload
41+
uses: actions/upload-artifact@v4
42+
with:
43+
name: Payload
44+
path: tv4play-install.elf
45+
if-no-files-found: error
46+
47+
48+
release:
49+
needs: build
50+
permissions:
51+
contents: write
52+
53+
if: startsWith(github.ref, 'refs/tags/')
54+
runs-on: ubuntu-latest
55+
steps:
56+
- name: Download artifacts
57+
uses: actions/download-artifact@v4
58+
with:
59+
name: Payload
60+
path: .
61+
62+
- name: Create GitHub Release (pre-release)
63+
uses: softprops/action-gh-release@v2
64+
with:
65+
tag_name: ${{ github.ref_name }}
66+
prerelease: true
67+
files: tv4play-install.elf

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*~
2+
*.elf
3+
webapp/token.js

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (C) 2026 John Törnblom
2+
#
3+
# This file is free software; you can redistribute it and/or modify it
4+
# under the terms of the GNU General Public License as published by
5+
# the Free Software Foundation; either version 3 of the License, or
6+
# (at your option) any later version.
7+
#
8+
# This program is distributed in the hope that it will be useful, but
9+
# WITHOUT ANY WARRANTY; without even the implied warranty of
10+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11+
# General Public License for more details.
12+
#
13+
# You should have received a copy of the GNU General Public License
14+
# along with this program; see the file COPYING. If not see
15+
# <http://www.gnu.org/licenses/>.
16+
17+
PS5_HOST ?= ps5
18+
PS5_PORT ?= 9021
19+
20+
ifdef PS5_PAYLOAD_SDK
21+
include $(PS5_PAYLOAD_SDK)/toolchain/prospero.mk
22+
else
23+
$(error PS5_PAYLOAD_SDK is undefined)
24+
endif
25+
26+
ELF := tv4play-install.elf
27+
28+
CFLAGS := -Wall -Werror -g -DTITLE_ID="\"BREW10004\""
29+
LDADD := -lSceIpmi -lSceAppInstUtil
30+
31+
all: $(ELF)
32+
33+
$(ELF): main.c
34+
$(CC) $(CFLAGS) $(LDADD) -o $@ $^
35+
36+
install.c: sce_sys/param.json sce_sys/icon0.png sce_sys/pic1.png
37+
38+
clean:
39+
rm -f $(ELF)
40+
41+
install: $(ELF)
42+
$(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^
43+
44+
test: install

README.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)