live-sfu-demo/internal/sfu/cloudflare/client.go

162 lines
5.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cloudflare
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const defaultBaseURL = "https://rtc.live.cloudflare.com/v1"
// IceServer 对应 Cloudflare Realtime session 返回的 ICE 配置(可能含 TURN 凭证)。
type IceServer struct {
URLs []string `json:"urls"`
Username string `json:"username,omitempty"`
Credential string `json:"credential,omitempty"`
}
type SessionInfo struct {
SessionID string `json:"sessionId"`
AppID string `json:"appId"`
RequesterIP string `json:"requesterIp,omitempty"`
IceServers []IceServer `json:"iceServers,omitempty"`
}
type SessionDescription struct {
Type string `json:"type"`
SDP string `json:"sdp"`
}
// TrackSpec 描述一条要加入 session 的轨道。
// location=local 表示本端发布;location=remote + SessionID 表示订阅对端 SFU 轨道。
type TrackSpec struct {
Location string `json:"location"`
SessionID string `json:"sessionId,omitempty"`
TrackName string `json:"trackName,omitempty"`
Kind string `json:"kind,omitempty"`
BidirectionalMediaStream bool `json:"bidirectionalMediaStream,omitempty"`
}
type TrackRequest struct {
SessionDescription *SessionDescription `json:"sessionDescription,omitempty"`
Tracks []TrackSpec `json:"tracks,omitempty"`
AutoDiscover bool `json:"autoDiscover,omitempty"`
}
type TrackResult struct {
TrackName string `json:"trackName,omitempty"`
MID string `json:"mid,omitempty"`
Location string `json:"location,omitempty"`
SessionID string `json:"sessionId,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorDescription string `json:"errorDescription,omitempty"`
}
type TracksResponse struct {
SessionDescription *SessionDescription `json:"sessionDescription,omitempty"`
Tracks []TrackResult `json:"tracks,omitempty"`
RequiresImmediateRenegotiation bool `json:"requiresImmediateRenegotiation,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorDescription string `json:"errorDescription,omitempty"`
}
// Client 封装 Cloudflare Realtime REST API(服务端持有 AppSecret)。
type Client struct {
appID string
appSecret string
baseURL string
httpClient *http.Client
}
func NewClient(appID, appSecret, baseURL string) *Client {
if baseURL == "" {
baseURL = defaultBaseURL
}
return &Client{
appID: appID,
appSecret: appSecret,
baseURL: baseURL,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
func (c *Client) appsPath() string { return c.baseURL + "/apps/" + c.appID }
// CreateSession 创建一个空的 Cloudflare Realtime session(correlationId 用于房间关联)。
func (c *Client) CreateSession(correlationID string) (string, error) {
path := c.appsPath() + "/sessions/new"
if correlationID != "" {
path += "?correlationId=" + url.QueryEscape(correlationID)
}
var resp struct {
SessionID string `json:"sessionId"`
}
if err := c.doJSON(http.MethodPost, path, nil, &resp); err != nil {
return "", err
}
return resp.SessionID, nil
}
func (c *Client) GetSession(sessionID string) (*SessionInfo, error) {
var resp SessionInfo
if err := c.doJSON(http.MethodGet, c.appsPath()+"/sessions/"+sessionID, nil, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// AddTracks 在 session 上发布(location=local)或订阅(location=remote)轨道,返回 SDP answer。
func (c *Client) AddTracks(sessionID string, req *TrackRequest) (*TracksResponse, error) {
var resp TracksResponse
if err := c.doJSON(http.MethodPost, c.appsPath()+"/sessions/"+sessionID+"/tracks/new", req, &resp); err != nil {
return nil, err
}
return &resp, nil
}
// DeleteSession 终止 session 并关闭其全部轨道(用于停推 / 踢人)。
func (c *Client) DeleteSession(sessionID string) error {
return c.doJSON(http.MethodDelete, c.appsPath()+"/sessions/"+sessionID, nil, nil)
}
func (c *Client) doJSON(method, path string, body, target interface{}) error {
var reader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("cf marshal: %w", err)
}
reader = bytes.NewReader(data)
}
req, err := http.NewRequest(method, path, reader)
if err != nil {
return fmt.Errorf("cf build request: %w", err)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if c.appSecret != "" {
req.Header.Set("Authorization", "Bearer "+c.appSecret)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("cf request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
b, _ := io.ReadAll(resp.Body)
return fmt.Errorf("cf api error status=%d body=%s", resp.StatusCode, string(b))
}
if target == nil {
return nil
}
if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
return fmt.Errorf("cf decode: %w", err)
}
return nil
}