Twitch - Enable DVR

testing...

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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;
}