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

Место общения программистов C#
Ответить Пред. темаСлед. тема
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 МБ.

  • Похожие темы
    Ответы
    Просмотры
    Последнее сообщение
  • Почему видеопотоки WebRTC работают на ПК, но не на iPhone для многопользовательских вызовов?
    Anonymous » » в форуме C#
    0 Ответы
    12 Просмотры
    Последнее сообщение Anonymous
  • Почему видеопотоки WebRTC работают на ПК, но не на iPhone для многопользовательских вызовов?
    Anonymous » » в форуме C#
    0 Ответы
    20 Просмотры
    Последнее сообщение Anonymous
  • Видеопотоки AWS Kinesis — Producer SDK Java — исключение JNI
    Anonymous » » в форуме JAVA
    0 Ответы
    13 Просмотры
    Последнее сообщение Anonymous
  • Как обращаться с системой аутентификации пользователей в многопользовательских играх?
    Anonymous » » в форуме JAVA
    0 Ответы
    32 Просмотры
    Последнее сообщение Anonymous
  • Ошибки синхронизации многопользовательских районов Unity
    Anonymous » » в форуме C#
    0 Ответы
    5 Просмотры
    Последнее сообщение Anonymous

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