Unify all WebRTC signaling in the API

Users, workers, and the coordinator will now send WebRTC signaling
information through a single API endpoint with a unified structure. The
payload should contain either an sdp or ice field for further processing
by the designated handlers.

Replaced API endpoints:
  - (101) WebrtcOffer -> WebrtcSignal
    Removed API endpoints:
  - WebrtcAnswer (102)
  - WebrtcIce (103)
This commit is contained in:
sergystepanov 2026-06-06 18:16:41 +03:00
parent f72202684f
commit a4b4e0458f
13 changed files with 63 additions and 69 deletions

View file

@ -70,9 +70,7 @@ const (
CheckLatency PT = 3
InitSession PT = 4
InitWebrtcStream PT = 100
WebrtcOffer PT = 101
WebrtcAnswer PT = 102
WebrtcIce PT = 103
WebrtcSignal PT = 101
StartGame PT = 104
QuitGame PT = 105
SaveGame PT = 106
@ -84,7 +82,6 @@ const (
ResetGame PT = 113
RegisterRoom PT = 201
CloseRoom PT = 202
IceCandidate = WebrtcIce
TerminateSession PT = 204
AppVideoChange PT = 150
LibNewGameList PT = 205
@ -99,12 +96,8 @@ func (p PT) String() string {
return "InitSession"
case InitWebrtcStream:
return "InitWebrtcStream"
case WebrtcOffer:
return "WebrtcOffer"
case WebrtcAnswer:
return "WebrtcAnswer"
case WebrtcIce:
return "WebrtcIce"
case WebrtcSignal:
return "WebrtcSignal"
case StartGame:
return "StartGame"
case ChangePlayer:

View file

@ -31,9 +31,10 @@ type (
Title string `json:"title"`
System string `json:"system"`
}
WebrtcAnswerUserRequest string
WebrtcUserIceCandidate string
WebrtcSignalUser struct {
Ice *string `json:"ice,omitempty"`
Sdp *string `json:"sdp,omitempty"`
}
InitUserWebrtcStreamRequest struct {
Initiator bool `json:"initiator"`
Sdp string `json:"sdp,omitempty"`

View file

@ -41,13 +41,10 @@ type (
}
RecordGameResponse string
TerminateSessionRequest Stateful
WebrtcAnswerRequest struct {
WebrtcSignalRequest struct {
Stateful
Sdp string `json:"sdp"`
}
WebrtcIceCandidateRequest struct {
Stateful
Candidate string `json:"candidate"`
Sdp *string `json:"sdp,omitempty"`
Ice *string `json:"ice,omitempty"`
}
InitWebrtcStreamRequest struct {
// Stateful

View file

@ -48,10 +48,8 @@ func (u *User) HandleRequests(info HasServerInfo, conf config.CoordinatorConfig)
switch x.T {
case api.InitWebrtcStream:
err = api.Do(x, u.HandleInitWebrtcStream)
case api.WebrtcAnswer:
err = api.Do(x, u.HandleWebrtcAnswer)
case api.WebrtcIce:
err = api.Do(x, u.HandleWebrtcIceCandidate)
case api.WebrtcSignal:
err = api.Do(x, u.HandleWebrtcSignal)
case api.StartGame:
err = api.Do(x, func(d api.GameStartUserRequest) { u.HandleStartGame(d, conf) })
case api.QuitGame:

View file

@ -27,10 +27,14 @@ func (u *User) InitSession(wid string, ice []config.IceServer, games []api.AppMe
}
// SendWebrtcOffer sends SDP offer back to the user.
func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
func (u *User) SendWebrtcOffer(sdp string) {
u.Notify(api.WebrtcSignal, api.WebrtcSignalUser{Sdp: &sdp})
}
// SendWebrtcIceCandidate sends remote ICE candidate back to the user.
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }
func (u *User) SendWebrtcIceCandidate(candidate string) {
u.Notify(api.WebrtcSignal, api.WebrtcSignalUser{Ice: &candidate})
}
// StartGame signals the user that everything is ready to start a game.
func (u *User) StartGame(av *api.AppVideoInfo, kbMouse bool) {

View file

@ -21,12 +21,8 @@ func (u *User) HandleInitWebrtcStream(rq api.InitUserWebrtcStreamRequest) {
u.SendWebrtcOffer(string(*resp))
}
func (u *User) HandleWebrtcAnswer(rq api.WebrtcAnswerUserRequest) {
u.w.WebrtcAnswer(u.Id().String(), string(rq))
}
func (u *User) HandleWebrtcIceCandidate(rq api.WebrtcUserIceCandidate) {
u.w.WebrtcIceCandidate(u.Id().String(), string(rq))
func (u *User) HandleWebrtcSignal(rq api.WebrtcSignalUser) {
u.w.WebrtcSignal(u.Id().String(), rq.Sdp, rq.Ice)
}
func (u *User) HandleStartGame(rq api.GameStartUserRequest, conf config.CoordinatorConfig) {

View file

@ -85,9 +85,12 @@ func (w *Worker) HandleRequests(users HasUserRegistry) chan struct{} {
})
case api.CloseRoom:
err = api.Do(p, w.HandleCloseRoom)
case api.IceCandidate:
err = api.DoE(p, func(d api.WebrtcIceCandidateRequest) error {
return w.HandleIceCandidate(d, users)
case api.WebrtcSignal:
err = api.DoE(p, func(rq api.WebrtcSignalRequest) error {
if rq.Ice == nil {
return fmt.Errorf("ice candidate is missing")
}
return w.HandleIceCandidate(rq, users)
})
case api.LibNewGameList:
err = api.DoE(p, w.HandleLibGameList)

View file

@ -7,14 +7,10 @@ func (w *Worker) InitWebrtcStream(id string, initiator bool, sdp string) (*api.I
w.Send(api.InitWebrtcStream, api.InitWebrtcStreamRequest{Id: id, Initiator: initiator, Sdp: sdp}))
}
func (w *Worker) WebrtcAnswer(id string, sdp string) {
w.Notify(api.WebrtcAnswer,
api.WebrtcAnswerRequest{Stateful: api.Stateful{Id: id}, Sdp: sdp})
}
func (w *Worker) WebrtcIceCandidate(id string, candidate string) {
w.Notify(api.WebrtcIce,
api.WebrtcIceCandidateRequest{Stateful: api.Stateful{Id: id}, Candidate: candidate})
func (w *Worker) WebrtcSignal(id string, sdp, ice *string) {
w.Notify(api.WebrtcSignal, api.WebrtcSignalRequest{
Stateful: api.Stateful{Id: id}, Ice: ice, Sdp: sdp,
})
}
func (w *Worker) StartGame(id string, req api.GameStartUserRequest) (*api.StartGameResponse, error) {

View file

@ -11,9 +11,9 @@ func (w *Worker) HandleCloseRoom(rq api.CloseRoomRequest) {
}
}
func (w *Worker) HandleIceCandidate(rq api.WebrtcIceCandidateRequest, users HasUserRegistry) error {
func (w *Worker) HandleIceCandidate(rq api.WebrtcSignalRequest, users HasUserRegistry) error {
if usr := users.Find(rq.Id); usr != nil {
usr.SendWebrtcIceCandidate(rq.Candidate)
usr.SendWebrtcIceCandidate(*rq.Ice)
} else {
w.log.Warn().Str("id", rq.Id).Msg("unknown session")
}

View file

@ -84,10 +84,8 @@ func (c *coordinator) HandleRequests(w *Worker) chan struct{} {
err = api.Do(x, func(d api.ChangePlayerRequest) { out = c.HandleChangePlayer(d, w) })
case api.RecordGame:
err = api.Do(x, func(d api.RecordGameRequest) { out = c.HandleRecordGame(d, w) })
case api.WebrtcAnswer:
err = api.Do(x, func(d api.WebrtcAnswerRequest) { c.HandleWebrtcAnswer(d, w) })
case api.WebrtcIce:
err = api.Do(x, func(d api.WebrtcIceCandidateRequest) { c.HandleWebrtcIceCandidate(d, w) })
case api.WebrtcSignal:
err = api.Do(x, func(d api.WebrtcSignalRequest) { c.HandleWebrtcSignal(d, w) })
case api.TerminateSession:
err = api.Do(x, func(d api.TerminateSessionRequest) { c.HandleTerminateSession(d, w) })
case api.QuitGame:
@ -110,9 +108,9 @@ func (c *coordinator) RegisterRoom(id string) { c.Notify(api.RegisterRoom, id) }
// CloseRoom sends a signal to coordinator which will remove that room from its list.
func (c *coordinator) CloseRoom(id string) { c.Notify(api.CloseRoom, id) }
func (c *coordinator) IceCandidate(candidate string, sessionId string) {
c.Notify(api.WebrtcIce, api.WebrtcIceCandidateRequest{
Stateful: api.Stateful{Id: sessionId},
Candidate: candidate,
c.Notify(api.WebrtcSignal, api.WebrtcSignalRequest{
Stateful: api.Stateful{Id: sessionId},
Ice: &candidate,
})
}

View file

@ -72,18 +72,22 @@ func (c *coordinator) HandleInitWebrtcStream(rq api.InitWebrtcStreamRequest, w *
return api.Out{Payload: sdp}
}
func (c *coordinator) HandleWebrtcAnswer(rq api.WebrtcAnswerRequest, w *Worker) {
if user := w.router.FindUser(rq.Id); user != nil {
if err := room.WithWebRTC(user.Session).SetRemoteSDP(rq.Sdp, fromJson); err != nil {
func (c *coordinator) HandleWebrtcSignal(rq api.WebrtcSignalRequest, w *Worker) {
user := w.router.FindUser(rq.Id)
if user == nil {
return
}
if rq.Sdp != nil {
if err := room.WithWebRTC(user.Session).SetRemoteSDP(*rq.Sdp, fromJson); err != nil {
c.log.Error().Err(err).Msgf("cannot set remote SDP of client [%v]", rq.Id)
}
}
}
func (c *coordinator) HandleWebrtcIceCandidate(rs api.WebrtcIceCandidateRequest, w *Worker) {
if user := w.router.FindUser(rs.Id); user != nil {
if err := room.WithWebRTC(user.Session).AddCandidate(rs.Candidate, fromJson); err != nil {
c.log.Error().Err(err).Msgf("cannot add ICE candidate of the client [%v]", rs.Id)
if rq.Ice != nil {
if err := room.WithWebRTC(user.Session).AddCandidate(*rq.Ice, fromJson); err != nil {
c.log.Error().Err(err).Msgf("cannot add ICE candidate of the client [%v]", rq.Id)
}
}
}

View file

@ -4,9 +4,7 @@ const endpoints = {
LATENCY_CHECK: 3,
INIT: 4,
INIT_WEBRTC_STREAM: 100,
OFFER: 101,
ANSWER: 102,
ICE_CANDIDATE: 103,
WEBRTC_SIGNAL: 101,
GAME_START: 104,
GAME_QUIT: 105,
GAME_SAVE: 106,
@ -347,8 +345,8 @@ export const api = {
...(sdpOffer && { sdp: toJson(sdpOffer) }),
}),
sendIceCandidate: (candidate) =>
packet(endpoints.ICE_CANDIDATE, toJson(candidate)),
sendSdp: (sdp) => packet(endpoints.ANSWER, toJson(sdp)),
packet(endpoints.WEBRTC_SIGNAL, { ice: toJson(candidate) }),
sendSdp: (sdp) => packet(endpoints.WEBRTC_SIGNAL, { sdp: toJson(sdp) }),
latencyCheck: (id, list) => packet(endpoints.LATENCY_CHECK, list, id),
getWorkerList: () => packet(endpoints.GET_WORKER_LIST),
},

View file

@ -184,11 +184,17 @@ const onMessage = (m) => {
const initiator = !options.webrtcWaitOffer;
handleWebrtcStart({ data: payload, initiator });
break;
case api.endpoint.OFFER:
webrtc.answer(api.fromJson(payload));
break;
case api.endpoint.ICE_CANDIDATE:
webrtc.candidate(payload ? api.fromJson(payload) : "");
case api.endpoint.WEBRTC_SIGNAL:
const data = payload;
// it is eaither sdp or ice
if (Object.hasOwn(data, "ice")) {
webrtc.candidate(data.ice ? api.fromJson(data.ice) : "");
return;
}
if (Object.hasOwn(data, "sdp")) {
webrtc.answer(api.fromJson(data.sdp));
return;
}
break;
case api.endpoint.GAME_START:
if (payload.av) pub(APP_VIDEO_CHANGED, payload.av);