inoculatemedia's picture
Update index.html
6111745 verified
<!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 direct fetch first if it's same origin
try {
const response = await fetch(url);
if (response.ok) {
return response;
}
} catch (error) {
console.warn('Direct fetch failed:', error);
}
// Then try proxies
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;
}
}
// If all proxies fail, try JSONP approach with allorigins
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) {
// Create a new response with the 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;
// Get the manifest content element and its URL
const manifestContent = event.target.closest('.line-numbers-content');
const manifestType = manifestContent.dataset.manifestType;
const manifestUrl = manifestContent.closest('.tab').querySelector('.manifest-url').textContent.trim();
// Make sure manifestUrl ends with a slash if it's a directory
const baseUrl = manifestUrl.endsWith('/') ? manifestUrl :
manifestUrl.substring(0, manifestUrl.lastIndexOf('/') + 1);
// Show duration tooltip for any manifest type
const result = calculateSelectedDuration(selectedText);
if (result.duration > 0) {
// Create a tooltip if it doesn't exist
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';
}
// Only proceed with player creation if enabled and for Media Manifests
if (!ENABLE_PREVIEW_PLAYER || manifestType !== "Media Manifest") {
return;
}
const fullContent = manifestContent.textContent;
const lines = fullContent.split('\n');
// Find initialization segment
const initSegmentLine = lines.find(line => line.includes('#EXT-X-MAP'));
const initSegment = initSegmentLine ?
resolveUrl(initSegmentLine.match(/URI="([^"]+)"/)[1], manifestUrl) : null;
// Parse selected segments
const selectedLines = selectedText.split('\n');
// Find the most recent init segment from the full content up to the selection
const fullLines = fullContent.split('\n');
let lastInitSegment = initSegment; // Default from earlier search
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) {
// Get the last init segment before our selection
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('#')) {
// Get the context around this segment
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 // Add the init segment that applies to this segment
});
currentDuration = null;
}
});
if (segments.length > 0) {
createAndPlayTempPlaylist(segments);
}
}
// Add this helper function to resolve URLs
function resolveUrl(url, baseUrl) {
if (!url) return url;
if (url.startsWith('http://') || url.startsWith('https://')) {
return url; // Already absolute
}
try {
// Handle paths that start with 'hls/' or other relative paths
if (url.startsWith('/')) {
// For root-relative URLs
const baseUrlObj = new URL(baseUrl);
return `${baseUrlObj.origin}${url}`;
} else {
// For relative URLs, remove the filename from baseUrl if it exists
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 {
// Fetch master manifest
const masterResponse = await fetchWithCorsProxy(url);
const masterManifestContent = await masterResponse.text();
const masterOrigin = new URL(url).origin;
// Parse video variants
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);
}
// Parse audio streams
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 });
}
// Parse subtitles
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 });
}
// Fetch all manifests
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
};
}
}));
// Save to history
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();
}
}
// Display the manifests
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');
// Check if click is outside the manifest content and player
if (!event.target.closest('.line-numbers-content') &&
!event.target.closest('#floating-player')) {
// Clear selection
selection.removeAllRanges();
// Hide tooltip
if (tooltip) {
tooltip.style.display = 'none';
}
// Stop and hide player
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()) {
// Hide tooltip
if (tooltip) {
tooltip.style.display = 'none';
}
// Stop and hide player
if (playerContainer && playerContainer.style.display !== 'none') {
// Handle play/pause more gracefully
const cleanup = () => {
if (hls) {
hls.destroy();
hls = null;
}
playerContainer.style.display = 'none';
};
// Check if video is actually playing before trying to pause
if (!video.paused) {
video.pause()
.then(cleanup)
.catch(() => {
// If pause fails, still cleanup
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 preview player is disabled, clean up any existing player
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");
});
// Make sure first tab is active by default
$("ul li a:first").addClass("active");
// Load saved settings
$("#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 = "";
// Create tabLink and tabContent BEFORE any conditional logic
const tabLink = $('<li></li>'); // Create empty li first
const tabContent = $(`<div id="${tabId}" class="tab"></div>`);
if (manifest.type === "Multivariant") {
manifestName = manifest.type;
// Create the anchor with the name and tooltip AFTER we know them
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}`;
}
// Create the anchor with the name and tooltip AFTER we know them
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}`;
}
// Create the anchor with the name and tooltip AFTER we know them
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}`;
}
// Create the anchor with the name and tooltip AFTER we know them
tabLink.html(`<a href="#${tabId}" ${tooltipText ? `title="${tooltipText}"` : ''}>${manifestName}</a>`);
subCount++;
} else {
manifestName = "Other M3U8";
// Create the anchor with the name and tooltip AFTER we know them
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; // For Safari
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
}
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 this segment has a different init segment than current, add it to playlist
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);
// const video = document.getElementById('video');
// let camSrc = document.querySelector("#tab-0 > div.manifest-url");
//function initPlayer() {
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">
<!-- Main Player Column -->
<div class="lg:col-span-2 space-y-6">
<!-- Video Player -->
<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>
<!-- Timeline Controls -->
<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>
<!-- Stream Controls -->
<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>
<!-- Sidebar Column -->
<div class="space-y-6">
<!-- Recording Panel -->
<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>
<!-- Upscale Panel -->
<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>
<!-- Upscale Preview -->
<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() {
// Video elements
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');
// Recording elements
const recordBtn = document.getElementById('recordBtn');
const recordingIndicator = document.getElementById('recordingIndicator');
const recordingsList = document.getElementById('recordingsList');
// Timeline elements
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');
// Upscale elements
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');
// Player state
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;
// Initialize HLS player
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')) {
// For Safari
video.src = url;
video.addEventListener('loadedmetadata', function() {
video.play();
generateWaveform();
});
}
}
// Generate fake waveform for demo
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);
}
}
// Update time display
function updateTimeDisplay() {
const currentTime = video.currentTime || 0;
const duration = video.duration || 0;
currentTimeDisplay.textContent = formatTime(currentTime);
durationDisplay.textContent = formatTime(duration);
// Update progress indicator
if (duration > 0) {
const progressPercent = (currentTime / duration) * 100;
progressIndicator.style.left = `${progressPercent}%`;
}
}
// Format time as HH:MM:SS
function formatTime(seconds) {
const date = new Date(0);
date.setSeconds(seconds);
return date.toISOString().substr(11, 8);
}
// Start recording
async function startRecording() {
if (!video.src) {
alert('No video stream to record');
return;
}
try {
// In a real implementation, we would use MediaRecorder with WebCodecs
// This is a simplified version for demonstration
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');
// Simulate recording with WebCodecs
console.log('Recording started with WebCodecs (simulated)');
// In a real implementation, we would:
// 1. Create a MediaStream from the video element
// 2. Use MediaRecorder with WebCodecs to encode to MP4
// 3. Process frames with GPU acceleration
} catch (error) {
console.error('Recording error:', error);
stopRecording();
}
}
// Stop recording
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');
// Simulate saving the recording
console.log('Recording stopped');
// In a real implementation, we would:
// 1. Finalize the MP4 file
// 2. Save to disk or offer download
// Add to recordings list for demo
addRecordingToUI();
}
// Add recording to UI (demo)
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);
}
// Set clip start time
function setClipStart() {
startTime = video.currentTime;
updateClipRangeUI();
}
// Set clip end time
function setClipEnd() {
endTime = video.currentTime;
updateClipRangeUI();
}
// Update the clip range UI
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');
}
}
}
// Export clip
function exportClip() {
if (startTime >= endTime || startTime < 0 || endTime < 0) {
alert('Please set a valid start and end time');
return;
}
// In a real implementation, we would:
// 1. Use WebCodecs to extract the clip between startTime and endTime
// 2. Encode as MP4
// 3. Offer download
alert(`Clip exported from ${formatTime(startTime)} to ${formatTime(endTime)}`);
console.log('Exporting clip with WebCodecs (simulated)');
}
// Upscale video
function upscaleVideo() {
if (!video.src) {
alert('No video loaded');
return;
}
// Show upscale overlay
upscaleOverlay.classList.remove('hidden');
// Simulate upscaling with Real-ESRGAN
// In a real implementation, this would require server-side processing
// or a WebAssembly port of Real-ESRGAN
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);
}
// Show upscale preview
function showUpscalePreview() {
// For demo purposes, we'll use placeholder images
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');
}
// Event listeners
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);
// Timeline dragging
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;
});
// Initialize with default stream
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>