Not receiving video, onicecandidate is not executing - javascript

So I was following this tutorial to learn how to implement a WebRTC server-client setup. Once I got that working I wanted to split the client into two parts, one sender and one receiver. Now they can establish a connection with each other but the receiver never gets the stream from the sender.
I managed to determine that the code flow between the original code and split versions remains the same, except that neither peer executes the onicecandidate event.
According to this I need to explicitly include OfferToReceiveAudio: true and OfferToReceiveVideo: true since I'm using Chrome, which I did but it didn't seem to make any difference.
Currently, they both receive SDP from each other, there is a local and remote description in the peerConnection, and iceGatheringState is "new" but iceConnectionState is "checking" (unlike the second link where he states it should also be "new")
How come they aren't exchanging ICE candidates when it's split in two like this?
Sender.js
const HTTPSPort = 3434;
const domain = '127.0.0.1';
const wssHost = 'wss://' + domain + ':' + HTTPSPort + '/websocket/';
// Feed settings
const video = true;
const audio = true;
const constraints = { "audio": audio, "video": video };
var videoContainer = null, feed = null,
pC = null, wsc = new WebSocket(wssHost),
pCConfig = [
{ 'url': 'stun:stun.services.mozilla.com' },
{ 'url': 'stun:stun.l.google.com:19302' }
];
function pageReady() {
// Check browser WebRTC availability
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {
videoContainer = document.getElementById('videoFeed');
// Get the feed and show it in the local video element
feed = stream;
videoContainer.srcObject = feed;
}).catch(function () {
alert("Sorry, your browser does not support WebRTC!");
});
}
wsc.onmessage = function (evt) {
if (!pC) {
// Initiate peerConnection
pC = new RTCPeerConnection(pCConfig);
// Send any ice candidates to the other peer
pC.onicecandidate = onIceCandidateHandler;
pC.addStream(feed);
}
// Read the message
var signal = JSON.parse(evt.data);
if (signal.sdp) {
log('Received SDP from remote peer.');
pC.setRemoteDescription(new RTCSessionDescription(signal.sdp));
answerCall();
} else if (signal.candidate) {
log('Received ICECandidate from remote peer.');
pC.addIceCandidate(new RTCIceCandidate(signal.candidate));
}
};
function answerCall() {
pC.createAnswer().then(function (answer) {
var ans = new RTCSessionDescription(answer);
pC.setLocalDescription(ans).then(function () {
wsc.send(JSON.stringify({ 'sdp': ans }));
}).catch(errorHandler);
}).catch(errorHandler);
}
function onIceCandidateHandler(evt) {
if (!evt || !evt.candidate) return;
wsc.send(JSON.stringify({ 'candidate': evt.candidate }));
};
Receiver.js
const HTTPSPort = 3434;
const domain = '127.0.0.1';
const wssHost = 'wss://' + domain + ':' + HTTPSPort + '/websocket/';
var remoteVideo = null,
pC = null, wsc = new WebSocket(wssHost),
pCConfig = [
{ 'url': 'stun:stun.services.mozilla.com' },
{ 'url': 'stun:stun.l.google.com:19302' }
],
mediaConstraints = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
};
function pageReady() {
remoteVideo = document.getElementById('remoteVideo');
icebutton = document.getElementById('checkICE');
icebutton.addEventListener('click', function (evt) {
console.log(pC);
})
};
wsc.onopen = function () {
// Initiates peerConnection
pC = new RTCPeerConnection(pCConfig);
// Send any ICE candidates to the other peer
pC.onicecandidate = onIceCandidateHandler;
// Once remote stream arrives, show it in the remote video element
pC.onaddstream = onAddStreamHandler;
// Offer a connection to the server
createAndSendOffer();
};
function createAndSendOffer() {
pC.createOffer(mediaConstraints).then(function (offer) {
var off = new RTCSessionDescription(offer);
pC.setLocalDescription(off).then(function () {
wsc.send(JSON.stringify({ 'sdp': off }));
}).catch(errorHandler);
}).catch(errorHandler);
}
wsc.onmessage = function (evt) {
// Read the message
var signal = JSON.parse(evt.data);
if (signal.sdp) {
console.log('Received SDP from remote peer.');
pC.setRemoteDescription(new RTCSessionDescription(signal.sdp));
} else if (signal.candidate) {
console.log('Received ICECandidate from remote peer.');
pC.addIceCandidate(new RTCIceCandidate(signal.candidate));
}
};
function onIceCandidateHandler(evt) {
if (!evt || !evt.candidate) return;
wsc.send(JSON.stringify({ 'candidate': evt.candidate }));
};
function onAddStreamHandler(evt) {
// Set remote video stream as source for remote video HTML element
remoteVideo.srcObject = evt.stream;
};

You forgot iceServers. Change
pCConfig = [
{ 'url': 'stun:stun.services.mozilla.com' },
{ 'url': 'stun:stun.l.google.com:19302' }
];
to
pCConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
Additionally:
url has been deprecated, use urls.
The mozilla stun server has been decommissioned, so save yourself some time and exclude it.
mandatory and OfferToReceiveAudio have been deprecated. Use offerToReceiveAudio.
mandatory and OfferToReceiveVideo have been deprecated. Use offerToReceiveVideo.
Tips
Kudos for using promises (unlike the tutorial)! Note you can return them to flatten things:
function createAndSendOffer() {
return pC.createOffer(mediaConstraints).then(function (offer) {
return pC.setLocalDescription(offer);
})
.then(function () {
wsc.send(JSON.stringify({ sdp: pC.localDescription }));
})
.catch(errorHandler);
}

Related

webrtc - connecting multiple clients to one specific client - python & javascript

I am currently working on webrtc project with multiple clients (5 - 4 with input video streams and 1 for showing them) and signaling server. Main goal is to connect clients on this way:
Every client with input video stream should be connected to client that will show all of video streams. It should be connected like that because I want to use webrtc so I can have low latency. I would also like to use web sockets for signaling server. Server is written in Python and clients in Javascript. Does anyone know how to accomplish that?
Server:
from aiohttp import web
import socketio
ROOM = 'room'
sessionIDs = []
sio = socketio.AsyncServer(cors_allowed_origins='*', ping_timeout=35)
app = web.Application()
sio.attach(app)
#sio.event
async def connect(sid, environ):
print('Connected', sid)
sessionIDs.append(sid)
await sio.emit('ready', room=ROOM, skip_sid=sid)
sio.enter_room(sid, ROOM)
#sio.event
def disconnect(sid):
sio.leave_room(sid, ROOM)
print('Disconnected', sid)
#sio.event
async def data(sid, data):
print('Message from {}: {}'.format(sid, data))
await sio.emit('data', data, room=ROOM, skip_sid=sid)
if __name__ == '__main__':
web.run_app(app, port=9999)
Client:
const SIGNALING_SERVER_URL = 'http://localhost:9999';
const TURN_SERVER_URL = 'localhost:3478';
const TURN_SERVER_USERNAME = 'username';
const TURN_SERVER_CREDENTIAL = 'credential';
const PC_CONFIG = {
iceServers: [
{
urls: 'turn:' + TURN_SERVER_URL + '?transport=tcp',
username: TURN_SERVER_USERNAME,
credential: TURN_SERVER_CREDENTIAL
},
{
urls: 'turn:' + TURN_SERVER_URL + '?transport=udp',
username: TURN_SERVER_USERNAME,
credential: TURN_SERVER_CREDENTIAL
}
]
};
// Signaling methods
let socket = io(SIGNALING_SERVER_URL, { autoConnect: false }, /*{query: 'loggeduser=finalClient'}*/ /*{auth: {token: 'my-token'}}*/);
socket.on('data', (data) => {
console.log('Data received: ', data);
handleSignalingData(data);
});
socket.on('ready', () => {
console.log('Ready');
// Connection with signaling server is ready, and so is local stream
createPeerConnection();
sendOffer();
});
let sendData = (data) => {
socket.emit('data', data);
};
// WebRTC methods
let pc;
let localStream;
let remoteStreamElement = document.querySelector('#remoteStream');
let getLocalStream = () => {
socket.connect();
}
let createPeerConnection = () => {
try {
pc = new RTCPeerConnection(PC_CONFIG);
pc.onicecandidate = onIceCandidate;
pc.ontrack = onTrack;
pc.addStream(localStream);
console.log('PeerConnection created');
} catch (error) {
console.error('PeerConnection failed: ', error);
}
};
let sendOffer = () => {
console.log('Send offer');
pc.createOffer().then(
setAndSendLocalDescription,
(error) => { console.error('Send offer failed: ', error); }
);
};
let sendAnswer = () => {
console.log('Send answer');
pc.createAnswer().then(
setAndSendLocalDescription,
(error) => { console.error('Send answer failed: ', error); }
);
};
let setAndSendLocalDescription = (sessionDescription) => {
pc.setLocalDescription(sessionDescription);
console.log('Local description set');
sendData(sessionDescription);
};
let onIceCandidate = (event) => {
if (event.candidate) {
console.log('ICE candidate');
sendData({
type: 'candidate',
candidate: event.candidate
});
}
};
let onTrack = (event) => {
console.log('Add track');
remoteStreamElement.srcObject = event.streams[0];
};
let handleSignalingData = (data) => {
switch (data.type) {
case 'offer':
createPeerConnection();
pc.setRemoteDescription(new RTCSessionDescription(data));
sendAnswer();
break;
case 'answer':
pc.setRemoteDescription(new RTCSessionDescription(data));
break;
case 'candidate':
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
break;
}
};
// Start connection
getLocalStream();
Currently main functionality is peer to peer connection between them, but I want to make new peer to peer connection (for every client - with input stream - connected to server) with specific client that will show all of them.

Invalid state Error on websockets when sending message

I'm working on app which send message via websockets (managed by django channels) and in return it receives json from django db as a message and renders frontend based on that json.
I have Invalid State Error when I try to send message by websocket, why? Messages send are usually Json. I works properly all the time but commented part doesn't and I don't know why please explain me.
function main() {
configGame();
}
function configGame() {
const socket = "ws://" + window.location.host + window.location.pathname;
const websocket = new WebSocket(socket);
const playerName = document.querySelector(".playerName_header").textContent;
function asignEvents() {
const ready_btn = document.querySelector(".--ready_btn");
const start_btn = document.querySelector(".--start_btn");
ready_btn.addEventListener("click", () => {
let mess = JSON.stringify({
player: playerName,
action: "ready",
});
sendMess(mess);
});
start_btn.addEventListener("click", () => {
let mess = JSON.stringify({
player: playerName,
action: "start",
});
sendMess(mess);
});
}
function openWebsocket() {
console.log("Establishing Websocket Connection...");
websocket.onopen = () => {
console.log("Websocket Connection Established!");
};
}
function setWebsocket() {
websocket.onmessage = (mess) => {
console.log(`Message: ${mess.data}`);
dataJson = JSON.parse(mess.data);
dataJson = JSON.parse(dataJson.message);
//Player Ready (jeszcze z max_players zrobic kontrolke)
if (dataJson.action === "player_ready") {
const playersReadyText = document.querySelector(".players_ready_text");
playersReadyText.textContent = `Players ready: ${dataJson.players_ready}`;
}
};
websocket.onclose = () => {
console.log("Websocket Connection Terminated!");
};
}
/*
function checkState() {
let mess = JSON.stringify({
player: playerName,
action: "game state",
});
sendMess(mess);
}
*/
function sendMess(messText) {
websocket.send(messText);
}
openWebsocket();
checkState(); //This one doesn't work
asignEvents();
setWebsocket();
}
// Asigning Event Listneres to DOM ELEMENTS
function asignEvents() {
const ready_btn = document.querySelector(".--ready_btn");
const start_btn = document.querySelector(".--start_btn");
ready_btn.addEventListener("click", () => {
console.log("Ready");
});
start_btn.addEventListener("click", () => {
console.log("Start");
});
}
main();
Error:
Console (Safari) returns InvalidState error and points to
method checkState and sendMess.
InvalidStateError: The object is in an invalid state.
Is the websocket connected?
sendMess(messText) {
if (websocket.readyState === WebSocket.OPEN) {
websocket.send(messText);
} else {
console.warn("websocket is not connected");
}
}

No sound when making outgoing call via JsSIP (Asterisk)

I'm trying to debug the existent system, where calls are made via Asteriks.
When accepting incoming calls, everything works fine, but on making outgoing call there is apparently no sound (but I accept 'addstream' event and attach stream to audio). Production code takes 500 lines, but this code does pretty much the same, but doesn't work as well
const socket = new JsSIP.WebSocketInterface('wss://callwss.agdevelopments.net');
socket.via_transport = 'wss';
const configuration = {
password: "SIP4003!",
realm: "callws,s.agdevelopments.net",
register: true,
session_timers: false,
uri: "sip:4003#callwss.agdevelopments.net",
sockets: [socket]
}
const ua = new JsSIP.UA(configuration)
// Setup events
ua.on('connected', function () {
console.log('Connected')
})
ua.on('disconnected', function () {
console.log('Connected')
})
// Make a call
const eventHandlers = {
'progress': function (e) {
console.log('call is in progress');
},
'failed': function (e) {
console.log('call failed with cause: ' + (e.data ? e.data.cause : 'no cause'), e);
},
'ended': function (e) {
console.log('call ended with cause: ' + (e.data ? e.data.cause : 'no cause'), e);
},
'confirmed': function (e) {
console.log('call confirmed');
},
'addstream': (e) => {
console.log('Add stream (event handlers)')
audio.srcObject = e.stream
audio.play()
}
};
const options = {
'eventHandlers': eventHandlers,
'mediaConstraints': {'audio': true, 'video': false}
};
const audio = new window.Audio()
ua.on('registered', function () {
const session = ua.call('0513887341', options)
if (session.connection) {
console.log('Connection is valid')
session.connection.addEventListener('addstream', e => {
console.log('Add stream')
audio.srcObject = e.stream
audio.play()
})
session.on('addstream', function(e){
// set remote audio stream (to listen to remote audio)
// remoteAudio is <audio> element on page
const remoteAudio = audio
remoteAudio.src = window.URL.createObjectURL(e.stream);
remoteAudio.play();
});
session.connection.addEventListener('peerconnection', e => {
console.log('Peer connection')
audio.srcObject = e.stream
audio.play()
})
} else {
console.log('Connection is null')
}
})
ua.on('newRTCSession', (data) => {
console.log('New RTC Session')
const session = data.session
session.on('addstream', function(e){
// set remote audio stream (to listen to remote audio)
// remoteAudio is <audio> element on page
const remoteAudio = audio
remoteAudio.src = window.URL.createObjectURL(e.stream);
remoteAudio.play();
});
})
ua.start()
Also attaching screenshots from Asterisk. The first one is outgoing call with no sound, the second is incoming with sound
The issue was solved from IT side. There was no problems in JsSIP or code

Remote video not display using webrtc scaledrone

Hi can someone check is there something wrong with my code cause it not displayig the remote video. The log always return 'Start web RTC as 'offerer'' for both local and remote video. And I think mostly the sample code almost similar with my code so I dont know. Much love appreciate if you guys can help me. Thanks!
if (!location.hash) {
location.hash = Math.floor(Math.random() * 0xFFFFFF).toString(16);
}
const roomHash = location.hash.substring(1);
//const roomHash = '5dfd9b';
console.log("ROOM ID: >> " +roomHash);
// TODO: Replace with your own channel ID
const drone = new ScaleDrone('AUpfMxm16E9bfdgA');
// Room name needs to be prefixed with 'observable-'
//const roomName = 'observable-' + roomHash;
const roomName = 'observable-testPHR';
const configuration = {
iceServers: [{
urls: 'stun:stun.l.google.com:19302'
}]
};
let room;
let pc;
function onSuccess() {
console.log("Connection sucess");
};
function onError(error) {
console.log("Connection failed!");
//console.error(error);
};
drone.on('open', error => {
if (error) {
console.log( " Error open drone >>");
return console.error(error);
}
console.log(" Drone open >>")
room = drone.subscribe(roomName);
room.on('open', error => {
if (error) {
onError(error);
}
});
// We're connected to the room and received an array of 'members'
// connected to the room (including us). Signaling server is ready.
room.on('members', members => {
console.log('MEMBERS', members);
// If we are the second user to connect to the room we will be creating the offer
const isOfferer = members.length === 2;
startWebRTC(isOfferer);
});
});
// Send signaling data via Scaledrone
function sendMessage(message) {
console.log("Sending signal via scaledrone >>");
drone.publish({
room: roomName,
message
});
}
function startWebRTC(isOfferer) {
console.log('Starting WebRTC in as', isOfferer ? 'offerer' : 'waiter');
pc = new RTCPeerConnection(configuration);
console.log(" Test A ");
// 'onicecandidate' notifies us whenever an ICE agent needs to deliver a
// message to the other peer through the signaling server
pc.onicecandidate = event => {
console.log("Send Message to Candidate");
if (event.candidate) {
sendMessage({'candidate': event.candidate});
}
};
console.log(" Test B ");
// If user is offerer let the 'negotiationneeded' event create the offer
if (isOfferer) {
console.log(" Create Offer ");
pc.onnegotiationneeded = () => {
pc.createOffer().then(localDescCreated).catch(onError);
}
}
console.log(" Test C ");
// When a remote stream arrives display it in the #remoteVideo element
pc.ontrack = event => {
console.log("Display remote video >>>")
const stream = event.streams[0];
console.log(" Stream : >>" +stream);
if (!remoteVideo.srcObject || remoteVideo.srcObject.id !== stream.id) {
remoteVideo.srcObject = stream;
}
};
console.log(" Test D ");
navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
}).then(stream => {
console.log(" Display Local Video >> ");
// Display your local video in #localVideo element
localVideo.srcObject = stream;
// Add your stream to be sent to the conneting peer
stream.getTracks().forEach(track => pc.addTrack(track, stream));
}, onError);
console.log(" Test E ");
// Listen to signaling data from Scaledrone
room.on('data', (message, client) => {
// Message was sent by us
if (client.id === drone.clientId) {
return;
}
console.log(" Test F ");
if (message.sdp) {
// This is called after receiving an offer or answer from another peer
pc.setRemoteDescription(new RTCSessionDescription(message.sdp), () => {
// When receiving an offer lets answer it
if (pc.remoteDescription.type === 'offer') {
console.log(" Answer call ");
pc.createAnswer().then(localDescCreated).catch(onError);
}
}, onError);
} else if (message.candidate) {
// Add the new ICE candidate to our connections remote description
pc.addIceCandidate(
new RTCIceCandidate(message.candidate), onSuccess, onError
);
}
});
}
function localDescCreated(desc) {
pc.setLocalDescription(
desc,
() => sendMessage({'sdp': pc.localDescription}),
onError
);
}

How to send and receive desktop capture stream generated via getUsermedia()

I am making a Screen sharing app with WebRTC + Socket.io and stuck at a place.
Connected with two browser using WebRTC + Socket.io and can send text
I am taking support from codelab but it is not for stream.(If solution is based on this link then highly helpful)
How can I send getUserMedia() stream:
dataChannel.send(stream);
And receive same stream on channel.onmessage():
I am getting event.data as '[object MediaStream]' not stream.
channel.onmessage = function(event){
// unable to get correct stream
// event.data is "[object MediaStream]" in string
}
function createPeerConnection(isInitiator, config) {
console.log('Creating Peer connection as initiator?', isInitiator, 'config:', config);
peerConn = new RTCPeerConnection(config);
// send any ice candidates to the other peer
peerConn.onicecandidate = function (event) {
console.log('onIceCandidate event:', event);
if (event.candidate) {
sendMessage({
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
console.log('End of candidates.');
}
};
if (isInitiator) {
console.log('Creating Data Channel');
dataChannel = peerConn.createDataChannel("screen");
onDataChannelCreated(dataChannel);
console.log('Creating an offer');
peerConn.createOffer(onLocalSessionCreated, logError);
} else {
peerConn.ondatachannel = function (event) {
console.log('ondatachannel:', event.channel);
dataChannel = event.channel;
onDataChannelCreated(dataChannel);
};
}
}
It is working fine for string or json i.e. dataChannel.send('Hello');
I have created a wiki page for same: wiki
Please help.
Please try something like this: (explanation at the end of the code)
var btnShareYourCamera = document.querySelector('#share-your-camera');
var localVideo = document.querySelector('#local-video');
var remoteVideo = document.querySelector('#remote-video');
var websocket = new WebSocket('wss://path-to-server:port/');
websocket.onmessage = function(event) {
var data = JSON.parse(event.data);
if (data.sdp) {
if (data.sdp.type === 'offer') {
getUserMedia(function(video_stream) {
localVideo.srcObject = video_stream;
answererPeer(new RTCSessionDescription(data.sdp), video_stream);
});
}
if (data.sdp.type === 'answer') {
offerer.setRemoteDescription(new RTCSessionDescription(data.sdp));
}
}
if (data.candidate) {
addIceCandidate((offerer || answerer), new RTCIceCandidate(data.candidate));
}
};
var iceTransportPolicy = 'all';
var iceTransportLimitation = 'udp';
function addIceCandidate(peer, candidate) {
if (iceTransportLimitation === 'tcp') {
if (candidate.candidate.toLowerCase().indexOf('tcp') === -1) {
return; // ignore UDP
}
}
peer.addIceCandidate(candidate);
}
var offerer, answerer;
var iceServers = {
iceServers: [{
'urls': [
'stun:stun.l.google.com:19302',
'stun:stun1.l.google.com:19302',
'stun:stun2.l.google.com:19302',
'stun:stun.l.google.com:19302?transport=udp',
]
}],
iceTransportPolicy: iceTransportPolicy,
rtcpMuxPolicy: 'require',
bundlePolicy: 'max-bundle'
};
// https://https;//cdn.webrtc-experiment.com/IceServersHandler.js
if (typeof IceServersHandler !== 'undefined') {
iceServers.iceServers = IceServersHandler.getIceServers();
}
var mediaConstraints = {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
};
/* offerer */
function offererPeer(video_stream) {
offerer = new RTCPeerConnection(iceServers);
offerer.idx = 1;
video_stream.getTracks().forEach(function(track) {
offerer.addTrack(track, video_stream);
});
offerer.ontrack = function(event) {
remoteVideo.srcObject = event.streams[0];
};
offerer.onicecandidate = function(event) {
if (!event || !event.candidate) return;
websocket.send(JSON.stringify({
candidate: event.candidate
}));
};
offerer.createOffer(mediaConstraints).then(function(offer) {
offerer.setLocalDescription(offer).then(function() {
websocket.send(JSON.stringify({
sdp: offer
}));
});
});
}
/* answerer */
function answererPeer(offer, video_stream) {
answerer = new RTCPeerConnection(iceServers);
answerer.idx = 2;
video_stream.getTracks().forEach(function(track) {
answerer.addTrack(track, video_stream);
});
answerer.ontrack = function(event) {
remoteVideo.srcObject = event.streams[0];
};
answerer.onicecandidate = function(event) {
if (!event || !event.candidate) return;
websocket.send(JSON.stringify({
candidate: event.candidate
}));
};
answerer.setRemoteDescription(offer).then(function() {
answerer.createAnswer(mediaConstraints).then(function(answer) {
answerer.setLocalDescription(answer).then(function() {
websocket.send(JSON.stringify({
sdp: answer
}));
});
});
});
}
var video_constraints = {
mandatory: {},
optional: []
};
function getUserMedia(successCallback) {
function errorCallback(e) {
alert(JSON.stringify(e, null, '\t'));
}
var mediaConstraints = {
video: true,
audio: true
};
navigator.mediaDevices.getUserMedia(mediaConstraints).then(successCallback).catch(errorCallback);
}
btnShareYourCamera.onclick = function() {
getUserMedia(function(video_stream) {
localVideo.srcObject = video_stream;
offererPeer(video_stream);
});
};
You must attach stream using peer.addTrack as you can see in the above example
You must receive remote stream using peer.ontrack as you can see in the above example
i.e. use addTrack to attach your camera and use ontrack to receive remote camera.
You must never send your stream using dataChannel.send. Both are totally different protocols. A MediaStream must be shared using RTP; not SCTP. RTP is used only if you call peer.addTrack method to attach your camera stream.
This process happens before you open or join a room.
See single-page demo here: https://www.webrtc-experiment.com/getStats/
HTML for above code snippet:
<button id="share-your-camera"></button>
<video id="local-video" controls autoplay playsinline></video>
<video id="remote-video" controls autoplay playsinline></video>

Categories