blob: 83e1bd3919a3c240b5754884572eaeb228f6049f [file] [log] [blame]
// Copyright (c) 2012 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 "media/video/capture/win/video_capture_device_win.h"
#include <ks.h>
#include <ksmedia.h>
#include <algorithm>
#include <list>
#include "base/strings/sys_string_conversions.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/scoped_variant.h"
#include "media/video/capture/win/video_capture_device_mf_win.h"
using base::win::ScopedCoMem;
using base::win::ScopedComPtr;
using base::win::ScopedVariant;
namespace media {
// Check if a Pin matches a category.
bool PinMatchesCategory(IPin* pin, REFGUID category) {
DCHECK(pin);
bool found = false;
ScopedComPtr<IKsPropertySet> ks_property;
HRESULT hr = ks_property.QueryFrom(pin);
if (SUCCEEDED(hr)) {
GUID pin_category;
DWORD return_value;
hr = ks_property->Get(AMPROPSETID_Pin, AMPROPERTY_PIN_CATEGORY, NULL, 0,
&pin_category, sizeof(pin_category), &return_value);
if (SUCCEEDED(hr) && (return_value == sizeof(pin_category))) {
found = (pin_category == category);
}
}
return found;
}
// Check if a Pin's MediaType matches a given |major_type|.
bool PinMatchesMajorType(IPin* pin, REFGUID major_type) {
DCHECK(pin);
AM_MEDIA_TYPE connection_media_type;
HRESULT hr = pin->ConnectionMediaType(&connection_media_type);
return SUCCEEDED(hr) && connection_media_type.majortype == major_type;
}
// Finds and creates a DirectShow Video Capture filter matching the |device_id|.
// |class_id| is usually CLSID_VideoInputDeviceCategory for standard DirectShow
// devices but might also be AM_KSCATEGORY_CAPTURE or AM_KSCATEGORY_CROSSBAR, to
// enumerate WDM capture devices or WDM crossbars, respectively.
// static
HRESULT VideoCaptureDeviceWin::GetDeviceFilter(const std::string& device_id,
const CLSID device_class_id,
IBaseFilter** filter) {
DCHECK(filter);
ScopedComPtr<ICreateDevEnum> dev_enum;
HRESULT hr = dev_enum.CreateInstance(CLSID_SystemDeviceEnum, NULL,
CLSCTX_INPROC);
if (FAILED(hr))
return hr;
ScopedComPtr<IEnumMoniker> enum_moniker;
hr = dev_enum->CreateClassEnumerator(device_class_id, enum_moniker.Receive(),
0);
// CreateClassEnumerator returns S_FALSE on some Windows OS
// when no camera exist. Therefore the FAILED macro can't be used.
if (hr != S_OK)
return NULL;
ScopedComPtr<IMoniker> moniker;
ScopedComPtr<IBaseFilter> capture_filter;
DWORD fetched = 0;
while (enum_moniker->Next(1, moniker.Receive(), &fetched) == S_OK) {
ScopedComPtr<IPropertyBag> prop_bag;
hr = moniker->BindToStorage(0, 0, IID_IPropertyBag, prop_bag.ReceiveVoid());
if (FAILED(hr)) {
moniker.Release();
continue;
}
// Find the device via DevicePath, Description or FriendlyName, whichever is
// available first.
static const wchar_t* kPropertyNames[] = {
L"DevicePath", L"Description", L"FriendlyName"
};
ScopedVariant name;
for (size_t i = 0;
i < arraysize(kPropertyNames) && name.type() != VT_BSTR; ++i) {
prop_bag->Read(kPropertyNames[i], name.Receive(), 0);
}
if (name.type() == VT_BSTR) {
std::string device_path(base::SysWideToUTF8(V_BSTR(&name)));
if (device_path.compare(device_id) == 0) {
// We have found the requested device
hr = moniker->BindToObject(0, 0, IID_IBaseFilter,
capture_filter.ReceiveVoid());
DLOG_IF(ERROR, FAILED(hr)) << "Failed to bind camera filter: "
<< logging::SystemErrorCodeToString(hr);
break;
}
}
moniker.Release();
}
*filter = capture_filter.Detach();
if (!*filter && SUCCEEDED(hr))
hr = HRESULT_FROM_WIN32(ERROR_NOT_FOUND);
return hr;
}
// Finds an IPin on an IBaseFilter given the direction, Category and/or Major
// Type. If either |category| or |major_type| are GUID_NULL, they are ignored.
// static
ScopedComPtr<IPin> VideoCaptureDeviceWin::GetPin(IBaseFilter* filter,
PIN_DIRECTION pin_dir,
REFGUID category,
REFGUID major_type) {
ScopedComPtr<IPin> pin;
ScopedComPtr<IEnumPins> pin_enum;
HRESULT hr = filter->EnumPins(pin_enum.Receive());
if (pin_enum == NULL)
return pin;
// Get first unconnected pin.
hr = pin_enum->Reset(); // set to first pin
while ((hr = pin_enum->Next(1, pin.Receive(), NULL)) == S_OK) {
PIN_DIRECTION this_pin_dir = static_cast<PIN_DIRECTION>(-1);
hr = pin->QueryDirection(&this_pin_dir);
if (pin_dir == this_pin_dir) {
if ((category == GUID_NULL || PinMatchesCategory(pin, category)) &&
(major_type == GUID_NULL || PinMatchesMajorType(pin, major_type))) {
return pin;
}
}
pin.Release();
}
DCHECK(!pin);
return pin;
}
// static
VideoPixelFormat VideoCaptureDeviceWin::TranslateMediaSubtypeToPixelFormat(
const GUID& sub_type) {
static struct {
const GUID& sub_type;
VideoPixelFormat format;
} pixel_formats[] = {
{ kMediaSubTypeI420, PIXEL_FORMAT_I420 },
{ MEDIASUBTYPE_IYUV, PIXEL_FORMAT_I420 },
{ MEDIASUBTYPE_RGB24, PIXEL_FORMAT_RGB24 },
{ MEDIASUBTYPE_YUY2, PIXEL_FORMAT_YUY2 },
{ MEDIASUBTYPE_MJPG, PIXEL_FORMAT_MJPEG },
{ MEDIASUBTYPE_UYVY, PIXEL_FORMAT_UYVY },
{ MEDIASUBTYPE_ARGB32, PIXEL_FORMAT_ARGB },
{ kMediaSubTypeHDYC, PIXEL_FORMAT_UYVY },
};
for (size_t i = 0; i < arraysize(pixel_formats); ++i) {
if (sub_type == pixel_formats[i].sub_type)
return pixel_formats[i].format;
}
#ifndef NDEBUG
WCHAR guid_str[128];
StringFromGUID2(sub_type, guid_str, arraysize(guid_str));
DVLOG(2) << "Device (also) supports an unknown media type " << guid_str;
#endif
return PIXEL_FORMAT_UNKNOWN;
}
void VideoCaptureDeviceWin::ScopedMediaType::Free() {
if (!media_type_)
return;
DeleteMediaType(media_type_);
media_type_= NULL;
}
AM_MEDIA_TYPE** VideoCaptureDeviceWin::ScopedMediaType::Receive() {
DCHECK(!media_type_);
return &media_type_;
}
// Release the format block for a media type.
// http://msdn.microsoft.com/en-us/library/dd375432(VS.85).aspx
void VideoCaptureDeviceWin::ScopedMediaType::FreeMediaType(AM_MEDIA_TYPE* mt) {
if (mt->cbFormat != 0) {
CoTaskMemFree(mt->pbFormat);
mt->cbFormat = 0;
mt->pbFormat = NULL;
}
if (mt->pUnk != NULL) {
NOTREACHED();
// pUnk should not be used.
mt->pUnk->Release();
mt->pUnk = NULL;
}
}
// Delete a media type structure that was allocated on the heap.
// http://msdn.microsoft.com/en-us/library/dd375432(VS.85).aspx
void VideoCaptureDeviceWin::ScopedMediaType::DeleteMediaType(
AM_MEDIA_TYPE* mt) {
if (mt != NULL) {
FreeMediaType(mt);
CoTaskMemFree(mt);
}
}
VideoCaptureDeviceWin::VideoCaptureDeviceWin(const Name& device_name)
: device_name_(device_name),
state_(kIdle) {
DetachFromThread();
}
VideoCaptureDeviceWin::~VideoCaptureDeviceWin() {
DCHECK(CalledOnValidThread());
if (media_control_)
media_control_->Stop();
if (graph_builder_) {
if (sink_filter_) {
graph_builder_->RemoveFilter(sink_filter_);
sink_filter_ = NULL;
}
if (capture_filter_)
graph_builder_->RemoveFilter(capture_filter_);
if (mjpg_filter_)
graph_builder_->RemoveFilter(mjpg_filter_);
if (crossbar_filter_)
graph_builder_->RemoveFilter(crossbar_filter_);
}
}
bool VideoCaptureDeviceWin::Init() {
DCHECK(CalledOnValidThread());
HRESULT hr;
if (device_name_.capture_api_type() == Name::DIRECT_SHOW_WDM_CROSSBAR) {
hr = InstantiateWDMFiltersAndPins();
} else {
hr = GetDeviceFilter(device_name_.id(), CLSID_VideoInputDeviceCategory,
capture_filter_.Receive());
}
if (!capture_filter_) {
DLOG(ERROR) << "Failed to create capture filter: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
output_capture_pin_ =
GetPin(capture_filter_, PINDIR_OUTPUT, PIN_CATEGORY_CAPTURE, GUID_NULL);
if (!output_capture_pin_) {
DLOG(ERROR) << "Failed to get capture output pin";
return false;
}
// Create the sink filter used for receiving Captured frames.
sink_filter_ = new SinkFilter(this);
if (sink_filter_ == NULL) {
DLOG(ERROR) << "Failed to create send filter";
return false;
}
input_sink_pin_ = sink_filter_->GetPin(0);
hr = graph_builder_.CreateInstance(CLSID_FilterGraph, NULL,
CLSCTX_INPROC_SERVER);
if (FAILED(hr)) {
DLOG(ERROR) << "Failed to create graph builder: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
hr = graph_builder_.QueryInterface(media_control_.Receive());
if (FAILED(hr)) {
DLOG(ERROR) << "Failed to create media control builder: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
hr = graph_builder_->AddFilter(capture_filter_, NULL);
if (FAILED(hr)) {
DLOG(ERROR) << "Failed to add the capture device to the graph: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
if (device_name_.capture_api_type() == Name::DIRECT_SHOW_WDM_CROSSBAR &&
FAILED(AddWDMCrossbarFilterToGraphAndConnect())) {
DLOG(ERROR) << "Failed to add the WDM Crossbar filter to the graph.";
return false;
}
hr = graph_builder_->AddFilter(sink_filter_, NULL);
if (FAILED(hr)) {
DLOG(ERROR) << "Failed to add the send filter to the graph: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
return CreateCapabilityMap();
}
void VideoCaptureDeviceWin::AllocateAndStart(
const VideoCaptureParams& params,
scoped_ptr<VideoCaptureDevice::Client> client) {
DCHECK(CalledOnValidThread());
if (state_ != kIdle)
return;
client_ = client.Pass();
// Get the camera capability that best match the requested format.
const CapabilityWin found_capability =
GetBestMatchedCapability(params.requested_format, capabilities_);
VideoCaptureFormat format = found_capability.supported_format;
// Reduce the frame rate if the requested frame rate is lower
// than the capability.
format.frame_rate =
std::min(format.frame_rate, params.requested_format.frame_rate);
ScopedComPtr<IAMStreamConfig> stream_config;
HRESULT hr = output_capture_pin_.QueryInterface(stream_config.Receive());
if (FAILED(hr)) {
SetErrorState("Can't get the Capture format settings");
return;
}
int count = 0, size = 0;
hr = stream_config->GetNumberOfCapabilities(&count, &size);
if (FAILED(hr)) {
SetErrorState("Failed to GetNumberOfCapabilities");
return;
}
scoped_ptr<BYTE[]> caps(new BYTE[size]);
ScopedMediaType media_type;
// Get the windows capability from the capture device.
// GetStreamCaps can return S_FALSE which we consider an error. Therefore the
// FAILED macro can't be used.
hr = stream_config->GetStreamCaps(
found_capability.stream_index, media_type.Receive(), caps.get());
if (hr != S_OK) {
SetErrorState("Failed to get capture device capabilities");
return;
} else {
if (media_type->formattype == FORMAT_VideoInfo) {
VIDEOINFOHEADER* h =
reinterpret_cast<VIDEOINFOHEADER*>(media_type->pbFormat);
if (format.frame_rate > 0)
h->AvgTimePerFrame = kSecondsToReferenceTime / format.frame_rate;
}
// Set the sink filter to request this format.
sink_filter_->SetRequestedMediaFormat(format);
// Order the capture device to use this format.
hr = stream_config->SetFormat(media_type.get());
if (FAILED(hr)) {
// TODO(grunell): Log the error. http://crbug.com/405016.
SetErrorState("Failed to set capture device output format");
return;
}
}
if (format.pixel_format == PIXEL_FORMAT_MJPEG && !mjpg_filter_.get()) {
// Create MJPG filter if we need it.
hr = mjpg_filter_.CreateInstance(CLSID_MjpegDec, NULL, CLSCTX_INPROC);
if (SUCCEEDED(hr)) {
input_mjpg_pin_ = GetPin(mjpg_filter_, PINDIR_INPUT, GUID_NULL,
GUID_NULL);
output_mjpg_pin_ = GetPin(mjpg_filter_, PINDIR_OUTPUT, GUID_NULL,
GUID_NULL);
hr = graph_builder_->AddFilter(mjpg_filter_, NULL);
}
if (FAILED(hr)) {
mjpg_filter_.Release();
input_mjpg_pin_.Release();
output_mjpg_pin_.Release();
}
}
SetAntiFlickerInCaptureFilter();
if (format.pixel_format == PIXEL_FORMAT_MJPEG && mjpg_filter_.get()) {
// Connect the camera to the MJPEG decoder.
hr = graph_builder_->ConnectDirect(output_capture_pin_, input_mjpg_pin_,
NULL);
// Connect the MJPEG filter to the Capture filter.
hr += graph_builder_->ConnectDirect(output_mjpg_pin_, input_sink_pin_,
NULL);
} else if (media_type->subtype == kMediaSubTypeHDYC) {
// HDYC pixel format, used by the DeckLink capture card, needs an AVI
// decompressor filter after source, let |graph_builder_| add it.
hr = graph_builder_->Connect(output_capture_pin_, input_sink_pin_);
} else {
hr = graph_builder_->ConnectDirect(output_capture_pin_, input_sink_pin_,
NULL);
}
if (FAILED(hr)) {
SetErrorState("Failed to connect the Capture graph.");
return;
}
hr = media_control_->Pause();
if (FAILED(hr)) {
SetErrorState("Failed to Pause the Capture device. "
"Is it already occupied?");
return;
}
// Get the format back from the sink filter after the filter have been
// connected.
capture_format_ = sink_filter_->ResultingFormat();
// Start capturing.
hr = media_control_->Run();
if (FAILED(hr)) {
SetErrorState("Failed to start the Capture device.");
return;
}
state_ = kCapturing;
}
void VideoCaptureDeviceWin::StopAndDeAllocate() {
DCHECK(CalledOnValidThread());
if (state_ != kCapturing)
return;
HRESULT hr = media_control_->Stop();
if (FAILED(hr)) {
SetErrorState("Failed to stop the capture graph.");
return;
}
graph_builder_->Disconnect(output_capture_pin_);
graph_builder_->Disconnect(input_sink_pin_);
// If the _mjpg filter exist disconnect it even if it has not been used.
if (mjpg_filter_) {
graph_builder_->Disconnect(input_mjpg_pin_);
graph_builder_->Disconnect(output_mjpg_pin_);
}
if (crossbar_filter_) {
graph_builder_->Disconnect(analog_video_input_pin_);
graph_builder_->Disconnect(crossbar_video_output_pin_);
}
if (FAILED(hr)) {
SetErrorState("Failed to Stop the Capture device");
return;
}
client_.reset();
state_ = kIdle;
}
// Implements SinkFilterObserver::SinkFilterObserver.
void VideoCaptureDeviceWin::FrameReceived(const uint8* buffer,
int length) {
client_->OnIncomingCapturedData(
buffer, length, capture_format_, 0, base::TimeTicks::Now());
}
bool VideoCaptureDeviceWin::CreateCapabilityMap() {
DCHECK(CalledOnValidThread());
ScopedComPtr<IAMStreamConfig> stream_config;
HRESULT hr = output_capture_pin_.QueryInterface(stream_config.Receive());
if (FAILED(hr)) {
DPLOG(ERROR) << "Failed to get IAMStreamConfig interface from "
"capture device: " << logging::SystemErrorCodeToString(hr);
return false;
}
// Get interface used for getting the frame rate.
ScopedComPtr<IAMVideoControl> video_control;
hr = capture_filter_.QueryInterface(video_control.Receive());
DLOG_IF(WARNING, FAILED(hr)) << "IAMVideoControl Interface NOT SUPPORTED: "
<< logging::SystemErrorCodeToString(hr);
int count = 0, size = 0;
hr = stream_config->GetNumberOfCapabilities(&count, &size);
if (FAILED(hr)) {
DLOG(ERROR) << "Failed to GetNumberOfCapabilities: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
scoped_ptr<BYTE[]> caps(new BYTE[size]);
for (int stream_index = 0; stream_index < count; ++stream_index) {
ScopedMediaType media_type;
hr = stream_config->GetStreamCaps(
stream_index, media_type.Receive(), caps.get());
// GetStreamCaps() may return S_FALSE, so don't use FAILED() or SUCCEED()
// macros here since they'll trigger incorrectly.
if (hr != S_OK) {
DLOG(ERROR) << "Failed to GetStreamCaps: "
<< logging::SystemErrorCodeToString(hr);
return false;
}
if (media_type->majortype == MEDIATYPE_Video &&
media_type->formattype == FORMAT_VideoInfo) {
VideoCaptureFormat format;
format.pixel_format =
TranslateMediaSubtypeToPixelFormat(media_type->subtype);
if (format.pixel_format == PIXEL_FORMAT_UNKNOWN)
continue;
VIDEOINFOHEADER* h =
reinterpret_cast<VIDEOINFOHEADER*>(media_type->pbFormat);
format.frame_size.SetSize(h->bmiHeader.biWidth, h->bmiHeader.biHeight);
// Try to get a better |time_per_frame| from IAMVideoControl. If not, use
// the value from VIDEOINFOHEADER.
REFERENCE_TIME time_per_frame = h->AvgTimePerFrame;
if (video_control) {
ScopedCoMem<LONGLONG> max_fps;
LONG list_size = 0;
const SIZE size = {format.frame_size.width(),
format.frame_size.height()};
hr = video_control->GetFrameRateList(
output_capture_pin_, stream_index, size, &list_size, &max_fps);
// Can't assume the first value will return the max fps.
// Sometimes |list_size| will be > 0, but max_fps will be NULL. Some
// drivers may return an HRESULT of S_FALSE which SUCCEEDED() translates
// into success, so explicitly check S_OK. See http://crbug.com/306237.
if (hr == S_OK && list_size > 0 && max_fps) {
time_per_frame = *std::min_element(max_fps.get(),
max_fps.get() + list_size);
}
}
format.frame_rate =
(time_per_frame > 0)
? (kSecondsToReferenceTime / static_cast<float>(time_per_frame))
: 0.0;
capabilities_.emplace_back(stream_index, format);
}
}
return !capabilities_.empty();
}
// Set the power line frequency removal in |capture_filter_| if available.
void VideoCaptureDeviceWin::SetAntiFlickerInCaptureFilter() {
const int power_line_frequency = GetPowerLineFrequencyForLocation();
if (power_line_frequency != kPowerLine50Hz &&
power_line_frequency != kPowerLine60Hz) {
return;
}
ScopedComPtr<IKsPropertySet> ks_propset;
DWORD type_support = 0;
HRESULT hr;
if (SUCCEEDED(hr = ks_propset.QueryFrom(capture_filter_)) &&
SUCCEEDED(hr = ks_propset->QuerySupported(PROPSETID_VIDCAP_VIDEOPROCAMP,
KSPROPERTY_VIDEOPROCAMP_POWERLINE_FREQUENCY, &type_support)) &&
(type_support & KSPROPERTY_SUPPORT_SET)) {
KSPROPERTY_VIDEOPROCAMP_S data = {};
data.Property.Set = PROPSETID_VIDCAP_VIDEOPROCAMP;
data.Property.Id = KSPROPERTY_VIDEOPROCAMP_POWERLINE_FREQUENCY;
data.Property.Flags = KSPROPERTY_TYPE_SET;
data.Value = (power_line_frequency == kPowerLine50Hz) ? 1 : 2;
data.Flags = KSPROPERTY_VIDEOPROCAMP_FLAGS_MANUAL;
hr = ks_propset->Set(PROPSETID_VIDCAP_VIDEOPROCAMP,
KSPROPERTY_VIDEOPROCAMP_POWERLINE_FREQUENCY,
&data, sizeof(data), &data, sizeof(data));
DLOG_IF(ERROR, FAILED(hr)) << "Anti-flicker setting failed: "
<< logging::SystemErrorCodeToString(hr);
DVLOG_IF(2, SUCCEEDED(hr)) << "Anti-flicker set correctly.";
} else {
DVLOG(2) << "Anti-flicker setting not supported.";
}
}
// Instantiate a WDM Crossbar Filter and the associated WDM Capture Filter,
// extract the correct pins from each. The necessary pins are device specific
// and usually the first Crossbar output pin, with a name similar to "Video
// Decoder Out" and the first Capture input pin, with a name like "Analog Video
// In". These pins have no special Category.
HRESULT VideoCaptureDeviceWin::InstantiateWDMFiltersAndPins() {
HRESULT hr = VideoCaptureDeviceWin::GetDeviceFilter(
device_name_.id(),
AM_KSCATEGORY_CROSSBAR,
crossbar_filter_.Receive());
DPLOG_IF(ERROR, FAILED(hr)) << "Failed to bind WDM Crossbar filter";
if (FAILED(hr) || !crossbar_filter_)
return E_FAIL;
// Find Crossbar Video Output Pin: This is usually the first output pin.
crossbar_video_output_pin_ = GetPin(crossbar_filter_, PINDIR_OUTPUT,
GUID_NULL, MEDIATYPE_AnalogVideo);
DLOG_IF(ERROR, !crossbar_video_output_pin_)
<< "Failed to find Crossbar Video Output pin";
if (!crossbar_video_output_pin_)
return E_FAIL;
// Use the WDM capture filter associated to the WDM Crossbar filter.
hr = VideoCaptureDeviceWin::GetDeviceFilter(device_name_.capabilities_id(),
AM_KSCATEGORY_CAPTURE,
capture_filter_.Receive());
DPLOG_IF(ERROR, FAILED(hr)) << "Failed to bind WDM Capture filter";
if (FAILED(hr) || !capture_filter_)
return E_FAIL;
// Find the WDM Capture Filter's Analog Video input Pin: usually the first
// input pin.
analog_video_input_pin_ = GetPin(capture_filter_, PINDIR_INPUT, GUID_NULL,
MEDIATYPE_AnalogVideo);
DLOG_IF(ERROR, !analog_video_input_pin_) << "Failed to find WDM Video Input";
if (!analog_video_input_pin_)
return E_FAIL;
return S_OK;
}
// Add the WDM Crossbar filter to the Graph and connect the pins previously
// found.
HRESULT VideoCaptureDeviceWin::AddWDMCrossbarFilterToGraphAndConnect() {
HRESULT hr = graph_builder_->AddFilter(crossbar_filter_, NULL);
DPLOG_IF(ERROR, FAILED(hr)) << "Failed to add Crossbar filter to the graph";
if (FAILED(hr))
return E_FAIL;
hr = graph_builder_->ConnectDirect(
crossbar_video_output_pin_, analog_video_input_pin_, NULL);
DPLOG_IF(ERROR, FAILED(hr)) << "Failed to plug WDM filters to each other";
if (FAILED(hr))
return E_FAIL;
return S_OK;
}
void VideoCaptureDeviceWin::SetErrorState(const std::string& reason) {
DCHECK(CalledOnValidThread());
state_ = kError;
client_->OnError(reason);
}
} // namespace media