|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Advanced HLS Player with Recording & Upscaling</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.timeline-handle { |
|
width: 12px; |
|
height: 20px; |
|
background-color: #3b82f6; |
|
cursor: ew-resize; |
|
position: absolute; |
|
top: -4px; |
|
z-index: 10; |
|
} |
|
.timeline-range { |
|
background-color: rgba(59, 130, 246, 0.3); |
|
height: 12px; |
|
position: absolute; |
|
top: 0; |
|
} |
|
.waveform { |
|
background: linear-gradient(90deg, #4b5563 0%, #6b7280 100%); |
|
height: 60px; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
.waveform-bar { |
|
background-color: #d1d5db; |
|
width: 2px; |
|
position: absolute; |
|
bottom: 0; |
|
} |
|
.progress-indicator { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 2px; |
|
height: 100%; |
|
background-color: #ef4444; |
|
z-index: 5; |
|
} |
|
.upscale-preview { |
|
transition: all 0.3s ease; |
|
} |
|
.upscale-preview:hover { |
|
transform: scale(1.05); |
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); |
|
} |
|
.glow { |
|
animation: glow 2s infinite alternate; |
|
} |
|
@keyframes glow { |
|
from { |
|
box-shadow: 0 0 5px rgba(59, 130, 246, 0.5); |
|
} |
|
to { |
|
box-shadow: 0 0 15px rgba(59, 130, 246, 0.8); |
|
} |
|
} |
|
</style> |
|
<script> |
|
const manifestHistory = JSON.parse(localStorage.getItem('manifestHistory') || '[]'); |
|
let MAX_HISTORY = parseInt(localStorage.getItem('maxHistory') || '5'); |
|
let ALLOW_DUPLICATES = localStorage.getItem('allowDuplicates') !== 'false'; |
|
let HIGHLIGHT_DIVERGENT = localStorage.getItem('highlightDivergent') !== 'false'; |
|
let ENABLE_PREVIEW_PLAYER = localStorage.getItem('enablePreviewPlayer') !== 'false'; |
|
let currentTabType = null; |
|
let currentTabIndex = null; |
|
let hls = null; |
|
|
|
const CORS_PROXIES = [ |
|
'https://api.allorigins.win/raw?url=', |
|
'https://proxy.cors.sh/', |
|
'https://corsproxy.io/?', |
|
'https://cors-proxy.htmldriven.com/?url=', |
|
'https://crossorigin.me/' |
|
]; |
|
|
|
function isValidProxyResponse(response) { |
|
const contentType = response.headers.get('content-type'); |
|
return !contentType || ( |
|
contentType.includes('application/vnd.apple.mpegurl') || |
|
contentType.includes('application/x-mpegurl') || |
|
contentType.includes('text/plain') || |
|
contentType.includes('text/html') || |
|
contentType.includes('video/mp2t') || |
|
contentType.includes('application/octet-stream') || |
|
contentType.includes('audio/mpegurl') |
|
); |
|
} |
|
|
|
async function fetchWithCorsProxy(url) { |
|
let lastError; |
|
|
|
|
|
try { |
|
const response = await fetch(url); |
|
if (response.ok) { |
|
return response; |
|
} |
|
} catch (error) { |
|
console.warn('Direct fetch failed:', error); |
|
} |
|
|
|
|
|
for (const proxy of CORS_PROXIES) { |
|
try { |
|
const proxyUrl = proxy + encodeURIComponent(url); |
|
const response = await fetch(proxyUrl, { |
|
headers: { |
|
'x-requested-with': 'XMLHttpRequest', |
|
'origin': window.location.origin |
|
} |
|
}); |
|
if (response.ok) { |
|
return response; |
|
} |
|
} catch (error) { |
|
lastError = error; |
|
console.warn(`Proxy ${proxy} failed:`, error); |
|
continue; |
|
} |
|
} |
|
|
|
|
|
try { |
|
const jsonpUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`; |
|
const response = await fetch(jsonpUrl); |
|
if (response.ok) { |
|
const data = await response.json(); |
|
if (data.contents) { |
|
|
|
return new Response(data.contents, { |
|
status: 200, |
|
headers: new Headers({ |
|
'content-type': 'application/x-mpegurl' |
|
}) |
|
}); |
|
} |
|
} |
|
} catch (error) { |
|
console.warn('JSONP approach failed:', error); |
|
} |
|
|
|
throw lastError || new Error('All proxies failed'); |
|
} |
|
|
|
function calculateSelectedDuration(selectedText) { |
|
const extinfRegex = /#EXTINF:([0-9.]+),/g; |
|
let match; |
|
let totalDuration = 0; |
|
let count = 0; |
|
|
|
while ((match = extinfRegex.exec(selectedText)) !== null) { |
|
totalDuration += parseFloat(match[1]); |
|
count++; |
|
} |
|
|
|
return { duration: totalDuration.toFixed(3), count }; |
|
} |
|
|
|
function handleSelection(event) { |
|
const selection = window.getSelection(); |
|
const selectedText = selection.toString().trim(); |
|
|
|
if (!selectedText) return; |
|
|
|
|
|
const manifestContent = event.target.closest('.line-numbers-content'); |
|
const manifestType = manifestContent.dataset.manifestType; |
|
const manifestUrl = manifestContent.closest('.tab').querySelector('.manifest-url').textContent.trim(); |
|
|
|
|
|
const baseUrl = manifestUrl.endsWith('/') ? manifestUrl : |
|
manifestUrl.substring(0, manifestUrl.lastIndexOf('/') + 1); |
|
|
|
|
|
const result = calculateSelectedDuration(selectedText); |
|
if (result.duration > 0) { |
|
|
|
let tooltip = document.getElementById('selection-tooltip'); |
|
if (!tooltip) { |
|
tooltip = document.createElement('div'); |
|
tooltip.id = 'selection-tooltip'; |
|
tooltip.className = 'selection-tooltip'; |
|
document.body.appendChild(tooltip); |
|
} |
|
|
|
tooltip.innerHTML = `Selected: ${sanitizeHtml(result.count.toString())} segments<br>Duration: ${sanitizeHtml(result.duration.toString())}s`; |
|
tooltip.style.display = 'block'; |
|
tooltip.style.left = (event.pageX + 10) + 'px'; |
|
tooltip.style.top = (event.pageY + 10) + 'px'; |
|
} |
|
|
|
|
|
if (!ENABLE_PREVIEW_PLAYER || manifestType !== "Media Manifest") { |
|
return; |
|
} |
|
|
|
const fullContent = manifestContent.textContent; |
|
const lines = fullContent.split('\n'); |
|
|
|
|
|
const initSegmentLine = lines.find(line => line.includes('#EXT-X-MAP')); |
|
const initSegment = initSegmentLine ? |
|
resolveUrl(initSegmentLine.match(/URI="([^"]+)"/)[1], manifestUrl) : null; |
|
|
|
|
|
const selectedLines = selectedText.split('\n'); |
|
|
|
const fullLines = fullContent.split('\n'); |
|
let lastInitSegment = initSegment; |
|
const selectionStart = fullContent.indexOf(selectedText); |
|
const contentBeforeSelection = fullContent.substring(0, selectionStart); |
|
const initSegments = contentBeforeSelection.match(/#EXT-X-MAP:URI="([^"]+)"/g); |
|
if (initSegments && initSegments.length > 0) { |
|
|
|
const lastInitMatch = initSegments[initSegments.length - 1].match(/URI="([^"]+)"/); |
|
if (lastInitMatch) { |
|
lastInitSegment = resolveUrl(lastInitMatch[1], manifestUrl); |
|
} |
|
} |
|
|
|
const segments = []; |
|
let currentDuration = null; |
|
|
|
selectedLines.forEach((line, index) => { |
|
if (line.startsWith('#EXTINF:')) { |
|
currentDuration = parseFloat(line.split(':')[1]); |
|
} else if (line.trim() && !line.startsWith('#')) { |
|
|
|
const startIdx = Math.max(0, index - 5); |
|
const endIdx = Math.min(selectedLines.length, index + 5); |
|
const context = selectedLines.slice(startIdx, endIdx).join('\n'); |
|
|
|
segments.push({ |
|
duration: currentDuration, |
|
uri: resolveUrl(line.trim(), baseUrl), |
|
fullText: context, |
|
initSegment: lastInitSegment |
|
}); |
|
currentDuration = null; |
|
} |
|
}); |
|
|
|
if (segments.length > 0) { |
|
createAndPlayTempPlaylist(segments); |
|
} |
|
} |
|
|
|
|
|
function resolveUrl(url, baseUrl) { |
|
if (!url) return url; |
|
if (url.startsWith('http://') || url.startsWith('https://')) { |
|
return url; |
|
} |
|
|
|
try { |
|
|
|
if (url.startsWith('/')) { |
|
|
|
const baseUrlObj = new URL(baseUrl); |
|
return `${baseUrlObj.origin}${url}`; |
|
} else { |
|
|
|
let base = baseUrl; |
|
if (!baseUrl.endsWith('/')) { |
|
base = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1); |
|
} |
|
return new URL(url, base).href; |
|
} |
|
} catch (e) { |
|
console.warn('Failed to resolve URL:', url, baseUrl); |
|
return url; |
|
} |
|
} |
|
|
|
function calculateTotalDuration(manifestContent) { |
|
const extinfRegex = /#EXTINF:([0-9.]+),/g; |
|
let match; |
|
let totalDuration = 0; |
|
|
|
while ((match = extinfRegex.exec(manifestContent))) { |
|
totalDuration += parseFloat(match[1]); |
|
} |
|
|
|
return totalDuration.toFixed(6); |
|
} |
|
|
|
function calculateBreakCounts(manifestContent) { |
|
const lines = manifestContent.split('\n'); |
|
let discontinuityCount = 0; |
|
let cueOutCount = 0; |
|
|
|
for (const line of lines) { |
|
if (line.trim() === '#EXT-X-DISCONTINUITY') { |
|
discontinuityCount++; |
|
} |
|
if (line.trim().startsWith('#EXT-X-CUE-OUT')) { |
|
cueOutCount++; |
|
} |
|
} |
|
|
|
return { discontinuityCount, cueOutCount }; |
|
} |
|
|
|
function calculateSegCount(manifestContent) { |
|
const extinfRegex = /#EXTINF:([0-9.]+),/g; |
|
let match; |
|
let count = 0; |
|
|
|
while ((match = extinfRegex.exec(manifestContent))) { |
|
count++; |
|
} |
|
return count; |
|
} |
|
|
|
function getMediaSequenceInfo(manifestContent) { |
|
const lines = manifestContent.split('\n'); |
|
let mediaSequence = null; |
|
let discSequence = null; |
|
let hasEndlist = false; |
|
|
|
for (const line of lines) { |
|
const trimmedLine = line.trim(); |
|
if (trimmedLine.startsWith('#EXT-X-MEDIA-SEQUENCE:')) { |
|
mediaSequence = parseInt(trimmedLine.split(':')[1]); |
|
} |
|
if (trimmedLine.startsWith('#EXT-X-DISCONTINUITY-SEQUENCE:')) { |
|
discSequence = parseInt(trimmedLine.split(':')[1]); |
|
} |
|
if (trimmedLine === '#EXT-X-ENDLIST') { |
|
hasEndlist = true; |
|
} |
|
} |
|
|
|
return { mediaSequence, discSequence, hasEndlist }; |
|
} |
|
|
|
$(document).ready(function () { |
|
$("#fetch-form").on("submit", function (event) { |
|
event.preventDefault(); |
|
const url = $("#url-input").val(); |
|
|
|
if (!isValidUrl(url)) { |
|
$("#error-message").text("Please enter a valid HTTP(S) URL").show(); |
|
return; |
|
} |
|
|
|
const loadingMessage = document.getElementById("loadingMessage"); |
|
const errorMessage = $("#error-message"); |
|
|
|
loadingMessage.style.display = "block"; |
|
errorMessage.hide(); |
|
|
|
try { |
|
fetchManifests(url); |
|
} catch (error) { |
|
loadingMessage.style.display = "none"; |
|
errorMessage.text("Error: " + error.message).show(); |
|
} |
|
}); |
|
|
|
async function fetchManifests(url) { |
|
const loadingMessage = document.getElementById("loadingMessage"); |
|
const errorMessage = $("#error-message"); |
|
|
|
try { |
|
|
|
const masterResponse = await fetchWithCorsProxy(url); |
|
const masterManifestContent = await masterResponse.text(); |
|
const masterOrigin = new URL(url).origin; |
|
|
|
|
|
const regex = /#EXT-X-STREAM-INF:(.+)\n(.+)/g; |
|
const videoUrls = []; |
|
const bandwidths = []; |
|
let match; |
|
|
|
while ((match = regex.exec(masterManifestContent))) { |
|
const attributes = match[1]; |
|
let manifestUrl = match[2]; |
|
const bandwidthMatch = attributes.match(/BANDWIDTH=(\d+)/); |
|
const bandwidth = bandwidthMatch ? parseInt(bandwidthMatch[1]) : null; |
|
|
|
if (!manifestUrl.startsWith("http")) { |
|
manifestUrl = new URL(manifestUrl, new URL(url)).href; |
|
} |
|
|
|
videoUrls.push(manifestUrl); |
|
bandwidths.push(bandwidth); |
|
} |
|
|
|
|
|
const regexA = /#EXT-X-MEDIA:TYPE=AUDIO.*?URI="(.+?)"/g; |
|
const audioUrls = []; |
|
const audioInfo = []; |
|
|
|
while ((match = regexA.exec(masterManifestContent))) { |
|
let audioUrl = match[1]; |
|
const groupMatch = match[0].match(/GROUP-ID="([^"]+)"/); |
|
const langMatch = match[0].match(/LANGUAGE="([^"]+)"/); |
|
const nameMatch = match[0].match(/NAME="([^"]+)"/); |
|
const groupId = groupMatch ? groupMatch[1] : 'unknown'; |
|
const language = langMatch ? langMatch[1] : nameMatch ? nameMatch[1] : 'unknown'; |
|
|
|
if (!audioUrl.startsWith("http")) { |
|
audioUrl = new URL(audioUrl, new URL(url)).href; |
|
} |
|
|
|
audioUrls.push(audioUrl); |
|
audioInfo.push({ url: audioUrl, groupId, language }); |
|
} |
|
|
|
|
|
const subtitleUrls = []; |
|
const subtitleInfo = []; |
|
const subtitleRegex = /#EXT-X-MEDIA:TYPE=SUBTITLES.*?URI="(.+?)"/g; |
|
|
|
while ((match = subtitleRegex.exec(masterManifestContent))) { |
|
let subtitleUrl = match[1]; |
|
const groupMatch = match[0].match(/GROUP-ID="([^"]+)"/); |
|
const langMatch = match[0].match(/LANGUAGE="([^"]+)"/); |
|
const nameMatch = match[0].match(/NAME="([^"]+)"/); |
|
const groupId = groupMatch ? groupMatch[1] : 'unknown'; |
|
const language = langMatch ? langMatch[1] : nameMatch ? nameMatch[1] : 'unknown'; |
|
|
|
if (!subtitleUrl.startsWith("http")) { |
|
subtitleUrl = new URL(subtitleUrl, new URL(url)).href; |
|
} |
|
subtitleUrls.push(subtitleUrl); |
|
subtitleInfo.push({ url: subtitleUrl, groupId, language }); |
|
} |
|
|
|
|
|
const manifestUrls = [url, ...videoUrls, ...audioUrls, ...subtitleUrls]; |
|
const manifests = await Promise.all(manifestUrls.map(async (manifestUrl, index) => { |
|
try { |
|
const response = await fetchWithCorsProxy(manifestUrl); |
|
if (!response.ok) { |
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
} |
|
const content = await response.text(); |
|
|
|
let type; |
|
let bandwidth = null; |
|
let metadata = null; |
|
|
|
if (manifestUrl === url) { |
|
type = "Multivariant"; |
|
} else if (audioUrls.includes(manifestUrl)) { |
|
type = "Audio Manifest"; |
|
const audioMeta = audioInfo.find(info => info.url === manifestUrl); |
|
if (audioMeta) { |
|
metadata = { |
|
groupId: audioMeta.groupId, |
|
language: audioMeta.language |
|
}; |
|
} |
|
} else if (subtitleUrls.includes(manifestUrl)) { |
|
type = "Subtitle Manifest"; |
|
const subtitleMeta = subtitleInfo.find(info => info.url === manifestUrl); |
|
if (subtitleMeta) { |
|
metadata = { |
|
groupId: subtitleMeta.groupId, |
|
language: subtitleMeta.language |
|
}; |
|
} |
|
} else { |
|
type = "Media Manifest"; |
|
const mediaIndex = videoUrls.indexOf(manifestUrl); |
|
if (mediaIndex !== -1) { |
|
bandwidth = bandwidths[mediaIndex]; |
|
} |
|
} |
|
|
|
return { |
|
type, |
|
content: content.replace(/(#EXT[^:]+:)/g, "\n$1").replace(/\n(\n+)/g, "\n"), |
|
bandwidth, |
|
metadata, |
|
url: manifestUrl |
|
}; |
|
} catch (error) { |
|
console.error(`Error fetching manifest ${manifestUrl}:`, error.message); |
|
return { |
|
type: "Error", |
|
content: `Failed to fetch manifest: ${error.message}`, |
|
url: manifestUrl, |
|
error: true |
|
}; |
|
} |
|
})); |
|
|
|
|
|
if (url) { |
|
const isDuplicate = manifestHistory.some(entry => entry.url === url); |
|
|
|
if (!isDuplicate || ALLOW_DUPLICATES) { |
|
const historyEntry = { |
|
url: url, |
|
timestamp: new Date().toLocaleString(), |
|
manifests: manifests |
|
}; |
|
|
|
manifestHistory.unshift(historyEntry); |
|
while (manifestHistory.length > MAX_HISTORY) { |
|
manifestHistory.pop(); |
|
} |
|
|
|
localStorage.setItem('manifestHistory', JSON.stringify(manifestHistory)); |
|
updateHistoryDropdown(); |
|
} |
|
} |
|
|
|
|
|
displayManifests(manifests, url); |
|
loadingMessage.style.display = "none"; |
|
|
|
} catch (error) { |
|
console.error("Error:", error); |
|
loadingMessage.style.display = "none"; |
|
errorMessage.text(`Error fetching manifests: ${error.message}`).show(); |
|
} |
|
} |
|
|
|
function updateHistoryDropdown() { |
|
const historyList = $("#history-list"); |
|
const historyTabsContainer = $(".history-tabs-container"); |
|
historyList.empty(); |
|
historyTabsContainer.empty(); |
|
|
|
manifestHistory.forEach((entry, index) => { |
|
const truncatedUrl = entry.url.length > 100 |
|
? sanitizeHtml(entry.url.substring(0, 97) + '...') |
|
: sanitizeHtml(entry.url); |
|
|
|
const listItem = $(` |
|
<div class="history-item"> |
|
<div class="history-info"> |
|
<div class="history-header"> |
|
<span class="history-index">#${sanitizeHtml(index + 1)}</span> |
|
<button class="copy-url-btn" data-url="${sanitizeHtml(entry.url)}" title="Copy URL"> |
|
📋 |
|
</button> |
|
</div> |
|
<span class="history-url" title="${sanitizeHtml(entry.url)}">${truncatedUrl}</span> |
|
<span class="history-timestamp">${sanitizeHtml(entry.timestamp)}</span> |
|
</div> |
|
<button class="view-history-btn" data-index="${sanitizeHtml(index)}">View</button> |
|
</div> |
|
`); |
|
historyList.append(listItem); |
|
|
|
const historyTab = $(` |
|
<button class="history-tab ${index === 0 ? 'active' : ''}" |
|
data-index="${sanitizeHtml(index)}" |
|
title="Fetched: ${sanitizeHtml(entry.timestamp)}"> |
|
${sanitizeHtml(truncatedUrl.substring(0, 30))} |
|
${truncatedUrl.length > 30 ? '...' : ''} |
|
<span class="history-tab-index">#${sanitizeHtml(index + 1)}</span> |
|
</button> |
|
`); |
|
historyTabsContainer.append(historyTab); |
|
}); |
|
} |
|
|
|
$(document).on("click", ".history-tab", function() { |
|
const index = $(this).data("index"); |
|
const historyEntry = manifestHistory[index]; |
|
|
|
$(".history-tab").removeClass("active"); |
|
$(this).addClass("active"); |
|
|
|
displayManifests(historyEntry.manifests); |
|
|
|
if (currentTabType) { |
|
let targetTab = null; |
|
|
|
$("#tabs li a").each(function() { |
|
const tabText = $(this).text(); |
|
if (currentTabType === "Multivariant") { |
|
if (tabText === currentTabType) { |
|
targetTab = $(this); |
|
return false; |
|
} |
|
} else if (tabText.includes(currentTabType)) { |
|
const tabIndex = parseInt(tabText.split('[')[1]) || 1; |
|
if (tabIndex === currentTabIndex) { |
|
targetTab = $(this); |
|
return false; |
|
} |
|
} |
|
}); |
|
|
|
if (targetTab) { |
|
const target = targetTab.attr("href"); |
|
$(".tab").hide(); |
|
$("ul li a").removeClass("active"); |
|
$(target).show(); |
|
targetTab.addClass("active"); |
|
} else { |
|
$("#tabs li a").first().click(); |
|
} |
|
} |
|
}); |
|
|
|
$("#history-toggle").click(function() { |
|
$("#history-dropdown").toggleClass("show"); |
|
}); |
|
|
|
document.addEventListener('mousedown', function(event) { |
|
const selection = window.getSelection(); |
|
const tooltip = document.getElementById('selection-tooltip'); |
|
const playerContainer = document.getElementById('floating-player'); |
|
const video = document.getElementById('video'); |
|
|
|
|
|
if (!event.target.closest('.line-numbers-content') && |
|
!event.target.closest('#floating-player')) { |
|
|
|
|
|
selection.removeAllRanges(); |
|
|
|
|
|
if (tooltip) { |
|
tooltip.style.display = 'none'; |
|
} |
|
|
|
|
|
if (playerContainer && playerContainer.style.display !== 'none') { |
|
video.pause(); |
|
if (hls) { |
|
hls.destroy(); |
|
hls = null; |
|
} |
|
playerContainer.style.display = 'none'; |
|
} |
|
} |
|
}); |
|
|
|
document.addEventListener('selectionchange', function() { |
|
const selection = window.getSelection(); |
|
const tooltip = document.getElementById('selection-tooltip'); |
|
const playerContainer = document.getElementById('floating-player'); |
|
const video = document.getElementById('video'); |
|
|
|
if (!selection.toString().trim()) { |
|
|
|
if (tooltip) { |
|
tooltip.style.display = 'none'; |
|
} |
|
|
|
|
|
if (playerContainer && playerContainer.style.display !== 'none') { |
|
|
|
const cleanup = () => { |
|
if (hls) { |
|
hls.destroy(); |
|
hls = null; |
|
} |
|
playerContainer.style.display = 'none'; |
|
}; |
|
|
|
|
|
if (!video.paused) { |
|
video.pause() |
|
.then(cleanup) |
|
.catch(() => { |
|
|
|
cleanup(); |
|
}); |
|
} else { |
|
cleanup(); |
|
} |
|
} |
|
} |
|
}); |
|
|
|
$("#settings-toggle").click(function() { |
|
$("#settings-modal").show(); |
|
}); |
|
|
|
$("#close-settings").click(function() { |
|
$("#settings-modal").hide(); |
|
}); |
|
|
|
$("#save-settings").click(function() { |
|
const newMaxHistory = parseInt($("#max-history-input").val()); |
|
const newAllowDuplicates = $("#allow-duplicates-input").is(":checked"); |
|
const newHighlightDivergent = $("#highlight-divergent-input").is(":checked"); |
|
const newEnablePreviewPlayer = $("#enable-preview-player-input").is(":checked"); |
|
|
|
if (newMaxHistory >= 1 && newMaxHistory <= 10) { |
|
MAX_HISTORY = newMaxHistory; |
|
ALLOW_DUPLICATES = newAllowDuplicates; |
|
HIGHLIGHT_DIVERGENT = newHighlightDivergent; |
|
ENABLE_PREVIEW_PLAYER = newEnablePreviewPlayer; |
|
|
|
localStorage.setItem('maxHistory', MAX_HISTORY); |
|
localStorage.setItem('allowDuplicates', ALLOW_DUPLICATES); |
|
localStorage.setItem('highlightDivergent', HIGHLIGHT_DIVERGENT); |
|
localStorage.setItem('enablePreviewPlayer', ENABLE_PREVIEW_PLAYER); |
|
|
|
while (manifestHistory.length > MAX_HISTORY) { |
|
manifestHistory.pop(); |
|
} |
|
|
|
localStorage.setItem('manifestHistory', JSON.stringify(manifestHistory)); |
|
updateHistoryDropdown(); |
|
if (manifestHistory.length > 0) { |
|
displayManifests(manifestHistory[0].manifests); |
|
} |
|
$("#settings-modal").hide(); |
|
|
|
|
|
if (!ENABLE_PREVIEW_PLAYER) { |
|
const playerContainer = document.getElementById('floating-player'); |
|
const video = document.getElementById('video'); |
|
if (playerContainer && playerContainer.style.display !== 'none') { |
|
video.pause(); |
|
if (hls) { |
|
hls.destroy(); |
|
hls = null; |
|
} |
|
playerContainer.style.display = 'none'; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
$("#clear-history").click(function() { |
|
if (confirm("Are you sure you want to clear all history?")) { |
|
manifestHistory.length = 0; |
|
localStorage.setItem('manifestHistory', JSON.stringify(manifestHistory)); |
|
updateHistoryDropdown(); |
|
$("#manifests-container").empty(); |
|
$("#tabs").empty(); |
|
} |
|
}); |
|
|
|
$(document).on("click", ".copy-url-btn", function(e) { |
|
e.stopPropagation(); |
|
const url = $(this).data("url"); |
|
navigator.clipboard.writeText(url).then(() => { |
|
const btn = $(this); |
|
btn.text("✓").css("color", "#4CAF50"); |
|
setTimeout(() => { |
|
btn.text("📋").css("color", "inherit"); |
|
}, 1000); |
|
}); |
|
}); |
|
|
|
$(document).on("click", "ul li a", function (event) { |
|
event.preventDefault(); |
|
const target = $(this).attr("href"); |
|
|
|
const tabText = $(this).text(); |
|
if (tabText.includes('[')) { |
|
const type = tabText.split('[')[0].trim(); |
|
const index = parseInt(tabText.split('[')[1]) || 1; |
|
currentTabType = type; |
|
currentTabIndex = index; |
|
} else { |
|
currentTabType = tabText; |
|
currentTabIndex = 1; |
|
} |
|
|
|
$(".tab").hide(); |
|
$("ul li a").removeClass("active"); |
|
|
|
$(target).show(); |
|
$(this).addClass("active"); |
|
}); |
|
|
|
|
|
$("ul li a:first").addClass("active"); |
|
|
|
|
|
$("#max-history-input").val(MAX_HISTORY); |
|
$("#allow-duplicates-input").prop('checked', ALLOW_DUPLICATES); |
|
$("#highlight-divergent-input").prop('checked', HIGHLIGHT_DIVERGENT); |
|
$("#enable-preview-player-input").prop('checked', ENABLE_PREVIEW_PLAYER); |
|
}); |
|
|
|
function displayManifests(manifests, url = '') { |
|
$("#manifests-container").empty(); |
|
$("#tabs").empty(); |
|
let videoCount = 1; |
|
let audioCount = 1; |
|
let subCount = 1; |
|
|
|
for (let i = 0; i < manifests.length; i++) { |
|
const manifest = manifests[i]; |
|
const tabId = `tab-${i}`; |
|
let manifestName = ""; |
|
let tooltipText = ""; |
|
|
|
|
|
const tabLink = $('<li></li>'); |
|
const tabContent = $(`<div id="${tabId}" class="tab"></div>`); |
|
|
|
if (manifest.type === "Multivariant") { |
|
manifestName = manifest.type; |
|
|
|
tabLink.html(`<a href="#${tabId}">${manifestName}</a>`); |
|
|
|
const mediaSequences = []; |
|
const discSequences = []; |
|
|
|
manifests.forEach(m => { |
|
if (m.type !== "Multivariant") { |
|
const sequenceInfo = getMediaSequenceInfo(m.content); |
|
if (sequenceInfo.mediaSequence !== null && !sequenceInfo.hasEndlist) { |
|
mediaSequences.push(sequenceInfo.mediaSequence); |
|
} |
|
if (sequenceInfo.discSequence !== null) { |
|
discSequences.push(sequenceInfo.discSequence); |
|
} |
|
} |
|
}); |
|
|
|
if (mediaSequences.length > 0 || discSequences.length > 0) { |
|
const mostCommonMediaSeq = mediaSequences.length > 0 |
|
? mediaSequences.reduce((a, b) => |
|
mediaSequences.filter(v => v === a).length >= mediaSequences.filter(v => v === b).length ? a : b |
|
) |
|
: null; |
|
|
|
const mostCommonDiscSeq = discSequences.length > 0 |
|
? discSequences.reduce((a, b) => |
|
discSequences.filter(v => v === a).length >= discSequences.filter(v => v === b).length ? a : b |
|
) |
|
: null; |
|
|
|
const formattedMediaSeqs = mediaSequences.map(seq => |
|
HIGHLIGHT_DIVERGENT && seq !== mostCommonMediaSeq |
|
? `<span class="divergent-sequence">${seq}</span>` |
|
: seq |
|
); |
|
|
|
const formattedDiscSeqs = discSequences.map(seq => |
|
HIGHLIGHT_DIVERGENT && seq !== mostCommonDiscSeq |
|
? `<span class="divergent-sequence">${seq}</span>` |
|
: seq |
|
); |
|
|
|
const metricsHtml = ` |
|
<div class="metrics-container"> |
|
${mediaSequences.length > 0 ? ` |
|
<div class="metric-box"> |
|
<h3>Media Sequences</h3> |
|
<div class="metric-value">${formattedMediaSeqs.join(', ')}</div> |
|
</div> |
|
` : ''} |
|
${discSequences.length > 0 ? ` |
|
<div class="metric-box"> |
|
<h3>Discontinuity Sequences</h3> |
|
<div class="metric-value">${formattedDiscSeqs.join(', ')}</div> |
|
</div> |
|
` : ''} |
|
</div> |
|
`; |
|
tabContent.append(metricsHtml); |
|
} |
|
} else if (manifest.type === "Media Manifest") { |
|
manifestName = `${manifest.type}[${videoCount}]`; |
|
if (manifest.bandwidth) { |
|
tooltipText = `BW: ${manifest.bandwidth}`; |
|
} |
|
|
|
tabLink.html(`<a href="#${tabId}" ${tooltipText ? `title="${tooltipText}"` : ''}>${manifestName}</a>`); |
|
videoCount++; |
|
} else if (manifest.type === "Audio Manifest") { |
|
manifestName = `${manifest.type}[${audioCount}]`; |
|
if (manifest.metadata) { |
|
tooltipText = `${manifest.metadata.groupId};${manifest.metadata.language}`; |
|
} |
|
|
|
tabLink.html(`<a href="#${tabId}" ${tooltipText ? `title="${tooltipText}"` : ''}>${manifestName}</a>`); |
|
audioCount++; |
|
} else if (manifest.type === "Subtitle Manifest") { |
|
manifestName = `${manifest.type}[${subCount}]`; |
|
if (manifest.metadata) { |
|
tooltipText = `${manifest.metadata.groupId};${manifest.metadata.language}`; |
|
} |
|
|
|
tabLink.html(`<a href="#${tabId}" ${tooltipText ? `title="${tooltipText}"` : ''}>${manifestName}</a>`); |
|
subCount++; |
|
} else { |
|
manifestName = "Other M3U8"; |
|
|
|
tabLink.html(`<a href="#${tabId}">${manifestName}</a>`); |
|
} |
|
|
|
const formatDuration = (seconds) => { |
|
const h = Math.floor(seconds / 3600); |
|
const m = Math.floor((seconds % 3600) / 60); |
|
const s = (seconds % 60).toFixed(2); |
|
return [ |
|
h > 0 ? `${h}h` : '', |
|
m > 0 ? `${m}m` : '', |
|
s > 0 ? `${s}s` : '' |
|
].filter(Boolean).join(''); |
|
}; |
|
|
|
const totalDuration = calculateTotalDuration(manifest.content); |
|
const segmentCount = calculateSegCount(manifest.content); |
|
const breakCounts = calculateBreakCounts(manifest.content); |
|
const sequenceInfo = getMediaSequenceInfo(manifest.content); |
|
|
|
if (manifest.type !== "Multivariant") { |
|
const metricsHtml = ` |
|
<div class="metrics-container"> |
|
<div class="metric-box"> |
|
<h3>Total Duration</h3> |
|
<div class="metric-value"> |
|
${sanitizeHtml(formatDuration(totalDuration))} (${sanitizeHtml(totalDuration)} seconds) |
|
</div> |
|
</div> |
|
<div class="metric-box"> |
|
<h3>Segment Count</h3> |
|
<div class="metric-value">${sanitizeHtml(segmentCount.toString())}</div> |
|
</div> |
|
<div class="metric-box"> |
|
<h3>Discontinuities</h3> |
|
<div class="metric-value">${sanitizeHtml(breakCounts.discontinuityCount.toString())}</div> |
|
</div> |
|
<div class="metric-box"> |
|
<h3>Cue-outs</h3> |
|
<div class="metric-value">${sanitizeHtml(breakCounts.cueOutCount.toString())}</div> |
|
</div> |
|
</div> |
|
`; |
|
tabContent.append(metricsHtml); |
|
} |
|
|
|
const lines = manifest.content.split('\n'); |
|
const lineNumbersHtml = lines.map((_, i) => `<span class="line-number">${i + 1}</span>`).join(''); |
|
|
|
const manifestContentHtml = ` |
|
<div class="line-numbers"> |
|
<div class="line-numbers-rows"> |
|
${lineNumbersHtml} |
|
</div> |
|
<div class="line-numbers-content" onmouseup="handleSelection(event)" data-manifest-type="${sanitizeHtml(manifest.type)}"> |
|
<pre><code>${sanitizeHtml(manifest.content)}</code></pre> |
|
</div> |
|
</div> |
|
<button onclick="topFunction()" id="scrollToTopBtn" title="Go to top">Back to Top</button> |
|
<div class="manifest-url"> |
|
${sanitizeHtml(manifest.url || url)} |
|
</div> |
|
`; |
|
|
|
tabContent.append(manifestContentHtml); |
|
$("#tabs").append(tabLink); |
|
$("#manifests-container").append(tabContent); |
|
} |
|
|
|
$(".tab:first").show(); |
|
$("pre code").each(function (i, block) { |
|
hljs.highlightElement(block); |
|
}); |
|
} |
|
|
|
function topFunction() { |
|
document.body.scrollTop = 0; |
|
document.documentElement.scrollTop = 0; |
|
} |
|
|
|
function createAndPlayTempPlaylist(segments) { |
|
let playlist = '#EXTM3U\n'; |
|
playlist += '#EXT-X-VERSION:6\n'; |
|
playlist += '#EXT-X-TARGETDURATION:6\n'; |
|
playlist += '#EXT-X-MEDIA-SEQUENCE:0\n'; |
|
playlist += '#EXT-X-INDEPENDENT-SEGMENTS\n'; |
|
|
|
let currentInitSegment = null; |
|
|
|
segments.forEach((segment, index) => { |
|
const segmentText = segment.fullText || ''; |
|
|
|
if (segment.initSegment !== currentInitSegment) { |
|
currentInitSegment = segment.initSegment; |
|
playlist += `#EXT-X-MAP:URI="${currentInitSegment}"\n`; |
|
} |
|
|
|
if (index > 0 && segmentText.includes('#EXT-X-DISCONTINUITY')) { |
|
playlist += '#EXT-X-DISCONTINUITY\n'; |
|
} |
|
|
|
playlist += `#EXTINF:${segment.duration || 6.0},\n`; |
|
playlist += `${segment.uri}\n`; |
|
}); |
|
|
|
playlist += '#EXT-X-ENDLIST'; |
|
|
|
const blob = new Blob([playlist], { type: 'application/x-mpegURL' }); |
|
const playlistUrl = URL.createObjectURL(blob); |
|
|
|
const vid_src = document.querySelector("#tab-1 > div.manifest-url").innerHTML |
|
const blob2 = new Blob([vid_src], { type: 'application/x-mpegURL' }); |
|
const mani_url = URL.createObjectURL(blob2); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.log(playlistUrl); |
|
console.log(mani_url); |
|
</script> |
|
</head> |
|
<body class="bg-gray-900 text-gray-100 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<div class="flex justify-between items-center mb-8"> |
|
<h1 class="text-3xl font-bold bg-gradient-to-r from-blue-500 to-purple-600 bg-clip-text text-transparent"> |
|
<i class="fas fa-video mr-2"></i>Advanced HLS Player |
|
</h1> |
|
<div class="flex space-x-4"> |
|
<button id="recordBtn" class="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg flex items-center"> |
|
<i class="fas fa-circle mr-2"></i> Record |
|
</button> |
|
<button id="upscaleBtn" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg flex items-center"> |
|
<i class="fas fa-expand mr-2"></i> Upscale |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
<div class="lg:col-span-2 space-y-6"> |
|
|
|
<div class="relative bg-black rounded-xl overflow-hidden shadow-2xl"> |
|
<video id="video" controls playsinline class="w-full aspect-video"></video> |
|
<div id="recordingIndicator" class="absolute top-4 right-4 bg-red-600 px-3 py-1 rounded-full hidden"> |
|
<span class="flex items-center"> |
|
<span class="h-2 w-2 bg-white rounded-full mr-2 animate-pulse"></span> |
|
REC |
|
</span> |
|
</div> |
|
<div id="upscaleOverlay" class="absolute inset-0 bg-black bg-opacity-70 flex items-center justify-center hidden"> |
|
<div class="text-center p-6 bg-gray-800 rounded-lg max-w-md"> |
|
<div class="spinner mb-4"> |
|
<i class="fas fa-spinner fa-spin text-4xl text-purple-500"></i> |
|
</div> |
|
<h3 class="text-xl font-bold mb-2">Upscaling Video</h3> |
|
<p class="text-gray-300">Processing with Real-ESRGAN...</p> |
|
<div class="w-full bg-gray-700 h-2 mt-4 rounded-full overflow-hidden"> |
|
<div id="upscaleProgress" class="bg-purple-500 h-full" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 p-4 rounded-xl shadow-lg"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h3 class="font-semibold text-lg"> |
|
<i class="fas fa-cut mr-2 text-blue-400"></i>Clip Editor |
|
</h3> |
|
<div class="flex space-x-2"> |
|
<button id="setStartBtn" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm"> |
|
Set Start |
|
</button> |
|
<button id="setEndBtn" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm"> |
|
Set End |
|
</button> |
|
<button id="exportClipBtn" class="px-3 py-1 bg-green-600 hover:bg-green-700 rounded text-sm"> |
|
Export Clip |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="relative waveform rounded-lg mb-2"> |
|
<div id="waveformBars"></div> |
|
<div id="progressIndicator" class="progress-indicator"></div> |
|
<div id="timelineRange" class="timeline-range hidden"></div> |
|
<div id="startHandle" class="timeline-handle hidden"></div> |
|
<div id="endHandle" class="timeline-handle hidden"></div> |
|
</div> |
|
|
|
<div class="flex justify-between text-xs text-gray-400"> |
|
<span id="currentTime">00:00:00</span> |
|
<span id="duration">00:00:00</span> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 p-4 rounded-xl shadow-lg"> |
|
<h3 class="font-semibold text-lg mb-4"> |
|
<i class="fas fa-stream mr-2 text-blue-400"></i>Stream Controls |
|
</h3> |
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">HLS Stream URL</label> |
|
<div class="flex"> |
|
<input id="streamUrl" type="text" value="https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8" |
|
class="flex-1 px-3 py-2 bg-gray-700 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<button id="loadStreamBtn" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-r-lg"> |
|
Load |
|
</button> |
|
</div> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Quality</label> |
|
<select id="qualitySelect" class="w-full px-3 py-2 bg-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"> |
|
<option value="auto">Auto</option> |
|
<option value="high">High</option> |
|
<option value="medium">Medium</option> |
|
<option value="low">Low</option> |
|
</select> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="space-y-6"> |
|
|
|
<div class="bg-gray-800 p-4 rounded-xl shadow-lg"> |
|
<h3 class="font-semibold text-lg mb-4 flex items-center"> |
|
<i class="fas fa-record-vinyl mr-2 text-red-400"></i>Recordings |
|
</h3> |
|
<div id="recordingsList" class="space-y-2 max-h-60 overflow-y-auto"> |
|
<div class="text-center py-4 text-gray-500"> |
|
<i class="fas fa-folder-open text-2xl mb-2"></i> |
|
<p>No recordings yet</p> |
|
</div> |
|
</div> |
|
<div class="mt-4 pt-4 border-t border-gray-700"> |
|
<label class="block text-sm font-medium mb-2">Recording Format</label> |
|
<div class="flex space-x-2"> |
|
<button class="flex-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm"> |
|
MP4 |
|
</button> |
|
<button class="flex-1 px-3 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm"> |
|
WebM |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-gray-800 p-4 rounded-xl shadow-lg"> |
|
<h3 class="font-semibold text-lg mb-4 flex items-center"> |
|
<i class="fas fa-expand mr-2 text-purple-400"></i>Upscale Settings |
|
</h3> |
|
<div class="space-y-4"> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Model</label> |
|
<select class="w-full px-3 py-2 bg-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"> |
|
<option>Real-ESRGAN (General)</option> |
|
<option>Real-ESRGAN (Anime)</option> |
|
<option>Real-ESRGAN+</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Scale Factor</label> |
|
<select class="w-full px-3 py-2 bg-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"> |
|
<option>2x</option> |
|
<option>4x</option> |
|
<option>8x (Experimental)</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm font-medium mb-1">Denoise Level</label> |
|
<input type="range" min="0" max="100" value="50" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="upscalePreviewContainer" class="bg-gray-800 p-4 rounded-xl shadow-lg hidden"> |
|
<h3 class="font-semibold text-lg mb-4 flex items-center"> |
|
<i class="fas fa-eye mr-2 text-green-400"></i>Upscale Preview |
|
</h3> |
|
<div class="grid grid-cols-2 gap-2 mb-4"> |
|
<div> |
|
<p class="text-xs text-center mb-1">Original</p> |
|
<img id="originalPreview" src="" class="w-full rounded border border-gray-600 upscale-preview"> |
|
</div> |
|
<div> |
|
<p class="text-xs text-center mb-1">Upscaled</p> |
|
<img id="upscaledPreview" src="" class="w-full rounded border border-purple-500 border-2 upscale-preview glow"> |
|
</div> |
|
</div> |
|
<button class="w-full py-2 bg-purple-600 hover:bg-purple-700 rounded-lg"> |
|
Apply to Entire Video |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const video = document.getElementById('video'); |
|
const streamUrlInput = document.getElementById('streamUrl'); |
|
const loadStreamBtn = document.getElementById('loadStreamBtn'); |
|
const qualitySelect = document.getElementById('qualitySelect'); |
|
const currentTimeDisplay = document.getElementById('currentTime'); |
|
const durationDisplay = document.getElementById('duration'); |
|
|
|
|
|
const recordBtn = document.getElementById('recordBtn'); |
|
const recordingIndicator = document.getElementById('recordingIndicator'); |
|
const recordingsList = document.getElementById('recordingsList'); |
|
|
|
|
|
const waveformBars = document.getElementById('waveformBars'); |
|
const progressIndicator = document.getElementById('progressIndicator'); |
|
const timelineRange = document.getElementById('timelineRange'); |
|
const startHandle = document.getElementById('startHandle'); |
|
const endHandle = document.getElementById('endHandle'); |
|
const setStartBtn = document.getElementById('setStartBtn'); |
|
const setEndBtn = document.getElementById('setEndBtn'); |
|
const exportClipBtn = document.getElementById('exportClipBtn'); |
|
|
|
|
|
const upscaleBtn = document.getElementById('upscaleBtn'); |
|
const upscaleOverlay = document.getElementById('upscaleOverlay'); |
|
const upscaleProgress = document.getElementById('upscaleProgress'); |
|
const upscalePreviewContainer = document.getElementById('upscalePreviewContainer'); |
|
const originalPreview = document.getElementById('originalPreview'); |
|
const upscaledPreview = document.getElementById('upscaledPreview'); |
|
|
|
|
|
let hls = null; |
|
let isRecording = false; |
|
let mediaRecorder = null; |
|
let recordedChunks = []; |
|
let startTime = 0; |
|
let endTime = 0; |
|
let isDraggingStart = false; |
|
let isDraggingEnd = false; |
|
let isDraggingProgress = false; |
|
|
|
|
|
function initHlsPlayer(url) { |
|
if (hls) { |
|
hls.destroy(); |
|
} |
|
|
|
if (Hls.isSupported()) { |
|
hls = new Hls(); |
|
hls.loadSource(url); |
|
hls.attachMedia(video); |
|
hls.on(Hls.Events.MANIFEST_PARSED, function() { |
|
console.log('Manifest parsed'); |
|
video.play(); |
|
generateWaveform(); |
|
}); |
|
|
|
hls.on(Hls.Events.ERROR, function(event, data) { |
|
console.error('HLS Error:', data); |
|
}); |
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) { |
|
|
|
video.src = url; |
|
video.addEventListener('loadedmetadata', function() { |
|
video.play(); |
|
generateWaveform(); |
|
}); |
|
} |
|
} |
|
|
|
|
|
function generateWaveform() { |
|
waveformBars.innerHTML = ''; |
|
const width = waveformBars.clientWidth; |
|
const height = waveformBars.clientHeight; |
|
|
|
for (let i = 0; i < 200; i++) { |
|
const bar = document.createElement('div'); |
|
bar.className = 'waveform-bar'; |
|
bar.style.left = `${(i / 200) * 100}%`; |
|
bar.style.height = `${Math.random() * 80 + 20}%`; |
|
waveformBars.appendChild(bar); |
|
} |
|
} |
|
|
|
|
|
function updateTimeDisplay() { |
|
const currentTime = video.currentTime || 0; |
|
const duration = video.duration || 0; |
|
|
|
currentTimeDisplay.textContent = formatTime(currentTime); |
|
durationDisplay.textContent = formatTime(duration); |
|
|
|
|
|
if (duration > 0) { |
|
const progressPercent = (currentTime / duration) * 100; |
|
progressIndicator.style.left = `${progressPercent}%`; |
|
} |
|
} |
|
|
|
|
|
function formatTime(seconds) { |
|
const date = new Date(0); |
|
date.setSeconds(seconds); |
|
return date.toISOString().substr(11, 8); |
|
} |
|
|
|
|
|
async function startRecording() { |
|
if (!video.src) { |
|
alert('No video stream to record'); |
|
return; |
|
} |
|
|
|
try { |
|
|
|
|
|
recordedChunks = []; |
|
isRecording = true; |
|
recordBtn.innerHTML = '<i class="fas fa-stop mr-2"></i> Stop'; |
|
recordBtn.classList.remove('bg-red-600'); |
|
recordBtn.classList.add('bg-gray-600'); |
|
recordingIndicator.classList.remove('hidden'); |
|
|
|
|
|
console.log('Recording started with WebCodecs (simulated)'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) { |
|
console.error('Recording error:', error); |
|
stopRecording(); |
|
} |
|
} |
|
|
|
|
|
function stopRecording() { |
|
isRecording = false; |
|
recordBtn.innerHTML = '<i class="fas fa-circle mr-2"></i> Record'; |
|
recordBtn.classList.add('bg-red-600'); |
|
recordBtn.classList.remove('bg-gray-600'); |
|
recordingIndicator.classList.add('hidden'); |
|
|
|
|
|
console.log('Recording stopped'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
addRecordingToUI(); |
|
} |
|
|
|
|
|
function addRecordingToUI() { |
|
if (recordingsList.querySelector('.text-center')) { |
|
recordingsList.innerHTML = ''; |
|
} |
|
|
|
const recordingItem = document.createElement('div'); |
|
recordingItem.className = 'flex items-center justify-between p-2 bg-gray-700 rounded-lg'; |
|
recordingItem.innerHTML = ` |
|
<div class="flex items-center"> |
|
<i class="fas fa-file-video text-blue-400 mr-3"></i> |
|
<div> |
|
<p class="font-medium">Recording_${new Date().toLocaleTimeString()}</p> |
|
<p class="text-xs text-gray-400">00:30:45 - MP4</p> |
|
</div> |
|
</div> |
|
<div class="flex space-x-2"> |
|
<button class="p-1 text-blue-400 hover:text-blue-300"> |
|
<i class="fas fa-play"></i> |
|
</button> |
|
<button class="p-1 text-green-400 hover:text-green-300"> |
|
<i class="fas fa-download"></i> |
|
</button> |
|
<button class="p-1 text-red-400 hover:text-red-300"> |
|
<i class="fas fa-trash"></i> |
|
</button> |
|
</div> |
|
`; |
|
|
|
recordingsList.prepend(recordingItem); |
|
} |
|
|
|
|
|
function setClipStart() { |
|
startTime = video.currentTime; |
|
updateClipRangeUI(); |
|
} |
|
|
|
|
|
function setClipEnd() { |
|
endTime = video.currentTime; |
|
updateClipRangeUI(); |
|
} |
|
|
|
|
|
function updateClipRangeUI() { |
|
if (video.duration) { |
|
const startPercent = (startTime / video.duration) * 100; |
|
const endPercent = (endTime / video.duration) * 100; |
|
|
|
if (startTime > 0 && endTime > 0 && startTime < endTime) { |
|
timelineRange.style.left = `${startPercent}%`; |
|
timelineRange.style.width = `${endPercent - startPercent}%`; |
|
timelineRange.classList.remove('hidden'); |
|
|
|
startHandle.style.left = `${startPercent}%`; |
|
startHandle.classList.remove('hidden'); |
|
|
|
endHandle.style.left = `${endPercent}%`; |
|
endHandle.classList.remove('hidden'); |
|
} |
|
} |
|
} |
|
|
|
|
|
function exportClip() { |
|
if (startTime >= endTime || startTime < 0 || endTime < 0) { |
|
alert('Please set a valid start and end time'); |
|
return; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
alert(`Clip exported from ${formatTime(startTime)} to ${formatTime(endTime)}`); |
|
console.log('Exporting clip with WebCodecs (simulated)'); |
|
} |
|
|
|
|
|
function upscaleVideo() { |
|
if (!video.src) { |
|
alert('No video loaded'); |
|
return; |
|
} |
|
|
|
|
|
upscaleOverlay.classList.remove('hidden'); |
|
|
|
|
|
|
|
|
|
|
|
let progress = 0; |
|
const interval = setInterval(() => { |
|
progress += 5; |
|
upscaleProgress.style.width = `${progress}%`; |
|
|
|
if (progress >= 100) { |
|
clearInterval(interval); |
|
setTimeout(() => { |
|
upscaleOverlay.classList.add('hidden'); |
|
showUpscalePreview(); |
|
}, 500); |
|
} |
|
}, 200); |
|
} |
|
|
|
|
|
function showUpscalePreview() { |
|
|
|
originalPreview.src = 'https://via.placeholder.com/200x112/4b5563/ffffff?text=Original'; |
|
upscaledPreview.src = 'https://via.placeholder.com/400x224/7e22ce/ffffff?text=Upscaled+4x'; |
|
upscalePreviewContainer.classList.remove('hidden'); |
|
} |
|
|
|
|
|
loadStreamBtn.addEventListener('click', () => { |
|
const url = streamUrlInput.value.trim(); |
|
if (url) { |
|
initHlsPlayer(url); |
|
} |
|
}); |
|
|
|
qualitySelect.addEventListener('change', () => { |
|
if (hls) { |
|
const level = qualitySelect.value; |
|
if (level === 'auto') { |
|
hls.currentLevel = -1; |
|
} else { |
|
const levels = { high: 0, medium: 1, low: 2 }; |
|
hls.currentLevel = levels[level]; |
|
} |
|
} |
|
}); |
|
|
|
recordBtn.addEventListener('click', () => { |
|
if (isRecording) { |
|
stopRecording(); |
|
} else { |
|
startRecording(); |
|
} |
|
}); |
|
|
|
video.addEventListener('timeupdate', updateTimeDisplay); |
|
|
|
setStartBtn.addEventListener('click', setClipStart); |
|
setEndBtn.addEventListener('click', setClipEnd); |
|
exportClipBtn.addEventListener('click', exportClip); |
|
|
|
upscaleBtn.addEventListener('click', upscaleVideo); |
|
|
|
|
|
waveformBars.addEventListener('mousedown', (e) => { |
|
if (!video.duration) return; |
|
|
|
const rect = waveformBars.getBoundingClientRect(); |
|
const percent = (e.clientX - rect.left) / rect.width; |
|
const time = percent * video.duration; |
|
|
|
if (startHandle.classList.contains('hidden') === false && |
|
Math.abs(percent - parseFloat(startHandle.style.left) / 100) < 0.02) { |
|
isDraggingStart = true; |
|
} else if (endHandle.classList.contains('hidden') === false && |
|
Math.abs(percent - parseFloat(endHandle.style.left) / 100) < 0.02) { |
|
isDraggingEnd = true; |
|
} else { |
|
isDraggingProgress = true; |
|
video.currentTime = time; |
|
} |
|
}); |
|
|
|
document.addEventListener('mousemove', (e) => { |
|
if (!video.duration) return; |
|
|
|
const rect = waveformBars.getBoundingClientRect(); |
|
const percent = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width)); |
|
const time = percent * video.duration; |
|
|
|
if (isDraggingStart) { |
|
startTime = time; |
|
if (startTime > endTime && endTime > 0) { |
|
startTime = endTime - 0.1; |
|
} |
|
updateClipRangeUI(); |
|
} else if (isDraggingEnd) { |
|
endTime = time; |
|
if (endTime < startTime) { |
|
endTime = startTime + 0.1; |
|
} |
|
updateClipRangeUI(); |
|
} else if (isDraggingProgress) { |
|
video.currentTime = time; |
|
} |
|
}); |
|
|
|
document.addEventListener('mouseup', () => { |
|
isDraggingStart = false; |
|
isDraggingEnd = false; |
|
isDraggingProgress = false; |
|
}); |
|
|
|
|
|
initHlsPlayer(streamUrlInput.value); |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=inoculatemedia/hls-player-and-recorder" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |