PageSourceSearch

https://videohosting38.ru/converter/

html videohosting38.ru collected 2026-10-06 07:05:55 UTC 26,544 bytes, 694 lines download raw bytes

1<!DOCTYPE html>
2<html lang="ru">
3<head>
4<meta charset="UTF-8">
5<meta name="viewport" content="width=device-width, initial-scale=1.0">
6<title>Видеоконвертер</title>
7<style>
8* { margin: 0; padding: 0; box-sizing: border-box; }
9body {
10    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12    min-height: 100vh;
13    padding: 20px;
14}
15.container { max-width: 800px; margin: 0 auto; }
16.card {
17    background: white;
18    border-radius: 20px;
19    padding: 40px;
20    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
21    margin-bottom: 20px;
22}
23h1 {
24    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
25    -webkit-background-clip: text;
26    -webkit-text-fill-color: transparent;
27    font-size: 32px;
28    margin-bottom: 10px;
29}
30.subtitle { color: #666; margin-bottom: 30px; }
31
32/* Drop-zone */
33.drop-zone {
34    border: 3px dashed #cbd5e0;
35    border-radius: 16px;
36    padding: 60px 30px;
37    text-align: center;
38    cursor: pointer;
39    transition: all 0.3s;
40    background: #f7fafc;
41}
42.drop-zone:hover, .drop-zone.dragover {
43    border-color: #667eea;
44    background: #edf2ff;
45}
46.drop-zone .icon { font-size: 64px; margin-bottom: 15px; }
47.drop-zone .hint { color: #718096; font-size: 14px; margin-top: 8px; }
48
49/* Информация о файле */
50.file-info {
51    display: none;
52    background: #f0f4ff;
53    border-radius: 12px;
54    padding: 20px;
55    margin-top: 20px;
56}
57.file-info.show { display: block; }
58.file-info h3 { color: #4c51bf; margin-bottom: 15px; font-size: 16px; }
59.info-grid {
60    display: grid;
61    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
62    gap: 12px;
63}
64.info-item {
65    background: white;
66    border-radius: 8px;
67    padding: 10px 14px;
68}
69.info-item .label { font-size: 11px; color: #718096; text-transform: uppercase; }
70.info-item .value { font-size: 15px; font-weight: 600; color: #2d3748; margin-top: 4px; }
71
72/* Секции */
73.section { margin-top: 25px; }
74.section h3 { font-size: 16px; color: #2d3748; margin-bottom: 12px; }
75.section .desc { font-size: 13px; color: #718096; margin-bottom: 12px; }
76
77/* Качества */
78.quality-grid {
79    display: grid;
80    grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
81    gap: 10px;
82}
83.quality-option {
84    padding: 15px;
85    border: 2px solid #e2e8f0;
86    border-radius: 12px;
87    cursor: pointer;
88    text-align: center;
89    transition: all 0.2s;
90    background: white;
91}
92.quality-option:hover:not(.disabled) {
93    border-color: #667eea;
94    background: #f0f4ff;
95}
96.quality-option.selected {
97    border-color: #667eea;
98    background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
99}
100.quality-option.disabled {
101    opacity: 0.4;
102    cursor: not-allowed;
103    background: #f7fafc;
104}
105.quality-option .qname { font-weight: 600; font-size: 16px; color: #2d3748; }
106.quality-option .qres  { font-size: 11px; color: #718096; margin-top: 4px; }
107.quality-option .qbit  { font-size: 10px; color: #a0aec0; margin-top: 2px; }
108
109/* Форматы */
110.format-list { display: flex; flex-direction: column; gap: 8px; }
111.format-option {
112    display: flex;
113    align-items: center;
114    padding: 12px 16px;
115    border: 2px solid #e2e8f0;
116    border-radius: 10px;
117    cursor: pointer;
118    transition: all 0.2s;
119}
120.format-option:hover { border-color: #667eea; background: #f0f4ff; }
121.format-option.selected {
122    border-color: #667eea;
123    background: linear-gradient(135deg, rgba(102, 126, 234, 0.15) 0%, rgba(118, 75, 162, 0.15) 100%);
124}
125.format-option input { margin-right: 12px; }
126
127/* Битрate */
128.bitrate-slider {
129    width: 100%;
130    margin-top: 8px;
131}
132.bitrate-values {
133    display: flex;
134    justify-content: space-between;
135    font-size: 11px;
136    color: #718096;
137    margin-top: 4px;
138}
139.bitrate-current {
140    text-align: center;
141    font-weight: 600;
142    color: #667eea;
143    font-size: 15px;
144    margin-top: 8px;
145}
146
147/* Чекбоксы */
148.checkbox-row {
149    display: flex;
150    align-items: center;
151    padding: 10px 0;
152    cursor: pointer;
153}
154.checkbox-row input { margin-right: 10px; }
155
156/* Кнопка */
157.btn {
158    width: 100%;
159    padding: 16px;
160    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
161    color: white;
162    border: none;
163    border-radius: 12px;
164    font-size: 16px;
165    font-weight: 600;
166    cursor: pointer;
167    transition: all 0.2s;
168    margin-top: 25px;
169}
170.btn:hover:not(:disabled) {
171    transform: translateY(-2px);
172    box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
173}
174.btn:disabled { opacity: 0.5; cursor: not-allowed; }
175
176/* Прогресс */
177.progress-section { display: none; margin-top: 25px; }
178.progress-section.show { display: block; }
179.progress-bar-wrap {
180    height: 12px;
181    background: #e2e8f0;
182    border-radius: 6px;
183    overflow: hidden;
184    margin: 10px 0;
185}
186.progress-bar-fill {
187    height: 100%;
188    background: linear-gradient(90deg, #667eea, #764ba2);
189    width: 0%;
190    transition: width 0.3s;
191    border-radius: 6px;
192}
193.progress-status {
194    font-size: 13px;
195    color: #4a5568;
196    margin-top: 8px;
197}
198.progress-percent {
199    font-size: 20px;
200    font-weight: 700;
201    color: #667eea;
202    text-align: center;
203    margin-bottom: 8px;
204}
205
206/* Лог */
207.log-box {
208    background: #1a202c;
209    color: #68d391;
210    font-family: 'Courier New', monospace;
211    font-size: 11px;
212    padding: 15px;
213    border-radius: 10px;
214    max-height: 200px;
215    overflow-y: auto;
216    margin-top: 15px;
217    white-space: pre-wrap;
218    word-wrap: break-word;
219}
220
221/* Результат */
222.result-section { display: none; text-align: center; padding: 20px; }
223.result-section.show { display: block; }
224.result-section h2 { color: #38a169; margin-bottom: 15px; }
225.download-btn {
226    display: inline-block;
227    padding: 15px 40px;
228    background: #38a169;
229    color: white;
230    text-decoration: none;
231    border-radius: 12px;
232    font-weight: 600;
233    margin-top: 15px;
234    transition: all 0.2s;
235}
236.download-btn:hover {
237    background: #2f855a;
238    transform: translateY(-2px);
239    box-shadow: 0 10px 25px rgba(56, 161, 105, 0.4);
240}
241
242/* Ошибка */
243.error-box {
244    display: none;
245    background: #fed7d7;
246    color: #c53030;
247    padding: 15px;
248    border-radius: 10px;
249    margin-top: 15px;
250}
251.error-box.show { display: block; }
252
253@media (max-width: 600px) {
254    .card { padding: 20px; }
255    h1 { font-size: 24px; }
256    .quality-grid { grid-template-columns: repeat(2, 1fr); }
257}
258</style>
259</head>
260<body>
261<div class="container">
262    <div class="card">
263        <h1>🎬 Видеоконвертер</h1>
264        <p class="subtitle">Загрузите видео, выберите качество и формат — получите готовый файл</p>
265
266        <div class="drop-zone" id="dropZone">
267            <div class="icon">📁</div>
268            <div><strong>Перетащите видео сюда</strong> или нажмите для выбора</div>
269            <div class="hint">MP4, AVI, MOV, MKV, WEBM, FLV, WMV, 3GP — до 2 ГБ</div>
270        </div>
271        <input type="file" id="fileInput" accept="video/*,audio/*" style="display:none">
272
273        <!-- Информация о файле -->
274        <div class="file-info" id="fileInfo">
275            <h3>📊 Информация о видео</h3>
276            <div class="info-grid" id="infoGrid"></div>
277        </div>
278
279        <!-- Качества -->
280        <div class="section" id="qualitySection" style="display:none;">
281            <h3>🎯 Выберите качество</h3>
282            <p class="desc">Недоступные качества выше исходного заблокированы</p>
283            <div class="quality-grid" id="qualityGrid"></div>
284        </div>
285
286        <!-- Формат -->
287        <div class="section" id="formatSection" style="display:none;">
288            <h3>📦 Формат вывода</h3>
289            <div class="format-list" id="formatList">
290                                    <label class="format-option" data-format="mp4">
291                        <input type="radio" name="format" value="mp4" checked>
292                        <span>MP4 (H.264 + AAC) — универсальный</span>
293                    </label>
294                                    <label class="format-option" data-format="webm">
295                        <input type="radio" name="format" value="webm" >
296                        <span>WEBM (VP9 + Opus) — для веба</span>
297                    </label>
298                                    <label class="format-option" data-format="mkv">
299                        <input type="radio" name="format" value="mkv" >
300                        <span>MKV (H.265 + AAC) — для архива</span>
301                    </label>
302                                    <label class="format-option" data-format="mp3">
303                        <input type="radio" name="format" value="mp3" >
304                        <span>MP3 — только звук</span>
305                    </label>
306                                    <label class="format-option" data-format="gif">
307                        <input type="radio" name="format" value="gif" >
308                        <span>GIF — анимация</span>
309                    </label>
310                            </div>
311        </div>
312
313        <!-- Битрейт -->
314        <div class="section" id="bitrateSection" style="display:none;">
315            <h3>⚙️ Качество кодирования</h3>
316            <p class="desc">Снижение битрейта уменьшит размер файла, но и качество</p>
317            <input type="range" class="bitrate-slider" id="bitrateSlider" min="30" max="100" value="100" step="5">
318            <div class="bitrate-values">
319                <span>30% (меньше размер)</span>
320                <span>100% (лучшее качество)</span>
321            </div>
322            <div class="bitrate-current" id="bitrateCurrent">100% от стандартного битрейта</div>
323        </div>
324
325        <!-- Доп. опции -->
326        <div class="section" id="optionsSection" style="display:none;">
327            <h3>🔧 Дополнительно</h3>
328            <label class="checkbox-row">
329                <input type="checkbox" id="noAudio"> Убрать звук
330            </label>
331        </div>
332
333        <!-- Кнопка -->
334        <button class="btn" id="convertBtn" disabled>🎬 Конвертировать</button>
335
336        <!-- Ошибка -->
337        <div class="error-box" id="errorBox"></div>
338
339        <!-- Прогресс -->
340        <div class="progress-section" id="progressSection">
341            <div class="progress-percent" id="progressPercent">0%</div>
342            <div class="progress-bar-wrap">
343                <div class="progress-bar-fill" id="progressFill"></div>
344            </div>
345            <div class="progress-status" id="progressStatus">Ожидание...</div>
346            <div class="log-box" id="logBox" style="display:none;"></div>
347        </div>
348
349        <!-- Результат -->
350        <div class="result-section" id="resultSection">
351            <h2>✅ Готово!</h2>
352            <div id="resultInfo"></div>
353            <a href="#" class="download-btn" id="downloadBtn">⬇️ Скачать файл</a>
354        </div>
355    </div>
356</div>
357
358<script>
359(function() {
360    'use strict';
361
362    // ============= DOM =============
363    const dropZone        = document.getElementById('dropZone');
364    const fileInput       = document.getElementById('fileInput');
365    const fileInfo        = document.getElementById('fileInfo');
366    const infoGrid        = document.getElementById('infoGrid');
367    const qualitySection  = document.getElementById('qualitySection');
368    const qualityGrid     = document.getElementById('qualityGrid');
369    const formatSection   = document.getElementById('formatSection');
370    const formatList      = document.getElementById('formatList');
371    const bitrateSection  = document.getElementById('bitrateSection');
372    const bitrateSlider   = document.getElementById('bitrateSlider');
373    const bitrateCurrent  = document.getElementById('bitrateCurrent');
374    const optionsSection  = document.getElementById('optionsSection');
375    const noAudio         = document.getElementById('noAudio');
376    const convertBtn      = document.getElementById('convertBtn');
377    const errorBox        = document.getElementById('errorBox');
378    const progressSection = document.getElementById('progressSection');
379    const progressPercent = document.getElementById('progressPercent');
380    const progressFill    = document.getElementById('progressFill');
381    const progressStatus  = document.getElementById('progressStatus');
382    const logBox          = document.getElementById('logBox');
383    const resultSection   = document.getElementById('resultSection');
384    const resultInfo      = document.getElementById('resultInfo');
385    const downloadBtn     = document.getElementById('downloadBtn');
386
387    // ============= Состояние =============
388    let selectedFile = null;
389    let videoInfo = null;
390    let selectedQuality = null;
391    let jobId = null;
392    let eventSource = null;
393
394    // ============= Качества =============
395    const QUALITIES = {
396        '240p':  { height: 240,  bitrate: 400,  audio: 96  },
397        '360p':  { height: 360,  bitrate: 700,  audio: 128 },
398        '480p':  { height: 480,  bitrate: 1200, audio: 128 },
399        '720p':  { height: 720,  bitrate: 2500, audio: 128 },
400        '1080p': { height: 1080, bitrate: 5000, audio: 192 },
401    };
402
403    // ============= Drag & drop =============
404    dropZone.addEventListener('click', () => fileInput.click());
405    dropZone.addEventListener('dragover', (e) => {
406        e.preventDefault();
407        dropZone.classList.add('dragover');
408    });
409    dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
410    dropZone.addEventListener('drop', (e) => {
411        e.preventDefault();
412        dropZone.classList.remove('dragover');
413        if (e.dataTransfer.files.length > 0) handleFile(e.dataTransfer.files[0]);
414    });
415    fileInput.addEventListener('change', (e) => {
416        if (e.target.files.length > 0) handleFile(e.target.files[0]);
417    });
418
419    // ============= Обработка файла =============
420    async function handleFile(file) {
421        selectedFile = file;
422
423        // Проверки
424        const allowedExts = ['mp4','avi','mov','mkv','webm','flv','wmv','m4v','mpg','mpeg','3gp'];
425        const ext = file.name.split('.').pop().toLowerCase();
426        if (!allowedExts.includes(ext)) {
427            showError('Неподдерживаемый формат: .' + ext);
428            return;
429        }
430        if (file.size > 2 * 1024 * 1024 * 1024) {
431            showError('Файл слишком большой (макс. 2 ГБ)');
432            return;
433        }
434
435        hideError();
436
437        // Показываем в drop-zone
438        dropZone.querySelector('.icon').textContent = '🎥';
439        dropZone.querySelector('strong').textContent = file.name;
440        dropZone.querySelector('.hint').textContent = formatBytes(file.size) + ' — загрузка и анализ...';
441
442        // Отправляем на сервер для анализа
443        const fd = new FormData();
444        fd.append('video_file', file);
445        fd.append('action', 'probe');
446
447        try {
448            const resp = await fetch('convert.php', { method: 'POST', body: fd });
449            const data = await resp.json();
450
451            if (!data.success) {
452                showError(data.error || 'Ошибка анализа');
453                return;
454            }
455
456            videoInfo = data.info;
457            jobId = data.job_id;
458            renderFileInfo();
459            renderQualityOptions();
460            renderFormatOptions();
461
462            qualitySection.style.display = 'block';
463            formatSection.style.display = 'block';
464            bitrateSection.style.display = 'block';
465            optionsSection.style.display = 'block';
466            convertBtn.disabled = false;
467
468            dropZone.querySelector('.hint').textContent = 'Готово — выберите параметры ниже';
469
470        } catch (err) {
471            showError('Ошибка соединения: ' + err.message);
472        }
473    }
474
475    // ============= Рендер информации =============
476    function renderFileInfo() {
477        const v = videoInfo;
478        const items = [
479            { label: 'Разрешение', value: v.video ? `${v.video.width}×${v.video.height}` : '—' },
480            { label: 'Длительность', value: formatDuration(v.duration) },
481            { label: 'Размер', value: formatBytes(v.size) },
482            { label: 'Видеокодек', value: v.video ? v.video.codec.toUpperCase() : '—' },
483            { label: 'FPS', value: v.video ? v.video.fps : '—' },
484            { label: 'Аудио', value: v.audio ? `${v.audio.codec.toUpperCase()} ${v.audio.channels}ch` : 'Нет' },
485            { label: 'Битрейт', value: v.bitrate ? Math.round(v.bitrate / 1000) + ' kbps' : '—' },
486            { label: 'Поворот', value: v.video && v.video.rotation ? v.video.rotation + '°' : 'Нет' },
487        ];
488        infoGrid.innerHTML = items.map(i =>
489            `<div class="info-item"><div class="label">${i.label}</div><div class="value">${i.value}</div></div>`
490        ).join('');
491        fileInfo.classList.add('show');
492    }
493
494    // ============= Рендер качеств =============
495    function renderQualityOptions() {
496        const srcHeight = videoInfo.video ? videoInfo.video.height : 0;
497        const srcWidth  = videoInfo.video ? videoInfo.video.width : 0;
498        const isPortrait = srcHeight > srcWidth;
499        const srcMinDim = isPortrait ? srcWidth : srcHeight;
500
501        qualityGrid.innerHTML = '';
502
503        // Плюс "Оригинал"
504        const origHtml = `
505            <div class="quality-option selected" data-quality="original">
506                <div class="qname">Оригинал</div>
507                <div class="qres">${srcWidth}×${srcHeight}</div>
508                <div class="qbit">без изменений</div>
509            </div>`;
510        qualityGrid.insertAdjacentHTML('beforeend', origHtml);
511        selectedQuality = 'original';
512
513        Object.entries(QUALITIES).forEach(([key, q]) => {
514            // Проверяем: доступно ли это качество?
515            const available = isPortrait
516                ? q.height <= srcWidth   // для вертикальных сравниваем с шириной
517                : q.height <= srcHeight;
518
519            const cls = available ? 'quality-option' : 'quality-option disabled';
520            const dims = isPortrait
521                ? `${Math.round(q.height * (srcWidth / srcHeight))}×${q.height}`
522                : `${Math.round(q.height * (srcWidth / srcHeight))}×${q.height}`;
523
524            const html = `
525                <div class="${cls}" data-quality="${key}">
526                    <div class="qname">${key}</div>
527                    <div class="qres">${dims}</div>
528                    <div class="qbit">${q.bitrate} kbps</div>
529                </div>`;
530            qualityGrid.insertAdjacentHTML('beforeend', html);
531        });
532
533        // Клик по качествам
534        qualityGrid.querySelectorAll('.quality-option:not(.disabled)').forEach(el => {
535            el.addEventListener('click', () => {
536                qualityGrid.querySelectorAll('.quality-option').forEach(o => o.classList.remove('selected'));
537                el.classList.add('selected');
538                selectedQuality = el.dataset.quality;
539            });
540        });
541    }
542
543    // ============= Рендер форматов =============
544    function renderFormatOptions() {
545        formatList.querySelectorAll('.format-option').forEach(el => {
546            el.addEventListener('click', () => {
547                formatList.querySelectorAll('.format-option').forEach(o => o.classList.remove('selected'));
548                el.classList.add('selected');
549                el.querySelector('input').checked = true;
550            });
551        });
552        formatList.querySelector('.format-option').classList.add('selected');
553    }
554
555    // ============= Битрейт =============
556    bitrateSlider.addEventListener('input', () => {
557        const val = bitrateSlider.value;
558        bitrateCurrent.textContent = val + '% от стандартного битрейта';
559    });
560
561    // ============= Кнопка "Конвертировать" =============
562    convertBtn.addEventListener('click', async () => {
563        if (!selectedFile || !videoInfo) return;
564
565        const format = document.querySelector('input[name="format"]:checked').value;
566        const bitrateScale = parseInt(bitrateSlider.value) / 100;
567        const removeAudio = noAudio.checked;
568
569        convertBtn.disabled = true;
570        convertBtn.textContent = '⏳ Отправка...';
571        progressSection.classList.add('show');
572        resultSection.classList.remove('show');
573        logBox.style.display = 'block';
574        logBox.textContent = 'Отправка задания...';
575
576        // Отправляем запрос на запуск конвертации
577        const fd = new FormData();
578        fd.append('action', 'convert');
579        fd.append('job_id', jobId);
580        fd.append('quality', selectedQuality);
581        fd.append('format', format);
582        fd.append('bitrate_scale', bitrateScale);
583        fd.append('remove_audio', removeAudio ? '1' : '0');
584
585        try {
586            const resp = await fetch('convert.php', { method: 'POST', body: fd });
587            const data = await resp.json();
588
589            if (!data.success) {
590                showError(data.error || 'Ошибка запуска');
591                convertBtn.disabled = false;
592                convertBtn.textContent = '🎬 Конвертировать';
593                return;
594            }
595
596            // Запускаем SSE
597            startSSE(jobId);
598
599        } catch (err) {
600            showError('Ошибка: ' + err.message);
601            convertBtn.disabled = false;
602            convertBtn.textContent = '🎬 Конвертировать';
603        }
604    });
605
606    // ============= SSE =============
607    function startSSE(id) {
608        if (eventSource) eventSource.close();
609
610        progressStatus.textContent = 'Подключение...';
611        progressFill.style.width = '0%';
612        progressPercent.textContent = '0%';
613
614        eventSource = new EventSource('progress.php?id=' + id);
615
616        eventSource.addEventListener('progress', (e) => {
617            try {
618                const data = JSON.parse(e.data);
619                
620                if (typeof data.progress === 'number') {
621                    const p = Math.max(0, Math.min(100, data.progress));
622                    progressFill.style.width = p + '%';
623                    progressPercent.textContent = p + '%';
624                }
625                if (data.status) progressStatus.textContent = data.status;
626                if (data.log) logBox.textContent = data.log;
627            } catch (err) { console.error(err); }
628        });
629
630        eventSource.addEventListener('complete', (e) => {
631            try {
632                const data = JSON.parse(e.data);
633                progressFill.style.width = '100%';
634                progressPercent.textContent = '100%';
635                progressStatus.textContent = '✅ Готово!';
636
637                resultInfo.innerHTML = `
638                    <p><strong>Файл:</strong> ${data.filename}</p>
639                    <p><strong>Размер:</strong> ${data.filesize}</p>
640                    <p><strong>Время:</strong> ${data.elapsed} сек</p>
641                `;
642                downloadBtn.href = 'download.php?id=' + id;
643                resultSection.classList.add('show');
644                convertBtn.textContent = '🎬 Конвертировать ещё';
645
646                eventSource.close();
647                eventSource = null;
648            } catch (err) { console.error(err); }
649        });
650
651        eventSource.addEventListener('error', (e) => {
652            if (e.data) {
653                try {
654                    const data = JSON.parse(e.data);
655                    showError('Ошибка: ' + (data.error || 'неизвестная'));
656                    progressStatus.textContent = '❌ Ошибка';
657                    convertBtn.disabled = false;
658                    convertBtn.textContent = '🎬 Попробовать снова';
659                } catch (err) {}
660            }
661            if (eventSource && eventSource.readyState === EventSource.CLOSED) {
662                eventSource.close();
663                eventSource = null;
664            }
665        });
666    }
667
668    // ============= Утилиты =============
669    function showError(msg) {
670        errorBox.textContent = '❌ ' + msg;
671        errorBox.classList.add('show');
672        setTimeout(() => errorBox.classList.remove('show'), 8000);
673    }
674    function hideError() { errorBox.classList.remove('show'); }
675
676    function formatBytes(bytes) {
677        if (bytes < 1024) return bytes + ' B';
678        if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
679        if (bytes < 1024 * 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB';
680        return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
681    }
682
683    function formatDuration(seconds) {
684        const h = Math.floor(seconds / 3600);
685        const m = Math.floor((seconds % 3600) / 60);
686        const s = Math.floor(seconds % 60);
687        return h > 0
688            ? `${h}:${String(m).padStart(2, '0')}
688:${String(s).padStart(2, '0')}`
689            : `${m}:${String(s).padStart(2, '0')}`;
690    }
691})();
692</script>
692
693</body>
694</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.