diff --git a/base/base.user.js b/base/base.user.js index eda2139..745345d 100644 --- a/base/base.user.js +++ b/base/base.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name TwitchAdSolutions // @namespace https://github.com/pixeltris/TwitchAdSolutions -// @version 1.3 +// @version 1.4 // @description Multiple solutions for blocking Twitch ads // @author pixeltris // @match *://*.twitch.tv/* @@ -58,8 +58,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -91,6 +89,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -130,6 +129,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -205,7 +207,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -261,20 +263,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -323,8 +347,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -373,33 +395,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -538,6 +545,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -869,6 +877,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -945,6 +967,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -972,11 +999,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/mute-black/mute-black-ublock-origin.js b/mute-black/mute-black-ublock-origin.js index 9f707a0..f045e86 100644 --- a/mute-black/mute-black-ublock-origin.js +++ b/mute-black/mute-black-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/mute-black/mute-black.user.js b/mute-black/mute-black.user.js index 45d8780..e7c12b2 100644 --- a/mute-black/mute-black.user.js +++ b/mute-black/mute-black.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (mute-black) // @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 // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/mute-black/mute-black.user.js // @description Multiple solutions for blocking Twitch ads (mute-black) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/notify-reload/notify-reload-ublock-origin.js b/notify-reload/notify-reload-ublock-origin.js index a95a91b..44129f8 100644 --- a/notify-reload/notify-reload-ublock-origin.js +++ b/notify-reload/notify-reload-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/notify-reload/notify-reload.user.js b/notify-reload/notify-reload.user.js index 52bbdac..b03a5d5 100644 --- a/notify-reload/notify-reload.user.js +++ b/notify-reload/notify-reload.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (notify-reload) // @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 // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-reload/notify-reload.user.js // @description Multiple solutions for blocking Twitch ads (notify-reload) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/notify-strip-reload/notify-strip-reload-ublock-origin.js b/notify-strip-reload/notify-strip-reload-ublock-origin.js index 3365520..db3225c 100644 --- a/notify-strip-reload/notify-strip-reload-ublock-origin.js +++ b/notify-strip-reload/notify-strip-reload-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/notify-strip-reload/notify-strip-reload.user.js b/notify-strip-reload/notify-strip-reload.user.js index 131b379..0bbe022 100644 --- a/notify-strip-reload/notify-strip-reload.user.js +++ b/notify-strip-reload/notify-strip-reload.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (notify-strip-reload) // @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 // @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) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/notify-strip/notify-strip-ublock-origin.js b/notify-strip/notify-strip-ublock-origin.js index dbdee51..080767f 100644 --- a/notify-strip/notify-strip-ublock-origin.js +++ b/notify-strip/notify-strip-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/notify-strip/notify-strip.user.js b/notify-strip/notify-strip.user.js index 0fce897..df97ca7 100644 --- a/notify-strip/notify-strip.user.js +++ b/notify-strip/notify-strip.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (notify-strip) // @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 // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/notify-strip/notify-strip.user.js // @description Multiple solutions for blocking Twitch ads (notify-strip) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/proxy-m3u8/proxy-m3u8-ublock-origin.js b/proxy-m3u8/proxy-m3u8-ublock-origin.js index 6bf3843..8e9257b 100644 --- a/proxy-m3u8/proxy-m3u8-ublock-origin.js +++ b/proxy-m3u8/proxy-m3u8-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/proxy-m3u8/proxy-m3u8.user.js b/proxy-m3u8/proxy-m3u8.user.js index 453f6c2..6bed9a7 100644 --- a/proxy-m3u8/proxy-m3u8.user.js +++ b/proxy-m3u8/proxy-m3u8.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (proxy-m3u8) // @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 // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/proxy-m3u8/proxy-m3u8.user.js // @description Multiple solutions for blocking Twitch ads (proxy-m3u8) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/strip/strip-ublock-origin.js b/strip/strip-ublock-origin.js index 7906568..d5f71b3 100644 --- a/strip/strip-ublock-origin.js +++ b/strip/strip-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/strip/strip.user.js b/strip/strip.user.js index 740820b..09316de 100644 --- a/strip/strip.user.js +++ b/strip/strip.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (strip) // @namespace https://github.com/pixeltris/TwitchAdSolutions -// @version 1.3 +// @version 1.4 // @updateURL 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) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/utils.cs b/utils.cs index 92d5cbf..45f1514 100644 --- a/utils.cs +++ b/utils.cs @@ -126,6 +126,10 @@ namespace TwitchAdUtils { modifiedOptions = true; } + if (lineTrimmed.StartsWith("// @name ")) + { + line = line += " (" + dirInfo.Name + ")"; + } if (lineTrimmed.StartsWith("// @description")) { string url = "https://github.com/pixeltris/TwitchAdSolutions/raw/master/" + dirInfo.Name + "/" + dirInfo.Name + suffixUserscript; diff --git a/video-swap/video-swap-ublock-origin.js b/video-swap/video-swap-ublock-origin.js index edfb7fd..061fc22 100644 --- a/video-swap/video-swap-ublock-origin.js +++ b/video-swap/video-swap-ublock-origin.js @@ -49,8 +49,6 @@ twitch-videoad.js application/javascript scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -82,6 +80,7 @@ twitch-videoad.js application/javascript ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -121,6 +120,9 @@ twitch-videoad.js application/javascript else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { 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')) { segInfo.dateTimeLineIndex = 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); } @@ -252,20 +254,42 @@ twitch-videoad.js application/javascript } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -314,8 +338,6 @@ twitch-videoad.js application/javascript return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -364,33 +386,12 @@ twitch-videoad.js application/javascript var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -529,6 +536,7 @@ twitch-videoad.js application/javascript streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -860,6 +868,7 @@ twitch-videoad.js application/javascript } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -936,6 +958,11 @@ twitch-videoad.js application/javascript if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -963,11 +990,11 @@ twitch-videoad.js application/javascript var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch(); diff --git a/video-swap/video-swap.user.js b/video-swap/video-swap.user.js index dd74762..934d432 100644 --- a/video-swap/video-swap.user.js +++ b/video-swap/video-swap.user.js @@ -1,7 +1,7 @@ // ==UserScript== -// @name TwitchAdSolutions +// @name TwitchAdSolutions (video-swap) // @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 // @downloadURL https://github.com/pixeltris/TwitchAdSolutions/raw/master/video-swap/video-swap.user.js // @description Multiple solutions for blocking Twitch ads (video-swap) @@ -60,8 +60,6 @@ scope.StreamInfos = []; scope.StreamInfosByUrl = []; scope.CurrentChannelNameFromM3U8 = null; - scope.LastAdUrl = null; - scope.LastAdTime = 0; // Need this in both scopes. Window scope needs to update this to worker scope. scope.gql_device_id = null; } @@ -93,6 +91,7 @@ ${processM3U8.toString()} ${getSegmentInfos.toString()} ${getSegmentInfosLines.toString()} + ${getFinalSegUrl.toString()} ${hookWorkerFetch.toString()} ${declareOptions.toString()} ${getAccessToken.toString()} @@ -132,6 +131,9 @@ else if (e.data.key == 'UboReloadPlayer') { reloadTwitchPlayer(); } + else if (e.data.key == 'UboPauseResumePlayer') { + reloadTwitchPlayer(true); + } } function getAdDiv() { var playerRootDiv = document.querySelector('.video-player'); @@ -207,7 +209,7 @@ if (i >= 2 && lines[i - 2].startsWith('#EXT-X-PROGRAM-DATE-TIME')) { segInfo.dateTimeLineIndex = 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); } @@ -263,20 +265,42 @@ } 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) { var haveAdTags = textStr.includes(AD_SIGNIFIER); if (OPT_MODE_STRIP_AD_SEGMENTS) { var si = StreamInfosByUrl[url]; if (si != null) { - si.BackupSeqNumber = -1; var lines = textStr.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (lines[i].startsWith('#EXT-X-MEDIA-SEQUENCE:')) { var oldRealSeq = si.RealSeqNumber; si.RealSeqNumber = parseInt(/#EXT-X-MEDIA-SEQUENCE:([0-9]*)/.exec(lines[i])[1]); if (!haveAdTags && si.FakeSeqNumber > 0) { - // We previously modified the sequence number, we need to keep doing so (alternatively pause/playing might work better) - si.FakeSeqNumber += Math.max(0, si.RealSeqNumber - oldRealSeq); + /*// We have some sequencing issues... for now lets pause/play and stop modifying sequence. + // 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; console.log('No ad, but modifying sequence realSeq:' + si.RealSeqNumber + ' fakeSeq:' + si.FakeSeqNumber); } @@ -325,8 +349,6 @@ return textStr; } if (haveAdTags) { - LastAdUrl = url; - LastAdTime = Date.now(); var streamInfo = StreamInfosByUrl[url]; if (streamInfo == null) { console.log('Unknown stream url ' + url); @@ -375,33 +397,12 @@ var lines = textStr.replace('\r', '').split('\n'); var newLines = []; 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 segInfos = getSegmentInfos(streamInfo, lines, backupLines); newLines.push('#EXTM3U'); newLines.push('#EXT-X-VERSION:3'); 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 //newLines.push('#EXT-X-TWITCH-ELAPSED-SECS:' + streamInfo.backup.elapsedSecs); //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) { console.log('liveSegs:' + pushedLiveSegs + ' backupSegs:' + pushedBackupSegs + ' prefetch:' + pushedPrefetchSegs + ' realSeq:' + streamInfo.RealSeqNumber + ' fakeSeq:' + streamInfo.FakeSeqNumber); } else { @@ -540,6 +547,7 @@ streamInfo.RealSeqNumber = -1; streamInfo.BackupSeqNumber = -1; streamInfo.FakeSeqNumber = 0; + streamInfo.FinalSegUrl = null; var lines = encodingsM3u8.replace('\r', '').split('\n'); for (var i = 0; i < lines.length; i++) { if (!lines[i].startsWith('#') && lines[i].includes('.m3u8')) { @@ -871,6 +879,7 @@ } } function pollForAds() { + //console.log('pollForAds ' + new Date(Date.now())); //check ad by looking for text banner var adBanner = document.querySelectorAll("span.tw-c-text-overlay"); 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 // 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 @@ -947,6 +969,11 @@ if (player.paused) { return; } + if (isPausePlay) { + player.pause(); + player.play(); + return; + } const sink = player.mediaSinkManager || (player.core ? player.core.mediaSinkManager : null); if (sink && sink.video && sink.video._ffz_compressor) { const video = sink.video; @@ -974,11 +1001,11 @@ var script = document.createElement('script'); script.src = "https://cdn.jsdelivr.net/npm/hls.js@latest"; script.onload = function() { - pollForAds(); + pollForAdsObserver(); } document.head.appendChild(script); } else { - pollForAds(); + pollForAdsObserver(); } } hookFetch();