Почему видеопотоки WebRTC работают на ПК, но не на iPhone для многопользовательских вызовов?Html

Программисты Html
Ответить
Anonymous
 Почему видеопотоки WebRTC работают на ПК, но не на iPhone для многопользовательских вызовов?

Сообщение Anonymous »

Я работаю над приложением для видеовызовов на базе WebRTC с использованием Knockout.js и SignalR, и оно отлично работает на ПК для нескольких пользователей. Однако когда я инициирую звонок со своего iPhone (iOS Safari), получатели не видят потоки друг друга. Видеоконтейнеры добавляются, но экраны остаются пустыми у всех получателей, и они видят только меня (отправителя).
Например:
Если я звоню с ноутбука на iPhone 1 и iPhone 2, то все работает нормально, стабильно.
А вот если я звоню с iPhone 1 или iPhone 2 на свой ноутбук и оставшийся iPhone, то отправитель видит все видеопотоки как обычно, но получатели не видят каждый чужой видеопоток.

Код: Выделить всё

import * as ko from "knockout";
import * as signalR from "@microsoft/signalr";
import { Modal } from "bootstrap";

let receiverUserIds: string[] = [];

async function getUserMedia() {
try {
return await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
} catch (error) {
throw new Error(`Error fetching user media: ${error}`);
}
}

function generateRandomNumber(min: number, max: number) {
// Generate a random number between min and max (inclusive).
return Math.floor(Math.random() * (max - min + 1)) + min; // Corrected to include max in the range.
}

async function signalrStartHubConnection() {
try {
const connection = new signalR.HubConnectionBuilder()
.withUrl("/signalr") // Specify the SignalR hub URL.
.configureLogging(signalR.LogLevel.Information) // Set logging level.
.withAutomaticReconnect() // Enable automatic reconnection.
.build(); // Build the SignalR connection.

await connection.start(); // Start the SignalR connection.
return connection; // Return the connection object.
} catch (error) {
throw new Error(`Error starting SignalR connection: ${error}`); // Throw an error if connection fails.
}
}

async function signalrRegisterWebrtcUserId(
signalrHubConnection: signalR.HubConnection,
webrtcUserId: string
) {
try {
await signalrHubConnection.invoke("RegisterUser", webrtcUserId); // Invoke SignalR method to register user.
} catch (error) {
throw new Error(`Error registering user: ${error}`); // Throw an error if registration fails.
}
}

// Enums
enum SignalTypeEnum {
REGISTER = "register",
INVITE = "invite",
ICE_CANDIDATE = "ice_candidate",
ACCEPT = "accept",
MESH = "mesh",
}

enum RoleEnum {
SENDER = "sender",
RECEIVER = "receiver",
}

// Interfaces
interface PeerConnectionsInterface {
[key: string]: RTCPeerConnection; // Map of user IDs to their respective RTCPeerConnection objects.
}

interface IceCandidatesInterface {
[key: string]: RTCIceCandidate[];
}

interface IceCandidatesObservableInterface {
[key: string]: KnockoutObservableArray;
}

interface MeshNetworkUserIdsInterface {
[key: string]: string[];
}

interface SignalDtoInterface {
signalType: SignalTypeEnum;
receiverUserId: string; // The user ID of whom you want to send data to.
senderUserId: string;
data: string;
role: RoleEnum;
}

class Conference {
currentSignal!: SignalDtoInterface;
userMedia!: MediaStream;
webrtcIceCandidates: IceCandidatesInterface;
webrtcGatheredIceCandidates: KnockoutObservable;
webrtcPeerConnections!: PeerConnectionsInterface; // Optional observable for peer connections.
webrtcUserId: KnockoutObservable; // Observable for WebRTC user ID.
webrtcReceiverUserIdsInputField: KnockoutObservable; // Observable input field for receiver user IDs.

signalrHubConnection!: KnockoutObservable; // SignalR hub connection instance.
signalrConnectionId!: KnockoutObservable; // SignalR connection ID, initially null.
signalrSenderConnectionId: KnockoutObservable; // Observable for the sender's SignalR connection ID.

isCallToggled: KnockoutObservable; // Observable to track call toggle status.

configuration = {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" }, // STUN server
],
};

constructor() {
this.webrtcPeerConnections = {};
this.webrtcIceCandidates = {};
this.webrtcGatheredIceCandidates =
ko.observable({});
this.webrtcUserId = ko.observable(
generateRandomNumber(1, 19).toString()
);  // Generate a random WebRTC user ID and store it as a Knockout observable.
this.webrtcReceiverUserIdsInputField = ko.observable(""); // Initialize the receiver user IDs input field as an empty string.
this.signalrSenderConnectionId = ko.observable(); // Initialize as undefined. Should show the ID of the original sender on the receivers screen.
this.isCallToggled = ko.observable(false); // Initialize call toggle state as false.

// Bind methods to the current instance to ensure `this` context is correct.
this.toggleCall = this.toggleCall.bind(this);
this.startCall = this.startCall.bind(this);
this.stopCall = this.stopCall.bind(this);
this.acceptCall = this.acceptCall.bind(this);
this.declineCall = this.declineCall.bind(this);
}

addOrUpdateIceCandidate(receiverUserId: string, candidate: RTCIceCandidate) {
if (!this.webrtcGatheredIceCandidates()[receiverUserId]) {
this.webrtcGatheredIceCandidates()[receiverUserId] =
ko.observableArray([]);
}
this.webrtcGatheredIceCandidates()[receiverUserId].push(candidate); // Knockout will track this.
}

async startMediaStream(elementId: string, mediaStream: MediaStream) {
try {
const container = document.getElementById("videos"); // Target the container where you want to append the video element.

if (!container) {
throw new Error("Container element not found");
}

let videoElement = document.getElementById(elementId) as HTMLVideoElement;

// If the video element doesn't exist, create it and append it to the container
if (!videoElement) {
videoElement = document.createElement("video"); // Create a new video element
videoElement.id = elementId; // Set the element ID
videoElement.autoplay = true; // Ensure the video plays automatically
videoElement.muted = false; // Mute the video to avoid echo when playing local stream
videoElement.playsInline = true; // For mobile devices to support inline video playback
videoElement.className =
"w-full bg-black rounded-lg object-contain aspect-video"; // Apply the same styling as other video elements

// Create a wrapper div if needed (like in your current structure)
const videoWrapper = document.createElement("div");
videoWrapper.className = "w-full";

// Append the video element to the wrapper div
videoWrapper.appendChild(videoElement);

// Append the wrapper div to the container
container.appendChild(videoWrapper);
}

// Assign the media stream to the video element
videoElement.srcObject = mediaStream;
} catch (err) {
console.error("Error accessing media devices:", err);
alert(
"Error accessing your camera and microphone.  Please check your device permissions."
);
}
}

async init() {
this.userMedia = await getUserMedia();
this.startMediaStream("localVideo", this.userMedia); // Ensure local stream is always shown

const hubConnection = await signalrStartHubConnection();
this.signalrHubConnection = ko.observable(hubConnection); // Start the SignalR connection.
await signalrRegisterWebrtcUserId(hubConnection, this.webrtcUserId());
this.signalrConnectionId = ko.observable(hubConnection.connectionId); // Store the SignalR connection ID.

// Bind view model to UI after initialization.
const viewModel = {
webrtcUserId: this.webrtcUserId, // Bind WebRTC user ID.
signalrConnectionId: this.signalrConnectionId, // Bind SignalR connection ID.
webrtcReceiverUserIdsInputField: this.webrtcReceiverUserIdsInputField, // Bind receiver user IDs input field.
signalrSenderConnectionId: this.signalrSenderConnectionId, // Bind the SignalR sender connection ID.
isCallToggled: this.isCallToggled, // Bind the call toggle status.
toggleCall: this.toggleCall, // Bind the toggle call function.
acceptCall: this.acceptCall, // Bind with current signal.
declineCall: this.declineCall, // Bind the decline call function.
};

ko.applyBindings(viewModel); // Apply Knockout bindings to the view model.
this.listenForIncomingSignals();
}

async toggleCall() {
const toggleValue = this.isCallToggled();
if (toggleValue) {
receiverUserIds = this.receiverUserdIds(); // Get receiver user IDs from input.
this.startCallMeshUsers();
receiverUserIds.forEach((userId) => this.startCall(userId));
} else {
await this.stopCall();
}
}

async peerConnectionCreateOffer(peerConnection: RTCPeerConnection) {
try {
const offer = await peerConnection.createOffer();
return offer;
} catch (error) {
throw new Error(`Error creating peer connection offer: ${error}`);
}
}

async sendOffer(
signalrHubConnection: signalR.HubConnection,
receiverUserId: string,
senderUserId: string,
data: string
) {
const signalDto: SignalDtoInterface = {
signalType: SignalTypeEnum.INVITE,
receiverUserId,
senderUserId,
data: data,
role: RoleEnum.SENDER,
};

try {
await signalrHubConnection.invoke("SendSignal", signalDto);
} catch (error) {
throw new Error(`Error sending offer to remote peer: ${error}`);
}
}

async sendAnswer(
signalrHubConnection: signalR.HubConnection,
receiverUserId: string,
senderUserId: string,
data: string
) {
const signalDto: SignalDtoInterface = {
signalType: SignalTypeEnum.ACCEPT,
receiverUserId,
senderUserId,
data: data,
role: RoleEnum.RECEIVER,
};

try {
await signalrHubConnection.invoke("SendSignal", signalDto);
} catch (error) {
throw new Error(`Error sending answer to remote peer: ${error}`);
}
}

async gatherIceCandidates(
peerConnection: RTCPeerConnection,
receiverUserId: string
) {
peerConnection.onicecandidate = async (event) => {
const candidate = event.candidate;
if (candidate) {
if (!this.webrtcIceCandidates[receiverUserId]) {
this.webrtcIceCandidates[receiverUserId] = [candidate];
} else {
this.webrtcIceCandidates[receiverUserId].push(candidate);
}
}
};
}

parseJson(stringData: string) {
try {
return JSON.parse(stringData);
} catch (error) {
throw new Error(`Error parsing string data: ${error}`);
}
}

receiverUserdIds(): string[] {
const inputField = this.webrtcReceiverUserIdsInputField();
const ids = inputField
.trim()
.split(",")
.map((id) => id.trim())
.filter((id) =>  id !== "");
if (!ids?.length) throw new Error("Error extracting IDs from input field.");
return ids;
}

async startCall(receiverUserId: string) {
const senderUserId = this.webrtcUserId();
const config = this.configuration;
const hubConnection = this.signalrHubConnection();

const peerConnection = new RTCPeerConnection(config);
const mediaStream = this.userMedia;

// Add tracks to the peer connection only if they are not already added.
mediaStream.getTracks().forEach((track) => {
const senders = peerConnection.getSenders(); // Get the current senders.
const isTrackAlreadyAdded = senders.some(
(sender) => sender.track === track
); // Check if track is already added.

if (!isTrackAlreadyAdded) {
peerConnection.addTrack(track, mediaStream); // Add the track if it is not already added.
}
});

const offer = await this.peerConnectionCreateOffer(peerConnection);
await peerConnection.setLocalDescription(offer);

this.gatherIceCandidates(peerConnection, receiverUserId);
this.webrtcPeerConnections[receiverUserId] = peerConnection;

// // Handle receiving remote media streams.
this.webrtcPeerConnections[receiverUserId].ontrack = (event) => {
if (event.streams && event.streams[0]) {
const stream = event.streams[0];
this.startMediaStream(
"remoteVideo_" + receiverUserId,
event.streams[0]
); // Use unique element ID.
} else {
console.error("No streams found in the event.");
}
};

// Send the offer to the receiver.
this.sendOffer(
hubConnection,
receiverUserId,
senderUserId,
JSON.stringify(offer)
);
}

async stopCall() {
for (const id in this.webrtcPeerConnections) {
const peerConnection = this.webrtcPeerConnections[id];
if (peerConnection) {
// Close the peer connection and remove senders.
peerConnection
.getSenders()
.forEach((sender) => peerConnection.removeTrack(sender));
peerConnection.close();
}
}
this.webrtcPeerConnections = {}; // Clear the peer connections map.
}

openConfirmationDialog(signal: SignalDtoInterface): void {
// currentDialogValue = signal;
const inviteModalElement = document.getElementById("inviteModal");
if (inviteModalElement) {
const inviteModal = new Modal(inviteModalElement);
inviteModal.show();
}
}

closeConfirmationDialog(): void {
const inviteModalElement = document.getElementById("inviteModal");
if (inviteModalElement) {
const inviteModal = Modal.getInstance(inviteModalElement);
inviteModal?.hide(); // Hide the modal if it's initialized.
}
}

async acceptCall(signal: SignalDtoInterface): Promise {
const { senderUserId, receiverUserId } = signal;
const sdpOffer = this.parseJson(signal.data);

let peerConnection = this.webrtcPeerConnections[senderUserId];
if (!peerConnection) {
const config = this.configuration;
this.webrtcPeerConnections[senderUserId] = new RTCPeerConnection(config);
peerConnection = this.webrtcPeerConnections[senderUserId];
}

const mediaStream = this.userMedia;

// Add tracks to the peer connection only if they are not already added.
mediaStream.getTracks().forEach((track) => {
const senders = peerConnection.getSenders(); // Get all current senders (tracks already added).
const isTrackAlreadyAdded = senders.some(
(sender) => sender.track === track
); // Check if the track is already added.

if (!isTrackAlreadyAdded) {
peerConnection.addTrack(track, mediaStream); // Add the track if it is not already added.
}
});

// Handle the remote track event to receive the incoming media streams.
this.webrtcPeerConnections[senderUserId].ontrack = (event) => {
const stream = event.streams;
if (stream?.length) {
this.startMediaStream("remoteVideo_" + senderUserId, stream[0]);  // Use unique element ID.
} else {
console.error("No streams found in the event.");
}
};

await peerConnection.setRemoteDescription(sdpOffer);
const sdpAnswer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(sdpAnswer);

const hubConnection = this.signalrHubConnection();
this.gatherIceCandidates(peerConnection, senderUserId);
this.sendAnswer(
hubConnection,
senderUserId,
receiverUserId,
JSON.stringify(sdpAnswer)
);
}

declineCall() {
this.closeConfirmationDialog();
}

listenForIncomingSignals() {
const hubConnection = this.signalrHubConnection();
hubConnection.on("ReceiveSignal", async (signal) => {
const { signalType, receiverUserId, senderUserId } = signal;
const data = this.parseJson(signal.data);
this.currentSignal = signal;

switch (signalType) {
case SignalTypeEnum.INVITE: {
let peerConnection = this.webrtcPeerConnections[senderUserId];
if (!peerConnection) {
peerConnection = new RTCPeerConnection(this.configuration);
this.webrtcPeerConnections[senderUserId] = peerConnection;
}
await this.acceptCall(signal);

break;
}
case SignalTypeEnum.ICE_CANDIDATE: {
if (signal.role === RoleEnum.RECEIVER) {
data.forEach((candidate: RTCIceCandidate) => {
this.webrtcPeerConnections[senderUserId].addIceCandidate(
candidate
);
});

const signalDto: SignalDtoInterface = {
signalType: SignalTypeEnum.ICE_CANDIDATE,
receiverUserId: senderUserId,
senderUserId: receiverUserId,
data: JSON.stringify(this.webrtcIceCandidates[senderUserId]),
role: RoleEnum.SENDER,
};

try {
await this.signalrHubConnection().invoke("SendSignal", signalDto);
} catch (error) {
throw new Error(`Error sending offer to remote peer: ${error}`);
}
} else {
data.forEach((candidate: RTCIceCandidate) => {
this.webrtcPeerConnections[senderUserId].addIceCandidate(
candidate
);
});
}
break;
}
case SignalTypeEnum.ACCEPT: {
console.log(senderUserId, data);
await this.webrtcPeerConnections[senderUserId].setRemoteDescription(
data
);

const signalDto: SignalDtoInterface = {
signalType: SignalTypeEnum.ICE_CANDIDATE,
receiverUserId: senderUserId,
senderUserId: receiverUserId,
data: JSON.stringify(this.webrtcIceCandidates[senderUserId]),
role: RoleEnum.RECEIVER,
};

try {
await this.signalrHubConnection().invoke("SendSignal", signalDto);
} catch (error) {
throw new Error(`Error sending offer to remote peer: ${error}`);
}
break;
}
case SignalTypeEnum.MESH: {
await this.startCall(`${data}`);
break;
}
}
});
}

generateCallStructure(ids: string[]) {
let callStructure: { [key: string]: string[] } = {};

ids.forEach((user, index) =>  {
callStructure[user] = ids.slice(index + 1);
});

return callStructure;
}

async startCallMeshUsers() {
const signalDto: SignalDtoInterface = {
signalType: SignalTypeEnum.MESH,
receiverUserId: "",
senderUserId: "",
data: "",
role: RoleEnum.SENDER,
};

// Iterate over the receiverUserIds asynchronously
for (const [index, key] of receiverUserIds.entries()) {
/**
*
"1": ["2", "3", "4"],  // User 1 calls users 2, 3, and 4.
"2": ["3", "4"],       // User 2 only calls users 3 and 4 (not 1 because 1 has already called).
"3": ["4"],            // User 3 only calls user 4 (not 1 or 2 because they have already called).
"4": []                // User 4 doesn't call anyone (all users have already connected).

NOT:
1: Array(4) [ "2", "3", "4", … ]
2: Array(4) [ "1", "3", "4", … ]
3: Array(4) [ "1", "2", "4", … ]
4: Array(4) [ "1", "2", "3", … ]
5: Array(4) [ "1", "2", "3", … ]

*/

// Filter out users that have already been "called" by this user.
const mesh: string[] = receiverUserIds.slice(index + 1); // Get users after the current key.

// Only proceed if there are remaining users to call.
if (mesh.length > 0) {
for (const value of mesh) {
// Set up the signalDto values.
signalDto.receiverUserId = key; // The current user making the call.
signalDto.senderUserId = key; // The same user as the sender.
signalDto.data = value; // The user being called.

try {
// Await the asynchronous call to SendSignal.
await this.signalrHubConnection().invoke("SendSignal", signalDto);
} catch (error) {
// Throw an error if the call fails.
throw new Error(
`Error calling mesh users: ${JSON.stringify(mesh)}`
);
}
}
}
}
}
}

const conference = new Conference(); // Create a new Conference instance.
conference.init(); // Initialize the conference.

HTML

Код: Выделить всё






WebRTC and SignalR Test
[*]






































My User ID

Loading...

My SignalR Registration ID
data-bind="text: signalrConnectionId">
Loading...









class="overflow-y-scroll bg-gray-100 p-3 border border-gray-300 rounded-md font-mono whitespace-pre-wrap h-full">










Incoming Video Call


Do you want to accept the call?


Accept Decline


[list]
⚡ Barack Obama
[*][url=https://youtube.com]YouTube[/url]
[/list]




Может кто-нибудь помочь мне решить эту проблему? Я понятия не имею, где что-то идет не так, и я не получаю никаких сообщений об ошибках в консоли. Как на стороне отправителя, так и на стороне получателя.
Я попробовал реструктурировать код. Размещение ожидания и удаление ожидания. И переупорядочение кода. Я также пробовал разные браузеры и проверял все свои разрешения (камера и микрофон).

Подробнее здесь: https://stackoverflow.com/questions/790 ... user-calls
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Html»