| #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, int sub) : |
| resizeAfterOpen(false), timingsError(false), fd(-1), frameRate(fr), |
| lastFrameIndex(-1), height(600), width(800), subSampling(sub), |
| 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) |
| { |
| if (!timingsError) |
| { |
| log<level::ERR>("Failed to query timings", |
| entry("ERROR=%s", strerror(errno))); |
| timingsError = true; |
| } |
| |
| restart(); |
| return false; |
| } |
| else |
| { |
| timingsError = 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, restart", |
| entry("ERROR=%s", strerror(errno))); |
| restart(); |
| return; |
| } |
| |
| 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; |
| v4l2_control ctrl; |
| |
| if (fd >= 0) |
| { |
| return; |
| } |
| |
| input.sendWakeupPacket(); |
| |
| 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))); |
| } |
| |
| ctrl.id = V4L2_CID_JPEG_CHROMA_SUBSAMPLING; |
| ctrl.value = subSampling |
| ? V4L2_JPEG_CHROMA_SUBSAMPLING_420 : V4L2_JPEG_CHROMA_SUBSAMPLING_444; |
| rc = ioctl(fd, VIDIOC_S_CTRL, &ctrl); |
| if (rc < 0) |
| { |
| log<level::WARNING>("Failed to set video jpeg subsampling", |
| 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 |