Why Convert M3U8 to MP4?
HLS (HTTP Live Streaming) is a streaming protocol introduced by Apple that uses M3U8 as a playlist file and splits video into multiple TS (Transport Stream) segments for transmission. This format is ideal for live and on-demand streaming scenarios, but in practice, we often need to convert M3U8 streams to the more universal MP4 format for the following purposes:
- Play back or archive videos offline locally
- Upload to platforms that do not support HLS
- Perform video editing or secondary processing
- Simplify sharing and distribution workflows
Traditional solutions rely on backend services or FFmpeg for transcoding, but with Mux.js, we can complete this conversion directly in the browser without server resources, achieving true client-side transmuxing.
Technical Principle Overview
The entire conversion process can be divided into three core steps:
- Fetch the M3U8 playlist: Parse the M3U8 file and extract all TS segment URLs.
- Download TS segments: Concurrently download all TS segments and concatenate them sequentially into a complete TS data stream.
- Transmux to MP4: Use Mux.js to multiplex the TS stream into fragmented MP4 (fMP4) or standard MP4 format.
The core role of Mux.js in this process is transmuxing, which converts the TS container format to the MP4 container format without involving encoding/decoding (transcoding), resulting in fast speed and lossless quality.
Core Code Implementation
Below is the complete core implementation of M3U8 to MP4 conversion, including M3U8 parsing, TS downloading, and Mux.js multiplexing.
1. Parse M3U8 Playlist:
// Parse M3U8 file and extract TS segment URLs
async function parseM3U8(m3u8Url: string): Promise<string[]> {
const response = await fetch(m3u8Url);
const content = await response.text();
const lines = content.split('\n');
const tsUrls: string[] = [];
const baseUrl = m3u8Url.substring(0, m3u8Url.lastIndexOf('/') + 1);
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('#') || trimmed === '') continue;
const url = trimmed.startsWith('http') ? trimmed : baseUrl + trimmed;
tsUrls.push(url);
}
return tsUrls;
}2. Download TS Segments Concurrently:
// Download all TS segments concurrently
async function downloadTsSegments(tsUrls: string[]): Promise<Uint8Array[]> {
const segments: Uint8Array[] = [];
const concurrency = 5;
for (let i = 0; i < tsUrls.length; i += concurrency) {
const batch = tsUrls.slice(i, i + concurrency);
const promises = batch.map(async (url) => {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
});
const results = await Promise.all(promises);
segments.push(...results);
}
return segments;
}3. Transmux to MP4 Using Mux.js:
import * as muxjs from 'mux.js';
function transmuxToMp4(tsSegments: Uint8Array[]): Uint8Array {
return new Promise((resolve, reject) => {
const transmuxer = new muxjs.ts.Transmuxer();
const mp4Chunks: Uint8Array[] = [];
transmuxer.on('data', (segment: any) => {
if (segment.video) {
mp4Chunks.push(new Uint8Array(segment.video.data));
}
if (segment.audio) {
mp4Chunks.push(new Uint8Array(segment.audio.data));
}
});
transmuxer.on('done', () => {
const totalLength = mp4Chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of mp4Chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
resolve(result);
});
for (const segment of tsSegments) {
transmuxer.push(segment);
}
transmuxer.flush();
});
}Complete Conversion Flow
Combining the three steps above forms the complete M3U8 to MP4 conversion flow:
// Complete M3U8 to MP4 conversion function
async function convertM3U8ToMp4(m3u8Url: string): Promise<Uint8Array> {
try {
console.log('📋 Parsing M3U8 playlist...');
const tsUrls = await parseM3U8(m3u8Url);
console.log(`✅ Found ${tsUrls.length} TS segments`);
console.log('⬇️ Downloading TS segments...');
const tsSegments = await downloadTsSegments(tsUrls);
console.log(`✅ Download complete, total size: ${formatSize(tsSegments.reduce((sum, seg) => sum + seg.length, 0))}`);
console.log('🔄 Transmuxing to MP4...');
const mp4Data = await transmuxToMp4(tsSegments);
console.log(`✅ Conversion complete, MP4 size: ${formatSize(mp4Data.length)}`);
return mp4Data;
} catch (error) {
console.error('❌ Conversion failed:', error);
throw error;
}
}
async function downloadM3U8AsMp4(m3u8Url: string, fileName: string = 'output.mp4') {
const mp4Data = await convertM3U8ToMp4(m3u8Url);
const blob = new Blob([mp4Data], { type: 'video/mp4' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}Key Technical Details
Concurrency Control:
To avoid browser concurrent connection limits and memory overflow, it is recommended to control the number of concurrent downloads (typically 3-5). The example above uses concurrency = 5 for batch downloading.
Memory Management:
For longer videos (e.g., over 1 hour), storing all TS segments in memory simultaneously may cause performance issues. Use streaming processing instead: download TS segments while pushing them to the Mux.js transmuxer to reduce memory usage.
Error Handling and Retries:
Unstable network conditions may cause some TS segment downloads to fail. Implementing a retry mechanism is recommended:
async function fetchWithRetry(url: string, maxRetries: number = 3): Promise<Uint8Array> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
throw new Error('Download failed');
}Usage Example
Integrating the conversion functionality in a Vue component:
<template>
<div>
<input v-model="m3u8Url" placeholder="Enter M3U8 URL" />
<button @click="handleConvert" :disabled="loading">
{{ loading ? 'Converting...' : 'Start Conversion' }}
</button>
<div v-if="progress">Progress: {{ progress }}%</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { downloadM3U8AsMp4 } from './m3u8-converter';
const m3u8Url = ref('');
const loading = ref(false);
const progress = ref(0);
const handleConvert = async () => {
if (!m3u8Url.value) return;
loading.value = true;
progress.value = 0;
try {
await downloadM3U8AsMp4(m3u8Url.value);
alert('Conversion complete!');
} catch (error) {
alert('Conversion failed: ' + error.message);
} finally {
loading.value = false;
progress.value = 0;
}
};
</script>Performance Optimization Tips
- Use Web Workers: Move Mux.js multiplexing logic to a Web Worker to avoid blocking the main thread and improve page responsiveness.
- Progressive Download: For large videos, use streaming processing instead of downloading all segments at once to significantly reduce memory pressure.
- Cache Reuse: For repeated conversions of the same live stream, cache TS segment data to avoid redundant network requests.
- Segment Download Optimization: Use HTTP Range requests or CDN acceleration to improve overall download speed.
Common Issues and Solutions
1. Cross-Origin Issues (CORS):
M3U8 and TS segments may come from different domains, requiring the server to be configured with the correct CORS headers. If you cannot control the server side, use a proxy or backend forwarding to resolve this.
2. Encrypted Streams (AES-128):
Some HLS streams use AES-128 encryption, requiring you to first obtain the key (KEY) and decrypt the TS segments. Mux.js itself does not handle decryption; decryption must be done before feeding data into the transmuxer.
3. Audio/Video Desynchronization:
Ensure TS segments are pushed in the correct order and use the flush() method to flush the buffer. Mux.js automatically handles timestamp alignment.
Summary
With Mux.js, we can elegantly implement M3U8 to MP4 conversion in the browser without relying on backend services. This solution offers the following advantages:
- No server required: Fully client-side, saving server resources
- Real-time conversion: Supports streaming processing, converting while downloading
- Lossless conversion: Transmuxing does not change the encoding format, preserving original quality
- Lightweight: Mux.js is small in size, suitable for web applications
I hope this article helps you understand and implement M3U8 to MP4 conversion functionality.