PTZ, Wake-up and Bitrate

Pre-requisite: The user has to be signed in to perform the following operations.


Operate PTZ of the Device

To move the PTZ of the device, The following method can be used:

  • spaceId (required): The space ID of the device.
  • deviceId (required): The device ID.
  • request (required): MovePtzRequest(pan:tilt:nonStop:) — how far to move on each axis, and whether the camera should keep going until it is told to stop.
Factory.deviceService.movePtz(spaceId: String, deviceId: String, request: MovePtzRequest) -> IVPublisher<Void>

pan and tilt are not directions, they are distances. How far one press should move the camera comes from its ptz cluster, so a device that reports clusters needs its pan and tilt scaled from the cluster before the request is sent:

Helper on DeviceModel What it gives you
supportPTZ Whether the camera has a ptz cluster at all
supportsPTZPan, supportsPTZTilt Which axes it can move — build the D-pad from these
ptzPanMultiplier, ptzTiltMultiplier The distance of one step on each axis
ptzPanStepsCount, ptzTiltStepsCount The full travel of each axis, max - min of the attribute
flipImage Whether the image is rotated, from the rotationalAngle cluster

A camera only reports clusters from clusterGroupVersion v1 onwards; an older one has none of these values and takes the raw direction unscaled, so gate the scaling on the version:

extension DeviceModel {
    var supportsClusters: Bool {
        let version = clusterGroupVersion ?? "v0"
        return !version.isEmpty && version.lowercased() != "v0"
    }
}

Which of the two distances you use is what separates a step from a continuous move:

func movePtz(_ device: DeviceModel, pan: Int, tilt: Int, nonStop: Bool) {
    var panValue = pan     // -1, 0 or 1 — the direction the user pressed
    var tiltValue = tilt

    if device.supportsClusters {
        if nonStop {
            // Continuous: send the whole travel range so the camera keeps moving
            // until stopPtz is called.
            panValue  = -(device.ptzPanStepsCount ?? 4000) * pan
            tiltValue = -(device.ptzTiltStepsCount ?? 4000) * tilt
        } else {
            // Single step: one nudge in that direction.
            panValue  = (device.ptzPanMultiplier ?? -100) * pan
            tiltValue = (device.ptzTiltMultiplier ?? -50) * tilt
        }

        // A camera mounted upside down sees the user's "up" as its own "down".
        if device.flipImage {
            panValue  = -panValue
            tiltValue = -tiltValue
        }
    }

    Factory.deviceService.movePtz(
        spaceId: device.spaceId,
        deviceId: device.id,
        request: MovePtzRequest(pan: panValue, tilt: tiltValue, nonStop: nonStop)
    )
    .sink { _ in }
    .store(in: &cancellables)
}

For nonStop: true use the steps count, not the multiplier. The camera moves the distance it is given, so a continuous move is expressed as the axis’s entire travel — anything smaller stops early, part way through the press. The fallbacks of 4000 are deliberately larger than any real range, for a camera whose cluster does not report min and max.

A nonStop move must be stopped by you. Nothing ends it but stopPtz, so pair every one with a stop when the user lets go:

// User presses and holds a direction
movePtz(device, pan: 1, tilt: 0, nonStop: true)

// User releases
Factory.deviceService.stopPtz(spaceId: device.spaceId, deviceId: device.id)
    .sink { _ in }
    .store(in: &cancellables)

A short tap is the other case: send nonStop: false with the multiplier and no stop is needed — the camera moves one step and settles. The reference app decides between them with a timer, treating a press held longer than 0.4 seconds as continuous and anything shorter as a single step.

flipImage reads the rotationalAngle cluster, which is the same value the live view uses to rotate the picture. Apply it to the request rather than to the UI: the buttons should keep meaning what they say, and it is the camera’s idea of up that has changed.


Stop PTZ Movement

Ends a continuous move. Required after every movePtz sent with nonStop: true, and harmless otherwise.

To stop the PTZ movement of the device, The following method can be used:

  • spaceId (required): The space ID of the device.
  • deviceId (required): The device ID.
Factory.deviceService.stopPtz(spaceId: String, deviceId: String) -> IVPublisher<Void>

Reset PTZ to default position

To reset the PTZ of the device, The following method can be used:

  • spaceId (required): The space ID of the device.
  • deviceId (required): The device ID.
Factory.deviceService.resetPtz(spaceId: String, deviceId: String) -> IVPublisher<Void>

Wake Up Device from sleep state

To wake up the device, The following method can be used:

  • spaceId (required): The space ID of the device.
  • deviceId (required): The device ID.
  • request (required): WakeUpRequest(reason:) — a reason string recorded against the device, e.g. "ChangeSettings" or "LiveViewStart".
Factory.deviceService.wakeup(spaceId: String, deviceId: String, request: WakeUpRequest) -> IVPublisher<Void>

Wake Up the Device and send a follow up request

To wake the device and deliver a follow up payload in the same round trip, The following method can be used:

  • spaceId (required): The space ID of the device.
  • deviceId (required): The device ID.
  • wakeUpRequest (required): The request object containing the reason for the wake up.
  • request (optional): The payload to be delivered once the device is awake.
Factory.deviceService.wakeupDevice(spaceId: String, deviceId: String, wakeUpRequest: WakeUpRequest, request: Codable?) -> IVPublisher<Codable?>

Set the Device Bitrate

To set the streaming bitrate of the device, The following method can be used:

  • spaceId (required): The space ID of the device.
  • deviceId (required): The device ID.
  • request (required): The request object containing the bitrate. BitrateRequest(bitrate:) takes the raw value: Bitrate.auto.rawValue ("Auto") or Bitrate.hd.rawValue ("HD").
Factory.deviceService.setDeviceBitrate(spaceId: String, deviceId: String, request: BitrateRequest) -> IVPublisher<Void>

The lullaby player is controlled through the same service; it is documented with the Baby product under Lullaby.