我目前正在创建一个简单的家庭监控系统,我可以作为流媒体或消费者进入应用程序。
技术
仅 localhost 用于开发目的。 我从 Windows 的 WSL 终端运行客户端和服务器。 该应用程序可从 Windows 计算机访问。
当前行为
访问 http://localhost/?stream 后,我开始流式传输并等待来自 websocket 的传入消息以开始向新的对等方发送信号。
当消费者访问 http://localhost 时,将执行 WebRTC 连接并从流媒体端接收曲目。
peerConnection.ontrack
消费者端的事件确实被触发了,但媒体流似乎只包含 1 个视频轨道,并且被静音。
在流媒体端做
peerConnection.addTrack
时,我实际上设置了2个轨道,一个用于视频,另一个用于音频。
问题
当消费者从流媒体接收到轨道/流时,它将其设置为
video.srcObject
属性,但什么也没有发生。没有视频或音频播放。
预期行为
来自流媒体的视频和音频应在接收到轨道/流并将其设置为消费者的
video.srcObject
属性后在消费者端播放。
我尝试过的
我已经浏览了 StackOverflow 上的各种相关问题,并尝试应用建议的各种修复程序,但没有一个能正常工作。
video.srcObject
代码
下面我将主要应用逻辑涉及到的应用文件贴出来:
包含整个应用程序的实际存储库可以在以下位置找到:https://github.com/Jesus-Gonzalez/home-watcher - 您可以使用
yarn start:client
和 yarn start:server
命令克隆并运行应用程序。然后,对于流媒体端,访问 http://localhost:5173/?stream 上的应用程序;对于消费者应用程序,访问 http://localhost:5173 上的应用程序。
别担心,这个存储库确实是问题的一个非常基本和最小的示例(它没有那么大)。
信令Websocket服务
import { Server } from "socket.io";
const io = new Server();
let streamer = null;
let users = new Set();
io.on("connect", (socket) => {
if (users.has(socket.id)) {
return;
}
console.log(
`Socket ${socket.id} connected - Client IP Address: ${socket.handshake.address}`
);
socket.on("disconnect", (reason) => {
console.log(`Socket: ${socket.id} disconnected - Reason: ${reason}`);
});
socket.on("begin-stream", () => {
console.log("begin stream", socket.id);
streamer = socket.id;
});
socket.on("request-start-stream", (data) => {
console.log("request-start-stream");
socket.to(streamer).emit("handle-request-start-stream", {
to: socket.id,
offer: data.offer,
});
});
socket.on("response-start-stream", (data) => {
console.log("response-start-stream");
socket.to(data.to).emit("handle-response-start-stream", data.answer);
});
});
io.listen(5432, { cors: true });
Streamer.tsx
import { useEffect } from "react";
import socket from "./socket";
import useVideo from "./useVideo";
export default function Streamer() {
const { videoRef, element: videoElement } = useVideo();
useEffect(() => {
init();
async function init() {
if (!videoRef.current) return;
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
videoRef.current.srcObject = stream;
socket.emit("begin-stream");
socket.on("handle-request-start-stream", async ({ to, offer }) => {
const peerConnection = new RTCPeerConnection();
stream
.getTracks()
.forEach(
(track) =>
console.log("track", track) ||
peerConnection.addTrack(track, stream)
);
await peerConnection.setRemoteDescription(
new RTCSessionDescription(offer)
);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(
new RTCSessionDescription(answer)
);
socket.emit("response-start-stream", { to, answer });
});
}
}, []);
return videoElement;
}
Consumer.tsx
import { useEffect } from "react";
import socket from "./socket";
import useVideo from "./useVideo";
export default function Consumer() {
const { videoRef, element: videoElement } = useVideo();
useEffect(() => {
init();
async function init() {
const peerConnection = new RTCPeerConnection();
peerConnection.addEventListener("track", ({ streams: [stream] }) => {
if (!videoRef.current) return;
console.log('stream', stream)
videoRef.current.srcObject = stream;
});
const offer = await peerConnection.createOffer({
// workaround to receive the tracks
// if this is not specified, I will never receive the tracks
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await peerConnection.setLocalDescription(
new RTCSessionDescription(offer)
);
socket.emit("request-start-stream", { offer });
socket.on("handle-response-start-stream", async (answer) => {
await peerConnection.setRemoteDescription(
new RTCSessionDescription(answer)
);
});
}
}, []);
return videoElement;
}
useVideo.tsx(视频标签挂钩)
import { useRef } from "react";
export default function useVideo() {
const videoRef = useRef<HTMLVideoElement>(null);
const element = <video ref={videoRef} autoPlay />;
return { videoRef, element };
}
终于找到bug原因了。
ICE Candidate 交换逻辑缺失,我只需添加它。
信令服务
socket.on("send-candidate", (data) => {
socket.to(data.to).emit("handle-send-candidate", data.candidate);
});
Streamer.tsx
peerConnection.addEventListener("icecandidate", (event) => {
if (event.candidate === null) {
return;
}
socket.emit("send-candidate", {
to,
candidate: event.candidate,
});
});
Consumer.tsx
socket.on("handle-send-candidate", (candidate) => {
peerConnection.addIceCandidate(candidate);
});