fix: implement correct sdp offer/answer exchange and ice handling

This commit is contained in:
gari 2025-04-13 10:52:03 +02:00
parent 8558241aa5
commit 3d88b76c0d

View file

@ -89,34 +89,103 @@ func (api *API) previewHandler(c echo.Context) error {
} }
defer conn.Close() defer conn.Close()
// --- WebRTC Signal Handling ---
// on-negotiation-needed: Triggered when webrtcbin needs to create an offer or answer.
// In our case (server receiving an offer), this fires after the remote description is set.
previewPipeline.webrtcbin.Connect("on-negotiation-needed", func(self *gst.Element) { previewPipeline.webrtcbin.Connect("on-negotiation-needed", func(self *gst.Element) {
log.Debug().Msg("🧠 Negotiation needed (ignored)") log.Info().Msg("🧠 Negotiation needed, creating answer...")
// Create Answer
promise, err := self.Emit("create-answer")
if err != nil {
log.Error().Err(err).Msg("Failed to emit create-answer")
return
}
if promise == nil {
log.Error().Msg("Emit create-answer returned nil promise")
return
}
// Handle the promise result (asynchronously)
promise.(*gst.Promise).Interrupt() // Interrupt any previous waits
promise.(*gst.Promise).Wait() // Wait for the answer to be created
reply := promise.(*gst.Promise).GetReply()
if reply == nil {
log.Error().Msg("Promise reply for create-answer was nil")
return
}
answerValue, ok := reply.GetValue("answer")
if !ok || answerValue == nil {
log.Error().Msg("Failed to get answer from promise reply")
return
}
answer, ok := answerValue.(*gst.WebRTCSessionDescription)
if !ok || answer == nil {
log.Error().Msg("Answer value is not a WebRTCSessionDescription")
return
}
log.Debug().Str("sdp", answer.GetSDP().String()).Msg("✅ Answer created")
// Set Local Description
promise, err = self.Emit("set-local-description", answer)
if err != nil {
log.Error().Err(err).Msg("Failed to emit set-local-description")
return
}
if promise == nil {
log.Error().Msg("Emit set-local-description returned nil promise")
return
}
promise.(*gst.Promise).Interrupt() // Interrupt any previous waits
log.Info().Msg("➡️ Set local description (answer)")
// Send Answer back to the client
log.Info().Msg("📨 Sending SDP answer back to browser")
response := SignalMessage{
Type: "answer",
SDP: answer.GetSDP().String(),
}
msg, err := json.Marshal(response)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal answer")
return
}
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
log.Error().Err(err).Msg("Failed to send answer over WebSocket")
}
}) })
previewPipeline.webrtcbin.Connect("on-ice-candidate", func(self *gst.Element, mlineindex int, candidate string) { // on-ice-candidate: Triggered when webrtcbin generates a new ICE candidate.
previewPipeline.webrtcbin.Connect("on-ice-candidate", func(self *gst.Element, mlineindex uint, candidate string) {
log.Debug().Uint("mlineindex", mlineindex).Str("candidate", candidate).Msg("🧊 Generated ICE candidate")
// Note: The 'sdpMid' is often derived from the mlineindex or context,
// but the signal itself only provides mlineindex and candidate string directly.
// The JS side seems to handle this structure correctly.
ice := SignalMessage{ ice := SignalMessage{
Type: "ice", Type: "ice",
ICE: &IceCandidate{ ICE: &IceCandidate{
Candidate: candidate, Candidate: candidate,
SDPMLineIndex: uint16(mlineindex), SDPMLineIndex: uint16(mlineindex), // Assuming JS expects uint16
// sdpMid might need to be derived if required, but often index is enough
}, },
} }
msg, _ := json.Marshal(ice) msg, err := json.Marshal(ice)
conn.WriteMessage(websocket.TextMessage, msg) if err != nil {
}) log.Error().Err(err).Msg("Failed to marshal ICE candidate")
return
previewPipeline.webrtcbin.Connect("on-sdp-answer", func(self *gst.Element, answer string) { }
log.Info().Msg("📨 Sending SDP answer back to browser") if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
response := SignalMessage{ log.Error().Err(err).Msg("Failed to send ICE candidate over WebSocket")
Type: "answer",
SDP: answer,
} }
msg, _ := json.Marshal(response)
conn.WriteMessage(websocket.TextMessage, msg)
}) })
// --- WebSocket Message Loop ---
for { for {
_, message, err := conn.ReadMessage() messageType, message, err := conn.ReadMessage()
if err != nil { if err != nil {
log.Error().Err(err).Msg("WebSocket read error") log.Error().Err(err).Msg("WebSocket read error")
break break
@ -124,22 +193,57 @@ func (api *API) previewHandler(c echo.Context) error {
var signal SignalMessage var signal SignalMessage
if err := json.Unmarshal(message, &signal); err != nil { if err := json.Unmarshal(message, &signal); err != nil {
log.Warn().Err(err).Msg("Failed to unmarshal signal message") log.Warn().Err(err).Msg("Failed to unmarshal signal message")
log.Warn().Err(err).Str("message", string(message)).Msg("Failed to unmarshal signal message")
continue continue
} }
if signal.Type == "offer" { switch signal.Type {
case "offer":
log.Info().Msg("📥 Received SDP offer from browser") log.Info().Msg("📥 Received SDP offer from browser")
// Pass the SDP offer to webrtcbin log.Debug().Str("sdp", signal.SDP).Msg("Offer SDP")
previewPipeline.webrtcbin.Emit("set-remote-description", "offer", signal.SDP)
// Create an answer // Create WebRTCSessionDescription for the offer
previewPipeline.webrtcbin.Emit("create-answer") offerDesc, err := gst.NewWebRTCSessionDescription(gst.WebRTCSDPTypeOffer, gst.NewSDPMessageFromString(signal.SDP))
log.Debug().Msg("➡️ Sent SDP offer into webrtcbin and requested answer") if err != nil {
} log.Error().Err(err).Msg("Failed to create offer description from SDP")
continue
}
if signal.Type == "ice" && signal.ICE != nil { // Set Remote Description
log.Debug().Str("candidate", signal.ICE.Candidate).Msg(" Adding ICE candidate") // This will trigger on-negotiation-needed if successful and state allows
previewPipeline.webrtcbin.Emit("add-ice-candidate", signal.ICE.SDPMLineIndex, signal.ICE.Candidate) promise, err := previewPipeline.webrtcbin.Emit("set-remote-description", offerDesc)
if err != nil {
log.Error().Err(err).Msg("Failed to emit set-remote-description")
continue
}
if promise == nil {
log.Error().Msg("Emit set-remote-description returned nil promise")
continue
}
promise.(*gst.Promise).Interrupt() // Interrupt previous waits if any
log.Info().Msg("➡️ Set remote description (offer)")
// Answer creation is now handled in on-negotiation-needed
case "ice":
if signal.ICE == nil {
log.Warn().Msg("Received ICE signal with nil ICE field")
continue
}
log.Debug().Str("candidate", signal.ICE.Candidate).Uint16("mlineindex", signal.ICE.SDPMLineIndex).Str("sdpMid", signal.ICE.SDPMid).Msg(" Received ICE candidate from browser")
// Add ICE Candidate
// Note: The signal takes mlineindex (uint) and candidate (string).
// sdpMid is usually associated but not directly passed here.
_, err := previewPipeline.webrtcbin.Emit("add-ice-candidate", uint(signal.ICE.SDPMLineIndex), signal.ICE.Candidate)
if err != nil {
log.Error().Err(err).Msg("Failed to emit add-ice-candidate")
} else {
log.Debug().Msg("Added ICE candidate")
}
default:
log.Warn().Str("type", signal.Type).Msg("Received unknown signal type")
} }
} }