### Install from source using pip
Source: https://cyndilib.readthedocs.io/en/latest/overview.html
Install the project in the current directory using pip.
```bash
pip install .
```
--------------------------------
### Install cyndilib via pip
Source: https://cyndilib.readthedocs.io/en/latest/overview.html
Standard installation method using pre-built wheels from PyPI.
```bash
pip install cyndilib
```
--------------------------------
### Build documentation
Source: https://cyndilib.readthedocs.io/en/latest/_sources/development.rst.txt
Commands to install documentation dependencies and generate the HTML documentation.
```bash
pip install -r doc/requirements.txt
```
```bash
python setup.py build_ext --inplace -j 12
cd doc
make html
```
--------------------------------
### Install dependencies
Source: https://cyndilib.readthedocs.io/en/latest/_sources/development.rst.txt
Commands to install project dependencies using either uv or pip.
```bash
uv sync
```
```bash
pip install setuptools cython numpy
pip install -e .
```
--------------------------------
### Install test dependencies
Source: https://cyndilib.readthedocs.io/en/latest/_sources/development.rst.txt
Installs the necessary packages for running the test suite.
```bash
pip install pytest pytest-doctestplus psutil
```
--------------------------------
### Router Example
Source: https://cyndilib.readthedocs.io/en/latest/_sources/examples.rst.txt
Demonstrates creating virtual NDI sources on the network using the cyndilib.router module and a RoutingMatrix.
```python
.. literalinclude:: ../../examples/router.py
:language: python
:linenos:
:name: router
:caption: :github_permalink:`examples/router.py`
```
--------------------------------
### FFmpeg Sender Example
Source: https://cyndilib.readthedocs.io/en/latest/_sources/examples.rst.txt
Sends video frames generated by an ffmpeg subprocess and audio samples generated by numpy. Requires ffmpeg installed on the system.
```python
.. literalinclude:: ../../examples/ffmpeg_sender.py
:language: python
:linenos:
:name: ffmpeg_sender
:caption: :github_permalink:`examples/ffmpeg_sender.py`
```
--------------------------------
### Install dependencies using uv
Source: https://cyndilib.readthedocs.io/en/latest/overview.html
Use the uv package manager to sync project dependencies.
```bash
uv sync
```
--------------------------------
### Example NDI Metadata XML
Source: https://cyndilib.readthedocs.io/en/latest/reference/metadata_frame.html
Example XML structures for NDI tally and product information.
```xml
```
--------------------------------
### PTZ Control Example
Source: https://cyndilib.readthedocs.io/en/latest/_sources/examples.rst.txt
Showcases how to invoke PTZ control methods on an NDI receiver.
```python
.. literalinclude:: ../../examples/ptz.py
:language: python
:linenos:
:name: ptz
:caption: :github_permalink:`examples/ptz.py`
```
--------------------------------
### Audio Player Example
Source: https://cyndilib.readthedocs.io/en/latest/_sources/examples.rst.txt
Receives audio frames from an NDI source and plays them using the sounddevice library.
```python
.. literalinclude:: ../../examples/audio_player.py
:language: python
:linenos:
:name: audio_player
:caption: :github_permalink:`examples/audio_player.py`
```
--------------------------------
### NDI Sender Implementation with ffmpeg
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
This example demonstrates creating an NDI sender that processes video frames from an ffmpeg subprocess and generates audio samples using numpy. It requires a UNIX-like environment for pipe support.
```python
1from __future__ import annotations
2
3from typing import NamedTuple, Literal, Generator, Any, cast, get_args
4from typing_extensions import Self
5import enum
6import io
7import subprocess
8import shlex
9from fractions import Fraction
10from contextlib import contextmanager
11
12import numpy as np
13
14import click
15
16from cyndilib import (
17 Sender,
18 VideoSendFrame,
19 AudioSendFrame,
20 FourCC,
21 AudioReference,
22)
23
24TestSource = Literal[
25 'testsrc2', 'yuvtestsrc', 'rgbtestsrc', 'smptebars', 'smptehdbars',
26 'zoneplate', 'colorspectrum',
27]
28
29FF_CMD = '{ffmpeg} -f lavfi -i {source}=size={xres}x{yres}:rate={fps} \
30 -pix_fmt {pix_fmt.name} -f rawvideo pipe: '
31
32
33
34class PixFmt(enum.Enum):
35 """Maps ffmpeg's ``pix_fmt`` names to their corresponding
36 :class:`FourCC ` types
37 """
38 uyvy422 = FourCC.UYVY #: uyvy422
39 nv12 = FourCC.NV12 #: nv12
40 rgba = FourCC.RGBA #: rgba
41 rgb0 = FourCC.RGBX #: rgb0
42 bgra = FourCC.BGRA #: bgra
43 bgr0 = FourCC.BGRX #: bgr0
44 p216be = FourCC.P216 #: p216be
45 yuv420p = FourCC.I420 #: yuv420p (i420)
46
47 @classmethod
48 def from_str(cls, name: str) -> Self:
49 return cls.__members__[name]
50
51
52class Options(NamedTuple):
53 """Options set through the cli
54 """
55 source: TestSource #: Source to use for the test pattern
56 pix_fmt: PixFmt #: Pixel format to send
57 xres: int #: Horizontal resolution
58 yres: int #: Vertical resolution
59 fps: str #: Frame rate
60 sender_name: str = 'ffmpeg_sender' #: NDI name for the sender
61 sine_freq: float = 1000.0 #: Frequency of the sine wave
62 sine_vol_dB: float = -20 #: Volume of the sine wave in dBVU
63 sample_rate: int = 48000 #: Sample rate of the audio
64 audio_channels: int = 2 #: Number of audio channels
65 audio_reference: AudioReference = AudioReference.dBVU #: Audio reference level
66 ffmpeg: str = 'ffmpeg' #: Name/Path of the "ffmpeg" executable
67
68
69def parse_frame_rate(fr: str) -> Fraction:
70 """Helper for NTSC frame rates (29.97, 59.94)
71 """
72 if '/' in fr:
73 n, d = [int(s) for s in fr.split('/')]
74 elif '.' in fr:
75 n = round(float(fr)) * 1000
76 d = 1001
77 else:
78 n = int(fr)
79 d = 1
80 return Fraction(n, d)
81
82class Signal:
83 """Signal helper
84
85 Allows for iteration over samples of a sine wave signal aligned with the
86 frame rate.
87 """
88 def __init__(self, opts: Options) -> None:
89 self.fps = parse_frame_rate(opts.fps)
90 self.opts = opts
91 self.amplitude = 10 ** (opts.sine_vol_dB / 20.0)
92 spf = opts.sample_rate / self.fps
93 if spf % 1 == 0:
94 samples_per_frame = [int(spf)]
95 max_samples_per_frame = int(spf)
96 else:
97 assert opts.sample_rate == 48000
98 if self.fps == Fraction(30000, 1001):
99 # These sample counts will align with the frame rate every 5 frames.
100 samples_per_frame = [
101 1602, 1601, 1602, 1601, 1602,
102 ]
```
--------------------------------
### Audio Sender Example
Source: https://cyndilib.readthedocs.io/en/latest/_sources/examples.rst.txt
Sends audio frames using a sine wave generated by numpy and blank video frames. Uses the write_video_and_audio method for synchronized transmission.
```python
.. literalinclude:: ../../examples/audio_sender.py
:language: python
:linenos:
:name: audio_sender
:caption: :github_permalink:`examples/audio_sender.py`
```
--------------------------------
### FFplay Receiver Example
Source: https://cyndilib.readthedocs.io/en/latest/_sources/examples.rst.txt
Receives video frames from an NDI source and displays them using ffplay. Utilizes the buffer protocol to feed data directly to the ffplay subprocess.
```python
.. literalinclude:: ../../examples/ffplay_receiver.py
:language: python
:linenos:
:name: ffplay_receiver
:caption: :github_permalink:`examples/ffplay_receiver.py`
```
--------------------------------
### FrameSync.audio_samples_available
Source: https://cyndilib.readthedocs.io/en/latest/reference/framesync.html
Get the number of audio samples currently available for capture.
```APIDOC
## FrameSync.audio_samples_available()
### Description
Returns the number of audio samples currently available for capture.
### Returns
- **int** - The count of available audio samples.
```
--------------------------------
### Retrieve Source Names
Source: https://cyndilib.readthedocs.io/en/latest/_sources/guide/finder.rst.txt
Get a list of names for all sources currently discovered by the Finder.
```python
>>> source_names = finder.get_source_names()
>>> source_names
['... (Example Video Source)']
```
--------------------------------
### Initialize and Wait for Sources
Source: https://cyndilib.readthedocs.io/en/latest/_sources/guide/finder.rst.txt
Open the Finder and block until sources are discovered within the specified timeout.
```python
>>> from cyndilib.finder import Finder
>>> finder = Finder()
>>> finder.open()
>>> changed = finder.wait_for_sources(timeout=5)
>>> changed
True
```
--------------------------------
### Clone the repository
Source: https://cyndilib.readthedocs.io/en/latest/_sources/development.rst.txt
Initial step to obtain the project source code.
```bash
git clone https://github.com/nocarryr/cyndilib.git
cd cyndilib
```
--------------------------------
### Routing Matrix Implementation
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Demonstrates creating a RoutingMatrix, setting a routing table, and monitoring status via a progress bar.
```python
1from __future__ import annotations
2from typing import TYPE_CHECKING
3import time
4
5import click
6if TYPE_CHECKING:
7 from click._termui_impl import ProgressBar
8
9
10from cyndilib.router import Router, RoutingMatrix
11
12
13
14def format_matrix(matrix: RoutingMatrix|None) -> str:
15 """Format the routing matrix for display in the progress bar
16
17 Each :class:`~cyndilib.router.Router` will be displayed in the format
18 ``dest -> source - N connections`` with text styled to indicate the
19 status of the router.
20 """
21
22 def format_router(router: Router) -> str:
23 is_active = router.is_active
24 num_connections = router.get_num_connections()
25
26 sep = click.style(' -> ', fg='white', bold=True)
27 dest_src = sep.join([
28 click.style(f'{router.dest}', fg='cyan', bold=is_active),
29 click.style(f'{router.source}', fg='magenta', bold=is_active)
30 ])
31 c_str = click.style(f'{num_connections} connections', fg='white', dim=True)
32 return f'{dest_src} - {c_str}'
33
34 if matrix is None:
35 return ''
36 routers = [format_router(router) for router in matrix]
37 sep = click.style(' | ', fg='white', bold=True)
38 prefix = click.style('Routing Matrix:', fg='yellow', bold=True)
39 return sep.join([prefix] + routers)
40
41
42
43def main(routing_table: dict[str, str | None], duration: float|None):
44 """Main function to run the routing matrix with the specified routing table
45 and duration.
46 """
47 sleep_interval = 0.1
48
49 # Calculate progress bar parameters using milliseconds
50 # since it requires integer values.
51 duration_ms = int(duration * 1000) if duration is not None else None
52 sleep_interval_ms = int(sleep_interval * 1000)
53 n_steps = duration_ms // sleep_interval_ms if duration_ms is not None else 1
54
55
56 click.echo('Building matrix with routing table:')
57 for dest, source in routing_table.items():
58 click.echo(f" '{dest}' -> '{source}'")
59 click.echo('')
60
61 # Build the routing matrix and set the routing table
62 matrix = RoutingMatrix()
63 matrix.set_routing_table(routing_table)
64
65
66 if duration is not None:
67 click.echo(f"Running for {duration} seconds...")
68 else:
69 click.echo("Running indefinitely (press Ctrl+C to stop)...")
70
71
72 # Open the routing matrix and display the routing status in a progress bar
73 # until the specified duration has elapsed or the user interrupts with Ctrl+C
74 with matrix:
75
76 # The progress bar is only used to display the routing status on the
77 # terminal without filling it with log messages.
78 bar: ProgressBar[RoutingMatrix] = click.progressbar(
79 length=n_steps,
80 show_eta=False,
81 show_percent=False,
82 bar_template='%(label)s %(info)s',
83 item_show_func=format_matrix,
84 )
85
86 with bar:
87 start_time = time.time()
88 i = 0
89 while True:
90 try:
91 time.sleep(sleep_interval)
92 elapsed = time.time() - start_time
93 bar.label = time.strftime("%H:%M:%S", time.gmtime(elapsed))
94 bar.update(1, current_item=matrix)
95 if duration_ms is not None:
96 i += 1
97 if i >= n_steps:
98 break
99 except KeyboardInterrupt:
100 break
101 click.echo("Routing matrix closed.")
102
103
104
105@click.command()
106@click.option(
107 '--duration',
108 default=None,
109 show_default=True,
110 type=float,
111 help='Time in seconds to run. If not specified, run indefinitely until interrupted.'
112)
113@click.option(
114 '--route', '-r',
115 multiple=True,
116 help='Routing in the format "dest:source". Can be specified multiple times.'
117)
118def cli(duration: float|None, route: list[str]):
119 """Create a routing matrix with the specified routing table and run it for
120 the specified duration.
121
122 Each route specified ('-r' or '--route') should be in the format "dest:source",
123 where "dest" is the name of the router to create
124 and "source" is the full name of the |NDI| source to connect to that router.
125
126 If "source" is "None", the router will be created with no source,
127 effectively a blank route.
128
129 \b
130 Example usage:
```
--------------------------------
### Iterate over NDI sources
Source: https://cyndilib.readthedocs.io/en/latest/reference/finder.html
Demonstrates how to iterate over available NDI sources using the Finder instance as an iterator within a context manager.
```python
>>> with Finder() as finder:
... for source in finder:
... ...
```
--------------------------------
### Control NDI PTZ Functions
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Demonstrates how to discover an NDI source, initialize a receiver, and execute various PTZ commands including movement, zoom, and preset recall.
```python
import time
from cyndilib.finder import Finder
from cyndilib.receiver import Receiver
from cyndilib.wrapper.ndi_recv import RecvColorFormat, RecvBandwidth
def main():
finder = Finder()
finder.open()
for i in range(5):
has_source = finder.wait_for_sources(timeout=5)
if has_source:
break
print(f"No sources detected ({i})")
source_names = finder.get_source_names()
print(source_names)
source = source_names[0]
source_obj = finder.get_source(source)
print(source_obj)
receiver = Receiver(
color_format=RecvColorFormat.fastest,
bandwidth=RecvBandwidth.metadata_only,
recv_name="obs_ndi_ptz"
)
receiver.set_source(source_obj)
time.sleep(1.5)
if not receiver.is_ptz_supported():
raise f"The NDI '{source}' does not indicate PTZ support."
ptz = receiver.ptz
print("pan to center, tilt to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(5)
print("zoom to min")
ptz.set_zoom_level(0)
time.sleep(2)
print("zoom to max")
ptz.set_zoom_level(1)
time.sleep(2)
print("zoom to min")
ptz.set_zoom_level(0)
time.sleep(2)
print("zoom in")
for _ in range(0, 100):
time.sleep(0.01)
ptz.zoom(1)
print("zoom out")
for _ in range(0, 100):
time.sleep(0.01)
ptz.zoom(-0.5)
print("zoom to min")
ptz.set_zoom_level(0)
time.sleep(2)
print("pan to left, tilt to middle")
ptz.set_pan_and_tilt_values(-0.5, 0.0)
time.sleep(5)
print("pan to center, tilt to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(5)
print("pan to right, tilt to middle")
ptz.set_pan_and_tilt_values(0.5, 0.0)
time.sleep(5)
print("pan to center, tilt to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(5)
print("pan to center, title to down")
ptz.set_pan_and_tilt_values(0.0, -1.0)
time.sleep(5)
print("pan to center, tilt to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(5)
print("pan to center, tilt to up")
ptz.set_pan_and_tilt_values(0.0, 1.0)
time.sleep(5)
print("continuously pan left")
for _ in range(0, 100):
time.sleep(0.05)
ptz.pan(.5)
print("continuously pan right")
for _ in range(0, 100):
time.sleep(0.05)
ptz.pan(-.5)
print("pan to center, title to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(2)
print("continuously tilt down")
for _ in range(0, 100):
time.sleep(0.05)
ptz.tilt(-.5)
print("continuously tilt up")
for _ in range(0, 100):
time.sleep(0.05)
ptz.tilt(.5)
print("pan to center, title to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(2)
print("store as preset to slot 10")
ptz.store_preset(10)
print("move")
ptz.set_pan_and_tilt_values(.5, .5)
time.sleep(2)
print("store as preset to slot 11")
ptz.store_preset(11)
print("recall preset in slot 10")
ptz.recall_preset(10, 1.0)
time.sleep(2)
print("recall preset in slot 11")
ptz.recall_preset(11, 1.0)
time.sleep(2)
print("recall preset in slot 10, slower")
ptz.recall_preset(10, 0.5)
time.sleep(4)
print("recall preset in slot 11, slowest")
ptz.recall_preset(11, 0.0)
time.sleep(6)
print("pan to center, title to middle")
ptz.set_pan_and_tilt_values(0.0, 0.0)
time.sleep(2)
```
--------------------------------
### CLI Configuration for Audio Player
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Defines command-line options for the audio player using the Click library.
```python
@click.command()
@click.option(
'-s', '--source-name',
type=str,
default='audio_sender',
show_default=True,
help='NDI source name to receive from',
)
@click.option('--audio-channels', type=int, default=2, show_default=True)
@click.option(
'--sample-rate', type=int, default=48000, show_default=True)
@click.option(
'--audio-reference',
type=click.Choice([m.name for m in AudioReference]),
default=AudioReference.dBVU.name,
show_default=True,
help='Audio reference level',
)
@click.option(
'--block-size', type=int, default=1024, show_default=True,
help='Block size for sounddevice output stream',
)
def main(
source_name: str,
audio_channels: int,
sample_rate: int,
audio_reference: str,
block_size: int,
) -> None:
options = Options(
source_name=source_name,
audio_channels=audio_channels,
sample_rate=sample_rate,
audio_reference=AudioReference[audio_reference],
block_size=block_size,
)
play(options)
if __name__ == '__main__':
main()
```
--------------------------------
### NDI Receiver with ffplay integration
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
This script uses a Finder to locate an NDI source, initializes a Receiver with a VideoFrameSync, and pipes the captured video data to ffplay.
```python
1from __future__ import annotations
2
3from typing import NamedTuple, TYPE_CHECKING
4from typing_extensions import Self
5import enum
6import time
7import subprocess
8import shlex
9
10import click
11
12from cyndilib.wrapper.ndi_structs import FourCC
13from cyndilib.wrapper.ndi_recv import RecvColorFormat, RecvBandwidth
14from cyndilib.video_frame import VideoFrameSync
15from cyndilib.receiver import Receiver
16from cyndilib.finder import Finder
17if TYPE_CHECKING:
18 from cyndilib.finder import Source
19
20
21FF_PLAY = '{ffplay} -video_size {xres}x{yres} -pixel_format {pix_fmt} -f rawvideo -i pipe:'
22"""ffplay command line format"""
23
24
25pix_fmts = {
26 FourCC.UYVY: 'uyvy422',
27 FourCC.NV12: 'nv12',
28 FourCC.RGBA: 'rgba',
29 FourCC.BGRA: 'bgra',
30 FourCC.RGBX: 'rgba',
31 FourCC.BGRX: 'bgra',
32}
33"""Mapping of :class:`FourCC ` types to
34ffmpeg's ``pix_fmt`` definitions
35"""
36
37
38class RecvFmt(enum.Enum):
39 """Pixel format to receive (mapped to values of
40 :class:`cyndilib.wrapper.ndi_recv.RecvColorFormat`)
41 """
42 uyvy = RecvColorFormat.UYVY_RGBA #: UYVY (RGBA if alpha is present)
43 rgb = RecvColorFormat.RGBX_RGBA #: RGB / RGBA
44 bgr = RecvColorFormat.BGRX_BGRA #: BGR / BGRA
45
46 @classmethod
47 def from_str(cls, name: str) -> Self:
48 return cls.__members__[name]
49
50
51class Bandwidth(enum.Enum):
52 """Receive bandwidth
53 """
54 lowest = RecvBandwidth.lowest #: Lowest
55 highest = RecvBandwidth.highest #: Highest
56
57 @classmethod
58 def from_str(cls, name: str) -> Self:
59 return cls.__members__[name]
60
61
62class Options(NamedTuple):
63 """Options set through the cli
64 """
65 sender_name: str = 'ffmpeg_sender'
66 """The name of the |NDI| source to connect to"""
67
68 recv_fmt: RecvFmt = RecvFmt.uyvy
69 """Receive pixel format"""
70
71 recv_bandwidth: Bandwidth = Bandwidth.highest
72 """Receive bandwidth"""
73
74 ffplay: str = 'ffplay'
75 """Name/Path of the ``ffplay`` executable"""
76
77
78def get_source(finder: Finder, name: str) -> Source:
79 """Use the Finder to search for an NDI source by name using either its
80 full name or its :attr:`~cyndilib.finder.Source.stream_name`
81 """
82 click.echo('waiting for ndi sources...')
83 finder.wait_for_sources(10)
84 for source in finder:
85 if source.name == name or source.stream_name == name:
86 return source
87 raise Exception(f'source not found. {finder.get_source_names()=}')
88
89
90def wait_for_first_frame(receiver: Receiver) -> None:
91 """The first few frames contain no data. Capture frames until the first
92 non-empty one
93 """
94 vf = receiver.frame_sync.video_frame
95 assert vf is not None
96 frame_rate = vf.get_frame_rate()
97 wait_time = float(1 / frame_rate)
98 click.echo('waiting for frame...')
99 while receiver.is_connected():
100 receiver.frame_sync.capture_video()
101 resolution = vf.get_resolution()
102 if min(resolution) > 0 and vf.get_data_size() > 0:
103 click.echo('have frame')
104 return
105 time.sleep(wait_time)
106
107
108def play(options: Options) -> None:
109 """Create the :class:`~cyndilib.receiver.Receiver` and send the frames to
110 ``ffplay``
111 """
112 # Get the NDI source and keep the Finder open until exit
113 with Finder() as finder:
114 source = get_source(finder, options.sender_name)
115
116 # Build the receiver and video frame
117 receiver = Receiver(
118 color_format=options.recv_fmt.value,
119 bandwidth=options.recv_bandwidth.value,
120 )
121 vf = VideoFrameSync()
122 frame_sync = receiver.frame_sync
123 frame_sync.set_video_frame(vf)
124
125 # Set the receiver source and wait for it to connect
126 receiver.set_source(source)
127 click.echo(f'connecting to "{source.name}"...')
128 i = 0
129 while not receiver.is_connected():
130 if i > 30:
131 raise Exception('timeout waiting for connection')
132 time.sleep(.5)
133 i += 1
134 click.echo('connected')
```
--------------------------------
### Configure Audio Scaling for NDI
Source: https://cyndilib.readthedocs.io/en/latest/_sources/guide/audio.rst.txt
Set the reference level to dBFS_smpte to automatically scale normalized audio samples when sending or receiving.
```python
>>> from cyndilib import AudioReference, AudioSendFrame, AudioFrameSync
>>> send_frame = AudioSendFrame()
>>> send_frame.reference_level = AudioReference.dBFS_smpte
>>> # Now send_frame can be filled with normalized audio samples
>>> # and they will be scaled up by 20 dB (10x amplitude) when sent.
>>> recv_frame = AudioFrameSync()
>>> recv_frame.reference_level = AudioReference.dBFS_smpte
>>> # Now recv_frame will provide normalized audio samples
>>> # by scaling down the received samples by 20 dB (0.1x amplitude).
```
--------------------------------
### Command Line Interface for Audio Sender
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Configuration of command-line options for the audio sender using the click library.
```python
@click.command()
@click.option('--xres', type=int, default=640, show_default=True)
@click.option('--yres', type=int, default=480, show_default=True)
@click.option('--fps', type=int, default=30, show_default=True)
@click.option('-f', '--sine-freq', type=float, default=1000.0, show_default=True)
@click.option(
'-s', '--sine-vol', type=float, default=-20.0, show_default=True,
help='Volume of the sine wave in dB (unit depends on audio reference)',
)
@click.option(
'--audio-reference',
type=click.Choice([m.name for m in AudioReference]),
default=AudioReference.dBVU.name,
show_default=True,
help='Audio reference level',
)
@click.option('--sample-rate', type=int, default=48000, show_default=True)
@click.option('--audio-channels', type=int, default=2, show_default=True)
@click.option(
'-n', '--num-frames', type=int, default=None, show_default=True,
help='Number of frames to send, or None for infinite',
)
@click.option(
'--sender-name', type=str, default='audio_sender', show_default=True,
help='NDI name for the sender',
)
def main(
xres: int,
yres: int,
fps: int,
sine_freq: float,
sine_vol: float,
audio_reference: str,
sample_rate: int,
audio_channels: int,
num_frames: int | None,
sender_name: str,
) -> None:
"""Send a sine wave audio signal as an NDI stream."""
audio_reference_enum = AudioReference[audio_reference]
opts = Options(
xres=xres,
yres=yres,
fps=fps,
sine_freq=sine_freq,
sine_vol_dB=sine_vol,
audio_reference=audio_reference_enum,
sample_rate=sample_rate,
audio_channels=audio_channels,
num_frames=num_frames,
sender_name=sender_name,
)
try:
send(opts)
finally:
click.echo('')
if __name__ == '__main__':
main()
```
--------------------------------
### Run tests
Source: https://cyndilib.readthedocs.io/en/latest/_sources/development.rst.txt
Executes the test suite or doctests.
```bash
py.test
```
```bash
py.test doc/
```
--------------------------------
### Control PTZ Camera Settings
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Demonstrates how to adjust focus, white balance, and exposure settings on a PTZ camera. Requires a configured ptz object and time module for delays.
```python
print("trigger autofocus")
ptz.autofocus()
time.sleep(1)
print("focus min (infinity)")
ptz.set_focus(0.0)
time.sleep(2)
print("focus max")
ptz.set_focus(1.0)
time.sleep(2)
print("decrease focus")
for _ in range(0, 100):
time.sleep(0.05)
ptz.focus(-.5)
print("increase focus")
for _ in range(0, 100):
time.sleep(0.05)
ptz.focus(.5)
print("trigger autofocus")
ptz.autofocus()
time.sleep(1)
print("trigger auto white-balance")
ptz.white_balance_auto()
time.sleep(2)
print("set indoor white-balance")
ptz.white_balance_indoor()
time.sleep(2)
print("set outdoor white-balance")
ptz.white_balance_outdoor()
time.sleep(2)
print("trigger oneshot white-balance")
ptz.white_balance_oneshot()
time.sleep(2)
print("set white-balance to min")
ptz.set_white_balance(0.0, 0.0)
time.sleep(2)
print("set white-balance to max")
ptz.set_white_balance(1.0, 1.0)
time.sleep(2)
print("trigger auto white-balance")
ptz.white_balance_auto()
time.sleep(2)
print("(re-)enable auto exposure")
ptz.exposure_auto()
time.sleep(2)
print("set exposure to dark")
ptz.set_exposure_coarse(0.0)
time.sleep(2)
print("set exposure to bright")
ptz.set_exposure_coarse(1.0)
time.sleep(2)
print("set exposure to dark (fine adjustment)")
ptz.set_exposure_fine(.0, .0, .0)
time.sleep(2)
print("set exposure to bright (fine adjustment)")
ptz.set_exposure_fine(1.0, 1.0, 1.0)
time.sleep(2)
print("re-enable auto exposure")
ptz.exposure_auto()
time.sleep(2)
if __name__ == "__main__":
main()
```
--------------------------------
### Use Finder as Context Manager
Source: https://cyndilib.readthedocs.io/en/latest/_sources/guide/finder.rst.txt
Manage the Finder lifecycle automatically using the with statement.
```python
>>> with Finder() as finder:
... changed = finder.wait_for_sources(timeout=5)
... [source for source in finder]
[]
```
--------------------------------
### Configure AudioSendFrame for normalized audio
Source: https://cyndilib.readthedocs.io/en/latest/guide/audio.html
Sets the reference level to dBFS_smpte to automatically scale normalized audio samples up by 20 dB when sending.
```python
>>> from cyndilib import AudioReference, AudioSendFrame, AudioFrameSync
>>> send_frame = AudioSendFrame()
>>> send_frame.reference_level = AudioReference.dBFS_smpte
>>> # Now send_frame can be filled with normalized audio samples
>>> # and they will be scaled up by 20 dB (10x amplitude) when sent.
```
--------------------------------
### Audio Sender Implementation
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
A complete implementation for sending NDI audio and video streams. Requires numpy for signal generation and cyndilib for NDI communication.
```python
1from __future__ import annotations
2from typing import NamedTuple, Generator
3from fractions import Fraction
4import time
5
6import numpy as np
7import click
8
9from cyndilib.wrapper.ndi_structs import FourCC
10from cyndilib import VideoSendFrame, AudioSendFrame, AudioReference
11from cyndilib.sender import Sender
12
13
14FloatArray2D = np.ndarray[tuple[int, int], np.dtype[np.float32]]
15FloatArray3D = np.ndarray[tuple[int, int, int], np.dtype[np.float32]]
16
17
18
19class Options(NamedTuple):
20 """Options set through the cli
21 """
22 xres: int #: Horizontal resolution
23 yres: int #: Vertical resolution
24 fps: int #: Frame rate
25 sine_freq: float = 1000.0 #: Frequency of the sine wave
26 sine_vol_dB: float = -20 #: Volume of the sine wave in dBVU
27 sample_rate: int = 48000 #: Sample rate of the audio
28 audio_channels: int = 2 #: Number of audio channels
29 audio_reference: AudioReference = AudioReference.dBVU
30 """Audio reference level"""
31 num_frames: int|None = None #: Number of frames to send, or None for infinite
32 sender_name: str = 'audio_sender' #: NDI name for the sender
33
34
35
36def build_blank_frame(xres: int, yres: int):
37 """Build an array of black pixels in UYVY422 format."""
38 cw, ch = xres >> 1, yres
39 num_bytes = xres * yres + (cw * ch * 2)
40 data = np.zeros(num_bytes, dtype=np.uint8)
41 data[1::2] = 16 # Y channel
42 data[0::2] = 128 # U/V channels
43 return data
44
45
46def gen_sine_wave(
47 sample_rate: int,
48 num_channels: int,
49 center_freq: float,
50 num_samples: int,
51 amplitude: float = 1.0,
52 t_offset: float = 0.0,
53):
54 """Build a sine wave signal.
55 """
56 t = np.arange(num_samples) / sample_rate
57 t += t_offset
58 sig = amplitude * np.sin(2 * np.pi * center_freq * t)
59 sig = np.reshape(sig, (1, num_samples))
60 if num_channels > 1:
61 sig = np.repeat(sig, num_channels, axis=0)
62 assert sig.shape == (num_channels, num_samples)
63 return sig.astype(np.float32)
64
65
66
67class Signal:
68 """Signal helper
69
70 Allows for iteration over samples of a sine wave signal aligned with the
71 frame rate.
72 """
73 def __init__(self, opts: Options) -> None:
74 self.opts = opts
75 self.amplitude = 10 ** (opts.sine_vol_dB / 20.0)
76 self.samples_per_frame = opts.sample_rate // opts.fps
77 one_sample = Fraction(1, opts.sample_rate)
78 fc = 1 / Fraction(opts.sine_freq)
79 self.samples_per_cycle = fc / one_sample
80 self.cycles_per_frame = self.samples_per_frame / self.samples_per_cycle
81 self.frame_count = 0
82
83 @property
84 def time_offset(self) -> float:
85 """Time offset in seconds for the current frame."""
86 return self.frame_count / self.opts.fps
87
88 def __iter__(self) -> Generator[FloatArray2D, None, None]:
89 while True:
90 sig = gen_sine_wave(
91 sample_rate=self.opts.sample_rate,
92 num_channels=self.opts.audio_channels,
93 center_freq=self.opts.sine_freq,
94 amplitude=self.amplitude,
95 num_samples=self.samples_per_frame,
96 t_offset=self.time_offset,
97 )
98 assert sig.shape == (self.opts.audio_channels, self.samples_per_frame)
99 yield sig
100 self.frame_count += 1
101
102
103
104def send(opts: Options) -> None:
105 """Send a sine wave audio signal as an NDI stream."""
106
107 sig_generator = Signal(opts)
108
109 sender = Sender(opts.sender_name)
110
111 # Build a VideoSendFrame and set its resolution and frame rate
112 # to match the options argument.
113 vf = VideoSendFrame()
114 vf.set_resolution(opts.xres, opts.yres)
115 vf.set_frame_rate(Fraction(opts.fps))
116 vf.set_fourcc(FourCC.UYVY)
117
118 # Build an AudioSendFrame and set its sample rate and number of channels
119 af = AudioSendFrame()
120 af.sample_rate = opts.sample_rate
121 af.num_channels = opts.audio_channels
122 af.reference_level = opts.audio_reference
123
124 # Set `max_num_samples` to the number of samples per frame
125 af.set_max_num_samples(sig_generator.samples_per_frame)
126
127 # Add the video and audio frames to the sender
128 sender.set_video_frame(vf)
```
--------------------------------
### CLI Command Configuration for Sender
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Defines command-line options for configuring the sender, including source selection, resolution, and audio parameters using the Click library.
```python
@click.command()
@click.option(
'--source',
type=click.Choice(
choices=[m for m in get_args(TestSource)],
),
default='testsrc2',
show_default=True,
help='Name of the ffmpeg test source to use',
)
@click.option(
'--pix-fmt',
type=click.Choice(choices=[m.name for m in PixFmt]),
default=PixFmt.uyvy422.name,
show_default=True,
show_choices=True,
)
@click.option('-x', '--x-res', type=int, default=1920, show_default=True)
@click.option('-y', '--y-res', type=int, default=1080, show_default=True)
@click.option('--fps', type=str, default='30', show_default=True)
@click.option(
'-n', '--sender-name',
type=str,
default='ffmpeg_sender',
show_default=True,
help='NDI name for the sender',
)
@click.option('-f', '--sine-freq', type=float, default=1000.0, show_default=True)
@click.option(
'-s', '--sine-vol', type=float, default=-20.0, show_default=True,
help='Volume of the sine wave in dB (unit depends on audio reference)',
)
@click.option(
'--audio-reference',
type=click.Choice([m.name for m in AudioReference]),
default=AudioReference.dBVU.name,
show_default=True,
help='Audio reference level',
)
@click.option('--sample-rate', type=int, default=48000, show_default=True)
@click.option('--audio-channels', type=int, default=2, show_default=True)
@click.option(
'--ffmpeg',
type=str,
default='ffmpeg',
show_default=True,
help='Name/Path of the "ffmpeg" executable',
)
def main(
source: TestSource,
pix_fmt: str,
x_res: int,
y_res: int,
fps: str,
sender_name: str,
sine_freq: float,
sine_vol: float,
audio_reference: str,
sample_rate: int,
audio_channels: int,
ffmpeg: str
):
opts = Options(
source=source,
pix_fmt=PixFmt.from_str(pix_fmt),
xres=x_res,
yres=y_res,
fps=fps,
sine_freq=sine_freq,
sine_vol_dB=sine_vol,
audio_reference=AudioReference[audio_reference],
sample_rate=sample_rate,
audio_channels=audio_channels,
sender_name=sender_name,
ffmpeg=ffmpeg,
)
send(opts)
if __name__ == '__main__':
main()
```
--------------------------------
### Configure CLI for NDI Receiver
Source: https://cyndilib.readthedocs.io/en/latest/examples.html
Defines command-line options for NDI source name, pixel format, bandwidth, and ffplay path using the click library.
```python
@click.command()
@click.option(
'-s', '--sender-name',
type=str,
default='ffmpeg_sender',
show_default=True,
help='The NDI source name to connect to',
)
@click.option(
'-f', '--recv-fmt',
type=click.Choice(choices=[m.name for m in RecvFmt]),
default='uyvy',
show_default=True,
show_choices=True,
help='Pixel format'
)
@click.option(
'-b', '--recv-bandwidth',
type=click.Choice(choices=[m.name for m in Bandwidth]),
default='highest',
show_default=True,
show_choices=True,
)
@click.option(
'--ffplay',
type=str,
default='ffplay',
show_default=True,
help='Name/Path of the "ffplay" executable',
)
def main(sender_name: str, recv_fmt: str, recv_bandwidth: str, ffplay: str):
options = Options(
sender_name=sender_name,
recv_fmt=RecvFmt.from_str(recv_fmt),
recv_bandwidth=Bandwidth.from_str(recv_bandwidth),
ffplay=ffplay,
)
play(options)
if __name__ == '__main__':
main()
```
--------------------------------
### FrameSync.capture_available_audio
Source: https://cyndilib.readthedocs.io/en/latest/reference/framesync.html
Capture all currently available audio samples.
```APIDOC
## FrameSync.capture_available_audio()
### Description
Capture all available audio samples. After this call, the captured data is available in the audio_frame.
### Returns
- **int** - The number of samples captured.
```
--------------------------------
### Configure AudioFrameSync for normalized audio
Source: https://cyndilib.readthedocs.io/en/latest/guide/audio.html
Sets the reference level to dBFS_smpte to automatically scale received audio samples down by 20 dB for normalization.
```python
>>> recv_frame = AudioFrameSync()
>>> recv_frame.reference_level = AudioReference.dBFS_smpte
>>> # Now recv_frame will provide normalized audio samples
>>> # by scaling down the received samples by 20 dB (0.1x amplitude).
```