livekit-server-sdk 0.8.3 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -0
- data/.github/banner_dark.png +0 -0
- data/.github/banner_light.png +0 -0
- data/.github/workflows/generate-protobufs.yml +82 -0
- data/.github/workflows/test-api.yml +43 -0
- data/.github/workflows/test.yml +10 -7
- data/CHANGELOG.md +18 -0
- data/Gemfile.lock +70 -39
- data/README.md +7 -5
- data/Rakefile +8 -2
- data/bin/bootstrap.sh +1 -1
- data/lib/livekit/agent_dispatch_service_client.rb +11 -9
- data/lib/livekit/auth_mixin.rb +23 -8
- data/lib/livekit/connector_service_client.rb +87 -0
- data/lib/livekit/dial_timeout.rb +26 -0
- data/lib/livekit/egress_service_client.rb +41 -19
- data/lib/livekit/errors.rb +60 -0
- data/lib/livekit/failover.rb +215 -0
- data/lib/livekit/grants.rb +6 -2
- data/lib/livekit/ingress_service_client.rb +8 -6
- data/lib/livekit/livekit_api.rb +67 -0
- data/lib/livekit/proto/livekit_agent_dispatch_pb.rb +3 -1
- data/lib/livekit/proto/livekit_agent_dispatch_twirp.rb +1 -1
- data/lib/livekit/proto/livekit_agent_pb.rb +2 -1
- data/lib/livekit/proto/livekit_agent_twirp.rb +1 -1
- data/lib/livekit/proto/livekit_connector_pb.rb +20 -0
- data/lib/livekit/proto/livekit_connector_twilio_pb.rb +22 -0
- data/lib/livekit/proto/livekit_connector_twilio_twirp.rb +8 -0
- data/lib/livekit/proto/livekit_connector_twirp.rb +21 -0
- data/lib/livekit/proto/livekit_connector_whatsapp_pb.rb +32 -0
- data/lib/livekit/proto/livekit_connector_whatsapp_twirp.rb +8 -0
- data/lib/livekit/proto/livekit_egress_pb.rb +33 -16
- data/lib/livekit/proto/livekit_egress_twirp.rb +6 -5
- data/lib/livekit/proto/livekit_ingress_pb.rb +2 -1
- data/lib/livekit/proto/livekit_ingress_twirp.rb +1 -1
- data/lib/livekit/proto/livekit_metrics_pb.rb +3 -1
- data/lib/livekit/proto/livekit_metrics_twirp.rb +1 -1
- data/lib/livekit/proto/livekit_models_pb.rb +20 -1
- data/lib/livekit/proto/livekit_models_twirp.rb +1 -1
- data/lib/livekit/proto/livekit_room_pb.rb +8 -1
- data/lib/livekit/proto/livekit_room_twirp.rb +4 -1
- data/lib/livekit/proto/livekit_rtc_pb.rb +86 -0
- data/lib/livekit/proto/livekit_rtc_twirp.rb +8 -0
- data/lib/livekit/proto/livekit_sip_pb.rb +20 -1
- data/lib/livekit/proto/livekit_sip_twirp.rb +4 -1
- data/lib/livekit/proto/livekit_webhook_pb.rb +1 -1
- data/lib/livekit/proto/livekit_webhook_twirp.rb +1 -1
- data/lib/livekit/proto/logger/options_pb.rb +19 -0
- data/lib/livekit/proto/logger/options_twirp.rb +8 -0
- data/lib/livekit/room_service_client.rb +31 -13
- data/lib/livekit/sip_service_client.rb +147 -21
- data/lib/livekit/version.rb +1 -1
- data/lib/livekit.rb +3 -0
- data/livekit_server_sdk.gemspec +8 -8
- metadata +59 -32
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "livekit/proto/livekit_egress_twirp"
|
|
4
4
|
require "livekit/auth_mixin"
|
|
5
5
|
require 'livekit/utils'
|
|
6
|
+
require 'livekit/failover'
|
|
6
7
|
|
|
7
8
|
module LiveKit
|
|
8
9
|
class EgressServiceClient < Twirp::Client
|
|
@@ -10,10 +11,11 @@ module LiveKit
|
|
|
10
11
|
include AuthMixin
|
|
11
12
|
attr_accessor :api_key, :api_secret
|
|
12
13
|
|
|
13
|
-
def initialize(base_url, api_key: nil, api_secret: nil)
|
|
14
|
-
super(
|
|
14
|
+
def initialize(base_url, api_key: nil, api_secret: nil, token: nil, failover: true, connection: nil)
|
|
15
|
+
super(connection || LiveKit::Failover.connection(base_url, failover))
|
|
15
16
|
@api_key = api_key
|
|
16
17
|
@api_secret = api_secret
|
|
18
|
+
@token = token
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
def start_room_composite_egress(
|
|
@@ -31,7 +33,11 @@ module LiveKit
|
|
|
31
33
|
# true to record only audio
|
|
32
34
|
audio_only: false,
|
|
33
35
|
# true to record only video
|
|
34
|
-
video_only: false
|
|
36
|
+
video_only: false,
|
|
37
|
+
# DEFAULT_MIXING, DUAL_CHANNEL_AGENT, DUAL_CHANNEL_ALTERNATE
|
|
38
|
+
audio_mixing: nil,
|
|
39
|
+
# list of extra webhooks to call for this request
|
|
40
|
+
webhooks: nil
|
|
35
41
|
)
|
|
36
42
|
request = Proto::RoomCompositeEgressRequest.new(
|
|
37
43
|
room_name: room_name,
|
|
@@ -41,9 +47,11 @@ module LiveKit
|
|
|
41
47
|
custom_base_url: custom_base_url,
|
|
42
48
|
audio_only: audio_only,
|
|
43
49
|
video_only: video_only,
|
|
50
|
+
audio_mixing: audio_mixing,
|
|
51
|
+
webhooks: webhooks,
|
|
44
52
|
)
|
|
45
53
|
self.set_output(request, output)
|
|
46
|
-
|
|
54
|
+
rpc!(
|
|
47
55
|
:StartRoomCompositeEgress,
|
|
48
56
|
request,
|
|
49
57
|
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
|
|
@@ -60,7 +68,9 @@ module LiveKit
|
|
|
60
68
|
# EncodingOptionsPreset, only one of preset or advanced could be set
|
|
61
69
|
preset: nil,
|
|
62
70
|
# EncodingOptions, only one of preset or advanced could be set
|
|
63
|
-
advanced: nil
|
|
71
|
+
advanced: nil,
|
|
72
|
+
# list of extra webhooks to call for this request
|
|
73
|
+
webhooks: nil
|
|
64
74
|
)
|
|
65
75
|
request = Proto::ParticipantEgressRequest.new(
|
|
66
76
|
room_name: room_name,
|
|
@@ -68,9 +78,10 @@ module LiveKit
|
|
|
68
78
|
screen_share: screen_share,
|
|
69
79
|
preset: preset,
|
|
70
80
|
advanced: advanced,
|
|
81
|
+
webhooks: webhooks,
|
|
71
82
|
)
|
|
72
83
|
self.set_output(request, output)
|
|
73
|
-
|
|
84
|
+
rpc!(
|
|
74
85
|
:StartParticipantEgress,
|
|
75
86
|
request,
|
|
76
87
|
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
|
|
@@ -88,7 +99,9 @@ module LiveKit
|
|
|
88
99
|
# EncodingOptionsPreset, only one of preset or advanced could be set
|
|
89
100
|
preset: nil,
|
|
90
101
|
# EncodingOptions, only one of preset or advanced could be set
|
|
91
|
-
advanced: nil
|
|
102
|
+
advanced: nil,
|
|
103
|
+
# list of extra webhooks to call for this request
|
|
104
|
+
webhooks: nil
|
|
92
105
|
)
|
|
93
106
|
request = Proto::TrackCompositeEgressRequest.new(
|
|
94
107
|
room_name: room_name,
|
|
@@ -96,9 +109,10 @@ module LiveKit
|
|
|
96
109
|
advanced: advanced,
|
|
97
110
|
audio_track_id: audio_track_id,
|
|
98
111
|
video_track_id: video_track_id,
|
|
112
|
+
webhooks: webhooks,
|
|
99
113
|
)
|
|
100
114
|
self.set_output(request, output)
|
|
101
|
-
|
|
115
|
+
rpc!(
|
|
102
116
|
:StartTrackCompositeEgress,
|
|
103
117
|
request,
|
|
104
118
|
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
|
|
@@ -109,18 +123,21 @@ module LiveKit
|
|
|
109
123
|
room_name,
|
|
110
124
|
# either a DirectFileOutput - egress to storage or string - egress to WebSocket URL
|
|
111
125
|
output,
|
|
112
|
-
track_id
|
|
126
|
+
track_id,
|
|
127
|
+
# list of extra webhooks to call for this request
|
|
128
|
+
webhooks: nil
|
|
113
129
|
)
|
|
114
130
|
request = Proto::TrackEgressRequest.new(
|
|
115
131
|
room_name: room_name,
|
|
116
132
|
track_id: track_id,
|
|
133
|
+
webhooks: webhooks,
|
|
117
134
|
)
|
|
118
135
|
if output.filepath
|
|
119
136
|
request.file = output
|
|
120
137
|
else
|
|
121
138
|
request.websocket_url = output
|
|
122
139
|
end
|
|
123
|
-
|
|
140
|
+
rpc!(
|
|
124
141
|
:StartTrackEgress,
|
|
125
142
|
request,
|
|
126
143
|
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
|
|
@@ -140,7 +157,9 @@ module LiveKit
|
|
|
140
157
|
# true to record only video
|
|
141
158
|
video_only: false,
|
|
142
159
|
# true to await START_RECORDING chrome log
|
|
143
|
-
await_start_signal: false
|
|
160
|
+
await_start_signal: false,
|
|
161
|
+
# list of extra webhooks to call for this request
|
|
162
|
+
webhooks: nil
|
|
144
163
|
)
|
|
145
164
|
request = Proto::WebEgressRequest.new(
|
|
146
165
|
url: url,
|
|
@@ -149,9 +168,10 @@ module LiveKit
|
|
|
149
168
|
audio_only: audio_only,
|
|
150
169
|
video_only: video_only,
|
|
151
170
|
await_start_signal: await_start_signal,
|
|
171
|
+
webhooks: webhooks,
|
|
152
172
|
)
|
|
153
173
|
self.set_output(request, output)
|
|
154
|
-
|
|
174
|
+
rpc!(
|
|
155
175
|
:StartWebEgress,
|
|
156
176
|
request,
|
|
157
177
|
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
|
|
@@ -159,7 +179,7 @@ module LiveKit
|
|
|
159
179
|
end
|
|
160
180
|
|
|
161
181
|
def update_layout(egress_id, layout)
|
|
162
|
-
|
|
182
|
+
rpc!(
|
|
163
183
|
:UpdateLayout,
|
|
164
184
|
Proto::UpdateLayoutRequest.new(
|
|
165
185
|
egress_id: egress_id,
|
|
@@ -173,7 +193,7 @@ module LiveKit
|
|
|
173
193
|
add_output_urls: [],
|
|
174
194
|
remove_output_urls: []
|
|
175
195
|
)
|
|
176
|
-
|
|
196
|
+
rpc!(
|
|
177
197
|
:UpdateStream,
|
|
178
198
|
Proto::UpdateStreamRequest.new(
|
|
179
199
|
egress_id: egress_id,
|
|
@@ -190,7 +210,7 @@ module LiveKit
|
|
|
190
210
|
egress_id: nil,
|
|
191
211
|
active: false
|
|
192
212
|
)
|
|
193
|
-
|
|
213
|
+
rpc!(
|
|
194
214
|
:ListEgress,
|
|
195
215
|
Proto::ListEgressRequest.new(
|
|
196
216
|
room_name: room_name,
|
|
@@ -202,7 +222,7 @@ module LiveKit
|
|
|
202
222
|
end
|
|
203
223
|
|
|
204
224
|
def stop_egress(egress_id)
|
|
205
|
-
|
|
225
|
+
rpc!(
|
|
206
226
|
:StopEgress,
|
|
207
227
|
Proto::StopEgressRequest.new(egress_id: egress_id),
|
|
208
228
|
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
|
|
@@ -234,13 +254,15 @@ module LiveKit
|
|
|
234
254
|
end
|
|
235
255
|
end
|
|
236
256
|
elsif output.is_a? LiveKit::Proto::EncodedFileOutput
|
|
237
|
-
|
|
257
|
+
# The singular field is deprecated and absent on some requests (e.g.
|
|
258
|
+
# ParticipantEgressRequest); the plural *_outputs is the current field.
|
|
259
|
+
request.file = output if request.respond_to?(:file=)
|
|
238
260
|
request.file_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::EncodedFileOutput, [output])
|
|
239
261
|
elsif output.is_a? LiveKit::Proto::SegmentedFileOutput
|
|
240
|
-
request.segments = output
|
|
262
|
+
request.segments = output if request.respond_to?(:segments=)
|
|
241
263
|
request.segment_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::SegmentedFileOutput, [output])
|
|
242
264
|
elsif output.is_a? LiveKit::Proto::StreamOutput
|
|
243
|
-
request.stream = output
|
|
265
|
+
request.stream = output if request.respond_to?(:stream=)
|
|
244
266
|
request.stream_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::StreamOutput, [output])
|
|
245
267
|
elsif output.is_a? LiveKit::Proto::ImageOutput
|
|
246
268
|
request.image_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::ImageOutput, [output])
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LiveKit
|
|
4
|
+
# Raised when a LiveKit server API call fails. Exposes the error code, message,
|
|
5
|
+
# and any metadata the server attached.
|
|
6
|
+
class ServerError < StandardError
|
|
7
|
+
# @return [String] the error code, e.g. "not_found" or "permission_denied"
|
|
8
|
+
attr_reader :code
|
|
9
|
+
# @return [Hash{String=>String}] error metadata returned by the server
|
|
10
|
+
attr_reader :metadata
|
|
11
|
+
|
|
12
|
+
def initialize(code, message, metadata: {})
|
|
13
|
+
@code = code
|
|
14
|
+
@metadata = metadata || {}
|
|
15
|
+
super(message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Builds a ServerError (or SipCallError) from the underlying error. Returns nil
|
|
19
|
+
# when +err+ is nil, so it can be called directly on a response's error.
|
|
20
|
+
# @return [LiveKit::ServerError, nil]
|
|
21
|
+
def self.from(err)
|
|
22
|
+
return nil if err.nil?
|
|
23
|
+
|
|
24
|
+
meta = err.meta || {}
|
|
25
|
+
klass = meta.key?("sip_status_code") ? SipCallError : self
|
|
26
|
+
klass.new(err.code.to_s, err.msg, metadata: meta)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Raised when a SIP dialing call (create_sip_participant / transfer_sip_participant)
|
|
31
|
+
# fails with a SIP response status. The SIP code and reason are exposed
|
|
32
|
+
# directly; any other metadata remains available via {#metadata}.
|
|
33
|
+
class SipCallError < ServerError
|
|
34
|
+
# @return [Integer, nil] the SIP response code, e.g. 486 (Busy Here)
|
|
35
|
+
def sip_status_code
|
|
36
|
+
raw = metadata["sip_status_code"]
|
|
37
|
+
raw && raw.to_i
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @return [String, nil] the SIP reason phrase, e.g. "Busy Here"
|
|
41
|
+
def sip_status
|
|
42
|
+
metadata["sip_status"]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A clear, SIP-specific representation, including any extra metadata.
|
|
46
|
+
def message
|
|
47
|
+
code_str = metadata["sip_status_code"]
|
|
48
|
+
return super if code_str.nil?
|
|
49
|
+
|
|
50
|
+
reason = metadata["sip_status"]
|
|
51
|
+
result = "SIP call failed: #{code_str}"
|
|
52
|
+
result += " #{reason}" if reason && !reason.empty?
|
|
53
|
+
result += " (#{code})"
|
|
54
|
+
extra = metadata.reject { |k, _| %w[sip_status_code sip_status error_details].include?(k) }
|
|
55
|
+
result += " [#{extra.map { |k, v| "#{k}=#{v}" }.join(", ")}]" unless extra.empty?
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
alias to_s message
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'set'
|
|
6
|
+
require 'uri'
|
|
7
|
+
require 'livekit/utils'
|
|
8
|
+
require 'livekit/version'
|
|
9
|
+
|
|
10
|
+
module LiveKit
|
|
11
|
+
# Region failover for the Twirp API clients.
|
|
12
|
+
#
|
|
13
|
+
# On a retryable failure (any transport error or HTTP 5xx) the client
|
|
14
|
+
# discovers alternative LiveKit Cloud regions via /settings/regions and
|
|
15
|
+
# replays the request against the next region, with exponential backoff. 4xx
|
|
16
|
+
# responses are returned immediately.
|
|
17
|
+
#
|
|
18
|
+
# The +failover+ argument accepted by the service clients is a boolean
|
|
19
|
+
# (default true). When enabled, failover engages for LiveKit Cloud hosts only.
|
|
20
|
+
# The total attempt count and backoff are fixed (not user-configurable) so
|
|
21
|
+
# retries can't be tuned to values that could overwhelm the server.
|
|
22
|
+
module Failover
|
|
23
|
+
MAX_ATTEMPTS = 3
|
|
24
|
+
BACKOFF_BASE = 0.2
|
|
25
|
+
# Default per-request timeout (seconds). Calls that dial a phone override it
|
|
26
|
+
# via TIMEOUT_HEADER (see SIPServiceClient).
|
|
27
|
+
DEFAULT_TIMEOUT = 10
|
|
28
|
+
# Below this per-request timeout a retry is unlikely to help and many clients
|
|
29
|
+
# would retry in lockstep across regions, so a short request gets a single
|
|
30
|
+
# attempt (thundering-herd guard).
|
|
31
|
+
MIN_FAILOVER_TIMEOUT = 5
|
|
32
|
+
# Internal header carrying a per-request timeout override (seconds). Consumed
|
|
33
|
+
# by the middleware and not sent to the server.
|
|
34
|
+
TIMEOUT_HEADER = 'X-Lk-Request-Timeout'
|
|
35
|
+
|
|
36
|
+
# Total request attempts including the initial one; 1 means no failover.
|
|
37
|
+
# Failover only engages when enabled, the host is a LiveKit Cloud domain, and
|
|
38
|
+
# the request timeout is long enough to retry. +force+ bypasses the
|
|
39
|
+
# cloud-host check and is for internal testing only.
|
|
40
|
+
def self.attempts(enabled, host, force, timeout)
|
|
41
|
+
return 1 unless enabled && (force || cloud?(host))
|
|
42
|
+
return 1 if timeout && timeout < MIN_FAILOVER_TIMEOUT
|
|
43
|
+
|
|
44
|
+
MAX_ATTEMPTS
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Builds a Faraday connection wired with the region-failover middleware and
|
|
48
|
+
# the LiveKit Twirp base URL. Passed to Twirp::Client in place of a URL.
|
|
49
|
+
def self.connection(base_url, failover)
|
|
50
|
+
url = File.join(Utils.to_http_url(base_url), '/twirp')
|
|
51
|
+
Faraday.new(url: url) do |f|
|
|
52
|
+
f.headers['User-Agent'] = "livekit-server-sdk-ruby/#{VERSION}"
|
|
53
|
+
f.options.timeout = DEFAULT_TIMEOUT
|
|
54
|
+
f.use RegionFailoverMiddleware, failover: failover
|
|
55
|
+
f.adapter Faraday.default_adapter
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Failover only engages for LiveKit Cloud project domains.
|
|
60
|
+
def self.cloud?(host)
|
|
61
|
+
!host.nil? && host.end_with?('.livekit.cloud')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Normalizes a region URL to an http(s) scheme (ws -> http, wss -> https).
|
|
65
|
+
def self.to_http(url)
|
|
66
|
+
url.start_with?('ws') ? "http#{url[2..]}" : url
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.host_key(uri)
|
|
70
|
+
"#{uri.host}:#{uri.port}".downcase
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the first region whose host has not yet been attempted.
|
|
74
|
+
def self.pick_next(regions, attempted)
|
|
75
|
+
regions.find { |uri| !attempted.include?(host_key(uri)) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@cache = {}
|
|
79
|
+
@cache_mutex = Mutex.new
|
|
80
|
+
|
|
81
|
+
# Returns alternative region origins (URIs) for the given origin, fetching
|
|
82
|
+
# /settings/regions if the cache is stale. Best-effort: on a fetch failure
|
|
83
|
+
# it serves a stale cached list when available, otherwise an empty list.
|
|
84
|
+
# Forwards headers so a valid token — and any test directives — reach the
|
|
85
|
+
# discovery endpoint.
|
|
86
|
+
def self.region_uris(origin, headers)
|
|
87
|
+
key = host_key(origin)
|
|
88
|
+
@cache_mutex.synchronize do
|
|
89
|
+
entry = @cache[key]
|
|
90
|
+
return entry[:uris] if entry && (Time.now - entry[:fetched_at]) < entry[:ttl]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
uris, ttl = fetch(origin, headers)
|
|
95
|
+
rescue StandardError
|
|
96
|
+
return @cache_mutex.synchronize { @cache[key]&.fetch(:uris, nil) } || []
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A zero TTL (e.g. Cache-Control: max-age=0) means "do not cache".
|
|
100
|
+
if ttl.positive?
|
|
101
|
+
@cache_mutex.synchronize do
|
|
102
|
+
@cache[key] = { uris: uris, fetched_at: Time.now, ttl: ttl }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
uris
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.fetch(origin, headers)
|
|
109
|
+
forward = headers.reject { |k, _| %w[content-type content-length].include?(k.to_s.downcase) }
|
|
110
|
+
url = "#{origin.scheme}://#{origin.host}:#{origin.port}/settings/regions"
|
|
111
|
+
resp = Faraday.get(url) do |req|
|
|
112
|
+
# Short timeout so a slow/unreachable discovery endpoint doesn't stall
|
|
113
|
+
# the failover path.
|
|
114
|
+
req.options.timeout = 2
|
|
115
|
+
forward.each { |k, v| req.headers[k] = v }
|
|
116
|
+
end
|
|
117
|
+
raise "region discovery failed: #{resp.status}" unless resp.status == 200
|
|
118
|
+
|
|
119
|
+
ttl = parse_max_age(resp.headers['cache-control'])
|
|
120
|
+
body = JSON.parse(resp.body || '{}')
|
|
121
|
+
uris = (body['regions'] || [])
|
|
122
|
+
.map { |r| r['url'] }
|
|
123
|
+
.reject { |u| u.nil? || u.empty? }
|
|
124
|
+
.map { |u| URI.parse(to_http(u)) }
|
|
125
|
+
[uris, ttl]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.parse_max_age(cache_control)
|
|
129
|
+
return 0 if cache_control.nil? || cache_control.empty?
|
|
130
|
+
|
|
131
|
+
cache_control.split(',').each do |directive|
|
|
132
|
+
directive = directive.strip.downcase
|
|
133
|
+
if directive.start_with?('max-age=')
|
|
134
|
+
secs = directive.sub('max-age=', '').to_i
|
|
135
|
+
return secs.positive? ? secs : 0
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Faraday middleware that fails over to alternative LiveKit Cloud regions on a
|
|
143
|
+
# retryable error: any transport error or HTTP 5xx. A 4xx is returned
|
|
144
|
+
# immediately. The request body and headers are replayed intact against the
|
|
145
|
+
# next untried region, with exponential backoff.
|
|
146
|
+
class RegionFailoverMiddleware < Faraday::Middleware
|
|
147
|
+
def initialize(app, options = {})
|
|
148
|
+
super(app)
|
|
149
|
+
@failover = options.fetch(:failover, true)
|
|
150
|
+
# +force+ / +backoff_base+ are internal test-only knobs.
|
|
151
|
+
@force = options.fetch(:force, false)
|
|
152
|
+
@backoff_base = options.fetch(:backoff_base, Failover::BACKOFF_BASE)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def call(env)
|
|
156
|
+
original_url = env.url.dup
|
|
157
|
+
request_body = env.body
|
|
158
|
+
|
|
159
|
+
# A per-request timeout override (e.g. for SIP dialing) travels as an
|
|
160
|
+
# internal header; consume it so it isn't sent to the server. Otherwise
|
|
161
|
+
# use the connection's configured timeout.
|
|
162
|
+
timeout = env.request_headers.delete(Failover::TIMEOUT_HEADER)&.to_f
|
|
163
|
+
timeout ||= env.request.timeout
|
|
164
|
+
env.request.timeout = timeout if timeout
|
|
165
|
+
|
|
166
|
+
request_headers = env.request_headers.dup
|
|
167
|
+
max_attempts = Failover.attempts(@failover, original_url.host, @force, timeout)
|
|
168
|
+
|
|
169
|
+
attempted = [Failover.host_key(original_url)]
|
|
170
|
+
regions = nil
|
|
171
|
+
current_url = original_url
|
|
172
|
+
attempt = 0
|
|
173
|
+
|
|
174
|
+
loop do
|
|
175
|
+
is_last = attempt + 1 >= max_attempts
|
|
176
|
+
env.url = current_url
|
|
177
|
+
env.body = request_body
|
|
178
|
+
|
|
179
|
+
response = nil
|
|
180
|
+
error = nil
|
|
181
|
+
begin
|
|
182
|
+
response = @app.call(env)
|
|
183
|
+
rescue Faraday::Error => e
|
|
184
|
+
error = e
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Success or a non-retryable 4xx is terminal.
|
|
188
|
+
return response if response && response.status < 500
|
|
189
|
+
|
|
190
|
+
nxt = nil
|
|
191
|
+
unless is_last
|
|
192
|
+
regions ||= Failover.region_uris(original_url, request_headers)
|
|
193
|
+
nxt = Failover.pick_next(regions, attempted)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if nxt.nil?
|
|
197
|
+
return response if response
|
|
198
|
+
raise error
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
reason = response ? "status #{response.status}" : (error&.message || 'error')
|
|
202
|
+
warn("livekit API request to #{current_url.host} failed (#{reason}), " \
|
|
203
|
+
"retrying with fallback url #{nxt}")
|
|
204
|
+
sleep(@backoff_base * (2**attempt))
|
|
205
|
+
attempted << Failover.host_key(nxt)
|
|
206
|
+
# Swap only the region's scheme/host/port, preserving the request path.
|
|
207
|
+
current_url = original_url.dup
|
|
208
|
+
current_url.scheme = nxt.scheme
|
|
209
|
+
current_url.host = nxt.host
|
|
210
|
+
current_url.port = nxt.port
|
|
211
|
+
attempt += 1
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
data/lib/livekit/grants.rb
CHANGED
|
@@ -62,8 +62,8 @@ module LiveKit
|
|
|
62
62
|
using LiveKit::Utils::StringifyKeysRefinement
|
|
63
63
|
|
|
64
64
|
attr_accessor :roomCreate, :roomJoin, :roomList, :roomRecord, :roomAdmin,
|
|
65
|
-
:room, :canPublish, :canPublishSources, :canSubscribe,
|
|
66
|
-
:canUpdateOwnMetadata, :hidden, :recorder, :ingressAdmin
|
|
65
|
+
:room, :destinationRoom, :canPublish, :canPublishSources, :canSubscribe,
|
|
66
|
+
:canPublishData, :canUpdateOwnMetadata, :hidden, :recorder, :ingressAdmin
|
|
67
67
|
|
|
68
68
|
def initialize(
|
|
69
69
|
# true if can create or delete rooms
|
|
@@ -78,6 +78,8 @@ module LiveKit
|
|
|
78
78
|
roomAdmin: nil,
|
|
79
79
|
# name of the room for join or admin permissions
|
|
80
80
|
room: nil,
|
|
81
|
+
# destination room a participant may be forwarded or moved to
|
|
82
|
+
destinationRoom: nil,
|
|
81
83
|
# for join tokens, can participant publish, true by default
|
|
82
84
|
canPublish: nil,
|
|
83
85
|
# TrackSource types that a participant may publish
|
|
@@ -101,6 +103,7 @@ module LiveKit
|
|
|
101
103
|
@roomRecord = roomRecord
|
|
102
104
|
@roomAdmin = roomAdmin
|
|
103
105
|
@room = room
|
|
106
|
+
@destinationRoom = destinationRoom
|
|
104
107
|
@canPublish = canPublish
|
|
105
108
|
@canPublishSources = canPublishSources
|
|
106
109
|
@canSubscribe = canSubscribe
|
|
@@ -123,6 +126,7 @@ module LiveKit
|
|
|
123
126
|
roomRecord: hash["roomRecord"],
|
|
124
127
|
roomAdmin: hash["roomAdmin"],
|
|
125
128
|
room: hash["room"],
|
|
129
|
+
destinationRoom: hash["destinationRoom"],
|
|
126
130
|
canPublish: hash["canPublish"],
|
|
127
131
|
canPublishSources: hash["canPublishSources"],
|
|
128
132
|
canSubscribe: hash["canSubscribe"],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "livekit/proto/livekit_ingress_twirp"
|
|
2
2
|
require "livekit/auth_mixin"
|
|
3
3
|
require 'livekit/utils'
|
|
4
|
+
require 'livekit/failover'
|
|
4
5
|
|
|
5
6
|
module LiveKit
|
|
6
7
|
class IngressServiceClient < Twirp::Client
|
|
@@ -8,10 +9,11 @@ module LiveKit
|
|
|
8
9
|
include AuthMixin
|
|
9
10
|
attr_accessor :api_key, :api_secret
|
|
10
11
|
|
|
11
|
-
def initialize(base_url, api_key: nil, api_secret: nil)
|
|
12
|
-
super(
|
|
12
|
+
def initialize(base_url, api_key: nil, api_secret: nil, token: nil, failover: true, connection: nil)
|
|
13
|
+
super(connection || LiveKit::Failover.connection(base_url, failover))
|
|
13
14
|
@api_key = api_key
|
|
14
15
|
@api_secret = api_secret
|
|
16
|
+
@token = token
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def create_ingress(
|
|
@@ -48,7 +50,7 @@ module LiveKit
|
|
|
48
50
|
enable_transcoding: enable_transcoding,
|
|
49
51
|
url: url,
|
|
50
52
|
)
|
|
51
|
-
|
|
53
|
+
rpc!(
|
|
52
54
|
:CreateIngress,
|
|
53
55
|
request,
|
|
54
56
|
headers:auth_header(video_grant: VideoGrant.new(ingressAdmin: true)),
|
|
@@ -86,7 +88,7 @@ module LiveKit
|
|
|
86
88
|
bypass_transcoding: bypass_transcoding,
|
|
87
89
|
enable_transcoding: enable_transcoding,
|
|
88
90
|
)
|
|
89
|
-
|
|
91
|
+
rpc!(
|
|
90
92
|
:UpdateIngress,
|
|
91
93
|
request,
|
|
92
94
|
headers:auth_header(video_grant: VideoGrant.new(ingressAdmin: true)),
|
|
@@ -103,7 +105,7 @@ module LiveKit
|
|
|
103
105
|
room_name: room_name,
|
|
104
106
|
ingress_id: ingress_id,
|
|
105
107
|
)
|
|
106
|
-
|
|
108
|
+
rpc!(
|
|
107
109
|
:ListIngress,
|
|
108
110
|
request,
|
|
109
111
|
headers:auth_header(video_grant: VideoGrant.new(ingressAdmin: true)),
|
|
@@ -114,7 +116,7 @@ module LiveKit
|
|
|
114
116
|
request = Proto::DeleteIngressRequest.new(
|
|
115
117
|
ingress_id: ingress_id
|
|
116
118
|
)
|
|
117
|
-
|
|
119
|
+
rpc!(
|
|
118
120
|
:DeleteIngress,
|
|
119
121
|
request,
|
|
120
122
|
headers:auth_header(video_grant: VideoGrant.new(ingressAdmin: true)),
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "livekit/room_service_client"
|
|
4
|
+
require "livekit/egress_service_client"
|
|
5
|
+
require "livekit/ingress_service_client"
|
|
6
|
+
require "livekit/sip_service_client"
|
|
7
|
+
require "livekit/agent_dispatch_service_client"
|
|
8
|
+
require "livekit/connector_service_client"
|
|
9
|
+
require "livekit/failover"
|
|
10
|
+
|
|
11
|
+
module LiveKit
|
|
12
|
+
# A single entry point to every LiveKit server API, exposing each service
|
|
13
|
+
# through a reader.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# api = LiveKit::LiveKitAPI.new("https://my.livekit.host", api_key: k, api_secret: s)
|
|
17
|
+
# api.room.create_room("my-room")
|
|
18
|
+
class LiveKitAPI
|
|
19
|
+
# @return [RoomServiceClient]
|
|
20
|
+
attr_reader :room
|
|
21
|
+
# @return [EgressServiceClient]
|
|
22
|
+
attr_reader :egress
|
|
23
|
+
# @return [IngressServiceClient]
|
|
24
|
+
attr_reader :ingress
|
|
25
|
+
# @return [SIPServiceClient]
|
|
26
|
+
attr_reader :sip
|
|
27
|
+
# @return [AgentDispatchServiceClient]
|
|
28
|
+
attr_reader :agent_dispatch
|
|
29
|
+
# @return [ConnectorServiceClient]
|
|
30
|
+
attr_reader :connector
|
|
31
|
+
|
|
32
|
+
# Authenticate with either an API key and secret (recommended for backend
|
|
33
|
+
# use), or a pre-signed +token+ (for client-side use, where the API secret
|
|
34
|
+
# must not be exposed). Any omitted value falls back to its environment
|
|
35
|
+
# variable: +LIVEKIT_URL+, +LIVEKIT_TOKEN+, +LIVEKIT_API_KEY+, +LIVEKIT_API_SECRET+.
|
|
36
|
+
def initialize(url = nil, api_key: nil, api_secret: nil, token: nil, failover: true)
|
|
37
|
+
url ||= ENV["LIVEKIT_URL"]
|
|
38
|
+
|
|
39
|
+
# Only fall back to environment credentials when none were provided
|
|
40
|
+
# explicitly, so an ambient LIVEKIT_TOKEN can't silently override an
|
|
41
|
+
# explicit api_key/secret (or vice versa).
|
|
42
|
+
if token.nil? && api_key.nil? && api_secret.nil?
|
|
43
|
+
token = ENV["LIVEKIT_TOKEN"]
|
|
44
|
+
end
|
|
45
|
+
if token.nil? && api_key.nil? && api_secret.nil?
|
|
46
|
+
api_key = ENV["LIVEKIT_API_KEY"]
|
|
47
|
+
api_secret = ENV["LIVEKIT_API_SECRET"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "url is required (pass it or set LIVEKIT_URL)" if url.nil? || url.empty?
|
|
51
|
+
if token.nil? && (api_key.nil? || api_secret.nil?)
|
|
52
|
+
raise ArgumentError, "either a token, or api_key and api_secret, are required"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Share one Faraday connection (and its adapter/middleware) across every
|
|
56
|
+
# service instead of letting each open its own.
|
|
57
|
+
connection = LiveKit::Failover.connection(url, failover)
|
|
58
|
+
opts = { api_key: api_key, api_secret: api_secret, token: token, failover: failover, connection: connection }
|
|
59
|
+
@room = RoomServiceClient.new(url, **opts)
|
|
60
|
+
@egress = EgressServiceClient.new(url, **opts)
|
|
61
|
+
@ingress = IngressServiceClient.new(url, **opts)
|
|
62
|
+
@sip = SIPServiceClient.new(url, **opts)
|
|
63
|
+
@agent_dispatch = AgentDispatchServiceClient.new(url, **opts)
|
|
64
|
+
@connector = ConnectorServiceClient.new(url, **opts)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
require 'google/protobuf'
|
|
6
6
|
|
|
7
7
|
require 'livekit_agent_pb'
|
|
8
|
+
require 'logger/options_pb'
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
descriptor_data = "\n\x1clivekit_agent_dispatch.proto\x12\x07livekit\x1a\x13livekit_agent.proto\"
|
|
11
|
+
descriptor_data = "\n\x1clivekit_agent_dispatch.proto\x12\x07livekit\x1a\x13livekit_agent.proto\x1a\x14logger/options.proto\"\x9d\x02\n\x1a\x43reateAgentDispatchRequest\x12\x12\n\nagent_name\x18\x01 \x01(\t\x12\x0c\n\x04room\x18\x02 \x01(\t\x12\x15\n\x08metadata\x18\x03 \x01(\tB\x03\xc0P\x01\x12\x31\n\x0erestart_policy\x18\x04 \x01(\x0e\x32\x19.livekit.JobRestartPolicy\x12\x12\n\ndeployment\x18\x05 \x01(\t\x12L\n\nattributes\x18\x06 \x03(\x0b\x32\x33.livekit.CreateAgentDispatchRequest.AttributesEntryB\x03\xc0P\x01\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xfd\x01\n\x11RoomAgentDispatch\x12\x12\n\nagent_name\x18\x01 \x01(\t\x12\x15\n\x08metadata\x18\x02 \x01(\tB\x03\xc0P\x01\x12\x31\n\x0erestart_policy\x18\x03 \x01(\x0e\x32\x19.livekit.JobRestartPolicy\x12\x12\n\ndeployment\x18\x04 \x01(\t\x12\x43\n\nattributes\x18\x05 \x03(\x0b\x32*.livekit.RoomAgentDispatch.AttributesEntryB\x03\xc0P\x01\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"N\n\x1a\x44\x65leteAgentDispatchRequest\x12\"\n\x0b\x64ispatch_id\x18\x01 \x01(\tB\r\xbaP\ndispatchID\x12\x0c\n\x04room\x18\x02 \x01(\t\"L\n\x18ListAgentDispatchRequest\x12\"\n\x0b\x64ispatch_id\x18\x01 \x01(\tB\r\xbaP\ndispatchID\x12\x0c\n\x04room\x18\x02 \x01(\t\"M\n\x19ListAgentDispatchResponse\x12\x30\n\x10\x61gent_dispatches\x18\x01 \x03(\x0b\x32\x16.livekit.AgentDispatch\"\xbb\x02\n\rAgentDispatch\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nagent_name\x18\x02 \x01(\t\x12\x0c\n\x04room\x18\x03 \x01(\t\x12\x15\n\x08metadata\x18\x04 \x01(\tB\x03\xc0P\x01\x12*\n\x05state\x18\x05 \x01(\x0b\x32\x1b.livekit.AgentDispatchState\x12\x31\n\x0erestart_policy\x18\x06 \x01(\x0e\x32\x19.livekit.JobRestartPolicy\x12\x12\n\ndeployment\x18\x07 \x01(\t\x12?\n\nattributes\x18\x08 \x03(\x0b\x32&.livekit.AgentDispatch.AttributesEntryB\x03\xc0P\x01\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"X\n\x12\x41gentDispatchState\x12\x1a\n\x04jobs\x18\x01 \x03(\x0b\x32\x0c.livekit.Job\x12\x12\n\ncreated_at\x18\x02 \x01(\x03\x12\x12\n\ndeleted_at\x18\x03 \x01(\x03*5\n\x10JobRestartPolicy\x12\x12\n\x0eJRP_ON_FAILURE\x10\x00\x12\r\n\tJRP_NEVER\x10\x01\x32\x8b\x02\n\x14\x41gentDispatchService\x12M\n\x0e\x43reateDispatch\x12#.livekit.CreateAgentDispatchRequest\x1a\x16.livekit.AgentDispatch\x12M\n\x0e\x44\x65leteDispatch\x12#.livekit.DeleteAgentDispatchRequest\x1a\x16.livekit.AgentDispatch\x12U\n\x0cListDispatch\x12!.livekit.ListAgentDispatchRequest\x1a\".livekit.ListAgentDispatchResponseBFZ#github.com/livekit/protocol/livekit\xaa\x02\rLiveKit.Proto\xea\x02\x0eLiveKit::Protob\x06proto3"
|
|
11
12
|
|
|
12
13
|
pool = Google::Protobuf::DescriptorPool.generated_pool
|
|
13
14
|
pool.add_serialized_file(descriptor_data)
|
|
@@ -21,5 +22,6 @@ module LiveKit
|
|
|
21
22
|
ListAgentDispatchResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("livekit.ListAgentDispatchResponse").msgclass
|
|
22
23
|
AgentDispatch = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("livekit.AgentDispatch").msgclass
|
|
23
24
|
AgentDispatchState = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("livekit.AgentDispatchState").msgclass
|
|
25
|
+
JobRestartPolicy = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("livekit.JobRestartPolicy").enummodule
|
|
24
26
|
end
|
|
25
27
|
end
|