Fix buffering issue after ad segments

- Also switch to an MutationObserver model for mute-black / vide-swap #16
This commit is contained in:
pixeltris 2021-02-03 14:15:11 +00:00
parent a6928c9d04
commit 2424ab3cc1
16 changed files with 934 additions and 525 deletions

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @description Multiple solutions for blocking Twitch ads // @description Multiple solutions for blocking Twitch ads
// @author pixeltris // @author pixeltris
// @match *://*.twitch.tv/* // @match *://*.twitch.tv/*
@ -58,8 +58,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -91,6 +89,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -130,6 +129,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -205,7 +207,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -261,20 +263,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -323,8 +347,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -373,33 +395,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -427,6 +428,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -538,6 +545,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -869,6 +877,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -902,9 +911,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -945,6 +967,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -972,11 +999,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (mute-black)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/mute-black/mute-black.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/mute-black/mute-black.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/mute-black/mute-black.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/mute-black/mute-black.user.js
// @description Multiple solutions for blocking Twitch ads (mute-black) // @description Multiple solutions for blocking Twitch ads (mute-black)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (notify-reload)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-reload/notify-reload.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-reload/notify-reload.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-reload/notify-reload.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-reload/notify-reload.user.js
// @description Multiple solutions for blocking Twitch ads (notify-reload) // @description Multiple solutions for blocking Twitch ads (notify-reload)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (notify-strip-reload)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip-reload/notify-strip-reload.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip-reload/notify-strip-reload.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip-reload/notify-strip-reload.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip-reload/notify-strip-reload.user.js
// @description Multiple solutions for blocking Twitch ads (notify-strip-reload) // @description Multiple solutions for blocking Twitch ads (notify-strip-reload)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (notify-strip)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip/notify-strip.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip/notify-strip.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip/notify-strip.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip/notify-strip.user.js
// @description Multiple solutions for blocking Twitch ads (notify-strip) // @description Multiple solutions for blocking Twitch ads (notify-strip)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (proxy-m3u8)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/proxy-m3u8/proxy-m3u8.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/proxy-m3u8/proxy-m3u8.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/proxy-m3u8/proxy-m3u8.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/proxy-m3u8/proxy-m3u8.user.js
// @description Multiple solutions for blocking Twitch ads (proxy-m3u8) // @description Multiple solutions for blocking Twitch ads (proxy-m3u8)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (strip)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/strip/strip.user.js
// @description Multiple solutions for blocking Twitch ads (strip) // @description Multiple solutions for blocking Twitch ads (strip)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -126,6 +126,10 @@ namespace TwitchAdUtils
{ {
modifiedOptions = true; modifiedOptions = true;
} }
if (lineTrimmed.StartsWith("// @name "))
{
line = line += " (" + dirInfo.Name + ")";
}
if (lineTrimmed.StartsWith("// @description")) if (lineTrimmed.StartsWith("// @description"))
{ {
string url = "https://github.com/pixeltris/TwitchAdSolutions/raw/master/" + dirInfo.Name + "/" + dirInfo.Name + suffixUserscript; string url = "https://github.com/pixeltris/TwitchAdSolutions/raw/master/" + dirInfo.Name + "/" + dirInfo.Name + suffixUserscript;

View File

@ -49,8 +49,6 @@ twitch-videoad.js application/javascript
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -82,6 +80,7 @@ twitch-videoad.js application/javascript
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -121,6 +120,9 @@ twitch-videoad.js application/javascript
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -196,7 +198,7 @@ twitch-videoad.js application/javascript
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -252,20 +254,42 @@ twitch-videoad.js application/javascript
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -314,8 +338,6 @@ twitch-videoad.js application/javascript
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -364,33 +386,12 @@ twitch-videoad.js application/javascript
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -418,6 +419,12 @@ twitch-videoad.js application/javascript
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -529,6 +536,7 @@ twitch-videoad.js application/javascript
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -860,6 +868,7 @@ twitch-videoad.js application/javascript
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -893,9 +902,22 @@ twitch-videoad.js application/javascript
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -936,6 +958,11 @@ twitch-videoad.js application/javascript
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -963,11 +990,11 @@ twitch-videoad.js application/javascript
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name TwitchAdSolutions // @name TwitchAdSolutions (video-swap)
// @namespace https://github.com/pixeltris/TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions
// @version 1.3 // @version 1.4
// @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap/video-swap.user.js // @updateURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap/video-swap.user.js
// @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap/video-swap.user.js // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap/video-swap.user.js
// @description Multiple solutions for blocking Twitch ads (video-swap) // @description Multiple solutions for blocking Twitch ads (video-swap)
@ -60,8 +60,6 @@
scope.StreamInfos = []; scope.StreamInfos = [];
scope.StreamInfosByUrl = []; scope.StreamInfosByUrl = [];
scope.CurrentChannelNameFromM3U8 = null; scope.CurrentChannelNameFromM3U8 = null;
scope.LastAdUrl = null;
scope.LastAdTime = 0;
// Need this in both scopes. Window scope needs to update this to worker scope. // Need this in both scopes. Window scope needs to update this to worker scope.
scope.gql_device_id = null; scope.gql_device_id = null;
} }
@ -93,6 +91,7 @@
${processM3U8.toString()} ${processM3U8.toString()}
${getSegmentInfos.toString()} ${getSegmentInfos.toString()}
${getSegmentInfosLines.toString()} ${getSegmentInfosLines.toString()}
${getFinalSegUrl.toString()}
${hookWorkerFetch.toString()} ${hookWorkerFetch.toString()}
${declareOptions.toString()} ${declareOptions.toString()}
${getAccessToken.toString()} ${getAccessToken.toString()}
@ -132,6 +131,9 @@
else if (e.data.key == 'UboReloadPlayer') { else if (e.data.key == 'UboReloadPlayer') {
reloadTwitchPlayer(); reloadTwitchPlayer();
} }
else if (e.data.key == 'UboPauseResumePlayer') {
reloadTwitchPlayer(true);
}
} }
function getAdDiv() { function getAdDiv() {
var playerRootDiv = document.querySelector('.video-player'); var playerRootDiv = document.querySelector('.video-player');
@ -207,7 +209,7 @@
if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) {
segInfo.dateTimeLineIndex = i - 2; segInfo.dateTimeLineIndex = i - 2;
segInfo.dateTimeLine = lines[i - 2]; segInfo.dateTimeLine = lines[i - 2];
segInfo.dateTime = new Date(lines[i - 2].split(':')[1]); segInfo.dateTime = new Date(lines[i - 2].substr(lines[i - 2].indexOf(':')));
} }
result.segs.push(segInfo); result.segs.push(segInfo);
} }
@ -263,20 +265,42 @@
} }
return result; return result;
} }
function getFinalSegUrl(lines) {
for (var i = lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith("http")) {
return lines[i];
}
}
return null;
}
async function processM3U8(url, textStr, realFetch) { async function processM3U8(url, textStr, realFetch) {
var haveAdTags = textStr.includes(AD_SIGNIFIER); var haveAdTags = textStr.includes(AD_SIGNIFIER);
if (OPT_MODE_STRIP_AD_SEGMENTS) { if (OPT_MODE_STRIP_AD_SEGMENTS) {
var si = StreamInfosByUrl[url]; var si = StreamInfosByUrl[url];
if (si != null) { if (si != null) {
si.BackupSeqNumber = -1;
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) {
var oldRealSeq = si.RealSeqNumber; var oldRealSeq = si.RealSeqNumber;
si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]);
if (!haveAdTags && si.FakeSeqNumber > 0) { if (!haveAdTags && si.FakeSeqNumber > 0) {
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence.
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); // TODO: Improve sequencing (determine if the m3u8 urls have actually changed)
si.FakeSeqNumber = 0;
si.BackupSeqNumber = -1;
postMessage({key:'UboPauseResumePlayer'});*/
// We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better
var finalSegUrl = getFinalSegUrl(lines);
if (finalSegUrl != si.FinalSegUrl) {
si.FinalSegUrl = finalSegUrl;
// TODO: Maybe only do the jump check if there was an ad recently? (within the last 5 m3u8 requests)
var jump = Math.max(0, si.RealSeqNumber - oldRealSeq);
if (jump <= 3) {
si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq);
} else if (jump > 0) {
si.FakeSeqNumber++;
}
}
lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber; lines[i] = '#EXT-X-MEDIA-SEQUENCE:' + si.FakeSeqNumber;
console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber);
} }
@ -325,8 +349,6 @@
return textStr; return textStr;
} }
if (haveAdTags) { if (haveAdTags) {
LastAdUrl = url;
LastAdTime = Date.now();
var streamInfo = StreamInfosByUrl[url]; var streamInfo = StreamInfosByUrl[url];
if (streamInfo == null) { if (streamInfo == null) {
console.log('Unknown stream url ' + url); console.log('Unknown stream url ' + url);
@ -375,33 +397,12 @@
var lines = textStr.replace('\r', '').split('\n'); var lines = textStr.replace('\r', '').split('\n');
var newLines = []; var newLines = [];
if (backupM3u8 != null) { if (backupM3u8 != null) {
var seqMatch = /#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(backupM3u8);
if (seqMatch != null) {
var oldBackupSeqNumber = streamInfo.BackupSeqNumber;
streamInfo.BackupSeqNumber = Math.max(0, parseInt(seqMatch[1]));
if (streamInfo.RealSeqNumber > 0) {
// We already have a real stream, this must be a midroll. We should therefore increment rather than just using backup directly.
// - If we don't do this then our sequence number will be broken and the stream will get stuck in a loading state.
if (streamInfo.FakeSeqNumber == 0) {
streamInfo.FakeSeqNumber = streamInfo.RealSeqNumber;
}
if (oldBackupSeqNumber == -1) {
// First backup sequence, assume +1
streamInfo.FakeSeqNumber++;
}
else {
streamInfo.FakeSeqNumber += Math.max(0, streamInfo.BackupSeqNumber - oldBackupSeqNumber);
}
} else {
streamInfo.FakeSeqNumber = streamInfo.BackupSeqNumber;
}
}
var backupLines = backupM3u8.replace('\r', '').split('\n'); var backupLines = backupM3u8.replace('\r', '').split('\n');
var segInfos = getSegmentInfos(streamInfo, lines, backupLines); var segInfos = getSegmentInfos(streamInfo, lines, backupLines);
newLines.push('#EXTM3U'); newLines.push('#EXTM3U');
newLines.push('#EXT-X-VERSION:3'); newLines.push('#EXT-X-VERSION:3');
newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration); newLines.push('#EXT-X-TARGETDURATION:' + segInfos.backup.targetDuration);
newLines.push('#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber); newLines.push('');//#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber);
// The following will could cause issues when we stop stripping segments // The following will could cause issues when we stop stripping segments
//newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs);
//newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs); //newLines.push('#EXT-X-TWITCH-TOTAL-SECS:' + streamInfo.backup.totalSecs);
@ -429,6 +430,12 @@
} }
} }
} }
var finalSegUrl = getFinalSegUrl(newLines);
if (finalSegUrl != streamInfo.FinalSegUrl) {
streamInfo.FinalSegUrl = finalSegUrl;
streamInfo.FakeSeqNumber++;// We might need something better than this for lager jumps in seq?
}
newLines[3] = '#EXT-X-MEDIA-SEQUENCE:' + streamInfo.FakeSeqNumber;
if (pushedLiveSegs > 0 || pushedBackupSegs > 0) { if (pushedLiveSegs > 0 || pushedBackupSegs > 0) {
console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber);
} else { } else {
@ -540,6 +547,7 @@
streamInfo.RealSeqNumber = -1; streamInfo.RealSeqNumber = -1;
streamInfo.BackupSeqNumber = -1; streamInfo.BackupSeqNumber = -1;
streamInfo.FakeSeqNumber = 0; streamInfo.FakeSeqNumber = 0;
streamInfo.FinalSegUrl = null;
var lines = encodingsM3u8.replace('\r', '').split('\n'); var lines = encodingsM3u8.replace('\r', '').split('\n');
for (var i = 0; i < lines.length; i++) { for (var i = 0; i < lines.length; i++) {
if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) {
@ -871,6 +879,7 @@
} }
} }
function pollForAds() { function pollForAds() {
//console.log('pollForAds ' + new Date(Date.now()));
//check ad by looking for text banner //check ad by looking for text banner
var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); var adBanner = document.querySelectorAll("span.tw-c-text-overlay");
var foundAd = false; var foundAd = false;
@ -904,9 +913,22 @@
} }
} }
} }
setTimeout(pollForAds,100); //setTimeout(pollForAds,100);
} }
function reloadTwitchPlayer() { function pollForAdsObserver() {
pollForAds();
var vids = document.getElementsByClassName('video-player');
for (var i = 0; i < vids.length; i++) {
var observer = new MutationObserver(pollForAds);
observer.observe(vids[i], {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
}
}
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
@ -947,6 +969,11 @@
if (player.paused) { if (player.paused) {
return; return;
} }
if (isPausePlay) {
player.pause();
player.play();
return;
}
const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null);
if (sink && sink.video && sink.video._ffz_compressor) { if (sink && sink.video && sink.video._ffz_compressor) {
const video = sink.video; const video = sink.video;
@ -974,11 +1001,11 @@
var script = document.createElement('script'); var script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest";
script.onload = function() { script.onload = function() {
pollForAds(); pollForAdsObserver();
} }
document.head.appendChild(script); document.head.appendChild(script);
} else { } else {
pollForAds(); pollForAdsObserver();
} }
} }
hookFetch(); hookFetch();