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

Side by Side Diff: content/renderer/pepper/pepper_platform_audio_output.cc

Issue 2755613002: Support audio output device enumeration and selection in PPAPI (Closed)
Patch Set: Should not include local change to ppapi/generators/idl_outfile.py Created 3 years, 9 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
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 "content/renderer/pepper/pepper_platform_audio_output.h" 5 #include "content/renderer/pepper/pepper_platform_audio_output.h"
6 6
7 #include "base/bind.h" 7 #include "base/bind.h"
8 #include "base/location.h" 8 #include "base/location.h"
9 #include "base/logging.h" 9 #include "base/logging.h"
10 #include "base/single_thread_task_runner.h" 10 #include "base/single_thread_task_runner.h"
11 #include "base/threading/thread_task_runner_handle.h" 11 #include "base/threading/thread_task_runner_handle.h"
12 #include "base/time/time.h"
13 #include "base/timer/timer.h"
12 #include "build/build_config.h" 14 #include "build/build_config.h"
13 #include "content/child/child_process.h" 15 #include "content/child/child_process.h"
16 #include "content/common/content_constants_internal.h"
14 #include "content/common/media/audio_messages.h" 17 #include "content/common/media/audio_messages.h"
15 #include "content/renderer/media/audio_message_filter.h" 18 #include "content/renderer/media/audio_message_filter.h"
16 #include "content/renderer/pepper/audio_helper.h" 19 #include "content/renderer/pepper/audio_helper.h"
20 #include "content/renderer/pepper/pepper_audio_output_host.h"
21 #include "content/renderer/pepper/pepper_media_device_manager.h"
22 #include "content/renderer/render_frame_impl.h"
17 #include "content/renderer/render_thread_impl.h" 23 #include "content/renderer/render_thread_impl.h"
24 #include "media/audio/audio_device_description.h"
18 #include "ppapi/shared_impl/ppb_audio_config_shared.h" 25 #include "ppapi/shared_impl/ppb_audio_config_shared.h"
19 26
20 namespace content { 27 namespace content {
21 28
22 // static 29 // static
23 PepperPlatformAudioOutput* PepperPlatformAudioOutput::Create( 30 PepperPlatformAudioOutput* PepperPlatformAudioOutput::Create(
24 int sample_rate, 31 int sample_rate,
25 int frames_per_buffer, 32 int frames_per_buffer,
26 int source_render_frame_id, 33 int source_render_frame_id,
27 AudioHelper* client) { 34 AudioHelper* client) {
28 scoped_refptr<PepperPlatformAudioOutput> audio_output( 35 scoped_refptr<PepperPlatformAudioOutput> audio_output(
29 new PepperPlatformAudioOutput()); 36 new PepperPlatformAudioOutput());
30 if (audio_output->Initialize(sample_rate, 37 if (audio_output->Initialize(sample_rate,
31 frames_per_buffer, 38 frames_per_buffer,
32 source_render_frame_id, 39 source_render_frame_id,
33 client)) { 40 client)) {
34 // Balanced by Release invoked in 41 // Balanced by Release invoked in
35 // PepperPlatformAudioOutput::ShutDownOnIOThread(). 42 // PepperPlatformAudioOutput::ShutDownOnIOThread().
36 audio_output->AddRef(); 43 audio_output->AddRef();
37 return audio_output.get(); 44 return audio_output.get();
38 } 45 }
39 return NULL; 46 return NULL;
40 } 47 }
41 48
49 // static
50 PepperPlatformAudioOutput* PepperPlatformAudioOutput::Create(
51 int render_frame_id,
52 const std::string& device_id,
53 const GURL& document_url,
54 int sample_rate,
55 int frames_per_buffer,
56 PepperAudioOutputHost* client) {
57 scoped_refptr<PepperPlatformAudioOutput> audio_output(
58 new PepperPlatformAudioOutput(render_frame_id, device_id, document_url,
59 // Set authorization request timeout at 80% of renderer hung timeout,
60 // but no more than kMaxAuthorizationTimeout.
61 base::TimeDelta::FromMilliseconds(
62 std::min(kHungRendererDelayMs * 8 / 10,
63 kMaxAuthorizationTimeoutMs))));
64
65 if (audio_output->Initialize(sample_rate,
66 frames_per_buffer,
67 client)) {
68 // Balanced by Release invoked in
69 // PepperPlatformAudioOutput::ShutDownOnIOThread().
70 audio_output->AddRef();
71 return audio_output.get();
72 }
73 return NULL;
74 }
75
76 void PepperPlatformAudioOutput::RequestDeviceAuthorization() {
77 if (ipc_) {
78 io_task_runner_->PostTask(
79 FROM_HERE,
80 base::Bind(
81 &PepperPlatformAudioOutput::RequestDeviceAuthorizationOnIOThread,
82 this));
83 }
84 }
85
42 bool PepperPlatformAudioOutput::StartPlayback() { 86 bool PepperPlatformAudioOutput::StartPlayback() {
43 if (ipc_) { 87 if (ipc_) {
44 io_task_runner_->PostTask( 88 io_task_runner_->PostTask(
45 FROM_HERE, 89 FROM_HERE,
46 base::Bind(&PepperPlatformAudioOutput::StartPlaybackOnIOThread, this)); 90 base::Bind(&PepperPlatformAudioOutput::StartPlaybackOnIOThread, this));
47 return true; 91 return true;
48 } 92 }
49 return false; 93 return false;
50 } 94 }
51 95
(...skipping 15 matching lines...) Expand all
67 this, volume)); 111 this, volume));
68 return true; 112 return true;
69 } 113 }
70 return false; 114 return false;
71 } 115 }
72 116
73 void PepperPlatformAudioOutput::ShutDown() { 117 void PepperPlatformAudioOutput::ShutDown() {
74 // Called on the main thread to stop all audio callbacks. We must only change 118 // Called on the main thread to stop all audio callbacks. We must only change
75 // the client on the main thread, and the delegates from the I/O thread. 119 // the client on the main thread, and the delegates from the I/O thread.
76 client_ = NULL; 120 client_ = NULL;
121 host_client_ = NULL;
77 io_task_runner_->PostTask( 122 io_task_runner_->PostTask(
78 FROM_HERE, 123 FROM_HERE,
79 base::Bind(&PepperPlatformAudioOutput::ShutDownOnIOThread, this)); 124 base::Bind(&PepperPlatformAudioOutput::ShutDownOnIOThread, this));
80 } 125 }
81 126
82 void PepperPlatformAudioOutput::OnError() {} 127 void PepperPlatformAudioOutput::OnError() {
128 DCHECK(io_task_runner_->BelongsToCurrentThread());
129
130 // Do nothing if the stream has been closed.
131 if (state_ < CREATING_STREAM)
132 return;
133
134 DLOG(WARNING) << "PepperPlatformAudioOutput::OnError()";
135
136 main_task_runner_->PostTask(FROM_HERE,
137 base::Bind(&PepperPlatformAudioOutput::NotifyStreamCreationFailed, this));
bbudge 2017/03/23 18:05:44 This looks like a change to the behavior of PPB_Au
Xing 2017/03/29 21:14:32 New implementation has been moved to pepper_platfo
138 }
83 139
84 void PepperPlatformAudioOutput::OnDeviceAuthorized( 140 void PepperPlatformAudioOutput::OnDeviceAuthorized(
85 media::OutputDeviceStatus device_status, 141 media::OutputDeviceStatus device_status,
86 const media::AudioParameters& output_params, 142 const media::AudioParameters& output_params,
87 const std::string& matched_device_id) { 143 const std::string& matched_device_id) {
88 NOTREACHED(); 144 DCHECK(io_task_runner_->BelongsToCurrentThread());
145
146 auth_timeout_action_.reset();
147
148 // Do nothing if late authorization is received after timeout.
149 if (state_ == IPC_CLOSED)
150 return;
151
152 LOG_IF(WARNING, device_status == media::OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT)
153 << "Output device authorization timed out";
154
155 DCHECK_EQ(state_, AUTHORIZING);
156
157 // It may happen that a second authorization is received as a result to a
158 // call to StartPlayback() after Shutdown(). If the status for the second
159 // authorization differs from the first, it will not be reflected in
160 // |device_status_| to avoid a race.
161 // This scenario is unlikely. If it occurs, the new value will be
162 // different from OUTPUT_DEVICE_STATUS_OK, so the PepperPlatformAudioOutput
163 // will enter the IPC_CLOSED state anyway, which is the safe thing to do.
164 // This is preferable to holding a lock.
165 if (!did_receive_auth_.IsSignaled())
166 device_status_ = device_status;
167
168 if (device_status == media::OUTPUT_DEVICE_STATUS_OK) {
169 state_ = AUTHORIZED;
170 if (!did_receive_auth_.IsSignaled()) {
171 output_params_ = output_params;
172
173 // It's possible to not have a matched device obtained via session id. It
174 // means matching output device through |session_id_| failed and the
175 // default device is used.
176 DCHECK(media::AudioDeviceDescription::UseSessionIdToSelectDevice(
177 session_id_,
178 device_id_) ||
179 matched_device_id_.empty());
180 matched_device_id_ = matched_device_id;
181
182 DVLOG(1) << "PepperPlatformAudioOutput authorized, session_id: "
183 << session_id_ << ", device_id: " << device_id_
184 << ", matched_device_id: " << matched_device_id_;
185
186 did_receive_auth_.Signal();
187 }
188 if (start_on_authorized_)
189 CreateStreamOnIOThread(params_);
190 }
191 else {
192 // Closing IPC forces a Signal(), so no clients are locked waiting
193 // indefinitely after this method returns.
194 ipc_->CloseStream();
195 OnIPCClosed();
196 main_task_runner_->PostTask(FROM_HERE,
197 base::Bind(&PepperPlatformAudioOutput::NotifyStreamCreationFailed,
198 this));
199 }
89 } 200 }
90 201
91 void PepperPlatformAudioOutput::OnStreamCreated( 202 void PepperPlatformAudioOutput::OnStreamCreated(
92 base::SharedMemoryHandle handle, 203 base::SharedMemoryHandle handle,
93 base::SyncSocket::Handle socket_handle, 204 base::SyncSocket::Handle socket_handle,
94 int length) { 205 int length) {
95 #if defined(OS_WIN) 206 #if defined(OS_WIN)
96 DCHECK(handle.IsValid()); 207 DCHECK(handle.IsValid());
97 DCHECK(socket_handle); 208 DCHECK(socket_handle);
98 #else 209 #else
99 DCHECK(base::SharedMemory::IsHandleValid(handle)); 210 DCHECK(base::SharedMemory::IsHandleValid(handle));
100 DCHECK_NE(-1, socket_handle); 211 DCHECK_NE(-1, socket_handle);
101 #endif 212 #endif
102 DCHECK(length); 213 DCHECK(length);
103 214
104 if (base::ThreadTaskRunnerHandle::Get().get() == main_task_runner_.get()) { 215 if (base::ThreadTaskRunnerHandle::Get().get() == main_task_runner_.get()) {
105 // Must dereference the client only on the main thread. Shutdown may have 216 // Must dereference the client only on the main thread. Shutdown may have
106 // occurred while the request was in-flight, so we need to NULL check. 217 // occurred while the request was in-flight, so we need to NULL check.
107 if (client_) 218 if (client_)
108 client_->StreamCreated(handle, length, socket_handle); 219 client_->StreamCreated(handle, length, socket_handle);
220 else if (host_client_)
221 host_client_->StreamCreated(handle, length, socket_handle);
109 } else { 222 } else {
110 main_task_runner_->PostTask( 223 DCHECK(io_task_runner_->BelongsToCurrentThread());
111 FROM_HERE, base::Bind(&PepperPlatformAudioOutput::OnStreamCreated, this, 224 if (state_ != CREATING_STREAM)
112 handle, socket_handle, length)); 225 return;
226
227 state_ = PAUSED;
228 if (play_on_start_)
229 StartPlaybackOnIOThread();
230
231 main_task_runner_->PostTask(FROM_HERE,
232 base::Bind(&PepperPlatformAudioOutput::OnStreamCreated, this,
233 handle, socket_handle, length));
113 } 234 }
114 } 235 }
115 236
116 void PepperPlatformAudioOutput::OnIPCClosed() { ipc_.reset(); } 237 void PepperPlatformAudioOutput::OnIPCClosed() {
238 DCHECK(io_task_runner_->BelongsToCurrentThread());
239 state_ = IPC_CLOSED;
240 ipc_.reset();
241
242 // Signal to unblock any blocked threads waiting for parameters
243 did_receive_auth_.Signal();
244 }
117 245
118 PepperPlatformAudioOutput::~PepperPlatformAudioOutput() { 246 PepperPlatformAudioOutput::~PepperPlatformAudioOutput() {
119 // Make sure we have been shut down. Warning: this will usually happen on 247 // Make sure we have been shut down. Warning: this will usually happen on
120 // the I/O thread! 248 // the I/O thread!
121 DCHECK(!ipc_); 249 DCHECK(!ipc_);
122 DCHECK(!client_); 250 DCHECK(!client_);
251 DCHECK(!host_client_);
123 } 252 }
124 253
125 PepperPlatformAudioOutput::PepperPlatformAudioOutput() 254 PepperPlatformAudioOutput::PepperPlatformAudioOutput()
126 : client_(NULL), 255 : client_(NULL),
256 host_client_(NULL),
127 main_task_runner_(base::ThreadTaskRunnerHandle::Get()), 257 main_task_runner_(base::ThreadTaskRunnerHandle::Get()),
128 io_task_runner_(ChildProcess::current()->io_task_runner()) { 258 io_task_runner_(ChildProcess::current()->io_task_runner()),
259 render_frame_id_(MSG_ROUTING_NONE),
260 state_(IDLE),
261 start_on_authorized_(true),
262 play_on_start_(false),
263 session_id_(0),
264 did_receive_auth_(base::WaitableEvent::ResetPolicy::MANUAL,
265 base::WaitableEvent::InitialState::NOT_SIGNALED),
266 device_status_(media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL) {
267 }
268
269 PepperPlatformAudioOutput::PepperPlatformAudioOutput(int render_frame_id,
270 const std::string& device_id,
271 const GURL& document_url,
272 base::TimeDelta authorization_timeout)
273 : client_(NULL),
274 host_client_(NULL),
275 main_task_runner_(base::ThreadTaskRunnerHandle::Get()),
276 io_task_runner_(ChildProcess::current()->io_task_runner()),
277 render_frame_id_(render_frame_id),
278 state_(IDLE),
279 start_on_authorized_(true),
280 play_on_start_(false),
281 session_id_(0),
282 device_id_(device_id),
283 security_origin_(document_url),
284 did_receive_auth_(base::WaitableEvent::ResetPolicy::MANUAL,
285 base::WaitableEvent::InitialState::NOT_SIGNALED),
286 device_status_(media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL),
287 auth_timeout_(authorization_timeout) {
129 } 288 }
130 289
131 bool PepperPlatformAudioOutput::Initialize(int sample_rate, 290 bool PepperPlatformAudioOutput::Initialize(int sample_rate,
132 int frames_per_buffer, 291 int frames_per_buffer,
133 int source_render_frame_id, 292 int source_render_frame_id,
134 AudioHelper* client) { 293 AudioHelper* client) {
135 DCHECK(client); 294 DCHECK(client);
136 client_ = client; 295 client_ = client;
137 296
138 RenderThreadImpl* const render_thread = RenderThreadImpl::current(); 297 RenderThreadImpl* const render_thread = RenderThreadImpl::current();
139 ipc_ = render_thread->audio_message_filter()->CreateAudioOutputIPC( 298 ipc_ = render_thread->audio_message_filter()->CreateAudioOutputIPC(
140 source_render_frame_id); 299 source_render_frame_id);
141 CHECK(ipc_); 300 CHECK(ipc_);
142 301
143 media::AudioParameters params(media::AudioParameters::AUDIO_PCM_LOW_LATENCY, 302 params_.Reset(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
144 media::CHANNEL_LAYOUT_STEREO, 303 media::CHANNEL_LAYOUT_STEREO,
145 sample_rate, 304 sample_rate,
146 ppapi::kBitsPerAudioOutputSample, 305 ppapi::kBitsPerAudioOutputSample,
147 frames_per_buffer); 306 frames_per_buffer);
148 307
149 io_task_runner_->PostTask( 308 io_task_runner_->PostTask(
150 FROM_HERE, base::Bind(&PepperPlatformAudioOutput::InitializeOnIOThread, 309 FROM_HERE, base::Bind(&PepperPlatformAudioOutput::InitializeOnIOThread,
151 this, params)); 310 this, params_));
311
152 return true; 312 return true;
153 } 313 }
154 314
315 bool PepperPlatformAudioOutput::Initialize(int sample_rate,
316 int frames_per_buffer,
317 PepperAudioOutputHost* client) {
318 DCHECK(main_task_runner_->BelongsToCurrentThread());
319
320 RenderFrameImpl* const render_frame =
321 RenderFrameImpl::FromRoutingID(render_frame_id_);
322 if (!render_frame || !client)
323 return false;
324
325 host_client_ = client;
326
327 RenderThreadImpl* const render_thread = RenderThreadImpl::current();
328 ipc_ = render_thread->audio_message_filter()->CreateAudioOutputIPC(
329 render_frame_id_);
330 CHECK(ipc_);
331
332 params_.Reset(media::AudioParameters::AUDIO_PCM_LOW_LATENCY,
333 media::CHANNEL_LAYOUT_STEREO,
334 sample_rate,
335 ppapi::kBitsPerAudioOutputSample,
336 frames_per_buffer);
337
338 io_task_runner_->PostTask(
339 FROM_HERE, base::Bind(&PepperPlatformAudioOutput::CreateStreamOnIOThread,
340 this, params_));
341
342 return true;
343 }
344
345 void PepperPlatformAudioOutput::RequestDeviceAuthorizationOnIOThread() {
346 DCHECK(io_task_runner_->BelongsToCurrentThread());
347 DCHECK_EQ(state_, IDLE);
348
349 if (!ipc_)
350 return;
351
352 state_ = AUTHORIZING;
353 ipc_->RequestDeviceAuthorization(
354 this, session_id_, device_id_, security_origin_);
355
356 if (auth_timeout_ > base::TimeDelta()) {
357 // Create the timer on the thread it's used on. It's guaranteed to be
358 // deleted on the same thread since users must call ShutDown() before
359 // deleting PepperPlatformAudioOutput; see ShutDownOnIOThread().
360 auth_timeout_action_.reset(new base::OneShotTimer());
361 auth_timeout_action_->Start(
362 FROM_HERE, auth_timeout_,
363 base::Bind(&PepperPlatformAudioOutput::OnDeviceAuthorized, this,
364 media::OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT,
365 media::AudioParameters(), std::string()));
366 }
367 }
368
369 void PepperPlatformAudioOutput::CreateStreamOnIOThread(
370 const media::AudioParameters& params) {
371 DCHECK(io_task_runner_->BelongsToCurrentThread());
372 switch (state_) {
373 case IPC_CLOSED:
374 main_task_runner_->PostTask(FROM_HERE,
375 base::Bind(&PepperPlatformAudioOutput::NotifyStreamCreationFailed,
376 this));
377 break;
378
379 case IDLE:
380 if (did_receive_auth_.IsSignaled() && device_id_.empty() &&
381 security_origin_.unique()) {
382 state_ = CREATING_STREAM;
383 ipc_->CreateStream(this, params);
384 }
385 else {
386 RequestDeviceAuthorizationOnIOThread();
387 start_on_authorized_ = true;
388 }
389 break;
390
391 case AUTHORIZING:
392 start_on_authorized_ = true;
393 break;
394
395 case AUTHORIZED:
396 state_ = CREATING_STREAM;
397 ipc_->CreateStream(this, params);
398 start_on_authorized_ = false;
399 break;
400
401 case CREATING_STREAM:
402 case PAUSED:
403 case PLAYING:
404 NOTREACHED();
405 break;
406 }
407 }
408
155 void PepperPlatformAudioOutput::InitializeOnIOThread( 409 void PepperPlatformAudioOutput::InitializeOnIOThread(
156 const media::AudioParameters& params) { 410 const media::AudioParameters& params) {
157 DCHECK(io_task_runner_->BelongsToCurrentThread()); 411 DCHECK(io_task_runner_->BelongsToCurrentThread());
158 if (ipc_) 412 if (ipc_) {
413 // To maintain the compatibility of the old Pepper audio interface,
414 // We bypass the device autherization process, and try to create audio
415 // stream directly.
416 state_ = CREATING_STREAM;
159 ipc_->CreateStream(this, params); 417 ipc_->CreateStream(this, params);
418 }
160 } 419 }
161 420
162 void PepperPlatformAudioOutput::StartPlaybackOnIOThread() { 421 void PepperPlatformAudioOutput::StartPlaybackOnIOThread() {
163 DCHECK(io_task_runner_->BelongsToCurrentThread()); 422 DCHECK(io_task_runner_->BelongsToCurrentThread());
164 if (ipc_) 423 if (!ipc_)
424 return;
425
426 if (state_ == PAUSED) {
165 ipc_->PlayStream(); 427 ipc_->PlayStream();
428 state_ = PLAYING;
429 play_on_start_ = false;
430 }
431 else {
432 if (state_ < CREATING_STREAM)
433 CreateStreamOnIOThread(params_);
434
435 play_on_start_ = true;
436 }
437 }
438
439 void PepperPlatformAudioOutput::StopPlaybackOnIOThread() {
440 DCHECK(io_task_runner_->BelongsToCurrentThread());
441 if (!ipc_)
442 return;
443
444 if (state_ == PLAYING) {
445 ipc_->PauseStream();
446 state_ = PAUSED;
447 }
448 play_on_start_ = false;
166 } 449 }
167 450
168 void PepperPlatformAudioOutput::SetVolumeOnIOThread(double volume) { 451 void PepperPlatformAudioOutput::SetVolumeOnIOThread(double volume) {
169 DCHECK(io_task_runner_->BelongsToCurrentThread()); 452 DCHECK(io_task_runner_->BelongsToCurrentThread());
170 if (ipc_) 453 if (!ipc_)
171 ipc_->SetVolume(volume); 454 return;
172 }
173 455
174 void PepperPlatformAudioOutput::StopPlaybackOnIOThread() { 456 if (state_ >= CREATING_STREAM)
175 DCHECK(io_task_runner_->BelongsToCurrentThread()); 457 ipc_->SetVolume(volume);
176 if (ipc_)
177 ipc_->PauseStream();
178 } 458 }
179 459
180 void PepperPlatformAudioOutput::ShutDownOnIOThread() { 460 void PepperPlatformAudioOutput::ShutDownOnIOThread() {
181 DCHECK(io_task_runner_->BelongsToCurrentThread()); 461 DCHECK(io_task_runner_->BelongsToCurrentThread());
182 462
183 // Make sure we don't call shutdown more than once. 463 // Make sure we don't call shutdown more than once.
184 if (!ipc_) 464 if (!ipc_)
185 return; 465 return;
186 466
187 ipc_->CloseStream(); 467 // Close the stream, if we haven't already.
188 ipc_.reset(); 468 if (state_ >= AUTHORIZING) {
469 ipc_->CloseStream();
470 ipc_.reset();
471 state_ = IDLE;
472 }
473 start_on_authorized_ = false;
189 474
190 Release(); // Release for the delegate, balances out the reference taken in 475 // Destoy the timer on the thread it's used on.
191 // PepperPlatformAudioOutput::Create. 476 auth_timeout_action_.reset();
477
478 // Release for the delegate, balances out the reference taken in
479 // PepperPlatformAudioOutput::Create.
480 Release();
481 }
482
483 void PepperPlatformAudioOutput::NotifyStreamCreationFailed() {
484 DCHECK(main_task_runner_->BelongsToCurrentThread());
485
486 if (host_client_)
487 host_client_->StreamCreationFailed();
192 } 488 }
193 489
194 } // namespace content 490 } // namespace content
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698