Add V4L2 video class implementation
Change-Id: I9fd9cdbc711fb48de542efcaf02d0630ae0872b2
Signed-off-by: Eddie James <eajames@linux.ibm.com>
diff --git a/ikvm_video.cpp b/ikvm_video.cpp
index 46505a8..db074a7 100644
--- a/ikvm_video.cpp
+++ b/ikvm_video.cpp
@@ -1,15 +1,495 @@
#include "ikvm_video.hpp"
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/videodev2.h>
+#include <poll.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <phosphor-logging/elog-errors.hpp>
+#include <phosphor-logging/elog.hpp>
+#include <phosphor-logging/log.hpp>
+#include <xyz/openbmc_project/Common/Device/error.hpp>
+#include <xyz/openbmc_project/Common/File/error.hpp>
+
namespace ikvm
{
+const int Video::bitsPerSample(8);
+const int Video::bytesPerPixel(4);
+const int Video::samplesPerPixel(3);
+
+using namespace phosphor::logging;
+using namespace sdbusplus::xyz::openbmc_project::Common::File::Error;
+using namespace sdbusplus::xyz::openbmc_project::Common::Device::Error;
+
Video::Video(const std::string& p, Input& input, int fr) :
+ resizeAfterOpen(false), fd(-1), frameRate(fr), lastFrameIndex(-1),
height(600), width(800), input(input), path(p)
{
}
Video::~Video()
{
+ stop();
+}
+
+char* Video::getData()
+{
+ if (lastFrameIndex >= 0)
+ {
+ return (char*)buffers[lastFrameIndex].data;
+ }
+
+ return nullptr;
+}
+
+void Video::getFrame()
+{
+ int rc(0);
+ int fd_flags;
+ v4l2_buffer buf;
+ fd_set fds;
+ timeval tv;
+
+ if (fd < 0)
+ {
+ return;
+ }
+
+ FD_ZERO(&fds);
+ FD_SET(fd, &fds);
+
+ tv.tv_sec = 1;
+ tv.tv_usec = 0;
+
+ memset(&buf, 0, sizeof(v4l2_buffer));
+ buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = V4L2_MEMORY_MMAP;
+
+ // Switch to non-blocking in order to safely dequeue all buffers; if the
+ // video signal is lost while blocking to dequeue, the video driver may
+ // wait forever if signal is not re-acquired
+ fd_flags = fcntl(fd, F_GETFL);
+ fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK);
+
+ rc = select(fd + 1, &fds, NULL, NULL, &tv);
+ if (rc > 0)
+ {
+ do
+ {
+ rc = ioctl(fd, VIDIOC_DQBUF, &buf);
+ if (rc >= 0)
+ {
+ buffers[buf.index].queued = false;
+
+ if (!(buf.flags & V4L2_BUF_FLAG_ERROR))
+ {
+ lastFrameIndex = buf.index;
+ buffers[lastFrameIndex].payload = buf.bytesused;
+ break;
+ }
+ else
+ {
+ buffers[buf.index].payload = 0;
+ }
+ }
+ } while (rc >= 0);
+ }
+
+ fcntl(fd, F_SETFL, fd_flags);
+
+ for (unsigned int i = 0; i < buffers.size(); ++i)
+ {
+ if (i == (unsigned int)lastFrameIndex)
+ {
+ continue;
+ }
+
+ if (!buffers[i].queued)
+ {
+ memset(&buf, 0, sizeof(v4l2_buffer));
+ buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = V4L2_MEMORY_MMAP;
+ buf.index = i;
+
+ rc = ioctl(fd, VIDIOC_QBUF, &buf);
+ if (rc)
+ {
+ log<level::ERR>("Failed to queue buffer",
+ entry("ERROR=%s", strerror(errno)));
+ }
+ else
+ {
+ buffers[i].queued = true;
+ }
+ }
+ }
+}
+
+bool Video::needsResize()
+{
+ int rc;
+ v4l2_dv_timings timings;
+
+ if (fd < 0)
+ {
+ return false;
+ }
+
+ if (resizeAfterOpen)
+ {
+ return true;
+ }
+
+ memset(&timings, 0, sizeof(v4l2_dv_timings));
+ rc = ioctl(fd, VIDIOC_QUERY_DV_TIMINGS, &timings);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to query timings",
+ entry("ERROR=%s", strerror(errno)));
+ return false;
+ }
+
+ if (timings.bt.width != width || timings.bt.height != height)
+ {
+ width = timings.bt.width;
+ height = timings.bt.height;
+
+ if (!width || !height)
+ {
+ log<level::ERR>("Failed to get new resolution",
+ entry("WIDTH=%d", width),
+ entry("HEIGHT=%d", height));
+ elog<Open>(
+ xyz::openbmc_project::Common::File::Open::ERRNO(-EPROTO),
+ xyz::openbmc_project::Common::File::Open::PATH(path.c_str()));
+ }
+
+ lastFrameIndex = -1;
+ return true;
+ }
+
+ return false;
+}
+
+void Video::resize()
+{
+ int rc;
+ unsigned int i;
+ bool needsResizeCall(false);
+ v4l2_buf_type type(V4L2_BUF_TYPE_VIDEO_CAPTURE);
+ v4l2_requestbuffers req;
+
+ if (fd < 0)
+ {
+ return;
+ }
+
+ if (resizeAfterOpen)
+ {
+ resizeAfterOpen = false;
+ return;
+ }
+
+ for (i = 0; i < buffers.size(); ++i)
+ {
+ if (buffers[i].data)
+ {
+ needsResizeCall = true;
+ break;
+ }
+ }
+
+ if (needsResizeCall)
+ {
+ rc = ioctl(fd, VIDIOC_STREAMOFF, &type);
+ if (rc)
+ {
+ log<level::ERR>("Failed to stop streaming",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+ }
+
+ for (i = 0; i < buffers.size(); ++i)
+ {
+ if (buffers[i].data)
+ {
+ munmap(buffers[i].data, buffers[i].size);
+ buffers[i].data = nullptr;
+ buffers[i].queued = false;
+ }
+ }
+
+ if (needsResizeCall)
+ {
+ v4l2_dv_timings timings;
+
+ memset(&req, 0, sizeof(v4l2_requestbuffers));
+ req.count = 0;
+ req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ req.memory = V4L2_MEMORY_MMAP;
+ rc = ioctl(fd, VIDIOC_REQBUFS, &req);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to zero streaming buffers",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ memset(&timings, 0, sizeof(v4l2_dv_timings));
+ rc = ioctl(fd, VIDIOC_QUERY_DV_TIMINGS, &timings);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to query timings",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ rc = ioctl(fd, VIDIOC_S_DV_TIMINGS, &timings);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to set timings",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ buffers.clear();
+ }
+
+ memset(&req, 0, sizeof(v4l2_requestbuffers));
+ req.count = 3;
+ req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ req.memory = V4L2_MEMORY_MMAP;
+ rc = ioctl(fd, VIDIOC_REQBUFS, &req);
+ if (rc < 0 || req.count < 2)
+ {
+ log<level::ERR>("Failed to request streaming buffers",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_ERRNO(
+ errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ buffers.resize(req.count);
+
+ for (i = 0; i < buffers.size(); ++i)
+ {
+ v4l2_buffer buf;
+
+ memset(&buf, 0, sizeof(v4l2_buffer));
+ buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ buf.memory = V4L2_MEMORY_MMAP;
+ buf.index = i;
+
+ rc = ioctl(fd, VIDIOC_QUERYBUF, &buf);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to query buffer",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ buffers[i].data = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
+ MAP_SHARED, fd, buf.m.offset);
+ if (buffers[i].data == MAP_FAILED)
+ {
+ log<level::ERR>("Failed to mmap buffer",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ buffers[i].size = buf.length;
+
+ rc = ioctl(fd, VIDIOC_QBUF, &buf);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to queue buffer",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_ERRNO(errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ buffers[i].queued = true;
+ }
+
+ rc = ioctl(fd, VIDIOC_STREAMON, &type);
+ if (rc)
+ {
+ log<level::ERR>("Failed to start streaming",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_ERRNO(
+ errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+}
+
+void Video::start()
+{
+ int rc;
+ size_t oldHeight = height;
+ size_t oldWidth = width;
+ v4l2_capability cap;
+ v4l2_format fmt;
+ v4l2_streamparm sparm;
+
+ if (fd >= 0)
+ {
+ return;
+ }
+
+ fd = open(path.c_str(), O_RDWR);
+ if (fd < 0)
+ {
+ unsigned short xx = SHRT_MAX;
+ char wakeupReport[6] = {0};
+
+ wakeupReport[0] = 2;
+ memcpy(&wakeupReport[2], &xx, 2);
+
+ input.sendRaw(wakeupReport, 6);
+
+ fd = open(path.c_str(), O_RDWR);
+ if (fd < 0)
+ {
+ log<level::ERR>("Failed to open video device",
+ entry("PATH=%s", path.c_str()),
+ entry("ERROR=%s", strerror(errno)));
+ elog<Open>(
+ xyz::openbmc_project::Common::File::Open::ERRNO(errno),
+ xyz::openbmc_project::Common::File::Open::PATH(path.c_str()));
+ }
+ }
+
+ memset(&cap, 0, sizeof(v4l2_capability));
+ rc = ioctl(fd, VIDIOC_QUERYCAP, &cap);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to query video device capabilities",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_ERRNO(
+ errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) ||
+ !(cap.capabilities & V4L2_CAP_STREAMING))
+ {
+ log<level::ERR>("Video device doesn't support this application");
+ elog<Open>(
+ xyz::openbmc_project::Common::File::Open::ERRNO(errno),
+ xyz::openbmc_project::Common::File::Open::PATH(path.c_str()));
+ }
+
+ memset(&fmt, 0, sizeof(v4l2_format));
+ fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ rc = ioctl(fd, VIDIOC_G_FMT, &fmt);
+ if (rc < 0)
+ {
+ log<level::ERR>("Failed to query video device format",
+ entry("ERROR=%s", strerror(errno)));
+ elog<ReadFailure>(
+ xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_ERRNO(
+ errno),
+ xyz::openbmc_project::Common::Device::ReadFailure::
+ CALLOUT_DEVICE_PATH(path.c_str()));
+ }
+
+ memset(&sparm, 0, sizeof(v4l2_streamparm));
+ sparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ sparm.parm.capture.timeperframe.numerator = 1;
+ sparm.parm.capture.timeperframe.denominator = frameRate;
+ rc = ioctl(fd, VIDIOC_S_PARM, &sparm);
+ if (rc < 0)
+ {
+ log<level::WARNING>("Failed to set video device frame rate",
+ entry("ERROR=%s", strerror(errno)));
+ }
+
+ height = fmt.fmt.pix.height;
+ width = fmt.fmt.pix.width;
+
+ resize();
+
+ if (oldHeight != height || oldWidth != width)
+ {
+ resizeAfterOpen = true;
+ }
+}
+
+void Video::stop()
+{
+ int rc;
+ unsigned int i;
+ v4l2_buf_type type(V4L2_BUF_TYPE_VIDEO_CAPTURE);
+
+ if (fd < 0)
+ {
+ return;
+ }
+
+ lastFrameIndex = -1;
+
+ rc = ioctl(fd, VIDIOC_STREAMOFF, &type);
+ if (rc)
+ {
+ log<level::ERR>("Failed to stop streaming",
+ entry("ERROR=%s", strerror(errno)));
+ }
+
+ for (i = 0; i < buffers.size(); ++i)
+ {
+ if (buffers[i].data)
+ {
+ munmap(buffers[i].data, buffers[i].size);
+ buffers[i].data = nullptr;
+ buffers[i].queued = false;
+ }
+ }
+
+ close(fd);
+ fd = -1;
}
} // namespace ikvm
diff --git a/ikvm_video.hpp b/ikvm_video.hpp
index 1ff6c61..0e5f3af 100644
--- a/ikvm_video.hpp
+++ b/ikvm_video.hpp
@@ -2,7 +2,9 @@
#include "ikvm_input.hpp"
+#include <mutex>
#include <string>
+#include <vector>
namespace ikvm
{
@@ -29,6 +31,45 @@
Video& operator=(Video&&) = default;
/*
+ * @brief Gets the video frame data
+ *
+ * @return Pointer to the video frame data
+ */
+ char* getData();
+ /* @brief Performs read to grab latest video frame */
+ void getFrame();
+ /*
+ * @brief Gets whether or not the video frame needs to be resized
+ *
+ * @return Boolean indicating if the frame needs to be resized
+ */
+ bool needsResize();
+ /* @brief Performs the resize and re-allocates framebuffer */
+ void resize();
+ /* @brief Starts streaming from the video device */
+ void start();
+ /* @brief Stops streaming from the video device */
+ void stop();
+
+ /*
+ * @brief Gets the desired video frame rate in frames per second
+ *
+ * @return Value of the desired frame rate
+ */
+ inline int getFrameRate() const
+ {
+ return frameRate;
+ }
+ /*
+ * @brief Gets the size of the video frame data
+ *
+ * @return Value of the size of the video frame data in bytes
+ */
+ inline size_t getFrameSize() const
+ {
+ return buffers[lastFrameIndex].payload;
+ }
+ /*
* @brief Gets the height of the video frame
*
* @return Value of the height of video frame in pixels
@@ -47,7 +88,47 @@
return width;
}
+ /* @brief Number of bits per component of a pixel */
+ static const int bitsPerSample;
+ /* @brief Number of bytes of storage for a pixel */
+ static const int bytesPerPixel;
+ /* @brief Number of components in a pixel (i.e. 3 for RGB pixel) */
+ static const int samplesPerPixel;
+
private:
+ /*
+ * @struct Buffer
+ * @brief Store the address and size of frame data from streaming
+ * operations
+ */
+ struct Buffer
+ {
+ Buffer() : data(nullptr), queued(false), payload(0), size(0)
+ {
+ }
+ ~Buffer() = default;
+ Buffer(const Buffer&) = default;
+ Buffer& operator=(const Buffer&) = default;
+ Buffer(Buffer&&) = default;
+ Buffer& operator=(Buffer&&) = default;
+
+ void* data;
+ bool queued;
+ size_t payload;
+ size_t size;
+ };
+
+ /*
+ * @brief Boolean to indicate whether the resize was triggered during
+ * the open operation
+ */
+ bool resizeAfterOpen;
+ /* @brief File descriptor for the V4L2 video device */
+ int fd;
+ /* @brief Desired frame rate of video stream in frames per second */
+ int frameRate;
+ /* @brief Buffer index for the last video frame */
+ int lastFrameIndex;
/* @brief Height in pixels of the video frame */
size_t height;
/* @brief Width in pixels of the video frame */
@@ -56,6 +137,8 @@
Input& input;
/* @brief Path to the V4L2 video device */
const std::string path;
+ /* @brief Streaming buffer storage */
+ std::vector<Buffer> buffers;
};
} // namespace ikvm