Added ad stripping to vaft / video-swap-new
- Improved vaft buffering mitigation - Changed 2k/4k player reload to only occur when the 2k/4k quality is active - Added new script "strip" which just strips ad segments - Removed all usage of "var"
This commit is contained in:
@@ -13,9 +13,9 @@ Proxies are the most reliable way of avoiding ads ([buffering / downtime info](f
|
|||||||
Alternatively:
|
Alternatively:
|
||||||
|
|
||||||
- `Alternate Player for Twitch.tv` - [chrome](https://chrome.google.com/webstore/detail/alternate-player-for-twit/bhplkbgoehhhddaoolmakpocnenplmhf) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch_5/)
|
- `Alternate Player for Twitch.tv` - [chrome](https://chrome.google.com/webstore/detail/alternate-player-for-twit/bhplkbgoehhhddaoolmakpocnenplmhf) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch_5/)
|
||||||
- `Purple AdBlock` - [chrome](https://chrome.google.com/webstore/detail/purple-adblock/lkgcfobnmghhbhgekffaadadhmeoindg) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/purpleadblock/) / [code](https://github.com/arthurbolsoni/Purple-adblock/)
|
- `Purple AdBlock` - [chrome](https://chrome.google.com/webstore/detail/purple-adblock/lkgcfobnmghhbhgekffaadadhmeoindg) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/purpleadblock/) / [userscript](https://raw.githubusercontent.com/arthurbolsoni/Purple-adblock/refs/heads/main/platform/tampermonkey/dist/purpleadblocker.user.js) / [code](https://github.com/arthurbolsoni/Purple-adblock/)
|
||||||
- `AdGuard Extra` - [chrome](https://chrome.google.com/webstore/detail/adguard-extra-beta/mglpocjcjbekdckiahfhagndealpkpbj) / [firefox](https://github.com/AdguardTeam/AdGuardExtra/#firefox) / [userscript](https://userscripts.adtidy.org/release/adguard-extra/1.0/adguard-extra.user.js)
|
- `AdGuard Extra` - [chrome](https://chrome.google.com/webstore/detail/adguard-extra-beta/mglpocjcjbekdckiahfhagndealpkpbj) / [firefox](https://github.com/AdguardTeam/AdGuardExtra/#firefox) / [userscript](https://userscripts.adtidy.org/release/adguard-extra/1.0/adguard-extra.user.js)
|
||||||
- `video-swap-new` - see below
|
- `vaft` - see below
|
||||||
|
|
||||||
[Read this for a full list and descriptions.](full-list.md)
|
[Read this for a full list and descriptions.](full-list.md)
|
||||||
|
|
||||||
@@ -25,16 +25,19 @@ Alternatively:
|
|||||||
|
|
||||||
**There are better / easier to use methods in the above recommendations.**
|
**There are better / easier to use methods in the above recommendations.**
|
||||||
|
|
||||||
- video-swap-new - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js) / [ublock](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/video-swap-new/video-swap-new-ublock-origin.js) / [ublock (permalink)](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/8d37319d094e3db2923c52f5f43356580505b0c4/video-swap-new/video-swap-new-ublock-origin.js)
|
|
||||||
- Uses a lower resolution stream during ads.
|
|
||||||
- vaft - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js) / [ublock](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/vaft/vaft-ublock-origin.js) / [ublock (permalink)](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/8d37319d094e3db2923c52f5f43356580505b0c4/vaft/vaft-ublock-origin.js)
|
- vaft - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js) / [ublock](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/vaft/vaft-ublock-origin.js) / [ublock (permalink)](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/8d37319d094e3db2923c52f5f43356580505b0c4/vaft/vaft-ublock-origin.js)
|
||||||
- The same as `video-swap-new` but attempts to get a clean stream faster (may suffer from more freezing / playback issues).
|
- Attempts to get a clean stream as fast as it can
|
||||||
|
- If it fails to get a clean stream it removes ad segements (no playback until ad-free stream is found)
|
||||||
|
- video-swap-new - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js) / [ublock](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/video-swap-new/video-swap-new-ublock-origin.js) / [ublock (permalink)](https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/8d37319d094e3db2923c52f5f43356580505b0c4/video-swap-new/video-swap-new-ublock-origin.js)
|
||||||
|
- Attempts to get a clean stream
|
||||||
|
- If it fails to get a clean stream it removes ad segments (no playback until ad-free stream is found)
|
||||||
|
- Not recommended, `vaft` is a better script
|
||||||
|
|
||||||
## Applying a script (uBlock Origin)
|
## Applying a script (uBlock Origin)
|
||||||
|
|
||||||
- Navigate to the uBlock Origin Dashboard (the extension options)
|
- Navigate to the uBlock Origin Dashboard (the extension options)
|
||||||
- Under the `My filters` tab add `twitch.tv##+js(twitch-videoad)`.
|
- Under the `My filters` tab add `twitch.tv##+js(twitch-videoad)`.
|
||||||
- Under the `Settings` tab, enable `I am an advanced user`, then click the cog that appears. Modify the value of `userResourcesLocation` from `unset` to the full url of the solution you wish to use (if a url is already in use, add a space after the existing url). e.g. `userResourcesLocation https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/video-swap-new/video-swap-new-ublock-origin.js`
|
- Under the `Settings` tab, enable `I am an advanced user`, then click the cog that appears. Modify the value of `userResourcesLocation` from `unset` to the full url of the solution you wish to use (if a url is already in use, add a space after the existing url). e.g. `userResourcesLocation https://raw.githubusercontent.com/pixeltris/TwitchAdSolutions/master/vaft/vaft-ublock-origin.js`
|
||||||
- To ensure uBlock Origin loads the script I recommend that you disable/enable the uBlock Origin extension (or restart your browser).
|
- To ensure uBlock Origin loads the script I recommend that you disable/enable the uBlock Origin extension (or restart your browser).
|
||||||
|
|
||||||
To stop using a script remove the filter and make the url `unset`.
|
To stop using a script remove the filter and make the url `unset`.
|
||||||
|
|||||||
+39
-28
@@ -7,11 +7,11 @@
|
|||||||
- `TTV LOL PRO (v1)` - [code](https://github.com/younesaassila/ttv-lol-pro/tree/v1)
|
- `TTV LOL PRO (v1)` - [code](https://github.com/younesaassila/ttv-lol-pro/tree/v1)
|
||||||
- The older, deprecated version of `TTV LOL PRO` that still uses TTV LOL-compatible proxies. Only use this if you're having issues with the current version and know what you're doing.
|
- The older, deprecated version of `TTV LOL PRO` that still uses TTV LOL-compatible proxies. Only use this if you're having issues with the current version and know what you're doing.
|
||||||
- `Alternate Player for Twitch.tv` - [chrome](https://chrome.google.com/webstore/detail/alternate-player-for-twit/bhplkbgoehhhddaoolmakpocnenplmhf) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch_5/)
|
- `Alternate Player for Twitch.tv` - [chrome](https://chrome.google.com/webstore/detail/alternate-player-for-twit/bhplkbgoehhhddaoolmakpocnenplmhf) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/twitch_5/)
|
||||||
- Removes ad segments (no playback until ad-free stream).
|
- Removes ad segments (no playback until ad-free stream). Can use low resolution stream when available during midroll ads.
|
||||||
- `Purple AdBlock` - [chrome](https://chrome.google.com/webstore/detail/purple-adblock/lkgcfobnmghhbhgekffaadadhmeoindg) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/purpleadblock/) / [code](https://github.com/arthurbolsoni/Purple-adblock/)
|
- `Purple AdBlock` - [chrome](https://chrome.google.com/webstore/detail/purple-adblock/lkgcfobnmghhbhgekffaadadhmeoindg) / [firefox](https://addons.mozilla.org/en-US/firefox/addon/purpleadblock/) / [userscript](https://raw.githubusercontent.com/arthurbolsoni/Purple-adblock/refs/heads/main/platform/tampermonkey/dist/purpleadblocker.user.js) / [code](https://github.com/arthurbolsoni/Purple-adblock/)
|
||||||
- Replaces ad segments with ad-free segments. Proxy fallback which is currently broken. Loading wheel when all methods fail.
|
- Replaces ad segments with ad-free segments. Can use a proxy fallback. Removes ad segments if both methods fail (no playback until ad-free stream is found).
|
||||||
- `AdGuard Extra` - [chrome](https://chrome.google.com/webstore/detail/adguard-extra-beta/mglpocjcjbekdckiahfhagndealpkpbj) / [firefox](https://github.com/AdguardTeam/AdGuardExtra/#firefox) / [userscript](https://userscripts.adtidy.org/release/adguard-extra/1.0/adguard-extra.user.js)
|
- `AdGuard Extra` - [chrome](https://chrome.google.com/webstore/detail/adguard-extra-beta/mglpocjcjbekdckiahfhagndealpkpbj) / [firefox](https://github.com/AdguardTeam/AdGuardExtra/#firefox) / [userscript](https://userscripts.adtidy.org/release/adguard-extra/1.0/adguard-extra.user.js)
|
||||||
- Forked from one of the userscripts from this repo. Should behave similarly to `vaft` / `video-swap-new`.
|
- Forked from `vaft` (should behave similarly to `vaft`). Attempts to get a high resolution clean stream as fast as it can.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -20,6 +20,40 @@
|
|||||||
- `luminous-ttv` - [server code](https://github.com/AlyoshaVasilieva/luminous-ttv) / [extension code](https://github.com/AlyoshaVasilieva/luminous-ttv-ext)
|
- `luminous-ttv` - [server code](https://github.com/AlyoshaVasilieva/luminous-ttv) / [extension code](https://github.com/AlyoshaVasilieva/luminous-ttv-ext)
|
||||||
- Uses a proxy on the main m3u8 file to get a stream without ads.
|
- Uses a proxy on the main m3u8 file to get a stream without ads.
|
||||||
|
|
||||||
|
## Web browser scripts (userscript)
|
||||||
|
|
||||||
|
- https://greasyfork.org/en/scripts/371186-twitch-mute-ads-and-optionally-hide-them/code
|
||||||
|
- Mute ads and optionally hide them (tweak the settings at the top of the script).
|
||||||
|
- `vaft` - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js)
|
||||||
|
- Attempts to get a clean stream as fast as it can.
|
||||||
|
- If it fails to get a clean stream it removes ad segements (no playback until ad-free stream is found).
|
||||||
|
- `video-swap-new` - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js)
|
||||||
|
- Attempts to get a clean stream (often low resolution).
|
||||||
|
- If it fails to get a clean stream it removes ad segments (no playback until ad-free stream is found).
|
||||||
|
- Not recommended, `vaft` is a better script.
|
||||||
|
- `strip` - [userscript](https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.js)
|
||||||
|
- Strips ad segments (no playback until ad-free stream is found).
|
||||||
|
- Not recommended, `vaft` is a better script.
|
||||||
|
|
||||||
|
## Applications / third party websites
|
||||||
|
- `streamlink` - [code](https://github.com/streamlink/streamlink) / [website](https://streamlink.github.io/streamlink-twitch-gui/)
|
||||||
|
- Removes ad segments (no playback until ad-free stream).
|
||||||
|
- Use [this](https://github.com/2bc4/streamlink-ttvlol) modified file for uninterrupted playback.
|
||||||
|
- `Xtra for Twitch` (fork) - [apks](https://github.com/crackededed/Xtra/releases) / [code](https://github.com/crackededed/Xtra)
|
||||||
|
- An alternate Twitch player for Android with extra features, including ad blocking. This currently only uses the TTV LOL API for proxying. However, TTV LOL itself no longer works, so entering a custom proxy URL in settings is required for ad-blocking capabilities. For example: `https://eu.luminous.dev/live/$channel?allow_source=true&allow_audio_only=true&fast_bread=true`
|
||||||
|
- `ReVanced` - [code](https://github.com/revanced)
|
||||||
|
- A collection of tools that allows you to patch Twitch and other Android apps such as YouTube to remove ads. ReVanced's Twitch patches use the TTV LOL and PurpleAdBlocker proxies (can be toggled between in settings). The setup is complicated, so anyone who doesn't want the hassle should just use Xtra instead.
|
||||||
|
- https://github.com/level3tjg/TwitchAdBlock
|
||||||
|
- Blocks Twitch ads on iOS
|
||||||
|
- https://reddit.com/r/Twitch/comments/kisdsy/i_did_a_little_test_regarding_ads_on_twitch_and/
|
||||||
|
- Some countries don't get ads. A simple VPN/VPS could be used to block ads by proxying the m3u8 without having to proxy all your traffic (just the initial m3u8).
|
||||||
|
|
||||||
|
## Proxy issues
|
||||||
|
|
||||||
|
Proxy solutions can have downtime and you'll either see ads or error 2000. This isn't Twitch retaliating.
|
||||||
|
|
||||||
|
Buffering may occur at higher resolutions. This happens because traffic comes from a Twitch server closest to the initial m3u8 proxy request. The only solution to this is asking the proxy maintainer to add a proxy in a country closer to you. If they wont then you'll need to use a lower resolution, or use a different ad blocking solution. A VPN might also be better solution for you.
|
||||||
|
|
||||||
## Web browser extensions (unmaintained)
|
## Web browser extensions (unmaintained)
|
||||||
|
|
||||||
- `TTV LOL` - [chrome](https://chrome.google.com/webstore/detail/ttv-lol/ofbbahodfeppoklmgjiokgfdgcndngjm) / [code](https://github.com/TTV-LOL/extensions)
|
- `TTV LOL` - [chrome](https://chrome.google.com/webstore/detail/ttv-lol/ofbbahodfeppoklmgjiokgfdgcndngjm) / [code](https://github.com/TTV-LOL/extensions)
|
||||||
@@ -31,30 +65,7 @@
|
|||||||
- `ttv-tools` - [firefox (manual install)](https://github.com/Nerixyz/ttv-tools/releases) / [code](https://github.com/Nerixyz/ttv-tools)
|
- `ttv-tools` - [firefox (manual install)](https://github.com/Nerixyz/ttv-tools/releases) / [code](https://github.com/Nerixyz/ttv-tools)
|
||||||
- Removes ad segments (no playback until ad-free stream).
|
- Removes ad segments (no playback until ad-free stream).
|
||||||
|
|
||||||
## Web browser scripts (userscript)
|
## Web browser scripts (userscript) (unmaintained)
|
||||||
|
|
||||||
- https://greasyfork.org/en/scripts/371186-twitch-mute-ads-and-optionally-hide-them/code
|
|
||||||
- Mute ads and optionally hide them (tweak the settings at the top of the script).
|
|
||||||
- https://greasyfork.org/en/scripts/415412-twitch-refresh-on-advert/code
|
- https://greasyfork.org/en/scripts/415412-twitch-refresh-on-advert/code
|
||||||
- Reloads the player (or page) when it detects the ad banner in DOM.
|
- Reloads the player (or page) when it detects the ad banner in DOM.
|
||||||
|
|
||||||
## Applications / third party websites
|
|
||||||
- `streamlink` - [code](https://github.com/streamlink/streamlink) / [website](https://streamlink.github.io/streamlink-twitch-gui/)
|
|
||||||
- Removes ad segments (no playback until ad-free stream).
|
|
||||||
- Use [this](https://github.com/2bc4/streamlink-ttvlol) modified file for uninterrupted playback.
|
|
||||||
- `Xtra for Twitch` (fork) - [apks](https://github.com/crackededed/Xtra/releases) / [code](https://github.com/crackededed/Xtra)
|
|
||||||
- An alternate Twitch player for Android with extra features, including ad blocking. This currently only uses the TTV LOL API for proxying. However, TTV LOL itself no longer works, so entering a custom proxy URL in settings is required for ad-blocking capabilities. For example: `https://eu.luminous.dev/live/$channel?allow_source=true&allow_audio_only=true&fast_bread=true`
|
|
||||||
- `ReVanced` - [code](https://github.com/revanced)
|
|
||||||
- A collection of tools that allows you to patch Twitch and other Android apps such as YouTube to remove ads. ReVanced's Twitch patches use the TTV LOL and PurpleAdBlocker proxies (can be toggled between in settings). The setup is complicated, so anyone who doesn't want the hassle should just use Xtra instead.
|
|
||||||
- https://github.com/level3tjg/TwitchAdBlock
|
|
||||||
- Blocks Twitch ads on iOS
|
|
||||||
- https://twitchls.com/
|
|
||||||
- Uses the `embed` player. Purple screen may display every 10-15 mins.
|
|
||||||
- https://reddit.com/r/Twitch/comments/kisdsy/i_did_a_little_test_regarding_ads_on_twitch_and/
|
|
||||||
- Some countries don't get ads. A simple VPN/VPS could be used to block ads by proxying the m3u8 without having to proxy all your traffic (just the initial m3u8).
|
|
||||||
|
|
||||||
## Proxy issues
|
|
||||||
|
|
||||||
Proxy solutions can have downtime and you'll either see ads or error 2000. This isn't Twitch retaliating.
|
|
||||||
|
|
||||||
Buffering may occur at higher resolutions. This happens because traffic comes from a Twitch server closest to the initial m3u8 proxy request. The only solution to this is asking the proxy maintainer to add a proxy in a country closer to you. If they wont then you'll need to use a lower resolution, or use a different ad blocking solution. A VPN might also be better solution for you.
|
|
||||||
|
|||||||
@@ -6,37 +6,47 @@ If you're using the uBlock Origin version of the script you need to make sure th
|
|||||||
|
|
||||||
## Streams sometimes appear offline when ads occur
|
## Streams sometimes appear offline when ads occur
|
||||||
|
|
||||||
This needs to be fixed but currently the exact cause is unknown.
|
This needs to be fixed but currently the exact cause is unknown. https://github.com/pixeltris/TwitchAdSolutions/issues/477
|
||||||
|
|
||||||
## The scripts don't work on mobile (m.twitch.tv)
|
## The player shows a loading wheel for a long time
|
||||||
|
|
||||||
|
If it says `Blocking ads (stripping)` in the top left of the stream then it's actively removing the ad segments but doesn't have a backup stream to show you. If it doesn't say `Blocking ads (stripping)` provide additional information in https://github.com/pixeltris/TwitchAdSolutions/issues/474
|
||||||
|
|
||||||
|
## The script don't work on mobile (m.twitch.tv)
|
||||||
|
|
||||||
There are no plans of implementing the scripts on m.twitch.tv but there are other solutions which blocking ads on Twitch for mobile. See https://github.com/pixeltris/TwitchAdSolutions/blob/master/full-list.md
|
There are no plans of implementing the scripts on m.twitch.tv but there are other solutions which blocking ads on Twitch for mobile. See https://github.com/pixeltris/TwitchAdSolutions/blob/master/full-list.md
|
||||||
|
|
||||||
|
## Long black screen during ads
|
||||||
|
|
||||||
|
The scripts can reload the player when entering/leaving ads. On some systems this may cause a long period of time where the player is black as the player is loading back in. There currently isn't any fix for this. Try a different script / solution.
|
||||||
|
|
||||||
## `vaft`
|
## `vaft`
|
||||||
|
|
||||||
### Freezing / buffering
|
### Freezing / buffering / repeating segments / audio desyncs
|
||||||
|
|
||||||
The stream may have playback problems during ads. There is an integrated solution that attempts to automatically press pause/play if buffering occurs during ads. If this isn't working you could try changing `AlwaysReloadPlayerOnAd` from `false` to `true` which will trigger a player reload as it enters/leaves ads which may add some stability. If neither of those are working how you'd like you can disable the pause/play buffer fixing attempt by modifying the userscript and change `FixPlayerBufferingInsideAds` from `true` to `false`. You then need to find an alternative solution to the buffering such as the folloing:
|
`vaft` has a long standing problem of playback problems (freezing / buffering / repeating segments / audio desyncs).
|
||||||
|
|
||||||
- https://github.com/pixeltris/TwitchAdSolutions/issues/299#issuecomment-2509500697
|
This happens because the script forces the player to consume multiple different m3u8 files during ads and it screws with the player. Simply pressing pause/play often fixes this.
|
||||||
- https://github.com/pixeltris/TwitchAdSolutions/issues/313
|
|
||||||
|
|
||||||
If you're still having problems it's recommended to switch to using `video-swap-new` (which is the recommended script in the README).
|
The script is configured to do this automatically for you:
|
||||||
|
|
||||||
### Audio desyncs
|
```js
|
||||||
|
PlayerBufferingFix = true;// If true this will pause/play the player when it gets stuck buffering
|
||||||
|
PlayerBufferingDelay = 500;// How often should we check the player state (in milliseconds)
|
||||||
|
PlayerBufferingSameStateCount = 3;// How many times of seeing the same player state until we trigger pause/play (it will only trigger it one time until the player state changes again)
|
||||||
|
PlayerBufferingDangerZone = 1;// The buffering time left (in seconds) when we should ignore the players playback position in the player state check
|
||||||
|
PlayerBufferingDoPlayerReload = false;// If true this will do a player reload instead of pause/play (player reloading is better at fixing the playback issues but it takes slightly longer)
|
||||||
|
PlayerBufferingMinRepeatDelay = 5000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems)
|
||||||
|
```
|
||||||
|
|
||||||
This script can be susceptible to audio desyncs due to the way that it works. There currently isn't any fix for this. Try a different script / solution.
|
- If it triggers but it still freezes try setting `PlayerBufferingDoPlayerReload` to `true`. Player reloads generally have less problems.
|
||||||
|
- If you're having issues with it triggering when the player is genuinely buffering then adjust `PlayerBufferingDelay` / `PlayerBufferingSameStateCount`.
|
||||||
### Freezing / buffering after ads
|
- If it triggers too frequently then increase the value of `PlayerBufferingMinRepeatDelay`. Decrease this if it triggers, freezes, then has to wait a long time to re-trigger.
|
||||||
|
- If you don't want to use this and would like to fix the buffering manually yourself you can set `PlayerBufferingFix` to `false`.
|
||||||
You can try changing `FixPlayerBufferingOutsideAds` from `false` to `true` which will scan the player for buffering when ads aren't happening and will trigger a pause/play which may work for you.
|
- Setting `AlwaysReloadPlayerOnAd` to `true` may reduce freezing issues when entering into ads.
|
||||||
|
|
||||||
## `video-swap-new`
|
## `video-swap-new`
|
||||||
|
|
||||||
### Long black screen during ads
|
|
||||||
|
|
||||||
The script reloads the player as it enters/leaves ads to switch the active m3u8 file that is being used. On some systems this may cause a long period of time where the player is black as the player is loading back in. There currently isn't any fix for this. Try a different script / solution. *This also applies to `vaft` where the streamer has a 2k/4k quality setting as a player reload is required to handle these.*
|
|
||||||
|
|
||||||
### Freezing / buffering during ads
|
### Freezing / buffering during ads
|
||||||
|
|
||||||
Generally `video-swap-new` should be less susceptible to buffering / freezing than `vaft` as all it does is switch the active m3u8 file as it enters / leaves ads. There currently isn't a fix for freezing / buffering issues for `video-swap-new`. Try a different script / solution.
|
There currently isn't a fix for freezing / buffering issues for `video-swap-new`. Try a different script / solution.
|
||||||
|
|||||||
@@ -0,0 +1,537 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name TwitchAdSolutions (strip) - BAD, DON'T USE THIS
|
||||||
|
// @namespace https://github.com/pixeltris/TwitchAdSolutions
|
||||||
|
// @version 1.0
|
||||||
|
// @description Multiple solutions for blocking Twitch ads (strip)
|
||||||
|
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.user.js
|
||||||
|
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.user.js
|
||||||
|
// @author pixeltris
|
||||||
|
// @match *://*.twitch.tv/*
|
||||||
|
// @run-at document-start
|
||||||
|
// @inject-into page
|
||||||
|
// @grant none
|
||||||
|
// ==/UserScript==
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
|
||||||
|
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
||||||
|
console.log("skipping strip as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
||||||
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
|
function declareOptions(scope) {
|
||||||
|
scope.ReloadPlayerAfterAds = false;// Doesn't seem to help with high latency
|
||||||
|
scope.ReloadPlayerDelay = 10000;
|
||||||
|
scope.LastPlayerReload = 0;
|
||||||
|
scope.ForceAccessTokenPlayerType = 'site';
|
||||||
|
scope.DisableMatureConentPopup = false;// If true this avoids having to log in to watch age gated content
|
||||||
|
scope.AdSegmentCache = new Map();
|
||||||
|
scope.AllSegmentsAreAdSegments = false;
|
||||||
|
scope.NumStrippedSegments = 0;
|
||||||
|
scope.M3U8Whitelist = new Set();
|
||||||
|
scope.M3U8ChannelCache = new Map();
|
||||||
|
scope.V2API = false;
|
||||||
|
}
|
||||||
|
let localStorageHookFailed = false;
|
||||||
|
let adBlockDiv = null;
|
||||||
|
const twitchWorkers = [];
|
||||||
|
const workerStringConflicts = [
|
||||||
|
'twitch',
|
||||||
|
'isVariantA'// TwitchNoSub
|
||||||
|
];
|
||||||
|
const workerStringAllow = [];
|
||||||
|
const workerStringReinsert = [
|
||||||
|
'isVariantA',// TwitchNoSub (prior to (0.9))
|
||||||
|
'besuper/',// TwitchNoSub (0.9)
|
||||||
|
'${patch_url}'// TwitchNoSub (0.9.1)
|
||||||
|
];
|
||||||
|
function getCleanWorker(worker) {
|
||||||
|
let root = null;
|
||||||
|
let parent = null;
|
||||||
|
let proto = worker;
|
||||||
|
while (proto) {
|
||||||
|
const workerString = proto.toString();
|
||||||
|
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
||||||
|
if (parent !== null) {
|
||||||
|
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (root === null) {
|
||||||
|
root = proto;
|
||||||
|
}
|
||||||
|
parent = proto;
|
||||||
|
}
|
||||||
|
proto = Object.getPrototypeOf(proto);
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
function getWorkersForReinsert(worker) {
|
||||||
|
const result = [];
|
||||||
|
let proto = worker;
|
||||||
|
while (proto) {
|
||||||
|
const workerString = proto.toString();
|
||||||
|
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
||||||
|
result.push(proto);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
proto = Object.getPrototypeOf(proto);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
function reinsertWorkers(worker, reinsert) {
|
||||||
|
let parent = worker;
|
||||||
|
for (let i = 0; i < reinsert.length; i++) {
|
||||||
|
Object.setPrototypeOf(reinsert[i], parent);
|
||||||
|
parent = reinsert[i];
|
||||||
|
}
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
function isValidWorker(worker) {
|
||||||
|
const workerString = worker.toString();
|
||||||
|
return !workerStringConflicts.some((x) => workerString.includes(x))
|
||||||
|
|| workerStringAllow.some((x) => workerString.includes(x))
|
||||||
|
|| workerStringReinsert.some((x) => workerString.includes(x));
|
||||||
|
}
|
||||||
|
function hookWindowWorker() {
|
||||||
|
const reinsert = getWorkersForReinsert(window.Worker);
|
||||||
|
const newWorker = class Worker extends getCleanWorker(window.Worker) {
|
||||||
|
constructor(twitchBlobUrl, options) {
|
||||||
|
let isTwitchWorker = false;
|
||||||
|
try {
|
||||||
|
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
||||||
|
} catch {}
|
||||||
|
if (!isTwitchWorker) {
|
||||||
|
super(twitchBlobUrl, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newBlobStr = `
|
||||||
|
${stripAdSegments.toString()}
|
||||||
|
${hookWorkerFetch.toString()}
|
||||||
|
${declareOptions.toString()}
|
||||||
|
${getWasmWorkerJs.toString()}
|
||||||
|
${getServerTimeFromM3u8.toString()}
|
||||||
|
${replaceServerTimeInM3u8.toString()}
|
||||||
|
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
||||||
|
declareOptions(self);
|
||||||
|
self.addEventListener('message', function(e) {
|
||||||
|
if (e.data.key == 'AllSegmentsAreAdSegments') {
|
||||||
|
AllSegmentsAreAdSegments = !AllSegmentsAreAdSegments;
|
||||||
|
console.log('AllSegmentsAreAdSegments: ' + AllSegmentsAreAdSegments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hookWorkerFetch();
|
||||||
|
eval(workerString);
|
||||||
|
`;
|
||||||
|
super(URL.createObjectURL(new Blob([newBlobStr])), options);
|
||||||
|
twitchWorkers.push(this);
|
||||||
|
this.addEventListener('message', (e) => {
|
||||||
|
if (e.data.key == 'ReloadPlayer') {
|
||||||
|
reloadTwitchPlayer();
|
||||||
|
} else if (e.data.key == 'UpdateAdBlockBannerStripping') {
|
||||||
|
if (adBlockDiv == null) {
|
||||||
|
adBlockDiv = getAdBlockDiv();
|
||||||
|
}
|
||||||
|
if (adBlockDiv != null) {
|
||||||
|
if (e.data.isStrippingAdSegments) {
|
||||||
|
adBlockDiv.P.textContent = 'Stripping' + (e.data.isMidroll ? ' midroll' : '') + ' ads' + (e.data.numStrippedAdSegments > 0 ? ` (${e.data.numStrippedAdSegments})` : '');
|
||||||
|
adBlockDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
adBlockDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function getAdBlockDiv() {
|
||||||
|
//To display a notification to the user, that an ad is being blocked.
|
||||||
|
const playerRootDiv = document.querySelector('.video-player');
|
||||||
|
let adBlockDiv = null;
|
||||||
|
if (playerRootDiv != null) {
|
||||||
|
adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
|
||||||
|
if (adBlockDiv == null) {
|
||||||
|
adBlockDiv = document.createElement('div');
|
||||||
|
adBlockDiv.className = 'adblock-overlay';
|
||||||
|
adBlockDiv.innerHTML = '<div class="player-adblock-notice" style="color: white; background-color: rgba(0, 0, 0, 0.8); position: absolute; top: 0px; left: 0px; padding: 5px;"><p></p></div>';
|
||||||
|
adBlockDiv.style.display = 'none';
|
||||||
|
adBlockDiv.P = adBlockDiv.querySelector('p');
|
||||||
|
playerRootDiv.appendChild(adBlockDiv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return adBlockDiv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let workerInstance = reinsertWorkers(newWorker, reinsert);
|
||||||
|
Object.defineProperty(window, 'Worker', {
|
||||||
|
get: function() {
|
||||||
|
return workerInstance;
|
||||||
|
},
|
||||||
|
set: function(value) {
|
||||||
|
if (isValidWorker(value)) {
|
||||||
|
workerInstance = value;
|
||||||
|
} else {
|
||||||
|
console.log('Attempt to set twitch worker denied');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getWasmWorkerJs(twitchBlobUrl) {
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.open('GET', twitchBlobUrl, false);
|
||||||
|
req.overrideMimeType("text/javascript");
|
||||||
|
req.send();
|
||||||
|
return req.responseText;
|
||||||
|
}
|
||||||
|
function hookWorkerFetch() {
|
||||||
|
console.log('hookWorkerFetch (strip)');
|
||||||
|
const realFetch = fetch;
|
||||||
|
fetch = async function(url, options) {
|
||||||
|
if (typeof url === 'string') {
|
||||||
|
if (AdSegmentCache.has(url)) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const processAfter = async function(response) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
resolve(await realFetch('data:video/mp4;base64,AAAAKGZ0eXBtcDQyAAAAAWlzb21tcDQyZGFzaGF2YzFpc282aGxzZgAABEltb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAYagAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAABqHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAURtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAADvbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAACzc3RibAAAAGdzdHNkAAAAAAAAAAEAAABXbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAAzZXNkcwAAAAADgICAIgABAASAgIAUQBUAAAAAAAAAAAAAAAWAgIACEZAGgICAAQIAAAAQc3R0cwAAAAAAAAAAAAAAEHN0c2MAAAAAAAAAAAAAABRzdHN6AAAAAAAAAAAAAAAAAAAAEHN0Y28AAAAAAAAAAAAAAeV0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAoAAAAFoAAAAAAGBbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAA9CQAAAAABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABLG1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAOxzdGJsAAAAoHN0c2QAAAAAAAAAAQAAAJBhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAoABaABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAOmF2Y0MBTUAe/+EAI2dNQB6WUoFAX/LgLUBAQFAAAD6AAA6mDgAAHoQAA9CW7y4KAQAEaOuPIAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAASG12ZXgAAAAgdHJleAAAAAAAAAABAAAAAQAAAC4AAAAAAoAAAAAAACB0cmV4AAAAAAAAAAIAAAABAACCNQAAAAACQAAA'));
|
||||||
|
} else {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const send = function() {
|
||||||
|
return realFetch(url, options).then(function(response) {
|
||||||
|
processAfter(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (M3U8Whitelist.has(url)) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const processAfter = async function(response) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
resolve(new Response(stripAdSegments(await response.text())));
|
||||||
|
} else {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const send = function() {
|
||||||
|
return realFetch(url, options).then(function(response) {
|
||||||
|
processAfter(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
} else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
||||||
|
V2API = url.includes('/api/v2/');
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const processAfter = async function(response) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
const channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||||
|
let encodingsM3u8 = await response.text();
|
||||||
|
const cachedM3u8 = M3U8ChannelCache.get(channelName);
|
||||||
|
if (cachedM3u8 && (await realFetch(cachedM3u8.match(/^https:.*\.m3u8$/m)[0])).status === 200) {
|
||||||
|
encodingsM3u8 = replaceServerTimeInM3u8(cachedM3u8, getServerTimeFromM3u8(encodingsM3u8));
|
||||||
|
} else if (encodingsM3u8.includes('.m3u8')) {
|
||||||
|
M3U8ChannelCache.set(channelName, encodingsM3u8);
|
||||||
|
}
|
||||||
|
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
|
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
||||||
|
M3U8Whitelist.add(lines[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(new Response(encodingsM3u8));
|
||||||
|
} else {
|
||||||
|
resolve(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const send = function() {
|
||||||
|
return realFetch(url, options).then(function(response) {
|
||||||
|
processAfter(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return realFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getServerTimeFromM3u8(encodingsM3u8) {
|
||||||
|
if (V2API) {
|
||||||
|
const matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
||||||
|
return matches.length > 1 ? matches[1] : null;
|
||||||
|
}
|
||||||
|
const matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
||||||
|
return matches.length > 1 ? matches[1] : null;
|
||||||
|
}
|
||||||
|
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
||||||
|
if (V2API) {
|
||||||
|
return newServerTime ? encodingsM3u8.replace(/(#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE=")[^"]+(")/, `$1${newServerTime}$2`) : encodingsM3u8;
|
||||||
|
}
|
||||||
|
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
||||||
|
}
|
||||||
|
function stripAdSegments(textStr, stripAllSegments) {
|
||||||
|
let hasStrippedAdSegments = false;
|
||||||
|
let isMidroll = false;
|
||||||
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
|
const newAdUrl = 'https://twitch.tv';
|
||||||
|
let isLastSegmentLive = false;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i];
|
||||||
|
// Remove tracking urls which appear in the overlay UI
|
||||||
|
line = line
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`)
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
|
||||||
|
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
|
||||||
|
const segmentUrl = lines[i + 1];
|
||||||
|
if (!AdSegmentCache.has(segmentUrl)) {
|
||||||
|
NumStrippedSegments++;
|
||||||
|
}
|
||||||
|
AdSegmentCache.set(segmentUrl, Date.now());
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
if (line.startsWith('#EXTINF')) {
|
||||||
|
isLastSegmentLive = line.includes(',live');
|
||||||
|
}
|
||||||
|
if (line.includes('stitched-ad')) {
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
isLastSegmentLive = false;
|
||||||
|
}
|
||||||
|
if (line.includes('"MIDROLL"') || line.includes('"midroll"')) {
|
||||||
|
isMidroll = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isLastSegmentLive) {
|
||||||
|
// NOTE: If the player position is far behind the buffer position it'll hide the banner before playback
|
||||||
|
hasStrippedAdSegments = false;
|
||||||
|
}
|
||||||
|
if (hasStrippedAdSegments) {
|
||||||
|
// No low latency during ads (otherwise it's possible for the player to prefetch and display ad segments)
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith('#EXT-X-TWITCH-PREFETCH:')) {
|
||||||
|
lines[i] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (NumStrippedSegments > 0 && ReloadPlayerAfterAds) {
|
||||||
|
postMessage({key:'ReloadPlayer'});
|
||||||
|
}
|
||||||
|
NumStrippedSegments = 0;
|
||||||
|
}
|
||||||
|
AdSegmentCache.forEach((key, value, map) => {
|
||||||
|
if (value < Date.now() - 120000) {
|
||||||
|
map.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
postMessage({
|
||||||
|
key: 'UpdateAdBlockBannerStripping',
|
||||||
|
isStrippingAdSegments: hasStrippedAdSegments,
|
||||||
|
numStrippedAdSegments: NumStrippedSegments,
|
||||||
|
isMidroll: isMidroll
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
function hookFetch() {
|
||||||
|
const realFetch = window.fetch;
|
||||||
|
window.realFetch = realFetch;
|
||||||
|
window.fetch = function(url, init, ...args) {
|
||||||
|
if (typeof url === 'string') {
|
||||||
|
if (url.includes('gql')) {
|
||||||
|
if (ForceAccessTokenPlayerType && typeof init.body === 'string' && init.body.includes('PlaybackAccessToken') && !init.body.includes('picture-by-picture') && !init.body.includes('frontpage')) {
|
||||||
|
let replacedPlayerType = '';
|
||||||
|
const newBody = JSON.parse(init.body);
|
||||||
|
if (Array.isArray(newBody)) {
|
||||||
|
for (let i = 0; i < newBody.length; i++) {
|
||||||
|
if (newBody[i]?.variables?.playerType && newBody[i]?.variables?.playerType !== ForceAccessTokenPlayerType) {
|
||||||
|
replacedPlayerType = newBody[i].variables.playerType;
|
||||||
|
newBody[i].variables.playerType = ForceAccessTokenPlayerType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (newBody?.variables?.playerType && newBody?.variables?.playerType !== ForceAccessTokenPlayerType) {
|
||||||
|
replacedPlayerType = newBody.variables.playerType;
|
||||||
|
newBody.variables.playerType = ForceAccessTokenPlayerType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (replacedPlayerType) {
|
||||||
|
console.log(`Replaced '${replacedPlayerType}' player type with '${ForceAccessTokenPlayerType}' player type`);
|
||||||
|
init.body = JSON.stringify(newBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (DisableMatureConentPopup) {
|
||||||
|
const newBody2 = JSON.parse(init.body);
|
||||||
|
if (Array.isArray(newBody2)) {
|
||||||
|
let hasRemovedClassification = false;
|
||||||
|
for (let i = 0; i < newBody2.length; i++) {
|
||||||
|
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
||||||
|
hasRemovedClassification = true;
|
||||||
|
// Doesn't seem like it if we remove this element from the array so instead we duplicate another entry into this index. TODO: Find out why
|
||||||
|
newBody2[i] = newBody2[i == 0 && newBody2.length > 1 ? 1 : 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasRemovedClassification) {
|
||||||
|
init.body = JSON.stringify(newBody2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return realFetch.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function reloadTwitchPlayer(isPausePlay) {
|
||||||
|
// Taken from ttv-tools / ffz
|
||||||
|
// https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
|
||||||
|
// https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
|
||||||
|
function findReactNode(root, constraint) {
|
||||||
|
if (root.stateNode && constraint(root.stateNode)) {
|
||||||
|
return root.stateNode;
|
||||||
|
}
|
||||||
|
let node = root.child;
|
||||||
|
while (node) {
|
||||||
|
const result = findReactNode(node, constraint);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
node = node.sibling;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function findReactRootNode() {
|
||||||
|
let reactRootNode = null;
|
||||||
|
const rootNode = document.querySelector('#root');
|
||||||
|
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
||||||
|
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
||||||
|
}
|
||||||
|
if (reactRootNode == null) {
|
||||||
|
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
||||||
|
if (containerName != null) {
|
||||||
|
reactRootNode = rootNode[containerName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reactRootNode;
|
||||||
|
}
|
||||||
|
const reactRootNode = findReactRootNode();
|
||||||
|
if (!reactRootNode) {
|
||||||
|
console.log('Could not find react root');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
||||||
|
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
||||||
|
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
||||||
|
if (!player) {
|
||||||
|
console.log('Could not find player');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!playerState) {
|
||||||
|
console.log('Could not find player state');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (player.paused || player.core?.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isPausePlay) {
|
||||||
|
player.pause();
|
||||||
|
player.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (LastPlayerReload > Date.now() - ReloadPlayerDelay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LastPlayerReload = Date.now();
|
||||||
|
const lsKeyQuality = 'video-quality';
|
||||||
|
const lsKeyMuted = 'video-muted';
|
||||||
|
const lsKeyVolume = 'volume';
|
||||||
|
let currentQualityLS = null;
|
||||||
|
let currentMutedLS = null;
|
||||||
|
let currentVolumeLS = null;
|
||||||
|
try {
|
||||||
|
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
||||||
|
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
||||||
|
currentVolumeLS = localStorage.getItem(lsKeyVolume);
|
||||||
|
if (localStorageHookFailed && player?.core?.state) {
|
||||||
|
localStorage.setItem(lsKeyMuted, JSON.stringify({default:player.core.state.muted}));
|
||||||
|
localStorage.setItem(lsKeyVolume, player.core.state.volume);
|
||||||
|
}
|
||||||
|
if (localStorageHookFailed && player?.core?.state?.quality?.group) {
|
||||||
|
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
console.log('Reloading Twitch player');
|
||||||
|
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
|
||||||
|
player.play();
|
||||||
|
if (localStorageHookFailed && (currentQualityLS || currentMutedLS || currentVolumeLS)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
if (currentQualityLS) {
|
||||||
|
localStorage.setItem(lsKeyQuality, currentQualityLS);
|
||||||
|
}
|
||||||
|
if (currentMutedLS) {
|
||||||
|
localStorage.setItem(lsKeyMuted, currentMutedLS);
|
||||||
|
}
|
||||||
|
if (currentVolumeLS) {
|
||||||
|
localStorage.setItem(lsKeyVolume, currentVolumeLS);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onContentLoaded() {
|
||||||
|
// Hooks for preserving volume / resolution
|
||||||
|
try {
|
||||||
|
const keysToCache = [
|
||||||
|
'video-quality',
|
||||||
|
'video-muted',
|
||||||
|
'volume',
|
||||||
|
'lowLatencyModeEnabled',// Low Latency
|
||||||
|
'persistenceEnabled',// Mini Player
|
||||||
|
];
|
||||||
|
const cachedValues = new Map();
|
||||||
|
for (let i = 0; i < keysToCache.length; i++) {
|
||||||
|
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
||||||
|
}
|
||||||
|
const realSetItem = localStorage.setItem;
|
||||||
|
localStorage.setItem = function(key, value) {
|
||||||
|
if (cachedValues.has(key)) {
|
||||||
|
cachedValues.set(key, value);
|
||||||
|
}
|
||||||
|
realSetItem.apply(this, arguments);
|
||||||
|
};
|
||||||
|
const realGetItem = localStorage.getItem;
|
||||||
|
localStorage.getItem = function(key) {
|
||||||
|
if (cachedValues.has(key)) {
|
||||||
|
return cachedValues.get(key);
|
||||||
|
}
|
||||||
|
return realGetItem.apply(this, arguments);
|
||||||
|
};
|
||||||
|
if (!localStorage.getItem.toString().includes(Object.keys({cachedValues})[0])) {
|
||||||
|
// These hooks are useful to preserve player state on player reload
|
||||||
|
// Firefox doesn't allow hooking of localStorage functions but chrome does
|
||||||
|
localStorageHookFailed = true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('localStorageHooks failed ' + err)
|
||||||
|
localStorageHookFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.reloadTwitchPlayer = reloadTwitchPlayer;
|
||||||
|
window.allSegmentsAreAdSegments = () => {
|
||||||
|
postTwitchWorkerMessage('AllSegmentsAreAdSegments');
|
||||||
|
};
|
||||||
|
declareOptions(window);
|
||||||
|
hookWindowWorker();
|
||||||
|
hookFetch();
|
||||||
|
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
|
||||||
|
onContentLoaded();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("DOMContentLoaded", function() {
|
||||||
|
onContentLoaded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
+306
-203
@@ -2,7 +2,7 @@ twitch-videoad.js text/javascript
|
|||||||
(function() {
|
(function() {
|
||||||
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
|
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
|
||||||
'use strict';
|
'use strict';
|
||||||
var ourTwitchAdSolutionsVersion = 16;// Used to prevent conflicts with outdated versions of the scripts
|
const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
|
||||||
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
||||||
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
||||||
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
@@ -15,13 +15,17 @@ twitch-videoad.js text/javascript
|
|||||||
scope.BackupPlayerTypes = [
|
scope.BackupPlayerTypes = [
|
||||||
'embed',//Source
|
'embed',//Source
|
||||||
'site',//Source
|
'site',//Source
|
||||||
'autoplay'//360p
|
'autoplay',//360p
|
||||||
|
'picture-by-picture-CACHED'//360p (-CACHED is an internal suffix and is removed)
|
||||||
];
|
];
|
||||||
scope.FallbackPlayerType = 'embed';
|
scope.FallbackPlayerType = 'embed';
|
||||||
scope.ForceAccessTokenPlayerType = 'site';
|
scope.ForceAccessTokenPlayerType = 'site';
|
||||||
scope.SkipPlayerReloadOnHevc = false;// If true this will skip player reload on streams which have 2k/4k quality (if you enable this and you use the 2k/4k quality setting you'll get error #4000 / #3000 / spinning wheel on chrome based browsers)
|
scope.SkipPlayerReloadOnHevc = false;// If true this will skip player reload on streams which have 2k/4k quality (if you enable this and you use the 2k/4k quality setting you'll get error #4000 / #3000 / spinning wheel on chrome based browsers)
|
||||||
scope.AlwaysReloadPlayerOnAd = false;
|
scope.AlwaysReloadPlayerOnAd = false;// Always pause/play when entering/leaving ads
|
||||||
scope.PlayerReloadLowResTime = 1500;
|
scope.ReloadPlayerAfterAd = true;// After the ad finishes do a player reload instead of pause/play
|
||||||
|
scope.PlayerReloadMinimalRequestsTime = 1500;
|
||||||
|
scope.PlayerReloadMinimalRequestsPlayerIndex = 0;//embed
|
||||||
|
scope.HasTriggeredPlayerReload = false;
|
||||||
scope.DisableMatureConentPopup = false;// If true this avoids having to log in to watch age gated content
|
scope.DisableMatureConentPopup = false;// If true this avoids having to log in to watch age gated content
|
||||||
scope.StreamInfos = [];
|
scope.StreamInfos = [];
|
||||||
scope.StreamInfosByUrl = [];
|
scope.StreamInfosByUrl = [];
|
||||||
@@ -31,33 +35,37 @@ twitch-videoad.js text/javascript
|
|||||||
scope.ClientIntegrityHeader = null;
|
scope.ClientIntegrityHeader = null;
|
||||||
scope.AuthorizationHeader = undefined;
|
scope.AuthorizationHeader = undefined;
|
||||||
scope.SimulatedAdsDepth = 0;
|
scope.SimulatedAdsDepth = 0;
|
||||||
scope.IsPlayerBuffering = false;
|
scope.PlayerBufferingFix = true;// If true this will pause/play the player when it gets stuck buffering
|
||||||
scope.LastPausePlay = 0;
|
scope.PlayerBufferingDelay = 500;// How often should we check the player state (in milliseconds)
|
||||||
scope.FixPlayerBufferingInsideAds = true;
|
scope.PlayerBufferingSameStateCount = 3;// How many times of seeing the same player state until we trigger pause/play (it will only trigger it one time until the player state changes again)
|
||||||
scope.FixPlayerBufferingOutsideAds = false;
|
scope.PlayerBufferingDangerZone = 1;// The buffering time left (in seconds) when we should ignore the players playback position in the player state check
|
||||||
scope.DelayBetweenEachPlayerFixBufferAttempt = 3000;
|
scope.PlayerBufferingDoPlayerReload = false;// If true this will do a player reload instead of pause/play (player reloading is better at fixing the playback issues but it takes slightly longer)
|
||||||
scope.ActiveStreamInfo = null;
|
scope.PlayerBufferingMinRepeatDelay = 5000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems)
|
||||||
scope.V2API = false;
|
scope.V2API = false;
|
||||||
|
scope.IsAdStrippingEnabled = true;
|
||||||
|
scope.AdSegmentCache = new Map();
|
||||||
|
scope.AllSegmentsAreAdSegments = false;
|
||||||
}
|
}
|
||||||
var localStorageHookFailed = false;
|
let isActivelyStrippingAds = false;
|
||||||
var twitchWorkers = [];
|
let localStorageHookFailed = false;
|
||||||
var adBlockDiv = null;
|
const twitchWorkers = [];
|
||||||
var workerStringConflicts = [
|
let adBlockDiv = null;
|
||||||
|
const workerStringConflicts = [
|
||||||
'twitch',
|
'twitch',
|
||||||
'isVariantA'// TwitchNoSub
|
'isVariantA'// TwitchNoSub
|
||||||
];
|
];
|
||||||
var workerStringAllow = [];
|
const workerStringAllow = [];
|
||||||
var workerStringReinsert = [
|
const workerStringReinsert = [
|
||||||
'isVariantA',// TwitchNoSub (prior to (0.9))
|
'isVariantA',// TwitchNoSub (prior to (0.9))
|
||||||
'besuper/',// TwitchNoSub (0.9)
|
'besuper/',// TwitchNoSub (0.9)
|
||||||
'${patch_url}'// TwitchNoSub (0.9.1)
|
'${patch_url}'// TwitchNoSub (0.9.1)
|
||||||
];
|
];
|
||||||
function getCleanWorker(worker) {
|
function getCleanWorker(worker) {
|
||||||
var root = null;
|
let root = null;
|
||||||
var parent = null;
|
let parent = null;
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
||||||
if (parent !== null) {
|
if (parent !== null) {
|
||||||
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
||||||
@@ -73,10 +81,10 @@ twitch-videoad.js text/javascript
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
function getWorkersForReinsert(worker) {
|
function getWorkersForReinsert(worker) {
|
||||||
var result = [];
|
const result = [];
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
||||||
result.push(proto);
|
result.push(proto);
|
||||||
} else {
|
} else {
|
||||||
@@ -86,24 +94,24 @@ twitch-videoad.js text/javascript
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
function reinsertWorkers(worker, reinsert) {
|
function reinsertWorkers(worker, reinsert) {
|
||||||
var parent = worker;
|
let parent = worker;
|
||||||
for (var i = 0; i < reinsert.length; i++) {
|
for (let i = 0; i < reinsert.length; i++) {
|
||||||
Object.setPrototypeOf(reinsert[i], parent);
|
Object.setPrototypeOf(reinsert[i], parent);
|
||||||
parent = reinsert[i];
|
parent = reinsert[i];
|
||||||
}
|
}
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
function isValidWorker(worker) {
|
function isValidWorker(worker) {
|
||||||
var workerString = worker.toString();
|
const workerString = worker.toString();
|
||||||
return !workerStringConflicts.some((x) => workerString.includes(x))
|
return !workerStringConflicts.some((x) => workerString.includes(x))
|
||||||
|| workerStringAllow.some((x) => workerString.includes(x))
|
|| workerStringAllow.some((x) => workerString.includes(x))
|
||||||
|| workerStringReinsert.some((x) => workerString.includes(x));
|
|| workerStringReinsert.some((x) => workerString.includes(x));
|
||||||
}
|
}
|
||||||
function hookWindowWorker() {
|
function hookWindowWorker() {
|
||||||
var reinsert = getWorkersForReinsert(window.Worker);
|
const reinsert = getWorkersForReinsert(window.Worker);
|
||||||
var newWorker = class Worker extends getCleanWorker(window.Worker) {
|
const newWorker = class Worker extends getCleanWorker(window.Worker) {
|
||||||
constructor(twitchBlobUrl, options) {
|
constructor(twitchBlobUrl, options) {
|
||||||
var isTwitchWorker = false;
|
let isTwitchWorker = false;
|
||||||
try {
|
try {
|
||||||
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -111,8 +119,9 @@ twitch-videoad.js text/javascript
|
|||||||
super(twitchBlobUrl, options);
|
super(twitchBlobUrl, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var newBlobStr = `
|
const newBlobStr = `
|
||||||
const pendingFetchRequests = new Map();
|
const pendingFetchRequests = new Map();
|
||||||
|
${stripAdSegments.toString()}
|
||||||
${getStreamUrlForResolution.toString()}
|
${getStreamUrlForResolution.toString()}
|
||||||
${processM3U8.toString()}
|
${processM3U8.toString()}
|
||||||
${hookWorkerFetch.toString()}
|
${hookWorkerFetch.toString()}
|
||||||
@@ -123,8 +132,7 @@ twitch-videoad.js text/javascript
|
|||||||
${getWasmWorkerJs.toString()}
|
${getWasmWorkerJs.toString()}
|
||||||
${getServerTimeFromM3u8.toString()}
|
${getServerTimeFromM3u8.toString()}
|
||||||
${replaceServerTimeInM3u8.toString()}
|
${replaceServerTimeInM3u8.toString()}
|
||||||
${tryFixPlayerBuffering.toString()}
|
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
||||||
var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
|
||||||
declareOptions(self);
|
declareOptions(self);
|
||||||
GQLDeviceID = ${GQLDeviceID ? "'" + GQLDeviceID + "'" : null};
|
GQLDeviceID = ${GQLDeviceID ? "'" + GQLDeviceID + "'" : null};
|
||||||
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
||||||
@@ -161,20 +169,14 @@ twitch-videoad.js text/javascript
|
|||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (e.data.key == 'TriggeredPlayerReload') {
|
||||||
|
HasTriggeredPlayerReload = true;
|
||||||
} else if (e.data.key == 'SimulateAds') {
|
} else if (e.data.key == 'SimulateAds') {
|
||||||
SimulatedAdsDepth = e.data.value;
|
SimulatedAdsDepth = e.data.value;
|
||||||
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
||||||
}
|
} else if (e.data.key == 'AllSegmentsAreAdSegments') {
|
||||||
if (e.data.funcName) {
|
AllSegmentsAreAdSegments = !AllSegmentsAreAdSegments;
|
||||||
if (e.data.funcName == 'onClientSinkBuffering') {
|
console.log('AllSegmentsAreAdSegments: ' + AllSegmentsAreAdSegments);
|
||||||
IsPlayerBuffering = true;
|
|
||||||
} else if (e.data.funcName == 'onClientSinkPlaying') {
|
|
||||||
IsPlayerBuffering = false;
|
|
||||||
LastPausePlay = Date.now();
|
|
||||||
} else if (e.data.funcName == 'playIntent' || e.data.funcName == 'play') {
|
|
||||||
LastPausePlay = Date.now();
|
|
||||||
}
|
|
||||||
tryFixPlayerBuffering();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hookWorkerFetch();
|
hookWorkerFetch();
|
||||||
@@ -183,25 +185,21 @@ twitch-videoad.js text/javascript
|
|||||||
super(URL.createObjectURL(new Blob([newBlobStr])), options);
|
super(URL.createObjectURL(new Blob([newBlobStr])), options);
|
||||||
twitchWorkers.push(this);
|
twitchWorkers.push(this);
|
||||||
this.addEventListener('message', (e) => {
|
this.addEventListener('message', (e) => {
|
||||||
if (e.data.key == 'ShowAdBlockBanner') {
|
if (e.data.key == 'UpdateAdBlockBanner') {
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
adBlockDiv = getAdBlockDiv();
|
adBlockDiv = getAdBlockDiv();
|
||||||
}
|
}
|
||||||
if (adBlockDiv != null) {
|
if (adBlockDiv != null) {
|
||||||
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
|
isActivelyStrippingAds = e.data.isStrippingAdSegments;
|
||||||
adBlockDiv.style.display = 'block';
|
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads' + (e.data.isStrippingAdSegments ? ' (stripping)' : '');
|
||||||
}
|
adBlockDiv.style.display = e.data.hasAds ? 'block' : 'none';
|
||||||
} else if (e.data.key == 'HideAdBlockBanner') {
|
|
||||||
if (adBlockDiv == null) {
|
|
||||||
adBlockDiv = getAdBlockDiv();
|
|
||||||
}
|
|
||||||
if (adBlockDiv != null) {
|
|
||||||
adBlockDiv.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
} else if (e.data.key == 'PauseResumePlayer') {
|
} else if (e.data.key == 'PauseResumePlayer') {
|
||||||
doTwitchPlayerTask(true, false);
|
doTwitchPlayerTask(true, false, false);
|
||||||
} else if (e.data.key == 'ReloadPlayer') {
|
} else if (e.data.key == 'ReloadPlayer') {
|
||||||
doTwitchPlayerTask(false, true);
|
doTwitchPlayerTask(false, true, false);
|
||||||
|
} else if (e.data.key == 'MonitorPlayerForBuffering') {
|
||||||
|
doTwitchPlayerTask(false, false, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.addEventListener('message', async event => {
|
this.addEventListener('message', async event => {
|
||||||
@@ -216,8 +214,8 @@ twitch-videoad.js text/javascript
|
|||||||
});
|
});
|
||||||
function getAdBlockDiv() {
|
function getAdBlockDiv() {
|
||||||
//To display a notification to the user, that an ad is being blocked.
|
//To display a notification to the user, that an ad is being blocked.
|
||||||
var playerRootDiv = document.querySelector('.video-player');
|
const playerRootDiv = document.querySelector('.video-player');
|
||||||
var adBlockDiv = null;
|
let adBlockDiv = null;
|
||||||
if (playerRootDiv != null) {
|
if (playerRootDiv != null) {
|
||||||
adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
|
adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
@@ -233,7 +231,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var workerInstance = reinsertWorkers(newWorker, reinsert);
|
let workerInstance = reinsertWorkers(newWorker, reinsert);
|
||||||
Object.defineProperty(window, 'Worker', {
|
Object.defineProperty(window, 'Worker', {
|
||||||
get: function() {
|
get: function() {
|
||||||
return workerInstance;
|
return workerInstance;
|
||||||
@@ -248,7 +246,7 @@ twitch-videoad.js text/javascript
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getWasmWorkerJs(twitchBlobUrl) {
|
function getWasmWorkerJs(twitchBlobUrl) {
|
||||||
var req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open('GET', twitchBlobUrl, false);
|
req.open('GET', twitchBlobUrl, false);
|
||||||
req.overrideMimeType("text/javascript");
|
req.overrideMimeType("text/javascript");
|
||||||
req.send();
|
req.send();
|
||||||
@@ -256,20 +254,32 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
function hookWorkerFetch() {
|
function hookWorkerFetch() {
|
||||||
console.log('hookWorkerFetch (vaft)');
|
console.log('hookWorkerFetch (vaft)');
|
||||||
var realFetch = fetch;
|
const realFetch = fetch;
|
||||||
fetch = async function(url, options) {
|
fetch = async function(url, options) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
|
if (AdSegmentCache.has(url)) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const send = function() {
|
||||||
|
return realFetch('data:video/mp4;base64,AAAAKGZ0eXBtcDQyAAAAAWlzb21tcDQyZGFzaGF2YzFpc282aGxzZgAABEltb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAYagAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAABqHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAURtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAADvbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAACzc3RibAAAAGdzdHNkAAAAAAAAAAEAAABXbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAAzZXNkcwAAAAADgICAIgABAASAgIAUQBUAAAAAAAAAAAAAAAWAgIACEZAGgICAAQIAAAAQc3R0cwAAAAAAAAAAAAAAEHN0c2MAAAAAAAAAAAAAABRzdHN6AAAAAAAAAAAAAAAAAAAAEHN0Y28AAAAAAAAAAAAAAeV0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAoAAAAFoAAAAAAGBbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAA9CQAAAAABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABLG1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAOxzdGJsAAAAoHN0c2QAAAAAAAAAAQAAAJBhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAoABaABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAOmF2Y0MBTUAe/+EAI2dNQB6WUoFAX/LgLUBAQFAAAD6AAA6mDgAAHoQAA9CW7y4KAQAEaOuPIAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAASG12ZXgAAAAgdHJleAAAAAAAAAABAAAAAQAAAC4AAAAAAoAAAAAAACB0cmV4AAAAAAAAAAIAAAABAACCNQAAAAACQAAA', options).then(function(response) {
|
||||||
|
resolve(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
}
|
||||||
url = url.trimEnd();
|
url = url.trimEnd();
|
||||||
if (url.endsWith('m3u8')) {
|
if (url.endsWith('m3u8')) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var processAfter = async function(response) {
|
const processAfter = async function(response) {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
resolve(new Response(await processM3U8(url, await response.text(), realFetch)));
|
resolve(new Response(await processM3U8(url, await response.text(), realFetch)));
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var send = function() {
|
const send = function() {
|
||||||
return realFetch(url, options).then(function(response) {
|
return realFetch(url, options).then(function(response) {
|
||||||
processAfter(response);
|
processAfter(response);
|
||||||
})['catch'](function(err) {
|
})['catch'](function(err) {
|
||||||
@@ -280,28 +290,30 @@ twitch-videoad.js text/javascript
|
|||||||
});
|
});
|
||||||
} else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
} else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
||||||
V2API = url.includes('/api/v2/');
|
V2API = url.includes('/api/v2/');
|
||||||
var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
const channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||||
if (ForceAccessTokenPlayerType) {
|
if (ForceAccessTokenPlayerType) {
|
||||||
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
||||||
var tempUrl = new URL(url);
|
const tempUrl = new URL(url);
|
||||||
tempUrl.searchParams.delete('parent_domains');
|
tempUrl.searchParams.delete('parent_domains');
|
||||||
url = tempUrl.toString();
|
url = tempUrl.toString();
|
||||||
}
|
}
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var processAfter = async function(response) {
|
const processAfter = async function(response) {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
var encodingsM3u8 = await response.text();
|
const encodingsM3u8 = await response.text();
|
||||||
var serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
const serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
||||||
var streamInfo = StreamInfos[channelName];
|
let streamInfo = StreamInfos[channelName];
|
||||||
|
let isNewStream = false;
|
||||||
if (streamInfo != null && streamInfo.EncodingsM3U8 != null && (await realFetch(streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
if (streamInfo != null && streamInfo.EncodingsM3U8 != null && (await realFetch(streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
||||||
// The cached encodings are dead (the stream probably restarted)
|
// The cached encodings are dead (the stream probably restarted)
|
||||||
streamInfo = null;
|
streamInfo = null;
|
||||||
}
|
}
|
||||||
if (streamInfo == null || streamInfo.EncodingsM3U8 == null) {
|
if (streamInfo == null || streamInfo.EncodingsM3U8 == null) {
|
||||||
|
isNewStream = true;
|
||||||
StreamInfos[channelName] = streamInfo = {
|
StreamInfos[channelName] = streamInfo = {
|
||||||
ChannelName: channelName,
|
ChannelName: channelName,
|
||||||
IsShowingAd: false,
|
IsShowingAd: false,
|
||||||
AdStartTime: 0,
|
LastPlayerReload: 0,
|
||||||
EncodingsM3U8: encodingsM3u8,
|
EncodingsM3U8: encodingsM3u8,
|
||||||
ModifiedM3U8: null,
|
ModifiedM3U8: null,
|
||||||
IsUsingModifiedM3U8: false,
|
IsUsingModifiedM3U8: false,
|
||||||
@@ -311,15 +323,16 @@ twitch-videoad.js text/javascript
|
|||||||
ResolutionList: [],
|
ResolutionList: [],
|
||||||
BackupEncodingsM3U8Cache: [],
|
BackupEncodingsM3U8Cache: [],
|
||||||
ActiveBackupPlayerType: null,
|
ActiveBackupPlayerType: null,
|
||||||
IsMidroll: false
|
IsMidroll: false,
|
||||||
|
IsStrippingAdSegments: false
|
||||||
};
|
};
|
||||||
var lines = encodingsM3u8.replace('\r', '').split('\n');
|
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length - 1; i++) {
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(lines[i]);
|
const attributes = parseAttributes(lines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
var resolutionInfo = {
|
const resolutionInfo = {
|
||||||
Resolution: resolution,
|
Resolution: resolution,
|
||||||
FrameRate: attributes['FRAME-RATE'],
|
FrameRate: attributes['FRAME-RATE'],
|
||||||
Codecs: attributes['CODECS'],
|
Codecs: attributes['CODECS'],
|
||||||
@@ -331,17 +344,17 @@ twitch-videoad.js text/javascript
|
|||||||
StreamInfosByUrl[lines[i + 1]] = streamInfo;
|
StreamInfosByUrl[lines[i + 1]] = streamInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var nonHevcResolutionList = streamInfo.ResolutionList.filter((element) => element.Codecs.startsWith('avc') || element.Codecs.startsWith('av0'));
|
const nonHevcResolutionList = streamInfo.ResolutionList.filter((element) => element.Codecs.startsWith('avc') || element.Codecs.startsWith('av0'));
|
||||||
if (AlwaysReloadPlayerOnAd || (nonHevcResolutionList.length > 0 && streamInfo.ResolutionList.some((element) => element.Codecs.startsWith('hev') || element.Codecs.startsWith('hvc')) && !SkipPlayerReloadOnHevc)) {
|
if (AlwaysReloadPlayerOnAd || (nonHevcResolutionList.length > 0 && streamInfo.ResolutionList.some((element) => element.Codecs.startsWith('hev') || element.Codecs.startsWith('hvc')) && !SkipPlayerReloadOnHevc)) {
|
||||||
if (nonHevcResolutionList.length > 0) {
|
if (nonHevcResolutionList.length > 0) {
|
||||||
for (var i = 0; i < lines.length - 1; i++) {
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
var resSettings = parseAttributes(lines[i].substring(lines[i].indexOf(':') + 1));
|
const resSettings = parseAttributes(lines[i].substring(lines[i].indexOf(':') + 1));
|
||||||
const codecsKey = 'CODECS';
|
const codecsKey = 'CODECS';
|
||||||
if (resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) {
|
if (resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) {
|
||||||
var oldResolution = resSettings['RESOLUTION'];
|
const oldResolution = resSettings['RESOLUTION'];
|
||||||
const [targetWidth, targetHeight] = oldResolution.split('x').map(Number);
|
const [targetWidth, targetHeight] = oldResolution.split('x').map(Number);
|
||||||
var newResolutionInfo = nonHevcResolutionList.sort((a, b) => {
|
const newResolutionInfo = nonHevcResolutionList.sort((a, b) => {
|
||||||
// TODO: Take into account 'Frame-Rate' when sorting (i.e. 1080p60 vs 1080p30)
|
// TODO: Take into account 'Frame-Rate' when sorting (i.e. 1080p60 vs 1080p30)
|
||||||
const [streamWidthA, streamHeightA] = a.Resolution.split('x').map(Number);
|
const [streamWidthA, streamHeightA] = a.Resolution.split('x').map(Number);
|
||||||
const [streamWidthB, streamHeightB] = b.Resolution.split('x').map(Number);
|
const [streamWidthB, streamHeightB] = b.Resolution.split('x').map(Number);
|
||||||
@@ -356,25 +369,21 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
if (nonHevcResolutionList.length > 0 || AlwaysReloadPlayerOnAd) {
|
if (nonHevcResolutionList.length > 0 || AlwaysReloadPlayerOnAd) {
|
||||||
streamInfo.ModifiedM3U8 = lines.join('\n');
|
streamInfo.ModifiedM3U8 = lines.join('\n');
|
||||||
var streamM3u8Url = streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0];
|
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
|
||||||
if (streamM3u8Response.status == 200) {
|
|
||||||
var streamM3u8 = await streamM3u8Response.text();
|
|
||||||
if (streamM3u8.includes(AdSignifier) || SimulatedAdsDepth > 0) {
|
|
||||||
streamInfo.IsUsingModifiedM3U8 = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
postMessage({
|
||||||
} else {
|
key: 'MonitorPlayerForBuffering'
|
||||||
streamInfo.IsUsingModifiedM3U8 = streamInfo.IsShowingAd && streamInfo.ModifiedM3U8;
|
});
|
||||||
|
if (streamInfo.IsUsingModifiedM3U8 || isNewStream) {
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
}
|
}
|
||||||
resolve(new Response(replaceServerTimeInM3u8(streamInfo.IsUsingModifiedM3U8 ? streamInfo.ModifiedM3U8 : streamInfo.EncodingsM3U8, serverTime)));
|
resolve(new Response(replaceServerTimeInM3u8(streamInfo.IsUsingModifiedM3U8 ? streamInfo.ModifiedM3U8 : streamInfo.EncodingsM3U8, serverTime)));
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var send = function() {
|
const send = function() {
|
||||||
return realFetch(url, options).then(function(response) {
|
return realFetch(url, options).then(function(response) {
|
||||||
processAfter(response);
|
processAfter(response);
|
||||||
})['catch'](function(err) {
|
})['catch'](function(err) {
|
||||||
@@ -390,10 +399,10 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
function getServerTimeFromM3u8(encodingsM3u8) {
|
function getServerTimeFromM3u8(encodingsM3u8) {
|
||||||
if (V2API) {
|
if (V2API) {
|
||||||
var matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
const matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
var matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
const matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
||||||
@@ -402,18 +411,53 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
||||||
}
|
}
|
||||||
|
function stripAdSegments(textStr, stripAllSegments, streamInfo) {
|
||||||
|
let hasStrippedAdSegments = false;
|
||||||
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
|
const newAdUrl = 'https://twitch.tv';
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i];
|
||||||
|
// Remove tracking urls which appear in the overlay UI
|
||||||
|
line = line
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`)
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
|
||||||
|
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
|
||||||
|
const segmentUrl = lines[i + 1];
|
||||||
|
AdSegmentCache.set(segmentUrl, Date.now());
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
if (line.includes(AdSignifier)) {
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStrippedAdSegments) {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
// No low latency during ads (otherwise it's possible for the player to prefetch and display ad segments)
|
||||||
|
if (lines[i].startsWith('#EXT-X-TWITCH-PREFETCH:')) {
|
||||||
|
lines[i] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamInfo.IsStrippingAdSegments = hasStrippedAdSegments;
|
||||||
|
AdSegmentCache.forEach((key, value, map) => {
|
||||||
|
if (value < Date.now() - 120000) {
|
||||||
|
map.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
||||||
var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
|
const encodingsLines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
||||||
var matchedResolutionUrl = null;
|
let matchedResolutionUrl = null;
|
||||||
var matchedFrameRate = false;
|
let matchedFrameRate = false;
|
||||||
var closestResolutionUrl = null;
|
let closestResolutionUrl = null;
|
||||||
var closestResolutionDifference = Infinity;
|
let closestResolutionDifference = Infinity;
|
||||||
for (var i = 0; i < encodingsLines.length - 1; i++) {
|
for (let i = 0; i < encodingsLines.length - 1; i++) {
|
||||||
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(encodingsLines[i]);
|
const attributes = parseAttributes(encodingsLines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
var frameRate = attributes['FRAME-RATE'];
|
const frameRate = attributes['FRAME-RATE'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
||||||
matchedResolutionUrl = encodingsLines[i + 1];
|
matchedResolutionUrl = encodingsLines[i + 1];
|
||||||
@@ -423,7 +467,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [width, height] = resolution.split('x').map(Number);
|
const [width, height] = resolution.split('x').map(Number);
|
||||||
var difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
const difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
||||||
if (difference < closestResolutionDifference) {
|
if (difference < closestResolutionDifference) {
|
||||||
closestResolutionUrl = encodingsLines[i + 1];
|
closestResolutionUrl = encodingsLines[i + 1];
|
||||||
closestResolutionDifference = difference;
|
closestResolutionDifference = difference;
|
||||||
@@ -433,31 +477,22 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
return closestResolutionUrl;
|
return closestResolutionUrl;
|
||||||
}
|
}
|
||||||
function tryFixPlayerBuffering() {
|
|
||||||
// NOTE: This "ActiveStreamInfo" variable isn't ideal but now that squad streams are removed it should be correct
|
|
||||||
if (IsPlayerBuffering && LastPausePlay < Date.now() - DelayBetweenEachPlayerFixBufferAttempt && ActiveStreamInfo &&
|
|
||||||
((ActiveStreamInfo.IsShowingAd && FixPlayerBufferingInsideAds) || (!ActiveStreamInfo.IsShowingAd && FixPlayerBufferingOutsideAds))
|
|
||||||
) {
|
|
||||||
console.log("Attempting to fix player buffering by doing pause/play");
|
|
||||||
LastPausePlay = Date.now();
|
|
||||||
postMessage({
|
|
||||||
key: 'PauseResumePlayer'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function processM3U8(url, textStr, realFetch) {
|
async function processM3U8(url, textStr, realFetch) {
|
||||||
var streamInfo = StreamInfosByUrl[url];
|
const streamInfo = StreamInfosByUrl[url];
|
||||||
if (!streamInfo) {
|
if (!streamInfo) {
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
ActiveStreamInfo = streamInfo;
|
if (HasTriggeredPlayerReload) {
|
||||||
var haveAdTags = textStr.includes(AdSignifier) || SimulatedAdsDepth > 0;
|
HasTriggeredPlayerReload = false;
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
|
}
|
||||||
|
const haveAdTags = textStr.includes(AdSignifier) || SimulatedAdsDepth > 0;
|
||||||
if (haveAdTags) {
|
if (haveAdTags) {
|
||||||
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
||||||
if (!streamInfo.IsMidroll) {
|
if (!streamInfo.IsMidroll) {
|
||||||
var lines = textStr.replace('\r', '').split('\n');
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const line = lines[i];
|
||||||
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
||||||
if (!line.includes(',live') && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
if (!line.includes(',live') && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
||||||
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
||||||
@@ -469,54 +504,50 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
streamInfo.Resolution
|
const currentResolution = streamInfo.Urls[url];
|
||||||
var currentResolution = streamInfo.Urls[url];
|
|
||||||
if (!currentResolution) {
|
if (!currentResolution) {
|
||||||
console.log('Ads will leak due to missing resolution info for ' + url);
|
console.log('Ads will leak due to missing resolution info for ' + url);
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
if (streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
|
|
||||||
streamInfo.AdStartTime = Date.now();
|
|
||||||
}
|
|
||||||
if (!streamInfo.IsShowingAd) {
|
if (!streamInfo.IsShowingAd) {
|
||||||
streamInfo.AdStartTime = Date.now();
|
|
||||||
streamInfo.IsShowingAd = true;
|
streamInfo.IsShowingAd = true;
|
||||||
if (streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
|
}
|
||||||
|
const isHevc = currentResolution.Codecs.startsWith('hev') || currentResolution.Codecs.startsWith('hvc');
|
||||||
|
if (((isHevc && !SkipPlayerReloadOnHevc) || AlwaysReloadPlayerOnAd) && streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
|
||||||
|
streamInfo.IsUsingModifiedM3U8 = true;
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
postMessage({
|
postMessage({
|
||||||
key: 'ReloadPlayer'
|
key: 'ReloadPlayer'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
postMessage({
|
let backupPlayerType = null;
|
||||||
key: 'ShowAdBlockBanner',
|
let backupM3u8 = null;
|
||||||
isMidroll: streamInfo.IsMidroll
|
let fallbackM3u8 = null;
|
||||||
});
|
let startIndex = 0;
|
||||||
|
let isDoingMinimalRequests = false;
|
||||||
|
if (streamInfo.LastPlayerReload > Date.now() - PlayerReloadMinimalRequestsTime) {
|
||||||
|
// When doing player reload there are a lot of requests which causes the backup stream to load in slow. Briefly prefer using a single version to prevent long delays
|
||||||
|
startIndex = PlayerReloadMinimalRequestsPlayerIndex;
|
||||||
|
isDoingMinimalRequests = true;
|
||||||
}
|
}
|
||||||
var backupPlayerType = null;
|
for (let playerTypeIndex = startIndex; !backupM3u8 && playerTypeIndex < BackupPlayerTypes.length; playerTypeIndex++) {
|
||||||
var backupM3u8 = null;
|
const playerType = BackupPlayerTypes[playerTypeIndex];
|
||||||
var fallbackM3u8 = null;
|
const realPlayerType = playerType.replace('-CACHED', '');
|
||||||
var startIndex = 0;
|
const isFullyCachedPlayerType = playerType != realPlayerType;
|
||||||
if ((streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) ||
|
for (let i = 0; i < 2; i++) {
|
||||||
(streamInfo.ModifiedM3U8 && streamInfo.AdStartTime > Date.now() - PlayerReloadLowResTime)
|
|
||||||
) {
|
|
||||||
// When doing player reload there are a lot of requests which causes the backup stream to load in slow. Briefly prefer using the low res version to prevent long delays
|
|
||||||
startIndex = BackupPlayerTypes.length - 1;
|
|
||||||
}
|
|
||||||
for (var playerTypeIndex = startIndex; !backupM3u8 && playerTypeIndex < BackupPlayerTypes.length; playerTypeIndex++) {
|
|
||||||
var playerType = BackupPlayerTypes[playerTypeIndex];
|
|
||||||
for (var i = 0; i < 2; i++) {
|
|
||||||
// This caches the m3u8 if it doesn't have ads. If the already existing cache has ads it fetches a new version (second loop)
|
// This caches the m3u8 if it doesn't have ads. If the already existing cache has ads it fetches a new version (second loop)
|
||||||
var isFreshM3u8 = false;
|
let isFreshM3u8 = false;
|
||||||
var encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType];
|
let encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType];
|
||||||
if (!encodingsM3u8) {
|
if (!encodingsM3u8) {
|
||||||
isFreshM3u8 = true;
|
isFreshM3u8 = true;
|
||||||
try {
|
try {
|
||||||
var accessTokenResponse = await getAccessToken(streamInfo.ChannelName, playerType);
|
const accessTokenResponse = await getAccessToken(streamInfo.ChannelName, realPlayerType);
|
||||||
if (accessTokenResponse.status === 200) {
|
if (accessTokenResponse.status === 200) {
|
||||||
var accessToken = await accessTokenResponse.json();
|
const accessToken = await accessTokenResponse.json();
|
||||||
var urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
const urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
||||||
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
||||||
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
||||||
var encodingsM3u8Response = await realFetch(urlInfo.href);
|
const encodingsM3u8Response = await realFetch(urlInfo.href);
|
||||||
if (encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response.status === 200) {
|
||||||
encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType] = await encodingsM3u8Response.text();
|
encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType] = await encodingsM3u8Response.text();
|
||||||
}
|
}
|
||||||
@@ -525,10 +556,10 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
if (encodingsM3u8) {
|
if (encodingsM3u8) {
|
||||||
try {
|
try {
|
||||||
var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, currentResolution);
|
const streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, currentResolution);
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status == 200) {
|
if (streamM3u8Response.status == 200) {
|
||||||
var m3u8Text = await streamM3u8Response.text();
|
const m3u8Text = await streamM3u8Response.text();
|
||||||
if (m3u8Text) {
|
if (m3u8Text) {
|
||||||
if (playerType == FallbackPlayerType) {
|
if (playerType == FallbackPlayerType) {
|
||||||
fallbackM3u8 = m3u8Text;
|
fallbackM3u8 = m3u8Text;
|
||||||
@@ -538,13 +569,13 @@ twitch-videoad.js text/javascript
|
|||||||
backupM3u8 = m3u8Text;
|
backupM3u8 = m3u8Text;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (isDoingMinimalRequests || isFullyCachedPlayerType) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
if (startIndex != 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
streamInfo.BackupEncodingsM3U8Cache[playerType] = null;
|
streamInfo.BackupEncodingsM3U8Cache[playerType] = null;
|
||||||
if (isFreshM3u8) {
|
if (isFreshM3u8) {
|
||||||
break;
|
break;
|
||||||
@@ -562,18 +593,34 @@ twitch-videoad.js text/javascript
|
|||||||
console.log(`Blocking${(streamInfo.IsMidroll ? ' midroll ' : ' ')}ads (${backupPlayerType})`);
|
console.log(`Blocking${(streamInfo.IsMidroll ? ' midroll ' : ' ')}ads (${backupPlayerType})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: Improve hevc stripping. It should always strip when there is a codec mismatch (both ways)
|
||||||
|
const stripHevc = isHevc && streamInfo.ModifiedM3U8;
|
||||||
|
if (IsAdStrippingEnabled || stripHevc) {
|
||||||
|
textStr = stripAdSegments(textStr, stripHevc, streamInfo);
|
||||||
|
}
|
||||||
} else if (streamInfo.IsShowingAd) {
|
} else if (streamInfo.IsShowingAd) {
|
||||||
console.log('Finished blocking ads');
|
console.log('Finished blocking ads');
|
||||||
streamInfo.IsShowingAd = false;
|
streamInfo.IsShowingAd = false;
|
||||||
|
streamInfo.IsStrippingAdSegments = false;
|
||||||
streamInfo.ActiveBackupPlayerType = null;
|
streamInfo.ActiveBackupPlayerType = null;
|
||||||
|
if (streamInfo.IsUsingModifiedM3U8 || ReloadPlayerAfterAd) {
|
||||||
|
streamInfo.IsUsingModifiedM3U8 = false;
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
postMessage({
|
postMessage({
|
||||||
key: streamInfo.IsUsingModifiedM3U8 ? 'ReloadPlayer' : 'PauseResumePlayer'
|
key: 'ReloadPlayer'
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
postMessage({
|
postMessage({
|
||||||
key: 'HideAdBlockBanner'
|
key: 'PauseResumePlayer'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
tryFixPlayerBuffering();
|
}
|
||||||
|
postMessage({
|
||||||
|
key: 'UpdateAdBlockBanner',
|
||||||
|
isMidroll: streamInfo.IsMidroll,
|
||||||
|
hasAds: streamInfo.IsShowingAd,
|
||||||
|
isStrippingAdSegments: streamInfo.IsStrippingAdSegments
|
||||||
|
});
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
function parseAttributes(str) {
|
function parseAttributes(str) {
|
||||||
@@ -589,44 +636,41 @@ twitch-videoad.js text/javascript
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
function getAccessToken(channelName, playerType) {
|
function getAccessToken(channelName, playerType) {
|
||||||
var body = null;
|
const body = {
|
||||||
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "android", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "android", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
|
operationName: 'PlaybackAccessToken',
|
||||||
body = {
|
|
||||||
operationName: 'PlaybackAccessToken_Template',
|
|
||||||
query: templateQuery,
|
|
||||||
variables: {
|
variables: {
|
||||||
'isLive': true,
|
isLive: true,
|
||||||
'login': channelName,
|
login: channelName,
|
||||||
'isVod': false,
|
isVod: false,
|
||||||
'vodID': '',
|
vodID: "",
|
||||||
'playerType': playerType
|
playerType: playerType,
|
||||||
|
platform: playerType == 'autoplay' ? 'android' : 'web'
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
persistedQuery: {
|
||||||
|
version:1,
|
||||||
|
sha256Hash:"ed230aa1e33e07eebb8928504583da78a5173989fadfb1ac94be06a04f3cdbe9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return gqlRequest(body, playerType);
|
return gqlRequest(body, playerType);
|
||||||
}
|
}
|
||||||
function gqlRequest(body, playerType) {
|
function gqlRequest(body, playerType) {
|
||||||
if (!GQLDeviceID) {
|
if (!GQLDeviceID) {
|
||||||
var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
const dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
var dcharactersLength = dcharacters.length;
|
const dcharactersLength = dcharacters.length;
|
||||||
for (var i = 0; i < 32; i++) {
|
for (let i = 0; i < 32; i++) {
|
||||||
GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
|
GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var headers = {
|
let headers = {
|
||||||
'Client-ID': ClientID,
|
'Client-ID': ClientID,
|
||||||
'Device-ID': GQLDeviceID,
|
|
||||||
'X-Device-Id': GQLDeviceID,
|
'X-Device-Id': GQLDeviceID,
|
||||||
'Authorization': AuthorizationHeader,
|
'Authorization': AuthorizationHeader,
|
||||||
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader}),
|
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader}),
|
||||||
...(ClientVersion && {'Client-Version': ClientVersion}),
|
...(ClientVersion && {'Client-Version': ClientVersion}),
|
||||||
...(ClientSession && {'Client-Session-Id': ClientSession})
|
...(ClientSession && {'Client-Session-Id': ClientSession})
|
||||||
};
|
};
|
||||||
if (playerType != 'site') {
|
|
||||||
headers = {
|
|
||||||
'Client-Id': ClientID,
|
|
||||||
'X-Device-Id': GQLDeviceID
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const requestId = Math.random().toString(36).substring(2, 15);
|
const requestId = Math.random().toString(36).substring(2, 15);
|
||||||
const fetchRequest = {
|
const fetchRequest = {
|
||||||
@@ -648,7 +692,50 @@ twitch-videoad.js text/javascript
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function doTwitchPlayerTask(isPausePlay, isReload) {
|
let playerForMonitoringBuffering = null;
|
||||||
|
const playerBufferState = {
|
||||||
|
position: 0,
|
||||||
|
bufferedPosition: 0,
|
||||||
|
bufferDuration: 0,
|
||||||
|
numSame: 0,
|
||||||
|
lastFixTime: 0
|
||||||
|
};
|
||||||
|
function monitorPlayerBuffering() {
|
||||||
|
if (playerForMonitoringBuffering) {
|
||||||
|
try {
|
||||||
|
const player = playerForMonitoringBuffering;
|
||||||
|
if (!player.isPaused() && !player.getHTMLVideoElement()?.ended && playerBufferState.lastFixTime <= Date.now() - PlayerBufferingMinRepeatDelay && !isActivelyStrippingAds) {
|
||||||
|
const position = player.core?.state?.position;
|
||||||
|
const bufferedPosition = player.core?.state?.bufferedPosition;
|
||||||
|
const bufferDuration = player.getBufferDuration();
|
||||||
|
//console.log('position:' + position + ' bufferDuration:' + bufferDuration + ' bufferPosition:' + bufferedPosition);
|
||||||
|
// NOTE: This could be improved. It currently lets the player fully eat the full buffer before it triggers pause/play
|
||||||
|
if ((playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) &&
|
||||||
|
playerBufferState.bufferedPosition == bufferedPosition &&
|
||||||
|
playerBufferState.bufferDuration >= bufferDuration &&
|
||||||
|
(position != 0 || bufferedPosition != 0 || bufferDuration != 0)
|
||||||
|
) {
|
||||||
|
playerBufferState.numSame++;
|
||||||
|
if (playerBufferState.numSame == PlayerBufferingSameStateCount) {
|
||||||
|
console.log('Attempt to fix buffering position:' + playerBufferState.position + ' bufferedPosition:' + playerBufferState.bufferedPosition + ' bufferDuration:' + playerBufferState.bufferDuration);
|
||||||
|
doTwitchPlayerTask(!PlayerBufferingDoPlayerReload, PlayerBufferingDoPlayerReload, false);
|
||||||
|
playerBufferState.lastFixTime = Date.now();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerBufferState.numSame = 0;
|
||||||
|
}
|
||||||
|
playerBufferState.position = position;
|
||||||
|
playerBufferState.bufferedPosition = bufferedPosition;
|
||||||
|
playerBufferState.bufferDuration = bufferDuration;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error when monitoring player for buffering: ' + err);
|
||||||
|
playerForMonitoringBuffering = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(monitorPlayerBuffering, PlayerBufferingDelay);
|
||||||
|
}
|
||||||
|
function doTwitchPlayerTask(isPausePlay, isReload, monitorPlayerForBuffering) {
|
||||||
function findReactNode(root, constraint) {
|
function findReactNode(root, constraint) {
|
||||||
if (root.stateNode && constraint(root.stateNode)) {
|
if (root.stateNode && constraint(root.stateNode)) {
|
||||||
return root.stateNode;
|
return root.stateNode;
|
||||||
@@ -664,27 +751,27 @@ twitch-videoad.js text/javascript
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function findReactRootNode() {
|
function findReactRootNode() {
|
||||||
var reactRootNode = null;
|
let reactRootNode = null;
|
||||||
var rootNode = document.querySelector('#root');
|
const rootNode = document.querySelector('#root');
|
||||||
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
||||||
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
||||||
}
|
}
|
||||||
if (reactRootNode == null) {
|
if (reactRootNode == null) {
|
||||||
var containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
||||||
if (containerName != null) {
|
if (containerName != null) {
|
||||||
reactRootNode = rootNode[containerName];
|
reactRootNode = rootNode[containerName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reactRootNode;
|
return reactRootNode;
|
||||||
}
|
}
|
||||||
var reactRootNode = findReactRootNode();
|
const reactRootNode = findReactRootNode();
|
||||||
if (!reactRootNode) {
|
if (!reactRootNode) {
|
||||||
console.log('Could not find react root');
|
console.log('Could not find react root');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
||||||
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
||||||
var playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
console.log('Could not find player');
|
console.log('Could not find player');
|
||||||
return;
|
return;
|
||||||
@@ -693,7 +780,14 @@ twitch-videoad.js text/javascript
|
|||||||
console.log('Could not find player state');
|
console.log('Could not find player state');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (player.paused || player.core?.paused) {
|
if (!playerForMonitoringBuffering) {
|
||||||
|
playerForMonitoringBuffering = player;
|
||||||
|
}
|
||||||
|
if (player.isPaused() || player.core?.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (monitorPlayerForBuffering) {
|
||||||
|
playerBufferState.lastFixTime = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPausePlay) {
|
if (isPausePlay) {
|
||||||
@@ -705,9 +799,9 @@ twitch-videoad.js text/javascript
|
|||||||
const lsKeyQuality = 'video-quality';
|
const lsKeyQuality = 'video-quality';
|
||||||
const lsKeyMuted = 'video-muted';
|
const lsKeyMuted = 'video-muted';
|
||||||
const lsKeyVolume = 'volume';
|
const lsKeyVolume = 'volume';
|
||||||
var currentQualityLS = null;
|
let currentQualityLS = null;
|
||||||
var currentMutedLS = null;
|
let currentMutedLS = null;
|
||||||
var currentVolumeLS = null;
|
let currentVolumeLS = null;
|
||||||
try {
|
try {
|
||||||
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
||||||
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
||||||
@@ -720,7 +814,9 @@ twitch-videoad.js text/javascript
|
|||||||
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
|
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
console.log('Reloading Twitch player');
|
||||||
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
|
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
|
||||||
|
postTwitchWorkerMessage('TriggeredPlayerReload');
|
||||||
player.play();
|
player.play();
|
||||||
if (localStorageHookFailed && (currentQualityLS || currentMutedLS || currentVolumeLS)) {
|
if (localStorageHookFailed && (currentQualityLS || currentMutedLS || currentVolumeLS)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -740,7 +836,9 @@ twitch-videoad.js text/javascript
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.reloadTwitchPlayer = doTwitchPlayerTask;
|
window.reloadTwitchPlayer = () => {
|
||||||
|
doTwitchPlayerTask(false, true);
|
||||||
|
};
|
||||||
function postTwitchWorkerMessage(key, value) {
|
function postTwitchWorkerMessage(key, value) {
|
||||||
twitchWorkers.forEach((worker) => {
|
twitchWorkers.forEach((worker) => {
|
||||||
worker.postMessage({key: key, value: value});
|
worker.postMessage({key: key, value: value});
|
||||||
@@ -766,13 +864,12 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hookFetch() {
|
function hookFetch() {
|
||||||
var realFetch = window.fetch;
|
const realFetch = window.fetch;
|
||||||
window.realFetch = realFetch;
|
window.realFetch = realFetch;
|
||||||
window.fetch = function(url, init, ...args) {
|
window.fetch = function(url, init, ...args) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
if (url.includes('gql')) {
|
if (url.includes('gql')) {
|
||||||
//Device ID is used when notifying Twitch of ads.
|
let deviceId = init.headers['X-Device-Id'];
|
||||||
var deviceId = init.headers['X-Device-Id'];
|
|
||||||
if (typeof deviceId !== 'string') {
|
if (typeof deviceId !== 'string') {
|
||||||
deviceId = init.headers['Device-ID'];
|
deviceId = init.headers['Device-ID'];
|
||||||
}
|
}
|
||||||
@@ -816,7 +913,7 @@ twitch-videoad.js text/javascript
|
|||||||
if (DisableMatureConentPopup) {
|
if (DisableMatureConentPopup) {
|
||||||
const newBody2 = JSON.parse(init.body);
|
const newBody2 = JSON.parse(init.body);
|
||||||
if (Array.isArray(newBody2)) {
|
if (Array.isArray(newBody2)) {
|
||||||
var hasRemovedClassification = false;
|
let hasRemovedClassification = false;
|
||||||
for (let i = 0; i < newBody2.length; i++) {
|
for (let i = 0; i < newBody2.length; i++) {
|
||||||
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
||||||
hasRemovedClassification = true;
|
hasRemovedClassification = true;
|
||||||
@@ -853,13 +950,13 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}catch{}
|
}catch{}
|
||||||
var block = e => {
|
const block = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
};
|
};
|
||||||
let wasVideoPlaying = true;
|
let wasVideoPlaying = true;
|
||||||
var visibilityChange = e => {
|
const visibilityChange = e => {
|
||||||
if (typeof chrome !== 'undefined') {
|
if (typeof chrome !== 'undefined') {
|
||||||
const videos = document.getElementsByTagName('video');
|
const videos = document.getElementsByTagName('video');
|
||||||
if (videos.length > 0) {
|
if (videos.length > 0) {
|
||||||
@@ -893,25 +990,25 @@ twitch-videoad.js text/javascript
|
|||||||
}catch{}
|
}catch{}
|
||||||
// Hooks for preserving volume / resolution
|
// Hooks for preserving volume / resolution
|
||||||
try {
|
try {
|
||||||
var keysToCache = [
|
const keysToCache = [
|
||||||
'video-quality',
|
'video-quality',
|
||||||
'video-muted',
|
'video-muted',
|
||||||
'volume',
|
'volume',
|
||||||
'lowLatencyModeEnabled',// Low Latency
|
'lowLatencyModeEnabled',// Low Latency
|
||||||
'persistenceEnabled',// Mini Player
|
'persistenceEnabled',// Mini Player
|
||||||
];
|
];
|
||||||
var cachedValues = new Map();
|
const cachedValues = new Map();
|
||||||
for (var i = 0; i < keysToCache.length; i++) {
|
for (let i = 0; i < keysToCache.length; i++) {
|
||||||
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
||||||
}
|
}
|
||||||
var realSetItem = localStorage.setItem;
|
const realSetItem = localStorage.setItem;
|
||||||
localStorage.setItem = function(key, value) {
|
localStorage.setItem = function(key, value) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
cachedValues.set(key, value);
|
cachedValues.set(key, value);
|
||||||
}
|
}
|
||||||
realSetItem.apply(this, arguments);
|
realSetItem.apply(this, arguments);
|
||||||
};
|
};
|
||||||
var realGetItem = localStorage.getItem;
|
const realGetItem = localStorage.getItem;
|
||||||
localStorage.getItem = function(key) {
|
localStorage.getItem = function(key) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
return cachedValues.get(key);
|
return cachedValues.get(key);
|
||||||
@@ -931,6 +1028,9 @@ twitch-videoad.js text/javascript
|
|||||||
declareOptions(window);
|
declareOptions(window);
|
||||||
hookWindowWorker();
|
hookWindowWorker();
|
||||||
hookFetch();
|
hookFetch();
|
||||||
|
if (PlayerBufferingFix) {
|
||||||
|
monitorPlayerBuffering();
|
||||||
|
}
|
||||||
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
|
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
|
||||||
onContentLoaded();
|
onContentLoaded();
|
||||||
} else {
|
} else {
|
||||||
@@ -945,4 +1045,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
postTwitchWorkerMessage('SimulateAds', depth);
|
postTwitchWorkerMessage('SimulateAds', depth);
|
||||||
};
|
};
|
||||||
|
window.allSegmentsAreAdSegments = () => {
|
||||||
|
postTwitchWorkerMessage('AllSegmentsAreAdSegments');
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
+307
-204
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name TwitchAdSolutions (vaft)
|
// @name TwitchAdSolutions (vaft)
|
||||||
// @namespace https://github.com/pixeltris/TwitchAdSolutions
|
// @namespace https://github.com/pixeltris/TwitchAdSolutions
|
||||||
// @version 29.0.0
|
// @version 30.0.0
|
||||||
// @description Multiple solutions for blocking Twitch ads (vaft)
|
// @description Multiple solutions for blocking Twitch ads (vaft)
|
||||||
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
|
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
|
||||||
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
|
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/vaft/vaft.user.js
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
var ourTwitchAdSolutionsVersion = 16;// Used to prevent conflicts with outdated versions of the scripts
|
const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
|
||||||
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
||||||
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
console.log("skipping vaft as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
||||||
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
@@ -26,13 +26,17 @@
|
|||||||
scope.BackupPlayerTypes = [
|
scope.BackupPlayerTypes = [
|
||||||
'embed',//Source
|
'embed',//Source
|
||||||
'site',//Source
|
'site',//Source
|
||||||
'autoplay'//360p
|
'autoplay',//360p
|
||||||
|
'picture-by-picture-CACHED'//360p (-CACHED is an internal suffix and is removed)
|
||||||
];
|
];
|
||||||
scope.FallbackPlayerType = 'embed';
|
scope.FallbackPlayerType = 'embed';
|
||||||
scope.ForceAccessTokenPlayerType = 'site';
|
scope.ForceAccessTokenPlayerType = 'site';
|
||||||
scope.SkipPlayerReloadOnHevc = false;// If true this will skip player reload on streams which have 2k/4k quality (if you enable this and you use the 2k/4k quality setting you'll get error #4000 / #3000 / spinning wheel on chrome based browsers)
|
scope.SkipPlayerReloadOnHevc = false;// If true this will skip player reload on streams which have 2k/4k quality (if you enable this and you use the 2k/4k quality setting you'll get error #4000 / #3000 / spinning wheel on chrome based browsers)
|
||||||
scope.AlwaysReloadPlayerOnAd = false;
|
scope.AlwaysReloadPlayerOnAd = false;// Always pause/play when entering/leaving ads
|
||||||
scope.PlayerReloadLowResTime = 1500;
|
scope.ReloadPlayerAfterAd = true;// After the ad finishes do a player reload instead of pause/play
|
||||||
|
scope.PlayerReloadMinimalRequestsTime = 1500;
|
||||||
|
scope.PlayerReloadMinimalRequestsPlayerIndex = 0;//embed
|
||||||
|
scope.HasTriggeredPlayerReload = false;
|
||||||
scope.DisableMatureConentPopup = false;// If true this avoids having to log in to watch age gated content
|
scope.DisableMatureConentPopup = false;// If true this avoids having to log in to watch age gated content
|
||||||
scope.StreamInfos = [];
|
scope.StreamInfos = [];
|
||||||
scope.StreamInfosByUrl = [];
|
scope.StreamInfosByUrl = [];
|
||||||
@@ -42,33 +46,37 @@
|
|||||||
scope.ClientIntegrityHeader = null;
|
scope.ClientIntegrityHeader = null;
|
||||||
scope.AuthorizationHeader = undefined;
|
scope.AuthorizationHeader = undefined;
|
||||||
scope.SimulatedAdsDepth = 0;
|
scope.SimulatedAdsDepth = 0;
|
||||||
scope.IsPlayerBuffering = false;
|
scope.PlayerBufferingFix = true;// If true this will pause/play the player when it gets stuck buffering
|
||||||
scope.LastPausePlay = 0;
|
scope.PlayerBufferingDelay = 500;// How often should we check the player state (in milliseconds)
|
||||||
scope.FixPlayerBufferingInsideAds = true;
|
scope.PlayerBufferingSameStateCount = 3;// How many times of seeing the same player state until we trigger pause/play (it will only trigger it one time until the player state changes again)
|
||||||
scope.FixPlayerBufferingOutsideAds = false;
|
scope.PlayerBufferingDangerZone = 1;// The buffering time left (in seconds) when we should ignore the players playback position in the player state check
|
||||||
scope.DelayBetweenEachPlayerFixBufferAttempt = 3000;
|
scope.PlayerBufferingDoPlayerReload = false;// If true this will do a player reload instead of pause/play (player reloading is better at fixing the playback issues but it takes slightly longer)
|
||||||
scope.ActiveStreamInfo = null;
|
scope.PlayerBufferingMinRepeatDelay = 5000;// Minimum delay (in milliseconds) between each pause/play (this is to avoid over pressing pause/play when there are genuine buffering problems)
|
||||||
scope.V2API = false;
|
scope.V2API = false;
|
||||||
|
scope.IsAdStrippingEnabled = true;
|
||||||
|
scope.AdSegmentCache = new Map();
|
||||||
|
scope.AllSegmentsAreAdSegments = false;
|
||||||
}
|
}
|
||||||
var localStorageHookFailed = false;
|
let isActivelyStrippingAds = false;
|
||||||
var twitchWorkers = [];
|
let localStorageHookFailed = false;
|
||||||
var adBlockDiv = null;
|
const twitchWorkers = [];
|
||||||
var workerStringConflicts = [
|
let adBlockDiv = null;
|
||||||
|
const workerStringConflicts = [
|
||||||
'twitch',
|
'twitch',
|
||||||
'isVariantA'// TwitchNoSub
|
'isVariantA'// TwitchNoSub
|
||||||
];
|
];
|
||||||
var workerStringAllow = [];
|
const workerStringAllow = [];
|
||||||
var workerStringReinsert = [
|
const workerStringReinsert = [
|
||||||
'isVariantA',// TwitchNoSub (prior to (0.9))
|
'isVariantA',// TwitchNoSub (prior to (0.9))
|
||||||
'besuper/',// TwitchNoSub (0.9)
|
'besuper/',// TwitchNoSub (0.9)
|
||||||
'${patch_url}'// TwitchNoSub (0.9.1)
|
'${patch_url}'// TwitchNoSub (0.9.1)
|
||||||
];
|
];
|
||||||
function getCleanWorker(worker) {
|
function getCleanWorker(worker) {
|
||||||
var root = null;
|
let root = null;
|
||||||
var parent = null;
|
let parent = null;
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
||||||
if (parent !== null) {
|
if (parent !== null) {
|
||||||
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
||||||
@@ -84,10 +92,10 @@
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
function getWorkersForReinsert(worker) {
|
function getWorkersForReinsert(worker) {
|
||||||
var result = [];
|
const result = [];
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
||||||
result.push(proto);
|
result.push(proto);
|
||||||
} else {
|
} else {
|
||||||
@@ -97,24 +105,24 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
function reinsertWorkers(worker, reinsert) {
|
function reinsertWorkers(worker, reinsert) {
|
||||||
var parent = worker;
|
let parent = worker;
|
||||||
for (var i = 0; i < reinsert.length; i++) {
|
for (let i = 0; i < reinsert.length; i++) {
|
||||||
Object.setPrototypeOf(reinsert[i], parent);
|
Object.setPrototypeOf(reinsert[i], parent);
|
||||||
parent = reinsert[i];
|
parent = reinsert[i];
|
||||||
}
|
}
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
function isValidWorker(worker) {
|
function isValidWorker(worker) {
|
||||||
var workerString = worker.toString();
|
const workerString = worker.toString();
|
||||||
return !workerStringConflicts.some((x) => workerString.includes(x))
|
return !workerStringConflicts.some((x) => workerString.includes(x))
|
||||||
|| workerStringAllow.some((x) => workerString.includes(x))
|
|| workerStringAllow.some((x) => workerString.includes(x))
|
||||||
|| workerStringReinsert.some((x) => workerString.includes(x));
|
|| workerStringReinsert.some((x) => workerString.includes(x));
|
||||||
}
|
}
|
||||||
function hookWindowWorker() {
|
function hookWindowWorker() {
|
||||||
var reinsert = getWorkersForReinsert(window.Worker);
|
const reinsert = getWorkersForReinsert(window.Worker);
|
||||||
var newWorker = class Worker extends getCleanWorker(window.Worker) {
|
const newWorker = class Worker extends getCleanWorker(window.Worker) {
|
||||||
constructor(twitchBlobUrl, options) {
|
constructor(twitchBlobUrl, options) {
|
||||||
var isTwitchWorker = false;
|
let isTwitchWorker = false;
|
||||||
try {
|
try {
|
||||||
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -122,8 +130,9 @@
|
|||||||
super(twitchBlobUrl, options);
|
super(twitchBlobUrl, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var newBlobStr = `
|
const newBlobStr = `
|
||||||
const pendingFetchRequests = new Map();
|
const pendingFetchRequests = new Map();
|
||||||
|
${stripAdSegments.toString()}
|
||||||
${getStreamUrlForResolution.toString()}
|
${getStreamUrlForResolution.toString()}
|
||||||
${processM3U8.toString()}
|
${processM3U8.toString()}
|
||||||
${hookWorkerFetch.toString()}
|
${hookWorkerFetch.toString()}
|
||||||
@@ -134,8 +143,7 @@
|
|||||||
${getWasmWorkerJs.toString()}
|
${getWasmWorkerJs.toString()}
|
||||||
${getServerTimeFromM3u8.toString()}
|
${getServerTimeFromM3u8.toString()}
|
||||||
${replaceServerTimeInM3u8.toString()}
|
${replaceServerTimeInM3u8.toString()}
|
||||||
${tryFixPlayerBuffering.toString()}
|
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
||||||
var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
|
||||||
declareOptions(self);
|
declareOptions(self);
|
||||||
GQLDeviceID = ${GQLDeviceID ? "'" + GQLDeviceID + "'" : null};
|
GQLDeviceID = ${GQLDeviceID ? "'" + GQLDeviceID + "'" : null};
|
||||||
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
||||||
@@ -172,20 +180,14 @@
|
|||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (e.data.key == 'TriggeredPlayerReload') {
|
||||||
|
HasTriggeredPlayerReload = true;
|
||||||
} else if (e.data.key == 'SimulateAds') {
|
} else if (e.data.key == 'SimulateAds') {
|
||||||
SimulatedAdsDepth = e.data.value;
|
SimulatedAdsDepth = e.data.value;
|
||||||
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
||||||
}
|
} else if (e.data.key == 'AllSegmentsAreAdSegments') {
|
||||||
if (e.data.funcName) {
|
AllSegmentsAreAdSegments = !AllSegmentsAreAdSegments;
|
||||||
if (e.data.funcName == 'onClientSinkBuffering') {
|
console.log('AllSegmentsAreAdSegments: ' + AllSegmentsAreAdSegments);
|
||||||
IsPlayerBuffering = true;
|
|
||||||
} else if (e.data.funcName == 'onClientSinkPlaying') {
|
|
||||||
IsPlayerBuffering = false;
|
|
||||||
LastPausePlay = Date.now();
|
|
||||||
} else if (e.data.funcName == 'playIntent' || e.data.funcName == 'play') {
|
|
||||||
LastPausePlay = Date.now();
|
|
||||||
}
|
|
||||||
tryFixPlayerBuffering();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hookWorkerFetch();
|
hookWorkerFetch();
|
||||||
@@ -194,25 +196,21 @@
|
|||||||
super(URL.createObjectURL(new Blob([newBlobStr])), options);
|
super(URL.createObjectURL(new Blob([newBlobStr])), options);
|
||||||
twitchWorkers.push(this);
|
twitchWorkers.push(this);
|
||||||
this.addEventListener('message', (e) => {
|
this.addEventListener('message', (e) => {
|
||||||
if (e.data.key == 'ShowAdBlockBanner') {
|
if (e.data.key == 'UpdateAdBlockBanner') {
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
adBlockDiv = getAdBlockDiv();
|
adBlockDiv = getAdBlockDiv();
|
||||||
}
|
}
|
||||||
if (adBlockDiv != null) {
|
if (adBlockDiv != null) {
|
||||||
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
|
isActivelyStrippingAds = e.data.isStrippingAdSegments;
|
||||||
adBlockDiv.style.display = 'block';
|
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads' + (e.data.isStrippingAdSegments ? ' (stripping)' : '');
|
||||||
}
|
adBlockDiv.style.display = e.data.hasAds ? 'block' : 'none';
|
||||||
} else if (e.data.key == 'HideAdBlockBanner') {
|
|
||||||
if (adBlockDiv == null) {
|
|
||||||
adBlockDiv = getAdBlockDiv();
|
|
||||||
}
|
|
||||||
if (adBlockDiv != null) {
|
|
||||||
adBlockDiv.style.display = 'none';
|
|
||||||
}
|
}
|
||||||
} else if (e.data.key == 'PauseResumePlayer') {
|
} else if (e.data.key == 'PauseResumePlayer') {
|
||||||
doTwitchPlayerTask(true, false);
|
doTwitchPlayerTask(true, false, false);
|
||||||
} else if (e.data.key == 'ReloadPlayer') {
|
} else if (e.data.key == 'ReloadPlayer') {
|
||||||
doTwitchPlayerTask(false, true);
|
doTwitchPlayerTask(false, true, false);
|
||||||
|
} else if (e.data.key == 'MonitorPlayerForBuffering') {
|
||||||
|
doTwitchPlayerTask(false, false, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.addEventListener('message', async event => {
|
this.addEventListener('message', async event => {
|
||||||
@@ -227,8 +225,8 @@
|
|||||||
});
|
});
|
||||||
function getAdBlockDiv() {
|
function getAdBlockDiv() {
|
||||||
//To display a notification to the user, that an ad is being blocked.
|
//To display a notification to the user, that an ad is being blocked.
|
||||||
var playerRootDiv = document.querySelector('.video-player');
|
const playerRootDiv = document.querySelector('.video-player');
|
||||||
var adBlockDiv = null;
|
let adBlockDiv = null;
|
||||||
if (playerRootDiv != null) {
|
if (playerRootDiv != null) {
|
||||||
adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
|
adBlockDiv = playerRootDiv.querySelector('.adblock-overlay');
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
@@ -244,7 +242,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var workerInstance = reinsertWorkers(newWorker, reinsert);
|
let workerInstance = reinsertWorkers(newWorker, reinsert);
|
||||||
Object.defineProperty(window, 'Worker', {
|
Object.defineProperty(window, 'Worker', {
|
||||||
get: function() {
|
get: function() {
|
||||||
return workerInstance;
|
return workerInstance;
|
||||||
@@ -259,7 +257,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getWasmWorkerJs(twitchBlobUrl) {
|
function getWasmWorkerJs(twitchBlobUrl) {
|
||||||
var req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open('GET', twitchBlobUrl, false);
|
req.open('GET', twitchBlobUrl, false);
|
||||||
req.overrideMimeType("text/javascript");
|
req.overrideMimeType("text/javascript");
|
||||||
req.send();
|
req.send();
|
||||||
@@ -267,20 +265,32 @@
|
|||||||
}
|
}
|
||||||
function hookWorkerFetch() {
|
function hookWorkerFetch() {
|
||||||
console.log('hookWorkerFetch (vaft)');
|
console.log('hookWorkerFetch (vaft)');
|
||||||
var realFetch = fetch;
|
const realFetch = fetch;
|
||||||
fetch = async function(url, options) {
|
fetch = async function(url, options) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
|
if (AdSegmentCache.has(url)) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const send = function() {
|
||||||
|
return realFetch('data:video/mp4;base64,AAAAKGZ0eXBtcDQyAAAAAWlzb21tcDQyZGFzaGF2YzFpc282aGxzZgAABEltb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAYagAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAABqHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAURtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAADvbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAACzc3RibAAAAGdzdHNkAAAAAAAAAAEAAABXbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAAzZXNkcwAAAAADgICAIgABAASAgIAUQBUAAAAAAAAAAAAAAAWAgIACEZAGgICAAQIAAAAQc3R0cwAAAAAAAAAAAAAAEHN0c2MAAAAAAAAAAAAAABRzdHN6AAAAAAAAAAAAAAAAAAAAEHN0Y28AAAAAAAAAAAAAAeV0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAoAAAAFoAAAAAAGBbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAA9CQAAAAABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABLG1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAOxzdGJsAAAAoHN0c2QAAAAAAAAAAQAAAJBhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAoABaABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAOmF2Y0MBTUAe/+EAI2dNQB6WUoFAX/LgLUBAQFAAAD6AAA6mDgAAHoQAA9CW7y4KAQAEaOuPIAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAASG12ZXgAAAAgdHJleAAAAAAAAAABAAAAAQAAAC4AAAAAAoAAAAAAACB0cmV4AAAAAAAAAAIAAAABAACCNQAAAAACQAAA', options).then(function(response) {
|
||||||
|
resolve(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
}
|
||||||
url = url.trimEnd();
|
url = url.trimEnd();
|
||||||
if (url.endsWith('m3u8')) {
|
if (url.endsWith('m3u8')) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var processAfter = async function(response) {
|
const processAfter = async function(response) {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
resolve(new Response(await processM3U8(url, await response.text(), realFetch)));
|
resolve(new Response(await processM3U8(url, await response.text(), realFetch)));
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var send = function() {
|
const send = function() {
|
||||||
return realFetch(url, options).then(function(response) {
|
return realFetch(url, options).then(function(response) {
|
||||||
processAfter(response);
|
processAfter(response);
|
||||||
})['catch'](function(err) {
|
})['catch'](function(err) {
|
||||||
@@ -291,28 +301,30 @@
|
|||||||
});
|
});
|
||||||
} else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
} else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
||||||
V2API = url.includes('/api/v2/');
|
V2API = url.includes('/api/v2/');
|
||||||
var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
const channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||||
if (ForceAccessTokenPlayerType) {
|
if (ForceAccessTokenPlayerType) {
|
||||||
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
||||||
var tempUrl = new URL(url);
|
const tempUrl = new URL(url);
|
||||||
tempUrl.searchParams.delete('parent_domains');
|
tempUrl.searchParams.delete('parent_domains');
|
||||||
url = tempUrl.toString();
|
url = tempUrl.toString();
|
||||||
}
|
}
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var processAfter = async function(response) {
|
const processAfter = async function(response) {
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
var encodingsM3u8 = await response.text();
|
const encodingsM3u8 = await response.text();
|
||||||
var serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
const serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
||||||
var streamInfo = StreamInfos[channelName];
|
let streamInfo = StreamInfos[channelName];
|
||||||
|
let isNewStream = false;
|
||||||
if (streamInfo != null && streamInfo.EncodingsM3U8 != null && (await realFetch(streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
if (streamInfo != null && streamInfo.EncodingsM3U8 != null && (await realFetch(streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
||||||
// The cached encodings are dead (the stream probably restarted)
|
// The cached encodings are dead (the stream probably restarted)
|
||||||
streamInfo = null;
|
streamInfo = null;
|
||||||
}
|
}
|
||||||
if (streamInfo == null || streamInfo.EncodingsM3U8 == null) {
|
if (streamInfo == null || streamInfo.EncodingsM3U8 == null) {
|
||||||
|
isNewStream = true;
|
||||||
StreamInfos[channelName] = streamInfo = {
|
StreamInfos[channelName] = streamInfo = {
|
||||||
ChannelName: channelName,
|
ChannelName: channelName,
|
||||||
IsShowingAd: false,
|
IsShowingAd: false,
|
||||||
AdStartTime: 0,
|
LastPlayerReload: 0,
|
||||||
EncodingsM3U8: encodingsM3u8,
|
EncodingsM3U8: encodingsM3u8,
|
||||||
ModifiedM3U8: null,
|
ModifiedM3U8: null,
|
||||||
IsUsingModifiedM3U8: false,
|
IsUsingModifiedM3U8: false,
|
||||||
@@ -322,15 +334,16 @@
|
|||||||
ResolutionList: [],
|
ResolutionList: [],
|
||||||
BackupEncodingsM3U8Cache: [],
|
BackupEncodingsM3U8Cache: [],
|
||||||
ActiveBackupPlayerType: null,
|
ActiveBackupPlayerType: null,
|
||||||
IsMidroll: false
|
IsMidroll: false,
|
||||||
|
IsStrippingAdSegments: false
|
||||||
};
|
};
|
||||||
var lines = encodingsM3u8.replace('\r', '').split('\n');
|
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length - 1; i++) {
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(lines[i]);
|
const attributes = parseAttributes(lines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
var resolutionInfo = {
|
const resolutionInfo = {
|
||||||
Resolution: resolution,
|
Resolution: resolution,
|
||||||
FrameRate: attributes['FRAME-RATE'],
|
FrameRate: attributes['FRAME-RATE'],
|
||||||
Codecs: attributes['CODECS'],
|
Codecs: attributes['CODECS'],
|
||||||
@@ -342,17 +355,17 @@
|
|||||||
StreamInfosByUrl[lines[i + 1]] = streamInfo;
|
StreamInfosByUrl[lines[i + 1]] = streamInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var nonHevcResolutionList = streamInfo.ResolutionList.filter((element) => element.Codecs.startsWith('avc') || element.Codecs.startsWith('av0'));
|
const nonHevcResolutionList = streamInfo.ResolutionList.filter((element) => element.Codecs.startsWith('avc') || element.Codecs.startsWith('av0'));
|
||||||
if (AlwaysReloadPlayerOnAd || (nonHevcResolutionList.length > 0 && streamInfo.ResolutionList.some((element) => element.Codecs.startsWith('hev') || element.Codecs.startsWith('hvc')) && !SkipPlayerReloadOnHevc)) {
|
if (AlwaysReloadPlayerOnAd || (nonHevcResolutionList.length > 0 && streamInfo.ResolutionList.some((element) => element.Codecs.startsWith('hev') || element.Codecs.startsWith('hvc')) && !SkipPlayerReloadOnHevc)) {
|
||||||
if (nonHevcResolutionList.length > 0) {
|
if (nonHevcResolutionList.length > 0) {
|
||||||
for (var i = 0; i < lines.length - 1; i++) {
|
for (let i = 0; i < lines.length - 1; i++) {
|
||||||
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
if (lines[i].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
var resSettings = parseAttributes(lines[i].substring(lines[i].indexOf(':') + 1));
|
const resSettings = parseAttributes(lines[i].substring(lines[i].indexOf(':') + 1));
|
||||||
const codecsKey = 'CODECS';
|
const codecsKey = 'CODECS';
|
||||||
if (resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) {
|
if (resSettings[codecsKey].startsWith('hev') || resSettings[codecsKey].startsWith('hvc')) {
|
||||||
var oldResolution = resSettings['RESOLUTION'];
|
const oldResolution = resSettings['RESOLUTION'];
|
||||||
const [targetWidth, targetHeight] = oldResolution.split('x').map(Number);
|
const [targetWidth, targetHeight] = oldResolution.split('x').map(Number);
|
||||||
var newResolutionInfo = nonHevcResolutionList.sort((a, b) => {
|
const newResolutionInfo = nonHevcResolutionList.sort((a, b) => {
|
||||||
// TODO: Take into account 'Frame-Rate' when sorting (i.e. 1080p60 vs 1080p30)
|
// TODO: Take into account 'Frame-Rate' when sorting (i.e. 1080p60 vs 1080p30)
|
||||||
const [streamWidthA, streamHeightA] = a.Resolution.split('x').map(Number);
|
const [streamWidthA, streamHeightA] = a.Resolution.split('x').map(Number);
|
||||||
const [streamWidthB, streamHeightB] = b.Resolution.split('x').map(Number);
|
const [streamWidthB, streamHeightB] = b.Resolution.split('x').map(Number);
|
||||||
@@ -367,25 +380,21 @@
|
|||||||
}
|
}
|
||||||
if (nonHevcResolutionList.length > 0 || AlwaysReloadPlayerOnAd) {
|
if (nonHevcResolutionList.length > 0 || AlwaysReloadPlayerOnAd) {
|
||||||
streamInfo.ModifiedM3U8 = lines.join('\n');
|
streamInfo.ModifiedM3U8 = lines.join('\n');
|
||||||
var streamM3u8Url = streamInfo.EncodingsM3U8.match(/^https:.*\.m3u8$/m)[0];
|
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
|
||||||
if (streamM3u8Response.status == 200) {
|
|
||||||
var streamM3u8 = await streamM3u8Response.text();
|
|
||||||
if (streamM3u8.includes(AdSignifier) || SimulatedAdsDepth > 0) {
|
|
||||||
streamInfo.IsUsingModifiedM3U8 = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
postMessage({
|
||||||
} else {
|
key: 'MonitorPlayerForBuffering'
|
||||||
streamInfo.IsUsingModifiedM3U8 = streamInfo.IsShowingAd && streamInfo.ModifiedM3U8;
|
});
|
||||||
|
if (streamInfo.IsUsingModifiedM3U8 || isNewStream) {
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
}
|
}
|
||||||
resolve(new Response(replaceServerTimeInM3u8(streamInfo.IsUsingModifiedM3U8 ? streamInfo.ModifiedM3U8 : streamInfo.EncodingsM3U8, serverTime)));
|
resolve(new Response(replaceServerTimeInM3u8(streamInfo.IsUsingModifiedM3U8 ? streamInfo.ModifiedM3U8 : streamInfo.EncodingsM3U8, serverTime)));
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var send = function() {
|
const send = function() {
|
||||||
return realFetch(url, options).then(function(response) {
|
return realFetch(url, options).then(function(response) {
|
||||||
processAfter(response);
|
processAfter(response);
|
||||||
})['catch'](function(err) {
|
})['catch'](function(err) {
|
||||||
@@ -401,10 +410,10 @@
|
|||||||
}
|
}
|
||||||
function getServerTimeFromM3u8(encodingsM3u8) {
|
function getServerTimeFromM3u8(encodingsM3u8) {
|
||||||
if (V2API) {
|
if (V2API) {
|
||||||
var matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
const matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
var matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
const matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
||||||
@@ -413,18 +422,53 @@
|
|||||||
}
|
}
|
||||||
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
||||||
}
|
}
|
||||||
|
function stripAdSegments(textStr, stripAllSegments, streamInfo) {
|
||||||
|
let hasStrippedAdSegments = false;
|
||||||
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
|
const newAdUrl = 'https://twitch.tv';
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i];
|
||||||
|
// Remove tracking urls which appear in the overlay UI
|
||||||
|
line = line
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`)
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
|
||||||
|
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
|
||||||
|
const segmentUrl = lines[i + 1];
|
||||||
|
AdSegmentCache.set(segmentUrl, Date.now());
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
if (line.includes(AdSignifier)) {
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStrippedAdSegments) {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
// No low latency during ads (otherwise it's possible for the player to prefetch and display ad segments)
|
||||||
|
if (lines[i].startsWith('#EXT-X-TWITCH-PREFETCH:')) {
|
||||||
|
lines[i] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streamInfo.IsStrippingAdSegments = hasStrippedAdSegments;
|
||||||
|
AdSegmentCache.forEach((key, value, map) => {
|
||||||
|
if (value < Date.now() - 120000) {
|
||||||
|
map.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
||||||
var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
|
const encodingsLines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
||||||
var matchedResolutionUrl = null;
|
let matchedResolutionUrl = null;
|
||||||
var matchedFrameRate = false;
|
let matchedFrameRate = false;
|
||||||
var closestResolutionUrl = null;
|
let closestResolutionUrl = null;
|
||||||
var closestResolutionDifference = Infinity;
|
let closestResolutionDifference = Infinity;
|
||||||
for (var i = 0; i < encodingsLines.length - 1; i++) {
|
for (let i = 0; i < encodingsLines.length - 1; i++) {
|
||||||
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(encodingsLines[i]);
|
const attributes = parseAttributes(encodingsLines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
var frameRate = attributes['FRAME-RATE'];
|
const frameRate = attributes['FRAME-RATE'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
||||||
matchedResolutionUrl = encodingsLines[i + 1];
|
matchedResolutionUrl = encodingsLines[i + 1];
|
||||||
@@ -434,7 +478,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [width, height] = resolution.split('x').map(Number);
|
const [width, height] = resolution.split('x').map(Number);
|
||||||
var difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
const difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
||||||
if (difference < closestResolutionDifference) {
|
if (difference < closestResolutionDifference) {
|
||||||
closestResolutionUrl = encodingsLines[i + 1];
|
closestResolutionUrl = encodingsLines[i + 1];
|
||||||
closestResolutionDifference = difference;
|
closestResolutionDifference = difference;
|
||||||
@@ -444,31 +488,22 @@
|
|||||||
}
|
}
|
||||||
return closestResolutionUrl;
|
return closestResolutionUrl;
|
||||||
}
|
}
|
||||||
function tryFixPlayerBuffering() {
|
|
||||||
// NOTE: This "ActiveStreamInfo" variable isn't ideal but now that squad streams are removed it should be correct
|
|
||||||
if (IsPlayerBuffering && LastPausePlay < Date.now() - DelayBetweenEachPlayerFixBufferAttempt && ActiveStreamInfo &&
|
|
||||||
((ActiveStreamInfo.IsShowingAd && FixPlayerBufferingInsideAds) || (!ActiveStreamInfo.IsShowingAd && FixPlayerBufferingOutsideAds))
|
|
||||||
) {
|
|
||||||
console.log("Attempting to fix player buffering by doing pause/play");
|
|
||||||
LastPausePlay = Date.now();
|
|
||||||
postMessage({
|
|
||||||
key: 'PauseResumePlayer'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function processM3U8(url, textStr, realFetch) {
|
async function processM3U8(url, textStr, realFetch) {
|
||||||
var streamInfo = StreamInfosByUrl[url];
|
const streamInfo = StreamInfosByUrl[url];
|
||||||
if (!streamInfo) {
|
if (!streamInfo) {
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
ActiveStreamInfo = streamInfo;
|
if (HasTriggeredPlayerReload) {
|
||||||
var haveAdTags = textStr.includes(AdSignifier) || SimulatedAdsDepth > 0;
|
HasTriggeredPlayerReload = false;
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
|
}
|
||||||
|
const haveAdTags = textStr.includes(AdSignifier) || SimulatedAdsDepth > 0;
|
||||||
if (haveAdTags) {
|
if (haveAdTags) {
|
||||||
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
||||||
if (!streamInfo.IsMidroll) {
|
if (!streamInfo.IsMidroll) {
|
||||||
var lines = textStr.replace('\r', '').split('\n');
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const line = lines[i];
|
||||||
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
||||||
if (!line.includes(',live') && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
if (!line.includes(',live') && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
||||||
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
||||||
@@ -480,54 +515,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
streamInfo.Resolution
|
const currentResolution = streamInfo.Urls[url];
|
||||||
var currentResolution = streamInfo.Urls[url];
|
|
||||||
if (!currentResolution) {
|
if (!currentResolution) {
|
||||||
console.log('Ads will leak due to missing resolution info for ' + url);
|
console.log('Ads will leak due to missing resolution info for ' + url);
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
if (streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
|
|
||||||
streamInfo.AdStartTime = Date.now();
|
|
||||||
}
|
|
||||||
if (!streamInfo.IsShowingAd) {
|
if (!streamInfo.IsShowingAd) {
|
||||||
streamInfo.AdStartTime = Date.now();
|
|
||||||
streamInfo.IsShowingAd = true;
|
streamInfo.IsShowingAd = true;
|
||||||
if (streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
|
}
|
||||||
|
const isHevc = currentResolution.Codecs.startsWith('hev') || currentResolution.Codecs.startsWith('hvc');
|
||||||
|
if (((isHevc && !SkipPlayerReloadOnHevc) || AlwaysReloadPlayerOnAd) && streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) {
|
||||||
|
streamInfo.IsUsingModifiedM3U8 = true;
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
postMessage({
|
postMessage({
|
||||||
key: 'ReloadPlayer'
|
key: 'ReloadPlayer'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
postMessage({
|
let backupPlayerType = null;
|
||||||
key: 'ShowAdBlockBanner',
|
let backupM3u8 = null;
|
||||||
isMidroll: streamInfo.IsMidroll
|
let fallbackM3u8 = null;
|
||||||
});
|
let startIndex = 0;
|
||||||
|
let isDoingMinimalRequests = false;
|
||||||
|
if (streamInfo.LastPlayerReload > Date.now() - PlayerReloadMinimalRequestsTime) {
|
||||||
|
// When doing player reload there are a lot of requests which causes the backup stream to load in slow. Briefly prefer using a single version to prevent long delays
|
||||||
|
startIndex = PlayerReloadMinimalRequestsPlayerIndex;
|
||||||
|
isDoingMinimalRequests = true;
|
||||||
}
|
}
|
||||||
var backupPlayerType = null;
|
for (let playerTypeIndex = startIndex; !backupM3u8 && playerTypeIndex < BackupPlayerTypes.length; playerTypeIndex++) {
|
||||||
var backupM3u8 = null;
|
const playerType = BackupPlayerTypes[playerTypeIndex];
|
||||||
var fallbackM3u8 = null;
|
const realPlayerType = playerType.replace('-CACHED', '');
|
||||||
var startIndex = 0;
|
const isFullyCachedPlayerType = playerType != realPlayerType;
|
||||||
if ((streamInfo.ModifiedM3U8 && !streamInfo.IsUsingModifiedM3U8) ||
|
for (let i = 0; i < 2; i++) {
|
||||||
(streamInfo.ModifiedM3U8 && streamInfo.AdStartTime > Date.now() - PlayerReloadLowResTime)
|
|
||||||
) {
|
|
||||||
// When doing player reload there are a lot of requests which causes the backup stream to load in slow. Briefly prefer using the low res version to prevent long delays
|
|
||||||
startIndex = BackupPlayerTypes.length - 1;
|
|
||||||
}
|
|
||||||
for (var playerTypeIndex = startIndex; !backupM3u8 && playerTypeIndex < BackupPlayerTypes.length; playerTypeIndex++) {
|
|
||||||
var playerType = BackupPlayerTypes[playerTypeIndex];
|
|
||||||
for (var i = 0; i < 2; i++) {
|
|
||||||
// This caches the m3u8 if it doesn't have ads. If the already existing cache has ads it fetches a new version (second loop)
|
// This caches the m3u8 if it doesn't have ads. If the already existing cache has ads it fetches a new version (second loop)
|
||||||
var isFreshM3u8 = false;
|
let isFreshM3u8 = false;
|
||||||
var encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType];
|
let encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType];
|
||||||
if (!encodingsM3u8) {
|
if (!encodingsM3u8) {
|
||||||
isFreshM3u8 = true;
|
isFreshM3u8 = true;
|
||||||
try {
|
try {
|
||||||
var accessTokenResponse = await getAccessToken(streamInfo.ChannelName, playerType);
|
const accessTokenResponse = await getAccessToken(streamInfo.ChannelName, realPlayerType);
|
||||||
if (accessTokenResponse.status === 200) {
|
if (accessTokenResponse.status === 200) {
|
||||||
var accessToken = await accessTokenResponse.json();
|
const accessToken = await accessTokenResponse.json();
|
||||||
var urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
const urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
||||||
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
||||||
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
||||||
var encodingsM3u8Response = await realFetch(urlInfo.href);
|
const encodingsM3u8Response = await realFetch(urlInfo.href);
|
||||||
if (encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response.status === 200) {
|
||||||
encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType] = await encodingsM3u8Response.text();
|
encodingsM3u8 = streamInfo.BackupEncodingsM3U8Cache[playerType] = await encodingsM3u8Response.text();
|
||||||
}
|
}
|
||||||
@@ -536,10 +567,10 @@
|
|||||||
}
|
}
|
||||||
if (encodingsM3u8) {
|
if (encodingsM3u8) {
|
||||||
try {
|
try {
|
||||||
var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, currentResolution);
|
const streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, currentResolution);
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status == 200) {
|
if (streamM3u8Response.status == 200) {
|
||||||
var m3u8Text = await streamM3u8Response.text();
|
const m3u8Text = await streamM3u8Response.text();
|
||||||
if (m3u8Text) {
|
if (m3u8Text) {
|
||||||
if (playerType == FallbackPlayerType) {
|
if (playerType == FallbackPlayerType) {
|
||||||
fallbackM3u8 = m3u8Text;
|
fallbackM3u8 = m3u8Text;
|
||||||
@@ -549,13 +580,13 @@
|
|||||||
backupM3u8 = m3u8Text;
|
backupM3u8 = m3u8Text;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (isDoingMinimalRequests || isFullyCachedPlayerType) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
}
|
}
|
||||||
if (startIndex != 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
streamInfo.BackupEncodingsM3U8Cache[playerType] = null;
|
streamInfo.BackupEncodingsM3U8Cache[playerType] = null;
|
||||||
if (isFreshM3u8) {
|
if (isFreshM3u8) {
|
||||||
break;
|
break;
|
||||||
@@ -573,18 +604,34 @@
|
|||||||
console.log(`Blocking${(streamInfo.IsMidroll ? ' midroll ' : ' ')}ads (${backupPlayerType})`);
|
console.log(`Blocking${(streamInfo.IsMidroll ? ' midroll ' : ' ')}ads (${backupPlayerType})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// TODO: Improve hevc stripping. It should always strip when there is a codec mismatch (both ways)
|
||||||
|
const stripHevc = isHevc && streamInfo.ModifiedM3U8;
|
||||||
|
if (IsAdStrippingEnabled || stripHevc) {
|
||||||
|
textStr = stripAdSegments(textStr, stripHevc, streamInfo);
|
||||||
|
}
|
||||||
} else if (streamInfo.IsShowingAd) {
|
} else if (streamInfo.IsShowingAd) {
|
||||||
console.log('Finished blocking ads');
|
console.log('Finished blocking ads');
|
||||||
streamInfo.IsShowingAd = false;
|
streamInfo.IsShowingAd = false;
|
||||||
|
streamInfo.IsStrippingAdSegments = false;
|
||||||
streamInfo.ActiveBackupPlayerType = null;
|
streamInfo.ActiveBackupPlayerType = null;
|
||||||
|
if (streamInfo.IsUsingModifiedM3U8 || ReloadPlayerAfterAd) {
|
||||||
|
streamInfo.IsUsingModifiedM3U8 = false;
|
||||||
|
streamInfo.LastPlayerReload = Date.now();
|
||||||
postMessage({
|
postMessage({
|
||||||
key: streamInfo.IsUsingModifiedM3U8 ? 'ReloadPlayer' : 'PauseResumePlayer'
|
key: 'ReloadPlayer'
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
postMessage({
|
postMessage({
|
||||||
key: 'HideAdBlockBanner'
|
key: 'PauseResumePlayer'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
tryFixPlayerBuffering();
|
}
|
||||||
|
postMessage({
|
||||||
|
key: 'UpdateAdBlockBanner',
|
||||||
|
isMidroll: streamInfo.IsMidroll,
|
||||||
|
hasAds: streamInfo.IsShowingAd,
|
||||||
|
isStrippingAdSegments: streamInfo.IsStrippingAdSegments
|
||||||
|
});
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
function parseAttributes(str) {
|
function parseAttributes(str) {
|
||||||
@@ -600,44 +647,41 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
function getAccessToken(channelName, playerType) {
|
function getAccessToken(channelName, playerType) {
|
||||||
var body = null;
|
const body = {
|
||||||
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "android", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "android", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
|
operationName: 'PlaybackAccessToken',
|
||||||
body = {
|
|
||||||
operationName: 'PlaybackAccessToken_Template',
|
|
||||||
query: templateQuery,
|
|
||||||
variables: {
|
variables: {
|
||||||
'isLive': true,
|
isLive: true,
|
||||||
'login': channelName,
|
login: channelName,
|
||||||
'isVod': false,
|
isVod: false,
|
||||||
'vodID': '',
|
vodID: "",
|
||||||
'playerType': playerType
|
playerType: playerType,
|
||||||
|
platform: playerType == 'autoplay' ? 'android' : 'web'
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
persistedQuery: {
|
||||||
|
version:1,
|
||||||
|
sha256Hash:"ed230aa1e33e07eebb8928504583da78a5173989fadfb1ac94be06a04f3cdbe9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return gqlRequest(body, playerType);
|
return gqlRequest(body, playerType);
|
||||||
}
|
}
|
||||||
function gqlRequest(body, playerType) {
|
function gqlRequest(body, playerType) {
|
||||||
if (!GQLDeviceID) {
|
if (!GQLDeviceID) {
|
||||||
var dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
const dcharacters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
var dcharactersLength = dcharacters.length;
|
const dcharactersLength = dcharacters.length;
|
||||||
for (var i = 0; i < 32; i++) {
|
for (let i = 0; i < 32; i++) {
|
||||||
GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
|
GQLDeviceID += dcharacters.charAt(Math.floor(Math.random() * dcharactersLength));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var headers = {
|
let headers = {
|
||||||
'Client-ID': ClientID,
|
'Client-ID': ClientID,
|
||||||
'Device-ID': GQLDeviceID,
|
|
||||||
'X-Device-Id': GQLDeviceID,
|
'X-Device-Id': GQLDeviceID,
|
||||||
'Authorization': AuthorizationHeader,
|
'Authorization': AuthorizationHeader,
|
||||||
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader}),
|
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader}),
|
||||||
...(ClientVersion && {'Client-Version': ClientVersion}),
|
...(ClientVersion && {'Client-Version': ClientVersion}),
|
||||||
...(ClientSession && {'Client-Session-Id': ClientSession})
|
...(ClientSession && {'Client-Session-Id': ClientSession})
|
||||||
};
|
};
|
||||||
if (playerType != 'site') {
|
|
||||||
headers = {
|
|
||||||
'Client-Id': ClientID,
|
|
||||||
'X-Device-Id': GQLDeviceID
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const requestId = Math.random().toString(36).substring(2, 15);
|
const requestId = Math.random().toString(36).substring(2, 15);
|
||||||
const fetchRequest = {
|
const fetchRequest = {
|
||||||
@@ -659,7 +703,50 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function doTwitchPlayerTask(isPausePlay, isReload) {
|
let playerForMonitoringBuffering = null;
|
||||||
|
const playerBufferState = {
|
||||||
|
position: 0,
|
||||||
|
bufferedPosition: 0,
|
||||||
|
bufferDuration: 0,
|
||||||
|
numSame: 0,
|
||||||
|
lastFixTime: 0
|
||||||
|
};
|
||||||
|
function monitorPlayerBuffering() {
|
||||||
|
if (playerForMonitoringBuffering) {
|
||||||
|
try {
|
||||||
|
const player = playerForMonitoringBuffering;
|
||||||
|
if (!player.isPaused() && !player.getHTMLVideoElement()?.ended && playerBufferState.lastFixTime <= Date.now() - PlayerBufferingMinRepeatDelay && !isActivelyStrippingAds) {
|
||||||
|
const position = player.core?.state?.position;
|
||||||
|
const bufferedPosition = player.core?.state?.bufferedPosition;
|
||||||
|
const bufferDuration = player.getBufferDuration();
|
||||||
|
//console.log('position:' + position + ' bufferDuration:' + bufferDuration + ' bufferPosition:' + bufferedPosition);
|
||||||
|
// NOTE: This could be improved. It currently lets the player fully eat the full buffer before it triggers pause/play
|
||||||
|
if ((playerBufferState.position == position || bufferDuration < PlayerBufferingDangerZone) &&
|
||||||
|
playerBufferState.bufferedPosition == bufferedPosition &&
|
||||||
|
playerBufferState.bufferDuration >= bufferDuration &&
|
||||||
|
(position != 0 || bufferedPosition != 0 || bufferDuration != 0)
|
||||||
|
) {
|
||||||
|
playerBufferState.numSame++;
|
||||||
|
if (playerBufferState.numSame == PlayerBufferingSameStateCount) {
|
||||||
|
console.log('Attempt to fix buffering position:' + playerBufferState.position + ' bufferedPosition:' + playerBufferState.bufferedPosition + ' bufferDuration:' + playerBufferState.bufferDuration);
|
||||||
|
doTwitchPlayerTask(!PlayerBufferingDoPlayerReload, PlayerBufferingDoPlayerReload, false);
|
||||||
|
playerBufferState.lastFixTime = Date.now();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playerBufferState.numSame = 0;
|
||||||
|
}
|
||||||
|
playerBufferState.position = position;
|
||||||
|
playerBufferState.bufferedPosition = bufferedPosition;
|
||||||
|
playerBufferState.bufferDuration = bufferDuration;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('error when monitoring player for buffering: ' + err);
|
||||||
|
playerForMonitoringBuffering = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setTimeout(monitorPlayerBuffering, PlayerBufferingDelay);
|
||||||
|
}
|
||||||
|
function doTwitchPlayerTask(isPausePlay, isReload, monitorPlayerForBuffering) {
|
||||||
function findReactNode(root, constraint) {
|
function findReactNode(root, constraint) {
|
||||||
if (root.stateNode && constraint(root.stateNode)) {
|
if (root.stateNode && constraint(root.stateNode)) {
|
||||||
return root.stateNode;
|
return root.stateNode;
|
||||||
@@ -675,27 +762,27 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function findReactRootNode() {
|
function findReactRootNode() {
|
||||||
var reactRootNode = null;
|
let reactRootNode = null;
|
||||||
var rootNode = document.querySelector('#root');
|
const rootNode = document.querySelector('#root');
|
||||||
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
||||||
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
||||||
}
|
}
|
||||||
if (reactRootNode == null) {
|
if (reactRootNode == null) {
|
||||||
var containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
||||||
if (containerName != null) {
|
if (containerName != null) {
|
||||||
reactRootNode = rootNode[containerName];
|
reactRootNode = rootNode[containerName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reactRootNode;
|
return reactRootNode;
|
||||||
}
|
}
|
||||||
var reactRootNode = findReactRootNode();
|
const reactRootNode = findReactRootNode();
|
||||||
if (!reactRootNode) {
|
if (!reactRootNode) {
|
||||||
console.log('Could not find react root');
|
console.log('Could not find react root');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
||||||
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
||||||
var playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
console.log('Could not find player');
|
console.log('Could not find player');
|
||||||
return;
|
return;
|
||||||
@@ -704,7 +791,14 @@
|
|||||||
console.log('Could not find player state');
|
console.log('Could not find player state');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (player.paused || player.core?.paused) {
|
if (!playerForMonitoringBuffering) {
|
||||||
|
playerForMonitoringBuffering = player;
|
||||||
|
}
|
||||||
|
if (player.isPaused() || player.core?.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (monitorPlayerForBuffering) {
|
||||||
|
playerBufferState.lastFixTime = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPausePlay) {
|
if (isPausePlay) {
|
||||||
@@ -716,9 +810,9 @@
|
|||||||
const lsKeyQuality = 'video-quality';
|
const lsKeyQuality = 'video-quality';
|
||||||
const lsKeyMuted = 'video-muted';
|
const lsKeyMuted = 'video-muted';
|
||||||
const lsKeyVolume = 'volume';
|
const lsKeyVolume = 'volume';
|
||||||
var currentQualityLS = null;
|
let currentQualityLS = null;
|
||||||
var currentMutedLS = null;
|
let currentMutedLS = null;
|
||||||
var currentVolumeLS = null;
|
let currentVolumeLS = null;
|
||||||
try {
|
try {
|
||||||
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
||||||
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
||||||
@@ -731,7 +825,9 @@
|
|||||||
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
|
localStorage.setItem(lsKeyQuality, JSON.stringify({default:player.core.state.quality.group}));
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
console.log('Reloading Twitch player');
|
||||||
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
|
playerState.setSrc({ isNewMediaPlayerInstance: true, refreshAccessToken: true });
|
||||||
|
postTwitchWorkerMessage('TriggeredPlayerReload');
|
||||||
player.play();
|
player.play();
|
||||||
if (localStorageHookFailed && (currentQualityLS || currentMutedLS || currentVolumeLS)) {
|
if (localStorageHookFailed && (currentQualityLS || currentMutedLS || currentVolumeLS)) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -751,7 +847,9 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.reloadTwitchPlayer = doTwitchPlayerTask;
|
window.reloadTwitchPlayer = () => {
|
||||||
|
doTwitchPlayerTask(false, true);
|
||||||
|
};
|
||||||
function postTwitchWorkerMessage(key, value) {
|
function postTwitchWorkerMessage(key, value) {
|
||||||
twitchWorkers.forEach((worker) => {
|
twitchWorkers.forEach((worker) => {
|
||||||
worker.postMessage({key: key, value: value});
|
worker.postMessage({key: key, value: value});
|
||||||
@@ -777,13 +875,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hookFetch() {
|
function hookFetch() {
|
||||||
var realFetch = window.fetch;
|
const realFetch = window.fetch;
|
||||||
window.realFetch = realFetch;
|
window.realFetch = realFetch;
|
||||||
window.fetch = function(url, init, ...args) {
|
window.fetch = function(url, init, ...args) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
if (url.includes('gql')) {
|
if (url.includes('gql')) {
|
||||||
//Device ID is used when notifying Twitch of ads.
|
let deviceId = init.headers['X-Device-Id'];
|
||||||
var deviceId = init.headers['X-Device-Id'];
|
|
||||||
if (typeof deviceId !== 'string') {
|
if (typeof deviceId !== 'string') {
|
||||||
deviceId = init.headers['Device-ID'];
|
deviceId = init.headers['Device-ID'];
|
||||||
}
|
}
|
||||||
@@ -827,7 +924,7 @@
|
|||||||
if (DisableMatureConentPopup) {
|
if (DisableMatureConentPopup) {
|
||||||
const newBody2 = JSON.parse(init.body);
|
const newBody2 = JSON.parse(init.body);
|
||||||
if (Array.isArray(newBody2)) {
|
if (Array.isArray(newBody2)) {
|
||||||
var hasRemovedClassification = false;
|
let hasRemovedClassification = false;
|
||||||
for (let i = 0; i < newBody2.length; i++) {
|
for (let i = 0; i < newBody2.length; i++) {
|
||||||
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
||||||
hasRemovedClassification = true;
|
hasRemovedClassification = true;
|
||||||
@@ -864,13 +961,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}catch{}
|
}catch{}
|
||||||
var block = e => {
|
const block = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
};
|
};
|
||||||
let wasVideoPlaying = true;
|
let wasVideoPlaying = true;
|
||||||
var visibilityChange = e => {
|
const visibilityChange = e => {
|
||||||
if (typeof chrome !== 'undefined') {
|
if (typeof chrome !== 'undefined') {
|
||||||
const videos = document.getElementsByTagName('video');
|
const videos = document.getElementsByTagName('video');
|
||||||
if (videos.length > 0) {
|
if (videos.length > 0) {
|
||||||
@@ -904,25 +1001,25 @@
|
|||||||
}catch{}
|
}catch{}
|
||||||
// Hooks for preserving volume / resolution
|
// Hooks for preserving volume / resolution
|
||||||
try {
|
try {
|
||||||
var keysToCache = [
|
const keysToCache = [
|
||||||
'video-quality',
|
'video-quality',
|
||||||
'video-muted',
|
'video-muted',
|
||||||
'volume',
|
'volume',
|
||||||
'lowLatencyModeEnabled',// Low Latency
|
'lowLatencyModeEnabled',// Low Latency
|
||||||
'persistenceEnabled',// Mini Player
|
'persistenceEnabled',// Mini Player
|
||||||
];
|
];
|
||||||
var cachedValues = new Map();
|
const cachedValues = new Map();
|
||||||
for (var i = 0; i < keysToCache.length; i++) {
|
for (let i = 0; i < keysToCache.length; i++) {
|
||||||
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
||||||
}
|
}
|
||||||
var realSetItem = localStorage.setItem;
|
const realSetItem = localStorage.setItem;
|
||||||
localStorage.setItem = function(key, value) {
|
localStorage.setItem = function(key, value) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
cachedValues.set(key, value);
|
cachedValues.set(key, value);
|
||||||
}
|
}
|
||||||
realSetItem.apply(this, arguments);
|
realSetItem.apply(this, arguments);
|
||||||
};
|
};
|
||||||
var realGetItem = localStorage.getItem;
|
const realGetItem = localStorage.getItem;
|
||||||
localStorage.getItem = function(key) {
|
localStorage.getItem = function(key) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
return cachedValues.get(key);
|
return cachedValues.get(key);
|
||||||
@@ -942,6 +1039,9 @@
|
|||||||
declareOptions(window);
|
declareOptions(window);
|
||||||
hookWindowWorker();
|
hookWindowWorker();
|
||||||
hookFetch();
|
hookFetch();
|
||||||
|
if (PlayerBufferingFix) {
|
||||||
|
monitorPlayerBuffering();
|
||||||
|
}
|
||||||
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
|
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
|
||||||
onContentLoaded();
|
onContentLoaded();
|
||||||
} else {
|
} else {
|
||||||
@@ -956,4 +1056,7 @@
|
|||||||
}
|
}
|
||||||
postTwitchWorkerMessage('SimulateAds', depth);
|
postTwitchWorkerMessage('SimulateAds', depth);
|
||||||
};
|
};
|
||||||
|
window.allSegmentsAreAdSegments = () => {
|
||||||
|
postTwitchWorkerMessage('AllSegmentsAreAdSegments');
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
twitch-videoad.js text/javascript
|
twitch-videoad.js text/javascript
|
||||||
(function() {
|
(function() {
|
||||||
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
|
if ( /(^|\.)twitch\.tv$/.test(document.location.hostname) === false ) { return; }
|
||||||
var ourTwitchAdSolutionsVersion = 16;// Used to prevent conflicts with outdated versions of the scripts
|
const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
|
||||||
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
||||||
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
||||||
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
@@ -10,8 +10,7 @@ twitch-videoad.js text/javascript
|
|||||||
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
function declareOptions(scope) {
|
function declareOptions(scope) {
|
||||||
// Options / globals
|
// Options / globals
|
||||||
scope.OPT_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ];
|
scope.OPT_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'autoplay-ALT', 'embed' ];
|
||||||
scope.OPT_BACKUP_PLATFORM = 'android';
|
|
||||||
scope.OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE = 'site';
|
scope.OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE = 'site';
|
||||||
scope.OPT_DISABLE_MATURE_CONTENT_POPUP = false;// If true this avoids having to log in to watch age gated content
|
scope.OPT_DISABLE_MATURE_CONTENT_POPUP = false;// If true this avoids having to log in to watch age gated content
|
||||||
scope.AD_SIGNIFIER = 'stitched-ad';
|
scope.AD_SIGNIFIER = 'stitched-ad';
|
||||||
@@ -26,26 +25,29 @@ twitch-videoad.js text/javascript
|
|||||||
scope.AuthorizationHeader = undefined;
|
scope.AuthorizationHeader = undefined;
|
||||||
scope.SimulatedAdsDepth = 0;
|
scope.SimulatedAdsDepth = 0;
|
||||||
scope.V2API = false;
|
scope.V2API = false;
|
||||||
|
scope.IsAdStrippingEnabled = true;
|
||||||
|
scope.AdSegmentCache = new Map();
|
||||||
|
scope.AllSegmentsAreAdSegments = false;
|
||||||
}
|
}
|
||||||
var localStorageHookFailed = false;
|
let localStorageHookFailed = false;
|
||||||
var adBlockDiv = null;
|
let adBlockDiv = null;
|
||||||
var twitchWorkers = [];
|
const twitchWorkers = [];
|
||||||
var workerStringConflicts = [
|
const workerStringConflicts = [
|
||||||
'twitch',
|
'twitch',
|
||||||
'isVariantA'// TwitchNoSub
|
'isVariantA'// TwitchNoSub
|
||||||
];
|
];
|
||||||
var workerStringAllow = [];
|
const workerStringAllow = [];
|
||||||
var workerStringReinsert = [
|
const workerStringReinsert = [
|
||||||
'isVariantA',// TwitchNoSub (prior to (0.9))
|
'isVariantA',// TwitchNoSub (prior to (0.9))
|
||||||
'besuper/',// TwitchNoSub (0.9)
|
'besuper/',// TwitchNoSub (0.9)
|
||||||
'${patch_url}'// TwitchNoSub (0.9.1)
|
'${patch_url}'// TwitchNoSub (0.9.1)
|
||||||
];
|
];
|
||||||
function getCleanWorker(worker) {
|
function getCleanWorker(worker) {
|
||||||
var root = null;
|
let root = null;
|
||||||
var parent = null;
|
let parent = null;
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
||||||
if (parent !== null) {
|
if (parent !== null) {
|
||||||
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
||||||
@@ -61,10 +63,10 @@ twitch-videoad.js text/javascript
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
function getWorkersForReinsert(worker) {
|
function getWorkersForReinsert(worker) {
|
||||||
var result = [];
|
const result = [];
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
||||||
result.push(proto);
|
result.push(proto);
|
||||||
} else {
|
} else {
|
||||||
@@ -74,24 +76,24 @@ twitch-videoad.js text/javascript
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
function reinsertWorkers(worker, reinsert) {
|
function reinsertWorkers(worker, reinsert) {
|
||||||
var parent = worker;
|
let parent = worker;
|
||||||
for (var i = 0; i < reinsert.length; i++) {
|
for (let i = 0; i < reinsert.length; i++) {
|
||||||
Object.setPrototypeOf(reinsert[i], parent);
|
Object.setPrototypeOf(reinsert[i], parent);
|
||||||
parent = reinsert[i];
|
parent = reinsert[i];
|
||||||
}
|
}
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
function isValidWorker(worker) {
|
function isValidWorker(worker) {
|
||||||
var workerString = worker.toString();
|
const workerString = worker.toString();
|
||||||
return !workerStringConflicts.some((x) => workerString.includes(x))
|
return !workerStringConflicts.some((x) => workerString.includes(x))
|
||||||
|| workerStringAllow.some((x) => workerString.includes(x))
|
|| workerStringAllow.some((x) => workerString.includes(x))
|
||||||
|| workerStringReinsert.some((x) => workerString.includes(x));
|
|| workerStringReinsert.some((x) => workerString.includes(x));
|
||||||
}
|
}
|
||||||
function hookWindowWorker() {
|
function hookWindowWorker() {
|
||||||
var reinsert = getWorkersForReinsert(window.Worker);
|
const reinsert = getWorkersForReinsert(window.Worker);
|
||||||
var newWorker = class Worker extends getCleanWorker(window.Worker) {
|
const newWorker = class Worker extends getCleanWorker(window.Worker) {
|
||||||
constructor(twitchBlobUrl, options) {
|
constructor(twitchBlobUrl, options) {
|
||||||
var isTwitchWorker = false;
|
let isTwitchWorker = false;
|
||||||
try {
|
try {
|
||||||
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -99,8 +101,9 @@ twitch-videoad.js text/javascript
|
|||||||
super(twitchBlobUrl, options);
|
super(twitchBlobUrl, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var newBlobStr = `
|
const newBlobStr = `
|
||||||
const pendingFetchRequests = new Map();
|
const pendingFetchRequests = new Map();
|
||||||
|
${stripAdSegments.toString()}
|
||||||
${processM3U8.toString()}
|
${processM3U8.toString()}
|
||||||
${hookWorkerFetch.toString()}
|
${hookWorkerFetch.toString()}
|
||||||
${declareOptions.toString()}
|
${declareOptions.toString()}
|
||||||
@@ -113,7 +116,7 @@ twitch-videoad.js text/javascript
|
|||||||
${getServerTimeFromM3u8.toString()}
|
${getServerTimeFromM3u8.toString()}
|
||||||
${replaceServerTimeInM3u8.toString()}
|
${replaceServerTimeInM3u8.toString()}
|
||||||
${getStreamUrlForResolution.toString()}
|
${getStreamUrlForResolution.toString()}
|
||||||
var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
||||||
declareOptions(self);
|
declareOptions(self);
|
||||||
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
|
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
|
||||||
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
||||||
@@ -145,6 +148,9 @@ twitch-videoad.js text/javascript
|
|||||||
} else if (e.data.key == 'SimulateAds') {
|
} else if (e.data.key == 'SimulateAds') {
|
||||||
SimulatedAdsDepth = e.data.value;
|
SimulatedAdsDepth = e.data.value;
|
||||||
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
||||||
|
} else if (e.data.key == 'AllSegmentsAreAdSegments') {
|
||||||
|
AllSegmentsAreAdSegments = !AllSegmentsAreAdSegments;
|
||||||
|
console.log('AllSegmentsAreAdSegments: ' + AllSegmentsAreAdSegments);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hookWorkerFetch();
|
hookWorkerFetch();
|
||||||
@@ -154,14 +160,25 @@ twitch-videoad.js text/javascript
|
|||||||
twitchWorkers.push(this);
|
twitchWorkers.push(this);
|
||||||
this.addEventListener('message', (e) => {
|
this.addEventListener('message', (e) => {
|
||||||
// NOTE: Removed adDiv caching as '.video-player' can change between streams?
|
// NOTE: Removed adDiv caching as '.video-player' can change between streams?
|
||||||
if (e.data.key == 'UboShowAdBanner') {
|
if (e.data.key == 'UboShowAdBanner' || e.data.key == 'UpdateAdBlockBannerStripping') {
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
adBlockDiv = getAdDiv();
|
adBlockDiv = getAdDiv();
|
||||||
}
|
}
|
||||||
if (adBlockDiv != null) {
|
if (adBlockDiv != null) {
|
||||||
|
if (e.data.key == 'UpdateAdBlockBannerStripping') {
|
||||||
|
const strippingAdsSuffix = ' (stripping)';
|
||||||
|
if (adBlockDiv.P.textContent.endsWith(strippingAdsSuffix)) {
|
||||||
|
if (!e.data.isStrippingAdSegments) {
|
||||||
|
adBlockDiv.P.textContent = adBlockDiv.P.textContent.substring(0, adBlockDiv.P.textContent.length - strippingAdsSuffix.length);
|
||||||
|
}
|
||||||
|
} else if (e.data.isStrippingAdSegments) {
|
||||||
|
adBlockDiv.P.textContent += strippingAdsSuffix;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
|
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
|
||||||
adBlockDiv.style.display = 'block';
|
adBlockDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (e.data.key == 'UboHideAdBanner') {
|
} else if (e.data.key == 'UboHideAdBanner') {
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
adBlockDiv = getAdDiv();
|
adBlockDiv = getAdDiv();
|
||||||
@@ -170,7 +187,7 @@ twitch-videoad.js text/javascript
|
|||||||
adBlockDiv.style.display = 'none';
|
adBlockDiv.style.display = 'none';
|
||||||
}
|
}
|
||||||
} else if (e.data.key == 'UboReloadPlayer') {
|
} else if (e.data.key == 'UboReloadPlayer') {
|
||||||
reloadTwitchPlayer();
|
reloadTwitchPlayer(false);
|
||||||
} else if (e.data.key == 'UboPauseResumePlayer') {
|
} else if (e.data.key == 'UboPauseResumePlayer') {
|
||||||
reloadTwitchPlayer(true);
|
reloadTwitchPlayer(true);
|
||||||
}
|
}
|
||||||
@@ -186,8 +203,8 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
function getAdDiv() {
|
function getAdDiv() {
|
||||||
var playerRootDiv = document.querySelector('.video-player');
|
const playerRootDiv = document.querySelector('.video-player');
|
||||||
var adDiv = null;
|
let adDiv = null;
|
||||||
if (playerRootDiv != null) {
|
if (playerRootDiv != null) {
|
||||||
adDiv = playerRootDiv.querySelector('.ubo-overlay');
|
adDiv = playerRootDiv.querySelector('.ubo-overlay');
|
||||||
if (adDiv == null) {
|
if (adDiv == null) {
|
||||||
@@ -203,7 +220,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var workerInstance = reinsertWorkers(newWorker, reinsert);
|
let workerInstance = reinsertWorkers(newWorker, reinsert);
|
||||||
Object.defineProperty(window, 'Worker', {
|
Object.defineProperty(window, 'Worker', {
|
||||||
get: function() {
|
get: function() {
|
||||||
return workerInstance;
|
return workerInstance;
|
||||||
@@ -218,23 +235,23 @@ twitch-videoad.js text/javascript
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getWasmWorkerJs(twitchBlobUrl) {
|
function getWasmWorkerJs(twitchBlobUrl) {
|
||||||
var req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open('GET', twitchBlobUrl, false);
|
req.open('GET', twitchBlobUrl, false);
|
||||||
req.overrideMimeType("text/javascript");
|
req.overrideMimeType("text/javascript");
|
||||||
req.send();
|
req.send();
|
||||||
return req.responseText;
|
return req.responseText;
|
||||||
}
|
}
|
||||||
function setStreamInfoUrls(streamInfo, encodingsM3u8) {
|
function setStreamInfoUrls(streamInfo, encodingsM3u8) {
|
||||||
var lines = encodingsM3u8.replace('\r', '').split('\n');
|
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
|
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
|
||||||
StreamInfosByUrl[lines[i].trimEnd()] = streamInfo;
|
StreamInfosByUrl[lines[i].trimEnd()] = streamInfo;
|
||||||
}
|
}
|
||||||
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(lines[i]);
|
const attributes = parseAttributes(lines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
var resolutionInfo = {
|
const resolutionInfo = {
|
||||||
Resolution: resolution,
|
Resolution: resolution,
|
||||||
FrameRate: attributes['FRAME-RATE'],
|
FrameRate: attributes['FRAME-RATE'],
|
||||||
Url: lines[i + 1]
|
Url: lines[i + 1]
|
||||||
@@ -245,37 +262,37 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
|
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
|
||||||
var result = textStr;
|
let result = textStr;
|
||||||
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
||||||
var playerTypes = OPT_BACKUP_PLAYER_TYPES;
|
const playerTypes = OPT_BACKUP_PLAYER_TYPES;
|
||||||
if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) {
|
if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) {
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) {
|
if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) {
|
||||||
var streamM3u8Url = getStreamUrlForResolution(streamInfo.BackupEncodings, resolutionInfo);
|
const streamM3u8Url = getStreamUrlForResolution(streamInfo.BackupEncodings, resolutionInfo);
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status === 200) {
|
if (streamM3u8Response.status === 200) {
|
||||||
return await streamM3u8Response.text();
|
return await streamM3u8Response.text();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var backupPlayerTypeInfo = '';
|
let backupPlayerTypeInfo = '';
|
||||||
for (var i = 0; i < playerTypes.length; i++) {
|
for (let i = 0; i < playerTypes.length; i++) {
|
||||||
var playerType = playerTypes[i];
|
const playerType = playerTypes[i];
|
||||||
if (!streamInfo.BackupEncodingsStatus.has(playerType)) {
|
if (!streamInfo.BackupEncodingsStatus.has(playerType)) {
|
||||||
try {
|
try {
|
||||||
var accessTokenResponse = await getAccessToken(streamInfo.ChannelName, playerType, OPT_BACKUP_PLATFORM);
|
const accessTokenResponse = await getAccessToken(streamInfo.ChannelName, playerType);
|
||||||
if (accessTokenResponse != null && accessTokenResponse.status === 200) {
|
if (accessTokenResponse != null && accessTokenResponse.status === 200) {
|
||||||
var accessToken = await accessTokenResponse.json();
|
const accessToken = await accessTokenResponse.json();
|
||||||
var urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
const urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
||||||
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
||||||
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
||||||
var encodingsM3u8Response = await realFetch(urlInfo.href);
|
const encodingsM3u8Response = await realFetch(urlInfo.href);
|
||||||
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
||||||
var encodingsM3u8 = await encodingsM3u8Response.text();
|
let encodingsM3u8 = await encodingsM3u8Response.text();
|
||||||
var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo);
|
const streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo);
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status === 200) {
|
if (streamM3u8Response.status === 200) {
|
||||||
var backTextStr = await streamM3u8Response.text();
|
const backTextStr = await streamM3u8Response.text();
|
||||||
if ((!backTextStr.includes(AD_SIGNIFIER) && (SimulatedAdsDepth == 0 || i >= SimulatedAdsDepth - 1)) || i >= playerTypes.length - 1) {
|
if ((!backTextStr.includes(AD_SIGNIFIER) && (SimulatedAdsDepth == 0 || i >= SimulatedAdsDepth - 1)) || i >= playerTypes.length - 1) {
|
||||||
result = backTextStr;
|
result = backTextStr;
|
||||||
backupPlayerTypeInfo = ' (' + playerType + ')';
|
backupPlayerTypeInfo = ' (' + playerType + ')';
|
||||||
@@ -283,14 +300,14 @@ twitch-videoad.js text/javascript
|
|||||||
streamInfo.BackupEncodingsPlayerTypeIndex = i;
|
streamInfo.BackupEncodingsPlayerTypeIndex = i;
|
||||||
if (streamInfo.Encodings != null) {
|
if (streamInfo.Encodings != null) {
|
||||||
// Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the low res URLs into the main m3u8
|
// Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the low res URLs into the main m3u8
|
||||||
var normalEncodingsM3u8 = streamInfo.Encodings;
|
const normalEncodingsM3u8 = streamInfo.Encodings;
|
||||||
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
|
const normalLines = normalEncodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var j = 0; j < normalLines.length - 1; j++) {
|
for (let j = 0; j < normalLines.length - 1; j++) {
|
||||||
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
const resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
||||||
var lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
|
const lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
|
||||||
var lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
|
const lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
|
||||||
var lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
|
const lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
|
||||||
//console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
|
//console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
|
||||||
const codecsKey = 'CODECS';
|
const codecsKey = 'CODECS';
|
||||||
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
||||||
@@ -328,26 +345,59 @@ twitch-videoad.js text/javascript
|
|||||||
postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
|
postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
function stripAdSegments(textStr, stripAllSegments) {
|
||||||
|
let hasStrippedAdSegments = false;
|
||||||
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
|
const newAdUrl = 'https://twitch.tv';
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i];
|
||||||
|
// Remove tracking urls which appear in the overlay UI
|
||||||
|
line = line
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`)
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
|
||||||
|
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
|
||||||
|
const segmentUrl = lines[i + 1];
|
||||||
|
AdSegmentCache.set(segmentUrl, Date.now());
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
if (line.includes(AD_SIGNIFIER)) {
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStrippedAdSegments) {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
// No low latency during ads (otherwise it's possible for the player to prefetch and display ad segments)
|
||||||
|
if (lines[i].startsWith('#EXT-X-TWITCH-PREFETCH:')) {
|
||||||
|
lines[i] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AdSegmentCache.forEach((key, value, map) => {
|
||||||
|
if (value < Date.now() - 120000) {
|
||||||
|
map.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
postMessage({
|
||||||
|
key: 'UpdateAdBlockBannerStripping',
|
||||||
|
isStrippingAdSegments: hasStrippedAdSegments
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
async function processM3U8(url, textStr, realFetch) {
|
async function processM3U8(url, textStr, realFetch) {
|
||||||
var streamInfo = StreamInfosByUrl[url];
|
const streamInfo = StreamInfosByUrl[url];
|
||||||
if (streamInfo == null) {
|
if (!streamInfo) {
|
||||||
//console.log('Unknown stream url ' + url);
|
|
||||||
//postMessage({key:'UboHideAdBanner'});
|
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
var currentResolution = streamInfo.Urls.get(url);
|
const currentResolution = streamInfo.Urls.get(url);
|
||||||
if (!currentResolution) {
|
if (!currentResolution) {
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
var haveAdTags = textStr.includes(AD_SIGNIFIER);
|
const haveAdTags = textStr.includes(AD_SIGNIFIER) || (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex));
|
||||||
if (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex)) {
|
|
||||||
haveAdTags = true;
|
|
||||||
}
|
|
||||||
if (streamInfo.BackupEncodings) {
|
if (streamInfo.BackupEncodings) {
|
||||||
var streamM3u8Url = streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0];
|
const streamM3u8Url = streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0];
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status == 200) {
|
if (streamM3u8Response.status == 200) {
|
||||||
var streamM3u8 = await streamM3u8Response.text();
|
const streamM3u8 = await streamM3u8Response.text();
|
||||||
if (streamM3u8 != null) {
|
if (streamM3u8 != null) {
|
||||||
if (!streamM3u8.includes(AD_SIGNIFIER) && SimulatedAdsDepth == 0) {
|
if (!streamM3u8.includes(AD_SIGNIFIER) && SimulatedAdsDepth == 0) {
|
||||||
console.log('No more ads on main stream. Triggering player reload to go back to main stream...');
|
console.log('No more ads on main stream. Triggering player reload to go back to main stream...');
|
||||||
@@ -358,9 +408,9 @@ twitch-videoad.js text/javascript
|
|||||||
postMessage({key:'UboHideAdBanner'});
|
postMessage({key:'UboHideAdBanner'});
|
||||||
postMessage({key:'UboReloadPlayer'});
|
postMessage({key:'UboReloadPlayer'});
|
||||||
} else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
|
} else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
|
||||||
var lines = streamM3u8.replace('\r', '').split('\n');
|
const lines = streamM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const line = lines[i];
|
||||||
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
||||||
if (!line.includes(LIVE_SIGNIFIER) && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
if (!line.includes(LIVE_SIGNIFIER) && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
||||||
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
||||||
@@ -382,19 +432,34 @@ twitch-videoad.js text/javascript
|
|||||||
} else {
|
} else {
|
||||||
postMessage({key:'UboHideAdBanner'});
|
postMessage({key:'UboHideAdBanner'});
|
||||||
}
|
}
|
||||||
|
if (IsAdStrippingEnabled) {
|
||||||
|
textStr = stripAdSegments(textStr);
|
||||||
|
}
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
function hookWorkerFetch() {
|
function hookWorkerFetch() {
|
||||||
console.log('hookWorkerFetch (video-swap-new)');
|
console.log('hookWorkerFetch (video-swap-new)');
|
||||||
var realFetch = fetch;
|
const realFetch = fetch;
|
||||||
fetch = async function(url, options) {
|
fetch = async function(url, options) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
|
if (AdSegmentCache.has(url)) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const send = function() {
|
||||||
|
return realFetch('data:video/mp4;base64,AAAAKGZ0eXBtcDQyAAAAAWlzb21tcDQyZGFzaGF2YzFpc282aGxzZgAABEltb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAYagAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAABqHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAURtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAADvbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAACzc3RibAAAAGdzdHNkAAAAAAAAAAEAAABXbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAAzZXNkcwAAAAADgICAIgABAASAgIAUQBUAAAAAAAAAAAAAAAWAgIACEZAGgICAAQIAAAAQc3R0cwAAAAAAAAAAAAAAEHN0c2MAAAAAAAAAAAAAABRzdHN6AAAAAAAAAAAAAAAAAAAAEHN0Y28AAAAAAAAAAAAAAeV0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAoAAAAFoAAAAAAGBbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAA9CQAAAAABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABLG1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAOxzdGJsAAAAoHN0c2QAAAAAAAAAAQAAAJBhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAoABaABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAOmF2Y0MBTUAe/+EAI2dNQB6WUoFAX/LgLUBAQFAAAD6AAA6mDgAAHoQAA9CW7y4KAQAEaOuPIAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAASG12ZXgAAAAgdHJleAAAAAAAAAABAAAAAQAAAC4AAAAAAoAAAAAAACB0cmV4AAAAAAAAAAIAAAABAACCNQAAAAACQAAA', options).then(function(response) {
|
||||||
|
resolve(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
}
|
||||||
url = url.trimEnd();
|
url = url.trimEnd();
|
||||||
if (url.endsWith('m3u8')) {
|
if (url.endsWith('m3u8')) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var processAfter = async function(response) {
|
const processAfter = async function(response) {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
var str = await processM3U8(url, await response.text(), realFetch);
|
const str = await processM3U8(url, await response.text(), realFetch);
|
||||||
resolve(new Response(str, {
|
resolve(new Response(str, {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
@@ -404,7 +469,7 @@ twitch-videoad.js text/javascript
|
|||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var send = function() {
|
const send = function() {
|
||||||
return realFetch(url, options).then(function(response) {
|
return realFetch(url, options).then(function(response) {
|
||||||
processAfter(response);
|
processAfter(response);
|
||||||
})['catch'](function(err) {
|
})['catch'](function(err) {
|
||||||
@@ -417,22 +482,22 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
||||||
V2API = url.includes('/api/v2/');
|
V2API = url.includes('/api/v2/');
|
||||||
var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
const channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||||
if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
|
if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
|
||||||
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
||||||
var tempUrl = new URL(url);
|
const tempUrl = new URL(url);
|
||||||
tempUrl.searchParams.delete('parent_domains');
|
tempUrl.searchParams.delete('parent_domains');
|
||||||
url = tempUrl.toString();
|
url = tempUrl.toString();
|
||||||
}
|
}
|
||||||
return new Promise(async function(resolve, reject) {
|
return new Promise(async function(resolve, reject) {
|
||||||
// - First m3u8 request is the m3u8 with the video encodings (360p,480p,720p,etc).
|
// - First m3u8 request is the m3u8 with the video encodings (360p,480p,720p,etc).
|
||||||
// - Second m3u8 request is the m3u8 for the given encoding obtained in the first request. At this point we will know if there's ads.
|
// - Second m3u8 request is the m3u8 for the given encoding obtained in the first request. At this point we will know if there's ads.
|
||||||
var streamInfo = StreamInfos[channelName];
|
let streamInfo = StreamInfos[channelName];
|
||||||
if (streamInfo != null && streamInfo.Encodings != null && (await realFetch(streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
if (streamInfo != null && streamInfo.Encodings != null && (await realFetch(streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
||||||
// The cached encodings are dead (the stream probably restarted)
|
// The cached encodings are dead (the stream probably restarted)
|
||||||
streamInfo = null;
|
streamInfo = null;
|
||||||
}
|
}
|
||||||
var serverTime = null;
|
let serverTime = null;
|
||||||
if (streamInfo == null || streamInfo.Encodings == null) {
|
if (streamInfo == null || streamInfo.Encodings == null) {
|
||||||
StreamInfos[channelName] = streamInfo = {
|
StreamInfos[channelName] = streamInfo = {
|
||||||
RequestedAds: new Set(),
|
RequestedAds: new Set(),
|
||||||
@@ -447,16 +512,16 @@ twitch-videoad.js text/javascript
|
|||||||
UsherParams: (new URL(url)).search,
|
UsherParams: (new URL(url)).search,
|
||||||
Urls: new Map(),
|
Urls: new Map(),
|
||||||
};
|
};
|
||||||
var encodingsM3u8Response = await realFetch(url, options);
|
const encodingsM3u8Response = await realFetch(url, options);
|
||||||
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
||||||
var encodingsM3u8 = await encodingsM3u8Response.text();
|
const encodingsM3u8 = await encodingsM3u8Response.text();
|
||||||
streamInfo.Encodings = encodingsM3u8;
|
streamInfo.Encodings = encodingsM3u8;
|
||||||
setStreamInfoUrls(streamInfo, encodingsM3u8);
|
setStreamInfoUrls(streamInfo, encodingsM3u8);
|
||||||
serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
||||||
var resolutionInfo = streamInfo.Urls.values().next().value;
|
const resolutionInfo = streamInfo.Urls.values().next().value;
|
||||||
var streamM3u8Response = await realFetch(resolutionInfo.Url);
|
const streamM3u8Response = await realFetch(resolutionInfo.Url);
|
||||||
if (streamM3u8Response.status == 200) {
|
if (streamM3u8Response.status == 200) {
|
||||||
var streamM3u8 = await streamM3u8Response.text();
|
const streamM3u8 = await streamM3u8Response.text();
|
||||||
if (streamM3u8.includes(AD_SIGNIFIER) || SimulatedAdsDepth > 0) {
|
if (streamM3u8.includes(AD_SIGNIFIER) || SimulatedAdsDepth > 0) {
|
||||||
await onFoundAd(streamInfo, streamM3u8, false, realFetch, resolutionInfo.Url, resolutionInfo);
|
await onFoundAd(streamInfo, streamM3u8, false, realFetch, resolutionInfo.Url, resolutionInfo);
|
||||||
}
|
}
|
||||||
@@ -470,7 +535,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!serverTime) {
|
if (!serverTime) {
|
||||||
var encodingsM3u8Response = await realFetch(url, options);
|
const encodingsM3u8Response = await realFetch(url, options);
|
||||||
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
||||||
serverTime = getServerTimeFromM3u8(await encodingsM3u8Response.text());
|
serverTime = getServerTimeFromM3u8(await encodingsM3u8Response.text());
|
||||||
}
|
}
|
||||||
@@ -485,10 +550,10 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
function getServerTimeFromM3u8(encodingsM3u8) {
|
function getServerTimeFromM3u8(encodingsM3u8) {
|
||||||
if (V2API) {
|
if (V2API) {
|
||||||
var matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
const matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
var matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
const matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
||||||
@@ -498,17 +563,17 @@ twitch-videoad.js text/javascript
|
|||||||
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
||||||
}
|
}
|
||||||
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
||||||
var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
|
const encodingsLines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
||||||
var matchedResolutionUrl = null;
|
let matchedResolutionUrl = null;
|
||||||
var matchedFrameRate = false;
|
let matchedFrameRate = false;
|
||||||
var closestResolutionUrl = null;
|
let closestResolutionUrl = null;
|
||||||
var closestResolutionDifference = Infinity;
|
let closestResolutionDifference = Infinity;
|
||||||
for (var i = 0; i < encodingsLines.length - 1; i++) {
|
for (let i = 0; i < encodingsLines.length - 1; i++) {
|
||||||
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(encodingsLines[i]);
|
const attributes = parseAttributes(encodingsLines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
var frameRate = attributes['FRAME-RATE'];
|
const frameRate = attributes['FRAME-RATE'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
||||||
matchedResolutionUrl = encodingsLines[i + 1];
|
matchedResolutionUrl = encodingsLines[i + 1];
|
||||||
@@ -518,7 +583,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [width, height] = resolution.split('x').map(Number);
|
const [width, height] = resolution.split('x').map(Number);
|
||||||
var difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
const difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
||||||
if (difference < closestResolutionDifference) {
|
if (difference < closestResolutionDifference) {
|
||||||
closestResolutionUrl = encodingsLines[i + 1];
|
closestResolutionUrl = encodingsLines[i + 1];
|
||||||
closestResolutionDifference = difference;
|
closestResolutionDifference = difference;
|
||||||
@@ -528,21 +593,23 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
return closestResolutionUrl.trimEnd();
|
return closestResolutionUrl.trimEnd();
|
||||||
}
|
}
|
||||||
function getAccessToken(channelName, playerType, platform) {
|
function getAccessToken(channelName, playerType) {
|
||||||
if (!platform) {
|
const realPlayerType = playerType.replace('-ALT', '');
|
||||||
platform = 'web';
|
const body = {
|
||||||
}
|
operationName: 'PlaybackAccessToken',
|
||||||
var body = null;
|
|
||||||
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "' + platform + '", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "' + platform + '", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
|
|
||||||
body = {
|
|
||||||
operationName: 'PlaybackAccessToken_Template',
|
|
||||||
query: templateQuery,
|
|
||||||
variables: {
|
variables: {
|
||||||
'isLive': true,
|
isLive: true,
|
||||||
'login': channelName,
|
login: channelName,
|
||||||
'isVod': false,
|
isVod: false,
|
||||||
'vodID': '',
|
vodID: "",
|
||||||
'playerType': playerType
|
playerType: realPlayerType,
|
||||||
|
platform: realPlayerType == 'autoplay' ? 'android' : 'web'
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
persistedQuery: {
|
||||||
|
version:1,
|
||||||
|
sha256Hash:"ed230aa1e33e07eebb8928504583da78a5173989fadfb1ac94be06a04f3cdbe9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return gqlRequest(body, playerType);
|
return gqlRequest(body, playerType);
|
||||||
@@ -554,14 +621,13 @@ twitch-videoad.js text/javascript
|
|||||||
gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));
|
gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var headers = {
|
let headers = {
|
||||||
'Client-Id': CLIENT_ID,
|
'Client-Id': CLIENT_ID,
|
||||||
'Device-ID': gql_device_id,
|
|
||||||
'X-Device-Id': gql_device_id,
|
'X-Device-Id': gql_device_id,
|
||||||
'Authorization': AuthorizationHeader,
|
'Authorization': AuthorizationHeader,
|
||||||
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader})
|
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader})
|
||||||
};
|
};
|
||||||
if (playerType != 'site') {
|
if (playerType.includes('-ALT')) {
|
||||||
headers = {
|
headers = {
|
||||||
'Client-Id': CLIENT_ID,
|
'Client-Id': CLIENT_ID,
|
||||||
'X-Device-Id': gql_device_id
|
'X-Device-Id': gql_device_id
|
||||||
@@ -625,12 +691,12 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hookFetch() {
|
function hookFetch() {
|
||||||
var realFetch = window.fetch;
|
const realFetch = window.fetch;
|
||||||
window.realFetch = realFetch;
|
window.realFetch = realFetch;
|
||||||
window.fetch = function(url, init, ...args) {
|
window.fetch = function(url, init, ...args) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
if (url.includes('gql')) {
|
if (url.includes('gql')) {
|
||||||
var deviceId = init.headers['X-Device-Id'];
|
let deviceId = init.headers['X-Device-Id'];
|
||||||
if (typeof deviceId !== 'string') {
|
if (typeof deviceId !== 'string') {
|
||||||
deviceId = init.headers['Device-ID'];
|
deviceId = init.headers['Device-ID'];
|
||||||
}
|
}
|
||||||
@@ -668,7 +734,7 @@ twitch-videoad.js text/javascript
|
|||||||
if (OPT_DISABLE_MATURE_CONTENT_POPUP) {
|
if (OPT_DISABLE_MATURE_CONTENT_POPUP) {
|
||||||
const newBody2 = JSON.parse(init.body);
|
const newBody2 = JSON.parse(init.body);
|
||||||
if (Array.isArray(newBody2)) {
|
if (Array.isArray(newBody2)) {
|
||||||
var hasRemovedClassification = false;
|
let hasRemovedClassification = false;
|
||||||
for (let i = 0; i < newBody2.length; i++) {
|
for (let i = 0; i < newBody2.length; i++) {
|
||||||
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
||||||
hasRemovedClassification = true;
|
hasRemovedClassification = true;
|
||||||
@@ -686,7 +752,7 @@ twitch-videoad.js text/javascript
|
|||||||
return realFetch.apply(this, arguments);
|
return realFetch.apply(this, arguments);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function reloadTwitchPlayer(isSeek, isPausePlay) {
|
function reloadTwitchPlayer(isPausePlay) {
|
||||||
// Taken from ttv-tools / ffz
|
// Taken from ttv-tools / ffz
|
||||||
// https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
|
// https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
|
||||||
// https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
|
// https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
|
||||||
@@ -705,27 +771,27 @@ twitch-videoad.js text/javascript
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function findReactRootNode() {
|
function findReactRootNode() {
|
||||||
var reactRootNode = null;
|
let reactRootNode = null;
|
||||||
var rootNode = document.querySelector('#root');
|
const rootNode = document.querySelector('#root');
|
||||||
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
||||||
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
||||||
}
|
}
|
||||||
if (reactRootNode == null) {
|
if (reactRootNode == null) {
|
||||||
var containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
||||||
if (containerName != null) {
|
if (containerName != null) {
|
||||||
reactRootNode = rootNode[containerName];
|
reactRootNode = rootNode[containerName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reactRootNode;
|
return reactRootNode;
|
||||||
}
|
}
|
||||||
var reactRootNode = findReactRootNode();
|
const reactRootNode = findReactRootNode();
|
||||||
if (!reactRootNode) {
|
if (!reactRootNode) {
|
||||||
console.log('Could not find react root');
|
console.log('Could not find react root');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
||||||
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
||||||
var playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
console.log('Could not find player');
|
console.log('Could not find player');
|
||||||
return;
|
return;
|
||||||
@@ -734,14 +800,7 @@ twitch-videoad.js text/javascript
|
|||||||
console.log('Could not find player state');
|
console.log('Could not find player state');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (player.paused || player.core?.paused) {
|
if (player.isPaused() || player.core?.paused) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isSeek) {
|
|
||||||
console.log('Force seek to reset player (hopefully fixing any audio desync) pos:' + player.getPosition() + ' range:' + JSON.stringify(player.getBuffered()));
|
|
||||||
var pos = player.getPosition();
|
|
||||||
player.seekTo(0);
|
|
||||||
player.seekTo(pos);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPausePlay) {
|
if (isPausePlay) {
|
||||||
@@ -752,9 +811,9 @@ twitch-videoad.js text/javascript
|
|||||||
const lsKeyQuality = 'video-quality';
|
const lsKeyQuality = 'video-quality';
|
||||||
const lsKeyMuted = 'video-muted';
|
const lsKeyMuted = 'video-muted';
|
||||||
const lsKeyVolume = 'volume';
|
const lsKeyVolume = 'volume';
|
||||||
var currentQualityLS = null;
|
let currentQualityLS = null;
|
||||||
var currentMutedLS = null;
|
let currentMutedLS = null;
|
||||||
var currentVolumeLS = null;
|
let currentVolumeLS = null;
|
||||||
try {
|
try {
|
||||||
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
||||||
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
||||||
@@ -804,13 +863,13 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}catch{}
|
}catch{}
|
||||||
var block = e => {
|
const block = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
};
|
};
|
||||||
let wasVideoPlaying = true;
|
let wasVideoPlaying = true;
|
||||||
var visibilityChange = e => {
|
const visibilityChange = e => {
|
||||||
if (typeof chrome !== 'undefined') {
|
if (typeof chrome !== 'undefined') {
|
||||||
const videos = document.getElementsByTagName('video');
|
const videos = document.getElementsByTagName('video');
|
||||||
if (videos.length > 0) {
|
if (videos.length > 0) {
|
||||||
@@ -844,25 +903,25 @@ twitch-videoad.js text/javascript
|
|||||||
}catch{}
|
}catch{}
|
||||||
// Hooks for preserving volume / resolution
|
// Hooks for preserving volume / resolution
|
||||||
try {
|
try {
|
||||||
var keysToCache = [
|
const keysToCache = [
|
||||||
'video-quality',
|
'video-quality',
|
||||||
'video-muted',
|
'video-muted',
|
||||||
'volume',
|
'volume',
|
||||||
'lowLatencyModeEnabled',// Low Latency
|
'lowLatencyModeEnabled',// Low Latency
|
||||||
'persistenceEnabled',// Mini Player
|
'persistenceEnabled',// Mini Player
|
||||||
];
|
];
|
||||||
var cachedValues = new Map();
|
const cachedValues = new Map();
|
||||||
for (var i = 0; i < keysToCache.length; i++) {
|
for (let i = 0; i < keysToCache.length; i++) {
|
||||||
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
||||||
}
|
}
|
||||||
var realSetItem = localStorage.setItem;
|
const realSetItem = localStorage.setItem;
|
||||||
localStorage.setItem = function(key, value) {
|
localStorage.setItem = function(key, value) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
cachedValues.set(key, value);
|
cachedValues.set(key, value);
|
||||||
}
|
}
|
||||||
realSetItem.apply(this, arguments);
|
realSetItem.apply(this, arguments);
|
||||||
};
|
};
|
||||||
var realGetItem = localStorage.getItem;
|
const realGetItem = localStorage.getItem;
|
||||||
localStorage.getItem = function(key) {
|
localStorage.getItem = function(key) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
return cachedValues.get(key);
|
return cachedValues.get(key);
|
||||||
@@ -897,4 +956,7 @@ twitch-videoad.js text/javascript
|
|||||||
}
|
}
|
||||||
postTwitchWorkerMessage('SimulateAds', depth);
|
postTwitchWorkerMessage('SimulateAds', depth);
|
||||||
};
|
};
|
||||||
|
window.allSegmentsAreAdSegments = () => {
|
||||||
|
postTwitchWorkerMessage('AllSegmentsAreAdSegments');
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name TwitchAdSolutions (video-swap-new)
|
// @name TwitchAdSolutions (video-swap-new)
|
||||||
// @namespace https://github.com/pixeltris/TwitchAdSolutions
|
// @namespace https://github.com/pixeltris/TwitchAdSolutions
|
||||||
// @version 1.48
|
// @version 1.49
|
||||||
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
|
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
|
||||||
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
|
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap-new/video-swap-new.user.js
|
||||||
// @description Multiple solutions for blocking Twitch ads (video-swap-new)
|
// @description Multiple solutions for blocking Twitch ads (video-swap-new)
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
var ourTwitchAdSolutionsVersion = 16;// Used to prevent conflicts with outdated versions of the scripts
|
const ourTwitchAdSolutionsVersion = 17;// Used to prevent conflicts with outdated versions of the scripts
|
||||||
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
if (typeof window.twitchAdSolutionsVersion !== 'undefined' && window.twitchAdSolutionsVersion >= ourTwitchAdSolutionsVersion) {
|
||||||
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
console.log("skipping video-swap-new as there's another script active. ourVersion:" + ourTwitchAdSolutionsVersion + " activeVersion:" + window.twitchAdSolutionsVersion);
|
||||||
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
window.twitchAdSolutionsVersion = ourTwitchAdSolutionsVersion;
|
||||||
function declareOptions(scope) {
|
function declareOptions(scope) {
|
||||||
// Options / globals
|
// Options / globals
|
||||||
scope.OPT_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'embed' ];
|
scope.OPT_BACKUP_PLAYER_TYPES = [ 'autoplay', 'picture-by-picture', 'autoplay-ALT', 'embed' ];
|
||||||
scope.OPT_BACKUP_PLATFORM = 'android';
|
|
||||||
scope.OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE = 'site';
|
scope.OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE = 'site';
|
||||||
scope.OPT_DISABLE_MATURE_CONTENT_POPUP = false;// If true this avoids having to log in to watch age gated content
|
scope.OPT_DISABLE_MATURE_CONTENT_POPUP = false;// If true this avoids having to log in to watch age gated content
|
||||||
scope.AD_SIGNIFIER = 'stitched-ad';
|
scope.AD_SIGNIFIER = 'stitched-ad';
|
||||||
@@ -38,26 +37,29 @@
|
|||||||
scope.AuthorizationHeader = undefined;
|
scope.AuthorizationHeader = undefined;
|
||||||
scope.SimulatedAdsDepth = 0;
|
scope.SimulatedAdsDepth = 0;
|
||||||
scope.V2API = false;
|
scope.V2API = false;
|
||||||
|
scope.IsAdStrippingEnabled = true;
|
||||||
|
scope.AdSegmentCache = new Map();
|
||||||
|
scope.AllSegmentsAreAdSegments = false;
|
||||||
}
|
}
|
||||||
var localStorageHookFailed = false;
|
let localStorageHookFailed = false;
|
||||||
var adBlockDiv = null;
|
let adBlockDiv = null;
|
||||||
var twitchWorkers = [];
|
const twitchWorkers = [];
|
||||||
var workerStringConflicts = [
|
const workerStringConflicts = [
|
||||||
'twitch',
|
'twitch',
|
||||||
'isVariantA'// TwitchNoSub
|
'isVariantA'// TwitchNoSub
|
||||||
];
|
];
|
||||||
var workerStringAllow = [];
|
const workerStringAllow = [];
|
||||||
var workerStringReinsert = [
|
const workerStringReinsert = [
|
||||||
'isVariantA',// TwitchNoSub (prior to (0.9))
|
'isVariantA',// TwitchNoSub (prior to (0.9))
|
||||||
'besuper/',// TwitchNoSub (0.9)
|
'besuper/',// TwitchNoSub (0.9)
|
||||||
'${patch_url}'// TwitchNoSub (0.9.1)
|
'${patch_url}'// TwitchNoSub (0.9.1)
|
||||||
];
|
];
|
||||||
function getCleanWorker(worker) {
|
function getCleanWorker(worker) {
|
||||||
var root = null;
|
let root = null;
|
||||||
var parent = null;
|
let parent = null;
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
if (workerStringConflicts.some((x) => workerString.includes(x)) && !workerStringAllow.some((x) => workerString.includes(x))) {
|
||||||
if (parent !== null) {
|
if (parent !== null) {
|
||||||
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
Object.setPrototypeOf(parent, Object.getPrototypeOf(proto));
|
||||||
@@ -73,10 +75,10 @@
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
function getWorkersForReinsert(worker) {
|
function getWorkersForReinsert(worker) {
|
||||||
var result = [];
|
const result = [];
|
||||||
var proto = worker;
|
let proto = worker;
|
||||||
while (proto) {
|
while (proto) {
|
||||||
var workerString = proto.toString();
|
const workerString = proto.toString();
|
||||||
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
if (workerStringReinsert.some((x) => workerString.includes(x))) {
|
||||||
result.push(proto);
|
result.push(proto);
|
||||||
} else {
|
} else {
|
||||||
@@ -86,24 +88,24 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
function reinsertWorkers(worker, reinsert) {
|
function reinsertWorkers(worker, reinsert) {
|
||||||
var parent = worker;
|
let parent = worker;
|
||||||
for (var i = 0; i < reinsert.length; i++) {
|
for (let i = 0; i < reinsert.length; i++) {
|
||||||
Object.setPrototypeOf(reinsert[i], parent);
|
Object.setPrototypeOf(reinsert[i], parent);
|
||||||
parent = reinsert[i];
|
parent = reinsert[i];
|
||||||
}
|
}
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
function isValidWorker(worker) {
|
function isValidWorker(worker) {
|
||||||
var workerString = worker.toString();
|
const workerString = worker.toString();
|
||||||
return !workerStringConflicts.some((x) => workerString.includes(x))
|
return !workerStringConflicts.some((x) => workerString.includes(x))
|
||||||
|| workerStringAllow.some((x) => workerString.includes(x))
|
|| workerStringAllow.some((x) => workerString.includes(x))
|
||||||
|| workerStringReinsert.some((x) => workerString.includes(x));
|
|| workerStringReinsert.some((x) => workerString.includes(x));
|
||||||
}
|
}
|
||||||
function hookWindowWorker() {
|
function hookWindowWorker() {
|
||||||
var reinsert = getWorkersForReinsert(window.Worker);
|
const reinsert = getWorkersForReinsert(window.Worker);
|
||||||
var newWorker = class Worker extends getCleanWorker(window.Worker) {
|
const newWorker = class Worker extends getCleanWorker(window.Worker) {
|
||||||
constructor(twitchBlobUrl, options) {
|
constructor(twitchBlobUrl, options) {
|
||||||
var isTwitchWorker = false;
|
let isTwitchWorker = false;
|
||||||
try {
|
try {
|
||||||
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
isTwitchWorker = new URL(twitchBlobUrl).origin.endsWith('.twitch.tv');
|
||||||
} catch {}
|
} catch {}
|
||||||
@@ -111,8 +113,9 @@
|
|||||||
super(twitchBlobUrl, options);
|
super(twitchBlobUrl, options);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var newBlobStr = `
|
const newBlobStr = `
|
||||||
const pendingFetchRequests = new Map();
|
const pendingFetchRequests = new Map();
|
||||||
|
${stripAdSegments.toString()}
|
||||||
${processM3U8.toString()}
|
${processM3U8.toString()}
|
||||||
${hookWorkerFetch.toString()}
|
${hookWorkerFetch.toString()}
|
||||||
${declareOptions.toString()}
|
${declareOptions.toString()}
|
||||||
@@ -125,7 +128,7 @@
|
|||||||
${getServerTimeFromM3u8.toString()}
|
${getServerTimeFromM3u8.toString()}
|
||||||
${replaceServerTimeInM3u8.toString()}
|
${replaceServerTimeInM3u8.toString()}
|
||||||
${getStreamUrlForResolution.toString()}
|
${getStreamUrlForResolution.toString()}
|
||||||
var workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
const workerString = getWasmWorkerJs('${twitchBlobUrl.replaceAll("'", "%27")}');
|
||||||
declareOptions(self);
|
declareOptions(self);
|
||||||
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
|
gql_device_id = ${gql_device_id ? "'" + gql_device_id + "'" : null};
|
||||||
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
AuthorizationHeader = ${AuthorizationHeader ? "'" + AuthorizationHeader + "'" : undefined};
|
||||||
@@ -157,6 +160,9 @@
|
|||||||
} else if (e.data.key == 'SimulateAds') {
|
} else if (e.data.key == 'SimulateAds') {
|
||||||
SimulatedAdsDepth = e.data.value;
|
SimulatedAdsDepth = e.data.value;
|
||||||
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
console.log('SimulatedAdsDepth: ' + SimulatedAdsDepth);
|
||||||
|
} else if (e.data.key == 'AllSegmentsAreAdSegments') {
|
||||||
|
AllSegmentsAreAdSegments = !AllSegmentsAreAdSegments;
|
||||||
|
console.log('AllSegmentsAreAdSegments: ' + AllSegmentsAreAdSegments);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hookWorkerFetch();
|
hookWorkerFetch();
|
||||||
@@ -166,14 +172,25 @@
|
|||||||
twitchWorkers.push(this);
|
twitchWorkers.push(this);
|
||||||
this.addEventListener('message', (e) => {
|
this.addEventListener('message', (e) => {
|
||||||
// NOTE: Removed adDiv caching as '.video-player' can change between streams?
|
// NOTE: Removed adDiv caching as '.video-player' can change between streams?
|
||||||
if (e.data.key == 'UboShowAdBanner') {
|
if (e.data.key == 'UboShowAdBanner' || e.data.key == 'UpdateAdBlockBannerStripping') {
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
adBlockDiv = getAdDiv();
|
adBlockDiv = getAdDiv();
|
||||||
}
|
}
|
||||||
if (adBlockDiv != null) {
|
if (adBlockDiv != null) {
|
||||||
|
if (e.data.key == 'UpdateAdBlockBannerStripping') {
|
||||||
|
const strippingAdsSuffix = ' (stripping)';
|
||||||
|
if (adBlockDiv.P.textContent.endsWith(strippingAdsSuffix)) {
|
||||||
|
if (!e.data.isStrippingAdSegments) {
|
||||||
|
adBlockDiv.P.textContent = adBlockDiv.P.textContent.substring(0, adBlockDiv.P.textContent.length - strippingAdsSuffix.length);
|
||||||
|
}
|
||||||
|
} else if (e.data.isStrippingAdSegments) {
|
||||||
|
adBlockDiv.P.textContent += strippingAdsSuffix;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
|
adBlockDiv.P.textContent = 'Blocking' + (e.data.isMidroll ? ' midroll' : '') + ' ads';
|
||||||
adBlockDiv.style.display = 'block';
|
adBlockDiv.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (e.data.key == 'UboHideAdBanner') {
|
} else if (e.data.key == 'UboHideAdBanner') {
|
||||||
if (adBlockDiv == null) {
|
if (adBlockDiv == null) {
|
||||||
adBlockDiv = getAdDiv();
|
adBlockDiv = getAdDiv();
|
||||||
@@ -182,7 +199,7 @@
|
|||||||
adBlockDiv.style.display = 'none';
|
adBlockDiv.style.display = 'none';
|
||||||
}
|
}
|
||||||
} else if (e.data.key == 'UboReloadPlayer') {
|
} else if (e.data.key == 'UboReloadPlayer') {
|
||||||
reloadTwitchPlayer();
|
reloadTwitchPlayer(false);
|
||||||
} else if (e.data.key == 'UboPauseResumePlayer') {
|
} else if (e.data.key == 'UboPauseResumePlayer') {
|
||||||
reloadTwitchPlayer(true);
|
reloadTwitchPlayer(true);
|
||||||
}
|
}
|
||||||
@@ -198,8 +215,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
function getAdDiv() {
|
function getAdDiv() {
|
||||||
var playerRootDiv = document.querySelector('.video-player');
|
const playerRootDiv = document.querySelector('.video-player');
|
||||||
var adDiv = null;
|
let adDiv = null;
|
||||||
if (playerRootDiv != null) {
|
if (playerRootDiv != null) {
|
||||||
adDiv = playerRootDiv.querySelector('.ubo-overlay');
|
adDiv = playerRootDiv.querySelector('.ubo-overlay');
|
||||||
if (adDiv == null) {
|
if (adDiv == null) {
|
||||||
@@ -215,7 +232,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var workerInstance = reinsertWorkers(newWorker, reinsert);
|
let workerInstance = reinsertWorkers(newWorker, reinsert);
|
||||||
Object.defineProperty(window, 'Worker', {
|
Object.defineProperty(window, 'Worker', {
|
||||||
get: function() {
|
get: function() {
|
||||||
return workerInstance;
|
return workerInstance;
|
||||||
@@ -230,23 +247,23 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getWasmWorkerJs(twitchBlobUrl) {
|
function getWasmWorkerJs(twitchBlobUrl) {
|
||||||
var req = new XMLHttpRequest();
|
const req = new XMLHttpRequest();
|
||||||
req.open('GET', twitchBlobUrl, false);
|
req.open('GET', twitchBlobUrl, false);
|
||||||
req.overrideMimeType("text/javascript");
|
req.overrideMimeType("text/javascript");
|
||||||
req.send();
|
req.send();
|
||||||
return req.responseText;
|
return req.responseText;
|
||||||
}
|
}
|
||||||
function setStreamInfoUrls(streamInfo, encodingsM3u8) {
|
function setStreamInfoUrls(streamInfo, encodingsM3u8) {
|
||||||
var lines = encodingsM3u8.replace('\r', '').split('\n');
|
const lines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
|
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
|
||||||
StreamInfosByUrl[lines[i].trimEnd()] = streamInfo;
|
StreamInfosByUrl[lines[i].trimEnd()] = streamInfo;
|
||||||
}
|
}
|
||||||
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
if (lines[i].startsWith('#EXT-X-STREAM-INF') && lines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(lines[i]);
|
const attributes = parseAttributes(lines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
var resolutionInfo = {
|
const resolutionInfo = {
|
||||||
Resolution: resolution,
|
Resolution: resolution,
|
||||||
FrameRate: attributes['FRAME-RATE'],
|
FrameRate: attributes['FRAME-RATE'],
|
||||||
Url: lines[i + 1]
|
Url: lines[i + 1]
|
||||||
@@ -257,37 +274,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
|
async function onFoundAd(streamInfo, textStr, reloadPlayer, realFetch, url, resolutionInfo) {
|
||||||
var result = textStr;
|
let result = textStr;
|
||||||
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
streamInfo.IsMidroll = textStr.includes('"MIDROLL"') || textStr.includes('"midroll"');
|
||||||
var playerTypes = OPT_BACKUP_PLAYER_TYPES;
|
const playerTypes = OPT_BACKUP_PLAYER_TYPES;
|
||||||
if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) {
|
if (streamInfo.BackupEncodingsStatus.size >= playerTypes.length) {
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) {
|
if (streamInfo.BackupEncodings && !streamInfo.BackupEncodings.includes(url)) {
|
||||||
var streamM3u8Url = getStreamUrlForResolution(streamInfo.BackupEncodings, resolutionInfo);
|
const streamM3u8Url = getStreamUrlForResolution(streamInfo.BackupEncodings, resolutionInfo);
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status === 200) {
|
if (streamM3u8Response.status === 200) {
|
||||||
return await streamM3u8Response.text();
|
return await streamM3u8Response.text();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var backupPlayerTypeInfo = '';
|
let backupPlayerTypeInfo = '';
|
||||||
for (var i = 0; i < playerTypes.length; i++) {
|
for (let i = 0; i < playerTypes.length; i++) {
|
||||||
var playerType = playerTypes[i];
|
const playerType = playerTypes[i];
|
||||||
if (!streamInfo.BackupEncodingsStatus.has(playerType)) {
|
if (!streamInfo.BackupEncodingsStatus.has(playerType)) {
|
||||||
try {
|
try {
|
||||||
var accessTokenResponse = await getAccessToken(streamInfo.ChannelName, playerType, OPT_BACKUP_PLATFORM);
|
const accessTokenResponse = await getAccessToken(streamInfo.ChannelName, playerType);
|
||||||
if (accessTokenResponse != null && accessTokenResponse.status === 200) {
|
if (accessTokenResponse != null && accessTokenResponse.status === 200) {
|
||||||
var accessToken = await accessTokenResponse.json();
|
const accessToken = await accessTokenResponse.json();
|
||||||
var urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
const urlInfo = new URL('https://usher.ttvnw.net/api/' + (V2API ? 'v2/' : '') + 'channel/hls/' + streamInfo.ChannelName + '.m3u8' + streamInfo.UsherParams);
|
||||||
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
urlInfo.searchParams.set('sig', accessToken.data.streamPlaybackAccessToken.signature);
|
||||||
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
urlInfo.searchParams.set('token', accessToken.data.streamPlaybackAccessToken.value);
|
||||||
var encodingsM3u8Response = await realFetch(urlInfo.href);
|
const encodingsM3u8Response = await realFetch(urlInfo.href);
|
||||||
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
||||||
var encodingsM3u8 = await encodingsM3u8Response.text();
|
let encodingsM3u8 = await encodingsM3u8Response.text();
|
||||||
var streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo);
|
const streamM3u8Url = getStreamUrlForResolution(encodingsM3u8, resolutionInfo);
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status === 200) {
|
if (streamM3u8Response.status === 200) {
|
||||||
var backTextStr = await streamM3u8Response.text();
|
const backTextStr = await streamM3u8Response.text();
|
||||||
if ((!backTextStr.includes(AD_SIGNIFIER) && (SimulatedAdsDepth == 0 || i >= SimulatedAdsDepth - 1)) || i >= playerTypes.length - 1) {
|
if ((!backTextStr.includes(AD_SIGNIFIER) && (SimulatedAdsDepth == 0 || i >= SimulatedAdsDepth - 1)) || i >= playerTypes.length - 1) {
|
||||||
result = backTextStr;
|
result = backTextStr;
|
||||||
backupPlayerTypeInfo = ' (' + playerType + ')';
|
backupPlayerTypeInfo = ' (' + playerType + ')';
|
||||||
@@ -295,14 +312,14 @@
|
|||||||
streamInfo.BackupEncodingsPlayerTypeIndex = i;
|
streamInfo.BackupEncodingsPlayerTypeIndex = i;
|
||||||
if (streamInfo.Encodings != null) {
|
if (streamInfo.Encodings != null) {
|
||||||
// Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the low res URLs into the main m3u8
|
// Low resolution streams will reduce the number of resolutions in the UI. To fix this we merge the low res URLs into the main m3u8
|
||||||
var normalEncodingsM3u8 = streamInfo.Encodings;
|
const normalEncodingsM3u8 = streamInfo.Encodings;
|
||||||
var normalLines = normalEncodingsM3u8.replace('\r', '').split('\n');
|
const normalLines = normalEncodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var j = 0; j < normalLines.length - 1; j++) {
|
for (let j = 0; j < normalLines.length - 1; j++) {
|
||||||
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
if (normalLines[j].startsWith('#EXT-X-STREAM-INF')) {
|
||||||
var resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
const resSettings = parseAttributes(normalLines[j].substring(normalLines[j].indexOf(':') + 1));
|
||||||
var lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
|
const lowResUrl = getStreamUrlForResolution(encodingsM3u8, streamInfo.Urls.get(normalLines[j + 1].trimEnd()));
|
||||||
var lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
|
const lowResInf = encodingsM3u8.match(new RegExp(`^.*(?=\n.*${lowResUrl})`, 'm'))[0];
|
||||||
var lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
|
const lowResSettings = parseAttributes(lowResInf.substring(lowResInf.indexOf(':') + 1));
|
||||||
//console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
|
//console.log('map ' + resSettings['RESOLUTION'] + ' to ' + lowResSettings['RESOLUTION']);
|
||||||
const codecsKey = 'CODECS';
|
const codecsKey = 'CODECS';
|
||||||
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
if (typeof resSettings[codecsKey] === 'string' && typeof lowResSettings[codecsKey] === 'string' &&
|
||||||
@@ -340,26 +357,59 @@
|
|||||||
postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
|
postMessage({key:'UboShowAdBanner',isMidroll:streamInfo.IsMidroll});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
function stripAdSegments(textStr, stripAllSegments) {
|
||||||
|
let hasStrippedAdSegments = false;
|
||||||
|
const lines = textStr.replaceAll('\r', '').split('\n');
|
||||||
|
const newAdUrl = 'https://twitch.tv';
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
let line = lines[i];
|
||||||
|
// Remove tracking urls which appear in the overlay UI
|
||||||
|
line = line
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`)
|
||||||
|
.replaceAll(/(X-TV-TWITCH-AD-CLICK-TRACKING-URL=")(?:[^"]*)(")/g, `$1${newAdUrl}$2`);
|
||||||
|
if (i < lines.length - 1 && line.startsWith('#EXTINF') && (!line.includes(',live') || stripAllSegments || AllSegmentsAreAdSegments)) {
|
||||||
|
const segmentUrl = lines[i + 1];
|
||||||
|
AdSegmentCache.set(segmentUrl, Date.now());
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
if (line.includes(AD_SIGNIFIER)) {
|
||||||
|
hasStrippedAdSegments = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStrippedAdSegments) {
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
// No low latency during ads (otherwise it's possible for the player to prefetch and display ad segments)
|
||||||
|
if (lines[i].startsWith('#EXT-X-TWITCH-PREFETCH:')) {
|
||||||
|
lines[i] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AdSegmentCache.forEach((key, value, map) => {
|
||||||
|
if (value < Date.now() - 120000) {
|
||||||
|
map.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
postMessage({
|
||||||
|
key: 'UpdateAdBlockBannerStripping',
|
||||||
|
isStrippingAdSegments: hasStrippedAdSegments
|
||||||
|
});
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
async function processM3U8(url, textStr, realFetch) {
|
async function processM3U8(url, textStr, realFetch) {
|
||||||
var streamInfo = StreamInfosByUrl[url];
|
const streamInfo = StreamInfosByUrl[url];
|
||||||
if (streamInfo == null) {
|
if (!streamInfo) {
|
||||||
//console.log('Unknown stream url ' + url);
|
|
||||||
//postMessage({key:'UboHideAdBanner'});
|
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
var currentResolution = streamInfo.Urls.get(url);
|
const currentResolution = streamInfo.Urls.get(url);
|
||||||
if (!currentResolution) {
|
if (!currentResolution) {
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
var haveAdTags = textStr.includes(AD_SIGNIFIER);
|
const haveAdTags = textStr.includes(AD_SIGNIFIER) || (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex));
|
||||||
if (SimulatedAdsDepth > 0 && (!streamInfo.BackupEncodings || !streamInfo.BackupEncodings.includes(url) || SimulatedAdsDepth - 1 > streamInfo.BackupEncodingsPlayerTypeIndex)) {
|
|
||||||
haveAdTags = true;
|
|
||||||
}
|
|
||||||
if (streamInfo.BackupEncodings) {
|
if (streamInfo.BackupEncodings) {
|
||||||
var streamM3u8Url = streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0];
|
const streamM3u8Url = streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0];
|
||||||
var streamM3u8Response = await realFetch(streamM3u8Url);
|
const streamM3u8Response = await realFetch(streamM3u8Url);
|
||||||
if (streamM3u8Response.status == 200) {
|
if (streamM3u8Response.status == 200) {
|
||||||
var streamM3u8 = await streamM3u8Response.text();
|
const streamM3u8 = await streamM3u8Response.text();
|
||||||
if (streamM3u8 != null) {
|
if (streamM3u8 != null) {
|
||||||
if (!streamM3u8.includes(AD_SIGNIFIER) && SimulatedAdsDepth == 0) {
|
if (!streamM3u8.includes(AD_SIGNIFIER) && SimulatedAdsDepth == 0) {
|
||||||
console.log('No more ads on main stream. Triggering player reload to go back to main stream...');
|
console.log('No more ads on main stream. Triggering player reload to go back to main stream...');
|
||||||
@@ -370,9 +420,9 @@
|
|||||||
postMessage({key:'UboHideAdBanner'});
|
postMessage({key:'UboHideAdBanner'});
|
||||||
postMessage({key:'UboReloadPlayer'});
|
postMessage({key:'UboReloadPlayer'});
|
||||||
} else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
|
} else if (!streamM3u8.includes('"MIDROLL"') && !streamM3u8.includes('"midroll"')) {
|
||||||
var lines = streamM3u8.replace('\r', '').split('\n');
|
const lines = streamM3u8.replaceAll('\r', '').split('\n');
|
||||||
for (var i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
var line = lines[i];
|
const line = lines[i];
|
||||||
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
if (line.startsWith('#EXTINF') && lines.length > i + 1) {
|
||||||
if (!line.includes(LIVE_SIGNIFIER) && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
if (!line.includes(LIVE_SIGNIFIER) && !streamInfo.RequestedAds.has(lines[i + 1])) {
|
||||||
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
// Only request one .ts file per .m3u8 request to avoid making too many requests
|
||||||
@@ -394,19 +444,34 @@
|
|||||||
} else {
|
} else {
|
||||||
postMessage({key:'UboHideAdBanner'});
|
postMessage({key:'UboHideAdBanner'});
|
||||||
}
|
}
|
||||||
|
if (IsAdStrippingEnabled) {
|
||||||
|
textStr = stripAdSegments(textStr);
|
||||||
|
}
|
||||||
return textStr;
|
return textStr;
|
||||||
}
|
}
|
||||||
function hookWorkerFetch() {
|
function hookWorkerFetch() {
|
||||||
console.log('hookWorkerFetch (video-swap-new)');
|
console.log('hookWorkerFetch (video-swap-new)');
|
||||||
var realFetch = fetch;
|
const realFetch = fetch;
|
||||||
fetch = async function(url, options) {
|
fetch = async function(url, options) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
|
if (AdSegmentCache.has(url)) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
const send = function() {
|
||||||
|
return realFetch('data:video/mp4;base64,AAAAKGZ0eXBtcDQyAAAAAWlzb21tcDQyZGFzaGF2YzFpc282aGxzZgAABEltb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAYagAAAAAAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAABqHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAURtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAALuAAAAAAFXEAAAAAAAtaGRscgAAAAAAAAAAc291bgAAAAAAAAAAAAAAAFNvdW5kSGFuZGxlcgAAAADvbWluZgAAABBzbWhkAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAACzc3RibAAAAGdzdHNkAAAAAAAAAAEAAABXbXA0YQAAAAAAAAABAAAAAAAAAAAAAgAQAAAAALuAAAAAAAAzZXNkcwAAAAADgICAIgABAASAgIAUQBUAAAAAAAAAAAAAAAWAgIACEZAGgICAAQIAAAAQc3R0cwAAAAAAAAAAAAAAEHN0c2MAAAAAAAAAAAAAABRzdHN6AAAAAAAAAAAAAAAAAAAAEHN0Y28AAAAAAAAAAAAAAeV0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAoAAAAFoAAAAAAGBbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAA9CQAAAAABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABLG1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAOxzdGJsAAAAoHN0c2QAAAAAAAAAAQAAAJBhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAoABaABIAAAASAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGP//AAAAOmF2Y0MBTUAe/+EAI2dNQB6WUoFAX/LgLUBAQFAAAD6AAA6mDgAAHoQAA9CW7y4KAQAEaOuPIAAAABBzdHRzAAAAAAAAAAAAAAAQc3RzYwAAAAAAAAAAAAAAFHN0c3oAAAAAAAAAAAAAAAAAAAAQc3RjbwAAAAAAAAAAAAAASG12ZXgAAAAgdHJleAAAAAAAAAABAAAAAQAAAC4AAAAAAoAAAAAAACB0cmV4AAAAAAAAAAIAAAABAACCNQAAAAACQAAA', options).then(function(response) {
|
||||||
|
resolve(response);
|
||||||
|
})['catch'](function(err) {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
}
|
||||||
url = url.trimEnd();
|
url = url.trimEnd();
|
||||||
if (url.endsWith('m3u8')) {
|
if (url.endsWith('m3u8')) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var processAfter = async function(response) {
|
const processAfter = async function(response) {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
var str = await processM3U8(url, await response.text(), realFetch);
|
const str = await processM3U8(url, await response.text(), realFetch);
|
||||||
resolve(new Response(str, {
|
resolve(new Response(str, {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
@@ -416,7 +481,7 @@
|
|||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var send = function() {
|
const send = function() {
|
||||||
return realFetch(url, options).then(function(response) {
|
return realFetch(url, options).then(function(response) {
|
||||||
processAfter(response);
|
processAfter(response);
|
||||||
})['catch'](function(err) {
|
})['catch'](function(err) {
|
||||||
@@ -429,22 +494,22 @@
|
|||||||
}
|
}
|
||||||
else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
else if (url.includes('/channel/hls/') && !url.includes('picture-by-picture')) {
|
||||||
V2API = url.includes('/api/v2/');
|
V2API = url.includes('/api/v2/');
|
||||||
var channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
const channelName = (new URL(url)).pathname.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||||
if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
|
if (OPT_FORCE_ACCESS_TOKEN_PLAYER_TYPE) {
|
||||||
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
// parent_domains is used to determine if the player is embeded and stripping it gets rid of fake ads
|
||||||
var tempUrl = new URL(url);
|
const tempUrl = new URL(url);
|
||||||
tempUrl.searchParams.delete('parent_domains');
|
tempUrl.searchParams.delete('parent_domains');
|
||||||
url = tempUrl.toString();
|
url = tempUrl.toString();
|
||||||
}
|
}
|
||||||
return new Promise(async function(resolve, reject) {
|
return new Promise(async function(resolve, reject) {
|
||||||
// - First m3u8 request is the m3u8 with the video encodings (360p,480p,720p,etc).
|
// - First m3u8 request is the m3u8 with the video encodings (360p,480p,720p,etc).
|
||||||
// - Second m3u8 request is the m3u8 for the given encoding obtained in the first request. At this point we will know if there's ads.
|
// - Second m3u8 request is the m3u8 for the given encoding obtained in the first request. At this point we will know if there's ads.
|
||||||
var streamInfo = StreamInfos[channelName];
|
let streamInfo = StreamInfos[channelName];
|
||||||
if (streamInfo != null && streamInfo.Encodings != null && (await realFetch(streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
if (streamInfo != null && streamInfo.Encodings != null && (await realFetch(streamInfo.Encodings.match(/^https:.*\.m3u8$/m)[0])).status !== 200) {
|
||||||
// The cached encodings are dead (the stream probably restarted)
|
// The cached encodings are dead (the stream probably restarted)
|
||||||
streamInfo = null;
|
streamInfo = null;
|
||||||
}
|
}
|
||||||
var serverTime = null;
|
let serverTime = null;
|
||||||
if (streamInfo == null || streamInfo.Encodings == null) {
|
if (streamInfo == null || streamInfo.Encodings == null) {
|
||||||
StreamInfos[channelName] = streamInfo = {
|
StreamInfos[channelName] = streamInfo = {
|
||||||
RequestedAds: new Set(),
|
RequestedAds: new Set(),
|
||||||
@@ -459,16 +524,16 @@
|
|||||||
UsherParams: (new URL(url)).search,
|
UsherParams: (new URL(url)).search,
|
||||||
Urls: new Map(),
|
Urls: new Map(),
|
||||||
};
|
};
|
||||||
var encodingsM3u8Response = await realFetch(url, options);
|
const encodingsM3u8Response = await realFetch(url, options);
|
||||||
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
||||||
var encodingsM3u8 = await encodingsM3u8Response.text();
|
const encodingsM3u8 = await encodingsM3u8Response.text();
|
||||||
streamInfo.Encodings = encodingsM3u8;
|
streamInfo.Encodings = encodingsM3u8;
|
||||||
setStreamInfoUrls(streamInfo, encodingsM3u8);
|
setStreamInfoUrls(streamInfo, encodingsM3u8);
|
||||||
serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
serverTime = getServerTimeFromM3u8(encodingsM3u8);
|
||||||
var resolutionInfo = streamInfo.Urls.values().next().value;
|
const resolutionInfo = streamInfo.Urls.values().next().value;
|
||||||
var streamM3u8Response = await realFetch(resolutionInfo.Url);
|
const streamM3u8Response = await realFetch(resolutionInfo.Url);
|
||||||
if (streamM3u8Response.status == 200) {
|
if (streamM3u8Response.status == 200) {
|
||||||
var streamM3u8 = await streamM3u8Response.text();
|
const streamM3u8 = await streamM3u8Response.text();
|
||||||
if (streamM3u8.includes(AD_SIGNIFIER) || SimulatedAdsDepth > 0) {
|
if (streamM3u8.includes(AD_SIGNIFIER) || SimulatedAdsDepth > 0) {
|
||||||
await onFoundAd(streamInfo, streamM3u8, false, realFetch, resolutionInfo.Url, resolutionInfo);
|
await onFoundAd(streamInfo, streamM3u8, false, realFetch, resolutionInfo.Url, resolutionInfo);
|
||||||
}
|
}
|
||||||
@@ -482,7 +547,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!serverTime) {
|
if (!serverTime) {
|
||||||
var encodingsM3u8Response = await realFetch(url, options);
|
const encodingsM3u8Response = await realFetch(url, options);
|
||||||
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
if (encodingsM3u8Response != null && encodingsM3u8Response.status === 200) {
|
||||||
serverTime = getServerTimeFromM3u8(await encodingsM3u8Response.text());
|
serverTime = getServerTimeFromM3u8(await encodingsM3u8Response.text());
|
||||||
}
|
}
|
||||||
@@ -497,10 +562,10 @@
|
|||||||
}
|
}
|
||||||
function getServerTimeFromM3u8(encodingsM3u8) {
|
function getServerTimeFromM3u8(encodingsM3u8) {
|
||||||
if (V2API) {
|
if (V2API) {
|
||||||
var matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
const matches = encodingsM3u8.match(/#EXT-X-SESSION-DATA:DATA-ID="SERVER-TIME",VALUE="([^"]+)"/);
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
var matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
const matches = encodingsM3u8.match('SERVER-TIME="([0-9.]+)"');
|
||||||
return matches.length > 1 ? matches[1] : null;
|
return matches.length > 1 ? matches[1] : null;
|
||||||
}
|
}
|
||||||
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
function replaceServerTimeInM3u8(encodingsM3u8, newServerTime) {
|
||||||
@@ -510,17 +575,17 @@
|
|||||||
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
return newServerTime ? encodingsM3u8.replace(new RegExp('(SERVER-TIME=")[0-9.]+"'), `SERVER-TIME="${newServerTime}"`) : encodingsM3u8;
|
||||||
}
|
}
|
||||||
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
function getStreamUrlForResolution(encodingsM3u8, resolutionInfo) {
|
||||||
var encodingsLines = encodingsM3u8.replace('\r', '').split('\n');
|
const encodingsLines = encodingsM3u8.replaceAll('\r', '').split('\n');
|
||||||
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
const [targetWidth, targetHeight] = resolutionInfo.Resolution.split('x').map(Number);
|
||||||
var matchedResolutionUrl = null;
|
let matchedResolutionUrl = null;
|
||||||
var matchedFrameRate = false;
|
let matchedFrameRate = false;
|
||||||
var closestResolutionUrl = null;
|
let closestResolutionUrl = null;
|
||||||
var closestResolutionDifference = Infinity;
|
let closestResolutionDifference = Infinity;
|
||||||
for (var i = 0; i < encodingsLines.length - 1; i++) {
|
for (let i = 0; i < encodingsLines.length - 1; i++) {
|
||||||
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
if (encodingsLines[i].startsWith('#EXT-X-STREAM-INF') && encodingsLines[i + 1].includes('.m3u8')) {
|
||||||
var attributes = parseAttributes(encodingsLines[i]);
|
const attributes = parseAttributes(encodingsLines[i]);
|
||||||
var resolution = attributes['RESOLUTION'];
|
const resolution = attributes['RESOLUTION'];
|
||||||
var frameRate = attributes['FRAME-RATE'];
|
const frameRate = attributes['FRAME-RATE'];
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
if (resolution == resolutionInfo.Resolution && (!matchedResolutionUrl || (!matchedFrameRate && frameRate == resolutionInfo.FrameRate))) {
|
||||||
matchedResolutionUrl = encodingsLines[i + 1];
|
matchedResolutionUrl = encodingsLines[i + 1];
|
||||||
@@ -530,7 +595,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const [width, height] = resolution.split('x').map(Number);
|
const [width, height] = resolution.split('x').map(Number);
|
||||||
var difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
const difference = Math.abs((width * height) - (targetWidth * targetHeight));
|
||||||
if (difference < closestResolutionDifference) {
|
if (difference < closestResolutionDifference) {
|
||||||
closestResolutionUrl = encodingsLines[i + 1];
|
closestResolutionUrl = encodingsLines[i + 1];
|
||||||
closestResolutionDifference = difference;
|
closestResolutionDifference = difference;
|
||||||
@@ -540,21 +605,23 @@
|
|||||||
}
|
}
|
||||||
return closestResolutionUrl.trimEnd();
|
return closestResolutionUrl.trimEnd();
|
||||||
}
|
}
|
||||||
function getAccessToken(channelName, playerType, platform) {
|
function getAccessToken(channelName, playerType) {
|
||||||
if (!platform) {
|
const realPlayerType = playerType.replace('-ALT', '');
|
||||||
platform = 'web';
|
const body = {
|
||||||
}
|
operationName: 'PlaybackAccessToken',
|
||||||
var body = null;
|
|
||||||
var templateQuery = 'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "' + platform + '", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "' + platform + '", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}';
|
|
||||||
body = {
|
|
||||||
operationName: 'PlaybackAccessToken_Template',
|
|
||||||
query: templateQuery,
|
|
||||||
variables: {
|
variables: {
|
||||||
'isLive': true,
|
isLive: true,
|
||||||
'login': channelName,
|
login: channelName,
|
||||||
'isVod': false,
|
isVod: false,
|
||||||
'vodID': '',
|
vodID: "",
|
||||||
'playerType': playerType
|
playerType: realPlayerType,
|
||||||
|
platform: realPlayerType == 'autoplay' ? 'android' : 'web'
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
persistedQuery: {
|
||||||
|
version:1,
|
||||||
|
sha256Hash:"ed230aa1e33e07eebb8928504583da78a5173989fadfb1ac94be06a04f3cdbe9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return gqlRequest(body, playerType);
|
return gqlRequest(body, playerType);
|
||||||
@@ -566,14 +633,13 @@
|
|||||||
gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));
|
gql_device_id += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var headers = {
|
let headers = {
|
||||||
'Client-Id': CLIENT_ID,
|
'Client-Id': CLIENT_ID,
|
||||||
'Device-ID': gql_device_id,
|
|
||||||
'X-Device-Id': gql_device_id,
|
'X-Device-Id': gql_device_id,
|
||||||
'Authorization': AuthorizationHeader,
|
'Authorization': AuthorizationHeader,
|
||||||
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader})
|
...(ClientIntegrityHeader && {'Client-Integrity': ClientIntegrityHeader})
|
||||||
};
|
};
|
||||||
if (playerType != 'site') {
|
if (playerType.includes('-ALT')) {
|
||||||
headers = {
|
headers = {
|
||||||
'Client-Id': CLIENT_ID,
|
'Client-Id': CLIENT_ID,
|
||||||
'X-Device-Id': gql_device_id
|
'X-Device-Id': gql_device_id
|
||||||
@@ -637,12 +703,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function hookFetch() {
|
function hookFetch() {
|
||||||
var realFetch = window.fetch;
|
const realFetch = window.fetch;
|
||||||
window.realFetch = realFetch;
|
window.realFetch = realFetch;
|
||||||
window.fetch = function(url, init, ...args) {
|
window.fetch = function(url, init, ...args) {
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
if (url.includes('gql')) {
|
if (url.includes('gql')) {
|
||||||
var deviceId = init.headers['X-Device-Id'];
|
let deviceId = init.headers['X-Device-Id'];
|
||||||
if (typeof deviceId !== 'string') {
|
if (typeof deviceId !== 'string') {
|
||||||
deviceId = init.headers['Device-ID'];
|
deviceId = init.headers['Device-ID'];
|
||||||
}
|
}
|
||||||
@@ -680,7 +746,7 @@
|
|||||||
if (OPT_DISABLE_MATURE_CONTENT_POPUP) {
|
if (OPT_DISABLE_MATURE_CONTENT_POPUP) {
|
||||||
const newBody2 = JSON.parse(init.body);
|
const newBody2 = JSON.parse(init.body);
|
||||||
if (Array.isArray(newBody2)) {
|
if (Array.isArray(newBody2)) {
|
||||||
var hasRemovedClassification = false;
|
let hasRemovedClassification = false;
|
||||||
for (let i = 0; i < newBody2.length; i++) {
|
for (let i = 0; i < newBody2.length; i++) {
|
||||||
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
if (newBody2[i]?.operationName == 'ContentClassificationContext') {
|
||||||
hasRemovedClassification = true;
|
hasRemovedClassification = true;
|
||||||
@@ -698,7 +764,7 @@
|
|||||||
return realFetch.apply(this, arguments);
|
return realFetch.apply(this, arguments);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function reloadTwitchPlayer(isSeek, isPausePlay) {
|
function reloadTwitchPlayer(isPausePlay) {
|
||||||
// Taken from ttv-tools / ffz
|
// Taken from ttv-tools / ffz
|
||||||
// https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
|
// https://github.com/Nerixyz/ttv-tools/blob/master/src/context/twitch-player.ts
|
||||||
// https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
|
// https://github.com/FrankerFaceZ/FrankerFaceZ/blob/master/src/sites/twitch-twilight/modules/player.jsx
|
||||||
@@ -717,27 +783,27 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
function findReactRootNode() {
|
function findReactRootNode() {
|
||||||
var reactRootNode = null;
|
let reactRootNode = null;
|
||||||
var rootNode = document.querySelector('#root');
|
const rootNode = document.querySelector('#root');
|
||||||
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
if (rootNode && rootNode._reactRootContainer && rootNode._reactRootContainer._internalRoot && rootNode._reactRootContainer._internalRoot.current) {
|
||||||
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
reactRootNode = rootNode._reactRootContainer._internalRoot.current;
|
||||||
}
|
}
|
||||||
if (reactRootNode == null) {
|
if (reactRootNode == null) {
|
||||||
var containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
const containerName = Object.keys(rootNode).find(x => x.startsWith('__reactContainer'));
|
||||||
if (containerName != null) {
|
if (containerName != null) {
|
||||||
reactRootNode = rootNode[containerName];
|
reactRootNode = rootNode[containerName];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return reactRootNode;
|
return reactRootNode;
|
||||||
}
|
}
|
||||||
var reactRootNode = findReactRootNode();
|
const reactRootNode = findReactRootNode();
|
||||||
if (!reactRootNode) {
|
if (!reactRootNode) {
|
||||||
console.log('Could not find react root');
|
console.log('Could not find react root');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
let player = findReactNode(reactRootNode, node => node.setPlayerActive && node.props && node.props.mediaPlayerInstance);
|
||||||
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
player = player && player.props && player.props.mediaPlayerInstance ? player.props.mediaPlayerInstance : null;
|
||||||
var playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
const playerState = findReactNode(reactRootNode, node => node.setSrc && node.setInitialPlaybackSettings);
|
||||||
if (!player) {
|
if (!player) {
|
||||||
console.log('Could not find player');
|
console.log('Could not find player');
|
||||||
return;
|
return;
|
||||||
@@ -746,14 +812,7 @@
|
|||||||
console.log('Could not find player state');
|
console.log('Could not find player state');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (player.paused || player.core?.paused) {
|
if (player.isPaused() || player.core?.paused) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isSeek) {
|
|
||||||
console.log('Force seek to reset player (hopefully fixing any audio desync) pos:' + player.getPosition() + ' range:' + JSON.stringify(player.getBuffered()));
|
|
||||||
var pos = player.getPosition();
|
|
||||||
player.seekTo(0);
|
|
||||||
player.seekTo(pos);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPausePlay) {
|
if (isPausePlay) {
|
||||||
@@ -764,9 +823,9 @@
|
|||||||
const lsKeyQuality = 'video-quality';
|
const lsKeyQuality = 'video-quality';
|
||||||
const lsKeyMuted = 'video-muted';
|
const lsKeyMuted = 'video-muted';
|
||||||
const lsKeyVolume = 'volume';
|
const lsKeyVolume = 'volume';
|
||||||
var currentQualityLS = null;
|
let currentQualityLS = null;
|
||||||
var currentMutedLS = null;
|
let currentMutedLS = null;
|
||||||
var currentVolumeLS = null;
|
let currentVolumeLS = null;
|
||||||
try {
|
try {
|
||||||
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
currentQualityLS = localStorage.getItem(lsKeyQuality);
|
||||||
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
currentMutedLS = localStorage.getItem(lsKeyMuted);
|
||||||
@@ -816,13 +875,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}catch{}
|
}catch{}
|
||||||
var block = e => {
|
const block = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
};
|
};
|
||||||
let wasVideoPlaying = true;
|
let wasVideoPlaying = true;
|
||||||
var visibilityChange = e => {
|
const visibilityChange = e => {
|
||||||
if (typeof chrome !== 'undefined') {
|
if (typeof chrome !== 'undefined') {
|
||||||
const videos = document.getElementsByTagName('video');
|
const videos = document.getElementsByTagName('video');
|
||||||
if (videos.length > 0) {
|
if (videos.length > 0) {
|
||||||
@@ -856,25 +915,25 @@
|
|||||||
}catch{}
|
}catch{}
|
||||||
// Hooks for preserving volume / resolution
|
// Hooks for preserving volume / resolution
|
||||||
try {
|
try {
|
||||||
var keysToCache = [
|
const keysToCache = [
|
||||||
'video-quality',
|
'video-quality',
|
||||||
'video-muted',
|
'video-muted',
|
||||||
'volume',
|
'volume',
|
||||||
'lowLatencyModeEnabled',// Low Latency
|
'lowLatencyModeEnabled',// Low Latency
|
||||||
'persistenceEnabled',// Mini Player
|
'persistenceEnabled',// Mini Player
|
||||||
];
|
];
|
||||||
var cachedValues = new Map();
|
const cachedValues = new Map();
|
||||||
for (var i = 0; i < keysToCache.length; i++) {
|
for (let i = 0; i < keysToCache.length; i++) {
|
||||||
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
cachedValues.set(keysToCache[i], localStorage.getItem(keysToCache[i]));
|
||||||
}
|
}
|
||||||
var realSetItem = localStorage.setItem;
|
const realSetItem = localStorage.setItem;
|
||||||
localStorage.setItem = function(key, value) {
|
localStorage.setItem = function(key, value) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
cachedValues.set(key, value);
|
cachedValues.set(key, value);
|
||||||
}
|
}
|
||||||
realSetItem.apply(this, arguments);
|
realSetItem.apply(this, arguments);
|
||||||
};
|
};
|
||||||
var realGetItem = localStorage.getItem;
|
const realGetItem = localStorage.getItem;
|
||||||
localStorage.getItem = function(key) {
|
localStorage.getItem = function(key) {
|
||||||
if (cachedValues.has(key)) {
|
if (cachedValues.has(key)) {
|
||||||
return cachedValues.get(key);
|
return cachedValues.get(key);
|
||||||
@@ -909,4 +968,7 @@
|
|||||||
}
|
}
|
||||||
postTwitchWorkerMessage('SimulateAds', depth);
|
postTwitchWorkerMessage('SimulateAds', depth);
|
||||||
};
|
};
|
||||||
|
window.allSegmentsAreAdSegments = () => {
|
||||||
|
postTwitchWorkerMessage('AllSegmentsAreAdSegments');
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
Reference in New Issue
Block a user