plutotv / index.html
AptlyDigital's picture
Update index.html
dbe26ce verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Clarke Player Ultra • Premium TV Experience</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Clarke Player Ultra - Premium TV Interface */
:root {
--primary: #0066FF;
--primary-dark: #0052D4;
--primary-light: #4D94FF;
--accent: #FF3366;
--accent-dark: #E62E5C;
--success: #00CC88;
--warning: #FFAA00;
--info: #00C6FF;
--bg-dark: #0A0E17;
--bg-darker: #05070F;
--bg-card: #13182B;
--bg-panel: #1C243F;
--bg-surface: rgba(255, 255, 255, 0.05);
--text-primary: #FFFFFF;
--text-secondary: #B0B7D6;
--text-muted: #6B7299;
--border: rgba(255, 255, 255, 0.08);
--border-light: rgba(255, 255, 255, 0.04);
--glow-primary: 0 0 30px rgba(0, 102, 255, 0.4);
--glow-accent: 0 0 30px rgba(255, 51, 102, 0.4);
--shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
--gradient-primary: linear-gradient(135deg, var(--primary), var(--primary-dark));
--gradient-accent: linear-gradient(135deg, var(--accent), var(--accent-dark));
--gradient-dark: linear-gradient(180deg, #0A0E17 0%, #05070F 100%);
--glass-bg: rgba(19, 24, 43, 0.8);
--glass-border: rgba(255, 255, 255, 0.1);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--gradient-dark);
color: var(--text-primary);
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* App Container */
.app-container {
display: grid;
grid-template-columns: 1fr 400px;
grid-template-rows: auto 1fr auto;
height: 100vh;
gap: 0;
position: relative;
overflow: hidden;
}
/* Premium TV Header */
.tv-header {
grid-column: 1 / -1;
background: var(--glass-bg);
backdrop-filter: blur(40px);
-webkit-backdrop-filter: blur(40px);
border-bottom: 1px solid var(--border);
padding: 0.8rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
position: relative;
}
.header-left {
display: flex;
align-items: center;
gap: 1.5rem;
}
.logo-container {
display: flex;
align-items: center;
gap: 0.75rem;
}
.logo-icon {
font-size: 2rem;
color: var(--primary);
position: relative;
}
.logo-icon::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
background: var(--primary);
border-radius: 50%;
filter: blur(15px);
opacity: 0.3;
z-index: -1;
}
.logo-text {
display: flex;
flex-direction: column;
line-height: 1.2;
}
.logo-main {
font-size: 1.8rem;
font-weight: 800;
background: linear-gradient(135deg, var(--text-primary), var(--primary-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.logo-sub {
font-size: 0.75rem;
color: var(--text-secondary);
letter-spacing: 1px;
text-transform: uppercase;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 20px;
border: 1px solid var(--border);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
position: relative;
}
.status-dot::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--success);
opacity: 0.3;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.3; transform: translate(-50%, -50%) scale(1); }
50% { opacity: 0.6; transform: translate(-50%, -50%) scale(1.2); }
}
.status-text {
font-size: 0.85rem;
color: var(--text-secondary);
}
.header-controls {
display: flex;
align-items: center;
gap: 0.75rem;
}
.tv-control-btn {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.tv-control-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent);
opacity: 0;
transition: var(--transition);
}
.tv-control-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
transform: translateY(-2px);
border-color: var(--primary);
box-shadow: var(--glow-primary);
}
.tv-control-btn:hover::before {
opacity: 1;
}
.tv-control-btn.active {
background: var(--gradient-primary);
color: white;
border-color: transparent;
box-shadow: var(--glow-primary);
}
/* Main Video Area */
.video-area {
grid-column: 1;
grid-row: 2;
display: flex;
flex-direction: column;
position: relative;
background: #000;
overflow: hidden;
}
.video-container {
flex: 1;
position: relative;
background: #000;
overflow: hidden;
}
#videoPlayer {
width: 100%;
height: 100%;
display: block;
outline: none;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.9) 100%);
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 2rem;
opacity: 0;
transition: var(--transition-slow);
pointer-events: none;
}
.video-container:hover .video-overlay {
opacity: 1;
}
.channel-display {
display: flex;
align-items: center;
gap: 1.5rem;
margin-bottom: 1rem;
}
.channel-logo {
width: 70px;
height: 70px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
color: var(--primary-light);
border: 1px solid var(--border);
position: relative;
overflow: hidden;
}
.channel-logo::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.1), transparent);
}
.channel-info {
flex: 1;
}
.channel-name {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
background: linear-gradient(135deg, var(--text-primary), var(--primary-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.channel-meta {
display: flex;
align-items: center;
gap: 1rem;
}
.channel-region {
padding: 0.4rem 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
backdrop-filter: blur(20px);
}
.channel-quality {
padding: 0.4rem 1rem;
background: var(--gradient-primary);
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
color: white;
}
.player-controls {
position: absolute;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 1.5rem;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(40px);
padding: 1rem 2rem;
border-radius: 50px;
border: 1px solid var(--border);
opacity: 0;
transition: var(--transition);
}
.video-container:hover .player-controls {
opacity: 1;
}
.control-btn {
width: 52px;
height: 52px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.control-btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.2), transparent);
opacity: 0;
transition: var(--transition);
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.control-btn:hover::before {
opacity: 1;
}
.control-btn.play-btn {
background: var(--gradient-primary);
border: none;
box-shadow: var(--glow-primary);
}
.control-btn.play-btn:hover {
background: var(--primary-dark);
transform: scale(1.1);
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
z-index: 100;
}
.tv-spinner {
width: 80px;
height: 80px;
border: 4px solid rgba(255, 255, 255, 0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1.5s linear infinite;
position: relative;
}
.tv-spinner::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border: 4px solid transparent;
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 2s linear infinite reverse;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* TV Info Bar */
.tv-info-bar {
grid-column: 1;
grid-row: 3;
background: var(--glass-bg);
backdrop-filter: blur(40px);
border-top: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.playback-info {
display: flex;
align-items: center;
gap: 1rem;
}
.time-display {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.2rem;
font-weight: 600;
}
.separator {
color: var(--text-muted);
}
.progress-container {
flex: 1;
max-width: 400px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
margin: 0 2rem;
}
.progress-bar {
height: 100%;
background: var(--gradient-primary);
width: 0%;
transition: width 0.3s ease;
position: relative;
}
.progress-bar::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 8px;
background: white;
border-radius: 50%;
transform: translate(50%, -2px);
box-shadow: 0 0 10px var(--primary);
}
.tv-shortcuts {
display: flex;
gap: 2rem;
}
.shortcut-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.shortcut-key {
padding: 0.3rem 0.6rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid var(--border);
border-radius: 6px;
font-family: 'Inter', monospace;
font-size: 0.85rem;
font-weight: 600;
min-width: 40px;
text-align: center;
}
.shortcut-label {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* Channels Sidebar */
.channels-sidebar {
grid-column: 2;
grid-row: 2 / 4;
background: var(--glass-bg);
backdrop-filter: blur(40px);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(0, 0, 0, 0.3);
}
.sidebar-title {
font-size: 1.2rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.sidebar-title i {
color: var(--primary);
}
.channel-count {
padding: 0.3rem 0.8rem;
background: var(--gradient-primary);
border-radius: 20px;
font-size: 0.85rem;
font-weight: 600;
color: white;
}
.sidebar-controls {
display: flex;
gap: 0.5rem;
}
.sidebar-btn {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.sidebar-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.sidebar-btn.active {
background: var(--gradient-primary);
color: white;
border-color: transparent;
}
/* Playlist Input Panel */
.playlist-panel {
padding: 1.5rem;
border-bottom: 1px solid var(--border);
background: rgba(0, 0, 0, 0.3);
}
.panel-title {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.input-group {
margin-bottom: 1rem;
}
.input-with-icon {
position: relative;
}
.input-with-icon i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
.input-with-icon input {
width: 100%;
padding: 0.875rem 1rem 0.875rem 3rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 0.9rem;
transition: var(--transition);
}
.input-with-icon input:focus {
outline: none;
border-color: var(--primary);
background: rgba(255, 255, 255, 0.08);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
/* File Upload Area */
.file-upload-area {
border: 2px dashed var(--border);
border-radius: 12px;
padding: 2rem 1rem;
text-align: center;
cursor: pointer;
transition: var(--transition);
background: rgba(255, 255, 255, 0.02);
position: relative;
overflow: hidden;
}
.file-upload-area:hover {
border-color: var(--primary);
background: rgba(255, 255, 255, 0.05);
}
.file-upload-area.dragover {
border-color: var(--primary);
background: rgba(0, 102, 255, 0.1);
box-shadow: var(--glow-primary);
}
.file-upload-area i {
font-size: 2.5rem;
color: var(--text-muted);
margin-bottom: 1rem;
display: block;
}
.file-upload-area p {
font-size: 0.9rem;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
}
.file-upload-area .file-name {
font-size: 0.85rem;
color: var(--primary-light);
margin-top: 0.5rem;
font-weight: 500;
}
.file-input {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
.quick-presets {
margin-top: 1.5rem;
}
.preset-title {
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.preset-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.preset-btn {
padding: 0.6rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-secondary);
font-size: 0.8rem;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
transition: var(--transition);
text-align: center;
}
.preset-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--primary);
color: var(--text-primary);
transform: translateY(-2px);
}
.preset-btn i {
font-size: 1rem;
}
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
margin-top: 1.5rem;
}
.tv-btn {
padding: 0.875rem;
border: none;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
transition: var(--transition);
}
.tv-btn-primary {
background: var(--gradient-primary);
color: white;
}
.tv-btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--glow-primary);
}
.tv-btn-secondary {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--text-secondary);
}
.tv-btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
/* Channels List */
.channels-list-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.channels-filter {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
gap: 0.75rem;
}
.filter-btn {
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text-secondary);
font-size: 0.85rem;
cursor: pointer;
transition: var(--transition);
white-space: nowrap;
}
.filter-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text-primary);
}
.filter-btn.active {
background: var(--gradient-primary);
color: white;
border-color: transparent;
}
.search-box {
position: relative;
flex: 1;
}
.search-box i {
position: absolute;
left: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
.search-box input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text-primary);
font-size: 0.85rem;
outline: none;
}
.channels-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
/* Hide scrollbar but keep functionality */
.channels-list::-webkit-scrollbar {
width: 6px;
}
.channels-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
.channels-list::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 3px;
}
.channel-item {
display: flex;
align-items: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: var(--transition);
border: 1px solid transparent;
position: relative;
overflow: hidden;
}
.channel-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.05), transparent);
opacity: 0;
transition: var(--transition);
}
.channel-item:hover {
background: rgba(255, 255, 255, 0.06);
border-color: var(--border);
transform: translateX(5px);
}
.channel-item:hover::before {
opacity: 1;
}
.channel-item.active {
background: rgba(0, 102, 255, 0.15);
border-color: var(--primary);
box-shadow: 0 0 20px rgba(0, 102, 255, 0.2);
}
.channel-item.active::before {
opacity: 1;
}
.channel-number {
width: 40px;
height: 40px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: var(--text-primary);
margin-right: 1rem;
flex-shrink: 0;
position: relative;
}
.channel-item.active .channel-number {
background: var(--gradient-primary);
color: white;
}
.channel-info {
flex: 1;
min-width: 0;
}
.channel-title {
font-weight: 600;
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.channel-category {
font-size: 0.75rem;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.channel-category::before {
content: '';
width: 6px;
height: 6px;
background: var(--primary);
border-radius: 50%;
}
.channel-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.fav-btn {
width: 36px;
height: 36px;
border-radius: 10px;
background: transparent;
border: 1px solid var(--border);
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.fav-btn:hover {
color: #FFD700;
border-color: rgba(255, 215, 0, 0.3);
transform: scale(1.1);
}
.fav-btn.active {
color: #FFD700;
border-color: rgba(255, 215, 0, 0.3);
background: rgba(255, 215, 0, 0.1);
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 2rem;
text-align: center;
color: var(--text-secondary);
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.5;
color: var(--primary);
}
.empty-state h3 {
font-size: 1.5rem;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
/* TV Stats Panel */
.tv-stats {
padding: 1.5rem;
border-top: 1px solid var(--border);
background: rgba(0, 0, 0, 0.3);
}
.stats-title {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.stat-item {
text-align: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
border: 1px solid var(--border);
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(0, 102, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 0.75rem;
color: var(--primary);
font-size: 1.2rem;
}
.stat-value {
font-size: 1.2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: var(--text-secondary);
}
/* TV Overlay */
.tv-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(20px);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 2000;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.overlay-content {
max-width: 500px;
width: 90%;
background: var(--glass-bg);
border-radius: 24px;
border: 1px solid var(--border);
padding: 2.5rem;
text-align: center;
box-shadow: var(--shadow-xl);
}
.overlay-icon {
font-size: 4rem;
color: var(--primary);
margin-bottom: 1.5rem;
}
.overlay-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--text-primary), var(--primary-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.overlay-message {
color: var(--text-secondary);
margin-bottom: 2rem;
line-height: 1.6;
}
.overlay-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
/* Responsive Design */
@media (max-width: 1400px) {
.app-container {
grid-template-columns: 1fr 350px;
}
}
@media (max-width: 1200px) {
.app-container {
grid-template-columns: 1fr;
}
.channels-sidebar {
display: none;
}
.sidebar-toggle {
display: flex !important;
}
}
.sidebar-toggle {
display: none;
position: absolute;
top: 1rem;
right: 1rem;
z-index: 100;
}
</style>
</head>
<body>
<div class="app-container">
<!-- TV Header -->
<header class="tv-header">
<div class="header-left">
<div class="logo-container">
<div class="logo-icon">
<i class="fas fa-broadcast-tower"></i>
</div>
<div class="logo-text">
<div class="logo-main">CLARKE ULTRA</div>
<div class="logo-sub">PREMIUM TV EXPERIENCE</div>
</div>
</div>
<div class="status-indicator">
<div class="status-dot"></div>
<span class="status-text" id="statusText">Ready</span>
</div>
</div>
<div class="header-controls">
<button class="tv-control-btn" id="guideBtn" title="TV Guide">
<i class="fas fa-tv"></i>
</button>
<button class="tv-control-btn" id="favoritesBtn" title="Favorites">
<i class="fas fa-star"></i>
</button>
<button class="tv-control-btn" id="settingsBtn" title="Settings">
<i class="fas fa-sliders-h"></i>
</button>
<button class="tv-control-btn" id="fullscreenBtn" title="Fullscreen">
<i class="fas fa-expand"></i>
</button>
<button class="sidebar-toggle tv-control-btn" id="sidebarToggle">
<i class="fas fa-bars"></i>
</button>
</div>
</header>
<!-- Main Video Area -->
<main class="video-area">
<div class="video-container">
<video id="videoPlayer" controls playsinline></video>
<div class="video-overlay">
<div class="channel-display">
<div class="channel-logo">
<i class="fas fa-satellite"></i>
</div>
<div class="channel-info">
<div class="channel-name" id="currentChannel">Welcome to Clarke Ultra</div>
<div class="channel-meta">
<span class="channel-region" id="currentRegion">TV</span>
<span class="channel-quality" id="currentQuality">4K READY</span>
</div>
</div>
</div>
</div>
<div class="player-controls">
<button class="control-btn" id="prevChannel">
<i class="fas fa-step-backward"></i>
</button>
<button class="control-btn play-btn" id="playPauseBtn">
<i class="fas fa-play"></i>
</button>
<button class="control-btn" id="nextChannel">
<i class="fas fa-step-forward"></i>
</button>
<div class="volume-control">
<i class="fas fa-volume-up"></i>
<input type="range" id="volumeSlider" min="0" max="100" value="80">
</div>
</div>
<div class="loading-overlay" id="loadingOverlay">
<div class="tv-spinner"></div>
<p>Loading broadcast stream...</p>
</div>
</div>
</main>
<!-- TV Info Bar -->
<div class="tv-info-bar">
<div class="playback-info">
<div class="time-display">
<span id="currentTime">00:00</span>
<span class="separator">/</span>
<span id="totalTime">00:00</span>
</div>
</div>
<div class="progress-container">
<div class="progress-bar" id="bufferProgress"></div>
</div>
<div class="tv-shortcuts">
<div class="shortcut-item">
<kbd class="shortcut-key">SPACE</kbd>
<span class="shortcut-label">Play/Pause</span>
</div>
<div class="shortcut-item">
<kbd class="shortcut-key">F</kbd>
<span class="shortcut-label">Fullscreen</span>
</div>
<div class="shortcut-item">
<kbd class="shortcut-key">↑↓</kbd>
<span class="shortcut-label">Channels</span>
</div>
</div>
</div>
<!-- Channels Sidebar -->
<aside class="channels-sidebar" id="channelsSidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<i class="fas fa-th-list"></i>
<span>CHANNEL GUIDE</span>
</div>
<div class="channel-count" id="channelCount">0</div>
</div>
<!-- Playlist Input -->
<div class="playlist-panel">
<div class="panel-title">
<i class="fas fa-satellite-dish"></i>
<span>STREAM SOURCE</span>
</div>
<div class="input-group">
<div class="input-with-icon">
<i class="fas fa-link"></i>
<input type="text" id="playlistUrl"
placeholder="Enter M3U/M3U8 URL"
value="https://i.mjh.nz/PlutoTV/us.m3u8">
</div>
</div>
<!-- File Upload Area -->
<div class="input-group">
<div class="file-upload-area" id="fileUploadArea">
<i class="fas fa-file-upload"></i>
<p>Drag & drop .m3u/.m3u8 file here<br>or click to browse</p>
<div class="file-name" id="fileName"></div>
<input type="file" id="fileInput" class="file-input" accept=".m3u,.m3u8">
</div>
</div>
<div class="quick-presets">
<div class="preset-title">
<i class="fas fa-bolt"></i>
<span>QUICK PRESETS</span>
</div>
<div class="preset-buttons">
<button class="preset-btn" data-url="https://i.mjh.nz/PlutoTV/us.m3u8">
<i class="fas fa-flag-usa"></i>
<span>US TV</span>
</button>
<button class="preset-btn" data-url="https://i.mjh.nz/PlutoTV/gb.m3u8">
<i class="fas fa-flag-uk"></i>
<span>UK TV</span>
</button>
<button class="preset-btn" data-url="https://iptv-org.github.io/iptv/index.m3u">
<i class="fas fa-globe"></i>
<span>Global</span>
</button>
</div>
</div>
<div class="action-buttons">
<button class="tv-btn tv-btn-primary" id="loadPlaylistBtn">
<i class="fas fa-play-circle"></i>
<span>LOAD URL</span>
</button>
<button class="tv-btn tv-btn-secondary" id="clearBtn">
<i class="fas fa-trash"></i>
<span>CLEAR</span>
</button>
</div>
</div>
<!-- Channels List -->
<div class="channels-list-container">
<div class="channels-filter">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="channelSearch" placeholder="Search channels...">
</div>
</div>
<div class="channels-filter">
<button class="filter-btn active" data-filter="all">ALL</button>
<button class="filter-btn" data-filter="us">US</button>
<button class="filter-btn" data-filter="gb">UK</button>
<button class="filter-btn" data-filter="fav">
<i class="fas fa-star"></i>
</button>
</div>
<div class="channels-list" id="channelsList">
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-broadcast-tower"></i>
</div>
<h3>No Channels Loaded</h3>
<p>Enter a playlist URL or upload a file to begin</p>
</div>
</div>
</div>
<!-- TV Stats -->
<div class="tv-stats">
<div class="stats-title">
<i class="fas fa-chart-line"></i>
<span>STREAM STATS</span>
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon">
<i class="fas fa-wifi"></i>
</div>
<div class="stat-value" id="bitrateStat">0</div>
<div class="stat-label">BITRATE</div>
</div>
<div class="stat-item">
<div class="stat-icon">
<i class="fas fa-tachometer-alt"></i>
</div>
<div class="stat-value" id="bufferStat">0s</div>
<div class="stat-label">BUFFER</div>
</div>
<div class="stat-item">
<div class="stat-icon">
<i class="fas fa-heartbeat"></i>
</div>
<div class="stat-value" id="healthStat">100%</div>
<div class="stat-label">HEALTH</div>
</div>
</div>
</div>
</aside>
</div>
<!-- Loading Overlay -->
<div class="tv-overlay" id="initialLoadOverlay">
<div class="overlay-content">
<div class="overlay-icon">
<i class="fas fa-satellite"></i>
</div>
<h1 class="overlay-title">CLARKE ULTRA</h1>
<p class="overlay-message">Premium Television Experience<br>Loading broadcast system...</p>
<div class="tv-spinner" style="width: 50px; height: 50px; margin: 0 auto 2rem;"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.10/dist/hls.min.js"></script>
<script>
// Clarke Player Ultra - Premium TV JavaScript
class ClarkePlayerUltra {
constructor() {
// State Management
this.state = {
channels: [],
filteredChannels: [],
favorites: JSON.parse(localStorage.getItem('clarke_ultra_favs')) || {},
currentChannel: null,
currentFilter: 'all',
searchQuery: '',
hls: null,
isPlaying: false,
stats: {
bitrate: 0,
buffer: 0,
health: 100
},
currentFile: null
};
// DOM Elements
this.elements = {
video: document.getElementById('videoPlayer'),
playlistUrl: document.getElementById('playlistUrl'),
fileInput: document.getElementById('fileInput'),
fileUploadArea: document.getElementById('fileUploadArea'),
fileName: document.getElementById('fileName'),
loadPlaylistBtn: document.getElementById('loadPlaylistBtn'),
clearBtn: document.getElementById('clearBtn'),
channelsList: document.getElementById('channelsList'),
currentChannel: document.getElementById('currentChannel'),
currentRegion: document.getElementById('currentRegion'),
currentQuality: document.getElementById('currentQuality'),
statusText: document.getElementById('statusText'),
channelCount: document.getElementById('channelCount'),
channelSearch: document.getElementById('channelSearch'),
playPauseBtn: document.getElementById('playPauseBtn'),
prevChannel: document.getElementById('prevChannel'),
nextChannel: document.getElementById('nextChannel'),
volumeSlider: document.getElementById('volumeSlider'),
loadingOverlay: document.getElementById('loadingOverlay'),
bufferProgress: document.getElementById('bufferProgress'),
currentTime: document.getElementById('currentTime'),
totalTime: document.getElementById('totalTime'),
bitrateStat: document.getElementById('bitrateStat'),
bufferStat: document.getElementById('bufferStat'),
healthStat: document.getElementById('healthStat'),
sidebarToggle: document.getElementById('sidebarToggle'),
channelsSidebar: document.getElementById('channelsSidebar'),
initialLoadOverlay: document.getElementById('initialLoadOverlay'),
favoritesBtn: document.getElementById('favoritesBtn'),
guideBtn: document.getElementById('guideBtn'),
settingsBtn: document.getElementById('settingsBtn'),
fullscreenBtn: document.getElementById('fullscreenBtn')
};
// Initialize
this.init();
}
async init() {
console.log('🚀 Clarke Player Ultra Initializing...');
// Hide initial overlay after 2 seconds
setTimeout(() => {
this.elements.initialLoadOverlay.style.display = 'none';
this.updateStatus('Ready');
}, 2000);
// Setup event listeners
this.setupEventListeners();
this.setupKeyboardControls();
// Load default playlist if URL exists
if (this.elements.playlistUrl.value) {
setTimeout(() => {
this.loadPlaylistFromUrl(this.elements.playlistUrl.value);
}, 500);
}
// Start stats update loop
this.updateStatsLoop();
// Set initial volume
this.elements.video.volume = this.elements.volumeSlider.value / 100;
}
setupEventListeners() {
// Load playlist from URL button
this.elements.loadPlaylistBtn.addEventListener('click', () => {
this.loadPlaylistFromUrl(this.elements.playlistUrl.value);
});
// File input change
this.elements.fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
this.handleFileSelect(e.target.files[0]);
}
});
// Drag and drop for file upload
this.elements.fileUploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
e.currentTarget.classList.add('dragover');
});
this.elements.fileUploadArea.addEventListener('dragleave', (e) => {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
});
this.elements.fileUploadArea.addEventListener('drop', (e) => {
e.preventDefault();
e.currentTarget.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
const file = e.dataTransfer.files[0];
if (file.name.endsWith('.m3u') || file.name.endsWith('.m3u8')) {
this.handleFileSelect(file);
} else {
this.showNotification('Please select a .m3u or .m3u8 file', 'error');
}
}
});
// Clear button
this.elements.clearBtn.addEventListener('click', () => {
this.clearPlaylist();
});
// Quick preset buttons
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const url = e.currentTarget.dataset.url;
this.elements.playlistUrl.value = url;
this.loadPlaylistFromUrl(url);
});
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const filter = e.currentTarget.dataset.filter;
this.setFilter(filter);
// Update active state
document.querySelectorAll('.filter-btn').forEach(b => {
b.classList.remove('active');
});
e.currentTarget.classList.add('active');
});
});
// Favorites button
this.elements.favoritesBtn.addEventListener('click', () => {
this.setFilter('fav');
this.elements.favoritesBtn.classList.toggle('active');
// Update filter button
document.querySelectorAll('.filter-btn').forEach(b => {
b.classList.toggle('active', b.dataset.filter === 'fav');
});
});
// Search input
this.elements.channelSearch.addEventListener('input', (e) => {
this.state.searchQuery = e.target.value.toLowerCase();
this.filterChannels();
});
// Player controls
this.elements.playPauseBtn.addEventListener('click', () => {
this.togglePlayPause();
});
this.elements.prevChannel.addEventListener('click', () => {
this.selectPreviousChannel();
});
this.elements.nextChannel.addEventListener('click', () => {
this.selectNextChannel();
});
// Volume control
this.elements.volumeSlider.addEventListener('input', (e) => {
this.elements.video.volume = e.target.value / 100;
});
// Fullscreen
this.elements.fullscreenBtn.addEventListener('click', () => {
this.toggleFullscreen();
});
// Sidebar toggle
this.elements.sidebarToggle.addEventListener('click', () => {
this.elements.channelsSidebar.style.display =
this.elements.channelsSidebar.style.display === 'none' ? 'flex' : 'none';
});
// Video events
this.elements.video.addEventListener('play', () => {
this.state.isPlaying = true;
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
this.updateStatus('Playing');
});
this.elements.video.addEventListener('pause', () => {
this.state.isPlaying = false;
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
this.updateStatus('Paused');
});
this.elements.video.addEventListener('waiting', () => {
this.showLoading(true);
this.updateStatus('Buffering...');
});
this.elements.video.addEventListener('playing', () => {
this.showLoading(false);
this.updateStatus('Playing');
});
this.elements.video.addEventListener('timeupdate', () => {
this.updatePlaybackInfo();
});
this.elements.video.addEventListener('loadedmetadata', () => {
this.updatePlaybackInfo();
});
this.elements.video.addEventListener('error', (e) => {
console.error('Video error:', e);
this.updateStatus('Playback error');
this.showLoading(false);
});
// Auto-hide controls
let controlsTimeout;
this.elements.video.addEventListener('mousemove', () => {
const overlay = document.querySelector('.video-overlay');
const controls = document.querySelector('.player-controls');
overlay.style.opacity = '1';
controls.style.opacity = '1';
clearTimeout(controlsTimeout);
controlsTimeout = setTimeout(() => {
if (!document.fullscreenElement) {
overlay.style.opacity = '0';
controls.style.opacity = '0';
}
}, 3000);
});
// Settings button
this.elements.settingsBtn.addEventListener('click', () => {
this.showSettings();
});
}
setupKeyboardControls() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
switch (e.key.toLowerCase()) {
case ' ':
e.preventDefault();
this.togglePlayPause();
break;
case 'arrowleft':
e.preventDefault();
this.seek(-10);
break;
case 'arrowright':
e.preventDefault();
this.seek(10);
break;
case 'arrowup':
e.preventDefault();
this.selectPreviousChannel();
break;
case 'arrowdown':
e.preventDefault();
this.selectNextChannel();
break;
case 'f':
e.preventDefault();
this.toggleFullscreen();
break;
case 'm':
e.preventDefault();
this.toggleMute();
break;
case 'l':
e.preventDefault();
this.loadPlaylistFromUrl(this.elements.playlistUrl.value);
break;
case 'escape':
if (document.fullscreenElement) {
document.exitFullscreen();
}
break;
}
});
}
handleFileSelect(file) {
if (!file.name.endsWith('.m3u') && !file.name.endsWith('.m3u8')) {
this.showNotification('Please select a .m3u or .m3u8 file', 'error');
return;
}
this.state.currentFile = file;
this.elements.fileName.textContent = file.name;
this.updateStatus('File selected: ' + file.name);
// Auto-load the file
this.loadPlaylistFromFile(file);
}
async loadPlaylistFromFile(file) {
this.updateStatus('Loading playlist file...');
this.showLoading(true);
try {
const text = await file.text();
await this.parsePlaylist(text, file.name);
this.showNotification(`Playlist loaded: ${file.name}`, 'success');
} catch (error) {
console.error('Failed to load file:', error);
this.showNotification('Failed to load playlist file', 'error');
this.updateStatus('Load failed');
} finally {
this.showLoading(false);
}
}
async loadPlaylistFromUrl(url) {
if (!url) {
this.showNotification('Please enter a playlist URL', 'error');
return;
}
this.updateStatus('Loading playlist...');
this.showLoading(true);
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const text = await response.text();
await this.parsePlaylist(text, url);
this.showNotification('Playlist loaded successfully', 'success');
} catch (error) {
console.error('Failed to load playlist:', error);
this.showNotification('Failed to load playlist. Please check the URL.', 'error');
this.updateStatus('Load failed');
} finally {
this.showLoading(false);
}
}
async parsePlaylist(content, source) {
const channels = [];
const lines = content.split('\n');
let currentChannel = {};
let channelNumber = 1;
for (let line of lines) {
line = line.trim();
if (line.startsWith('#EXTINF')) {
const titleMatch = line.match(/,(.*)$/);
const logoMatch = line.match(/tvg-logo="([^"]+)"/);
const groupMatch = line.match(/group-title="([^"]+)"/);
currentChannel = {
id: `channel_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
title: titleMatch ? this.cleanTitle(titleMatch[1]) : `Channel ${channelNumber}`,
logo: logoMatch ? logoMatch[1] : '',
group: groupMatch ? groupMatch[1] : 'General',
region: source.includes('us') ? 'US' : source.includes('gb') ? 'UK' : 'INT',
number: channelNumber,
url: '',
category: this.detectCategory(titleMatch ? titleMatch[1] : '')
};
channelNumber++;
}
else if (line.startsWith('http')) {
currentChannel.url = line;
channels.push({...currentChannel});
}
}
this.state.channels = channels;
this.setFilter('all');
if (channels.length > 0) {
this.selectChannel(0);
}
}
cleanTitle(title) {
return title
.replace(/^Pluto TV\s*/i, '')
.replace(/^\[.*?\]\s*/, '')
.replace(/\|.*$/, '')
.trim();
}
detectCategory(title) {
const lowerTitle = title.toLowerCase();
if (lowerTitle.includes('news') || lowerTitle.includes('cnn') || lowerTitle.includes('bbc')) {
return 'News';
} else if (lowerTitle.includes('sport') || lowerTitle.includes('espn') || lowerTitle.includes('football')) {
return 'Sports';
} else if (lowerTitle.includes('movie') || lowerTitle.includes('cinema') || lowerTitle.includes('film')) {
return 'Movies';
} else if (lowerTitle.includes('music') || lowerTitle.includes('mtv') || lowerTitle.includes('vibe')) {
return 'Music';
} else if (lowerTitle.includes('kids') || lowerTitle.includes('cartoon') || lowerTitle.includes('disney')) {
return 'Kids';
} else {
return 'Entertainment';
}
}
renderChannels() {
const container = this.elements.channelsList;
if (this.state.filteredChannels.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">
<i class="fas fa-tv"></i>
</div>
<h3>No Channels Found</h3>
<p>Try a different filter or search term</p>
</div>
`;
return;
}
let html = '';
this.state.filteredChannels.forEach((channel, index) => {
const isActive = this.state.currentChannel &&
this.state.currentChannel.id === channel.id;
const isFavorite = this.state.favorites[channel.id];
html += `
<div class="channel-item ${isActive ? 'active' : ''}"
data-index="${index}"
data-id="${channel.id}">
<div class="channel-number">${channel.number}</div>
<div class="channel-info">
<div class="channel-title">${channel.title}</div>
<div class="channel-category">${channel.category}</div>
</div>
<div class="channel-actions">
<button class="fav-btn ${isFavorite ? 'active' : ''}"
data-id="${channel.id}">
<i class="fas fa-star"></i>
</button>
</div>
</div>
`;
});
container.innerHTML = html;
// Add event listeners
container.querySelectorAll('.channel-item').forEach(item => {
item.addEventListener('click', (e) => {
if (!e.target.closest('.fav-btn')) {
const index = parseInt(item.dataset.index);
this.selectChannel(index);
}
});
});
container.querySelectorAll('.fav-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const channelId = btn.dataset.id;
this.toggleFavorite(channelId);
btn.classList.toggle('active');
});
});
}
setFilter(filter) {
this.state.currentFilter = filter;
this.filterChannels();
}
filterChannels() {
let filtered = this.state.channels;
// Apply filter
if (this.state.currentFilter === 'us') {
filtered = filtered.filter(ch => ch.region === 'US');
} else if (this.state.currentFilter === 'gb') {
filtered = filtered.filter(ch => ch.region === 'UK');
} else if (this.state.currentFilter === 'fav') {
filtered = filtered.filter(ch => this.state.favorites[ch.id]);
}
// Apply search
if (this.state.searchQuery) {
filtered = filtered.filter(ch =>
ch.title.toLowerCase().includes(this.state.searchQuery) ||
ch.category.toLowerCase().includes(this.state.searchQuery)
);
}
this.state.filteredChannels = filtered;
this.renderChannels();
this.elements.channelCount.textContent = filtered.length;
}
selectChannel(index) {
if (index < 0 || index >= this.state.filteredChannels.length) return;
const channel = this.state.filteredChannels[index];
this.state.currentChannel = channel;
// Update UI
this.elements.currentChannel.textContent = channel.title;
this.elements.currentRegion.textContent = channel.region;
this.elements.currentQuality.textContent = 'HD';
// Update active state
document.querySelectorAll('.channel-item').forEach(item => {
item.classList.remove('active');
});
const selectedItem = document.querySelector(`.channel-item[data-index="${index}"]`);
if (selectedItem) {
selectedItem.classList.add('active');
selectedItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// Play channel
this.playChannel(channel.url);
this.updateStatus(`Playing: ${channel.title}`);
}
selectNextChannel() {
if (!this.state.currentChannel || this.state.filteredChannels.length === 0) return;
const currentIndex = this.state.filteredChannels.findIndex(
ch => ch.id === this.state.currentChannel.id
);
if (currentIndex >= 0) {
const nextIndex = (currentIndex + 1) % this.state.filteredChannels.length;
this.selectChannel(nextIndex);
} else if (this.state.filteredChannels.length > 0) {
this.selectChannel(0);
}
}
selectPreviousChannel() {
if (!this.state.currentChannel || this.state.filteredChannels.length === 0) return;
const currentIndex = this.state.filteredChannels.findIndex(
ch => ch.id === this.state.currentChannel.id
);
if (currentIndex >= 0) {
const prevIndex = currentIndex - 1 >= 0 ?
currentIndex - 1 :
this.state.filteredChannels.length - 1;
this.selectChannel(prevIndex);
} else if (this.state.filteredChannels.length > 0) {
this.selectChannel(0);
}
}
playChannel(url) {
this.showLoading(true);
// Destroy previous HLS instance
if (this.state.hls) {
this.state.hls.destroy();
this.state.hls = null;
}
// Stop current video
this.elements.video.pause();
this.elements.video.src = '';
if (Hls.isSupported()) {
this.state.hls = new Hls({
enableWorker: true,
lowLatencyMode: true,
backBufferLength: 30,
maxBufferLength: 60,
debug: false
});
this.state.hls.loadSource(url);
this.state.hls.attachMedia(this.elements.video);
this.state.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.elements.video.play().then(() => {
this.showLoading(false);
this.state.isPlaying = true;
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
}).catch(error => {
console.log('Autoplay prevented:', error);
this.showLoading(false);
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
});
});
this.state.hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS Error:', data);
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
this.updateStatus('Network error - retrying...');
this.state.hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
this.updateStatus('Media error - recovering...');
this.state.hls.recoverMediaError();
break;
default:
this.state.hls.destroy();
this.showNotification('Stream error', 'error');
break;
}
}
});
this.state.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
const level = this.state.hls.levels[data.level];
if (level) {
this.state.stats.bitrate = Math.round(level.bitrate / 1000);
this.elements.bitrateStat.textContent = this.state.stats.bitrate;
}
});
} else if (this.elements.video.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS support
this.elements.video.src = url;
this.elements.video.play().then(() => {
this.showLoading(false);
this.state.isPlaying = true;
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
}).catch(error => {
console.log('Native HLS autoplay prevented:', error);
this.showLoading(false);
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
});
} else {
this.showLoading(false);
this.showNotification('Browser not supported', 'error');
}
}
togglePlayPause() {
if (this.elements.video.paused) {
this.elements.video.play();
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
} else {
this.elements.video.pause();
this.elements.playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
}
}
toggleFavorite(channelId) {
if (this.state.favorites[channelId]) {
delete this.state.favorites[channelId];
this.showNotification('Removed from favorites', 'info');
} else {
this.state.favorites[channelId] = true;
this.showNotification('Added to favorites', 'success');
}
localStorage.setItem('clarke_ultra_favs', JSON.stringify(this.state.favorites));
// Refresh if in favorites filter
if (this.state.currentFilter === 'fav') {
this.filterChannels();
}
}
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.log(`Fullscreen error: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
toggleMute() {
this.elements.video.muted = !this.elements.video.muted;
this.updateStatus(this.elements.video.muted ? 'Muted' : 'Unmuted');
}
seek(seconds) {
this.elements.video.currentTime += seconds;
}
updateStatus(text) {
this.elements.statusText.textContent = text;
}
updatePlaybackInfo() {
const current = this.elements.video.currentTime;
const duration = this.elements.video.duration || 0;
// Format time
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
this.elements.currentTime.textContent = formatTime(current);
this.elements.totalTime.textContent = formatTime(duration);
// Update buffer
if (this.elements.video.buffered.length > 0) {
const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1);
const bufferPercentage = duration > 0 ? (bufferedEnd / duration) * 100 : 0;
this.elements.bufferProgress.style.width = `${bufferPercentage}%`;
this.state.stats.buffer = Math.round(bufferedEnd - current);
this.elements.bufferStat.textContent = `${this.state.stats.buffer}s`;
}
}
updateStatsLoop() {
// Update health based on buffering
if (this.state.hls && this.elements.video.buffered.length > 0) {
const bufferedEnd = this.elements.video.buffered.end(this.elements.video.buffered.length - 1);
const bufferTime = bufferedEnd - this.elements.video.currentTime;
if (bufferTime > 5) {
this.state.stats.health = 100;
} else if (bufferTime > 2) {
this.state.stats.health = 80;
} else if (bufferTime > 0) {
this.state.stats.health = 60;
} else {
this.state.stats.health = 40;
}
this.elements.healthStat.textContent = `${this.state.stats.health}%`;
}
// Continue loop
setTimeout(() => this.updateStatsLoop(), 1000);
}
clearPlaylist() {
this.state.channels = [];
this.state.filteredChannels = [];
this.state.currentChannel = null;
this.state.currentFile = null;
if (this.state.hls) {
this.state.hls.destroy();
this.state.hls = null;
}
this.elements.video.pause();
this.elements.video.src = '';
this.elements.fileName.textContent = '';
this.elements.fileInput.value = '';
this.renderChannels();
this.elements.channelCount.textContent = '0';
this.updateStatus('Playlist cleared');
this.elements.currentChannel.textContent = 'Welcome to Clarke Ultra';
this.elements.currentRegion.textContent = 'TV';
this.elements.currentQuality.textContent = '4K READY';
this.showNotification('Playlist cleared', 'info');
}
showLoading(show) {
this.elements.loadingOverlay.style.display = show ? 'flex' : 'none';
}
showSettings() {
const settings = `
<div class="overlay-content" style="max-width: 500px;">
<div class="overlay-icon">
<i class="fas fa-cog"></i>
</div>
<h2 class="overlay-title">Settings</h2>
<div style="text-align: left; margin-bottom: 2rem;">
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">Video Quality</label>
<select style="width: 100%; padding: 0.75rem; background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary);">
<option>Auto</option>
<option>1080p</option>
<option>720p</option>
<option>480p</option>
</select>
</div>
<div style="margin-bottom: 1rem;">
<label style="display: block; margin-bottom: 0.5rem; color: var(--text-secondary);">Buffer Size</label>
<select style="width: 100%; padding: 0.75rem; background: rgba(255,255,255,0.05); border: 1px solid var(--border); border-radius: 8px; color: var(--text-primary);">
<option>30 seconds</option>
<option>60 seconds</option>
<option>90 seconds</option>
</select>
</div>
</div>
<div class="overlay-buttons">
<button class="tv-btn tv-btn-primary" style="padding: 0.75rem 2rem;">
Save
</button>
<button class="tv-btn tv-btn-secondary" style="padding: 0.75rem 2rem;">
Cancel
</button>
</div>
</div>
`;
this.showOverlay(settings);
}
showNotification(message, type = 'info') {
// Create notification
const notification = document.createElement('div');
notification.className = 'tv-overlay';
notification.style.display = 'flex';
notification.style.zIndex = '3000';
notification.style.animation = 'fadeIn 0.3s ease';
notification.innerHTML = `
<div class="overlay-content" style="max-width: 400px;">
<div class="overlay-icon">
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
</div>
<p class="overlay-message">${message}</p>
<div class="overlay-buttons">
<button class="tv-btn tv-btn-primary" style="padding: 0.75rem 2rem;">
OK
</button>
</div>
</div>
`;
document.body.appendChild(notification);
// Add click to close
notification.querySelector('button').addEventListener('click', () => {
notification.style.animation = 'fadeIn 0.3s ease reverse';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
});
// Auto-close after 3 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.style.animation = 'fadeIn 0.3s ease reverse';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}, 3000);
}
showOverlay(content) {
const overlay = document.createElement('div');
overlay.className = 'tv-overlay';
overlay.style.display = 'flex';
overlay.innerHTML = content;
document.body.appendChild(overlay);
// Add close button functionality
overlay.addEventListener('click', (e) => {
if (e.target === overlay || e.target.closest('.tv-btn-secondary')) {
overlay.style.animation = 'fadeIn 0.3s ease reverse';
setTimeout(() => {
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
}, 300);
}
});
}
}
// Initialize Clarke Player Ultra
let player;
document.addEventListener('DOMContentLoaded', () => {
player = new ClarkePlayerUltra();
});
</script>
</body>
</html>