testing...
// ==UserScript==
// @name Twitch - Enable DVR
// @namespace https://greasyfork.org/ja/users/941284-ぐらんぴ
// @version 2026-03-09
// @description testing...
// @author ぐらんぴ
// @match https://www.twitch.tv/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitch.tv
// @grant GM_xmlhttpRequest
// @connect gql.twitch.tv
// @run-at document-start
// @license MIT
// ==/UserScript==
// ---- Utilities ----
let l = console.log;
let w = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window, d = document;
let $S = s => document.querySelector(s), $SA = s => document.querySelectorAll(s);
let $C = (tag, props = {}, styles = {}) => {
const s = document.createElement(tag)
Object.assign(s, props)
Object.assign(s.style, styles)
return s
};
// ---- Main ----
let intervalId, origVideo, channelName, m3u8Url;
let resolveFound;
const foundPromise = new Promise((res) => { resolveFound = res; });
async function main() {
await loadHlsJs();
intervalId = setInterval(() => {
let v = $S('video');
if (!v) return;
clearInterval(intervalId);
v.pause(); v.src = ""; v.load();
$S('[data-a-target="video-ref"]').remove();
origVideo = $C('video');
origVideo.id = "GRMP"; origVideo.controls = true; origVideo.style.width = "100%"; origVideo.style.height = "100%";
try {
const m = localStorage.getItem('video-muted');
const v = localStorage.getItem('volume');
if(m == '{"default":true}') origVideo.muted = true;
if(!!v) origVideo.volume = v;
} catch {};
resolveFound();
}, 500);
await foundPromise;
getM3u8()
};
function getM3u8(){
channelName = location.pathname.slice(1).toLowerCase();
const query = {
"operationName": "PlaybackAccessToken",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712"
}
},
"variables": {
"isLive": true,
"login": "",
"isVod": false,
"vodID": "",
"playerType": "site"
}
};
query.variables.login = channelName;
GM_xmlhttpRequest({
method: "POST",
url: "https://gql.twitch.tv/gql",
headers: {
"Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko",
"Content-Type": "application/json"
},
data: JSON.stringify(query),
onload: function(response){
const data = JSON.parse(response.responseText);
const token = data.data.streamPlaybackAccessToken;
if(token){
m3u8Url = `https://usher.ttvnw.net/api/channel/hls/${channelName}.m3u8?client_id=${token.signature}&token=${encodeURIComponent(token.value)}&sig=${token.signature}&allow_source=true`;
if(typeof Hls !== 'undefined' && Hls.isSupported()){
const hls = new Hls();
hls.loadSource(m3u8Url);
hls.attachMedia(origVideo);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
//l('HLS manifest parsed successfully');
origVideo.play().catch(err => {
console.error('Playback failed:', err.message);
setTimeout(()=>{
origVideo.load(); origVideo.play();
},2000)
});
});
}else if (origVideo.canPlayType('application/vnd.apple.mpegurl')){
origVideo.src = m3u8Url;
origVideo.play().catch(err => console.error('Playback failed:', err));
}else{
l('HLS is not supported in this browser');
}
}else{
console.error('Token not found');
}
},
onerror: function(error){
console.error('Request failed', error);
}
});
aa()
};
// execute
const origAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
if(type === "loadstart"){
const recordWrapper = function(e){
if(location.href == "https://www.twitch.tv/") return;
if(!isTargetPage()) return;
main();
};
origAddEventListener.call(this, type, recordWrapper, options);
}
return origAddEventListener.call(this, type, listener, options);
};
function aa(){
let lastUrl = location.href;
function handleIfChanged(){
const current = location.href;
if(current === lastUrl) return;
lastUrl = current;
location.reload();
}
const mo = new MutationObserver(handleIfChanged);
function startMO(){ try{ mo.observe(document,{childList:true,subtree:true}); }catch(e){} }
if(document.documentElement) startMO(); else document.addEventListener('DOMContentLoaded', startMO, {once:true});
}
// ---- helper ----
async function loadHlsJs(callback){
if(w.Hls){
callback();
return;
}
const script = $C('script');
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
script.onload = callback;
d.head.appendChild(script);
}
// Hook Fetch
const origFetch = w.fetch;
w.fetch = async function(url, init) {
let res = await origFetch(url, init);
let data;
if(res.status === 204 || res.status === 205) return res;
try{
const text = await res.text();
data = text ? JSON.parse(text) : {};
}catch(err){//console.error('Failed to parse JSON:', err);
return res;
}
try{ if(url.startsWith('https://edge.ads.twitch.tv/ads')) data = '' }
catch(err){//console.error('err modifying data:', err);
}
return new Response(JSON.stringify(data), {
headers: res.headers,
status: res.status,
statusText: res.statusText,
});
};
function isTargetPage(){
if(location.href == "https://www.twitch.tv/directory") return;
const pathSegments = location.pathname.split('/').filter(Boolean);
return pathSegments.length === 1;
}