TikTok Live Heartbeat

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
        });
    } 
})();