Skip to content

Instantly share code, notes, and snippets.

@sonygod
Created January 23, 2025 06:36
Show Gist options
  • Save sonygod/341dfc990dae8aa45ecf4156dc119a1e to your computer and use it in GitHub Desktop.
Save sonygod/341dfc990dae8aa45ecf4156dc119a1e to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name HeiMuer TV Video Player
// @namespace http://tampermonkey.net/
// @version 0.3
// @description Add video player for HeiMuer TV
// @author You
// @match https://heimuer.tv/index.php/vod/detail/id/*
// @grant GM_log
// @require https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.10/hls.min.js
// ==/UserScript==
(function () {
'use strict';
const processedUrls = new Set();
let currentHls = null;
function createVideoPlayer() {
// Create main panel
const panelDiv = document.createElement('div');
panelDiv.className = 'stui-pannel clearfix';
// Create header
const headDiv = document.createElement('div');
headDiv.className = 'stui-pannel__head clearfix';
// Create title
const title = document.createElement('h3');
title.className = 'title';
title.textContent = '在线播放';
headDiv.appendChild(title);
// Create episode list container
const episodeListDiv = document.createElement('div');
episodeListDiv.className = 'episode-list';
episodeListDiv.style.cssText = 'margin: 10px 0; display: flex; flex-wrap: wrap; gap: 5px;';
headDiv.appendChild(episodeListDiv);
// Create video wrapper
const wrapper = document.createElement('div');
wrapper.className = 'video-wrapper';
wrapper.style.cssText = 'width: 100%; position: relative; aspect-ratio: 16/9;';
// Create video container
const container = document.createElement('div');
container.className = 'custom-video-player';
container.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%;';
// Create video element
const video = document.createElement('video');
video.controls = true;
video.style.cssText = 'width: 100%; height: 100%; object-fit: contain;';
// Assemble DOM
container.appendChild(video);
wrapper.appendChild(container);
panelDiv.appendChild(headDiv);
panelDiv.appendChild(wrapper);
// Add loading indicator
const loadingDiv = document.createElement('div');
loadingDiv.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
display: none;
`;
loadingDiv.textContent = '加载中...';
container.appendChild(loadingDiv);
return {
container: panelDiv,
episodeList: episodeListDiv,
video: video,
playUrl: function (url) {
loadingDiv.style.display = 'block';
if (currentHls) {
currentHls.destroy();
}
const tryPlay = () => {
video.play().catch(error => {
console.log("Retrying autoplay...");
setTimeout(tryPlay, 1000);
});
};
if (Hls.isSupported()) {
currentHls = new Hls();
currentHls.loadSource(url);
currentHls.attachMedia(video);
currentHls.on(Hls.Events.MANIFEST_LOADED, () => {
console.log("Manifest loaded");
});
currentHls.on(Hls.Events.MANIFEST_PARSED, () => {
console.log("Manifest parsed");
loadingDiv.style.display = 'none';
tryPlay();
});
currentHls.on(Hls.Events.ERROR, (event, data) => {
console.log("HLS error:", data);
loadingDiv.style.display = 'none';
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
video.addEventListener('loadedmetadata', () => {
loadingDiv.style.display = 'none';
tryPlay();
});
}
}
};
}
function init() {
const descDiv = document.getElementById('desc');
if (!descDiv) return;
// Parse all episodes
const episodes = [];
document.querySelectorAll('.hidden-xs').forEach(el => {
const text = el.textContent || '';
if (text.includes('m3u8.heimuertv.com')) {
const [number, fullUrl] = text.split('$');
const url = fullUrl.match(/https:\/\/m3u8\.heimuertv\.com\/play\/[\w\.]+\.m3u8/)?.[0];
if (url) {
console.log('Episode number:', number); // Debug log
episodes.push({ number, url });
}
}
});
if (episodes.length > 0) {
// Sort episodes
episodes.sort((a, b) => Number(a.number) - Number(b.number));
// Create player
const player = createVideoPlayer();
descDiv.parentNode.insertBefore(player.container, descDiv);
// Create episode buttons
episodes.forEach((episode, idx) => {
const btn = document.createElement('button');
const episodeNum = (idx + 1).toString().padStart(2, '0'); // Convert index to episode number
btn.textContent = `第${episodeNum}集`;
btn.style.cssText = `
padding: 5px 15px;
border: 1px solid ${idx === 0 ? '#007bff' : '#ddd'};
background: ${idx === 0 ? '#007bff' : '#fff'};
color: ${idx === 0 ? '#fff' : '#333'};
border-radius: 3px;
cursor: pointer;
`;
btn.onclick = () => {
// Update button styles
player.episodeList.querySelectorAll('button').forEach(b => {
b.style.background = '#fff';
b.style.color = '#333';
b.style.borderColor = '#ddd';
});
btn.style.background = '#007bff';
btn.style.color = '#fff';
btn.style.borderColor = '#007bff';
// Play episode
player.playUrl(episode.url);
};
player.episodeList.appendChild(btn);
});
// Play first episode
// Trigger initial play with slight delay
setTimeout(() => {
player.playUrl(episodes[0].url);
}, 500);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
@sonygod
Copy link
Author

sonygod commented Jan 23, 2025

// ==UserScript==
// @name         HeiMuer TV Video Player
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Add video player for HeiMuer TV
// @author       You
// @match        https://heimuer.tv/index.php/vod/detail/id/*
// @grant        GM_log
// @require      https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.10/hls.min.js
// ==/UserScript==

(function () {
    'use strict';

    const processedUrls = new Set();
    let currentHls = null;

    function createVideoPlayer() {
        // Create main panel
        const panelDiv = document.createElement('div');
        panelDiv.className = 'stui-pannel clearfix';

        // Create header
        const headDiv = document.createElement('div');
        headDiv.className = 'stui-pannel__head clearfix';

        // Create title
        const title = document.createElement('h3');
        title.className = 'title';
        title.textContent = '在线播放';
        headDiv.appendChild(title);

        // Create episode list container
        const episodeListDiv = document.createElement('div');
        episodeListDiv.className = 'episode-list';
        episodeListDiv.style.cssText = 'margin: 10px 0; display: flex; flex-wrap: wrap; gap: 5px;';
        headDiv.appendChild(episodeListDiv);

        // Create video wrapper
        const wrapper = document.createElement('div');
        wrapper.className = 'video-wrapper';
        wrapper.style.cssText = 'width: 100%; position: relative; aspect-ratio: 16/9;';

        // Create video container
        const container = document.createElement('div');
        container.className = 'custom-video-player';
        container.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%;';

        // Create video element
        const video = document.createElement('video');
        video.controls = true;
        video.style.cssText = 'width: 100%; height: 100%; object-fit: contain;';

        // Assemble DOM
        container.appendChild(video);
        wrapper.appendChild(container);
        panelDiv.appendChild(headDiv);
        panelDiv.appendChild(wrapper);

        // Add loading indicator
        const loadingDiv = document.createElement('div');
        loadingDiv.style.cssText = `
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(0,0,0,0.7);
        color: white;
        padding: 10px 20px;
        border-radius: 4px;
        display: none;
    `;
        loadingDiv.textContent = '加载中...';
        container.appendChild(loadingDiv);

        return {
            container: panelDiv,
            episodeList: episodeListDiv,
            video: video,
            playUrl: function (url) {
                loadingDiv.style.display = 'block';

                if (currentHls) {
                    currentHls.destroy();
                }

                const tryPlay = () => {
                    video.play().catch(error => {
                        console.log("Retrying autoplay...");
                        setTimeout(tryPlay, 1000);
                    });
                };

                if (Hls.isSupported()) {

                    const hlsConfig = {
                        // Core settings
                        autoStartLoad: true,
                        startPosition: -1,
                        debug: false,

                        // Buffering settings
                        maxBufferSize: 30 * 1000 * 1000, // 30MB
                        maxBufferLength: 30,
                        maxMaxBufferLength: 600,
                        backBufferLength: 30,

                        // Retry settings
                        fragLoadingMaxRetry: 6,
                        manifestLoadingMaxRetry: 6,
                        levelLoadingMaxRetry: 6,
                        fragLoadingRetryDelay: 1000,
                        manifestLoadingRetryDelay: 1000,
                        levelLoadingRetryDelay: 1000,

                        // Performance settings
                        enableWorker: true,
                        lowLatencyMode: false,

                        // ABR settings
                        abrEwmaDefaultEstimate: 500000,
                        abrEwmaFastLive: 3,
                        abrEwmaSlowLive: 9
                    };
                    currentHls = new Hls(hlsConfig);
                    currentHls.loadSource(url);
                    currentHls.attachMedia(video);

                    currentHls.on(Hls.Events.MANIFEST_LOADED, () => {
                        console.log("Manifest loaded");
                    });

                    currentHls.on(Hls.Events.MANIFEST_PARSED, () => {
                        console.log("Manifest parsed");
                        loadingDiv.style.display = 'none';
                        tryPlay();
                    });

                    currentHls.on(Hls.Events.ERROR, (event, data) => {
                        console.error("HLS error:", data);

                        if (data.fatal) {
                            switch (data.type) {
                                case Hls.ErrorTypes.NETWORK_ERROR:
                                    console.error("Network error encountered, attempting recovery...");
                                    currentHls.startLoad();
                                    break;
                                case Hls.ErrorTypes.MEDIA_ERROR:
                                    console.error("Media error encountered, attempting recovery...");
                                    currentHls.recoverMediaError();
                                    break;
                                default:
                                    console.error("Unrecoverable error encountered, destroying HLS instance...");
                                    currentHls.destroy();
                                    break;
                            }
                        }
                        loadingDiv.style.display = 'none';
                    });
                } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
                    video.src = url;
                    video.addEventListener('loadedmetadata', () => {
                        loadingDiv.style.display = 'none';
                        tryPlay();
                    });
                }

            }
        };
    }

    function init() {
        const descDiv = document.getElementById('desc');
        if (!descDiv) return;

        // Parse all episodes
        const episodes = [];
        document.querySelectorAll('.hidden-xs').forEach(el => {
            const text = el.textContent || '';
            if (text.includes('m3u8.heimuertv.com')) {
                const [number, fullUrl] = text.split('$');
                const url = fullUrl.match(/https:\/\/m3u8\.heimuertv\.com\/play\/[\w\.]+\.m3u8/)?.[0];
                if (url) {
                    console.log('Episode number:', number); // Debug log
                    episodes.push({ number, url });
                }
            }
        });

        if (episodes.length > 0) {
            // Sort episodes
            episodes.sort((a, b) => Number(a.number) - Number(b.number));

            // Create player
            const player = createVideoPlayer();
            descDiv.parentNode.insertBefore(player.container, descDiv);

            // Create episode buttons
            episodes.forEach((episode, idx) => {
                const btn = document.createElement('button');
                const episodeNum = (idx + 1).toString().padStart(2, '0'); // Convert index to episode number
                btn.textContent = `第${episodeNum}集`;
                btn.style.cssText = `
                    padding: 5px 15px;
                    border: 1px solid ${idx === 0 ? '#007bff' : '#ddd'};
                    background: ${idx === 0 ? '#007bff' : '#fff'};
                    color: ${idx === 0 ? '#fff' : '#333'};
                    border-radius: 3px;
                    cursor: pointer;
                `;
                btn.onclick = () => {
                    // Update button styles
                    player.episodeList.querySelectorAll('button').forEach(b => {
                        b.style.background = '#fff';
                        b.style.color = '#333';
                        b.style.borderColor = '#ddd';
                    });
                    btn.style.background = '#007bff';
                    btn.style.color = '#fff';
                    btn.style.borderColor = '#007bff';

                    // Play episode
                    player.playUrl(episode.url);
                };
                player.episodeList.appendChild(btn);
            });

            // Play first episode
            // Trigger initial play with slight delay
            setTimeout(() => {
                player.playUrl(episodes[0].url);
            }, 500);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment