diff --git a/README.md b/README.md index 0c03b32..75b2c84 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,8 @@ Settings are stored in `~/.config/sendspin/`: | `use_mpris` | boolean | TUI/daemon | Enable MPRIS integration (default: true) | | `hook_start` | string | TUI/daemon | Command to run when audio stream starts | | `hook_stop` | string | TUI/daemon | Command to run when audio stream stops | -| `source` | string | serve | Default audio source (file path or URL) | +| `source` | string | serve | Default audio source (file path or URL, ffmpeg input) | +| `source_format` | string | serve | ffmpeg container format for audio source | | `clients` | array | serve | Client URLs to connect to (`--client`) | Settings are automatically saved when changed through the TUI. You can also edit the JSON file directly while the client is not running. diff --git a/sendspin/cli.py b/sendspin/cli.py index 883785d..1b2d069 100644 --- a/sendspin/cli.py +++ b/sendspin/cli.py @@ -75,6 +75,11 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: default=None, help="Audio source: local file path or URL (http/https)", ) + serve_parser.add_argument( + "--source-format", + default=None, + help="ffmpeg container format for source audio", + ) serve_parser.add_argument( "--demo", action="store_true", @@ -392,6 +397,7 @@ async def _run_serve_mode(args: argparse.Namespace) -> int: serve_config = ServeConfig( source=source, + source_format=args.source_format or settings.source_format, port=args.port, name=args.name, clients=args.clients or settings.clients, diff --git a/sendspin/serve/__init__.py b/sendspin/serve/__init__.py index be9d36f..e1c076d 100644 --- a/sendspin/serve/__init__.py +++ b/sendspin/serve/__init__.py @@ -63,6 +63,7 @@ class ServeConfig: """Configuration for the serve command.""" source: str + source_format: str | None = None port: int = 8928 name: str = "Sendspin Server" clients: list[str] | None = None @@ -220,7 +221,7 @@ def on_server_event(server: SendspinServer, event: SendspinEvent) -> None: # Decode and stream audio try: - audio_source = await decode_audio(config.source) + audio_source = await decode_audio(config.source, source_format=config.source_format) media_stream = MediaStream( main_channel_source=audio_source.generator, main_channel_format=audio_source.format, diff --git a/sendspin/serve/source.py b/sendspin/serve/source.py index 1d46866..1a5c9e4 100644 --- a/sendspin/serve/source.py +++ b/sendspin/serve/source.py @@ -50,6 +50,7 @@ async def decode_audio( *, target_sample_rate: int = 48000, target_channels: int = 2, + source_format: str | None = None, ) -> AudioSource: """ Decode an audio source (file path or URL) to PCM. @@ -78,7 +79,7 @@ async def pcm_generator() -> AsyncGenerator[bytes, None]: if container is not None: container.close() - container = av.open(source) + container = av.open(format=source_format, file=source) resampler = av.AudioResampler(format="s16", layout=layout, rate=target_sample_rate) for frame in container.decode(container.streams.audio[0]): for resampled in resampler.resample(frame): diff --git a/sendspin/settings.py b/sendspin/settings.py index fd739e6..3bd7643 100644 --- a/sendspin/settings.py +++ b/sendspin/settings.py @@ -201,6 +201,7 @@ class ServeSettings(BaseSettings): """Settings for serve mode.""" source: str | None = None + source_format: str | None = None clients: list[str] | None = None def update( @@ -210,6 +211,7 @@ def update( log_level: str | None = None, listen_port: int | None = None, source: str | None = None, + source_format: str | None = None, clients: list[str] | None = None, ) -> None: """Update settings fields. Only changed fields trigger a save.""" @@ -219,6 +221,7 @@ def update( "log_level": log_level, "listen_port": listen_port, "source": source, + "source_format": source_format, "clients": clients, } ) @@ -238,6 +241,7 @@ def _load(self) -> None: self.log_level = data.get("log_level") self.listen_port = data.get("listen_port") self.source = data.get("source") + self.source_format = data.get("source_format") self.clients = data.get("clients") logger.info("Loaded settings from %s", self._settings_file) except (json.JSONDecodeError, OSError) as e: