1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 /* 18 * Contains code capturing video frames from a camera device on Windows. 19 * This code uses capXxx API, available via capCreateCaptureWindow. 20 */ 21 22 #include <vfw.h> 23 #include "android/camera/camera-capture.h" 24 #include "android/camera/camera-format-converters.h" 25 26 #define E(...) derror(__VA_ARGS__) 27 #define W(...) dwarning(__VA_ARGS__) 28 #define D(...) VERBOSE_PRINT(camera,__VA_ARGS__) 29 #define D_ACTIVE VERBOSE_CHECK(camera) 30 31 /* the T(...) macro is used to dump traffic */ 32 #define T_ACTIVE 0 33 34 #if T_ACTIVE 35 #define T(...) VERBOSE_PRINT(camera,__VA_ARGS__) 36 #else 37 #define T(...) ((void)0) 38 #endif 39 40 /* Default name for the capture window. */ 41 static const char* _default_window_name = "AndroidEmulatorVC"; 42 43 typedef struct WndCameraDevice WndCameraDevice; 44 /* Windows-specific camera device descriptor. */ 45 struct WndCameraDevice { 46 /* Common camera device descriptor. */ 47 CameraDevice header; 48 /* Capture window name. (default is AndroidEmulatorVC) */ 49 char* window_name; 50 /* Input channel (video driver index). (default is 0) */ 51 int input_channel; 52 53 /* 54 * Set when framework gets initialized. 55 */ 56 57 /* Video capturing window. Null indicates that device is not connected. */ 58 HWND cap_window; 59 /* DC for frame bitmap manipulation. Null indicates that frames are not 60 * being capturing. */ 61 HDC dc; 62 /* Bitmap info for the frames obtained from the video capture driver. */ 63 BITMAPINFO* frame_bitmap; 64 /* Bitmap info to use for GetDIBits calls. We can't really use bitmap info 65 * obtained from the video capture driver, because of the two issues. First, 66 * the driver may return an incompatible 'biCompresstion' value. For instance, 67 * sometimes it returns a "fourcc' pixel format value instead of BI_XXX, 68 * which causes GetDIBits to fail. Second, the bitmap that represents a frame 69 * that has been actually obtained from the device is not necessarily matches 70 * bitmap info that capture driver has returned. Sometimes the captured bitmap 71 * is a 32-bit RGB, while bit count reported by the driver is 16. So, to 72 * address these issues we need to have another bitmap info, that can be used 73 * in GetDIBits calls. */ 74 BITMAPINFO* gdi_bitmap; 75 /* Framebuffer large enough to fit the frame. */ 76 uint8_t* framebuffer; 77 /* Framebuffer size. */ 78 size_t framebuffer_size; 79 /* Framebuffer's pixel format. */ 80 uint32_t pixel_format; 81 /* If != 0, frame bitmap is "top-down". If 0, frame bitmap is "bottom-up". */ 82 int is_top_down; 83 }; 84 85 /******************************************************************************* 86 * CameraDevice routines 87 ******************************************************************************/ 88 89 /* Allocates an instance of WndCameraDevice structure. 90 * Return: 91 * Allocated instance of WndCameraDevice structure. Note that this routine 92 * also sets 'opaque' field in the 'header' structure to point back to the 93 * containing WndCameraDevice instance. 94 */ 95 static WndCameraDevice* 96 _camera_device_alloc(void) 97 { 98 WndCameraDevice* cd = (WndCameraDevice*)malloc(sizeof(WndCameraDevice)); 99 if (cd != NULL) { 100 memset(cd, 0, sizeof(WndCameraDevice)); 101 cd->header.opaque = cd; 102 } else { 103 E("%s: Unable to allocate WndCameraDevice instance", __FUNCTION__); 104 } 105 return cd; 106 } 107 108 /* Uninitializes and frees WndCameraDevice descriptor. 109 * Note that upon return from this routine memory allocated for the descriptor 110 * will be freed. 111 */ 112 static void 113 _camera_device_free(WndCameraDevice* cd) 114 { 115 if (cd != NULL) { 116 if (cd->cap_window != NULL) { 117 /* Disconnect from the driver. */ 118 capDriverDisconnect(cd->cap_window); 119 120 if (cd->dc != NULL) { 121 W("%s: Frames should not be capturing at this point", 122 __FUNCTION__); 123 ReleaseDC(cd->cap_window, cd->dc); 124 cd->dc = NULL; 125 } 126 /* Destroy the capturing window. */ 127 DestroyWindow(cd->cap_window); 128 cd->cap_window = NULL; 129 } 130 if (cd->gdi_bitmap != NULL) { 131 free(cd->gdi_bitmap); 132 } 133 if (cd->frame_bitmap != NULL) { 134 free(cd->frame_bitmap); 135 } 136 if (cd->window_name != NULL) { 137 free(cd->window_name); 138 } 139 if (cd->framebuffer != NULL) { 140 free(cd->framebuffer); 141 } 142 AFREE(cd); 143 } else { 144 W("%s: No descriptor", __FUNCTION__); 145 } 146 } 147 148 /* Resets camera device after capturing. 149 * Since new capture request may require different frame dimensions we must 150 * reset frame info cached in the capture window. The only way to do that would 151 * be closing, and reopening it again. */ 152 static void 153 _camera_device_reset(WndCameraDevice* cd) 154 { 155 if (cd != NULL && cd->cap_window != NULL) { 156 capDriverDisconnect(cd->cap_window); 157 if (cd->dc != NULL) { 158 ReleaseDC(cd->cap_window, cd->dc); 159 cd->dc = NULL; 160 } 161 if (cd->gdi_bitmap != NULL) { 162 free(cd->gdi_bitmap); 163 cd->gdi_bitmap = NULL; 164 } 165 if (cd->frame_bitmap != NULL) { 166 free(cd->frame_bitmap); 167 cd->frame_bitmap = NULL; 168 } 169 if (cd->framebuffer != NULL) { 170 free(cd->framebuffer); 171 cd->framebuffer = NULL; 172 } 173 174 /* Recreate the capturing window. */ 175 DestroyWindow(cd->cap_window); 176 cd->cap_window = capCreateCaptureWindow(cd->window_name, WS_CHILD, 0, 0, 177 0, 0, HWND_MESSAGE, 1); 178 } 179 } 180 181 /* Gets an absolute value out of a signed integer. */ 182 static __inline__ int 183 _abs(int val) 184 { 185 return (val < 0) ? -val : val; 186 } 187 188 /******************************************************************************* 189 * CameraDevice API 190 ******************************************************************************/ 191 192 CameraDevice* 193 camera_device_open(const char* name, int inp_channel) 194 { 195 WndCameraDevice* wcd; 196 197 /* Allocate descriptor and initialize windows-specific fields. */ 198 wcd = _camera_device_alloc(); 199 if (wcd == NULL) { 200 E("%s: Unable to allocate WndCameraDevice instance", __FUNCTION__); 201 return NULL; 202 } 203 wcd->window_name = (name != NULL) ? ASTRDUP(name) : 204 ASTRDUP(_default_window_name); 205 if (wcd->window_name == NULL) { 206 E("%s: Unable to save window name", __FUNCTION__); 207 _camera_device_free(wcd); 208 return NULL; 209 } 210 wcd->input_channel = inp_channel; 211 212 /* Create capture window that is a child of HWND_MESSAGE window. 213 * We make it invisible, so it doesn't mess with the UI. Also 214 * note that we supply standard HWND_MESSAGE window handle as 215 * the parent window, since we don't want video capturing 216 * machinery to be dependent on the details of our UI. */ 217 wcd->cap_window = capCreateCaptureWindow(wcd->window_name, WS_CHILD, 0, 0, 218 0, 0, HWND_MESSAGE, 1); 219 if (wcd->cap_window == NULL) { 220 E("%s: Unable to create video capturing window '%s': %d", 221 __FUNCTION__, wcd->window_name, GetLastError()); 222 _camera_device_free(wcd); 223 return NULL; 224 } 225 226 return &wcd->header; 227 } 228 229 int 230 camera_device_start_capturing(CameraDevice* cd, 231 uint32_t pixel_format, 232 int frame_width, 233 int frame_height) 234 { 235 WndCameraDevice* wcd; 236 HBITMAP bm_handle; 237 BITMAP bitmap; 238 size_t format_info_size; 239 240 if (cd == NULL || cd->opaque == NULL) { 241 E("%s: Invalid camera device descriptor", __FUNCTION__); 242 return -1; 243 } 244 wcd = (WndCameraDevice*)cd->opaque; 245 246 /* wcd->dc is an indicator of capturing: !NULL - capturing, NULL - not */ 247 if (wcd->dc != NULL) { 248 W("%s: Capturing is already on on device '%s'", 249 __FUNCTION__, wcd->window_name); 250 return 0; 251 } 252 253 /* Connect capture window to the video capture driver. */ 254 if (!capDriverConnect(wcd->cap_window, wcd->input_channel)) { 255 return -1; 256 } 257 258 /* Get current frame information from the driver. */ 259 format_info_size = capGetVideoFormatSize(wcd->cap_window); 260 if (format_info_size == 0) { 261 E("%s: Unable to get video format size: %d", 262 __FUNCTION__, GetLastError()); 263 _camera_device_reset(wcd); 264 return -1; 265 } 266 wcd->frame_bitmap = (BITMAPINFO*)malloc(format_info_size); 267 if (wcd->frame_bitmap == NULL) { 268 E("%s: Unable to allocate frame bitmap info buffer", __FUNCTION__); 269 _camera_device_reset(wcd); 270 return -1; 271 } 272 if (!capGetVideoFormat(wcd->cap_window, wcd->frame_bitmap, 273 format_info_size)) { 274 E("%s: Unable to obtain video format: %d", __FUNCTION__, GetLastError()); 275 _camera_device_reset(wcd); 276 return -1; 277 } 278 279 /* Lets see if we need to set different frame dimensions */ 280 if (wcd->frame_bitmap->bmiHeader.biWidth != frame_width || 281 abs(wcd->frame_bitmap->bmiHeader.biHeight) != frame_height) { 282 /* Dimensions don't match. Set new frame info. */ 283 wcd->frame_bitmap->bmiHeader.biWidth = frame_width; 284 wcd->frame_bitmap->bmiHeader.biHeight = frame_height; 285 /* We need to recalculate image size, since the capture window / driver 286 * will use image size provided by us. */ 287 if (wcd->frame_bitmap->bmiHeader.biBitCount == 24) { 288 /* Special case that may require WORD boundary alignment. */ 289 uint32_t bpl = (frame_width * 3 + 1) & ~1; 290 wcd->frame_bitmap->bmiHeader.biSizeImage = bpl * frame_height; 291 } else { 292 wcd->frame_bitmap->bmiHeader.biSizeImage = 293 (frame_width * frame_height * wcd->frame_bitmap->bmiHeader.biBitCount) / 8; 294 } 295 if (!capSetVideoFormat(wcd->cap_window, wcd->frame_bitmap, 296 format_info_size)) { 297 E("%s: Unable to set video format: %d", __FUNCTION__, GetLastError()); 298 _camera_device_reset(wcd); 299 return -1; 300 } 301 } 302 303 if (wcd->frame_bitmap->bmiHeader.biCompression > BI_PNG) { 304 D("%s: Video capturing driver has reported pixel format %.4s", 305 __FUNCTION__, (const char*)&wcd->frame_bitmap->bmiHeader.biCompression); 306 } 307 308 /* Most of the time frame bitmaps come in "bottom-up" form, where its origin 309 * is the lower-left corner. However, it could be in the normal "top-down" 310 * form with the origin in the upper-left corner. So, we must adjust the 311 * biHeight field, since the way "top-down" form is reported here is by 312 * setting biHeight to a negative value. */ 313 if (wcd->frame_bitmap->bmiHeader.biHeight < 0) { 314 wcd->frame_bitmap->bmiHeader.biHeight = 315 -wcd->frame_bitmap->bmiHeader.biHeight; 316 wcd->is_top_down = 1; 317 } else { 318 wcd->is_top_down = 0; 319 } 320 321 /* Get DC for the capturing window that will be used when we deal with 322 * bitmaps obtained from the camera device during frame capturing. */ 323 wcd->dc = GetDC(wcd->cap_window); 324 if (wcd->dc == NULL) { 325 E("%s: Unable to obtain DC for %s: %d", 326 __FUNCTION__, wcd->window_name, GetLastError()); 327 _camera_device_reset(wcd); 328 return -1; 329 } 330 331 /* 332 * At this point we need to grab a frame to properly setup framebuffer, and 333 * calculate pixel format. The problem is that bitmap information obtained 334 * from the driver doesn't necessarily match the actual bitmap we're going to 335 * obtain via capGrabFrame / capEditCopy / GetClipboardData 336 */ 337 338 /* Grab a frame, and post it to the clipboard. Not very effective, but this 339 * is how capXxx API is operating. */ 340 if (!capGrabFrameNoStop(wcd->cap_window) || 341 !capEditCopy(wcd->cap_window) || 342 !OpenClipboard(wcd->cap_window)) { 343 E("%s: Device '%s' is unable to save frame to the clipboard: %d", 344 __FUNCTION__, wcd->window_name, GetLastError()); 345 _camera_device_reset(wcd); 346 return -1; 347 } 348 349 /* Get bitmap handle saved into clipboard. Note that bitmap is still 350 * owned by the clipboard here! */ 351 bm_handle = (HBITMAP)GetClipboardData(CF_BITMAP); 352 if (bm_handle == NULL) { 353 E("%s: Device '%s' is unable to obtain frame from the clipboard: %d", 354 __FUNCTION__, wcd->window_name, GetLastError()); 355 CloseClipboard(); 356 _camera_device_reset(wcd); 357 return -1; 358 } 359 360 /* Get bitmap object that is initialized with the actual bitmap info. */ 361 if (!GetObject(bm_handle, sizeof(BITMAP), &bitmap)) { 362 E("%s: Device '%s' is unable to obtain frame's bitmap: %d", 363 __FUNCTION__, wcd->window_name, GetLastError()); 364 CloseClipboard(); 365 _camera_device_reset(wcd); 366 return -1; 367 } 368 369 /* Now that we have all we need in 'bitmap' */ 370 CloseClipboard(); 371 372 /* Make sure that dimensions match. Othewise - fail. */ 373 if (wcd->frame_bitmap->bmiHeader.biWidth != bitmap.bmWidth || 374 wcd->frame_bitmap->bmiHeader.biHeight != bitmap.bmHeight ) { 375 E("%s: Requested dimensions %dx%d do not match the actual %dx%d", 376 __FUNCTION__, frame_width, frame_height, 377 wcd->frame_bitmap->bmiHeader.biWidth, 378 wcd->frame_bitmap->bmiHeader.biHeight); 379 _camera_device_reset(wcd); 380 return -1; 381 } 382 383 /* Create bitmap info that will be used with GetDIBits. */ 384 wcd->gdi_bitmap = (BITMAPINFO*)malloc(wcd->frame_bitmap->bmiHeader.biSize); 385 if (wcd->gdi_bitmap == NULL) { 386 E("%s: Unable to allocate gdi bitmap info", __FUNCTION__); 387 _camera_device_reset(wcd); 388 return -1; 389 } 390 memcpy(wcd->gdi_bitmap, wcd->frame_bitmap, 391 wcd->frame_bitmap->bmiHeader.biSize); 392 wcd->gdi_bitmap->bmiHeader.biCompression = BI_RGB; 393 wcd->gdi_bitmap->bmiHeader.biBitCount = bitmap.bmBitsPixel; 394 wcd->gdi_bitmap->bmiHeader.biSizeImage = bitmap.bmWidthBytes * bitmap.bmWidth; 395 /* Adjust GDI's bitmap biHeight for proper frame direction ("top-down", or 396 * "bottom-up") We do this trick in order to simplify pixel format conversion 397 * routines, where we always assume "top-down" frames. The trick he is to 398 * have negative biHeight in 'gdi_bitmap' if driver provides "bottom-up" 399 * frames, and positive biHeight in 'gdi_bitmap' if driver provides "top-down" 400 * frames. This way GetGDIBits will always return "top-down" frames. */ 401 if (wcd->is_top_down) { 402 wcd->gdi_bitmap->bmiHeader.biHeight = 403 wcd->frame_bitmap->bmiHeader.biHeight; 404 } else { 405 wcd->gdi_bitmap->bmiHeader.biHeight = 406 -wcd->frame_bitmap->bmiHeader.biHeight; 407 } 408 409 /* Allocate framebuffer. */ 410 wcd->framebuffer = (uint8_t*)malloc(wcd->gdi_bitmap->bmiHeader.biSizeImage); 411 if (wcd->framebuffer == NULL) { 412 E("%s: Unable to allocate %d bytes for framebuffer", 413 __FUNCTION__, wcd->gdi_bitmap->bmiHeader.biSizeImage); 414 _camera_device_reset(wcd); 415 return -1; 416 } 417 418 /* Lets see what pixel format we will use. */ 419 if (wcd->gdi_bitmap->bmiHeader.biBitCount == 16) { 420 wcd->pixel_format = V4L2_PIX_FMT_RGB565; 421 } else if (wcd->gdi_bitmap->bmiHeader.biBitCount == 24) { 422 wcd->pixel_format = V4L2_PIX_FMT_BGR24; 423 } else if (wcd->gdi_bitmap->bmiHeader.biBitCount == 32) { 424 wcd->pixel_format = V4L2_PIX_FMT_BGR32; 425 } else { 426 E("%s: Unsupported number of bits per pixel %d", 427 __FUNCTION__, wcd->gdi_bitmap->bmiHeader.biBitCount); 428 _camera_device_reset(wcd); 429 return -1; 430 } 431 432 D("%s: Capturing device '%s': %d bits per pixel in %.4s [%dx%d] frame", 433 __FUNCTION__, wcd->window_name, wcd->gdi_bitmap->bmiHeader.biBitCount, 434 (const char*)&wcd->pixel_format, wcd->frame_bitmap->bmiHeader.biWidth, 435 wcd->frame_bitmap->bmiHeader.biHeight); 436 437 return 0; 438 } 439 440 int 441 camera_device_stop_capturing(CameraDevice* cd) 442 { 443 WndCameraDevice* wcd; 444 if (cd == NULL || cd->opaque == NULL) { 445 E("%s: Invalid camera device descriptor", __FUNCTION__); 446 return -1; 447 } 448 wcd = (WndCameraDevice*)cd->opaque; 449 450 /* wcd->dc is the indicator of capture. */ 451 if (wcd->dc == NULL) { 452 W("%s: Device '%s' is not capturing video", 453 __FUNCTION__, wcd->window_name); 454 return 0; 455 } 456 ReleaseDC(wcd->cap_window, wcd->dc); 457 wcd->dc = NULL; 458 459 /* Reset the device in preparation for the next capture. */ 460 _camera_device_reset(wcd); 461 462 return 0; 463 } 464 465 int 466 camera_device_read_frame(CameraDevice* cd, 467 ClientFrameBuffer* framebuffers, 468 int fbs_num) 469 { 470 WndCameraDevice* wcd; 471 HBITMAP bm_handle; 472 473 /* Sanity checks. */ 474 if (cd == NULL || cd->opaque == NULL) { 475 E("%s: Invalid camera device descriptor", __FUNCTION__); 476 return -1; 477 } 478 wcd = (WndCameraDevice*)cd->opaque; 479 if (wcd->dc == NULL) { 480 W("%s: Device '%s' is not captuing video", 481 __FUNCTION__, wcd->window_name); 482 return -1; 483 } 484 485 /* Grab a frame, and post it to the clipboard. Not very effective, but this 486 * is how capXxx API is operating. */ 487 if (!capGrabFrameNoStop(wcd->cap_window) || 488 !capEditCopy(wcd->cap_window) || 489 !OpenClipboard(wcd->cap_window)) { 490 E("%s: Device '%s' is unable to save frame to the clipboard: %d", 491 __FUNCTION__, wcd->window_name, GetLastError()); 492 return -1; 493 } 494 495 /* Get bitmap handle saved into clipboard. Note that bitmap is still 496 * owned by the clipboard here! */ 497 bm_handle = (HBITMAP)GetClipboardData(CF_BITMAP); 498 if (bm_handle == NULL) { 499 E("%s: Device '%s' is unable to obtain frame from the clipboard: %d", 500 __FUNCTION__, wcd->window_name, GetLastError()); 501 CloseClipboard(); 502 return -1; 503 } 504 505 /* Get bitmap buffer. */ 506 if (wcd->gdi_bitmap->bmiHeader.biHeight > 0) { 507 wcd->gdi_bitmap->bmiHeader.biHeight = -wcd->gdi_bitmap->bmiHeader.biHeight; 508 } 509 510 if (!GetDIBits(wcd->dc, bm_handle, 0, wcd->frame_bitmap->bmiHeader.biHeight, 511 wcd->framebuffer, wcd->gdi_bitmap, DIB_RGB_COLORS)) { 512 E("%s: Device '%s' is unable to transfer frame to the framebuffer: %d", 513 __FUNCTION__, wcd->window_name, GetLastError()); 514 CloseClipboard(); 515 return -1; 516 } 517 518 if (wcd->gdi_bitmap->bmiHeader.biHeight < 0) { 519 wcd->gdi_bitmap->bmiHeader.biHeight = -wcd->gdi_bitmap->bmiHeader.biHeight; 520 } 521 522 CloseClipboard(); 523 524 /* Convert framebuffer. */ 525 return convert_frame(wcd->framebuffer, 526 wcd->pixel_format, 527 wcd->gdi_bitmap->bmiHeader.biSizeImage, 528 wcd->frame_bitmap->bmiHeader.biWidth, 529 wcd->frame_bitmap->bmiHeader.biHeight, 530 framebuffers, fbs_num); 531 } 532 533 void 534 camera_device_close(CameraDevice* cd) 535 { 536 /* Sanity checks. */ 537 if (cd == NULL || cd->opaque == NULL) { 538 E("%s: Invalid camera device descriptor", __FUNCTION__); 539 } else { 540 WndCameraDevice* wcd = (WndCameraDevice*)cd->opaque; 541 _camera_device_free(wcd); 542 } 543 } 544 545 int 546 enumerate_camera_devices(CameraInfo* cis, int max) 547 { 548 int inp_channel, found = 0; 549 550 for (inp_channel = 0; inp_channel < 10 && found < max; inp_channel++) { 551 char name[256]; 552 CameraDevice* cd; 553 554 snprintf(name, sizeof(name), "%s%d", _default_window_name, found); 555 cd = camera_device_open(name, inp_channel); 556 if (cd != NULL) { 557 WndCameraDevice* wcd = (WndCameraDevice*)cd->opaque; 558 559 /* Unfortunately, on Windows we have to start capturing in order to get the 560 * actual frame properties. Note that on Windows camera_device_start_capturing 561 * will ignore the pixel format parameter, since it will be determined during 562 * the course of the routine. Also note that on Windows all frames will be 563 * 640x480. */ 564 if (!camera_device_start_capturing(cd, V4L2_PIX_FMT_RGB32, 640, 480)) { 565 /* capXxx API supports only single frame size (always observed 640x480, 566 * but the actual numbers may vary). */ 567 cis[found].frame_sizes = (CameraFrameDim*)malloc(sizeof(CameraFrameDim)); 568 if (cis[found].frame_sizes != NULL) { 569 char disp_name[24]; 570 sprintf(disp_name, "webcam%d", found); 571 cis[found].display_name = ASTRDUP(disp_name); 572 cis[found].device_name = ASTRDUP(name); 573 cis[found].direction = ASTRDUP("front"); 574 cis[found].inp_channel = inp_channel; 575 cis[found].frame_sizes->width = wcd->frame_bitmap->bmiHeader.biWidth; 576 cis[found].frame_sizes->height = wcd->frame_bitmap->bmiHeader.biHeight; 577 cis[found].frame_sizes_num = 1; 578 cis[found].pixel_format = wcd->pixel_format; 579 cis[found].in_use = 0; 580 found++; 581 } else { 582 E("%s: Unable to allocate dimensions", __FUNCTION__); 583 } 584 camera_device_stop_capturing(cd); 585 } else { 586 /* No more cameras. */ 587 camera_device_close(cd); 588 break; 589 } 590 camera_device_close(cd); 591 } else { 592 /* No more cameras. */ 593 break; 594 } 595 } 596 597 return found; 598 } 599