Go语言之pion/ion-sfu中的sfu源码分析

1,442 阅读8分钟

「这是我参与2022首次更文挑战的第37天,活动详情查看:2022首次更文挑战

sfu包的分析

这是一个配置结构,用于加载配置文件config.toml. 还有一些字段对应的结构体,后面遇到再细说.

type Config struct {
  SFU struct {
    Ballast   int64 `mapstructure:"ballast"`
    WithStats bool  `mapstructure:"withstats"`
  } `mapstructure:"sfu"`
  WebRTC WebRTCConfig `mapstructure:"webrtc"`
  Log    log.Config   `mapstructure:"log"`
  Router RouterConfig `mapstructure:"router"`
  Turn   TurnConfig   `mapstructure:"turn"`
}

在sfu的入口处有这么几行,这就是加载配置:

var conf = sfu.Config{}
err = viper.GetViper().Unmarshal(&conf)
s := sfu.NewSFU(conf)

整个配置Config最后是用来初始化sfu实例.

下面我们按照源文件顺序来分析一下

sfu.go

对外暴露的第一个类型,传输配置,创建PeerConection时需要用到这个配置.

type WebRTCTransportConfig struct {
  configuration webrtc.Configuration
  setting       webrtc.SettingEngine
  router        RouterConfig
}

// 构造传输配置
func NewWebRTCTransportConfig(c Config) WebRTCTransportConfig {
  se := webrtc.SettingEngine{}

  var icePortStart, icePortEnd uint16

  // 如果开启了turn中继,端口范围是46884-60999
  // 如果没开启,优先使用配置文件config.toml中指定的范围
  if c.Turn.Enabled {
    icePortStart = sfuMinPort
    icePortEnd = sfuMaxPort
  } else if len(c.WebRTC.ICEPortRange) == 2 {
    icePortStart = c.WebRTC.ICEPortRange[0]
    icePortEnd = c.WebRTC.ICEPortRange[1]
  }

  if icePortStart != 0 || icePortEnd != 0 {
    if err := se.SetEphemeralUDPPortRange(
        icePortStart, icePortEnd); err != nil {
      panic(err)
    }
  }

  var iceServers []webrtc.ICEServer
  if c.WebRTC.Candidates.IceLite {
    se.SetLite(c.WebRTC.Candidates.IceLite)
  } else {
    for _, iceServer := range c.WebRTC.ICEServers {
      s := webrtc.ICEServer{
        URLs:       iceServer.URLs,
        Username:   iceServer.Username,
        Credential: iceServer.Credential,
      }
      iceServers = append(iceServers, s)
    }
  }

  se.BufferFactory = bufferFactory.GetOrNew

  sdpSemantics := webrtc.SDPSemanticsUnifiedPlan
  switch c.WebRTC.SDPSemantics {
  case "unified-plan-with-fallback":
    sdpSemantics = webrtc.SDPSemanticsUnifiedPlanWithFallback
  case "plan-b":
    sdpSemantics = webrtc.SDPSemanticsPlanB
  }

  w := WebRTCTransportConfig{
    configuration: webrtc.Configuration{
      ICEServers:   iceServers,
      SDPSemantics: sdpSemantics,
    },
    setting: se,
    router:  c.Router,
  }

  if len(c.WebRTC.Candidates.NAT1To1IPs) > 0 {
    w.setting.SetNAT1To1IPs(c.WebRTC.Candidates.NAT1To1IPs, webrtc.ICECandidateTypeHost)
  }

  if c.SFU.WithStats {
    w.router.WithStats = true
    stats.InitStats()
  }

  return w
}

这里的传输对象构造,是基于配置来的,会应用于所有会话,整个过程中是不会变更的. sdp的两种组织方式 plan b和unified plan, 首先出现的是plan a,一个peerconnection对应一个流; 之后被plan b取代,plan b是sdp中的一个媒体级(m=)包含多个流,流和流之间用msid区分; 现在plan b逐渐被jsep规定的unified plan取代, unified plan是sdp中的一个媒体级(m=)表示一个流,sdp可以包含多个媒体级, 现在主流浏览器都慢慢切到jsep规定的标准上了.

对外暴露的第二个类型是SFU,SFU表示一个sfu实例.

type SFU struct {
  sync.RWMutex
  webrtc    WebRTCTransportConfig
  router    RouterConfig
  turn      *turn.Server
  sessions  map[string]*Session
  withStats bool
}

SFU包含了传输配置WebRTCTransportConfig,也包含了一个turn服务, 同时还维护了一个会话列表,会话是webrtc流转发的边界.

func NewSFU(c Config) *SFU {
  // Init random seed
  rand.Seed(time.Now().UnixNano())
  // Init ballast
  ballast := make([]byte, c.SFU.Ballast*1024*1024)
  // Init buffer factory
  bufferFactory = buffer.NewBufferFactory()
  // Init packet factory
  packetFactory = &sync.Pool{
    New: func() interface{} {
      return make([]byte, 1460)
    },
  }

  w := NewWebRTCTransportConfig(c)

  s := &SFU{
    webrtc:    w,
    sessions:  make(map[string]*Session),
    withStats: c.Router.WithStats,
  }

  if c.Turn.Enabled {
    ts, err := initTurnServer(c.Turn, nil)
    if err != nil {
      log.Panicf("Could not init turn server err: %v", err)
    }
    s.turn = ts
  }

  runtime.KeepAlive(ballast)
  return s
}

这个sfu实例构造有些好玩的东西:

  • sync.Pool 临时对象单独的存放和检索,目的是减少gc压力
  • runtime.KeepAlive 保活

ballast申请指定大小的内存,用意是减少触发gc的次数.

先不看具体的标准库,来看看sfu实例的构造还做了什么事: initTurnServer,因为pion实现了自己的turn服务,也集成到ion项目中了, 此处是直接启动turn服务.

对于SFU,还有几个方法:

// 这里是构建了一个新的Session,下一步就分析这个
func (s *SFU) newSession(id string) *Session {
  session := NewSession(id)

  session.OnClose(func() {
    s.Lock()
    delete(s.sessions, id)
    s.Unlock()

    if s.withStats {
      stats.Sessions.Dec()
    }
  })

  s.Lock()
  s.sessions[id] = session
  s.Unlock()

  if s.withStats {
    stats.Sessions.Inc()
  }

  return session
}

func (s *SFU) getSession(id string) *Session {
  s.RLock()
  defer s.RUnlock()
  return s.sessions[id]
}

func (s *SFU) GetSession(sid string) (*Session, WebRTCTransportConfig) {
  session := s.getSession(sid)
  if session == nil {
    session = s.newSession(sid)
  }
  return session, s.webrtc
}

SFU内嵌了sync.RWMutex读写锁. SFU.GetSession()总会返回一个会话和传输配置, 传输配置是固定的,如果会话id不存在,就新建一个会话.

session.go

对外暴露的是Session类型:

type Session struct {
  id             string
  mu             sync.RWMutex
  peers          map[string]*Peer
  onCloseHandler func()
  closed         bool
}

Session.peers维护了当前会话中的所有参与者. 一个会话中的参与者会自动订阅其他参与者的流.

构造非常简单,只需要提供会话id.

func NewSession(id string) *Session {
  return &Session{
    id:     id,
    peers:  make(map[string]*Peer),
    closed: false,
  }
}

sfu.Peer是代表PeerConnection的对象,是下一步分析的类型. sfu.Session中peers是非暴露的,所以会有方法来对参与者做维护:

func (s *Session) AddPeer(peer *Peer) {
  s.mu.Lock()
  s.peers[peer.id] = peer
  s.mu.Unlock()
}

func (s *Session) RemovePeer(pid string) {
  s.mu.Lock()
  log.Infof("RemovePeer %s from session %s", pid, s.id)
  delete(s.peers, pid)
  s.mu.Unlock()

  if len(s.peers) == 0 && s.onCloseHandler != nil && !s.closed {
    s.onCloseHandler()
    s.closed = true
  }
}

参与者离开会话,当会话的参与者数量为0时,会触发一次sfu.Session.onCloseHandler(), 这个是在会话关闭时调用的.

每个参与者都有一个区分的标识符,下面是通过标识符查参与者/设置会话关闭处理:

func (s *Session) Peers() map[string]*Peer {
  s.mu.RLock()
  defer s.mu.RUnlock()
  return s.peers
}

func (s *Session) OnClose(f func()) {
  s.onCloseHandler = f
}

发布和订阅. 发布Publish的颗粒度比较细,让会话中的其他人订阅Router. Subcribe订阅颗粒度较粗,是让会话中的其他人订阅某个人. Subcribe还做了一个datachannel的连接.

func (s *Session) Publish(router Router, r Receiver) {
  s.mu.RLock()
  defer s.mu.RUnlock()

  for pid, p := range s.peers {
    // Don't sub to self
    if router.ID() == pid {
      continue
    }

    log.Infof("Publishing track to peer %s", pid)

    if err := router.AddDownTracks(p.subscriber, r); err != nil {
      log.Errorf("Error subscribing transport to router: %s", err)
      continue
    }
  }
}

// Subscribe will create a Sender for every other Receiver in the session
func (s *Session) Subscribe(peer *Peer) {
  s.mu.RLock()
  defer s.mu.RUnlock()

  subdChans := false
  for pid, p := range s.peers {
    if pid == peer.id {
      continue
    }
    err := p.publisher.GetRouter().AddDownTracks(peer.subscriber, nil)
    if err != nil {
      log.Errorf("Subscribing to router err: %v", err)
      continue
    }

    if !subdChans {
      for _, dc := range p.subscriber.channels {
        label := dc.Label()
        n, err := peer.subscriber.AddDataChannel(label)

        if err != nil {
          log.Errorf("error adding datachannel: %s", err)
          continue
        }

        n.OnMessage(func(msg webrtc.DataChannelMessage) {
          s.onMessage(peer.id, label, msg)
        })
      }
      subdChans = true

      peer.subscriber.negotiate()
    }
  }
}

在订阅中,datachannel连接好之后,通过DataChannel.OnMessage设置了消息处理函数.

func (s *Session) onMessage(
  origin, label string, msg webrtc.DataChannelMessage) {
  s.mu.RLock()
  defer s.mu.RUnlock()
  for pid, p := range s.peers {
    if origin == pid {
      continue
    }

    dc := p.subscriber.channels[label]
    if dc != nil && dc.ReadyState() == webrtc.DataChannelStateOpen {
      if msg.IsString {
        if err := dc.SendText(string(msg.Data)); err != nil {
          log.Errorf("Sending dc message err: %v", err)
        }
      } else {
        if err := dc.Send(msg.Data); err != nil {
          log.Errorf("Sending dc message err: %v", err)
        }
      }
    }
  }
}

从代码中可以看出,Session.onMessage是将通过datachannel将消息广播给其他参与者.

func (s *Session) AddDatachannel(owner string, dc *webrtc.DataChannel) {
  label := dc.Label()

  s.mu.RLock()
  defer s.mu.RUnlock()

  s.peers[owner].subscriber.channels[label] = dc

  dc.OnMessage(func(msg webrtc.DataChannelMessage) {
    s.onMessage(owner, label, msg)
  })

  for pid, p := range s.peers {
    if owner == pid {
      continue
    }
    n, err := p.subscriber.AddDataChannel(label)

    if err != nil {
      log.Errorf("error adding datachannel: %s", err)
      continue
    }

    pid := pid
    n.OnMessage(func(msg webrtc.DataChannelMessage) {
      s.onMessage(pid, label, msg)
    })

    p.subscriber.negotiate()
  }
}

Session.AddDatachannel是给指定参与者指定一个datachannel, 并让其他参与者的datachannel都和指定参与者的datachannnel相连.

从功能上看, AddDatachannel+Publish=Subcribe.

peer.go

对外暴露的类型:

type SessionProvider interface {
  GetSession(sid string) (*Session, WebRTCTransportConfig)
}

SFU类型就实现了这个接口.因为Peer对象是需要获取会话信息的,正好可以利用这个接口.

type Peer struct {
  sync.Mutex
  id         string
  session    *Session
  provider   SessionProvider
  publisher  *Publisher
  subscriber *Subscriber

  OnOffer                    func(*webrtc.SessionDescription)
  OnIceCandidate             func(*webrtc.ICECandidateInit, int)
  OnICEConnectionStateChange func(webrtc.ICEConnectionState)

  remoteAnswerPending bool
  negotiationPending  bool
}

Peer表示的是一对p2p连接.

func NewPeer(provider SessionProvider) *Peer {
  return &Peer{
    provider: provider,
  }
}

构造之后就可以发送信令了. 使用时,构成参数可以使用SFU实例.

Peer提供了如下方法,对应着Peer端的流程:

  • Join 使用会话id来初始化一个Peer,会带一个sdp offer
  • Answer 处理一个标准的额sdp offer,返回sdp answer
  • SetRemoteDescription 收到一个sdp answer后,调用此方法处理
  • Trickle 处理ice候选
  • Close 关闭Peer

下面来一一看看

func (p *Peer) Join(
  sid string,
  sdp webrtc.SessionDescription) (*webrtc.SessionDescription, error) {

  if p.publisher != nil {
    log.Debugf("peer already exists")
    return nil, ErrTransportExists
  }

  pid := cuid.New()
  p.id = pid
  var (
    cfg WebRTCTransportConfig
    err error
  )

  // 获取会话信息和传输对象的信息
  p.session, cfg = p.provider.GetSession(sid)

  // 构造一个新的Subsciber,这个下一步分析
  p.subscriber, err = NewSubscriber(pid, cfg)
  if err != nil {
    return nil, fmt.Errorf("error creating transport: %v", err)
  }

  // 构造一个新的Publisher,这个下一步分析
  p.publisher, err = NewPublisher(p.session, pid, cfg)
  if err != nil {
    return nil, fmt.Errorf("error creating transport: %v", err)
  }

  // 设置NegotiationNeeded处理
  p.subscriber.OnNegotiationNeeded(func() {
    p.Lock()
    defer p.Unlock()

    if p.remoteAnswerPending {
      p.negotiationPending = true
      return
    }

    log.Debugf("peer %s negotiation needed", p.id)
    offer, err := p.subscriber.CreateOffer()
    if err != nil {
      log.Errorf("CreateOffer error: %v", err)
      return
    }

    p.remoteAnswerPending = true
    if p.OnOffer != nil {
      log.Infof("peer %s send offer", p.id)
      p.OnOffer(&offer)
    }
  })

  // 设置subscriber/publisher的ice处理
  p.subscriber.OnICECandidate(func(c *webrtc.ICECandidate) {
    log.Debugf("on subscriber ice candidate called for peer " + p.id)
    if c == nil {
      return
    }

    if p.OnIceCandidate != nil {
      json := c.ToJSON()
      p.OnIceCandidate(&json, subscriber)
    }
  })

  p.publisher.OnICECandidate(func(c *webrtc.ICECandidate) {
    log.Debugf("on publisher ice candidate called for peer " + p.id)
    if c == nil {
      return
    }

    if p.OnIceCandidate != nil {
      json := c.ToJSON()
      p.OnIceCandidate(&json, publisher)
    }
  })

  p.publisher.OnICEConnectionStateChange(func(s webrtc.ICEConnectionState) {
    if p.OnICEConnectionStateChange != nil {
      p.OnICEConnectionStateChange(s)
    }
  })

  // Peer初始化完成后,添加到会话列表中
  p.session.AddPeer(p)

  log.Infof("peer %s join session %s", p.id, sid)

  // 处理offer,返回answer
  answer, err := p.publisher.Answer(sdp)
  if err != nil {
    return nil, fmt.Errorf("error setting remote description: %v", err)
  }

  log.Infof("peer %s send answer", p.id)

  // 同时新建的Peer会订阅其他参与者的流
  p.session.Subscribe(p)

  return &answer, nil
}

所有的推拉流,只有先执行Join之后才能进行.

func (p *Peer) Answer(
  sdp webrtc.SessionDescription) (*webrtc.SessionDescription, error) {

  if p.subscriber == nil {
    return nil, ErrNoTransportEstablished
  }

  log.Infof("peer %s got offer", p.id)

  // 如果协商状态不是初始状态,就忽略此次sdp offer处理请求
  if p.publisher.SignalingState() != webrtc.SignalingStateStable {
    return nil, ErrOfferIgnored
  }

  answer, err := p.publisher.Answer(sdp)
  if err != nil {
    return nil, fmt.Errorf("error creating answer: %v", err)
  }

  log.Infof("peer %s send answer", p.id)

  return &answer, nil
}

func (p *Peer) SetRemoteDescription(sdp webrtc.SessionDescription) error {
  if p.subscriber == nil {
    return ErrNoTransportEstablished
  }
  p.Lock()
  defer p.Unlock()

  log.Infof("peer %s got answer", p.id)
  if err := p.subscriber.SetRemoteDescription(sdp); err != nil {
    return fmt.Errorf("error setting remote description: %v", err)
  }

  p.remoteAnswerPending = false

  if p.negotiationPending {
    p.negotiationPending = false
    go p.subscriber.negotiate()
  }

  return nil
}

这里设置远端sdp,用的是subscriber,这块后续需要详细看看.

func (p *Peer) Trickle(candidate webrtc.ICECandidateInit, target int) error {
  if p.subscriber == nil || p.publisher == nil {
    return ErrNoTransportEstablished
  }
  log.Infof("peer %s trickle", p.id)
  switch target {
  case publisher:
    if err := p.publisher.AddICECandidate(candidate); err != nil {
      return fmt.Errorf("error setting ice candidate: %s", err)
    }
  case subscriber:
    if err := p.subscriber.AddICECandidate(candidate); err != nil {
      return fmt.Errorf("error setting ice candidate: %s", err)
    }
  }
  return nil
}

处理ice候选,通过target来选择publisher或subscriber.

func (p *Peer) Close() error {
  if p.session != nil {
    p.session.RemovePeer(p.id)
  }
  if p.publisher != nil {
    p.publisher.Close()
  }
  if p.subscriber != nil {
    if err := p.subscriber.Close(); err != nil {
      return err
    }
  }
  return nil
}

Close会清理会话的参与者列表,也会清理publisher和subscriber.

publisher.go

对外暴露的是Publisher类型:

type Publisher struct {
  id string
  pc *webrtc.PeerConnection

  router     Router
  session    *Session
  candidates []webrtc.ICECandidateInit

  onTrackHandler                    func(*webrtc.TrackRemote, *webrtc.RTPReceiver)
  onICEConnectionStateChangeHandler atomic.Value // func(webrtc.ICEConnectionState)

  closeOnce sync.Once
}

在Publisher中包含了webrtc.PeerConnection.ice候选缓存, 同时还支持轨道处理/ice连接状态处理.

Router封装了一个轨道的rtp/rtcp,下一步分析这个.

func NewPublisher(session *Session, id string,
  cfg WebRTCTransportConfig) (*Publisher, error) {

  // 构造webrtc.MediaEngine对象
  // 具体的分析在构造函数下方
  me, err := getPublisherMediaEngine()
  if err != nil {
    log.Errorf("NewPeer error: %v", err)
    return nil, errPeerConnectionInitFailed
  }

  // 构造一个webrtc api对象
  // 这里的写法非常有意思,参考了option模式
  api := webrtc.NewAPI(webrtc.WithMediaEngine(me), webrtc.WithSettingEngine(cfg.setting))

  // 之后通过api来构造一个PeerConnection
  pc, err := api.NewPeerConnection(cfg.configuration)

  if err != nil {
    log.Errorf("NewPeer error: %v", err)
    return nil, errPeerConnectionInitFailed
  }

  p := &Publisher{
    id:      id,
    pc:      pc,
    session: session,
    router:  newRouter(pc, id, cfg.router),
  }

  pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
    log.Debugf("Peer %s got remote track id: %s \
      mediaSSRC: %d rid :%s streamID: %s",
      p.id, track.ID(), track.SSRC(), track.RID(), track.StreamID())
    if r, pub := p.router.AddReceiver(receiver, track); pub {
      p.session.Publish(p.router, r)
    }
  })

  pc.OnDataChannel(func(dc *webrtc.DataChannel) {
    if dc.Label() == apiChannelLabel {
      // terminate api data channel
      return
    }
    p.session.AddDatachannel(id, dc)
  })

  pc.OnICEConnectionStateChange(
    func(connectionState webrtc.ICEConnectionState) {
    log.Debugf("ice connection state: %s", connectionState)
    switch connectionState {
    case webrtc.ICEConnectionStateFailed:
      fallthrough
    case webrtc.ICEConnectionStateClosed:
      log.Debugf("webrtc ice closed for peer: %s", p.id)
      p.Close()
    }

    if handler, ok := p.onICEConnectionStateChangeHandler.Load().(
      func(webrtc.ICEConnectionState)); ok && handler != nil {

      handler(connectionState)
    }
  })

  return p, nil
}

在Publisher的构造中,第一个就是构造一个webrtc.MediaEngine. 构造的过程在mediaengine.go中,简单列一下:

  • 构造一个空的webrtc.MediaEngine
  • 注册opus 111
  • 注册视频编码
    • vp8 96
    • vp9 profile-id=0 98
    • vp9 profile-id=1 100
    • h264
      • level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 102
      • level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f 127
      • level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 125
      • level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f 108
      • level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032 123
  • 设置音视频的扩展头