| // Copyright 2021 The Chromium OS 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 "media_capabilities/v4l2.h" |
| |
| #include <fcntl.h> |
| #include <linux/videodev2.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| |
| #include <algorithm> |
| #include <map> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/containers/contains.h> |
| #include <base/files/file_path.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.h> |
| #include <base/posix/eintr_wrapper.h> |
| |
| #include "media_capabilities/common.h" |
| |
| namespace { |
| |
| enum class Codec { |
| kH264 = 0, |
| kVP8, |
| kVP9, |
| kJPEG, |
| kUnknown, |
| }; |
| |
| const char* CodecToString(Codec codec) { |
| switch (codec) { |
| case Codec::kH264: |
| return "H264"; |
| case Codec::kVP8: |
| return "VP8"; |
| case Codec::kVP9: |
| return "VP9"; |
| case Codec::kJPEG: |
| return "JPEG"; |
| default: |
| LOG(FATAL) << "Unknown codec: " << static_cast<int>(codec); |
| return ""; |
| } |
| } |
| |
| Codec GetCodec(uint32_t format) { |
| switch (format) { |
| case V4L2_PIX_FMT_H264: |
| case V4L2_PIX_FMT_H264_SLICE: |
| return Codec::kH264; |
| case V4L2_PIX_FMT_VP8: |
| case V4L2_PIX_FMT_VP8_FRAME: |
| return Codec::kVP8; |
| case V4L2_PIX_FMT_VP9: |
| case V4L2_PIX_FMT_VP9_FRAME: |
| return Codec::kVP9; |
| case V4L2_PIX_FMT_JPEG: |
| case V4L2_PIX_FMT_JPEG_RAW: |
| return Codec::kJPEG; |
| default: |
| return Codec::kUnknown; |
| } |
| } |
| |
| Profile V4L2ProfileToProfile(Codec codec, uint32_t profile) { |
| switch (codec) { |
| case Codec::kH264: |
| switch (profile) { |
| case V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE: |
| case V4L2_MPEG_VIDEO_H264_PROFILE_CONSTRAINED_BASELINE: |
| return Profile::kH264Baseline; |
| case V4L2_MPEG_VIDEO_H264_PROFILE_MAIN: |
| return Profile::kH264Main; |
| case V4L2_MPEG_VIDEO_H264_PROFILE_HIGH: |
| return Profile::kH264High; |
| case V4L2_MPEG_VIDEO_H264_PROFILE_EXTENDED: |
| case V4L2_MPEG_VIDEO_H264_PROFILE_STEREO_HIGH: |
| case V4L2_MPEG_VIDEO_H264_PROFILE_MULTIVIEW_HIGH: |
| break; |
| } |
| break; |
| case Codec::kVP8: |
| switch (profile) { |
| case V4L2_MPEG_VIDEO_VP8_PROFILE_0: |
| case V4L2_MPEG_VIDEO_VP8_PROFILE_1: |
| case V4L2_MPEG_VIDEO_VP8_PROFILE_2: |
| case V4L2_MPEG_VIDEO_VP8_PROFILE_3: |
| return Profile::kVP8; |
| } |
| break; |
| case Codec::kVP9: |
| switch (profile) { |
| case V4L2_MPEG_VIDEO_VP9_PROFILE_0: |
| return Profile::kVP9Profile0; |
| case V4L2_MPEG_VIDEO_VP9_PROFILE_2: |
| return Profile::kVP9Profile2; |
| case V4L2_MPEG_VIDEO_VP9_PROFILE_1: |
| case V4L2_MPEG_VIDEO_VP9_PROFILE_3: |
| break; |
| } |
| break; |
| default: |
| break; |
| } |
| return Profile::kNone; |
| } |
| |
| // Return supported profiles for |codec|. If this function is called, a driver |
| // must support at least one profile because the codec is enumerated by |
| // VIDIOC_ENUM_FMT. |
| std::vector<Profile> GetSupportedProfiles(int device_fd, const Codec codec) { |
| // Since there is only one JPEG profile, there is no API to acquire the |
| // supported JPEG profile. Returns the only JPEG profile. |
| if (codec == Codec::kJPEG) |
| return {Profile::kJPEG}; |
| // TODO(b/189169588): Once drivers support V4L2_CID_MPEG_VIDEO_VP8_PROFILE, |
| // call VIDIOC_QUERYMENU with it. |
| if (codec == Codec::kVP8) |
| return {Profile::kVP8}; |
| |
| uint32_t query_id = 0; |
| switch (codec) { |
| case Codec::kH264: |
| query_id = V4L2_CID_MPEG_VIDEO_H264_PROFILE; |
| break; |
| case Codec::kVP9: |
| query_id = V4L2_CID_MPEG_VIDEO_VP9_PROFILE; |
| break; |
| case Codec::kVP8: |
| case Codec::kJPEG: |
| default: |
| LOG(FATAL) << "Unknown codec: " << static_cast<uint32_t>(codec); |
| return {}; |
| } |
| |
| v4l2_queryctrl query_ctrl; |
| memset(&query_ctrl, 0, sizeof(query_ctrl)); |
| query_ctrl.id = query_id; |
| if (Ioctl(device_fd, VIDIOC_QUERYCTRL, &query_ctrl) != 0) { |
| PLOG(FATAL) << "VIDIOC_QUERYCTRL failed: "; |
| return {}; |
| } |
| |
| std::vector<Profile> profiles; |
| v4l2_querymenu query_menu; |
| memset(&query_menu, 0, sizeof(query_menu)); |
| query_menu.id = query_ctrl.id; |
| for (query_menu.index = query_ctrl.minimum; |
| static_cast<int>(query_menu.index) <= query_ctrl.maximum; |
| query_menu.index++) { |
| if (Ioctl(device_fd, VIDIOC_QUERYMENU, &query_menu) == 0) { |
| const Profile profile = V4L2ProfileToProfile(codec, query_menu.index); |
| if (profile != Profile::kNone && !base::Contains(profiles, profile)) |
| profiles.push_back(profile); |
| } |
| } |
| |
| LOG_IF(FATAL, profiles.empty()) << "No profile is supported even though the " |
| << "codec is enumerated by VIDIOC_ENUM_FMT"; |
| return profiles; |
| } |
| |
| std::pair<int, int> GetMaxResolution(int device_fd, const uint32_t format) { |
| std::pair<int, int> max_resolution(0, 0); |
| v4l2_frmsizeenum frame_size; |
| memset(&frame_size, 0, sizeof(frame_size)); |
| frame_size.pixel_format = format; |
| for (; Ioctl(device_fd, VIDIOC_ENUM_FRAMESIZES, &frame_size) == 0; |
| ++frame_size.index) { |
| if (frame_size.type == V4L2_FRMSIZE_TYPE_DISCRETE) { |
| if (frame_size.discrete.width >= |
| static_cast<uint32_t>(max_resolution.first) && |
| frame_size.discrete.height >= |
| static_cast<uint32_t>(max_resolution.second)) { |
| max_resolution.first = frame_size.discrete.width; |
| max_resolution.second = frame_size.discrete.height; |
| } |
| } else if (frame_size.type == V4L2_FRMSIZE_TYPE_STEPWISE || |
| frame_size.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) { |
| max_resolution.first = frame_size.stepwise.max_width; |
| max_resolution.second = frame_size.stepwise.max_height; |
| break; |
| } |
| } |
| |
| return max_resolution; |
| } |
| |
| std::vector<Capability> GetCapabilitiesInPath(const base::FilePath& path, |
| bool decode) { |
| base::ScopedFD device_fd( |
| HANDLE_EINTR(open(path.value().c_str(), O_RDWR | O_CLOEXEC))); |
| if (!device_fd.is_valid()) |
| return {}; |
| |
| std::vector<uint32_t> formats; |
| v4l2_fmtdesc fmtdesc; |
| memset(&fmtdesc, 0, sizeof(fmtdesc)); |
| fmtdesc.type = decode ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE |
| : V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
| for (; Ioctl(device_fd.get(), VIDIOC_ENUM_FMT, &fmtdesc) == 0; |
| ++fmtdesc.index) { |
| formats.push_back(fmtdesc.pixelformat); |
| } |
| |
| std::vector<Capability> capabilities; |
| for (uint32_t format : formats) { |
| const Codec codec = GetCodec(format); |
| if (codec == Codec::kUnknown) |
| continue; |
| |
| const std::vector<Profile> profiles = |
| GetSupportedProfiles(device_fd.get(), codec); |
| LOG_ASSERT(!profiles.empty()); |
| const std::pair<int, int> max_resolution = |
| GetMaxResolution(device_fd.get(), format); |
| const std::vector<Resolution> resolutions = |
| GetInterestingResolutionsUpTo(max_resolution); |
| LOG_IF(FATAL, resolutions.empty()) |
| << "The maximum supported resolution for " << CodecToString(codec) |
| << " is too small: " << max_resolution.first << "x" |
| << max_resolution.second; |
| |
| // V4L2 API doesn't have a way of querying supported subsamplings and color |
| // depth. |
| for (const Profile profile : profiles) { |
| // TODO(b/172229001, b/188598699): For JPEG profiles, actually, supported |
| // subsamplings can be queried by V4L2_CID_JPEG_CHROMA_SUBSAMPLING. But it |
| // has never been used in Chrome OS. Call it once we confirm that it works |
| // on all V4L2 devices. We temporarily do as if all subsamplings are |
| // supported to avoid false negatives. |
| // TODO(b/172229001): For other profiles, we should guess them from |
| // supported YUV formats of CAPTURE queue for decoding and of OUTPUT queue |
| // for encoding. |
| std::vector<Subsampling> subsamplings = {Subsampling::kYUV420}; |
| if (profile == Profile::kJPEG) { |
| subsamplings = {Subsampling::kYUV420, Subsampling::kYUV422, |
| Subsampling::kYUV444}; |
| } |
| |
| for (const Subsampling subsampling : subsamplings) { |
| const ColorDepth color_depth = profile == Profile::kVP9Profile2 |
| ? ColorDepth::k10bit |
| : ColorDepth::k8bit; |
| for (const Resolution resolution : resolutions) { |
| capabilities.push_back(Capability(profile, decode, resolution, |
| subsampling, color_depth)); |
| } |
| } |
| } |
| } |
| |
| return capabilities; |
| } |
| |
| std::vector<Capability> GetCapabilitiesInPaths( |
| const std::vector<base::FilePath>& paths, bool decode) { |
| std::vector<Capability> capabilities; |
| for (const base::FilePath& path : paths) { |
| for (auto&& c : GetCapabilitiesInPath(path, decode)) { |
| if (!base::Contains(capabilities, c)) |
| capabilities.push_back(std::move(c)); |
| } |
| } |
| |
| return capabilities; |
| } |
| |
| std::vector<Capability> GetDecodeCapabilities() { |
| const base::FilePath kVideoDecoderDevicePath("/dev/video-dec"); |
| const base::FilePath kJpegDecoderDevicePath("/dev/jpeg-dec"); |
| std::vector<base::FilePath> device_paths; |
| auto video_decoder_device_paths = |
| GetAllFilesWithPrefix(kVideoDecoderDevicePath); |
| auto jpeg_decoder_device_paths = |
| GetAllFilesWithPrefix(kJpegDecoderDevicePath); |
| device_paths.insert(device_paths.end(), video_decoder_device_paths.begin(), |
| video_decoder_device_paths.end()); |
| device_paths.insert(device_paths.end(), jpeg_decoder_device_paths.begin(), |
| jpeg_decoder_device_paths.end()); |
| |
| return GetCapabilitiesInPaths(device_paths, /*decode=*/true); |
| } |
| |
| std::vector<Capability> GetEncodeCapabilities() { |
| const base::FilePath kVideoEncoderDevicePath("/dev/video-enc"); |
| const base::FilePath kJpegEncoderDevicePath("/dev/jpeg-enc"); |
| std::vector<base::FilePath> device_paths; |
| auto video_encoder_device_paths = |
| GetAllFilesWithPrefix(kVideoEncoderDevicePath); |
| auto jpeg_encoder_device_paths = |
| GetAllFilesWithPrefix(kJpegEncoderDevicePath); |
| device_paths.insert(device_paths.end(), video_encoder_device_paths.begin(), |
| video_encoder_device_paths.end()); |
| device_paths.insert(device_paths.end(), jpeg_encoder_device_paths.begin(), |
| jpeg_encoder_device_paths.end()); |
| |
| return GetCapabilitiesInPaths(device_paths, /*decode=*/false); |
| } |
| } // namespace |
| |
| std::vector<Capability> DetectV4L2Capabilities() { |
| auto decode_capabilities = GetEncodeCapabilities(); |
| auto encode_capabilities = GetDecodeCapabilities(); |
| |
| auto& capabilities = decode_capabilities; |
| capabilities.insert(capabilities.end(), encode_capabilities.begin(), |
| encode_capabilities.end()); |
| return capabilities; |
| } |