1 /*
2 * libjingle
3 * Copyright 2004 Google Inc.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 * 3. The name of the author may not be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28 #include "talk/media/devices/devicemanager.h"
29
30 #ifdef WIN32
31 #include <objbase.h>
32 #include "webrtc/base/win32.h"
33 #endif
34 #include <string>
35
36 #include "talk/media/base/fakevideocapturer.h"
37 #include "talk/media/base/screencastid.h"
38 #include "talk/media/base/testutils.h"
39 #include "talk/media/base/videocapturerfactory.h"
40 #include "talk/media/devices/filevideocapturer.h"
41 #include "talk/media/devices/v4llookup.h"
42 #include "webrtc/base/arraysize.h"
43 #include "webrtc/base/fileutils.h"
44 #include "webrtc/base/gunit.h"
45 #include "webrtc/base/logging.h"
46 #include "webrtc/base/pathutils.h"
47 #include "webrtc/base/scoped_ptr.h"
48 #include "webrtc/base/stream.h"
49 #include "webrtc/base/windowpickerfactory.h"
50
51 #ifdef WEBRTC_LINUX
52 // TODO(juberti): Figure out why this doesn't compile on Windows.
53 #include "webrtc/base/fileutils_mock.h"
54 #endif // WEBRTC_LINUX
55
56 using rtc::Pathname;
57 using rtc::FileTimeType;
58 using rtc::scoped_ptr;
59 using cricket::Device;
60 using cricket::DeviceManager;
61 using cricket::DeviceManagerFactory;
62 using cricket::DeviceManagerInterface;
63
64 const cricket::VideoFormat kVgaFormat(640, 480,
65 cricket::VideoFormat::FpsToInterval(30),
66 cricket::FOURCC_I420);
67 const cricket::VideoFormat kHdFormat(1280, 720,
68 cricket::VideoFormat::FpsToInterval(30),
69 cricket::FOURCC_I420);
70
71 class FakeVideoDeviceCapturerFactory :
72 public cricket::VideoDeviceCapturerFactory {
73 public:
FakeVideoDeviceCapturerFactory()74 FakeVideoDeviceCapturerFactory() {}
~FakeVideoDeviceCapturerFactory()75 virtual ~FakeVideoDeviceCapturerFactory() {}
76
Create(const cricket::Device & device)77 virtual cricket::VideoCapturer* Create(const cricket::Device& device) {
78 return new cricket::FakeVideoCapturer;
79 }
80 };
81
82 class FakeScreenCapturerFactory : public cricket::ScreenCapturerFactory {
83 public:
FakeScreenCapturerFactory()84 FakeScreenCapturerFactory() {}
~FakeScreenCapturerFactory()85 virtual ~FakeScreenCapturerFactory() {}
86
Create(const cricket::ScreencastId & screenid)87 virtual cricket::VideoCapturer* Create(
88 const cricket::ScreencastId& screenid) {
89 return new cricket::FakeVideoCapturer;
90 }
91 };
92
93 class DeviceManagerTestFake : public testing::Test {
94 public:
SetUp()95 virtual void SetUp() {
96 dm_.reset(DeviceManagerFactory::Create());
97 EXPECT_TRUE(dm_->Init());
98 DeviceManager* device_manager = static_cast<DeviceManager*>(dm_.get());
99 device_manager->SetVideoDeviceCapturerFactory(
100 new FakeVideoDeviceCapturerFactory());
101 device_manager->SetScreenCapturerFactory(
102 new FakeScreenCapturerFactory());
103 }
104
TearDown()105 virtual void TearDown() {
106 dm_->Terminate();
107 }
108
109 protected:
110 scoped_ptr<DeviceManagerInterface> dm_;
111 };
112
113
114 // Test that we startup/shutdown properly.
TEST(DeviceManagerTest,StartupShutdown)115 TEST(DeviceManagerTest, StartupShutdown) {
116 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
117 EXPECT_TRUE(dm->Init());
118 dm->Terminate();
119 }
120
121 // Test CoInitEx behavior
122 #ifdef WIN32
TEST(DeviceManagerTest,CoInitialize)123 TEST(DeviceManagerTest, CoInitialize) {
124 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
125 std::vector<Device> devices;
126 // Ensure that calls to video device work if COM is not yet initialized.
127 EXPECT_TRUE(dm->Init());
128 EXPECT_TRUE(dm->GetVideoCaptureDevices(&devices));
129 dm->Terminate();
130 // Ensure that the ref count is correct.
131 EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED));
132 CoUninitialize();
133 // Ensure that Init works in COINIT_APARTMENTTHREADED setting.
134 EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_APARTMENTTHREADED));
135 EXPECT_TRUE(dm->Init());
136 dm->Terminate();
137 CoUninitialize();
138 // Ensure that the ref count is correct.
139 EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_APARTMENTTHREADED));
140 CoUninitialize();
141 // Ensure that Init works in COINIT_MULTITHREADED setting.
142 EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED));
143 EXPECT_TRUE(dm->Init());
144 dm->Terminate();
145 CoUninitialize();
146 // Ensure that the ref count is correct.
147 EXPECT_EQ(S_OK, CoInitializeEx(NULL, COINIT_MULTITHREADED));
148 CoUninitialize();
149 }
150 #endif
151
152 // Test enumerating devices (although we may not find any).
TEST(DeviceManagerTest,GetDevices)153 TEST(DeviceManagerTest, GetDevices) {
154 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
155 std::vector<Device> audio_ins, audio_outs, video_ins;
156 std::vector<cricket::Device> video_in_devs;
157 cricket::Device def_video;
158 EXPECT_TRUE(dm->Init());
159 EXPECT_TRUE(dm->GetAudioInputDevices(&audio_ins));
160 EXPECT_TRUE(dm->GetAudioOutputDevices(&audio_outs));
161 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins));
162 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_in_devs));
163 EXPECT_EQ(video_ins.size(), video_in_devs.size());
164 // If we have any video devices, we should be able to pick a default.
165 EXPECT_TRUE(dm->GetVideoCaptureDevice(
166 cricket::DeviceManagerInterface::kDefaultDeviceName, &def_video)
167 != video_ins.empty());
168 }
169
170 // Test that we return correct ids for default and bogus devices.
TEST(DeviceManagerTest,GetAudioDeviceIds)171 TEST(DeviceManagerTest, GetAudioDeviceIds) {
172 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
173 Device device;
174 EXPECT_TRUE(dm->Init());
175 EXPECT_TRUE(dm->GetAudioInputDevice(
176 cricket::DeviceManagerInterface::kDefaultDeviceName, &device));
177 EXPECT_EQ("-1", device.id);
178 EXPECT_TRUE(dm->GetAudioOutputDevice(
179 cricket::DeviceManagerInterface::kDefaultDeviceName, &device));
180 EXPECT_EQ("-1", device.id);
181 EXPECT_FALSE(dm->GetAudioInputDevice("_NOT A REAL DEVICE_", &device));
182 EXPECT_FALSE(dm->GetAudioOutputDevice("_NOT A REAL DEVICE_", &device));
183 }
184
185 // Test that we get the video capture device by name properly.
TEST(DeviceManagerTest,GetVideoDeviceIds)186 TEST(DeviceManagerTest, GetVideoDeviceIds) {
187 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
188 Device device;
189 EXPECT_TRUE(dm->Init());
190 EXPECT_FALSE(dm->GetVideoCaptureDevice("_NOT A REAL DEVICE_", &device));
191 std::vector<Device> video_ins;
192 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins));
193 if (!video_ins.empty()) {
194 // Get the default device with the parameter kDefaultDeviceName.
195 EXPECT_TRUE(dm->GetVideoCaptureDevice(
196 cricket::DeviceManagerInterface::kDefaultDeviceName, &device));
197
198 // Get the first device with the parameter video_ins[0].name.
199 EXPECT_TRUE(dm->GetVideoCaptureDevice(video_ins[0].name, &device));
200 EXPECT_EQ(device.name, video_ins[0].name);
201 EXPECT_EQ(device.id, video_ins[0].id);
202 }
203 }
204
TEST(DeviceManagerTest,GetVideoDeviceIds_File)205 TEST(DeviceManagerTest, GetVideoDeviceIds_File) {
206 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
207 EXPECT_TRUE(dm->Init());
208 Device device;
209 const std::string test_file =
210 cricket::GetTestFilePath("captured-320x240-2s-48.frames");
211 EXPECT_TRUE(dm->GetVideoCaptureDevice(test_file, &device));
212 EXPECT_TRUE(cricket::FileVideoCapturer::IsFileVideoCapturerDevice(device));
213 }
214
TEST(DeviceManagerTest,VerifyDevicesListsAreCleared)215 TEST(DeviceManagerTest, VerifyDevicesListsAreCleared) {
216 const std::string imaginary("_NOT A REAL DEVICE_");
217 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
218 std::vector<Device> audio_ins, audio_outs, video_ins;
219 audio_ins.push_back(Device(imaginary, imaginary));
220 audio_outs.push_back(Device(imaginary, imaginary));
221 video_ins.push_back(Device(imaginary, imaginary));
222 EXPECT_TRUE(dm->Init());
223 EXPECT_TRUE(dm->GetAudioInputDevices(&audio_ins));
224 EXPECT_TRUE(dm->GetAudioOutputDevices(&audio_outs));
225 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins));
226 for (size_t i = 0; i < audio_ins.size(); ++i) {
227 EXPECT_NE(imaginary, audio_ins[i].name);
228 }
229 for (size_t i = 0; i < audio_outs.size(); ++i) {
230 EXPECT_NE(imaginary, audio_outs[i].name);
231 }
232 for (size_t i = 0; i < video_ins.size(); ++i) {
233 EXPECT_NE(imaginary, video_ins[i].name);
234 }
235 }
236
CompareDeviceList(std::vector<Device> & devices,const char * const device_list[],int list_size)237 static bool CompareDeviceList(std::vector<Device>& devices,
238 const char* const device_list[], int list_size) {
239 if (list_size != static_cast<int>(devices.size())) {
240 return false;
241 }
242 for (int i = 0; i < list_size; ++i) {
243 if (devices[i].name.compare(device_list[i]) != 0) {
244 return false;
245 }
246 }
247 return true;
248 }
249
TEST(DeviceManagerTest,VerifyFilterDevices)250 TEST(DeviceManagerTest, VerifyFilterDevices) {
251 static const char* const kTotalDevicesName[] = {
252 "Google Camera Adapters are tons of fun.",
253 "device1",
254 "device2",
255 "device3",
256 "device4",
257 "device5",
258 "Google Camera Adapter 0",
259 "Google Camera Adapter 1",
260 };
261 static const char* const kFilteredDevicesName[] = {
262 "device2",
263 "device4",
264 "Google Camera Adapter",
265 NULL,
266 };
267 static const char* const kDevicesName[] = {
268 "device1",
269 "device3",
270 "device5",
271 };
272 std::vector<Device> devices;
273 for (int i = 0; i < arraysize(kTotalDevicesName); ++i) {
274 devices.push_back(Device(kTotalDevicesName[i], i));
275 }
276 EXPECT_TRUE(CompareDeviceList(devices, kTotalDevicesName,
277 arraysize(kTotalDevicesName)));
278 // Return false if given NULL as the exclusion list.
279 EXPECT_TRUE(DeviceManager::FilterDevices(&devices, NULL));
280 // The devices should not change.
281 EXPECT_TRUE(CompareDeviceList(devices, kTotalDevicesName,
282 arraysize(kTotalDevicesName)));
283 EXPECT_TRUE(DeviceManager::FilterDevices(&devices, kFilteredDevicesName));
284 EXPECT_TRUE(CompareDeviceList(devices, kDevicesName,
285 arraysize(kDevicesName)));
286 }
287
288 #ifdef WEBRTC_LINUX
289 class FakeV4LLookup : public cricket::V4LLookup {
290 public:
FakeV4LLookup(std::vector<std::string> device_paths)291 explicit FakeV4LLookup(std::vector<std::string> device_paths)
292 : device_paths_(device_paths) {}
293
294 protected:
CheckIsV4L2Device(const std::string & device)295 bool CheckIsV4L2Device(const std::string& device) {
296 return std::find(device_paths_.begin(), device_paths_.end(), device)
297 != device_paths_.end();
298 }
299
300 private:
301 std::vector<std::string> device_paths_;
302 };
303
TEST(DeviceManagerTest,GetVideoCaptureDevices_K2_6)304 TEST(DeviceManagerTest, GetVideoCaptureDevices_K2_6) {
305 std::vector<std::string> devices;
306 devices.push_back("/dev/video0");
307 devices.push_back("/dev/video5");
308 cricket::V4LLookup::SetV4LLookup(new FakeV4LLookup(devices));
309
310 std::vector<rtc::FakeFileSystem::File> files;
311 files.push_back(rtc::FakeFileSystem::File("/dev/video0", ""));
312 files.push_back(rtc::FakeFileSystem::File("/dev/video5", ""));
313 files.push_back(rtc::FakeFileSystem::File(
314 "/sys/class/video4linux/video0/name", "Video Device 1"));
315 files.push_back(rtc::FakeFileSystem::File(
316 "/sys/class/video4linux/video1/model", "Bad Device"));
317 files.push_back(
318 rtc::FakeFileSystem::File("/sys/class/video4linux/video5/model",
319 "Video Device 2"));
320 rtc::FilesystemScope fs(new rtc::FakeFileSystem(files));
321
322 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
323 std::vector<Device> video_ins;
324 EXPECT_TRUE(dm->Init());
325 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins));
326 EXPECT_EQ(2u, video_ins.size());
327 EXPECT_EQ("Video Device 1", video_ins.at(0).name);
328 EXPECT_EQ("Video Device 2", video_ins.at(1).name);
329 }
330
TEST(DeviceManagerTest,GetVideoCaptureDevices_K2_4)331 TEST(DeviceManagerTest, GetVideoCaptureDevices_K2_4) {
332 std::vector<std::string> devices;
333 devices.push_back("/dev/video0");
334 devices.push_back("/dev/video5");
335 cricket::V4LLookup::SetV4LLookup(new FakeV4LLookup(devices));
336
337 std::vector<rtc::FakeFileSystem::File> files;
338 files.push_back(rtc::FakeFileSystem::File("/dev/video0", ""));
339 files.push_back(rtc::FakeFileSystem::File("/dev/video5", ""));
340 files.push_back(rtc::FakeFileSystem::File(
341 "/proc/video/dev/video0",
342 "param1: value1\nname: Video Device 1\n param2: value2\n"));
343 files.push_back(rtc::FakeFileSystem::File(
344 "/proc/video/dev/video1",
345 "param1: value1\nname: Bad Device\n param2: value2\n"));
346 files.push_back(rtc::FakeFileSystem::File(
347 "/proc/video/dev/video5",
348 "param1: value1\nname: Video Device 2\n param2: value2\n"));
349 rtc::FilesystemScope fs(new rtc::FakeFileSystem(files));
350
351 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
352 std::vector<Device> video_ins;
353 EXPECT_TRUE(dm->Init());
354 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins));
355 EXPECT_EQ(2u, video_ins.size());
356 EXPECT_EQ("Video Device 1", video_ins.at(0).name);
357 EXPECT_EQ("Video Device 2", video_ins.at(1).name);
358 }
359
TEST(DeviceManagerTest,GetVideoCaptureDevices_KUnknown)360 TEST(DeviceManagerTest, GetVideoCaptureDevices_KUnknown) {
361 std::vector<std::string> devices;
362 devices.push_back("/dev/video0");
363 devices.push_back("/dev/video5");
364 cricket::V4LLookup::SetV4LLookup(new FakeV4LLookup(devices));
365
366 std::vector<rtc::FakeFileSystem::File> files;
367 files.push_back(rtc::FakeFileSystem::File("/dev/video0", ""));
368 files.push_back(rtc::FakeFileSystem::File("/dev/video1", ""));
369 files.push_back(rtc::FakeFileSystem::File("/dev/video5", ""));
370 rtc::FilesystemScope fs(new rtc::FakeFileSystem(files));
371
372 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
373 std::vector<Device> video_ins;
374 EXPECT_TRUE(dm->Init());
375 EXPECT_TRUE(dm->GetVideoCaptureDevices(&video_ins));
376 EXPECT_EQ(2u, video_ins.size());
377 EXPECT_EQ("/dev/video0", video_ins.at(0).name);
378 EXPECT_EQ("/dev/video5", video_ins.at(1).name);
379 }
380 #endif // WEBRTC_LINUX
381
382 // TODO(noahric): These are flaky on windows on headless machines.
383 #ifndef WIN32
TEST(DeviceManagerTest,GetWindows)384 TEST(DeviceManagerTest, GetWindows) {
385 if (!rtc::WindowPickerFactory::IsSupported()) {
386 LOG(LS_INFO) << "skipping test: window capturing is not supported with "
387 << "current configuration.";
388 return;
389 }
390 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
391 dm->SetScreenCapturerFactory(new FakeScreenCapturerFactory());
392 std::vector<rtc::WindowDescription> descriptions;
393 EXPECT_TRUE(dm->Init());
394 if (!dm->GetWindows(&descriptions) || descriptions.empty()) {
395 LOG(LS_INFO) << "skipping test: window capturing. Does not have any "
396 << "windows to capture.";
397 return;
398 }
399 scoped_ptr<cricket::VideoCapturer> capturer(dm->CreateScreenCapturer(
400 cricket::ScreencastId(descriptions.front().id())));
401 EXPECT_FALSE(capturer.get() == NULL);
402 // TODO(hellner): creating a window capturer and immediately deleting it
403 // results in "Continuous Build and Test Mainline - Mac opt" failure (crash).
404 // Remove the following line as soon as this has been resolved.
405 rtc::Thread::Current()->ProcessMessages(1);
406 }
407
TEST(DeviceManagerTest,GetDesktops)408 TEST(DeviceManagerTest, GetDesktops) {
409 if (!rtc::WindowPickerFactory::IsSupported()) {
410 LOG(LS_INFO) << "skipping test: desktop capturing is not supported with "
411 << "current configuration.";
412 return;
413 }
414 scoped_ptr<DeviceManagerInterface> dm(DeviceManagerFactory::Create());
415 dm->SetScreenCapturerFactory(new FakeScreenCapturerFactory());
416 std::vector<rtc::DesktopDescription> descriptions;
417 EXPECT_TRUE(dm->Init());
418 if (!dm->GetDesktops(&descriptions) || descriptions.empty()) {
419 LOG(LS_INFO) << "skipping test: desktop capturing. Does not have any "
420 << "desktops to capture.";
421 return;
422 }
423 scoped_ptr<cricket::VideoCapturer> capturer(dm->CreateScreenCapturer(
424 cricket::ScreencastId(descriptions.front().id())));
425 EXPECT_FALSE(capturer.get() == NULL);
426 }
427 #endif // !WIN32
428
TEST_F(DeviceManagerTestFake,CaptureConstraintsWhitelisted)429 TEST_F(DeviceManagerTestFake, CaptureConstraintsWhitelisted) {
430 const Device device("white", "white_id");
431 dm_->SetVideoCaptureDeviceMaxFormat(device.name, kHdFormat);
432 scoped_ptr<cricket::VideoCapturer> capturer(
433 dm_->CreateVideoCapturer(device));
434 cricket::VideoFormat best_format;
435 capturer->set_enable_camera_list(true);
436 EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format));
437 EXPECT_EQ(kHdFormat, best_format);
438 }
439
TEST_F(DeviceManagerTestFake,CaptureConstraintsNotWhitelisted)440 TEST_F(DeviceManagerTestFake, CaptureConstraintsNotWhitelisted) {
441 const Device device("regular", "regular_id");
442 scoped_ptr<cricket::VideoCapturer> capturer(
443 dm_->CreateVideoCapturer(device));
444 cricket::VideoFormat best_format;
445 capturer->set_enable_camera_list(true);
446 EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format));
447 EXPECT_EQ(kHdFormat, best_format);
448 }
449
TEST_F(DeviceManagerTestFake,CaptureConstraintsUnWhitelisted)450 TEST_F(DeviceManagerTestFake, CaptureConstraintsUnWhitelisted) {
451 const Device device("un_white", "un_white_id");
452 dm_->SetVideoCaptureDeviceMaxFormat(device.name, kHdFormat);
453 dm_->ClearVideoCaptureDeviceMaxFormat(device.name);
454 scoped_ptr<cricket::VideoCapturer> capturer(
455 dm_->CreateVideoCapturer(device));
456 cricket::VideoFormat best_format;
457 capturer->set_enable_camera_list(true);
458 EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format));
459 EXPECT_EQ(kHdFormat, best_format);
460 }
461
TEST_F(DeviceManagerTestFake,CaptureConstraintsWildcard)462 TEST_F(DeviceManagerTestFake, CaptureConstraintsWildcard) {
463 const Device device("any_device", "any_device");
464 dm_->SetVideoCaptureDeviceMaxFormat("*", kHdFormat);
465 scoped_ptr<cricket::VideoCapturer> capturer(
466 dm_->CreateVideoCapturer(device));
467 cricket::VideoFormat best_format;
468 capturer->set_enable_camera_list(true);
469 EXPECT_TRUE(capturer->GetBestCaptureFormat(kHdFormat, &best_format));
470 EXPECT_EQ(kHdFormat, best_format);
471 }
472