mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-07 22:35:16 +03:00
342 lines
11 KiB
Go
342 lines
11 KiB
Go
package telemost
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
mathrand "math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pion/interceptor"
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
)
|
|
|
|
const (
|
|
APIBase = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
|
Origin = "https://telemost.yandex.ru"
|
|
)
|
|
|
|
var CapabilitiesOffer = map[string][]string{
|
|
"offerAnswerMode": {"SEPARATE"},
|
|
"initialSubscriberOffer": {"ON_HELLO"},
|
|
"slotsMode": {"FROM_CONTROLLER"},
|
|
"simulcastMode": {"DISABLED", "STATIC"},
|
|
"selfVadStatus": {"FROM_SERVER", "FROM_CLIENT"},
|
|
"dataChannelSharing": {"TO_RTP"},
|
|
"videoEncoderConfig": {"NO_CONFIG", "ONLY_INIT_CONFIG", "RUNTIME_CONFIG"},
|
|
"dataChannelVideoCodec": {"VP8", "UNIQUE_CODEC_FROM_TRACK_DESCRIPTION"},
|
|
"bandwidthLimitationReason": {"BANDWIDTH_REASON_DISABLED", "BANDWIDTH_REASON_ENABLED"},
|
|
"sdkDefaultDeviceManagement": {"SDK_DEFAULT_DEVICE_MANAGEMENT_DISABLED", "SDK_DEFAULT_DEVICE_MANAGEMENT_ENABLED"},
|
|
"joinOrderLayout": {"JOIN_ORDER_LAYOUT_DISABLED", "JOIN_ORDER_LAYOUT_ENABLED"},
|
|
"pinLayout": {"PIN_LAYOUT_DISABLED"},
|
|
"sendSelfViewVideoSlot": {"SEND_SELF_VIEW_VIDEO_SLOT_DISABLED", "SEND_SELF_VIEW_VIDEO_SLOT_ENABLED"},
|
|
"serverLayoutTransition": {"SERVER_LAYOUT_TRANSITION_DISABLED"},
|
|
"sdkPublisherOptimizeBitrate": {"SDK_PUBLISHER_OPTIMIZE_BITRATE_DISABLED", "SDK_PUBLISHER_OPTIMIZE_BITRATE_FULL", "SDK_PUBLISHER_OPTIMIZE_BITRATE_ONLY_SELF"},
|
|
"sdkNetworkLostDetection": {"SDK_NETWORK_LOST_DETECTION_DISABLED"},
|
|
"sdkNetworkPathMonitor": {"SDK_NETWORK_PATH_MONITOR_DISABLED"},
|
|
"publisherVp9": {"PUBLISH_VP9_DISABLED", "PUBLISH_VP9_ENABLED"},
|
|
"svcMode": {"SVC_MODE_DISABLED", "SVC_MODE_L3T3", "SVC_MODE_L3T3_KEY"},
|
|
"subscriberOfferAsyncAck": {"SUBSCRIBER_OFFER_ASYNC_ACK_DISABLED", "SUBSCRIBER_OFFER_ASYNC_ACK_ENABLED"},
|
|
"subscriberDtlsPassiveMode": {"SUBSCRIBER_DTLS_PASSIVE_MODE_DISABLED", "SUBSCRIBER_DTLS_PASSIVE_MODE_ENABLED"},
|
|
"androidBluetoothRoutingFix": {"ANDROID_BLUETOOTH_ROUTING_FIX_DISABLED"},
|
|
"fixedIceCandidatesPoolSize": {"FIXED_ICE_CANDIDATES_POOL_SIZE_DISABLED"},
|
|
"sdkAndroidTelecomIntegration": {"SDK_ANDROID_TELECOM_INTEGRATION_DISABLED"},
|
|
"setActiveCodecsMode": {"SET_ACTIVE_CODECS_MODE_DISABLED", "SET_ACTIVE_CODECS_MODE_VIDEO_ONLY"},
|
|
"publisherOpusDred": {"PUBLISHER_OPUS_DRED_DISABLED"},
|
|
"publisherOpusLowBitrate": {"PUBLISHER_OPUS_LOW_BITRATE_DISABLED"},
|
|
"sdkAndroidDestroySessionOnTaskRemoved": {"SDK_ANDROID_DESTROY_SESSION_ON_TASK_REMOVED_DISABLED"},
|
|
"svcModes": {"FALSE"},
|
|
"reportTelemetryModes": {"TRUE"},
|
|
"keepDefaultDevicesModes": {"FALSE"},
|
|
}
|
|
|
|
var StartupSlotSizes = [][][2]int{
|
|
{{0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}, {0, 0}},
|
|
{{464, 261}, {464, 261}, {464, 261}, {336, 189}, {272, 153}, {272, 153}, {272, 153}, {272, 153}, {224, 126}, {224, 126}, {224, 126}, {224, 126}},
|
|
{{464, 261}, {464, 261}, {464, 261}, {336, 189}, {272, 153}, {272, 153}, {272, 153}, {272, 153}, {224, 126}, {224, 126}, {224, 126}, {224, 126}},
|
|
{{672, 378}, {672, 378}, {464, 261}, {336, 189}, {320, 180}, {320, 180}, {320, 180}, {320, 180}, {272, 153}, {272, 153}, {224, 126}, {224, 126}},
|
|
}
|
|
|
|
type SlotBindEvent struct {
|
|
Slot int
|
|
ParticipantID string
|
|
Mid string
|
|
Reason string
|
|
}
|
|
|
|
type Client struct {
|
|
HTTP *http.Client
|
|
Cookie string
|
|
UserAgent string
|
|
AppVersion string
|
|
InstanceID string
|
|
}
|
|
|
|
func (c *Client) Do(method, path string, body interface{}) ([]byte, int, error) {
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
data, _ := json.Marshal(body)
|
|
bodyReader = strings.NewReader(string(data))
|
|
}
|
|
req, err := http.NewRequest(method, APIBase+path, bodyReader)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
ua := c.UserAgent
|
|
if ua == "" {
|
|
ua = common.UserAgent
|
|
}
|
|
instanceID := c.InstanceID
|
|
if instanceID == "" {
|
|
instanceID = uuid.New().String()
|
|
}
|
|
req.Header.Set("User-Agent", ua)
|
|
req.Header.Set("Origin", Origin)
|
|
req.Header.Set("Referer", Origin+"/")
|
|
req.Header.Set("Client-Instance-Id", instanceID)
|
|
if c.Cookie != "" {
|
|
req.Header.Set("Cookie", c.Cookie)
|
|
}
|
|
if c.AppVersion != "" {
|
|
req.Header.Set("X-Telemost-Client-Version", c.AppVersion)
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
client := c.HTTP
|
|
if client == nil {
|
|
client = http.DefaultClient
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
data, err := io.ReadAll(resp.Body)
|
|
return data, resp.StatusCode, err
|
|
}
|
|
|
|
func (c *Client) TMRequest(method, path string) ([]byte, int, error) {
|
|
return c.Do(method, path, nil)
|
|
}
|
|
|
|
func (c *Client) RequestStates(joinURI, peerID string) error {
|
|
confURL := url.QueryEscape(joinURI)
|
|
body := map[string]interface{}{
|
|
"peers": []map[string]string{{"peer_id": peerID}},
|
|
"permissions": map[string]interface{}{},
|
|
"conference": map[string]interface{}{"version": -1},
|
|
}
|
|
r, status, err := c.Do("POST", "/conferences/"+confURL+"/request-states", body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status != 200 {
|
|
return fmt.Errorf("status %d: %s", status, string(r))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func NewAPI(settingEngine *webrtc.SettingEngine) (*webrtc.API, error) {
|
|
mediaEngine := &webrtc.MediaEngine{}
|
|
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, uri := range []string{
|
|
"urn:ietf:params:rtp-hdrext:toffset",
|
|
"http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",
|
|
"urn:3gpp:video-orientation",
|
|
"http://www.webrtc.org/experiments/rtp-hdrext/playout-delay",
|
|
"http://www.webrtc.org/experiments/rtp-hdrext/video-content-type",
|
|
"http://www.webrtc.org/experiments/rtp-hdrext/video-timing",
|
|
"http://www.webrtc.org/experiments/rtp-hdrext/color-space",
|
|
} {
|
|
if err := mediaEngine.RegisterHeaderExtension(
|
|
webrtc.RTPHeaderExtensionCapability{URI: uri},
|
|
webrtc.RTPCodecTypeVideo,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("register header extension %s: %w", uri, err)
|
|
}
|
|
}
|
|
registry := &interceptor.Registry{}
|
|
if err := webrtc.RegisterDefaultInterceptors(mediaEngine, registry); err != nil {
|
|
return nil, err
|
|
}
|
|
opts := []func(*webrtc.API){
|
|
webrtc.WithMediaEngine(mediaEngine),
|
|
webrtc.WithInterceptorRegistry(registry),
|
|
}
|
|
if settingEngine != nil {
|
|
opts = append(opts, webrtc.WithSettingEngine(*settingEngine))
|
|
}
|
|
return webrtc.NewAPI(opts...), nil
|
|
}
|
|
|
|
func NewPeerConnection(config webrtc.Configuration) (*webrtc.PeerConnection, error) {
|
|
api, err := NewAPI(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return api.NewPeerConnection(config)
|
|
}
|
|
|
|
func MungeSDPAddVideoContent(sdp string) string {
|
|
lines := strings.Split(sdp, "\r\n")
|
|
out := make([]string, 0, len(lines)+4)
|
|
inVideo := false
|
|
inserted := false
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "m=") {
|
|
if inVideo && !inserted {
|
|
out = append(out, "a=content:speaker,main")
|
|
inserted = true
|
|
}
|
|
inVideo = strings.HasPrefix(line, "m=video")
|
|
inserted = false
|
|
}
|
|
out = append(out, line)
|
|
if inVideo && !inserted && strings.HasPrefix(line, "a=mid:") {
|
|
out = append(out, "a=content:speaker,main")
|
|
inserted = true
|
|
}
|
|
}
|
|
return strings.Join(out, "\r\n")
|
|
}
|
|
|
|
func SlotsConfigBindings(v interface{}) []SlotBindEvent {
|
|
m, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
slots, _ := m["slots"].([]interface{})
|
|
var out []SlotBindEvent
|
|
for idx, s := range slots {
|
|
sm, _ := s.(map[string]interface{})
|
|
if pv, _ := sm["participantVideoByMid"].(map[string]interface{}); pv != nil {
|
|
pid, _ := pv["participantId"].(string)
|
|
mid, _ := pv["mid"].(string)
|
|
reason, _ := pv["limitationReason"].(string)
|
|
out = append(out, SlotBindEvent{Slot: idx, ParticipantID: pid, Mid: mid, Reason: reason})
|
|
continue
|
|
}
|
|
if p, _ := sm["participant"].(map[string]interface{}); p != nil {
|
|
pid, _ := p["participantId"].(string)
|
|
out = append(out, SlotBindEvent{Slot: idx, ParticipantID: pid})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func BriefJSON(v interface{}) string {
|
|
const max = 240
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprintf("<json err: %v>", err)
|
|
}
|
|
if len(b) > max {
|
|
return string(b[:max]) + "...(+" + fmt.Sprintf("%d", len(b)-max) + "B)"
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func SetSlotsMessage(key int) map[string]interface{} {
|
|
rnd := mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
|
|
return slotsMessageWithSizes(key, StartupSlotSizes[len(StartupSlotSizes)-1], rnd)
|
|
}
|
|
|
|
func StartupSetSlotsMessage(i, key int) map[string]interface{} {
|
|
rnd := mathrand.New(mathrand.NewSource(time.Now().UnixNano() + int64(i)))
|
|
return slotsMessageWithSizes(key, StartupSlotSizes[i], rnd)
|
|
}
|
|
|
|
func SetSlotsOffsetMessage(offset int) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"setSlotsOffset": map[string]interface{}{"offset": offset},
|
|
}
|
|
}
|
|
|
|
func SdkCodecsInfoMessage() map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"sdkCodecsInfo": map[string]interface{}{
|
|
"vp8": map[string]interface{}{
|
|
"supported": "CODEC_FEATURE_SUPPORTED",
|
|
"hwDecode": "CODEC_FEATURE_NOT_SUPPORTED",
|
|
"hwEncode": "CODEC_FEATURE_NOT_SUPPORTED",
|
|
"isoString": "vp8",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func UpdatePublisherTrackDescriptionMessage(pc *webrtc.PeerConnection, audioLabel, videoLabel string) map[string]interface{} {
|
|
descs := []map[string]interface{}{}
|
|
for _, tr := range pc.GetTransceivers() {
|
|
sender := tr.Sender()
|
|
if sender == nil || sender.Track() == nil {
|
|
continue
|
|
}
|
|
kind := strings.ToUpper(sender.Track().Kind().String())
|
|
mid := tr.Mid()
|
|
label := videoLabel
|
|
groupId := 2
|
|
if kind == "AUDIO" {
|
|
label = audioLabel
|
|
groupId = 1
|
|
}
|
|
descs = append(descs, map[string]interface{}{
|
|
"mid": mid,
|
|
"transceiverMid": mid,
|
|
"kind": kind,
|
|
"priority": 0,
|
|
"label": label,
|
|
"codecs": map[string]interface{}{},
|
|
"groupId": groupId,
|
|
"description": "",
|
|
})
|
|
}
|
|
return map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"updatePublisherTrackDescription": map[string]interface{}{
|
|
"publisherTrackDescriptions": descs,
|
|
},
|
|
}
|
|
}
|
|
|
|
func jitterSize(width int, rnd *mathrand.Rand) (int, int) {
|
|
if width == 0 {
|
|
return 0, 0
|
|
}
|
|
w := width + rnd.Intn(11) - 5
|
|
return w, w * 9 / 16
|
|
}
|
|
|
|
func slotsMessageWithSizes(key int, template [][2]int, rnd *mathrand.Rand) map[string]interface{} {
|
|
slots := make([]map[string]interface{}, len(template))
|
|
for i, wh := range template {
|
|
w, h := wh[0], wh[1]
|
|
if rnd != nil {
|
|
w, h = jitterSize(wh[0], rnd)
|
|
}
|
|
slots[i] = map[string]interface{}{"width": w, "height": h}
|
|
}
|
|
return map[string]interface{}{
|
|
"uid": uuid.New().String(),
|
|
"setSlots": map[string]interface{}{
|
|
"slots": slots,
|
|
"audioSlotsCount": 0,
|
|
"key": key,
|
|
"shutdownAllVideo": nil,
|
|
"withSelfView": true,
|
|
"selfViewVisibility": "ON_LOADING_THEN_SHOW",
|
|
"gridConfig": map[string]interface{}{},
|
|
},
|
|
}
|
|
}
|