LoginSignup
11

More than 5 years have passed since last update.

[memo] WebRTC + dataChannel + VideoStream の接続フロー

Posted at

生 WebRTC API で dataChannel での文字列チャットの接続フロー、何回か調べてる気がするので張っておく。
流れを思い出すためのものなので、適当に動くだけで細かい例外処理や、ハンドリングしてないイベントや、構造化はしてない。ちょっといじれば Video も送れる。一対一しかできない。

かける側と受ける側の peer は別オブジェクトにしてるのも、流れを思い出しやすくするため。

サーバ側は socket.io で適当に返している。

client.js

function prittysdp(sdp) {
  var text = 'type:' + sdp.type + '\n';
  text += sdp.sdp;
  return text
}

function prittyice(ice) {
  var text = JSON.stringify(ice, null, "    ");
  return text;
}

var config = {
  media: {
    video: true,
    audio: false
  },
  peer: {
    iceServers: []
  },
  rtcoption: {
    mandatory: {
      OfferToReceiveAudio:true,
      OfferToReceiveVideo:true
    }
  },
  channel: {
    ordered: false,
    maxRetransmitTime: 3000
  }
}

// サーバに接続(localhost)
var socket = io.connect();

var requester = null;
var responser = null;
var localStream = null;

function chat(dataChannel) {
  var $chatView = $('#chatView');
  var $chatInput = $('#chatInput');
  var $chatSubmit = $('#chatSubmit');

  dataChannel.onopen = function () {
    console.log('chat open');
    $chatInput.prop('disabled', false);
  };

  dataChannel.onmessage = function (e) {
    $chatView.val($chatView.val() + e.data + '\n');
    console.log("Got Data Channel Message:", e.data);
  };

  dataChannel.onerror = function (error) {
    $chatInput.prop('disabled', true);
    console.log("Data Channel Error:", error);
  };

  dataChannel.onclose = function () {
    $chatInput.prop('disabled', true);
    console.log("The Data Channel is Closed");
  };

  $chatSubmit.click(function() {
    var message = $chatInput.val();
    dataChannel.send(message);
    $chatView.val($chatView.val() + message + '\n');
  });
}

$(function() {
  var $localVideo = $('#local-video').get(0);
  var $remoteVideo = $('#remote-video').get(0);

  socket.on('offer', function(offer) {
    // create peer responser
    responser = new webkitRTCPeerConnection(config.responser);
    responser.setRemoteDescription(new RTCSessionDescription(offer));

    // add stream
    if(localStream) {
      responser.addStream(localStream);
    }
    responser.onaddstream = function(e) {
      $remoteVideo.src = URL.createObjectURL(e.stream);
    }
    responser.onremovestream = function(e) {
      $remoteVideo.src = '';
    }

    // candidate
    responser.onicecandidate = function(ice) {
      if (ice.candidate) {
        console.log(prittyice(ice.candidate));
        socket.emit('ice', ice.candidate);
      } else {
        console.log('==END CANDIDATE==');
      }
    }

    // answer
    responser.createAnswer(function success(ans) {
      responser.setLocalDescription(ans);

      responser.ondatachannel = function(e) {
        // data channel
        var dataChannel = e.channel;
        chat(dataChannel);

      };
      console.log(prittysdp(ans));
      socket.emit('answer', ans);
    }, console.error, config.rtcoption);
  });

  socket.on('answer', function(sdp) {
    requester.setRemoteDescription(new RTCSessionDescription(sdp));
  });

  socket.on('ice', function(ice) {
    var candidate = new RTCIceCandidate(ice);
    if (requester) {
      requester.addIceCandidate(candidate);
    } else {
      responser.addIceCandidate(candidate);
    }
  });

  socket.on('stop', function() {
    $('#stop').click();
  });

  $('#connect').click(function() {
    // create peer requester
    requester = new webkitRTCPeerConnection(config.requester);

    // data channel
    var dataChannel = requester.createDataChannel('RTCDataChannel', config.channel);
    chat(dataChannel);

    // add stream
    if (localStream) {
      requester.addStream(localStream);
    }
    requester.onaddstream = function(e) {
      $remoteVideo.src = URL.createObjectURL(e.stream);
    }
    requester.onremovestream = function(e) {
      $remoteVideo.src = '';
    }

    // candidate
    requester.onicecandidate = function(ice) {
      if (ice.candidate) {
        console.log(prittyice(ice.candidate));
        socket.emit('ice', ice.candidate);
      } else {
        console.log('==END CANDIDATE==');
      }
    }

    // offer
    requester.createOffer(function success(offer) {
      console.log(prittysdp(offer));
      requester.setLocalDescription(offer);
      socket.emit('offer', offer);
    }, console.error, config.rtcoption);
  });

  // stop
  $('#stop').click(function() {
    $localVideo.src = '';
    $remoteVideo.src = '';
    if (localStream) {
      localStream.stop();
    }
    socket.disconnect();
    if (requester) {
      requester.close();
      requester = null;
    } else if (responser) {
      responser.close();
      responser = null;
    }
  });

  $('#video').click(function() {
    // start video
    navigator.webkitGetUserMedia(config.media, function success(stream) {
      localStream = stream;
      $localVideo.src = URL.createObjectURL(stream);
      $localVideo.play();
      $localVideo.volume = 0;
    }, console.error);
  });
});

server.js

// Express
var express = require('express')
  , http = require('http')
  , app = express();

app.use(express.static(__dirname + '/public'));

var server = http.createServer(app).listen(3000);
console.log('server start:', 3000);

// Socket.IO
var io = require('socket.io')
  , io = io.listen(server);

io.sockets.on('connection', function(socket) {
  socket.on('offer', function(data) {
    socket.broadcast.emit('offer', data);
  });

  socket.on('answer', function(data) {
    socket.broadcast.emit('answer', data);
  });

  socket.on('ice', function(data) {
    socket.broadcast.emit('ice', data);
  });

  socket.on('disconnect', function(data) {
    socket.broadcast.emit('stop', true);
  });
});

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11