视频流下载

这段 Python 代码是一个 图形界面的 Windows 视频流下载器,使用了 PyQt5 作为 GUI 框架,yt-dlp 作为下载引擎,并结合 ffmpeg 实现音视频的自动合并。


🔍 代码总览

核心模块:

模块作用
PyQt5 图形界面,用户输入视频地址/选择目录等操作
yt-dlp 视频/音频流解析与下载(支持 m3u8/YouTube 等)
ffmpeg 音视频合并,生成完整 mp4 文件

📌 自动下载视频流的机制

关键点在于 yt-dlp 强大的自动处理能力,它:

  1. 自动识别视频地址(支持 YouTube、B站、m3u8等)

  2. 分析视频/音频轨道(DASH/HLS 分离流)

  3. 下载所有轨道并自动调用 ffmpeg 进行合并


🧠 为什么它能找到并下载视频流?

yt-dlp 的作用

with yt_dlp.YoutubeDL(ydl_opts) as ydl:
    ydl.download([url])
  • yt-dlpyoutube-dl 的高级分支,具备以下能力:

    • 自动解析网页并提取视频/音频链接(如 m3u8, dash, mp4);

    • 按格式选择最高质量的音视频;

    • 下载后自动调用 ffmpeg 进行合并(如 video.webm + audio.webm -> mp4);

    • 支持断点续传、代理、证书忽略等功能。


⚙️ 视频流下载配置详解(_build_opts 方法)

def _build_opts(self, save_dir: str, hook):
    return {
        'format': 'bestvideo+bestaudio/best',  # 自动选最高质量的视频 + 音频
        'merge_output_format': 'mp4',          # 合并后输出 mp4 格式
        'postprocessors': [{                   # 合并所需处理器
            'key': 'FFmpegVideoConvertor',
            'preferedformat': 'mp4',
        }],
        'continuedl': True,                    # 支持断点续传
        'outtmpl': '.../%(title)s.%(ext)s',    # 输出命名格式
        'paths': {'temp': save_dir},           # 临时文件存放路径
        'proxy': self.proxy,                   # 可选代理
        'nocheckcertificate': True,            # 忽略 HTTPS 证书验证
        'progress_hooks': [hook],              # 日志打印钩子
        ...
    }

🧩 音视频合并的自动流程

yt-dlp 下载流程简化如下:

  1. yt-dlp 根据 URL 解析出视频格式列表;

  2. format='bestvideo+bestaudio' 选择两个文件;

  3. 分别下载视频流和音频流(通常为 .webm.m4a);

  4. 如果安装了 ffmpeg.exe

    • 自动调用命令:

      ffmpeg -i video.webm -i audio.webm -c copy output.mp4
      
  5. 合并成功后,生成最终 mp4 文件。


📂 文件结构与输出控制

'outtmpl': str(pathlib.Path(save_dir) / '%(title)s.%(ext)s'),
  • 自动将视频命名为其原始标题;

  • 视频和音频临时文件会放在 save_dir 指定的目录中;

  • 下载完成后会清理临时文件,只保留合成好的 mp4


🔐 HTTPS 支持与代理

  • nocheckcertificate=True → 忽略证书错误(常见于某些自签名站点);

  • proxy=http://...socks5://... → 支持FQ代理。


👁️ 日志钩子显示实时状态

def hook(d):
    if d['status'] == 'downloading':
        self.emitter.log.emit(f"{d.get('_percent_str','').strip()}  {d.get('filename','')}")
    elif d['status'] == 'finished':
        self.emitter.log.emit(f"✅ 已完成:{d['filename']}")
  • 实时显示下载进度、文件名;

  • 下载完毕会提示“已完成”。


🖼️ 图形界面功能说明

控件功能描述
url_edit 输入视频地址(YouTube/m3u8等)
dir_edit 设置保存目录
proxy_edit 可选代理地址
ignore_cert 是否忽略证书
start_btn 启动后台线程开始下载
log_view 实时显示日志/状态

✅ 总结:为什么能自动识别并下载合并视频流

  • yt-dlp 提供完整的自动化下载 + 合并流程;

  • format=bestvideo+bestaudio 精选音视频轨道;

  • 内部调用 ffmpeg 实现合并;

  • outtmplcontinuedlproxy 等提供用户级控制;

  • 下载进度通过钩子函数实时显示。


一句话结论  
下载 **ffmpeg-master-latest-win64-gpl.zip**(约 100 MB),解压后把里面的 `bin\ffmpeg.exe` 放到与你 `video_downloader.py` **同一目录**即可。  

--------------------------------------------------
逐步操作(Windows)  

1. 打开页面  
   https://github.com/BtbN/FFmpeg-Builds/releases  

2. 在 **Assets** 区域找到  
   ```
   ffmpeg-master-latest-win64-gpl.zip
   ```
   点它下载。  

3. 解压  
   - 解压后得到  
     ```
     ffmpeg-master-latest-win64-gpl\
       ├─ bin\
       │   ├─ ffmpeg.exe   ← 只要它
       │   └─ ffprobe.exe
       └─ ...
     ```

4. 放文件  
   把 `ffmpeg.exe` 复制到 `video_downloader.py` 所在文件夹,例如  
   ```
   D:\Tools\VideoDownloader\
     ├─ video_downloader.py
     ├─ ffmpeg.exe           ← 放这里
   ```

5. 运行  
   双击 `video_downloader.py`(或 `python video_downloader.py`),
   日志出现  
   ```
   [Merger] Merging formats into "xxx.mp4"
   ```
   说明 ffmpeg 已生效,音视频自动合并。

  

 

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Windows 视频流下载器(单文件最终版)
Python 3.x + PyQt5 + yt-dlp
功能:
1. 图形界面
2. 断点续传
3. 自动音画合并(需 ffmpeg.exe 同目录或 PATH)
4. 支持代理 / 忽略证书
"""

import sys, os, pathlib, threading
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
                             QPushButton, QLineEdit, QLabel, QFileDialog,
                             QPlainTextEdit, QMessageBox, QCheckBox)
from PyQt5.QtCore import QObject, pyqtSignal

# ---------- 下载核心 ----------
import yt_dlp

class Downloader:
    def __init__(self, proxy: str = None, ignore_cert: bool = False):
        self.proxy = proxy
        self.ignore_cert = ignore_cert

    def _build_opts(self, save_dir: str, hook):
        return {
            'outtmpl': str(pathlib.Path(save_dir) / '%(title)s.%(ext)s'),
            'progress_hooks': [hook],
            'noplaylist': True,
            'ignoreerrors': True,
            'no_warnings': False,
            'verbose': False,
            'continuedl': True,
            'paths': {'temp': save_dir},

            # 关键:自动选最佳视频+音频并合并成 mp4
            'format': 'bestvideo+bestaudio/best',
            'merge_output_format': 'mp4',
            'postprocessors': [{
                'key': 'FFmpegVideoConvertor',
                'preferedformat': 'mp4',
            }],
        } | ({'nocheckcertificate': True} if self.ignore_cert else {}) \
          | ({'proxy': self.proxy} if self.proxy else {})

    def download(self, url: str, save_dir: str, hook):
        ydl_opts = self._build_opts(save_dir, hook)
        with yt_dlp.YoutubeDL(ydl_opts) as ydl:
            ydl.download([url])

# ---------- GUI ----------
class LogEmitter(QObject):
    log = pyqtSignal(str)

class MainWin(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("视频流下载器(单文件最终版)")
        self.resize(550, 420)

        # 控件
        self.url_edit = QLineEdit()
        self.url_edit.setPlaceholderText("粘贴视频/直播/m3u8 地址")

        self.dir_edit = QLineEdit()
        self.dir_edit.setPlaceholderText("保存目录")
        self.browse_btn = QPushButton("浏览")

        self.proxy_edit = QLineEdit()
        self.proxy_edit.setPlaceholderText("http://127.0.0.1:1080  或 socks5://127.0.0.1:1080")

        self.ignore_cert_cb = QCheckBox("忽略证书错误")
        self.start_btn = QPushButton("开始下载")
        self.log_view = QPlainTextEdit(readOnly=True)

        # 布局
        layout = QVBoxLayout(self)
        layout.addWidget(QLabel("视频地址:"))
        layout.addWidget(self.url_edit)

        h_dir = QHBoxLayout()
        h_dir.addWidget(self.dir_edit)
        h_dir.addWidget(self.browse_btn)
        layout.addWidget(QLabel("保存目录:"))
        layout.addLayout(h_dir)

        layout.addWidget(QLabel("代理(可选):"))
        layout.addWidget(self.proxy_edit)
        layout.addWidget(self.ignore_cert_cb)
        layout.addWidget(self.start_btn)
        layout.addWidget(QLabel("日志:"))
        layout.addWidget(self.log_view)

        # 信号
        self.emitter = LogEmitter()
        self.emitter.log.connect(self.log_view.appendPlainText)

        # 事件
        self.browse_btn.clicked.connect(self.choose_dir)
        self.start_btn.clicked.connect(self.start_download)

    def choose_dir(self):
        path = QFileDialog.getExistingDirectory(self, "选择保存目录")
        if path:
            self.dir_edit.setText(path)

    def start_download(self):
        url = self.url_edit.text().strip()
        save_dir = self.dir_edit.text().strip()
        if not url or not save_dir:
            QMessageBox.warning(self, "提示", "地址和目录不能为空")
            return
        os.makedirs(save_dir, exist_ok=True)

        proxy = self.proxy_edit.text().strip() or None
        ignore_cert = self.ignore_cert_cb.isChecked()

        self.downloader = Downloader(proxy=proxy, ignore_cert=ignore_cert)
        self.start_btn.setEnabled(False)
        self.log_view.clear()
        self.thread = threading.Thread(target=self.run_download,
                                       args=(url, save_dir),
                                       daemon=True)
        self.thread.start()

    def run_download(self, url, save_dir):
        def hook(d):
            if d['status'] == 'downloading':
                self.emitter.log.emit(f"{d.get('_percent_str','').strip()}  {d.get('filename','')}")
            elif d['status'] == 'finished':
                self.emitter.log.emit(f"✅ 已完成:{d['filename']}")
        try:
            self.downloader.download(url, save_dir, hook)
            self.emitter.log.emit("全部任务下载完成!")
        except Exception as e:
            self.emitter.log.emit(f"❌ 错误:{e}")
        finally:
            self.start_btn.setEnabled(True)

# ---------- 入口 ----------
if __name__ == '__main__':
    app = QApplication(sys.argv)
    win = MainWin()
    win.show()
    sys.exit(app.exec_())

  

posted on 2025-07-19 04:36  吃草的青蛙  阅读(35)  评论(0)    收藏  举报

导航