instrumentisto/medea

Medea server 1 <=> 1 WebRTC P2P

Closed this issue · 5 comments

Архитектура

Control API пока нет. Будет одна захардкоженая комната с двумя захардкожеными пользователями.

Комнату в терминах Control API можно выразить следующим образом:

kind: Room
spec:
  pipeline:
    caller:
      kind: Member
      spec:
        pipeline:
          publish:
            kind: WebRtcPublishEndpoint
            spec:
              p2p: Always
          play:
            kind: WebRtcPlayEndpoint
            spec:
              src: "local://video-call-2/responder/publish"
    responder:
      kind: Member
      spec:
        pipeline:
          publish:
            kind: WebRtcPublishEndpoint
            spec:
              p2p: Always
          play:
            kind: WebRtcPlayEndpoint
            spec:
              src: "local://video-call-2/caller/publish"

Сценарий обмена сообщениями - первые пять пунктов из примера 1 <=> 1 P2P with unpublish and republish в 0002-webrc-client-api.

Ознакомление с 0001-control-api и 0002-webrc-client-api строго обязательное.

Примерное распределение по пакетам следующее:

  1. /api/client - все что связяно с Client API, а пока это: инициализация WsListener, обработка получаемых по сокету сообщений, протокольные ДТО, WsSessionsRepository.
  2. '/api/control' - все что связано с Control API. Пока это: Member, MemberRepository.
  3. /media - предметная область: Peer, Track. PeerRepository.
  4. /log - враппер предоставляющий средства для логирования внутри приложения чего угодно.
  5. /conf - слой реализующий конфигурацию приложения и предоставляющий средства для работы с ней.

Elements considerations

Важный момент - разделение между терминами Control API и терминами нашей внутренней модели данных. Так как основной задачей Control API было максимально упрощение задачи взаимодействия управляющего сервиса c Medea, его термины не ложаться на реальную модель данных 1 к 1.

Например

WebRtc<_>Endpoint'ы интуитивно приравниваются к RTCPeerConnection. Т.е. каждый пользователь будет иметь по два RTCPeerConnection. Но это будет верно только для некоторых сценариев с использование hub сервера.

И хотя такая реализация для N<=>N P2P возможна, намного эффективней будет держать по одному дюплексному RTCPeerConnection'у у каждого пользователя на каждого из его собеседников. Таким образом, в данном примере, WebRtcPublishEndpoint фактически является RTCRtpSender, а WebRtcPlayEndpoint = RTCRtpReceiver.

Так как Control API в 0.1.0 milestone не фигурирует, задача трансляции его модели данных в модель данных Medea(парсинг Control API спеки) пока не стоит.

Предлагается придерживаться примитивов обозначенных в 0002-webrc-client-api:

  1. Member
  2. Peer
  3. Track

В добавлении Room из 0001-control-api пока смысла нет - комната одна будет.

Что хочется получить:

  1. Удобные top-level абстракции.
  2. Корректная Peer state-machine. Тут можно частично вдохновится списоком состояний RTCPeerConnection: signaling_state, connection_state.
  3. Peer сам знает что отправить удаленному клиенту чтобы тот синхронизировал свое состояние. В идеале, синхронизация состояний должна происходить по вызову специального метода (допустим Peer.update_tracks()).

Последний пункт достаточно неоднозначный. Draft подразумевает что все так, но над этим еще надо будет подумать. Сокет, дергающий Peerа, дергающего сокет - может привести к проблемам.

Peer and Track state-machine draft

Постарался набросать как это примерно может выглядить. На прямое руководство к действию пока не тянет и многие моменты опущены.

struct Peer<S> {
    id: peer::Id,
    member_id: member::Id,
    signaling_state: S,
}

// signaling_states
struct New {}
struct WaitLocalSDP {}
struct WaitLocalHaveRemote {}
struct WaitRemoteSDP {}
struct Stable {}

impl Peer<New> {
    fn new(id: peer::Id, member_id: member::Id) -> Peer<New> {}

    fn add_sender<T>(&mut self, track: Track<T, New>) -> Track<T, Send> {}

    fn add_receiver<T>(&mut self, track: Track<T, Send>) -> Track<T, SendRecv> {}
    
    fn update_tracks(self) -> Peer<WaitLocalSDP> {} // sends PeerCreated

    fn set_local_sdp(self, offer: &str) -> Peer<WaitRemoteSDP> {}
    
    fn set_remote_sdp(self, offer: &str) -> Peer<WaitLocalHaveRemote> {}     // sends PeerCreated
}

impl Peer<WaitRemoteSDP> {
    fn set_remote_sdp(self, offer: &str) -> Peer<Stable> {}     // sends SdpAnswerMade
}

impl Peer<WaitLocalSDP> {
    fn set_local_sdp(self, offer: &str) -> Peer<WaitRemoteSDP> {}
}

impl Peer<WaitLocalHaveRemote> {
    fn set_local_sdp(self, offer: &str) -> Peer<Stable> {
    }
}

struct Track<T, S> {
    id: u64,
    media_type: T,
    track_state: S,
}

// media_types

struct Audio {}
struct Video {}

// track_states

struct Send {}
struct SendRecv {}
struct Active {}

И пример как это будет выглядить:

fn main() {

    // init peers and tracks
    let mut peer1: Peer<New> = Peer::new(peer::Id(1), member::Id(String::from("caller")));

    let peer1_audio: Track<Audio, Send> = peer1.add_sender(Track::<Audio, New>::new(1, Audio {})); //ads new audio track to list of peer1 sending tracks
    let peer1_video: Track<Video, Send> = peer1.add_sender(Track::<Video, New>::new(2, Video {})); //ads new video track to list of peer1 sending tracks

    let mut peer2: Peer<New> = Peer::new(peer::Id(2), member::Id(String::from("responder")));

    let peer2_audio: Track<Audio, Send> = peer2.add_sender(Track::<Audio, New>::new(3, Audio {})); //ads new audio track to list of peer1 sending tracks
    let peer2_video: Track<Video, Send> = peer2.add_sender(Track::<Video, New>::new(4, Video {})); //ads new video track to list of peer1 sending tracks

    // connect tracks
    let peer2_to_peer1_audio: Track<Audio, SendRecv> = peer1.add_receiver(peer2_audio); // ads peer2_audio track to list of peer1 receiving tracks
    let peer2_to_peer1_video: Track<Video, SendRecv> = peer1.add_receiver(peer2_video); // ads peer2_video track to list of peer1 receiving tracks

    let peer1_to_peer2_audio: Track<Audio, SendRecv> = peer2.add_receiver(peer1_audio); // ads peer1_audio track to list of peer2 receiving tracks
    let peer1_to_peer2_video: Track<Video, SendRecv> = peer2.add_receiver(peer1_video); // ads peer1_video track to list of peer2 receiving tracks

    // init connection
    let peer1: Peer<WaitLocalSDP> = peer1.update_tracks(); // => PeerCreated {1, no_offer};

    // MakeSdpOffer from caller
    let peer1: Peer<WaitRemoteSDP> = peer1.set_local_sdp("caller_offer");
    let peer2: Peer<WaitLocalHaveRemote> = peer2.set_remote_sdp("caller_offer"); // => PeerCreated {2, offer}

    // MakeSdpAnswer from responder
    let peer1: Peer<Stable> = peer1.set_remote_sdp("responder_answer"); // => SdpAnswerMade {1}
    let peer2: Peer<Stable> = peer2.set_local_sdp("responder_answer");
}

Roadmap

  • Add Logger (#12).
  • Add Member(id, credentials(str)), MemberRepository (get_by_id(),get_by_credentials()), hardcode caller and responder (#13).
  • Add WsHandler (just handles upgrade request), add WsSessions repository (#14)
  • Add Configuration (#15).
  • Implement signaling (#16)
    • Add Track, Peer, PeerRepository
    • Create Peer on Member connect
    • Send PeerCreated to first Member on both Members connected.
    • Handle MakeSdpOffer, send PeerCreated to second Member.
    • Handle MakeSdpAnswer, send SdpAnswerMade.
    • Handle SetIceCandidate, send IceCandidateDiscovered.
    • Clean up on Member disconnect.
    • Add integration tests - full cycle from ws established to dropped.
  • Add Coturn (#20).
    • Dockerize app, add Coturn to docker-compose.
    • Pass Coturn uri via configuration.

@alexlapa I think it's OK at the moment. Just do not forget to update roadmap when things change.

@alexlapa объясните за треки, пожалуйста, а то не складывается описание из rfc-0002 и здешнего примера:

  1. когда мы добавляем трек `peer.add_sender(Track):
    а. трек сохраняется в пире, и создается и возвращается ответный пир
    б. в пире сохраняется только ид трека, а сам трек, реализованый в виде state machine, изменяет состояние и возвращается
  2. когда трек становится Active?
  3. что делать с треками SendRecv?

@Kirguir ,

когда мы добавляем трек `peer.add_sender(Track):

Думаю, треки лучше хранить внутри Peer'ов. Например так:

Peer {
    recv: HashMap<TrackId, Arc<Track>>
    send: HashMap<TrackId, Arc<Track>>
} 

Один трек может быть у нескольких пиров. Например, если Peer 1 публикует в Peer 2, то у Peer 1 он будет в send, у Peer 2 в recv. Делать ли их как state machine - смотрите как будет лучше клеиться.

когда трек становится Active?

Пока предлагаю ограничиться таким условием: Все треки можно считать active, если оба пира заэмитили и получили ice кандидатов. Но не вижу необходимости обрабатывать это в текущем milestone.

что делать с треками SendRecv?

Meh, тут возникла небольшая путаница в терминологии - мой косяк. В случае с треками я хотел в треке отразить что трек имеет "два конца" - и сендера и ресивера. На самом деле, sendonly/recvonly/sendrecv - характеристика RTCPeerConnection, которая означает что он будет только отправлять/получать/отправлять и получать медиа. Это содержиться в теле sdp.

А сам вопрос "что с ними делать" требует уточнения.

@alexlapa по результатам дисскусию:

  1. Изучить правильное использование cancel_future.
  2. Создавать Peer's при старте комнаты.
  3. Если в процессе сигналинга произошла ошибка, пиры переходят в состояние Failure, клиентам отправляются эвенты об удалении пиров.
  4. Эвенты должны отправляться, по-возможности (есть живая сессия), сразу. Если нельзя отправить эвент, то это ошибка и пиры тоже переходят в состояние Failure. Т.к. нельзя отправить эвент (используя ту же цепочку футур в которой обрабатывается команда) тому же клиенту, что отправил команду используя метод RpcConnection.send_event, эвент можно возвращать как результат футуры.