Node.js中使用ytdl-core的chooseFormat无法下载纯视频文件
问题:ytdl-core选择格式下载纯视频时生成0字节文件
我使用Node.js和ytdl-core包下载YouTube视频,基础下载代码可正常运行:
ytdl('http://www.youtube.com/watch?v=aqz-KE-bpKQ') .pipe(fs.createWriteStream('video.mp4'));
但要获取高清视频需单独下载音视频文件再用ffmpeg合并。目前通过自定义函数调用「选择视频格式」功能下载纯视频时遇到问题:
我设置了/download端点,调用controller中的download()函数,该函数会依次执行getVideoInfo()和downloadVideo()。其中getVideoInfo()会在/public目录生成getInfo.json和videoFormat.json文件,downloadVideo()需将视频文件下载到/public目录。
JSON文件能成功生成,但下载的视频文件大小为0字节。相关代码如下:
const fs = require("fs") const path = require("path") const ytdl = require("ytdl-core") const cmd = require("node-cmd") const axios = require("axios") async function download(req, res) { const url = "https://www.youtube.com/watch?v=Qdi_yp9FA-I" const videoPath = "public/video_only.mp4" try { const videoFormat = await getVideoInfo(url) await downloadVideo(url, videoFormat, videoPath) console.log('Video is downloaded.') } catch (error) { console.error("Error in download function:", error); res.status(500).send("Internal Server Error"); } } async function getVideoInfo(url) { const info = await ytdl.getInfo(url) console.log("Title: " + info.videoDetails.title) // Save info to a JSON file fs.writeFileSync("public/getInfo.json", JSON.stringify(info, null, 2)) const videoFormat = ytdl.chooseFormat(info.formats, { quality: "137" }) // Save videoFormat to a JSON file fs.writeFileSync("public/videoFormat.json", JSON.stringify(videoFormat, null, 2)) return videoFormat } async function downloadVideo(url, videoFormat, videoPath) { try { if (!videoFormat) { reject(new Error("No suitable format found.")) return } ytdl(url, { format: videoFormat }).pipe(fs.createWriteStream(videoPath)) } catch (error) { throw new Error("Error downloading video: " + error.message) } } module.exports = { download, }
问题原因
downloadVideo是async函数,但内部的ytdl.pipe()属于流式异步操作,未被封装为Promise,导致await downloadVideo()不会等待下载完成就执行后续代码,文件流还未写入内容就被终止,生成0字节文件。downloadVideo中直接调用reject()但未处于Promise上下文,会触发未捕获的错误。
解决方案
将downloadVideo改造为返回Promise的函数,通过监听流的finish和error事件控制Promise状态,确保await能正确等待下载完成:
const fs = require("fs") const path = require("path") const ytdl = require("ytdl-core") const cmd = require("node-cmd") const axios = require("axios") async function download(req, res) { const url = "https://www.youtube.com/watch?v=Qdi_yp9FA-I" const videoPath = "public/video_only.mp4" try { const videoFormat = await getVideoInfo(url) await downloadVideo(url, videoFormat, videoPath) console.log('Video is downloaded.') // 下载完成后给客户端返回响应 res.status(200).send("Video downloaded successfully") } catch (error) { console.error("Error in download function:", error); res.status(500).send("Internal Server Error"); } } async function getVideoInfo(url) { const info = await ytdl.getInfo(url) console.log("Title: " + info.videoDetails.title) // 确保public目录存在,避免写入失败 if (!fs.existsSync("public")) { fs.mkdirSync("public") } fs.writeFileSync("public/getInfo.json", JSON.stringify(info, null, 2)) const videoFormat = ytdl.chooseFormat(info.formats, { quality: "137" }) fs.writeFileSync("public/videoFormat.json", JSON.stringify(videoFormat, null, 2)) return videoFormat } // 改造为返回Promise的函数 function downloadVideo(url, videoFormat, videoPath) { return new Promise((resolve, reject) => { if (!videoFormat) { reject(new Error("No suitable format found.")) return } const videoStream = ytdl(url, { format: videoFormat }) const writeStream = fs.createWriteStream(videoPath) videoStream.pipe(writeStream) // 监听写入完成事件 writeStream.on("finish", () => { resolve() }) // 监听流错误事件 videoStream.on("error", (err) => { reject(new Error("Error downloading video: " + err.message)) }) writeStream.on("error", (err) => { reject(new Error("Error writing video file: " + err.message)) }) }) } module.exports = { download, }
额外优化点
- 增加
public目录存在性检查,避免因目录不存在导致文件写入失败。 - 在
download函数中添加下载完成后的客户端响应,让前端感知下载状态。 - 分别监听视频流和写入流的错误事件,捕获更全面的异常。
内容的提问来源于stack exchange,提问作者Hamid
相关产品推荐
相关产品推荐

