peer-connection - ICE

webrtc点对点会话建立过程:https://blog.csdn.net/zhuiyuanqingya/article/details/84108763

本地Candidate收集

本地的IceCandidate收集过程起始于PeerConnection::SetLocalDescription,其中会启动收集

  // MaybeStartGathering needs to be called after posting
  // MSG_SET_SESSIONDESCRIPTION_SUCCESS, so that we don't signal any candidates
  // before signaling that SetLocalDescription completed.
  transport_controller_->MaybeStartGathering();

MaybeStartGathering具体实现是:

void JsepTransportController::MaybeStartGathering() {
  if (!network_thread_->IsCurrent()) {
    network_thread_->Invoke<void>(RTC_FROM_HERE,
                                  [&] { MaybeStartGathering(); });
    return;
  }

  for (auto& dtls : GetDtlsTransports()) {
    dtls->ice_transport()->MaybeStartGathering();
  }
}

在这里会在network_thread_启动收集函数,然后对每个DtlsTransports启动收集过程,转到P2PTransportChannel::MaybeStartGathering():

    if (pooled_session) {
      AddAllocatorSession(std::move(pooled_session));
      PortAllocatorSession* raw_pooled_session =
          allocator_sessions_.back().get();
      // Process the pooled session's existing candidates/ports, if they exist.
      OnCandidatesReady(raw_pooled_session,
                        raw_pooled_session->ReadyCandidates());
      for (PortInterface* port : allocator_sessions_.back()->ReadyPorts()) {
        OnPortReady(raw_pooled_session, port);
      }
      if (allocator_sessions_.back()->CandidatesAllocationDone()) {
        OnCandidatesAllocationDone(raw_pooled_session);
      }
    } else {
      AddAllocatorSession(allocator_->CreateSession(
          transport_name(), component(), ice_parameters_.ufrag,
          ice_parameters_.pwd));
      allocator_sessions_.back()->StartGettingPorts();
    }

然后启动port收集BasicPortAllocatorSession::StartGettingPorts()

void BasicPortAllocatorSession::StartGettingPorts() {
  RTC_DCHECK_RUN_ON(network_thread_);
  state_ = SessionState::GATHERING;
  if (!socket_factory_) {
    owned_socket_factory_.reset(
        new rtc::BasicPacketSocketFactory(network_thread_));
    socket_factory_ = owned_socket_factory_.get();
  }

  network_thread_->Post(RTC_FROM_HERE, this, MSG_CONFIG_START);

  RTC_LOG(LS_INFO) << "Start getting ports with turn_port_prune_policy "
                   << turn_port_prune_policy_;
}

此时的post消息是放到消息队列中MessageQueue::Post,会在this的BasicPortAllocatorSession::OnMessage中监听消息并进行处理:MSG_CONFIG_START->MSG_CONFIG_READY->MSG_ALLOCATE->MSG_SEQUENCEOBJECTS_CREATED

void BasicPortAllocatorSession::OnMessage(rtc::Message* message) {
  switch (message->message_id) {
    case MSG_CONFIG_START:
      GetPortConfigurations();
      break;
    case MSG_CONFIG_READY:
      OnConfigReady(static_cast<PortConfiguration*>(message->pdata));
      break;
    case MSG_ALLOCATE:
      OnAllocate();
      break;
    case MSG_SEQUENCEOBJECTS_CREATED:
      OnAllocationSequenceObjectsCreated();
      break;
    case MSG_CONFIG_STOP:
      OnConfigStop();
      break;
    default:
      RTC_NOTREACHED();
  }
}

OnAllocate中启动端口分配:遍历所有网络设备(Network 对象),创建 AllocationSequence 对象,调用其 InitStart 函数,分配 port。在init中会创建udp_socket_(CreateUdpSocket),在启用boundle的时候检测StunPorts的时候会和普通UDP端口共用一个socket。

void AllocationSequence::Start() {
  state_ = kRunning;
  session_->network_thread()->Post(RTC_FROM_HERE, this, MSG_ALLOCATION_PHASE);
  // Take a snapshot of the best IP, so that when DisableEquivalentPhases is
  // called next time, we enable all phases if the best IP has since changed.
  previous_best_ip_ = network_->GetBestIP();
}

发送post消息后会进入AllocationSequence对象本身的OnMessage:

void AllocationSequence::OnMessage(rtc::Message* msg) {
  RTC_DCHECK(rtc::Thread::Current() == session_->network_thread());
  RTC_DCHECK(msg->message_id == MSG_ALLOCATION_PHASE);

  const char* const PHASE_NAMES[kNumPhases] = {"Udp", "Relay", "Tcp"};

  // Perform all of the phases in the current step.
  RTC_LOG(LS_INFO) << network_->ToString()
                   << ": Allocation Phase=" << PHASE_NAMES[phase_];

  switch (phase_) {
    case PHASE_UDP:
      CreateUDPPorts();
      CreateStunPorts();
      break;

    case PHASE_RELAY:
      CreateRelayPorts();
      break;

    case PHASE_TCP:
      CreateTCPPorts();
      state_ = kCompleted;
      break;

    default:
      RTC_NOTREACHED();
  }

  if (state() == kRunning) {
    ++phase_;
    session_->network_thread()->PostDelayed(RTC_FROM_HERE,
                                            session_->allocator()->step_delay(),
                                            this, MSG_ALLOCATION_PHASE);
  } else {
    // If all phases in AllocationSequence are completed, no allocation
    // steps needed further. Canceling  pending signal.
    session_->network_thread()->Clear(this, MSG_ALLOCATION_PHASE);
    SignalPortAllocationComplete(this);
  }
}

AllocationSequence 的 phase_ 成员在对象创建时初始化为 0, 等于 PHASE_UDP ,所以首先会进入 PHASE_UDP 的处理过程,处理完成后会进入下一个处理session_->network_thread()->PostDelayed即PHASE_RELAY。

UDP phase 会收集两种类型的 candidate:host 和 srflx。会先收集host,然后srflx,但是PORTALLOCATOR_ENABLE_SHARED_SOCKET设置为true时会共享一个socket。

void AllocationSequence::CreateUDPPorts() {
  if (IsFlagSet(PORTALLOCATOR_DISABLE_UDP)) {
    RTC_LOG(LS_VERBOSE) << "AllocationSequence: UDP ports disabled, skipping.";
    return;
  }

  // TODO(mallinath) - Remove UDPPort creating socket after shared socket
  // is enabled completely.
  std::unique_ptr<UDPPort> port;
  bool emit_local_candidate_for_anyaddress =
      !IsFlagSet(PORTALLOCATOR_DISABLE_DEFAULT_LOCAL_CANDIDATE);
  if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET) && udp_socket_) {
    port = UDPPort::Create(
        session_->network_thread(), session_->socket_factory(), network_,
        udp_socket_.get(), session_->username(), session_->password(),
        session_->allocator()->origin(), emit_local_candidate_for_anyaddress,
        session_->allocator()->stun_candidate_keepalive_interval());
  } else {
    port = UDPPort::Create(
        session_->network_thread(), session_->socket_factory(), network_,
        session_->allocator()->min_port(), session_->allocator()->max_port(),
        session_->username(), session_->password(),
        session_->allocator()->origin(), emit_local_candidate_for_anyaddress,
        session_->allocator()->stun_candidate_keepalive_interval());
  }

  if (port) {
    // If shared socket is enabled, STUN candidate will be allocated by the
    // UDPPort.
    if (IsFlagSet(PORTALLOCATOR_ENABLE_SHARED_SOCKET)) {
      udp_port_ = port.get();
      port->SignalDestroyed.connect(this, &AllocationSequence::OnPortDestroyed);

      // If STUN is not disabled, setting stun server address to port.
      if (!IsFlagSet(PORTALLOCATOR_DISABLE_STUN)) {
        if (config_ && !config_->StunServers().empty()) {
          RTC_LOG(LS_INFO)
              << "AllocationSequence: UDPPort will be handling the "
                 "STUN candidate generation.";
          port->set_server_addresses(config_->StunServers());
        }
      }
    }

    session_->AddAllocatedPort(port.release(), this, true);
  }
}

之后进入BasicPortAllocatorSession::AddAllocatedPort,这个时候会关联信号和槽并将此port存入队列中:

void BasicPortAllocatorSession::AddAllocatedPort(Port* port,
                                                 AllocationSequence* seq,
                                                 bool prepare_address) {
  RTC_DCHECK_RUN_ON(network_thread_);
  if (!port)
    return;

  RTC_LOG(LS_INFO) << "Adding allocated port for " << content_name();
  port->set_content_name(content_name());
  port->set_component(component());
  port->set_generation(generation());
  if (allocator_->proxy().type != rtc::PROXY_NONE)
    port->set_proxy(allocator_->user_agent(), allocator_->proxy());
  port->set_send_retransmit_count_attribute(
      (flags() & PORTALLOCATOR_ENABLE_STUN_RETRANSMIT_ATTRIBUTE) != 0);
  //将port存入队列
  PortData data(port, seq);
  ports_.push_back(data);
  //关联信号和槽
  port->SignalCandidateReady.connect(
      this, &BasicPortAllocatorSession::OnCandidateReady);
  port->SignalCandidateError.connect(
      this, &BasicPortAllocatorSession::OnCandidateError);
  port->SignalPortComplete.connect(this,
                                   &BasicPortAllocatorSession::OnPortComplete);
  port->SignalDestroyed.connect(this,
                                &BasicPortAllocatorSession::OnPortDestroyed);
  port->SignalPortError.connect(this, &BasicPortAllocatorSession::OnPortError);
  RTC_LOG(LS_INFO) << port->ToString() << ": Added port to allocator";

  if (prepare_address)
    port->PrepareAddress();
}

之后进入UDPPort::PrepareAddress,然后转到UDPPort::OnLocalAddressReady

void UDPPort::OnLocalAddressReady(rtc::AsyncPacketSocket* socket,
                                  const rtc::SocketAddress& address) {
  // When adapter enumeration is disabled and binding to the any address, the
  // default local address will be issued as a candidate instead if
  // |emit_local_for_anyaddress| is true. This is to allow connectivity for
  // applications which absolutely requires a HOST candidate.
  rtc::SocketAddress addr = address;

  // If MaybeSetDefaultLocalAddress fails, we keep the "any" IP so that at
  // least the port is listening.
  MaybeSetDefaultLocalAddress(&addr);

  AddAddress(addr, addr, rtc::SocketAddress(), UDP_PROTOCOL_NAME, "", "",
             LOCAL_PORT_TYPE, ICE_TYPE_PREFERENCE_HOST, 0, "", false);
  MaybePrepareStunCandidate();
}

会调用Port::AddAddress->Port::FinishAddingAddress在这里会触发SignalCandidateReady,转到回调函数

void Port::FinishAddingAddress(const Candidate& c, bool is_final) {
  candidates_.push_back(c);
  SignalCandidateReady(this, c);

  PostAddAddress(is_final);
}
void BasicPortAllocatorSession::OnCandidateReady(Port* port,
                                                 const Candidate& c) {
    ......
// If the current port is not pruned yet, SignalPortReady.
    if (!data->pruned()) {
      RTC_LOG(LS_INFO) << port->ToString() << ": Port ready.";
      SignalPortReady(this, port);
      port->KeepAliveUntilPruned();
    }
  }

  if (data->ready() && CheckCandidateFilter(c)) {
    std::vector<Candidate> candidates;
    candidates.push_back(allocator_->SanitizeCandidate(c));
    SignalCandidatesReady(this, candidates);
  } else {
    RTC_LOG(LS_INFO) << "Discarding candidate because it doesn't match filter.";
  }
........
}

信号和槽关联流转:

BasicPortAllocatorSession::OnCandidateReady
                     ↓SignalCandidatesReady
P2PTransportChannel::OnCandidatesReady
                     ↓SignalCandidateGathered
JsepTransportController::OnTransportCandidateGathered_n
                     ↓SignalIceCandidatesGathered
PeerConnection::OnTransportControllerCandidatesGathered
                     ↓
PeerConnection::OnIceCandidate
SignalPortReady
P2PTransportChannel::OnPortReady
                     ↓
P2PTransportChannel::CreateConnection
                     ↓
P2PTransportChannel::SortConnectionsAndUpdateState
                     ↓
void PeerConnection::OnIceCandidate(
    std::unique_ptr<IceCandidateInterface> candidate) {
  if (IsClosed()) {
    return;
  }
  ReportIceCandidateCollected(candidate->candidate());
  Observer()->OnIceCandidate(candidate.get());//然后客户端就会监听到ICECandidate收集到的消息
}

复用 socket 的情况下, AllocationSequence::CreateStunPorts函数会直接返回,因为早在 AllocationSequence::CreateUDPPorts函数的执行过程中,就已经执行了 STUN Binding request 的发送逻辑。

发送 STUN Binding request:

UDPPort::OnLocalAddressReady(CreateStunPorts)
            ↓
UDPPort::MaybePrepareStunCandidate
            ↓
UDPPort::SendStunBindingRequests
            ↓
UDPPort::SendStunBindingRequest
            ↓
StunRequestManager::Send
            ↓
StunRequestManager::SendDelayed
            ↓MSG_STUN_SEND
StunRequest::OnMessage
            ↓SignalSendPacket
UDPPort::OnSendPacket
            ↓
AsyncUDPSocket::SendTo
            ↓
PhysicalSocket::SendTo
            ↓

收到 STUN Binding response:

PhysicalSocketServer::Wait
            ↓
SocketDispatcher::OnEvent
            ↓
AsyncUDPSocket::OnReadEvent
            ↓SignalReadPacket
AllocationSequence::OnReadPacket
            ↓
UDPPort::HandleIncomingPacket
            ↓
UDPPort::OnReadPacket
            ↓
StunRequestManager::CheckResponse
            ↓
StunBindingRequest::OnResponse
            ↓
UDPPort::OnStunBindingRequestSucceeded
            ↓
Port::AddAddress
            ↓
Port::FinishAddingAddress
            ↓
BasicPortAllocatorSession::OnCandidateReady
            ↓SignalCandidatesReady

StunRequest 类是对 STUN request 的定义和封装,基类里实现了 request 超时管理、重发的逻辑,各种特定类型的逻辑由子类实现,例如 StunBindingRequest 和 TurnAllocateRequest

StunRequestManager 则实现了 response 和 request 匹配的逻辑:manager 按 transaction id => request 的 hash 保存了所有的 request,收到 response 后,根据 transaction id 即可找到对应的 request,进而可以执行 request 对象的回调。

candidate 收集状态:

  • P2PTransportChannel 创建时, gathering_state_
    为 kIceGatheringNew;
  • P2PTransportChannel::MaybeStartGathering
    里开始收集 candidate 时,如果当前未处于 Gathering 状态,则切换到 kIceGatheringGathering 状态;
  • 在收到 OnCandidatesAllocationDone 回调时,切换到kIceGatheringComplete状态;
    • BasicPortAllocatorSession::CandidatesAllocationDone为 true 时,就会触发这个回调;
    • 这意味着创建了 AllocationSequence,且所有的 AllocationSequence 都不处于 kRunning 状态,且所有的 port 都不处于 STATE_INPROGRESS
      状态;
    • AllocationSequence 创建时处于 kInit 状态, AllocationSequence::Start
      里切换为 kRunning 状态,TCP phase 结束后切换为 kCompleted 状态,如果调用了 AllocationSequence::Stop,则会切换到 kStopped 状态;
    • port 创建时就处于 STATE_INPROGRESS状态,当被 prune、发生错误时,分别切换到 STATE_PRUNEDSTATE_ERROR状态,TurnPort 和 TcpPort 收集到 candidate 后调用 Port::AddAddress时,就会切换到 STATE_COMPLETE状态,RelayPort(GTURN)和 UdpPort 也会在收集到 candidate 后切换到 STATE_COMPLETE状态,StunPort 则会在收集完 candidate(即向所有 STUN server 完成了 binding request)之后切换到 STATE_COMPLETE状态.
BasicPortAllocatorSession::
OnAllocationSequenceObjectsCreated也会调用检测是否完成

AllocationSequence::OnMessage                     Port::FinishAddingAddress
            ↓SignalPortAllocationComplete                   ↓
OnPortAllocationComplete                      Port::PostAddAddress(bool is_final)
            ↓                                            ↓SignalPortComplete
            ↓                        BasicPortAllocatorSession::OnPortComplete
            ↓                            ↓
BasicPortAllocatorSession::MaybeSignalCandidatesAllocationDone
            ↓
BasicPortAllocatorSession::CandidatesAllocationDone
            ↓true
SignalCandidatesAllocationDone
            ↓
OnCandidatesAllocationDone

远端Candidates设置

PeerConnection::SetRemoteDescription    UpdateStats(kStatsOutputLevelStandard)
            ↓
PeerConnection::UseCandidatesInSessionDescription
            ↓
PeerConnection::UseCandidate
            ↓SetIceConnectionState(PeerConnectionInterface::kIceConnectionChecking)
JsepTransportController::AddRemoteCandidates
            ↓
JsepTransport::AddRemoteCandidates
            ↓
P2PTransportChannel::AddRemoteCandidate
            ↓
P2PTransportChannel::FinishAddingRemoteCandidate
            ↓
P2PTransportChannel::CreateConnections
            ↓
P2PTransportChannel::CreateConnection
            ↓
UDPPort::CreateConnection
            ↓
Port::AddOrReplaceConnection   SignalConnectionCreated(这个信号未关联,无用)
            ↓
P2PTransportChannel::AddConnection
            ↓
P2PTransportChannel::RememberRemoteCandidate (维护remote_candidates_列表)
P2PTransportChannel::FinishAddingRemoteCandidate
            ↓
P2PTransportChannel::SortConnectionsAndUpdateState
            ↓
P2PTransportChannel::MaybeStartPinging
            ↓
P2PTransportChannel::CheckAndPing    FindNextPingableConnection
            ↓
P2PTransportChannel::PingConnection
            ↓
Connection::Ping
            ↓
StunRequestManager::Send  IceCandidatePairState::IN_PROGRESS
            ↓
StunRequestManager::SendDelayed
            ↓
           ...这个和STUN Binding response类似
            ↓
StunRequestManager::CheckResponse
            ↓
ConnectionRequest::OnResponse
            ↓
Connection::OnConnectionRequestResponse
            ↓
Connection::ReceivedPingResponse
            ↓
Connection::set_write_state STATE_WRITABLE
            ↓SignalStateChange
P2PTransportChannel::OnConnectionStateChange
            ↓
RequestSortAndStateUpdate
            ↓
P2PTransportChannel::SortConnectionsAndUpdateState

P2PTransportChannel::SwitchSelectedConnection
                    ↓ sig slot (SignalReadyToSend)
DtlsTransport::OnReadyToSend
                    ↓ sig slot (SignalReadyToSend)
RtpTransport::OnReadyToSend

AddConnection会关联端口的发送和接收

void P2PTransportChannel::AddConnection(Connection* connection) {
  RTC_DCHECK_RUN_ON(network_thread_);
  connections_.push_back(connection);
  unpinged_connections_.insert(connection);
  connection->set_remote_ice_mode(remote_ice_mode_);
  connection->set_receiving_timeout(config_.receiving_timeout);
  connection->set_unwritable_timeout(config_.ice_unwritable_timeout);
  connection->set_unwritable_min_checks(config_.ice_unwritable_min_checks);
  connection->set_inactive_timeout(config_.ice_inactive_timeout);
  connection->SignalReadPacket.connect(this,
                                       &P2PTransportChannel::OnReadPacket);
  connection->SignalReadyToSend.connect(this,
                                        &P2PTransportChannel::OnReadyToSend);
  connection->SignalStateChange.connect(
      this, &P2PTransportChannel::OnConnectionStateChange);
  connection->SignalDestroyed.connect(
      this, &P2PTransportChannel::OnConnectionDestroyed);
  connection->SignalNominated.connect(this, &P2PTransportChannel::OnNominated);

  had_connection_ = true;

  connection->set_ice_event_log(&ice_event_log_);
  LogCandidatePairConfig(connection,
                         webrtc::IceCandidatePairConfigType::kAdded);
}

原文地址:https://www.cnblogs.com/bloglearning/p/12128510.html

时间: 2024-10-21 08:16:21

peer-connection - ICE的相关文章

Android IOS WebRTC 音视频开发总结(七)

前面写的一系列总结都是讲webrtc如何下载,编译,开发的,有些人可能有点云里雾里了,WEBRTC不是用来搞跨浏览器开发的吗,怎么我讲的这些跟浏览器扯不上任何关系,其实看看下面这个架构图,你就明白了(本系列文章转载请说明出处:http://www.cnblogs.com/lingyunhu). 我前面讲的这些内容都封装在browser里面了,如音视频的采集,编码,传输,回声消除,丢包重传.所以如果你想将这些功能集成到你的产品里面就必须理解这些东西. 如果你只想做基于浏览器的视频通话功能,上面这些

腾讯IVWEB团队:WebRTC 点对点直播

WebRTC 全称为:Web Real-Time Communication.它是为了解决 Web 端无法捕获音视频的能力,并且提供了 peer-to-peer(就是浏览器间)的视频交互.实际上,细分看来,它包含三个部分: MediaStream:捕获音视频流 RTCPeerConnection:传输音视频流(一般用在 peer-to-peer 的场景) RTCDataChannel: 用来上传音视频二进制数据(一般用到流的上传) 但通常,peer-to-peer 的场景实际上应用不大.对比与去

真实场景中WebRTC 用到的服务 STUN, TURN 和 signaling

FQ收录转自:WebRTC in the real world: STUN, TURN and signaling WebRTC enables peer to peer communication. BUT... WebRTC still needs servers: For clients to exchange metadata to coordinate communication: this is called signaling. To cope with network addre

请教一个问题:关于 webrtc 通信的问题

在下是webrtc开发新手,目前遇到了个问题,翻来翻去一直没弄明白.可能英文不佳,看文档看蒙了,所以一直没找到解决方法. 开发环境: node.js 搭建的服务器 目前是用socket.io做通信 开发目的: A同学向B同学发起请求,B同学收到后双方互相视频直播. 如果有清楚的同学麻烦告知我哈,真的麻烦大家了 O(∩_∩)O~~ 问题描述: 目前在搭建两人视频的demo,但是在双方信号交换信息上出问题了.控制台打印出来是这样的: 代码如下: 1 var servers = null; 2 3 f

webrtc学习——RTCPeerConnection

The RTCPeerConnection interface represents a WebRTC connection and handles efficient streaming of data between two peers. Warning: RTCPeerConnection and RTCSessionDescription are currently prefixed in most browsers. You should include a polyfill if y

OpenVPN网桥模式实现LAN-TO-LAN

1 配置如下: [[email protected] config]# cat  server.conf local 172.18.1.132 port 3889 proto udp dev tap0 ca /root/openvpn-2.3.3/config/ca.crt cert /root/openvpn-2.3.3/config/VPNS.crt key /root/openvpn-2.3.3/config/VPNS.key  # This file should be kept sec

OpenVPN路由模式实现LAN-TO-LAN

1 网络拓扑: 2 配置如下: [[email protected] config]# cat  server.conf local 172.18.1.132 port 3889 proto udp dev tun ca /root/openvpn-2.3.3/config/ca.crt cert /root/openvpn-2.3.3/config/VPNS.crt key /root/openvpn-2.3.3/config/VPNS.key  # This file should be k

OpenVPN(待修改)

1    OpenVPN的工作原理 VPN技术通过密钥交换.封装.认证.加密手段在公共网络上建立起私密的隧道,保障传输数据的完整性.私密性和有效性.OpenVPN是近年来新出现的开放源码项目,实现了SSLVPN的一种解决方案. 传统SSL VPN通过端口代理的方法实现,代理服务器根据应用协议的类型(如http,telnet等)做相应的端口代理,客户端与代理服务器之间建立SSL安全连接,客户端与应用服务器之间的所有数据传输通过代理服务器转发.这种实现方式烦琐,应用范围也比较窄:仅适用于用TCP固定

P2P通信原理与实现(C++)

1.简介 当今互联网到处存在着一些中间件(MIddleBoxes),如NAT和防火墙,导致两个(不在同一内网)中的客户端无法直接通信.这些问题即便是到了IPV6时代也会存在,因为即使不需要NAT,但还有其他中间件如防火墙阻挡了链接的建立. 当今部署的中间件大多都是在C/S架构上设计的,其中相对隐匿的客户机主动向周知的服务端(拥有静态IP地址和DNS名称)发起链接请求.大多数中间件实现了一种非对称的通讯模型,即内网中的主机可以初始化对外的链接,而外网的主机却不能初始化对内网的链接,除非经过中间件管

HTML5新特性之WebRTC[转]

原文:http://www.cnblogs.com/jscode/p/3601648.html?comefrom=http://blogread.cn/news/ 1.概述 WebRTC是“网络实时通信”(Web Real Time Communication)的缩写,它主要用来让浏览器实时获取和交换视频.音频和数据. WebRTC共分三个API. MediaStream(又称getUserMedia) RTCPeerConnection RTCDataChannel getUserMedia主