Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
## [Unreleased]

## [1.0.0]

### Added

- `LiveKit::LiveKitAPI`, a single entry point exposing every service through a reader (`room`, `egress`, `ingress`, `sip`, `agent_dispatch`, `connector`).
- Token authentication: construct clients (or `LiveKitAPI`) with a pre-signed `token:` that is sent verbatim, enabling client-side use without an API secret. Credentials fall back to `LIVEKIT_URL`, `LIVEKIT_TOKEN`, `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET`.
- `LiveKit::SipCallError` (a `LiveKit::TwirpError`) raised by SIP dialing calls, exposing `sip_status_code` and `sip_status`.

### Changed

- **Breaking:** service methods now return the response message directly and raise `LiveKit::TwirpError` on failure, instead of returning a `Twirp::ClientResp`. Replace `resp = client.create_room(...); resp.data` with `room = client.create_room(...)`, and rescue `LiveKit::TwirpError` for errors.
- `faraday` is now a declared runtime dependency.

### Fixed

- `EgressServiceClient` no longer raises when a single output is passed to `start_participant_egress` (the request has no deprecated singular output field).
- `AgentDispatchServiceClient#get_dispatch` / `#list_dispatch` now return correctly (they previously assumed the RPC returned data directly).

## [0.1.0] - 2021-07-25

- Initial release
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
PATH
remote: .
specs:
livekit-server-sdk (0.9.0)
livekit-server-sdk (1.0.0)
faraday (>= 2.0, < 3.0)
google-protobuf (~> 4.30, >= 4.30.2)
jwt (>= 2.2.3, < 3.0)
twirp (~> 1.13, >= 1.13.1)
Expand Down
2 changes: 2 additions & 0 deletions lib/livekit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "livekit/access_token"
require "livekit/utils"
require "livekit/grants"
require "livekit/errors"
require "livekit/token_verifier"
require "livekit/version"

Expand All @@ -14,3 +15,4 @@
require "livekit/sip_service_client"
require "livekit/agent_dispatch_service_client"
require "livekit/connector_service_client"
require "livekit/livekit_api"
13 changes: 7 additions & 6 deletions lib/livekit/agent_dispatch_service_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ class AgentDispatchServiceClient < Twirp::Client
include AuthMixin
attr_accessor :api_key, :api_secret

def initialize(base_url, api_key: nil, api_secret: nil, failover: true)
super(LiveKit::Failover.connection(base_url, failover))
def initialize(base_url, api_key: nil, api_secret: nil, token: nil, failover: true, connection: nil)
super(connection || LiveKit::Failover.connection(base_url, failover))
@api_key = api_key
@api_secret = api_secret
@token = token
end

# Creates a new agent dispatch for a named agent
Expand All @@ -35,7 +36,7 @@ def create_dispatch(
agent_name: agent_name,
metadata: metadata,
)
self.rpc(
rpc!(
:CreateDispatch,
request,
headers: auth_header(video_grant: VideoGrant.new(roomAdmin: true, room: room_name)),
Expand All @@ -51,7 +52,7 @@ def delete_dispatch(dispatch_id, room_name)
dispatch_id: dispatch_id,
room: room_name,
)
self.rpc(
rpc!(
:DeleteDispatch,
request,
headers: auth_header(video_grant: VideoGrant.new(roomAdmin: true, room: room_name)),
Expand All @@ -67,7 +68,7 @@ def get_dispatch(dispatch_id, room_name)
dispatch_id: dispatch_id,
room: room_name,
)
res = self.rpc(
res = rpc!(
:ListDispatch,
request,
headers: auth_header(video_grant: VideoGrant.new(roomAdmin: true, room: room_name)),
Expand All @@ -85,7 +86,7 @@ def list_dispatch(room_name)
request = Proto::ListAgentDispatchRequest.new(
room: room_name,
)
res = self.rpc(
res = rpc!(
:ListDispatch,
request,
headers: auth_header(video_grant: VideoGrant.new(roomAdmin: true, room: room_name)),
Expand Down
31 changes: 23 additions & 8 deletions lib/livekit/auth_mixin.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
# frozen_string_literal: true

require "livekit/errors"

module LiveKit
# Create authenticated headers when keys are provided
# Shared behavior for the Twirp-based service clients: builds authenticated
# request headers and issues RPCs that raise on error.
module AuthMixin
# Builds request headers. When a pre-signed token is set it is sent verbatim
# (the caller is responsible for its grants); otherwise a token is signed per
# call from the API key and secret.
def auth_header(
video_grant: nil,
sip_grant: nil
)
headers = {}
t = ::LiveKit::AccessToken.new(api_key: @api_key, api_secret: @api_secret)
if video_grant != nil
t.video_grant = video_grant
end
if sip_grant != nil
t.sip_grant = sip_grant
if @token
headers["Authorization"] = "Bearer #{@token}"
return headers
end

t = ::LiveKit::AccessToken.new(api_key: @api_key, api_secret: @api_secret)
t.video_grant = video_grant if video_grant != nil
t.sip_grant = sip_grant if sip_grant != nil
headers["Authorization"] = "Bearer #{t.to_jwt}"
headers["User-Agent"] = "LiveKit Ruby SDK"
headers
end

# Issues a Twirp RPC and returns the response message, raising a
# {LiveKit::ServerError} (or {LiveKit::SipCallError}) on failure.
def rpc!(rpc_method, input, headers:)
resp = rpc(rpc_method, input, headers: headers)
raise ::LiveKit::ServerError.from(resp.error) if resp.error

resp.data
end
end
end
25 changes: 12 additions & 13 deletions lib/livekit/connector_service_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ class ConnectorServiceClient < Twirp::Client
include AuthMixin
attr_accessor :api_key, :api_secret

def initialize(base_url, api_key: nil, api_secret: nil, failover: true)
super(LiveKit::Failover.connection(base_url, failover))
def initialize(base_url, api_key: nil, api_secret: nil, token: nil, failover: true, connection: nil)
super(connection || LiveKit::Failover.connection(base_url, failover))
@api_key = api_key
@api_secret = api_secret
@token = token
end

# Initiates an outbound WhatsApp call.
# @param request [Proto::DialWhatsAppCallRequest]
# @return [Proto::DialWhatsAppCallResponse]
def dial_whatsapp_call(request)
self.rpc(
rpc!(
:DialWhatsAppCall,
request,
headers: auth_header(video_grant: VideoGrant.new(roomCreate: true)),
Expand All @@ -35,28 +36,26 @@ def dial_whatsapp_call(request)
# Accepts an inbound WhatsApp call.
# @param request [Proto::AcceptWhatsAppCallRequest]
# @param timeout [Numeric, nil] optional request timeout in seconds. When the
# request waits for an answer, it defaults to a longer value (dialing takes
# time) and is raised, if needed, to stay above the request's ringing_timeout.
# request waits for the inbound party to join, it defaults to the standard
# ring window.
# @return [Proto::AcceptWhatsAppCallResponse]
def accept_whatsapp_call(request, timeout: nil)
headers = auth_header(video_grant: VideoGrant.new(roomCreate: true))
# Accept can block until the call is answered, so default the request timeout
# to the standard ring window; otherwise honor any user timeout. The caller
# overrides via +timeout+ and should set it above the ringing_timeout passed
# to dial_whatsapp_call (the two calls are separate).
# Waiting for the inbound party to join can block, so default the request
# timeout to the standard ring window; otherwise honor any user timeout.
if request.wait_until_answered
headers[Failover::TIMEOUT_HEADER] = (timeout || DialTimeout::DEFAULT_RINGING_TIMEOUT).to_s
elsif timeout
headers[Failover::TIMEOUT_HEADER] = timeout.to_s
end
self.rpc(:AcceptWhatsAppCall, request, headers: headers)
rpc!(:AcceptWhatsAppCall, request, headers: headers)
end

# Connects an established WhatsApp call (used for business-initiated calls).
# @param request [Proto::ConnectWhatsAppCallRequest]
# @return [Proto::ConnectWhatsAppCallResponse]
def connect_whatsapp_call(request)
self.rpc(
rpc!(
:ConnectWhatsAppCall,
request,
headers: auth_header(video_grant: VideoGrant.new(roomCreate: true)),
Expand All @@ -67,7 +66,7 @@ def connect_whatsapp_call(request)
# @param request [Proto::DisconnectWhatsAppCallRequest]
# @return [Proto::DisconnectWhatsAppCallResponse]
def disconnect_whatsapp_call(request)
self.rpc(
rpc!(
:DisconnectWhatsAppCall,
request,
headers: auth_header(video_grant: VideoGrant.new(roomCreate: true)),
Expand All @@ -78,7 +77,7 @@ def disconnect_whatsapp_call(request)
# @param request [Proto::ConnectTwilioCallRequest]
# @return [Proto::ConnectTwilioCallResponse]
def connect_twilio_call(request)
self.rpc(
rpc!(
:ConnectTwilioCall,
request,
headers: auth_header(video_grant: VideoGrant.new(roomCreate: true)),
Expand Down
6 changes: 3 additions & 3 deletions lib/livekit/dial_timeout.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true

module LiveKit
# Request-timeout handling shared by calls that dial a phone and wait for an
# answer (SIP CreateSIPParticipant/TransferSIPParticipant, WhatsApp
# Request-timeout handling shared by calls that may block until a call is
# answered (SIP CreateSIPParticipant/TransferSIPParticipant, WhatsApp
# AcceptWhatsAppCall). These take longer than a normal request, and the request
# must outlast ringing or it would abort before the call can be answered.
# must outlast the wait or it would abort before the call can be answered.
module DialTimeout
# Ring window (seconds) assumed when a request doesn't set a ringing timeout;
# matches the server default. A dialing request must outlast it.
Expand Down
31 changes: 17 additions & 14 deletions lib/livekit/egress_service_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ class EgressServiceClient < Twirp::Client
include AuthMixin
attr_accessor :api_key, :api_secret

def initialize(base_url, api_key: nil, api_secret: nil, failover: true)
super(LiveKit::Failover.connection(base_url, failover))
def initialize(base_url, api_key: nil, api_secret: nil, token: nil, failover: true, connection: nil)
super(connection || LiveKit::Failover.connection(base_url, failover))
@api_key = api_key
@api_secret = api_secret
@token = token
end

def start_room_composite_egress(
Expand Down Expand Up @@ -50,7 +51,7 @@ def start_room_composite_egress(
webhooks: webhooks,
)
self.set_output(request, output)
self.rpc(
rpc!(
:StartRoomCompositeEgress,
request,
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
Expand Down Expand Up @@ -80,7 +81,7 @@ def start_participant_egress(
webhooks: webhooks,
)
self.set_output(request, output)
self.rpc(
rpc!(
:StartParticipantEgress,
request,
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
Expand Down Expand Up @@ -111,7 +112,7 @@ def start_track_composite_egress(
webhooks: webhooks,
)
self.set_output(request, output)
self.rpc(
rpc!(
:StartTrackCompositeEgress,
request,
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
Expand All @@ -136,7 +137,7 @@ def start_track_egress(
else
request.websocket_url = output
end
self.rpc(
rpc!(
:StartTrackEgress,
request,
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
Expand Down Expand Up @@ -170,15 +171,15 @@ def start_web_egress(
webhooks: webhooks,
)
self.set_output(request, output)
self.rpc(
rpc!(
:StartWebEgress,
request,
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
)
end

def update_layout(egress_id, layout)
self.rpc(
rpc!(
:UpdateLayout,
Proto::UpdateLayoutRequest.new(
egress_id: egress_id,
Expand All @@ -192,7 +193,7 @@ def update_stream(egress_id,
add_output_urls: [],
remove_output_urls: []
)
self.rpc(
rpc!(
:UpdateStream,
Proto::UpdateStreamRequest.new(
egress_id: egress_id,
Expand All @@ -209,7 +210,7 @@ def list_egress(
egress_id: nil,
active: false
)
self.rpc(
rpc!(
:ListEgress,
Proto::ListEgressRequest.new(
room_name: room_name,
Expand All @@ -221,7 +222,7 @@ def list_egress(
end

def stop_egress(egress_id)
self.rpc(
rpc!(
:StopEgress,
Proto::StopEgressRequest.new(egress_id: egress_id),
headers:auth_header(video_grant: VideoGrant.new(roomRecord: true)),
Expand Down Expand Up @@ -253,13 +254,15 @@ def set_output(request, output)
end
end
elsif output.is_a? LiveKit::Proto::EncodedFileOutput
request.file = output
# The singular field is deprecated and absent on some requests (e.g.
# ParticipantEgressRequest); the plural *_outputs is the current field.
request.file = output if request.respond_to?(:file=)
request.file_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::EncodedFileOutput, [output])
elsif output.is_a? LiveKit::Proto::SegmentedFileOutput
request.segments = output
request.segments = output if request.respond_to?(:segments=)
request.segment_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::SegmentedFileOutput, [output])
elsif output.is_a? LiveKit::Proto::StreamOutput
request.stream = output
request.stream = output if request.respond_to?(:stream=)
request.stream_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::StreamOutput, [output])
elsif output.is_a? LiveKit::Proto::ImageOutput
request.image_outputs = Google::Protobuf::RepeatedField.new(:message, Proto::ImageOutput, [output])
Expand Down
Loading
Loading