使用nodejs 实现利用手机播放电脑声音 实时
打开手机点个外卖,找个电影,发现台式机没有外放。。。。 所以开发了这款程序,可以利用手机播放电脑声音。打开原理 使用 FFmpeg 解码出来的音频数据 PCM 格式,使用H5的 Web Audio Api 来播放听起来简单,实现起来发现没办法获取window播放设备,只能读取到输入设备~尝试多种方案,发现可以使用 directshow 源捕获设备 实现虚拟设备1.安装环境
安装nodejs 环境 :https://nodejs.org/ 安装ffmpeg并设为 目录下 bin 为全局变量安装 screen-capture-recorder-to-video-windows-free
安装后自动虚拟出 screen-capture-recorder 和 virtual-audio-capturer 设备 可通过 ffmpeg -list_devices true -f dshow -i dummy 指令查看screen-capture-recorder 获取的是电脑视频流virtual-audio-capturer 获取的是播放扬声器音频流
2.编写服务端代码 server.js
const express = require('express');
const WebSocket = require('ws');
const { spawn } = require('child_process');
const path = require('path');
const os = require('os');
// 获取当前 IP 地址
function getIpAddress() {
const networkInterfaces = os.networkInterfaces();
let ipAddress = '';
for (let interface in networkInterfaces) {
const interfaces = networkInterfaces;
for (let i = 0; i < interfaces.length; i++) {
const address = interfaces.address;
if (interfaces.family === 'IPv4' && !interfaces.internal) {
ipAddress = address;
break;
}
}
}
return ipAddress;
}
const app = express();
const host = '0.0.0.0';// 指定绑定的 IP 地址
const PORT = 3000;
const cors = require('cors');
app.use(cors({
origin: "*", // 允许所有来源,或设置为特定的IP或域名
}));
app.use(express.static('public'));
// 获取音频设备列表的 API
app.get('/audio-devices', (req, res) => {
const ffmpeg = spawn('ffmpeg', ['-list_devices', 'true', '-f', 'dshow', '-i', 'dummy']);
let output = '';
ffmpeg.stderr.on('data', (data) => {
output += data.toString();
});
ffmpeg.on('close', () => {
// 解析音频设备列表
const deviceList = [];
const regex = /"([^"]+)"/g;
let match;
while ((match = regex.exec(output)) !== null) {
deviceList.push(match);
}
res.json(deviceList);
});
});
// 返回当前 IP 地址
app.get('/get-ip', (req, res) => {
const ipAddress = getIpAddress();
res.json({ ip: ipAddress });
});
// 路由提供音频文件
app.get('/output', (req, res) => {
res.sendFile(path.join(__dirname, 'output.wav'));
});
// WebSocket 服务器,用于实时传输音频
const wss = new WebSocket.Server({ noServer: true });
wss.on('connection', (ws, req) => {
const selectedDevice = req.url.replace('/?device=', '');
// 使用用户选择的设备启动 FFmpeg 子进程捕获音频
const ffmpeg = spawn('ffmpeg', [
'-f', 'dshow',
'-i', `audio=${decodeURIComponent(selectedDevice)}`, // 使用选定的设备
'-f', 'wav',
'pipe:1'
]);
ffmpeg.stdout.on('data', (data) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(data); // 将音频数据发送到 WebSocket 客户端
}
});
ffmpeg.stderr.on('data', (data) => {
console.error(`FFmpeg error: ${data}`);
});
ws.on('close', () => {
ffmpeg.kill(); // 关闭 FFmpeg 进程
});
});
// 启动 HTTP 服务器并升级到 WebSocket
const server = app.listen(PORT,host, () => {
console.log(`Server is running on http://${getIpAddress()}:${PORT}`);
});
server.on('upgrade', (request, socket, head) => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
大致代码解析
/audio-devices接口 获取本地设备列表。
/get-ip接口 获取本机局域网IP
/output接口 输出默认音频,在ios设备需要通过用户点击 可以通过它播放,
编写websocket 接口
读取设备时发现ffmpeg 无法获取播放设备列表,于是找了各种方式最合适的方式就是使用ffmpeg官方提供的解决方案 :directshow 桌面/屏幕源捕获过滤器
3.编写前端代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Select Audio Device</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>电脑 PCM 播放器</h1>
<select id="deviceSelect"></select>
<button>读取音频</button>
<h1>当前 IP 地址:</h1>
<p id="ip-address">加载中...</p>
<audio ref="audio"src="../output" controls></audio>
<!-- <audio id="audioPlayer" controls></audio>
-->
<p>
<input type="button" id="toggle" value="防锁屏:off" />
</p>
<script type="text/javascript" src="js/NosSleep.js"></script>
<script type="text/javascript" src="js/pcm-player.js"></script>
<script>
var noSleep = new NoSleep();
var wakeLockEnabled = false;
var toggleEl = document.querySelector("#toggle");
toggleEl.addEventListener('click', function() {
if (!wakeLockEnabled) {
noSleep.enable(); // keep the screen on!
wakeLockEnabled = true;
toggleEl.value = "防锁屏:on";
document.body.style.backgroundColor = "red";
} else {
noSleep.disable(); // let the screen turn off.
wakeLockEnabled = false;
toggleEl.value = "防锁屏:off";
document.body.style.backgroundColor = "";
}
}, false);
// 加载时获取 IP 地址
async function fetchIp() {
try {
const response = await fetch('/get-ip');
const data = await response.json();
document.getElementById('ip-address').textContent = data.ip;
} catch (error) {
console.error('获取 IP 地址失败:', error);
document.getElementById('ip-address').textContent = '无法获取 IP 地址';
}
}
fetchIp();
// 加载音频设备
async function loadDevices() {
const response = await fetch('/audio-devices');
const devices = await response.json();
const deviceSelect = document.getElementById('deviceSelect');
devices.forEach(device => {
const option = document.createElement('option');
option.value = device;
option.textContent = device;
deviceSelect.appendChild(option);
});
}
const videodd = document.querySelector("video");
let ws;
// 开始音频流
function startStreaming() {
const ip = document.getElementById('ip-address').textContent;
let audioContext;
const selectedDevice = document.getElementById('deviceSelect').value;
ws = new WebSocket(`ws://${ip}:3000/?device=${encodeURIComponent(selectedDevice)}`);
console.log(`ws://${ip}:3000/?device=${encodeURIComponent(selectedDevice)}`)
ws.onopen = function () {
console.log('WebSocket connected');
};
var player = new PCMPlayer({
encoding: '16bitInt',
channels: 2,
sampleRate: 48000,
flushingTime: 0
});
ws.binaryType = 'arraybuffer';
ws.onopen = function() {
//如果是重连则关闭轮询
timeid && window.clearInterval(timeid);
if(reconnect){
alert('重连成功,部分');
}
};
ws.onmessage = function(e){
var data = new Uint8Array(e.data);
if (player.audioCtx.state === 'suspended') {
player.audioCtx.resume();// 确保 AudioContext 已被激活
}
player.feed(data);
// document.getElementById('ip-address').textContent = player
player.volume(1);
};
//当断开时进行判断
ws.onclose=function(e){
setTimeout(startStreaming, 5000);// 重新连接
}
// ws.addEventListener('message',function(event) {
// const stream = new Blob(, { type: 'audio/mp3' });
// videodd.srcObject = stream;
// // 创建 MediaStreamAudioSourceNode // 将 HTMLMediaElement 提供给它
// const audioCtx = new AudioContext();
// const source = audioCtx.createMediaStreamSource(stream);
// // 创建双二阶滤波器
// const biquadFilter = audioCtx.createBiquadFilter();
// biquadFilter.type = "lowshelf";
// biquadFilter.frequency.value = 1000;
// biquadFilter.gain.value = range.value;
// // 将 AudioBufferSourceNode 连接到 gainNode // 并将 gainNode 连接到目的地,这样我们就可以播放 // 音乐并使用鼠标光标调整音量
// source.connect(biquadFilter);
// });
// ws.onmessage = (event) => {
// // 假设事件中包含的是音频数据
// console.log('Received audio data');
// const audioBlob = new Blob(, { type: 'audio/mp3' });
// const audioUrl = URL.createObjectURL(audioBlob);
// // 获取 audio 元素并设置其源
// const audioPlayer = document.getElementById('audioPlayer');
// audioPlayer.src = audioUrl;
// // 播放音频
// audioPlayer.play().catch((error) => {
// console.error('播放音频时出错:', error);
// });
// };
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onopen = () => {
console.log('WebSocket connection established');
};
}
// 初始化设备列表
loadDevices();
</script>
</body>
</html>
手机选择 virtual-audio-capturer 设备即可获取电脑播放的声音前端主要难点,在于web屏幕常亮和web播放二进制流wav数据完整项目地址:winodwsAudio
完整项目地址:https://gitee.com/pmhw/winodwsAudio 大佬牛逼 大佬牛逼
大佬牛逼 非常实用欸 最近正在想,台式机没外放,怎么转手机上来。LZ就搞出来了,强 台式机没外设,这不无敌 大佬牛逼 厉害 厉害
页:
[1]
2