Skip to content

plugins.twitch: always filter out ads #6579

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

bastimeyer
Copy link
Member

Drop --twitch-disable-ads and always filter out ads. Suppress the plugin argument, so we don't introduce breaking changes. Suppressed plugin arguments automatically show a deprecation warning.

The reason for this change is that Twitch has switched from MPEG-TS to fragmented MPEG-4 streams for their higher quality streams that are currently in beta. This is also the case for any re-encoded lower stream qualities if the input stream is available in a higher quality. This is independent of the used video codec.

Unfortunately, stream discontinuities that occur during an ad-transition when not filtering out ads are harder to recover from when the stream uses an fMPEG-4 container, causing more severe playback issues in players compared to MPEG-TS streams when incoherent stream segments of the actual stream and ads are concatenated. This means that we need to always filter out ads and don't let the users decide, to ensure the least amount of playback issues among all players.

Since the requested playerType has always been "embed" when requesting an access token, only the purple "ads in progress" screen was shown instead of actual ads, so making the plugin always filter out ads (aka "non-stream segments") is the sensible thing to do.

Keep the "Will filter out ads" info log message that was previously only shown when the plugin argument was set, so the user is made aware of this change, as gaps in the output will now always occur.

@bastimeyer bastimeyer added the plugin issue A Plugin does not work correctly label Jun 24, 2025
@bastimeyer
Copy link
Member Author

The docs will need an update before this can be merged. Leaving this open until that's done.

Discussions are welcome.

@Hakkin
Copy link
Contributor

Hakkin commented Jun 29, 2025

I think this is a good idea in general, but I think it's worth taking another look over the ad detection code before pushing it in.

I'm mostly concerned about false positives incorrectly filtering segments, I'm wondering if this might be a little too aggressive. IIRC Twitch also sometimes triggers banner ads through the playlist, while keeping live segments, I wonder if that will also trigger the ad detection for the period of the banner ad? Unfortunately I can't actually seem to get Twitch to serve me any banner ads right now so I haven't been able to confirm one way or another...I can also imagine other kinds of ads that could trigger this kind of check if they implement them in the future, maybe PiP ads or audio ads or something.

This also seems to be a bit out of date, there seems to be various titles the ads can have now, besides just "Amazon". I'm seeing "DMC" and also just random numbers as well.

For now, as far as I've seen, the daterange.classname == "twitch-stitched-ad" check seems to be enough to filter all ads currently, and has pretty much 0 risk of false positives, it might be safest to just leave that check if the feature will always be enabled.

@bastimeyer
Copy link
Member Author

I'm mostly concerned about false positives incorrectly filtering segments

I have not seen any false positives for years, or maybe ever. If there are false positives, for whatever reason, then this should be fixed separately. I am not concerned at all about the current ad filtering implementation, unless Twitch changes their ad metadata, in which case it needs to be fixed anyway.

Here's the plugin's commit history. The last ad-related change was about logging ad durations and a revert of ad detection in prefetch segments which was not working correctly, but which was then fixed. This happened last year. Previous ad-related changes were years ago. As said, I haven't seen any false positives.
https://github.com/streamlink/streamlink/commits/ba6959cd50ced9811b93925e201b49ba40f0062f/src/streamlink/plugins/twitch.py

I'm wondering if this might be a little too aggressive

This is about dateranges with the twitch-specific X-TV-TWITCH-AD-* keys, like X-TV-TWITCH-AD-ROLL-TYPE={PREROLL,MIDROLL}, X-TV-TWITCH-AD-COMMERCIAL-ID=... or X-TV-TWITCH-AD-POD-FILLED-DURATION=...

Here's the respective test for these attributes:

def test_hls_disable_ads_has_preroll_and_midstream(self, mock_log):
ads1a = TagDateRangeAd(
start=DATETIME_BASE,
duration=2,
custom={"X-TV-TWITCH-AD-ROLL-TYPE": "PREROLL"},
)
ads1b = TagDateRangeAd(
start=DATETIME_BASE,
duration=1,
)
ads2 = TagDateRangeAd(
start=DATETIME_BASE + timedelta(seconds=4),
duration=4,
custom={
"X-TV-TWITCH-AD-ROLL-TYPE": "MIDROLL",
"X-TV-TWITCH-AD-COMMERCIAL-ID": "123",
},
)
ads3 = TagDateRangeAd(
start=DATETIME_BASE + timedelta(seconds=8),
duration=1,
custom={
"X-TV-TWITCH-AD-ROLL-TYPE": "MIDROLL",
"X-TV-TWITCH-AD-COMMERCIAL-ID": "456",
"X-TV-TWITCH-AD-POD-FILLED-DURATION": ".9",
},
)
segments = self.subject(
[
Playlist(0, [ads1a, ads1b, Segment(0)]),
Playlist(1, [ads1a, ads1b, Segment(1)]),
Playlist(2, [Segment(2), Segment(3)]),
Playlist(4, [ads2, Segment(4), Segment(5)]),
Playlist(6, [ads2, Segment(6), Segment(7)]),
Playlist(8, [ads3, Segment(8), Segment(9)], end=True),
],
streamoptions={"disable_ads": True, "low_latency": False},
)
self.await_write(10)
data = self.await_read(read_all=True)
assert data == self.content(segments, cond=lambda s: s.num not in (0, 1, 4, 5, 6, 7, 8)), "Filters out all ad segments"
assert all(self.called(s) for s in segments.values()), "Downloads all segments"
assert mock_log.info.mock_calls == [
call("Will skip ad segments"),
call("Waiting for pre-roll ads to finish, be patient"),
call("Detected advertisement break of 2 seconds"),
call("Detected advertisement break of 4 seconds"),
call("Detected advertisement break of 1 second"),
]

banner ads
PiP ads or audio ads or something

Those are not embedded ads then that replace the actual stream. They can't add segments for separate streams in the same playlist without massively violating the HLS protocol, and I doubt they would do this, even with custom metadata.

This also seems to be a bit out of date

That's a fallback check which was implemented in 8105bfc three years ago, in order to avoid more expensive daterange checks if the detection is as simple as checking known segment titles. It's not important to remove this. It's simply a redundancy.

For now, as far as I've seen, the daterange.classname == "twitch-stitched-ad" check seems to be enough to filter all ads currently, and has pretty much 0 risk of false positives, it might be safest to just leave that check if the feature will always be enabled.

Please show me a false positive, and then we can remove redundant ad checks. The highest priority is filtering all potential ads, not being as efficient as possible with the ad detection.

@Hakkin
Copy link
Contributor

Hakkin commented Jun 30, 2025

I have not seen any false positives for years, or maybe ever.

I believe the likelihood anyone would actually notice a false positive is low, they would have to

  1. Actually be actively using --twitch-disable-ads
  2. Be logging with --loglevel=all so they can actually review the playlist response
  3. Be using various non-default --twitch-access-token-param, since the defaults will (seemingly?) always give you the embedded ad screens instead of any actual ads.
  4. Actually understand that there are ads being detected when there shouldn't be

The overlap between all of these is pretty much non-existent, so I would never expect anyone to notice, but that doesn't necessarily mean it couldn't be happening.

This is about dateranges with the twitch-specific X-TV-TWITCH-AD-* keys, like X-TV-TWITCH-AD-ROLL-TYPE={PREROLL,MIDROLL}, X-TV-TWITCH-AD-COMMERCIAL-ID=... or X-TV-TWITCH-AD-POD-FILLED-DURATION=...
...

banner ads
PiP ads or audio ads or something

Those are not embedded ads then that replace the actual stream. They can't add segments for separate streams in the same playlist without massively violating the HLS protocol, and I doubt they would do this, even with custom metadata.

Yes, but I'm saying I believe I've seen this X-TV-TWITCH-AD daterange used for banner ads in the past, which don't replace the segments. There's nothing that says this daterange is restricted to actual stitched video ads, in the future (or currently?) there could be X-TV-TWITCH-AD-ROLL-TYPE=OVERLAY or SIDEBAR, or any other combination of the X-TV-TWITCH-AD-* tags.

The highest priority is filtering all potential ads, not being as efficient as possible with the ad detection.

IMO it should be the opposite, the ad filtering should be as lax as possible while still filtering all ads currently know about, which matching daterange.classname == "twitch-stitched-ad" does. The requirements change when we go from "optional switch for people who don't want to see ads" to "forced enabled for everyone".

Personally speaking, if this went through as is, I would revert it on my local fork because I don't trust the current code to not filter non-ads.

@bastimeyer bastimeyer force-pushed the plugins/twitch/remove-disable-ads branch from dbc1875 to 0090022 Compare June 30, 2025 11:55
@bastimeyer
Copy link
Member Author

the ad filtering should be as lax as possible while still filtering all ads currently know about, which matching daterange.classname == "twitch-stitched-ad" does

The "while still filtering all ads" is the important part, which was the reason why the current implementation on master exists. There have been cases where we didn't catch all ads, so multiple redundant checks were added in the past. This was also done at a time where the embedded ads were new.

As said, I personally have not seen any cases were the current implementation on master made false positive detections. But we can remove the redundant EXT-X-DATERANGE attribute checks (X-TV-TWITCH-AD-*).

I'd like to see a playlist with contents that would result in a false positive though. So far, this is only a speculative claim based on the ads filtering implementation that checks for the X-TV-TWITCH-AD-* attributes of any daterange, potentially not affecting the actual stream segments.

In this recent push to the PR branch I've also added trace log messages for ad dateranges and ad segments. The whole daterange data gets logged and only the relevant parts of the filtered out segment. If there are false positives, then this should log them.

The "Amazon" in segment.title check can be kept as a fast path. This won't ever result in false positives. Whether other titles are used for certain ads doesn't matter. The daterange check will catch it.

@Hakkin
Copy link
Contributor

Hakkin commented Jun 30, 2025

I'd like to see a playlist with contents that would result in a false positive though. So far, this is only a speculative claim based on the ads filtering implementation that checks for the X-TV-TWITCH-AD-* attributes of any daterange, potentially not affecting the actual stream segments.

I've been trying and failing to get Twitch to serve me any banner ads, at this point I'm assuming they're simply not serving them currently for whatever reason.

The best I can do for now is point you to a section of Twitch's Javascript to prove this false positive is possible:

If you search for X-TV-TWITCH-AD-VLM in Twitch's Javascript, you can find a section dealing with #EXT-X-DATERANGE:CLASS="twitch-maf-ad", these are client side ads, as opposed to server-side stitched video ads.
You can see

{
...
isVLM: '1' === i['X-TV-TWITCH-AD-VLM'],
...
}

where i is the DATERANGE tag where CLASS == 'twitch-maf-ad'. This means, at the very least, Twitch at least sometimes expects this specific X-TV-TWITCH-AD-* tag on purely client-side ads, which would trigger a false positive in the ad detection code. When or how this X-TV-TWITCH-AD-VLM tag gets added, I don't know.

Drop `--twitch-disable-ads` and always filter out ads.
Suppress the plugin argument, so we don't introduce breaking changes.
Suppressed plugin arguments automatically show a deprecation warning.

The reason for this change is that Twitch has switched from MPEG-TS
to fragmented MPEG-4 streams for their higher quality streams that
are currently in beta. This is also the case for any re-encoded lower
stream qualities if the input stream is available in a higher quality.
This is independent of the used video codec.

Unfortunately, stream discontinuities that occur during
an ad-transition when not filtering out ads are harder to recover from
when the stream uses an fMPEG-4 container, causing more severe playback
issues in players compared to MPEG-TS streams when incoherent stream
segments of the actual stream and ads are concatenated. This means that
we need to always filter out ads and don't let the users decide,
to ensure the least amount of playback issues among all players.

Since the requested `playerType` has always been "embed" when requesting
an access token, only the purple "ads in progress" screen was shown
instead of actual ads, so making the plugin always filter out ads
(aka "non-stream segments") is the sensible thing to do.

Keep the "Will filter out ads" info log message that was previously
only shown when the plugin argument was set, so the user is made
aware of this change, as gaps in the output will now always occur.
Checking for `X-TV-TWITCH-AD-` attributes in `EXT-X-DATERANGE` tags
is unnecessary. It was previously used as a fallback mechanism.
@bastimeyer bastimeyer force-pushed the plugins/twitch/remove-disable-ads branch from 0090022 to ce00f5e Compare July 7, 2025 15:38
@bastimeyer bastimeyer merged commit dd7c6fd into streamlink:master Jul 7, 2025
19 checks passed
@bastimeyer bastimeyer deleted the plugins/twitch/remove-disable-ads branch July 7, 2025 15:43
biodrone added a commit to dangeroustech/StreamDL that referenced this pull request Jul 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plugin issue A Plugin does not work correctly
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants