TikTok Live Heartbeat

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
        });
    } 
})();