feat(gateway): repair native Apollo provider path
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
protocol "git.sechmachine.io.vn/sechmachine/VerseVDI-Protocol/gen/go/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
maxApolloRTSPHeaders = 16 << 10
|
||||
maxApolloRTSPBody = 48 << 10
|
||||
maxApolloRTSPPort = 65535
|
||||
apolloEncryptionAll = 0x07
|
||||
)
|
||||
|
||||
type apolloRTSPMessage struct {
|
||||
status int
|
||||
cseq uint32
|
||||
headers map[string]string
|
||||
body []byte
|
||||
raw []byte
|
||||
}
|
||||
|
||||
type apolloRTSPSetup struct {
|
||||
sessionID string
|
||||
audioPort int
|
||||
videoPort int
|
||||
controlPort int
|
||||
audioPing []byte
|
||||
videoPing []byte
|
||||
controlConnect uint32
|
||||
streamHost string
|
||||
streamPort int64
|
||||
providerWork protocol.ProviderSessionWork
|
||||
streamKey []byte
|
||||
streamKeyID uint32
|
||||
}
|
||||
|
||||
func (b *NativeApolloBackend) performRTSPHandshake(ctx context.Context, work protocol.ProviderSessionWork, key []byte, keyID uint32, streamURL *url.URL) (*apolloRTSPSetup, []byte, error) {
|
||||
if streamURL == nil || streamURL.Scheme != "rtspenc" || streamURL.Hostname() != work.StreamHost || streamURL.Port() != strconv.FormatInt(work.StreamPort, 10) || streamURL.User != nil || streamURL.RawQuery != "" || streamURL.Fragment != "" {
|
||||
return nil, nil, ErrProviderMalformed
|
||||
}
|
||||
codec, err := newEncryptedRTSPCodec(key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
request := func(method, target, session string, headers []apolloRTSPHeader, body []byte, sequence uint32) (apolloRTSPMessage, error) {
|
||||
return b.encryptedRTSPRequest(ctx, work, codec, method, target, session, headers, body, sequence)
|
||||
}
|
||||
options, err := request("OPTIONS", streamURL.String(), "", nil, nil, 1)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
describe, err := request("DESCRIBE", streamURL.String(), "", []apolloRTSPHeader{{"Accept", "application/sdp"}, {"If-Modified-Since", "Thu, 01 Jan 1970 00:00:00 GMT"}}, nil, 2)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := validateApolloDescribe(describe); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
setupHeaders := []apolloRTSPHeader{{"Transport", "unicast;X-GS-ClientPort=50000-50001"}, {"If-Modified-Since", "Thu, 01 Jan 1970 00:00:00 GMT"}}
|
||||
audio, err := request("SETUP", "streamid=audio/0/0", "", setupHeaders, nil, 3)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sessionID, err := apolloRTSPSession(audio)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
audioPort, err := apolloRTSPServerPort(audio)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
audioPing, err := apolloRTSPPingPayload(audio)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
video, err := request("SETUP", "streamid=video/0/0", sessionID, setupHeaders, nil, 4)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := apolloRTSPMatchSession(video, sessionID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
videoPort, err := apolloRTSPServerPort(video)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
videoPing, err := apolloRTSPPingPayload(video)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
control, err := request("SETUP", "streamid=control/13/0", sessionID, setupHeaders, nil, 5)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := apolloRTSPMatchSession(control, sessionID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
controlPort, err := apolloRTSPServerPort(control)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
connectData, err := apolloRTSPConnectData(control)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
announceBody := apolloAnnounceProfile()
|
||||
announce, err := request("ANNOUNCE", "streamid=control/13/0", sessionID, []apolloRTSPHeader{{"Content-Type", "application/sdp"}}, announceBody, 6)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := apolloRTSPMatchSession(announce, sessionID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
play, err := request("PLAY", "/", sessionID, nil, nil, 7)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := apolloRTSPMatchSession(play, sessionID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_ = options
|
||||
setup := &apolloRTSPSetup{
|
||||
sessionID: sessionID, audioPort: audioPort, videoPort: videoPort, controlPort: controlPort,
|
||||
audioPing: audioPing, videoPing: videoPing, controlConnect: connectData, streamHost: work.StreamHost,
|
||||
streamPort: work.StreamPort, providerWork: work, streamKey: append([]byte(nil), key...), streamKeyID: keyID,
|
||||
}
|
||||
return setup, append([]byte(nil), control.raw...), nil
|
||||
}
|
||||
|
||||
type apolloRTSPHeader struct{ key, value string }
|
||||
|
||||
func (b *NativeApolloBackend) encryptedRTSPRequest(ctx context.Context, work protocol.ProviderSessionWork, codec *encryptedRTSPCodec, method, target, session string, headers []apolloRTSPHeader, body []byte, sequence uint32) (apolloRTSPMessage, error) {
|
||||
if b == nil || b.Dialer == nil || codec == nil || sequence == 0 || len(body) > maxApolloRTSPBody || method == "" || target == "" {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
conn, err := b.Dialer.DialContext(ctx, "tcp", net.JoinHostPort(work.StreamHost, strconv.FormatInt(work.StreamPort, 10)))
|
||||
if err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
defer conn.Close()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
||||
deadline = contextDeadline
|
||||
}
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
plaintext, err := buildApolloRTSPRequest(method, target, session, headers, body, sequence)
|
||||
if err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
frame, err := codec.SealClient(plaintext)
|
||||
if err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
if _, err := conn.Write(frame); err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
response, err := readEncryptedRTSPMessage(conn, codec)
|
||||
if err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
if response.status != 200 || response.cseq != sequence {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func buildApolloRTSPRequest(method, target, session string, headers []apolloRTSPHeader, body []byte, sequence uint32) ([]byte, error) {
|
||||
if method == "" || target == "" || strings.ContainsAny(method, "\r\n ") || strings.ContainsAny(target, "\r\n") || sequence == 0 {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
var builder strings.Builder
|
||||
builder.Grow(256 + len(body))
|
||||
fmt.Fprintf(&builder, "%s %s RTSP/1.0\r\nCSeq: %d\r\n", method, target, sequence)
|
||||
if session != "" {
|
||||
if !validApolloRTSPToken(session) {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
fmt.Fprintf(&builder, "Session: %s\r\n", session)
|
||||
}
|
||||
seen := map[string]struct{}{"cseq": {}, "session": {}}
|
||||
for _, header := range headers {
|
||||
key := strings.ToLower(header.key)
|
||||
if !validApolloRTSPToken(header.key) || header.value == "" || len(header.value) > 1024 || strings.ContainsAny(header.value, "\r\n") {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
fmt.Fprintf(&builder, "%s: %s\r\n", header.key, header.value)
|
||||
}
|
||||
if len(body) != 0 {
|
||||
if _, ok := seen["content-length"]; ok {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
fmt.Fprintf(&builder, "Content-Length: %d\r\n", len(body))
|
||||
}
|
||||
builder.WriteString("\r\n")
|
||||
builder.Write(body)
|
||||
return []byte(builder.String()), nil
|
||||
}
|
||||
|
||||
func readEncryptedRTSPMessage(conn net.Conn, codec *encryptedRTSPCodec) (apolloRTSPMessage, error) {
|
||||
header := make([]byte, encryptedRTSPHeaderSize)
|
||||
if _, err := io.ReadFull(conn, header); err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
length := uint64(header[0]&0x7f)<<24 | uint64(header[1])<<16 | uint64(header[2])<<8 | uint64(header[3])
|
||||
if header[0]&0x80 == 0 || length == 0 || length > encryptedRTSPMaxPayload {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
frame := make([]byte, encryptedRTSPHeaderSize+int(length))
|
||||
copy(frame, header)
|
||||
if _, err := io.ReadFull(conn, frame[encryptedRTSPHeaderSize:]); err != nil {
|
||||
return apolloRTSPMessage{}, err
|
||||
}
|
||||
plaintext, err := codec.OpenHost(frame)
|
||||
if err != nil {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
return parseApolloRTSPMessage(plaintext)
|
||||
}
|
||||
|
||||
func parseApolloRTSPMessage(data []byte) (apolloRTSPMessage, error) {
|
||||
if len(data) == 0 || len(data) > encryptedRTSPMaxPayload {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
headerEnd := strings.Index(string(data), "\r\n\r\n")
|
||||
if headerEnd < 0 || headerEnd+4 > maxApolloRTSPHeaders {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
lines := strings.Split(string(data[:headerEnd]), "\r\n")
|
||||
if len(lines) < 1 {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
parts := strings.SplitN(lines[0], " ", 3)
|
||||
if len(parts) != 3 || parts[0] != "RTSP/1.0" || len(parts[2]) == 0 || len(parts[2]) > 128 {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
status, err := strconv.Atoi(parts[1])
|
||||
if err != nil || status < 100 || status > 599 {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
message := apolloRTSPMessage{status: status, headers: make(map[string]string), raw: append([]byte(nil), data...)}
|
||||
for _, line := range lines[1:] {
|
||||
key, value, ok := strings.Cut(line, ":")
|
||||
key = strings.ToLower(strings.TrimSpace(key))
|
||||
value = strings.TrimSpace(value)
|
||||
if !ok || !validApolloRTSPToken(key) || value == "" || len(value) > 1024 {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
if _, duplicate := message.headers[key]; duplicate {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
message.headers[key] = value
|
||||
}
|
||||
cseq, ok := message.headers["cseq"]
|
||||
if !ok {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
parsedCSeq, err := strconv.ParseUint(cseq, 10, 32)
|
||||
if err != nil || parsedCSeq == 0 {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
message.cseq = uint32(parsedCSeq)
|
||||
message.body = append([]byte(nil), data[headerEnd+4:]...)
|
||||
if len(message.body) > maxApolloRTSPBody {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
if length, hasLength := message.headers["content-length"]; hasLength {
|
||||
declared, err := strconv.ParseUint(length, 10, 16)
|
||||
if err != nil || int(declared) != len(message.body) {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
} else if len(message.body) != 0 {
|
||||
return apolloRTSPMessage{}, ErrProviderMalformed
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func validateApolloDescribe(message apolloRTSPMessage) error {
|
||||
if message.headers["content-type"] != "application/sdp" || len(message.body) == 0 {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
body := string(message.body)
|
||||
lineEnding := "\n"
|
||||
if strings.Contains(body, "\r\n") {
|
||||
if strings.Contains(strings.ReplaceAll(body, "\r\n", ""), "\n") {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
lineEnding = "\r\n"
|
||||
}
|
||||
if !strings.HasSuffix(body, lineEnding) {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
attributes := map[string]string{}
|
||||
seen := map[string]struct{}{}
|
||||
stage := 0
|
||||
stereo := false
|
||||
for _, line := range strings.Split(strings.TrimSuffix(body, lineEnding), lineEnding) {
|
||||
if line == "" {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
if !strings.HasPrefix(line, "a=") {
|
||||
if line == "sprop-parameter-sets=AAAAAU" && stage >= 3 && stage <= 4 {
|
||||
stage = 5
|
||||
continue
|
||||
}
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
key, value, ok := strings.Cut(strings.TrimPrefix(line, "a="), ":")
|
||||
if !ok || key == "" || value == "" || len(key) > 128 || len(value) > 256 {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
if key == "fmtp" {
|
||||
if stage < 3 || !validApolloSurroundParameters(value) {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
if _, duplicate := seen["fmtp:"+value]; duplicate {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
seen["fmtp:"+value] = struct{}{}
|
||||
if value == "97 surround-params=21101" {
|
||||
stereo = true
|
||||
}
|
||||
stage = 6
|
||||
continue
|
||||
}
|
||||
if key == "rtpmap" && value == "98 AV1/90000" && stage >= 3 && stage <= 5 {
|
||||
if _, duplicate := seen[key]; duplicate {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
stage = 6
|
||||
continue
|
||||
}
|
||||
if key == "x-nv-video[0].refPicInvalidation" && value == "1" && stage == 3 {
|
||||
if _, duplicate := seen[key]; duplicate {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
stage = 4
|
||||
continue
|
||||
}
|
||||
if key != "x-ss-general.featureFlags" && key != "x-ss-general.encryptionSupported" && key != "x-ss-general.encryptionRequested" {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
if _, duplicate := attributes[key]; duplicate || (key == "x-ss-general.featureFlags" && stage != 0) || (key == "x-ss-general.encryptionSupported" && stage != 1) || (key == "x-ss-general.encryptionRequested" && stage != 2) {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
attributes[key] = value
|
||||
stage++
|
||||
}
|
||||
featureFlags, featureFlagsOK := attributes["x-ss-general.featureFlags"]
|
||||
supported, supportedOK := attributes["x-ss-general.encryptionSupported"]
|
||||
requested, requestedOK := attributes["x-ss-general.encryptionRequested"]
|
||||
if !featureFlagsOK || !supportedOK || !requestedOK || !stereo {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
if _, err := strconv.ParseUint(featureFlags, 10, 32); err != nil {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
supportedFlags, err := strconv.ParseUint(supported, 10, 32)
|
||||
if err != nil || supportedFlags&apolloEncryptionAll != apolloEncryptionAll {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
requestedFlags, err := strconv.ParseUint(requested, 10, 32)
|
||||
if err != nil || requestedFlags&^supportedFlags != 0 || requestedFlags&1 == 0 {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validApolloSurroundParameters(value string) bool {
|
||||
const prefix = "97 surround-params="
|
||||
if !strings.HasPrefix(value, prefix) {
|
||||
return false
|
||||
}
|
||||
parameters := strings.TrimPrefix(value, prefix)
|
||||
if len(parameters) < 5 || len(parameters) > 11 {
|
||||
return false
|
||||
}
|
||||
channels := int(parameters[0] - '0')
|
||||
streams := int(parameters[1] - '0')
|
||||
coupled := int(parameters[2] - '0')
|
||||
if (channels != 2 && channels != 6 && channels != 8) || len(parameters) != channels+3 || streams+coupled != channels || streams == 0 {
|
||||
return false
|
||||
}
|
||||
used := [8]bool{}
|
||||
for _, character := range parameters[3:] {
|
||||
if character < '0' || int(character-'0') >= channels || used[character-'0'] {
|
||||
return false
|
||||
}
|
||||
used[character-'0'] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func apolloRTSPSession(message apolloRTSPMessage) (string, error) {
|
||||
value, ok := message.headers["session"]
|
||||
if !ok {
|
||||
return "", ErrProviderMalformed
|
||||
}
|
||||
token, _, _ := strings.Cut(value, ";")
|
||||
token = strings.TrimSpace(token)
|
||||
if !validApolloRTSPToken(token) || len(token) > 256 {
|
||||
return "", ErrProviderMalformed
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func apolloRTSPMatchSession(message apolloRTSPMessage, expected string) error {
|
||||
actual, err := apolloRTSPSession(message)
|
||||
if err != nil || actual != expected {
|
||||
return ErrProviderMalformed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func apolloRTSPServerPort(message apolloRTSPMessage) (int, error) {
|
||||
transport, ok := message.headers["transport"]
|
||||
if !ok {
|
||||
return 0, ErrProviderMalformed
|
||||
}
|
||||
parts := strings.Split(transport, ";")
|
||||
if len(parts) != 2 || parts[0] != "unicast" {
|
||||
return 0, ErrProviderMalformed
|
||||
}
|
||||
key, value, ok := strings.Cut(parts[1], "=")
|
||||
port, err := strconv.Atoi(value)
|
||||
if !ok || key != "server_port" || err != nil || port < 1 || port > maxApolloRTSPPort {
|
||||
return 0, ErrProviderMalformed
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
func apolloRTSPPingPayload(message apolloRTSPMessage) ([]byte, error) {
|
||||
payload, ok := message.headers["x-ss-ping-payload"]
|
||||
if !ok || len(payload) != 16 || !validApolloRTSPToken(payload) {
|
||||
return nil, ErrProviderMalformed
|
||||
}
|
||||
return []byte(payload), nil
|
||||
}
|
||||
|
||||
func apolloRTSPConnectData(message apolloRTSPMessage) (uint32, error) {
|
||||
value, ok := message.headers["x-ss-connect-data"]
|
||||
if !ok {
|
||||
return 0, ErrProviderMalformed
|
||||
}
|
||||
if value == "" || strings.Trim(value, "0123456789") != "" {
|
||||
return 0, ErrProviderMalformed
|
||||
}
|
||||
parsed, err := strconv.ParseUint(value, 10, 32)
|
||||
if err != nil || parsed == 0 {
|
||||
return 0, ErrProviderMalformed
|
||||
}
|
||||
return uint32(parsed), nil
|
||||
}
|
||||
|
||||
func apolloAnnounceProfile() []byte {
|
||||
return []byte("v=0\r\n" +
|
||||
"o=android 0 0 IN IP4 0.0.0.0\r\n" +
|
||||
"s=NVIDIA Streaming Client\r\n" +
|
||||
"a=x-nv-video[0].clientViewportWd:1920\r\n" +
|
||||
"a=x-nv-video[0].clientViewportHt:1080\r\n" +
|
||||
"a=x-nv-video[0].maxFPS:60\r\n" +
|
||||
"a=x-nv-video[0].packetSize:1024\r\n" +
|
||||
"a=x-nv-video[0].videoEncoderSlicesPerFrame:1\r\n" +
|
||||
"a=x-nv-video[0].maxNumReferenceFrames:0\r\n" +
|
||||
"a=x-nv-vqos[0].bitStreamFormat:0\r\n" +
|
||||
"a=x-nv-vqos[0].bw.maximumBitrateKbps:8000\r\n" +
|
||||
"a=x-nv-vqos[0].fec.minRequiredFecPackets:2\r\n" +
|
||||
"a=x-nv-vqos[0].qosTrafficType:5\r\n" +
|
||||
"a=x-nv-audio.surround.numChannels:2\r\n" +
|
||||
"a=x-nv-audio.surround.channelMask:3\r\n" +
|
||||
"a=x-nv-audio.surround.AudioQuality:0\r\n" +
|
||||
"a=x-nv-aqos.packetDuration:5\r\n" +
|
||||
"a=x-nv-aqos.qosTrafficType:4\r\n" +
|
||||
"a=x-nv-general.useReliableUdp:13\r\n" +
|
||||
"a=x-nv-general.featureFlags:167\r\n" +
|
||||
"a=x-ml-general.featureFlags:0\r\n" +
|
||||
"a=x-ml-video.configuredBitrateKbps:8000\r\n" +
|
||||
"a=x-ss-general.encryptionEnabled:7\r\n" +
|
||||
"a=x-ss-video[0].chromaSamplingType:0\r\n" +
|
||||
"a=x-ss-video[0].intraRefresh:0\r\n")
|
||||
}
|
||||
|
||||
func validApolloRTSPToken(value string) bool {
|
||||
if value == "" || len(value) > 128 {
|
||||
return false
|
||||
}
|
||||
for _, character := range value {
|
||||
if character <= 0x20 || character >= 0x7f || strings.ContainsRune("()<>@,;:\\\"/[]?={}", character) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user