Published on

m3u8转mp4的几种方法

Authors
  • avatar
    Name
    Adain
    Twitter

m3u8转mp4的几种方法

m3u8是一种常见的流媒体播放列表格式,通常用于HLS(HTTP Live Streaming)视频传输。在某些情况下,我们需要将m3u8文件转换为mp4格式以便本地保存或进一步处理。本文将介绍几种实用的转换方法。

什么是m3u8?

m3u8是一种基于文本的播放列表文件格式,它包含了一系列媒体文件的URL列表。HLS协议将视频分割成多个小片段(通常是.ts文件),然后通过m3u8文件来组织这些片段的播放顺序。

典型的m3u8文件内容:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXTINF:10.0,
segment1.ts
#EXTINF:10.0,
segment2.ts
#EXTINF:10.0,
segment3.ts
#EXT-X-ENDLIST

方法一:使用FFmpeg(推荐)

FFmpeg是最强大和最可靠的视频处理工具,支持几乎所有视频格式的转换。

安装FFmpeg

Windows:

# 使用Chocolatey
choco install ffmpeg

# 或下载预编译版本
# https://ffmpeg.org/download.html

macOS:

# 使用Homebrew
brew install ffmpeg

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install ffmpeg

基本转换命令

# 简单转换
ffmpeg -i "https://example.com/playlist.m3u8" -c copy output.mp4

# 指定编解码器
ffmpeg -i "https://example.com/playlist.m3u8" -c:v libx264 -c:a aac output.mp4

# 重新编码并设置质量
ffmpeg -i "https://example.com/playlist.m3u8" -c:v libx264 -crf 23 -c:a aac -b:a 128k output.mp4

高级参数说明

# 完整的转换命令示例
ffmpeg -i "playlist.m3u8" \
  -c:v libx264 \           # 视频编码器
  -c:a aac \               # 音频编码器
  -crf 23 \                # 视频质量 (18-28, 越小质量越好)
  -preset medium \         # 编码速度预设
  -movflags +faststart \   # 优化网络播放
  -y \                     # 覆盖输出文件
  output.mp4

批量转换脚本

Bash脚本 (Linux/macOS):

#!/bin/bash
# batch_convert.sh

for file in *.m3u8; do
    if [ -f "$file" ]; then
        output="${file%.m3u8}.mp4"
        echo "转换 $file$output"
        ffmpeg -i "$file" -c copy "$output"
    fi
done

批处理脚本 (Windows):

@echo off
for %%f in (*.m3u8) do (
    echo 转换 %%f
    ffmpeg -i "%%f" -c copy "%%~nf.mp4"
)
pause

方法二:使用Python脚本

对于需要更多控制或批量处理的场景,可以使用Python编写自定义脚本。

安装依赖

pip install requests m3u8 ffmpeg-python

基础转换脚本

import m3u8
import requests
import os
import subprocess
from urllib.parse import urljoin, urlparse

def download_m3u8_to_mp4(m3u8_url, output_file):
    """
    下载m3u8流并转换为mp4
    """
    try:
        # 解析m3u8文件
        playlist = m3u8.load(m3u8_url)

        if playlist.is_variant:
            # 如果是多码率播放列表,选择最高质量
            best_playlist = max(playlist.playlists,
                              key=lambda p: p.stream_info.bandwidth or 0)
            m3u8_url = urljoin(m3u8_url, best_playlist.uri)
            playlist = m3u8.load(m3u8_url)

        # 获取基础URL
        base_url = m3u8_url.rsplit('/', 1)[0] + '/'

        # 创建临时目录
        temp_dir = "temp_segments"
        os.makedirs(temp_dir, exist_ok=True)

        # 下载所有片段
        segment_files = []
        for i, segment in enumerate(playlist.segments):
            segment_url = urljoin(base_url, segment.uri)
            segment_file = f"{temp_dir}/segment_{i:04d}.ts"

            print(f"下载片段 {i+1}/{len(playlist.segments)}")

            response = requests.get(segment_url, stream=True)
            response.raise_for_status()

            with open(segment_file, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)

            segment_files.append(segment_file)

        # 使用FFmpeg合并片段
        concat_file = f"{temp_dir}/concat_list.txt"
        with open(concat_file, 'w') as f:
            for segment_file in segment_files:
                f.write(f"file '{os.path.abspath(segment_file)}'\n")

        # FFmpeg命令
        cmd = [
            'ffmpeg',
            '-f', 'concat',
            '-safe', '0',
            '-i', concat_file,
            '-c', 'copy',
            '-y',
            output_file
        ]

        print("合并视频片段...")
        subprocess.run(cmd, check=True)

        # 清理临时文件
        for file in segment_files:
            os.remove(file)
        os.remove(concat_file)
        os.rmdir(temp_dir)

        print(f"转换完成: {output_file}")

    except Exception as e:
        print(f"转换失败: {str(e)}")

# 使用示例
if __name__ == "__main__":
    m3u8_url = "https://example.com/playlist.m3u8"
    output_file = "output_video.mp4"
    download_m3u8_to_mp4(m3u8_url, output_file)

增强版Python脚本

import m3u8
import requests
import os
import subprocess
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin
import time

class m3u8Downloader:
    def __init__(self, max_workers=5, timeout=30):
        self.max_workers = max_workers
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        })

    def download_segment(self, segment_info):
        """下载单个视频片段"""
        url, filepath, index, total = segment_info

        for attempt in range(3):  # 重试3次
            try:
                response = self.session.get(url, timeout=self.timeout)
                response.raise_for_status()

                with open(filepath, 'wb') as f:
                    f.write(response.content)

                print(f"\r下载进度: {index+1}/{total} ({(index+1)/total*100:.1f}%)", end='', flush=True)
                return True

            except Exception as e:
                if attempt == 2:  # 最后一次尝试
                    print(f"\n下载失败 {filepath}: {str(e)}")
                    return False
                time.sleep(1)  # 重试前等待

    def convert_m3u8_to_mp4(self, m3u8_url, output_file, quality='best'):
        """
        转换m3u8到mp4
        quality: 'best', 'worst', 或指定带宽
        """
        try:
            print(f"解析m3u8: {m3u8_url}")
            playlist = m3u8.load(m3u8_url)

            # 处理多码率播放列表
            if playlist.is_variant:
                if quality == 'best':
                    selected = max(playlist.playlists,
                                 key=lambda p: p.stream_info.bandwidth or 0)
                elif quality == 'worst':
                    selected = min(playlist.playlists,
                                 key=lambda p: p.stream_info.bandwidth or float('inf'))
                else:
                    # 按带宽选择
                    target_bandwidth = int(quality)
                    selected = min(playlist.playlists,
                                 key=lambda p: abs((p.stream_info.bandwidth or 0) - target_bandwidth))

                print(f"选择码率: {selected.stream_info.bandwidth} bps")
                m3u8_url = urljoin(m3u8_url, selected.uri)
                playlist = m3u8.load(m3u8_url)

            # 准备下载
            base_url = m3u8_url.rsplit('/', 1)[0] + '/'
            temp_dir = f"temp_{int(time.time())}"
            os.makedirs(temp_dir, exist_ok=True)

            # 准备片段下载任务
            download_tasks = []
            for i, segment in enumerate(playlist.segments):
                segment_url = urljoin(base_url, segment.uri)
                segment_file = f"{temp_dir}/segment_{i:04d}.ts"
                download_tasks.append((segment_url, segment_file, i, len(playlist.segments)))

            # 并发下载片段
            print(f"\n开始下载 {len(download_tasks)} 个片段...")

            with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
                futures = [executor.submit(self.download_segment, task) for task in download_tasks]

                success_count = 0
                for future in as_completed(futures):
                    if future.result():
                        success_count += 1

            print(f"\n下载完成: {success_count}/{len(download_tasks)} 个片段")

            if success_count == 0:
                raise Exception("没有成功下载任何片段")

            # 合并片段
            print("合并视频片段...")
            self.merge_segments(temp_dir, output_file)

            # 清理临时文件
            import shutil
            shutil.rmtree(temp_dir)

            print(f"转换完成: {output_file}")

        except Exception as e:
            print(f"转换失败: {str(e)}")
            raise

    def merge_segments(self, temp_dir, output_file):
        """使用FFmpeg合并视频片段"""

        # 创建文件列表
        concat_file = f"{temp_dir}/concat_list.txt"
        segment_files = sorted([f for f in os.listdir(temp_dir) if f.endswith('.ts')])

        with open(concat_file, 'w', encoding='utf-8') as f:
            for segment_file in segment_files:
                f.write(f"file '{os.path.abspath(os.path.join(temp_dir, segment_file))}'\n")

        # FFmpeg命令
        cmd = [
            'ffmpeg',
            '-f', 'concat',
            '-safe', '0',
            '-i', concat_file,
            '-c', 'copy',
            '-movflags', '+faststart',
            '-y',
            output_file
        ]

        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise Exception(f"FFmpeg错误: {result.stderr}")

def main():
    parser = argparse.ArgumentParser(description='m3u8转mp4工具')
    parser.add_argument('m3u8_url', help='m3u8文件URL')
    parser.add_argument('-o', '--output', default='output.mp4', help='输出文件名')
    parser.add_argument('-q', '--quality', default='best',
                       help='视频质量: best, worst, 或指定带宽')
    parser.add_argument('-w', '--workers', type=int, default=5,
                       help='并发下载线程数')

    args = parser.parse_args()

    downloader = m3u8Downloader(max_workers=args.workers)
    downloader.convert_m3u8_to_mp4(args.m3u8_url, args.output, args.quality)

if __name__ == "__main__":
    main()

方法三:使用在线工具

对于不想安装软件的用户,可以使用在线转换工具:

推荐的在线工具

  1. CloudConvert

  2. Online Video Converter

  3. Convertio

注意事项

  • 在线工具的处理速度取决于网络和服务器负载
  • 大文件可能有上传限制
  • 注意隐私安全,避免上传敏感内容

方法四:使用专门的下载工具

you-get

# 安装
pip install you-get

# 使用
you-get -o /path/to/output "https://example.com/playlist.m3u8"

yt-dlp

# 安装
pip install yt-dlp

# 使用
yt-dlp -o "%(title)s.%(ext)s" "https://example.com/playlist.m3u8"

# 指定格式
yt-dlp -f "best[ext=mp4]" "https://example.com/playlist.m3u8"

N_m3u8DL-CLI

Windows专用的m3u8下载工具:

# 下载并解压工具
# https://github.com/nilaoda/N_m3u8DL-CLI

# 使用
N_m3u8DL-CLI.exe "https://example.com/playlist.m3u8" --workDir "C:\Downloads"

常见问题和解决方案

1. 网络超时问题

# FFmpeg增加网络缓冲
ffmpeg -i "playlist.m3u8" -timeout 30000000 -c copy output.mp4

# 使用代理
ffmpeg -http_proxy "http://proxy:port" -i "playlist.m3u8" -c copy output.mp4

2. 权限验证问题

某些m3u8需要特定的请求头:

# 添加User-Agent
ffmpeg -user_agent "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" -i "playlist.m3u8" -c copy output.mp4

# 添加Referer
ffmpeg -headers "Referer: https://example.com" -i "playlist.m3u8" -c copy output.mp4

3. 加密的m3u8流

对于AES加密的HLS流:

import m3u8
from Crypto.Cipher import AES
import requests

def decrypt_segment(encrypted_data, key, iv):
    """解密AES加密的片段"""
    cipher = AES.new(key, AES.MODE_CBC, iv)
    return cipher.decrypt(encrypted_data)

# 在下载脚本中处理加密
def download_encrypted_segment(segment, key_uri, base_url):
    # 获取密钥
    key_response = requests.get(urljoin(base_url, key_uri))
    key = key_response.content

    # 下载加密片段
    segment_url = urljoin(base_url, segment.uri)
    response = requests.get(segment_url)

    # 解密
    iv = bytes.fromhex(segment.key.iv.replace('0x', '')) if segment.key.iv else b'\x00' * 16
    decrypted_data = decrypt_segment(response.content, key, iv)

    return decrypted_data

4. 大文件处理

对于特别大的m3u8文件:

# 使用流复制模式,减少内存占用
ffmpeg -i "playlist.m3u8" -c copy -avoid_negative_ts make_zero output.mp4

# 分段处理
ffmpeg -i "playlist.m3u8" -ss 00:00:00 -t 01:00:00 -c copy part1.mp4
ffmpeg -i "playlist.m3u8" -ss 01:00:00 -t 01:00:00 -c copy part2.mp4

性能优化建议

1. 并发下载优化

# 调整并发数量
max_workers = min(32, (os.cpu_count() or 1) + 4)

# 使用连接池
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
    pool_connections=100,
    pool_maxsize=100
)
session.mount('http://', adapter)
session.mount('https://', adapter)

2. 内存优化

# 流式下载大文件
def download_large_segment(url, filepath):
    with requests.get(url, stream=True) as response:
        response.raise_for_status()
        with open(filepath, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                f.write(chunk)

3. 错误处理和重试

import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_robust_session():
    session = requests.Session()

    retry_strategy = Retry(
        total=3,
        backoff_factor=1,
        status_forcelist=[429, 500, 502, 503, 504],
    )

    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)

    return session

总结

m3u8转mp4的方法各有优劣:

  • FFmpeg: 功能最强大,支持最全面,推荐用于技术用户
  • Python脚本: 灵活性最高,可定制性强,适合开发者
  • 在线工具: 最简单易用,适合偶尔使用的用户
  • 专门工具: 针对性强,通常有更好的用户体验

选择哪种方法取决于你的具体需求、技术水平和使用频率。对于大多数用户,我推荐先尝试FFmpeg的简单命令,如果需要更多控制,再考虑使用Python脚本或专门的下载工具。

记住要遵守相关的版权法律,只下载你有权限访问的内容。