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 строго обязательное.
Примерное распределение по пакетам следующее:
/api/client
- все что связяно с Client API, а пока это: инициализацияWsListener
, обработка получаемых по сокету сообщений, протокольные ДТО,WsSessionsRepository
.- '/api/control' - все что связано с
Control API
. Пока это: Member,MemberRepository
. /media
- предметная область:Peer
,Track
.PeerRepository
./log
- враппер предоставляющий средства для логирования внутри приложения чего угодно./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:
Member
Peer
Track
В добавлении Room
из 0001-control-api пока смысла нет - комната одна будет.
Что хочется получить:
- Удобные top-level абстракции.
- Корректная
Peer
state-machine. Тут можно частично вдохновится списоком состоянийRTCPeerConnection
: signaling_state, connection_state. 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()
), hardcodecaller
andresponder
(#13). - Add
WsHandler
(just handlesupgrade
request), add WsSessions repository (#14) - Add
Configuration
(#15). - Implement signaling (#16)
- Add
Track
,Peer
,PeerRepository
- Create
Peer
onMember
connect - Send
PeerCreated
to firstMember
on bothMember
s connected. - Handle
MakeSdpOffer
, sendPeerCreated
to secondMember
. - Handle
MakeSdpAnswer
, sendSdpAnswerMade
. - Handle
SetIceCandidate
, sendIceCandidateDiscovered
. - Clean up on Member disconnect.
- Add integration tests - full cycle from ws established to dropped.
- Add
- 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 и здешнего примера:
- когда мы добавляем трек `peer.add_sender(Track):
а. трек сохраняется в пире, и создается и возвращается ответный пир
б. в пире сохраняется только ид трека, а сам трек, реализованый в виде state machine, изменяет состояние и возвращается - когда трек становится Active?
- что делать с треками 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 по результатам дисскусию:
- Изучить правильное использование
cancel_future
. - Создавать Peer's при старте комнаты.
- Если в процессе сигналинга произошла ошибка, пиры переходят в состояние
Failure
, клиентам отправляются эвенты об удалении пиров. - Эвенты должны отправляться, по-возможности (есть живая сессия), сразу. Если нельзя отправить эвент, то это ошибка и пиры тоже переходят в состояние
Failure
. Т.к. нельзя отправить эвент (используя ту же цепочку футур в которой обрабатывается команда) тому же клиенту, что отправил команду используя методRpcConnection.send_event
, эвент можно возвращать как результат футуры.