Skip to content

Commit

Permalink
Merge pull request #1051 from ggenny/milestione/add-tls-skip-param
Browse files Browse the repository at this point in the history
Integrate WebRTC with RESTful API for Milestone XProtect VMS
  • Loading branch information
AlexxIT committed Apr 28, 2024
2 parents 0eeb3c7 + c309bb8 commit 51c5d51
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 3 deletions.
3 changes: 1 addition & 2 deletions internal/mp4/mp4.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
return
}

src := query.Get("src")
stream := streams.Get(src)
stream := streams.GetOrPatch(query)
if stream == nil {
http.Error(w, api.StreamNotFound, http.StatusNotFound)
return
Expand Down
4 changes: 3 additions & 1 deletion internal/webrtc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ func streamsHandler(rawURL string) (core.Producer, error) {
}

case "http", "https":
if format == "wyze" {
if format == "milestone" {
return milestoneClient(rawURL, query)
} else if format == "wyze" {
// https://github.com/mrlt8/docker-wyze-bridge
return wyzeClient(rawURL)
} else {
Expand Down
218 changes: 218 additions & 0 deletions internal/webrtc/milestone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package webrtc

import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
)

// This package handles the Milestone WebRTC session lifecycle, including authentication,
// session creation, and session update with an SDP answer. It is designed to be used with
// a specific URL format that encodes session parameters. For example:
// webrtc:https://milestone-host/api#format=milestone#username=User#password=TestPassword#cameraId=a539f254-af05-4d67-a1bb-cd9b3c74d122
//
// https://github.com/milestonesys/mipsdk-samples-protocol/tree/main/WebRTC_JavaScript

type milestoneAPI struct {
url string
query url.Values
token string
sessionID string
}

func (m *milestoneAPI) GetToken() error {
data := url.Values{
"client_id": {"GrantValidatorClient"},
"grant_type": {"password"},
"username": m.query["username"],
"password": m.query["password"],
}

req, err := http.NewRequest("POST", m.url+"/IDP/connect/token", strings.NewReader(data.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

// support httpx protocol
res, err := tcp.Do(req)
if err != nil {
return err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return errors.New("milesone: authentication failed: " + res.Status)
}

var payload map[string]interface{}
if err = json.NewDecoder(res.Body).Decode(&payload); err != nil {
return err
}

token, ok := payload["access_token"].(string)
if !ok {
return errors.New("milesone: token not found in the response")
}

m.token = token

return nil
}

func parseFloat(s string) float64 {
if s == "" {
return 0
}
f, _ := strconv.ParseFloat(s, 64)
return f
}

func (m *milestoneAPI) GetOffer() (string, error) {
request := struct {
CameraId string `json:"cameraId"`
StreamId string `json:"streamId,omitempty"`
PlaybackTimeNode struct {
PlaybackTime string `json:"playbackTime,omitempty"`
SkipGaps bool `json:"skipGaps,omitempty"`
Speed float64 `json:"speed,omitempty"`
} `json:"playbackTimeNode,omitempty"`
//ICEServers []string `json:"iceServers,omitempty"`
//Resolution string `json:"resolution,omitempty"`
}{
CameraId: m.query.Get("cameraId"),
StreamId: m.query.Get("streamId"),
}
request.PlaybackTimeNode.PlaybackTime = m.query.Get("playbackTime")
request.PlaybackTimeNode.SkipGaps = m.query.Has("skipGaps")
request.PlaybackTimeNode.Speed = parseFloat(m.query.Get("speed"))

data, err := json.Marshal(request)
if err != nil {
return "", err
}

req, err := http.NewRequest("POST", m.url+"/REST/v1/WebRTC/Session", bytes.NewBuffer(data))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+m.token)
req.Header.Set("Content-Type", "application/json")

res, err := tcp.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return "", errors.New("milesone: create session: " + res.Status)
}

var response struct {
SessionId string `json:"sessionId"`
OfferSDP string `json:"offerSDP"`
}
if err = json.NewDecoder(res.Body).Decode(&response); err != nil {
return "", err
}

var offer pion.SessionDescription
if err = json.Unmarshal([]byte(response.OfferSDP), &offer); err != nil {
return "", err
}

m.sessionID = response.SessionId

return offer.SDP, nil
}

func (m *milestoneAPI) SetAnswer(sdp string) error {
answer := pion.SessionDescription{
Type: pion.SDPTypeAnswer,
SDP: sdp,
}
data, err := json.Marshal(answer)
if err != nil {
return err
}

request := struct {
AnswerSDP string `json:"answerSDP"`
}{
AnswerSDP: string(data),
}
if data, err = json.Marshal(request); err != nil {
return err
}

req, err := http.NewRequest("PATCH", m.url+"/REST/v1/WebRTC/Session/"+m.sessionID, bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+m.token)
req.Header.Set("Content-Type", "application/json")

res, err := tcp.Do(req)
if err != nil {
return err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return errors.New("milesone: patch session: " + res.Status)
}

return nil
}

func milestoneClient(rawURL string, query url.Values) (core.Producer, error) {
mc := &milestoneAPI{url: rawURL, query: query}
if err := mc.GetToken(); err != nil {
return nil, err
}

api, err := webrtc.NewAPI()
if err != nil {
return nil, err
}

conf := pion.Configuration{}
pc, err := api.NewPeerConnection(conf)
if err != nil {
return nil, err
}

prod := webrtc.NewConn(pc)
prod.Desc = "WebRTC/Milestone"
prod.Mode = core.ModeActiveProducer

offer, err := mc.GetOffer()
if err != nil {
return nil, err
}

if err = prod.SetOffer(offer); err != nil {
return nil, err
}

answer, err := prod.GetAnswer()
if err != nil {
return nil, err
}

if err = mc.SetAnswer(answer); err != nil {
return nil, err
}

return prod, nil
}

0 comments on commit 51c5d51

Please sign in to comment.