mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-13 17:44:00 +00:00
333 lines
8.1 KiB
Go
333 lines
8.1 KiB
Go
package livekit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/kulikov0/headless-client/webrtc"
|
|
"github.com/kulikov0/headless-client/websocket"
|
|
)
|
|
|
|
const (
|
|
EventOther = iota
|
|
EventJoin
|
|
EventAnswer
|
|
EventOffer
|
|
EventTrickle
|
|
EventUpdate
|
|
EventLeave
|
|
EventToken
|
|
)
|
|
|
|
type TrickleEvent struct {
|
|
Candidate webrtc.ICECandidateInit
|
|
Target int
|
|
}
|
|
|
|
type Event struct {
|
|
Kind int
|
|
Join *JoinResponse
|
|
SDP string
|
|
Trickle *TrickleEvent
|
|
Participants []ParticipantInfo
|
|
Token string
|
|
Leave *LeaveInfo
|
|
}
|
|
|
|
type AddTrackParams struct {
|
|
CID string
|
|
Name string
|
|
Type int
|
|
Source int
|
|
Width uint32
|
|
Height uint32
|
|
}
|
|
|
|
type Codec interface {
|
|
DialURL(serverURL, token string) (string, error)
|
|
MessageType() int
|
|
Accepts(messageType int) bool
|
|
Decode(data []byte) (Event, error)
|
|
EncodeOffer(sdp string) []byte
|
|
EncodeAnswer(sdp string) []byte
|
|
EncodeTrickle(candidate webrtc.ICECandidateInit, target int) []byte
|
|
EncodeAddTrack(p AddTrackParams) []byte
|
|
EncodePing(timestamp int64) []byte
|
|
EncodeLeave() []byte
|
|
}
|
|
|
|
type ProtoCodec struct{}
|
|
|
|
func (ProtoCodec) DialURL(serverURL, token string) (string, error) {
|
|
u, err := url.Parse(serverURL)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parse url: %w", err)
|
|
}
|
|
u.Path = "/rtc"
|
|
q := u.Query()
|
|
q.Set("access_token", token)
|
|
q.Set("protocol", ProtocolVersion)
|
|
q.Set("sdk", SDKName)
|
|
q.Set("version", SDKVersion)
|
|
q.Set("auto_subscribe", "1")
|
|
q.Set("adaptive_stream", "true")
|
|
u.RawQuery = q.Encode()
|
|
return u.String(), nil
|
|
}
|
|
|
|
func (ProtoCodec) MessageType() int { return websocket.BinaryMessage }
|
|
|
|
func (ProtoCodec) Accepts(messageType int) bool { return messageType == websocket.BinaryMessage }
|
|
|
|
func (ProtoCodec) Decode(data []byte) (Event, error) {
|
|
sr, err := decSignalResponse(data)
|
|
if err != nil {
|
|
return Event{}, err
|
|
}
|
|
var ev Event
|
|
switch sr.Kind {
|
|
case signalRespJoin:
|
|
if sr.Join == nil {
|
|
return ev, nil
|
|
}
|
|
ev.Kind = EventJoin
|
|
ev.Join = sr.Join
|
|
case signalRespAnswer:
|
|
if sr.SDP == nil {
|
|
return ev, nil
|
|
}
|
|
ev.Kind = EventAnswer
|
|
ev.SDP = sr.SDP.SDP
|
|
case signalRespOffer:
|
|
if sr.SDP == nil {
|
|
return ev, nil
|
|
}
|
|
ev.Kind = EventOffer
|
|
ev.SDP = sr.SDP.SDP
|
|
case signalRespTrickle:
|
|
if sr.Trickle == nil || sr.Trickle.CandidateInit == "" {
|
|
return ev, nil
|
|
}
|
|
var ic webrtc.ICECandidateInit
|
|
if err := json.Unmarshal([]byte(sr.Trickle.CandidateInit), &ic); err != nil {
|
|
return ev, fmt.Errorf("decode trickle candidate: %w", err)
|
|
}
|
|
ev.Kind = EventTrickle
|
|
ev.Trickle = &TrickleEvent{Candidate: ic, Target: sr.Trickle.Target}
|
|
case signalRespRefreshToken:
|
|
if sr.Token == "" {
|
|
return ev, nil
|
|
}
|
|
ev.Kind = EventToken
|
|
ev.Token = sr.Token
|
|
case signalRespLeave:
|
|
ev.Kind = EventLeave
|
|
ev.Leave = sr.Leave
|
|
case signalRespUpdate:
|
|
if len(sr.Participants) == 0 {
|
|
return ev, nil
|
|
}
|
|
ev.Kind = EventUpdate
|
|
ev.Participants = sr.Participants
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func (ProtoCodec) EncodeOffer(sdp string) []byte {
|
|
return encSignalRequestOffer(sessionDescription{Type: "offer", SDP: sdp})
|
|
}
|
|
|
|
func (ProtoCodec) EncodeAnswer(sdp string) []byte {
|
|
return encSignalRequestAnswer(sessionDescription{Type: "answer", SDP: sdp})
|
|
}
|
|
|
|
func (ProtoCodec) EncodeTrickle(candidate webrtc.ICECandidateInit, target int) []byte {
|
|
js, _ := json.Marshal(candidate)
|
|
return encSignalRequestTrickle(trickleMsg{CandidateInit: string(js), Target: target})
|
|
}
|
|
|
|
func (ProtoCodec) EncodeAddTrack(p AddTrackParams) []byte {
|
|
return encSignalRequestAddTrack(p.CID, p.Name, p.Type, p.Source, p.Width, p.Height)
|
|
}
|
|
|
|
func (ProtoCodec) EncodePing(timestamp int64) []byte {
|
|
return encSignalRequestPing(timestamp)
|
|
}
|
|
|
|
func (ProtoCodec) EncodeLeave() []byte { return encSignalRequestLeave() }
|
|
|
|
const (
|
|
jsonTargetPublisher = "PUBLISHER"
|
|
jsonTargetSubscriber = "SUBSCRIBER"
|
|
)
|
|
|
|
type jsonSDP struct {
|
|
SDP string `json:"sdp"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
type jsonTrickle struct {
|
|
CandidateInit string `json:"candidateInit"`
|
|
Target string `json:"target"`
|
|
}
|
|
|
|
type jsonICEServer struct {
|
|
URLs []string `json:"urls"`
|
|
Username string `json:"username"`
|
|
Credential string `json:"credential"`
|
|
}
|
|
|
|
type jsonJoin struct {
|
|
RoomID string `json:"roomId"`
|
|
PingInterval int32 `json:"pingInterval"`
|
|
ICEServers []jsonICEServer `json:"iceServers"`
|
|
Local struct {
|
|
SID string `json:"sid"`
|
|
Name string `json:"name"`
|
|
UserID string `json:"userId"`
|
|
} `json:"localParticipant"`
|
|
}
|
|
|
|
type jsonVideoLayer struct {
|
|
Quality string `json:"quality"`
|
|
Width uint32 `json:"width"`
|
|
Height uint32 `json:"height"`
|
|
}
|
|
|
|
type jsonAddTrack struct {
|
|
CID string `json:"cid"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Source string `json:"source"`
|
|
Width uint32 `json:"width"`
|
|
Height uint32 `json:"height"`
|
|
Layers []jsonVideoLayer `json:"layers"`
|
|
}
|
|
|
|
type JSONCodec struct{}
|
|
|
|
func (JSONCodec) DialURL(serverURL, _ string) (string, error) { return serverURL, nil }
|
|
|
|
func (JSONCodec) MessageType() int { return websocket.TextMessage }
|
|
|
|
func (JSONCodec) Accepts(messageType int) bool {
|
|
return messageType == websocket.TextMessage || messageType == websocket.BinaryMessage
|
|
}
|
|
|
|
func (JSONCodec) Decode(data []byte) (Event, error) {
|
|
var envelope map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
|
return Event{}, err
|
|
}
|
|
var ev Event
|
|
for kind, raw := range envelope {
|
|
switch kind {
|
|
case "joinResponse":
|
|
var ji jsonJoin
|
|
if err := json.Unmarshal(raw, &ji); err != nil {
|
|
return ev, fmt.Errorf("joinResponse: %w", err)
|
|
}
|
|
join := JoinResponse{
|
|
RoomName: ji.RoomID,
|
|
ParticipantSID: ji.Local.SID,
|
|
ParticipantID: ji.Local.Name,
|
|
LocalUserID: ji.Local.UserID,
|
|
PingIntervalSec: ji.PingInterval,
|
|
}
|
|
for _, is := range ji.ICEServers {
|
|
join.ICEServers = append(join.ICEServers, ICEServer(is))
|
|
}
|
|
ev.Kind = EventJoin
|
|
ev.Join = &join
|
|
return ev, nil
|
|
case "offer", "answer":
|
|
var m jsonSDP
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return ev, fmt.Errorf("%s: %w", kind, err)
|
|
}
|
|
if kind == "offer" {
|
|
ev.Kind = EventOffer
|
|
} else {
|
|
ev.Kind = EventAnswer
|
|
}
|
|
ev.SDP = m.SDP
|
|
return ev, nil
|
|
case "trickle":
|
|
var m jsonTrickle
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return ev, fmt.Errorf("trickle: %w", err)
|
|
}
|
|
if m.CandidateInit == "" {
|
|
return ev, nil
|
|
}
|
|
var ic webrtc.ICECandidateInit
|
|
if err := json.Unmarshal([]byte(m.CandidateInit), &ic); err != nil {
|
|
return ev, fmt.Errorf("decode trickle candidate: %w", err)
|
|
}
|
|
target := TargetPublisher
|
|
if m.Target == jsonTargetSubscriber {
|
|
target = TargetSubscriber
|
|
}
|
|
ev.Kind = EventTrickle
|
|
ev.Trickle = &TrickleEvent{Candidate: ic, Target: target}
|
|
return ev, nil
|
|
}
|
|
}
|
|
return ev, nil
|
|
}
|
|
|
|
func (JSONCodec) encode(envelope string, payload any) []byte {
|
|
body, err := json.Marshal(map[string]any{envelope: payload})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return body
|
|
}
|
|
|
|
func (c JSONCodec) EncodeOffer(sdp string) []byte {
|
|
return c.encode("offer", jsonSDP{SDP: sdp, Type: "offer"})
|
|
}
|
|
|
|
func (c JSONCodec) EncodeAnswer(sdp string) []byte {
|
|
return c.encode("answer", jsonSDP{SDP: sdp, Type: "answer"})
|
|
}
|
|
|
|
func (c JSONCodec) EncodeTrickle(candidate webrtc.ICECandidateInit, target int) []byte {
|
|
js, err := json.Marshal(candidate)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
wire := jsonTrickle{CandidateInit: string(js), Target: jsonTargetPublisher}
|
|
if target == TargetSubscriber {
|
|
wire.Target = jsonTargetSubscriber
|
|
}
|
|
return c.encode("trickle", wire)
|
|
}
|
|
|
|
func (c JSONCodec) EncodeAddTrack(p AddTrackParams) []byte {
|
|
kind := "VIDEO"
|
|
if p.Type == TrackTypeAudio {
|
|
kind = "AUDIO"
|
|
}
|
|
source := "CAMERA"
|
|
if p.Source == TrackSourceScreenShare {
|
|
source = "SCREEN_SHARE"
|
|
}
|
|
return c.encode("addTrack", jsonAddTrack{
|
|
CID: p.CID,
|
|
Name: p.Name,
|
|
Type: kind,
|
|
Source: source,
|
|
Width: p.Width,
|
|
Height: p.Height,
|
|
Layers: []jsonVideoLayer{{Quality: "HIGH", Width: p.Width, Height: p.Height}},
|
|
})
|
|
}
|
|
|
|
func (c JSONCodec) EncodePing(timestamp int64) []byte {
|
|
return c.encode("pingReq", map[string]any{"timestamp": timestamp, "rtt": 0})
|
|
}
|
|
|
|
func (JSONCodec) EncodeLeave() []byte { return nil }
|