spatialreal 0.2.3__tar.gz → 0.3.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: spatialreal
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: SpatialReal Python SDK — realtime avatar sessions (audio in, animation/egress out)
5
5
  Project-URL: Documentation, https://docs.spatialreal.ai
6
6
  Project-URL: Website, https://www.spatialreal.ai/
@@ -64,6 +64,31 @@ await session.interrupt() # interrupts the latest request
64
64
  await session.close()
65
65
  ```
66
66
 
67
+ ### Agora source-timeline-only viewers
68
+
69
+ Set `AgoraEgressConfig(source_timeline_only=True, ...)` only when every viewer
70
+ plays the source-timeline audio and manages its own entry transition. The default
71
+ is `False`, which retains the existing RTC delivery policy. This setting does not
72
+ change input sample rates or audio data.
73
+
74
+ `start()` requires the server's `agora_source_timeline_only` capability when this
75
+ mode is requested. A confirmation without it raises `UnsupportedConfigurationError`
76
+ (`protocolError`, `websocket_handshake`, not retryable) and closes the connection
77
+ before audio is sent. A standard WebSocket policy rejection (`1008`) is also not
78
+ retryable. Other transport failures retain their normal retry behavior.
79
+
80
+ For the coordinated bounded-burst profile, also set
81
+ `source_playback_profile=RtcSourcePlaybackProfile.BOUNDED_BURST_V1` (import the
82
+ enum from `spatialreal`). It requires `source_timeline_only=True` and both
83
+ `agora_source_timeline_only` and `agora_source_burst_v1` in the server's actual
84
+ confirmation. `session.capabilities` is the read-only tuple from that handshake.
85
+ Missing acceptance closes the connection before the read loop or audio starts;
86
+ there is no silent fallback. The default `UNSPECIFIED`/0 is omitted on the wire
87
+ and retains existing behavior. Unknown values and booleans are rejected.
88
+
89
+ Every animation viewer must independently opt into the same profile using a
90
+ compatible viewer SDK; the publisher's handshake cannot negotiate room viewers.
91
+
67
92
  ### Pause / resume (egress mode)
68
93
 
69
94
  When the server declares the `playback_control` capability, the current
@@ -102,6 +127,12 @@ implements the server's WebSocket close-code contract (40xx: don't retry,
102
127
 
103
128
  ## What's new
104
129
 
130
+ - 0.3.0: Agora source playback negotiation. `AgoraEgressConfig(source_timeline_only=True)`
131
+ requests source-timeline-only playback; `source_playback_profile=RtcSourcePlaybackProfile.BOUNDED_BURST_V1`
132
+ requests the bounded source playback profile. `start()` refuses the session
133
+ (`UnsupportedConfigurationError`, not retryable) before any audio is sent when the
134
+ server does not confirm the matching capabilities (`agora_source_timeline_only`,
135
+ `agora_source_burst_v1`). Defaults are unchanged.
105
136
  - 0.2.3: `app_id` is optional (the API key identifies the app); protobuf floor lowered
106
137
  to 5.29 so the SDK installs next to packages that pin protobuf 5.x (TEN Framework agents).
107
138
  - `on_playback(PlaybackSignal)` — structured playback lifecycle; no protobuf
@@ -31,6 +31,31 @@ await session.interrupt() # interrupts the latest request
31
31
  await session.close()
32
32
  ```
33
33
 
34
+ ### Agora source-timeline-only viewers
35
+
36
+ Set `AgoraEgressConfig(source_timeline_only=True, ...)` only when every viewer
37
+ plays the source-timeline audio and manages its own entry transition. The default
38
+ is `False`, which retains the existing RTC delivery policy. This setting does not
39
+ change input sample rates or audio data.
40
+
41
+ `start()` requires the server's `agora_source_timeline_only` capability when this
42
+ mode is requested. A confirmation without it raises `UnsupportedConfigurationError`
43
+ (`protocolError`, `websocket_handshake`, not retryable) and closes the connection
44
+ before audio is sent. A standard WebSocket policy rejection (`1008`) is also not
45
+ retryable. Other transport failures retain their normal retry behavior.
46
+
47
+ For the coordinated bounded-burst profile, also set
48
+ `source_playback_profile=RtcSourcePlaybackProfile.BOUNDED_BURST_V1` (import the
49
+ enum from `spatialreal`). It requires `source_timeline_only=True` and both
50
+ `agora_source_timeline_only` and `agora_source_burst_v1` in the server's actual
51
+ confirmation. `session.capabilities` is the read-only tuple from that handshake.
52
+ Missing acceptance closes the connection before the read loop or audio starts;
53
+ there is no silent fallback. The default `UNSPECIFIED`/0 is omitted on the wire
54
+ and retains existing behavior. Unknown values and booleans are rejected.
55
+
56
+ Every animation viewer must independently opt into the same profile using a
57
+ compatible viewer SDK; the publisher's handshake cannot negotiate room viewers.
58
+
34
59
  ### Pause / resume (egress mode)
35
60
 
36
61
  When the server declares the `playback_control` capability, the current
@@ -69,6 +94,12 @@ implements the server's WebSocket close-code contract (40xx: don't retry,
69
94
 
70
95
  ## What's new
71
96
 
97
+ - 0.3.0: Agora source playback negotiation. `AgoraEgressConfig(source_timeline_only=True)`
98
+ requests source-timeline-only playback; `source_playback_profile=RtcSourcePlaybackProfile.BOUNDED_BURST_V1`
99
+ requests the bounded source playback profile. `start()` refuses the session
100
+ (`UnsupportedConfigurationError`, not retryable) before any audio is sent when the
101
+ server does not confirm the matching capabilities (`agora_source_timeline_only`,
102
+ `agora_source_burst_v1`). Defaults are unchanged.
72
103
  - 0.2.3: `app_id` is optional (the API key identifies the app); protobuf floor lowered
73
104
  to 5.29 so the SDK installs next to packages that pin protobuf 5.x (TEN Framework agents).
74
105
  - `on_playback(PlaybackSignal)` — structured playback lifecycle; no protobuf
@@ -24,9 +24,10 @@ from .config import (
24
24
  AgoraEgressConfig,
25
25
  AudioFormat,
26
26
  LiveKitEgressConfig,
27
+ RtcSourcePlaybackProfile,
27
28
  SessionConfig,
28
29
  )
29
- from .errors import AvatarSDKError, AvatarSDKErrorCode, CloseCode, SessionTokenError
30
+ from .errors import AvatarSDKError, AvatarSDKErrorCode, CloseCode, SessionTokenError, UnsupportedConfigurationError
30
31
  from .events import InterruptReason, PlaybackSignal, PlaybackState, PlaybackStateEvent
31
32
  from .logid import generate_log_id
32
33
  from .session import AvatarSession, new_avatar_session
@@ -40,12 +41,14 @@ __all__ = [
40
41
  "AvatarSession",
41
42
  "CloseCode",
42
43
  "LiveKitEgressConfig",
44
+ "RtcSourcePlaybackProfile",
43
45
  "PlaybackSignal",
44
46
  "PlaybackStateEvent",
45
47
  "PlaybackState",
46
48
  "InterruptReason",
47
49
  "SessionConfig",
48
50
  "SessionTokenError",
51
+ "UnsupportedConfigurationError",
49
52
  "__version__",
50
53
  "generate_log_id",
51
54
  "new_avatar_session",
@@ -5,10 +5,18 @@ from __future__ import annotations
5
5
  from collections.abc import Callable
6
6
  from dataclasses import dataclass, field
7
7
  from datetime import datetime
8
- from enum import Enum
8
+ from enum import Enum, IntEnum
9
9
 
10
10
  from .environments import DEFAULT_ENVIRONMENT
11
11
  from .events import PlaybackSignal, PlaybackStateEvent
12
+ from .proto.generated import message_pb2
13
+
14
+
15
+ class RtcSourcePlaybackProfile(IntEnum):
16
+ """Explicit source playback contract; independent RTC viewers must also opt in."""
17
+
18
+ UNSPECIFIED = message_pb2.RTC_SOURCE_PLAYBACK_PROFILE_UNSPECIFIED
19
+ BOUNDED_BURST_V1 = message_pb2.RTC_SOURCE_PLAYBACK_PROFILE_BOUNDED_BURST_V1
12
20
 
13
21
 
14
22
  class AudioFormat(str, Enum):
@@ -52,6 +60,22 @@ class AgoraEgressConfig:
52
60
  token: str = field(default="", repr=False)
53
61
  uid: int = 0
54
62
  publisher_id: str = ""
63
+ # Only for viewers that play source-timeline audio and manage entry locally.
64
+ # The server must explicitly acknowledge this mode during the handshake.
65
+ source_timeline_only: bool = False
66
+ source_playback_profile: RtcSourcePlaybackProfile = RtcSourcePlaybackProfile.UNSPECIFIED
67
+
68
+ def __post_init__(self) -> None:
69
+ if not isinstance(self.source_timeline_only, bool):
70
+ raise ValueError("source_timeline_only must be a boolean")
71
+ if isinstance(self.source_playback_profile, bool) or not isinstance(self.source_playback_profile, int):
72
+ raise ValueError("source_playback_profile must be a known RtcSourcePlaybackProfile")
73
+ try:
74
+ self.source_playback_profile = RtcSourcePlaybackProfile(self.source_playback_profile)
75
+ except ValueError as exc:
76
+ raise ValueError("source_playback_profile must be a known RtcSourcePlaybackProfile") from exc
77
+ if self.source_playback_profile and not self.source_timeline_only:
78
+ raise ValueError("source_playback_profile requires source_timeline_only=True")
55
79
 
56
80
 
57
81
  @dataclass
@@ -12,6 +12,8 @@ from __future__ import annotations
12
12
 
13
13
  from enum import Enum, IntEnum
14
14
 
15
+ from websockets.frames import CloseCode as WebSocketCloseCode
16
+
15
17
 
16
18
  class AvatarSDKErrorCode(str, Enum):
17
19
  """Stable, log-friendly error codes surfaced by the SDK."""
@@ -117,6 +119,8 @@ class AvatarSDKError(Exception):
117
119
  )
118
120
  if self.close_code in _RETRYABLE_CLOSE_CODES:
119
121
  return True
122
+ if self.close_code == WebSocketCloseCode.POLICY_VIOLATION:
123
+ return False
120
124
  if 4000 <= self.close_code < 4500:
121
125
  return False
122
126
  return True
@@ -125,6 +129,22 @@ class AvatarSDKError(Exception):
125
129
  return f"{self.code.value}: {self.message}"
126
130
 
127
131
 
132
+ class UnsupportedConfigurationError(AvatarSDKError):
133
+ """The server confirmed a session without accepting a required mode.
134
+
135
+ This is a local handshake classification, not a new wire error code.
136
+ Retrying the same configuration cannot negotiate the missing capability.
137
+ """
138
+
139
+ def __init__(self, message: str, *, connection_id: str | None = None):
140
+ super().__init__(
141
+ code=AvatarSDKErrorCode.protocolError,
142
+ message=message,
143
+ phase="websocket_handshake",
144
+ connection_id=connection_id,
145
+ )
146
+
147
+
128
148
  class SessionTokenError(AvatarSDKError):
129
149
  """Raised when the session-token exchange with the console API fails."""
130
150
 
@@ -0,0 +1,74 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: driveningress/v2/driveningress.proto
5
+ # Protobuf Python Version: 5.29.0
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 5,
15
+ 29,
16
+ 0,
17
+ '',
18
+ 'driveningress/v2/driveningress.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$driveningress/v2/driveningress.proto\x12\x10\x64riveningress.v2\"\xa7\x02\n\x13LiveKitEgressConfig\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x02 \x01(\t\x12\x12\n\napi_secret\x18\x03 \x01(\t\x12\x11\n\troom_name\x18\x04 \x01(\t\x12\x14\n\x0cpublisher_id\x18\x05 \x01(\t\x12T\n\x10\x65xtra_attributes\x18\x06 \x03(\x0b\x32:.driveningress.v2.LiveKitEgressConfig.ExtraAttributesEntry\x12\x14\n\x0cidle_timeout\x18\x07 \x01(\x05\x12\x11\n\tapi_token\x18\x08 \x01(\t\x1a\x36\n\x14\x45xtraAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xc6\x01\n\x11\x41goraEgressConfig\x12\x14\n\x0c\x63hannel_name\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0b\n\x03uid\x18\x03 \x01(\r\x12\x14\n\x0cpublisher_id\x18\x04 \x01(\t\x12\x1c\n\x14source_timeline_only\x18\x06 \x01(\x08\x12K\n\x17source_playback_profile\x18\x07 \x01(\x0e\x32*.driveningress.v2.RtcSourcePlaybackProfile\"\xe7\x02\n\x16\x43lientConfigureSession\x12\x13\n\x0bsample_rate\x18\x01 \x01(\x05\x12\x0f\n\x07\x62itrate\x18\x02 \x01(\x05\x12\x33\n\x0c\x61udio_format\x18\x03 \x01(\x0e\x32\x1d.driveningress.v2.AudioFormat\x12\x45\n\x15transport_compression\x18\x04 \x01(\x0e\x32&.driveningress.v2.TransportCompression\x12\x31\n\x0b\x65gress_type\x18\x05 \x01(\x0e\x32\x1c.driveningress.v2.EgressType\x12=\n\x0elivekit_egress\x18\x06 \x01(\x0b\x32%.driveningress.v2.LiveKitEgressConfig\x12\x39\n\x0c\x61gora_egress\x18\x07 \x01(\x0b\x32#.driveningress.v2.AgoraEgressConfig\"C\n\x14ServerConfirmSession\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\">\n\x10\x43lientAudioInput\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x08\x12\r\n\x05\x61udio\x18\x03 \x01(\x0c\"S\n\x0bServerError\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x0e\n\x06req_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\x05\x12\x0f\n\x07message\x18\x04 \x01(\t\"s\n\x17ServerResponseAnimation\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x0e\n\x06req_id\x18\x02 \x01(\t\x12\x0b\n\x03\x65nd\x18\x03 \x01(\x08\x12\x11\n\tanimation\x18\x04 \x01(\x0c\x12\x11\n\tavatar_id\x18\x05 \x01(\t\"!\n\x0f\x43lientInterrupt\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\x1d\n\x0b\x43lientPause\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\x1e\n\x0c\x43lientResume\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\xf0\x01\n\x13ServerPlaybackState\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x0e\n\x06req_id\x18\x02 \x01(\t\x12:\n\x05state\x18\x03 \x01(\x0e\x32+.driveningress.v2.ServerPlaybackState.State\x12\x11\n\tplayed_ms\x18\x04 \x01(\x03\x12\x0e\n\x06reason\x18\x05 \x01(\t\"S\n\x05State\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x0b\n\x07PLAYING\x10\x01\x12\n\n\x06PAUSED\x10\x02\x12\t\n\x05\x45NDED\x10\x03\x12\x0f\n\x0bINTERRUPTED\x10\x04\"\x9c\x05\n\x07Message\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.driveningress.v2.MessageType\x12L\n\x18\x63lient_configure_session\x18\x02 \x01(\x0b\x32(.driveningress.v2.ClientConfigureSessionH\x00\x12H\n\x16server_confirm_session\x18\x03 \x01(\x0b\x32&.driveningress.v2.ServerConfirmSessionH\x00\x12@\n\x12\x63lient_audio_input\x18\x04 \x01(\x0b\x32\".driveningress.v2.ClientAudioInputH\x00\x12\x35\n\x0cserver_error\x18\x05 \x01(\x0b\x32\x1d.driveningress.v2.ServerErrorH\x00\x12N\n\x19server_response_animation\x18\x06 \x01(\x0b\x32).driveningress.v2.ServerResponseAnimationH\x00\x12=\n\x10\x63lient_interrupt\x18\x08 \x01(\x0b\x32!.driveningress.v2.ClientInterruptH\x00\x12\x35\n\x0c\x63lient_pause\x18\t \x01(\x0b\x32\x1d.driveningress.v2.ClientPauseH\x00\x12\x37\n\rclient_resume\x18\n \x01(\x0b\x32\x1e.driveningress.v2.ClientResumeH\x00\x12\x46\n\x15server_playback_state\x18\x0b \x01(\x0b\x32%.driveningress.v2.ServerPlaybackStateH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x07\x10\x08*\xcd\x02\n\x0bMessageType\x12\x17\n\x13MESSAGE_UNSPECIFIED\x10\x00\x12$\n MESSAGE_CLIENT_CONFIGURE_SESSION\x10\x01\x12\"\n\x1eMESSAGE_SERVER_CONFIRM_SESSION\x10\x02\x12\x1e\n\x1aMESSAGE_CLIENT_AUDIO_INPUT\x10\x03\x12\x18\n\x14MESSAGE_SERVER_ERROR\x10\x04\x12%\n!MESSAGE_SERVER_RESPONSE_ANIMATION\x10\x05\x12\x1c\n\x18MESSAGE_CLIENT_INTERRUPT\x10\x07\x12\x18\n\x14MESSAGE_CLIENT_PAUSE\x10\x08\x12\x19\n\x15MESSAGE_CLIENT_RESUME\x10\t\x12!\n\x1dMESSAGE_SERVER_PLAYBACK_STATE\x10\n\"\x04\x08\x06\x10\x06*D\n\x0b\x41udioFormat\x12\x1a\n\x16\x41UDIO_FORMAT_PCM_S16LE\x10\x00\x12\x19\n\x15\x41UDIO_FORMAT_OGG_OPUS\x10\x01*6\n\x14TransportCompression\x12\x1e\n\x1aTRANSPORT_COMPRESSION_NONE\x10\x00*Y\n\nEgressType\x12\x1b\n\x17\x45GRESS_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x45GRESS_TYPE_LIVEKIT\x10\x01\x12\x15\n\x11\x45GRESS_TYPE_AGORA\x10\x02*y\n\x18RtcSourcePlaybackProfile\x12+\n\'RTC_SOURCE_PLAYBACK_PROFILE_UNSPECIFIED\x10\x00\x12\x30\n,RTC_SOURCE_PLAYBACK_PROFILE_BOUNDED_BURST_V1\x10\x01\x62\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'driveningress.v2.driveningress_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._loaded_options = None
35
+ _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._serialized_options = b'8\001'
36
+ _globals['_MESSAGETYPE']._serialized_start=2267
37
+ _globals['_MESSAGETYPE']._serialized_end=2600
38
+ _globals['_AUDIOFORMAT']._serialized_start=2602
39
+ _globals['_AUDIOFORMAT']._serialized_end=2670
40
+ _globals['_TRANSPORTCOMPRESSION']._serialized_start=2672
41
+ _globals['_TRANSPORTCOMPRESSION']._serialized_end=2726
42
+ _globals['_EGRESSTYPE']._serialized_start=2728
43
+ _globals['_EGRESSTYPE']._serialized_end=2817
44
+ _globals['_RTCSOURCEPLAYBACKPROFILE']._serialized_start=2819
45
+ _globals['_RTCSOURCEPLAYBACKPROFILE']._serialized_end=2940
46
+ _globals['_LIVEKITEGRESSCONFIG']._serialized_start=59
47
+ _globals['_LIVEKITEGRESSCONFIG']._serialized_end=354
48
+ _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._serialized_start=300
49
+ _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._serialized_end=354
50
+ _globals['_AGORAEGRESSCONFIG']._serialized_start=357
51
+ _globals['_AGORAEGRESSCONFIG']._serialized_end=555
52
+ _globals['_CLIENTCONFIGURESESSION']._serialized_start=558
53
+ _globals['_CLIENTCONFIGURESESSION']._serialized_end=917
54
+ _globals['_SERVERCONFIRMSESSION']._serialized_start=919
55
+ _globals['_SERVERCONFIRMSESSION']._serialized_end=986
56
+ _globals['_CLIENTAUDIOINPUT']._serialized_start=988
57
+ _globals['_CLIENTAUDIOINPUT']._serialized_end=1050
58
+ _globals['_SERVERERROR']._serialized_start=1052
59
+ _globals['_SERVERERROR']._serialized_end=1135
60
+ _globals['_SERVERRESPONSEANIMATION']._serialized_start=1137
61
+ _globals['_SERVERRESPONSEANIMATION']._serialized_end=1252
62
+ _globals['_CLIENTINTERRUPT']._serialized_start=1254
63
+ _globals['_CLIENTINTERRUPT']._serialized_end=1287
64
+ _globals['_CLIENTPAUSE']._serialized_start=1289
65
+ _globals['_CLIENTPAUSE']._serialized_end=1318
66
+ _globals['_CLIENTRESUME']._serialized_start=1320
67
+ _globals['_CLIENTRESUME']._serialized_end=1350
68
+ _globals['_SERVERPLAYBACKSTATE']._serialized_start=1353
69
+ _globals['_SERVERPLAYBACKSTATE']._serialized_end=1593
70
+ _globals['_SERVERPLAYBACKSTATE_STATE']._serialized_start=1510
71
+ _globals['_SERVERPLAYBACKSTATE_STATE']._serialized_end=1593
72
+ _globals['_MESSAGE']._serialized_start=1596
73
+ _globals['_MESSAGE']._serialized_end=2264
74
+ # @@protoc_insertion_point(module_scope)
@@ -33,6 +33,11 @@ class EgressType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
33
33
  EGRESS_TYPE_UNSPECIFIED: _ClassVar[EgressType]
34
34
  EGRESS_TYPE_LIVEKIT: _ClassVar[EgressType]
35
35
  EGRESS_TYPE_AGORA: _ClassVar[EgressType]
36
+
37
+ class RtcSourcePlaybackProfile(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
38
+ __slots__ = ()
39
+ RTC_SOURCE_PLAYBACK_PROFILE_UNSPECIFIED: _ClassVar[RtcSourcePlaybackProfile]
40
+ RTC_SOURCE_PLAYBACK_PROFILE_BOUNDED_BURST_V1: _ClassVar[RtcSourcePlaybackProfile]
36
41
  MESSAGE_UNSPECIFIED: MessageType
37
42
  MESSAGE_CLIENT_CONFIGURE_SESSION: MessageType
38
43
  MESSAGE_SERVER_CONFIRM_SESSION: MessageType
@@ -49,6 +54,8 @@ TRANSPORT_COMPRESSION_NONE: TransportCompression
49
54
  EGRESS_TYPE_UNSPECIFIED: EgressType
50
55
  EGRESS_TYPE_LIVEKIT: EgressType
51
56
  EGRESS_TYPE_AGORA: EgressType
57
+ RTC_SOURCE_PLAYBACK_PROFILE_UNSPECIFIED: RtcSourcePlaybackProfile
58
+ RTC_SOURCE_PLAYBACK_PROFILE_BOUNDED_BURST_V1: RtcSourcePlaybackProfile
52
59
 
53
60
  class LiveKitEgressConfig(_message.Message):
54
61
  __slots__ = ("url", "api_key", "api_secret", "room_name", "publisher_id", "extra_attributes", "idle_timeout", "api_token")
@@ -78,16 +85,20 @@ class LiveKitEgressConfig(_message.Message):
78
85
  def __init__(self, url: _Optional[str] = ..., api_key: _Optional[str] = ..., api_secret: _Optional[str] = ..., room_name: _Optional[str] = ..., publisher_id: _Optional[str] = ..., extra_attributes: _Optional[_Mapping[str, str]] = ..., idle_timeout: _Optional[int] = ..., api_token: _Optional[str] = ...) -> None: ...
79
86
 
80
87
  class AgoraEgressConfig(_message.Message):
81
- __slots__ = ("channel_name", "token", "uid", "publisher_id")
88
+ __slots__ = ("channel_name", "token", "uid", "publisher_id", "source_timeline_only", "source_playback_profile")
82
89
  CHANNEL_NAME_FIELD_NUMBER: _ClassVar[int]
83
90
  TOKEN_FIELD_NUMBER: _ClassVar[int]
84
91
  UID_FIELD_NUMBER: _ClassVar[int]
85
92
  PUBLISHER_ID_FIELD_NUMBER: _ClassVar[int]
93
+ SOURCE_TIMELINE_ONLY_FIELD_NUMBER: _ClassVar[int]
94
+ SOURCE_PLAYBACK_PROFILE_FIELD_NUMBER: _ClassVar[int]
86
95
  channel_name: str
87
96
  token: str
88
97
  uid: int
89
98
  publisher_id: str
90
- def __init__(self, channel_name: _Optional[str] = ..., token: _Optional[str] = ..., uid: _Optional[int] = ..., publisher_id: _Optional[str] = ...) -> None: ...
99
+ source_timeline_only: bool
100
+ source_playback_profile: RtcSourcePlaybackProfile
101
+ def __init__(self, channel_name: _Optional[str] = ..., token: _Optional[str] = ..., uid: _Optional[int] = ..., publisher_id: _Optional[str] = ..., source_timeline_only: bool = ..., source_playback_profile: _Optional[_Union[RtcSourcePlaybackProfile, str]] = ...) -> None: ...
91
102
 
92
103
  class ClientConfigureSession(_message.Message):
93
104
  __slots__ = ("sample_rate", "bitrate", "audio_format", "transport_compression", "egress_type", "livekit_egress", "agora_egress")
@@ -27,6 +27,7 @@ from .errors import (
27
27
  AvatarSDKError,
28
28
  AvatarSDKErrorCode,
29
29
  SessionTokenError,
30
+ UnsupportedConfigurationError,
30
31
  error_code_for_close_code,
31
32
  )
32
33
  from .events import PlaybackSignal, PlaybackState, PlaybackStateEvent
@@ -40,6 +41,11 @@ SESSION_TOKEN_PATH = "/v1/auth/session-token"
40
41
  LEGACY_CONSOLE_SUFFIX = "/v1/console"
41
42
  DRIVENINGRESS_PATH = "/v2/driveningress"
42
43
  INGRESS_WEBSOCKET_PATH = "/websocket"
44
+ # A configuration rejection may carry an unclassified ServerError followed by
45
+ # the authoritative standard close. Bound the wait; no close retains the error.
46
+ CONFIGURE_CLOSE_GRACE_SECONDS = 1.0
47
+ _SOURCE_TIMELINE_CAPABILITY = "agora_source_timeline_only"
48
+ _SOURCE_BURST_CAPABILITY = "agora_source_burst_v1"
43
49
 
44
50
  logger = logging.getLogger(__name__)
45
51
 
@@ -240,6 +246,8 @@ class AvatarSession:
240
246
  s.agora_egress.token = ag.token
241
247
  s.agora_egress.uid = ag.uid
242
248
  s.agora_egress.publisher_id = ag.publisher_id
249
+ s.agora_egress.source_timeline_only = ag.source_timeline_only
250
+ s.agora_egress.source_playback_profile = ag.source_playback_profile
243
251
 
244
252
  try:
245
253
  await self._connection.send(msg.SerializeToString())
@@ -280,10 +288,40 @@ class AvatarSession:
280
288
  # Forward-compatible: the field lands with server-side playback
281
289
  # control; older protos simply don't have it.
282
290
  self._capabilities = tuple(getattr(confirm, "capabilities", ()) or ())
291
+ if (
292
+ self._config.agora_egress is not None
293
+ and self._config.agora_egress.source_timeline_only
294
+ and _SOURCE_TIMELINE_CAPABILITY not in self._capabilities
295
+ ):
296
+ raise UnsupportedConfigurationError(
297
+ "Server did not accept the requested Agora source-timeline-only mode",
298
+ connection_id=confirm.connection_id,
299
+ )
300
+ if (
301
+ self._config.agora_egress is not None
302
+ and self._config.agora_egress.source_playback_profile
303
+ and _SOURCE_BURST_CAPABILITY not in self._capabilities
304
+ ):
305
+ raise UnsupportedConfigurationError(
306
+ "Server did not accept the requested Agora source playback profile",
307
+ connection_id=confirm.connection_id,
308
+ )
283
309
  return confirm.connection_id
284
310
 
285
311
  if envelope.type == message_pb2.MESSAGE_SERVER_ERROR:
286
- raise _server_error(envelope.server_error, phase="websocket_handshake")
312
+ error = _server_error(envelope.server_error, phase="websocket_handshake")
313
+ if (
314
+ self._config.agora_egress is not None
315
+ and self._config.agora_egress.source_timeline_only
316
+ and error.close_code is None
317
+ ):
318
+ try:
319
+ await asyncio.wait_for(self._connection.wait_closed(), timeout=CONFIGURE_CLOSE_GRACE_SECONDS)
320
+ except asyncio.TimeoutError:
321
+ pass
322
+ else:
323
+ error.close_code = self._connection.close_code
324
+ raise error
287
325
 
288
326
  raise AvatarSDKError(
289
327
  code=AvatarSDKErrorCode.protocolError,
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -1,72 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Generated by the protocol buffer compiler. DO NOT EDIT!
3
- # NO CHECKED-IN PROTOBUF GENCODE
4
- # source: driveningress/v2/driveningress.proto
5
- # Protobuf Python Version: 5.29.0
6
- """Generated protocol buffer code."""
7
- from google.protobuf import descriptor as _descriptor
8
- from google.protobuf import descriptor_pool as _descriptor_pool
9
- from google.protobuf import runtime_version as _runtime_version
10
- from google.protobuf import symbol_database as _symbol_database
11
- from google.protobuf.internal import builder as _builder
12
- _runtime_version.ValidateProtobufRuntimeVersion(
13
- _runtime_version.Domain.PUBLIC,
14
- 5,
15
- 29,
16
- 0,
17
- '',
18
- 'driveningress/v2/driveningress.proto'
19
- )
20
- # @@protoc_insertion_point(imports)
21
-
22
- _sym_db = _symbol_database.Default()
23
-
24
-
25
-
26
-
27
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$driveningress/v2/driveningress.proto\x12\x10\x64riveningress.v2\"\xa7\x02\n\x13LiveKitEgressConfig\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x02 \x01(\t\x12\x12\n\napi_secret\x18\x03 \x01(\t\x12\x11\n\troom_name\x18\x04 \x01(\t\x12\x14\n\x0cpublisher_id\x18\x05 \x01(\t\x12T\n\x10\x65xtra_attributes\x18\x06 \x03(\x0b\x32:.driveningress.v2.LiveKitEgressConfig.ExtraAttributesEntry\x12\x14\n\x0cidle_timeout\x18\x07 \x01(\x05\x12\x11\n\tapi_token\x18\x08 \x01(\t\x1a\x36\n\x14\x45xtraAttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"[\n\x11\x41goraEgressConfig\x12\x14\n\x0c\x63hannel_name\x18\x01 \x01(\t\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0b\n\x03uid\x18\x03 \x01(\r\x12\x14\n\x0cpublisher_id\x18\x04 \x01(\t\"\xe7\x02\n\x16\x43lientConfigureSession\x12\x13\n\x0bsample_rate\x18\x01 \x01(\x05\x12\x0f\n\x07\x62itrate\x18\x02 \x01(\x05\x12\x33\n\x0c\x61udio_format\x18\x03 \x01(\x0e\x32\x1d.driveningress.v2.AudioFormat\x12\x45\n\x15transport_compression\x18\x04 \x01(\x0e\x32&.driveningress.v2.TransportCompression\x12\x31\n\x0b\x65gress_type\x18\x05 \x01(\x0e\x32\x1c.driveningress.v2.EgressType\x12=\n\x0elivekit_egress\x18\x06 \x01(\x0b\x32%.driveningress.v2.LiveKitEgressConfig\x12\x39\n\x0c\x61gora_egress\x18\x07 \x01(\x0b\x32#.driveningress.v2.AgoraEgressConfig\"C\n\x14ServerConfirmSession\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x14\n\x0c\x63\x61pabilities\x18\x02 \x03(\t\">\n\x10\x43lientAudioInput\x12\x0e\n\x06req_id\x18\x01 \x01(\t\x12\x0b\n\x03\x65nd\x18\x02 \x01(\x08\x12\r\n\x05\x61udio\x18\x03 \x01(\x0c\"S\n\x0bServerError\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x0e\n\x06req_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63ode\x18\x03 \x01(\x05\x12\x0f\n\x07message\x18\x04 \x01(\t\"s\n\x17ServerResponseAnimation\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x0e\n\x06req_id\x18\x02 \x01(\t\x12\x0b\n\x03\x65nd\x18\x03 \x01(\x08\x12\x11\n\tanimation\x18\x04 \x01(\x0c\x12\x11\n\tavatar_id\x18\x05 \x01(\t\"!\n\x0f\x43lientInterrupt\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\x1d\n\x0b\x43lientPause\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\x1e\n\x0c\x43lientResume\x12\x0e\n\x06req_id\x18\x01 \x01(\t\"\xf0\x01\n\x13ServerPlaybackState\x12\x15\n\rconnection_id\x18\x01 \x01(\t\x12\x0e\n\x06req_id\x18\x02 \x01(\t\x12:\n\x05state\x18\x03 \x01(\x0e\x32+.driveningress.v2.ServerPlaybackState.State\x12\x11\n\tplayed_ms\x18\x04 \x01(\x03\x12\x0e\n\x06reason\x18\x05 \x01(\t\"S\n\x05State\x12\x15\n\x11STATE_UNSPECIFIED\x10\x00\x12\x0b\n\x07PLAYING\x10\x01\x12\n\n\x06PAUSED\x10\x02\x12\t\n\x05\x45NDED\x10\x03\x12\x0f\n\x0bINTERRUPTED\x10\x04\"\x9c\x05\n\x07Message\x12+\n\x04type\x18\x01 \x01(\x0e\x32\x1d.driveningress.v2.MessageType\x12L\n\x18\x63lient_configure_session\x18\x02 \x01(\x0b\x32(.driveningress.v2.ClientConfigureSessionH\x00\x12H\n\x16server_confirm_session\x18\x03 \x01(\x0b\x32&.driveningress.v2.ServerConfirmSessionH\x00\x12@\n\x12\x63lient_audio_input\x18\x04 \x01(\x0b\x32\".driveningress.v2.ClientAudioInputH\x00\x12\x35\n\x0cserver_error\x18\x05 \x01(\x0b\x32\x1d.driveningress.v2.ServerErrorH\x00\x12N\n\x19server_response_animation\x18\x06 \x01(\x0b\x32).driveningress.v2.ServerResponseAnimationH\x00\x12=\n\x10\x63lient_interrupt\x18\x08 \x01(\x0b\x32!.driveningress.v2.ClientInterruptH\x00\x12\x35\n\x0c\x63lient_pause\x18\t \x01(\x0b\x32\x1d.driveningress.v2.ClientPauseH\x00\x12\x37\n\rclient_resume\x18\n \x01(\x0b\x32\x1e.driveningress.v2.ClientResumeH\x00\x12\x46\n\x15server_playback_state\x18\x0b \x01(\x0b\x32%.driveningress.v2.ServerPlaybackStateH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x07\x10\x08*\xcd\x02\n\x0bMessageType\x12\x17\n\x13MESSAGE_UNSPECIFIED\x10\x00\x12$\n MESSAGE_CLIENT_CONFIGURE_SESSION\x10\x01\x12\"\n\x1eMESSAGE_SERVER_CONFIRM_SESSION\x10\x02\x12\x1e\n\x1aMESSAGE_CLIENT_AUDIO_INPUT\x10\x03\x12\x18\n\x14MESSAGE_SERVER_ERROR\x10\x04\x12%\n!MESSAGE_SERVER_RESPONSE_ANIMATION\x10\x05\x12\x1c\n\x18MESSAGE_CLIENT_INTERRUPT\x10\x07\x12\x18\n\x14MESSAGE_CLIENT_PAUSE\x10\x08\x12\x19\n\x15MESSAGE_CLIENT_RESUME\x10\t\x12!\n\x1dMESSAGE_SERVER_PLAYBACK_STATE\x10\n\"\x04\x08\x06\x10\x06*D\n\x0b\x41udioFormat\x12\x1a\n\x16\x41UDIO_FORMAT_PCM_S16LE\x10\x00\x12\x19\n\x15\x41UDIO_FORMAT_OGG_OPUS\x10\x01*6\n\x14TransportCompression\x12\x1e\n\x1aTRANSPORT_COMPRESSION_NONE\x10\x00*Y\n\nEgressType\x12\x1b\n\x17\x45GRESS_TYPE_UNSPECIFIED\x10\x00\x12\x17\n\x13\x45GRESS_TYPE_LIVEKIT\x10\x01\x12\x15\n\x11\x45GRESS_TYPE_AGORA\x10\x02\x62\x06proto3')
28
-
29
- _globals = globals()
30
- _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
- _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'driveningress.v2.driveningress_pb2', _globals)
32
- if not _descriptor._USE_C_DESCRIPTORS:
33
- DESCRIPTOR._loaded_options = None
34
- _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._loaded_options = None
35
- _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._serialized_options = b'8\001'
36
- _globals['_MESSAGETYPE']._serialized_start=2159
37
- _globals['_MESSAGETYPE']._serialized_end=2492
38
- _globals['_AUDIOFORMAT']._serialized_start=2494
39
- _globals['_AUDIOFORMAT']._serialized_end=2562
40
- _globals['_TRANSPORTCOMPRESSION']._serialized_start=2564
41
- _globals['_TRANSPORTCOMPRESSION']._serialized_end=2618
42
- _globals['_EGRESSTYPE']._serialized_start=2620
43
- _globals['_EGRESSTYPE']._serialized_end=2709
44
- _globals['_LIVEKITEGRESSCONFIG']._serialized_start=59
45
- _globals['_LIVEKITEGRESSCONFIG']._serialized_end=354
46
- _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._serialized_start=300
47
- _globals['_LIVEKITEGRESSCONFIG_EXTRAATTRIBUTESENTRY']._serialized_end=354
48
- _globals['_AGORAEGRESSCONFIG']._serialized_start=356
49
- _globals['_AGORAEGRESSCONFIG']._serialized_end=447
50
- _globals['_CLIENTCONFIGURESESSION']._serialized_start=450
51
- _globals['_CLIENTCONFIGURESESSION']._serialized_end=809
52
- _globals['_SERVERCONFIRMSESSION']._serialized_start=811
53
- _globals['_SERVERCONFIRMSESSION']._serialized_end=878
54
- _globals['_CLIENTAUDIOINPUT']._serialized_start=880
55
- _globals['_CLIENTAUDIOINPUT']._serialized_end=942
56
- _globals['_SERVERERROR']._serialized_start=944
57
- _globals['_SERVERERROR']._serialized_end=1027
58
- _globals['_SERVERRESPONSEANIMATION']._serialized_start=1029
59
- _globals['_SERVERRESPONSEANIMATION']._serialized_end=1144
60
- _globals['_CLIENTINTERRUPT']._serialized_start=1146
61
- _globals['_CLIENTINTERRUPT']._serialized_end=1179
62
- _globals['_CLIENTPAUSE']._serialized_start=1181
63
- _globals['_CLIENTPAUSE']._serialized_end=1210
64
- _globals['_CLIENTRESUME']._serialized_start=1212
65
- _globals['_CLIENTRESUME']._serialized_end=1242
66
- _globals['_SERVERPLAYBACKSTATE']._serialized_start=1245
67
- _globals['_SERVERPLAYBACKSTATE']._serialized_end=1485
68
- _globals['_SERVERPLAYBACKSTATE_STATE']._serialized_start=1402
69
- _globals['_SERVERPLAYBACKSTATE_STATE']._serialized_end=1485
70
- _globals['_MESSAGE']._serialized_start=1488
71
- _globals['_MESSAGE']._serialized_end=2156
72
- # @@protoc_insertion_point(module_scope)
@@ -1 +0,0 @@
1
- __version__ = "0.2.3"
File without changes
File without changes
File without changes