Greasy Fork is available in English.

TikTok Live Heartbeat

Script to send heartbeat to TikTok live streams to keep them alive

// ==UserScript==
// @name         TikTok Live Heartbeat
// @namespace    https://tiktakgames.com.tr/
// @version      1.2.5
// @description  Script to send heartbeat to TikTok live streams to keep them alive
// @author       TikTakGames
// @match        https://www.tiktok.com/*/live
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tiktok.com
// @license      MIT
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// ==/UserScript==


(function () {
    'use strict';  

 

    // url hask #tiktakgames|550e8400-e29b-41d4-a716-446655440000|f005d178-94f8-4091-bf48-0cee83bbf39f gibi olmalı. tag|ACCOUNT_UUID|GAME_UUID
    const urlHash = window.location.hash;

    if(!urlHash) return;
    // eğer hash #tiktakgames ile başlamıyorsa return
    if(!urlHash.startsWith("#tiktakgames")) return;
    const hashParts = urlHash.split('|');
    if(hashParts.length < 3) return;
    
    const TAG = hashParts[0];
    const ACCOUNT_UUID = hashParts[1];
    const GAME_UUID = hashParts[2];

    // tarayıcnın userAgent'ini alıyoruz.
    const USER_AGENT = navigator.userAgent; 
    const BASE_URL = window.location.href; 







    const translates = {
        "title_running": {
            "tr": "Çalışıyor. Yayın boyunca bu sayfayı böylece açık tutun. Daha az kaynak tüketimi için video akışı durdurulmuş olacaktır.",
            "en": "Running. Keep this page open throughout the broadcast. Video streaming is stopped for less resource consumption."
        },
        "title_stopped": {
            "tr": "Durduruldu. Artık sayfayı kapatabilirsiniz. Yayını tekrar başlattığınızdan yeni bir pencere açmanız gerekecektir.",
            "en": "Stopped. You can close the page now. You will need to open a new window when you start the broadcast again."
        },
        "heartbeat_sending": {
            "tr": "Bilgi gönderiliyor...",
            "en": "Sending heartbeat..."
        },
        "heartbeat_sent": {
            "tr": "Bilgi gönderildi!",
            "en": "Heartbeat sent!"
        },
        "heartbeat_not_sent": {
            "tr": "Bilgi gönderilemedi!",
            "en": "Heartbeat not sent!"
        },
        "live_ended": {
            "tr": "Yayın bitti!",
            "en": "Live stream ended!"
        }, 
        "video_muted": {
            "tr": "Video susturuldu!",
            "en": "Video muted!"
        },
        "audio_muted": {
            "tr": "Ses susturuldu!",
            "en": "Audio muted!"
        },
        "started" : {
            "tr": "TikTok Live Heartbeat başladı!",
            "en": "TikTok Live Heartbeat started!"
        },
        "native_functions_backing_up": {
            "tr": "Native XMLHttpRequest.open, WebSocket ve Response.json fonksiyonları yedekleniyor...",
            "en": "Native XMLHttpRequest.open, WebSocket and Response.json functions are backing up..."
        },
        "overriding_native_functions": {
            "tr": "Native XMLHttpRequest.open, WebSocket ve Response.json fonksiyonları değiştiriliyor...",
            "en": "Native XMLHttpRequest.open, WebSocket and Response.json functions are being overridden..."
        },
        "websocket_closed": {
            "tr": "WebSocket kapandı!",
            "en": "WebSocket closed!"
        },
        "detecting_room_info": {
            "tr": "Oda bilgisi tespit ediliyor...",
            "en": "Detecting room info..."
        },
        "room_id_not_found": {
            "tr": "Oda ID bulunamadı!",
            "en": "Room ID not found!"
        },
        "room_id_detected": {
            "tr": "Oda ID tespit edildi!",
            "en": "Room ID detected!"
        },
        "user_id_not_found": {
            "tr": "Kullanıcı ID bulunamadı! Muhtemelen TikTok'a giriş yapmadınız.",
            "en": "User ID not found! You probably didn't log in to TikTok."
        },
        "user_id_detected": {
            "tr": "Kullanıcı ID tespit edildi!",
            "en": "User ID detected!"
        },
    };
    const getTranslate = (key) => {
        let lang = (window.navigator.language || "en").substr(0, 2);
        return translates[key][lang] || translates[key]["en"];
    }






    const createDashboard = () => {
        const HTML = `<div id="tiktak-games-dashboard">
            <div id="tiktak-games-wrapper"> 
                <div id="tiktak-games-header"> 
                    Yayın boyunca bu sayfayı böylece açık tutun. Keep this page open throughout the broadcast. 
                </div> 
                <div id="tiktak-games-log-container">
                    <div style="font-size:30px; text-align:center;opacity:.5; padding:8px;">TikTakGames. Interactive Game Platform</div>
                </div> 
            </div> 
        </div>
        <link href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap" rel="stylesheet">
        `
        const CSS = `
            #tiktak-games-dashboard {
                position: fixed;
                left: 0px;
                top: 0px;
                right: 0px;
                bottom: 0px;
                z-index: 999999;
                display: flex;
                justify-content: center;
                align-items: center;
                /* backdrop filter */
                backdrop-filter: blur(2px);
                -webkit-backdrop-filter: blur(2px); 
                pointer-events: none; 
                background-color: rgba(255, 255, 255, 0.3);
            }
            
            #tiktak-games-dashboard * {
                font-family: "Ubuntu Mono", monospace !important;
            }
            
            #tiktak-games-wrapper {
                background-color: rgba(0, 0, 0, 0.9);
                width: 80%;
                height: 80%;
                border-radius: 14px;
                padding: 15px;
                display: flex;
                flex-direction: column;
                gap: 10px;
                pointer-events: auto; 
                background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2My42NyA2NC4yNiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48ZGVmcz48Y2xpcFBhdGggaWQ9IkEiPjxwYXRoIGQ9Ik0wIDBoNjMuNjd2NjQuMjZIMHoiIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iMCIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNBKSIgZmlsbD0iI2ZmZiIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNNTguMDcgNTQuNjJoLTIuMTZjMC0uMi0uMDctLjM3LS4yMS0uNTFzLS4zMS0uMjEtLjUxLS4yMWgtMy42MmMtLjE0IDAtLjI3LjA0LS4zOS4xMXMtLjIxLjE3LS4yNi4zYS43My43MyAwIDAgMC0uMDYuMy43NS43NSAwIDAgMCAuMDUuMjZjLjA5LjIuMy4zLjY2LjNoLjkxdjIuMTdoLS45MWMtLjY2IDAtMS4yMS0uMTUtMS42Ni0uNDQtLjQ1LS4zLS43OC0uNjgtLjk5LTEuMTYtLjE1LS4zNy0uMjItLjc1LS4yMi0xLjE0cy4wOS0uNzUuMjMtMS4xMWEyLjc3IDIuNzcgMCAwIDEgMS4wNS0xLjI5Yy40OC0uMzIgMS4wMS0uNDcgMS41OC0uNDVoMy42MmEyLjcxIDIuNzEgMCAwIDEgMi4wMy44NSAyLjc0IDIuNzQgMCAwIDEgLjg1IDIuMDNtLTkuMzcgMy4yNmgyLjE2YzAgLjIuMDcuMzcuMjEuNTFzLjMxLjIxLjUxLjIxaDMuNjJjLjE0IDAgLjI3LS4wMy4zOS0uMS4xMi0uMDguMjEtLjE4LjI2LS4zMWEuNzMuNzMgMCAwIDAgLjA2LS4zLjguOCAwIDAgMC0uMDUtLjI2Yy0uMDktLjItLjMtLjMtLjY2LS4zaC0uOXYtMi4xN2guOWMuNjYgMCAxLjIxLjE1IDEuNjYuNDRhMi42NCAyLjY0IDAgMCAxIC45OSAxLjE4Yy4xNS4zNi4yMy43NC4yMiAxLjEzIDAgLjM5LS4wOS43NS0uMjMgMS4xMS0uMjEuNTQtLjU2Ljk3LTEuMDUgMS4yOS0uNDguMzItMS4wMS40Ny0xLjU5LjQ1aC0zLjYyYTIuNzEgMi43MSAwIDAgMS0yLjAzLS44NSAyLjc0IDIuNzQgMCAwIDEtLjg1LTIuMDN6bS00LjA1LTIuOTR2Mi4xNkg0MC4zdjEuNWg3LjA0djIuMTZoLTkuMnYtNS44Mmg2LjV6bS02LjUtMy4yaDkuMnYyLjE2SDQwLjN2LjU0aC0yLjE2di0yLjd6bS0xMy4wNSA5LjAydi00LjVoMi4xNnY0LjVoLTIuMTZ6bS0uMTktOS4wMmgyLjU1bDMuMjQgNS4xNyAzLjIzLTUuMTdoMi4zNXY5LjAyaC0yLjE2di01LjI1bC0yLjE1IDMuNDVoLTIuNTVsLTQuNTItNy4yMnptLTguNTggOS4wMmgtMi4zNWwzLjg2LTkuMDJoMi4zNWwzLjg3IDkuMDJIMjEuN2wtLjY5LTEuNjJoLTIuNTJ2LTIuMTZoMS42bC0xLjA3LTIuNS0yLjY5IDYuMjh6bS04LjItNy4wNGMtLjQ3LjItLjg1LjUxLTEuMTQuOTRzLS40My45LS40MyAxLjQyYTIuNDQgMi40NCAwIDAgMCAuNzQgMS43OGMuNDkuNDkgMS4wOS43NCAxLjc5Ljc0czEuMjktLjI1IDEuNzgtLjc1YTIuNzYgMi43NiAwIDAgMCAuNS0uNjlIOS4wOFY1NWg0LjY4djEuMDljMCAuNjItLjEyIDEuMjEtLjM1IDEuNzktLjI0LjU3LS41NyAxLjA4LTEuMDIgMS41Mi0uNDQuNDQtLjk1Ljc4LTEuNTIgMS4wMmE0LjU3IDQuNTcgMCAwIDEtMS43OS4zNWMtLjYyIDAtMS4yMi0uMTItMS43OS0uMzUtLjU3LS4yNC0xLjA4LS41Ny0xLjUyLTEuMDItLjQ0LS40NC0uNzgtLjk1LTEuMDItMS41MmE0LjU3IDQuNTcgMCAwIDEtLjM1LTEuNzkgNC41OSA0LjU5IDAgMCAxIC44LTIuNjEgNC42OSA0LjY5IDAgMCAxIDIuMTItMS43NGwuODEgMS45OXptNC40My0uODFsLTEuNiAxLjQ1Yy0uMjQtLjI3LS41Mi0uNDctLjg1LS42MWEyLjU1IDIuNTUgMCAwIDAtMS4wMi0uMjF2LTIuMTZjLjY1IDAgMS4yOC4xMyAxLjg4LjQuNjEuMjcgMS4xNC42NCAxLjU4IDEuMTN6bTEzLjM2LTQuMDNIMXYxNC4zN2g2MC40OFY0OC44OUgzNS42MmwtNC4zOS01LjM2LTQuMzggNS4zNmgtLjkzek05LjExIDMzLjQ4bDUuMTUtMy40MnYxNi4yNkg5LjExVjMzLjQ4em0xMy4yNy04LjU0SDF2NS4xMmgyMS4zOHYtNS4xMnptMTEuNjQgMGgtNS41OWwtOS4xNSAyMS4zOWg1LjU5bDEuMDctMi41MmgwbDMuNjItOC40NGgwbDEuNjgtMy45MyAxLjMzIDMuMTIgMy40IDcuOTIuNjYgMS41NC45OSAyLjMxaDUuNThsLTkuMTgtMjEuMzl6TTQ0Ljg5IDFoLTUuMTJ2MTIuODRoNS4xMlYxem0xNy43OCAwaC02LjUzTDQwLjY4IDIwLjM0bC0uMDYuMDYtMS40OSAxLjk4aDYuNDRsNS4xNS02LjQ0IDQuNTQgNi40NGg2LjIybC03LjQ0LTEwLjYxTDYyLjY3IDF6TTMzLjggMjIuMzhoLTUuMTJWOS41NGw1LjEyLTMuNDJ2MTYuMjZ6TTMzLjggMWgtNS4xMnY1LjEyaDUuMTJWMXpNOS4xMSA5LjU0bDUuMTUtMy40MnYxNi4yNkg5LjExVjkuNTR6TTIyLjM4IDFIMXY1LjEyaDIxLjM4VjF6Ii8+PHBhdGggZD0iTTQ0Ljg5IDI0Ljk0aC01LjEydjEyLjg0aDUuMTJWMjQuOTR6bTE3Ljc4IDBoLTYuNTNMNDAuNjggNDQuMjhsLS4wNi4wNi0xLjQ5IDEuOThoNi40NGw1LjE1LTYuNDQgNC41NCA2LjQ0aDYuMjJsLTcuNDQtMTAuNjEgOC42My0xMC43N3oiLz48L2c+PC9zdmc+");
                background-size: 150px auto;  
                background-position: right 30px bottom 30px;
                background-repeat: no-repeat; 
            }
            
            #tiktak-games-header {
                display: table;
                padding: 14px 24px;
                border-radius: 15px;
                margin: 0 auto;
                font-family: consolas;
                font-size: 20px;
                color: white;
                text-align: center;  
            }
            #tiktak-games-header[data-title="title_running"] {
                background-color: #059669;
                color: white; 
            }
            #tiktak-games-header[data-title="title_stopped"] { 
                background-color: #f43f5e;
                color: white; 
                animation: shake 1s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite;
                animation-delay: 2s;
            } 
            @keyframes shake {
                0% { transform: translateX(0); }
                25% { transform: translateX(-5px); }
                50% { transform: translateX(5px); }
                75% { transform: translateX(-5px); }
                100% { transform: translateX(0); }
            }
            
            #tiktak-games-header div {
                display: table-cell;
                vertical-align: middle;
            }
            
            #tiktak-games-header img {
                width: 30px;
                height: 30px;
                margin-right: 10px;
            }
            
            #tiktak-games-log-container {
                color: #fff;
                flex: 1;
                overflow-y: auto;
                font-weight: 700;
            }
            
            #tiktak-games-log-container > * {
                line-height: 24px;
            }
            #tiktak-games-log-container .log-success {
                color: #22c55e;
            }
            
            #tiktak-games-log-container .log-error {
                color: #f43f5e;
            }
            
            #tiktak-games-log-container .log-warning {
                color: #f4b400;
            }
            
            #tiktak-games-log-container .log-info {
                color: #00a8ff;
            }
            
            #tiktak-games-log-container .log-debug {
                color: #94a3b8;
            }
            #tiktak-games-log-container .log-heartbeat {
                color: #94a3b8;
                transition: all 0.3s ease-in-out;  
            }  
            #tiktak-games-log-container .log-heartbeat.timeout{  
                margin-top:-24px;
                opacity: 0;
            }
            
        `;
        let e = document.createElement('div');
        e.innerHTML = HTML; 
        document.body.appendChild(e); 
        let style = document.createElement('style');
        style.innerHTML = CSS; 
        document.head.appendChild(style);



        setDashboardTitle('title_running');
    }; 
    const setDashboardTitle = (titleKey) => {
        let e = document.getElementById('tiktak-games-header');
        e.innerText = getTranslate(titleKey);
        e.setAttribute('data-title', titleKey);

    };
    
    
    
    let logQueue = [];
    const getLogContainer = () => {
        return document.getElementById('tiktak-games-log-container');
    };
    const createLog = (message, type) => { 
        let logContainer = getLogContainer();
        if(!logContainer){
            logQueue.push({message, type});
            return;
        }
        if(logQueue.length > 0) {
            let queue = logQueue || [];
            logQueue = [];
            queue.forEach(log => {
                createLog(log.message, log.type);
            });
        }
        let logElement = document.createElement('div');
        logElement.className = 'tiktak-games-log log-' + type;
        logElement.innerText = `[${new Date().toLocaleTimeString()}] ${message}`;
        logContainer.appendChild(logElement);
        if(logContainer.children.length >= 300) logContainer.removeChild(logContainer.children[0]);
        if(logContainer.getAttribute('data-hover') !== 'true') logElement.scrollIntoView();
    }; 
    const addSuccessLog = (message) => {
        createLog(message, 'success');
    };
    const addErrorLog = (message) => {
        createLog(message, 'error');
    };
    const addWarningLog = (message) => {
        createLog(message, 'warning');
    };
    const addInfoLog = (message) => {
        createLog(message, 'info');
    };
    const addLog = (message) => {
        createLog(message, 'log');
    };

 
 
 
    let ROOM_ID = "";
    let STREAMER_ID = "";
    let STREAMER_USERNAME = "";
    let PAGE_INFO = null;
    let PAGE_INFO_DETECTED = false;
    let SOCKET_CLOSED = false;


    addSuccessLog(getTranslate("started"));
    
    addLog(getTranslate("native_functions_backing_up"));
    let _XMLHttpRequestOpen = window.XMLHttpRequest.prototype.open;
    let _WebSocket = window.WebSocket;
    let _ResponseJson = window.Response.prototype.json; 
 
    addLog(getTranslate("overriding_native_functions")); 
    window.WebSocket = function (url, protocols) {
        addInfoLog("WebSocket function is called!");
        addLog("URL: " + url);
        let ws = new (Function.prototype.bind.call(_WebSocket, null, url, protocols));

        if (url && url.includes('/webcast/im/') && url.includes(ROOM_ID)) {
            // ws.addEventListener('message', function (msg) {  
            //     console.log(msg)
            //     addSuccessLog("WebSocket message received! " + msg.data.byteLength + " bytes");
            // })

            ws.addEventListener('close', () => {
                SOCKET_CLOSED = true;
                addWarningLog(getTranslate("websocket_closed"));
            })
        }

        return ws;
    }   


    function detectRoomInfo() {
        if (PAGE_INFO_DETECTED) return;

        addLog(getTranslate("detecting_room_info"));

        const sigiSateElement = document.getElementById('SIGI_STATE');
        if (!sigiSateElement) {
            addErrorLog("SIGI_STATE not found!");
            return;
        } 

        addLog("Parsing SIGI_STATE...");
        const sigiStateJson = sigiSateElement.innerText;
        if(!sigiStateJson) {
            addErrorLog("SIGI_STATE is empty!");
            return;
        } 

        addLog("Checking if SIGI_STATE is a valid JSON...");
        PAGE_INFO = JSON.parse(sigiStateJson);
        if(!PAGE_INFO) {
            addErrorLog("SIGI_STATE is not a valid JSON!");
            return;
        } 
        console.log(PAGE_INFO);

        ROOM_ID = PAGE_INFO?.LiveRoom?.liveRoomUserInfo?.user?.roomId; 
        if (!ROOM_ID) {
            addErrorLog("Room ID not found!");
            return;
        } 
        if(ROOM_ID === "unknown") {
            addErrorLog(getTranslate("room_id_not_found"));
            return;
        } 
        addSuccessLog(getTranslate("room_id_detected") + " " + ROOM_ID); 

        STREAMER_ID = PAGE_INFO?.AppContext?.appContext?.user?.uid; 
        if(!STREAMER_ID) {
            addErrorLog(getTranslate("user_id_not_found"));
            return;
        } 
        addSuccessLog(getTranslate("user_id_detected") + " " + STREAMER_ID); 
        STREAMER_USERNAME = PAGE_INFO?.LiveRoom?.liveRoomUserInfo?.user?.uniqueId;

        PAGE_INFO_DETECTED = true; 
        onDetectRoomInfo();
    };

    function onDetectRoomInfo() { 
        addSuccessLog("Room ID: " + ROOM_ID);
        addSuccessLog("User ID: " + STREAMER_ID);

        disableVideoAndAudio();
        startHeartbeat();
    };

    function disableVideoAndAudio() { 
        setInterval(() => {
            document.querySelectorAll("video, audio").forEach(element => { 
                if (element.muted && element.paused) return;
                element.muted = true;
                element.pause();
                if(element.tagName === "VIDEO") {
                    addSuccessLog(getTranslate("video_muted"));
                }
                if(element.tagName === "AUDIO") {
                    addSuccessLog(getTranslate("audio_muted"));
                }
            });
        }, 1000);
    }

    function startHeartbeat(){  
        const interval = setInterval(function(){  
            if(SOCKET_CLOSED) {
                setDashboardTitle('title_stopped');
                clearInterval(interval);
                return;
            }
            addLog(getTranslate("heartbeat_sending"));
            let xml = `<?xml version="1.0" encoding="utf-16"?>
            <package xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
                <type>Heartbeat</type>
                <game_uuid>${GAME_UUID}</game_uuid>
                <account_uuid>${ACCOUNT_UUID}</account_uuid>
                <account_identity>${STREAMER_USERNAME}</account_identity>
                <platform_id>1</platform_id>
                <heartbeat>
                    <stream_id>${ROOM_ID}</stream_id>
                </heartbeat>
            </package>`; 
            GM_xmlhttpRequest({
                method: "POST",
                url: "https://panel.tiktakgames.com.tr/service/heartbeat",
                data: xml,
                onload: function(response) {
                    addSuccessLog(getTranslate("heartbeat_sent"));
                }
            }); 
        }, 15000);
    }

    function checkLiveEnd() {
        if (document.querySelector('[class*="LiveEndContainer"]') !== null) {
            onLiveEnd();
        }
    };
    function onLiveEnd() {
        if(SOCKET_CLOSED) return;
        addWarningLog(getTranslate("live_ended")); 
        SOCKET_CLOSED = true;
    };
 
    function run() { 
        createDashboard();
        // run detectRoomInfo every 1 second until it's detected
        const interval = setInterval(() => {
            if (PAGE_INFO_DETECTED) {
                clearInterval(interval);
                return;
            }
            detectRoomInfo();
        }, 1000);
    }
 
    if(document.body) {
        run();
    }
    else {
        document.addEventListener('DOMContentLoaded', () => {
            run();
        });
    } 
})();