convert / MP4toTS.html
KEXEL's picture
1.11
fb83349 verified
<!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">
<!-- File Drop Zone -->
<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>
<!-- File Info -->
<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>
<!-- Video Preview -->
<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>
<!-- Conversion Settings -->
<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>
<!-- Progress Bar -->
<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>
<!-- Conversion Log -->
<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>
<!-- Convert Button -->
<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>
<!-- Download Section -->
<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() {
// Elements
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');
// FFmpeg setup
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({
log: true,
corePath: 'https://unpkg.com/@ffmpeg/core@0.11.0/dist/ffmpeg-core.js',
progress: ({ ratio }) => {
// 0 to 1 => 0 to 100
const percent = Math.round(ratio * 100);
updateProgress(percent);
statusMessage.textContent = `Processing: ${percent}% complete`;
}
});
// Capture FFmpeg logs
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 = {};
// Toggle settings panel
settingsToggle.addEventListener('click', function() {
settingsPanel.classList.toggle('open');
settingsIcon.classList.toggle('transform');
settingsIcon.classList.toggle('rotate-180');
});
// Handle file selection via browse button
browseBtn.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function(e) {
if (e.target.files.length > 0) {
handleFileSelection(e.target.files[0]);
}
});
// Handle drag and drop
['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);
}
});
// Handle file selection
function handleFileSelection(file) {
if (!file.type.match('video/mp4') && !file.name.match(/\.mp4$/i)) {
showError('Please select an MP4 video file.');
return;
}
selectedFile = file;
// Display file info
fileName.textContent = file.name;
fileSize.textContent = `Size: ${formatFileSize(file.size)}`;
fileInfo.classList.remove('hidden');
// Enable convert button
convertBtn.disabled = false;
// Show video preview
const videoURL = URL.createObjectURL(file);
videoPreview.src = videoURL;
videoPreviewContainer.classList.remove('hidden');
// Hide dropzone
dropzone.classList.add('hidden');
// Extract video metadata
extractVideoMetadata(videoURL);
}
// Extract video metadata (duration, resolution)
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}`;
};
}
// Format duration (seconds to HH:MM:SS)
function formatDuration(seconds) {
const date = new Date(0);
date.setSeconds(seconds);
return date.toISOString().substr(11, 8);
}
// Remove file
removeFile.addEventListener('click', function() {
resetConverter();
});
// Format file size
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];
}
// Convert button click
convertBtn.addEventListener('click', async function() {
if (!selectedFile) return;
// Show progress and log
progressContainer.classList.remove('hidden');
conversionLogContainer.classList.remove('hidden');
conversionLog.innerHTML = '';
statusMessage.textContent = "Initializing conversion...";
// Disable convert button and show spinner
convertBtn.disabled = true;
convertSpinner.classList.remove('hidden');
try {
// Load FFmpeg if not already loaded
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');
// Write the file to FFmpeg's virtual file system
const inputName = 'input.mp4';
ffmpeg.FS('writeFile', inputName, await fetchFile(selectedFile));
// Get conversion settings
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;
// Run FFmpeg command
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);
// Read the result
addLogEntry('Conversion complete, reading output file...', 'info');
const data = ffmpeg.FS('readFile', outputName);
// Create download blob
convertedBlob = new Blob([data.buffer], {
type: outputFormat === 'm3u8' ? 'application/x-mpegURL' : 'video/mp2t'
});
// Complete the process
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;
}
});
// Add entry to conversion log
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;
}
// Show error message
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>
`;
// Insert after the dropzone
dropzone.parentNode.insertBefore(errorDiv, dropzone.nextSibling);
// Remove after 5 seconds
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
function updateProgress(percent) {
progressBar.style.width = percent + '%';
progressPercent.textContent = Math.round(percent) + '%';
}
function conversionComplete() {
// Hide spinner
convertSpinner.classList.add('hidden');
// Show download section
downloadSection.classList.remove('hidden');
// Update status
statusMessage.textContent = "Conversion completed successfully!";
addLogEntry('Conversion process finished successfully', 'success');
}
// Download button
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);
});
// Reset converter to initial state
function resetConverter() {
selectedFile = null;
convertedBlob = null;
videoMetadata = {};
// Reset UI
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 = '';
// Show dropzone again
dropzone.classList.remove('hidden');
// Reset file input
fileInput.value = '';
// Revoke object URLs
if (videoPreview.src) {
URL.revokeObjectURL(videoPreview.src);
videoPreview.src = '';
}
// Clear metadata display
fileDuration.textContent = '';
fileResolution.textContent = '';
}
});
</script>
</body>
</html>