tvg-chno: channel numbers in IPTV playlists, and what every player does with them
No standard defines this attribute. One measured playlist carries 1,355 of them over 263 distinct values, and no two of the applications that read it agree.
tvg-chno="1" 107 entries
tvg-chno="2" 92
tvg-chno="3" 80
tvg-chno="4" 72
tvg-chno="5" 68
tvg-chno="#" 4
tvg-chno="22.1" 1
That is the head of one real playlist's channel-number histogram, with two of its stranger values pinned underneath. The file holds 1,895 entries. 1,355 of them carry the attribute, and those 1,355 numbers cover 263 distinct values, a mean of 5.15 entries per value. The single most popular number in the file is claimed by 107 unrelated channels.
No standard defines what any of that means. There is no specification for tvg-chno, no registry, and no owner. The reference implementation documents it in one line of a README, and the applications that read it disagree about the spelling, the type, the fallback and whether to show the value at all.
Where the attribute came from, as far as anyone can show
RFC 8216 is an Informational submission rather than a standards-track document. It defines EXTM3U in section 4.3.1.1, EXTINF in section 4.3.2.1, and the #EXT-X- family, and the string tvg does not appear in it anywhere. Its attribute-list grammar is comma-separated with a fixed name form, which the space-separated tvg- convention does not follow, and the same section tells clients to ignore names they do not recognize. That instruction is what lets the convention live beside the RFC: everything below is a private agreement between one program and the playlists it happened to be shown.
The earliest appearance of tvg-chno I can point at is pull request #92 against Kodi's IPTV Simple add-on, opened 2016-11-24 and merged 2016-12-17. I cannot prove it is the first, only that nothing earlier is public. It adds one marker constant, and its author's stated motivation was syncing channel numbers with a backend. The negative evidence around it is stronger than the commit: an issue on the same tracker from 2015-07-20 says plainly that the M3U format has no tag for a channel number and proposes packing the number into the channel name instead. The author of #92 found that issue five minutes after opening his pull request, and the maintainer later closed it as fixed by his patch. As of mid-2015 the add-on's own community did not know of any such attribute.
The tvg- prefix is almost four years older than the number. tvg-name, tvg-logo and group-title sit in the add-on's initial project commit of 2013-01-17, tvg-id lands the same day, and tvg-shift four days after that. What tvg abbreviates is folklore: everyone reads it as "TV guide" and the sibling attributes that carry the guide address support that reading, but no first-party source states the expansion, and the accounts tracing the attribute set to an earlier Windows player ecosystem are second-hand. The negative is firmer. XMLTV is where these attributes point, not where they came from.
Adoption after Kodi runs Tvheadend emitting it in March 2018, Emby documenting it during 2018, Jellyfin in February 2019. The most irritating detail in the harvest is that the Tvheadend commit message says it adds a tvg-chnum attribute while the code it lands writes tvg-chno=, and on the way back in it reads tvh-chnum first, falling back to tvg-chno. At least five spellings carry a channel number in real files: tvg-chno, channel-number, ch-number, tvh-chnum, and a channel-id some readers treat as a number.
1,176 of the 1,355 numbers are shared with another row
We measured three sample playlists we hold locally, 2,209 entries in total. Two of the three number nothing at all: zero of their 314 entries carry the attribute. Everything here is the third file.
The values are lopsided. 179 of the 263 distinct values are used exactly once, so most of the mass sits in a short head: 107 entries on number 1, then 92, 80, 72 and 68. The range runs from 1 to 4,999, so the parser's ceiling of 99,999 has never fired. 1,176 of the 1,355 rows, 86.8 percent, share their number with at least one other row, and a unique key laid over the file would drop 1,092 of them. Counting only the values our parser accepts, the sharing figure is 1,172 of 1,299, or 90.2 percent.
Why nothing in one of these files is a stable key is settled in the extended M3U article, and I will not re-argue it here. What that article does not do is kill the obvious repair: if numbers duplicate across a file, scope them to the group they sit in and each group gets a clean sequence.
It does not work.
33 of the 85 groups that carry any number at all contain at least one duplicated number inside the group. 69 of those 85 contain a channel numbered 1. One group holds 7 entries all numbered 1, and inside the largest group of the file, 79 numbered entries cover 74 distinct numbers. Grouping reduces the collision count without removing it, and a fix that works for two thirds of your groups fails silently on the other third.
The absence rate matters as much as the duplication. 596 of the 1,895 entries, 31.5 percent, end with no usable number: 540 never carried the attribute, and 56 carried something the parser refuses. Cross-referencing with the id attribute, 398 rows carry neither a number nor an id, the population nothing in the file can address. A guide row with nothing to attach to surfaces as a schedule wrong on some channels and right on others, never as a numbering complaint.
Every decimal in the file is Argentine
25 entries use major.minor numbering, over 24 distinct values running from 22.1 to 36.2, and only 32.1 repeats. Every one of the 25 carries country AR, an id ending .ar, and the group Argentina, which holds 26 entries and numbers all 26. Argentina broadcasts ISDB-Tb. ATSC and ISDB-Tb both use the major.minor shape, and the shape is all they share here. Our own source comment calls these values ATSC-style, which is true of the shape and false about the origin.
Those 25 rows are why the number is a floating point value rather than an integer. The type was cheap. The migration was not. The column cannot be backfilled, because the number lives in the source's payload and we do not retain the raw playlist. Every existing install would have carried a null on every row, and a list sorted by that number would have put an entire library in the unnumbered tail. The migration deletes the snapshot fingerprint and the materialized marker, forcing the next sync to rebuild the catalog. Every existing install paid a full re-materialization to display a badge.
Fifty-six values are not numbers at all
56 of the 1,355 values, over 53 distinct spellings, are refused outright. They fall into four shapes:
- A bare
#on 4 entries, a placeholder the emitter never filled in. - 49 broadcast-system codes: a
D011toD081run, aBS101toBS4K101family, a longCS055toCS363run, and a singleJCOM307. This is a real national numbering scheme, and the attribute has no room for it. CH 106andCH 108, the same number with a prefix and a space.- One value that is a platform name written in Japanese with
527on the end.
A refusal costs the row its number and never the row, so all 56 keep their place and sort into the unnumbered tail.
Several refusals are rules with no witness: a leading sign puts +5 and -5 outside the accepted styles, and zero is refused because an absent number is not channel zero. Neither shape occurs once in 1,355 values. Leading zeros are the lenient case: 007 parses to 7.
That split needs one caveat, because the measurement script emulates our parser in Python and float() is looser than the style set we pass. I checked the corpus for every shape where the two disagree, a leading plus or minus, exponents, underscores, whitespace padding and leading zeros, and found zero of each. The 1,299 against 56 split is the shipped parser's.
No two of these readers agree
Nothing below comes from a specification, because there is not one. Each cell is read from that project's source or its own documentation.
| Consumer | Reads it | Also accepts | Decimal | Two rows, one number | Attribute absent |
|---|---|---|---|---|---|
| Kodi IPTV Simple | Yes, since 2016-12 | ch-number, unquoted values, and any prefixed spelling, because the match is a substring search | Split at the dot into a major and a subchannel number | No detection; both survive, core sorts by number and breaks ties on name | Running counter continues from the previous channel plus one |
| Jellyfin | Yes, since 2019-02 | A numeric tvg-id; a numeric channel-id, but only when tvg-id is present and non-numeric; a leading number in the name; the URL file name | Parsed as a double, original string kept | Nothing in the parser | Falls through the chain; -1 and 0 rejected as invalid |
| Emby | Yes, during 2018 | None documented | Not documented | Not documented | Documented as automatic detection from available tags |
| xTeVe | No, stored and ignored | None; numbering is internal | Only from its own UI | Cannot happen; the allocator takes the first free number | Same as present: first free number at or above a configurable start, 1000 by default |
| Threadfin | Yes | tvg-chno only | Stored as a string, parsed as a float to sort | Copied verbatim, so collisions persist | First free number from the group's starting number |
| Channels DVR | channel-number, per its documentation | channel-id, which its documentation makes the required field | Not documented | Merged across sources, the prioritized channel wins | Not documented |
| Plex | No M3U ingestion at all | Not applicable | Whatever the proxy in front of it sends | Whatever the proxy sends | Numbers come from a tuner lineup |
| VLC | No | Reads tvg-logo, tvg-name, tvg-language, tvg-id, group-title and drops the rest | Not applicable | Not applicable | No channel-number concept, only playlist position |
Kodi's reader is the most permissive in the set and the most surprising in effect. Each entry is seeded from a running counter that resets to whatever number a channel claims, so a single tvg-chno="500" two thirds of the way down a file renumbers every later unnumbered channel from 501. One attribute on one row moves hundreds of others. And after all that parsing, Kodi core's setting to use backend channel numbers is off by default: the all-channels group renumbers itself 1 to n, other groups inherit that, and the playlist's number stays on the channel without being shown.
Jellyfin's is the strictest: its regex requires a double-quoted value, so tvg-chno=101 without quotes is invisible to it while Kodi reads it happily, and our own reader has the same restriction. Its fallback chain ends at the file name of the stream URL, the point where a channel number stops being data and starts being a guess about a path.
xTeVe is the interesting refusal. It parses the attribute into generic metadata and never uses it, allocating its own numbers from the first free slot at or above 1000, and a request to honor tvg-chno was closed as not planned. That is the only design here where two rows claiming one number cannot collide, because the playlist's opinion is never consulted, and the price is that a carefully numbered source arrives renumbered. Threadfin, its fork, went the other way and copies the value verbatim, so collisions come through intact; its deduplication hashes the URL, the tvg-id and the source id, never the number.
Plex is the odd row, because it has no first-party M3U ingestion and nothing inside it parses the attribute; its community-supported-tuners page asks for a Windows BDA or Linux DVB device. The usual route from a playlist is an HDHomeRun-style proxy, where the number arrives as GuideNumber in lineup.json, and which number that is depends on the proxy: xTeVe substitutes its own, Threadfin passes the string straight through.
Six rows produce four usable numbers
Here is a fragment built to hit every path at once. Every host is a reserved example domain and every name is invented.
#EXTINF:-1 tvg-id="one.example" tvg-chno="1",Example One
https://example.com/live/one.m3u8
#EXTINF:-1 tvg-id="two.example" tvg-chno="1",Example Two
https://example.com/live/two.m3u8
#EXTINF:-1 tvg-chno=101,Example Three
https://example.com/live/three.m3u8
#EXTINF:-1 tvg-chno="22.1",Example Four
https://example.com/live/four.m3u8
#EXTINF:-1 tvg-chno="BS4K101",Example Five
https://example.com/live/five.m3u8
#EXTINF:-1 tvg-chno="007",Example Six
https://example.com/live/six.m3u8
One and Two both claim number 1, which is this file's normal case rather than its edge. Our comparator keeps both, ordered by a natural-order comparison of their names and then an ordinal comparison of their ids, so the pair is deterministic. Kodi keeps both too. xTeVe would hand them 1000 and 1001 and never read the attribute.
Three is unquoted, so Kodi reads 101 while we read nothing at all, as Jellyfin also would: the row lands in the unnumbered tail on our side and at channel 101 on Kodi's.
Four is the subchannel case. We keep 22.1 as a decimal, Kodi splits it into major 22 with subchannel 1, Jellyfin parses a double and keeps the original string for display, and any consumer with an integer column shows 22.
Five is refused by our number parser and keeps its row, so it joins Three in the tail. Kodi comes off worse than we do: its reader hands the value to atoi, which returns 0 for a string whose digits do not lead, and nothing guards the setter, so the row becomes channel 0 and the running counter is reset to 0 with it. The next unnumbered channel becomes 1. A junk value there does more damage than an absent one.
Six is the leading-zero case: 007 normalizes to 7 for us, and Jellyfin parses the same 7 while still displaying 007.
Sorted by the source's number, our list reads Example One, Example Two, Example Six at 7, Example Four at 22.1, then Example Five and Example Three in name order, because an absent number sorts after every present one rather than as zero.
A collision resolves differently depending on what the viewer is doing. It took us three mechanisms rather than one. For sorting, the collision is absorbed by the name tie-break. For keypad entry, typing 1 walks the matches: every row with that number is collected and repeated presses cycle through them, wrapping at the end, because pinning the first of 107 forever would make the other 106 unreachable. For aggregation, where several sources contribute one logical channel, the naming member's number wins, and failing that the first member in priority order that has one. Those same members are the queue a stalled stream fails over into, which has its own article.
The keypad path has a gap I do not like. It accumulates digit keys only, with a 1.2 second window between them and a reset after five digits, and it parses with no number styles enabled, so there is no way to type 22.1. A subchannel is reachable by search, where the query goes through the parser that accepts a decimal point, and unreachable by the remote. The honest reason that has not been fixed is that 25 rows in our corpus need it.
A number you assign is not the number the file sent
The source's number is never rewritten. A reordering gesture writes a separate fractional key, and the two live side by side.
The mechanism is chosen by a constraint that has nothing to do with ordering. Our sync batch cap is 500 changes and a single rejected record fails the whole batch, taking every unrelated change traveling with it, so renumbering a group on every drop was never viable. Instead every channel gets an implicit key from its position in the fallback order, a placed channel uses its synced key, and a drop between two rows takes the midpoint, which writes exactly one row. Implicit keys sit one apart, so each drop into the same gap halves it: against a minimum gap of 1e-9 the thirtieth consecutive drop into one slot still lands, and the thirty-first rebalances the group to evenly spaced keys. That rebalance is the only path here that writes thousands of rows, and it is logged with its size instead of running quietly. Three comments in our own source say fifty. We got this wrong from intuition, the first draft of this article repeated it, and the figure only became thirty when someone did the arithmetic.
The badge shows the row's position rather than that key, because implicit keys shift whenever the source adds or removes channels, and a number that moves on its own after a sync is worse than none. An earlier version of this ordering lived in the view model, where a saved order applied to source category groups and silently did not apply to All channels, favorites or custom groups.
The Windows and Xbox apps parse all of this on your device, from the playlist you point them at, and you can create a free account to try either. Nothing about your channel numbers reaches us, because the file is never uploaded and the parse happens locally.
If you emit playlists, write tvg-chno on every row and keep it unique inside the file, because the consumers that honor it share no rule for what to do when you do not, and one of them will renumber everything downstream of your first numbered channel. If you read playlists, count the distinct values before you build anything on the attribute. In our corpus that count is the difference between 1,355 numbers and 263.
What this article measured46 claims, each with the evidence behind it
| Claim | Evidence | Counted |
|---|---|---|
| RFC 8216 is an Informational independent submission. It defines EXTM3U in section 4.3.1.1, EXTINF in section 4.3.2.1 and the EXT-X- tag family, and it defines no tvg- attribute and no channel-number attribute; the string tvg does not appear in it. Its section 4.2 attribute-list grammar is comma-separated with a fixed AttributeName form, which the space-separated tvg- convention does not follow. Section 4.2 also requires clients to ignore unrecognized tags and unrecognized AttributeNames, which is why the convention survives alongside it.RFC 8216, Informational, sections 4.2, 4.3.1.1 and 4.3.2.1. Full text re-read 2026-08-23; no occurrence of the string tvg. | Specification | Not applicable |
| The earliest verifiable public appearance of tvg-chno is PR #92 against Kodi's PVR IPTV Simple Client, opened 2016-11-24 and merged 2016-12-17, commit 28e607bb7d192c54759bae8e2bb1a5d51297e002 by sangwoo shin, message add channel number tag, which introduces the marker constant tvg-chno= in the playlist loader.https://github.com/kodi-pvr/pvr.iptvsimple/pull/92 and commit 28e607bb7d192c54759bae8e2bb1a5d51297e002. Constant re-verified 2026-08-23 against branch Piers: TVG_INFO_CHNO_MARKER = tvg-chno= and CHANNEL_NUMBER_MARKER = ch-number= in src/iptvsimple/PlaylistLoader.h. | Specification | Not applicable |
| In July 2015 the same add-on's issue tracker stated that the M3U format had no tag for a channel number and proposed encoding it in the channel name instead. The author of PR #92 did not cite that issue as his motivation: his PR body gives backend sync as the reason, and he noted the issue five minutes after opening the PR. The maintainer closed the issue as fixed by #92 on 2016-12-17.https://github.com/kodi-pvr/pvr.iptvsimple/issues/31, opened 2015-07-20, closed 2016-12-17 as fixed by #92. PR #92 body by excorp, 2016-11-24T04:19Z: Added channel number tag to m3u file. It is useful for syncing channel number with backend in kodi. His follow-up comment: I found something similar in issue #31. | Specification | Not applicable |
| tvg-name, tvg-logo and group-title are present in the add-on's initial project commit, 095e2fd of 2013-01-17 by Anton Fedchin. tvg-id lands the same day in 46c58ef, and tvg-shift on 2013-01-21 in 68a61c4. That project's wiki documents six attributes, tvg-id, tvg-name, tvg-logo, tvg-shift, group-name and radio, without stating their origin.afedchin/xbmc-addon-iptvsimple, commit 095e2fd (2013-01-17) at src/iptvsimple/PVRDemoData.cpp:31-33, commit 46c58ef (2013-01-17) at src/iptvsimple/PVRIptvData.cpp:35, commit 68a61c4 (2013-01-21), and https://github.com/afedchin/xbmc-addon-iptvsimple/wiki/IPTV-Simple-Home | Specification | Not applicable |
| No first-party source states what tvg abbreviates. TV guide is the universal reading and the sibling attributes x-tvg-url and url-tvg carry the XMLTV address, which supports it, but the expansion is uncited.Absence of any expansion in the add-on wiki, the add-on README and the original repository | Specification | Not applicable |
| Tvheadend writes tvg-chno= into its own M3U output, and emits it in major.minor form via channel_get_number_as_str, which makes it a first-party emitter of decimal channel numbers. The commit that added it, 35c83be2 of 2018-03-20 by Jaroslav Kysela, has a message reading add tvg-chnum attribute while the emitted token is tvg-chno.tvheadend commit 35c83be2fc8cdded (2018-03-20), message: m3u playlist: add tvg-chnum attribute with the channel number, fixes #5011. The emitter sits at src/webui/webui.c:601 on master today, which is not the line number in that commit. | Specification | Not applicable |
| Tvheadend's own IPTV auto-network reader takes tvh-chnum first and falls back to tvg-chno, so the project reads a spelling it does not write.tvheadend/tvheadend, src/input/mpegts/iptv/iptv_auto.c:106-107 | Specification | Not applicable |
| At least five spellings carry a channel number in the wild: tvg-chno, channel-number, ch-number, tvh-chnum and a numeric channel-id.tvg-chno and ch-number at kodi-pvr/pvr.iptvsimple src/iptvsimple/PlaylistLoader.h:33-34; channel-number in the Channels DVR custom-channels documentation and in TVIP set-top firmware 4.1.12+ at https://wiki.tvip.tv/en/m3u, whose example is unquoted (channel-number=10); tvh-chnum at tvheadend/tvheadend src/input/mpegts/iptv/iptv_auto.c:106-107; numeric channel-id at jellyfin/jellyfin src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs:191-195 | Specification | Not applicable |
| Across three sample playlists held locally, holding 2,209 entries, only one file numbers anything: 1,355 of its 1,895 entries carry tvg-chno, 71.5 percent. The other two files carry the attribute on zero of their 314 entries. | n = 2209 | Aug 23, 2026 |
| The 1,355 values cover 263 distinct numbers, a mean of 5.15 entries per value. 179 values are used exactly once. The top five are 1 on 107 entries, 2 on 92, 3 on 80, 4 on 72 and 5 on 68. The numeric range is 1 to 4,999. | n = 1355 | Aug 23, 2026 |
| 1,176 of the 1,355 entries share their number with at least one other row, 86.8 percent, and a unique key would drop 1,092 of them. Counting only values the parser accepts, 1,172 of 1,299 share their number, 90.2 percent. | n = 1355 | Aug 23, 2026 |
| 33 of the 85 groups that carry any number contain at least one duplicated number inside the group, 69 of 85 contain a channel numbered 1, and one group holds 7 entries all numbered 1. Inside the largest group, 79 numbered entries cover 74 distinct numbers. | n = 1895 | Aug 23, 2026 |
| 596 of the 1,895 entries end with no usable number, 31.5 percent: 540 never carried the attribute and 56 carried a value the parser refuses. | n = 1895 | Aug 23, 2026 |
| In the same file 1,250 entries carry both tvg-chno and tvg-id, 105 carry a number and no id, 142 an id and no number, and 398 carry neither. | n = 1895 | Aug 23, 2026 |
| 25 entries over 24 distinct values use major.minor subchannel numbering, running from 22.1 to 36.2, and 32.1 is the only decimal that repeats, on 2 entries. Every one of the 25 carries tvg-country=AR, a tvg-id ending .ar and group-title Argentina. That group holds 26 entries in total and numbers all 26. Argentina broadcasts ISDB-Tb rather than ATSC; the two systems share the major.minor shape and not the geography. | n = 1895 | Aug 23, 2026 |
| The number is typed as a double and stored in a SQLite REAL column, and both places state subchannel numbering as the reason. | n = 1 | Aug 23, 2026 |
| The column cannot be backfilled in place, because the number lives in the source payload and the raw playlist is not retained, so the migration deletes the snapshot fingerprint and the materialized marker to force the next sync to rebuild the catalog rather than short-circuit. | n = 1 | Aug 23, 2026 |
| The parser accepts a decimal point, leading and trailing whitespace and invariant culture only. A leading sign, an exponent, a thousands separator and a comma decimal are outside the style set. Non-finite, non-positive and above 99,999 are refused. Leading zeros are accepted and value-normalized, so 007 becomes 7. A refusal returns nothing and costs the row its number, never the row. | n = 1 | Aug 23, 2026 |
| 22.1 parses and 22,1 does not, including under a French culture, and the badge formats 5 rather than 5.0. | n = 2 | Aug 23, 2026 |
| 56 values over 53 distinct spellings are refused: 4 are a literal #, 49 are broadcast-system codes in BS, CS, D and JCOM families such as D011, BS4K101, CS363 and JCOM307, 2 read CH 106 and CH 108, and 1 is a platform name in Japanese ending in 527. | n = 1355 | Aug 23, 2026 |
| The measurement script emulates the parser with Python float(), which is looser than the C# style set. The corpus was checked for every shape where the two disagree, a leading plus or minus, exponent notation, underscores, whitespace padding and leading zeros, with zero occurrences of each, so the 1,299 accepted against 56 refused split is the shipped parser's split. | n = 1355 | Aug 23, 2026 |
| No index, unique or otherwise, exists on the channel-number column, and three separate source files state in place that nothing may key on the value. | n = 3 | Aug 23, 2026 |
| One parser serves both wire formats, the M3U attribute and the portal protocol's num field, and the portal field is read through a flexible converter because panels emit it as a string or as a number interchangeably. | n = 2 | Aug 23, 2026 |
| Sorting by the source's number puts unnumbered rows after numbered ones rather than treating absent as zero, and equal numbers fall to a natural-order name comparison and then to an ordinal id comparison. | n = 1 | Aug 23, 2026 |
| The badge shows the source's number only while the list is sorted by that number, and otherwise the row's 1-based position; an unnumbered row also falls back to position, because a blank badge in a numbered column reads as a broken row. | n = 1 | Aug 23, 2026 |
| Keypad entry collects every row matching the typed number and walks them on repeat presses, wrapping at the end, rather than pinning the first match. | n = 1 | Aug 23, 2026 |
| The multi-digit buffer accumulates digit keys only and parses with no number styles enabled, so a subchannel number cannot be reached from the keypad; the same value typed into search does match, because search runs the query through the number parser, which accepts a decimal point. | n = 1 | Aug 23, 2026 |
| When several sources contribute one logical channel, the naming member's number wins, and if it has none the first member in the bucket that advertises one wins; bucket order is already priority order. | n = 1 | Aug 23, 2026 |
| A user-assigned order is a separate fractional key, never a rewrite of the source's number. A drop writes exactly one sync row because the sync batch cap is 500 changes and one rejected record fails the whole batch. Implicit keys are one apart and each drop into the same gap halves it, so with a minimum gap of 1e-9 the thirtieth consecutive drop into one slot still lands and the thirty-first rebalances; the rebalance is the only path that writes more than one row. | n = 1 | Aug 23, 2026 |
| Before ordering was pulled into one pure function, a synced order applied to source category groups and silently did not apply to All channels, favorites or custom groups. | n = 1 | Aug 23, 2026 |
| Kodi's marker reader searches for the marker anywhere in the line and accepts a quoted or a space-delimited value, so an unquoted tvg-chno=101 is read and a prefixed spelling such as x-tvg-chno= also matches. The value is split at the dot into a major number and a subchannel number.kodi-pvr/pvr.iptvsimple branch Piers, head 78a023cb31, src/iptvsimple/PlaylistLoader.cpp:336, :375-390 and ReadMarkerValue at :622-664 | Specification | Not applicable |
| Kodi seeds each new entry from a running counter that starts at the Start channel number setting, resets the counter to whatever number a channel claims, and increments after each add, so one numbered channel renumbers every later unnumbered channel from that point.kodi-pvr/pvr.iptvsimple, src/iptvsimple/PlaylistLoader.cpp:174 and src/iptvsimple/Channels.cpp:37, :100, :121 | Specification | Not applicable |
| Kodi converts the marker value with atoi, which returns 0 for a string whose digits do not lead, such as BS4K101. Nothing guards the setter, so the row is given channel number 0 and the running counter is reset to 0, which renumbers the next unnumbered channel to 1. A junk value is more destructive there than an absent one.kodi-pvr/pvr.iptvsimple, std::atoi at src/iptvsimple/PlaylistLoader.cpp:388, unguarded SetChannelNumber(int) at src/iptvsimple/Channel.h:64, counter assignment at src/iptvsimple/Channels.cpp:100 and increment at :121 | Specification | Not applicable |
| Kodi core's pvrmanager.usebackendchannelnumbers setting defaults to false. With backend numbers off, the all-channels group renumbers its members 1..n and other groups inherit that numbering, because the 1..n path runs only for the channels owner or when startgroupchannelnumbersfromone is set, and that setting also defaults to false. The client channel number is retained on the member and simply not displayed.xbmc/xbmc at 3f152e7c12, system/settings/settings.xml:1832 for usebackendchannelnumbers with <default>false</default> and :1841 for startgroupchannelnumbersfromone with <default>false</default>; xbmc/pvr/channels/PVRChannelGroup.cpp:915-919 for the 1..n condition and :927 for SetClientChannelNumber | Specification | Not applicable |
| The add-on ships a setting that ignores tvg-chno outright and numbers channels only by their order in the file.kodi-pvr/pvr.iptvsimple README: Only number by channel order in M3U, which ignores any tvg-chno tags | Specification | Not applicable |
| The reference implementation documents the attribute in one line of its README, so the attribute is undocumented by any standard rather than undocumented outright.kodi-pvr/pvr.iptvsimple README line 515: tvg-chno: The number to be used for this channel. | Specification | Not applicable |
| Jellyfin's attribute regex requires a double-quoted non-empty value, so an unquoted number is invisible to it. Its channel number falls back through tvg-chno, then a numeric tvg-id, then, only when tvg-id is present and non-numeric, a numeric channel-id, then a leading number in the display name, and finally the file name of the URL. -1 and 0 are rejected as invalid.jellyfin/jellyfin, src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs:36 for the GeneratedRegex, :170-255 for GetChannelNumber with the channel-id branch nested as an else-if inside the tvg-id branch at :185-195, :267-277 for IsValidChannelNumber with -1 and 0 rejected at :270-271. Re-verified against master 2026-08-23. | Specification | Not applicable |
| Jellyfin gained tvg-chno support in PR #863, created 2019-02-10 and merged 2019-02-14.https://github.com/jellyfin/jellyfin/pull/863 | Specification | Not applicable |
| Emby documents tvg-chno as the channel number and states that missing values are detected automatically from whatever tags are available. The behavior beyond that is not documented and the shipping parser is closed source. The support date is 2018; the only source found for it is the Jellyfin PR body, which says Emby announced this back in July of 2018.EmbySupport/Emby.Docs/M3U-Tuners.md:41, last updated 2026-07-18, published at https://emby.media/support/articles/M3U-Tuners.html; date attribution from the body of https://github.com/jellyfin/jellyfin/pull/863 | Specification | Not applicable |
| The public Emby source snapshot is frozen at 3.5.3 of 2018-09-20 and its M3U parser contains no tvg-chno at all. That file is the direct ancestor of Jellyfin's parser, which is why both carry the same name-prefix and URL file-name fallbacks.MediaBrowser/Emby at 1d7c2ab4bf, Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs:159-252, last touched 2017-08-20 | Specification | Not applicable |
| xTeVe stores every playlist attribute as generic metadata and has no channel-number field on the struct it parses a playlist row into. It allocates its own numbers, taking the first free number at or above a configurable first channel that defaults to 1000 in code rather than in documentation, so two rows claiming one number cannot collide by construction. A feature request to honor tvg-chno was closed as not planned.xteve-project/xTeVe at 0e999b85b9, src/internal/m3u-parser/xteve_m3uParser.go:54-80 for the generic attribute map with no chno handling, src/struct-system.go:202-216 for M3UChannelStructXEPG, src/system.go:129 for defaults[mapping.first.channel] = 1000, src/xepg.go:332-348 for the first-free scan; issue #269 closed as not planned | Specification | Not applicable |
| Threadfin, a fork of xTeVe, does read tvg-chno, stores it as a string, parses it as a float for sorting and for dedupe comparisons, and copies the value verbatim into the channel id it exposes, so collisions persist. Deduplication is an md5 over URL plus tvg-id plus M3U source id, falling back to URL plus source id when tvg-id is blank; a second pass breaks ties on the lower parsed channel number. Name-based duplicate removal is present but disabled in the source.Threadfin/Threadfin, branch main, head 6b9c0ccf16 dated 2025-09-29. String storage at src/struct-system.go:194 and :232; verbatim copy into xChannelID at src/xepg.go:643-647; float parse for sort at src/m3u.go:287-288; float parse for the dedupe tie-break at src/xepg.go:1923-1927; hash input at src/xepg.go:476-484, :508-516 and :1835-1843; disabled name-based removal at src/xepg.go:1853 | Specification | Not applicable |
| Plex Live TV has no first-party M3U ingestion, so nothing inside Plex ever parses tvg-chno. Its community-supported-tuners documentation asks for a Windows BDA or Linux DVB API device, and names TVHProxy among the routes people use. The usual route from a playlist is an HDHomeRun-style proxy, where the number arrives as GuideNumber in lineup.json, and which number that is depends on the proxy: xTeVe substitutes one of its own, Threadfin passes the tvg-chno string through verbatim.Plex Live TV and Community Supported Tuners documentation, which describes Windows BDA and Linux DVB API devices and names TVHProxy; the HDHomeRun lineup.json GuideNumber field; Threadfin/Threadfin src/xepg.go:643-647 into src/hdhr.go:160, which sets stream.GuideNumber from the value taken from tvg-chno | Specification | Not applicable |
| Channels DVR documents channel-number for custom channel sources, alongside a required channel-id, tvg-logo, tvg-name and its own tvc- extensions. Its documentation does not mention tvg-chno. It states that a number colliding with another source's lineup is merged and the prioritized channel is used.Channels DVR documentation for custom channel sources, verbatim: Be sure not to use a channel number that exists in another source's lineup. Channels will merge them together and use the prioritized channel. | Specification | Not applicable |
| VLC's M3U parser reads tvg-logo, tvg-name, tvg-language, tvg-id and group-title, and silently drops unknown tvg- keys including chno. VLC has no channel-number concept at all, only playlist position.VideoLAN vlc, modules/demux/playlist/m3u.c, tvg attribute handling added 2020-03-11 | Specification | Not applicable |
| The hand-built fragment in this article yields six rows, four of which carry a usable number under our parser, and a numbered sort of 1, 1, 7 and 22.1 followed by the two unnumbered rows in name order. | n = 6 | Aug 23, 2026 |