Skip to content
This repository has been archived by the owner on Aug 25, 2021. It is now read-only.

Tracking: Client WebRTC API #6

Open
1 of 4 tasks
alexlapa opened this issue Nov 20, 2018 · 9 comments
Open
1 of 4 tasks

Tracking: Client WebRTC API #6

alexlapa opened this issue Nov 20, 2018 · 9 comments
Labels
feature New feature or request k::api Related to API (application interface) k::design Related to overall design and/or architecture tracking Tracks implementation of something

Comments

@alexlapa
Copy link
Collaborator

alexlapa commented Nov 20, 2018


Необходимо формализировать пользовательское API. Пока затрагивается только протокол взаимодейстивия с WebRTC пользователями.

API должно покрывать следующие use-case'ы:

  1. N <=> N p2p
  2. N <=> N через media-server

В идеале, клиенту должно быть все-равно в каком режиме он работает. По результатам #4 было решено что p2p = true указывается только для публикующего.

Накидаю реализации для следующих случаев:

  1. 1 <=> 1 p2p
  2. 3 <=> 3 p2p (три пользователя, у каждого по два собеседника).
  3. 3 <=> 3 через media-server (три пользователя, у каждого по два собеседника).

Потом соберу одну подходящую под все варианты.

@alexlapa alexlapa self-assigned this Nov 20, 2018
@alexlapa alexlapa added research Involves researching and investigation k::api Related to API (application interface) k::design Related to overall design and/or architecture labels Nov 20, 2018
@alexlapa
Copy link
Collaborator Author

alexlapa commented Nov 20, 2018

1 <=> 1 p2p

fn main() {
    one_to_one_p2p();
}

struct Turns {
    turns: Vec<String>
}

struct UsersUpdate {
    users: Vec<u64>
}

struct Offer {
    user_id: u64,
    sdp_offer: String,
}

struct Answer {
    user_id: u64,
    sdp_answer: String,
}

struct Candidate {
    user_id: u64,
    candidate: String,
}

struct WSMessage<T> {
    payload: T
}

struct WS {
    url: String
}

impl WS {
    fn send<T>(&self, msg: WSMessage<T>) {}

    fn recv<T>(&self, payload: T) -> WSMessage<T> {
        WSMessage { payload }
    }
}

fn one_to_one_p2p() {

    // 1. User1 joins the room
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 2. User1 receives TURN servers list and just saves it
    let turns1 = user1_ws.recv(Turns {
        turns: Vec::new()
    });



    // 3. User2 joins the room
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 4. User2 receives TURN servers list and just saves it
    let turns2 = user2_ws.recv(Turns {
        turns: Vec::new()
    });

    // 5. User1 receives notification about second user entering the room
    let users_update = user1_ws.recv(UsersUpdate {
        users: vec![2]
    });

    // User 1 js
    // var pc = new PeerConnection(saved_turns);
    // var offer = pc.createOffer();
    // pc.setLocalDescription(offer);

    //6. User1 send offer to user 2
    user1_ws.send(WSMessage {
        payload: Offer {
            user_id: 2,
            sdp_offer: "user_1_sdp_offer".to_owned(),
        }
    });

    //7. User2 receives offer from user1
    let offer = user2_ws.recv(Offer {
        user_id: 1,
        sdp_offer: "user_1_sdp_offer".to_owned(),
    });

    // User2 js
    // var pc = new PeerConnection(saved_turns);
    // pc.setRemotDescription(offer);
    // var answer = pc.createAnswer();

    //8. User2 sends answer
    user2_ws.send(WSMessage {
        payload: Answer {
            user_id: 1,
            sdp_answer: "user_2_sdp_answer".to_owned(),
        }
    });

    //9. User1 receives answer
    let answer = user1_ws.recv(Answer {
        user_id: 2,
        sdp_answer: "user_2_sdp_answer".to_owned(),
    });

    //User1 js
    // pc.setRemoteDescription(answer);

    // Обмен ice candidates опущу - там уже ничего интересного нет. 
    // PeerConnection выплевывает кандидатов, мы их пересылаем.
}

Тут все достаточно просто: Пользователь_1 получает уведомление когда в комнату заходит Пользователь_2 и инициирует связь.

Уведомления о новых пользователях, пожалуй, стоит переделать на команду инициализации подключения. Сервер будет сам говорить клиентам кому и когда звонить. Таким образом мы избавимся от гонок, возникающих при перекрестных оферах.

@alexlapa
Copy link
Collaborator Author

alexlapa commented Nov 21, 2018

3 <=> 3 p2p

fn main() {
    three_to_three_p2p();
}

struct Turns {
    turns: Vec<String>
}

struct OfferRequired {
    user_id: u64
}

struct Offer {
    user_id: u64,
    sdp_offer: String,
}

struct Answer {
    user_id: u64,
    sdp_answer: String,
}

struct Candidate {
    user_id: u64,
    candidate: String,
}

struct WSMessage<T> {
    payload: T
}

struct WS {
    url: String
}

impl WS {
    fn send<T>(&self, msg: WSMessage<T>) {}

    fn recv<T>(&self, payload: T) -> WSMessage<T> {
        WSMessage { payload }
    }
}

// Упрощаем:
// 1. Убираем JS
// 2. Обьединяем ws connect и получение списка TURNS
// 3. Обьединяем отправку оффера с получением оффера, отправкой ответа, получением ответа
fn three_to_three_p2p() {

    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 3. User1 receives notification about User2 entering the room
    let users_update = user1_ws.recv(OfferRequired {
        user_id: 2
    });

    // 4. User1 send offer to User2, receives answer, candidates
    user1_ws.send(WSMessage {
        payload: Offer {
            user_id: 2,
            sdp_offer: "user_1_sdp_offer".to_owned(),
        }
    });

    //5. User3 joins the room, receives TURNS
    let user3_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder2_token".to_owned()
    };

    // 6. User1 receives notification about User3 entering the room
    let users_update = user1_ws.recv(OfferRequired {
        user_id: 3
    });

    // 7. User1 send offer to User3, receives answer, candidates
    user1_ws.send(WSMessage {
        payload: Offer {
            user_id: 3,
            sdp_offer: "user_1_sdp_offer".to_owned(),
        }
    });

    // 8. User2 receives notification about User3 entering the room
    let users_update = user2_ws.recv(OfferRequired {
        user_id: 3
    });

    // 9. User2 send offer to User3, receives answer, candidates
    user2_ws.send(WSMessage {
        payload: Offer {
            user_id: 3,
            sdp_offer: "user_1_sdp_offer".to_owned(),
        }
    });
}

Для п2п пока все складывается:

  1. Зашел 1.
  2. Зашел 2.
  3. Сервер говорит 1 набрать 2.
  4. Зашел 3.
  5. Сервер говорит 1 набрать 3.
  6. Сервер говорит 2 набрать 3.

Так можно продолжать до бесконечности.

Для каждого коннекта используется по одному RTCPeerConnection. В этом примере у всех будет по два.

А вот для трансляций через медиа сервер все не так однозначно.

Есть три стула:

  1. По коннекту на стрим(как в openvidu, janus). В 3 <=> 3 у каждого пользователя будет 3 RTCPeerConnection'а : 1 send, 2 recv.
  2. Все стримы через один коннект(как в jitsi-videobridge).
  3. Один коннекта на send, один на все recv.

Говорят, что один RTCPeerConnection на все(+1 на сервере) хорошо экономит ресурсы, но сложнее в использовании. Учитывая, что на сервере планируется использование gst webrtcbin, все сводится к наличию необходимого функционала в webrtcbin. При этом, в p2p варианте всегда будет по RTCPeerConnection на собеседника. Необходимо чтобы разрабатываемый протокол покрывал все сценарии:

  1. n <=> n p2p.
  2. n <=> n через медиа сервер, у всех клиентов по одному RTCPeerConnection.
  3. n <=> n через медиа сервер, у всех клиентов по одному RTCPeerConnection на каждый входящий/исходящий поток.

Но это чуть позже. Пока недостатки текущей реализации:

  1. Взаимодействие медиа-сервера с контролириующим сервисом подразумевает наличие callback'ов. В текущей реализации медиа-сервер не знает когда пользователь сделал unsubscribe/unpublish, leave происходит с разрывом сокета, что тоже не комильфо, так как исключает возможность реконнекта.
  2. Не покрыты сценарии 1 => 1, n => n. Когда пользователи имеют одностороннюю связь. Эта информация должна прокидываться в RTCPeerConnection при создании offer'а/answer'а(a=recvonly, a=sendrecv, a=sendonly). Также пока не очевидно как нам гарантировать направление потока(1 <- 1, 1 <-> 1, 1 -> 1, 1 - 1). Надо изучить.

@tyranron tyranron added the feature New feature or request label Nov 21, 2018
@tyranron tyranron changed the title User WebRTC API Client WebRTC API Nov 21, 2018
@alexlapa
Copy link
Collaborator Author

alexlapa commented Nov 22, 2018

Про контроль направления:

Тут есть 4 фактора:

  1. Наличие треков в offerer RTCPeerConnection (до createOffer() был вызван addTrack())
  2. Передаваемые в createOffer() значения OfferToReceiveAudio и OfferToReceiveVideo
  3. Наличие треков в answerer RTCPeerConnection (до createAnswer() был вызван addTrack())
  4. Передаваемые в createAnswer() значения OfferToReceiveAudio и OfferToReceiveVideo

Все коминации были протестированы(Chrome):

Create offer:

No tracks Has tracks
OfferToReceive is true recvonly sendrecv
OfferToReceive is false empty sendonly

Create answer:

empty offer recvonly offer sendonly offer sendrecv offer
No tracks, OfferToReceive = true empty answer => no video inactive answer=> no video recvonly answer => offerer -> answerer recvonly answer => offerer -> answerer
No tracks, OfferToReceive = false empty answer => no video inactive answer => no video inactive answer => no video inactive answer => no video
Has track, OfferToReceive = true empty answer => no video sendonly answer => offerer <- answerer recvonly answer => offerer -> answerer sendrecv answer => offerer <-> answerer
Has track, OfferToReceive = false empty answer => no video sendonly answer => offerer <- answerer inactive answer => no video sendonly answer => offerer <- answerer

Исходя из этого можно накидать как мы будем регулировать направления/ограничения. Например, 1 -> 1, и первым зашел подписчик:

  1. Сервер бросает подписчику OfferRequired с addTrack = false, OfferToReceive = true.
  2. Подписчик формирует recvonly offer, передает серверу.
  3. Сервер проверяет что там действительно recvonly, передает публикующему добавив addTrack = true, OfferToReceive = false.
  4. Публикующий получает recvonly offer с addTrack = true, OfferToReceive = false.
  5. Публикущий делает sendonly answer и передает его на сервер.
  6. Сервер проверяет что answer действительно sendonly и передает его подписчику.

@alexlapa
Copy link
Collaborator Author

Second draft.

Добавилось:

  1. Указание и контроль направления потока.
  2. Leave

Не хватает:

  1. Unpublish, Unsubscribe
  2. Multiple send/recv streams in single RTCPeerConnection
fn main() {
    one_to_one_p2p();
    one_to_one_p2p_single_publisher_publisher_enters_room_first();
    one_to_one_p2p__single_publisher_subscriber_enters_room_first();
}

struct RTCOfferOptions {
    ice_restart: bool,
    add_track: Option<Track>,
    offer_to_receive_audio: bool,
    offer_to_receive_video: bool,
}

struct Track {
    audio: bool,
    video: bool,
}

// server => user
struct Turns {
    turns: Vec<String>
}

// server => user
struct OfferRequest {
    peer_id: u64,
    rtc_offer_options: RTCOfferOptions,
}

// user => server => user
struct Offer {
    peer_id: u64,
    sdp_offer: String,
    rtc_answer_options: Option<RTCOfferOptions>, //added on server, before passing to answerer
}

// user => server => user
struct Answer {
    peer_id: u64,
    sdp_answer: String,
}

//user => server => user
struct Candidate {
    peer_id: u64,
    candidate: String,
}

struct Leave {}

struct WS {
    url: String
}

impl WS {
    fn send<T>(&self, payload: T) {}

    fn recv<T>(&self, payload: T) -> T {           
        payload
    }
}

fn one_to_one_p2p() {
    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 3. User1 receives OfferRequest
    let user1_offer_request = user1_ws.recv(OfferRequest {
        peer_id: 2,
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 4. User1 JS
    // check for endpoint_2 associated peerConnection => not found => create one
    // add_track == true => so getUserMedia, addTrack()
    // createOffer(offerToReceive: true) => sendrecv offer => setLocalDescription() => send to media server

    // 5. User1 sends offer
    user1_ws.send(Offer {
        peer_id: 2,
        sdp_offer: "sendrecv_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 6. User2 receives offer
    let user2_received_offer = user2_ws.recv(Offer {
        peer_id: 1,
        sdp_offer: "sendrecv_offer".to_owned(),
        rtc_answer_options: Some(RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        }),
    });

    // 7. User2 JS
    // check for endpoint_1 associated peerConnection => not found => create one
    // add_track == true => so getUserMedia(), addTrack()
    // setRemoteDescription(offer)
    // peerConection.createAnswer(offerToReceive: true) => sendrecv answer => send to media server

    // 8. User2 sends answer
    user2_ws.send(Answer { peer_id: 1, sdp_answer: "sendrecv_answer".to_owned() })

    // User1 receives answer, users exchange candidates
}

fn one_to_one_p2p_single_publisher_publisher_enters_room_first() {
    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 3. User1 receives OfferRequest
    let user1_offer_request = user1_ws.recv(OfferRequest {
        peer_id: 2,
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        },
    });

    // 4. User1 JS
    // check for endpoint_2 associated peerConnection => not found => create one
    // add_track == true => so getUserMedia, addTrack()
    // createOffer(offerToReceive: false) => sendrecv offer => setLocalDescription() => send to media server

    // 5. User1 sends offer
    user1_ws.send(Offer {
        peer_id: 2,
        sdp_offer: "sendonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 6. User2 receives offer
    let user2_received_offer = user2_ws.recv(Offer {
        peer_id: 1,
        sdp_offer: "sendonly_offer".to_owned(),
        rtc_answer_options: Some(RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        }),
    });

    // 7. User2 JS
    // check for endpoint_1 associated peerConnection => not found => create one
    // add_track == false
    // setRemoteDescription(offer)
    // peerConection.createAnswer(offerToReceive: true) => recvonly answer => send to media server

    // 8. User2 sends answer
    user2_ws.send(Answer { peer_id: 1, sdp_answer: "recvonly_answer".to_owned() })

    // User1 receives answer, users exchange candidates
}

fn one_to_one_p2p__single_publisher_subscriber_enters_room_first() {
    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 3. User1 receives OfferRequest
    let user1_offer_request = user1_ws.recv(OfferRequest {
        peer_id: 2,
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 4. User1 JS
    // check for endpoint_2 associated peerConnection => not found => create one
    // add_track == false
    // createOffer(offerToReceive: true) => recvonly offer => setLocalDescription() => send to media server

    // 5. User1 sends offer
    user1_ws.send(Offer {
        peer_id: 2,
        sdp_offer: "recvonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 6. User2 receives offer
    let user2_received_offer = user2_ws.recv(Offer {
        peer_id: 1,
        sdp_offer: "recvonly_offer".to_owned(),
        rtc_answer_options: Some(RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track{
                audio: true,
                video: true
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        }),
    });

    // 7. User2 JS
    // check for endpoint_1 associated peerConnection => not found => create one
    // add_track == true => getUserMedia(), addTrack()
    // setRemoteDescription(offer)
    // peerConection.createAnswer(offerToReceive: false) => sendonly answer => send to media server

    // 8. User2 sends answer
    user2_ws.send(Answer { peer_id: 1, sdp_answer: "sendonly".to_owned() })

    // User1 receives answer, users exchange candidates
}

@alexlapa
Copy link
Collaborator Author

alexlapa commented Nov 26, 2018

Third draft

Добавилось:

  1. Unpublish, Unsubscribe. Добавлены соответсвующие запросы. Они могут быть отправлены как от клиент => сервер, так и сервер => клиент. В случае отправки от клиента, сервер сообщение принимает, делает все необходимые приседания и возвращает полученное сообщение обратно. Клиент начинает предпринимать какие-то действия только после получения ответа. Формат сообщений одинаковый, но семантика зависит от направления.
  2. Multiple send/recv streams in single RTCPeerConnection. peer_id теперь Stream {peer_id, stream_id}.

Проблемы:

  1. Не хватает более глубокой конфигурации, но это дело наживное - механизмы есть.
  2. Проблемы с bulk добавлениями потоков. Например, сценарий конференции. В комнате публикуется 10 потоков, по текущему протколу это будет ряд последовательных операций:
    OfferRequest(server->client) -> createOffer(client) -> addStream(server), Answer(server->client). При этом, последовательность обязательна - чтобы гарантировать корректность ассоциаций stream_id. Когда хочется просто добавить все 10 потоков в серверный RTCPeerConnection и кинуть один offer+answer. Надо добавить bulk подписки-отписки, пересмотреть семантику stream_id.
  3. Как клиент будет ассоциировать потоки с Member'ами? Клиент должен понимать какому пользователю принадлежит поток(и), чтобы можно было докрутить бизнес логику: банальный, вывод имени пользователя под видео в сценарии конференции, окно отправки сообщений и т.д.
  4. Теперь, с появлением Unpublish и Unsubscribe не ясно как обыграть повторную подписку/публикацию. Надо добавлять Publish и Subscribe, которые не очень стыкуются и изначальной идеей о всезнающем и всеконтролирующем медиа-сервере.
  5. Оффер всегда создается на клиентской стороне. По идее, не проблема, но лучше иметь оба варианта.

@tyranron ,

По третьему пункту: думаю, надо давать клиенту данные с Control API.

Например, по этому примеру:

pipeline: hashmap!{
            "publisher" => Member {
                on_join: Some("grpc://127.0.0.1:9090"),
                on_leave: Some("grpc://127.0.0.1:9090"),
                pipeline: hashmap!{
                    "webrtc" => WebRtcPublishEndpoint {
                        p2p: false,
                        on_start: Some("grpc://127.0.0.1:9090"),
                        on_stop: Some("grpc://127.0.0.1:9090"),
                    }
                },
            },
            "player-1" => Member {
                on_join: Some("grpc://127.0.0.1:9090"),
                on_leave: Some("grpc://127.0.0.1:9090"),
                pipeline: hashmap!{
                    "webrtc" => WebRtcPlayEndpoint {
                        src: "member://publisher/webrtc",
                        on_start: Some("grpc://127.0.0.1:9090"),
                        on_stop: Some("grpc://127.0.0.1:9090"),
                    },
                },
            },
            "player-2" => Member {
                on_join: Some("grpc://127.0.0.1:9090"),
                on_leave: Some("grpc://127.0.0.1:9090"),
                pipeline: hashmap!{
                    "hls" => HlsPlayEndpoint {
                        src: "member://publisher/webrtc",
                        on_start: Some("grpc://127.0.0.1:9090"),
                        on_stop: Some("grpc://127.0.0.1:9090"),
                    },
                },
            },
        }

Пользователям "player-1" и "player-2" вместе с OfferRequest надо будет передать что они смотрят на "publisher".

fn main() {
    one_to_one_p2p();
    one_to_one_p2p_single_publisher_publisher_enters_room_first();
    one_to_one_p2p_single_publisher_subscriber_enters_room_first();
    two_pub_one_sub_with_two_peerconnections_sfu();
    two_pub_one_sub_with_one_peerconnection_sfu_and_unsubscribe();
}

struct RTCOfferOptions {
    ice_restart: bool,
    add_track: Option<Track>,
    offer_to_receive_audio: bool,
    offer_to_receive_video: bool,
}

struct Track {
    audio: bool,
    video: bool,
}

// server => user
struct Turns {
    turns: Vec<String>
}

struct Stream {
    peer_id: u64,
    stream_id: u64,
}

// server => user
struct OfferRequest {
    stream: Stream,
    //    stream_id: u64,
    rtc_offer_options: RTCOfferOptions,
}

// user => server => user
struct Offer {
    stream: Stream,
    //    stream_id: u64,
    sdp_offer: String,
    rtc_answer_options: Option<RTCOfferOptions>, //added on server, before passing to answerer
}

// user => server => user
struct Answer {
    stream: Stream,
    //    stream_id: u64,
    sdp_answer: String,
}

//user => server => user
struct Candidate {
    stream: Stream,
    stream_id: u64,
    candidate: String,
}

// user => server, server => user
struct Unpublish {
    stream: Stream,
}

// user => server, server => user
struct Unsubscribe {
    stream: Stream,
}

struct Leave {}

// multiple recv streams in single peer
// multiple send stremas in single peer

struct WS {
    url: String
}

impl WS {
    fn send<T>(&self, payload: T) {}

    fn recv<T>(&self, payload: T) -> T {
        payload
    }
}

fn one_to_one_p2p() {
    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 3. User1 receives OfferRequest
    let user1_offer_request = user1_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 4. User1 JS
    // check for endpoint_2 associated peerConnection => not found => create one
    // add_track == true => so getUserMedia, addTrack()
    // createOffer(offerToReceive: true) => sendrecv offer => setLocalDescription() => send to media server

    // 5. User1 sends offer
    user1_ws.send(Offer {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        sdp_offer: "sendrecv_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 6. User2 receives offer
    let user2_received_offer = user2_ws.recv(Offer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_offer: "sendrecv_offer".to_owned(),
        rtc_answer_options: Some(RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        }),
    });

    // 7. User2 JS
    // check for endpoint_1 associated peerConnection => not found => create one
    // add_track == true => so getUserMedia(), addTrack()
    // setRemoteDescription(offer)
    // peerConection.createAnswer(offerToReceive: true) => sendrecv answer => send to media server

    // 8. User2 sends answer
    user2_ws.send(Answer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_answer: "sendrecv_answer".to_owned(),
    })

    // User1 receives answer, users exchange candidates
}


fn one_to_one_p2p_single_publisher_publisher_enters_room_first() {
    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 3. User1 receives OfferRequest
    let user1_offer_request = user1_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        },
    });

    // 4. User1 JS
    // check for endpoint_2 associated peerConnection => not found => create one
    // add_track == true => so getUserMedia, addTrack()
    // createOffer(offerToReceive: false) => sendrecv offer => setLocalDescription() => send to media server

    // 5. User1 sends offer
    user1_ws.send(Offer {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        sdp_offer: "sendonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 6. User2 receives offer
    let user2_received_offer = user2_ws.recv(Offer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_offer: "sendonly_offer".to_owned(),
        rtc_answer_options: Some(RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        }),
    });

    // 7. User2 JS
    // check for endpoint_1 associated peerConnection => not found => create one
    // add_track == false
    // setRemoteDescription(offer)
    // peerConection.createAnswer(offerToReceive: true) => recvonly answer => send to media server

    // 8. User2 sends answer
    user2_ws.send(Answer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_answer: "recvonly_answer".to_owned(),
    })

    // User1 receives answer, users exchange candidates
}

fn one_to_one_p2p_single_publisher_subscriber_enters_room_first() {
    // 1. User1 joins the room, receives TURNS
    let user1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=responder_token".to_owned()
    };

    // 2. User2 joins the room, receives TURNS
    let user2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=caller_token".to_owned()
    };

    // 3. User1 receives OfferRequest
    let user1_offer_request = user1_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 4. User1 JS
    // check for endpoint_2 associated peerConnection => not found => create one
    // add_track == false
    // createOffer(offerToReceive: true) => recvonly offer => setLocalDescription() => send to media server

    // 5. User1 sends offer
    user1_ws.send(Offer {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        sdp_offer: "recvonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 6. User2 receives offer
    let user2_received_offer = user2_ws.recv(Offer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_offer: "recvonly_offer".to_owned(),
        rtc_answer_options: Some(RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        }),
    });

    // 7. User2 JS
    // check for endpoint_1 associated peerConnection => not found => create one
    // add_track == true => getUserMedia(), addTrack()
    // setRemoteDescription(offer)
    // peerConection.createAnswer(offerToReceive: false) => sendonly answer => send to media server

    // 8. User2 sends answer
    user2_ws.send(Answer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_answer: "sendonly".to_owned(),
    })

    // User1 receives answer, users exchange candidates
}


// two_pub_one_sub_with_two_peerconnections_sfu
//
//                        .-------SFU--------.      .--------sub--------.
// pub1 peer_con_1(1)--->-o(1)->--(1-3)---->-o(3)->-o(3)->peer_con_1(3) :
//                        :                  :      :                   :
// pub2 peer_con_1(2)--->-o(2)->--(2-4)---->-o(4)->-o(4)->peer_con_2(4) :
//                        '------------------'      '-------------------'
fn two_pub_one_sub_with_two_peerconnections_sfu() {


    // 1. Pub 1 publishes on server.
    // 1.1. pub1 joins the room, receives TURNS
    let pub1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=pub1_token".to_owned()
    };

    // 1.2. pub1 receives OfferRequest
    let pub1_offer_request = pub1_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        },
    });

    // 1.3. pub1 sends offer
    pub1_ws.send(Offer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_offer: "pub1_sendonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 1.4. server creates recvonly answer
    let pub1_answer = pub1_ws.recv(Answer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_answer: "pub1_recvonly_answer".to_owned(),
    });

    // 1.5. ice, connection pub1->server created

    // 2. Pub 2 publishes on server
    // 2.1. pub2 joins the room, receives TURNS
    let pub2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=pub2_token".to_owned()
    };

    // 2.2. pub2 receives OfferRequest
    let pub2_offer_request = pub2_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        },
    });

    // 2.3. pub2 sends offer
    pub2_ws.send(Offer {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        sdp_offer: "pub2_sendonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 2.4. server creates recvonly answer
    let pub2_answer = pub2_ws.recv(Answer {
        stream: Stream {
            peer_id: 2,
            stream_id: 0,
        },
        sdp_answer: "pub2_recvonly_answer".to_owned(),
    });

    // 2.5. ice, connection pub2->server created

    // 3. sub subscribes to pub1 video

    // 3.1. sub connects, sets TURNS
    let sub_ws = WS {
        url: "wss://medea.com/ws/room_id?token=sub_token".to_owned()
    };

    // 3.2. sub1 receives OfferRequest
    let sub_offer_request = sub_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 3.3. sub send offer
    sub_ws.send(Offer {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        },
        sdp_offer: "sub_recvonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 3.4. server creates answer
    let sub_answer = sub_ws.recv(Answer {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        },
        sdp_answer: "sub_sendonly_answer".to_owned(),
    });

    // 3.5. ice, connection sub<-server created
}

// two_pub_one_sub_with_one_peerconnection_sfu
//
//                        .---------SFU------.
// pub1 peer_con_1(1)--->-o(1)-->---o(1-3)---:       .-------sub---------.
//                        :         :        o(3)-->-o(3)->peer_con_1(3) :
// pub2 peer_con_1(2)--->-o(2)-->---o(2-3)---:       '-------------------'
//                        '------------------'

fn two_pub_one_sub_with_one_peerconnection_sfu_and_unsubscribe() {

    // 1. Pub 1 publishes on server.
    // 1.1. pub1 joins the room, receives TURNS
    let pub1_ws = WS {
        url: "wss://medea.com/ws/room_id?token=pub1_token".to_owned()
    };

    // 1.2. pub1 receives OfferRequest
    let pub1_offer_request = pub1_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        },
    });

    // 1.3. pub1 sends offer
    pub1_ws.send(Offer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_offer: "pub1_sendonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 1.4. server creates recvonly answer
    let pub1_answer = pub1_ws.recv(Answer {
        stream: Stream {
            peer_id: 1,
            stream_id: 0,
        },
        sdp_answer: "pub1_recvonly_answer".to_owned(),
    });

    // 1.5. ice, connection pub->server created

    // 2. sub subscribes to first publisher

    // 2.1. sub opens ws, receives turns
    let sub_ws = WS {
        url: "wss://medea.com/ws/room_id?token=sub_token".to_owned()
    };

    // 2.2. sub receives OfferRequest to subscribe to first pub
    let sub_offer_request = sub_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 2.3. sub send offer
    sub_ws.send(Offer {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        },
        sdp_offer: "sub_recvonly_1_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 2.4. server returns sendonly answer
    let sub_answer_1 = sub_ws.recv(Answer {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        },
        sdp_answer: "sub1_sendonly_answer".to_owned(),
    });

    // 2.5. ice, connection sub<-server(pub1) created

    // 3. Pub 2 publishes on server.
    // 3.1. pub2 joins the room, receives TURNS
    let pub2_ws = WS {
        url: "wss://medea.com/ws/room_id?token=pub2_token".to_owned()
    };

    // 3.2. pub2 receives OfferRequest
    let pub2_offer_request = pub2_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 2,
            stream_id: 1,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: Some(Track {
                audio: true,
                video: true,
            }),
            offer_to_receive_audio: false,
            offer_to_receive_video: false,
        },
    });

    // 3.3. pub2 sends offer
    pub2_ws.send(Offer {
        stream: Stream {
            peer_id: 2,
            stream_id: 1,
        },
        sdp_offer: "pub2_sendonly_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 3.4. server creates recvonly answer
    let pub2_answer = pub2_ws.recv(Answer {
        stream: Stream {
            peer_id: 2,
            stream_id: 1,
        },
        sdp_answer: "pub2_recvonly_answer".to_owned(),
    });

    // 3.5. ice, connection pub2->server created

    // 4. sub subscribes to first publisher
    let sub_ws = WS {
        url: "wss://medea.com/ws/room_id?token=sub_token".to_owned()
    };

    // 4.2. sub receives OfferRequest to subscribe to first pub
    let sub_offer_request = sub_ws.recv(OfferRequest {
        stream: Stream {
            peer_id: 3,
            stream_id: 1,
        },
        rtc_offer_options: RTCOfferOptions {
            ice_restart: false,
            add_track: None,
            offer_to_receive_audio: true,
            offer_to_receive_video: true,
        },
    });

    // 4.3. sub send offer
    sub_ws.send(Offer {
        stream: Stream {
            peer_id: 3,
            stream_id: 1,
        },
        sdp_offer: "sub_recvonly_1_offer".to_owned(),
        rtc_answer_options: None,
    });

    // 4.4. server returns sendonly answer
    let server_answer_1 = sub_ws.recv(Answer {
        stream: Stream {
            peer_id: 3,
            stream_id: 1,
        },
        sdp_answer: "sub1_sendonly_answer".to_owned(),
    });

    // 4.5. ice, connection sub<-server(pub2) created

    // role     direction   peer    stream
    // pub1     =>          1       0
    // sub      <=          3       0
    // pub2     =>          2       1
    // sub      <=          3       1

    // 5. Sub unsubscribes from pub1

    // 5.1. Sub initializes unsubscribe

    sub_ws.send(Unsubscribe {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        }
    });

    // 5.2. Server does any stuff that it needs to do(i.e invokes some callback) and returns same request
    let unsubscribe_request = sub_ws.recv(Unsubscribe {
        stream: Stream {
            peer_id: 3,
            stream_id: 0,
        }
    });
}

// two_pub_two_sub_with_one_peerconnection_sfu
//
//                        .----------SFU---------.
//                        :        .->--o(1-3)---:         .-------sub1-------.
// pub1 peer_con_1(1)--->-o(1)-->--:    :         o(3)-->--o(3)->peer_con1(3):
//                        :    .->-+-->-o(2-3)---:         '------------------'
//                        :    :   :             :
//                        :    .   '->--o(1-4)---:         .-------sub2-------.
// pub2 peer_con_1(2)--->-o(2)-:        :        o(4)-->---o(4)->peer_con1(4) :
//                        :    '->--->--o(2-4)---:         '------------------'
//                        '----------------------'
//

// two_pub_two_sub_with_two_peerconnections_sfu
//
//                        .----------SFU---------.          .-----sub1-------.
//                        :               .------o(1-3)-->--o(3)->peer_con_1 :
//                        :               :      :          |                :
// pub1 peer_con_1(1)--->-o(1)---->--->---:  .---o(2-4)-->--o(4)->peer_con_2 :
//                        :          .----+--'   :          '----------------'
//                        :          :    :      :          .----------------.
//                        :          :    '------o(1-5)-->--o(5)->peer_con_1 :
// pub2 peer_con_1(2)--->-o(2)--->---:           :          |                :
//                        :          '-----------o(2-6)-->--o(6)->peer_con_2 :
//                        '----------------------'          '----------------'

@alexlapa
Copy link
Collaborator Author

alexlapa commented Dec 13, 2018

4th draft

//   Used primitives
//
//   .-------------------------Member---------------------------.
//   : .-----------Peer----------.  .------------Peer---------. :
//   : : .--Track--. .--Track--. :  : .--Track--. .--Track--. : :
//   : : :  video  : :  audio  : :  : :  video  : :  audio  : : :
//   : : :         : :         : :  : :         : :         : : :
//   : : '----o----' '---------' :  : '---------' '----o----' : :
//   : '------|------------------'  '------------------|------' :
//   '--------V----------------------------------------Λ--------'
//            :                                        :
//            :                                        Λ
//            :------->------>-------.                 '---.
//            :                      :                     :
//            V                      :                     Λ
//            :                      :                     :
//   .--------V--------.    .--------V--------.   .--------Λ--------.
//   : .------|------. :    : .------|------. :   : .------|------. :
//   : : .----o----. : :    : : .----o----. : :   : : .----o----. : :
//   : : :  video  : : :    : : :  video  : : :   : : :  audio  : : :
//   : : :         : : :    : : :         : : :   : : :         : : :
//   : : '--Track--' : :    : : '--Track--' : :   : : '--Track--' : :
//   : '-----Peer----' :    : '-----Peer----' :   : '-----Peer----' :
//   '------Member-----'    '------Member-----'   '------Member-----'


// structs

struct Member {
    peer_id: u64,
    member_id: String,
}

struct Peer {
    peer_id: u64,

    // p2p peer send tracks, offers, answer, candidates MUST have remote peer reference
    // non p2p peer send tracks MAY have remote peers ref
    // non p2p peer offers, answers, candidates MUST NOT have remote peer ref
    p2p: bool,
    tracks: Vec<Track>,
}

struct Track {
    id: u64,
    media_type: TrackMediaType,
    direction: TrackDirection,
}

enum TrackDirection {
    // vec of receiver peers
    // p2p peer send tracks MUST have only one receiver
    // non p2p peer send tracks MAY have receivers(0-N)
    Send(Vec<u64>),

    // sender peer id
    Recv(u64),
}

enum TrackMediaType {
    Audio(AudioSettings),
    Video(VideoSettings),
}

struct AudioSettings {}

struct VideoSettings {}

struct RemotePeer {
    // None if remote peer is SFU without receivers
    peer_id: Option<u64>,
    member_id: String,
    can_rx: Option<RemotePeerTrackType>,
    can_tx: Option<RemotePeerTrackType>,
}

enum RemotePeerTrackType {
    Audio,
    Video,
    AudioVideo,
}

struct Error {}

// messages wrapper, wont be used in examples
// protocol demands every message(except Candidate) to be answered. Answer can have no payload.
struct WsMessage<T> {
    id: u64,
    payload: Option<Result<T, Error>>,
}

// messages
struct Turns {
    turns: Vec<String>
}

struct AddPeer {
    peer: Peer,

    // if None, client must create peer and send offer
    // if Some, client must create peer, set offer and send answer
    sdp_offer: Option<String>,
}

struct RemovePeer {
    peer_id: u64,
}

struct UpdateTracks {
    peer_id: u64,
    tracks: Vec<Track>,
}

struct RequestTracks {
    peer_id: u64,
    remote_peer_id: u64,
    rx: Option<RemotePeerTrackType>,
    tx: Option<RemotePeerTrackType>,
}

struct RemoveTracks {
    peer_id: u64,
    tracks: Vec<u64>,
}

struct Offer {
    peer_id: u64,

    // None if peer is not p2p
    remote_peer_id: Option<u64>,
    sdp_offer: String,
}

struct Answer {
    peer_id: u64,

    // None if peer is not p2p
    remote_peer_id: Option<u64>,
    sdp_answer: String,
}

struct Candidate {
    peer_id: u64,

    // None if peer is not p2p
    remote_peer_id: Option<u64>,
    candidate: String,
}

struct RemotePeers {
    peers: Vec<RemotePeer>
}

struct Ping {}

struct Leave {}

// user requests peers member info
struct GetMembers {
    peers: Vec<u64>,
}

// servers returns peers member info
struct Members {
    members: Vec<Member>
}

struct WS {
    url: String
}

impl WS {
    fn send<T>(&self, payload: T) {}

    fn recv<T>(&self, payload: T) -> T {
        payload
    }
}

// one_to_one_p2p
//
// .-----user1------.    .->-->-->--. .-------user2-------.
// :    pc_id = 2   o(1)=:          :=o(2)  pc_id = 1     :
// '----------------'    '-<--<--<--' '-------------------'
//
fn one_to_one_p2p_unpublish_publish() {

    // server => user1
    AddPeer {
        peer: Peer {
            peer_id: 1,
            p2p: true,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Send(vec![2]),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Send(vec![2]),
                },
                Track {
                    id: 3,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(2),
                },
                Track {
                    id: 4,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Recv(2),
                }],
        },
        sdp_offer: None,
    };

    // user1 => server
    Offer {
        peer_id: 1,
        remote_peer_id: Some(2),
        sdp_offer: "user1_sendrecv_offer".to_owned(),
    };

    // server => user2
    AddPeer {
        peer: Peer {
            peer_id: 2,
            p2p: true,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(1),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(1),
                },
                Track {
                    id: 3,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Send(vec![1]),
                },
                Track {
                    id: 4,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Send(vec![1]),
                }],
        },
        sdp_offer: Some("user1_sendrecv_offer".to_owned()),
    };

    // user2 => server => user1
    Answer {
        peer_id: 2,
        remote_peer_id: None,
        sdp_answer: "user2_sendrecv_answer".to_owned(),
    };

    // .----user1----.    .->-->-->--. .----user2----.
    // :             o(1)=:          :=o(2)          :
    // '-------------'    '-<--<--<--' '-------------'

    // unpublish

    // user1 => server
    RemoveTracks { peer_id: 1, tracks: vec![1, 2] };

    // server => user1
    RemoveTracks { peer_id: 1, tracks: vec![1, 2] };

    // server => user2
    RemoveTracks { peer_id: 2, tracks: vec![1, 2] };

    // renegotiation

    // .----user1----.         .----user2----.
    // :             o(1)-<--<-o(2)          :
    // '-------------'         '-------------'

    // server => user1
    RemotePeers {
        peers: vec![RemotePeer {
            peer_id: Some(2),
            member_id: "user_2".to_owned(),
            can_rx: None,
            can_tx: Some(RemotePeerTrackType::AudioVideo),
        }]
    };

    // server => user2
    RemotePeers {
        peers: vec![RemotePeer {
            peer_id: Some(1),
            member_id: "user_2".to_owned(),
            can_rx: Some(RemotePeerTrackType::AudioVideo),
            can_tx: None,
        }]
    };

    // user1 => server
    RequestTracks {
        peer_id: 1,
        remote_peer_id: 2,
        rx: None,
        tx: Some(RemotePeerTrackType::AudioVideo),
    };

    // server => user1
    UpdateTracks {
        peer_id: 1,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![2]),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![2]),
            },
        ],
    };

    // server => user2
    UpdateTracks {
        peer_id: 2,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Recv(1),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Recv(1),
            },
        ],
    };

    // renegotiation

    // .----user1----.    .->-->-->--. .----user2----.
    // :             o(1)=:          :=o(2)          :
    // '-------------'    '-<--<--<--' '-------------'
}

//-------------------------------------------------------------------------------------------------
// one_to_one_sfu_two_peerconnections
//
//   .------user1------.       .------SFU------.       .------user2-----.
//   :    pc_id = 1    o--->---o-->---->---->--o--->---o   pc_id = 2    :
//   :                 :       :               :       :                :
//   :    pc_id = 4    o---<---o--<----<-----<-o---<---o   pc_id = 3    :
//   '-----------------'       '---------------'       '----------------'
//

fn one_to_one_sfu_two_peerconnections() {

    // server => user1
    AddPeer {
        peer: Peer {
            peer_id: 1,
            p2p: false,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Send(vec![]),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Send(vec![]),
                }
            ],
        },
        sdp_offer: Some("server_user1_recvonly_offer".to_owned()),
    };

    // user1 => server
    Answer {
        peer_id: 1,
        remote_peer_id: None,
        sdp_answer: "user1_sendonly_answer".to_owned(),
    };

//   .------user1------.       .-------SFU-------.
//   :    pc_id = 1    o--->---o                 :
//   :                 :       :                 :
//   :                 :       :                 :
//   '-----------------'       '-----------------'

    // server => user2
    AddPeer {
        peer: Peer {
            peer_id: 2,
            p2p: false,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(1),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Recv(1),
                },
            ],
        },
        sdp_offer: Some("server_user2_sendonly_offer".to_owned()),
    };

    // user2 => server
    Answer {
        peer_id: 2,
        remote_peer_id: None,
        sdp_answer: "user2_recvonly_answer".to_owned(),
    };

    // server => user1
    // just telling user1 that he has subscriber now
    UpdateTracks {
        peer_id: 1,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![2]),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![2]),
            }
        ],
    };

//   .------user1------.       .-------SFU-----.       .------user2-----.
//   :    pc_id = 1    o--->---o-->---->---->--o--->---o   pc_id = 2    :
//   :                 :       :               :       :                :
//   :                 :       :               :       :                :
//   '-----------------'       '---------------'       '----------------'

    // server => user2
    AddPeer {
        peer: Peer {
            peer_id: 3,
            p2p: false,
            tracks: vec![
                Track {
                    id: 3,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Send(vec![]),
                },
                Track {
                    id: 4,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Send(vec![]),
                }
            ],
        },
        sdp_offer: Some("server_user2_recvonly_offer".to_owned()),
    };

    // user2 => server
    Answer {
        peer_id: 3,
        remote_peer_id: None,
        sdp_answer: "user2_sendonly_answer".to_owned(),
    };

//   .------user1------.       .------SFU------.       .------user2-----.
//   :    pc_id = 1    o--->---o-->---->---->--o--->---o   pc_id = 2    :
//   :                 :       :               :       :                :
//   :                 :       :               o---<---o   pc_id = 3    :
//   '-----------------'       '---------------'       '----------------'

    // server => user1
    AddPeer {
        peer: Peer {
            peer_id: 4,
            p2p: false,
            tracks: vec![
                Track {
                    id: 3,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(3),
                },
                Track {
                    id: 4,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Recv(3),
                },
            ],
        },
        sdp_offer: Some("server_user1_sendonly_offer".to_owned()),
    };

    // user2 => server
    Answer {
        peer_id: 4,
        remote_peer_id: None,
        sdp_answer: "user1_recvonly_answer".to_owned(),
    };

    // server => user1
    // just telling user2 that he has subscriber now
    UpdateTracks {
        peer_id: 3,
        tracks: vec![
            Track {
                id: 3,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![4]),
            },
            Track {
                id: 4,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![4]),
            }
        ],
    };

//   .------user1------.       .------SFU------.       .------user2-----.
//   :    pc_id = 1    o--->---o-->---->---->--o--->---o   pc_id = 2    :
//   :                 :       :               :       :                :
//   :    pc_id = 4    o---<---o--<----<-----<-o---<---o   pc_id = 3    :
//   '-----------------'       '---------------'       '----------------'
}


//-------------------------------------------------------------------------------------------------
//
//
//                                                          .-------user2------.
//                             .-------SFU-------.    .-->--o      pc_id = 2   :
//   .------user1------.       :       .--->-->--o-->-'     '------------------'
//   :     pc_id = 1   o-->-->-o--->---:         :
//   '-----------------'       :       '--->-->--o-->-.     .-------user3------.
//                             '-----------------'    '-->--o      pc_id = 3   :
//                                                          '------------------'

fn one_pub_two_sub_sfu() {
    // user1 enters room

    // server => user1
    AddPeer {
        peer: Peer {
            peer_id: 1,
            p2p: false,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Send(vec![]),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Send(vec![]),
                }],
        },
        sdp_offer: Some("server_user1_recvonly_offer".to_owned()),
    };

    // user1 => server
    Answer {
        peer_id: 1,
        remote_peer_id: None,
        sdp_answer: "user_1_sendonly_answer".to_owned(),
    };

//                             .-------SFU-------.
//   .------user1------.       :                 ;
//   :     pc_id = 1   o-->-->-o                 :
//   '-----------------'       :                 ;
//                             '-----------------'

    // server => user2
    AddPeer {
        peer: Peer {
            peer_id: 2,
            p2p: false,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(1),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Recv(1),
                }
            ],
        },
        sdp_offer: Some("server_user2_sendonly_offer".to_owned()),
    };

    //user2 => server
    Answer {
        peer_id: 1,
        remote_peer_id: None,
        sdp_answer: "user_2_recvonly_answer".to_owned(),
    };

//                                                          .-------user2------.
//                             .-------SFU-------.    .-->--o      pc_id = 2   :
//   .------user1------.       :       .---->----o-->-'     '------------------'
//   :     pc_id = 1   o-->-->-o--->---'         :
//   '-----------------'       :                 :
//                             '-----------------'


    // server => user3
    AddPeer {
        peer: Peer {
            peer_id: 3,
            p2p: false,
            tracks: vec![
                Track {
                    id: 1,
                    media_type: TrackMediaType::Audio(AudioSettings {}),
                    direction: TrackDirection::Recv(1),
                },
                Track {
                    id: 2,
                    media_type: TrackMediaType::Video(VideoSettings {}),
                    direction: TrackDirection::Recv(1),
                }
            ],
        },
        sdp_offer: Some("server_user3_sendonly_offer".to_owned()),
    };

    //user3 => server
    Answer {
        peer_id: 3,
        remote_peer_id: None,
        sdp_answer: "user_3_recvonly_answer".to_owned(),
    };

    //server => user1
    UpdateTracks {
        peer_id: 1,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![2, 3]),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![2, 3]),
            }],
    };

//                                                          .-------user2------.
//                             .-------SFU-------.    .-->--o      pc_id = 2   :
//   .------user1------.       :       .---->----o-->-'     '------------------'
//   :     pc_id = 1   o-->-->-o--->---:         :
//   '-----------------'       :       '---->----o-->-.     .-------user3------.
//                             '-----------------'    '-->--o      pc_id = 3   :
//                                                          '------------------'
}

fn one_pub_two_sub_sfu_unpublish_publish() {

    // having
    Peer {
        peer_id: 1,
        p2p: false,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![2, 3]),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![2, 3]),
            }],
    };

    Peer {
        peer_id: 2,
        p2p: false,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Recv(1),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Recv(1),
            }
        ],
    };

    Peer {
        peer_id: 3,
        p2p: false,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Recv(1),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Recv(1),
            }
        ],
    };

//                                                          .-------user2------.
//                             .-------SFU-------.    .-->--o      pc_id = 2   :
//   .------user1------.       :       .---->----o-->-'     '------------------'
//   :     pc_id = 1   o-->-->-o--->---:         :
//   '-----------------'       :       '---->----o-->-.     .-------user3------.
//                             '-----------------'    '-->--o      pc_id = 3   :
//                                                          '------------------'

// -----------------------Unpulbish all------------------------------

    //user1 => server
    RemoveTracks {
        peer_id: 1,
        tracks: vec![1, 2],
    };

    // server => user1
    RemoveTracks {
        peer_id: 1,
        tracks: vec![1, 2],
    };

    //server => user2

    RemoveTracks {
        peer_id: 2,
        tracks: vec![1, 2],
    };

    //server => user3

    RemoveTracks {
        peer_id: 3,
        tracks: vec![1, 2],
    };

//                                                        .-------user2------.
//                             .-------SFU-------.        :      pc_id = 2   :
//   .------user1------.       :                 :        '------------------'
//   :     pc_id = 1   :       :                 :
//   '-----------------'       :                 :        .-------user3------.
//                             '-----------------'        :      pc_id = 3   :
//                                                        '------------------'

// -----------------------Unpulbish to user3----------------------------

    // user1 => server
    UpdateTracks {
        peer_id: 1,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![2]),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![2]),
            }],
    };

    // server => user1
    UpdateTracks {
        peer_id: 1,
        tracks: vec![
            Track {
                id: 1,
                media_type: TrackMediaType::Audio(AudioSettings {}),
                direction: TrackDirection::Send(vec![2]),
            },
            Track {
                id: 2,
                media_type: TrackMediaType::Video(VideoSettings {}),
                direction: TrackDirection::Send(vec![2]),
            }],
    };

    // server => user3
    RemovePeer {
        peer_id: 3
    };

//                                                        .-------user2------.
//                             .-------SFU-------.    .->-o      pc_id = 2   :
//   .------user1------.       :       .---->----o-->-'   '------------------'
//   :     pc_id = 1   o-->-->-o--->---'         :
//   '-----------------'       :                 :        .-------user3------.
//                             '-----------------'        :      pc_id = 3   :
//                                                        '------------------'
}

// ------------------------------------------------------------------------------------------------
//
// .-----pub1------.
// :               o--.     .-----SFU-------.     .------sub-------.
// '---------------'  '-->--o->---------->--o-->--o                :
//                          :               :     :                :
// .------pub2-----.  .-->--o->----------->-o-->--o                :
// :               o--'     '---------------'     '----------------'
// '---------------'
//
// ------------------------------------------------------------------------------------------------
//
//                       .----------SFU---------.     .-----sub1-------.
//                       :               .------o-->--o                :
// .-----pub1------.     :               :      :     |                :
// : peer_con_1----o-->--o------->--->---:  .---o-->--o                :
// '---------------'     :          .----+--'   :     '----------------'
//                       :          :    :      :     .-----sub2-------.
// .------pub2-----.     :          :    '------o-->--o                :
// : peer_con_1----o-->--o------>---:           :     |                :
// '---------------'     :          '-----------o-->--o                :
//                       '----------------------'     '----------------'
// ------------------------------------------------------------------------------------------------

Протокол является достаточно гибким, позволяя реализовать, пожалуй, все варианты использования. Единственное что симулькаст не предусмотрен, но с ним пока все грустно - вменяемого апи нет, и все сводится к шаманству с SDP

Одна из идей - стремление максимально упростить жизнь разработчикам, которые будут прикручивать разрабатываемый клиент к своему фронту. Как работают практически все SFU'шные room-management протоколы:

  1. К конференцию зашел новый пользователь.
  2. Пользователь получил возможность публиковать.
  3. Пользователь получил информацию о всех активных стримах в комнате.
  4. Пользователь подписался на все интересующие кго стримы.
  5. Пользователь опубликовал.
  6. Остальные пользователи увидел что он опубликовал.
  7. Остальные пользователи подписались на его поток.

От разработчиков, взаимодейтсвующих с веб-клиентом требуется биндится к различным ивентам, самим дергать публикации, подписки, думать о настроках медиа.

Чего хотелось достичь:

  1. Пользователь зашел.
  2. Сервер сам все разрулил, на основе имеющейся Control API спеке.

От пользователей клиента не будет требоваться ничего связанного с медиа или сигналлингом. Просто дергаем клиент чтобы тот подключился, дальше сервер с клиентом сами поднимут все необходимое.

Но, естественно, будут случаи, в которых не обойтись без более ручного управиления. Необходимо чтобы публикующий мог выразить желание отключить видео/аудио для всех/кого-то конкретного. Подписчик должен иметь возможность отписаться от видео/аудио. Если есть возможность отменить публикацию/подписку, то необходимо предоставлять возможность переопубликовать/переподписаться. Если давать возможность переопубликовать/передописаться, то нужно предусмотреть механизмы предоставления пользователям текущего состяния комнаты - список потоков на которые они могут подписаться, список пользователей которым они могут опубликовать.

В итоге получается гибрид из двух вариантов API. Для первого варианта достаточно следующих методов:

  1. AddPeer(Server => User)
  2. RemovePeer(Server => User)
  3. UpdateTracks(Server => User)
  4. RemoveTracks(Server => User)
  5. Ну и offer, answer, candidate, turns, leave, ping, ясное дело.

Для ручного управления уже понадобиться:

  1. RemovePeer (User => Server)
  2. UpdateTracks (User => Server)
  3. RemoveTracks (User => Server)
  4. RequestTracks (User => Server)
  5. RemotePeers (Server => User)

Некоторые методы(RemovePeer, UpdateTracks, RemoveTracks) переиспользуются, но уже с другой семантикой. Если Server => User, то сервер говорит пользователю что второй должен сделать. Если User => Server, то пользователь говорит серверу что пользоатель хочет сделать. Предполагается что сервер будет делать все необходимые приседания и просто отвечать пользователю тем-же запросом(или, скорее, просто отправлять подтверждение, после которого клиент выполнит свой запрос, как-будто он получил его от сервера).

Предлагается начать с реализации первого варианта АПИ. Потом, по необходимости приступить к второму.

Недостаток - сравнительная сложность.

сс @tyranron , что думаете по этому варианту?

@alexlapa
Copy link
Collaborator Author

@tyranron ,

Эту issue можно закрывать?

@tyranron
Copy link
Member

@alexlapa nope, this is tracking issue, so it tracks relative RFC implementation. We should provide here in description the current state of how much RFC is implemented. We will close it when RFC is fully implemented.

@alexlapa
Copy link
Collaborator Author

@tyranron ,

Тогда вижу смысл добавить лейбл tracking.

@tyranron tyranron added the tracking Tracks implementation of something label Jan 14, 2019
@tyranron tyranron changed the title Client WebRTC API Tracking: Client WebRTC API Jan 14, 2019
@tyranron tyranron removed the research Involves researching and investigation label Jan 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature New feature or request k::api Related to API (application interface) k::design Related to overall design and/or architecture tracking Tracks implementation of something
Projects
None yet
Development

No branches or pull requests

2 participants