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

Side by Side Diff: media/midi/midi_manager_winrt.cc

Issue 2243183002: Web MIDI backend for Windows 10 (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: rebase, revise thread-safety Created 4 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
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "media/midi/midi_manager_winrt.h"
6
7 #include <robuffer.h>
8 #include <windows.devices.enumeration.h>
9 #include <windows.devices.midi.h>
10 #include <wrl/event.h>
11
12 #include "base/bind.h"
13 #include "base/containers/hash_tables.h"
14 #include "base/strings/string16.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "base/timer/timer.h"
17 #include "base/win/windows_version.h"
18
19 namespace media {
20 namespace midi {
21 namespace {
22
23 namespace WRL = Microsoft::WRL;
24
25 using namespace ABI::Windows::Devices::Enumeration;
26 using namespace ABI::Windows::Devices::Midi;
27 using namespace ABI::Windows::Foundation;
28 using namespace ABI::Windows::Storage::Streams;
29
30 // Factory functions that activate and create WinRT components. The caller takes
31 // ownership of the returning ComPtr.
32 template <typename InterfaceType, base::char16 const* runtime_class_id>
33 WRL::ComPtr<InterfaceType> WrlStaticsFactory() {
34 WRL::ComPtr<InterfaceType> com_ptr;
35
36 HRESULT hr = GetActivationFactory(
37 WRL::Wrappers::HStringReference(runtime_class_id).Get(), &com_ptr);
38 DCHECK(SUCCEEDED(hr));
39
40 return com_ptr;
41 }
42
43 WRL::ComPtr<IBufferFactory> GetBufferFactory() {
44 return WrlStaticsFactory<IBufferFactory,
45 RuntimeClass_Windows_Storage_Streams_Buffer>();
46 }
47
48 WRL::ComPtr<IDeviceInformationStatics> GetDeviceInformationStatics() {
49 return WrlStaticsFactory<
50 IDeviceInformationStatics,
51 RuntimeClass_Windows_Devices_Enumeration_DeviceInformation>();
52 }
53
54 template <typename T, HRESULT (T::*method)(HSTRING*)>
55 std::string GetStringFromObjectMethod(T* obj) {
56 HSTRING result;
57 HRESULT hr = (obj->*method)(&result);
58 DCHECK(SUCCEEDED(hr));
59
60 const base::char16* buffer = WindowsGetStringRawBuffer(result, nullptr);
61 if (buffer)
62 return base::WideToUTF8(buffer);
63 return std::string();
64 }
65
66 template <typename T>
67 std::string GetIdString(T* obj) {
68 return GetStringFromObjectMethod<T, &T::get_Id>(obj);
69 }
70
71 template <typename T>
72 std::string GetDeviceIdString(T* obj) {
73 return GetStringFromObjectMethod<T, &T::get_DeviceId>(obj);
74 }
75
76 std::string GetNameString(IDeviceInformation* info) {
77 return GetStringFromObjectMethod<IDeviceInformation,
78 &IDeviceInformation::get_Name>(info);
79 }
80
81 uint8_t* GetPointerToBufferData(IBuffer* buffer) {
82 WRL::ComPtr<IInspectable> inspectable(
83 reinterpret_cast<IInspectable*>(buffer));
84 WRL::ComPtr<Windows::Storage::Streams::IBufferByteAccess> buffer_byte_access;
85
86 HRESULT hr = inspectable.As(&buffer_byte_access);
87 DCHECK(SUCCEEDED(hr));
88
89 uint8_t* ptr = nullptr;
90 hr = buffer_byte_access->Buffer(&ptr);
91 DCHECK(SUCCEEDED(hr));
92
93 // Lifetime of the pointing buffer is controlled by the buffer object.
94 return ptr;
95 }
96
97 template <typename InterfaceType>
98 struct MidiPort {
99 MidiPort() = default;
100
101 uint32_t index;
102 WRL::ComPtr<InterfaceType> handle;
103 base::TimeTicks start_time;
104
105 private:
106 DISALLOW_COPY_AND_ASSIGN(MidiPort);
107 };
108
109 template <typename InterfaceType,
110 typename RuntimeType,
111 typename StaticsInterfaceType,
112 base::char16 const* runtime_class_id>
113 class MidiPortManager {
114 public:
115 // MidiPortManager instances should be constructed on the COM thread.
116 MidiPortManager(MidiManagerWinrt* midi_manager)
117 : midi_manager_(midi_manager),
118 task_runner_(base::ThreadTaskRunnerHandle::Get()) {}
119
120 void StartWatcher() {
Shao-Chuan Lee 2016/08/23 04:33:29 Missing |thread_checker_| check.
121 HRESULT hr;
122
123 midi_port_statics_ =
124 WrlStaticsFactory<StaticsInterfaceType, runtime_class_id>();
125
126 HSTRING device_selector = nullptr;
127 hr = midi_port_statics_->GetDeviceSelector(&device_selector);
128 DCHECK(SUCCEEDED(hr));
129
130 hr = GetDeviceInformationStatics()->CreateWatcherAqsFilter(device_selector,
131 &watcher_);
132 DCHECK(SUCCEEDED(hr));
133
134 // Register callbacks to WinRT that post state-modifying jobs back to COM
135 // thread. |weak_ptr| and |task_runner| are captured by lambda callbacks for
136 // posting jobs. Note that WinRT callback arguments should not be passed
137 // outside the callback since the pointers may be unavailable afterwards.
138 base::WeakPtr<MidiPortManager> weak_ptr = GetWeakPtrFromFactory();
139 scoped_refptr<base::SingleThreadTaskRunner> task_runner = task_runner_;
140
141 hr = watcher_->add_Added(
142 WRL::Callback<ITypedEventHandler<DeviceWatcher*, DeviceInformation*>>(
143 [weak_ptr, task_runner](IDeviceWatcher* watcher,
144 IDeviceInformation* info) {
145 std::string dev_id = GetIdString(info),
146 dev_name = GetNameString(info);
147
148 task_runner->PostTask(
149 FROM_HERE, base::Bind(&MidiPortManager::OnAdded, weak_ptr,
150 dev_id, dev_name));
151
152 return S_OK;
153 })
154 .Get(),
155 &token_Added_);
156 DCHECK(SUCCEEDED(hr));
157
158 hr = watcher_->add_EnumerationCompleted(
159 WRL::Callback<ITypedEventHandler<DeviceWatcher*, IInspectable*>>(
160 [weak_ptr, task_runner](IDeviceWatcher* watcher,
161 IInspectable* insp) {
162 task_runner->PostTask(
163 FROM_HERE,
164 base::Bind(&MidiPortManager::OnEnumerationCompleted,
165 weak_ptr));
166
167 return S_OK;
168 })
169 .Get(),
170 &token_EnumerationCompleted_);
171 DCHECK(SUCCEEDED(hr));
172
173 hr = watcher_->add_Removed(
174 WRL::Callback<
175 ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>>(
176 [weak_ptr, task_runner](IDeviceWatcher* watcher,
177 IDeviceInformationUpdate* update) {
178 std::string dev_id = GetIdString(update);
179
180 task_runner->PostTask(
181 FROM_HERE,
182 base::Bind(&MidiPortManager::OnRemoved, weak_ptr, dev_id));
183
184 return S_OK;
185 })
186 .Get(),
187 &token_Removed_);
188 DCHECK(SUCCEEDED(hr));
189
190 hr = watcher_->add_Stopped(
191 WRL::Callback<ITypedEventHandler<DeviceWatcher*, IInspectable*>>(
192 [](IDeviceWatcher* watcher, IInspectable* insp) {
193 // Placeholder, does nothing for now.
194 return S_OK;
195 })
196 .Get(),
197 &token_Stopped_);
198 DCHECK(SUCCEEDED(hr));
199
200 hr = watcher_->add_Updated(
201 WRL::Callback<
202 ITypedEventHandler<DeviceWatcher*, DeviceInformationUpdate*>>(
203 [](IDeviceWatcher* watcher, IDeviceInformationUpdate* update) {
204 // TODO(shaochuan): Check for fields to be updated here.
205 return S_OK;
206 })
207 .Get(),
208 &token_Updated_);
209 DCHECK(SUCCEEDED(hr));
210
211 hr = watcher_->Start();
212 DCHECK(SUCCEEDED(hr));
213 }
214
215 ~MidiPortManager() {
216 DCHECK(thread_checker_.CalledOnValidThread());
217
218 watcher_->remove_Added(token_Added_);
219 watcher_->remove_EnumerationCompleted(token_EnumerationCompleted_);
220 watcher_->remove_Removed(token_Removed_);
221 watcher_->remove_Stopped(token_Stopped_);
222 watcher_->remove_Updated(token_Updated_);
223
224 watcher_->Stop();
225 }
226
227 MidiPort<InterfaceType>* GetPortByDeviceId(std::string dev_id) {
228 DCHECK(thread_checker_.CalledOnValidThread());
229
230 auto it = ports_.find(dev_id);
231 if (it == ports_.end())
232 return nullptr;
233 return it->second.get();
234 }
235
236 MidiPort<InterfaceType>* GetPortByIndex(uint32_t port_index) {
237 DCHECK(thread_checker_.CalledOnValidThread());
238
239 auto it = ports_.find(port_ids_[port_index]);
240 if (it == ports_.end())
241 return nullptr;
242 return it->second.get();
243 }
244
245 protected:
246 // The owning MidiManagerWinrt.
247 MidiManagerWinrt* midi_manager_;
248
249 // Task runner of the COM thread.
250 scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
251
252 // Ensures all methods are called on the COM thread.
253 base::ThreadChecker thread_checker_;
254
255 private:
256 // DeviceWatcher callbacks:
257 void OnAdded(std::string dev_id, std::string dev_name) {
258 DCHECK(thread_checker_.CalledOnValidThread());
259
260 // TODO(shaochuan): Disable Microsoft GS Wavetable Synth due to security
261 // reasons. http://crbug.com/499279
262
263 port_names_[dev_id] = dev_name;
264
265 base::string16 dev_id_string16 = base::UTF8ToWide(dev_id);
266 HSTRING dev_id_hstring;
267 HRESULT hr = WindowsCreateString(
268 dev_id_string16.c_str(), static_cast<UINT32>(dev_id_string16.length()),
269 &dev_id_hstring);
270 DCHECK(SUCCEEDED(hr));
271
272 WRL::ComPtr<IAsyncOperation<RuntimeType*>> async_op;
273
274 hr = midi_port_statics_->FromIdAsync(dev_id_hstring, &async_op);
275 DCHECK(SUCCEEDED(hr));
276
277 WindowsDeleteString(dev_id_hstring);
278 dev_id_hstring = nullptr;
279
280 base::WeakPtr<MidiPortManager> weak_ptr = GetWeakPtrFromFactory();
281 scoped_refptr<base::SingleThreadTaskRunner> task_runner = task_runner_;
282
283 hr = async_op->put_Completed(
284 WRL::Callback<IAsyncOperationCompletedHandler<RuntimeType*>>(
285 [weak_ptr, task_runner](IAsyncOperation<RuntimeType*>* async_op,
286 AsyncStatus status) {
287 // TODO(shaochuan): Check if port open time is accurate.
288 const auto now = base::TimeTicks::Now();
289
290 InterfaceType* handle;
291 HRESULT hr = async_op->GetResults(&handle);
292 DCHECK(SUCCEEDED(hr));
293
294 // |async_op| is owned by |async_ops_|, safe to pass outside.
295 task_runner->PostTask(
296 FROM_HERE,
297 base::Bind(&MidiPortManager::OnCompletedGetPortFromIdAsync,
298 weak_ptr, handle, now, async_op));
299
300 return S_OK;
301 })
302 .Get());
303 DCHECK(SUCCEEDED(hr));
304
305 // Keep a reference to incompleted |async_op| to ensure lifetime.
306 async_ops_.insert(std::move(async_op));
307 }
308
309 void OnEnumerationCompleted() {
310 DCHECK(thread_checker_.CalledOnValidThread());
311
312 midi_manager_->OnPortManagerReady();
313 }
314
315 void OnRemoved(std::string dev_id) {
316 DCHECK(thread_checker_.CalledOnValidThread());
317
318 MidiPort<InterfaceType>* port = GetPortByDeviceId(dev_id);
319 DCHECK(port != nullptr);
320
321 SetPortState(port->index, MIDI_PORT_DISCONNECTED);
322
323 port->handle = nullptr;
324 }
325
326 void OnCompletedGetPortFromIdAsync(InterfaceType* handle,
327 base::TimeTicks start_time,
328 IAsyncOperation<RuntimeType*>* async_op) {
329 DCHECK(thread_checker_.CalledOnValidThread());
330
331 RegisterOnMessageReceived(handle);
332
333 std::string dev_id = GetDeviceIdString(handle);
334
335 MidiPort<InterfaceType>* port = GetPortByDeviceId(dev_id);
336
337 if (port == nullptr) {
338 // TODO(shaochuan): Fill in manufacturer and driver version.
339 AddPort(MidiPortInfo(dev_id, std::string("Manufacturer"),
340 port_names_[dev_id], std::string("DriverVersion"),
341 MIDI_PORT_OPENED));
342
343 port = new MidiPort<InterfaceType>;
344 port->index = static_cast<uint32_t>(port_ids_.size());
345
346 ports_[dev_id].reset(port);
347 port_ids_.push_back(dev_id);
348 } else {
349 SetPortState(port->index, MIDI_PORT_CONNECTED);
350 }
351
352 port->handle = handle;
353 port->start_time = start_time;
354
355 // Remove reference to completed |async_op|.
356 auto it = async_ops_.find(async_op);
357 DCHECK(it != async_ops_.end());
358 async_ops_.erase(it);
359 }
360
361 // Overrided by MidiInPortManager to listen to input ports.
362 virtual void RegisterOnMessageReceived(InterfaceType* handle) {}
363
364 // Calls midi_manager_->Add{Input,Output}Port.
365 virtual void AddPort(MidiPortInfo info) = 0;
366
367 // Calls midi_manager_->Set{Input,Output}PortState.
368 virtual void SetPortState(uint32_t port_index, MidiPortState state) = 0;
369
370 // WeakPtrFactory has to be declared in derived class, use this method to
371 // retrieve upcasted WeakPtr for posting tasks.
372 virtual base::WeakPtr<MidiPortManager> GetWeakPtrFromFactory() = 0;
373
374 // Midi{In,Out}PortStatics instance.
375 WRL::ComPtr<StaticsInterfaceType> midi_port_statics_;
376
377 // DeviceWatcher instance and event registration tokens for unsubscribing
378 // events in destructor.
379 WRL::ComPtr<IDeviceWatcher> watcher_;
380 EventRegistrationToken token_Added_, token_EnumerationCompleted_,
381 token_Removed_, token_Stopped_, token_Updated_;
382
383 // All manipulations to these fields should be done on COM thread.
384 base::hash_map<std::string, std::unique_ptr<MidiPort<InterfaceType>>> ports_;
385 std::vector<std::string> port_ids_;
386 base::hash_map<std::string, std::string> port_names_;
387
388 // Keeps AsyncOperation objects before the operation completes.
389 std::set<WRL::ComPtr<IAsyncOperation<RuntimeType*>>> async_ops_;
390 };
391
392 } // namespace
393
394 class MidiManagerWinrt::MidiInPortManager final
395 : public MidiPortManager<IMidiInPort,
396 MidiInPort,
397 IMidiInPortStatics,
398 RuntimeClass_Windows_Devices_Midi_MidiInPort> {
399 public:
400 MidiInPortManager(MidiManagerWinrt* midi_manager)
401 : MidiPortManager(midi_manager), weak_factory_(this) {}
402
403 private:
404 // MidiPortManager overrides:
405 void RegisterOnMessageReceived(IMidiInPort* handle) override {
406 DCHECK(thread_checker_.CalledOnValidThread());
407
408 EventRegistrationToken& token = tokens_[GetDeviceIdString(handle)];
409
410 base::WeakPtr<MidiInPortManager> weak_ptr = weak_factory_.GetWeakPtr();
411 scoped_refptr<base::SingleThreadTaskRunner> task_runner = task_runner_;
412
413 handle->add_MessageReceived(
414 WRL::Callback<
415 ITypedEventHandler<MidiInPort*, MidiMessageReceivedEventArgs*>>(
416 [weak_ptr, task_runner](IMidiInPort* handle,
417 IMidiMessageReceivedEventArgs* args) {
418 std::string dev_id = GetDeviceIdString(handle);
419
420 WRL::ComPtr<IMidiMessage> message;
421 HRESULT hr = args->get_Message(&message);
422 DCHECK(SUCCEEDED(hr));
423
424 WRL::ComPtr<IBuffer> buffer;
425 hr = message->get_RawData(&buffer);
426 DCHECK(SUCCEEDED(hr));
427
428 uint8_t* p_buffer_data = GetPointerToBufferData(buffer.Get());
429
430 uint32_t data_length;
431 hr = buffer->get_Length(&data_length);
432 DCHECK(SUCCEEDED(hr));
433 DCHECK(data_length != 0);
434
435 std::vector<uint8_t> data(p_buffer_data,
436 p_buffer_data + data_length);
437
438 // Time since port opened in 100-nanosecond units.
439 TimeSpan time_span;
440 hr = message->get_Timestamp(&time_span);
441 DCHECK(SUCCEEDED(hr));
442
443 task_runner->PostTask(
444 FROM_HERE,
445 base::Bind(&MidiInPortManager::OnMessageReceived, weak_ptr,
446 dev_id, data, base::TimeDelta::FromMicroseconds(
447 time_span.Duration / 10)));
448
449 return S_OK;
450 })
451 .Get(),
452 &token);
453 }
454
455 void AddPort(MidiPortInfo info) final { midi_manager_->AddInputPort(info); }
456
457 void SetPortState(uint32_t port_index, MidiPortState state) final {
458 midi_manager_->SetInputPortState(port_index, state);
459 }
460
461 base::WeakPtr<MidiPortManager> GetWeakPtrFromFactory() final {
462 DCHECK(thread_checker_.CalledOnValidThread());
463
464 return weak_factory_.GetWeakPtr();
465 }
466
467 // Callback on receiving MIDI input message.
468 void OnMessageReceived(std::string dev_id,
469 std::vector<uint8_t> data,
470 base::TimeDelta time_since_start) {
471 DCHECK(thread_checker_.CalledOnValidThread());
472
473 MidiPort<IMidiInPort>* port = GetPortByDeviceId(dev_id);
474 DCHECK(port != nullptr);
475
476 midi_manager_->ReceiveMidiData(port->index, &data[0], data.size(),
477 port->start_time + time_since_start);
478 }
479
480 // Event tokens for input message received events.
481 base::hash_map<std::string, EventRegistrationToken> tokens_;
482
483 // Last member to ensure destructed first.
484 base::WeakPtrFactory<MidiInPortManager> weak_factory_;
485
486 DISALLOW_COPY_AND_ASSIGN(MidiInPortManager);
487 };
488
489 class MidiManagerWinrt::MidiOutPortManager final
490 : public MidiPortManager<IMidiOutPort,
491 IMidiOutPort,
492 IMidiOutPortStatics,
493 RuntimeClass_Windows_Devices_Midi_MidiOutPort> {
494 public:
495 MidiOutPortManager(MidiManagerWinrt* midi_manager)
496 : MidiPortManager(midi_manager), weak_factory_(this) {}
497
498 private:
499 // MidiPortManager overrides:
500 void AddPort(MidiPortInfo info) final { midi_manager_->AddOutputPort(info); }
501
502 void SetPortState(uint32_t port_index, MidiPortState state) final {
503 midi_manager_->SetOutputPortState(port_index, state);
504 }
505
506 base::WeakPtr<MidiPortManager> GetWeakPtrFromFactory() final {
507 DCHECK(thread_checker_.CalledOnValidThread());
508
509 return weak_factory_.GetWeakPtr();
510 }
511
512 // Last member to ensure destructed first.
513 base::WeakPtrFactory<MidiOutPortManager> weak_factory_;
514
515 DISALLOW_COPY_AND_ASSIGN(MidiOutPortManager);
516 };
517
518 MidiManagerWinrt::MidiManagerWinrt() : com_thread_("Windows MIDI COM Thread") {}
519
520 MidiManagerWinrt::~MidiManagerWinrt() {}
521
522 void MidiManagerWinrt::StartInitialization() {
523 DCHECK(base::win::GetVersion() >= base::win::VERSION_WIN10);
524
525 com_thread_.init_com_with_mta(true);
526 com_thread_.Start();
527
528 com_thread_.task_runner()->PostTask(
529 FROM_HERE, base::Bind(&MidiManagerWinrt::InitializeOnComThread,
530 base::Unretained(this)));
531 }
532
533 void MidiManagerWinrt::Finalize() {
534 com_thread_.task_runner()->PostTask(
535 FROM_HERE, base::Bind(&MidiManagerWinrt::FinalizeOnComThread,
536 base::Unretained(this)));
537
538 // Blocks until FinalizeOnComThread() returns. Delayed MIDI send data tasks
539 // will be ignored.
540 com_thread_.Stop();
541
542 port_manager_ready_count_ = 0;
543 }
544
545 void MidiManagerWinrt::DispatchSendMidiData(MidiManagerClient* client,
546 uint32_t port_index,
547 const std::vector<uint8_t>& data,
548 double timestamp) {
549 base::AutoLock auto_lock(scheduler_lock_);
550 if (!scheduler_)
551 return;
552
553 scheduler_->PostSendDataTask(
554 client, data.size(), timestamp,
555 base::Bind(&MidiManagerWinrt::SendOnComThread, base::Unretained(this),
556 port_index, data),
557 com_thread_.task_runner());
558 }
559
560 void MidiManagerWinrt::InitializeOnComThread() {
561 if (!com_thread_checker_)
562 com_thread_checker_.reset(new base::ThreadChecker);
563 DCHECK(com_thread_checker_->CalledOnValidThread());
564
565 port_manager_in_.reset(new MidiInPortManager(this));
566 port_manager_out_.reset(new MidiOutPortManager(this));
567
568 {
569 base::AutoLock auto_lock(scheduler_lock_);
570 scheduler_.reset(new MidiScheduler(this));
571 }
572
573 port_manager_in_->StartWatcher();
574 port_manager_out_->StartWatcher();
575 }
576
577 void MidiManagerWinrt::FinalizeOnComThread() {
578 DCHECK(com_thread_checker_->CalledOnValidThread());
579
580 {
581 base::AutoLock auto_lock(scheduler_lock_);
582 scheduler_.reset();
583 }
584
585 port_manager_in_.reset();
586 port_manager_out_.reset();
587 }
588
589 void MidiManagerWinrt::SendOnComThread(uint32_t port_index,
590 const std::vector<uint8_t>& data) {
591 DCHECK(com_thread_checker_->CalledOnValidThread());
592
593 WRL::ComPtr<IBuffer> buffer;
594 HRESULT hr =
595 GetBufferFactory()->Create(static_cast<UINT32>(data.size()), &buffer);
596 DCHECK(SUCCEEDED(hr));
597
598 hr = buffer->put_Length(static_cast<UINT32>(data.size()));
599 DCHECK(SUCCEEDED(hr));
600
601 uint8_t* p_buffer_data = GetPointerToBufferData(buffer.Get());
602
603 std::copy(data.begin(), data.end(), p_buffer_data);
604
605 MidiPort<IMidiOutPort>* port = port_manager_out_->GetPortByIndex(port_index);
606 DCHECK(port != nullptr);
607
608 hr = port->handle->SendBuffer(buffer.Get());
609 DCHECK(SUCCEEDED(hr));
610 }
611
612 void MidiManagerWinrt::OnPortManagerReady() {
613 DCHECK(com_thread_checker_->CalledOnValidThread());
614 DCHECK(port_manager_ready_count_ < 2);
615
616 if (++port_manager_ready_count_ == 2)
617 CompleteInitialization(Result::OK);
618 }
619
620 MidiManager* MidiManager::Create() {
621 return new MidiManagerWinrt();
622 }
623
624 } // namespace midi
625 } // namespace media
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698