iosnode.jstwiliomobile-safarivideocall

Twilio Video Call issue on iOS Safari, After call start video freeze nodejs issue


I have created a video call application using Nodejs & Twilio CLI. And using this in my both mobile app Android & iOS. On Android is working perfectly. But on iOS, there is an issue, when users reach the video call page, it's showing preview but as the user clicks on the Join Room button, then his/her video stops and just showing a black screen. While he can talk with other users and can see the video of them. And the Second user also can see his/her video perfectly. Only the issue he/she can't see his/her video on that call.

My html code

<!DOCTYPE html>
<html>
<head>
<style>
    .joinbtn {
        border: none;
        padding: 10px 10px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        margin: 4px 2px;
        cursor: pointer;
        background-color: #2b96cc;
        color: #fff;
    }
    .stvbtn {
        border: none;
        padding: 10px 10px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        margin: 4px 2px;
        cursor: pointer;
        background-color: #2b96cc;
        color: #fff;
    }
    .endbtn {
        float:right;
        border: none;
        padding: 10px 10px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 14px;
        margin: 4px 2px;
        cursor: pointer;
        background-color: #dc3545;
        color: #fff;
    }
    @media screen and (max-width: 820px) {
        video {
            object-fit: cover;
            width: 100%;
            height: 47vh;
        }
    }
    @media screen and (min-width: 821px){
        video {
            object-fit: contain;
        }
    }
    .connect_btn{
        display: flex;
        justify-content: center;
        align-content: space-around;
        margin-top: -50px;
        opacity: 0.8;
        padding-bottom:8px;
    }
    button.endbtn:disabled, button.joinbtn:disabled {
       background-color: #607d8b;
        color: #ffffff;
    }
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/6.4.0/adapter.js" type="text/javascript"></script>
<script src="webrtc.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Clifix Video Chat</title>
</head>
<body>
    <div id="room-controls">
        <video id="video" autoplay muted playsinline loop width="100%"></video>
        <div class="connect_btn">
            <label for="passcode"></label>
            <input id="passcode" type="hidden" value="8514"/>
            <!--button class="stvbtn" id="start-video" onclick="viplay()">On/Off</button-->
            <button class="joinbtn" id="button-join">Join Room</button>
            <button class="endbtn" id="button-leave" disabled="disabled">End Call</button>
        </div>
    </div>
    <!-- EDIT_CODE -->
    <script src="//media.twiliocdn.com/sdk/js/video/releases/2.3.0/twilio-video.min.js"></script>
    <script src="index.js"></script>
</body>
</html>

My nodejs code:

'use strict';
(() => {
  //const ROOM_NAME = 'demo';
  var urltemp = location.search;
  var array = urltemp.split('?');
  var array1 = array[1];
  var array2 = array1.split('=');
  var id = array2[1];
  const ROOM_NAME = id;
  const Video = Twilio.Video;
  let videoRoom, localStream;
  const video = document.getElementById('video');
    
  // preview screen
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: true })
    .then((vid) => {
      video.srcObject = vid;
      localStream = vid;
    });

  // buttons
  const joinRoomButton = document.getElementById('button-join');
  const leaveRoomButton = document.getElementById('button-leave');
  joinRoomButton.onclick = () => {
    //video.play();
    // get access token
    fetch(`video-token?passcode=${getPasscode()}&room=${ROOM_NAME}`)
      .then((resp) => {
        if (resp.ok) {
            var url=window.location.href,
            separator = (url.indexOf("?")===-1)?"?":"&",
            newParam=separator + "join=true";
            var newUrl=url.replace(newParam,"");
            newUrl+=newParam;
            window.history.replaceState(null,null,newUrl);
          return resp.json();
        } else {
          console.error(resp);
          if (resp.status === 401) {
            throw new Error('Go Back & Join Again');
          } else {
            throw new Error('Unexpected error. Open dev tools for logs');
          }
        }
      })
      .then((body) => {
        const token = body.token;
        //console.log(token);
        //connect to room
        return Video.connect(token, { name: ROOM_NAME });
      })
      .then((room) => {
        //console.log(`Connected to Room ${room.name}`);
        videoRoom = room;

        room.participants.forEach(participantConnected);
        room.on('participantConnected', participantConnected);

        room.on('participantDisconnected', participantDisconnected);
        room.once('disconnected', (error) =>
          room.participants.forEach(participantDisconnected)
        );
        joinRoomButton.disabled = true;
        leaveRoomButton.disabled = false;
      })
      .catch((err) => {
        alert(err.message);
      });
  };
  // leave room
  leaveRoomButton.onclick = () => {
      var url=window.location.href,
            separator = (url.indexOf("?")===-1)?"?":"&",
            newParam=separator + "end=true";
            var newUrl=url.replace(newParam,"");
            newUrl+=newParam;
            window.history.replaceState(null,null,newUrl);
    videoRoom.disconnect();
    //console.log(`Disconnected from Room ${videoRoom.name}`);
    joinRoomButton.disabled = false;
    leaveRoomButton.disabled = true;
  };
})();

const getPasscode = () => {
  const passcodeInput = document.getElementById('passcode') || {};
  const passcode = passcodeInput.value;
  passcodeInput.value = '';

  return passcode;
};

// connect participant
const participantConnected = (participant) => {
  //console.log(`Participant ${participant.identity} connected'`);

  const div = document.createElement('div'); //create div for new participant
  div.id = participant.sid;

  participant.on('trackSubscribed', (track) => trackSubscribed(div, track));
  participant.on('trackUnsubscribed', trackUnsubscribed);
  participant.tracks.forEach((publication) => {
    if (publication.isSubscribed) {
      trackSubscribed(div, publication.track);
    }
  });
  document.body.appendChild(div);
};

const participantDisconnected = (participant) => {
  //console.log(`Participant ${participant.identity} disconnected.`);
  document.getElementById(participant.sid).remove();
};

const trackSubscribed = (div, track) => {
  div.appendChild(track.attach());
};

const trackUnsubscribed = (track) => {
  track.detach().forEach((element) => element.remove());
};

As per my understanding, before this my video was not working on iOS safari then I have done modifications in my HTML video code.

From this:

<video id="video" autoplay muted width="100%"></video>

To:

<video id="video" autoplay muted playsinline loop width="100%"></video>

Then it starts working as having video freezing at the iOS User side when he/she start calling.


Solution

  • Twilio developer evangelist here.

    When you call Video.connect the Video SDK will ask for permission to use your microphone and camera. Safari does not like giving access to the microphone and camera more than once at a time and since you also ask for media access to show the preview, it drops the preview tracks and creates new tracks for the video call. This is why the preview goes dark, but other participants can see and hear the video/audio.

    Instead, you should reuse the tracks that you got for the preview by storing a reference to them and then passing them to Video.connect as the tracks property in the ConnectOptions. You already store a reference to the localStream so you can use that when you get to connect, like this:

    return Video.connect(token, {
      name: ROOM_NAME,
      tracks: localStream.getTracks()
    });
    

    That way the tracks for the preview will be re-used for the video call and nothing should go dark.