add player

This commit is contained in:
2026-05-19 09:31:42 +00:00
commit c5d817dd41
5 changed files with 384 additions and 0 deletions

286
index.html Normal file
View File

@@ -0,0 +1,286 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CDNPlayerJS</title>
<script src="playerjs.js" type="text/javascript"></script>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
margin: 0;
background: #000;
color: #f5f8ff;
min-height: 100vh;
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
}
#playerjs {
width: 100vw;
height: 100vh;
background: #000;
overflow: hidden;
}
#status {
position: fixed;
left: 12px;
right: 12px;
bottom: 12px;
z-index: 9;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(9, 13, 24, 0.86);
color: #dbe5ff;
font-size: 12px;
line-height: 1.4;
pointer-events: none;
white-space: pre-wrap;
backdrop-filter: blur(3px);
}
</style>
</head>
<body>
<div id="playerjs"></div>
<div id="status">Ожидание ссылки в адресной строке (?src=... или ?enc=...).</div>
<script>
let playerInstance = null;
const DEFAULT_SECRET = 'cdnplayerjs';
const TTL_MS = 24 * 60 * 60 * 1000;
function setStatus(message) {
document.getElementById('status').textContent = message;
}
function base64UrlEncode(bytes) {
let str = '';
bytes.forEach((b) => { str += String.fromCharCode(b); });
return btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}
function base64UrlDecode(input) {
const padded = input.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - input.length % 4) % 4);
const raw = atob(padded);
return Uint8Array.from(raw, (c) => c.charCodeAt(0));
}
async function getAesKey(secret) {
const data = new TextEncoder().encode(secret.trim());
const digest = await crypto.subtle.digest('SHA-256', data);
return crypto.subtle.importKey('raw', digest, 'AES-GCM', false, ['encrypt', 'decrypt']);
}
async function encryptText(plainText, secret) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await getAesKey(secret);
const encoded = new TextEncoder().encode(plainText);
const cipher = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
const merged = new Uint8Array(iv.length + cipher.byteLength);
merged.set(iv, 0);
merged.set(new Uint8Array(cipher), iv.length);
return base64UrlEncode(merged);
}
async function decryptText(token, secret) {
const bytes = base64UrlDecode(token);
const iv = bytes.slice(0, 12);
const data = bytes.slice(12);
const key = await getAesKey(secret);
const plain = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, data);
return new TextDecoder().decode(plain);
}
function safeTitle(title, index) {
const cleaned = (title || `Канал ${index + 1}`).replace(/[\[\],]/g, ' ').trim();
return cleaned || `Канал ${index + 1}`;
}
function parsePlaylistText(text) {
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
const entries = [];
let pendingTitle = '';
for (const line of lines) {
if (line.startsWith('#EXTINF')) {
pendingTitle = line.includes(',') ? line.split(',').slice(1).join(',').trim() : '';
continue;
}
if (line.startsWith('#')) {
continue;
}
if (/^https?:\/\//i.test(line)) {
entries.push({ title: pendingTitle, url: line });
pendingTitle = '';
continue;
}
if (line.includes('|')) {
const [title, url] = line.split('|');
if (/^https?:\/\//i.test((url || '').trim())) {
entries.push({ title: title.trim(), url: url.trim() });
}
} else if (line.includes(',')) {
const [title, url] = line.split(',');
if (/^https?:\/\//i.test((url || '').trim())) {
entries.push({ title: title.trim(), url: url.trim() });
}
}
}
return entries;
}
function resolveUrl(baseUrl, value) {
try {
return new URL(value, baseUrl).toString();
} catch (_) {
return value;
}
}
function hasPlaylistContent(raw) {
return raw.includes('#EXTM3U') || raw.includes('#EXTINF') || /https?:\/\//i.test(raw);
}
function parsePlaylistFromInput(rawInput, baseUrl) {
const entries = parsePlaylistText(rawInput);
return entries
.map((entry, index) => `[${safeTitle(entry.title, index)}]${resolveUrl(baseUrl, entry.url)}`)
.join(',');
}
async function resolveSource(inputUrl) {
const trimmed = inputUrl.trim();
if (hasPlaylistContent(trimmed) && !/^https?:\/\//i.test(trimmed)) {
const fromInput = parsePlaylistFromInput(trimmed, window.location.href);
if (!fromInput) {
throw new Error('Плейлист из поля ввода пустой или неверного формата.');
}
return fromInput;
}
const isPlaylist = /\.(m3u8?|txt)(\?|#|$)/i.test(trimmed);
if (!isPlaylist || /\.m3u8(\?|#|$)/i.test(trimmed)) {
return trimmed;
}
if (!/^https?:\/\//i.test(trimmed)) {
return inputUrl;
}
const response = await fetch(trimmed);
if (!response.ok) {
throw new Error(`Не удалось загрузить плейлист (${response.status})`);
}
const text = await response.text();
const entries = parsePlaylistText(text);
if (!entries.length) {
throw new Error('Плейлист пустой или неверного формата.');
}
return entries
.map((entry, index) => `[${safeTitle(entry.title, index)}]${entry.url}`)
.join(',');
}
function createPlayer(fileSource) {
const container = document.getElementById('playerjs');
container.innerHTML = '';
playerInstance = new Playerjs({
id: 'playerjs',
file: fileSource,
autoplay: 1,
poster: ''
});
}
async function startPlayback(raw) {
if (!raw) {
setStatus('Добавьте ссылку в адресную строку: ?src=https://...');
return;
}
setStatus('Загрузка источника...');
try {
const fileSource = await resolveSource(raw.trim());
createPlayer(fileSource);
setStatus('Поток запущен.');
} catch (error) {
setStatus(`Ошибка: ${error.message}`);
}
}
async function createEncryptedUrl(source, secret) {
const payload = JSON.stringify({
stream: source,
issuedAt: Date.now(),
expiresAt: Date.now() + TTL_MS
});
const token = await encryptText(payload, secret);
const url = new URL(window.location.href);
url.search = '';
url.hash = '';
url.searchParams.set('enc', token);
return url.toString();
}
async function initFromUrl() {
const query = new URLSearchParams(window.location.search);
const source = query.get('src') || query.get('file') || query.get('stream');
const enc = query.get('enc') || new URLSearchParams(window.location.hash.replace(/^#/, '')).get('enc');
const key = query.get('k') || DEFAULT_SECRET;
if (source) {
try {
const encryptedUrl = await createEncryptedUrl(source, key);
window.history.replaceState({}, '', encryptedUrl);
setStatus('Ссылка зашифрована. Доступ к потоку активен 24 часа.');
} catch (error) {
setStatus(`Не удалось зашифровать ссылку: ${error.message}`);
}
startPlayback(source);
return;
}
if (!enc) {
setStatus('Ожидание ссылки. Пример: ?src=https://example.com/live.m3u8');
return;
}
try {
const json = await decryptText(enc, key);
const payload = JSON.parse(json);
const src = (payload && payload.stream) ? String(payload.stream).trim() : '';
const expiresAt = Number(payload.expiresAt || 0);
if (!src) {
throw new Error('В токене нет stream.');
}
if (!expiresAt || Number.isNaN(expiresAt)) {
throw new Error('В токене нет срока действия.');
}
if (Date.now() > expiresAt) {
const expiredAtText = new Date(expiresAt).toLocaleString('ru-RU');
throw new Error(`Срок действия ссылки истёк (${expiredAtText}).`);
}
const hoursLeft = Math.max(0, Math.ceil((expiresAt - Date.now()) / 3600000));
setStatus(`Ссылка активна. Осталось примерно ${hoursLeft} ч.`);
startPlayback(src);
} catch (error) {
setStatus(`Не удалось запустить поток: ${error.message}`);
}
}
initFromUrl();
</script>
</body>
</html>