Skip to content
4 changes: 2 additions & 2 deletions Formula/liveaudioserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
class Liveaudioserver < Formula
desc "Live audio streaming server (MP3 + AAC + HLS) for macOS"
homepage "https://github.com/dsward2/LiveAudioServer"
url "https://github.com/dsward2/LiveAudioServer/archive/refs/tags/v0.1.2.tar.gz"
sha256 "c5b000fd7965742bab0ea33276b310ce373b1709f5730ef209bdef8f53ccfd2c"
url "https://github.com/dsward2/LiveAudioServer/archive/refs/tags/v0.1.3.tar.gz"
sha256 "ed78fadcbbca56127d660418b4e313b9bbef8c0c6057f7b7500115b923b565c3"
license "Apache-2.0"
head "https://github.com/dsward2/LiveAudioServer.git", branch: "main"

Expand Down
19 changes: 17 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ let package = Package(
platforms: [
.macOS(.v13)
],
products: [
// Public library product so external SwiftPM packages (e.g. a SwiftUI
// host app) can `.package(url: …)` this repo and consume the server
// in-process. The CLI binary continues to exist as a separate
// executable product.
.library(name: "LiveAudioServerCore", targets: ["LiveAudioServerCore"]),
.executable(name: "LiveAudioServer", targets: ["LiveAudioServer"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-testing.git", from: "0.10.0")
],
Expand All @@ -16,15 +24,22 @@ let package = Package(
name: "CLame",
path: "Frameworks/Mp3Lame.xcframework"
),
// Server + encoders + streaming + config. Reusable from a host app.
.target(
name: "LiveAudioServerCore",
dependencies: ["CLame"],
path: "Sources/LiveAudioServerCore"
),
// Thin CLI shim: argument parsing, signal handling, process exit.
.executableTarget(
name: "LiveAudioServer",
dependencies: ["CLame"],
dependencies: ["LiveAudioServerCore"],
path: "Sources/LiveAudioServer"
),
.testTarget(
name: "LiveAudioServerTests",
dependencies: [
"LiveAudioServer",
"LiveAudioServerCore",
.product(name: "Testing", package: "swift-testing")
],
path: "Tests/LiveAudioServerTests"
Expand Down
106 changes: 104 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,108 @@ subsequent run.

---

## Use as a Swift Package

LiveAudioServer also ships as a SwiftPM library product so a host macOS app
(SwiftUI, AppKit, whatever) can start and stop the server in-process without
spawning the CLI. Add the dependency to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/dsward2/LiveAudioServer.git", from: "0.1.3"),
],
targets: [
.target(
name: "YourHostApp",
dependencies: [
.product(name: "LiveAudioServerCore", package: "LiveAudioServer"),
]
),
]
```

Then build a config, install a logger (or skip — the library defaults to
silent), and drive the lifecycle from Swift Concurrency:

```swift
import LiveAudioServerCore

// Optional: see library log lines. Default is a SilentLogger so library
// consumers never get unexpected stderr output.
LiveAudioServerLogging.logger = StderrLogger() // or your own LiveAudioServerLogger

var cfg = LiveAudioServerConfig() // = ServerConfig — CLI-default values
cfg.port = 9000
cfg.bindHost = "127.0.0.1"
cfg.inputSource = .udp(port: 7355) // Gqrx UDP, for example
cfg.fillerMode = .tone
cfg.bonjourName = "My Radio"
// …override any other fields here

let server = LiveAudioServer(config: cfg)
try await server.start()
// /stream.mp3, /stream.m4a, /hls/index.m3u8, and / are live until stop()
// returns. start() throws LiveAudioServerError on synchronous setup failures
// (bind, TLS load, encoder init).

// Later, when the user toggles the off switch:
await server.stop()
```

The same package vends both products, so depending on the library does not
pull in or build the executable.

### Embedding in a host app

If your host app already terminates TLS for its own listeners (for example, a
SwiftUI admin UI), you can hand LiveAudioServer the same pre-loaded
`sec_identity_t` so users only have to trust one self-signed certificate.
`ServerConfig.tlsIdentity` is the embed path; when set, it takes precedence
over `tlsIdentityPath` / `tlsPassword`:

```swift
import LiveAudioServerCore
import Network

// Your host app loads (or generates and persists in App Support) the .p12
// once, then reuses the resulting identity for every listener it owns.
// `loadTLSIdentity` is public so the host doesn't have to reimplement
// PKCS#12 → sec_identity_t.
let identity = try loadTLSIdentity(
p12Path: identityFileURL.path,
password: identityPassword
)

var cfg = LiveAudioServerConfig()
cfg.port = 8080
cfg.tlsPort = 8443
cfg.tlsIdentity = identity // injected — no path/password needed
cfg.inputSource = .udp(port: 7355)

let server = LiveAudioServer(config: cfg)
try await server.start()
```

`loadTLSIdentity(p12Path:password:)` and the `TLSIdentityError` enum it
throws are both public, so host apps that want to load their own `.p12` from
disk can share the same loader the CLI uses.

Notes for integrators:

- Multiple `LiveAudioServer` instances can coexist in one process, but they
share one global logger sink (`LiveAudioServerLogging.logger`).
- `signal(SIGPIPE, SIG_IGN)` is set inside `start()` so a client disconnect
won't kill the host process.
- Encoder lifecycle: do not call `start()` then `stop()` without the server
having processed at least some PCM. `lame_encode_flush` asserts internally
when the session received no audio. In practice this only matters for
unit tests; live PCM always produces frames before shutdown.
- `LiveAudioServerCore` is `import`able from any module that depends on the
library product. `import LiveAudioServer` (the executable target) is not
meant for external consumption.

---

## Build from source

### Requirements
Expand Down Expand Up @@ -789,7 +891,7 @@ paths and version. `path=/` is the conventional Safari Bonjour-bookmark key;
`status=/` is the same path under an explicit name for non-Safari clients:

```
ver=0.1.2
ver=0.1.3
path=/
status=/
mp3=/stream.mp3
Expand All @@ -802,7 +904,7 @@ details, so a LiveAudioServer-aware client can enumerate all streams in a
single Bonjour lookup without hitting `/status.json`:

```
ver=0.1.2
ver=0.1.3
path=/
status=/
rate=48000
Expand Down
Loading
Loading