pub(crate) struct AudioPlayback {
pub(crate) shared: Arc<PbShared>,
}Expand description
Python-facing mic-playback handle, symmetric to AudioCapture.
Same lifecycle protocol, but a PA playback stream instead of a record stream, and a
bounded drop-oldest queue fed by write / write_red instead of a Python callback.
Python never holds the PA handle — only the playback thread touches it — so a
close-versus-inflight-write use-after-free is structurally impossible here.
Fields§
Implementations§
Source§impl AudioPlayback
impl AudioPlayback
pub(crate) fn inner(&self) -> &Arc<Inner>
Sourcepub(crate) fn decode_packet(
&self,
py: Python<'_>,
data: &Bound<'_, PyAny>,
decode: impl FnOnce(&mut OpusPlaybackDecoder, &[u8], &PlayQueue) + Send,
) -> PyResult<()>
pub(crate) fn decode_packet( &self, py: Python<'_>, data: &Bound<'_, PyAny>, decode: impl FnOnce(&mut OpusPlaybackDecoder, &[u8], &PlayQueue) + Send, ) -> PyResult<()>
Run decode on this run’s Opus decoder with the bytes of a bytes-like data, off
the GIL. The shared body of write and write_red.
Gated on worker_alive: it raises once no playback thread services the queue (start
failure, stop, or a PA outage the session loop could not reconnect through), so the
caller’s reopen-on-error path engages instead of the audio being swallowed silently.
A reconnect in progress stays “alive” and keeps queueing.
A bytes payload is immutable, so it is borrowed in place across the GIL release.
Any other bytes-like object — a C-contiguous buffer-protocol exporter of any item
format, the same set CPython’s own y* argument parsing accepts (memoryview,
bytearray, array, NumPy, an AudioFrame, …) — may be mutated by another Python
thread once the GIL is dropped, so its bytes are copied under the GIL into the
decoder’s reusable scratch buffer first; an Opus packet is a few hundred bytes, and
the steady state allocates nothing.
Source§impl AudioPlayback
impl AudioPlayback
pub(crate) fn new() -> Self
Sourcepub(crate) fn start(
&self,
py: Python<'_>,
settings: &Bound<'_, PyAny>,
) -> PyResult<()>
pub(crate) fn start( &self, py: Python<'_>, settings: &Bound<'_, PyAny>, ) -> PyResult<()>
Start (or restart) mic playback into the virtual sink. The playback mirror of
start_capture.
Same shape as capture: a re-entrant start from the playback thread just undoes a
nested self-stop; the worker is spawned with the GIL released (the stop/clear ordering
lives in spawn_worker); the handle is registered for the atexit sweep; and the ~2 s
await_start handshake raises a FAILED start (with last_error) after tearing
down only the thread THIS call spawned (identity-checked, sparing a concurrent
winner), while a start still in its retry ladder returns Ok and is watched through
state / last_error. Before spawning, it applies this run’s byte bound + frame
alignment to the queue (dropping any stale audio) and creates the Opus decoder up
front, since the mic uplink is always Opus and write / write_red decode packets
to PCM off the GIL for this same run.
Sourcepub(crate) fn write(
&self,
py: Python<'_>,
data: &Bound<'_, PyAny>,
) -> PyResult<()>
pub(crate) fn write( &self, py: Python<'_>, data: &Bound<'_, PyAny>, ) -> PyResult<()>
Push one Opus mic packet for playback. The steady-state hot path.
data is any bytes-like object (bytes, memoryview, bytearray, an
AudioFrame, …); see decode_packet for the liveness gate and how the payload is
borrowed. The decode runs with the GIL released — it touches no Python state, so
dropping the GIL lets it run concurrently with the rest of the app — and a bad packet
is dropped rather than corrupting the stream. It never blocks on PA (drop-oldest
happens inside PlayQueue::push).
Sourcepub(crate) fn write_red(
&self,
py: Python<'_>,
data: &Bound<'_, PyAny>,
primary_ts: i64,
) -> PyResult<()>
pub(crate) fn write_red( &self, py: Python<'_>, data: &Bound<'_, PyAny>, primary_ts: i64, ) -> PyResult<()>
Play one RFC 2198 RED mic frame from the WebRTC/UDP uplink, recovering across any
packet loss on the way in. The lossy-transport counterpart of write.
The payload is de-framed, loss-recovered, and decoded entirely off the GIL by
decode_red_into_queue (see there for why RED exists and why the decode runs off the
GIL). primary_ts is the packet’s monotonic RTP timestamp; the redundant blocks carry
offsets back from it. Accepts any bytes-like data and is gated on worker_alive
exactly like write (see decode_packet).
Sourcepub(crate) fn stop(&self, py: Python<'_>)
pub(crate) fn stop(&self, py: Python<'_>)
Stop mic playback, joining the playback thread.
A re-entrant stop from the playback thread records a self-stop only (it cannot
self-join) and never clobbers an external stop already in effect. Otherwise it joins
with the GIL released so a slow PA disconnect cannot stall the interpreter; stop
returns only once the thread is joined and the sink is released.
Sourcepub(crate) fn is_running(&self) -> bool
pub(crate) fn is_running(&self) -> bool
True while a playback worker is connected and running with no stop pending; false
while still starting and after a failure — see state to tell those apart.
Sourcepub(crate) fn state(&self) -> &'static str
pub(crate) fn state(&self) -> &'static str
Lifecycle phase: "idle", "starting", "running" or "failed", the playback
mirror of AudioCapture.state.
Sourcepub(crate) fn last_error(&self) -> Option<String>
pub(crate) fn last_error(&self) -> Option<String>
Why the last run failed, or None while no run has failed since the last start.
Trait Implementations§
impl DerefToPyAny for AudioPlayback
Source§impl Drop for AudioPlayback
impl Drop for AudioPlayback
impl ExtractPyClassWithClone for AudioPlayback
Source§impl<'py> IntoPyObject<'py> for AudioPlayback
impl<'py> IntoPyObject<'py> for AudioPlayback
Source§type Target = AudioPlayback
type Target = AudioPlayback
Source§type Output = Bound<'py, <AudioPlayback as IntoPyObject<'py>>::Target>
type Output = Bound<'py, <AudioPlayback as IntoPyObject<'py>>::Target>
Source§fn into_pyobject(
self,
py: Python<'py>,
) -> Result<<Self as IntoPyObject<'_>>::Output, <Self as IntoPyObject<'_>>::Error>
fn into_pyobject( self, py: Python<'py>, ) -> Result<<Self as IntoPyObject<'_>>::Output, <Self as IntoPyObject<'_>>::Error>
Source§impl PyClass for AudioPlayback
impl PyClass for AudioPlayback
Source§impl PyClassImpl for AudioPlayback
impl PyClassImpl for AudioPlayback
Source§const MODULE: Option<&str> = ::core::option::Option::None
const MODULE: Option<&str> = ::core::option::Option::None
Source§const IS_BASETYPE: bool = false
const IS_BASETYPE: bool = false
Source§const IS_SUBCLASS: bool = false
const IS_SUBCLASS: bool = false
Source§const IS_MAPPING: bool = false
const IS_MAPPING: bool = false
Source§const IS_SEQUENCE: bool = false
const IS_SEQUENCE: bool = false
Source§const IS_IMMUTABLE_TYPE: bool = false
const IS_IMMUTABLE_TYPE: bool = false
Source§const RAW_DOC: &'static CStr = c"Python-facing mic-playback handle, symmetric to `AudioCapture`.\n\nSame lifecycle protocol, but a PA playback stream instead of a record stream, and a\nbounded drop-oldest queue fed by `write` / `write_red` instead of a Python callback.\nPython never holds the PA handle \xe2\x80\x94 only the playback thread touches it \xe2\x80\x94 so a\nclose-versus-inflight-write use-after-free is structurally impossible here.\x00"
const RAW_DOC: &'static CStr = c"Python-facing mic-playback handle, symmetric to `AudioCapture`.\n\nSame lifecycle protocol, but a PA playback stream instead of a record stream, and a\nbounded drop-oldest queue fed by `write` / `write_red` instead of a Python callback.\nPython never holds the PA handle \xe2\x80\x94 only the playback thread touches it \xe2\x80\x94 so a\nclose-versus-inflight-write use-after-free is structurally impossible here.\x00"
Source§const DOC: &'static CStr
const DOC: &'static CStr
text_signature if a constructor is defined. Read moreSource§type Layout = <<AudioPlayback as PyClassImpl>::BaseNativeType as PyClassBaseType>::Layout<AudioPlayback>
type Layout = <<AudioPlayback as PyClassImpl>::BaseNativeType as PyClassBaseType>::Layout<AudioPlayback>
Source§type ThreadChecker = NoopThreadChecker
type ThreadChecker = NoopThreadChecker
Source§type PyClassMutability = <<PyAny as PyClassBaseType>::PyClassMutability as PyClassMutability>::MutableChild
type PyClassMutability = <<PyAny as PyClassBaseType>::PyClassMutability as PyClassMutability>::MutableChild
Source§type BaseNativeType = PyAny
type BaseNativeType = PyAny
PyAny by default, and when you declare
#[pyclass(extends=PyDict)], it’s PyDict.fn items_iter() -> PyClassItemsIter
fn lazy_type_object() -> &'static LazyTypeObject<Self>
§fn dict_offset() -> Option<PyObjectOffset>
fn dict_offset() -> Option<PyObjectOffset>
§fn weaklist_offset() -> Option<PyObjectOffset>
fn weaklist_offset() -> Option<PyObjectOffset>
Source§impl PyClassNewTextSignature for AudioPlayback
impl PyClassNewTextSignature for AudioPlayback
const TEXT_SIGNATURE: &'static str = "()"
Source§impl PyMethods<AudioPlayback> for PyClassImplCollector<AudioPlayback>
impl PyMethods<AudioPlayback> for PyClassImplCollector<AudioPlayback>
fn py_methods(self) -> &'static PyClassItems
Source§impl PyTypeInfo for AudioPlayback
impl PyTypeInfo for AudioPlayback
Source§const NAME: &str = <Self as ::pyo3::PyClass>::NAME
const NAME: &str = <Self as ::pyo3::PyClass>::NAME
prefer using ::type_object(py).name() to get the correct runtime value
Source§const MODULE: Option<&str> = <Self as ::pyo3::impl_::pyclass::PyClassImpl>::MODULE
const MODULE: Option<&str> = <Self as ::pyo3::impl_::pyclass::PyClassImpl>::MODULE
prefer using ::type_object(py).module() to get the correct runtime value
Source§fn type_object_raw(py: Python<'_>) -> *mut PyTypeObject
fn type_object_raw(py: Python<'_>) -> *mut PyTypeObject
§fn type_object(py: Python<'_>) -> Bound<'_, PyType>
fn type_object(py: Python<'_>) -> Bound<'_, PyType>
§fn is_type_of(object: &Bound<'_, PyAny>) -> bool
fn is_type_of(object: &Bound<'_, PyAny>) -> bool
object is an instance of this type or a subclass of this type.§fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool
fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool
object is an instance of this type.Auto Trait Implementations§
impl Freeze for AudioPlayback
impl RefUnwindSafe for AudioPlayback
impl Send for AudioPlayback
impl Sync for AudioPlayback
impl Unpin for AudioPlayback
impl UnsafeUnpin for AudioPlayback
impl UnwindSafe for AudioPlayback
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<'py, T> IntoPyObjectExt<'py> for Twhere
T: IntoPyObject<'py>,
impl<'py, T> IntoPyObjectExt<'py> for Twhere
T: IntoPyObject<'py>,
§fn into_bound_py_any(self, py: Python<'py>) -> Result<Bound<'py, PyAny>, PyErr>
fn into_bound_py_any(self, py: Python<'py>) -> Result<Bound<'py, PyAny>, PyErr>
self into an owned Python object, dropping type information.§fn into_py_any(self, py: Python<'py>) -> Result<Py<PyAny>, PyErr>
fn into_py_any(self, py: Python<'py>) -> Result<Py<PyAny>, PyErr>
self into an owned Python object, dropping type information and unbinding it
from the 'py lifetime.§fn into_pyobject_or_pyerr(self, py: Python<'py>) -> Result<Self::Output, PyErr>
fn into_pyobject_or_pyerr(self, py: Python<'py>) -> Result<Self::Output, PyErr>
self into a Python object. Read more§impl<T> PyErrArguments for T
impl<T> PyErrArguments for T
§impl<T> PyTypeCheck for Twhere
T: PyTypeInfo,
impl<T> PyTypeCheck for Twhere
T: PyTypeInfo,
§fn type_check(object: &Bound<'_, PyAny>) -> bool
fn type_check(object: &Bound<'_, PyAny>) -> bool
§fn classinfo_object(py: Python<'_>) -> Bound<'_, PyAny>
fn classinfo_object(py: Python<'_>) -> Bound<'_, PyAny>
isinstance and issubclass function. Read more