Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(60)

Side by Side Diff: media/video/capture/win/video_capture_device_win.cc

Issue 546803002: Win Video Capture: add support for WDM capture devices using a WDM Crossbar filter. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: perkj@s comments Created 6 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « media/video/capture/win/video_capture_device_win.h ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "media/video/capture/win/video_capture_device_win.h" 5 #include "media/video/capture/win/video_capture_device_win.h"
6 6
7 #include <ks.h> 7 #include <ks.h>
8 #include <ksmedia.h> 8 #include <ksmedia.h>
9 9
10 #include <algorithm> 10 #include <algorithm>
11 #include <list> 11 #include <list>
12 12
13 #include "base/strings/string_tokenizer.h"
13 #include "base/strings/sys_string_conversions.h" 14 #include "base/strings/sys_string_conversions.h"
14 #include "base/win/scoped_co_mem.h" 15 #include "base/win/scoped_co_mem.h"
15 #include "base/win/scoped_variant.h" 16 #include "base/win/scoped_variant.h"
16 #include "media/video/capture/win/video_capture_device_mf_win.h" 17 #include "media/video/capture/win/video_capture_device_mf_win.h"
17 18
18 using base::win::ScopedCoMem; 19 using base::win::ScopedCoMem;
19 using base::win::ScopedComPtr; 20 using base::win::ScopedComPtr;
20 using base::win::ScopedVariant; 21 using base::win::ScopedVariant;
21 22
22 namespace media { 23 namespace media {
23 24
24 // Finds and creates a DirectShow Video Capture filter matching the device_name. 25 // Tries to find a |device_id| of class |device_class_id|. For this, it
26 // enumerates all system devices of the given class and does a string comparison
27 // of its |property_name| tag. This comparison can be exact or substring-wise
28 // depending on |exact_name_comparison|. If such a device is found, a moniker
29 // to it is returned.
25 // static 30 // static
26 HRESULT VideoCaptureDeviceWin::GetDeviceFilter(const std::string& device_id, 31 ScopedComPtr<IMoniker> FindDeviceAndReturnMoniker(const std::string& device_id,
27 IBaseFilter** filter) { 32 const CLSID device_class_id,
28 DCHECK(filter); 33 const wchar_t* property_name,
34 bool exact_name_comparison) {
29 35
36 ScopedComPtr<IMoniker> moniker;
30 ScopedComPtr<ICreateDevEnum> dev_enum; 37 ScopedComPtr<ICreateDevEnum> dev_enum;
31 HRESULT hr = dev_enum.CreateInstance(CLSID_SystemDeviceEnum, NULL, 38 HRESULT hr = dev_enum.CreateInstance(CLSID_SystemDeviceEnum, NULL,
32 CLSCTX_INPROC); 39 CLSCTX_INPROC);
40 DPLOG_IF(ERROR, FAILED(hr)) << "Create SystemDeviceEnum";
33 if (FAILED(hr)) 41 if (FAILED(hr))
34 return hr; 42 return moniker;
35 43
36 ScopedComPtr<IEnumMoniker> enum_moniker; 44 ScopedComPtr<IEnumMoniker> enum_moniker;
37 hr = dev_enum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, 45 hr = dev_enum->CreateClassEnumerator(device_class_id,
38 enum_moniker.Receive(), 0); 46 enum_moniker.Receive(), 0);
39 // CreateClassEnumerator returns S_FALSE on some Windows OS 47 // CreateClassEnumerator returns S_FALSE on some Windows OS when no camera
40 // when no camera exist. Therefore the FAILED macro can't be used. 48 // exist. Therefore the FAILED macro can't be used.
49 DPLOG_IF(ERROR, hr != S_OK) << "CreateClassEnumerator";
41 if (hr != S_OK) 50 if (hr != S_OK)
42 return NULL; 51 return moniker;
43 52
44 ScopedComPtr<IMoniker> moniker;
45 ScopedComPtr<IBaseFilter> capture_filter;
46 DWORD fetched = 0; 53 DWORD fetched = 0;
47 while (enum_moniker->Next(1, moniker.Receive(), &fetched) == S_OK) { 54 while (enum_moniker->Next(1, moniker.Receive(), &fetched) == S_OK) {
48 ScopedComPtr<IPropertyBag> prop_bag; 55 ScopedComPtr<IPropertyBag> prop_bag;
49 hr = moniker->BindToStorage(0, 0, IID_IPropertyBag, prop_bag.ReceiveVoid()); 56 hr = moniker->BindToStorage(0, 0, IID_IPropertyBag, prop_bag.ReceiveVoid());
50 if (FAILED(hr)) { 57 if (FAILED(hr)) {
51 moniker.Release(); 58 moniker.Release();
52 continue; 59 continue;
53 } 60 }
54 61
55 // Find the device via DevicePath, Description or FriendlyName, whichever is
56 // available first.
57 static const wchar_t* kPropertyNames[] = {
58 L"DevicePath", L"Description", L"FriendlyName"
59 };
60 ScopedVariant name; 62 ScopedVariant name;
61 for (size_t i = 0; 63 prop_bag->Read(property_name, name.Receive(), 0);
62 i < arraysize(kPropertyNames) && name.type() != VT_BSTR; ++i) { 64
63 prop_bag->Read(kPropertyNames[i], name.Receive(), 0);
64 }
65 if (name.type() == VT_BSTR) { 65 if (name.type() == VT_BSTR) {
66 std::string device_path(base::SysWideToUTF8(V_BSTR(&name))); 66 std::string device_path(base::SysWideToUTF8(V_BSTR(&name)));
67 if (device_path.compare(device_id) == 0) { 67 if ((exact_name_comparison && device_path == device_id) ||
68 // We have found the requested device 68 (!exact_name_comparison &&
69 hr = moniker->BindToObject(0, 0, IID_IBaseFilter, 69 device_path.find(device_id) != std::string::npos)) {
70 capture_filter.ReceiveVoid()); 70 return moniker;
71 DLOG_IF(ERROR, FAILED(hr)) << "Failed to bind camera filter: "
72 << logging::SystemErrorCodeToString(hr);
73 break;
74 } 71 }
75 } 72 }
76 moniker.Release(); 73 moniker.Release();
77 } 74 }
75 return moniker;
76 }
78 77
79 *filter = capture_filter.Detach(); 78 // Finds and creates a DirectShow Video Capture filter matching the |device_id|.
80 if (!*filter && SUCCEEDED(hr)) 79 // |class_id| is usually CLSID_VideoInputDeviceCategory for standard DirectShow
81 hr = HRESULT_FROM_WIN32(ERROR_NOT_FOUND); 80 // devices but might also be AM_KSCATEGORY_CAPTURE or AM_KSCATEGORY_CROSSBAR, to
81 // enumerate WDM capture devices or WDM crossbars, respectively.
82 // static
83 HRESULT VideoCaptureDeviceWin::GetDeviceFilter(const std::string& device_id,
84 const CLSID device_class_id,
85 IBaseFilter** filter) {
86 DCHECK(filter);
87 const bool kExactNameComparison = true;
88 ScopedComPtr<IMoniker> moniker;
82 89
83 return hr; 90 static const wchar_t* kPropertyNames[] = {
91 L"DevicePath", L"Description", L"FriendlyName"
92 };
93 for (size_t i = 0; i < arraysize(kPropertyNames); ++i) {
94 moniker = FindDeviceAndReturnMoniker(device_id, device_class_id,
95 kPropertyNames[i], kExactNameComparison);
96 if (!moniker)
97 continue;
98 ScopedComPtr<IBaseFilter> capture_filter;
99 HRESULT hr = moniker->BindToObject(0, 0, IID_IBaseFilter,
100 capture_filter.ReceiveVoid());
101 *filter = capture_filter.Detach();
102 if (!*filter && SUCCEEDED(hr))
103 hr = HRESULT_FROM_WIN32(ERROR_NOT_FOUND);
104 return hr;
105 }
106 DLOG(ERROR) << "Failed to find device " << device_id;
107 return E_FAIL;
84 } 108 }
85 109
86 // Check if a Pin matches a category. 110 // Check if a Pin matches a category.
87 // static 111 // static
88 bool VideoCaptureDeviceWin::PinMatchesCategory(IPin* pin, REFGUID category) { 112 bool VideoCaptureDeviceWin::PinMatchesCategory(IPin* pin, REFGUID category) {
89 DCHECK(pin); 113 DCHECK(pin);
90 bool found = false; 114 bool found = false;
91 ScopedComPtr<IKsPropertySet> ks_property; 115 ScopedComPtr<IKsPropertySet> ks_property;
92 HRESULT hr = ks_property.QueryFrom(pin); 116 HRESULT hr = ks_property.QueryFrom(pin);
93 if (SUCCEEDED(hr)) { 117 if (SUCCEEDED(hr)) {
(...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after
211 if (sink_filter_) { 235 if (sink_filter_) {
212 graph_builder_->RemoveFilter(sink_filter_); 236 graph_builder_->RemoveFilter(sink_filter_);
213 sink_filter_ = NULL; 237 sink_filter_ = NULL;
214 } 238 }
215 239
216 if (capture_filter_) 240 if (capture_filter_)
217 graph_builder_->RemoveFilter(capture_filter_); 241 graph_builder_->RemoveFilter(capture_filter_);
218 242
219 if (mjpg_filter_) 243 if (mjpg_filter_)
220 graph_builder_->RemoveFilter(mjpg_filter_); 244 graph_builder_->RemoveFilter(mjpg_filter_);
245
246 if(crossbar_filter_)
magjed_chromium 2014/09/09 16:50:24 nit: should have space between if and '('
247 graph_builder_->RemoveFilter(crossbar_filter_);
221 } 248 }
222 } 249 }
223 250
224 bool VideoCaptureDeviceWin::Init() { 251 bool VideoCaptureDeviceWin::Init() {
225 DCHECK(CalledOnValidThread()); 252 DCHECK(CalledOnValidThread());
226 HRESULT hr = GetDeviceFilter(device_name_.id(), capture_filter_.Receive()); 253 HRESULT hr;
254
255 if (device_name_.capture_api_type() == Name::DIRECT_SHOW_WDM_CROSSBAR) {
256 hr = InstantiateWDMFiltersAndPins();
257 } else {
258 hr = GetDeviceFilter(device_name_.id(), CLSID_VideoInputDeviceCategory,
259 capture_filter_.Receive());
260 }
227 if (!capture_filter_) { 261 if (!capture_filter_) {
228 DLOG(ERROR) << "Failed to create capture filter: " 262 DVLOG(2) << "Failed to create capture filter.";
229 << logging::SystemErrorCodeToString(hr);
230 return false; 263 return false;
231 } 264 }
232 265
233 output_capture_pin_ = 266 output_capture_pin_ =
234 GetPin(capture_filter_, PINDIR_OUTPUT, PIN_CATEGORY_CAPTURE); 267 GetPin(capture_filter_, PINDIR_OUTPUT, PIN_CATEGORY_CAPTURE);
235 if (!output_capture_pin_) { 268 if (!output_capture_pin_) {
236 DLOG(ERROR) << "Failed to get capture output pin"; 269 DVLOG(2) << "Failed to get capture output pin";
237 return false; 270 return false;
238 } 271 }
239 272
240 // Create the sink filter used for receiving Captured frames. 273 // Create the sink filter used for receiving Captured frames.
241 sink_filter_ = new SinkFilter(this); 274 sink_filter_ = new SinkFilter(this);
242 if (sink_filter_ == NULL) { 275 if (sink_filter_ == NULL) {
243 DLOG(ERROR) << "Failed to create send filter"; 276 DVLOG(2) << "Failed to create send filter";
244 return false; 277 return false;
245 } 278 }
246 279
247 input_sink_pin_ = sink_filter_->GetPin(0); 280 input_sink_pin_ = sink_filter_->GetPin(0);
248 281
249 hr = graph_builder_.CreateInstance(CLSID_FilterGraph, NULL, 282 hr = graph_builder_.CreateInstance(CLSID_FilterGraph, NULL,
250 CLSCTX_INPROC_SERVER); 283 CLSCTX_INPROC_SERVER);
251 if (FAILED(hr)) { 284 if (FAILED(hr)) {
252 DLOG(ERROR) << "Failed to create graph builder: " 285 DVLOG(2) << "Failed to create graph builder.";
253 << logging::SystemErrorCodeToString(hr);
254 return false; 286 return false;
255 } 287 }
256 288
257 hr = graph_builder_.QueryInterface(media_control_.Receive()); 289 hr = graph_builder_.QueryInterface(media_control_.Receive());
258 if (FAILED(hr)) { 290 if (FAILED(hr)) {
259 DLOG(ERROR) << "Failed to create media control builder: " 291 DVLOG(2) << "Failed to create media control builder.";
260 << logging::SystemErrorCodeToString(hr);
261 return false; 292 return false;
262 } 293 }
263 294
264 hr = graph_builder_->AddFilter(capture_filter_, NULL); 295 hr = graph_builder_->AddFilter(capture_filter_, NULL);
265 if (FAILED(hr)) { 296 if (FAILED(hr)) {
266 DLOG(ERROR) << "Failed to add the capture device to the graph: " 297 DVLOG(2) << "Failed to add the capture device to the graph.";
267 << logging::SystemErrorCodeToString(hr); 298 return false;
299 }
300
301 if (device_name_.capture_api_type() == Name::DIRECT_SHOW_WDM_CROSSBAR &&
302 FAILED(AddWDMCrossbarFilterToGraphAndConnect())) {
303 DVLOG(2)<< "Failed to add/connect the WDM Crossbar filter to the graph.";
268 return false; 304 return false;
269 } 305 }
270 306
271 hr = graph_builder_->AddFilter(sink_filter_, NULL); 307 hr = graph_builder_->AddFilter(sink_filter_, NULL);
272 if (FAILED(hr)) { 308 if (FAILED(hr)) {
273 DLOG(ERROR) << "Failed to add the send filter to the graph: " 309 DVLOG(2)<< "Failed to add the send filter to the graph.";
274 << logging::SystemErrorCodeToString(hr);
275 return false; 310 return false;
276 } 311 }
277 312
278 return CreateCapabilityMap(); 313 return CreateCapabilityMap();
279 } 314 }
280 315
281 void VideoCaptureDeviceWin::AllocateAndStart( 316 void VideoCaptureDeviceWin::AllocateAndStart(
282 const VideoCaptureParams& params, 317 const VideoCaptureParams& params,
283 scoped_ptr<VideoCaptureDevice::Client> client) { 318 scoped_ptr<VideoCaptureDevice::Client> client) {
284 DCHECK(CalledOnValidThread()); 319 DCHECK(CalledOnValidThread());
(...skipping 131 matching lines...) Expand 10 before | Expand all | Expand 10 after
416 } 451 }
417 452
418 graph_builder_->Disconnect(output_capture_pin_); 453 graph_builder_->Disconnect(output_capture_pin_);
419 graph_builder_->Disconnect(input_sink_pin_); 454 graph_builder_->Disconnect(input_sink_pin_);
420 455
421 // If the _mjpg filter exist disconnect it even if it has not been used. 456 // If the _mjpg filter exist disconnect it even if it has not been used.
422 if (mjpg_filter_) { 457 if (mjpg_filter_) {
423 graph_builder_->Disconnect(input_mjpg_pin_); 458 graph_builder_->Disconnect(input_mjpg_pin_);
424 graph_builder_->Disconnect(output_mjpg_pin_); 459 graph_builder_->Disconnect(output_mjpg_pin_);
425 } 460 }
461 if (crossbar_filter_) {
462 graph_builder_->Disconnect(analog_video_input_pin_);
463 graph_builder_->Disconnect(crossbar_video_output_pin_);
464 }
426 465
427 if (FAILED(hr)) { 466 if (FAILED(hr)) {
428 SetErrorState("Failed to Stop the Capture device"); 467 SetErrorState("Failed to Stop the Capture device");
429 return; 468 return;
430 } 469 }
431 client_.reset(); 470 client_.reset();
432 state_ = kIdle; 471 state_ = kIdle;
433 } 472 }
434 473
435 // Implements SinkFilterObserver::SinkFilterObserver. 474 // Implements SinkFilterObserver::SinkFilterObserver.
436 void VideoCaptureDeviceWin::FrameReceived(const uint8* buffer, 475 void VideoCaptureDeviceWin::FrameReceived(const uint8* buffer,
437 int length) { 476 int length) {
438 client_->OnIncomingCapturedData( 477 client_->OnIncomingCapturedData(
439 buffer, length, capture_format_, 0, base::TimeTicks::Now()); 478 buffer, length, capture_format_, 0, base::TimeTicks::Now());
440 } 479 }
441 480
442 bool VideoCaptureDeviceWin::CreateCapabilityMap() { 481 bool VideoCaptureDeviceWin::CreateCapabilityMap() {
443 DCHECK(CalledOnValidThread()); 482 DCHECK(CalledOnValidThread());
444 ScopedComPtr<IAMStreamConfig> stream_config; 483 ScopedComPtr<IAMStreamConfig> stream_config;
445 HRESULT hr = output_capture_pin_.QueryInterface(stream_config.Receive()); 484 HRESULT hr = output_capture_pin_.QueryInterface(stream_config.Receive());
446 if (FAILED(hr)) { 485 if (FAILED(hr)) {
447 DPLOG(ERROR) << "Failed to get IAMStreamConfig interface from " 486 DPLOG(ERROR) << "Failed to get IAMStreamConfig interface from "
448 "capture device: " << logging::SystemErrorCodeToString(hr); 487 "capture device: " << logging::SystemErrorCodeToString(hr);
449 return false; 488 return false;
450 } 489 }
451 490
452 // Get interface used for getting the frame rate. 491 // Get interface used for getting the frame rate.
453 ScopedComPtr<IAMVideoControl> video_control; 492 ScopedComPtr<IAMVideoControl> video_control;
454 hr = capture_filter_.QueryInterface(video_control.Receive()); 493 hr = capture_filter_.QueryInterface(video_control.Receive());
455 DLOG_IF(WARNING, FAILED(hr)) << "IAMVideoControl Interface NOT SUPPORTED: " 494 DLOG_IF(WARNING, FAILED(hr)) << "IAMVideoControl Interface NOT SUPPORTED: "
456 << logging::SystemErrorCodeToString(hr); 495 << logging::SystemErrorCodeToString(hr);
457 496
458 int count = 0, size = 0; 497 int count = 0, size = 0;
(...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after
554 KSPROPERTY_VIDEOPROCAMP_POWERLINE_FREQUENCY, 593 KSPROPERTY_VIDEOPROCAMP_POWERLINE_FREQUENCY,
555 &data, sizeof(data), &data, sizeof(data)); 594 &data, sizeof(data), &data, sizeof(data));
556 DLOG_IF(ERROR, FAILED(hr)) << "Anti-flicker setting failed: " 595 DLOG_IF(ERROR, FAILED(hr)) << "Anti-flicker setting failed: "
557 << logging::SystemErrorCodeToString(hr); 596 << logging::SystemErrorCodeToString(hr);
558 DVLOG_IF(2, SUCCEEDED(hr)) << "Anti-flicker set correctly."; 597 DVLOG_IF(2, SUCCEEDED(hr)) << "Anti-flicker set correctly.";
559 } else { 598 } else {
560 DVLOG(2) << "Anti-flicker setting not supported."; 599 DVLOG(2) << "Anti-flicker setting not supported.";
561 } 600 }
562 } 601 }
563 602
603 // Instantiate a WDM Crossbar Filter and the associated WDM Capture Filter,
604 // extract the correct pins from each. The necessary pins are device specific
605 // and usually the first Crossbar output pin, with a name similar to "Video
606 // Decoder Out" and the first Capture input pin, with a name like "Analog Video
607 // In". These pins have no special Category.
608 HRESULT VideoCaptureDeviceWin::InstantiateWDMFiltersAndPins() {
609 ScopedComPtr<IMoniker> crossbar_moniker = FindDeviceAndReturnMoniker(
610 device_name_.id(), AM_KSCATEGORY_CROSSBAR, L"DevicePath", true);
611 if (!crossbar_moniker)
612 return E_FAIL;
613
614 HRESULT hr = crossbar_moniker->BindToObject(0, 0, IID_IBaseFilter,
615 crossbar_filter_.ReceiveVoid());
616 DPLOG_IF(ERROR, FAILED(hr)) << "Failed to bind crossbar filter";
617 if (FAILED(hr) || !crossbar_filter_)
618 return E_FAIL;
619
620 // Find Crossbar Video Output Pin: This is usually the first output pin.
621 crossbar_video_output_pin_ = GetPin(
622 crossbar_filter_, PINDIR_OUTPUT, GUID_NULL);
623 DLOG_IF(ERROR, !crossbar_video_output_pin_)
624 << "Failed to find Crossbar Video Output pin";
625 if (!crossbar_video_output_pin_)
626 return E_FAIL;
627
628 // Find the WDM capture filter associated to the WDM Crossbar filter. This
629 // is a fuzzy matching: they have similar names to the naked eye. Empirically,
630 // use the words of the Crossbar Filter name one by one; they are usually
631 // Vendor, Chip model etc to search in the WDM Filters list.
632 base::StringTokenizer t(device_name_.name(), " ");
633 ScopedComPtr<IMoniker> wdm_source_moniker;
634 while (!wdm_source_moniker && t.GetNext()) {
635 wdm_source_moniker = FindDeviceAndReturnMoniker(
636 t.token(), AM_KSCATEGORY_CAPTURE, L"FriendlyName", false);
637 }
638 DLOG_IF(ERROR, wdm_source_moniker) << "Couldn't find WDM device named";
639 if (!wdm_source_moniker)
640 return E_FAIL;
641
642 hr = wdm_source_moniker->BindToObject(0, 0, IID_IBaseFilter,
643 capture_filter_.ReceiveVoid());
644 DPLOG_IF(ERROR, FAILED(hr)) << "Failed to bind WDM filter";
645 if (FAILED(hr) || !capture_filter_)
646 return E_FAIL;
647
648 // Find the WDM Capture Filter's Analog Video input Pin: usually the first
649 // input pin.
650 analog_video_input_pin_ = GetPin(capture_filter_, PINDIR_INPUT, GUID_NULL);
651 DLOG_IF(ERROR, !analog_video_input_pin_) << "Failed to find WDM Video Input";
652 if (!analog_video_input_pin_)
653 return E_FAIL;
654 return S_OK;
655 }
656
657 // Add the WDM Crossbar filter to the Graph and connect the pins previously
658 // found.
659 HRESULT VideoCaptureDeviceWin::AddWDMCrossbarFilterToGraphAndConnect() {
660 HRESULT hr = graph_builder_->AddFilter(crossbar_filter_, NULL);
661 DPLOG_IF(ERROR, FAILED(hr)) << "Failed to add Crossbar filter to the graph";
662 if (FAILED(hr))
663 return E_FAIL;
664
665 hr = graph_builder_->ConnectDirect(
666 crossbar_video_output_pin_, analog_video_input_pin_, NULL);
667 DPLOG_IF(ERROR, FAILED(hr)) << "Failed to plug WDM filters to each other";
668 if (FAILED(hr))
669 return E_FAIL;
670 return S_OK;
671 }
672
564 void VideoCaptureDeviceWin::SetErrorState(const std::string& reason) { 673 void VideoCaptureDeviceWin::SetErrorState(const std::string& reason) {
565 DCHECK(CalledOnValidThread()); 674 DCHECK(CalledOnValidThread());
675 DVLOG(1) << reason;
566 state_ = kError; 676 state_ = kError;
567 client_->OnError(reason); 677 client_->OnError(reason);
568 } 678 }
569 } // namespace media 679 } // namespace media
OLDNEW
« no previous file with comments | « media/video/capture/win/video_capture_device_win.h ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698