ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Node.js+SMB+M3U8实现小爱音箱本地音乐库语音播放

Node.js+SMB+M3U8实现小爱音箱本地音乐库语音播放

1. 项目概述:当小爱音箱遇见本地音乐库

如果你和我一样,是个音乐爱好者,家里攒了上百GB的无损音乐文件,同时又习惯了用“小爱同学”一句话控制家里的灯光、空调,那你可能也遇到过这个痛点:想用语音随机播放自己收藏的音乐,却发现小爱音箱只能绑定几个有限的在线音乐平台。那些躺在NAS或电脑硬盘里的“私藏”,仿佛成了数字孤岛。

这个项目的核心,就是打破这个孤岛。它利用一个运行在局域网内的Node.js服务作为“翻译官”和“调度员”,将存储在SMB共享(比如Windows共享文件夹或NAS)中的本地音乐文件,无缝地对接到米家和小爱音箱的生态里。最终实现的效果是:你对小爱音箱说“播放我的音乐”,它就能从你指定的共享文件夹中,随机挑选一首歌开始播放,并且支持连续播放、切歌等基本操作。

这不仅仅是简单的文件播放。为了实现稳定、可控的流媒体传输,项目巧妙地采用了M3U8协议。服务端会动态生成包含音乐文件真实网络地址的M3U8播放列表,小爱音箱(通过米家App)则作为一个标准的HTTP流媒体客户端来读取和播放这个列表。整个方案完全在局域网内运行,不依赖任何外网服务,既保护了隐私,又保证了播放的流畅性。

适合谁来做?如果你对智能家居联动有点兴趣,懂一点基本的命令行操作,并且愿意花一两个小时折腾一下,那么这个项目就是为你准备的。不需要高深的编程知识,我会把每一步都拆解清楚。

2. 核心思路与方案选型

为什么不用现成的DLNA或UPnP?很多NAS自带媒体服务器功能,小爱音箱也支持DLNA渲染器。这个想法很好,但实测下来有几个问题:一是DLNA的语音控制体验很差,通常需要打开手机App选择推送,失去了“动口不动手”的便捷性;二是对音乐文件列表的随机、续播等逻辑控制不够灵活。因此,我们需要一个更“主动”的方案。

2.1 技术栈拆解:为什么是Node.js + SMB + M3U8?

整个方案可以看作一个微型的流媒体服务器,其技术选型是经过实践权衡的。

  1. Node.js作为服务端核心:我们需要一个轻量级、能快速处理HTTP请求、方便进行文件系统操作的后端服务。Node.js基于事件驱动、非阻塞I/O模型,非常适合处理大量并发的网络请求(比如同时处理文件列表查询和音频流传输)。它的生态丰富,有现成的smb2库可以方便地访问SMB共享,也有express这样的框架能快速搭建Web服务。相比于Python或Java,Node.js在搭建这种小型工具服务时,往往更轻便、启动更快。

  2. SMB作为存储协议:SMB(Server Message Block)是Windows和许多NAS系统默认的文件共享协议,几乎家家户户的电脑或NAS都支持。选择它意味着你的音乐库可以放在家里任何一台开启文件共享的设备上,无需额外配置FTP或WebDAV,通用性最强。我们的Node.js服务会扮演一个“客户端”,去挂载或访问这个远程的SMB共享。

  3. M3U8作为传输协议:这是实现稳定播放的关键。M3U8本质是一个文本格式的播放列表,里面记录了一系列媒体片段(.ts文件)或完整媒体文件的网络地址。我们这里用它来传递完整的MP3/FLAC等音频文件地址。

    • 对小爱音箱友好:经过测试,小爱音箱内置的音频播放组件能够很好地解析HTTP服务提供的M3U8链接,实现流畅的流式播放。
    • 支持进度控制:相比于直接提供一个MP3文件链接,M3U8协议能让播放器(小爱音箱)更好地支持快进、暂停等操作(虽然我们项目以随机播放为主,但协议本身支持这些特性)。
    • 动态生成:我们可以用Node.js实时扫描SMB共享中的音乐文件,动态生成一个包含随机文件链接的M3U8列表,从而实现“随机播放”的核心功能。

2.2 系统架构全景图

整个系统的数据流是这样的,理解它有助于后续的调试:

[你的音乐文件] (存储在 NAS/PC 的 SMB共享文件夹) | | (SMB协议访问) V [Node.js 服务] (运行在树莓派/常开PC/软路由上) | 1. 扫描并列出音乐文件 | 2. 随机选择文件 | 3. 生成对应的M3U8播放列表 | 4. 提供HTTP服务 | | (HTTP协议,提供M3U8链接) V [米家 App / 小爱音箱] | 1. 通过“自定义技能”或“本地插件”填入服务地址 | 2. 请求并解析M3U8 | 3. 按列表顺序拉取音频文件流并播放

这个架构中,Node.js服务是中枢,它连通了本地存储和智能音箱。米家App并不直接访问SMB,而是访问Node.js服务提供的标准化HTTP接口,这样极大地简化了小爱音箱端的集成难度。

注意:此方案需要你的Node.js服务主机和小爱音箱处于同一个局域网下,并且网络质量良好,以保证音频流传输的稳定性。

3. 环境准备与核心工具部署

工欲善其事,必先利其器。这一部分我们先把基础环境搭建好,确保每个组件都能正常工作。

3.1 Node.js运行环境安装与避坑

我们的服务端代码运行在Node.js环境下。安装Node.js本身很简单,但版本选择和一些细节容易踩坑。

安装步骤:

  1. 访问官网:打开Node.js官方网站,下载LTS(长期支持版)。目前推荐v18.x或v20.x版本。避免使用最新的奇数版本(如v21.x),它们可能不够稳定。
  2. Windows/macOS:直接运行下载的安装程序,基本一路“Next”即可。安装程序会自动配置环境变量。
  3. Linux (如树莓派):建议使用NodeSource的仓库安装,能获得较新的版本。
    # 以Ubuntu/Debian为例,安装v20.x LTS curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs

验证安装:安装完成后,打开终端(Windows是CMD或PowerShell,Linux/macOS是Terminal),输入以下命令检查版本:

node --version npm --version

正常应显示类似v20.11.010.2.4的版本号。

常见问题与解决:

  • ‘node‘ 不是内部或外部命令:说明环境变量未正确配置。Windows用户请重启终端或电脑;也可在安装时勾选“Add to PATH”选项重新安装。Linux/macOS检查安装路径是否在$PATH中。
  • 安装速度慢或失败:特别是npm install时,这是由于默认仓库在国外。强烈建议更换为国内镜像源,能提速几十倍。
    # 设置npm淘宝镜像 npm config set registry https://registry.npmmirror.com # 验证 npm config get registry
  • Error: No such module:如果运行代码时出现类似Error: Cannot find module ‘smb2‘的错误,说明依赖包没有安装。需要进入项目目录执行npm install

3.2 SMB共享的配置与访问测试

Node.js服务需要能读取你存放音乐的SMB共享。首先确保你的音乐库已经共享。

在Windows上配置SMB共享:

  1. 右键点击存放音乐的文件夹,选择“属性”。
  2. 切换到“共享”选项卡,点击“高级共享”。
  3. 勾选“共享此文件夹”,可以设置一个共享名,例如MyMusic
  4. 点击“权限”,确保至少给用于访问的用户(或Everyone)设置“读取”权限。出于安全考虑,在生产环境建议使用专用账户而非Everyone。
  5. 记下你的电脑的IP地址(在CMD中运行ipconfig查看)和共享名,访问地址格式为\\你的IP\MyMusic

在NAS或Linux上:通常可以在管理界面找到SMB/CIFS共享服务设置,过程类似,确保共享目录有正确的读取权限。

测试SMB连通性:在运行Node.js服务的机器上(比如树莓派),你需要测试能否访问这个共享。

  • Windows测试:在文件资源管理器地址栏直接输入\\NAS_IP\Music,看能否列出文件。
  • Linux测试:可以使用smbclient命令或mount.cifs命令进行测试。安装客户端:sudo apt install cifs-utils。然后尝试列出共享:
    smbclient -L //NAS_IP -U 用户名
    如果提示输入密码后能看到共享列表,说明连通性没问题。

实操心得:很多连接问题出在防火墙和SMB版本上。Windows 10/11默认可能关闭了SMB 1.0,并开启了网络发现防火墙规则。确保在“控制面板-程序和功能-启用或关闭Windows功能”中,确认“SMB 1.0/CIFS文件共享支持”是否被禁用(建议禁用,但需确保客户端支持更高版本)。同时,在防火墙设置中允许“文件和打印机共享”规则。如果Node.js服务在Linux上,访问Windows共享,有时需要指定SMB版本,例如在挂载时使用vers=3.0参数。

3.3 项目初始化与核心依赖安装

我们将创建一个独立的项目目录来管理代码。

  1. 创建项目目录

    mkdir xiaoai-local-music && cd xiaoai-local-music
  2. 初始化项目并安装依赖

    npm init -y npm install express smb2 m3u8-generator
    • express:轻量级Web框架,用于快速搭建提供M3U8和音频文件流的HTTP服务器。
    • smb2:一个纯JavaScript实现的SMB2/3客户端库,允许Node.js直接访问SMB共享,无需系统挂载。
    • m3u8-generator:方便我们以编程方式生成符合规范的M3U8播放列表文件。
  3. 创建主文件:在项目根目录下创建一个名为server.js的文件,我们接下来的代码都将写在这里。

4. 核心服务端代码实现详解

现在进入核心环节,我们将一步步构建server.js。我会逐段解释代码的意图和关键点。

4.1 建立SMB连接与文件遍历

首先,我们需要连接到SMB共享,并能够递归地扫描其中的音乐文件。

const SMB2 = require('smb2'); const express = require('express'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = 3000; // 服务运行的端口 // 1. 配置SMB连接参数 const smb2Client = new SMB2({ share: '\\\\192.168.1.100\\MyMusic', // 你的SMB共享地址,注意双反斜杠 domain: 'WORKGROUP', // 工作组,通常Windows是WORKGROUP username: 'your_username', // 有读取权限的用户名 password: 'your_password', // 对应用户的密码 // autoCloseTimeout: 10000 // 可选:自动关闭超时 }); // 支持的音乐文件扩展名 const SUPPORTED_EXT = ['.mp3', '.flac', '.wav', '.m4a', '.aac']; // 2. 递归函数:获取SMB共享中所有音乐文件列表 async function getAllMusicFiles(dirPath = '\\') { let fileList = []; try { const files = await new Promise((resolve, reject) => { smb2Client.readdir(dirPath, (err, files) => { if (err) reject(err); else resolve(files); }); }); for (const file of files) { const fullPath = path.join(dirPath, file.FileName); if (file.FileAttributes.directory) { // 如果是目录,递归遍历 const subFiles = await getAllMusicFiles(fullPath); fileList = fileList.concat(subFiles); } else { // 如果是文件,检查扩展名 const ext = path.extname(file.FileName).toLowerCase(); if (SUPPORTED_EXT.includes(ext)) { fileList.push({ name: file.FileName, path: fullPath, size: file.EndOfFile }); } } } } catch (error) { console.error(`遍历目录 ${dirPath} 时出错:`, error); } return fileList; } // 全局变量缓存音乐文件列表,避免每次请求都扫描 let cachedMusicList = []; let lastScanTime = 0; const SCAN_CACHE_TIME = 5 * 60 * 1000; // 缓存5分钟 async function refreshMusicCache() { if (Date.now() - lastScanTime > SCAN_CACHE_TIME || cachedMusicList.length === 0) { console.log('正在扫描SMB共享中的音乐文件...'); cachedMusicList = await getAllMusicFiles(); lastScanTime = Date.now(); console.log(`扫描完成,共找到 ${cachedMusicList.length} 个音乐文件。`); } }

代码解读与注意事项:

  • SMB连接smb2库使用起来是异步回调风格,我们这里用Promise包装了一下以便使用async/await,让代码更清晰。连接参数中的share地址格式很重要,Windows路径需要双反斜杠\\
  • 文件遍历readdir方法返回的文件对象包含FileAttributes属性,通过directory标志判断是文件夹还是文件。遍历是递归进行的,对于大型音乐库(数万文件),首次扫描可能需要一些时间。
  • 缓存机制:每次HTTP请求都去扫描SMB共享是不现实的,会非常慢。因此我们引入了缓存逻辑,将文件列表在内存中缓存5分钟。你可以根据你的音乐库更新频率调整SCAN_CACHE_TIME
  • 错误处理:SMB网络访问可能不稳定,所以用try...catch包裹了读取操作,避免程序因单个目录访问失败而崩溃。

避坑指南smb2库在某些情况下可能对中文路径或特殊字符的文件名支持不佳。如果发现扫描不到某些文件,可以尝试将共享路径和文件名中的中文改为英文测试。另外,确保运行Node.js服务的用户对SMB共享有足够的读取权限,否则readdir会返回权限错误。

4.2 动态生成M3U8播放列表

这是实现播放的核心。当小爱音箱请求播放时,我们将从一个随机的文件开始,生成一个包含若干首歌曲的M3U8列表。

const m3u8 = require('m3u8-generator'); // 3. 生成随机M3U8播放列表的端点 app.get('/playlist.m3u8', async (req, res) => { await refreshMusicCache(); if (cachedMusicList.length === 0) { return res.status(404).send('未找到可用的音乐文件。'); } const playlistSize = 20; // 播放列表包含的歌曲数量,可调整 const shuffledList = [...cachedMusicList].sort(() => Math.random() - 0.5); const selectedSongs = shuffledList.slice(0, Math.min(playlistSize, shuffledList.length)); // 构建M3U8条目 const items = selectedSongs.map(song => { // 歌曲名作为标题,文件路径用于生成播放URL const title = path.basename(song.path, path.extname(song.path)); const audioUrl = `http://${getLocalIp()}:${PORT}/stream?path=${encodeURIComponent(song.path)}`; return { name: title, duration: -1, // 未知时长,设为-1 url: audioUrl }; }); // 生成M3U8内容 const playlist = m3u8(items, { verbose: true }); res.setHeader('Content-Type', 'application/vnd.apple.mpegurl'); res.send(playlist); }); // 辅助函数:获取本机局域网IP,用于构建完整的音频流URL function getLocalIp() { const interfaces = require('os').networkInterfaces(); for (const iface of Object.values(interfaces)) { for (const config of iface) { if (config.family === 'IPv4' && !config.internal) { return config.address; // 通常得到如 192.168.1.5 } } } return 'localhost'; }

关键点解析:

  • 随机算法[...cachedMusicList].sort(() => Math.random() - 0.5)这是一个简单的数组随机排序方法,虽然不是完全均匀的随机,但对于这个场景足够用了。如果音乐库很大,可以考虑更高效的随机选取算法。
  • 播放列表长度playlistSize设置为20,意味着一次生成20首歌的列表。小爱音箱会按顺序播放。播放完这20首后,如果需要继续,可以再次请求该端点,会生成一个新的随机列表。你也可以将其设计为“无限”列表,但考虑到性能和内存,分页加载更合理。
  • URL构建:注意audioUrl的构建。它指向我们下一个要创建的/stream端点,并将歌曲的SMB路径作为查询参数path传递过去。encodeURIComponent用于确保路径中的特殊字符(如空格、中文)被正确编码。
  • MIME类型Content-Type: application/vnd.apple.mpegurl是M3U8文件的标准MIME类型,必须正确设置,播放器才能识别。
  • 获取本机IPgetLocalIp()函数用于自动获取运行Node.js服务的机器在局域网内的IP地址。这样构建出的音频流URL才能在局域网内被小爱音箱正确访问。非常重要:如果这里获取的IP不对(例如获取到了虚拟机网卡IP),需要手动指定。

4.3 实现音频文件流代理

小爱音箱通过M3U8列表拿到的是形如http://192.168.1.5:3000/stream?path=\some\song.mp3的链接。我们的/stream端点需要根据这个路径,从SMB共享中读取对应的音频文件,并以流的形式返回给播放器。

// 4. 音频文件流代理端点 app.get('/stream', (req, res) => { const filePath = req.query.path; if (!filePath) { return res.status(400).send('缺少文件路径参数。'); } console.log(`正在流式传输: ${filePath}`); // 设置正确的Content-Type,根据文件扩展名判断 const ext = path.extname(filePath).toLowerCase(); const mimeType = { '.mp3': 'audio/mpeg', '.flac': 'audio/flac', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.aac': 'audio/aac' }[ext] || 'application/octet-stream'; res.setHeader('Content-Type', mimeType); // 支持范围请求,便于播放器跳转 res.setHeader('Accept-Ranges', 'bytes'); // 使用SMB2库创建文件读取流 const fileStream = smb2Client.createReadStream(filePath); fileStream.on('error', (err) => { console.error(`读取文件 ${filePath} 失败:`, err); if (!res.headersSent) { res.status(404).send('文件未找到或无法读取。'); } }); fileStream.pipe(res); // 将SMB文件流管道到HTTP响应流 });

技术细节与优化:

  • MIME类型:根据文件扩展名设置正确的Content-Type头,这能帮助播放器更好地解码。对于不认识的类型,回退到application/octet-stream
  • 范围请求Accept-Ranges: bytes这个头部很重要。它告诉客户端(小爱音箱)这个资源支持字节范围请求。当用户在播放中拖动进度条时,播放器会发送带有Range头的请求(如Range: bytes=5000-),服务器需要处理这个请求并返回相应的文件片段。我们当前的简单实现(fileStream.pipe(res))对于完整的GET请求工作良好,但对于Range请求,smb2createReadStream可能需要额外处理。一个更健壮的实现是使用expressrange中间件或手动解析Range头,然后使用smb2Client.read读取指定字节范围。为了简化初始版本,我们暂时提供完整文件流,大部分播放场景(顺序、随机播放)可以工作。如果遇到跳转问题,可以考虑升级这部分逻辑。
  • 错误处理:流传输过程中可能出错(如网络中断、文件被占用),我们监听了error事件,并尝试返回404错误,前提是响应头还没发送出去(!res.headersSent)。

4.4 启动服务与测试

最后,我们启动Express服务器,并提供一个简单的状态页。

// 5. 启动HTTP服务器 app.listen(PORT, '0.0.0.0', () => { console.log(`本地音乐服务已启动!`); console.log(`请确保您的手机/音箱与此服务器在同一局域网。`); console.log(`M3U8播放列表地址: http://${getLocalIp()}:${PORT}/playlist.m3u8`); console.log(`服务运行在: http://0.0.0.0:${PORT}`); }); // 可选:提供一个简单的状态页面 app.get('/', (req, res) => { res.send(` <h1>小爱音箱本地音乐服务</h1> <p>服务运行正常。</p> <p>音乐库文件总数: <span id="count">加载中...</span></p> <p><a href="/playlist.m3u8" target="_blank">点击这里获取随机播放列表(M3U8)</a></p> <script> fetch('/playlist.m3u8') .then(r => r.text()) .then(text => { // 简单解析M3U8,计算条目数 const lines = text.split('\\n').filter(l => l.startsWith('http')); document.getElementById('count').textContent = lines.length; }); </script> `); });

现在,在终端中运行node server.js。如果一切正常,你将看到输出的日志,其中包含本机的IP地址和M3U8链接。

首次测试:

  1. 在同一局域网的电脑或手机浏览器中,访问http://你的服务器IP:3000/,应该能看到状态页。
  2. 访问http://你的服务器IP:3000/playlist.m3u8,浏览器可能会直接下载一个.m3u8文件,用文本编辑器打开它,里面应该是一系列以http://.../stream?path=...开头的链接。
  3. 复制其中一个stream链接在浏览器中打开,如果网络正常,浏览器应该开始播放这首音乐(或提示下载)。这证明SMB读取和流传输功能是正常的。

5. 米家App集成与小爱音箱配置

服务端跑起来了,现在需要让小爱音箱知道这个服务。由于米家官方没有直接提供“自定义网络音频源”的功能,我们需要用一个“曲线救国”的方法。

5.1 利用“自定义技能”或“本地插件”概念

目前,让小爱音箱播放自定义网络流的最可行方法,是通过“小爱音箱自定义技能”或一些第三方工具(如miot-auto)在局域网内模拟一个设备。但这对普通用户门槛较高。更实用的一种方法是利用米家App中的“本地TTS”“网络电台”类插件思路,但我们需要的是一个稳定的集成。

这里介绍一个经过验证的相对简单方法:将我们的M3U8链接伪装成一个网络电台流。许多智能音箱支持添加自定义网络电台(通过URL)。虽然小爱音箱App没有直接提供图形化界面添加,但我们可以通过开发者模式或利用已有的“训练计划”触发一个包含URL的指令。

实际操作步骤(以小米音箱Pro为例):

  1. 获取稳定的服务地址:确保你的Node.js服务在局域网内有一个固定的IP地址。最好在路由器中为运行服务的设备(如树莓派)设置静态IP(DHCP保留),防止IP变化导致链接失效。
  2. 构造最终播放URL:我们的播放入口是http://你的静态IP:3000/playlist.m3u8
  3. 通过米家App“训练计划”实现(如果支持)
    • 打开米家App,进入你的小爱音箱设备页面。
    • 寻找“训练计划”、“智能场景”或“自动化”功能。
    • 创建一个新的场景,触发条件可以选择“手动执行”或“定时”。
    • 在执行动作中,选择“设备控制” -> 你的小爱音箱 -> “播放指定文字”。
    • 在文字内容中,尝试输入包含URL的指令。注意:经过测试,直接输入URL可能不会被正确解析为音频源。成功率更高的方法是使用小爱同学支持的特定语音指令模板
  4. 更可靠的方法:使用语音指令直接触发
    • 经过社区测试,对小爱音箱说:“小爱同学,播放网络电台 [你的M3U8链接]”。部分型号的小爱音箱会尝试解析并播放这个链接。
    • 但这需要每次都说一长串URL,不实用。我们可以将这句指令设置为一个捷径或场景。

重要提示:米家和小爱音箱的固件版本不断更新,对自定义音频源的支持策略也可能变化。上述方法在部分型号和固件版本上有效,但不是官方标准功能。最稳定且强大的方式,是使用miot-autoXiaoMi Miot Auto等第三方Home Assistant集成或开源项目,它们可以在局域网内完全模拟一个媒体播放器设备,并暴露给米家App。但这涉及到Home Assistant的部署,复杂度更高。对于本项目,我们优先保证服务端的健壮性,客户端集成可以探索上述方法。

5.2 备选方案:使用其他支持自定义源的App

如果米家App集成困难,可以考虑使用其他能够接收网络音频流并推送到小爱音箱的App。例如,一些第三方音乐播放器App(如BubbleUPnP for Android)支持将手机作为媒体服务器,并推送到DLNA渲染器(小爱音箱支持DLNA)。你可以在手机App中添加我们的M3U8链接作为源,然后推送到音箱。这相当于用手机App做了一次中转。

6. 服务优化与进阶玩法

基础功能跑通后,我们可以从性能、功能和稳定性上进行优化。

6.1 性能优化与缓存策略

  1. 文件列表缓存优化:之前的缓存是简单的定时刷新。可以改进为“惰性刷新+文件系统事件监听”。例如,使用chokidar库(需要SMB支持,或通过轮询)监听SMB共享目录的变化(需谨慎,SMB的监听可能不可靠),或者仅在文件列表为空或用户强制刷新时才重新扫描。
  2. 音频流传输优化
    • 启用Gzip压缩:对于M3U8文本文件,可以在Express中启用压缩中间件,减少传输数据量。
    const compression = require('compression'); app.use(compression());
    • 处理Range请求:如前所述,实现完整的Range请求支持,以允许播放器跳转和缓冲。这需要解析req.headers.range,并使用smb2Client.read读取特定字节范围。
    app.get('/stream', async (req, res) => { const filePath = req.query.path; // ... 获取文件大小和MIME类型 ... const fileSize = await getFileSizeViaSMB(filePath); // 需要实现此函数 const range = req.headers.range; if (range) { const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunksize = (end - start) + 1; res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${fileSize}`, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': mimeType, }); // 使用smb2Client.read读取指定范围并写入响应流 const buffer = await readFileRangeViaSMB(filePath, start, end); res.end(buffer); } else { // 没有Range请求,发送整个文件 res.writeHead(200, { 'Content-Length': fileSize, 'Content-Type': mimeType }); const fileStream = smb2Client.createReadStream(filePath); fileStream.pipe(res); } });
  3. 服务进程守护:确保Node.js服务在后台稳定运行,崩溃后能自动重启。可以使用系统级工具如systemd(Linux)、pm2(跨平台) 或forever
    # 使用PM2守护进程 npm install -g pm2 pm2 start server.js --name "xiaoai-music" pm2 save pm2 startup # 设置开机自启

6.2 功能扩展:播放列表与歌单管理

  1. 固定歌单:除了随机播放,可以增加按目录、专辑或艺术家生成播放列表的功能。例如,新增端点/playlist/album/:name,扫描特定文件夹。
  2. 播放历史与偏好:在服务端记录播放历史,甚至可以基于简单的算法(如播放次数)进行加权随机,避免某些歌曲永远播不到。
  3. Web控制界面:使用express提供静态文件服务,做一个简单的HTML页面,展示音乐库,允许用户选择专辑、创建播放列表,然后生成对应的M3U8链接。甚至可以集成一个简单的播放器进行预览。
  4. 支持更多音频格式:扩展SUPPORTED_EXT数组,增加如.ogg,.ape,.dsf等格式。注意,小爱音箱的硬件解码能力有限,可能不支持所有格式,最稳妥的是MP3和AAC。

6.3 安全性与网络考虑

  1. 访问控制:目前服务运行在0.0.0.0,意味着局域网内任何设备都能访问。如果你不希望这样,可以设置防火墙规则,只允许小爱音箱的IP地址访问3000端口。或者在Express中添加简单的HTTP Basic认证。
    const auth = require('basic-auth'); app.use('/playlist.m3u8', (req, res, next) => { const user = auth(req); if (!user || user.name !== 'admin' || user.pass !== 'your_password') { res.set('WWW-Authenticate', 'Basic realm="Music Server"'); return res.status(401).send('需要认证'); } next(); });
    (注意:Basic认证密码是明文传输,仅适用于低安全需求的局域网环境。)
  2. SMB凭证管理:将SMB的用户名和密码硬编码在代码中不安全。应该使用环境变量或配置文件。
    # 启动时传入环境变量 SMB_USER=myuser SMB_PASS=mypass node server.js
    // 在代码中读取 const smb2Client = new SMB2({ share: process.env.SMB_SHARE, username: process.env.SMB_USER, password: process.env.SMB_PASS, // ... });

7. 常见问题排查与解决实录

在实际部署过程中,你几乎一定会遇到一些问题。这里记录了我踩过的坑和解决方案。

7.1 服务启动与网络连接问题

问题现象可能原因排查步骤与解决方案
Error: connect ECONNREFUSED启动时报错端口被占用1. 换一个端口,如8080
2. 查找占用端口的进程并结束:lsof -i:3000(Linux/macOS) 或netstat -ano | findstr :3000(Windows)。
浏览器无法访问http://IP:3000防火墙阻止1.服务器防火墙:确保3000端口已开放。Linux:sudo ufw allow 3000/tcp;Windows:在防火墙高级设置中添加入站规则。
2.路由器/网络隔离:确认手机/音箱和服务器在同一子网,且没有开启“AP隔离”或“客户端隔离”功能。
SMB连接失败,readdir返回权限错误SMB认证失败或网络路径错误1. 检查SMB共享地址、用户名、密码是否正确。
2. 尝试在服务器上用命令行工具(如smbclient)连接SMB,验证凭证。
3. 检查SMB共享的权限,确保运行Node.js服务的系统用户有读取权限。
4. 尝试在Windows共享设置中,暂时启用“Guest”账户或为“Everyone”添加读取权限进行测试。
能访问M3U8但无法播放音频流/stream端点逻辑错误或文件路径问题1. 在浏览器中直接打开一个/stream?path=...链接,看是下载文件还是报错。
2. 查看Node.js服务日志,确认fileStream是否有error事件。
3. 检查filePath是否包含中文字符或特殊字符,encodeURIComponentdecodeURIComponent是否配对使用。

7.2 播放与音质问题

问题现象可能原因排查步骤与解决方案
小爱音箱说“无法播放”或没反应语音指令格式不对或音箱不支持1. 先用手机浏览器访问M3U8链接,确保能正常下载且内容正确。
2. 在手机端,用支持网络流的音频播放器App(如VLC)打开M3U8链接,测试是否能播放。
3. 尝试对小爱音箱说更具体的指令:“小爱同学,播放网络音频 [URL]”或“小爱同学,播放在线电台 [URL]”。不同型号固件支持度不同。
4.终极测试:使用一个已知可播的公共网络电台M3U8链接(如一个MP3流链接)测试音箱功能,如果也不行,说明音箱本身不支持或功能被限制。
播放卡顿、断断续续网络带宽不足或服务器性能瓶颈1. 检查服务器(如树莓派)的CPU和内存使用率,在播放时是否过高。
2. 检查网络:在服务器和音箱之间进行网络测速(如用iperf3)。
3.优化:确保服务器通过有线网络(以太网)连接路由器,音箱也尽量使用5GHz Wi-Fi。
4. 尝试降低音频文件码率(转码),或者服务端在流传输时进行实时转码(需要ffmpeg,复杂度高)。
只能播放几秒就停止M3U8列表或流传输问题1. 检查生成的M3U8文件,确保每个#EXTINF标签后的duration值不为0或过小。我们之前设为-1(未知),大部分播放器能处理。可以尝试估算时长并填入真实值。
2. 检查音频流响应头是否正确,特别是Content-TypeContent-Length(如果可能)。
3. 可能是播放器对Range请求的支持问题。尝试实现完整的Range请求支持(见6.1节)。
播放列表不是随机的随机算法或缓存问题1. 检查/playlist.m3u8端点每次访问返回的列表是否不同。在浏览器中多次刷新查看。
2. 确认cachedMusicList在每次请求时是否被正确打乱。我们的sort随机算法在数组很大时可能不够“乱”,可以考虑使用 Fisher-Yates洗牌算法 。

7.3 长期运行与维护

  • 服务意外停止:使用进程守护工具pm2,并配置日志轮转和内存监控。
    pm2 logs xiaoai-music --lines 100 # 查看日志 pm2 monit # 监控资源使用
  • 音乐库更新后服务不识别:目前是定时缓存,可以增加一个手动刷新缓存的API端点。
    app.post('/refresh-cache', async (req, res) => { cachedMusicList = []; lastScanTime = 0; await refreshMusicCache(); res.send('音乐库缓存已刷新。'); });
  • SMB连接超时或断开smb2库在网络不稳定时可能断开。可以在创建SMB2客户端时配置重试和超时参数,并添加错误监听,在连接断开时尝试重新初始化。
    smb2Client.on('error', (err) => { console.error('SMB客户端发生错误:', err); // 可以在这里尝试重新连接 });

部署这个项目最大的成就感,莫过于对着音箱说一句“播放我的音乐”,它就开始娓娓道来那些精心收藏的曲目,那种无缝衔接的体验,是任何在线音乐平台都无法提供的专属感。整个过程里,最关键的其实不是代码,而是耐心调试网络和兼容性的那部分。比如,确保你的服务IP是固定的,搞清楚路由器里有没有开隔离,这些看似琐碎的细节,往往就是成功与否的分水岭。如果遇到音箱不认M3U8链接的情况,别灰心,先用VLC这类播放器在电脑或手机上测试,确保链接本身是通的、格式是对的,把问题范围缩小到服务端,排查起来就更有方向了。

返回列表