, , , . , . , Frontend-, , Twilio. Github, , , — .
:
, , . , start:
git clone -b start https://github.com/adjeim/video-note-collab.git
, :
cd video-note-collab
npm install
.env :
cp .env.template .env
.env :
TWILIO_ACCOUNT_SID TWILIO_SYNC_SERVICE_SID TWILIO_API_KEY_SID TWILIO_API_KEY_SECRET
Twilio, Twilio Sync Service Twilio API Keys. TWILIO_SYNC_SERVICE_SID SID . , , Express:
npm start
http://localhost:3000/ , :
, . , . http://localhost:3000/ , , , , :
? Twilio Sync . Sync, notepad. , , Sync . , <textarea>. , . , , .
, , — . public/index.html , , Tailwind CSS Twilio Sync . <head>, Twilio Video, :
<html>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<link href='https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css' rel='stylesheet'>
<script type="text/javascript" src="https://media.twiliocdn.com/sdk/js/sync/v2.0/twilio-sync.min.js"></script>
<script src='https://sdk.twilio.com/js/video/releases/2.15.0/twilio-video.min.js'></script>
<title>Video Collaboration with Notes</title>
</head>
<body>. <form>, , .
<body class='bg-grey-100 p-10 flex flex-wrap container'>
<form id='login' class='w-full max-h-20 flex items-center py-2'>
<input class='appearance-none bg-transparent border-b border-green-500 mr-3 py-1 px-2 focus:outline-none'
id='identity' type='text' placeholder='Enter your name...' required>
<button id='joinOrLeaveRoom' class='bg-green-500 hover:bg-green-700 text-white py-1 px-4 rounded' type='submit'>
Join Video Call
</button>
</form>
<textarea id='notepad' class='h-44 w-full shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2'></textarea>
<textarea>. .
</form>
<textarea disabled id='notepad' class='bg-gray-200 h-140 w-6/12 shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2'></textarea>
<textarea> <div> :
<textarea disabled id='notepad' class='bg-gray-200 h-140 w-6/12 shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2'></textarea>
<div id='container' class='w-5/12 bg-green-100'>
<div id='participantsContainer'>
<div id='localParticipant'>
<div id='localVideoTrack' class='participant'></div>
</div>
<div id='remoteParticipants'>
<!-- Remote participants will be added here as they join the call -->
</div>
</div>
</div>
, , .
index.js , , SyncGrant. , VideoGrant.
SyncGrant VideoGrant. , , JSON express.json():
const AccessToken = require('twilio').jwt.AccessToken;
const SyncGrant = AccessToken.SyncGrant;
const VideoGrant = AccessToken.VideoGrant;
app.use(express.json());
, POST. VideoGrant .
app.post('/token', async (req, res) => {
if (!req.body.identity || !req.body.room) {
return res.status(400);
}
// Get the user's identity from the request
const identity = req.body.identity;
// Create a 'grant' identifying the Sync service instance for this app.
const syncGrant = new SyncGrant({
serviceSid: process.env.TWILIO_SYNC_SERVICE_SID
});
// Create a video grant
const videoGrant = new VideoGrant({
room: req.body.room
})
// Create an access token which we will sign and return to the client,
// containing the grant we just created and specifying their identity.
const token = new AccessToken(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_API_KEY_SID,
process.env.TWILIO_API_KEY_SECRET,
);
token.addGrant(syncGrant);
token.addGrant(videoGrant);
token.identity = identity;
// Serialize the token to a JWT string and include it in a JSON response
res.send({
identity: identity,
token: token.toJwt()
});
});
. , , .
public/index.html . <script> . , , , . , , , «Join Video Call» . . notepad , :
<script>
const notepad = document.getElementById('notepad');
const localVideoTrack = document.getElementById('localVideoTrack');
const login = document.getElementById('login');
const identityInput = document.getElementById('identity');
const joinLeaveButton = document.getElementById('joinOrLeaveRoom');
const localParticipant = document.getElementById('localParticipant');
const remoteParticipants = document.getElementById('remoteParticipants');
let connected = false;
let room;
let syncDocument;
let twilioSyncClient;
, , . , , . , addLocalVideo , :
const addLocalVideo = async () => {
const videoTrack = await Twilio.Video.createLocalVideoTrack();
const trackElement = videoTrack.attach();
localVideoTrack.appendChild(trackElement);
};
, <script>:
addLocalVideo();
</script>
connectOrDisconnect, , «Join Video Call». , . , . addLocalVideo:
const connectOrDisconnect = async (event) => {
event.preventDefault();
if (!connected) {
const identity = identityInput.value;
joinLeaveButton.disabled = true;
joinLeaveButton.innerHTML = 'Connecting...';
try {
await connect(identity);
} catch (error) {
console.log(error);
alert('Failed to connect to video room.');
joinLeaveButton.innerHTML = 'Join Video Call';
joinLeaveButton.disabled = false;
}
}
else {
disconnect();
}
};
<script> , . connectOrDisconnect , :
// Add listener
notepad.addEventListener('keyup', (event) => {
// Define array of triggers to sync (space, enter, and punctuation)
// Otherwise sync will fire every time
const syncKeys = [32, 13, 8, 188, 190];
if (syncKeys.includes(event.keyCode)) {
syncNotepad(twilioSyncClient);
}
})
login.addEventListener('submit', connectOrDisconnect);
fetch('/token') , , , , :
const connect = async (identity) => {
const response = await fetch('/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({'identity': identity, room: 'My Video Room'})
});
const data = await response.json();
const token = data.token;
// Set up sync document
twilioSyncClient = new Twilio.Sync.Client(token);
notepad.disabled = false;
notepad.classList.remove('bg-gray-200');
syncDocument = await twilioSyncClient.document('notepad');
// Load the existing Document
notepad.value = syncDocument.data.content || '';
// Listen to updates on the Document
syncDocument.on('updated', (event) => {
// Update the cursor position
let cursorStartPos = notepad.selectionStart;
let cursorEndPos = notepad.selectionEnd;
notepad.value = event.data.content;
// Reset the cursor position
notepad.selectionEnd = cursorEndPos;
console.log('Received Document update event. New value:', event.data.content);
})
// Set up the video room
room = await Twilio.Video.connect(token);
const identityDiv = document.createElement('div');
identityDiv.setAttribute('class', 'identity');
identityDiv.innerHTML = identity;
localParticipant.appendChild(identityDiv);
room.participants.forEach(participantConnected);
room.on('participantConnected', participantConnected);
room.on('participantDisconnected', participantDisconnected);
connected = true;
joinLeaveButton.innerHTML = 'Leave Video Call';
joinLeaveButton.disabled = false;
identityInput.style.display = 'none';
};
, room. , , . «Join Video Call» «Leave Video Call», . , , :
const disconnect = () => {
room.disconnect();
let removeParticipants = remoteParticipants.getElementsByClassName('participant');
while (removeParticipants[0]) {
remoteParticipants.removeChild(removeParticipants[0]);
}
joinLeaveButton.innerHTML = 'Join Video Call';
connected = false;
identityInput.style.display = 'inline-block';
localParticipant.removeChild(localParticipant.lastElementChild);
syncDocument.close();
twilioSyncClient = null;
notepad.value = '';
notepad.disabled = true;
notepad.classList.add('bg-gray-200');
};
disconnect , «Leave Video Call». . false, , «Leave Video Call» «Join Video Call». , , - , .
, , . ParticipantConnected, <div> , <div>, .
, - . - , . participantConnected public/index.html:
const participantConnected = (participant) => {
const participantDiv = document.createElement('div');
participantDiv.setAttribute('id', participant.sid);
participantDiv.setAttribute('class', 'participant');
const tracksDiv = document.createElement('div');
participantDiv.appendChild(tracksDiv);
const identityDiv = document.createElement('div');
identityDiv.setAttribute('class', 'identity');
identityDiv.innerHTML = participant.identity;
participantDiv.appendChild(identityDiv);
remoteParticipants.appendChild(participantDiv);
participant.tracks.forEach(publication => {
if (publication.isSubscribed) {
trackSubscribed(tracksDiv, publication.track);
}
});
participant.on('trackSubscribed', track => trackSubscribed(tracksDiv, track));
participant.on('trackUnsubscribed', trackUnsubscribed);
};
ParticipantDisconnected , . , sid ( ) div DOM. participantDisconnected ParticipantConnected:
const participantDisconnected = (participant) => {
document.getElementById(participant.sid).remove();
};
, - . trackSubscribed trackUnsubscribed public/index.html participantDisconnected:
const trackSubscribed = (div, track) => {
const trackElement = track.attach();
div.appendChild(trackElement);
};
const trackUnsubscribed = (track) => {
track.detach().forEach(element => {
element.remove()
});
};
. .
http://localhost:3000/. , , :
«Join Video Call». . , . http://localhost:3000/ . , , , :
, , , .
?
. , , , — , . -, Frontend-, , Fullstack- Python, , .