feat(protocol): negotiate registered gateway profiles
This commit is contained in:
+42
-14
@@ -13,7 +13,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const SchemaSHA256 = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
|
||||
const SchemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
||||
const ProtocolVersion = "1.0.0"
|
||||
const CurrentWireVersion = "1"
|
||||
const NMinus1WireVersion = "0"
|
||||
@@ -68,12 +68,12 @@ type BrokerSession struct {
|
||||
}
|
||||
|
||||
type CapabilityProfile struct {
|
||||
Transport string `json:"transport"`
|
||||
Framing string `json:"framing"`
|
||||
Media string `json:"media"`
|
||||
Audio string `json:"audio"`
|
||||
SourceRateControl string `json:"source_rate_control"`
|
||||
ClientDecode string `json:"client_decode"`
|
||||
Transport string `json:"transport"`
|
||||
Framing string `json:"framing"`
|
||||
Media string `json:"media"`
|
||||
Audio string `json:"audio"`
|
||||
SourceRateControl string `json:"source_rate_control"`
|
||||
ClientDecode []string `json:"client_decode"`
|
||||
}
|
||||
|
||||
type ChannelFrame struct {
|
||||
@@ -884,14 +884,26 @@ func (v CapabilityProfile) Validate() error {
|
||||
if len(v.SourceRateControl) > 64 {
|
||||
violations = append(violations, FieldViolation{Field: "source_rate_control", Code: "max_length"})
|
||||
}
|
||||
if v.ClientDecode == "" {
|
||||
if v.ClientDecode == nil {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "required"})
|
||||
}
|
||||
if len(v.ClientDecode) < 1 && v.ClientDecode != "" {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_length"})
|
||||
if len(v.ClientDecode) < 1 {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "min_items"})
|
||||
}
|
||||
if len(v.ClientDecode) > 64 {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_length"})
|
||||
if len(v.ClientDecode) > 2 {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "max_items"})
|
||||
}
|
||||
for _, item := range v.ClientDecode {
|
||||
if !(item == "h264-opus" || item == "hevc-opus") {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "invalid_item"})
|
||||
}
|
||||
}
|
||||
for index, item := range v.ClientDecode {
|
||||
for prior := 0; prior < index; prior++ {
|
||||
if item == v.ClientDecode[prior] {
|
||||
violations = append(violations, FieldViolation{Field: "client_decode", Code: "duplicate_item"})
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
return ValidationError{Violations: violations}
|
||||
@@ -4990,16 +5002,32 @@ func IntersectCapabilityProfiles(profiles ...CapabilityProfile) (CapabilityProfi
|
||||
if err := selected.Validate(); err != nil {
|
||||
return CapabilityProfile{}, ErrNoCapabilityOverlap
|
||||
}
|
||||
common := append([]string(nil), selected.ClientDecode...)
|
||||
for _, profile := range profiles[1:] {
|
||||
if err := profile.Validate(); err != nil || profile != selected {
|
||||
if err := profile.Validate(); err != nil || profile.Transport != selected.Transport || profile.Framing != selected.Framing || profile.Media != selected.Media || profile.Audio != selected.Audio || profile.SourceRateControl != selected.SourceRateControl {
|
||||
return CapabilityProfile{}, ErrNoCapabilityOverlap
|
||||
}
|
||||
next := common[:0]
|
||||
for _, candidate := range common {
|
||||
for _, offered := range profile.ClientDecode {
|
||||
if candidate == offered {
|
||||
next = append(next, candidate)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
common = next
|
||||
if len(common) == 0 {
|
||||
return CapabilityProfile{}, ErrNoCapabilityOverlap
|
||||
}
|
||||
}
|
||||
selected.ClientDecode = common
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func (v TunnelAdmissionRequest) DeviceAdmissionTranscript() []byte {
|
||||
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, v.Capabilities.ClientDecode}
|
||||
fields := []string{v.SessionID, v.GatewayID, v.Audience, v.Grant, fmt.Sprintf("%d", v.ReconnectSequence), v.ClientNonce, v.Capabilities.Transport, v.Capabilities.Framing, v.Capabilities.Media, v.Capabilities.Audio, v.Capabilities.SourceRateControl, fmt.Sprintf("%d", len(v.Capabilities.ClientDecode))}
|
||||
fields = append(fields, v.Capabilities.ClientDecode...)
|
||||
var transcript strings.Builder
|
||||
transcript.WriteString("versevdi/tunnel-admission/v1")
|
||||
for _, field := range fields {
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
"2"
|
||||
]
|
||||
},
|
||||
"generator_sha256": "922983e07a8ecc559771778fbf139155b14664742d9873be062880102777dccb",
|
||||
"generator_sha256": "00fdba050eb924a54dd3d63aac0a38560341b675f0de4e3e9631ee895057a9b6",
|
||||
"protocol_version": "1.0.0",
|
||||
"schema_sha256": "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
|
||||
"schema_sha256": "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
||||
}
|
||||
|
||||
Binary file not shown.
+15
-10
@@ -1,6 +1,6 @@
|
||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||
#![allow(non_snake_case)]
|
||||
pub const SCHEMA_SHA256: &str = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b";
|
||||
pub const SCHEMA_SHA256: &str = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716";
|
||||
pub const CURRENT_WIRE_VERSION: &str = "1";
|
||||
pub const N_MINUS_1_WIRE_VERSION: &str = "0";
|
||||
pub const N_MINUS_2_WIRE_VERSION: &str = "-1";
|
||||
@@ -210,11 +210,11 @@ pub struct CapabilityProfile {
|
||||
media: String,
|
||||
audio: String,
|
||||
sourceRateControl: String,
|
||||
clientDecode: String,
|
||||
clientDecode: Vec<String>,
|
||||
}
|
||||
|
||||
impl CapabilityProfile {
|
||||
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) -> Result<Self, ValidationError> {
|
||||
pub fn new(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: Vec<String>) -> Result<Self, ValidationError> {
|
||||
let value = Self { transport, framing, media, audio, sourceRateControl, clientDecode };
|
||||
value.validate()?;
|
||||
Ok(value)
|
||||
@@ -235,9 +235,10 @@ impl CapabilityProfile {
|
||||
if self.sourceRateControl.is_empty() { return Err(ValidationError::new("source_rate_control", "required")); }
|
||||
if !self.sourceRateControl.is_empty() && self.sourceRateControl.len() < 1 { return Err(ValidationError::new("source_rate_control", "min_length")); }
|
||||
if self.sourceRateControl.len() > 64 { return Err(ValidationError::new("source_rate_control", "max_length")); }
|
||||
if self.clientDecode.is_empty() { return Err(ValidationError::new("client_decode", "required")); }
|
||||
if !self.clientDecode.is_empty() && self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_length")); }
|
||||
if self.clientDecode.len() > 64 { return Err(ValidationError::new("client_decode", "max_length")); }
|
||||
if self.clientDecode.len() < 1 { return Err(ValidationError::new("client_decode", "min_items")); }
|
||||
if self.clientDecode.len() > 2 { return Err(ValidationError::new("client_decode", "max_items")); }
|
||||
for item in self.clientDecode.iter() { if item != "h264-opus" && item != "hevc-opus" { return Err(ValidationError::new("client_decode", "invalid_item")); } }
|
||||
for (index, item) in self.clientDecode.iter().enumerate() { if self.clientDecode[..index].contains(item) { return Err(ValidationError::new("client_decode", "duplicate_item")); } }
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> &String { &self.transport }
|
||||
@@ -245,7 +246,7 @@ impl CapabilityProfile {
|
||||
pub fn media(&self) -> &String { &self.media }
|
||||
pub fn audio(&self) -> &String { &self.audio }
|
||||
pub fn sourceRateControl(&self) -> &String { &self.sourceRateControl }
|
||||
pub fn clientDecode(&self) -> &String { &self.clientDecode }
|
||||
pub fn clientDecode(&self) -> &Vec<String> { &self.clientDecode }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1729,7 +1730,9 @@ impl TunnelAdmissionRequest {
|
||||
pub fn capabilities(&self) -> &CapabilityProfile { &self.capabilities }
|
||||
pub fn device_admission_transcript(&self) -> Vec<u8> {
|
||||
let reconnect_sequence = self.reconnectSequence.to_string();
|
||||
let fields = [&self.sessionId, &self.gatewayId, &self.audience, &self.grant, &reconnect_sequence, &self.clientNonce, &self.capabilities.transport, &self.capabilities.framing, &self.capabilities.media, &self.capabilities.audio, &self.capabilities.sourceRateControl, &self.capabilities.clientDecode];
|
||||
let client_decode_count = self.capabilities.clientDecode.len().to_string();
|
||||
let mut fields = vec![self.sessionId.as_str(), self.gatewayId.as_str(), self.audience.as_str(), self.grant.as_str(), reconnect_sequence.as_str(), self.clientNonce.as_str(), self.capabilities.transport.as_str(), self.capabilities.framing.as_str(), self.capabilities.media.as_str(), self.capabilities.audio.as_str(), self.capabilities.sourceRateControl.as_str(), client_decode_count.as_str()];
|
||||
fields.extend(self.capabilities.clientDecode.iter().map(String::as_str));
|
||||
let mut transcript = String::from("versevdi/tunnel-admission/v1");
|
||||
for field in fields { transcript.push_str(&format!("{}:{}", field.as_bytes().len(), field)); }
|
||||
transcript.into_bytes()
|
||||
@@ -1759,11 +1762,13 @@ impl VersionNegotiation {
|
||||
}
|
||||
|
||||
pub fn intersect_capability_profiles(profiles: &[CapabilityProfile]) -> Result<CapabilityProfile, ValidationError> {
|
||||
let selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
|
||||
let mut selected = profiles.first().ok_or_else(|| ValidationError::new("capabilities", "no_overlap"))?.clone();
|
||||
selected.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
|
||||
for profile in &profiles[1..] {
|
||||
profile.validate().map_err(|_| ValidationError::new("capabilities", "no_overlap"))?;
|
||||
if profile != &selected { return Err(ValidationError::new("capabilities", "no_overlap")); }
|
||||
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { return Err(ValidationError::new("capabilities", "no_overlap")); }
|
||||
selected.clientDecode.retain(|candidate| profile.clientDecode.contains(candidate));
|
||||
if selected.clientDecode.is_empty() { return Err(ValidationError::new("capabilities", "no_overlap")); }
|
||||
}
|
||||
Ok(selected)
|
||||
}
|
||||
|
||||
+15
-10
@@ -1,7 +1,7 @@
|
||||
// Code generated by tools/generate.py; DO NOT EDIT.
|
||||
import Foundation
|
||||
public typealias JSONObject = [String: String]
|
||||
public let schemaSHA256 = "fe4d6be69665f09f04e2cd89273ebc5d2dddf4cde98b94a9d8c4f726504ffe1b"
|
||||
public let schemaSHA256 = "3aec8dd72bdbb6b9657c8df3160252c93034c7c1032d471e01eae2ef91e47716"
|
||||
public let currentWireVersion = "1"
|
||||
public let nMinus1WireVersion = "0"
|
||||
public let nMinus2WireVersion = "-1"
|
||||
@@ -247,7 +247,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
public let media: String
|
||||
public let audio: String
|
||||
public let sourceRateControl: String
|
||||
public let clientDecode: String
|
||||
public let clientDecode: [String]
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case transport = "transport"
|
||||
case framing = "framing"
|
||||
@@ -257,7 +257,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
case clientDecode = "client_decode"
|
||||
}
|
||||
|
||||
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: String) throws {
|
||||
public init(transport: String, framing: String, media: String, audio: String, sourceRateControl: String, clientDecode: [String]) throws {
|
||||
self.transport = transport
|
||||
self.framing = framing
|
||||
self.media = media
|
||||
@@ -271,7 +271,7 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
let all = try decoder.container(keyedBy: AnyCodingKey.self)
|
||||
for key in all.allKeys where CodingKeys(stringValue: key.stringValue) == nil { throw ContractValidationError(field: key.stringValue, code: "unknown_field") }
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
try self.init(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode(String.self, forKey: .clientDecode))
|
||||
try self.init(transport: try c.decode(String.self, forKey: .transport), framing: try c.decode(String.self, forKey: .framing), media: try c.decode(String.self, forKey: .media), audio: try c.decode(String.self, forKey: .audio), sourceRateControl: try c.decode(String.self, forKey: .sourceRateControl), clientDecode: try c.decode([String].self, forKey: .clientDecode))
|
||||
}
|
||||
|
||||
public func validate() throws {
|
||||
@@ -290,9 +290,10 @@ public struct CapabilityProfile: Codable, Equatable {
|
||||
if self.sourceRateControl.isEmpty { throw ContractValidationError(field: "source_rate_control", code: "required") }
|
||||
if !self.sourceRateControl.isEmpty && self.sourceRateControl.utf8.count < 1 { throw ContractValidationError(field: "source_rate_control", code: "min_length") }
|
||||
if self.sourceRateControl.utf8.count > 64 { throw ContractValidationError(field: "source_rate_control", code: "max_length") }
|
||||
if self.clientDecode.isEmpty { throw ContractValidationError(field: "client_decode", code: "required") }
|
||||
if !self.clientDecode.isEmpty && self.clientDecode.utf8.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_length") }
|
||||
if self.clientDecode.utf8.count > 64 { throw ContractValidationError(field: "client_decode", code: "max_length") }
|
||||
if self.clientDecode.count < 1 { throw ContractValidationError(field: "client_decode", code: "min_items") }
|
||||
if self.clientDecode.count > 2 { throw ContractValidationError(field: "client_decode", code: "max_items") }
|
||||
for item in self.clientDecode where !["h264-opus", "hevc-opus"].contains(item) { throw ContractValidationError(field: "client_decode", code: "invalid_item") }
|
||||
if Set(self.clientDecode).count != self.clientDecode.count { throw ContractValidationError(field: "client_decode", code: "duplicate_item") }
|
||||
}
|
||||
|
||||
public static func decodeJSON(_ data: Data) throws -> Self { try JSONDecoder().decode(Self.self, from: data) }
|
||||
@@ -2321,7 +2322,8 @@ public struct VersionNegotiation: Codable, Equatable {
|
||||
|
||||
public extension TunnelAdmissionRequest {
|
||||
func deviceAdmissionTranscript() -> Data {
|
||||
let fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, capabilities.clientDecode]
|
||||
var fields = [sessionId, gatewayId, audience, grant, String(reconnectSequence), clientNonce, capabilities.transport, capabilities.framing, capabilities.media, capabilities.audio, capabilities.sourceRateControl, String(capabilities.clientDecode.count)]
|
||||
fields.append(contentsOf: capabilities.clientDecode)
|
||||
var transcript = "versevdi/tunnel-admission/v1"
|
||||
for field in fields { transcript += "\(field.utf8.count):\(field)" }
|
||||
return Data(transcript.utf8)
|
||||
@@ -2332,10 +2334,13 @@ public extension CapabilityProfile {
|
||||
static func intersection(_ profiles: [CapabilityProfile]) throws -> CapabilityProfile {
|
||||
guard let selected = profiles.first else { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
try selected.validate()
|
||||
var common = selected.clientDecode
|
||||
for profile in profiles.dropFirst() {
|
||||
try profile.validate()
|
||||
if profile != selected { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
if profile.transport != selected.transport || profile.framing != selected.framing || profile.media != selected.media || profile.audio != selected.audio || profile.sourceRateControl != selected.sourceRateControl { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
common = common.filter { profile.clientDecode.contains($0) }
|
||||
if common.isEmpty { throw ContractValidationError(field: "capabilities", code: "no_overlap") }
|
||||
}
|
||||
return selected
|
||||
return try CapabilityProfile(transport: selected.transport, framing: selected.framing, media: selected.media, audio: selected.audio, sourceRateControl: selected.sourceRateControl, clientDecode: common)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user