dptxa-proxy / src /pages /test-player.astro
TXAVLOG
Deploy DPTXA to Hugging Face Spaces
4bea261
Raw
History Blame Contribute Delete
30.6 kB
---
// src/pages/test-player.astro
// Standalone Ultra-Premium diagnostics dashboard for TXAPlayer and Supabase connection.
// Bypasses all decoy/Zalo blocks by not using Layout.astro.
import { txasupabase } from '../lib/txasupabase.js';
// Get movie details from Supabase to demonstrate active integration
const slug = 'nu-hoang-nuoc-mat';
const movieData = await txasupabase.getMovieBySlug(slug).catch(() => null);
const movieName = movieData?.movie?.name || 'Nữ Hoàng Nước Mắt';
const thumbUrl = movieData?.movie?.thumb_url || '';
const posterUrl = movieData?.movie?.poster_url || '';
// Extract first episode details
const episodes = movieData?.episodes || [];
const firstServer = episodes[0] || null;
const firstEpisode = firstServer?.server_data?.[0] || null;
const directM3u8 = firstEpisode?.link_m3u8 || 'https://s2.phim1280.tv/20240310/Qah2fQHw/index.m3u8';
const embedUrl = firstEpisode?.link_embed || 'https://player.phimapi.com/player/?url=https://s2.phim1280.tv/20240310/Qah2fQHw/index.m3u8';
// Verify Supabase Connection state
let isSupabaseConnected = false;
let dbErrorMsg = '';
try {
const { data, error } = await txasupabase.client.from('settings').select('key').limit(1);
if (!error) {
isSupabaseConnected = true;
} else {
dbErrorMsg = error.message;
}
} catch (e: any) {
dbErrorMsg = e.message;
}
---
<!doctype html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TXAPlayer Diagnostics Dashboard | Platinum Premium</title>
<!-- Premium Fonts & Icons -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<!-- Stylesheets for Custom Elements -->
<style>
:root {
--primary: #7c3aed;
--primary-glow: rgba(124, 58, 237, 0.4);
--accent: #facc15;
--dark-bg: #0a0b10;
--glass-bg: rgba(18, 20, 29, 0.7);
--glass-border: rgba(255, 255, 255, 0.08);
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
}
body {
margin: 0;
padding: 0;
background-color: var(--dark-bg);
color: var(--text-primary);
font-family: 'Outfit', sans-serif;
min-height: 100vh;
overflow-x: hidden;
background-image: radial-gradient(circle at 10% 20%, rgba(124, 58, 237, 0.08) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(6, 182, 212, 0.05) 0%, transparent 40%);
}
header {
border-b: 1px solid var(--glass-border);
background: rgba(10, 11, 16, 0.8);
backdrop-filter: blur(20px);
padding: 1.25rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
text-decoration: none;
}
.logo-icon {
width: 2.25rem;
height: 2.25rem;
border-radius: 12px;
background: linear-gradient(135deg, var(--primary), #a78bfa);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 900;
font-size: 1.2rem;
box-shadow: 0 0 15px var(--primary-glow);
}
.logo-text {
font-size: 1.5rem;
font-weight: 900;
letter-spacing: -0.5px;
color: #fff;
}
.logo-text span {
color: var(--primary);
}
.container {
max-width: 1400px;
margin: 2rem auto;
padding: 0 1.5rem;
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
@media (min-width: 1024px) {
.container {
grid-template-columns: 8fr 4fr;
}
}
.glass-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 2rem;
backdrop-filter: blur(20px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-card:hover {
border-color: rgba(124, 58, 237, 0.2);
box-shadow: 0 15px 40px rgba(124, 58, 237, 0.05);
}
h2 {
margin-top: 0;
font-size: 1.5rem;
font-weight: 800;
display: flex;
align-items: center;
gap: 0.75rem;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 1rem;
margin-bottom: 1.5rem;
}
h2 i {
color: var(--primary);
}
/* Player wrapper styling */
.player-container-box {
position: relative;
aspect-ratio: 16/9;
width: 100%;
background: #000;
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--glass-border);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.8);
}
#txa-player {
width: 100%;
height: 100%;
}
#txa-embed-player {
width: 100%;
height: 100%;
border: none;
}
/* Controls & diagnostics console */
.diagnostics-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
margin-top: 1.5rem;
}
@media (min-width: 640px) {
.diagnostics-grid {
grid-template-columns: 1fr 1fr;
}
}
.stat-badge {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 14px;
padding: 0.75rem 1rem;
font-size: 0.85rem;
font-weight: 600;
}
.stat-label {
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-value {
color: #fff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-value.success {
color: #10b981;
}
.stat-value.error {
color: #ef4444;
}
.stat-value.warning {
color: var(--accent);
}
/* Button group */
.source-selector {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
padding: 0.75rem 1.25rem;
border-radius: 16px;
font-size: 0.8rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.btn.active {
background: var(--primary);
border-color: var(--primary);
box-shadow: 0 0 15px var(--primary-glow);
color: #000;
}
/* Log console */
.console-log {
background: #050508;
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 16px;
padding: 1rem;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
max-height: 250px;
overflow-y: auto;
margin-top: 1rem;
white-space: pre-wrap;
color: #a78bfa;
line-height: 1.5;
scrollbar-width: thin;
}
.console-log::-webkit-scrollbar {
width: 6px;
}
.console-log::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.url-box {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 1rem;
margin-top: 1rem;
}
.url-title {
font-size: 9px;
font-weight: 900;
text-transform: uppercase;
color: #6b7280;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
display: block;
}
.url-text {
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--primary);
word-break: break-all;
}
/* Recommendation / Explain Box */
.explain-box {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.05) 0%, rgba(6, 182, 212, 0.03) 100%);
border: 1px solid rgba(124, 58, 237, 0.15);
border-radius: 20px;
padding: 1.5rem;
margin-top: 1.5rem;
border-left: 4px solid var(--primary);
}
.explain-title {
font-weight: 800;
font-size: 1rem;
color: #fff;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.explain-content {
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.6;
}
.explain-content strong {
color: #fff;
}
.explain-content ul {
margin: 0.5rem 0 0;
padding-left: 1.25rem;
}
.explain-content li {
margin-bottom: 0.4rem;
}
/* Info Grid Sidebar */
.info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.info-card {
background: rgba(255, 255, 255, 0.01);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 1.5rem;
}
.info-card-title {
font-size: 0.9rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--primary);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.info-card-text {
font-size: 0.85rem;
line-height: 1.5;
color: var(--text-secondary);
}
.movie-poster-box {
width: 100%;
border-radius: 16px;
overflow: hidden;
border: 1px solid var(--glass-border);
margin-bottom: 1rem;
aspect-ratio: 2/3;
}
.movie-poster-box img {
width: 100%;
height: 100%;
object-fit: cover;
}
.badge-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
}
.badge-indicator.active {
background-color: #10b981;
box-shadow: 0 0 10px #10b981;
}
.badge-indicator.inactive {
background-color: #ef4444;
box-shadow: 0 0 10px #ef4444;
}
</style>
</head>
<body>
<header>
<a href="/" class="logo">
<div class="logo-icon">T</div>
<span class="logo-text">T<span>PHIM</span>X</span>
</a>
<div style="font-size: 0.8rem; font-weight: 800; color: var(--text-secondary); text-transform: uppercase; tracking-wider: 1px;">
<span class="badge-indicator active"></span> Local Diagnostics Active
</div>
</header>
<div class="container">
<!-- Main Column: Player & Diagnostics controls -->
<div class="space-y-8">
<!-- Player Panel -->
<div class="glass-card">
<h2><i class="fas fa-play-circle"></i> TXAPlayer Video Sandbox</h2>
<!-- Source Toggles -->
<div class="source-selector">
<button id="btn-direct" class="btn active" data-source="direct">
<i class="fas fa-bolt"></i> Direct M3U8 (No Proxy)
</button>
<button id="btn-proxied" class="btn" data-source="proxied">
<i class="fas fa-shield-alt"></i> Proxied M3U8 (CORS Bypass)
</button>
<button id="btn-embed" class="btn" data-source="embed">
<i class="fas fa-code"></i> Embed Player (Iframe)
</button>
</div>
<!-- Video Wrapper -->
<div class="player-container-box" id="player-container">
<div id="txa-player" class="w-full h-full"></div>
<iframe id="txa-embed-player" class="hidden w-full h-full border-0" allowfullscreen></iframe>
</div>
<!-- Current URL Details -->
<div class="url-box">
<span class="url-title" style="font-size: 0.7rem; font-weight: 900; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Đường dẫn nguồn đang phát:</span>
<span class="url-text" id="current-url-display">{directM3u8}</span>
</div>
<!-- Sandbox Action Controls -->
<div style="margin-top: 1.5rem; display: flex; gap: 0.75rem;">
<button id="btn-reinit" class="btn" style="background: rgba(124, 58, 237, 0.15); border-color: rgba(124, 58, 237, 0.3); color: #c084fc;">
<i class="fas fa-sync-alt"></i> Re-Initialize Player Instance
</button>
<button id="btn-clear-cache" class="btn">
<i class="fas fa-trash-alt"></i> Clear IndexedDB Cache
</button>
</div>
</div>
<!-- Real-Time Diagnostics Logger -->
<div class="glass-card">
<h2><i class="fas fa-terminal"></i> Real-time Logger & CORS Diagnostician</h2>
<div class="diagnostics-grid">
<div class="stat-badge">
<span class="stat-label"><i class="fas fa-server"></i> Supabase Database:</span>
<span class="stat-value" id="status-supabase">
{isSupabaseConnected ? (
<span class="success"><i class="fas fa-check-circle"></i> Connected</span>
) : (
<span class="error"><i class="fas fa-times-circle"></i> Error: {dbErrorMsg}</span>
)}
</span>
</div>
<div class="stat-badge">
<span class="stat-label"><i class="fas fa-shield-virus"></i> Client-Side CORS Test:</span>
<span class="stat-value" id="status-cors">
<span class="warning"><i class="fas fa-spinner animate-spin"></i> Testing Link...</span>
</span>
</div>
<div class="stat-badge">
<span class="stat-label"><i class="fas fa-file-video"></i> M3U8 Fetch Diagnostic:</span>
<span class="stat-value" id="status-m3u8-fetch">
<span class="warning"><i class="fas fa-spinner animate-spin"></i> Awaiting Init</span>
</span>
</div>
<div class="stat-badge">
<span class="stat-label"><i class="fas fa-bolt"></i> Local Dev Environment:</span>
<span class="stat-value success">
<i class="fas fa-desktop"></i> localhost:4321
</span>
</div>
</div>
<div class="console-log" id="terminal-output">[SYSTEM] Diagnostics Board initialized. Ready to record sandbox events...</div>
</div>
<!-- Vercel Deploy Technical Breakdown -->
<div class="glass-card">
<h2><i class="fas fa-info-circle text-blue-400"></i> Technical Analysis: Why it fails on Vercel</h2>
<div class="explain-box">
<div class="explain-title">
<i class="fas fa-exclamation-triangle text-amber-500"></i> Serverless Proxy Streaming Limitations
</div>
<div class="explain-content">
Khi deploy trên <strong>tphimx.vercel.app</strong> (Vercel serverless platform), player bị lỗi không load được phim khi qua cổng proxy <code>/api/proxy-media</code> vì các giới hạn nghiêm ngặt sau của Vercel Free Plan:
<ul>
<li><strong>Payload Body Size Limit (4.5 MB):</strong> Các file phân đoạn phim HLS (.ts) thường có kích thước lớn từ 3 MB đến 10 MB. Khi proxy cố tải toàn bộ file này vào memory (<code>response.arrayBuffer()</code>) và trả về, Vercel sẽ ngay lập tức chặn đứng response vì vượt quá giới hạn 4.5 MB, trả về mã lỗi <strong>502 (Bad Gateway)</strong> hoặc <strong>504 (Gateway Timeout)</strong>.</li>
<li><strong>Serverless Execution Timeout (10 giây):</strong> Quá trình tải các file segment lớn từ server phim gốc thông qua serverless function thường mất nhiều thời gian. Nếu quá 10 giây, Vercel sẽ ngắt kết nối hoàn toàn.</li>
<li><strong>Băng thông và Giới hạn Streaming:</strong> Serverless functions không được tối ưu để stream liên tục luồng video nặng.</li>
</ul>
<div style="margin-top: 1rem;">
<strong>Giải pháp tối ưu & chuyên nghiệp:</strong>
<ol style="margin-top: 0.5rem; padding-left: 1.25rem;">
<li><strong>Cho phép chạy Direct (Không Proxy):</strong> Hầu hết các server phim (như NguonC, KKPhim) đã bật sẵn CORS header (<code>Access-Control-Allow-Origin: *</code>). Hãy ưu tiên phát link gốc trực tiếp trên client để giải phóng hoàn toàn gánh nặng cho server.</li>
<li><strong>Reverse Proxy chuyên dụng:</strong> Thay vì dùng Astro endpoint (chạy serverless trên Vercel), hãy cài đặt một proxy nhẹ bằng <strong>Cloudflare Workers</strong> (miễn phí 100.000 requests/ngày, hỗ trợ streaming mượt mà) hoặc <strong>Nginx/Node.js Reverse Proxy</strong> chạy trên VPS giá rẻ.</li>
<li><strong>Sử dụng nguồn Embed (Iframe):</strong> Nguồn nhúng iframe chạy hoàn toàn trên sandbox độc lập của bên thứ ba, đảm bảo 100% không bị ảnh hưởng bởi CORS hay Vercel proxy.</li>
</ol>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar Column: Film Metadata & Supabase State -->
<div class="info-grid">
<div class="glass-card">
<div class="info-card-title"><i class="fas fa-compact-disc"></i> Movie Metadata</div>
{thumbUrl && (
<div class="movie-poster-box">
<img src={thumbUrl} alt={movieName} />
</div>
)}
<div style="space-y-4">
<div>
<span style="font-size: 0.75rem; color: var(--text-secondary); display:block; font-weight:800; text-transform:uppercase;">Tên Phim</span>
<span style="font-size: 1.1rem; font-weight: 800; color: #fff;">{movieName}</span>
</div>
<div style="margin-top: 1rem;">
<span style="font-size: 0.75rem; color: var(--text-secondary); display:block; font-weight:800; text-transform:uppercase;">Tập phim demo</span>
<span style="font-size: 0.9rem; font-weight: bold; color: var(--accent);"><i class="fas fa-play"></i> Tập 1 - Nguồn HLS</span>
</div>
<div style="margin-top: 1rem;">
<span style="font-size: 0.75rem; color: var(--text-secondary); display:block; font-weight:800; text-transform:uppercase;">Server luồng</span>
<span style="font-size: 0.85rem; font-weight: 600; color: #a78bfa;">{firstServer?.server_name || '#Hà Nội (Vietsub)'}</span>
</div>
</div>
</div>
<div class="glass-card">
<div class="info-card-title"><i class="fas fa-cogs"></i> Sandboxing API Status</div>
<div style="font-size: 0.8rem; line-height: 1.6; color: var(--text-secondary);" class="space-y-3">
<div>
<strong>Astro Mode:</strong> <code style="background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; color: #f472b6;">SSR / Dynamic</code>
</div>
<div style="margin-top: 0.5rem;">
<strong>Astro Local Dev:</strong> <code style="background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; color: #60a5fa;">active</code>
</div>
<div style="margin-top: 0.5rem;">
<strong>Supabase Host:</strong> <code style="background: rgba(255,255,255,0.05); padding: 2px 6px; border-radius: 4px; color: #34d399; font-size: 0.7rem;">jjmzipyewddbepnelawf.supabase.co</code>
</div>
</div>
</div>
<div class="glass-card">
<div class="info-card-title"><i class="fas fa-shield-alt"></i> Security Gatekeeper</div>
<p class="info-card-text" style="font-size: 0.8rem; margin: 0;">
Trang này được xây dựng độc lập và nằm ngoài bộ lọc <code>Layout.astro</code>. Tránh các lỗi kiểm duyệt Cookie, kiểm duyệt nguồn Referer hay chặn đứng do Zalo Gatekeeper, giúp việc lập trình và kiểm nghiệm <code>txaplayer.js</code> đạt hiệu quả tối đa.
</p>
</div>
</div>
</div>
<!-- TXA Support Assets in public directory -->
<script is:inline src="/lib/txa/txaformat.js"></script>
<script is:inline src="/lib/txa/txatooltip.js"></script>
<script is:inline src="/lib/txa/txatoast.js"></script>
<script is:inline src="/lib/txa/txamodal.js" type="module"></script>
<script is:inline src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script is:inline src="/lib/txa/txaplayer.js"></script>
<!-- Diagnostics Client Engine -->
<script is:inline define:vars={{ directM3u8, embedUrl, cloudflareProxyUrl: import.meta.env.PUBLIC_CLOUDFLARE_PROXY_URL }}>
// Global diagnostic helpers
const consoleOutput = document.getElementById('terminal-output');
function logToConsole(message, type = 'info') {
const timestamp = new Date().toLocaleTimeString();
let prefix = `[${timestamp}] [SYSTEM]`;
if (type === 'error') prefix = `[${timestamp}] [ERROR] ❌`;
if (type === 'success') prefix = `[${timestamp}] [SUCCESS] ✅`;
if (type === 'warning') prefix = `[${timestamp}] [WARN] ⚠️`;
const formattedMsg = `${prefix} ${message}`;
consoleOutput.innerText = consoleOutput.innerText + '\n' + formattedMsg;
consoleOutput.scrollTop = consoleOutput.scrollHeight;
console.log(formattedMsg);
}
// 1. Diagnose CORS support for direct M3U8 link on the client side
async function testClientCors(url) {
const corsStatusEl = document.getElementById('status-cors');
const m3u8StatusEl = document.getElementById('status-m3u8-fetch');
logToConsole(`Starting CORS pre-flight inspection for direct M3U8: ${url}...`);
try {
const start = performance.now();
// Fetch only first 100 bytes to verify CORS policy and connectivity
const res = await fetch(url, {
method: 'GET',
headers: {
'Range': 'bytes=0-100'
}
});
const duration = Math.round(performance.now() - start);
if (res.ok) {
logToConsole(`CORS check passed in ${duration}ms! Source server allows direct playback.`, 'success');
corsStatusEl.innerHTML = `<span class="success"><i class="fas fa-check-circle"></i> Allowed (${duration}ms)</span>`;
m3u8StatusEl.innerHTML = `<span class="success"><i class="fas fa-file-alt"></i> Available</span>`;
} else {
logToConsole(`CORS test returned status: ${res.status}.`, 'warning');
corsStatusEl.innerHTML = `<span class="warning"><i class="fas fa-exclamation-triangle"></i> Status ${res.status}</span>`;
m3u8StatusEl.innerHTML = `<span class="warning"><i class="fas fa-exclamation-triangle"></i> Limited</span>`;
}
} catch (err) {
logToConsole(`CORS policy restricts direct client-side requests! Error: ${err.message}`, 'error');
corsStatusEl.innerHTML = `<span class="error"><i class="fas fa-times-circle"></i> Blocked (CORS)</span>`;
m3u8StatusEl.innerHTML = `<span class="error"><i class="fas fa-times-circle"></i> Unreachable</span>`;
}
}
// Initialize the player instance
let activePlayer = null;
let currentSourceType = 'direct';
function getProxyUrl(url) {
if (cloudflareProxyUrl) {
return `${cloudflareProxyUrl}/?url=${encodeURIComponent(url)}`;
}
return `/api/proxy-media?url=${encodeURIComponent(url)}`;
}
function initTxaPlayerInstance() {
const playerContainer = document.getElementById('player-container');
const txaPlayer = document.getElementById('txa-player');
const embedPlayer = document.getElementById('txa-embed-player');
const displayUrlEl = document.getElementById('current-url-display');
// Reset DOM state
txaPlayer.innerHTML = '';
txaPlayer.className = 'w-full h-full';
embedPlayer.className = 'hidden w-full h-full border-0';
embedPlayer.removeAttribute('src');
if (activePlayer) {
logToConsole('Destroying existing TXAPlayer instance...');
try {
activePlayer.destroy();
} catch (e) {
console.warn(e);
}
activePlayer = null;
}
let videoUrl = '';
if (currentSourceType === 'direct') {
videoUrl = directM3u8;
txaPlayer.classList.remove('hidden');
displayUrlEl.textContent = videoUrl;
} else if (currentSourceType === 'proxied') {
videoUrl = getProxyUrl(directM3u8);
txaPlayer.classList.remove('hidden');
displayUrlEl.textContent = videoUrl;
} else if (currentSourceType === 'embed') {
videoUrl = embedUrl;
txaPlayer.classList.add('hidden');
embedPlayer.classList.remove('hidden');
embedPlayer.setAttribute('src', videoUrl);
displayUrlEl.textContent = videoUrl;
logToConsole(`Embed iframe mounted with source: ${videoUrl}`, 'success');
return;
}
logToConsole(`Initializing TXAPlayer v1.0 with videoUrl: ${videoUrl}`);
try {
if (typeof window.TXAPlayer === 'undefined') {
logToConsole('TXAPlayer library is missing from window! Check if txaplayer.js loaded properly.', 'error');
return;
}
activePlayer = new window.TXAPlayer({
container: '#txa-player',
title: 'Nữ Hoàng Nước Mắt - Queen of Tears',
videoUrl: videoUrl,
autoPlay: false,
themeColor: '#7c3aed',
markers: { intro: [0, 95], outro: [0, 0] },
nextEpisodeUrl: null,
prevEpisodeUrl: null,
autoSkipIntro: false,
autoNextEpisode: false,
isLoggedIn: true
});
logToConsole('TXAPlayer instance initialized successfully!', 'success');
// Wire hls events if Hls is running inside
setTimeout(() => {
if (activePlayer && activePlayer.hls) {
logToConsole('HLS.js player engine detected inside TXAPlayer.', 'success');
activePlayer.hls.on('hlsError', (event, data) => {
logToConsole(`HLS.js error event triggered: ${data.details} (${data.type})`, 'error');
});
}
}, 500);
} catch (err) {
logToConsole(`Failed to initialize TXAPlayer: ${err.message}`, 'error');
}
}
// Wire page control event listeners
document.getElementById('btn-direct').onclick = function() {
document.querySelectorAll('.source-selector button').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentSourceType = 'direct';
logToConsole('Switched sandbox source mode to DIRECT M3U8.');
initTxaPlayerInstance();
};
document.getElementById('btn-proxied').onclick = function() {
document.querySelectorAll('.source-selector button').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentSourceType = 'proxied';
logToConsole('Switched sandbox source mode to PROXIED M3U8.');
initTxaPlayerInstance();
};
document.getElementById('btn-embed').onclick = function() {
document.querySelectorAll('.source-selector button').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentSourceType = 'embed';
logToConsole('Switched sandbox source mode to EMBED IFRAME.');
initTxaPlayerInstance();
};
document.getElementById('btn-reinit').onclick = function() {
logToConsole('Re-initializing player sequence manually...');
initTxaPlayerInstance();
};
document.getElementById('btn-clear-cache').onclick = function() {
logToConsole('Attempting to clear IndexedDB blob stores (TXA_BLOB_CACHE)...');
const req = indexedDB.deleteDatabase('TXA_BLOB_CACHE');
req.onsuccess = () => logToConsole('Successfully deleted TXA_BLOB_CACHE IndexedDB store.', 'success');
req.onerror = () => logToConsole('Failed to delete IndexedDB store.', 'error');
req.onblocked = () => logToConsole('Database delete blocked by open connections. Close other tabs first.', 'warning');
};
// Run Diagnostics on startup
window.addEventListener('DOMContentLoaded', () => {
testClientCors(directM3u8);
setTimeout(initTxaPlayerInstance, 300);
});
</script>
</body>
</html>