You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Node.js开发YouTube下载器无法获取720p/1080p高清格式的求助

Node.js开发YouTube下载器无法获取720p/1080p高清格式的求助

我正在做一个类似Y2Mate或SaveFrom的YouTube视频下载器,用Node.js配合yt-dlp和FFmpeg来实现。但现在遇到个问题——不管下载哪个视频,最多只能拿到360p的格式,想支持720p和1080p却一直不行,有没有大佬能帮我看看问题出在哪?万分感谢!另外说明下,这个下载器只是我作为开发者的教育学习用途,绝对不会用来做非法活动。

我的Youtube.js代码

const express = require("express");
const router = express.Router();
const { exec } = require("child_process");
const path = require("path");
const YTDLP = path.join(__dirname, "..", "yt-dlp.exe");
const FFMPEG = path.join(__dirname, "..", "ffmpeg.exe");

// --------------------------
// GET VIDEO INFO (FORMATS)
// --------------------------
router.post("/video-info", (req, res) => {
  const { url } = req.body;
  if (!url) {
    return res.json({ success: false, error: "No URL provided" });
  }
  const cmd = `"${YTDLP}" --no-warnings -J "${url}"`;
  exec(cmd, (err, stdout) => {
    if (err) return res.json({ success: false, error: "Failed to get info" });
    try {
      const info = JSON.parse(stdout);
      const title = info.title;
      const thumbnail = info.thumbnail;
      const formats = [];
      const audioFormats = [];

      info.formats.forEach((f) => {
        if (!f.url) return;

        // AUDIO
        if (f.vcodec === "none" && f.acodec !== "none") {
          audioFormats.push({
            itag: f.format_id,
            quality: "High Quality",
            ext: "mp3",
            url: f.url
          });
        }

        // ONLY MP4 VIDEO QUALITIES
        if (f.ext === "mp4" && f.vcodec !== "none" && f.acodec !== "none") {
          formats.push({
            itag: f.format_id,
            quality: f.format_note || f.resolution,
            ext: "mp4",
            size: f.filesize || null,
            url: f.url
          });
        }
      });

      // SORT video formats by quality
      const qualityOrder = ["2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p"];
      const sortedFormats = qualityOrder
        .map(q => formats.find(f => (f.quality + "").includes(q)))
        .filter(Boolean); // remove null

      res.json({
        success: true,
        title,
        thumbnail,
        formats: sortedFormats,
        mp3: audioFormats[0] || null
      });
    } catch (e) {
      console.error(e);
      res.json({ success: false, error: "Parse error" });
    }
  });
});

// --------------------------
// DOWNLOAD ROUTE
// --------------------------
router.get("/download", (req, res) => {
  const { url, itag, title } = req.query;
  const safeName = title.replace(/[\/\\:*?"<>|]/g, "");
  const output = `${safeName}.mp4`;
  const cmd = `"${YTDLP}" -f ${itag}+bestaudio --merge-output-format mp4 -o "${output}" "${url}"`;

  exec(cmd, () => {
    res.download(output, () => {
      try {
        fs.unlinkSync(output);
      } catch {}
    });
  });
});

module.exports = router;

我的Index.html代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>YouTube Downloader – Y2Mate Style</title>
  <style>
    body { margin: 0; padding: 0; font-family: Arial, sans-serif; background: #f5f5f5; }
    header { padding: 25px; background: #ffffff; box-shadow: 0px 2px 5px rgba(0,0,0,0.1); text-align: center; }
    h1 { margin: 0; font-size: 30px; color: #333; }
    .container { max-width: 720px; margin: 40px auto; background: white; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); }
    .input-group { display: flex; gap: 10px; }
    input { flex: 1; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; }
    button { padding: 12px 18px; background: #4caf50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: bold; }
    button:hover { background: #43a047; }
    .video-card { margin-top: 25px; display: flex; gap: 15px; background: #fff; padding: 15px; border-radius: 10px; box-shadow: 0px 2px 8px rgba(0,0,0,0.1); }
    .video-card img { width: 160px; height: auto; border-radius: 6px; }
    table { width: 100%; margin-top: 20px; border-collapse: collapse; }
    table th, table td { border: 1px solid #eee; padding: 12px; text-align: left; }
    table th { background: #fafafa; }
    .download-btn { background: #1e88e5; padding: 8px 14px; color: white; border-radius: 6px; text-decoration: none; }
    .download-btn:hover { background: #1565c0; }
    #progressBar { position: fixed; top: 0; left: 0; height: 4px; width: 0%; background: #4caf50; z-index: 9999; transition: width 0.3s ease; }
  </style>
</head>
<body>
  <div id="progressBar"></div>
  <header>
    <h1>YouTube Downloader</h1>
  </header>
  <div class="container">
    <div class="input-group">
      <input type="text" id="videoURL" placeholder="Enter YouTube Video URL...">
      <button id="getInfoBtn">Get Download Options</button>
    </div>
    <div id="videoInfo" style="display: none;">
      <div class="video-card">
        <img id="thumbnail" src="" alt="Video Thumbnail">
        <div>
          <h2 id="videoTitle"></h2>
        </div>
      </div>
      <table>
        <thead>
          <tr>
            <th>Quality</th>
            <th>Format</th>
            <th>Size</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody id="formatsTable"></tbody>
      </table>
      <a id="mp3Download" class="download-btn" style="margin-top: 10px; display: inline-block;" href="#">Download MP3</a>
    </div>
  </div>

  <script>
    document.getElementById('getInfoBtn').addEventListener('click', async () => {
      const url = document.getElementById('videoURL').value;
      if (!url) return alert('Please enter a URL');
      
      const res = await fetch('/video-info', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ url })
      });
      const data = await res.json();
      
      if (!data.success) return alert(data.error);
      
      document.getElementById('videoTitle').textContent = data.title;
      document.getElementById('thumbnail').src = data.thumbnail;
      document.getElementById('mp3Download').href = `/download?url=${url}&itag=${data.mp3.itag}&title=${data.title}`;
      
      const tableBody = document.getElementById('formatsTable');
      tableBody.innerHTML = '';
      data.formats.forEach(format => {
        const row = document.createElement('tr');
        row.innerHTML = `
          <td>${format.quality}</td>
          <td>${format.ext}</td>
          <td>${format.size ? (format.size / 1024 / 1024).toFixed(2) + 'MB' : 'Unknown'}</td>
          <td><a href="/download?url=${url}&itag=${format.itag}&title=${data.title}" class="download-btn">Download</a></td>
        `;
        tableBody.appendChild(row);
      });
      
      document.getElementById('videoInfo').style.display = 'block';
    });
  </script>
</body>
</html>

问题根源分析

你现在拿不到720p/1080p高清格式的核心原因是YouTube的高清视频格式大多是音视频分离的——也就是一个格式只有视频流(vcodec有值,acodec为"none"),另一个格式只有音频流(acodec有值,vcodec为"none");而360p及以下的低清格式才会是音视频合并在同一个文件里的。

看你Youtube.js里的代码,在筛选视频格式时写了:

if (f.ext === "mp4" && f.vcodec !== "none" && f.acodec !== "none") {
  // 加入formats数组
}

这个条件直接把所有纯视频的高清格式给过滤掉了,导致前端只能看到音视频合并的低清格式。

解决方案

只需要调整/video-info路由里的格式筛选逻辑,把纯视频的高清格式也加入到formats数组中即可:

修改Youtube.js中的格式筛选代码

把原来的"ONLY MP4 VIDEO QUALITIES"部分替换成下面的代码:

// 包含所有MP4视频格式(包括纯视频的高清格式)
if (f.ext === "mp4" && f.vcodec !== "none") {
  formats.push({
    itag: f.format_id,
    quality: f.format_note || f.resolution,
    ext: "mp4",
    size: f.filesize || null,
    url: f.url
  });
}

为什么这样改就可以?

  1. 去掉了f.acodec !== "none"的限制,纯视频的高清格式(比如1080p的mp4视频流)就会被加入到formats数组中,前端就能看到这些选项。
  2. 你的下载路由其实已经写对了合并逻辑:-f ${itag}+bestaudio,当用户选择高清纯视频的itag时,yt-dlp会自动把这个视频流和最好的音频流通过FFmpeg合并成一个完整的mp4文件,完全不用你额外写合并代码。

额外注意点

  1. 确保你的yt-dlp是最新版本,因为YouTube的格式规则可能会更新,旧版本的yt-dlp可能无法识别最新的高清格式。
  2. 前端表格渲染时,纯视频格式的size可能为null,你可以在前端做兼容,显示"未知"或者计算合并后的预估大小(不过yt-dlp下载时会自动处理,不影响使用)。

这样修改后,你应该就能在前端看到720p、1080p的下载选项,点击下载后也能得到完整的高清视频文件了!

火山引擎 最新活动