N79823 发表于 2024-11-14 16:45

使用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




N79823 发表于 2024-11-14 16:46

完整项目地址:https://gitee.com/pmhw/winodwsAudio

52pojie19 发表于 2024-11-14 18:09

大佬牛逼

bingchuan111 发表于 2024-11-14 18:20

大佬牛逼

wuyemeigui 发表于 2024-11-14 22:13


大佬牛逼

Feik1K 发表于 2024-11-15 11:05

非常实用欸

Luncode 发表于 2024-11-15 11:39

最近正在想,台式机没外放,怎么转手机上来。LZ就搞出来了,强

aosikaiii 发表于 2024-11-16 11:42

台式机没外设,这不无敌

amnsdx 发表于 2024-11-16 12:41

大佬牛逼

shi147517631 发表于 2024-11-16 12:56

厉害 厉害
页: [1] 2
查看完整版本: 使用nodejs 实现利用手机播放电脑声音 实时