### Quick Start: Build and start a RaopServer Source: https://docs.rs/shairplay/latest/index.html Shows how to implement the AudioHandler and AudioSession traits, build a RaopServer with a name, hardware address, and port, then start it. The server becomes discoverable via AirPlay on the local network. The audio_process method receives F32 interleaved PCM samples, same format for AP1 and AP2. ```rust use std::sync::Arc; use shairplay::{RaopServer, AudioHandler, AudioSession, AudioFormat}; struct MyHandler; impl AudioHandler for MyHandler { fn audio_init(&self, format: AudioFormat) -> Box { Box::new(MySession) } } struct MySession; impl AudioSession for MySession { fn audio_process(&mut self, samples: &[f32]) { // F32 interleaved PCM — same format for AP1 and AP2 } } let handler = Arc::new(MyHandler); let mut server = RaopServer::builder() .name("My Speaker") .hwaddr([0x48, 0x5d, 0x60, 0x7c, 0xee, 0x22]) .port(5000) .build(handler)?; server.start().await?; // Server is now discoverable via AirPlay on the local network ``` -------------------------------- ### RaopServer::start() Source: https://docs.rs/shairplay/latest/shairplay/index.html Starts the AirPlay server, making it discoverable on the local network. ```APIDOC ## RaopServer::start() ### Description Starts the AirPlay server, making it discoverable on the local network. ### Method start ### Parameters None ### Returns Result<(), ShairplayError> ### Example ```rust server.start().await?; ``` ``` -------------------------------- ### RaopServer::start() Source: https://docs.rs/shairplay Starts the AirPlay server, making it discoverable on the local network. ```APIDOC ## RaopServer::start() ### Description Starts the AirPlay server, making it discoverable on the local network. ### Method start(&mut self) -> Result<(), ShairplayError> ### Parameters None ### Returns Result<(), ShairplayError> ``` -------------------------------- ### method: get HTTP method Source: https://docs.rs/shairplay/latest/src/shairplay/proto/http.rs.html Returns the HTTP method (GET, POST, SETUP, etc.) as an Option<&str>. ```rust pub fn method(&self) -> Option<&str> { self.method.as_deref() } ``` -------------------------------- ### BindConfig usage example (Rust) Source: https://docs.rs/shairplay/latest/src/shairplay/net/server.rs.html Shows how to construct BindConfig to bind to all interfaces (default), a specific IPv4 address, specific IPv4 and IPv6 addresses, and a specific port. The example is from the doc comment on BindConfig. ```rust use shairplay::BindConfig; use std::net::IpAddr; // Bind to all interfaces (default) let config = BindConfig::default(); // Bind to a specific IPv4 address let config = BindConfig::new().addrs(["192.168.1.100".parse().unwrap()]); // Bind to specific IPv4 + IPv6 let config = BindConfig::new() .addrs(["192.168.1.100".parse().unwrap(), "fd00::1".parse().unwrap()]); // Bind to a specific port let config = BindConfig::new().port(7000); ``` -------------------------------- ### SETUP routing: AP1 vs AP2 handler selection Source: https://docs.rs/shairplay/latest/src/shairplay/raop/rtsp.rs.html Selects the SETUP handler based on whether the connection is AP2 or the request body starts with 'bplist'. When the 'ap2' feature is enabled, it returns the AP2 handler; otherwise, it falls back to the standard handler. ```rust fn resolve_setup(conn: &RaopConnection, request: &HttpRequest) -> Option { #[cfg(feature = "ap2")] { let is_plist = request .data() .map(|d| d.starts_with(b"bplist")) .unwrap_or(false); if conn.is_ap2 || is_plist { return Some(handlers_ap2::handle_setup); } } let _ = (conn, request); // suppress unused warnings without ap2 Some(handlers::handle_setup) } ``` -------------------------------- ### RaopServer::start Source: https://docs.rs/shairplay/latest/shairplay/struct.RaopServer.html Starts the server: binds ports, registers mDNS services, and begins accepting connections. mDNS registration is skipped when the CI environment variable is set. ```APIDOC ## RaopServer::start ### Description Starts the server: bind ports, register mDNS services, begin accepting connections. mDNS registration is skipped when the `CI` environment variable is set (Bonjour/Avahi is typically unavailable on CI runners). ### Method `pub async fn start(&mut self) -> Result<(), ShairplayError>` ### Parameters - `self` (`&mut RaopServer`) - The server instance to start. ### Returns - `Result<(), ShairplayError>` - `Ok(())` on success, or an error if the server fails to start. ``` -------------------------------- ### FairPlay setup method (M1 mode selection) Source: https://docs.rs/shairplay/latest/src/shairplay/crypto/fairplay.rs.html Processes the M1 request, validates version and mode, resets key message length, and returns the pre-computed 142-byte reply for the selected mode. ```rust /// Process M1 (mode selection). Returns the 142-byte pre-computed reply. pub fn setup(&mut self, request: &[u8; 16]) -> Result<[u8; 142], CryptoError> { if request[4] != 0x03 { return Err(CryptoError::FairPlay("unsupported version".into())); } let mode = request[14] as usize; if mode >= 4 { return Err(CryptoError::FairPlay("invalid mode".into())); } self.keymsglen = 0; Ok(REPLY_MESSAGE[mode]) } ``` -------------------------------- ### Handle AP1 RTSP SETUP Source: https://docs.rs/shairplay/latest/src/shairplay/raop/handlers_ap1.rs.html Parses the Transport header to configure UDP/TCP ports and initializes the RTP session for audio streaming. ```rust /// AP1 RTSP SETUP: bind RTP ports and start audio receiver. pub(crate) fn handle_setup( conn: &mut RaopConnection, request: &HttpRequest, response: &mut HttpResponse, ) -> Option> { let transport = request.header("Transport")?; tracing::debug!(transport, "AP1 SETUP"); // Check for DACP remote control headers if let (Some(dacp_id), Some(active_remote)) = (request.header("DACP-ID"), request.header("Active-Remote")) { let addr_bytes = crate::raop::rtp::remote_addr_bytes(&conn.remote_socket.ip().to_string()); let remote = std::sync::Arc::new(crate::raop::DacpRemoteControl::new( dacp_id, active_remote, &addr_bytes, )); conn.shared.handler.on_remote_control(remote); } let use_udp = !transport.starts_with("RTP/AVP/TCP"); let mut remote_cport = 0u16; let mut remote_tport = 0u16; if use_udp { for part in transport.split(';') { if let Some(val) = part.strip_prefix("control_port=") { remote_cport = val.parse().unwrap_or(0); } else if let Some(val) = part.strip_prefix("timing_port=") { remote_tport = val.parse().unwrap_or(0); } } } if let Some(rtp) = &mut conn.raop_rtp { let (cport, tport, dport) = match rtp.start(use_udp, remote_cport, remote_tport) { Ok(ports) => ports, Err(e) => { tracing::warn!("AP1 SETUP rtp.start failed: {e}"); conn.shared.handler.on_error(&e); return None; } }; let transport_resp = if use_udp { format!( "RTP/AVP/UDP;unicast;mode=record;timing_port={tport};events;control_port={cport};server_port={dport}" ) } else { ``` -------------------------------- ### RaopServer::service_info Source: https://docs.rs/shairplay/latest/shairplay/struct.RaopServer.html Gets the mDNS service info for this server. ```APIDOC ## RaopServer::service_info ### Description Get the mDNS service info for this server. ### Method `pub fn service_info(&self) -> AirPlayServiceInfo` ### Parameters - `self` (`&RaopServer`) - The server instance. ### Returns - `AirPlayServiceInfo` - The mDNS service information for the server. ``` -------------------------------- ### Create fp-setup HttpRequest Source: https://docs.rs/shairplay/latest/src/shairplay/raop/handlers_ap1.rs.html Constructs an RTSP fp-setup request with the provided body data. ```rust fn fp_setup_request(body: &[u8]) -> HttpRequest { let mut msg = format!( "POST /fp-setup RTSP/1.0\r\nCSeq: 1\r\nContent-Length: {}\r\n\r\n", body.len() ) .into_bytes(); msg.extend_from_slice(body); let mut req = HttpRequest::new(); req.add_data(&msg).unwrap(); assert_eq!( req.data().map(|d| d.len()), Some(body.len()), "body should parse" ); req } ``` -------------------------------- ### RaopServerBuilder::new Source: https://docs.rs/shairplay/latest/shairplay/struct.RaopServerBuilder.html Creates a new RaopServerBuilder instance with default settings. ```APIDOC ## RaopServerBuilder::new ### Description Creates a new builder with default settings. ### Method new ### Signature pub fn new() -> Self ### Parameters None ### Returns - **Self** - A new RaopServerBuilder instance with default settings. ``` -------------------------------- ### data: get request body Source: https://docs.rs/shairplay/latest/src/shairplay/proto/http.rs.html Returns the request body, if present. ```rust pub fn data(&self) -> Option<&[u8]> { self.body.as_deref() } ``` -------------------------------- ### RaopServerBuilder::build() Source: https://docs.rs/shairplay Builds the RaopServer with the configured settings and the provided audio handler. ```APIDOC ## RaopServerBuilder::build() ### Description Builds the RaopServer with the configured settings and the provided audio handler. ### Method build(handler: Arc) -> Result ### Parameters - **handler** (Arc) - Required - The audio handler that will receive AirPlay events and create audio sessions. ### Returns Result ``` -------------------------------- ### url: get request URL Source: https://docs.rs/shairplay/latest/src/shairplay/proto/http.rs.html Returns the request URL/path as an Option<&str>. ```rust pub fn url(&self) -> Option<&str> { self.url.as_deref() } ``` -------------------------------- ### Initialize RAOP Audio Session and NTP Responder Source: https://docs.rs/shairplay/latest/src/shairplay/raop/rtp.rs.html Sets up the local ports, spawns the NTP timing responder, and initializes the audio session with the appropriate codec and sample rate. ```rust self.timing_lport = tsock.local_addr().map_err(NetworkError::Io)?.port(); self.data_lport = dsock.local_addr().map_err(NetworkError::Io)?.port(); // Spawn NTP timing responder for this connection. let remote_sockaddr = self.remote_socket; let mut timing_addr = remote_sockaddr; timing_addr.set_port(timing_rport); super::ntp::spawn_ntp_responder(tsock, timing_addr); let format = self.format; let mut session = self.handler.audio_init(AudioFormat { codec: AudioCodec::Pcm, bits: 32, channels: format.num_channels, // When resampling is enabled the audio delivered downstream is at // the output rate, so announce that (matching the non-UDP path); // fall back to the stream's native rate when not resampling. sample_rate: self.output_sample_rate.unwrap_or(format.sample_rate), }); ``` -------------------------------- ### pipewire_auth_setup_compat Source: https://docs.rs/shairplay/latest/src/shairplay/raop/server.rs.html Enables experimental classic-RAOP compatibility for PipeWire interoperability. This is gated by the 'pipewire-auth-setup-compat' feature and requires AirPlayMode::AirPlay1 when the 'ap2' feature is compiled in. ```APIDOC ## pipewire_auth_setup_compat ### Description Enables experimental classic-RAOP compatibility for PipeWire interoperability. This is not MFi receiver authentication or audio encryption. Discovery and HTTP Digest requirements stay unchanged. Password-protected PipeWire interoperability is not established. ### Method Builder method ### Signature pub fn pipewire_auth_setup_compat(mut self, enabled: bool) -> Self ### Parameters - **enabled** (bool) - Required - Whether to enable the compatibility mode. ### Notes - Requires the 'pipewire-auth-setup-compat' feature. - When 'ap2' is compiled in, requires AirPlayMode::AirPlay1. - Already AP2-paired connections remain excluded. ``` -------------------------------- ### header: case-insensitive header lookup Source: https://docs.rs/shairplay/latest/src/shairplay/proto/http.rs.html Gets a header value by name. Header lookup is ASCII case-insensitive. ```rust pub fn header(&self, name: &str) -> Option<&str> { self.headers .get(&name.to_ascii_lowercase()) .map(|s| s.as_str()) } ``` -------------------------------- ### RaopServerBuilder::build() Source: https://docs.rs/shairplay/latest/shairplay/index.html Builds the RaopServer with the provided audio handler. ```APIDOC ## RaopServerBuilder::build() ### Description Builds the RaopServer with the provided audio handler. ### Method build ### Parameters - **handler** (Arc) - Required - The audio handler that receives AirPlay events and creates audio sessions. ### Returns Result ### Example ```rust let handler = Arc::new(MyHandler); let server = RaopServer::builder() .name("My Speaker") .hwaddr([0x48, 0x5d, 0x60, 0x7c, 0xee, 0x22]) .port(5000) .build(handler)?; ``` ``` -------------------------------- ### FP_HEADER constant Source: https://docs.rs/shairplay/latest/src/shairplay/crypto/fairplay.rs.html A 12-byte constant header used in FairPlay responses, starting with 'FPLY' and ending with 0x14. ```rust const FP_HEADER: [u8; 12] = [ 0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x14, ]; ``` -------------------------------- ### Decrypt message vector Source: https://docs.rs/shairplay/latest/src/shairplay/crypto/fairplay.rs.html Verifies the decryption of a message vector by checking the hex representation of the start and end of the decrypted buffer. ```rust #[test] fn decrypt_message_vector() { let mut msg = [0x41u8; 164]; msg[0] = 0x46; msg[1] = 0x50; msg[2] = 0x4c; msg[3] = 0x49; msg[4] = 0x03; msg[12] = 0x01; let mut dec = [0u8; 128]; decrypt_message(&msg, &mut dec); assert_eq!(to_hex(&dec[..16]), "efbf616443ab486b700a3f743df20ddd"); assert_eq!(to_hex(&dec[112..128]), "71853546f2ef515d63e27a37c510de09"); } ``` -------------------------------- ### Test fp-setup success Source: https://docs.rs/shairplay/latest/src/shairplay/raop/handlers_ap1.rs.html Verifies that a well-formed fp-setup M1 request does not trigger an error and produces a valid reply. ```rust #[test] fn fp_setup_success_does_not_notify() { let handler = Arc::new(RecordingHandler::default()); let mut conn = test_connection(handler.clone()); // Valid M1: byte[4] = 0x03 (version), byte[14] = 0 (mode). let mut body = [0u8; 16]; body[4] = 0x03; let req = fp_setup_request(&body); let mut resp = HttpResponse::new("RTSP/1.0", 200, "OK"); let out = handle_fp_setup(&mut conn, &req, &mut resp); assert!(out.is_some(), "valid fp-setup M1 should produce a reply"); assert!( handler.errors.lock().unwrap().is_empty(), "no error expected on success" ); } ``` -------------------------------- ### RaopServer::builder() Source: https://docs.rs/shairplay/latest/shairplay/index.html Creates a new RaopServerBuilder instance to configure and build an AirPlay server. ```APIDOC ## RaopServer::builder() ### Description Creates a new RaopServerBuilder instance to configure and build an AirPlay server. ### Method builder ### Parameters None ### Returns RaopServerBuilder ### Example ```rust let mut server = RaopServer::builder() .name("My Speaker") .hwaddr([0x48, 0x5d, 0x60, 0x7c, 0xee, 0x22]) .port(5000) .build(handler)?; ``` ``` -------------------------------- ### sap_hash function (partial) Source: https://docs.rs/shairplay/latest/src/shairplay/crypto/fairplay.rs.html Starts the SAP hash function by converting the input block into 32-bit little-endian words. The snippet is incomplete as shown in the source. ```rust fn sap_hash(block_in: &[u8], key_out: &mut [u8; 16]) { let block_words: Vec = (0..16) ``` -------------------------------- ### handle_record: Acknowledge RTP streaming start Source: https://docs.rs/shairplay/latest/src/shairplay/raop/handlers_ap1.rs.html Handles the RTSP RECORD request by providing an Audio-Latency header, mirroring AP2 behavior for classic RAOP clients. ```rust pub(crate) fn handle_record( _conn: &mut RaopConnection, _request: &HttpRequest, response: &mut HttpResponse, ) -> Option> { // Mirror the AP2 RECORD response: classic RAOP clients expect an // Audio-Latency header. The value is a placeholder — actual playback // timing is handled by the RTP layer, not advertised here. response.add_header("Audio-Latency", "0"); None } ``` -------------------------------- ### FairPlay DRM handshake handler (fp-setup) Source: https://docs.rs/shairplay/latest/src/shairplay/raop/handlers_ap1.rs.html Handles fp-setup requests for FairPlay DRM. Expects data length 16 (M1) or 164 (M2). For M1, calls conn.fairplay.setup(req); for M2, calls conn.fairplay.handshake(req). On error, logs a warning and invokes the error handler. ```rust pub(crate) fn handle_fp_setup( conn: &mut RaopConnection, request: &HttpRequest, _response: &mut HttpResponse, ) -> Option> { let data = request.data()?; tracing::debug!(data_len = data.len(), "fp-setup"); match data.len() { 16 => { let req: &[u8; 16] = data.try_into().ok()?; let res = match conn.fairplay.setup(req) { Ok(r) => r, Err(e) => { tracing::warn!("fp-setup M1 failed: {e}"); conn.shared.handler.on_error(&ShairplayError::Crypto(e)); return None; } }; Some(res.to_vec()) } 164 => { let req: &[u8; 164] = data.try_into().ok()?; let res = match conn.fairplay.handshake(req) { Ok(r) => r, Err(e) => { tracing::warn!("fp-setup M2 failed: {e}"); conn.shared.handler.on_error(&ShairplayError::Crypto(e)); return None; } }; Some(res.to_vec()) } _ => None, } } ``` -------------------------------- ### RaopServer::builder() Source: https://docs.rs/shairplay Creates a new RaopServerBuilder instance for configuring and building an AirPlay server. ```APIDOC ## RaopServer::builder() ### Description Creates a new RaopServerBuilder instance for configuring and building an AirPlay server. ### Method builder() ### Parameters None ### Returns RaopServerBuilder ``` -------------------------------- ### Server state accessors and stop method Source: https://docs.rs/shairplay/latest/src/shairplay/net/server.rs.html Provides methods to check if the server is running, get the actual port, and stop the server by sending a shutdown signal and setting running to false. ```rust /// Whether the server is currently accepting connections. pub(crate) fn is_running(&self) -> bool { self.running } /// The actual port the server is listening on (after auto-sensing). pub(crate) fn port(&self) -> u16 { self.port } /// Stop the server and close all listeners. pub(crate) async fn stop(&mut self) { if let Some(tx) = self.shutdown_tx.take() { let _ = tx.send(true); } self.running = false; } } ``` -------------------------------- ### handle_pair_setup function Source: https://docs.rs/shairplay/latest/src/shairplay/raop/handlers_ap1.rs.html AP1 pair-setup handler: returns the Ed25519 public key. Requires request data length exactly 32 bytes; otherwise returns None. ```rust pub(crate) fn handle_pair_setup( conn: &mut RaopConnection, request: &HttpRequest, response: &mut HttpResponse, ) -> Option> { let data = request.data()?; if data.len() != 32 { return None; } let public_key = conn.shared.pairing.public_key(); response.add_header("Content-Type", "application/octet-stream"); Some(public_key.to_vec()) } ``` -------------------------------- ### normalize_request_version: rewrite RTSP/1.0 to HTTP/1.1 Source: https://docs.rs/shairplay/latest/src/shairplay/proto/http.rs.html Translates the request-line version from RTSP/1.0 to HTTP/1.1 in-place, because httparse only accepts HTTP. It operates on the first line of the buffer starting at request_line_start, strips a trailing CR, and rewrites the version token. ```rust if let Some(start) = self.request_line_start { let line = self.buffer[start..] .split_mut(|byte| *byte == b'\n') .next() .unwrap(); let end = line.len() - usize::from(line.ends_with(b"\r")); let line = &mut line[..end]; if line.ends_with(b" RTSP/1.0") { let start = line.len() - 8; line[start..start + 4].copy_from_slice(b"HTTP"); } } ``` -------------------------------- ### BindConfig::new Source: https://docs.rs/shairplay/latest/shairplay/struct.BindConfig.html Creates a new default bind configuration. ```APIDOC ## BindConfig::new ### Description Creates a new default bind configuration. ### Method pub fn new() -> Self ### Parameters None ### Returns Self - A new BindConfig instance with default settings. ``` -------------------------------- ### RaopConnectionHandler struct definition with AP2 fields Source: https://docs.rs/shairplay/latest/src/shairplay/raop/connection.rs.html Defines the handler struct holding the RAOP connection, remote address, connection start time, and optional AP2 cipher and pending secret fields gated by the 'ap2' feature. ```rust struct RaopConnectionHandler { conn: handlers::RaopConnection, remote_addr: String, /// Connection-start instant, used to log per-request elapsed time for /// connect-latency diagnostics (AP2 PTP-sync wait vs AP1 fast path). connected_at: std::time::Instant, #[cfg(feature = "ap2")] cipher: Option, #[cfg(feature = "ap2")] pending_secret: Option>, } ``` -------------------------------- ### Precomputed REPLY_MESSAGE array for four FairPlay modes Source: https://docs.rs/shairplay/latest/src/shairplay/crypto/fairplay.rs.html A constant array of four 142-byte precomputed reply messages, one per FairPlay mode. Each reply starts with the 'FPLY' header and contains mode-specific data. Used to respond to M1 requests. ```rust // Precomputed reply messages for the four FairPlay modes. const REPLY_MESSAGE: [[u8; 142]; 4] = [ [ 0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x00, 0x0f, 0x9f, 0x3f, 0x9e, 0x0a, 0x25, 0x21, 0xdb, 0xdf, 0x31, 0x2a, 0xb2, 0xbf, 0xb2, 0x9e, 0x8d, 0x23, 0x2b, 0x63, 0x76, 0xa8, 0xc8, 0x18, 0x70, 0x1d, 0x22, 0xae, 0x93, 0xd8, 0x27, 0x37, 0xfe, 0xaf, 0x9d, 0xb4, 0xfd, 0xf4, 0x1c, 0x2d, 0xba, 0x9d, 0x1f, 0x49, 0xca, 0xaa, 0xbf, 0x65, 0x91, 0xac, 0x1f, 0x7b, 0xc6, 0xf7, 0xe0, 0x66, 0x3d, 0x21, 0xaf, 0xe0, 0x15, 0x65, 0x95, 0x3e, 0xab, 0x81, 0xf4, 0x18, 0xce, 0xed, 0x09, 0x5a, 0xdb, 0x7c, 0x3d, 0x0e, 0x25, 0x49, 0x09, 0xa7, 0x98, 0x31, 0xd4, 0x9c, 0x39, 0x82, 0x97, 0x34, 0x34, 0xfa, 0xcb, 0x42, 0xc6, 0x3a, 0x1c, 0xd9, 0x11, 0xa6, 0xfe, 0x94, 0x1a, 0x8a, 0x6d, 0x4a, 0x74, 0x3b, 0x46, 0xc3, 0xa7, 0x64, 0x9e, 0x44, 0xc7, 0x89, 0x55, 0xe4, 0x9d, 0x81, 0x55, 0x00, 0x95, 0x49, 0xc4, 0xe2, 0xf7, 0xa3, 0xf6, 0xd5, 0xba, ], [ 0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x01, 0xcf, 0x32, 0xa2, 0x57, 0x14, 0xb2, 0x52, 0x4f, 0x8a, 0xa0, 0xad, 0x7a, 0xf1, 0x64, 0xe3, 0x7b, 0xcf, 0x44, 0x24, 0xe2, 0x00, 0x04, 0x7e, 0xfc, 0x0a, 0xd6, 0x7a, 0xfc, 0xd9, 0x5d, 0xed, 0x1c, 0x27, 0x30, 0xbb, 0x59, 0x1b, 0x96, 0x2e, 0xd6, 0x3a, 0x9c, 0x4d, 0xed, 0x88, 0xba, 0x8f, 0xc7, 0x8d, 0xe6, 0x4d, 0x91, 0xcc, 0xfd, 0x5c, 0x7b, 0x56, 0xda, 0x88, 0xe3, 0x1f, 0x5c, 0xce, 0xaf, 0xc7, 0x43, 0x19, 0x95, 0xa0, 0x16, 0x65, 0xa5, 0x4e, 0x19, 0x39, 0xd2, 0x5b, 0x94, 0xdb, 0x64, 0xb9, 0xe4, 0x5d, 0x8d, 0x06, 0x3e, 0x1e, 0x6a, 0xf0, 0x7e, 0x96, 0x56, 0x16, 0x2b, 0x0e, 0xfa, 0x40, 0x42, 0x75, 0xea, 0x5a, 0x44, 0xd9, 0x59, 0x1c, 0x72, 0x56, 0xb9, 0xfb, 0xe6, 0x51, 0x38, 0x98, 0xb8, 0x02, 0x27, 0x72, 0x19, 0x88, 0x57, 0x16, 0x50, 0x94, 0x2a, 0xd9, 0x46, 0x68, 0x8a, ], [ 0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x02, 0xc1, 0x69, 0xa3, 0x52, 0xee, 0xed, 0x35, 0xb1, 0x8c, 0xdd, 0x9c, 0x58, 0xd6, 0x4f, 0x16, 0xc1, 0x51, 0x9a, 0x89, 0xeb, 0x53, 0x17, 0xbd, 0x0d, 0x43, 0x36, 0xcd, 0x68, 0xf6, 0x38, 0xff, 0x9d, 0x01, 0x6a, 0x5b, 0x52, 0xb7, 0xfa, 0x92, 0x16, 0xb2, 0xb6, 0x54, 0x82, 0xc7, 0x84, 0x44, 0x11, 0x81, 0x21, 0xa2, 0xc7, 0xfe, 0xd8, 0x3d, 0xb7, 0x11, 0x9e, 0x91, 0x82, 0xaa, 0xd7, 0xd1, 0x8c, 0x70, 0x63, 0xe2, 0xa4, 0x57, 0x55, 0x59, 0x10, 0xaf, 0x9e, 0x0e, 0xfc, 0x76, 0x34, 0x7d, 0x16, 0x40, 0x43, 0x80, 0x7f, 0x58, 0x1e, 0xe4, 0xfb, 0xe4, 0x2c, 0xa9, 0xde, 0xdc, 0x1b, 0x5e, 0xb2, 0xa3, 0xaa, 0x3d, 0x2e, 0xcd, 0x59, 0xe7, 0xee, 0xe7, 0x0b, 0x36, 0x29, 0xf2, 0x2a, 0xfd, 0x16, 0x1d, 0x87, 0x73, 0x53, 0xdd, 0xb9, 0x9a, 0xdc, 0x8e, 0x07, 0x00, 0x6e, 0x56, 0xf8, 0x50, 0xce, ], [ 0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x03, 0x90, 0x01, 0xe1, 0x72, 0x7e, 0x0f, 0x57, 0xf9, 0xf5, 0x88, 0x0d, 0xb1, 0x04, 0xa6, 0x25, 0x7a, 0x23, 0xf5, 0xcf, 0xff, 0x1a, 0xbb, 0xe1, 0xe9, 0x30, 0x45, 0x25, 0x1a, 0xfb, 0x97, 0xeb, 0x9f, 0xc0, 0x01, 0x1e, 0xbe, 0x0f, 0x3a, 0x81, 0xdf, 0x5b, 0x69, 0x1d, 0x76, 0xac, 0xb2, 0xf7, 0xa5, 0xc7, 0x08, 0xe3, 0xd3, 0x28, 0xf5, 0x6b, 0xb3, 0x9d, 0xbd, 0xe5, 0xf2, 0x9c, 0x8a, 0x17, 0xf4, 0x81, 0x48, 0x7e, 0x3a, 0xe8, 0x63, 0xc6, 0x78, 0x32, 0x54, 0x22, 0xe6, 0xf7, 0x8e, 0x16, 0x6d, 0x18, 0xaa, 0x7f, 0xd6, 0x36, 0x25, 0x8b, 0xce, 0x28, 0x72, 0x6f, 0x66, 0x1f, 0x73, 0x88, 0x93, 0xce, 0x44, 0x31, 0x1e, 0x4b, 0xe6, 0xc0, 0x53, 0x51, 0x93, 0xe5, 0xef, 0x72, 0xe8, 0x68, 0x62, 0x33, 0x72, 0x9c, 0x22, 0x7d, 0x82, 0x0c, 0x99, 0x94, 0x45, 0xd8, 0x92, 0x46, 0xc8, 0xc3, 0x59, ], ]; ``` -------------------------------- ### Build RaopServer with validation and shared state Source: https://docs.rs/shairplay/latest/src/shairplay/raop/server.rs.html Builds the server with the given audio handler. Validates configuration: header diagnostics, max_clients, password length, AP1/AP2 compatibility, and PipeWire auth-setup compatibility. Generates RSA key, pairing, hardware address, and optional AP2 identity seed. Constructs RaopShared with all fields. ```rust pub fn build(self, handler: Arc) -> Result { #[cfg(feature = "diagnostic-headers")] self.header_diagnostics.validate()?; if self.max_clients == 0 { return Err(ServerError::MaxClients(0).into()); } if let Some(password) = self.password.as_ref() && password.len() > super::MAX_PASSWORD_LEN { return Err(ServerError::InvalidPassword(password.len()).into()); } #[cfg(feature = "ap2")] if self.mode == AirPlayMode::AirPlay2 && (self.ap1_codecs.is_some() || self.ap1_encryption.is_some()) { return Err(ServerError::InvalidConfiguration( "AP1 advertisement options require AirPlayMode::AirPlay1", ) .into()); } #[cfg(all(feature = "ap2", feature = "pipewire-auth-setup-compat"))] if self.pipewire_auth_setup_compat && self.mode != AirPlayMode::AirPlay1 { return Err(ServerError::InvalidConfiguration( "PipeWire auth-setup compatibility requires AirPlayMode::AirPlay1", ) .into()); } let ap1_advertisement = Ap1Advertisement::try_new(self.ap1_codecs, self.ap1_encryption)?; let rsakey = airport_rsakey(); let pairing = Arc::new(Pairing::generate()?); let hwaddr = match self.hwaddr { Some(addr) if addr.len() == super::MAX_HWADDR_LEN => addr, Some(addr) => return Err(ServerError::InvalidHwAddr(addr.len()).into()), None => random_hwaddr(), }; #[cfg(feature = "ap2")] let pairing_id = derive_pi_from_hwaddr(&hwaddr); #[cfg(feature = "ap2")] let device_id = crate::util::hwaddr_airplay(&hwaddr); #[cfg(feature = "ap2")] let airplay_name = self.name.clone(); #[cfg(feature = "ap2")] let pairing_store: Arc = self .pairing_store .unwrap_or_else(|| Arc::new(MemoryPairingStore::default())); // Resolve the accessory's long-term identity once: reuse a persisted seed // if the store has one, otherwise generate a random seed and hand it back // for persistence. (A store with no identity persistence — e.g. the default // in-memory one — yields a fresh identity each start; persist it via // `PairingStore::load_identity`/`save_identity` to avoid re-pairing.) #[cfg(feature = "ap2")] let identity_seed = pairing_store.load_identity().unwrap_or_else(|| { let seed = crate::crypto::pairing_homekit::generate_identity_seed(); pairing_store.save_identity(seed); seed }); let shared = Arc::new(RaopShared { rsakey, pairing, hwaddr: hwaddr.clone(), password: self.password.unwrap_or_default(), #[cfg(feature = "pipewire-auth-setup-compat")] pipewire_auth_setup_compat: self.pipewire_auth_setup_compat, handler, #[cfg(feature = "ap2")] pairing_store, #[cfg(feature = "ap2")] identity_seed, output_sample_rate: self.output_sample_rate, output_max_channels: self.output_max_channels, #[cfg(feature = "ap2")] pin: self.pin, #[cfg(feature = "video")] video_handler: self.video_handler, #[cfg(feature = "video")] video_ekey: Arc::new(std::sync::RwLock::new(None)), #[cfg(feature = "video")] video_eiv: Arc::new(std::sync::RwLock::new(None)), #[cfg(feature = "ap2")] pairing_id, #[cfg(feature = "ap2")] device_id, #[cfg(feature = "ap2")] airplay_name, #[cfg(feature = "ap2")] active_audio: std::sync::Mutex::new(None), #[cfg(feature = "hls")] }); ```