| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | /*
 | 
					
						
							|  |  |  |  * Asterisk -- An open source telephony toolkit. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Copyright 2007, Luigi Rizzo  | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * See http://www.asterisk.org for more information about
 | 
					
						
							|  |  |  |  * the Asterisk project. Please do not directly contact | 
					
						
							|  |  |  |  * any of the maintainers of this project for assistance; | 
					
						
							|  |  |  |  * the project provides a web site, mailing lists and IRC | 
					
						
							|  |  |  |  * channels for your use. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * This program is free software, distributed under the terms of | 
					
						
							|  |  |  |  * the GNU General Public License Version 2. See the LICENSE file | 
					
						
							|  |  |  |  * at the top of the source tree. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*
 | 
					
						
							|  |  |  |  * Video grabbers used in console_video. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * $Revision$ | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Each grabber is implemented through open/read/close calls, | 
					
						
							|  |  |  |  * plus an additional move() function used e.g. to change origin | 
					
						
							|  |  |  |  * for the X grabber (this may be extended in the future to support | 
					
						
							|  |  |  |  * more controls e.g. resolution changes etc.). | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * open() should try to open and initialize the grabber, returning NULL on error. | 
					
						
							|  |  |  |  * On success it allocates a descriptor for its private data (including | 
					
						
							|  |  |  |  * a buffer for the video) and returns a pointer to the descriptor. | 
					
						
							|  |  |  |  * read() will return NULL on failure, or a pointer to a buffer with data | 
					
						
							|  |  |  |  * on success. | 
					
						
							|  |  |  |  * close() should release resources. | 
					
						
							|  |  |  |  * move() is optional. | 
					
						
							|  |  |  |  * For more details look at the X11 grabber below. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * NOTE: at the moment we expect uncompressed video frames in YUV format, | 
					
						
							|  |  |  |  * because this is what current sources supply and also is a convenient | 
					
						
							|  |  |  |  * format for display. It is conceivable that one might want to support | 
					
						
							|  |  |  |  * an already compressed stream, in which case we should redesign the | 
					
						
							|  |  |  |  * pipeline used for the local source, which at the moment is | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  *                        .->--[loc_dpy] | 
					
						
							|  |  |  |  *   [src]-->--[enc_in]--+ | 
					
						
							|  |  |  |  *                        `->--[enc_out] | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include "asterisk.h"
 | 
					
						
							| 
									
										
										
										
											2008-01-10 00:50:39 +00:00
										 |  |  | ASTERISK_FILE_VERSION(__FILE__, "$Revision$") | 
					
						
							| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | #include <sys/ioctl.h>
 | 
					
						
							|  |  |  | #include "asterisk/file.h"
 | 
					
						
							|  |  |  | #include "asterisk/utils.h"	/* ast_calloc */
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include "console_video.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #if defined(HAVE_VIDEO_CONSOLE)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #ifdef HAVE_X11
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* A simple X11 grabber, supporting only truecolor formats */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include <X11/Xlib.h>
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief internal info used by the X11 grabber */ | 
					
						
							|  |  |  | struct grab_x11_desc { | 
					
						
							|  |  |  | 	Display		*dpy; | 
					
						
							|  |  |  | 	XImage		*image; | 
					
						
							|  |  |  | 	int		screen_width;	/* width of X screen */ | 
					
						
							|  |  |  | 	int		screen_height;	/* height of X screen */ | 
					
						
							|  |  |  | 	struct fbuf_t	b;		/* geometry and pointer into the XImage */ | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-07-14 16:50:54 +00:00
										 |  |  | static void *grab_x11_close(void *desc);	/* forward declaration */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | /*! \brief open the grabber.
 | 
					
						
							|  |  |  |  * We use the special name 'X11' to indicate this grabber. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | static void *grab_x11_open(const char *name, struct fbuf_t *geom, int fps) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	XImage *im; | 
					
						
							|  |  |  | 	int screen_num; | 
					
						
							|  |  |  | 	struct grab_x11_desc *v; | 
					
						
							|  |  |  | 	struct fbuf_t *b; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-06-29 12:53:45 +00:00
										 |  |  | 	/* all names starting with X11 identify this grabber */ | 
					
						
							|  |  |  | 	if (strncasecmp(name, "X11", 3)) | 
					
						
							| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | 		return NULL;	/* not us */ | 
					
						
							|  |  |  | 	v = ast_calloc(1, sizeof(*v)); | 
					
						
							|  |  |  | 	if (v == NULL) | 
					
						
							|  |  |  | 		return NULL;	/* no memory */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/* init the connection with the X server */ | 
					
						
							|  |  |  | 	v->dpy = XOpenDisplay(NULL); | 
					
						
							|  |  |  | 	if (v->dpy == NULL) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error opening display\n"); | 
					
						
							|  |  |  | 		goto error; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	v->b = *geom;	/* copy geometry */ | 
					
						
							|  |  |  | 	b = &v->b;	/* shorthand */ | 
					
						
							|  |  |  | 	/* find width and height of the screen */ | 
					
						
							|  |  |  | 	screen_num = DefaultScreen(v->dpy); | 
					
						
							|  |  |  | 	v->screen_width = DisplayWidth(v->dpy, screen_num); | 
					
						
							|  |  |  | 	v->screen_height = DisplayHeight(v->dpy, screen_num); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	v->image = im = XGetImage(v->dpy, | 
					
						
							|  |  |  | 		RootWindow(v->dpy, DefaultScreen(v->dpy)), | 
					
						
							|  |  |  | 		b->x, b->y, b->w, b->h, AllPlanes, ZPixmap); | 
					
						
							|  |  |  | 	if (v->image == NULL) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error creating Ximage\n"); | 
					
						
							|  |  |  | 		goto error; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	switch (im->bits_per_pixel) { | 
					
						
							|  |  |  | 	case 32: | 
					
						
							|  |  |  | 		b->pix_fmt = PIX_FMT_RGBA32; | 
					
						
							|  |  |  | 		break; | 
					
						
							|  |  |  | 	case 16: | 
					
						
							|  |  |  | 		b->pix_fmt = (im->green_mask == 0x7e0) ? PIX_FMT_RGB565 : PIX_FMT_RGB555; | 
					
						
							|  |  |  | 		break; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	ast_log(LOG_NOTICE, "image: data %p %d bpp fmt %d, mask 0x%lx 0x%lx 0x%lx\n", | 
					
						
							|  |  |  | 		im->data, | 
					
						
							|  |  |  | 		im->bits_per_pixel, | 
					
						
							|  |  |  | 		b->pix_fmt, | 
					
						
							|  |  |  | 		im->red_mask, im->green_mask, im->blue_mask); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/* set the pointer but not the size as this is not malloc'ed */ | 
					
						
							|  |  |  | 	b->data = (uint8_t *)im->data; | 
					
						
							|  |  |  | 	return v; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | error: | 
					
						
							| 
									
										
										
										
											2008-07-14 16:50:54 +00:00
										 |  |  | 	return grab_x11_close(v); | 
					
						
							| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static struct fbuf_t *grab_x11_read(void *desc) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	/* read frame from X11 */ | 
					
						
							|  |  |  | 	struct grab_x11_desc *v = desc; | 
					
						
							|  |  |  | 	struct fbuf_t *b = &v->b; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	XGetSubImage(v->dpy, | 
					
						
							|  |  |  | 		RootWindow(v->dpy, DefaultScreen(v->dpy)), | 
					
						
							|  |  |  | 			b->x, b->y, b->w, b->h, AllPlanes, ZPixmap, v->image, 0, 0); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	b->data = (uint8_t *)v->image->data; | 
					
						
							|  |  |  | 	return b; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static int boundary_checks(int x, int limit) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |         return (x <= 0) ? 0 : (x > limit ? limit : x); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief move the origin for the grabbed area, making sure we do not
 | 
					
						
							|  |  |  |  * overflow the screen. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | static void grab_x11_move(void *desc, int dx, int dy) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	struct grab_x11_desc *v = desc; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         v->b.x = boundary_checks(v->b.x + dx, v->screen_width - v->b.w); | 
					
						
							|  |  |  |         v->b.y = boundary_checks(v->b.y + dy, v->screen_height - v->b.h); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief disconnect from the server and release memory */ | 
					
						
							|  |  |  | static void *grab_x11_close(void *desc) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	struct grab_x11_desc *v = desc; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-07-14 16:50:54 +00:00
										 |  |  | 	if (v->dpy) | 
					
						
							|  |  |  | 		XCloseDisplay(v->dpy); | 
					
						
							| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | 	v->dpy = NULL; | 
					
						
							|  |  |  | 	v->image = NULL; | 
					
						
							|  |  |  | 	ast_free(v); | 
					
						
							|  |  |  | 	return NULL; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static struct grab_desc grab_x11_desc = { | 
					
						
							|  |  |  | 	.name = "X11", | 
					
						
							|  |  |  | 	.open = grab_x11_open, | 
					
						
							|  |  |  | 	.read = grab_x11_read, | 
					
						
							|  |  |  | 	.move = grab_x11_move, | 
					
						
							|  |  |  | 	.close = grab_x11_close, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | #endif	/* HAVE_X11 */
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #ifdef HAVE_VIDEODEV_H
 | 
					
						
							|  |  |  | #include <linux/videodev.h>	/* Video4Linux stuff is only used in grab_v4l1_open() */
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | struct grab_v4l1_desc { | 
					
						
							|  |  |  | 	int fd;			/* device handle */ | 
					
						
							|  |  |  | 	struct fbuf_t	b;	/* buffer (allocated) with grabbed image */ | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief
 | 
					
						
							|  |  |  |  * Open the local video source and allocate a buffer | 
					
						
							|  |  |  |  * for storing the image. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | static void *grab_v4l1_open(const char *dev, struct fbuf_t *geom, int fps) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	struct video_window vw = { 0 };	/* camera attributes */ | 
					
						
							|  |  |  | 	struct video_picture vp; | 
					
						
							|  |  |  | 	int fd, i; | 
					
						
							|  |  |  | 	struct grab_v4l1_desc *v; | 
					
						
							|  |  |  | 	struct fbuf_t *b; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2008-06-29 12:53:45 +00:00
										 |  |  | 	/* name should be something under /dev/ */ | 
					
						
							|  |  |  | 	if (strncmp(dev, "/dev/", 5))  | 
					
						
							|  |  |  | 		return NULL; | 
					
						
							| 
									
										
										
										
											2007-12-29 02:02:03 +00:00
										 |  |  | 	fd = open(dev, O_RDONLY | O_NONBLOCK); | 
					
						
							|  |  |  | 	if (fd < 0) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error opening camera %s\n", dev); | 
					
						
							|  |  |  | 		return NULL; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	v = ast_calloc(1, sizeof(*v)); | 
					
						
							|  |  |  | 	if (v == NULL) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "no memory for camera %s\n", dev); | 
					
						
							|  |  |  | 		close(fd); | 
					
						
							|  |  |  | 		return NULL;	/* no memory */ | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	v->fd = fd; | 
					
						
							|  |  |  | 	v->b = *geom; | 
					
						
							|  |  |  | 	b = &v->b;	/* shorthand */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	i = fcntl(fd, F_GETFL); | 
					
						
							|  |  |  | 	if (-1 == fcntl(fd, F_SETFL, i | O_NONBLOCK)) { | 
					
						
							|  |  |  | 		/* non fatal, just emit a warning */ | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error F_SETFL for %s [%s]\n", | 
					
						
							|  |  |  | 			dev, strerror(errno)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	/* set format for the camera.
 | 
					
						
							|  |  |  | 	 * In principle we could retry with a different format if the | 
					
						
							|  |  |  | 	 * one we are asking for is not supported. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	vw.width = b->w; | 
					
						
							|  |  |  | 	vw.height = b->h; | 
					
						
							|  |  |  | 	vw.flags = fps << 16; | 
					
						
							|  |  |  | 	if (ioctl(fd, VIDIOCSWIN, &vw) == -1) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error setting format for %s [%s]\n", | 
					
						
							|  |  |  | 			dev, strerror(errno)); | 
					
						
							|  |  |  | 		goto error; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if (ioctl(fd, VIDIOCGPICT, &vp) == -1) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error reading picture info\n"); | 
					
						
							|  |  |  | 		goto error; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	ast_log(LOG_WARNING, | 
					
						
							|  |  |  | 		"contrast %d bright %d colour %d hue %d white %d palette %d\n", | 
					
						
							|  |  |  | 		vp.contrast, vp.brightness, | 
					
						
							|  |  |  | 		vp.colour, vp.hue, | 
					
						
							|  |  |  | 		vp.whiteness, vp.palette); | 
					
						
							|  |  |  | 	/* set the video format. Here again, we don't necessary have to
 | 
					
						
							|  |  |  | 	 * fail if the required format is not supported, but try to use | 
					
						
							|  |  |  | 	 * what the camera gives us. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	b->pix_fmt = vp.palette; | 
					
						
							|  |  |  | 	vp.palette = VIDEO_PALETTE_YUV420P; | 
					
						
							|  |  |  | 	if (ioctl(v->fd, VIDIOCSPICT, &vp) == -1) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error setting palette, using %d\n", | 
					
						
							|  |  |  | 			b->pix_fmt); | 
					
						
							|  |  |  | 	} else | 
					
						
							|  |  |  | 		b->pix_fmt = vp.palette; | 
					
						
							|  |  |  | 	/* allocate the source buffer.
 | 
					
						
							|  |  |  | 	 * XXX, the code here only handles yuv411, for other formats | 
					
						
							|  |  |  | 	 * we need to look at pix_fmt and set size accordingly | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	b->size = (b->w * b->h * 3)/2;	/* yuv411 */ | 
					
						
							|  |  |  | 	ast_log(LOG_WARNING, "videodev %s opened, size %dx%d %d\n", | 
					
						
							|  |  |  | 		dev, b->w, b->h, b->size); | 
					
						
							|  |  |  | 	b->data = ast_calloc(1, b->size); | 
					
						
							|  |  |  | 	if (!b->data) { | 
					
						
							|  |  |  | 		ast_log(LOG_WARNING, "error allocating buffer %d bytes\n", | 
					
						
							|  |  |  | 			b->size); | 
					
						
							|  |  |  | 		goto error; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	ast_log(LOG_WARNING, "success opening camera\n"); | 
					
						
							|  |  |  | 	return v; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | error: | 
					
						
							|  |  |  | 	close(v->fd); | 
					
						
							|  |  |  | 	fbuf_free(b); | 
					
						
							|  |  |  | 	ast_free(v); | 
					
						
							|  |  |  | 	return NULL; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief read until error, no data or buffer full.
 | 
					
						
							|  |  |  |  * This might be blocking but no big deal since we are in the | 
					
						
							|  |  |  |  * display thread. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | static struct fbuf_t *grab_v4l1_read(void *desc) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	struct grab_v4l1_desc *v = desc; | 
					
						
							|  |  |  | 	struct fbuf_t *b = &v->b; | 
					
						
							|  |  |  | 	for (;;) { | 
					
						
							|  |  |  | 		int r, l = b->size - b->used; | 
					
						
							|  |  |  | 		r = read(v->fd, b->data + b->used, l); | 
					
						
							|  |  |  | 		// ast_log(LOG_WARNING, "read %d of %d bytes from webcam\n", r, l);
 | 
					
						
							|  |  |  | 		if (r < 0)	/* read error */ | 
					
						
							|  |  |  | 			break; | 
					
						
							|  |  |  | 		if (r == 0)	/* no data */ | 
					
						
							|  |  |  | 			break; | 
					
						
							|  |  |  | 		b->used += r; | 
					
						
							|  |  |  | 		if (r == l) { | 
					
						
							|  |  |  | 			b->used = 0; /* prepare for next frame */ | 
					
						
							|  |  |  | 			return b; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return NULL; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | static void *grab_v4l1_close(void *desc) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	struct grab_v4l1_desc *v = desc; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	close(v->fd); | 
					
						
							|  |  |  | 	v->fd = -1; | 
					
						
							|  |  |  | 	fbuf_free(&v->b); | 
					
						
							|  |  |  | 	ast_free(v); | 
					
						
							|  |  |  | 	return NULL; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief our descriptor. We don't have .move */ | 
					
						
							|  |  |  | static struct grab_desc grab_v4l1_desc = { | 
					
						
							|  |  |  | 	.name = "v4l1", | 
					
						
							|  |  |  | 	.open = grab_v4l1_open, | 
					
						
							|  |  |  | 	.read = grab_v4l1_read, | 
					
						
							|  |  |  | 	.close = grab_v4l1_close, | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | #endif /* HAVE_VIDEODEV_H */
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*
 | 
					
						
							|  |  |  |  * Here you can add more grabbers, e.g. V4L2, firewire, | 
					
						
							|  |  |  |  * a file, a still image... | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*! \brief The list of grabbers supported, with a NULL at the end */ | 
					
						
							|  |  |  | struct grab_desc *console_grabbers[] = { | 
					
						
							|  |  |  | #ifdef HAVE_X11
 | 
					
						
							|  |  |  | 	&grab_x11_desc, | 
					
						
							|  |  |  | #endif
 | 
					
						
							|  |  |  | #ifdef HAVE_VIDEODEV_H
 | 
					
						
							|  |  |  | 	&grab_v4l1_desc, | 
					
						
							|  |  |  | #endif
 | 
					
						
							|  |  |  | 	NULL | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #endif /* HAVE_VIDEO_CONSOLE */
 |