| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>MP4 to TS Converter | Advanced Video Conversion</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <script src="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.11.6/dist/ffmpeg.min.js"></script> |
| <style> |
| .dropzone { |
| border: 2px dashed #3b82f6; |
| transition: all 0.3s ease; |
| } |
| .dropzone.active { |
| border-color: #10b981; |
| background-color: #f0fdf4; |
| } |
| .progress-bar { |
| transition: width 0.3s ease; |
| } |
| #videoPreview { |
| max-width: 100%; |
| max-height: 200px; |
| } |
| .settings-panel { |
| transition: all 0.3s ease; |
| max-height: 0; |
| overflow: hidden; |
| } |
| .settings-panel.open { |
| max-height: 500px; |
| } |
| .conversion-log { |
| max-height: 200px; |
| overflow-y: auto; |
| font-family: monospace; |
| background-color: #1e293b; |
| color: #f8fafc; |
| padding: 1rem; |
| border-radius: 0.5rem; |
| font-size: 0.875rem; |
| } |
| .log-entry { |
| margin-bottom: 0.25rem; |
| } |
| .log-info { |
| color: #60a5fa; |
| } |
| .log-warning { |
| color: #fbbf24; |
| } |
| .log-error { |
| color: #f87171; |
| } |
| .log-success { |
| color: #4ade80; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="text-center mb-12"> |
| <h1 class="text-4xl font-bold text-blue-600 mb-2">Advanced MP4 to TS Converter</h1> |
| <p class="text-gray-600 max-w-2xl mx-auto">Convert MP4 videos to TS format with full control over conversion parameters</p> |
| </header> |
|
|
| <main class="max-w-4xl mx-auto bg-white rounded-xl shadow-md overflow-hidden p-6"> |
| |
| <div id="dropzone" class="dropzone rounded-lg p-8 text-center cursor-pointer mb-6"> |
| <div class="flex flex-col items-center justify-center"> |
| <i class="fas fa-file-video text-5xl text-blue-500 mb-4"></i> |
| <h2 class="text-xl font-semibold text-gray-700 mb-2">Drag & Drop MP4 File Here</h2> |
| <p class="text-gray-500 mb-4">or click to browse your files</p> |
| <input type="file" id="fileInput" class="hidden" accept="video/mp4"> |
| <button id="browseBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition"> |
| Select File |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="fileInfo" class="hidden mb-6"> |
| <div class="flex items-center justify-between bg-gray-50 p-4 rounded-lg"> |
| <div class="flex items-center"> |
| <i class="fas fa-file-video text-2xl text-blue-500 mr-3"></i> |
| <div> |
| <h3 id="fileName" class="font-medium text-gray-800"></h3> |
| <p id="fileSize" class="text-sm text-gray-500"></p> |
| <p id="fileDuration" class="text-sm text-gray-500"></p> |
| <p id="fileResolution" class="text-sm text-gray-500"></p> |
| </div> |
| </div> |
| <button id="removeFile" class="text-red-500 hover:text-red-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="videoPreviewContainer" class="hidden mb-6"> |
| <h3 class="text-lg font-medium text-gray-700 mb-3">Video Preview</h3> |
| <div class="flex justify-center bg-gray-100 rounded-lg p-4"> |
| <video id="videoPreview" controls class="rounded"></video> |
| </div> |
| </div> |
|
|
| |
| <div class="mb-6"> |
| <div class="flex items-center justify-between cursor-pointer" id="settingsToggle"> |
| <h3 class="text-lg font-medium text-gray-700">Advanced Conversion Settings</h3> |
| <i class="fas fa-chevron-down text-gray-500 transition-transform" id="settingsIcon"></i> |
| </div> |
| <div class="settings-panel mt-3" id="settingsPanel"> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Video Codec</label> |
| <select id="videoCodec" class="w-full p-2 border border-gray-300 rounded"> |
| <option value="libx264">H.264 (libx264)</option> |
| <option value="libx265">H.265 (libx265)</option> |
| <option value="mpeg2video">MPEG-2</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Video Bitrate (kbps)</label> |
| <input type="number" id="videoBitrate" value="2000" min="500" max="20000" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Audio Codec</label> |
| <select id="audioCodec" class="w-full p-2 border border-gray-300 rounded"> |
| <option value="aac">AAC</option> |
| <option value="mp3">MP3</option> |
| <option value="ac3">AC3</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Audio Bitrate (kbps)</label> |
| <input type="number" id="audioBitrate" value="128" min="64" max="320" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Segment Duration (seconds)</label> |
| <input type="number" id="segmentDuration" value="10" min="2" max="60" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Output Format</label> |
| <select id="outputFormat" class="w-full p-2 border border-gray-300 rounded"> |
| <option value="ts">TS (Transport Stream)</option> |
| <option value="m3u8">HLS (M3U8 Playlist)</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="progressContainer" class="hidden mb-6"> |
| <div class="flex justify-between mb-1"> |
| <span class="text-sm font-medium text-gray-700">Conversion Progress</span> |
| <span id="progressPercent" class="text-sm font-medium text-gray-700">0%</span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div id="progressBar" class="progress-bar bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div> |
| </div> |
| <div id="statusMessage" class="text-sm text-gray-500 mt-2"></div> |
| |
| |
| <div id="conversionLogContainer" class="mt-4 hidden"> |
| <h4 class="text-sm font-medium text-gray-700 mb-2">Conversion Log</h4> |
| <div id="conversionLog" class="conversion-log"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="text-center"> |
| <button id="convertBtn" class="bg-green-500 hover:bg-green-600 text-white font-medium py-3 px-8 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| <i class="fas fa-cog animate-spin hidden mr-2" id="convertSpinner"></i> |
| Convert to TS |
| </button> |
| </div> |
|
|
| |
| <div id="downloadSection" class="hidden mt-8"> |
| <div class="bg-green-50 border border-green-200 rounded-lg p-4"> |
| <div class="flex items-center justify-between"> |
| <div class="flex items-center"> |
| <i class="fas fa-check-circle text-2xl text-green-500 mr-3"></i> |
| <div> |
| <h3 class="font-medium text-green-800">Conversion Complete!</h3> |
| <p class="text-sm text-green-600">Your file is ready to download</p> |
| </div> |
| </div> |
| <div> |
| <button id="downloadBtn" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-6 rounded-lg transition"> |
| <i class="fas fa-download mr-2"></i> Download |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| <footer class="mt-12 text-center text-gray-500 text-sm"> |
| <p>Advanced MP4 to TS Converter - Uses FFmpeg.wasm for in-browser conversion. No files are uploaded to any server.</p> |
| <p class="mt-2">Note: First conversion may take longer as FFmpeg needs to load (~30MB).</p> |
| <p class="mt-2">© 2023 Video Converter Tool. All rights reserved.</p> |
| </footer> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const dropzone = document.getElementById('dropzone'); |
| const fileInput = document.getElementById('fileInput'); |
| const browseBtn = document.getElementById('browseBtn'); |
| const fileInfo = document.getElementById('fileInfo'); |
| const fileName = document.getElementById('fileName'); |
| const fileSize = document.getElementById('fileSize'); |
| const fileDuration = document.getElementById('fileDuration'); |
| const fileResolution = document.getElementById('fileResolution'); |
| const removeFile = document.getElementById('removeFile'); |
| const videoPreviewContainer = document.getElementById('videoPreviewContainer'); |
| const videoPreview = document.getElementById('videoPreview'); |
| const convertBtn = document.getElementById('convertBtn'); |
| const convertSpinner = document.getElementById('convertSpinner'); |
| const progressContainer = document.getElementById('progressContainer'); |
| const progressBar = document.getElementById('progressBar'); |
| const progressPercent = document.getElementById('progressPercent'); |
| const statusMessage = document.getElementById('statusMessage'); |
| const conversionLogContainer = document.getElementById('conversionLogContainer'); |
| const conversionLog = document.getElementById('conversionLog'); |
| const downloadSection = document.getElementById('downloadSection'); |
| const downloadBtn = document.getElementById('downloadBtn'); |
| const settingsToggle = document.getElementById('settingsToggle'); |
| const settingsPanel = document.getElementById('settingsPanel'); |
| const settingsIcon = document.getElementById('settingsIcon'); |
| |
| |
| const { createFFmpeg, fetchFile } = FFmpeg; |
| const ffmpeg = createFFmpeg({ |
| log: true, |
| corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js', |
| progress: ({ ratio }) => { |
| |
| const percent = Math.round(ratio * 100); |
| updateProgress(percent); |
| statusMessage.textContent = `Processing: ${percent}% complete`; |
| } |
| }); |
| |
| |
| ffmpeg.setLogger(({ type, message }) => { |
| if (!conversionLogContainer.classList.contains('hidden')) { |
| const logEntry = document.createElement('div'); |
| logEntry.className = `log-entry log-${type}`; |
| logEntry.textContent = message; |
| conversionLog.appendChild(logEntry); |
| conversionLog.scrollTop = conversionLog.scrollHeight; |
| } |
| }); |
| |
| let selectedFile = null; |
| let convertedBlob = null; |
| let videoMetadata = {}; |
| |
| |
| settingsToggle.addEventListener('click', function() { |
| settingsPanel.classList.toggle('open'); |
| settingsIcon.classList.toggle('transform'); |
| settingsIcon.classList.toggle('rotate-180'); |
| }); |
| |
| |
| browseBtn.addEventListener('click', function() { |
| fileInput.click(); |
| }); |
| |
| fileInput.addEventListener('change', function(e) { |
| if (e.target.files.length > 0) { |
| handleFileSelection(e.target.files[0]); |
| } |
| }); |
| |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| dropzone.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| dropzone.addEventListener(eventName, highlight, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| dropzone.addEventListener(eventName, unhighlight, false); |
| }); |
| |
| function highlight() { |
| dropzone.classList.add('active'); |
| } |
| |
| function unhighlight() { |
| dropzone.classList.remove('active'); |
| } |
| |
| dropzone.addEventListener('drop', function(e) { |
| const dt = e.dataTransfer; |
| const file = dt.files[0]; |
| if (file) { |
| handleFileSelection(file); |
| } |
| }); |
| |
| |
| function handleFileSelection(file) { |
| if (!file.type.match('video/mp4') && !file.name.match(/\.mp4$/i)) { |
| showError('Please select an MP4 video file.'); |
| return; |
| } |
| |
| selectedFile = file; |
| |
| |
| fileName.textContent = file.name; |
| fileSize.textContent = `Size: ${formatFileSize(file.size)}`; |
| fileInfo.classList.remove('hidden'); |
| |
| |
| convertBtn.disabled = false; |
| |
| |
| const videoURL = URL.createObjectURL(file); |
| videoPreview.src = videoURL; |
| videoPreviewContainer.classList.remove('hidden'); |
| |
| |
| dropzone.classList.add('hidden'); |
| |
| |
| extractVideoMetadata(videoURL); |
| } |
| |
| |
| function extractVideoMetadata(videoURL) { |
| videoPreview.onloadedmetadata = function() { |
| videoMetadata = { |
| duration: videoPreview.duration, |
| width: videoPreview.videoWidth, |
| height: videoPreview.videoHeight |
| }; |
| |
| fileDuration.textContent = `Duration: ${formatDuration(videoMetadata.duration)}`; |
| fileResolution.textContent = `Resolution: ${videoMetadata.width}x${videoMetadata.height}`; |
| }; |
| } |
| |
| |
| function formatDuration(seconds) { |
| const date = new Date(0); |
| date.setSeconds(seconds); |
| return date.toISOString().substr(11, 8); |
| } |
| |
| |
| removeFile.addEventListener('click', function() { |
| resetConverter(); |
| }); |
| |
| |
| function formatFileSize(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
| |
| |
| convertBtn.addEventListener('click', async function() { |
| if (!selectedFile) return; |
| |
| |
| progressContainer.classList.remove('hidden'); |
| conversionLogContainer.classList.remove('hidden'); |
| conversionLog.innerHTML = ''; |
| statusMessage.textContent = "Initializing conversion..."; |
| |
| |
| convertBtn.disabled = true; |
| convertSpinner.classList.remove('hidden'); |
| |
| try { |
| |
| if (!ffmpeg.isLoaded()) { |
| addLogEntry('Loading FFmpeg core (this may take a while on first run)...', 'info'); |
| await ffmpeg.load(); |
| addLogEntry('FFmpeg loaded successfully', 'success'); |
| } |
| |
| addLogEntry('Reading input file...', 'info'); |
| |
| |
| const inputName = 'input.mp4'; |
| ffmpeg.FS('writeFile', inputName, await fetchFile(selectedFile)); |
| |
| |
| const videoCodec = document.getElementById('videoCodec').value; |
| const videoBitrate = document.getElementById('videoBitrate').value; |
| const audioCodec = document.getElementById('audioCodec').value; |
| const audioBitrate = document.getElementById('audioBitrate').value; |
| const segmentDuration = document.getElementById('segmentDuration').value; |
| const outputFormat = document.getElementById('outputFormat').value; |
| |
| |
| addLogEntry('Starting video conversion...', 'info'); |
| |
| let outputName, command; |
| |
| if (outputFormat === 'm3u8') { |
| outputName = 'output.m3u8'; |
| command = [ |
| '-i', inputName, |
| '-c:v', videoCodec, |
| '-b:v', `${videoBitrate}k`, |
| '-c:a', audioCodec, |
| '-b:a', `${audioBitrate}k`, |
| '-hls_time', segmentDuration, |
| '-hls_playlist_type', 'vod', |
| '-f', 'hls', |
| outputName |
| ]; |
| } else { |
| outputName = 'output.ts'; |
| command = [ |
| '-i', inputName, |
| '-c:v', videoCodec, |
| '-b:v', `${videoBitrate}k`, |
| '-c:a', audioCodec, |
| '-b:a', `${audioBitrate}k`, |
| '-f', 'mpegts', |
| outputName |
| ]; |
| } |
| |
| addLogEntry(`Running FFmpeg command: ffmpeg ${command.join(' ')}`, 'info'); |
| |
| await ffmpeg.run(...command); |
| |
| |
| addLogEntry('Conversion complete, reading output file...', 'info'); |
| const data = ffmpeg.FS('readFile', outputName); |
| |
| |
| convertedBlob = new Blob([data.buffer], { |
| type: outputFormat === 'm3u8' ? 'application/x-mpegURL' : 'video/mp2t' |
| }); |
| |
| |
| conversionComplete(); |
| |
| } catch (error) { |
| console.error('Conversion error:', error); |
| addLogEntry(`Error: ${error.message}`, 'error'); |
| statusMessage.textContent = `Error: ${error.message}`; |
| convertSpinner.classList.add('hidden'); |
| convertBtn.disabled = false; |
| } |
| }); |
| |
| |
| function addLogEntry(message, type = 'info') { |
| const logEntry = document.createElement('div'); |
| logEntry.className = `log-entry log-${type}`; |
| logEntry.textContent = message; |
| conversionLog.appendChild(logEntry); |
| conversionLog.scrollTop = conversionLog.scrollHeight; |
| } |
| |
| |
| function showError(message) { |
| const errorDiv = document.createElement('div'); |
| errorDiv.className = 'bg-red-50 border-l-4 border-red-500 p-4 mb-4'; |
| errorDiv.innerHTML = ` |
| <div class="flex"> |
| <div class="flex-shrink-0"> |
| <i class="fas fa-exclamation-circle text-red-500"></i> |
| </div> |
| <div class="ml-3"> |
| <p class="text-sm text-red-700">${message}</p> |
| </div> |
| </div> |
| `; |
| |
| |
| dropzone.parentNode.insertBefore(errorDiv, dropzone.nextSibling); |
| |
| |
| setTimeout(() => { |
| errorDiv.remove(); |
| }, 5000); |
| } |
| |
| function updateProgress(percent) { |
| progressBar.style.width = percent + '%'; |
| progressPercent.textContent = Math.round(percent) + '%'; |
| } |
| |
| function conversionComplete() { |
| |
| convertSpinner.classList.add('hidden'); |
| |
| |
| downloadSection.classList.remove('hidden'); |
| |
| |
| statusMessage.textContent = "Conversion completed successfully!"; |
| addLogEntry('Conversion process finished successfully', 'success'); |
| } |
| |
| |
| downloadBtn.addEventListener('click', function() { |
| if (!convertedBlob) return; |
| |
| const url = URL.createObjectURL(convertedBlob); |
| const a = document.createElement('a'); |
| a.href = url; |
| |
| const outputFormat = document.getElementById('outputFormat').value; |
| const originalName = selectedFile.name.replace(/\.[^/.]+$/, ""); |
| |
| if (outputFormat === 'm3u8') { |
| a.download = `${originalName}.m3u8`; |
| } else { |
| a.download = `${originalName}.ts`; |
| } |
| |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| URL.revokeObjectURL(url); |
| }); |
| |
| |
| function resetConverter() { |
| selectedFile = null; |
| convertedBlob = null; |
| videoMetadata = {}; |
| |
| |
| fileInfo.classList.add('hidden'); |
| videoPreviewContainer.classList.add('hidden'); |
| progressContainer.classList.add('hidden'); |
| conversionLogContainer.classList.add('hidden'); |
| downloadSection.classList.add('hidden'); |
| convertBtn.disabled = true; |
| convertSpinner.classList.add('hidden'); |
| progressBar.style.width = '0%'; |
| progressPercent.textContent = '0%'; |
| statusMessage.textContent = ''; |
| |
| |
| dropzone.classList.remove('hidden'); |
| |
| |
| fileInput.value = ''; |
| |
| |
| if (videoPreview.src) { |
| URL.revokeObjectURL(videoPreview.src); |
| videoPreview.src = ''; |
| } |
| |
| |
| fileDuration.textContent = ''; |
| fileResolution.textContent = ''; |
| } |
| }); |
| </script> |
| </body> |
| </html> |