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

Side by Side Diff: media/tools/mfdecoder/main.cc

Issue 6628020: Cleaning up src/media to be consistent with static versus anonymous namespaces. (Closed) Base URL: svn://chrome-svn/chrome/trunk/src
Patch Set: fix namespaces Created 9 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 | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 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 #ifdef UNICODE 5 #ifdef UNICODE
6 #undef UNICODE 6 #undef UNICODE
7 #endif 7 #endif
8 8
9 #include <algorithm> 9 #include <algorithm>
10 10
11 #include <d3d9.h> 11 #include <d3d9.h>
12 #include <dxva2api.h> 12 #include <dxva2api.h>
13 #include <evr.h> 13 #include <evr.h>
14 #include <mfapi.h> 14 #include <mfapi.h>
15 #include <mfreadwrite.h> 15 #include <mfreadwrite.h>
16 #include <windows.h> 16 #include <windows.h>
17 17
18 #include "base/at_exit.h" 18 #include "base/at_exit.h"
19 #include "base/basictypes.h" 19 #include "base/basictypes.h"
20 #include "base/logging.h" 20 #include "base/logging.h"
21 #include "base/message_loop.h" 21 #include "base/message_loop.h"
22 #include "base/scoped_comptr_win.h" 22 #include "base/scoped_comptr_win.h"
23 #include "base/scoped_ptr.h" 23 #include "base/scoped_ptr.h"
24 #include "base/time.h" 24 #include "base/time.h"
25 #include "media/base/yuv_convert.h" 25 #include "media/base/yuv_convert.h"
26 #include "media/tools/mfdecoder/mfdecoder.h" 26 #include "media/tools/mfdecoder/mfdecoder.h"
27 #include "ui/gfx/gdi_util.h" 27 #include "ui/gfx/gdi_util.h"
28 28
29 namespace { 29 static const char* const kWindowClass = "Chrome_MF_Decoder";
30 static const char* const kWindowTitle = "MF Decoder";
31 static const int kWindowStyleFlags =
32 (WS_OVERLAPPEDWINDOW | WS_VISIBLE) & ~(WS_MAXIMIZEBOX | WS_THICKFRAME);
33 static bool g_render_to_window = false;
34 static bool g_render_asap = false;
30 35
31 const char* const kWindowClass = "Chrome_MF_Decoder"; 36 static base::TimeDelta* g_decode_time;
32 const char* const kWindowTitle = "MF Decoder"; 37 static base::TimeDelta* g_render_time;
33 const int kWindowStyleFlags = (WS_OVERLAPPEDWINDOW | WS_VISIBLE) & 38 static int64 g_num_frames = 0;
34 ~(WS_MAXIMIZEBOX | WS_THICKFRAME);
35 bool g_render_to_window = false;
36 bool g_render_asap = false;
37 39
38 base::TimeDelta* g_decode_time; 40 static void usage() {
39 base::TimeDelta* g_render_time;
40 int64 g_num_frames = 0;
41
42 void usage() {
43 static char* usage_msg = "Usage: mfdecoder (-s|-h) (-d|-r|-f) input-file\n" 41 static char* usage_msg = "Usage: mfdecoder (-s|-h) (-d|-r|-f) input-file\n"
44 "-s: Use software decoding\n" 42 "-s: Use software decoding\n"
45 "-h: Use hardware decoding\n" 43 "-h: Use hardware decoding\n"
46 "\n" 44 "\n"
47 "-d: Decode to YV12 as fast as possible, no " \ 45 "-d: Decode to YV12 as fast as possible, no " \
48 "rendering or color-space conversion\n" 46 "rendering or color-space conversion\n"
49 "-r: Render to window at 30ms per frame\n" 47 "-r: Render to window at 30ms per frame\n"
50 "-f: Decode and render as fast as possible\n" 48 "-f: Decode and render as fast as possible\n"
51 "\n" 49 "\n"
52 "To see this message: mfdecoder --help\n"; 50 "To see this message: mfdecoder --help\n";
53 fprintf(stderr, "%s", usage_msg); 51 fprintf(stderr, "%s", usage_msg);
54 } 52 }
55 53
56 // Converts an ASCII string to an Unicode string. This function allocates 54 // Converts an ASCII string to an Unicode string. This function allocates
57 // space for the returned Unicode string from the heap and it is caller's 55 // space for the returned Unicode string from the heap and it is caller's
58 // responsibility to free it. 56 // responsibility to free it.
59 // Returns: An equivalent Unicode string if successful, NULL otherwise. 57 // Returns: An equivalent Unicode string if successful, NULL otherwise.
60 wchar_t* ConvertASCIIStringToUnicode(const char* source) { 58 static wchar_t* ConvertASCIIStringToUnicode(const char* source) {
61 if (source == NULL) { 59 if (source == NULL) {
62 LOG(ERROR) << "ConvertASCIIStringToUnicode: source cannot be NULL"; 60 LOG(ERROR) << "ConvertASCIIStringToUnicode: source cannot be NULL";
63 return NULL; 61 return NULL;
64 } 62 }
65 DWORD string_length = MultiByteToWideChar(CP_ACP, 0, source, -1, NULL, 0); 63 DWORD string_length = MultiByteToWideChar(CP_ACP, 0, source, -1, NULL, 0);
66 if (string_length == 0) { 64 if (string_length == 0) {
67 LOG(ERROR) << "Error getting size of ansi string"; 65 LOG(ERROR) << "Error getting size of ansi string";
68 return NULL; 66 return NULL;
69 } 67 }
70 scoped_array<wchar_t> ret(new wchar_t[string_length]); 68 scoped_array<wchar_t> ret(new wchar_t[string_length]);
71 if (ret.get() == NULL) { 69 if (ret.get() == NULL) {
72 LOG(ERROR) << "Error allocating unicode string buffer"; 70 LOG(ERROR) << "Error allocating unicode string buffer";
73 return NULL; 71 return NULL;
74 } 72 }
75 if (MultiByteToWideChar(CP_ACP, 0, source, string_length, ret.get(), 73 if (MultiByteToWideChar(CP_ACP, 0, source, string_length, ret.get(),
76 string_length) == 0) { 74 string_length) == 0) {
77 LOG(ERROR) << "Error converting ansi string to unicode"; 75 LOG(ERROR) << "Error converting ansi string to unicode";
78 return NULL; 76 return NULL;
79 } 77 }
80 return ret.release(); 78 return ret.release();
81 } 79 }
82 80
83 // Converts the given raw data buffer into RGB32 format, and drawing the result 81 // Converts the given raw data buffer into RGB32 format, and drawing the result
84 // into the given window. This is only used when DXVA2 is not enabled. 82 // into the given window. This is only used when DXVA2 is not enabled.
85 // Returns: true on success. 83 // Returns: true on success.
86 bool ConvertToRGBAndDrawToWindow(HWND video_window, uint8* data, int width, 84 static bool ConvertToRGBAndDrawToWindow(HWND video_window, uint8* data,
87 int height, int stride) { 85 int width, int height, int stride) {
88 CHECK(video_window != NULL); 86 CHECK(video_window != NULL);
89 CHECK(data != NULL); 87 CHECK(data != NULL);
90 CHECK_GT(width, 0); 88 CHECK_GT(width, 0);
91 CHECK_GT(height, 0); 89 CHECK_GT(height, 0);
92 CHECK_GE(stride, width); 90 CHECK_GE(stride, width);
93 height = (height + 15) & ~15; 91 height = (height + 15) & ~15;
94 bool success = true; 92 bool success = true;
95 uint8* y_start = reinterpret_cast<uint8*>(data); 93 uint8* y_start = reinterpret_cast<uint8*>(data);
96 uint8* u_start = y_start + height * stride * 5 / 4; 94 uint8* u_start = y_start + height * stride * 5 / 4;
97 uint8* v_start = y_start + height * stride; 95 uint8* v_start = y_start + height * stride;
(...skipping 27 matching lines...) Expand all
125 success = false; 123 success = false;
126 } 124 }
127 EndPaint(video_window, &ps); 125 EndPaint(video_window, &ps);
128 126
129 return success; 127 return success;
130 } 128 }
131 129
132 // Obtains the underlying raw data buffer for the given IMFMediaBuffer, and 130 // Obtains the underlying raw data buffer for the given IMFMediaBuffer, and
133 // calls ConvertToRGBAndDrawToWindow() with it. 131 // calls ConvertToRGBAndDrawToWindow() with it.
134 // Returns: true on success. 132 // Returns: true on success.
135 bool PaintMediaBufferOntoWindow(HWND video_window, IMFMediaBuffer* video_buffer, 133 static bool PaintMediaBufferOntoWindow(HWND video_window,
136 int width, int height, int stride) { 134 IMFMediaBuffer* video_buffer,
135 int width, int height, int stride) {
137 CHECK(video_buffer != NULL); 136 CHECK(video_buffer != NULL);
138 HRESULT hr; 137 HRESULT hr;
139 BYTE* data; 138 BYTE* data;
140 DWORD buffer_length; 139 DWORD buffer_length;
141 DWORD data_length; 140 DWORD data_length;
142 hr = video_buffer->Lock(&data, &buffer_length, &data_length); 141 hr = video_buffer->Lock(&data, &buffer_length, &data_length);
143 if (FAILED(hr)) { 142 if (FAILED(hr)) {
144 LOG(ERROR) << "Failed to lock IMFMediaBuffer"; 143 LOG(ERROR) << "Failed to lock IMFMediaBuffer";
145 return false; 144 return false;
146 } 145 }
(...skipping 10 matching lines...) Expand all
157 } 156 }
158 *g_render_time += base::Time::Now() - render_start; 157 *g_render_time += base::Time::Now() - render_start;
159 } 158 }
160 video_buffer->Unlock(); 159 video_buffer->Unlock();
161 return true; 160 return true;
162 } 161 }
163 162
164 // Obtains the D3D9 surface from the given IMFMediaBuffer, then calls methods 163 // Obtains the D3D9 surface from the given IMFMediaBuffer, then calls methods
165 // in the D3D device to draw to the window associated with it. 164 // in the D3D device to draw to the window associated with it.
166 // Returns: true on success. 165 // Returns: true on success.
167 bool PaintD3D9BufferOntoWindow(IDirect3DDevice9* device, 166 static bool PaintD3D9BufferOntoWindow(IDirect3DDevice9* device,
168 IMFMediaBuffer* video_buffer) { 167 IMFMediaBuffer* video_buffer) {
169 CHECK(device != NULL); 168 CHECK(device != NULL);
170 ScopedComPtr<IDirect3DSurface9> surface; 169 ScopedComPtr<IDirect3DSurface9> surface;
171 HRESULT hr = MFGetService(video_buffer, MR_BUFFER_SERVICE, 170 HRESULT hr = MFGetService(video_buffer, MR_BUFFER_SERVICE,
172 IID_PPV_ARGS(surface.Receive())); 171 IID_PPV_ARGS(surface.Receive()));
173 if (FAILED(hr)) { 172 if (FAILED(hr)) {
174 LOG(ERROR) << "Failed to get D3D9 surface from buffer"; 173 LOG(ERROR) << "Failed to get D3D9 surface from buffer";
175 return false; 174 return false;
176 } 175 }
177 if (g_render_to_window) { 176 if (g_render_to_window) {
178 base::Time render_start(base::Time::Now()); 177 base::Time render_start(base::Time::Now());
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after
212 } 211 }
213 212
214 // Reads a sample from the given decoder, and draws the sample to the given 213 // Reads a sample from the given decoder, and draws the sample to the given
215 // window. Obtains the IMFMediaBuffer objects from the given IMFSample, and 214 // window. Obtains the IMFMediaBuffer objects from the given IMFSample, and
216 // calls either PaintMediaBufferOntoWindow() or PaintD3D9BufferOntoWindow() with 215 // calls either PaintMediaBufferOntoWindow() or PaintD3D9BufferOntoWindow() with
217 // each of them, depending on whether the decoder supports DXVA2. 216 // each of them, depending on whether the decoder supports DXVA2.
218 // The decoder should be initialized before calling this method. 217 // The decoder should be initialized before calling this method.
219 // For H.264 format, there should only be 1 buffer per sample, so each buffer 218 // For H.264 format, there should only be 1 buffer per sample, so each buffer
220 // represents 1 frame. 219 // represents 1 frame.
221 // Returns: true if successful. 220 // Returns: true if successful.
222 bool DrawVideoSample(HWND video_window, media::MFDecoder* decoder, 221 static bool DrawVideoSample(HWND video_window, media::MFDecoder* decoder,
223 IDirect3DDevice9* device) { 222 IDirect3DDevice9* device) {
224 CHECK(video_window != NULL); 223 CHECK(video_window != NULL);
225 CHECK(decoder != NULL); 224 CHECK(decoder != NULL);
226 CHECK(decoder->initialized()); 225 CHECK(decoder->initialized());
227 226
228 if (decoder->end_of_stream()) { 227 if (decoder->end_of_stream()) {
229 LOG(ERROR) << "Failed to obtain more samples from decoder because end of " 228 LOG(ERROR) << "Failed to obtain more samples from decoder because end of "
230 << "stream has been reached"; 229 << "stream has been reached";
231 return false; 230 return false;
232 } 231 }
233 ScopedComPtr<IMFSample> video_sample; 232 ScopedComPtr<IMFSample> video_sample;
(...skipping 27 matching lines...) Expand all
261 return PaintD3D9BufferOntoWindow(device, video_buffer); 260 return PaintD3D9BufferOntoWindow(device, video_buffer);
262 } else { 261 } else {
263 return PaintMediaBufferOntoWindow(video_window, video_buffer, 262 return PaintMediaBufferOntoWindow(video_window, video_buffer,
264 decoder->width(), decoder->height(), 263 decoder->width(), decoder->height(),
265 decoder->mfbuffer_stride()); 264 decoder->mfbuffer_stride());
266 } 265 }
267 } 266 }
268 267
269 // Creates a window with the given width and height. 268 // Creates a window with the given width and height.
270 // Returns: A handle to the window on success, NULL otherwise. 269 // Returns: A handle to the window on success, NULL otherwise.
271 HWND CreateDrawWindow(int width, int height) { 270 static HWND CreateDrawWindow(int width, int height) {
272 WNDCLASS window_class = {0}; 271 WNDCLASS window_class = {0};
273 window_class.lpszClassName = kWindowClass; 272 window_class.lpszClassName = kWindowClass;
274 window_class.hInstance = NULL; 273 window_class.hInstance = NULL;
275 window_class.hbrBackground = 0; 274 window_class.hbrBackground = 0;
276 window_class.lpfnWndProc = DefWindowProc; 275 window_class.lpfnWndProc = DefWindowProc;
277 window_class.hCursor = LoadCursor(0, IDC_ARROW); 276 window_class.hCursor = LoadCursor(0, IDC_ARROW);
278 277
279 if (RegisterClass(&window_class) == 0) { 278 if (RegisterClass(&window_class) == 0) {
280 LOG(ERROR) << "Failed to register window class"; 279 LOG(ERROR) << "Failed to register window class";
281 return false; 280 return false;
(...skipping 14 matching lines...) Expand all
296 return NULL; 295 return NULL;
297 } 296 }
298 return window; 297 return window;
299 } 298 }
300 299
301 // This function creates a D3D Device and a D3D Device Manager, sets the manager 300 // This function creates a D3D Device and a D3D Device Manager, sets the manager
302 // to use the device, and returns the manager. It also initializes the D3D 301 // to use the device, and returns the manager. It also initializes the D3D
303 // device. This function is used by mfdecoder.cc during the call to 302 // device. This function is used by mfdecoder.cc during the call to
304 // MFDecoder::GetDXVA2AttributesForSourceReader(). 303 // MFDecoder::GetDXVA2AttributesForSourceReader().
305 // Returns: The D3D manager object if successful. Otherwise, NULL is returned. 304 // Returns: The D3D manager object if successful. Otherwise, NULL is returned.
306 IDirect3DDeviceManager9* CreateD3DDevManager(HWND video_window, 305 static IDirect3DDeviceManager9* CreateD3DDevManager(HWND video_window,
307 IDirect3DDevice9** device) { 306 IDirect3DDevice9** device) {
308 CHECK(video_window != NULL); 307 CHECK(video_window != NULL);
309 CHECK(device != NULL); 308 CHECK(device != NULL);
310 int ret = -1; 309 int ret = -1;
311 310
312 ScopedComPtr<IDirect3DDeviceManager9> dev_manager; 311 ScopedComPtr<IDirect3DDeviceManager9> dev_manager;
313 ScopedComPtr<IDirect3D9> d3d; 312 ScopedComPtr<IDirect3D9> d3d;
314 d3d.Attach(Direct3DCreate9(D3D_SDK_VERSION)); 313 d3d.Attach(Direct3DCreate9(D3D_SDK_VERSION));
315 if (d3d == NULL) { 314 if (d3d == NULL) {
316 LOG(ERROR) << "Failed to create D3D9"; 315 LOG(ERROR) << "Failed to create D3D9";
317 return NULL; 316 return NULL;
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
359 } 358 }
360 *device = temp_device.Detach(); 359 *device = temp_device.Detach();
361 return dev_manager.Detach(); 360 return dev_manager.Detach();
362 } 361 }
363 362
364 // Resets the D3D device to prevent scaling from happening because it was 363 // Resets the D3D device to prevent scaling from happening because it was
365 // created with window before resizing occurred. We need to change the back 364 // created with window before resizing occurred. We need to change the back
366 // buffer dimensions to the actual video frame dimensions. 365 // buffer dimensions to the actual video frame dimensions.
367 // Both the decoder and device should be initialized before calling this method. 366 // Both the decoder and device should be initialized before calling this method.
368 // Returns: true if successful. 367 // Returns: true if successful.
369 bool AdjustD3DDeviceBackBufferDimensions(media::MFDecoder* decoder, 368 static bool AdjustD3DDeviceBackBufferDimensions(media::MFDecoder* decoder,
370 IDirect3DDevice9* device, 369 IDirect3DDevice9* device,
371 HWND video_window) { 370 HWND video_window) {
372 CHECK(decoder != NULL); 371 CHECK(decoder != NULL);
373 CHECK(decoder->initialized()); 372 CHECK(decoder->initialized());
374 CHECK(decoder->use_dxva2()); 373 CHECK(decoder->use_dxva2());
375 CHECK(device != NULL); 374 CHECK(device != NULL);
376 D3DPRESENT_PARAMETERS present_params = {0}; 375 D3DPRESENT_PARAMETERS present_params = {0};
377 present_params.BackBufferWidth = decoder->width(); 376 present_params.BackBufferWidth = decoder->width();
378 present_params.BackBufferHeight = decoder->height(); 377 present_params.BackBufferHeight = decoder->height();
379 present_params.BackBufferFormat = D3DFMT_UNKNOWN; 378 present_params.BackBufferFormat = D3DFMT_UNKNOWN;
380 present_params.BackBufferCount = 1; 379 present_params.BackBufferCount = 1;
381 present_params.SwapEffect = D3DSWAPEFFECT_DISCARD; 380 present_params.SwapEffect = D3DSWAPEFFECT_DISCARD;
382 present_params.hDeviceWindow = video_window; 381 present_params.hDeviceWindow = video_window;
383 present_params.Windowed = TRUE; 382 present_params.Windowed = TRUE;
384 present_params.Flags = D3DPRESENTFLAG_VIDEO; 383 present_params.Flags = D3DPRESENTFLAG_VIDEO;
385 present_params.FullScreen_RefreshRateInHz = 0; 384 present_params.FullScreen_RefreshRateInHz = 0;
386 present_params.PresentationInterval = 0; 385 present_params.PresentationInterval = 0;
387 386
388 return SUCCEEDED(device->Reset(&present_params)) ? true : false; 387 return SUCCEEDED(device->Reset(&present_params)) ? true : false;
389 } 388 }
390 389
391 // Post this task in the MessageLoop. This function keeps posting itself 390 // Post this task in the MessageLoop. This function keeps posting itself
392 // until DrawVideoSample fails. 391 // until DrawVideoSample fails.
393 void RepaintTask(media::MFDecoder* decoder, HWND video_window, 392 static void RepaintTask(media::MFDecoder* decoder, HWND video_window,
394 IDirect3DDevice9* device) { 393 IDirect3DDevice9* device) {
395 // This sends a WM_PAINT message so we can paint on the window later. 394 // This sends a WM_PAINT message so we can paint on the window later.
396 // If we are using D3D9, then we do not send a WM_PAINT message since the two 395 // If we are using D3D9, then we do not send a WM_PAINT message since the two
397 // do not work well together. 396 // do not work well together.
398 if (!decoder->use_dxva2()) 397 if (!decoder->use_dxva2())
399 InvalidateRect(video_window, NULL, TRUE); 398 InvalidateRect(video_window, NULL, TRUE);
400 base::Time start(base::Time::Now()); 399 base::Time start(base::Time::Now());
401 if (!DrawVideoSample(video_window, decoder, device)) { 400 if (!DrawVideoSample(video_window, decoder, device)) {
402 LOG(ERROR) << "DrawVideoSample failed, quitting MessageLoop"; 401 LOG(ERROR) << "DrawVideoSample failed, quitting MessageLoop";
403 MessageLoopForUI::current()->Quit(); 402 MessageLoopForUI::current()->Quit();
404 } else { 403 } else {
405 ++g_num_frames; 404 ++g_num_frames;
406 if (!g_render_asap && g_render_to_window) { 405 if (!g_render_asap && g_render_to_window) {
407 base::Time end(base::Time::Now()); 406 base::Time end(base::Time::Now());
408 int64 delta = (end-start).InMilliseconds(); 407 int64 delta = (end-start).InMilliseconds();
409 MessageLoopForUI::current()->PostDelayedTask( 408 MessageLoopForUI::current()->PostDelayedTask(
410 FROM_HERE, 409 FROM_HERE,
411 NewRunnableFunction(&RepaintTask, decoder, video_window, device), 410 NewRunnableFunction(&RepaintTask, decoder, video_window, device),
412 std::max<int64>(0L, 30-delta)); 411 std::max<int64>(0L, 30-delta));
413 } else { 412 } else {
414 MessageLoopForUI::current()->PostTask( 413 MessageLoopForUI::current()->PostTask(
415 FROM_HERE, 414 FROM_HERE,
416 NewRunnableFunction(&RepaintTask, decoder, video_window, device)); 415 NewRunnableFunction(&RepaintTask, decoder, video_window, device));
417 } 416 }
418 } 417 }
419 } 418 }
420 419
421 } // namespace
422
423 int main(int argc, char** argv) { 420 int main(int argc, char** argv) {
424 if (argc < 2) { 421 if (argc < 2) {
425 fprintf(stderr, "missing arguments\n"); 422 fprintf(stderr, "missing arguments\n");
426 usage(); 423 usage();
427 return -1; 424 return -1;
428 } 425 }
429 if (strcmp(argv[1], "--help") == 0) { 426 if (strcmp(argv[1], "--help") == 0) {
430 usage(); 427 usage();
431 return 0; 428 return 0;
432 } 429 }
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after
551 << "\nAverage decode time: " << ((g_num_frames == 0) ? 548 << "\nAverage decode time: " << ((g_num_frames == 0) ?
552 0 : (g_decode_time->InMillisecondsF() / g_num_frames)) 549 0 : (g_decode_time->InMillisecondsF() / g_num_frames))
553 << "\nRender time: " << g_render_time->InMilliseconds() << "ms" 550 << "\nRender time: " << g_render_time->InMilliseconds() << "ms"
554 << "\nAverage render time: " << ((g_num_frames == 0) ? 551 << "\nAverage render time: " << ((g_num_frames == 0) ?
555 0 : (g_render_time->InMillisecondsF() / g_num_frames)); 552 0 : (g_render_time->InMillisecondsF() / g_num_frames));
556 printf("Normal termination\n"); 553 printf("Normal termination\n");
557 delete g_decode_time; 554 delete g_decode_time;
558 delete g_render_time; 555 delete g_render_time;
559 return 0; 556 return 0;
560 } 557 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698