InputStream Player imports UTF-8 encoded M3U/M3U8 and JSON playlists (anything else is read as Windows-1252). A playlist can contain clear channels and channels with a ClearKey/raw key; keys are optional and must never be shared without the content owner’s permission.
Declare each channel with #EXTINF and its URL on the next line. The app reads
the tvg-logo and group-title attributes. tvg-id can remain in the
playlist as metadata for other clients.
#EXTM3U
#EXTINF:-1 tvg-id="demo-channel" tvg-logo="https://example.invalid/logo.png" group-title="Demo",Demo Channel
https://example.invalid/live/playlist.m3u8
Use Kodi’s inputstream.adaptive convention before the URL to declare the
manifest type or a ClearKey:
#EXTINF:-1 group-title="Demo",DASH channel with ClearKey
#KODIPROP:inputstream.adaptive.manifest_type=mpd
#KODIPROP:inputstream.adaptive.license_type=clearkey
#KODIPROP:inputstream.adaptive.license_key=00112233445566778899aabbccddeeff:ffeeddccbbaa99887766554433221100
https://example.invalid/channel/manifest.mpd
Supported manifest_type values:
| Value | Format |
|---|---|
hls |
HLS, including SAMPLE-AES with an authorized key |
mpd (or dash) |
DASH / MPD with an authorized ClearKey/raw key |
ism |
Smooth Streaming (.ism/.isml) with an authorized ClearKey/raw key |
Keys use KID:KEY, with both values encoded as 16-byte hexadecimal strings
(32 hexadecimal characters each). When each track is encrypted with a
different key, list several pairs separated by commas (KID1:KEY1,KID2:KEY2);
the ClearKey JSON object is accepted too.
#EXTVLCOPT:http-user-agent= can set a user agent, and
inputstream.adaptive.stream_headers can set HTTP headers, in the form
Name=value&Other=value:
#KODIPROP:inputstream.adaptive.stream_headers=X-Token=abc123&Referer=https://example.invalid/
Other ways to set headers, all accepted:
#EXTVLCOPT:http-referrer= and #EXTVLCOPT:http-origin= (Referer and Origin).#EXTHTTP:{"User-Agent":"…","Referer":"…"}: the headers as a JSON object.inputstream.adaptive.manifest_headers: same form as stream_headers. The
app sends them with every request, not only the manifest.|: https://…/playlist.m3u8|User-Agent=…&Referer=….#EXTGRP:Name on its own line works as group-title when the #EXTINF has none.
drm_legacy: the type and the license in one line,
#KODIPROP:inputstream.adaptive.drm_legacy=org.w3.clearkey|KID:KEY or
…=com.widevine.alpha|https://license.invalid/wv|Header=value.license_type=clearkey, a license_key
that is an address (https://license.invalid/clearkey|Header=value) is asked
for the keys when the channel opens. The request is the standard EME one (the
KIDs from the manifest) and the answer a JWK key set.license_key={"keys":[{"kty":"oct","k":"…","kid":"…"}]},
with kid and k in BASE64 (URL-safe or standard).license_type=com.widevine.alpha with the license server
address) only plays on Android with the ExoPlayer engine. On iPhone, iPad,
Apple TV, and Mac those channels show up dimmed and marked “Solo Android”.An #EXTINF with type="playlist" (a link to another playlist, as OTT
Navigator uses it) is skipped: it is not a channel.
A playlist can mix live channels with movies and episodes. On-demand entries show up under Emisiones → Bajo demanda (On demand), not in live TV. M3U has no field for this, so it is worked out from what each entry says, in this order:
tvg-type, type, media-type, or group-type set
to movie, series, vod, episode… (or live to force live)./movie/… and /series/… are on demand;
/live/… is live..mp4, .mkv, .avi, .mov… are video files.#EXTINF duration: channels use -1; a duration of two minutes or
more is taken as a movie.The group name does not count: there are live channels in groups called “Movies” or “Series”.
Episodes are grouped by series and season from their name: Show S01E02,
Show.s1.e2, Show - T01E02, or Show 1x02.
#EXTINF:120 tvg-type="movie" tvg-logo="https://example.invalid/poster.jpg" group-title="Movies",Demo Movie
https://example.invalid/vod/demo-movie.mp4
#EXTINF:-1 tvg-type="series" group-title="Series",Demo Show S01E01
https://example.invalid/vod/demo-show/s01e01.mkv
The flat format is an array of channels. name and url are required; the
rest are optional:
| Field | Purpose |
|---|---|
key |
ClearKey key, in the same forms as the M3U license_key |
group |
Channel group |
logo |
Logo |
tvg_id |
Channel identifier in the guide |
user_agent |
User agent used to request the video |
headers |
Other HTTP headers (a CDN token, Referer…): a JSON object or Name=value&Other=value text. A User-Agent in it wins over user_agent |
type |
movie, episode, series… for on-demand content; live for live |
serie_name, season, episode |
Which series, season, and episode it is |
catchup_url, catchup_key, catchup_days… |
See Catch-up |
[
{
"name": "Demo Channel",
"url": "https://example.invalid/live/playlist.m3u8",
"key": "00112233445566778899aabbccddeeff:ffeeddccbbaa99887766554433221100",
"tvg_id": "demo-channel",
"group": "Demo",
"logo": "https://example.invalid/demo-channel.png"
}
]
key uses the same KID:KEY syntax as M3U. tvg_id is accepted as extra
metadata for compatibility with existing playlists; the app safely ignores
fields it does not need.
The grouped schema is also supported: each group declares name, an optional
logo, and samples; each sample can declare name, uri, logo, kid,
key, and user_agent.
See the complete files in samples.
The OTT Navigator “OTC” format is also
read: an object with provider, categories, and channels.
{
"provider": { "epg": "https://example.invalid/guide.xml.gz", "archive_type": "shift", "archive_days": 3 },
"categories": { "news": { "name": "News", "name:es": "Noticias" } },
"channels": [
{
"category": "news",
"name": "Demo Channel",
"url": "https://example.invalid/live/demo/manifest.mpd",
"icon": "https://example.invalid/demo.png",
"epg_id": "demo-channel",
"user_agent": "Demo/1.0",
"referrer": "https://example.invalid/",
"drm_type": "clearkey",
"drm_key": "00112233445566778899aabbccddeeff:ffeeddccbbaa99887766554433221100"
}
]
}
| Field | Purpose |
|---|---|
provider.epg |
Guide for the whole list (offered like x-tvg-url) |
provider.archive_type, provider.archive_days |
Catch-up default for every channel |
provider.base64 |
Fields encoded in BASE64 ("url,name", or * for every text) |
categories |
Id → name; a channel’s category points here |
name, url |
Required |
icon |
Logo |
epg_id (or epg_name) |
Channel in the guide |
epg_shift |
Guide shift, like tvg-shift |
user_agent, referrer, headers |
HTTP headers (headers as Name=value&… or an object) |
drm_type |
clearkey or widevine (Widevine only on Android) |
drm_key |
ClearKey keys, as in M3U |
drm_url |
License server (ClearKey or Widevine) |
archive_type, archive_source, archive_days |
Catch-up, like catchup, catchup-source, catchup-days |
source-format |
hls, dash, or ss (Smooth) when the address does not say |
type |
movie, series… for on-demand content |
Per-language text ("name:es") works as in the catalogs. Fields that belong to
OTT Navigator’s own player (codec, preferred tracks, number…) are ignored.
For on-demand content with full details there is a third JSON schema: an
array of titles (or a single title). It is the same format as
OTT Navigator’s “Media library file”,
so its playlists work as they are. Everything in it goes to Bajo demanda
(On demand). A movie has video (or videos, a list of versions); a
series has seasons.
[
{
"name": "Demo Movie",
"category": "Movies",
"trailer": "https://example.invalid/trailers/demo-movie.mpd",
"video": "https://example.invalid/vod/demo-movie/manifest.mpd",
"drm": "clearkey",
"drmkey": "00112233445566778899aabbccddeeff:ffeeddccbbaa99887766554433221100",
"headers": "X-Token=abc123",
"info": {
"poster": "https://example.invalid/posters/demo-movie.jpg",
"bg": "https://example.invalid/backdrops/demo-movie.jpg",
"plot": "Movie plot.",
"rating": "7.4",
"year": "2025",
"genre": ["Drama", "Adventure"],
"cast": ["Actress One", "Actor Two"],
"director": ["Demo Director"],
"country": ["Spain"],
"duration": 6540
}
},
{
"name": "Demo Show",
"category": "Series",
"info": { "plot": "Plot of the whole series.", "year": "2026", "genre": "Comedy" },
"seasons": [
{
"season": 1,
"episodes": [
{
"episode": 1,
"name": "The beginning",
"video": "https://example.invalid/vod/demo-show/s01e01.m3u8",
"info": { "plot": "Episode plot.", "duration": 47 }
}
]
}
]
}
]
| Field | Purpose |
|---|---|
name |
Title |
category |
Group it appears in (Movies, Series…) |
video |
Video address (HLS, DASH, Smooth, or a file) |
videos |
The movie as a list of versions, each with its own video, drm, drmkey, headers, and info. The first one with a video is played |
seasons |
Seasons: season (number) and episodes, each with episode, name, video, drm, drmkey, headers, and info. Without numbers, they are numbered by position (1, 2, 3…) |
drm |
clearkey if encrypted; without it, it plays in the clear |
drmkey |
Key as KID:KEY (or several, separated by commas) |
headers |
HTTP headers, as Name=value&Other=value or as an object ({"User-Agent": "…"}) |
trailer |
Trailer: a video address, or a YouTube one (or just the video id), which opens in YouTube. Adds a “Tráiler” button to the detail page. Also accepted inside info |
info |
The details (see below) |
info fields, all optional:
| Field | Content |
|---|---|
poster, bg (or backdrop) |
Poster and backdrop |
plot |
Plot. In a series, the series plot; each episode can carry its own |
rating, year |
Rating and year; numbers or text both work |
genre, cast, director, country |
A list of names or comma-separated text |
duration |
In minutes (47) or seconds (3420): above 400 it is taken as seconds |
The parser is lenient: unused fields are ignored, an empty or zero value
("rating": "0", "plot": "") counts as unknown, and a title, season, or
episode that cannot be read is skipped without breaking the rest. An episode
inherits from the series whatever it lacks (year, genres, cast…), and without a
name it is called “Episodio N”.
As in OTT Navigator, these are accepted too:
"name:es": "…" replaces name when the device is
in that language (works for any field).{ "base64": "name,video" } in the list says
which fields are encoded (no padding); * marks every text value.drm only accepts clearkey: Widevine and others needing a license server
are not supported.
Complete example in samples/catalogo-vod.json.
The app matches each channel against an XMLTV guide. Matching uses tvg-id
first and falls back to the normalized channel name, so declare tvg-id with
the same identifier the guide uses.
A playlist can declare its guide in the #EXTM3U header with x-tvg-url
(url-tvg and tvg-url are also accepted). Separate several guides with
commas. The app offers to add it; it never downloads one without permission,
because guides are usually tens of megabytes.
#EXTM3U x-tvg-url="https://example.invalid/guide.xml.gz"
Plain and gzipped guides are both supported.
tvg-shift moves a channel’s guide, for “+1” channels that reuse the original’s
guide: in hours from -12 to 12 (tvg-shift="1", "1.5"), in seconds outside
that range. On the #EXTM3U header it applies to the whole playlist.
Catch-up plays a programme that already aired. The app needs two things: the guide, which says when each programme started and ended, and the archive declaration in the playlist, which says where to request it.
The attributes go on the #EXTINF line, and also work on the #EXTM3U header,
where they act as a default for the whole playlist: a channel only declares
what differs.
| Attribute | Purpose |
|---|---|
catchup (or catchup-type) |
Mode: default, append, shift, flussonic or xc. Defaults to default |
catchup-source |
The archive address (default) or the query fragment appended to the channel URL (append) |
catchup-days |
Days the archive keeps. Assumed to be 8 when absent |
catchup-correction |
Correction in hours, for archives not aligned with the guide |
timeshift, tvg-rec, catchup-time |
Legacy ways of declaring the archive length |
Modes:
default: catchup-source is the whole address.append: catchup-source is glued onto the channel URL, fixing the
separator (? or &) as needed.shift: no catchup-source needed; ?utc={utc}&lutc={lutc} is appended
to the channel URL.flussonic (also flussonic-hls, flussonic-dash, flussonic-ts, fs):
no catchup-source needed; the archive address is derived from the channel’s,
…/channel/index.m3u8 → …/channel/index-{utc}-{duration}.m3u8, and
…/channel/mpegts → …/channel/timeshift_abs-{utc}.ts.xc (Xtream Codes): no catchup-source needed;
…/live/user/pass/1234.ts → …/timeshift/user/pass/{duration:60}/{Y}-{m}-{d}:{H}-{M}/1234.ts.
The date goes in UTC; if your server wants its local time, use
catchup-correction.catchup-source placeholdersThey expand to the moment of the chosen programme. Times are computed in UTC.
| Placeholder | Value |
|---|---|
{utc} · ${start} |
Programme start, in seconds since the epoch |
{utcend} · ${end} |
Programme end |
{lutc} · ${now} · ${timestamp} |
Current moment |
{duration} · {duration:X} |
Duration in seconds, or divided by X |
{offset} · {offset:X} |
Seconds elapsed since the start, or divided by X |
{Y} {m} {d} {H} {M} {S} |
Parts of the start date, zero padded |
{utc:Ymd-H-M} · ${end:YmdHM} |
Composed date: the letters YmdHMS are substituted and anything else passes through as a separator |
{start_iso} · {end_iso} · {now_iso} |
ISO-8601 date in UTC: 2026-09-21T09:30:00Z |
The last three are not standard: no player defines them, and the usual way is
to compose the date by hand with {Y}-{m}-{d}T{H}:{M}:{S}Z, which is also
accepted. They exist because some archives want the time in that shape, and
writing it by hand is a typo waiting to happen.
Anything unrecognized is left untouched, so an address that contains braces for other reasons is not mangled.
If catchup-source carries no time placeholder at all, start_time and
end_time are appended in ISO form. That is what archives which only publish
their base address need: without the window they do not know what to serve.
Archives are usually encrypted with a different key from the live channel.
Declare it with its own #KODIPROP or with an attribute, whichever is handier:
#KODIPROP:inputstream.adaptive.catchup_license_key=KID:KEY
#KODIPROP:inputstream.adaptive.catchup_manifest_type=ism
or, on the #EXTINF line, catchup-key="KID:KEY" and
catchup-manifest-type="ism". It accepts the same forms as
inputstream.adaptive.license_key: kid:key, several comma-separated pairs,
and the JSON object.
Without its own key, the live one is used. A malformed catch-up key leaves the channel without an archive, but not without its live stream.
catchup-manifest-type is only needed when the archive uses a different
protocol from the live stream; otherwise it is inferred from the address.
#EXTM3U x-tvg-url="https://example.invalid/guide.xml.gz"
#EXTINF:-1 tvg-id="demo-channel" group-title="Demo" catchup="default" catchup-days="8" catchup-source="https://example.invalid/archive/demo-channel/Manifest?device_profile=mss&start_time={start_iso}&end_time={end_iso}",Demo Channel
#KODIPROP:inputstream.adaptive.manifest_type=ism
#KODIPROP:inputstream.adaptive.license_type=clearkey
#KODIPROP:inputstream.adaptive.license_key=00112233445566778899aabbccddeeff:ffeeddccbbaa99887766554433221100
#KODIPROP:inputstream.adaptive.catchup_license_key=aabbccddeeff00112233445566778899:99887766554433221100ffeeddccbbaa
https://example.invalid/live/demo-channel/Manifest
In append mode the same thing reads:
#EXTINF:-1 catchup="append" catchup-days="7" catchup-source="?utc={utc}&utcend={utcend}",Demo Channel
https://example.invalid/live/demo-channel/manifest.mpd
In the flat schema the catch-up fields are optional:
[
{
"name": "Demo Channel",
"url": "https://example.invalid/live/demo-channel/Manifest",
"key": "00112233445566778899aabbccddeeff:ffeeddccbbaa99887766554433221100",
"tvg_id": "demo-channel",
"group": "Demo",
"catchup_url": "https://example.invalid/archive/demo-channel/Manifest?device_profile=mss",
"catchup_key": "aabbccddeeff00112233445566778899:99887766554433221100ffeeddccbbaa",
"catchup_days": 8
}
]
| Field | Equivalent to |
|---|---|
catchup_url |
catchup-source in default mode |
catchup_key |
catchup_license_key |
catchup_days |
catchup-days |
catchup_type |
catchup |
catchup_source |
catchup-source, if you prefer the M3U name |
In the example catchup_url carries no placeholders, so the app appends
start_time and end_time in ISO form. A channel without catchup_url (or
with an empty one) simply offers no archive.
The server’s archive and the guide are different things, and both have a say: you can only watch a programme the archive keeps and the guide lists.
Most XMLTV guides look forward and carry little or no past. The app keeps the
already-aired programmes from each refresh, so the past fills in day by day up
to whatever catchup-days declares. Freshly installed you will only see what
your guide brings; the rest arrives with the days.
The M3U sample includes public Unified Streaming streams and an Axinom ClearKey test vector. They are provided to test app compatibility, not as a guarantee of availability or a source of content. Only add keys and URLs that you may legally use and redistribute.