From 8f0ee3614bcde4a7fc700141e2fbad78404a03e0 Mon Sep 17 00:00:00 2001 From: "Ilya (Marshal)" Date: Mon, 12 Oct 2026 03:19:59 +0200 Subject: [PATCH] Play Janus streams without an audio output, as on servers --- docs/source/examples/janus_streaming.md | 3 ++- examples/README.md | 6 +++--- examples/janus_streaming.py | 27 ++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/source/examples/janus_streaming.md b/docs/source/examples/janus_streaming.md index 363caa8..d38f9b1 100644 --- a/docs/source/examples/janus_streaming.md +++ b/docs/source/examples/janus_streaming.md @@ -1,7 +1,8 @@ # Live stream in the terminal Plays a stream of the public [Janus](https://janus.conf.meetecho.com/) demo server. The video is drawn with colored -characters and the audio plays on your speakers. It needs no account or key: +characters and the audio plays on your speakers. Without speakers, as on a server, it plays the video only. It needs no +account or key: ```bash uv run https://raw.githubusercontent.com/MarshalX/python-webrtc/main/examples/janus_streaming.py diff --git a/examples/README.md b/examples/README.md index e1f93ac..35f89a9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,14 +16,14 @@ without worrying about copyrights. **Voice chat with OpenAI GPT-Live.** Talks to the model through your microphone and speakers, with live transcripts in the terminal. No server: just an API key. Dependencies are declared inline, so [uv](https://docs.astral.sh/uv/) runs it with nothing to install: -``` +```bash OPENAI_API_KEY=sk-... uv run https://raw.githubusercontent.com/MarshalX/python-webrtc/main/examples/openai_live.py ``` ### [janus_streaming.py](janus_streaming.py) -**Watching a live stream in the terminal.** Plays a stream of the public [Janus](https://janus.conf.meetecho.com/) demo server: the video drawn with colored characters, the audio on your speakers. No account or key: +**Watching a live stream in the terminal.** Plays a stream of the public [Janus](https://janus.conf.meetecho.com/) demo server: the video drawn with colored characters, the audio on your speakers, if there are any, so it runs on a server too. No account or key: -``` +```bash uv run https://raw.githubusercontent.com/MarshalX/python-webrtc/main/examples/janus_streaming.py ``` diff --git a/examples/janus_streaming.py b/examples/janus_streaming.py index 20a7a42..925a456 100755 --- a/examples/janus_streaming.py +++ b/examples/janus_streaming.py @@ -15,7 +15,8 @@ uv run janus_streaming.py --id 1 uv run janus_streaming.py --id 3 # stereo music -The video is drawn with colored half blocks, so the terminal needs 24-bit color. Press Ctrl+C to stop. +The video is drawn with colored half blocks, so the terminal needs 24-bit color. Without an audio output, as on +a server, it plays the video only. Press Ctrl+C to stop. """ from __future__ import annotations @@ -31,11 +32,17 @@ from typing import TYPE_CHECKING, TypedDict import httpx -import sounddevice from typing_extensions import NotRequired, Required import webrtc +try: + import sounddevice +except OSError: + PORTAUDIO = False +else: + PORTAUDIO = True + if TYPE_CHECKING: from collections.abc import Generator from types import TracebackType @@ -168,6 +175,17 @@ def close(self) -> None: self.stream.close() +def speakers_found() -> bool: + """Whether audio can be played: servers often lack the PortAudio library or an output device.""" + if not PORTAUDIO: + return False + try: + sounddevice.query_devices(kind='output') + except sounddevice.PortAudioError: + return False + return True + + def draw(rgbx: bytes | bytearray, width: int, height: int, *, slot: int, slots: int) -> None: """Draws a frame in its slot of the screen with ▀, its foreground is the upper pixel, background the lower one.""" total, rows = shutil.get_terminal_size() @@ -289,13 +307,16 @@ async def main(stream_id: int | None) -> None: pc = webrtc.RTCPeerConnection() tasks: list[asyncio.Future[None]] = [] videos: list[webrtc.MediaStreamTrack] = [] + speakers = speakers_found() + if not speakers: + print('No audio output, playing the video only') @pc.on('track') def on_track(event: webrtc.RTCTrackEvent) -> None: if event.track.kind == 'video': videos.append(event.track) tasks.append(asyncio.ensure_future(watch(event.track, videos))) - else: + elif speakers: tasks.append(asyncio.ensure_future(listen(event.track))) async with Janus() as janus: