spatialreal 0.2.3__tar.gz → 0.4.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.4.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,34 @@ await session.interrupt() # interrupts the latest request
64
64
  await session.close()
65
65
  ```
66
66
 
67
+ ### Agora playback defaults
68
+
69
+ `AgoraEgressConfig(channel_name=..., token=..., uid=...)` defaults to
70
+ `source_timeline_only=True` and `BOUNDED_BURST_V1`. Current SpatialReal Web SDK
71
+ viewers detect this profile from the actual media, so no playback flags are
72
+ needed on either side. Audio input formats and sample rates are unchanged.
73
+
74
+ Before sending audio, `start()` requires both `agora_source_timeline_only` and
75
+ `agora_source_burst_v1` in the server's actual confirmation. Missing acceptance
76
+ raises `UnsupportedConfigurationError` (`protocolError`, `websocket_handshake`,
77
+ not retryable) and closes the connection. Policy rejection (`1008`) is also not
78
+ retryable; ordinary transport failures retain their existing retry policy.
79
+ `session.capabilities` is the read-only tuple from the handshake.
80
+
81
+ For older viewers that require native audio, explicitly set
82
+ `source_timeline_only=False`; the omitted profile then resolves to
83
+ `RtcSourcePlaybackProfile.UNSPECIFIED`/0. An explicit `source_playback_profile=0`
84
+ with source-only enabled preserves the earlier source-only contract. `None`
85
+ selects the profile from `source_timeline_only`; booleans and unknown profile
86
+ values are rejected. Burst with source-only disabled is invalid.
87
+
88
+ This changes the default for Agora publishers and requires a compatible server
89
+ and viewer SDK. The publisher handshake cannot discover or upgrade room viewers.
90
+ An incompatible server fails explicitly rather than silently falling back. Use a
91
+ Web SDK build with automatic profile detection, or explicitly configure
92
+ `agora.sourcePlaybackProfile: "bounded-burst-v1"` on Web SDK 1.0.0-beta.6.
93
+ Upgrading the publisher alone does not upgrade existing viewers.
94
+
67
95
  ### Pause / resume (egress mode)
68
96
 
69
97
  When the server declares the `playback_control` capability, the current
@@ -102,6 +130,19 @@ implements the server's WebSocket close-code contract (40xx: don't retry,
102
130
 
103
131
  ## What's new
104
132
 
133
+ - 0.4.0: **Behaviour change** — the Agora publisher default is now source-timeline-only
134
+ playback with the bounded burst profile: a plain `AgoraEgressConfig(channel_name=..., token=..., uid=...)`
135
+ requests `source_timeline_only=True` and `BOUNDED_BURST_V1`. Viewers need a Web SDK with
136
+ automatic profile detection (1.0.0-beta.7+), or must set
137
+ `agora.sourcePlaybackProfile: 'bounded-burst-v1'` explicitly on 1.0.0-beta.6. Pass
138
+ `source_timeline_only=False` for compatibility mode (older/native-audio viewers); the
139
+ omitted profile then resolves to zero. Server capability checks remain mandatory.
140
+ - 0.3.0: Agora source playback negotiation. `AgoraEgressConfig(source_timeline_only=True)`
141
+ requests source-timeline-only playback; `source_playback_profile=RtcSourcePlaybackProfile.BOUNDED_BURST_V1`
142
+ requests the bounded source playback profile. `start()` refuses the session
143
+ (`UnsupportedConfigurationError`, not retryable) before any audio is sent when the
144
+ server does not confirm the matching capabilities (`agora_source_timeline_only`,
145
+ `agora_source_burst_v1`). Defaults are unchanged.
105
146
  - 0.2.3: `app_id` is optional (the API key identifies the app); protobuf floor lowered
106
147
  to 5.29 so the SDK installs next to packages that pin protobuf 5.x (TEN Framework agents).
107
148
  - `on_playback(PlaybackSignal)` — structured playback lifecycle; no protobuf
@@ -31,6 +31,34 @@ await session.interrupt() # interrupts the latest request
31
31
  await session.close()
32
32
  ```
33
33
 
34
+ ### Agora playback defaults
35
+
36
+ `AgoraEgressConfig(channel_name=..., token=..., uid=...)` defaults to
37
+ `source_timeline_only=True` and `BOUNDED_BURST_V1`. Current SpatialReal Web SDK
38
+ viewers detect this profile from the actual media, so no playback flags are
39
+ needed on either side. Audio input formats and sample rates are unchanged.
40
+
41
+ Before sending audio, `start()` requires both `agora_source_timeline_only` and
42
+ `agora_source_burst_v1` in the server's actual confirmation. Missing acceptance
43
+ raises `UnsupportedConfigurationError` (`protocolError`, `websocket_handshake`,
44
+ not retryable) and closes the connection. Policy rejection (`1008`) is also not
45
+ retryable; ordinary transport failures retain their existing retry policy.
46
+ `session.capabilities` is the read-only tuple from the handshake.
47
+
48
+ For older viewers that require native audio, explicitly set
49
+ `source_timeline_only=False`; the omitted profile then resolves to
50
+ `RtcSourcePlaybackProfile.UNSPECIFIED`/0. An explicit `source_playback_profile=0`
51
+ with source-only enabled preserves the earlier source-only contract. `None`
52
+ selects the profile from `source_timeline_only`; booleans and unknown profile
53
+ values are rejected. Burst with source-only disabled is invalid.
54
+
55
+ This changes the default for Agora publishers and requires a compatible server
56
+ and viewer SDK. The publisher handshake cannot discover or upgrade room viewers.
57
+ An incompatible server fails explicitly rather than silently falling back. Use a
58
+ Web SDK build with automatic profile detection, or explicitly configure
59
+ `agora.sourcePlaybackProfile: "bounded-burst-v1"` on Web SDK 1.0.0-beta.6.
60
+ Upgrading the publisher alone does not upgrade existing viewers.
61
+
34
62
  ### Pause / resume (egress mode)
35
63
 
36
64
  When the server declares the `playback_control` capability, the current
@@ -69,6 +97,19 @@ implements the server's WebSocket close-code contract (40xx: don't retry,
69
97
 
70
98
  ## What's new
71
99
 
100
+ - 0.4.0: **Behaviour change** — the Agora publisher default is now source-timeline-only
101
+ playback with the bounded burst profile: a plain `AgoraEgressConfig(channel_name=..., token=..., uid=...)`
102
+ requests `source_timeline_only=True` and `BOUNDED_BURST_V1`. Viewers need a Web SDK with
103
+ automatic profile detection (1.0.0-beta.7+), or must set
104
+ `agora.sourcePlaybackProfile: 'bounded-burst-v1'` explicitly on 1.0.0-beta.6. Pass
105
+ `source_timeline_only=False` for compatibility mode (older/native-audio viewers); the
106
+ omitted profile then resolves to zero. Server capability checks remain mandatory.
107
+ - 0.3.0: Agora source playback negotiation. `AgoraEgressConfig(source_timeline_only=True)`
108
+ requests source-timeline-only playback; `source_playback_profile=RtcSourcePlaybackProfile.BOUNDED_BURST_V1`
109
+ requests the bounded source playback profile. `start()` refuses the session
110
+ (`UnsupportedConfigurationError`, not retryable) before any audio is sent when the
111
+ server does not confirm the matching capabilities (`agora_source_timeline_only`,
112
+ `agora_source_burst_v1`). Defaults are unchanged.
72
113
  - 0.2.3: `app_id` is optional (the API key identifies the app); protobuf floor lowered
73
114
  to 5.29 so the SDK installs next to packages that pin protobuf 5.x (TEN Framework agents).
74
115
  - `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
+ """Source playback contract; current Web SDK viewers detect it automatically."""
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,30 @@ class AgoraEgressConfig:
52
60
  token: str = field(default="", repr=False)
53
61
  uid: int = 0
54
62
  publisher_id: str = ""
63
+ # Current viewers play source-timeline audio and manage entry locally.
64
+ # The server must explicitly acknowledge both capabilities before start.
65
+ # Set source_timeline_only=False for legacy/native-audio viewers.
66
+ source_timeline_only: bool = True
67
+ # None selects burst with source-only, otherwise the compatibility profile.
68
+ source_playback_profile: RtcSourcePlaybackProfile | None = None
69
+
70
+ def __post_init__(self) -> None:
71
+ if not isinstance(self.source_timeline_only, bool):
72
+ raise ValueError("source_timeline_only must be a boolean")
73
+ if self.source_playback_profile is None:
74
+ self.source_playback_profile = (
75
+ RtcSourcePlaybackProfile.BOUNDED_BURST_V1
76
+ if self.source_timeline_only
77
+ else RtcSourcePlaybackProfile.UNSPECIFIED
78
+ )
79
+ if isinstance(self.source_playback_profile, bool) or not isinstance(self.source_playback_profile, int):
80
+ raise ValueError("source_playback_profile must be a known RtcSourcePlaybackProfile")
81
+ try:
82
+ self.source_playback_profile = RtcSourcePlaybackProfile(self.source_playback_profile)
83
+ except ValueError as exc:
84
+ raise ValueError("source_playback_profile must be a known RtcSourcePlaybackProfile") from exc
85
+ if self.source_playback_profile and not self.source_timeline_only:
86
+ raise ValueError("source_playback_profile requires source_timeline_only=True")
55
87
 
56
88
 
57
89
  @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.4.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