Skip to content

Instantly share code, notes, and snippets.

@digitallysavvy
Last active May 16, 2020 01:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save digitallysavvy/50e9a10adc4e2f995c7bd50e8891a43b to your computer and use it in GitHub Desktop.
Save digitallysavvy/50e9a10adc4e2f995c7bd50e8891a43b to your computer and use it in GitHub Desktop.
A bare bones implementation of the Agora RTC and RTM Web SDKs for use within the Agora WebXR tutorial.
// Agora settings
const agoraAppId = ''; // insert Agora AppID here
const channelName = 'WebAR';
var streamCount = 0;
// video profile settings
var cameraVideoProfile = '720p_6'; // 960 × 720 @ 30fps & 750kbs
// set log level:
// -- .DEBUG for dev
// -- .NONE for prod
AgoraRTC.Logger.setLogLevel(AgoraRTC.Logger.DEBUG);
// keep track of streams
var localStreams = {
uid: '',
camera: {
camId: '',
micId: '',
stream: {}
},
screen: {
id: '',
stream: {}
},
rtmActive: false
};
// keep track of devices
var devices = {
cameras: [],
mics: []
}
// setup the RTM client and channel
const rtmClient = AgoraRTM.createInstance(agoraAppId);
const rtmChannel = rtmClient.createChannel(channelName);
rtmClient.on('ConnectionStateChange', (newState, reason) => {
console.log('on connection state changed to ' + newState + ' reason: ' + reason);
});
// event listener for receiving a channel message
rtmChannel.on('ChannelMessage', ({ text }, senderId) => {
// text: text of the received channel message; senderId: user ID of the sender.
console.log('AgoraRTM msg from user ' + senderId + ' recieved: \n' + text);
// [TODO]: Handle RTM msg
});
// create RTC client
var rtcClient = AgoraRTC.createClient({mode: 'live', codec: 'vp8'}); // vp8 to work across mobile devices
rtcClient.init(agoraAppId, () => {
console.log('AgoraRTC client initialized');
joinChannel(); // join channel upon successfull init
}, function (err) {
console.log('[ERROR] : AgoraRTC client init failed', err);
});
rtcClient.on('stream-published', function (evt) {
console.log('Publish local stream successfully');
});
// connect remote streams
rtcClient.on('stream-added', (evt) => {
const stream = evt.stream;
const streamId = stream.getId();
console.log('New stream added: ' + streamId);
console.log('Subscribing to remote stream:' + streamId);
// Subscribe to the remote stream
rtcClient.subscribe(stream, (err) => {
console.log('[ERROR] : subscribe stream failed', err);
});
streamCount++; // Increase count of Active Stream Count
createBroadcaster(streamId); // Load 3D model with video texture
});
rtcClient.on('stream-removed', (evt) => {
const stream = evt.stream;
stream.stop(); // stop the stream
stream.close(); // clean up and close the camera stream
console.log('Remote stream is removed ' + stream.getId());
});
rtcClient.on('stream-subscribed', (evt) => {
const remoteStream = evt.stream;
const remoteId = remoteStream.getId();
console.log('Successfully subscribed to remote stream: ' + remoteStream.getId());
// get the designated video element and connect it to the remoteStream
var video = document.getElementById('faceVideo-' + remoteId);
connectStreamToVideo(remoteStream, video);
});
// remove the remote-container when a user leaves the channel
rtcClient.on('peer-leave', (evt) => {
console.log('Remote stream has left the channel: ' + evt.uid);
evt.stream.stop(); // stop the stream
const remoteId = evt.stream.getId();
// Remove the 3D and Video elements that were created
document.getElementById(remoteId).remove();
document.getElementById('faceVideo-' + remoteId).remove();
streamCount--; // Decrease count of Active Stream Count
});
// show mute icon whenever a remote has muted their mic
rtcClient.on('mute-audio', (evt) => {
console.log('mute-audio for: ' + evt.uid);
});
rtcClient.on('unmute-audio', (evt) => {
console.log('unmute-audio for: ' + evt.uid);
});
// show user icon whenever a remote has disabled their video
rtcClient.on('mute-video', (evt) => {
console.log('mute-video for: ' + evt.uid);
});
rtcClient.on('unmute-video', (evt) => {
console.log('unmute-video for: ' + evt.uid);
});
// join a channel
function joinChannel() {
const token = generateToken();
// set the role
rtcClient.setClientRole('audience', () => {
console.log('Client role set to audience');
}, (e) => {
console.log('setClientRole failed', e);
});
rtcClient.join(token, channelName, 0, (uid) => {
console.log('User ' + uid + ' join channel successfully');
localStreams.uid = uid
createBroadcaster(uid); // Load 3D model with video texture
createCameraStream(uid); // Create the camera stream
joinRTMChannel(uid); // join the RTM channel
}, (err) => {
console.log('[ERROR] : join channel failed', err);
});
}
function leaveChannel() {
rtcClient.leave(() => {
console.log('client leaves channel');
localStreams.camera.stream.stop() // stop the camera stream playback
localStreams.camera.stream.close(); // clean up and close the camera stream
rtcClient.unpublish(localStreams.camera.stream); // unpublish the camera stream
//disable the UI elements
$('#mic-btn').prop('disabled', true);
$('#video-btn').prop('disabled', true);
$('#exit-btn').prop('disabled', true);
}, (err) => {
console.log('client leave failed ', err); //error handling
});
}
// video streams for channel
function createCameraStream(uid) {
const localStream = AgoraRTC.createStream({
streamID: uid,
audio: true,
video: true,
screen: false
});
localStream.setVideoProfile(cameraVideoProfile);
// The user has granted access to the camera and mic.
localStream.on('accessAllowed', () => {
if(devices.cameras.length === 0 && devices.mics.length === 0) {
console.log('[DEBUG] : checking for cameras & mics');
getCameraDevices();
getMicDevices();
}
console.log('accessAllowed');
});
// The user has denied access to the camera and mic.
localStream.on('accessDenied', () => {
console.log('accessDenied');
});
localStream.init(() => {
console.log('getUserMedia successfully');
// Coonect the local stream video to the video texture
var video = document.getElementById('faceVideo-' + uid);
connectStreamToVideo(localStream, video);
enableUiControls(localStream);
// publish local stream
rtcClient.publish(localStream, (err) => {
console.log('[ERROR] : publish local stream error: ' + err);
});
// keep track of the camera stream for later
localStreams.camera.stream = localStream;
}, (err) => {
console.log('[ERROR] : getUserMedia failed', err);
});
}
function createBroadcaster(streamId) {
// [TODO]: Load 3D model with video texture
// create video element
// add video element to the DOM
// configure the new broadcaster element
// create the broadcaster
// add broadcaster to the scene
// add event listener for model loaded:
// - search the mesh's children for the face-geo
// - create video texture from video element
// - set node's material map to video texture
}
function connectStreamToVideo(agoraStream, video) {
// [TODO]: Coonect the local stream video to the video texture
}
function changeStreamSource (deviceIndex, deviceType) {
console.log('Switching stream sources for: ' + deviceType);
var deviceId;
if (deviceType === 'video') {
deviceId = devices.cameras[deviceIndex].deviceId
} else if(deviceType === 'audio') {
deviceId = devices.mics[deviceIndex].deviceId;
}
localStreams.camera.stream.switchDevice(deviceType, deviceId, () => {
console.log('successfully switched to new device with id: ' + JSON.stringify(deviceId));
// set the active device ids
if(deviceType === 'audio') {
localStreams.camera.micId = deviceId;
} else if (deviceType === 'video') {
localStreams.camera.camId = deviceId;
} else {
console.log('unable to determine deviceType: ' + deviceType);
}
}, () => {
console.log('failed to switch to new device with id: ' + JSON.stringify(deviceId));
});
}
function joinRTMChannel(uid){
console.log('uid:')
console.log(uid)
rtmClient.login({ token: null, uid: String(uid) }).then(() => {
console.log('AgoraRTM client login success');
// join a channel and send a message
rtmChannel.join().then(() => {
// join-channel success
localStreams.rtmActive = true
console.log('RTM Channel join success');
addCameraListener();
}).catch(error => {
// join-channel failure
console.log('failed to join channel for error: ' + error);
});
}).catch(err => {
console.log('AgoraRTM client login failure', err);
});
}
function sendChannelMessage(state, direction){
if (localStreams.rtmActive) {
// use a JSON object to send our instructions in a structured way
const jsonMsg = { };
// build the Agora RTM Message
const msg = {
description: undefined,
messageType: 'TEXT',
rawMessage: undefined,
text: JSON.stringify(jsonMsg)
};
rtmChannel.sendMessage(msg).then(() => {
// channel message-send success
console.log('sent msg success');
}).catch(error => {
// channel message-send failure
console.log('sent msg failure');
});
}
}
// helper methods
function getCameraDevices() {
console.log('Checking for Camera Devices.....')
rtcClient.getCameras ((cameras) => {
devices.cameras = cameras; // store cameras array
cameras.forEach((camera, i) => {
const name = camera.label.split('(')[0];
const optionId = 'camera_' + i;
const deviceId = camera.deviceId;
if(i === 0 && localStreams.camera.camId === ''){
localStreams.camera.camId = deviceId;
}
$('#camera-list').append('<a class=\'dropdown-item\' id= ' + optionId + '>' + name + '</a>');
});
$('#camera-list a').click((event) => {
const index = event.target.id.split('_')[1];
changeStreamSource (index, 'video');
});
});
}
function getMicDevices() {
console.log('Checking for Mic Devices.....')
rtcClient.getRecordingDevices((mics) => {
devices.mics = mics; // store mics array
mics.forEach((mic, i) => {
let name = mic.label.split('(')[0];
const optionId = 'mic_' + i;
const deviceId = mic.deviceId;
if(i === 0 && localStreams.camera.micId === ''){
localStreams.camera.micId = deviceId;
}
if(name.split('Default - ')[1] != undefined) {
name = '[Default Device]' // rename the default mic - only appears on Chrome & Opera
}
$('#mic-list').append('<a class=\'dropdown-item\' id= ' + optionId + '>' + name + '</a>');
});
$('#mic-list a').click((event) => {
const index = event.target.id.split('_')[1];
changeStreamSource (index, 'audio');
});
});
}
// use tokens for added security
function generateToken() {
return null; // TODO: add a token generation
}
function rotateModel(uid, direction, send) {
if (send) {
sendChannelMessage('rotation', direction)
}
var model = document.getElementById(uid)
if (direction === 'counter-clockwise') {
model.object3D.rotation.y += 0.1;
} else if (direction === 'clockwise') {
model.object3D.rotation.y -= 0.1;
}
}
function moveModel(uid, direction, send) {
if (send) {
sendChannelMessage('position', direction)
}
var model = document.getElementById(uid)
switch (direction){
case 'forward':
model.object3D.position.z += 0.1
break;
case 'backward':
model.object3D.position.z -= 0.1
break;
case 'left':
model.object3D.position.x -= 0.1
break;
case 'right':
model.object3D.position.x += 0.1
break;
default:
console.log('Unable to determin direction: ' + direction);
}
}
// UI
function toggleBtn(btn){
btn.toggleClass('btn-dark').toggleClass('btn-danger');
}
function toggleScreenShareBtn() {
$('#screen-share-btn').toggleClass('btn-danger');
$('#screen-share-icon').toggleClass('fas').toggleClass('fab').toggleClass('fa-slideshare').toggleClass('fa-times-circle');
}
function toggleVisibility(elementID, visible) {
if (visible) {
$(elementID).attr('style', 'display:block');
} else {
$(elementID).attr('style', 'display:none');
}
}
function toggleMic() {
toggleBtn($('#mic-btn')); // toggle button colors
toggleBtn($('#mic-dropdown'));
$('#mic-icon').toggleClass('fa-microphone').toggleClass('fa-microphone-slash'); // toggle the mic icon
if ($('#mic-icon').hasClass('fa-microphone')) {
localStreams.camera.stream.unmuteAudio(); // enable the local mic
} else {
localStreams.camera.stream.muteAudio(); // mute the local mic
}
}
function toggleVideo() {
toggleBtn($('#video-btn')); // toggle button colors
toggleBtn($('#cam-dropdown'));
if ($('#video-icon').hasClass('fa-video')) {
localStreams.camera.stream.muteVideo(); // enable the local video
console.log('muteVideo');
} else {
localStreams.camera.stream.unmuteVideo(); // disable the local video
console.log('unMuteVideo');
}
$('#video-icon').toggleClass('fa-video').toggleClass('fa-video-slash'); // toggle the video icon
}
function enableUiControls() {
$('#mic-btn').prop('disabled', false);
$('#video-btn').prop('disabled', false);
$('#exit-btn').prop('disabled', false);
$('#mic-btn').click(() => {
toggleMic();
});
$('#video-btn').click(() => {
toggleVideo();
});
$('#exit-btn').click(() => {
console.log('so sad to see you leave the channel');
leaveChannel();
});
// keyboard listeners
$(document).keypress((e) => {
switch (e.key) {
case 'm':
console.log('squick toggle the mic');
toggleMic();
break;
case 'v':
console.log('quick toggle the video');
toggleVideo();
break;
case 'q':
console.log('so sad to see you quit the channel');
leaveChannel();
break;
case 'r':
rotateModel(localStreams.uid, 'counter-clockwise', true)
break;
case 'e':
rotateModel(localStreams.uid, 'clockwise', true)
break;
case 'd':
// move the model forward
moveModel(localStreams.uid, 'forward', true)
break;
case 'x':
// move the model backward
moveModel(localStreams.uid, 'backward', true)
break;
case 'z':
// move the model left
moveModel(localStreams.uid, 'left', true)
break;
case 'c':
// move the model right
moveModel(localStreams.uid, 'right', true)
break;
default: // do nothing
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment