Node.js Express中ytdl-core流转Blob及播放列表音频下载问题
问题分析与解决方案
错误根源
你遇到的TypeError: source.on is not a function主要有三个原因:
- 浏览器API误用:
new Response().blob()是浏览器专属API,Node.js环境不支持,导致返回的不是有效Blob对象。 - 异步循环失效:
forEach(async song => {})不会等待每个异步操作完成,导致表单还未填充数据就提前发送了响应。 - 数据类型不兼容:Node.js版
form-data不接受浏览器Blob对象,它仅支持流、Buffer或字符串类型。
推荐解决方案:直接用流填充form-data(内存友好)
批量下载时转Blob会占用大量内存,直接使用ytdl返回的流是最优方案:
1. 修改音频获取函数(返回流而非Blob)
const getAudioAsStream = (videoId) => { return ytdl(videoId, { quality: "highestaudio", filter: "audioonly", }); };
2. 修复路由逻辑(等待所有流添加完成并流式响应)
app.post("/", async (req, res) => { console.log(`Request: ${req.url}`); const form = new FormData(); try { const playlist = await getListOfSongsFromPlaylist(req.body.url); console.log('Playlist name: ' + playlist.title); console.log('Total songs in playlist: ' + playlist.estimatedItemCount); // 用Promise.all等待所有流添加到表单 await Promise.all(playlist.items.map(async (song) => { const audioStream = getAudioAsStream(song.id); form.append(song.title, audioStream, { filename: `${song.title}.mp3`, contentType: 'audio/mpeg', }); })); // 设置响应头 res.setHeader('Content-Type', `multipart/form-data; boundary=${form.getBoundary()}`); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Origin', '*'); // 直接将表单流管道到响应,避免内存溢出 form.pipe(res); } catch (err) { console.error(err); res.status(500).send('下载失败'); } });
备选方案:转Blob后处理(不推荐批量场景)
如果确实需要Blob格式,可在Node.js中将流转换为Buffer再转Blob:
1. 新增流转Buffer工具函数
const streamToBuffer = (stream) => { return new Promise((resolve, reject) => { const chunks = []; stream.on('data', chunk => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks))); stream.on('error', reject); }); };
2. 修改音频转Blob函数
const getAudioAsBlob = async (videoId) => { const stream = ytdl(videoId, { quality: "highestaudio", filter: "audioonly", }); const buffer = await streamToBuffer(stream); return new Blob([buffer], { type: 'audio/mpeg' }); };
3. 修复路由中的表单填充逻辑
app.post("/", async (req, res) => { console.log(`Request: ${req.url}`); const form = new FormData(); try { const playlist = await getListOfSongsFromPlaylist(req.body.url); console.log('Playlist name: ' + playlist.title); console.log('Total songs in playlist: ' + playlist.estimatedItemCount); await Promise.all(playlist.items.map(async (song) => { const audioAsBlob = await getAudioAsBlob(song.id); // Node.js Blob需转成Buffer才能被form-data识别 form.append(song.title, Buffer.from(await audioAsBlob.arrayBuffer()), { filename: `${song.title}.mp3`, contentType: 'audio/mpeg', knownLength: audioAsBlob.size, }); })); res.writeHead(200, { 'Content-Type': `multipart/form-data; boundary=${form.getBoundary()}`, 'Content-Length': form.getLengthSync(), 'Access-Control-Allow-Credentials': 'true', 'Access-Control-Allow-Origin': '*', }); form.pipe(res); } catch (err) { console.error(err); res.status(500).send('下载失败'); } });
内容的提问来源于stack exchange,提问作者Rob Gravelle
相关产品推荐
相关产品推荐

