chromium/extensions/browser/api/webcam_private/v4l2_webcam.cc

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/api/webcam_private/v4l2_webcam.h"

#include <errno.h>
#include <fcntl.h>
#include <linux/uvcvideo.h>
#include <linux/videodev2.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>

#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"

#define V4L2_CID_PAN_SPEED (V4L2_CID_CAMERA_CLASS_BASE+32)
#define V4L2_CID_TILT_SPEED (V4L2_CID_CAMERA_CLASS_BASE+33)
#define V4L2_CID_PANTILT_CMD (V4L2_CID_CAMERA_CLASS_BASE+34)

// GUID of the Extension Unit for Logitech CC3300e motor control:
// {212de5ff-3080-2c4e-82d9-f587d00540bd}
#define UVC_GUID_LOGITECH_CC3000E_MOTORS                                    \
  {                                                                         \
    0x21, 0x2d, 0xe5, 0xff, 0x30, 0x80, 0x2c, 0x4e, 0x82, 0xd9, 0xf5, 0x87, \
        0xd0, 0x05, 0x40, 0xbd                                              \
  }

// GUID of the Extension Unit for AVER XU1 motor control:
// {cb666936-6e26-a047-8451-96ecf60330d6}
#define UVC_GUID_AVER                                                       \
  {                                                                         \
    0xcb, 0x66, 0x69, 0x36, 0x6e, 0x26, 0xa0, 0x47, 0x84, 0x51, 0x96, 0xec, \
        0xf6, 0x03, 0x30, 0xd6                                              \
  }

#define LOGITECH_MOTORCONTROL_PANTILT_CMD 2
#define AVER_UVCX_UCAM_PRESET 0x12

namespace {
const int kLogitechMenuIndexGoHome = 1;  // {0x02, Go Home}
const int kLogitechMenuIndexSetHome = 0; // {0x01, Set Home Preset}
const int kAverMenuIndexGoHome = 1;      // {0x01, Restore preset 0}
const int kAverMenuIndexSetHome = 0;     // {0x00, Set Preset 0}

// Note: Keep in sync with index constants above i.e kLogitechMenuIndex{Get, Set}Home
const uvc_menu_info kLogitechCmdMenu[] = {
    {0x01, "Set Home Preset"},
    {0x02, "Go Home"},
    {0x03, "Go Home"},
    {0x04, "Set Preset 1"},
    {0x0C, "Restore Preset 1"},
    {0x05, "Set Preset 2"},
    {0x0D, "Restore Preset 2"},
    {0x06, "Set Preset 3"},
    {0x0E, "Restore Preset 3"},
    {0x07, "Set Preset 4"},
    {0x0F, "Restore Preset 4"},
    {0x08, "Set Preset 5"},
    {0x10, "Restore Preset 5"},
    {0x09, "Set Preset 6"},
    {0x11, "Restore Preset 6"},
    {0x0A, "Set Preset 7"},
    {0x12, "Restore Preset 7"},
    {0x0B, "Set Preset 8"},
    {0x13, "Restore Preset 8"},
};

// Preset 0 is equivalent to HOME in Aver cameras.
// Note: Keep in sync with index constants above i.e kAverMenuIndex{Go, Set}Home.
const uvc_menu_info kAverPresetMenu[] = {
    /*0*/ {0x0000, "Set Preset 0"},  /*1*/ {0x0001, "Restore preset 0"},
    /*2*/ {0x0100, "Set Preset 1"},  /*3*/ {0x0101, "Restore preset 1"},
    /*4*/ {0x0200, "Set Preset 2"},  /*5*/ {0x0202, "Restore preset 2"},
    /*6*/ {0x0300, "Set Preset 3"},  /*7*/ {0x0303, "Restore preset 3"},
    /*8*/ {0x0400, "Set Preset 4"},  /*9*/ {0x0404, "Restore preset 4"},
    /*10*/ {0x0500, "Set Preset 5"}, /*11*/ {0x0505, "Restore preset 5"},
    /*12*/ {0x0600, "Set Preset 6"}, /*13*/ {0x0606, "Restore preset 6"},
    /*14*/ {0x0700, "Set Preset 7"}, /*15*/ {0x0707, "Restore preset 7"},
    /*16*/ {0x0800, "Set Preset 8"}, /*17*/ {0x0808, "Restore preset 8"},
    /*18*/ {0x0900, "Set Preset 9"}, /*19*/ {0x0909, "Restore preset 9"},
};

const uvc_xu_control_mapping kLogitechCmdMapping = {
    V4L2_CID_PANTILT_CMD,
    "Pan/Tilt Go",
    UVC_GUID_LOGITECH_CC3000E_MOTORS,
    LOGITECH_MOTORCONTROL_PANTILT_CMD,
    8,
    0,
    V4L2_CTRL_TYPE_MENU,
    UVC_CTRL_DATA_TYPE_ENUM,
    const_cast<uvc_menu_info*>(&kLogitechCmdMenu[0]),
    std::size(kLogitechCmdMenu),
};

const uvc_xu_control_mapping kLogitechPanAbsoluteMapping = {
    V4L2_CID_PAN_ABSOLUTE,
    "Pan (Absolute)",
    UVC_GUID_LOGITECH_CC3000E_MOTORS,
    12,
    32,
    0,
    V4L2_CTRL_TYPE_INTEGER,
    UVC_CTRL_DATA_TYPE_SIGNED,
};

const uvc_xu_control_mapping kLogitechTiltAbsoluteMapping = {
    V4L2_CID_TILT_ABSOLUTE,
    "Tilt (Absolute)",
    UVC_GUID_LOGITECH_CC3000E_MOTORS,
    12,
    32,
    32,
    V4L2_CTRL_TYPE_INTEGER,
    UVC_CTRL_DATA_TYPE_SIGNED,
};

const uvc_xu_control_mapping kAverCmdMapping = {
    V4L2_CID_PANTILT_CMD,
    "PTZ Preset",
    UVC_GUID_AVER,
    AVER_UVCX_UCAM_PRESET,
    24,
    0,
    V4L2_CTRL_TYPE_MENU,
    UVC_CTRL_DATA_TYPE_ENUM,
    const_cast<uvc_menu_info*>(kAverPresetMenu),
    std::size(kAverPresetMenu),
};

}  // namespace

namespace extensions {

V4L2Webcam::V4L2Webcam(const std::string& device_id) : device_id_(device_id) {
}

V4L2Webcam::~V4L2Webcam() {
}

bool V4L2Webcam::Open() {
  fd_.reset(HANDLE_EINTR(open(device_id_.c_str(), 0)));
  return fd_.is_valid();
}

bool V4L2Webcam::EnsureLogitechCommandsMapped() {
  int res =
      HANDLE_EINTR(ioctl(fd_.get(), UVCIOC_CTRL_MAP, &kLogitechCmdMapping));
  // If mapping is successful or it's already mapped, this is a Logitech
  // camera.
  // NOTE: On success, occasionally EFAULT is returned.  On a real error,
  // ENOMEM, EPERM, EINVAL, or EOVERFLOW should be returned.
  return res >= 0 || errno == EEXIST || errno == EFAULT;
}

bool V4L2Webcam::EnsureAverCommandsMapped() {
  int res = HANDLE_EINTR(ioctl(fd_.get(), UVCIOC_CTRL_MAP, &kAverCmdMapping));
  // If mapping is successful or it's already mapped, this is an Aver camera.
  // NOTE: On success, occasionally EFAULT is returned.  On a real error,
  // ENOMEM, EPERM, EINVAL, or EOVERFLOW should be returned.
  return res >= 0 || errno == EEXIST || errno == EFAULT;
}

bool V4L2Webcam::SetWebcamParameter(int fd, uint32_t control_id, int value) {
  // Try to map the V4L2 control to the Logitech extension unit. If the
  // connected camera does not implement these extension unit this will just
  // silently fail and the standard camera terminal controls will be used.
  if (control_id == V4L2_CID_PAN_ABSOLUTE) {
    HANDLE_EINTR(ioctl(fd, UVCIOC_CTRL_MAP, &kLogitechPanAbsoluteMapping));
  } else if (control_id == V4L2_CID_TILT_ABSOLUTE) {
    HANDLE_EINTR(ioctl(fd, UVCIOC_CTRL_MAP, &kLogitechTiltAbsoluteMapping));
  }

  struct v4l2_control v4l2_ctrl = {control_id, value};
  int res = HANDLE_EINTR(ioctl(fd, VIDIOC_S_CTRL, &v4l2_ctrl)) >= 0;
  return res >= 0;
}

bool V4L2Webcam::GetWebcamParameter(int fd,
                                    uint32_t control_id,
                                    int* value,
                                    int* min_value,
                                    int* max_value) {
  // Try to query current value for |control_id|. The getter fails if not
  // supported.
  struct v4l2_control v4l2_ctrl = {control_id};
  if (HANDLE_EINTR(ioctl(fd, VIDIOC_G_CTRL, &v4l2_ctrl)))
    return false;
  *value = v4l2_ctrl.value;

  // Try to query the valid range for |control_id|. Not supporting a range query
  // is not a failure.
  struct v4l2_queryctrl v4l2_range_query = {control_id};
  if (HANDLE_EINTR(ioctl(fd, VIDIOC_QUERYCTRL, &v4l2_range_query))) {
    *min_value = 0;
    *max_value = 0;
  } else {
    *min_value = v4l2_range_query.minimum;
    *max_value = v4l2_range_query.maximum;
  }

  return true;
}

void V4L2Webcam::GetPan(const GetPTZCompleteCallback& callback) {
  int value = 0;
  int min_value = 0;
  int max_value = 0;
  bool success = GetWebcamParameter(fd_.get(), V4L2_CID_PAN_ABSOLUTE, &value,
                                    &min_value, &max_value);

  callback.Run(success, value, min_value, max_value);
}

void V4L2Webcam::GetTilt(const GetPTZCompleteCallback& callback) {
  int value = 0;
  int min_value = 0;
  int max_value = 0;
  bool success = GetWebcamParameter(fd_.get(), V4L2_CID_TILT_ABSOLUTE, &value,
                                    &min_value, &max_value);
  callback.Run(success, value, min_value, max_value);
}

void V4L2Webcam::GetZoom(const GetPTZCompleteCallback& callback) {
  int value = 0;
  int min_value = 0;
  int max_value = 0;
  bool success = GetWebcamParameter(fd_.get(), V4L2_CID_ZOOM_ABSOLUTE, &value,
                                    &min_value, &max_value);
  callback.Run(success, value, min_value, max_value);
}

void V4L2Webcam::GetFocus(const GetPTZCompleteCallback& callback) {
  int value = 0;
  int min_value = 0;
  int max_value = 0;
  bool success = GetWebcamParameter(fd_.get(), V4L2_CID_FOCUS_ABSOLUTE, &value,
                                    &min_value, &max_value);
  callback.Run(success, value, min_value, max_value);
}

void V4L2Webcam::SetPan(int value,
                        int pan_speed,
                        const SetPTZCompleteCallback& callback) {
  callback.Run(SetWebcamParameter(fd_.get(), V4L2_CID_PAN_ABSOLUTE, value));
}

void V4L2Webcam::SetTilt(int value,
                         int tilt_speed,
                         const SetPTZCompleteCallback& callback) {
  callback.Run(SetWebcamParameter(fd_.get(), V4L2_CID_TILT_ABSOLUTE, value));
}

void V4L2Webcam::SetZoom(int value, const SetPTZCompleteCallback& callback) {
  callback.Run(SetWebcamParameter(fd_.get(), V4L2_CID_ZOOM_ABSOLUTE, value));
}

void V4L2Webcam::SetFocus(int value, const SetPTZCompleteCallback& callback) {
  callback.Run(SetWebcamParameter(fd_.get(), V4L2_CID_FOCUS_ABSOLUTE, value));
}

void V4L2Webcam::SetAutofocusState(AutofocusState state,
                                   const SetPTZCompleteCallback& callback) {
  const int value = (state == AUTOFOCUS_ON) ? 1 : 0;
  callback.Run(SetWebcamParameter(fd_.get(), V4L2_CID_FOCUS_AUTO, value));
}

void V4L2Webcam::SetPanDirection(PanDirection direction,
                                 int pan_speed,
                                 const SetPTZCompleteCallback& callback) {
  int direction_value = 0;
  switch (direction) {
    case PAN_STOP:
      direction_value = 0;
      break;

    case PAN_RIGHT:
      direction_value = 1;
      break;

    case PAN_LEFT:
      direction_value = -1;
      break;
  }
  callback.Run(
      SetWebcamParameter(fd_.get(), V4L2_CID_PAN_SPEED, direction_value));
}

void V4L2Webcam::SetTiltDirection(TiltDirection direction,
                                  int tilt_speed,
                                  const SetPTZCompleteCallback& callback) {
  int direction_value = 0;
  switch (direction) {
    case TILT_STOP:
      direction_value = 0;
      break;

    case TILT_UP:
      direction_value = 1;
      break;

    case TILT_DOWN:
      direction_value = -1;
      break;
  }
  callback.Run(
      SetWebcamParameter(fd_.get(), V4L2_CID_TILT_SPEED, direction_value));
}

void V4L2Webcam::SetHome(const SetPTZCompleteCallback& callback) {
  if (EnsureLogitechCommandsMapped()) {
    if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD,
                            kLogitechMenuIndexSetHome)) {
      LOG(WARNING) << "Failed to set HOME preset for Logitech webcam";
      callback.Run(false);
      return;
    }
    callback.Run(true);
    return;
  }
  if (EnsureAverCommandsMapped()) {
    if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD,
                            kAverMenuIndexSetHome)) {
      LOG(WARNING) << "Failed to set HOME preset for AVer CAM webcam";
      callback.Run(false);
      return;
    }
    callback.Run(true);
    return;
  }
  LOG(WARNING) << "Active webcam does not currently support seting HOME preset";
  callback.Run(false);
}

int getLogitechPresetCmdMenuIndex(int preset_number, bool set_preset) {
  // Preset must be within range [1, 8]
  if (preset_number < 1 || preset_number > 8) {
    LOG(WARNING) << "Preset number index for Logitech camera out of range.";
    return -1;
  }

  int set_preset_index = preset_number * 2 + 1;
  return set_preset ? set_preset_index : set_preset_index + 1;
}

int getAverPresetCmdMenuIndex(int preset_number, bool set_preset) {
  // Preset must be within range [0, 9]
  if (preset_number < 0 || preset_number > 9) {
    LOG(WARNING) << "Preset number index for AVer camera out of range.";
    return -1;
  }

  int set_preset_index = 2 * preset_number;
  return set_preset ? set_preset_index : set_preset_index + 1;
}

void V4L2Webcam::RestoreCameraPreset(int preset_number,
                                     const SetPTZCompleteCallback& callback) {
  if (EnsureLogitechCommandsMapped()) {
    int menu_index = getLogitechPresetCmdMenuIndex(preset_number, false);

    if (menu_index < 0) {
      callback.Run(false);
      return;
    }

    if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD, menu_index)) {
      LOG(WARNING) << "Failed to call preset " << preset_number
                   << " for Logitech webcam";
      callback.Run(false);
      return;
    }
    callback.Run(true);
    return;
  } else if (EnsureAverCommandsMapped()) {
    int menu_index = getAverPresetCmdMenuIndex(preset_number, false);

    if (menu_index < 0) {
      callback.Run(false);
      return;
    }

    if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD, menu_index)) {
      LOG(WARNING) << "Failed to call preset " << preset_number
                   << " for AVer webcam";
      callback.Run(false);
      return;
    }
    callback.Run(true);
    return;
  }

  LOG(WARNING)
      << "Active webcam does not currently support calling camera presets.";
  callback.Run(false);
}

void V4L2Webcam::SetCameraPreset(int preset_number,
                                 const SetPTZCompleteCallback& callback) {
  if (EnsureLogitechCommandsMapped()) {
    int menu_index = getLogitechPresetCmdMenuIndex(preset_number, true);

    if (menu_index < 0) {
      callback.Run(false);
      return;
    }

    if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD, menu_index)) {
      LOG(WARNING) << "Failed to set preset " << preset_number
                   << " for Logitech webcam";
      callback.Run(false);
      return;
    }
    callback.Run(true);
    return;
  } else if (EnsureAverCommandsMapped()) {
    int menu_index = getAverPresetCmdMenuIndex(preset_number, true);

    if (menu_index < 0) {
      callback.Run(false);
      return;
    }

    if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD, menu_index)) {
      LOG(WARNING) << "Failed to set preset " << preset_number
                   << " for AVer webcam";
      callback.Run(false);
      return;
    }
    callback.Run(true);
    return;
  }

  LOG(WARNING)
      << "Active webcam does not currently support setting camera presets.";
  callback.Run(false);
}

void V4L2Webcam::Reset(bool pan,
                       bool tilt,
                       bool zoom,
                       const SetPTZCompleteCallback& callback) {
  if (pan || tilt) {
    if (EnsureLogitechCommandsMapped()) {
      if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD,
                              kLogitechMenuIndexGoHome)) {
        callback.Run(false);
        return;
      }
    } else if (EnsureAverCommandsMapped()) {
      if (!SetWebcamParameter(fd_.get(), V4L2_CID_PANTILT_CMD,
                              kAverMenuIndexGoHome)) {
        callback.Run(false);
        return;
      }
    } else {
      if (pan) {
        struct v4l2_control v4l2_ctrl = {V4L2_CID_PAN_RESET};
        if (!HANDLE_EINTR(ioctl(fd_.get(), VIDIOC_S_CTRL, &v4l2_ctrl))) {
          callback.Run(false);
          return;
        }
      }

      if (tilt) {
        struct v4l2_control v4l2_ctrl = {V4L2_CID_TILT_RESET};
        if (!HANDLE_EINTR(ioctl(fd_.get(), VIDIOC_S_CTRL, &v4l2_ctrl))) {
          callback.Run(false);
          return;
        }
      }
    }
  }

  if (zoom) {
    const int kDefaultZoom = 100;
    if (!SetWebcamParameter(fd_.get(), V4L2_CID_ZOOM_ABSOLUTE, kDefaultZoom)) {
      callback.Run(false);
      return;
    }
  }

  callback.Run(true);
}

}  // namespace extensions