Commit 9b3a179c authored by mcasas's avatar mcasas Committed by Commit bot

Image Capture v4l2: reset all user controls to default values when closing device fd

This CL adds logic for resetting all User Controls to its default
value upon closing the device file descriptor; otherwise, as the
bug proves, they persist across camera uses, e.g. the zoom level
is unchanged.

This would be relatively simple except for the fact that controls
come in two types: user controls and camera controls, and
that some controls need another one to be set or cleared
beforehand (e.g. white_balance_auto must be set to off
before white_balance can be set, etc).

See [1] for Spec
[1]  https://www.linuxtv.org/downloads/legacy/video4linux/API/V4L2_API/spec-single/v4l2.html#control

BUG=662616

Review-Url: https://codereview.chromium.org/2479413002
Cr-Commit-Position: refs/heads/master@{#437695}
parent 3698b0a0
......@@ -172,6 +172,7 @@ test("capture_unittests") {
"content/video_capture_oracle_unittest.cc",
"video/fake_video_capture_device_unittest.cc",
"video/linux/camera_facing_chromeos_unittest.cc",
"video/linux/v4l2_capture_delegate_unittest.cc",
"video/mac/video_capture_device_factory_mac_unittest.mm",
"video/video_capture_device_unittest.cc",
]
......
......@@ -5,6 +5,7 @@
#include "media/capture/video/linux/v4l2_capture_delegate.h"
#include <linux/version.h>
#include <linux/videodev2.h>
#include <poll.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
......@@ -77,6 +78,16 @@ static struct {
{V4L2_PIX_FMT_JPEG, PIXEL_FORMAT_MJPEG, 1},
};
// Maximum number of ioctl retries before giving up trying to reset controls.
const int kMaxIOCtrlRetries = 5;
// Base id and class identifier for Controls to be reset.
static struct {
uint32_t control_base;
uint32_t class_id;
} const kControls[] = {{V4L2_CID_USER_BASE, V4L2_CID_USER_CLASS},
{V4L2_CID_CAMERA_CLASS_BASE, V4L2_CID_CAMERA_CLASS}};
// Fill in |format| with the given parameters.
static void FillV4L2Format(v4l2_format* format,
uint32_t width,
......@@ -134,6 +145,128 @@ static mojom::RangePtr RetrieveUserControlRange(int device_fd, int control_id) {
return capability;
}
// Determines if |control_id| is special, i.e. controls another one's state.
static bool IsSpecialControl(int control_id) {
switch (control_id) {
case V4L2_CID_AUTO_WHITE_BALANCE:
case V4L2_CID_EXPOSURE_AUTO:
case V4L2_CID_EXPOSURE_AUTO_PRIORITY:
case V4L2_CID_FOCUS_AUTO:
return true;
}
return false;
}
// Sets all user control to their default. Some controls are enabled by another
// flag, usually having the word "auto" in the name, see IsSpecialControl().
// These flags are preset beforehand, then set to their defaults individually
// afterwards.
static void ResetUserAndCameraControlsToDefault(int device_fd) {
// Set V4L2_CID_AUTO_WHITE_BALANCE to false first.
v4l2_control auto_white_balance = {};
auto_white_balance.id = V4L2_CID_AUTO_WHITE_BALANCE;
auto_white_balance.value = false;
int num_retries = 0;
// Setting up the first control right after stopping streaming seems
// not to the work the first time, so retry a few times.
for (; num_retries < kMaxIOCtrlRetries &&
HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_CTRL, &auto_white_balance)) < 0;
++num_retries) {
DPLOG(WARNING) << "VIDIOC_S_CTRL";
}
if (num_retries == kMaxIOCtrlRetries) {
DLOG(ERROR) << "Cannot access device controls";
return;
}
std::vector<struct v4l2_ext_control> special_camera_controls;
// Set V4L2_CID_EXPOSURE_AUTO to V4L2_EXPOSURE_MANUAL.
v4l2_ext_control auto_exposure = {};
auto_exposure.id = V4L2_CID_EXPOSURE_AUTO;
auto_exposure.value = V4L2_EXPOSURE_MANUAL;
special_camera_controls.push_back(auto_exposure);
// Set V4L2_CID_EXPOSURE_AUTO_PRIORITY to false.
v4l2_ext_control priority_auto_exposure = {};
priority_auto_exposure.id = V4L2_CID_EXPOSURE_AUTO_PRIORITY;
priority_auto_exposure.value = false;
special_camera_controls.push_back(priority_auto_exposure);
// Set V4L2_CID_FOCUS_AUTO to false.
v4l2_ext_control auto_focus = {};
auto_focus.id = V4L2_CID_FOCUS_AUTO;
auto_focus.value = false;
special_camera_controls.push_back(auto_focus);
struct v4l2_ext_controls ext_controls = {};
ext_controls.ctrl_class = V4L2_CID_CAMERA_CLASS;
ext_controls.count = special_camera_controls.size();
ext_controls.controls = special_camera_controls.data();
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_EXT_CTRLS, &ext_controls)) < 0)
DPLOG(ERROR) << "VIDIOC_S_EXT_CTRLS";
std::vector<struct v4l2_ext_control> camera_controls;
for (const auto& control : kControls) {
std::vector<struct v4l2_ext_control> camera_controls;
v4l2_queryctrl range = {};
range.id = control.control_base | V4L2_CTRL_FLAG_NEXT_CTRL;
while (0 == HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range))) {
if (V4L2_CTRL_ID2CLASS(range.id) != V4L2_CTRL_ID2CLASS(control.class_id))
break;
range.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
if (IsSpecialControl(range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL))
continue;
struct v4l2_ext_control ext_control = {};
ext_control.id = range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL;
ext_control.value = range.default_value;
camera_controls.push_back(ext_control);
}
if (!camera_controls.empty()) {
struct v4l2_ext_controls ext_controls = {};
ext_controls.ctrl_class = control.class_id;
ext_controls.count = camera_controls.size();
ext_controls.controls = camera_controls.data();
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_EXT_CTRLS, &ext_controls)) < 0)
DPLOG(ERROR) << "VIDIOC_S_EXT_CTRLS";
}
}
// Now set the special flags to the default values
v4l2_queryctrl range = {};
range.id = V4L2_CID_AUTO_WHITE_BALANCE;
HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range));
auto_white_balance.value = range.default_value;
HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_CTRL, &auto_white_balance));
special_camera_controls.clear();
memset(&range, 0, sizeof(struct v4l2_queryctrl));
range.id = V4L2_CID_EXPOSURE_AUTO;
HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range));
auto_exposure.value = range.default_value;
special_camera_controls.push_back(auto_exposure);
memset(&range, 0, sizeof(struct v4l2_queryctrl));
range.id = V4L2_CID_EXPOSURE_AUTO_PRIORITY;
HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range));
priority_auto_exposure.value = range.default_value;
special_camera_controls.push_back(priority_auto_exposure);
memset(&range, 0, sizeof(struct v4l2_queryctrl));
range.id = V4L2_CID_FOCUS_AUTO;
HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range));
auto_focus.value = range.default_value;
special_camera_controls.push_back(auto_focus);
memset(&ext_controls, 0, sizeof(struct v4l2_ext_controls));
ext_controls.ctrl_class = V4L2_CID_CAMERA_CLASS;
ext_controls.count = special_camera_controls.size();
ext_controls.controls = special_camera_controls.data();
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_EXT_CTRLS, &ext_controls)) < 0)
DPLOG(ERROR) << "VIDIOC_S_EXT_CTRLS";
}
// Class keeping track of a SPLANE V4L2 buffer, mmap()ed on construction and
// munmap()ed on destruction.
class V4L2CaptureDelegate::BufferTracker
......@@ -355,6 +488,7 @@ void V4L2CaptureDelegate::StopAndDeAllocate() {
if (HANDLE_EINTR(ioctl(device_fd_.get(), VIDIOC_REQBUFS, &r_buffer)) < 0)
SetErrorState(FROM_HERE, "Failed to VIDIOC_REQBUFS with count = 0");
ResetUserAndCameraControlsToDefault(device_fd_.get());
// At this point we can close the device.
// This is also needed for correctly changing settings later via VIDIOC_S_FMT.
device_fd_.reset();
......
......@@ -30,7 +30,7 @@ namespace media {
// capture specifics are implemented in derived classes. Created and destroyed
// on the owner's thread, otherwise living and operating on |v4l2_task_runner_|.
// TODO(mcasas): Make this class a non-ref-counted.
class V4L2CaptureDelegate final
class CAPTURE_EXPORT V4L2CaptureDelegate final
: public base::RefCountedThreadSafe<V4L2CaptureDelegate> {
public:
// Retrieves the #planes for a given |fourcc|, or 0 if unknown.
......@@ -65,6 +65,8 @@ class V4L2CaptureDelegate final
void SetRotation(int rotation);
private:
friend class V4L2CaptureDelegateTest;
friend class base::RefCountedThreadSafe<V4L2CaptureDelegate>;
~V4L2CaptureDelegate();
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include "base/files/file_enumerator.h"
#include "base/run_loop.h"
#include "base/threading/thread_task_runner_handle.h"
#include "media/capture/video/linux/v4l2_capture_delegate.h"
#include "media/capture/video/video_capture_device.h"
#include "media/capture/video/video_capture_device_descriptor.h"
#include "media/capture/video_capture_types.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
namespace media {
namespace {
ACTION_P(RunClosure, closure) {
closure.Run();
}
// Base id and class identifiers for Controls to be modified and later tested
// agains default values.
static struct {
uint32_t control_base;
uint32_t class_id;
} const kControls[] = {{V4L2_CID_USER_BASE, V4L2_CID_USER_CLASS},
{V4L2_CID_CAMERA_CLASS_BASE, V4L2_CID_CAMERA_CLASS}};
// Determines if |control_id| is special, i.e. controls another one's state.
static bool IsSpecialControl(int control_id) {
switch (control_id) {
case V4L2_CID_AUTO_WHITE_BALANCE:
case V4L2_CID_EXPOSURE_AUTO:
case V4L2_CID_EXPOSURE_AUTO_PRIORITY:
case V4L2_CID_FOCUS_AUTO:
return true;
}
return false;
}
static void SetControlsToMaxValues(int device_fd) {
// Set V4L2_CID_AUTO_WHITE_BALANCE to false first.
v4l2_control auto_white_balance = {};
auto_white_balance.id = V4L2_CID_AUTO_WHITE_BALANCE;
auto_white_balance.value = false;
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_CTRL, &auto_white_balance)) < 0)
DPLOG(ERROR) << "VIDIOC_S_CTRL";
std::vector<struct v4l2_ext_control> special_camera_controls;
// Set V4L2_CID_EXPOSURE_AUTO to V4L2_EXPOSURE_MANUAL.
v4l2_ext_control auto_exposure = {};
auto_exposure.id = V4L2_CID_EXPOSURE_AUTO;
auto_exposure.value = V4L2_EXPOSURE_MANUAL;
special_camera_controls.push_back(auto_exposure);
// Set V4L2_CID_EXPOSURE_AUTO_PRIORITY to false.
v4l2_ext_control priority_auto_exposure = {};
priority_auto_exposure.id = V4L2_CID_EXPOSURE_AUTO_PRIORITY;
priority_auto_exposure.value = false;
special_camera_controls.push_back(priority_auto_exposure);
// Set V4L2_CID_FOCUS_AUTO to false.
v4l2_ext_control auto_focus = {};
auto_focus.id = V4L2_CID_FOCUS_AUTO;
auto_focus.value = false;
special_camera_controls.push_back(auto_focus);
struct v4l2_ext_controls ext_controls = {};
ext_controls.ctrl_class = V4L2_CID_CAMERA_CLASS;
ext_controls.count = special_camera_controls.size();
ext_controls.controls = special_camera_controls.data();
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_EXT_CTRLS, &ext_controls)) < 0)
DPLOG(ERROR) << "VIDIOC_S_EXT_CTRLS";
for (const auto& control : kControls) {
std::vector<struct v4l2_ext_control> camera_controls;
v4l2_queryctrl range = {};
range.id = control.control_base | V4L2_CTRL_FLAG_NEXT_CTRL;
while (0 == HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range))) {
if (V4L2_CTRL_ID2CLASS(range.id) != V4L2_CTRL_ID2CLASS(control.class_id))
break;
range.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
if (IsSpecialControl(range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL))
continue;
DVLOG(1) << __func__ << " " << range.name << " set to " << range.maximum;
struct v4l2_ext_control ext_control = {};
ext_control.id = range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL;
ext_control.value = range.maximum;
camera_controls.push_back(ext_control);
}
if (!camera_controls.empty()) {
struct v4l2_ext_controls ext_controls = {};
ext_controls.ctrl_class = control.class_id;
ext_controls.count = camera_controls.size();
ext_controls.controls = camera_controls.data();
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_S_EXT_CTRLS, &ext_controls)) < 0)
DPLOG(ERROR) << "VIDIOC_S_EXT_CTRLS";
}
range.id = control.control_base | V4L2_CTRL_FLAG_NEXT_CTRL;
while (0 == HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range))) {
if (V4L2_CTRL_ID2CLASS(range.id) != V4L2_CTRL_ID2CLASS(control.class_id))
break;
range.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
if (IsSpecialControl(range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL))
continue;
DVLOG(1) << __func__ << " " << range.name << " set to " << range.maximum;
v4l2_control readback = {};
readback.id = range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL;
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_G_CTRL, &readback)) < 0)
DPLOG(ERROR) << range.name << ", failed to be read.";
EXPECT_EQ(range.maximum, readback.value) << " control " << range.name
<< " didnt set correctly";
}
}
}
static void VerifyUserControlsAreSetToDefaultValues(int device_fd) {
for (const auto& control : kControls) {
v4l2_queryctrl range = {};
range.id = control.control_base | V4L2_CTRL_FLAG_NEXT_CTRL;
while (0 == HANDLE_EINTR(ioctl(device_fd, VIDIOC_QUERYCTRL, &range))) {
if (V4L2_CTRL_ID2CLASS(range.id) != V4L2_CTRL_ID2CLASS(control.class_id))
break;
range.id |= V4L2_CTRL_FLAG_NEXT_CTRL;
DVLOG(1) << __func__ << " " << range.name << ": " << range.minimum << "-"
<< range.maximum << ", default: " << range.default_value;
v4l2_control current = {};
current.id = range.id & ~V4L2_CTRL_FLAG_NEXT_CTRL;
if (HANDLE_EINTR(ioctl(device_fd, VIDIOC_G_CTRL, &current)) < 0)
DPLOG(ERROR) << "control " << range.name;
EXPECT_EQ(range.default_value, current.value);
}
}
}
class MockVideoCaptureDeviceClient : public VideoCaptureDevice::Client {
public:
MOCK_METHOD7(OnIncomingCapturedData,
void(const uint8_t*,
int,
const VideoCaptureFormat&,
int,
base::TimeTicks,
base::TimeDelta,
int));
MOCK_METHOD4(ReserveOutputBuffer,
std::unique_ptr<Buffer>(const gfx::Size&,
media::VideoPixelFormat,
media::VideoPixelStorage,
int));
void OnIncomingCapturedBuffer(std::unique_ptr<Buffer> buffer,
const VideoCaptureFormat& frame_format,
base::TimeTicks reference_time,
base::TimeDelta timestamp) override {
DoOnIncomingCapturedBuffer();
}
MOCK_METHOD0(DoOnIncomingCapturedBuffer, void(void));
void OnIncomingCapturedVideoFrame(
std::unique_ptr<Buffer> buffer,
scoped_refptr<media::VideoFrame> frame) override {
DoOnIncomingCapturedVideoFrame();
}
MOCK_METHOD0(DoOnIncomingCapturedVideoFrame, void(void));
MOCK_METHOD4(ResurrectLastOutputBuffer,
std::unique_ptr<Buffer>(const gfx::Size&,
VideoPixelFormat,
VideoPixelStorage,
int));
MOCK_METHOD2(OnError,
void(const tracked_objects::Location& from_here,
const std::string& reason));
MOCK_CONST_METHOD0(GetBufferPoolUtilization, double(void));
};
class V4L2CaptureDelegateTest : public ::testing::Test {
public:
V4L2CaptureDelegateTest()
: device_descriptor_("Device 0", "/dev/video0"),
delegate_(new V4L2CaptureDelegate(device_descriptor_,
base::ThreadTaskRunnerHandle::Get(),
50)) {}
~V4L2CaptureDelegateTest() override = default;
base::MessageLoop loop_;
VideoCaptureDeviceDescriptor device_descriptor_;
scoped_refptr<V4L2CaptureDelegate> delegate_;
};
} // anonymous namespace
TEST_F(V4L2CaptureDelegateTest, CreateAndDestroyAndVerifyControls) {
// Check that there is at least a video device, otherwise bail.
const base::FilePath path("/dev/");
base::FileEnumerator enumerator(path, false, base::FileEnumerator::FILES,
"video*");
if (enumerator.Next().empty()) {
DLOG(INFO) << " No devices found, skipping test";
return;
}
// Open device, manipulate user and camera controls, and close it.
{
base::ScopedFD device_fd(
HANDLE_EINTR(open(device_descriptor_.device_id.c_str(), O_RDWR)));
ASSERT_TRUE(device_fd.is_valid());
SetControlsToMaxValues(device_fd.get());
base::RunLoop().RunUntilIdle();
}
// Start and stop capturing, triggering the resetting of user and camera
// control values.
{
std::unique_ptr<MockVideoCaptureDeviceClient> client(
new MockVideoCaptureDeviceClient());
MockVideoCaptureDeviceClient* client_ptr = client.get();
delegate_->AllocateAndStart(320 /* width */, 240 /* height */,
10.0 /* frame_rate */, std::move(client));
base::RunLoop run_loop;
base::Closure quit_closure = run_loop.QuitClosure();
EXPECT_CALL(*client_ptr, OnIncomingCapturedData(_, _, _, _, _, _, _))
.Times(1)
.WillOnce(RunClosure(quit_closure));
run_loop.Run();
delegate_->StopAndDeAllocate();
base::RunLoop().RunUntilIdle();
}
// Reopen the device and verify all user and camera controls should be back to
// their |default_value|s.
{
base::ScopedFD device_fd(
HANDLE_EINTR(open(device_descriptor_.device_id.c_str(), O_RDWR)));
ASSERT_TRUE(device_fd.is_valid());
VerifyUserControlsAreSetToDefaultValues(device_fd.get());
}
}
}; // namespace media
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment