Video chat with co-editing capabilities using Twilio Sync

, , , . , . , Frontend-, , Twilio. Github, , , — .






:





  • Twilio. , 10 Twilio .





  • Node.js ( 14.16.1 ) npm.





  • .





, , . , 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>. , . , , .





, Google Docs Notion. . —





, , — . 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/ . , , , :





, , , .





?

: . , GitHub.





. , , , — , . -, Frontend-, , Fullstack- Python, , .





,      :





  • Data Scientist





  • Data Analyst





  •  Data Engineering









  • Fullstack-  Python





  • Java-





  • QA-  JAVA





  • Frontend-









  • C++





  •  Unity





  • -





  • iOS-  





  • Android-  









  •  Machine Learning





  • «Machine Learning  Deep Learning»





  • « Data Science»





  • «  Machine Learning Data Science»





  • «Python -»





  • «   »





  •  





  •  DevOps








All Articles