1
0
peddlers-of-ketran/client/src/MediaControl.js
James Ketrenos 3bbff22ec2 Attempt to work with just audio sites
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
2022-03-18 17:13:52 -07:00

544 lines
18 KiB
JavaScript

import React, { useState, useEffect, useRef, useCallback,
useContext } from "react";
import "./MediaControl.css";
import VolumeOff from '@mui/icons-material/VolumeOff';
import VolumeUp from '@mui/icons-material/VolumeUp';
import MicOff from '@mui/icons-material/MicOff';
import Mic from '@mui/icons-material/Mic';
import VideocamOff from '@mui/icons-material/VideocamOff';
import Videocam from '@mui/icons-material/Videocam';
import { GlobalContext } from "./GlobalContext.js";
const debug = true;
/* Proxy object so we can pass in srcObject to <audio> */
const Video = ({ srcObject, local, ...props }) => {
const refVideo = useRef(null);
useEffect(() => {
if (!refVideo.current) {
return;
}
const ref = refVideo.current;
if (debug) console.log('media-control - video <video> bind');
ref.srcObject = srcObject;
if (local) {
ref.muted = true;
}
return () => {
if (debug) console.log('media-control - <video> unbind');
if (ref) {
ref.srcObject = undefined;
}
};
}, [srcObject, local]);
return <video ref={refVideo} {...props} />;
}
const MediaAgent = ({setPeers}) => {
const { name, ws } = useContext(GlobalContext);
const [ peers ] = useState({});
const [stream, setStream] = useState(undefined);
const onTrack = useCallback((event) => {
const connection = event.target;
console.log("media-agent - ontrack", event);
for (let peer in peers) {
if (peers[peer].connection === connection) {
console.log(`media-agent - ontrack - remote ${peer} stream assigned.`);
Object.assign(peers[peer].attributes, {
srcObject: event.streams[0]
});
/* Trigger update of MediaControl now that a stream is available */
setPeers(Object.assign({}, peers));
}
}
}, [peers, setPeers]);
const refOnTrack = useRef(onTrack);
const sendMessage = useCallback((data) => {
ws.send(JSON.stringify(data));
}, [ws]);
const onWsMessage = useCallback((event) => {
const addPeer = (config) => {
console.log('media-agent - Signaling server said to add peer:', config);
if (!stream) {
console.log(`media-agent - No local media stream`);
return;
}
const peer_id = config.peer_id;
if (peer_id in peers) {
if (!peers[peer_id].dead) {
/* This is normal when peers are added by other connecting
* peers through the signaling server */
console.log(`media-agent - addPeer - ${peer_id} already in peers`);
return;
}
}
const connection = new RTCPeerConnection({
configuration: {
offerToReceiveAudio: true,
offerToReceiveVideo: true
},
iceServers: [ {
urls: "turns:ketrenos.com:5349",
username: "ketra",
credential: "ketran"
},
/*
{
urls: "turn:numb.viagenie.ca",
username: "james_viagenie@ketrenos.com",
credential: "1!viagenie"
}
*/
]
});
/* Even if reviving, allocate a new Object so <MediaControl> will
* have its peer state change and trigger an update from
* <PlayerList> */
const peer = {
name: peer_id,
connection,
attributes: {},
};
if (peer_id in peers) {
peer.muted = peers[peer_id].muted;
peer.videoOn = peers[peer_id].videoOn;
console.log(`media-agent - addPeer - reviving dead peer ${peer_id}`, peer);
} else {
peer.muted = false;
peer.videoOn = true;
}
peers[peer_id] = peer;
console.log(`media-agent - addPeer - remote`, peers);
setPeers(Object.assign({}, peers));
connection.addEventListener('connectionstatechange', (event) => {
console.log(`media-agent - connectionstatechange - `, connection.connectionState, event);
});
connection.addEventListener('icecandidateerror', (event) => {
if (event.errorCode === 701) {
if (connection.icegatheringstate === 'gathering') {
console.log(`media-agent - Unable to reach host: ${event.url}`);
} else {
console.error(`media-agent - icecandidateerror - `, event.errorCode, event.hostcandidate, event.url, event.errorText);
}
}
});
connection.onicecandidate = (event) => {
if (!event.candidate) {
console.log(`media-agent - icecanditate - gathering is complete: ${connection.connectionState}`);
return;
}
/* If a srflx candidate was found, notify that the STUN server works! */
if (event.candidate.type === "srflx"){
console.log("media-agent - The STUN server is reachable!");
console.log(`media-agent - Your Public IP Address is: ${event.candidate.address}`);
}
/* If a relay candidate was found, notify that the TURN server works! */
if (event.candidate.type === "relay"){
console.log("media-agent - The TURN server is reachable !");
}
console.log(`media-agent - onicecandidate - `, event.candidate);
sendMessage({
type: 'relayICECandidate',
config: {
peer_id,
candidate: event.candidate
}
});
};
connection.ontrack = e => refOnTrack.current(e);
/* Add our local stream */
connection.addStream(stream);
/* Only one side of the peer connection should create the
* offer, the signaling server picks one to be the offerer.
* The other user will get a 'sessionDescription' event and will
* create an offer, then send back an answer 'sessionDescription'
* to us
*/
if (config.should_create_offer) {
if (debug) console.log(`media-agent - Creating RTC offer to ${peer_id}`);
return connection.createOffer()
.then((local_description) => {
if (debug) console.log(`media-agent - Local offer description is: `, local_description);
return connection.setLocalDescription(local_description)
.then(() => {
sendMessage({
type: 'relaySessionDescription',
config: {
peer_id,
'session_description': local_description
}
});
if (debug) console.log(`media-agent - Offer setLocalDescription succeeded`);
})
.catch((error) => {
console.error(`media-agent - Offer setLocalDescription failed!`);
});
})
.catch((error) => {
console.log(`media-agente - Error sending offer: `, error);
});
}
}
const sessionDescription = ({ peer_id, session_description }) => {
const peer = peers[peer_id];
if (!peer) {
console.error(`media-agent - sessionDescription - No peer for ${peer_id}`);
return;
}
const { connection } = peer;
const desc = new RTCSessionDescription(session_description);
return connection.setRemoteDescription(desc, () => {
if (debug) console.log(`media-agent - sessionDescription - setRemoteDescription succeeded`);
if (session_description.type === "offer") {
if (debug) console.log(`media-agent - sessionDescription - Creating answer`);
connection.createAnswer((local_description) => {
if (debug) console.log(`media-agent - sessionDescription - Answer description is: `, local_description);
connection.setLocalDescription(local_description, () => {
sendMessage({
type: 'relaySessionDescription',
config: {
peer_id,
session_description: local_description
}
});
if (debug) console.log(`media-agent - sessionDescription - Answer setLocalDescription succeeded`);
}, () => {
console.error(`media-agent - sessionDescription - Answer setLocalDescription failed!`);
});
}, (error) => {
console.error(error);
});
}
}, (error) => {
console.log(`media-agent - sessionDescription - setRemoteDescription error: `, error);
});
};
const removePeer = ({peer_id}) => {
console.log(`media-agent - removePeer - Signaling server said to remove peer ${peer_id}`);
if (peer_id in peers) {
if (peers[peer_id].connection) {
peers[peer_id].connection.close();
peers[peer_id].connection = undefined;
}
}
/* To maintain mute/videoOn states, we don't remove the peer but
* instead mark it as dead */
peers[peer_id].dead = true;
if (debug) console.log(`media-agent - removePeer`, peers);
setPeers(Object.assign({}, peers));
};
const iceCandidate = ({ peer_id, candidate }) => {
/**
* The offerer will send a number of ICE Candidate blobs to the
* answerer so they can begin trying to find the best path to one
* another on the net.
*/
const peer = peers[peer_id];
if (!peer) {
console.error(`media-agent - iceCandidate - No peer for ${peer_id}`, peers);
return;
}
peer.connection.addIceCandidate(new RTCIceCandidate(candidate))
.then(() => {
if (debug) console.log(`media-agent - iceCandidate - Successfully added Ice Candidate for ${peer_id}`);
})
.catch((error) => {
console.error(error, peer, candidate);
});
};
const data = JSON.parse(event.data);
if (data.type in [ 'addPeer', 'removePeer', 'iceCandidate', 'sessionDescription' ]) {
console.log(`media-agent - message - ${data.type}`, peers);
}
switch (data.type) {
case 'addPeer': addPeer(data.data); break;
case 'removePeer': removePeer(data.data); break;
case 'iceCandidate': iceCandidate(data.data); break;
case 'sessionDescription': sessionDescription(data.data); break;
default: break;
}
}, [ peers, setPeers, stream, refOnTrack, sendMessage ]);
const refWsMessage = useRef(onWsMessage);
const onWsClose = (event) => {
console.log(`media-agent - ${name} Disconnected from signaling server`);
/* Tear down all of our peer connections and remove all the
* media divs when we disconnect */
for (let peer_id in peers) {
if (peers[peer_id].local) {
continue;
}
peers[peer_id].connection.close();
peers[peer_id].connection = undefined;
}
for (let id in peers) {
peers[id].dead = true;
peers[id].connection = undefined;
}
if (debug) console.log(`media-agent - close`, peers);
setPeers(Object.assign({}, peers));
}
const refWsClose = useRef(onWsClose);
useEffect(() => {
refWsMessage.current = onWsMessage;
refWsClose.current = onWsClose;
refOnTrack.current = onTrack;
});
useEffect(() => {
if (!ws) {
return;
}
console.log(`media-control - Binding to WebSocket`);
const cbMessage = e => refWsMessage.current(e);
ws.addEventListener('message', cbMessage);
const cbClose = e => refWsClose.current(e);
ws.addEventListener('close', cbClose);
return () => {
ws.removeEventListener('message', cbMessage);
ws.removeEventListener('close', cbClose);
}
}, [ws, refWsMessage, refWsClose ]);
useEffect(() => {
console.log(`media-control - WebSocket or Stream changed`);
const join = () => {
sendMessage({ type: 'join' });
}
if (ws && stream) {
console.log(`media-conterol - issuing join request`);
for (let peer in peers) {
if (peers[peer].local && peers[peer].dead) {
/* Allocate a new Object so <MediaControl> will trigger */
peers[peer] = Object.assign({}, peers[peer]);
delete peers[peer].dead;
setPeers(Object.assign({}, peers));
}
}
join();
}
}, [ws, stream, peers, setPeers, sendMessage]);
useEffect(() => {
if (!name) {
return;
}
let update = false;
if (stream) {
if (!(name in peers)) {
update = true;
peers[name] = {
name: name,
local: true,
muted: true,
videoOn: false,
attributes: {
local: true,
srcObject: stream
}
};
}
}
/* Renaming the local connection requires the peer to be deleted
* and re-established with the signaling server */
for (let key in peers) {
if (peers[key].local && key !== name) {
delete peers[key];
update = true;
}
}
if (update) {
if (debug) console.log(`media-agent - Setting global peers`, peers);
setPeers(Object.assign({}, peers));
}
}, [peers, name, setPeers, stream]);
useEffect(() => {
if (!ws || !name) {
return;
}
const setup_local_media = () => {
/* Ask user for permission to use the computers microphone and/or camera,
* attach it to an <audio> or <video> tag if they give us access. */
console.log("media-agent - Requesting access to local audio / video inputs");
navigator.getUserMedia = (navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia);
return navigator.mediaDevices
.getUserMedia({audio: true, video: true})
.catch((error) => {
console.log(`media-agent - Access granted to audio and video ` +
`failed. Trying just audio.`);
return navigator.mediaDevices
.getUserMedia({ audio: true, video: false })
})
.then((media) => { /* user accepted access to a/v */
console.log("media-agent - Access granted to audio/video");
media.getVideoTracks().forEach((track) => {
track.applyConstraints({
"video": {
"width": {
"min": 160,
"max": 320
},
"height": {
"min": 120,
"max": 240
}
}
});
});
return media;
});
};
let abort = false;
if (!stream) {
if (debug) console.log(`media-agent - WebSocket open request. Attempting to create local media.`);
setup_local_media().then((media) => {
/* once the user has given us access to their
* microphone/camcorder, join the channel and start peering up */
if (abort) {
console.log(`media-agent - aborting setting local media`);
} else {
setStream(media);
}
}).catch((error) => { /* user denied access to a/v */
console.error(error);
console.log("media-agent - Access denied for audio/video");
});
}
return () => {
abort = true;
if (!stream) {
console.log(`media-agent - abort media setup!`);
}
};
}, [ws, setStream, stream, name, sendMessage]);
return <></>;
}
const MediaControl = ({isSelf, peer, className}) => {
const [media, setMedia] = useState(undefined);
const [muted, setMuted] = useState(undefined);
const [videoOn, setVideoOn] = useState(undefined);
/* local state is used to trigger re-renders, and the global
* state is kept up to date in the peers object so re-assignment
* of sessions doesn't kill the peer or change the mute/video states */
useEffect(() => {
if (!peer) {
setMedia(undefined);
return;
}
setMuted(peer.muted);
setVideoOn(peer.videoOn);
setMedia(peer);
}, [peer, setMedia, setMuted, setVideoOn]);
console.log(`media-control - render`);
const toggleMute = (event) => {
if (debug) console.log(`media-control - toggleMute - ${peer.name}`, !muted);
peer.muted = !muted;
setMuted(peer.muted);
event.stopPropagation();
}
const toggleVideo = (event) => {
if (debug) console.log(`media-control - toggleVideo - ${peer.name}`, !videoOn);
peer.videoOn = !videoOn;
setVideoOn(peer.videoOn);
event.stopPropagation();
}
useEffect(() => {
if (!media || media.dead || !peer) {
return;
}
if (media.attributes.srcObject) {
console.log(`media-control - audio enable - ${peer.name}:${!muted}`);
media.attributes.srcObject.getAudioTracks().forEach((track) => {
track.enabled = !muted;
});
}
}); /* run after every render to hit when ontrack has received and set
* the stream //, [media, muted]); */
useEffect(() => {
if (!media || media.dead || !peer) {
return;
}
if (media.attributes.srcObject) {
console.log(`media-control - video enable - ${peer.name}:${videoOn}`);
media.attributes.srcObject.getVideoTracks().forEach((track) => {
track.enabled = videoOn;
});
}
}); /* run after every render to hit when ontrack has received and set
* the stream //, [media, videoOn]); */
const isValid = media && !media.dead,
color = isValid ? 'primary' : 'disabled';
return <div className={`MediaControl ${className}`}>
<div className="Controls" >
{ isSelf && <div onClick={toggleMute}>
{ muted && <MicOff color={color}/> }
{ !muted && <Mic color={color}/> }
</div> }
{ !isSelf && <div onClick={toggleMute}>
{ muted && <VolumeOff color={color}/> }
{ !muted && <VolumeUp color={color}/> }
</div> }
<div onClick={toggleVideo}>
{ !videoOn && <VideocamOff color={color}/> }
{ videoOn && <Videocam color={color}/> }
</div>
</div>
{ isValid && <Video className="Video"
autoPlay='autoplay'
{...media.attributes}/>
}
{ !isValid && <video className="Video"></video> }
</div>;
};
export { MediaControl, MediaAgent };