Examples#

Note

The Sender and Receiver examples both make use of UNIX pipes and will only work on supported operating systems.

Sender#

Frames are generated by ffmpeg running in a subprocess which outputs the raw data to its stdout.

A Sender is created and an instance of VideoSendFrame is added to it.

The raw data from ffmpeg is then fed using the Sender’s write_video_async() method.

  1from __future__ import annotations
  2
  3from typing import NamedTuple, Generator, Any, cast
  4from typing_extensions import Self
  5import enum
  6import io
  7import subprocess
  8import shlex
  9from fractions import Fraction
 10from contextlib import contextmanager
 11
 12import click
 13
 14from cyndilib.wrapper.ndi_structs import FourCC
 15from cyndilib.video_frame import VideoSendFrame
 16from cyndilib.sender import Sender
 17
 18
 19FF_CMD = '{ffmpeg} -f lavfi -i testsrc2=size={xres}x{yres}:rate={fps} \
 20    -pix_fmt {pix_fmt.name} -f rawvideo pipe: '
 21
 22
 23
 24class PixFmt(enum.Enum):
 25    """Maps ffmpeg's ``pix_fmt`` names to their corresponding
 26    :class:`FourCC <cyndilib.wrapper.ndi_structs.FourCC>` types
 27    """
 28    uyvy422 = FourCC.UYVY   #: uyvy422
 29    nv12 = FourCC.NV12      #: nv12
 30    rgba = FourCC.RGBA      #: rgba
 31    bgra = FourCC.BGRA      #: bgra
 32
 33    @classmethod
 34    def from_str(cls, name: str) -> Self:
 35        return cls.__members__[name]
 36
 37
 38class Options(NamedTuple):
 39    """Options set through the cli
 40    """
 41    pix_fmt: PixFmt                     #: Pixel format to send
 42    xres: int                           #: Horizontal resolution
 43    yres: int                           #: Vertical resolution
 44    fps: str                            #: Frame rate
 45    sender_name: str = 'ffmpeg_sender'  #: NDI name for the sender
 46    ffmpeg: str = 'ffmpeg'              #: Name/Path of the "ffmpeg" executable
 47
 48
 49def parse_frame_rate(fr: str) -> Fraction:
 50    """Helper for NTSC frame rates (29.97, 59.94)
 51    """
 52    if '/' in fr:
 53        n, d = [int(s) for s in fr.split('/')]
 54    elif '.' in fr:
 55        n = round(float(fr)) * 1000
 56        d = 1001
 57    else:
 58        n = int(fr)
 59        d = 1
 60    return Fraction(n, d)
 61
 62
 63@contextmanager
 64def ffmpeg_proc(opts: Options) -> Generator[subprocess.Popen[bytes], Any, None]:
 65    """Context manager for the ffmpeg subprocess generating frames
 66    """
 67    ff_cmd = FF_CMD.format(**opts._asdict())
 68    ff_proc = subprocess.Popen(shlex.split(ff_cmd), stdout=subprocess.PIPE)
 69    try:
 70        ff_proc.poll()
 71        if ff_proc.returncode is None:
 72            yield ff_proc
 73    finally:
 74        ff_proc.kill()
 75
 76
 77def send(opts: Options) -> None:
 78    """Send frames generated by ffmpeg's ``testsrc2`` as an |NDI| stream
 79
 80    The raw frames generated by ffmpeg are sent to a pipe and read from its
 81    :attr:`~subprocess.Popen.stdout`.  The data is then fed directly to
 82    :meth:`cyndilib.sender.Sender.write_video_async` using an
 83    intermediate :class:`memoryview`
 84    """
 85    sender = Sender(opts.sender_name)
 86
 87    # Build a VideoSendFrame and set its resolution and frame rate
 88    # to match the options argument
 89    vf = VideoSendFrame()
 90    vf.set_resolution(opts.xres, opts.yres)
 91    fr = parse_frame_rate(opts.fps)
 92    vf.set_frame_rate(fr)
 93    vf.set_fourcc(opts.pix_fmt.value)
 94
 95    # Add the VideoSendFrame to the sender
 96    sender.set_video_frame(vf)
 97
 98    # Pre-allocate a bytearray to hold frame data and create a view of it
 99    # So we can buffer into it from ffmpeg then pass directly to the sender
100    frame_size_bytes = vf.get_data_size()
101    ba = bytearray(frame_size_bytes)
102    mv = memoryview(ba)
103
104    i = 0
105
106    with sender:
107        with ffmpeg_proc(opts) as ff_proc:
108            stdout = cast(io.BytesIO, ff_proc.stdout)
109            while True:
110                if ff_proc.returncode is not None:
111                    break
112                # Read from the ffmpeg process into a view of the bytearray
113                num_read = stdout.readinto(mv)
114
115                # The first few reads might be empty, ignore
116                if num_read == 0:
117                    continue
118
119                # Pass the memoryview directly to the sender
120                # (using the buffer protocol)
121                sender.write_video_async(mv)
122
123                i += 1
124                if i % 10 == 0:
125                    ff_proc.poll()
126
127
128
129@click.command()
130@click.option(
131    '--pix-fmt',
132    type=click.Choice(choices=[m.name for m in PixFmt]),
133    default=PixFmt.uyvy422.name,
134    show_default=True,
135    show_choices=True,
136)
137@click.option('-x', '--x-res', type=int, default=1920, show_default=True)
138@click.option('-y', '--y-res', type=int, default=1080, show_default=True)
139@click.option('--fps', type=str, default='30', show_default=True)
140@click.option(
141    '-n', '--sender-name',
142    type=str,
143    default='ffmpeg_sender',
144    show_default=True,
145    help='NDI name for the sender',
146)
147@click.option(
148    '--ffmpeg',
149    type=str,
150    default='ffmpeg',
151    show_default=True,
152    help='Name/Path of the "ffmpeg" executable',
153)
154def main(pix_fmt: str, x_res: int, y_res: int, fps: str, sender_name: str, ffmpeg: str):
155    opts = Options(
156        pix_fmt=PixFmt.from_str(pix_fmt),
157        xres=x_res,
158        yres=y_res,
159        fps=fps,
160        sender_name=sender_name,
161        ffmpeg=ffmpeg,
162    )
163    send(opts)
164
165
166if __name__ == '__main__':
167    main()

Audio Sender#

This example sends audio frames using a sine wave generated by numpy. The video frames are blank frames generated manually.

A Sender is created and AudioSendFrame and VideoSendFrame instances are added to it.

The audio and video data are then written using the Sender’s write_video_and_audio() method.

  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)
129    sender.set_audio_frame(af)
130
131    # Build data for a blank video frame
132    vid_data = build_blank_frame(opts.xres, opts.yres)
133
134    start_time = time.monotonic()
135    num_frames_sent = 0
136
137    with sender:
138        for samples in sig_generator:
139            if opts.num_frames is not None:
140                if num_frames_sent >= opts.num_frames:
141                    break
142
143            # Write the video and audio data to the sender
144            # Note that we don't have to wait in between frames,
145            # as the sender will handle the timing for us.
146            sender.write_video_and_audio(
147                video_data=vid_data,
148                audio_data=samples,
149            )
150
151            num_frames_sent += 1
152            now = time.monotonic()
153            elapsed = now - start_time
154            click.echo(f'\rFrames: {num_frames_sent:04d}\tDuration: {elapsed:.3f}s', nl=False)
155
156
157
158@click.command()
159@click.option('--xres', type=int, default=640, show_default=True)
160@click.option('--yres', type=int, default=480, show_default=True)
161@click.option('--fps', type=int, default=30, show_default=True)
162@click.option('-f', '--sine-freq', type=float, default=1000.0, show_default=True)
163@click.option(
164    '-s', '--sine-vol', type=float, default=-20.0, show_default=True,
165    help='Volume of the sine wave in dB (unit depends on audio reference)',
166)
167@click.option(
168    '--audio-reference',
169    type=click.Choice([m.name for m in AudioReference]),
170    default=AudioReference.dBVU.name,
171    show_default=True,
172    help='Audio reference level',
173)
174@click.option('--sample-rate', type=int, default=48000, show_default=True)
175@click.option('--audio-channels', type=int, default=2, show_default=True)
176@click.option(
177    '-n', '--num-frames', type=int, default=None, show_default=True,
178    help='Number of frames to send, or None for infinite',
179)
180@click.option(
181    '--sender-name', type=str, default='audio_sender', show_default=True,
182    help='NDI name for the sender',
183)
184def main(
185    xres: int,
186    yres: int,
187    fps: int,
188    sine_freq: float,
189    sine_vol: float,
190    audio_reference: str,
191    sample_rate: int,
192    audio_channels: int,
193    num_frames: int | None,
194    sender_name: str,
195) -> None:
196    """Send a sine wave audio signal as an NDI stream."""
197    audio_reference_enum = AudioReference[audio_reference]
198    opts = Options(
199        xres=xres,
200        yres=yres,
201        fps=fps,
202        sine_freq=sine_freq,
203        sine_vol_dB=sine_vol,
204        audio_reference=audio_reference_enum,
205        sample_rate=sample_rate,
206        audio_channels=audio_channels,
207        num_frames=num_frames,
208        sender_name=sender_name,
209    )
210    try:
211        send(opts)
212    finally:
213        click.echo('')
214
215
216if __name__ == '__main__':
217    main()

Receiver#

This example receives video frames from an NDI® Source and shows them using ffplay.

Finder is used to locate the Source with the given name.

A Receiver is then created and an instance of VideoFrameSync is added to it.

Video frames are then read using the FrameSync.capture_video method which is available from the frame_sync attribute on the receiver.

The data is then fed to the stdin of the fplay subprocess directly from the video frame using the buffer protocol.

  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 <cyndilib.wrapper.ndi_structs.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')
135
136        proc: subprocess.Popen|None = None
137
138        try:
139            wait_for_first_frame(receiver)
140            # At this point we should have received a frame, so the pixel format,
141            # resolution and frame rate should be populated.
142            fourcc = vf.get_fourcc()
143            frame_rate = vf.get_frame_rate()
144            wait_time = float(1 / frame_rate)
145            xres, yres = vf.get_resolution()
146
147            cmd_str = FF_PLAY.format(
148                xres=xres,
149                yres=yres,
150                pix_fmt=pix_fmts[fourcc],
151                ffplay=options.ffplay,
152            )
153            click.echo(f'{cmd_str=}')
154            proc = subprocess.Popen(shlex.split(cmd_str), stdin=subprocess.PIPE)
155            assert proc.stdin is not None
156
157            # Since we already have a frame with data, write it to ffplay
158            # Note that the frame object itself is directly used as the data source
159            # (since `VideoFrameSync` supports the buffer protocol)
160            proc.stdin.write(vf)
161
162            while receiver.is_connected():
163                # Not the best timing method, but we're using `FrameSync` to
164                # capture frames, so it'll correct things for us (within reason).
165                time.sleep(wait_time)
166                receiver.frame_sync.capture_video()
167                proc.poll()
168                if proc.returncode is not None:
169                    break
170                proc.stdin.write(vf)
171
172        finally:
173            if proc is not None:
174                proc.kill()
175
176
177@click.command()
178@click.option(
179    '-s', '--sender-name',
180    type=str,
181    default='ffmpeg_sender',
182    show_default=True,
183    help='The NDI source name to connect to',
184)
185@click.option(
186    '-f', '--recv-fmt',
187    type=click.Choice(choices=[m.name for m in RecvFmt]),
188    default='uyvy',
189    show_default=True,
190    show_choices=True,
191    help='Pixel format'
192)
193@click.option(
194    '-b', '--recv-bandwidth',
195    type=click.Choice(choices=[m.name for m in Bandwidth]),
196    default='highest',
197    show_default=True,
198    show_choices=True,
199)
200@click.option(
201    '--ffplay',
202    type=str,
203    default='ffplay',
204    show_default=True,
205    help='Name/Path of the "ffplay" executable',
206)
207def main(sender_name: str, recv_fmt: str, recv_bandwidth: str, ffplay: str):
208    options = Options(
209        sender_name=sender_name,
210        recv_fmt=RecvFmt.from_str(recv_fmt),
211        recv_bandwidth=Bandwidth.from_str(recv_bandwidth),
212        ffplay=ffplay,
213    )
214    play(options)
215
216
217if __name__ == '__main__':
218    main()

PTZ#

This example showcases the PTZ functions on an NDI® Receiver.

Finder is used to locate a Source. A Receiver is then created.

Various PTZ methods are then invoked.

  1import time
  2from cyndilib.finder import Finder
  3from cyndilib.receiver import Receiver
  4from cyndilib.wrapper.ndi_recv import RecvColorFormat, RecvBandwidth
  5
  6
  7def main():
  8    finder = Finder()
  9    finder.open()
 10    for i in range(5):
 11        has_source = finder.wait_for_sources(timeout=5)
 12        if has_source:
 13            break
 14        print(f"No sources detected ({i})")
 15
 16    source_names = finder.get_source_names()
 17    print(source_names)
 18
 19    source = source_names[0]
 20    source_obj = finder.get_source(source)
 21    print(source_obj)
 22
 23    receiver = Receiver(
 24        color_format=RecvColorFormat.fastest,
 25        bandwidth=RecvBandwidth.metadata_only,
 26        recv_name="obs_ndi_ptz"
 27    )
 28
 29    receiver.set_source(source_obj)
 30    time.sleep(1.5)
 31
 32    if not receiver.is_ptz_supported():
 33        raise f"The NDI '{source}' does not indicate PTZ support."
 34
 35    ptz = receiver.ptz
 36
 37    print("pan to center, tilt to middle")
 38    ptz.set_pan_and_tilt_values(0.0, 0.0)
 39    time.sleep(5)
 40
 41    print("zoom to min")
 42    ptz.set_zoom_level(0)
 43    time.sleep(2)
 44
 45    print("zoom to max")
 46    ptz.set_zoom_level(1)
 47    time.sleep(2)
 48
 49    print("zoom to min")
 50    ptz.set_zoom_level(0)
 51    time.sleep(2)
 52
 53    print("zoom in")
 54    for _ in range(0, 100):
 55        time.sleep(0.01)
 56        ptz.zoom(1)
 57
 58    print("zoom out")
 59    for _ in range(0, 100):
 60        time.sleep(0.01)
 61        ptz.zoom(-0.5)
 62
 63    print("zoom to min")
 64    ptz.set_zoom_level(0)
 65    time.sleep(2)
 66
 67    print("pan to left, tilt to middle")
 68    ptz.set_pan_and_tilt_values(-0.5, 0.0)
 69    time.sleep(5)
 70
 71    print("pan to center, tilt to middle")
 72    ptz.set_pan_and_tilt_values(0.0, 0.0)
 73    time.sleep(5)
 74
 75    print("pan to right, tilt to middle")
 76    ptz.set_pan_and_tilt_values(0.5, 0.0)
 77    time.sleep(5)
 78
 79    print("pan to center, tilt to middle")
 80    ptz.set_pan_and_tilt_values(0.0, 0.0)
 81    time.sleep(5)
 82
 83    print("pan to center, title to down")
 84    ptz.set_pan_and_tilt_values(0.0, -1.0)
 85    time.sleep(5)
 86
 87    print("pan to center, tilt to middle")
 88    ptz.set_pan_and_tilt_values(0.0, 0.0)
 89    time.sleep(5)
 90
 91    print("pan to center, tilt to up")
 92    ptz.set_pan_and_tilt_values(0.0, 1.0)
 93    time.sleep(5)
 94
 95    print("continuously pan left")
 96    for _ in range(0, 100):
 97        time.sleep(0.05)
 98        ptz.pan(.5)
 99
100    print("continuously pan right")
101    for _ in range(0, 100):
102        time.sleep(0.05)
103        ptz.pan(-.5)
104
105    print("pan to center, title to middle")
106    ptz.set_pan_and_tilt_values(0.0, 0.0)
107    time.sleep(2)
108
109    print("continuously tilt down")
110    for _ in range(0, 100):
111        time.sleep(0.05)
112        ptz.tilt(-.5)
113
114    print("continuously tilt up")
115    for _ in range(0, 100):
116        time.sleep(0.05)
117        ptz.tilt(.5)
118
119    print("pan to center, title to middle")
120    ptz.set_pan_and_tilt_values(0.0, 0.0)
121    time.sleep(2)
122
123    print("store as preset to slot 10")
124    ptz.store_preset(10)
125
126    print("move")
127    ptz.set_pan_and_tilt_values(.5, .5)
128    time.sleep(2)
129
130    print("store as preset to slot 11")
131    ptz.store_preset(11)
132
133    print("recall preset in slot 10")
134    ptz.recall_preset(10, 1.0)
135    time.sleep(2)
136
137    print("recall preset in slot 11")
138    ptz.recall_preset(11, 1.0)
139    time.sleep(2)
140
141    print("recall preset in slot 10, slower")
142    ptz.recall_preset(10, 0.5)
143    time.sleep(4)
144
145    print("recall preset in slot 11, slowest")
146    ptz.recall_preset(11, 0.0)
147    time.sleep(6)
148
149    print("pan to center, title to middle")
150    ptz.set_pan_and_tilt_values(0.0, 0.0)
151    time.sleep(2)
152
153    print("trigger autofocus")
154    ptz.autofocus()
155    time.sleep(1)
156
157    print("focus min (infinity)")
158    ptz.set_focus(0.0)
159    time.sleep(2)
160
161    print("focus max")
162    ptz.set_focus(1.0)
163    time.sleep(2)
164
165    print("decrease focus")
166    for _ in range(0, 100):
167        time.sleep(0.05)
168        ptz.focus(-.5)
169
170    print("increase focus")
171    for _ in range(0, 100):
172        time.sleep(0.05)
173        ptz.focus(.5)
174
175    print("trigger autofocus")
176    ptz.autofocus()
177    time.sleep(1)
178
179    print("trigger auto white-balance")
180    ptz.white_balance_auto()
181    time.sleep(2)
182
183    print("set indoor white-balance")
184    ptz.white_balance_indoor()
185    time.sleep(2)
186
187    print("set outdoor white-balance")
188    ptz.white_balance_outdoor()
189    time.sleep(2)
190
191    print("trigger oneshot white-balance")
192    ptz.white_balance_oneshot()
193    time.sleep(2)
194
195    print("set white-balance to min")
196    ptz.set_white_balance(0.0, 0.0)
197    time.sleep(2)
198
199    print("set white-balance to max")
200    ptz.set_white_balance(1.0, 1.0)
201    time.sleep(2)
202
203    print("trigger auto white-balance")
204    ptz.white_balance_auto()
205    time.sleep(2)
206
207    print("(re-)enable auto exposure")
208    ptz.exposure_auto()
209    time.sleep(2)
210
211    print("set exposure to dark")
212    ptz.set_exposure_coarse(0.0)
213    time.sleep(2)
214
215    print("set exposure to bright")
216    ptz.set_exposure_coarse(1.0)
217    time.sleep(2)
218
219    print("set exposure to dark (fine adjustment)")
220    ptz.set_exposure_fine(.0, .0, .0)
221    time.sleep(2)
222
223    print("set exposure to bright (fine adjustment)")
224    ptz.set_exposure_fine(1.0, 1.0, 1.0)
225    time.sleep(2)
226
227    print("re-enable auto exposure")
228    ptz.exposure_auto()
229    time.sleep(2)
230
231
232if __name__ == "__main__":
233    main()