/*
 * s2263mid.c
 * An API layer for 2263 using Video4Linux2
 * Using UVC driver - uvcvideo.ko
 * Copyright(c) 2013-2025 Sensoray Co., Inc.
 */
/*lint -e845 -e835 -e785 -e734 -e726 -e501 -e713 -e718 -e701 -e708*/
/*lint -e641 -e64 -e570 -e1053 -e712  -e838 -e18 -e31*/
/*lint -e438*/ // last value assigned not used
/*lint -e578*/ // variable hides function
/*lint -e454 -e455*/ // thread mutex tracking(lint error?)
/*lint -e746*/ // function prototype missing (libusb)

/* Ensure 64-bit offsets are used for creating large files */
#define _LARGEFILE64_SOURCE
#define _FILE_OFFSET_BITS 64

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <poll.h>
#include <pthread.h>

#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>
//#include <X11/Xatom.h>
#include <X11/extensions/Xv.h>
#include <X11/extensions/Xvlib.h>
#include <X11/extensions/XShm.h>

#include <linux/videodev2.h>
#include <linux/uvcvideo.h>
#include <linux/usb/video.h>
#include <linux/usb/ch9.h>
#include <linux/media.h>

#include <alsa/asoundlib.h>

#ifdef MAGICKWAND_MAJOR_VERSION
#if MAGICKWAND_MAJOR_VERSION >= 7
#include <MagickWand/MagickWand.h>
#else
#include <wand/MagickWand.h>
#endif
#endif

#include <libusb.h>

#include "uvc_xu_ctrls.h"
#include "uvc_xu_ctrls_3364.h"
#include "s2263mid.h"
#include "libmp4/mp4.h"
#include "libmp4/h264.h"
#include "libmp4/mpegts.h"
#include "libavi/avi.h"
#ifdef ENABLE_HID
#include "hidapi/hidapi.h"
#endif
#include "rb_simple.h"
//#define DISABLE_TIMEOUT_CHECKS

// for video_window_init
#define YUV_FORMAT_YUV_420 0
#define YUV_FORMAT_UYVY_422 1
#define YUV_FORMAT_YUY2_422 2
#define YUV_FORMAT_NV12_420 3

enum {
	STRM_CAP, // S2263_CAPTURE
	STRM_PRV, // S2263_PREVIEW
	STRM_OUT, // S2263_PLAYBACK
	STRM_AUD, // S2263_AUDIO
	STRM_3364,
	STRM_AUD_CAP, // audio capture
	MAX_STRMS,
};

static char cardname[32] = "Sensoray Model 2263";
static const char cardname3364[32] = "Sensoray Model 3364 Video";
static int G_debug = 0;

// static working buf for audio (MP3 record)
static unsigned char G_audio_buf[RB_BUF_SIZE];
#define RB_SIZE_AUDIO_PCM (4096*128)
#define RB_SIZE_FILESAVE (1024 * 1024 * 12) // 12MB buffer
#define RINGBUFFER_ID_AUDIO0 1 // PCM preview
#define RINGBUFFER_ID_AUDIO1 2 // PCM record
#define RINGBUFFER_ID_FILE   3 // file ringbuffer
struct ringbuffer {
	unsigned char *buf;
	unsigned int size;
	unsigned int head;
	unsigned int tail;
	pthread_cond_t cond;
	pthread_mutex_t lock;
	int id; // identifier for debug/overflow
	int exit; // don't wait for ringbuffer_full if stopping thread
	          // to prevent deadlock
};



// to accomodate PCM audio, we add an audio_thread for buffering to PCM
// audio_buffer_thread, audio_preview_thread
// audio_preview_thread will read the data from ringbuffer_pcm[IDX_RB_PREVIEW]
// audio buffer thread writes to both ringbuffers

struct stream {
	pthread_mutex_t mutex;
	struct dev *dev;
	int fd; // file descriptor of record or preview device
	pthread_t thread;
	pthread_t file_thread;
	pthread_t audio_thread;
	struct ringbuffer ringbuffer;
#define IDX_RB_RECORD  0
#define IDX_RB_AUDPREVIEW 1
	struct ringbuffer ringbuffer_pcm[2];
	struct rb_simple rb_audio;
	FILE *rec_file;
        S2263_cb_func_t callback;
	int xu_param;
	int mp4_max_minutes;
	int mp4_interval_seconds;
	unsigned char params[10];
	enum v4l2_buf_type type;
	unsigned active: 1;
	unsigned recording: 1;
	unsigned previewing: 1;
	unsigned enable_mp4: 1;
        unsigned playback: 1;
	unsigned snapshot: 1;
	unsigned restart: 1;
	unsigned audio_previewing: 1;
	unsigned audio_recording: 1; // for AVI pcm record
	unsigned enable_avi_vonly: 1;
	unsigned enable_avi_av: 1; // check firmware version
	unsigned avi_av_uses_preview: 1; // see above
	MID2263_AUDMODE audio_mode;
	int audio_br;//bitrate kbps
	MID2263_RECMODE playback_mode;
	int is_interlaced; // for 3364
	int fullsize_preview_mode; // for 3364
	FILE *snapshot_file;
	int errors; // more than 5 errors will trigger a restart
};


#define SRAY_FIRMWARE_TS_PCM 4107 // firmware supports TS PCM stream.
                                  // allows simultaneous audio preview

struct dev {
	int board_num;
	unsigned int parent_serial; // for 3364. hub chip contains serial
	unsigned int usb_serial;    // USB serial number
	unsigned int model;  // board model: 2263,3364,2267,2231
	// number for the board
	int is_3364; // is it a 3364 or not?
	int is_2253; // is it a 2253 or not?

	int fw_ver; // board firmware version
	struct v4l2_capability cap;
	struct v4l2_capability cap3364;
	const struct uvc_control_attr *ctrls;
	struct viddec_info viddec_info;
	struct stream strm[6]; // capture, preview, audio_preview, output, 3364dev, audio_capture (to RB)
	pthread_t log_thread;
	int preview_xid; // preview window x id
#ifdef ENABLE_HID	
	hid_device *hid_dev; // returned by hid_open
#endif

	libusb_device_handle *usb_dev; // libusb device used for overlays
	unsigned int usb_iface;  // interface number for overlay endpoint
	unsigned int usb_ep_max_packet_size; // wMaxPacketSize for overlay endpoint
	unsigned char usb_ep;     // endpoint address for overlay data
};

static struct dev device[MAX_DEVICES] = {
	[0 ... MAX_DEVICES-1] = { /* GCC designated initializers */
		.strm[0 ... MAX_STRMS-1] = {
			.fd = -1,
			.mutex = PTHREAD_MUTEX_INITIALIZER,
		},
		.preview_xid = -1,
	}
};

#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))

/*------------------- private functions ----------------------*/

static void *record_thread(void *env);
static void *preview_thread(void *env);
static void *preview_3364_thread(void *env);
static void *audio_preview_thread(void *env);
static void *audio_buffer_thread(void *env);
static void *playback_thread(void *env);
static void *file_thread(void *env);
static void *log_thread(void *env);

static void (*open_hook)(int devid) = NULL;

/* print a message if debugging is enabled */
static void dprint(const char *fmt, ...)
{
#if 1
	if (G_debug) {
		struct timeval tv;
		va_list ap;

		gettimeofday(&tv, NULL);
		printf("[%5d.%06d] ", (int)(tv.tv_sec % 100000), (int)tv.tv_usec);

		va_start(ap, fmt);
		vprintf(fmt, ap);
		va_end(ap);
	}
#else
	static FILE *f = NULL;
	if (!f) {
		f = fopen("/tmp/log.txt", "w");
	}
	if (f) {
		va_list ap;
		va_start(ap, fmt);
		vfprintf(f, fmt, ap);
		va_end(ap);
		fflush(f);
	}
#endif
}

void printBacktrace(int sig);


/* do an ioctl and return the errno */
static int xioctl(int fd, unsigned long request, void *arg)
{
	int rc;
	int tries = 10000;

	do {
		rc = ioctl(fd, request, arg);
		if (rc == -1) {
			char *msg = "ioctl";

			if (request == VIDIOC_ENUMINPUT && errno == EINVAL)
				break; // EINVAL is returned when no more entries

			switch (request) {
			case UVCIOC_CTRL_QUERY: msg = "UVCIOC_CTRL_QUERY"; break;
			case VIDIOC_ENUMINPUT: msg = "VIDIOC_ENUMINPUT"; break;
			case VIDIOC_S_INPUT: msg = "VIDIOC_S_INPUT"; break;
			case VIDIOC_G_INPUT: msg = "VIDIOC_G_INPUT"; break;
			default: break;
			}
			perror(msg);
		}
	} while (rc == -1 && (errno == EINTR || errno == EAGAIN) && tries-- > 0);
#if 0
	if (rc == -1) {
		char msg[100];
		sprintf(msg, "fd=%d request=%x", fd, request);
		perror(msg);

		printBacktrace(0);
	}
#endif
	return rc == -1 ? -errno : rc;
}

#if 0
/* check a device index, returns NULL if the device isn't open, 
 * otherwise return it with mutex locked */
static struct dev *GetDev(int devid)
{
	if ((unsigned int) devid >= MAX_DEVICES)
		return NULL;
	pthread_mutex_lock(&device[devid].mutex);
	if (device[devid].fd < 0) {
		pthread_mutex_unlock(&device[devid].mutex);
		dprint("%s: devid %d not open\n", __func__, devid);
		return NULL;
	}
	return &device[devid];
}

/* finish using a device, unlock mutex */
static void PutDev(struct dev *dev)
{
	if (dev)
		pthread_mutex_unlock(&dev->mutex);
}
#endif

static inline struct stream* _GetStrm(int devid, int strmidx)
{
	if ((unsigned int) devid >= MAX_DEVICES) {
		dprint("%s: invalid devid %d\n", __func__, devid);
		return NULL;
	}
	pthread_mutex_lock(&device[devid].strm[strmidx].mutex);
	if (strmidx == STRM_3364 && !device[devid].is_3364) {
		pthread_mutex_unlock(&device[devid].strm[strmidx].mutex);
		return NULL;
	}
	if (strmidx != STRM_OUT && strmidx != STRM_AUD && strmidx != STRM_AUD_CAP &&
		device[devid].strm[strmidx].fd < 0) {
		pthread_mutex_unlock(&device[devid].strm[strmidx].mutex);
		dprint("%s: devid %d strmidx %d not open\n", __func__, devid, strmidx);
#ifdef DEBUG
		printBacktrace(0);
#endif
		return NULL;
	}
	return &device[devid].strm[strmidx];
}

static inline struct stream *GetStrm(int devid, int strmidx)
{
	if ((unsigned int) strmidx > 1) {
		dprint("%s: invalid strmidx %d\n", __func__, strmidx);
		return NULL;
	}
	return _GetStrm(devid, strmidx);
}

static inline struct stream *GetAudioStrm(int devid)
{
	return _GetStrm(devid, STRM_AUD);
}

static inline struct stream *GetAudioCapStrm(int devid)
{
	return _GetStrm(devid, STRM_AUD_CAP);
}

static inline struct stream *GetOutputStrm(int devid)
{
	return _GetStrm(devid, STRM_OUT);
}

static inline struct stream *Get3364Strm(int devid)
{
	return _GetStrm(devid, STRM_3364);
}


/* finish using a device, unlock mutex */
static inline void PutStrm(struct stream *strm)
{
	if (strm)
		pthread_mutex_unlock(&strm->mutex);
}

static const char *ctrl_name(int id, struct dev *dev)
{
	return (char*)dev->ctrls[id].name;
}

int GetStrmFd(int devid, int strmidx)
{
	int ret = -1;
	struct stream *strm = _GetStrm(devid, strmidx);
	if (strm)
		ret = strm->fd;
	PutStrm(strm);
	return ret;
}

static void ms_sleep(unsigned int ms)
{
	struct timespec ts;
	ts.tv_sec = ms / 1000;
	ts.tv_nsec = (ms % 1000)*1000000;
	nanosleep(&ts, NULL);
}


static int set_unit_control_data(int unit, int id, const __u8 *data, int size, struct dev *dev, int strmidx)
{
	struct uvc_xu_control_query ctrl = {
		.unit = unit,
		.selector = id,
		.query = UVC_SET_CUR,
		.size = size,
		.data = (__u8*)data,
	};

	int rc = xioctl(dev->strm[strmidx].fd, UVCIOC_CTRL_QUERY, &ctrl);
	int io_errors = 0;

	//dprint("%s: unit=%d id=%d[%s] size=%d\n", __func__, unit, id, ctrl_name(id, dev), ctrl.size);

	while (rc == -EIO && ++io_errors < 100) {
		// UVC driver will return -EIO for all usb_control_msg errors,
		// one of which can be -EPIPE which means the control endpoint
		// sent a stall, in which case we should wait and retry
		ms_sleep(1);
		rc = xioctl(dev->strm[strmidx].fd, UVCIOC_CTRL_QUERY, &ctrl);
	}
	if (rc < 0) {
		dprint("%s: unit=%d id=%d[%s] size=%d rc=%d %s\n", __func__, unit,
			id, ctrl_name(id, dev), ctrl.size, rc, strerror(errno));
	}
	return rc;
}

static int get_unit_control_data(int unit, int id, __u8 *data, int size, struct dev *dev, int strmidx)
{
	struct uvc_xu_control_query ctrl = {
		.unit = unit,
		.selector = id,
		.query = UVC_GET_CUR,
		.size = size,
		.data = data,
	};
	//dprint("%s: unit=%d id=%d[%s] size=%d\n", __func__, unit, id, ctrl_name(id, dev), ctrl.size);
	int rc = xioctl(dev->strm[strmidx].fd, UVCIOC_CTRL_QUERY, &ctrl);
	if (rc < 0) {
		dprint("%s: unit=%d id=%d[%s] size=%d rc=%d %s\n", __func__, unit, 
			id, ctrl_name(id, dev), ctrl.size, rc, strerror(errno));
	}
	return rc;
}

static int set_xu_control_data(int id, const __u8 *data, struct dev *dev)
{
	return set_unit_control_data(UVC_ID_EXTENSION_UNIT, id, data, dev->ctrls[id].length, dev, 0);
}

static int get_xu_control_data(int id, __u8 *data, struct dev *dev)
{
	return get_unit_control_data(UVC_ID_EXTENSION_UNIT, id, data, dev->ctrls[id].length, dev, 0);
}

static int set_xu3364_control_data(int id, const __u8 *data, struct dev *dev)
{
	return set_unit_control_data(UVC3364_ID_EXTENSION_UNIT, id, data, 10, dev, STRM_3364);
}

static int get_xu3364_control_data(int id, __u8 *data, struct dev *dev)
{
	return get_unit_control_data(UVC3364_ID_EXTENSION_UNIT, id, data, 10, dev, STRM_3364);
}


static int get_xu_control_len(int id, struct dev *dev)
{
	__u8 data[2] = {};
	struct uvc_xu_control_query ctrl = {
		.unit = UVC_ID_EXTENSION_UNIT,
		.selector = id,
		.query = UVC_GET_LEN,
		.size = 2,
		.data = data,
	};
	int rc = xioctl(dev->strm[0].fd, UVCIOC_CTRL_QUERY, &ctrl);
	if (rc < 0)
		return rc;
	return data[0] + (data[1] << 8);
}


static int set_xu_control(int id, int value, struct dev *dev)
{
	__u8 data[4] = {
		value & 0xff,
		(value >> 8) & 0xff,
		(value >> 16) & 0xff,
		(value >> 24) & 0xff,
	};
	//dprint("set %s %d\n", sray_xu_controls[id].name, value);
	return set_xu_control_data(id, data, dev);
}

static int get_xu_control(int id, int *value, struct dev *dev)
{
	__u8 data[8] = {0, 0, 0, 0};
	int rc = get_xu_control_data(id, data, dev);
	if (rc >= 0) {
		*value = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
		//dprint("get %s %d\n", sray_xu_controls[id].name, *value);
	}
	return rc;
}

static int set_xu(int id, int value, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = set_xu_control(id, value, strm->dev);
	if (rc < 0)
		dprint("%s: id=%d rc=%d %s\n", __func__, id, rc, strerror(errno));
	PutStrm(strm);
	return rc;
}

static int get_xu(int id, int *value, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = get_xu_control(id, value, strm->dev);
	if (rc < 0)
		dprint("%s: id=%d rc=%d %s\n", __func__, id, rc, strerror(errno));
	PutStrm(strm);
	return rc;
}


static int libusb_initialized = 0;

// find associated libUSB device with same SN
static int FindLibUSBMatch(int sn, struct dev *sdev)
{
	libusb_device **devs;
	int rc, i;

	dprint("%s: sn=%d\n", __func__, sn);
	sdev->usb_dev = NULL;
	sdev->usb_iface = 0;

	if (!libusb_initialized) {
		rc = libusb_init(NULL);
		if (rc < 0) {
			dprint("libusb_init failed rc=%d\n", rc);
			return -1;
		}
		libusb_initialized = 1;
	}

	rc = libusb_get_device_list(NULL, &devs);
	if (rc < 0) {
		dprint("libusb_get_device_list failed rc=%d\n", rc);
		libusb_exit(NULL);
		return -1;
	}

	for (i = 0; devs[i] != NULL; i++) {
		libusb_device *dev = devs[i];
		libusb_device_handle *hdev;
		struct libusb_config_descriptor *config;
		struct libusb_device_descriptor desc;

		unsigned char serial[10] = {0};
		unsigned int j;

		if (libusb_get_device_descriptor(dev, &desc) < 0) {
			dprint("libusb_get_device_descriptor failed\n");
			continue;
		}

		// Search until model 2263 with matching serial
		if (desc.idVendor != 0x1943 || desc.idProduct != 0x2263)
			continue;

		rc = libusb_open(dev, &hdev);
		if (rc < 0) {
			static int once = 1;
			dprint("libusb_open failed rc=%d\n", rc);
			if (rc == LIBUSB_ERROR_ACCESS && once) {
				once = 0;
				dprint("consider adding the following line to /etc/udev/rules.d/51-usb-device.rules then replug device:\n"
					"SUBSYSTEM==\"usb\", ATTRS{idVendor}==\"1943\", ATTRS{idProduct}==\"2263\", GROUP=\"plugdev\", TAG+=\"uaccess\"\n");
			}
			continue;
		}
		// Get serial number string and compare it
		if (libusb_get_string_descriptor_ascii(hdev, desc.iSerialNumber, serial, sizeof(serial)) < 0) {
			dprint("libusb_get_string_descriptor failed\n");
			libusb_close(hdev);
			continue;
		}
		if (atoi((char*)serial) != sn) {
			libusb_close(hdev);
			continue;
		}
		// Search interface descriptors for class misc
		libusb_get_config_descriptor(dev, 0, &config);
		for (j = 0; j < config->bNumInterfaces; j++) {
			const struct libusb_interface *iface = &config->interface[j];
			const struct libusb_interface_descriptor *idesc = &iface->altsetting[0];

			if ((idesc->bInterfaceClass == USB_CLASS_MISC &&
			    idesc->bInterfaceSubClass == 2 &&
			    idesc->bInterfaceProtocol == 1 &&
			    idesc->bNumEndpoints >= 1) ||
			    (idesc->bInterfaceClass == USB_CLASS_VENDOR_SPEC &&
			    idesc->bInterfaceSubClass == 0 &&
			    idesc->bInterfaceProtocol == 0 &&
			    idesc->bNumEndpoints >= 1)) {
				const struct libusb_endpoint_descriptor *ep = &idesc->endpoint[0];

				sdev->usb_dev = hdev;
				sdev->usb_iface = j;
				sdev->usb_ep = ep->bEndpointAddress;
				sdev->usb_ep_max_packet_size = ep->wMaxPacketSize;
				dprint("overlay enabled on ep %02x, %u\n", ep->bEndpointAddress, ep->wMaxPacketSize);
				break;
			}
		}
#if 0
		// Check MS OS Config String Descriptor
		unsigned char ms[0x12] = {};
		if (0 && libusb_get_string_descriptor(hdev, 0xee, 0x0000, ms, sizeof(ms)) >= 0) {
		//if (libusb_get_string_descriptor(hdev, 0x01, 0x0409, ms, sizeof(ms)) >= 0) {
			unsigned char bMS_VendorCode = ms[16];
			unsigned char data[256];

			printf("len=0x%x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x %02x%02x\n",
				ms[0], ms[2],ms[3], ms[4],ms[5], ms[6],ms[7], 
				ms[8],ms[9], ms[10],ms[11], ms[12],ms[13], ms[14],ms[15], ms[16],ms[17]);

			printf("MS_OS: %c%c%c%c%c%c%c%c%c %02x\n", ms[0],ms[2],ms[4],ms[6],ms[8],ms[10],ms[12],ms[14],ms[16], bMS_VendorCode);

			int len = libusb_control_transfer(hdev,
				USB_DIR_IN | USB_TYPE_VENDOR,	// bmRequestType
				bMS_VendorCode,			// bRequest
				0,				// wValue
				4,				// wIndex
				data,				// data
				sizeof(data),			// wLength
				1000/*ms*/			// timeout
				);
			if (len > 0) {
				printf("%d\n", len);
				printf("dwLength: %d\n"
					"bcdVersion: %x\n"
					"wIndex: %x\n"
					"bCount: %d\n"
					"RESERVED: %02x %02x %02x %02x %02x %02x %02x\n",
					data[0]+data[1]*0x100+data[2]*0x10000+data[3]*0x1000000,
					data[4]+data[5]*0x100,
					data[6]+data[7]*0x100,
					data[8],
					data[9],data[10],data[11],data[12],data[13],data[14],data[15]);
				if (len > 16) {
					unsigned char *p = data+16;
					printf("bFirstInterfaceNumber: %d\n"
						"bInterfaceCount: %d\n"
						"compatibleID: %c%c%c%c%c%c%c%c\n"
						"subCompatibleID: %c%c%c%c%c%c%c%c\n"
						"RESERVED: %02x %02x %02x %02x %02x %02x\n",
						p[0],
						p[1],
						p[2],p[3],p[4],p[5],p[6],p[7],p[8],p[9],
						p[10],p[11],p[12],p[13],p[14],p[15],p[16],p[17],
						p[18],p[19],p[20],p[21],p[22],p[23]);
				}
			}
			len = libusb_control_transfer(hdev,
				USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_INTERFACE,	// bmRequestType
				bMS_VendorCode,			// bRequest
				0,				// wValue
				5,				// wIndex
				data,				// data
				sizeof(data),			// wLength
				1000/*ms*/			// timeout
				);
			if (len > 0) {
				printf("%d\n", len);
				printf("dwLength: %d\n"
					"bcdVersion: %x\n"
					"wIndex: %x\n"
					"bCount: %d\n"
					"RESERVED: %02x %02x %02x %02x %02x %02x %02x\n",
					data[0]+data[1]*0x100+data[2]*0x10000+data[3]*0x1000000,
					data[4]+data[5]*0x100,
					data[6]+data[7]*0x100,
					data[8],
					data[9],data[10],data[11],data[12],data[13],data[14],data[15]);
				if (len > 16) {
					unsigned char *p = data+16;
					printf("bFirstInterfaceNumber: %d\n"
						"bInterfaceCount: %d\n"
						"compatibleID: %c%c%c%c%c%c%c%c\n"
						"subCompatibleID: %c%c%c%c%c%c%c%c\n"
						"RESERVED: %02x %02x %02x %02x %02x %02x\n",
						p[0],
						p[1],
						p[2],p[3],p[4],p[5],p[6],p[7],p[8],p[9],
						p[10],p[11],p[12],p[13],p[14],p[15],p[16],p[17],
						p[18],p[19],p[20],p[21],p[22],p[23]);
				}
			}

			exit(0);
		}
#endif

		libusb_free_config_descriptor(config);
		if (sdev->usb_dev == NULL) {
			libusb_close(hdev);
		}
	}
	libusb_free_device_list(devs, 1);

	return 0;
}

static int parse_bus_info(const char *bus_info, uint8_t *ports, size_t port_size)
{
	int port_count = 0;
	const char *s = bus_info;

	s = strrchr(s, '-'); // last '-' before dotted ports
	while (s && (size_t)port_count < port_size) {
		s++; // skip '-' or '.'
		ports[port_count++] = atoi(s);
		s = strchr(s, '.'); // find next '.'
	}
	return port_count;
}

// search the connected usb devices to find one whose parent has the same serial 
// if serial is -1, return the serial of the parent of device matching vid,pid,ports
// otherwise, return the ports and count of the device matching vid,pid whose parent has serial
static int libusb_search(
		int vid, int pid,
		int parent_vid, int parent_pid,
		int serial,
		uint8_t *ports, size_t port_size)
{
	libusb_device **devs;
	int rc, i;

	if (!libusb_initialized) {
		rc = libusb_init(NULL);
		if (rc < 0) {
			dprint("libusb_init failed rc=%d\n", rc);
			return -1;
		}
		libusb_initialized = 1;
	}

	rc = libusb_get_device_list(NULL, &devs);
	if (rc < 0) {
		dprint("libusb_get_device_list failed rc=%d\n", rc);
		libusb_exit(NULL);
		return -1;
	}

	rc = -1;
	for (i = 0; devs[i] != NULL; i++) {
		libusb_device *dev = devs[i];
		struct libusb_device_descriptor desc;
		int j;
		uint8_t p[7];
		int pc;

		if (libusb_get_device_descriptor(dev, &desc) < 0) {
			dprint("libusb_get_device_descriptor failed\n");
			continue;
		}
		if (desc.idVendor != vid || desc.idProduct != pid)
			continue;

		//printf("idVendor=%x idProduct=%x serial=%d\n", desc.idVendor, desc.idProduct, serial);
		if (-1 == serial) {
			// check matching device ports
			pc = libusb_get_port_numbers(dev, p, ARRAY_SIZE(p));
			if ((size_t)pc != port_size || memcmp(p, ports, pc) != 0)
				continue; // not matched
		}
		libusb_device *parent = libusb_get_parent(dev);

		if (libusb_get_device_descriptor(parent, &desc) < 0) {
			dprint("libusb_get_device_descriptor failed\n");
			continue;
		}
		//printf("parent idVendor=%x idProduct=%x \n", desc.idVendor, desc.idProduct);
		if (desc.idVendor != parent_vid || desc.idProduct != parent_pid)
			continue;
		// now get parent ports
		pc = libusb_get_port_numbers(parent, p, ARRAY_SIZE(p));

		// look up /sys/bus/usb/devices/<bus>-<ports>/serial
		char path[64] = "/sys/bus/usb/devices/"; // max is 21+((1+7)*(3+1))+6 chars
		sprintf(path + strlen(path), "%d-", libusb_get_bus_number(parent));
		for (j = 0; j < pc; j++) {
			sprintf(path + strlen(path), "%d%c", p[j], "./"[j+1 == pc]); //lint !e514 //unusual boolean
		}
		strcat(path, "serial");

		int fd = open(path, O_RDONLY);
		if (fd == -1) {
			perror(path);
			continue;
		}
		char buf[64] = "";
		read(fd, buf, ARRAY_SIZE(buf));
		rc = atoi(buf);
		close(fd);

		if (-1 == serial)
			break; // return serial in rc

		if (serial == rc) {
			// parent serial match - return ports and count
			rc = libusb_get_port_numbers(dev, ports, port_size);
			break;
		}
		rc = -1;
	}
	libusb_free_device_list(devs, 1);

	return rc;
}





/* search for MediaController device that has the same bus info as the VideoDevice,
   and return its real card name (instead of generic "Video Capture 5" etc) */
static void enumMedia(struct v4l2_capability *cap)
{
#ifdef MEDIA_IOC_DEVICE_INFO
	int i, fd;
	char devname[16];
	struct media_device_info media;
	const char generic[] = "Video Capture";

	if (strncmp((char*)cap->card, generic, strlen(generic)) != 0) {
		return; // Not a generic name
	}

	for (i = 0; i < 256; i++) {
		sprintf(devname, "/dev/media%d", i);
		fd = open(devname, O_RDWR);
		if (fd == -1) {
			if (errno != ENOENT) perror(devname);
			continue;
		}
		memset(&media, 0, sizeof(media));
		if (xioctl(fd, MEDIA_IOC_DEVICE_INFO, &media) == 0) {
			dprint("%s:   bus %s (%s)\n", __func__, media.bus_info, media.model);
			if (strcmp(media.bus_info, (char*)cap->bus_info) == 0) {
				memcpy(cap->card, media.model, sizeof(cap->card));
				close(fd);
				return;
			}
		}
		close(fd);
	}
#endif
}


// find associated 3364 to the 2263 with same parent device serial number
static int Find3364Match(const char *bus_info, struct dev *dev)
{
	int i, fd;
	char devname[16];
	struct v4l2_capability cap;
	uint8_t ports[7] = {};
	int port_count;
	int serial;

	port_count = parse_bus_info(bus_info, ports, ARRAY_SIZE(ports));
	//dprint("%s: 2263  ports=", __func__);
	//for (i = 0; i < port_count; i++) dprint("%d%c", ports[i], ".\n"[i+1==port_count]);

	serial = libusb_search(0x1943, 0x2263, 0x4b4, 0x6502, -1, ports, port_count);
	//dprint("%s: parent serial %d\n", __func__, serial);
	if (serial < 0)
		return 0;  // libusb error?

	port_count = libusb_search(0x1943, 0x3364, 0x4b4, 0x6500, serial, ports, ARRAY_SIZE(ports));
	//dprint("%s: 3364  ports=", __func__);
	//for (i = 0; i < port_count; i++) dprint("%d%c", ports[i], ".\n"[i+1==port_count]);
	if (port_count < 0)
		return 0;  // libusb error?


	for (i = 0; i < 256; i++) {
		sprintf(devname, "/dev/video%d", i);
		fd = open(devname, O_RDWR);
		if (fd == -1) {
			if (errno != ENOENT) perror(devname);
			continue;
		}
		if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
			unsigned int caps = cap.capabilities & V4L2_CAP_DEVICE_CAPS ? cap.device_caps : cap.capabilities;
			/* Linux 4.18 introduced Metadata devices, which we need to skip */
			if ((caps & (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_OUTPUT)) == 0) {
				close(fd);
				continue;
			}
			dprint("%s: %s card %s\n", __func__, devname, cap.card);
			dprint("%s:   bus %s\n", __func__, cap.bus_info);
			enumMedia(&cap); /* Get cap.card from media controller with same bus info */ 

			if (strcmp((char*)cap.card, cardname3364) == 0) {
				uint8_t p[7] = {};
				int pc;

				pc = parse_bus_info((char*)cap.bus_info, p, ARRAY_SIZE(p));
				if (pc == port_count && memcmp(ports, p, pc) == 0) {
					dev->cap3364 = cap;
					dev->strm[STRM_3364].fd = fd;
					dev->parent_serial = serial;
					dprint("found match 3364!\n");
					return 1;
				}
			}
		}
		close(fd);
	}
	return 0;
}



/* if boardnum == -1, count boards only
   otherwise, open the fd of board streams in dev
   return total number of boards
 */
static int EnumVideo(int boardnum, struct dev *dev)
{
	int i, j, streamnum = 0, fd;
	char devname[16];
	struct v4l2_capability cap;
	int nboards = 0;

	for (i = 0; i < 256; i++) {
		sprintf(devname, "/dev/video%d", i);
		fd = open(devname, O_RDWR);
		if (fd == -1) {
			if (errno != ENOENT) perror(devname);
			continue;
		}
		if (xioctl(fd, VIDIOC_QUERYCAP, &cap) != 0) {
			close(fd);
			continue;
		}

		unsigned int caps = cap.capabilities & V4L2_CAP_DEVICE_CAPS ? cap.device_caps : cap.capabilities;
		/* Linux 4.18 introduced Metadata devices, which we need to skip */
		if ((caps & (V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_VIDEO_OUTPUT)) == 0) {
			close(fd);
			continue;
		}
		dprint("%s: %s card='%s' driver=%s caps=%x\n", __func__, devname, cap.card, cap.driver, caps);
		dprint("%s:   bus %s\n", __func__, cap.bus_info);
		enumMedia(&cap); /* Get cap.card from media controller with same bus info */

		/* Linux 4.13 appends iFunction to iProduct, so only compare the start of the string */
		if (strncmp((char*)cap.card, cardname, strlen(cardname)) != 0) {
			close(fd);
			continue;
		}

		for (j = 0; j < MAX_DEVICES; j++) {
			if (device[j].cap.bus_info[0] == 0) {
				// new entry, store bus info and increment board count
				strcpy((char*)device[j].cap.bus_info, (char*)cap.bus_info);
				nboards++;
				break;
			} else if (strcmp((char*)device[j].cap.bus_info, (char*)cap.bus_info) == 0) {
				// existing entry
				break;
			}
		}
		if (j != boardnum || !dev) {
			// not a board we are interested in
			close(fd);
			continue;
		}
		if (streamnum == 0 && (caps & V4L2_CAP_VIDEO_CAPTURE)) {
			dprint("board %d preview stream on %s\n", boardnum, devname);
			dev->cap = cap;
			dev->strm[STRM_PRV].fd = fd;
			dev->strm[STRM_PRV].type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			dev->board_num = boardnum;

			dev->is_3364 = Find3364Match((char*)cap.bus_info, dev);
			dev->is_2253 = strcmp((char*)cap.driver, "s2253") == 0;
			if (dev->is_2253) {
				dev->model = 2253;
				char *sn = strstr((char*)cap.card, "SN#");
				if (sn) {
					dev->usb_serial = atoi(sn+3);
				}
			}
			streamnum++;
			continue; // don't close fd
		}
		if (streamnum == 1 && (caps & V4L2_CAP_VIDEO_CAPTURE)) {
			dprint("board %d capture stream on %s\n", boardnum, devname);
			dev->strm[STRM_CAP].fd = fd;
			dev->strm[STRM_CAP].type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			streamnum++;
			continue; // don't close fd
		}
		if (streamnum == 2 && (caps & V4L2_CAP_VIDEO_OUTPUT)) {
			dprint("board %d output stream on %s\n", boardnum, devname);
			dev->strm[STRM_OUT].fd = fd;
			dev->strm[STRM_OUT].type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
			streamnum++;
			continue; // don't close fd
		}
		close(fd);
	}
	if (dev == NULL)
		return nboards;
	if ((dev->strm[STRM_PRV].fd >= 0 && dev->strm[STRM_CAP].fd < 0) ||
	    (dev->strm[STRM_CAP].fd >= 0 && dev->strm[STRM_PRV].fd < 0) ||
	    (dev->strm[STRM_OUT].fd >= 0 && (dev->strm[STRM_PRV].fd < 0 ||
					     dev->strm[STRM_CAP].fd < 0))) {
		// both capture and preview streams must be present
		if (dev->strm[STRM_PRV].fd >= 0) {
			close(dev->strm[STRM_PRV].fd);
			dev->strm[STRM_PRV].fd = -1;
		} else {
			dprint("Missing preview stream\n");
		}
		if (dev->strm[STRM_CAP].fd >= 0) {
			close(dev->strm[STRM_CAP].fd);
			dev->strm[STRM_CAP].fd = -1;
		} else {
			dprint("Missing capture stream\n");
		}
		if (dev->strm[STRM_OUT].fd >= 0) {
			close(dev->strm[STRM_OUT].fd);
			dev->strm[STRM_OUT].fd = -1;
		} // no 'else' (not all boards have output)
	}
	return nboards;
}

#ifdef DEBUG

#include <signal.h>
#include <execinfo.h>
void printBacktrace(int sig)
{
    void *buffer[20];
    char **strings;
    int i, size;
    fprintf(stderr, "Backtrace:\n");
    fflush(stderr);
    size = backtrace(buffer, 20);
    strings = backtrace_symbols(buffer, size);
    for (i = 0; i < size; i++) {
        fprintf(stderr, "%d: %p %s\n", i, buffer[i], strings[i]);
        fflush(stderr);
    }
    free(strings);
    if (sig) exit(1);
}

static __attribute__((constructor)) void init_handler(void)
{
	signal(SIGSEGV, printBacktrace);
}
#endif

static __attribute__((destructor)) void exit_handler(void)
{
	int i, n = 0;
	for (i = 0; i < MAX_DEVICES; i++) {
		if (device[i].strm[0].fd >= 0) {
			S2263_Close(i);
			n++;
		}
	}
	if (n)
		dprint("%s: closed %d devices\n", __func__, n);
	if (libusb_initialized)
		libusb_exit(NULL);
}


/**********************************************/
/**************** API functions ***************/
/**********************************************/

MID2263_API int S2263_GetAPIVersion()
{
	return MID2263_API_VERSION;
}


MID2263_API int S2263_Open(int boardnum)
{
	struct dev *dev = &device[boardnum];
	struct stream *strm = &dev->strm[STRM_CAP];

	if (boardnum < 0 || boardnum >= MAX_DEVICES)
		return -EINVAL;
	pthread_mutex_lock(&strm->mutex);
	if (strm->fd >= 0) {
		pthread_mutex_unlock(&strm->mutex);
		return -EBUSY;
	}
	dev->board_num = boardnum;
	dev->usb_serial = -1;
	dev->strm[STRM_CAP].dev = dev;
	dev->strm[STRM_PRV].dev = dev;
	dev->strm[STRM_AUD].dev = dev;
	dev->strm[STRM_OUT].dev = dev;
	dev->strm[STRM_3364].dev = dev;
	dev->strm[STRM_AUD_CAP].dev = dev;
	dev->strm[STRM_CAP].xu_param = UVC_XU_CAPTURE_PARAMS;
	dev->strm[STRM_PRV].xu_param = UVC_XU_PREVIEW_PARAMS;
	dev->strm[STRM_AUD].xu_param = UVC_XU_AUDIO_PARAMS;
	dev->strm[STRM_OUT].xu_param = UVC_XU_DISPLAY_PARAMS;
	dev->strm[0].audio_br = 192; // default
	dev->strm[1].audio_br = 192; // default
	dev->strm[0].audio_mode = 0; // default
	dev->strm[1].audio_mode = 0; // default
	EnumVideo(boardnum, dev);

	//dprint("%s: fd=%d pd=%d od=%d\n", __func__, dev->strm[0].fd, dev->strm[1].fd, dev->strm[2].fd);

	if (strm->fd < 0) {
		pthread_mutex_unlock(&strm->mutex);
		dprint("%s: boardnum %d not found\n", __func__, boardnum);
		return -ENODEV;
	}
	pthread_mutex_unlock(&strm->mutex);

	if (dev->is_2253) {
		// skip the following 2263-specific 
		return 0;
	}

	dev->ctrls = sray_xu_controls;
	int rc = get_xu_control(UVC_XU_FIRMWARE_VERSION, &dev->fw_ver, dev);
	if (rc < 0) {
		if (rc == -EPROTO) {
			// board is alive but not communicating correctly, try resetting the PHY
			set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_USB_PHY_RESET, dev);
		}
		dprint("%s: rc=%d fw_ver=%d serial=%d\n", __func__, rc, dev->fw_ver, dev->usb_serial);
		return rc;
	}

	if (dev->fw_ver < SRAY_XU_SWAP_CAPTURE_PREVIEW_VERSION) {
		/* capture and preview interface order changed */
		int tmp = dev->strm[0].fd;
		dev->strm[0].fd = dev->strm[1].fd;
		dev->strm[1].fd = tmp;
	}
	//if (dev->fw_ver == 3205)
	//	S2263_FirmwareUpdate(boardnum); // FIXME: why is this here?


	if (dev->fw_ver >= SRAY_XU_NEW_PARAM_VERSION) {
		/* get current params for each stream */
		get_xu_control_data(dev->strm[STRM_CAP].xu_param, dev->strm[STRM_CAP].params, dev);
		get_xu_control_data(dev->strm[STRM_PRV].xu_param, dev->strm[STRM_PRV].params, dev);
		if (dev->fw_ver >= 3105) {
			get_xu_control_data(dev->strm[STRM_AUD].xu_param, dev->strm[STRM_AUD].params, dev);
			get_xu_control_data(dev->strm[STRM_OUT].xu_param, dev->strm[STRM_OUT].params, dev);
		}
	}
	if (dev->fw_ver <= SRAY_XU_OLD_VERSION) {
		/* XU controls layout changed */
		dev->ctrls = sray_xu_controls_old;
		if (dev->usb_serial == (unsigned int)-1) {
			rc = get_xu_control(UVC_XU_SERIAL_NUMBER, (int*)&dev->usb_serial, strm->dev);
		}
	} else {
		struct board_info b;
		memset(&b, 0, sizeof(b));
		rc = get_xu_control_data(UVC_XU_BOARD_INFO, (unsigned char *)&b, strm->dev);
		if (rc == 0) {
			time_t timestamp = b.timestamp;
			struct tm *tm;

			tm = localtime(&timestamp);
			dev->model = b.model;
			dprint("model is %d, serial %d, fw %d, flags %x, build date %04d %02d%02d %02d%02d\n",
					b.model, b.serial, dev->fw_ver, b.features,
					tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_sec);
			if (dev->usb_serial == (unsigned int)-1)
				dev->usb_serial = b.serial;
		}
		int i;
		for (i = 0; i < (int)ARRAY_SIZE(sray_xu_controls); i++) {
			if (sray_xu_controls[i].length == 0) continue;
			int len = get_xu_control_len(i, dev);
			if (sray_xu_controls[i].length != len) {
				dprint("Control id %d %s length %d != %d\n",
						i, sray_xu_controls[i].name,
						sray_xu_controls[i].length,
						len);
			}
		}
	}
	FindLibUSBMatch(dev->usb_serial, dev);

	if (dev->model == 0) {
		dev->model = 2263;
	}

#ifdef ENABLE_HID
	{
		static int once = 1;
		wchar_t serial[40];
		swprintf(serial, ARRAY_SIZE(serial), L"%d", dev->usb_serial);

		if (once) {
			once = 0;
			hid_init();
		}

		dev->hid_dev = hid_open(1943, 2263, serial);
		printf("hid_dev=%p serial=%ls\n", dev->hid_dev, serial);
	}
#endif

	if (open_hook)
		open_hook(boardnum);

	return 0;
}

#ifndef DISABLE_TIMEOUT_CHECKS
// This is part of the restart sequence after a USB PHY reset.
// Waits until the device disconnects and get reenumerated.
// Closes all file descriptors and then attempts to reopen.
// It should be possible to resume streaming afterward, using the restart command.
// All stream mutexes will be locked to prevent other API functions from being called.
static void reopen_device(struct dev *dev)
{
	struct v4l2_capability cap;
	struct stream *strm = &dev->strm[STRM_CAP];
	int tries = 0;
	int i;

	// lock all streams
	for (i = 0; i < MAX_STRMS; i++) {
		pthread_mutex_lock(&dev->strm[i].mutex);
	}

	while (tries < 100) {
		int ret;
		ret = ioctl(strm->fd, VIDIOC_QUERYCAP, &cap);
		if (ret < 0) {
			if (errno == ENODEV)
				break; // device disconnected
			else
				perror("V4L2_QUERYCAP");
		}
		ms_sleep(10);
		if (++tries > 100) {
			dprint("Failed to close+reopen board %d\n", dev->board_num);
			goto done;
		}
	}

	// close all streams file descriptor
	for (i = 0; i < MAX_STRMS; i++) {
		strm = &dev->strm[i];
		if (strm->fd != -1) {
			//dprint("Closing stream %d\n", i);
			close(strm->fd);
			strm->fd = -1;
			strm->restart = 1;
		}
	}
	dprint("Closed device\n");
	dprint("Reopening bus info: %s\n", dev->cap.bus_info);

	tries = 0;
	do {
		ms_sleep(100);
		if (++tries > 10) {
			dprint("Failed to reopen board %d\n", dev->board_num);
			break; // failed to reopen
		}
		EnumVideo(dev->board_num, dev);
	} while (dev->strm[STRM_CAP].fd == -1);
done:
	// unlock all stream mutex 
	for (i = 0; i < MAX_STRMS; i++) {
		strm = &dev->strm[i];
		dprint("Stream %d fd=%d\n", i, strm->fd);
		pthread_mutex_unlock(&strm->mutex);
	}
}
#endif


MID2263_API int S2263_Close(int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	struct dev *dev;
	unsigned int i;
	//dprint("Closing board %d, strm=%p\n", devid, strm);
	if (!strm)
		return -ENODEV;
	PutStrm(strm);

	S2263_StopStream(devid, STRM_CAP);
	S2263_StopStream(devid, STRM_PRV);
	S2263_StopStream(devid, STRM_OUT);
	S2263_StopAudioPreview(devid);

	dev = &device[devid];
	for (i = 0; i < MAX_STRMS; i++) {
		strm = &dev->strm[i];
		//dprint("Closing stream %d\n", i);
		pthread_mutex_lock(&strm->mutex);
		if (strm->fd != -1) {
			close(strm->fd);
			strm->fd = -1;
		}
		pthread_mutex_unlock(&strm->mutex);
	}
#ifdef ENABLE_HID
	if (dev->hid_dev) {
		hid_close(dev->hid_dev);
		dev->hid_dev = NULL;
	}
#endif
	if (dev->usb_dev) {
		libusb_close(dev->usb_dev);
		dev->usb_dev = NULL;
	}
	return 0;
}


MID2263_API int S2263_GetNumDevices(int *num_devices)
{
	if (!num_devices)
		return -EINVAL;
	*num_devices = EnumVideo(-1, NULL);
	dprint("%s: %d\n", __func__, *num_devices);
	return 0;
}

#if 0 // not supported for now
MID2263_API int S2263_SetVidSys(MID2263_VIDSYS vidsys, int devid)
{
	/* Linux UVC driver doesn't support UVC video standard control */
	return -ENOSYS;
}
MID2263_API int S2263_GetVidSys(MID2263_VIDSYS *vidsys, int devid)
{
	/* Linux UVC driver doesn't support UVC video standard control */
	return -ENOSYS;
}
#endif

MID2263_API int S2263_SetInput(MID2263_VIDINPUT input, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (strm->dev->fw_ver < SRAY_XU_VIDEO_INPUT_VERSION) { 
		// VIDIOC_S_INPUT no longer working in UVC driver???
		struct v4l2_input inp = { .index = input };
		rc = xioctl(strm->fd, VIDIOC_S_INPUT, &inp);

		// this doesn't work either
		//unsigned char data = input;
		//rc = set_unit_control_data(UVC_ID_SELECTOR_UNIT, UVC_SU_INPUT_SELECT_CONTROL, &data, 1, strm->dev, 0);
	} else {
		rc = set_xu_control(UVC_XU_VIDEO_INPUT, input, strm->dev);
	}
	if (rc < 0)
		dprint("%s: index=%d rc=%d %d\n", __func__, input, rc, strm->dev->fw_ver);
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetInput(MID2263_VIDINPUT *input, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc;
	if (!strm)
		return -ENODEV;
	if (strm->dev->fw_ver < SRAY_XU_VIDEO_INPUT_VERSION) { 
		// VIDIOC_G_INPUT no longer working in UVC driver???
		struct v4l2_input inp = { .index = 0 };
		rc = xioctl(strm->fd, VIDIOC_G_INPUT, &inp);
		if (rc < 0)
			dprint("%s: rc=%d\n", __func__, rc);
		else 
			*input = inp.index;

		// this doesn't work either
		//unsigned char data = 0;
		//int rc = get_unit_control_data(UVC_ID_SELECTOR_UNIT, UVC_SU_INPUT_SELECT_CONTROL, &data, 1, strm->dev, 0);
		//if (rc < 0)
		//	dprint("%s: rc=%d\n", __func__, rc);
		//else
		//	*input = data;
	} else {
		int data;
		rc = get_xu_control(UVC_XU_VIDEO_INPUT, &data, strm->dev);
		if (rc < 0)
			dprint("%s: rc=%d\n", __func__, rc);
		else
			*input = data;
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetNumInputs(int *inputs, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	struct v4l2_input inp = { .index = 0 };
	int rc;
	while ((rc = xioctl(strm->fd, VIDIOC_ENUMINPUT, &inp)) == 0) {
		inp.index++;
	}
	if (rc == -EINVAL) {
		*inputs = inp.index;
		rc = 0;
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_SetLevel(MID2263_PARAM param, int value, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	struct v4l2_control ctrl = { .id = 0};
	int rc = 0;
	if (!strm)
		return -ENODEV;
	switch (param) {
	case MID2263_BRIGHTNESS: ctrl.id = V4L2_CID_BRIGHTNESS; break;
	case MID2263_CONTRAST: ctrl.id = V4L2_CID_CONTRAST; break;
	case MID2263_SATURATION: ctrl.id = V4L2_CID_SATURATION; break;
	case MID2263_HUE: ctrl.id = V4L2_CID_HUE; break;
	case MID2263_COLOR_KILLER:
		PutStrm(strm);
		dprint("colorkiller=%d devid=%d\n", value, devid);
		rc = set_xu(UVC_XU_COMMAND, value ?
			UVC_XU_COMMAND_COLORKILLER_ENABLE :
			UVC_XU_COMMAND_COLORKILLER_DISABLE, devid);
		return rc;
	default:
		PutStrm(strm);
		return -EINVAL;
	}
	ctrl.value = value;
	dprint("%s: ctrl %d = %d\n", __func__, ctrl.id, ctrl.value);
	rc = xioctl(strm->fd, VIDIOC_S_CTRL, &ctrl);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetLevel(MID2263_PARAM param, int *value, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	struct v4l2_control ctrl = { .id = 0 };
	int rc = 0;
	if (!strm)
		return -ENODEV;
	switch (param) {
	case MID2263_BRIGHTNESS: ctrl.id = V4L2_CID_BRIGHTNESS; break;
	case MID2263_CONTRAST: ctrl.id = V4L2_CID_CONTRAST; break;
	case MID2263_SATURATION: ctrl.id = V4L2_CID_SATURATION; break;
	case MID2263_HUE: ctrl.id = V4L2_CID_HUE; break;
	default:
		PutStrm(strm);
		return -EINVAL;
	}
	rc = xioctl(strm->fd, VIDIOC_G_CTRL, &ctrl);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);
	else if (value)
		*value = ctrl.value;
	PutStrm(strm);
	return rc;
}



MID2263_API int S2263_SetImageSize(int width, int height, int devid, int strmidx)
{
	struct stream *strm = _GetStrm(devid, strmidx);
	struct v4l2_format fmt = { 0 };
	int rc = 0;
	if (!strm)
		return -ENODEV;
	fmt.type = strm->type;
	dprint("%s: width=%d height=%d devid=%d strmidx=%d\n", __func__,
		width, height, devid, strmidx);
	if (width == -1 && height == -1) {
		PutStrm(strm);
		rc = S2263_GetVideoInfo(&width, &height, NULL, NULL, devid);
		dprint("%s: rc=%d width=%d height=%d\n", __func__, rc, width, height);
		_GetStrm(devid, strmidx);
		if (rc < 0) {
			return rc;
		}
		if (strmidx == STRM_PRV) {
			height = (width * 9 / height >= 16) ? 360 : 480; // 16:9 or 4:3
			width = 640;
		}
	}
	fmt.fmt.pix.width = width;
	fmt.fmt.pix.height = height;
	// note: using V4L2_PIX_FMT_JPEG turns into V4L2_PIX_FMT_UYVY by uvc_v4l2.c:uvc_v4l2_try_format()
	fmt.fmt.pix.pixelformat = strmidx != STRM_PRV ? V4L2_PIX_FMT_MJPEG : V4L2_PIX_FMT_UYVY;
	rc = xioctl(strm->fd, VIDIOC_S_FMT, &fmt);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);

	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetImageSize(int *width, int *height, int devid, int strmidx)
{
	struct stream *strm = _GetStrm(devid, strmidx);
	struct v4l2_format fmt = { 0 };
	int rc = 0;
	if (!strm)
		return -ENODEV;
	fmt.type = strm->type;
	rc = xioctl(strm->fd, VIDIOC_G_FMT, &fmt);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);
	if (rc == 0 && width)
		*width = fmt.fmt.pix.width;
	if (rc == 0 && height)
		*height = fmt.fmt.pix.height;
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_SetImageFormat(MID2263_FORMAT format, int width, int height, int devid, int strmidx)
{
	struct stream *strm = _GetStrm(devid, strmidx);
	struct v4l2_format fmt = { 0 };
	int rc = 0;
	if (!strm)
		return -ENODEV;
	switch (format) {
	case MID2263_FORMAT_UYVY: fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY; break;
	case MID2263_FORMAT_NV12: fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12; break;
	default: PutStrm(strm); return -EINVAL;
	}

	fmt.type = strm->type;
	dprint("%s: format=%d width=%d height=%d devid=%d strmidx=%d\n", __func__,
		format, width, height, devid, strmidx);
	if (width == -1 && height == -1) {
		PutStrm(strm);
		rc = S2263_GetVideoInfo(&width, &height, NULL, NULL, devid);
		dprint("%s: rc=%d width=%d height=%d\n", __func__, rc, width, height);
		_GetStrm(devid, strmidx);
		if (rc < 0) {
			return rc;
		}
		if (strmidx == STRM_PRV) {
			height = (width * 9 / height >= 16) ? 360 : 480; // 16:9 or 4:3
			width = 640;
		}
	}
	fmt.fmt.pix.width = width;
	fmt.fmt.pix.height = height;
	dprint("%s: S_FMT rc=%d %dx%d %c%c%c%c\n", __func__, rc,
				fmt.fmt.pix.width, fmt.fmt.pix.height, 
				(fmt.fmt.pix.pixelformat >> 0)&0xff,
				(fmt.fmt.pix.pixelformat >> 8)&0xff,
				(fmt.fmt.pix.pixelformat >> 16)&0xff,
				(fmt.fmt.pix.pixelformat >> 24)&0xff);

	rc = xioctl(strm->fd, VIDIOC_S_FMT, &fmt);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);
	rc = xioctl(strm->fd, VIDIOC_G_FMT, &fmt);
	dprint("%s: G_FMT rc=%d %dx%d %c%c%c%c\n", __func__, rc,
				fmt.fmt.pix.width, fmt.fmt.pix.height, 
				(fmt.fmt.pix.pixelformat >> 0)&0xff,
				(fmt.fmt.pix.pixelformat >> 8)&0xff,
				(fmt.fmt.pix.pixelformat >> 16)&0xff,
				(fmt.fmt.pix.pixelformat >> 24)&0xff);

	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetImageFormat(MID2263_FORMAT *format, int *width, int *height, int devid, int strmidx)
{
	struct stream *strm = _GetStrm(devid, strmidx);
	struct v4l2_format fmt = { 0 };
	int rc = 0;
	if (!strm)
		return -ENODEV;
	fmt.type = strm->type;
	rc = xioctl(strm->fd, VIDIOC_G_FMT, &fmt);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);
	if (rc == 0 && width)
		*width = fmt.fmt.pix.width;
	if (rc == 0 && height)
		*height = fmt.fmt.pix.height;
	if (rc == 0 && format)
		*format = fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12 ? MID2263_FORMAT_NV12 : MID2263_FORMAT_UYVY;

	PutStrm(strm);
	return rc;
}

#define unused __attribute__((unused))

static int
set_param(struct stream *strm, int param, int index, int mask, int value)
{
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (strm->dev->fw_ver < SRAY_XU_NEW_PARAM_VERSION) {
		if (param && (strm != &strm->dev->strm[1] || param == UVC_XU_PREVIEW_INTERLACED)) {
			rc = set_xu_control(param, value, strm->dev);
		} else {
			rc = -EPROTO;
		}
	} else {
		value <<= __builtin_ctz(mask);
		strm->params[index] = (strm->params[index] & ~(mask & 0xff)) | (value & mask & 0xff);
		if (mask >= 0x100)
			strm->params[index+1] = (strm->params[index+1] & ~(mask >> 8)) | ((value & mask) >> 8);
		rc = set_xu_control_data(strm->xu_param, strm->params, strm->dev);
	}
	PutStrm(strm);
	return rc;

}

static int
get_param(struct stream *strm, int param, int index, int mask, int *value)
{
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (value == NULL) {
		rc = -EINVAL;
	} else if (strm->dev->fw_ver < SRAY_XU_NEW_PARAM_VERSION) {
		if (param && (strm != &strm->dev->strm[1] || param == UVC_XU_PREVIEW_INTERLACED)) {
			rc = get_xu_control(param, value, strm->dev);
		} else {
			rc = -EPROTO;
		}
	} else {
		//dprint("param[%d] = %x & 0x%x\n", index, strm->params[index], mask);
		*value = strm->params[index] & mask;
		if (mask >= 0x100)
			*value |= (strm->params[index+1] << 8) & mask;
		*value >>= __builtin_ctz(mask);
	}
	PutStrm(strm);
	return rc;

}

MID2263_API int S2263_SetRecordMode(MID2263_RECMODE recmode, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	unsigned recmode_last = MID2263_RECMODE_AVI_NOAUDIO;
	int is_2273 = strm->dev->model == 2273;
	int use_h265 = 0;
	if (!strm)
		return -ENODEV;
	if (is_2273)
		recmode_last = MID2263_RECMODE_H265;
	if ((unsigned)recmode > recmode_last) {
		dprint("%s: invalid recmode for recording: %d\n", __func__, recmode);
		PutStrm(strm);
		return -EINVAL;
	}
	strm->enable_avi_vonly = 0;
	strm->enable_avi_av = 0;
	strm->enable_mp4 = 0;
	if (recmode == MID2263_RECMODE_MP4) {
		strm->enable_mp4 = 1;
		recmode = MID2263_RECMODE_TS;
	} else if (recmode == MID2263_RECMODE_MP4_NOAUDIO) {
		strm->enable_mp4 = 1;
		recmode = MID2263_RECMODE_TS_NOAUDIO;
	} else if (recmode == MID2263_RECMODE_AVI_NOAUDIO) {
		strm->enable_mp4 = 0;
		strm->enable_avi_vonly = 1;
		recmode = MID2263_RECMODE_TS_NOAUDIO;
	} else if (recmode == MID2263_RECMODE_AVI) {
		strm->enable_mp4 = 0;
		strm->enable_avi_av = 1;
		// AVI uses PCM audio from audio preview channel for
		// older firmware versions only.
		if (strm->dev->fw_ver < SRAY_FIRMWARE_TS_PCM) {
			dprint("uses preview for PCM\n");
			recmode = MID2263_RECMODE_TS_NOAUDIO;
			strm->avi_av_uses_preview = 1;
		} else {
			dprint("uses TS PCM stream\n");
			recmode = MID2263_RECMODE_TS;
			strm->avi_av_uses_preview = 0;
		}
	} else if (recmode == MID2263_RECMODE_H265) {
		recmode = MID2263_RECMODE_H264;
		use_h265 = 1;
	}
	if (is_2273) {
		int cur_h265 = 0;
		int rc = get_param(strm, UVC_XU_CAPTURE_MODE, 3, 0x40, &cur_h265);
		if (rc == 0 && use_h265 != cur_h265) {
			set_param(strm, UVC_XU_CAPTURE_MODE, 3, 0x40, use_h265);
		}
	} else if (use_h265) {
		dprint("%s: invalid recmode (requires 2273): %d\n", __func__, recmode);
		PutStrm(strm);
		return -EINVAL;
	}

	dprint("%s: strmidx=%d recmode=%d enable_mp4=%d\n", __func__, strmidx, recmode, strm->enable_mp4);
	return set_param(strm, UVC_XU_CAPTURE_MODE, 5, 0x0f, recmode);
}

MID2263_API int S2263_GetRecordMode(MID2263_RECMODE *recmode, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	int rc = 0;
	if (!strm)
		return -ENODEV;

	rc = get_param(strm, UVC_XU_CAPTURE_MODE, 5, 0x0f, (int*)recmode);
	if (rc == 0) {
		dprint("%s: strmidx=%d recmode=%d enable_mp4=%d\n",
			__func__, strmidx, *recmode, strm->enable_mp4);
		if (strm->enable_mp4) {
			if (*recmode == MID2263_RECMODE_TS_NOAUDIO)
				*recmode = MID2263_RECMODE_MP4_NOAUDIO;
			else
				*recmode = MID2263_RECMODE_MP4;
		} else if (*recmode == 5) {
			*recmode = MID2263_RECMODE_TS;
		} else if (strm->enable_avi_vonly) {
			if (*recmode == MID2263_RECMODE_TS)
				*recmode = MID2263_RECMODE_AVI_NOAUDIO;
		} else if (strm->enable_avi_av) {
			if (*recmode == MID2263_RECMODE_TS)
				*recmode = MID2263_RECMODE_AVI;
		}
	}
	return rc;
}

MID2263_API int S2263_SetH264Level(MID2263_H264_LEVEL level, int devid, int strmidx)
{
	switch (level) {
	case MID2263_H264_1_0: level = 0; break;
	case MID2263_H264_1B: level = 1; break;
	case MID2263_H264_1_1: level = 2; break;
	case MID2263_H264_1_2: level = 3; break;
	case MID2263_H264_1_3: level = 4; break;
	case MID2263_H264_2_0: level = 5; break;
	case MID2263_H264_2_1: level = 6; break;
	case MID2263_H264_2_2: level = 7; break;
	case MID2263_H264_3_0: level = 8; break;
	case MID2263_H264_3_1: level = 9; break;
	case MID2263_H264_3_2: level = 10; break;
	case MID2263_H264_4_0: level = 11; break;
	case MID2263_H264_4_1: level = 12; break;
	case MID2263_H264_4_2: level = 13; break;
	case MID2263_H264_5_0: level = 14; break;
	default: return -EINVAL;
	}
	return set_param(GetStrm(devid, strmidx), UVC_XU_H264_LEVEL, 4, 0xf, level);
}

MID2263_API int S2263_GetH264Level(MID2263_H264_LEVEL *level, int devid, int strmidx)
{
	static const MID2263_H264_LEVEL levels[] = {10, 9, 11, 12, 13, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50};
	int rc = get_param(GetStrm(devid, strmidx), UVC_XU_H264_LEVEL, 4, 0xf, (int*)level);
	if (rc >= 0) *level = levels[*level];
	return rc;
}

MID2263_API int S2263_SetH264Profile(MID2263_H264_PROFILE profile, int devid, int strmidx)
{
	switch (profile) {
	case MID2263_H264_BASELINE: profile = 0; break;
	case MID2263_H264_MAIN: profile = 1; break;
	case MID2263_H264_HIGH: profile = 3; break;
	default: return -EINVAL;
	}
	return set_param(GetStrm(devid, strmidx), UVC_XU_H264_PROFILE, 4, 0x30, profile);
}


MID2263_API int S2263_GetH264Profile(MID2263_H264_PROFILE *profile, int devid, int strmidx)
{
	static const MID2263_H264_PROFILE profiles[] = {MID2263_H264_BASELINE, MID2263_H264_MAIN, 88, MID2263_H264_HIGH};
	int rc = get_param(GetStrm(devid, strmidx), UVC_XU_H264_PROFILE, 4, 0x30, (int*)profile);
	if (rc >= 0) *profile = profiles[*profile];
	return rc;
}

MID2263_API int S2263_SetBitrate(int bitrate_kbps, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	if (!strm)
		return -ENODEV;
	if (bitrate_kbps < 100 ||
	    bitrate_kbps > (strm->dev->model == 2273 ? 60000 : 20000))
		return -EINVAL;
	return set_param(strm, UVC_XU_BITRATE, 0, 0xffff, bitrate_kbps);
}

MID2263_API int S2263_GetBitrate(int *bitrate_kbps, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), UVC_XU_BITRATE, 0, 0xffff, bitrate_kbps);
}

MID2263_API int S2263_SetGOPSize(int gopsize, int devid, int strmidx)
{
	if (gopsize < 1 || gopsize > 255)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), UVC_XU_GOPSIZE, 2, 0xff, gopsize);
}

MID2263_API int S2263_GetGOPSize(int *gopsize, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), UVC_XU_GOPSIZE, 2, 0xff, gopsize);
}

MID2263_API int S2263_SetRateMode(MID2263_RATEMODE ratemode, int devid, int strmidx)
{
	if ((unsigned)ratemode > MID2263_RATEMODE_FIXEDQP)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), UVC_XU_RATEMODE, 4, 0xc0, ratemode);
}

MID2263_API int S2263_GetRateMode(MID2263_RATEMODE *ratemode, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), UVC_XU_RATEMODE, 4, 0xc0, (int*)ratemode);
}

MID2263_API int S2263_SetJPEGQuality(int quality, int devid, int strmidx)
{
	if (quality < 2 || quality > 97)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), UVC_XU_JPEG_QUALITY, 9, 0x7f, quality);
}

MID2263_API int S2263_GetJPEGQuality(int *quality, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), UVC_XU_JPEG_QUALITY, 9, 0x7f, quality);
}

MID2263_API int S2263_SetInterlaceMode(MID2263_INTERLACE_MODE mode, int devid, int strmidx)
{
	if ((unsigned)mode > MID2263_FIELD_INTERLACED_BT)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), UVC_XU_CAPTURE_INTERLACED + strmidx, 5, 0xf0, mode);
}

MID2263_API int S2263_GetInterlaceMode(MID2263_INTERLACE_MODE *mode, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), UVC_XU_CAPTURE_INTERLACED + strmidx, 5, 0xf0, (int*)mode);
}


MID2263_API int S2263_SetFrameSkip(int skip, int devid, int strmidx)
{
	if ((unsigned)skip > 31)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), 0, 6, 0x1f, skip);
}

MID2263_API int S2263_GetFrameSkip(int *skip, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), 0, 6, 0x1f, skip);
}

MID2263_API int S2263_SetFrameCount(int count, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc;
	if (strmidx < 0 || strmidx > 1) 
		rc = -EINVAL;
	else if (strm->dev->fw_ver < SRAY_XU_FRAMECOUNT_VERSION)
		rc = -EPROTO; // Framecount control wasn't added yet
	else {
		int id = UVC_XU_CAPTURE_FC + strmidx;
		rc = set_xu_control(id, count, strm->dev);
		if (rc < 0)
			dprint("%s: id=%d rc=%d %s\n", __func__, id, rc, strerror(errno));
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetFrameCount(int *count, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc;
	if (strmidx < 0 || strmidx > 1) 
		rc = -EINVAL;
	else if (strm->dev->fw_ver < SRAY_XU_FRAMECOUNT_VERSION)
		rc = -EPROTO; // Framecount control wasn't added yet
	else {
		int id = UVC_XU_CAPTURE_FC + strmidx;
		rc = get_xu_control(id, count, strm->dev);
		if (rc < 0)
			dprint("%s: id=%d rc=%d %s\n", __func__, id, rc, strerror(errno));
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_SetSliceMode(int slicemode, int devid, int strmidx)
{
	if ((unsigned)slicemode > 7)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), 0, 6, 0xe0, slicemode);
}

MID2263_API int S2263_GetSliceMode(int *slicemode, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), 0, 6, 0xe0, slicemode);
}

MID2263_API int S2263_SetFlip(int flip, int devid, int strmidx)
{
	if ((unsigned)flip > MID2263_FLIP_MAX)
		return -EINVAL;
	set_param(GetStrm(devid, strmidx), 0, 9, 0x80, flip >> 2);
	return set_param(GetStrm(devid, strmidx), 0, 7, 0xc0, flip);
}

MID2263_API int S2263_GetFlip(int *flip, int devid, int strmidx)
{
	int rotate;
	int rc = get_param(GetStrm(devid, strmidx), 0, 7, 0xc0, flip);
	get_param(GetStrm(devid, strmidx), 0, 9, 0x80, &rotate);
	if (flip && rotate) *flip |= MID2263_ROTATE;
	return rc;
}

MID2263_API int S2263_SetCrop(int left, int top, int width, int height, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	__u8 data[8] = {
		left & 0xff,
		left >> 8,
		top & 0xff,
		top >> 8,
		width & 0xff,
		width >> 8,
		height & 0xff,
		height >> 8,
	};
	int rc = -EINVAL;

	if (!strm) return -ENODEV;

	if (strm->dev->fw_ver < SRAY_XU_CROP_VERSION) {
		dprint("%s: requires fw %d or later\n", __func__, SRAY_XU_CROP_VERSION);
		rc = -EPROTO;
	} else if (strmidx == MID2263_CAPTURE) {
		rc = set_xu_control_data(UVC_XU_CAPTURE_CROP, data, strm->dev);
	} else if (strmidx == MID2263_PREVIEW) {
		rc = set_xu_control_data(UVC_XU_PREVIEW_CROP, data, strm->dev);
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetCrop(int *left, int *top, int *width, int *height, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	__u8 data[8];

	int rc = -EINVAL;

	memset(data, 0, 8);
	if (!strm) return -ENODEV;

	if (strm->dev->fw_ver < SRAY_XU_CROP_VERSION) {
		dprint("%s: requires fw %d or later\n", __func__, SRAY_XU_CROP_VERSION);
		rc = -EPROTO;
	} else if (strmidx == MID2263_CAPTURE) {
		rc = get_xu_control_data(UVC_XU_CAPTURE_CROP, data, strm->dev);
	} else if (strmidx == MID2263_PREVIEW) {
		rc = get_xu_control_data(UVC_XU_PREVIEW_CROP, data, strm->dev);
	}
	if (rc == 0) {
		if (left)
			*left = data[0] | (data[1] << 8);
		if (top)
			*top = data[2] | (data[3] << 8);
		if (width)
			*width = data[4] | (data[5] << 8);
		if (height)
			*height = data[6] | (data[7] << 8);
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_SetPrivacyMask(int left, int top, int width, int height, int devid)
{
	struct stream *strm = GetStrm(devid, STRM_CAP);
	__u8 data[8] = {
		left & 0xff,
		left >> 8,
		top & 0xff,
		top >> 8,
		width & 0xff,
		width >> 8,
		height & 0xff,
		height >> 8,
	};
	int rc = -EINVAL;

	if (!strm) return -ENODEV;

	if (strm->dev->fw_ver < SRAY_XU_CROP_VERSION) {
		dprint("%s: requires fw %d or later\n", __func__, SRAY_XU_CROP_VERSION);
		rc = -EPROTO;
	} else {
		rc = set_xu_control_data(UVC_XU_PRIVACY_MASK, data, strm->dev);
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetPrivacyMask(int *left, int *top, int *width, int *height, int devid)
{
	struct stream *strm = GetStrm(devid, STRM_CAP);
	__u8 data[8];

	int rc = -EINVAL;

	if (!strm) return -ENODEV;
	memset(data, 0, 8);
	if (strm->dev->fw_ver < SRAY_XU_CROP_VERSION) {
		dprint("%s: requires fw %d or later\n", __func__, SRAY_XU_CROP_VERSION);
		rc = -EPROTO;
	} else {
		rc = get_xu_control_data(UVC_XU_PRIVACY_MASK, data, strm->dev);
	}
	if (rc == 0) {
		if (left)
			*left = data[0] | (data[1] << 8);
		if (top)
			*top = data[2] | (data[3] << 8);
		if (width)
			*width = data[4] | (data[5] << 8);
		if (height)
			*height = data[6] | (data[7] << 8);
	}
	PutStrm(strm);
	return rc;
}



MID2263_API int S2263_SetFixedQP(int intraframeqp, int interpframeqp, int devid, int strmidx)
{
	int rc;
	if ((unsigned int)intraframeqp > MID2263_FIXED_QP_MAX || 
	    (unsigned int)interpframeqp > MID2263_FIXED_QP_MAX)
		return -EINVAL;
	rc = set_param(GetStrm(devid, strmidx), 0, 7, 0x3f, intraframeqp);
	if (rc < 0) return rc;
	return set_param(GetStrm(devid, strmidx), 0, 8, 0x3f, interpframeqp);
}

MID2263_API int S2263_GetFixedQP(int *intraframeqp, int *interpframeqp, int devid, int strmidx)
{
	int rc = get_param(GetStrm(devid, strmidx), 0, 7, 0x3f, intraframeqp);
	if (rc < 0) return rc;
	return get_param(GetStrm(devid, strmidx), 0, 8, 0x3f, interpframeqp);
}

MID2263_API int S2263_SetAspectRatio(MID2263_ASPECT aspect, int devid, int strmidx)
{
	if ((unsigned)aspect > 2)
		return -EINVAL;
	return set_param(GetStrm(devid, strmidx), 0, 8, 0xc0, aspect);
}

MID2263_API int S2263_GetAspectRatio(MID2263_ASPECT *aspect, int devid, int strmidx)
{
	return get_param(GetStrm(devid, strmidx), 0, 8, 0xc0, (int*)aspect);
}


MID2263_API int S2263_SetOutput(MID2263_OUTPUT output, int devid)
{
	if (output != MID2263_OUTPUT_CVIDEO && output != MID2263_OUTPUT_DVI)
		return -EINVAL;
	return set_param(GetOutputStrm(devid), 0, 2, 0xff, output);
}

MID2263_API int S2263_GetOutput(MID2263_OUTPUT *output, int devid)
{
	return get_param(GetOutputStrm(devid), 0, 2, 0xff, (int*)output);
}

MID2263_API int S2263_SetOutputStd(MID2263_OUTPUT_STD std, int devid)
{
	if (std < MID2263_OUTPUT_AUTO || std > MID2263_OUTPUT_1080P24)
		return -EINVAL;
	return set_param(GetOutputStrm(devid), 0, 1, 0xff, std);
}

MID2263_API int S2263_GetOutputStd(MID2263_OUTPUT_STD *std, int devid)
{
	return get_param(GetOutputStrm(devid), 0, 1, 0xff, (int*)std);
}

MID2263_API int S2263_SetOutputMode(MID2263_OUTPUT_MODE mode, int devid)
{
	if (mode != MID2263_OUTPUT_IDLE &&
	    mode != MID2263_OUTPUT_PASSTHRU &&
	    mode != MID2263_OUTPUT_PASSTHRU_SD &&
	    mode != MID2263_OUTPUT_COLORBARS)
		return -EINVAL;
	return set_param(GetOutputStrm(devid), 0, 0, 0xff, mode);
}

MID2263_API int S2263_GetOutputMode(MID2263_OUTPUT_MODE *mode, int devid)
{
	return get_param(GetOutputStrm(devid), 0, 0, 0xff, (int*)mode);
}




MID2263_API int S2263_SetAudioBitrate(MID2263_AUDIO_BITRATE bitrate_kbps, int devid)
{
	struct stream *strm;

	// save the audio bitrate in the stream structure for both streams.
	strm = GetStrm(devid, 0);
	if (strm != NULL) {
		strm->audio_br = bitrate_kbps;
		PutStrm(strm);
	}
	strm = GetStrm(devid, 1);
	if (strm != NULL) {
		strm->audio_br = bitrate_kbps;
		PutStrm(strm);
	}
	if (bitrate_kbps != MID2263_AUDBR_96 &&
	    bitrate_kbps != MID2263_AUDBR_128 &&
	    bitrate_kbps != MID2263_AUDBR_192 &&
	    bitrate_kbps != MID2263_AUDBR_224 &&
	    bitrate_kbps != MID2263_AUDBR_256)
		return -EINVAL;
	//return set_param(GetAudioStrm(devid), UVC_XU_AUDIO_BITRATE, 
	return set_xu(UVC_XU_AUDIO_BITRATE, bitrate_kbps, devid);
}

MID2263_API int S2263_GetAudioBitrate(MID2263_AUDIO_BITRATE *bitrate_kbps, int devid)
{
	return get_xu(UVC_XU_AUDIO_BITRATE, (int*)bitrate_kbps, devid);
}

MID2263_API int S2263_SetAudioMode(MID2263_AUDMODE mode, int devid)
{
	struct stream *strm;

	// save the audio mode in the stream structure for both streams.
	strm = GetStrm(devid, 0);
	if (strm != NULL) {
		strm->audio_mode = mode;
		PutStrm(strm);
	}
	strm = GetStrm(devid, 1);
	if (strm != NULL) {
		strm->audio_mode = mode;
		PutStrm(strm);
	}
	return set_xu(UVC_XU_AUDIO_ENCODING, mode, devid);
}

MID2263_API int S2263_GetAudioMode(MID2263_AUDMODE *mode, int devid)
{
	return get_xu(UVC_XU_AUDIO_ENCODING, (int*)mode, devid);
}

MID2263_API int S2263_SetAudioInput(MID2263_AUDINPUT input, int devid)
{
	return set_xu(UVC_XU_AUDIO_INPUT, input, devid);
}

MID2263_API int S2263_GetAudioInput(MID2263_AUDINPUT *input, int devid)
{
	return get_xu(UVC_XU_AUDIO_INPUT, (int *) input, devid);
}

MID2263_API int S2263_SetAudioGain(int agc, int gain, int devid)
{
	if (agc < 0 || agc > 1 || gain < 0 || gain > MAX_GAIN)
		return -EINVAL;
	return set_xu(UVC_XU_AUDIO_VOLUME, (agc ? 0x8080 : 0) | (gain << 8) | gain, devid);

}

MID2263_API int S2263_GetAudioGain(int *agc, int *gain, int devid)
{
	int rc, val;
	rc = get_xu(UVC_XU_AUDIO_VOLUME, &val, devid);
	if (rc >= 0) {
		if (agc) *agc = (val >> 7) & 1;
		if (gain) *gain = val & 0x7f;
	}
	return rc;
}



MID2263_API int S2263_GetSerialNumber(int *serial_number, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (!serial_number) {
		dprint("%s: serial_number is NULL\n", __func__);
		rc = -EINVAL;
	} else {
		*serial_number = strm->dev->usb_serial;
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetFirmwareVersion(int *firmware_version, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (!firmware_version) {
		dprint("%s: firmware_version is NULL\n", __func__);
		rc = -EINVAL;
	} else {
		*firmware_version = strm->dev->fw_ver;
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetModelNumber(int *model, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (!model) {
		dprint("%s: model is NULL\n", __func__);
		rc = -EINVAL;
	} else {
		*model = strm->dev->model;
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetFeatureFlags(int *flags, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (!flags) {
		dprint("%s: flags is NULL\n", __func__);
		rc = -EINVAL;
	} else if (strm->dev->fw_ver <= SRAY_XU_OLD_VERSION) {
		/* XU controls layout changed */
		rc = get_xu_control(UVC_XU_FEATURE_FLAGS, (int*)flags, strm->dev);
	} else {
		struct board_info b;
		rc = get_xu_control_data(UVC_XU_BOARD_INFO, (unsigned char *)&b, strm->dev);
		if (rc == 0) {
			*flags = b.features;
		}
	}
	PutStrm(strm);
	return rc;
}


MID2263_API int S2263_FirmwareUpdate(int devid)
{
	dprint("%s\n", __func__);
	return set_xu(UVC_XU_COMMAND, UVC_XU_COMMAND_FW_UPLOAD, devid);
}

MID2263_API int S2263_SetTemperatureRobustness(int devid)
{
	dprint("%s\n", __func__);
	return set_xu(UVC_XU_COMMAND, UVC_XU_COMMAND_PERIODIC_USB_PHY_RESET, devid);
}

MID2263_API int S2263_ReadTempSensor(float *temp_C, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (!temp_C) {
		dprint("%s: temp_C is NULL\n", __func__);
		rc = -EINVAL;
	} else if (strm->dev->fw_ver < SRAY_XU_TEMP_SENSOR_VERSION) {
		dprint("%s: firmware version %d required\n", __func__,
				SRAY_XU_TEMP_SENSOR_VERSION);
		rc = -EPROTO;
	} else {
		__u8 data[2];
		rc = get_xu_control_data(UVC_XU_TEMP_SENSOR, data, strm->dev);
		if (rc >= 0) {
			*temp_C = (float)(data[0] | (data[1] << 8)) / 16.0;
		}
	}
	PutStrm(strm);
	return rc;
}


//MID2263_API int S2263_SetStreamWindow(int xid, int devid);
//MID2263_API int S2263_SetStreamWindowPosition(int x, int y, int devid);


MID2263_API int S2263_SetDebug(int param, int value, int devid)
{
	return set_xu(param, value, devid);
}

MID2263_API int S2263_GetDebug(int param, int *value, int devid)
{
	return get_xu(param, value, devid);
}


MID2263_API int S2263_SetDebugData(int id, void *data, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = set_xu_control_data(id, data, strm->dev);
	if (rc < 0)
		dprint("%s: id=%d[%s] rc=%d %s\n", __func__, 
			id, ctrl_name(id, strm->dev), rc, strerror(errno));
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetDebugData(int id, void *data, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = get_xu_control_data(id, data, strm->dev);
	if (rc < 0)
		dprint("%s: id=%d rc=%d %s\n", __func__, id, rc, strerror(errno));
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetDebugLen(int id, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = get_xu_control_len(id, strm->dev);
	if (rc < 0)
		dprint("%s: id=%d rc=%d %s\n", __func__, id, rc, strerror(errno));
	PutStrm(strm);
	return rc;
}

// must be called with stream locked
static int GetVideoInfo(int *width, int *height, int *framerate, int *interlaced, int *locked, struct dev *dev)
{
	int rc = 0;
	if (dev->fw_ver <= SRAY_XU_OLD_VERSION) {
		if (width) {
			rc = get_xu_control(UVC_XU_VIDDEC_WIDTH, width, dev);
			if (rc < 0)
				dprint("%s: width rc=%d %s\n", __func__, rc, strerror(errno));
		}
		if (height) {
			rc = get_xu_control(UVC_XU_VIDDEC_HEIGHT, height, dev);
			if (rc < 0)
				dprint("%s: height rc=%d %s\n", __func__, rc, strerror(errno));
		}
		if (framerate) {
			rc = get_xu_control(UVC_XU_VIDDEC_FRAMERATE, framerate, dev);
			if (rc < 0)
				dprint("%s: framerate rc=%d %s\n", __func__, rc, strerror(errno));
		}
		if (interlaced) {
			rc = get_xu_control(UVC_XU_VIDDEC_INTERLACED, interlaced, dev);
			if (rc < 0)
				dprint("%s: interlaced rc=%d %s\n", __func__, rc, strerror(errno));
		}
		if (locked) {
			rc = get_xu_control(UVC_XU_VIDEO_LOCK, locked, dev);
			if (rc < 0)
				dprint("%s: locked rc=%d %s\n", __func__, rc, strerror(errno));
		}
	} else {
		struct viddec_info info;
		rc = get_xu_control_data(UVC_XU_VIDDEC_INFO, (unsigned char *)&info, dev);
		if (rc < 0) {
			dprint("%s: viddec info rc=%d %s\n", __func__, rc, strerror(errno));
		} else {
			if (width) *width = info.width;
			if (height) *height = info.height;
			if (framerate) *framerate = info.framerate;
			if (interlaced) *interlaced = info.interlaced;
			if (locked) *locked = info.lock & 1;
		}
		if (0) { // logging
			static int count = 0;
			dprint("%s: %d %dx%d%c%d\n", __func__, count++,
				info.width, info.height,
				"pi"[info.interlaced],
				info.framerate);
		}
	}
	return rc;
}

MID2263_API int S2263_GetVideoInfo(int *width, int *height, int *framerate, int *interlaced, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = GetVideoInfo(width, height, framerate, interlaced, NULL, strm->dev);
	if (rc < 0)
		dprint("%s: rc=%d\n", __func__, rc);

	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetVideoLock(int *locked, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;

	if (strm->dev->fw_ver <= SRAY_XU_OLD_VERSION) {
		if (locked) {
			rc = get_xu_control(UVC_XU_VIDEO_LOCK, locked, strm->dev);
			if (rc < 0)
				dprint("%s: video lock rc=%d %s\n", __func__, rc, strerror(errno));
		}
	} else if (!locked) {
		rc = -EFAULT;
	} else {
		struct viddec_info info;
		rc = get_xu_control_data(UVC_XU_VIDDEC_INFO, (unsigned char*)&info, strm->dev);
		if (rc < 0) {
			dprint("%s: viddec info rc=%d %s\n", __func__, rc, strerror(errno));
		} else {
			*locked = info.lock & 1;
		}
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetAnalogHDStandard(MID2263_AHD_STANDARD *standard, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;

	/* version at which analog hd standard was added */
	if (strm->dev->fw_ver < SRAY_XU_AHD_STD_VERSION) {
		rc = -EPROTO;
	} else if (!standard) {
		rc = -EFAULT;
	} else {
		struct viddec_info info;
		rc = get_xu_control_data(UVC_XU_VIDDEC_INFO, (unsigned char*)&info, strm->dev);
		if (rc < 0) {
			dprint("%s: viddec info rc=%d %s\n", __func__, rc, strerror(errno));
		} else {
			*standard = (info.lock >> 1) & 3;
		}
	}
	PutStrm(strm);
	return rc;
}

static int validate_ahd_std(int width, int height, int framerate, int standard)
{
	if (standard > MID2263_CVI)
		return 0;
	if (width == 0 && height == 0) // auto
		return 1;
	if (standard == MID2263_CVBS) {
		if (width == 720 && height == 480)
			return 1;
		if (width == 720 && height == 576)
			return 1;
		return 0;
	}
	if (width == 1280 && height == 720) {
		return 	framerate == 25000 ||
			framerate == 30000 ||
			framerate == 50000 ||
			framerate == 60000;
	} else if (width == 1920 && height == 1080) {
		return  framerate == 25000 ||
			framerate == 30000;
	}
	return 0;
}

MID2263_API int S2263_SetAnalogHDStandard(MID2263_AHD_STANDARD standard,
		int width, int height, int framerate, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;

	/* version at which analog hd standard was added */
	if (strm->dev->fw_ver < SRAY_XU_AHD_STD_VERSION) {
		rc = -EPROTO;
	} else if (validate_ahd_std(width, height, framerate, standard)) {
		struct viddec_info info;
		info.width = width;
		info.height = height;
		info.framerate = framerate;
		info.interlaced = 0; // not used
		info.lock = standard << 1;
		rc = set_xu_control_data(UVC_XU_VIDDEC_INFO, (unsigned char*)&info, strm->dev);
		if (rc < 0) {
			dprint("%s: viddec info rc=%d %s\n", __func__, rc, strerror(errno));
		}
	} else {
		dprint("invalid AHD standard: %dx%d@%d %s\n", width, height,
				standard == MID2263_CVBS ? "CVBS" :
				standard == MID2263_AHD ? "AHD" :
				standard == MID2263_TVI ? "TVI" :
				standard == MID2263_TVI ? "CVI" : "other");
	}
	PutStrm(strm);
	return rc;
}


MID2263_API int S2263_SetGPO(int value, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;

	if (strm->dev->fw_ver < SRAY_XU_GPIO_VERSION) {
		rc = -EPROTO;
	} else {
		rc = set_xu_control(UVC_XU_GPIO, value, strm->dev);
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetGPI(int *value, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;

	if (strm->dev->fw_ver < SRAY_XU_GPIO_VERSION) {
		rc = -EPROTO;
	} else {
		rc = get_xu_control(UVC_XU_GPIO, value, strm->dev);
	}
	PutStrm(strm);
	return rc;
}



MID2263_API int S2263_SetDeintMode(int enable, int channel, int threshold_low, int threshold_high,
					int exclude_start, int exclude_count, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;
	int dt = threshold_high - threshold_low;

	/* version at which deinterlace params was added */
	if (strm->dev->fw_ver < SRAY_XU_DEINT_PARAMS_VERSION) {
		rc = -EPROTO;
	} else if (
		enable < 0 || enable > 1 ||
		channel < 0 || channel > 1 ||
		threshold_low < 0 || threshold_low > 255 ||
		(dt != 1 && dt != 2 && dt != 4 && dt != 8 && dt != 16 &&
		 dt != 32 && dt != 64 && dt != 128)) {
		rc = -EINVAL;
	} else {
		struct deint_params params = {
			.enable = enable,
			.channel = channel,
			.threshold_low = threshold_low,
			.threshold_high = threshold_high,
			.exclude_start = exclude_start,
			.exclude_count = exclude_count,
		};
		rc = set_xu_control_data(UVC_XU_DEINT_PARAMS, (unsigned char*)&params, strm->dev);
		if (rc < 0){
			dprint("%s: deint params rc=%d %s\n", __func__, rc, strerror(errno));
		}
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_GetDeintMode(int *enable, int *channel, int *threshold_low, int *threshold_high,
					int *exclude_start, int *exclude_count, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	int rc = 0;

	/* version at which deinterlace params was added */
	if (strm->dev->fw_ver < SRAY_XU_DEINT_PARAMS_VERSION) {
		rc = -EPROTO;
	} else {
		struct deint_params params;
		rc = get_xu_control_data(UVC_XU_DEINT_PARAMS, (unsigned char*)&params, strm->dev);
		if (rc < 0){
			dprint("%s: deint params rc=%d %s\n", __func__, rc, strerror(errno));
		} else {
			if (enable) *enable = params.enable;
			if (channel) *channel = params.channel;
			if (threshold_low) *threshold_low = params.threshold_low;
			if (threshold_high) *threshold_high = params.threshold_high;
			if (exclude_start) *exclude_start = params.exclude_start;
			if (exclude_count) *exclude_count = params.exclude_count;
		}
	}
	PutStrm(strm);
	return rc;
}




/* sets printf flag in the board struct. Loud output. for debug only */
MID2263_API int S2263_Debug(int bOnOff)
{
	G_debug = bOnOff ? 1 : 0;
	dprint("%s: debugging is now on\n", __func__);
	return 0;
}

MID2263_API int S2263_AVStreamRename(const char *name)
{
	strncpy(cardname, name, sizeof(cardname)-1);
	dprint("%s: cardname is %s\n", __func__, cardname);
	return 0;
}

MID2263_API void S2263_AVStreamOpenHook(void (*hook)(int devid))
{
	open_hook = hook;
}

MID2263_API int S2263_StartLog(int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (!strm->dev->log_thread) {
		rc = -pthread_create(
				&strm->dev->log_thread,
				NULL,
				log_thread,
				strm);
	}
	PutStrm(strm);
	return rc;
}

// returns 1 if audio needs freed, 0 otherwise
static int s2263_stop_audio_capture(int devidx, int strmidx)
{
	//printf("stop audio capture: %d\n", strmidx);
	if (strmidx == IDX_RB_RECORD) {
		struct stream *strm_audcap;
		// need to check if audio preview running first
		strm_audcap = GetAudioCapStrm(devidx);
		if (!strm_audcap->audio_recording) {
			printf("not recording\n");
			PutStrm(strm_audcap);
			strm_audcap->ringbuffer_pcm[strmidx].exit = 1;
			pthread_cond_signal(&strm_audcap->ringbuffer_pcm[strmidx].cond);
			return 0;
		}
		if (!strm_audcap->audio_previewing) {
			strm_audcap->active = 0;
		}
		// signal the record buffer ringbuffer
		strm_audcap->audio_recording = 0;
		if (!strm_audcap->audio_previewing) {
			strm_audcap->active = 0;
			PutStrm(strm_audcap);
			pthread_join(strm_audcap->thread, NULL);
		} else
			PutStrm(strm_audcap);
		strm_audcap->ringbuffer_pcm[strmidx].exit = 1;
		pthread_cond_signal(&strm_audcap->ringbuffer_pcm[strmidx].cond);
		return 1;
	} else if (strmidx == IDX_RB_AUDPREVIEW) {
		struct stream *strm_audcap;
		// need to check if file capture record is running first
		strm_audcap = GetAudioCapStrm(devidx);
		if (!strm_audcap->audio_previewing) {
			PutStrm(strm_audcap);
			strm_audcap->ringbuffer_pcm[strmidx].exit = 1;
			pthread_cond_signal(&strm_audcap->ringbuffer_pcm[strmidx].cond);
			return 0;
		}
		if (!strm_audcap->audio_recording) {
			strm_audcap->active = 0;
		}
		// signal the preview buffer ringbuffer
		// wait for the thread to exit if no video recording
		strm_audcap->audio_previewing = 0;
		if (!strm_audcap->audio_recording) {
			strm_audcap->active = 0;
			PutStrm(strm_audcap);
			pthread_join(strm_audcap->thread, NULL);
		} else {
			PutStrm(strm_audcap);
		}
		strm_audcap->ringbuffer_pcm[strmidx].exit = 1;
		pthread_cond_signal(&strm_audcap->ringbuffer_pcm[strmidx].cond);
		return 1;
	}
	return 0;
}
static int s2263_start_audio_capture(int devidx, int rb_idx);
static int s2263_free_audio_capture(int devidx, int strmidx);

MID2263_API int S2263_StartRecord(const char *filename, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	struct ringbuffer *rb;
	int rc = 0;

	if (!strm)
		return -ENODEV;
	if (strm->active) {
		dprint("%s: devid %d strmidx %d is busy\n", __func__, devid, strmidx);
		rc = -EBUSY;
		goto done;
	}
	// if AVI and older firmware, we need an audio thread
	if (strm->avi_av_uses_preview) {
		dprint("starting audio thread\n");
		(void) s2263_start_audio_capture(devid, IDX_RB_RECORD);
	}

// recording file needs to be write+read for mp4 updating 64-bit box size
	strm->rec_file = fopen(filename, "w+b");
	if (strm->rec_file == NULL) {
		rc = -errno;
		dprint("%s: failed to open file \"%s\": %s\n", __func__, 
			filename, strerror(-rc));
		goto done;
	}
	dprint("%s: filename=%s\n", __func__, filename);
	rb = &strm->ringbuffer;
	rb->size = RB_SIZE_FILESAVE;
	rb->buf = malloc(rb->size);
	rb->id = RINGBUFFER_ID_FILE;
	rb->exit = 0;
	if (!rb->buf) {
		dprint("%s: failed to allocate buffer\n", __func__);
		rc = -ENOMEM;
		goto fail;
	}
	rb->head = 0;
	rb->tail = 0;
	pthread_mutex_init(&rb->lock, NULL);
	pthread_cond_init(&rb->cond, NULL);
	rb_simple_init(&strm->rb_audio);
	// Standard operation record thread writes to ringbuffer, gets read by file thread.
	// AVI with PCM audio is the same, but we have 2 more ringbuffers for the audio_buffer_thread
	// The second ringbuffer is for audio preview (duplicate data)
        // The audio_buffer_thread may already be started by audio preview so we need to check that
	// here before starting


	rc = -pthread_create(
		&strm->thread,
		NULL,
		record_thread,
		strm);
	if (rc) {
		dprint("%s: failed to create thread\n", __func__);
		goto fail;
	}
	rc = -pthread_create(
		&strm->file_thread,
		NULL,
		file_thread,
		strm);
	if (rc) {
		dprint("%s: failed to create thread\n", __func__);
		pthread_mutex_unlock(&strm->mutex);
		pthread_join(strm->thread, NULL);
		pthread_mutex_lock(&strm->mutex);
		goto fail;//lint !e456 // execution path with different mutex lock state
	}
	strm->active = 1;
	strm->recording = 1;
	strm->restart = 0;
	goto done;

fail:
	free(rb->buf);
	rb->buf = NULL;
	if (strm->rec_file) {
		fclose(strm->rec_file);
		strm->rec_file = NULL;
	}
done://lint !e456 // execution path with different mutex lock state
	PutStrm(strm);
	return rc;
}

// rb_idx = IDX_RB_RECORD or IDX_RB_AUDPREVIEW
static int s2263_start_audio_capture(int devidx, int rb_idx)
{
	struct stream *strm_cap;
	struct ringbuffer *rb;
	int rc = 0;

	strm_cap = GetAudioCapStrm(devidx);
	if (!strm_cap) {
		return -ENODEV;
	}
	rb = &strm_cap->ringbuffer_pcm[rb_idx];
	switch (rb_idx) {
	case IDX_RB_AUDPREVIEW:
		strm_cap->audio_previewing = 1;
		break;
	case IDX_RB_RECORD:
		strm_cap->audio_recording = 1;
		break;
	default:
		PutStrm(strm_cap);
		return -EINVAL;
	}
	if (rb->buf != NULL) {
		return 0;
	}

	rb->size = RB_SIZE_AUDIO_PCM;
	rb->buf = malloc(rb->size);
	rb->id = rb_idx == IDX_RB_AUDPREVIEW ? RINGBUFFER_ID_AUDIO0 : RINGBUFFER_ID_AUDIO1;
	rb->exit = 0;
	if (!rb->buf) {
		dprint("%s: failed to allocate buffer\n", __func__);
			rc = -ENOMEM;
			goto fail;
	}
	rb->head = 0;
	rb->tail = 0;
	pthread_mutex_init(&rb->lock, NULL);
	pthread_cond_init(&rb->cond, NULL);
	if (strm_cap->active) {
		PutStrm(strm_cap);
		return 0;
	}
	rc = -pthread_create(
		&strm_cap->thread,
		NULL,
		audio_buffer_thread,
		strm_cap);
	strm_cap->active = (rc == 0);
fail:
	PutStrm(strm_cap);
	return rc;
}

static int s2263_free_audio_capture(int devidx, int strmidx)
{
	struct stream *strm_audcap;
	// need to check if audio preview running first
	strm_audcap = GetAudioCapStrm(devidx);
	if (strm_audcap->ringbuffer_pcm[strmidx].buf != NULL) {
		free(strm_audcap->ringbuffer_pcm[strmidx].buf);
		strm_audcap->ringbuffer_pcm[strmidx].buf = NULL;
	}
	PutStrm(strm_audcap);
	return 0;
}



// STRM_AUD_CAP controlled from STRM_CAP(if AVI audio) or STRM_AUD (audio preview)
// if both are running, STRM_AUD_CAP is left on, but the appropriate ringbuffer_pcm is released
// if only one is running, it is turned off and the two ringbuffer_pcm buffers are released
MID2263_API int S2263_StopStream(int devid, int strmidx)
{
	struct stream *strm = _GetStrm(devid, strmidx);
	int rc = 0;
	int audio_needs_freed = 0;
	if (!strm)
		return -ENODEV;
	if (!strm->active) {
		PutStrm(strm);
		return 0;
	}
	strm->active = 0;
	if (strm->recording) {
		if (strm->rec_file) {
			strm->ringbuffer.exit = 1;
			pthread_cond_signal(&strm->ringbuffer.cond);
		}
		if (strm->avi_av_uses_preview) {
			audio_needs_freed = s2263_stop_audio_capture(devid, IDX_RB_RECORD);
		}
		pthread_mutex_unlock(&strm->mutex);
		pthread_join(strm->thread, NULL);
		if (strm->rec_file) {
			pthread_join(strm->file_thread, NULL);
		}
		pthread_mutex_lock(&strm->mutex);
		if (strm->rec_file) {
			fclose(strm->rec_file);
			strm->rec_file = NULL;
			free(strm->ringbuffer.buf);
			strm->ringbuffer.buf = NULL;
		}
		if (audio_needs_freed) {
			s2263_free_audio_capture(devid, IDX_RB_RECORD);
		}
		strm->recording = 0;
	} else if (strm->previewing) {
		pthread_mutex_unlock(&strm->mutex);
		pthread_join(strm->thread, NULL);
		pthread_mutex_lock(&strm->mutex);
		strm->previewing = 0;
	} else if (strm->playback) {
		pthread_mutex_unlock(&strm->mutex);
		pthread_join(strm->thread, NULL);
		pthread_mutex_lock(&strm->mutex);
		if (strm->rec_file) {
			fclose(strm->rec_file);
			strm->rec_file = NULL;
		}
		strm->playback = 0;
	} else {
		if (strmidx == STRM_AUD) {
			audio_needs_freed = s2263_stop_audio_capture(devid, IDX_RB_AUDPREVIEW);
		}
		pthread_mutex_unlock(&strm->mutex);
		if (strm->thread)
			pthread_join(strm->thread, NULL);
		pthread_mutex_lock(&strm->mutex);
		if (audio_needs_freed) {
			s2263_free_audio_capture(devid, IDX_RB_AUDPREVIEW);
		}
	}
	PutStrm(strm);
	return rc;
}


MID2263_API int S2263_Snapshot(const char *filename, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);

	int rc = 0;
	int started = 0;

	if (!strm)
		return -ENODEV;
	if (strmidx != STRM_CAP) {
		dprint("snapshot not supported on stream %d\n", strmidx);
		rc = -EINVAL;
		goto done;
	}
	if (strm->dev->fw_ver < SRAY_XU_SNAPSHOT_VERSION) {
		dprint("snapshot requires newer firmware %d or newer\n", SRAY_XU_SNAPSHOT_VERSION);
		rc = -EINVAL;
		goto done;
	}

	strm->snapshot_file = fopen(filename, "wb");
	if (strm->snapshot_file == NULL) {
		rc = -errno;
		dprint("%s: failed to open file \"%s\": %s\n", __func__,
			filename, strerror(-rc));
		goto done;
	}
	dprint("%s: devid=%d strm=%p filename=%s\n", __func__, devid, strm, filename);


	if (!strm->active) {
		// stream not active

		rc = -pthread_create(
			&strm->thread,
			NULL,
			record_thread,
			strm);
		if (rc) {
			dprint("%s: failed to create thread\n", __func__);
			goto fail;
		}
		pthread_detach(strm->thread);  // no need to join

		strm->active = 1;
		strm->restart = 0;
		strm->previewing = 0;
		strm->recording = 0;
		strm->snapshot = 1;
		started = 1;
		goto done;
	} else {
		rc = set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_SNAPSHOT, strm->dev);
		if (rc == 0)
			goto done;
		dprint("%s: failed to set UVC_XU_COMMAND_SNAPSHOT rc=%d\n", __func__, rc);
	}
fail:
	if (strm->snapshot_file) {
		fclose(strm->snapshot_file);
		strm->snapshot_file = NULL;
	}
	if (started) {
		strm->active = 0;
	}
done:
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_StopRecord(int devid, int strmidx)
{
	return S2263_StopStream(devid, strmidx);
}

MID2263_API int S2263_StartPreview(int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (strm->active) {
		PutStrm(strm);
		return -EBUSY;
	}
	rc = -pthread_create(
		&strm->thread,
		NULL,
		preview_thread,
		strm);
	strm->active = (rc == 0);
	strm->previewing = strm->active;
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_StopPreview(int devid, int strmidx)
{
	return S2263_StopStream(devid, strmidx);
}

MID2263_API int S2263_StartPlayback(const char *filename, MID2263_RECMODE mode, int devid)
{
	struct stream *strm = GetOutputStrm(devid);

	int rc = 0;
	if (!strm)
		return -ENODEV;
	strm->rec_file = NULL;

	if (strm->active) {
		dprint("%s: devid %d playback is busy\n", __func__, devid);
		rc = -EBUSY;
		goto fail;
	}

	switch (mode) {
	case MID2263_RECMODE_TS:
	case MID2263_RECMODE_TS_NOAUDIO:
	case MID2263_RECMODE_H264:
	case MID2263_RECMODE_MJPEG:
	case MID2263_RECMODE_MP4:
	case MID2263_RECMODE_UYVY:
		strm->playback_mode = mode;
		break;
	default:
		rc = -EINVAL;
		goto fail;
	}

	strm->rec_file = fopen(filename, "rb");
	if (strm->rec_file == NULL) {
		rc = -errno;
		dprint("%s: failed to open file \"%s\": %s\n", __func__, 
			filename, strerror(-rc));
		goto fail;
	}
	dprint("%s: filename=%s\n", __func__, filename);

	rc = -pthread_create(
		&strm->thread,
		NULL,
		playback_thread,
		strm);
	if (rc) {
		dprint("%s: failed to create thread\n", __func__);
		goto fail;
	}

	strm->active = 1;
	strm->restart = 0;
	strm->playback = 1;
	goto done;

fail:
	if (strm->rec_file) {
		fclose(strm->rec_file);
		strm->rec_file = NULL;
	}
done:
	PutStrm(strm);
	return rc;
}


MID2263_API int S2263_StartAudioPreview(int devid)
{
	struct stream *strm_out = GetAudioStrm(devid);
	int rc = 0;
	if (!strm_out) {
		return -ENODEV;
	}
	if (strm_out->active) {
		PutStrm(strm_out);
		return -EBUSY;
	}
	// Check if there's a capture stream already started. If so, continue normally. If not,
	// start it.
	rc = s2263_start_audio_capture(devid, IDX_RB_AUDPREVIEW);
	//strm_cap->active = (rc == 0);
	if (!strm_out->active) {
		rc = -pthread_create(
			&strm_out->thread,
			NULL,
			audio_preview_thread,
			strm_out);
		strm_out->active = (rc == 0);
	}
	PutStrm(strm_out);
	return rc;
}

MID2263_API int S2263_StopAudioPreview(int devid)
{
	int rc;
	rc = S2263_StopStream(devid, STRM_AUD);
	return rc;
}

MID2263_API int S2263_StartCallback(int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (strm->active) {
		PutStrm(strm);
		return -EBUSY;
	}
	strm->active = 1;
	strm->restart = 0;
	strm->previewing = 0;

	rc = -pthread_create(
		&strm->thread,
		NULL,
		strmidx == 0 ? record_thread : preview_thread,
		strm);
	strm->recording = 1;
	if (rc) {
		dprint("%s: failed to create thread\n", __func__);
		strm->active = 0;
	}
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_RegisterCallback(S2263_cb_func_t callback, int devid, int strmidx)
{
	struct stream *strm = _GetStrm(devid, strmidx);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	strm->callback = callback;
	PutStrm(strm);
	return rc;
}


MID2263_API int S2263_PauseStream(int devid, int strmidx)
{
	switch (strmidx) {
	case STRM_CAP: return set_xu(UVC_XU_COMMAND, UVC_XU_COMMAND_CAPTURE_PAUSE, devid);
	case STRM_PRV: return set_xu(UVC_XU_COMMAND, UVC_XU_COMMAND_PREVIEW_PAUSE, devid);
	default: break;
	}
	return -EINVAL;
}

MID2263_API int S2263_ResumeStream(int devid, int strmidx)
{
	switch (strmidx) {
	case STRM_CAP: return set_xu(UVC_XU_COMMAND, UVC_XU_COMMAND_CAPTURE_RESUME, devid);
	case STRM_PRV: return set_xu(UVC_XU_COMMAND, UVC_XU_COMMAND_PREVIEW_RESUME, devid);
	default: break;
	}
	return -EINVAL;
}


// returns 1 when stream is active and not restarting
static int active_not_restart(struct stream *strm)
{
	int ret;
	pthread_mutex_lock(&strm->mutex);
	ret = strm->active && !strm->restart;
	pthread_mutex_unlock(&strm->mutex);
	return ret;
}

static void check_stream_stopped(struct stream *strm)
{
#ifndef DISABLE_TIMEOUT_CHECKS
	struct dev *dev = strm->dev;

	// not supported on this model
	if (dev->model == 2273) return;

	// is stream already stopped or restarting?
	if (!active_not_restart(strm)) {
		dprint("%s: stream active=%d restart=%d\n", __func__, strm->active, strm->restart);
		return;
	}
	// verify that stream is still running on device
	get_xu_control_data(strm->xu_param, strm->params, dev);
	//printf("%s: fw_ver=%d stream state=%x errors=%d\n", __func__, dev->fw_ver, strm->params[3], strm->errors);

	// check if the firmware supports the state in params
	if ((dev->fw_ver >= SRAY_XU_USB_PHY_RESET_VERSION && (strm->params[3] & 0x80) == 0) ||
	    strm->errors >= 5) {
		// stream was stopped on the device
		set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_USB_PHY_RESET, strm->dev);
		// after doing USB PHY reset, the USB driver may re-enumerate the device so
		// we need to close and reopen the same device
		reopen_device(dev);
		// strm->restart will be set now
	}
#else
	(void)strm;
#endif
}


#define NUM_BUFS 2

static void ringbuffer_full(struct stream *strm)
{
	struct ringbuffer *rb = &strm->ringbuffer;
	dprint("ringbuffer is full. ID: %d\n", rb->id);
	if (rb->exit)
		return;
	pthread_mutex_unlock(&rb->lock);
	ms_sleep(50);
	pthread_mutex_lock(&rb->lock);
}

static void ringbuffer_add(struct stream *strm, void *buf, unsigned int length)
{
	struct ringbuffer *rb = &strm->ringbuffer;
	unsigned int head = rb->head; // don't update rb->head until the end to avoid race condition

	pthread_mutex_lock(&rb->lock);
	while (head < rb->tail && head + sizeof(length) > rb->tail) {
		ringbuffer_full(strm);
		// make sure we aren't stopping thread
		// or we may deadlock here
		if (rb->exit)
			goto unlock_exit;
	}
	if (head + sizeof(length) > rb->size) {
		head = 0;
	}

	*(unsigned int *)(rb->buf + head) = length;
	head += sizeof(length);

	if (head + length > rb->size) {
		head = 0;
	}
	while (head < rb->tail && head + length > rb->tail) {
		ringbuffer_full(strm);
		// make sure we aren't stopping thread
		// or we may deadlock here
		if (rb->exit)
			goto unlock_exit;
	}
	pthread_mutex_unlock(&rb->lock);

	memcpy(rb->buf + head, buf, length);

	pthread_mutex_lock(&rb->lock);
	head += length;
	rb->head = head;

	pthread_cond_signal(&rb->cond);
unlock_exit:
	pthread_mutex_unlock(&rb->lock);
}

static void ringbuffer_audio_add(struct stream *strm, int idx, void *buf, unsigned int length)
{
	struct ringbuffer *rb = &strm->ringbuffer_pcm[idx];
	unsigned int head;
	pthread_mutex_lock(&rb->lock);
	// don't update rb->head until the end to avoid race condition
	head = rb->head;
	while (head < rb->tail && head + sizeof(length) > rb->tail) {
		ringbuffer_full(strm);
		if (rb->exit) {
			// make sure we aren't stopping thread
			// or we may deadlock here
			pthread_mutex_unlock(&rb->lock);
			return;
		}
	}
	if (head + sizeof(length) > rb->size) {
		head = 0;
	}

	*(unsigned int *)(rb->buf + head) = length;
	head += sizeof(length);

	if (head + length > rb->size) {
		head = 0;
	}
	while (head < rb->tail && head + length > rb->tail) {
		ringbuffer_full(strm);
		if (rb->exit) {
			// make sure we aren't stopping thread
			// or we may deadlock here
			pthread_mutex_unlock(&rb->lock);
			return;
		}
	}
	pthread_mutex_unlock(&rb->lock);
	memcpy(rb->buf + head, buf, length);
	pthread_mutex_lock(&rb->lock);
	head += length;
	rb->head = head;
	pthread_cond_signal(&rb->cond);
	pthread_mutex_unlock(&rb->lock);
}
// must be called with rb->lock locked
static unsigned int ringbuffer_get_len(struct ringbuffer *rb)
{
	unsigned int ret;
	if (rb->tail + sizeof(ret) > rb->size) {
		rb->tail = 0;
	}
	ret = *(unsigned int *)(rb->buf + rb->tail);
	rb->tail += sizeof(ret);
	return ret;
}

// must be called with rb->lock locked
static void *ringbuffer_get_buf(struct ringbuffer *rb, unsigned int length)
{
	if (rb->tail + length > rb->size) {
		rb->tail = 0;
	}
	return rb->buf + rb->tail;
}

static int is_jpeg(unsigned char *p, unsigned int len)
{
	if (0) printf("%s: %02x %02x  %02x %02x  len=%d\n", __func__,
		p[0], p[1], p[len-2], p[len-1], len);
	return p[0] == 0xff && p[1] == 0xd8 && p[len-2] == 0xff && p[len-1] == 0xd9;
}

static void stop_stream(struct stream *strm)
{
	if (strm == &strm->dev->strm[0]) {
		//dprint("stop a\n");
		set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_STOP_STREAM_A, strm->dev);
	} else if (strm == &strm->dev->strm[1]) {
		//dprint("stop b\n");
		set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_STOP_STREAM_B, strm->dev);
	}
}


static void *record_thread(void *env)
{
	struct stream *strm = (struct stream*) env;
	struct dev *dev = strm->dev;
	int strmidx = strm - dev->strm;
	struct v4l2_requestbuffers req;

	int vd;
	void			*buffer[NUM_BUFS] = {};
	size_t			length[NUM_BUFS] = {};
	struct v4l2_format	fmt = {};
	unsigned int i;
	unsigned long long	total = 0;
	struct timeval t0, t1;
	unsigned int frames = 0, frametime_ms;
	__u32 next_seq = 0;
	MID2263_RECMODE recmode;

	dprint("%s: started strmidx=%d strm=%p\n", __func__, strmidx, strm);

	gettimeofday(&t0, NULL);
	gettimeofday(&t1, NULL); // init t1 for debug, in case we exit early

	if (S2263_GetRecordMode(&recmode, dev->board_num, strmidx) < 0) {
		dprint("%s: failed to get record mode\n", __func__);
		goto done_unlocked;
	}

	// Estimate time per frame, used for poll timeout
	// Get the frame skip divided by captured frame rate
	{
		int frame_skip, framerate, interlaced;
		MID2263_INTERLACE_MODE cap_interlace;

		if (S2263_GetFrameSkip(&frame_skip, dev->board_num, strmidx) != 0) {
			dprint("%s: failed to get frame skip\n", __func__);
			goto done_unlocked;
		}
		if (S2263_GetInterlaceMode(&cap_interlace, dev->board_num, strmidx) < 0) {
			dprint("%s: failed to get interlace mode\n", __func__);
			goto done_unlocked;
		}

		pthread_mutex_lock(&strm->mutex);

		if (GetVideoInfo(NULL, NULL, &framerate, &interlaced, NULL, dev) != 0) {
			dprint("%s: failed to get frame rate\n", __func__);
			goto done;
		}
		if (framerate == 0) {
			dprint("%s: frame rate cannot be zero\n", __func__);
			goto done;
		}
		if (interlaced && !cap_interlace)
			frametime_ms = (frame_skip+1) * 2000000 / framerate;
		else
			frametime_ms = (frame_skip+1) * 1000000 / framerate;
		dprint("%s: frame_skip=%d frametime=%d ms\n", __func__,
			frame_skip, frametime_ms);
	}

full_restart: // strm->mutex must be locked before jumping to this label
	strm->restart = 0;
	vd = strm->fd;
	if (vd == -1) {
		//perror(videodev);
		goto done;
	}

	if (fmt.type == 0) {
		memset(&fmt, 0, sizeof(fmt));
		fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		if (xioctl(vd, VIDIOC_G_FMT, &fmt) < 0) {
			perror("VIDIOC_G_FMT");
			goto done;
		}
		dprint("%s: %dx%d active=%d\n", __func__, fmt.fmt.pix.width, fmt.fmt.pix.height, strm->active);
		// V4L2 recording format is always JPEG - (actual compressed format may vary)
		if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG) {
			dprint("setting jpeg format\n");
			fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
			if (ioctl(vd, VIDIOC_S_FMT, &fmt) < 0) {
				perror("VIDIOC_S_FMT");
				goto done;
			}
		}
	} else {
		// reuse existing format
		if (ioctl(vd, VIDIOC_S_FMT, &fmt) < 0) {
			perror("VIDIOC_S_FMT");
			goto done;
		}
	}

#if 0	
	// frame skip through VIDIOC_S_PARM, not currently implemented in firmware
	struct v4l2_streamparm sp;
	sp.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (ioctl(vd, VIDIOC_G_PARM, &sp) < 0) {
		perror("VIDIOC_G_PARM");
		goto done;
	}
	dprint("%s: timeperframe %d / %d\n", __func__,
			sp.parm.capture.timeperframe.numerator,
			sp.parm.capture.timeperframe.denominator);

	sp.parm.capture.timeperframe.denominator /= 2;
	if (ioctl(vd, VIDIOC_S_PARM, &sp) < 0) {
		perror("VIDIOC_S_PARM");
		goto done;
	}
	if (ioctl(vd, VIDIOC_G_PARM, &sp) < 0) {
		perror("VIDIOC_G_PARM");
		goto done;
	}
	dprint("%s: timeperframe %d / %d\n", __func__,
			sp.parm.capture.timeperframe.numerator,
			sp.parm.capture.timeperframe.denominator);
#endif

retry_reqbufs:
	memset(&req, 0, sizeof(req));
	req.count = NUM_BUFS;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		if (errno == EBUSY && strm->active) {
			pthread_mutex_unlock(&strm->mutex);
			dprint("retrying reqbufs\n");
			ms_sleep(100);
			pthread_mutex_lock(&strm->mutex);
			if (strm->active)
				goto retry_reqbufs;
		}
		perror("VIDIOC_REQBUFS");
		dprint("req .count=%i .type=%x .memory=%x\n", req.count, req.type, req.memory);
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QUERYBUF, &buf) < 0) {
			perror("VIDIOC_QUERYBUF");
			goto done;
		}
		if (buffer[i]) {
			if (munmap(buffer[i], length[i]) < 0) // undo any previous mapping
				perror("munmap"); // not fatal
		}

		length[i] = buf.length;
		buffer[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vd, buf.m.offset);
		if (buffer[i] == MAP_FAILED) {
			perror("mmap");
			goto done;
		}
	}
	dprint("reqbufs %d x %d\n", length[0], req.count);

	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			goto done;
		}
	}
	if (ioctl(vd, VIDIOC_STREAMON, &fmt.type) < 0) {
		perror("VIDIOC_STREAMON");
		goto done;
	}

	if (strm->snapshot) {
		set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_SNAPSHOT, strm->dev);
	}

	int timeout = 1000/*ms*/; // 1-second timeout initially

	strm->errors = 0; // reset error counts
	pthread_mutex_unlock(&strm->mutex);

	while (active_not_restart(strm)) {
		struct pollfd fds = { .fd = vd, .events = POLLIN };
		struct v4l2_buffer buf;
		int rc;
		// This poll timeout is adjustable to allow faster restart in case USB errors
		rc = poll(&fds, 1, timeout/*ms*/);

		if (rc == 0) {
			 // first frame timeout could be USB PHY problem?
			if (frames == 0 || strm->errors > 0)
				strm->errors++;
			dprint("record poll timeout, errs=%d, timeout=%dms revents=%x\n", strm->errors, timeout,
					fds.revents);
			// reset USB PHY if stream stopped accidentally, reopen & restart
			check_stream_stopped(strm);
			continue;
		}
		if (rc == 0 || (rc == -1 && errno == EINTR) ||
		    !(fds.revents & POLLIN)) {
			continue;
		}

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;

		if (ioctl(vd, VIDIOC_DQBUF, &buf) < 0) {
			if (errno == EAGAIN)
				continue;
			perror("VIDIOC_DQBUF");
			break;
		}
		if (buf.flags & V4L2_BUF_FLAG_ERROR) {
			dprint("buf %d reported error\n", buf.sequence);
		}
		if (buf.sequence != next_seq && frames != 0) {
			dprint("%d frame%s dropped, %u to %u, frames=%d\n", buf.sequence - next_seq,
					buf.sequence - next_seq != 1 ? "s" : "",
					next_seq-1, buf.sequence, frames);
		}
		if (frames == 0 || buf.sequence != 0) {
			next_seq = buf.sequence + 1;
			if (frames == 0) {
				// set start time to actual first frame
				gettimeofday(&t0, NULL);
			}
		}

		if (buf.bytesused) {
			unsigned char *p = buffer[buf.index];
			unsigned int len = buf.bytesused;

			//dprint("%d\n", buf.bytesused);
			//printf("%.6f\n", buf.timestamp.tv_sec+1e-6*buf.timestamp.tv_usec);
			// FIXME, what about recording jpeg
			if (recmode != MID2263_RECMODE_MJPEG &&
				is_jpeg(buffer[buf.index], buf.bytesused))
			{
				// snapshot - processed separately
				pthread_mutex_lock(&strm->mutex);
				if (strm->snapshot_file) {
					dprint("SNAPSHOT %d bytes\n", len);
					fwrite(p, len, 1, strm->snapshot_file);
					fclose(strm->snapshot_file);
					strm->snapshot_file = NULL;
					if (!strm->recording && !strm->previewing) {
						strm->active = 0; // stop thread if solo
					}
					strm->snapshot = 0;
				} else {
					dprint("snapshot received but no file open\n");
				}
				pthread_mutex_unlock(&strm->mutex);
			} else if (strm->rec_file) {
				ringbuffer_add(strm, buffer[buf.index], buf.bytesused);
			}
			if (strm->callback)
				strm->callback(buffer[buf.index], buf.bytesused, dev->board_num, strmidx);
			total += buf.bytesused;
			frames++;
			timeout = 3 * frametime_ms;  // 3-frame timeout after succesfully started
			strm->errors = 0; // reset error count on successful frame
		} else {
			if (frames > 0) {
				strm->errors++;
			}
		}

		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			break;
		}
	}

	pthread_mutex_lock(&strm->mutex);
	if (strm->active && strm->restart) {
		// we can get here due to errors after the file descriptor is reopened
		dprint("Record stream restarting\n");

		// Send the CAPTURE_RESTART command so the device knows to continue
		set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_CAPTURE_RESTART+strmidx, strm->dev);

		goto full_restart;
	}

	stop_stream(strm);
	gettimeofday(&t1, NULL);
	if (ioctl(vd, VIDIOC_STREAMOFF, &fmt.type) < 0) {
		perror("VIDIOC_STREAMOFF");
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		munmap(buffer[i], length[i]);
	}
	req.count = 0;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		goto done;
	}
	//set_xu_control(UVC_XU_CAPTURE_STATE, 0, dev);
done:
	pthread_mutex_unlock(&strm->mutex);
done_unlocked:
	dprint("%s: stopped, %llu bytes, %f Mbps, %f fps\n", __func__, total,
		total*8 / (1e6*(t1.tv_sec - t0.tv_sec) + (t1.tv_usec - t0.tv_usec)),
		frames / ((t1.tv_sec - t0.tv_sec) + 1e-6*(t1.tv_usec - t0.tv_usec))
		);
	return NULL;
}


struct remux_state {
	muxer_handle mp4;
	muxavi_handle avi;
	tsmux_handle ts;
	unsigned int video_track;
	unsigned int audio_track;
	unsigned int buffer_size;
	void *buffer;  // buffers elementary stream demuxed from ts

	int tick_Hz; // usually 90kHz
	int ticks_per_frame; // computed from frame-rate
	int sample_rate; // audio sample rate
	int channels; // audio channels
	int is_avi;
	int has_audio; // for AVI
	unsigned avi_av_uses_preview; // audio is from preview audio
	int devid;
};

static void aes3_to_pcm(unsigned char *aes3, unsigned char *pcm, int len, int swap);
static void remux_mp4(struct remux_state *m, unsigned char *buf, unsigned int buflen)
{
	unsigned int pid = 0;
	tsmux_handle ts = m->ts;
	struct stream *strm0 = GetStrm(m->devid, 0);
	struct rb_simple *rb_mp3 = &strm0->rb_audio;

	PutStrm(strm0);
	// this assumes that buf contains complete video and audio frames
	ts->scan_offset = 0;
	ts->video.read_offset = 0;
	ts->video.PES_length = 0;
	ts->video.PES_length_actual = 0;
	ts->audio.read_offset = 0;
	ts->audio.PES_length = 0;
	ts->audio.PES_length_actual = 0;

	for (;;) {
		//dprint("tsmux_scan buflen=%d\n", buflen);
		pid = tsmux_scan(ts, buf, buflen);

		if (pid == 0)
			break;

		/* grow the temporary buffer as needed */
		if (ts->dump_len > m->buffer_size) {
			while (ts->dump_len > m->buffer_size)
				m->buffer_size += 64 * 1024; // increase by 64KB
			//dprint("realloc buffer size %d\n", m->buffer_size);
			m->buffer = realloc(m->buffer, m->buffer_size); //lint !e698 // realloc warning
			if (!m->buffer) {
				dprint("Failed to reallocate memory\n");
				return;
			}
		}

		//dprint("tsmux_read pid=%x dumplen=%d vid=%x aud=%x\n", pid, 
		//	ts->dump_len, ts->video.PID, ts->audio.PID);
		unsigned int dump_len = ts->dump_len;
		unsigned int len = tsmux_read(ts, pid, m->buffer, buf, buflen);
		if (len == 0) {
			unsigned char *p = m->buffer;
			dprint("tsmux_scan returned pid %x, but tsmux_read returned %u\n", pid, len);
			dprint("%02x%02x%02x%02x %02x %02x %02x %02x %02x %02x\n",
					p[0], p[1], p[2], p[3],
					p[4], p[5], p[6], p[7],
					p[8], p[9]);
			continue;
		}
		if (len != dump_len)
			dprint("len=%u dump_len=%u pid=%d %c\n", len, dump_len,
					pid,
					pid == ts->video.PID ? 'V' :
					pid == ts->audio.PID ? 'A' : '?');

		if (pid == ts->video.PID) {
			int duration = ts->video.last_PTS - ts->video.cur_PTS;
			ts->video.cur_PTS = ts->video.last_PTS;
			int frames_dropped = (duration - m->ticks_per_frame / 2) / m->ticks_per_frame;
			unsigned char *p = m->buffer;
			unsigned char mode = 0;
			if (p[0]==0 && p[1]==0 && p[2]==0 && p[3]==1 && p[4]==9) {
				mode = p[5];
				p += 6;
				len -= 6;
			}
			if (frames_dropped) {
				dprint("frames_dropped=%d duration=%d ticks_per_frame=%d  \n",
					frames_dropped, duration, m->ticks_per_frame);
			}
			if (m->video_track == 0 && mode != 0x30) {
				if (0) printf("%02x%02x%02x%02x %02x %02x %02x %02x %02x %02x\n",
					p[0], p[1], p[2], p[3],
					p[4], p[5], p[6], p[7],
					p[8], p[9]);

				int width = 0, height = 0, profile = 0, level = 0;
				h264_info(p, len, &profile, &level, 
					&width, &height, NULL, NULL);
				//printf("width=%d height=%d profile=%d level=%d\n",
				//	width, height, profile, level);
				if (!profile || !level || !width || !height)
					continue; //skip this frame
				m->video_track = mp4_video_track_create(m->mp4,
					AVC1, m->tick_Hz, m->ticks_per_frame, 0, 
					width, height, profile, level);
				//printf("video track %d\n", m->video_track);
				duration = m->ticks_per_frame;
			}
			if (duration > m->ticks_per_frame*3) {
				dprint("Video frame big duration %u > %u\n", duration, m->ticks_per_frame);
			}
			mp4_write_track(m->mp4, m->video_track, p, len, duration);
		}
		if (pid == ts->audio.PID) {
			int duration = ts->audio.last_PTS - ts->audio.cur_PTS;
			ts->audio.cur_PTS = ts->audio.last_PTS;
			if (m->audio_track == 0) {
				if (m->video_track == 0)
					continue; // skip audio until valid video
				if (ts->audio.type == TS_TYPE_PCM_AUDIO) {
				// audio PCM in private data stream 0x06
					duration = ((len - 4) * 4) / 5;
					m->audio_track = mp4_audio_track_create(m->mp4, SOWT,
										m->tick_Hz, duration,
										0, m->sample_rate,
										m->channels);
				} else {
					unsigned int subtype;
					// default AAC TS_TYPE_AAC_AUDIO
					// mpeg1 layer 3 (".mp3") and AAC share the "mp4a" fourcc
					duration = 1024 * m->tick_Hz / m->sample_rate;
					subtype = ts->audio.type == TS_TYPE_AAC_AUDIO ?
						MP4A_SUBTYPE_AAC : MP4A_SUBTYPE_MP3;
					dprint("subtype %d \n", subtype);
					m->audio_track = mp4_mp4a_track_create(m->mp4,
									subtype,
									m->tick_Hz, duration,
									0, m->sample_rate,
									m->channels);
				}
			}
			if (ts->audio.type == TS_TYPE_PCM_AUDIO) {
				int pcmlen;
				// need to adjust the length for PCM
				pcmlen = ((len - 4) * 4) / 5;
				duration = pcmlen / 4; // PCM audio
				aes3_to_pcm(m->buffer + 4, m->buffer, len - 4, 0);
				len = pcmlen;
			}

			if (duration > 2 * 1024 * m->tick_Hz / m->sample_rate) {
				dprint("Audio frame big duration %u > %u\n", duration, 1024 * m->tick_Hz / m->sample_rate);
			}
			if (ts->audio.type == TS_TYPE_MP3_AUDIO) {
				// write to the simple audio ringbuf
				int rlen = len - (len % 4);
				unsigned char *buf = m->buffer;
				rb_simple_enqueue(rb_mp3, buf, len);
				//readback length multiple of 4
				rb_simple_dequeue(rb_mp3, G_audio_buf, rlen);
				mp4_write_track(m->mp4, m->audio_track, G_audio_buf, rlen, duration);
			} else {
				mp4_write_track(m->mp4, m->audio_track, m->buffer, len, duration);
			}
		}
	}
}

static const unsigned char byte_mirror[256] = {
	0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
	0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
	0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4,
	0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC,
	0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2,
	0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA,
	0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
	0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
	0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
	0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9,
	0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5,
	0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD,
	0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3,
	0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
	0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
	0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF,
};

// s302m PCM audio (AudioMode_PCM) is "encoded" differently
// convert to raw PCM (AudioMode_PCM2)
static void aes3_to_pcm(unsigned char *aes3, unsigned char *pcm, int len, int swap)
{
	int j = 0;
	int i = 0;
	unsigned int cval;
	//unsigned int sval;
	//unsigned short *pshort; // for alternate method
	//pshort = (unsigned short *)pcm;
	while (j < len) {
#if 1
		pcm[i++] = byte_mirror[aes3[j++]];
		pcm[i++] = byte_mirror[aes3[j++]];
		if (swap) {
			cval = pcm[i-1];
			pcm[i-1] = pcm[i-2];
			pcm[i-2] = cval;
		}
		pcm[i++] = (byte_mirror[aes3[j]] >> 4) | ((byte_mirror[aes3[j+1]] << 4) & 0xf0);

		pcm[i++] = ((byte_mirror[aes3[j+1]] >> 4) & 0x0f) | (((byte_mirror[aes3[j+2] & 0xf0]) << 4) & 0xf0);
		if (swap) {
			cval = pcm[i-1];
			pcm[i-1] = pcm[i-2];
			pcm[i-2] = cval;
		}
		j++;
		j++;
		j++;
#else
		// alternate method. faster but needs tested.
		*pshort++ = (byte_mirror[aes3[j+1]] <<  8) |
			byte_mirror[aes3[j+0]];
		*pshort++ = (byte_mirror[aes3[j+4] & 0xf0] << 12) |
			(byte_mirror[aes3[j+3]] <<  4) |
			(byte_mirror[aes3[j+2]] >>  4);
		j += 5;
#endif
	}

	return;
}

static int remux_avi(struct remux_state *m, unsigned char *buf, unsigned int buflen)
{
	unsigned int pid = 0;
	tsmux_handle ts = m->ts;
	struct stream *strm = GetAudioCapStrm(m->devid);
	struct ringbuffer *rb = &strm->ringbuffer_pcm[IDX_RB_RECORD];
	int rc = 0;
	struct stream *strm0 = GetStrm(m->devid, 0);
	struct rb_simple *rb_mp3 = &strm0->rb_audio;
	// this assumes that buf contains complete video and audio frames
	PutStrm(strm);
	PutStrm(strm0);
	ts->scan_offset = 0;
	ts->video.read_offset = 0;
	ts->video.PES_length = 0;
	ts->video.PES_length_actual = 0;
	ts->audio.read_offset = 0;
	ts->audio.PES_length = 0;
	ts->audio.PES_length_actual = 0;
	dprint("remux_avi begin\n");
	//avi_file_descriptor(m->avi, fd);

	for (;;) {
		//dprint("tsmux_scan buflen=%d\n", buflen);
		pid = tsmux_scan(ts, buf, buflen);
		//dprint("tsmux_scan %d\n", buflen);
		if (pid == 0)
			break;
		/* grow the temporary buffer as needed */
		if (ts->dump_len > m->buffer_size) {
			while (ts->dump_len > m->buffer_size)
				m->buffer_size += 64 * 1024; // increase by 64KB
			//dprint("realloc buffer size %d\n", m->buffer_size);
			m->buffer = realloc(m->buffer, m->buffer_size);
			if (!m->buffer) {
				dprint("Failed to reallocate memory\n");
				return 1;
			}
		}

		dprint("tsmux_read pid=%x dumplen=%d vid=%x aud=%x\n", pid,
			ts->dump_len, ts->video.PID, ts->audio.PID);
		unsigned int dump_len = ts->dump_len;
		unsigned int len = tsmux_read(ts, pid, m->buffer, buf, buflen);
		if (len == 0) {
			unsigned char *p = m->buffer;
			dprint("tsmux_scan returned pid %x, but tsmux_read returned %u\n", pid, len);
			dprint("%02x%02x%02x%02x %02x %02x %02x %02x %02x %02x\n",
					p[0], p[1], p[2], p[3],
					p[4], p[5], p[6], p[7],
					p[8], p[9]);
			continue;
		}
		if (len != dump_len)
			dprint("len=%u dump_len=%u pid=%d %c\n", len, dump_len,
					pid,
					pid == ts->video.PID ? 'V' :
					pid == ts->audio.PID ? 'A' : '?');
		//printf("pid %d, ts->video.PID %d ticks per %d\n", pid,
		//	ts->video.PID, m->ticks_per_frame);
		if (m->ticks_per_frame == 0)
			m->ticks_per_frame = 1;
		if (pid == ts->video.PID) {
			int duration = ts->video.last_PTS - ts->video.cur_PTS;
			ts->video.cur_PTS = ts->video.last_PTS;
			int frames_dropped = (duration - m->ticks_per_frame / 2) / m->ticks_per_frame;
			unsigned char *p = m->buffer;
			unsigned char mode = 0;
			//printf("video\n");
			if (p[0]==0 && p[1]==0 && p[2]==0 && p[3]==1 && p[4]==9) {
				mode = p[5];
				p += 6;
				len -= 6;
			}
			if (frames_dropped) {
				dprint("frames_dropped=%d duration=%d ticks_per_frame=%d  \n",
					frames_dropped, duration, m->ticks_per_frame);
			}
			if (m->video_track == 0 && mode != 0x30) {
				if (1) printf("%02x%02x%02x%02x %02x %02x %02x %02x %02x %02x\n",
					p[0], p[1], p[2], p[3],
					p[4], p[5], p[6], p[7],
					p[8], p[9]);

				int width = 0, height = 0, profile = 0, level = 0;
				h264_info(p, len, &profile, &level,
					&width, &height, NULL, NULL);
				//printf("width=%d height=%d profile=%d level=%d\n",
				//	width, height, profile, level);
				if (!profile || !level || !width || !height)
					continue; //skip this frame
				//printf("video track create\n");
				m->video_track = 2;
				duration = m->ticks_per_frame;
			}
			if (duration > m->ticks_per_frame*3) {
				dprint("Video frame big duration %u > %u\n", duration, m->ticks_per_frame);
			}
			avi_write_track(m->avi, 0, p, len, duration);
		}
		dprint("m->avi_av_uses_preview %d, pid %d audio %d\n", m->avi_av_uses_preview, pid, ts->audio.PID);
		if (pid == ts->audio.PID) {
			dprint("audio from TS stream\n");
			if (!m->avi_av_uses_preview) {
				int pcmlen = 0;
				//unsigned char *p = m->buffer;
				//printf("getting audio from transport stream\n");
				// Transport stream adds PCM stream option for newer firmware
				int duration = ts->audio.last_PTS - ts->audio.cur_PTS;
				ts->audio.cur_PTS = ts->audio.last_PTS;
				if (m->audio_track == 0) {
					// TODO: review dropping audio for MP3 and AAC
					if (m->video_track == 0)
						continue; // skip audio until valid video
					m->audio_track = 1;
					//duration = 1024 * m->tick_Hz / m->sample_rate;
					//m->audio_track = avi_audio_track_create(m->avi,
					//	MP4A, m->tick_Hz, duration,
					//	0, m->sample_rate, m->channels);
					//printf("audio track %d\n", m->audio_track);
				}
				if (duration > 2 * 1024 * m->tick_Hz / m->sample_rate) {
					dprint("Audio frame big duration %u > %u\n", duration, 1024 * m->tick_Hz / m->sample_rate);
				}
				dprint("audio type %d\n", ts->audio.type);
				if (ts->audio.type == TS_TYPE_PCM_AUDIO) {
					// AES3 to PCM
					// we can re-use the buffer safely as AES3
					// is 5/4 the size of the PCM data.
					// TS_TYPE_PCM_AUDIO = 0x06
					pcmlen = ((len - 4) * 4) / 5;
					aes3_to_pcm(m->buffer+4, m->buffer, len - 4, 0);
					dprint("write audio track %d\n", pcmlen);
					{
						unsigned char *p = m->buffer;
						dprint("audio data: %x %x %x %x\n", p[0], p[1],
							p[2], p[3]);
					}
					avi_write_atrack(m->avi, 1, m->buffer, pcmlen, 0);//pcmlen/4);//duration);
				} else if (ts->audio.type == TS_TYPE_AAC_AUDIO) {
					//dprint("len %d\n", len);
					avi_write_atrack(m->avi, 1, m->buffer, len, 0);
				} else if (ts->audio.type == TS_TYPE_MP3_AUDIO) {
					int rlen = len - (len % 4);
					unsigned char *buf = m->buffer;
					rb_simple_enqueue(rb_mp3, buf, len);
					//readback length multiple of 4
					rb_simple_dequeue(rb_mp3, G_audio_buf, rlen);
					avi_write_atrack(m->avi, 1, G_audio_buf, rlen, 0);
				} else {
					dprint("unknown audio type: write len %d\n", len);
					avi_write_atrack(m->avi, 1, m->buffer, len, 0);
				}
			} // if !avi_av_uses preview
		}
		if (m->avi_av_uses_preview) {
				// if firmware doesn't support TS with PCM
			// wait on ringbuffer
			if (m->has_audio && rb->buf != NULL) {
				int loops = 0;
				int maxloops = -1; // -1 for infinite
				pthread_mutex_lock(&rb->lock);
				if (!strm->audio_recording || !strm->active) {
					rc = 1; // signaled
					pthread_mutex_unlock(&rb->lock);
					break;
				}
				pthread_cond_wait(&rb->cond, &rb->lock);
				/* while not empty, process frames */
				while (rb->head != rb->tail) {
					unsigned int length = ringbuffer_get_len(rb);
					void *buf = ringbuffer_get_buf(rb, length);
					if (buf == NULL)
						break;
					if (length <= 0 || length > RB_SIZE_AUDIO_PCM) {
						printf("invalid length!\n");
						break;
					}
					//shouldn't need to unlock the ringbuffer
					//here. does this in file_thread
					//pthread_mutex_unlock(&rb->lock);
					dprint("avi write track %d\n", length);
					avi_write_atrack(m->avi, 1, buf, length, 0);
					//pthread_mutex_lock(&rb->lock);
					rb->tail += length;
					loops++;
					if (maxloops != -1 && loops > maxloops)
						break;

				}
				pthread_mutex_unlock(&rb->lock);
			}
			if (m->has_audio) {
				if (!strm->audio_recording || !strm->active) {
					rc = 1; // signaled
					break;
				}
			}
		} // else
	}
	dprint("remux_avi exit\n");
	return rc;
}

static void *file_thread(void *env)
{
	struct stream *strm = (struct stream*) env;
	struct dev *dev = strm->dev;
	int strmidx = strm - dev->strm;
	struct ringbuffer *rb = &strm->ringbuffer;
	struct remux_state mux = {NULL};
#ifdef DEBUG
	FILE *debug_file = NULL;
#endif
	int exit_cond = 0;
	pthread_mutex_lock(&strm->mutex);
	dprint("%s: started. mp4:%d avi_av: %d av_vonly:%d\n", __func__,
		strm->enable_mp4, strm->enable_avi_av,
		strm->enable_avi_vonly);

	if (strm->enable_mp4 || strm->enable_avi_av || strm->enable_avi_vonly) {
		int frame_rate, interlaced, frame_skip;
		int width, height;
		MID2263_INTERLACE_MODE cap_interlace;
		int devid = dev->board_num;
		int br = 2000000;

		pthread_mutex_unlock(&strm->mutex);
		if (S2263_GetVideoInfo(&width, &height, &frame_rate, &interlaced, devid) < 0) {
			dprint("get video info error\n");
			pthread_mutex_lock(&strm->mutex);
			goto done;
		}
		dprint("width x height: %dx%d\n", width, height);
		if (S2263_GetInterlaceMode(&cap_interlace, devid, strmidx) < 0) {
			dprint("interlace mode error\n");
			pthread_mutex_lock(&strm->mutex);
			goto done;
		}
		if (S2263_GetFrameSkip(&frame_skip, devid, strmidx) < 0) {
			dprint("frame skip error\n");
			pthread_mutex_lock(&strm->mutex);
			goto done;
		}
		S2263_GetBitrate(&br, devid, strmidx);
		pthread_mutex_lock(&strm->mutex);
		mux.is_avi = strm->enable_avi_av || strm->enable_avi_vonly;
		if (mux.is_avi) {
			int has_audio = strm->enable_avi_av ? 1 : 0;
			int audio_type = AVI_AUDTYPE_NONE;
			dprint("avi: britrate: %d, has audio %d\n", br, has_audio);
			if (has_audio) {
				switch (strm->audio_mode) {
				case MID2263_NONE:
				default:
					break;
				case MID2263_AAC:
					audio_type = AVI_AUDTYPE_AAC;
					break;
				case MID2263_PCM:
					audio_type = AVI_AUDTYPE_PCM;
					break;
				case MID2263_MP3:
					audio_type = AVI_AUDTYPE_MP3;
					break;
				}
			}
			dprint("audio type to set %d\n", audio_type);
			//frame_rate /= 1000;
			//printf("audio type %d\n", audio_type);
			if (interlaced)
				mux.avi = avi_writer_create(
					width, height, br, frame_rate/2, audio_type, 48000, strm->audio_br);
			else
				mux.avi = avi_writer_create(
					width, height, br, frame_rate, audio_type, 48000, strm->audio_br);
			mux.has_audio = has_audio;
			mux.avi_av_uses_preview = strm->avi_av_uses_preview;
			mux.devid = devid;
			if (!mux.avi) {
				dprint("%s: Failed to create avi\n");
				goto done;
			}
			dprint("avi created\n");
			mux.tick_Hz = 90000;
			avi_file_pointer(mux.avi, strm->rec_file);
		} else {
			mux.mp4 = mp4_writer_create();
			if (!mux.mp4) {
				dprint("%s: Failed to create mp4\n");
				goto done;
			}
			mp4_file_pointer(mux.mp4, strm->rec_file);
			mux.tick_Hz = 90000;
			mux.devid = devid;
			mp4_interleave_duration(mux.mp4, mux.tick_Hz/2); // 500ms interleave
			if (strm->mp4_max_minutes && strm->mp4_interval_seconds)
				mp4_set_moov_max_duration(mux.mp4,
							strm->mp4_max_minutes,
							strm->mp4_interval_seconds);
		}
		mux.ts = tsmux_create();
		if (!mux.ts) {
			dprint("%s: Failed to create tsdemux\n");
			goto done;
		}
		mux.video_track = 0;
		mux.audio_track = 0;
		mux.buffer_size = 0;
		mux.buffer = NULL;
		mux.ts->scan_whole_frame = 1;

		if (dev->fw_ver >= 2831 && cap_interlace != 0 ) {
			// interlace modes changed in fw
			cap_interlace = 0;
		}

		if (interlaced && !cap_interlace) {
			mux.ticks_per_frame = 2000 * mux.tick_Hz * (frame_skip+1) / frame_rate;
		} else {
			mux.ticks_per_frame = 1000 * mux.tick_Hz * (frame_skip+1) / frame_rate;
		}
		dprint("interlaced=%d cap_interlace=%d framerate=%d ticks_per_frame=%d\n",
			interlaced, cap_interlace, frame_rate, mux.ticks_per_frame);

		if (get_xu_control(UVC_XU_AUDIO_SAMPLERATE, &mux.sample_rate, dev) < 0)
			goto done;
		if (get_xu_control(UVC_XU_AUDIO_CHANNELS, &mux.channels, dev) < 0)
			goto done;
#ifdef DEBUG
		debug_file = fopen("debug.ts", "w");
#endif
	}

	if (strm->enable_avi_av || strm->enable_avi_vonly)
		avi_write_headers(mux.avi);

	while (strm->active) {
		int rc = 0;
		pthread_mutex_unlock(&strm->mutex);
		// wait on ringbuffer
		pthread_mutex_lock(&rb->lock);
		pthread_cond_wait(&rb->cond, &rb->lock);
		/* while not empty, process frames */
		while (rb->head != rb->tail) {
			unsigned int length = ringbuffer_get_len(rb);
			void *buf = ringbuffer_get_buf(rb, length);
			pthread_mutex_unlock(&rb->lock);
			if (strm->enable_mp4) {
				//dprint("remux buffer length %d\n", length);
				remux_mp4(&mux, buf, length);
#ifdef DEBUG
				fwrite(buf, 1, length, debug_file);
#endif
			} else if ((strm->enable_avi_av || strm->enable_avi_vonly)) {
				dprint("remux buffer length %d\n", length);
				exit_cond = remux_avi(&mux, buf, length);
#ifdef DEBUG
				fwrite(buf, 1, length, debug_file);
#endif
			} else {
				//printf("buf=%p length=%u  head=%u tail=%u\n", buf, length, rb->head, rb->tail);
				if (fwrite(buf, 1, length, strm->rec_file) < length) {
					dprint("%s: error writing file: %s\n",
						__func__, strerror(errno));
					//printf("buf=%p length=%p\n", buf, length);
					//abort();
					rc = -1;
					pthread_mutex_lock(&rb->lock);
					break;
				}
			}
			pthread_mutex_lock(&rb->lock);
			rb->tail += length;
			if (strm->active)
				break;
			if (exit_cond) {
				break;
			}
		}
		pthread_mutex_unlock(&rb->lock);
		pthread_mutex_lock(&strm->mutex);
		if (rc == -1)
			break;
	}
done:
	if (mux.mp4) {
		dprint("Closing MP4\n");
		mp4_write_movie(mux.mp4);
		dprint("Result: %s\n", mp4_get_error(mux.mp4));
		mp4_delete(mux.mp4);
#ifdef DEBUG
		fclose(debug_file);
#endif
	}
	if (mux.avi) {
		avi_delete(mux.avi);
	}
	if (mux.ts) {
		tsmux_delete(mux.ts);
	}
	if (mux.buffer) {
		free(mux.buffer);
		mux.buffer = NULL;
	}

	pthread_mutex_unlock(&strm->mutex);
	dprint("%s: stopped\n", __func__);
	return NULL;
}


//#define ENABLE_DECODED_PREVIEW
#ifdef ENABLE_DECODED_PREVIEW

#include <libavcodec/avcodec.h>

static void decode_frame(XvImage *dest, void *buffer, unsigned int len)
{
	static AVCodec *codec = NULL;
	static AVCodecContext *c = NULL;
	static AVFrame *picture = NULL;
	static AVPacket avpkt;
	static AVDictionary *opts = NULL;

	if (codec == NULL) {
		avcodec_register_all();
		av_init_packet(&avpkt);
		codec = avcodec_find_decoder(AV_CODEC_ID_H264);
		if (!codec) {
			printf("failed to find decoder\n");
			return;
		}
		c = avcodec_alloc_context3(codec);
		picture = avcodec_alloc_frame();

		if (avcodec_open2(c, codec, &opts) < 0) {
			printf("failed to open codec\n");
			return;
		}
	}

	avpkt.size = len;
	avpkt.data = buffer;
	while (avpkt.size > 0) {
		int got_picture = 0, len;
		len = avcodec_decode_video2(c, picture, &got_picture, &avpkt);
		if (len < 0) {
			printf("error decoding frame\n");
			return;
		}
		if (got_picture) {
			//printf("picture->linesize=%d %d %d\n", picture->linesize[0], picture->linesize[1], picture->linesize[2]);
			int p;
			for (p = 0; p < 3; p++) {
				char *dest_plane = dest->data + dest->offsets[p == 0 ? p : 3-p];
				int pitch = dest->pitches[p == 0 ? p : 3-p];
				int width = c->width >> (p > 0);
				int height = c->height >> (p > 0);
				int i;
				for (i = 0; i < height; i++) {
					memcpy(dest_plane + i * pitch, 
						picture->data[p] + i * picture->linesize[p],
						width);
				}
			}
		}

		avpkt.size -= len;
		avpkt.data += len;
	}
}



static void decode(XvImage *dest, void *buf, unsigned int buflen)
{
	static tsmux_handle ts = NULL;
	static unsigned int buffer_size = 0;
	static void *buffer = NULL;
	unsigned int pid = 0;

	if (!ts) {
		ts = tsmux_create();
		if (!ts) {
			dprint("Failed to allocate demuxer\n");
			return;
		}
	}

	ts->scan_offset = 0;
	ts->video.read_offset = 0;
	ts->audio.read_offset = 0;

	for (;;) {
		//dprint("tsmux_scan buflen=%d\n", buflen);
		pid = tsmux_scan(ts, buf, buflen);

		if (pid == 0)
			break;

		/* grow the temporary buffer as needed */
		if (ts->dump_len > buffer_size) {
			while (ts->dump_len > buffer_size)
				buffer_size += 64 * 1024;
			//dprint("realloc buffer size %d\n", buffer_size);
			buffer = realloc(buffer, buffer_size);
			if (!buffer) {
				dprint("Failed to reallocate memory\n");
				return;
			}
		}

		//dprint("tsmux_read pid=%d dumplen=%d vid=%d aud=%d\n", pid, 
		//	ts->dump_len, ts->video.PID, ts->audio.PID);
		unsigned int len = tsmux_read(ts, pid, buffer, buf, buflen);

		if (pid == ts->video.PID) {
			unsigned char *p = buffer;
			if (p[0]==0 && p[1]==0 && p[2]==0 && p[3]==1 && p[4]==9)
				p += 6;
			//printf("decoded %d  %02x %02x %02x %02x %02x %02x\n", 
			//	len, p[0], p[1], p[2], p[3], p[4], p[5]);
			decode_frame(dest, buffer, len);
		}
	}
}

#endif


/*
 * Based on Source: http://bellet.info/XVideo/testxv.c
 * Also: http://web.archive.org/web/20040206003311/http://www.cs.uni-potsdam.de/~wertmann/xv/testxv.c
 * License not specified, assumed to be public domain
 */

struct video_window_state {
	Display			*dpy;
	GC			gc;
	int			xv_port;
	int			window_id;
	Window			window;
	union {
		XvImage			*yuv_image;
		XImage                  *image;
	};

	/** for shm */
	XShmSegmentInfo		yuv_shminfo;
	int                     xshm_attached;
	int                     yuv_format;
	unsigned int            dest_guid;
};


static void video_window_cleanup(struct video_window_state *w)
{
	if (w->xshm_attached) {
		XShmDetach(w->dpy, &w->yuv_shminfo);
		w->xshm_attached = 0;
	}
	if (w->xv_port != -1) {
		XvUngrabPort(w->dpy, w->xv_port, CurrentTime);
		w->xv_port = -1;
	}
	XFree(w->yuv_image);
	w->yuv_image = NULL;
	if (w->gc) {
		XFreeGC(w->dpy, w->gc);
		w->gc = NULL;
	}
	if (w->dpy) {
		if (w->window_id == -1 && w->window) {
			XEvent event;

			XUnmapWindow(w->dpy, w->window);
			/** Wait for unmap. */
			do {
				XNextEvent(w->dpy, &event);
			} while (event.type != UnmapNotify || event.xmap.event != w->window);
			XDestroyWindow(w->dpy, w->window);
			w->window = 0;
		}
		XCloseDisplay(w->dpy);
		w->dpy = NULL;
	}
}

#define GUID_YUV12_PLANAR 0x32315659
#define GUID_YUY2_PACKED 0x32595559
#define GUID_UYVY_PACKED 0x59565955
#define GUID_I420_PLANAR 0x30323449
#define GUID_RGBA_PACKED 0x52474241

static int video_window_init(struct video_window_state *w, int yuv_width, int yuv_height,
							int yuv_format, int window_id)
{
	unsigned int		i;
	int			ret;
	unsigned int		p_version, p_release,
				p_request_base, p_event_base, p_error_base;
	unsigned int		p_num_adaptors;

	Display			*dpy = NULL;
	Window			window = 0;
	XSetWindowAttributes	xswa;
	XVisualInfo		vinfo;
	int			screen;
	unsigned long		mask;
	unsigned char		log = 0;

	memset(w, 0, sizeof(*w));
	w->xv_port = -1;

	dpy = XOpenDisplay(NULL);
	if (dpy == NULL) {
		dprint("Cannot open Display.\n");
		goto done;
	}
	w->dpy = dpy;

	screen = DefaultScreen(dpy);

	/** find best display */
	if (XMatchVisualInfo(dpy, screen, 24, TrueColor, &vinfo)) {
		if (log) dprint(" found 24bit TrueColor\n");
	} else if (XMatchVisualInfo(dpy, screen, 16, TrueColor, &vinfo)) {
		if (log) dprint(" found 16bit TrueColor\n");
	} else if (XMatchVisualInfo(dpy, screen, 15, TrueColor, &vinfo)) {
		if (log) dprint(" found 15bit TrueColor\n");
	} else if (XMatchVisualInfo(dpy, screen, 8, PseudoColor, &vinfo)) {
		if (log) dprint(" found 8bit PseudoColor\n");
	} else if (XMatchVisualInfo(dpy, screen, 8, GrayScale, &vinfo)) {
		if (log) dprint(" found 8bit GrayScale\n");
	} else if (XMatchVisualInfo(dpy, screen, 8, StaticGray, &vinfo)) {
		if (log) dprint(" found 8bit StaticGray\n");
	} else if (XMatchVisualInfo(dpy, screen, 1, StaticGray, &vinfo)) {
		if (log) dprint(" found 1bit StaticGray\n");
	} else {
		dprint("requires 24/16/15/8 bit display\n");
		goto done;
	}


	if (!XShmQueryExtension(dpy)) {
		dprint("no shmem available.\n");
		goto done;
	}


	/**--------------------------------- XV ------------------------------------*/
	if (log) dprint("beginning to parse the Xvideo extension...\n\n");

	/** query and print Xvideo properties */
	ret = XvQueryExtension(dpy, &p_version, &p_release, &p_request_base,
			 &p_event_base, &p_error_base);
	if (ret != Success) {
		if (ret == XvBadExtension)
			dprint("XvBadExtension returned at XvQueryExtension.\n");
		else if (ret == XvBadAlloc)
			dprint("XvBadAlloc returned at XvQueryExtension.\n");
		else
			dprint("other error happened at XvQueryExtension.\n");
		goto try_x11shm;
	}
	if (log) {
		dprint("========================================\n");
		dprint("XvQueryExtension returned the following:\n");
		dprint("p_version      : %u\n", p_version);
		dprint("p_release      : %u\n", p_release);
		dprint("p_request_base : %u\n", p_request_base);
		dprint("p_event_base   : %u\n", p_event_base);
		dprint("p_error_base   : %u\n", p_error_base);
		dprint("========================================\n");
	}

	XvAdaptorInfo		*ai;
	ret = XvQueryAdaptors(dpy, DefaultRootWindow(dpy),
			&p_num_adaptors, &ai);

	if (ret != Success) {
		if (ret == XvBadExtension)
			dprint("XvBadExtension returned at XvQueryExtension.\n");
		else if (ret == XvBadAlloc)
			dprint("XvBadAlloc returned at XvQueryExtension.\n");
		else
			dprint("other error happaned at XvQueryAdaptors.\n");
	}
	if (log) {
		dprint("=======================================\n");
		dprint("XvQueryAdaptors returned the following:\n");
		dprint("%d adaptors available.\n", p_num_adaptors);
	}
	for (i = 0; i < p_num_adaptors; i++) {
		if (log) dprint(" name:        %s\n"
			" type:        %s%s%s%s%s\n"
			" ports:       %ld\n"
			" first port:  %ld\n"
			" num_formats: %ld\n",
			ai[i].name,
			(ai[i].type & XvInputMask)	? "input | "	: "",
			(ai[i].type & XvOutputMask)	? "output | "	: "",
			(ai[i].type & XvVideoMask)	? "video | "	: "",
			(ai[i].type & XvStillMask)	? "still | "	: "",
			(ai[i].type & XvImageMask)	? "image | "	: "",
			ai[i].num_ports,
			ai[i].base_id,
			ai[i].num_formats);
		unsigned int j, p;

		if (log) dprint("adaptor %d ; format list:\n", i);
		for (j = 0; j < ai[i].num_formats; j++) {
			if (log) dprint(" depth=%d, visual=%ld\n",
				ai[i].formats[j].depth,
				ai[i].formats[j].visual_id);
		}
		for (p = ai[i].base_id; p < ai[i].base_id+ai[i].num_ports; p++) {
			XvEncodingInfo		*ei;
			XvAttribute		*at;
			XvImageFormatValues	*fo;
			int			k, attributes, formats;
			unsigned int		encodings;

			if (log) dprint(" encoding list for port %d\n", p);
			if (XvQueryEncodings(dpy, p, &encodings, &ei) != Success) {
				dprint("XvQueryEncodings failed.\n");
				continue;
			}
			if (log) for (j = 0; j < encodings; j++) {
				dprint("  id=%ld, name=%s, size=%ldx%ld, numerator=%d, denominator=%d\n",
					ei[j].encoding_id, ei[j].name, ei[j].width, ei[j].height,
					ei[j].rate.numerator, ei[j].rate.denominator);
			}
			XvFreeEncodingInfo(ei);

			if (log) dprint(" attribute list for port %d\n", p);
			at = XvQueryPortAttributes(dpy, p, &attributes);
			for (k = 0; k < attributes; k++) {
				int value = 0;
				XvGetPortAttribute(dpy, p, 
					XInternAtom(dpy, at[k].name, True),
					&value);
				if (log) dprint("  name: %-16s"
					"  flags: %s%s\t"
					"  min_color: %i\t"
					"  max_color: %i\t"
					"  cur: %i\n",
					at[k].name,
					(at[k].flags & XvGettable) ? " get" : "",
					(at[k].flags & XvSettable) ? " set" : "",
					at[k].min_value, at[k].max_value, value);
			}
			if (at)
				XFree(at);

			if (log) dprint(" image format list for port %d\n", p);

			unsigned char en_YUV12 = 0, en_YUY2 = 0, en_UYVY = 0, en_I420 = 0;
			fo = XvListImageFormats(dpy, p, &formats);
			for (k = 0; k < formats; k++) {
				switch (fo[k].id) {
				case GUID_YUV12_PLANAR: en_YUV12 = 1; break;
				case GUID_YUY2_PACKED: en_YUY2 = 1; break;
				case GUID_UYVY_PACKED: en_UYVY = 1; break;
				case GUID_I420_PLANAR: en_I420 = 1; break;
				}
				if (log) dprint("  0x%x (%4.4s) %s\n",
					fo[k].id,
					(char *)&fo[k].id,
					(fo[k].format == XvPacked) ? "packed" : "planar");
			}
			if (w->xv_port == -1) {
				unsigned int dest_guid = 0;

				if ((yuv_format == YUV_FORMAT_UYVY_422 || yuv_format == YUV_FORMAT_YUY2_422) && !en_UYVY && en_YUV12) {
					dest_guid = GUID_YUV12_PLANAR;
				} else if ((yuv_format == YUV_FORMAT_UYVY_422 || yuv_format == YUV_FORMAT_YUY2_422) && !en_UYVY && en_I420) {
					dest_guid = GUID_I420_PLANAR;
				} else if (yuv_format == YUV_FORMAT_YUV_420 && en_YUV12) {
					dest_guid = GUID_YUV12_PLANAR;
				} else if (yuv_format == YUV_FORMAT_UYVY_422 && en_UYVY) {
					dest_guid = GUID_UYVY_PACKED;
				} else if (yuv_format == YUV_FORMAT_YUY2_422 && en_YUY2) {
					dest_guid = GUID_YUY2_PACKED;
				}

				if (XvGrabPort(dpy, p, CurrentTime) == Success) {
					w->dest_guid = dest_guid;
					w->xv_port = p;
				}
			}

			if (fo)
				XFree(fo);
		}
	}

	if (p_num_adaptors > 0)
		XvFreeAdaptorInfo(ai);
try_x11shm:
	if (w->xv_port == -1) {
		dprint("no xv format for requested yuv format %x\n", yuv_format);
		if (vinfo.depth != 24)
			goto done;
		// Attempt to use RGB
		w->dest_guid = GUID_RGBA_PACKED;
	}

	xswa.colormap =  XCreateColormap(dpy, DefaultRootWindow(w->dpy), vinfo.visual, AllocNone);
	xswa.event_mask = StructureNotifyMask | ExposureMask | ButtonPressMask;
	xswa.background_pixel = 0;
	xswa.border_pixel = 0;

	mask = CWBackPixel | CWBorderPixel | CWColormap | CWEventMask;

	if (window_id != -1) {
		window = window_id;
	} else {

		window = XCreateWindow(w->dpy, DefaultRootWindow(dpy),
					0, 0,
					yuv_width,
					yuv_height,
					0, vinfo.depth,
					InputOutput,
					vinfo.visual,
					mask, &xswa);

		XStoreName(dpy, window, "Preview");
		XSetIconName(dpy, window, "Preview");

		//XSelectInput(dpy, window, StructureNotifyMask); // not needed due to xswa.event_mask

		/** Map window */
		XMapWindow(dpy, window);

		/* support the delete window protocol */
		Atom WM_DELETE_WINDOW = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
		XSetWMProtocols(dpy, window, &WM_DELETE_WINDOW, 1);

		if (0) { // causes excessive CPU use
			// hide the window borders
			struct {
				unsigned long flags;
				unsigned long functions;
				unsigned long decorations;
				long inputMode;
				unsigned long status;
			} hints = { .flags = 2, .decorations = 0 };
			Atom property;

			property = XInternAtom(dpy, "_MOTIF_WM_HINTS", True);
			XChangeProperty(dpy, window, property, property, 32, PropModeReplace,
				(unsigned char *)&hints, 5);
		}

		/** Wait for map. */
		XEvent event;
		do {
			XNextEvent(dpy, &event);
		} while (event.type != MapNotify || event.xmap.event != window);
	}
	w->window = window;
	w->yuv_format = yuv_format;

	w->gc = XCreateGC(dpy, window, 0, 0);


	if (w->xv_port != -1) {
		w->yuv_image = XvShmCreateImage(dpy, w->xv_port, w->dest_guid, 0, yuv_width, yuv_height, &w->yuv_shminfo);
		dprint("yuv_image = %p xv_port=%d\n", w->yuv_image, w->xv_port);

		w->yuv_shminfo.shmid = shmget(IPC_PRIVATE, w->yuv_image->data_size, IPC_CREAT | 0777);
		w->yuv_shminfo.shmaddr = w->yuv_image->data = shmat(w->yuv_shminfo.shmid, NULL, 0);

		dprint("yuv_image: %s width=%d height=%d data_size=%d num_planes=%d\n",
			w->dest_guid == GUID_YUV12_PLANAR ? "GUID_YUV12_PLANAR" :
			w->dest_guid == GUID_I420_PLANAR ? "GUID_I420_PLANAR" :
			w->dest_guid == GUID_UYVY_PACKED ? "GUID_UYVY_PACKED" :
			w->dest_guid == GUID_YUY2_PACKED ? "GUID_YUY2_PACKED" :
				"unknown GUID",
			w->yuv_image->width, w->yuv_image->height,
			w->yuv_image->data_size,
			w->yuv_image->num_planes);
		for (i = 0; (int)i < w->yuv_image->num_planes; i++) {
			dprint("plane %d: pitch=%d offset=%d\n",
				i, w->yuv_image->pitches[i], w->yuv_image->offsets[i]);
		}

	} else { // RGB Pixmap
		int data_size = yuv_width * yuv_height * 4;
		w->yuv_shminfo.shmid = shmget(IPC_PRIVATE, data_size, IPC_CREAT | 0777);
		w->yuv_shminfo.shmaddr = shmat(w->yuv_shminfo.shmid, NULL, 0);
		dprint("shmaddr=%p\n", w->yuv_shminfo.shmaddr);
		w->image = XShmCreateImage(dpy, vinfo.visual, vinfo.depth, ZPixmap, w->yuv_shminfo.shmaddr, &w->yuv_shminfo, yuv_width, yuv_height);
		dprint("image: RGBA width=%d height=%d data_size=%d bytes_per_line=%d bits_per_pixel=%d\n",
			w->image->width, w->image->height, data_size,
			w->image->bytes_per_line, w->image->bits_per_pixel);
	}

	w->yuv_shminfo.readOnly = False;
	w->xshm_attached = XShmAttach(dpy, &w->yuv_shminfo);
	if (!w->xshm_attached) {
		dprint("XShmAttach failed !\n");
		goto done;
	}
#if 0	
	// test pattern
	for (i = 0; i < w->yuv_image->height; i++) {
		int j;
		for (j = 0; j < w->yuv_image->width; j++) {
			w->yuv_image->data[w->yuv_image->width*i + j] = i*j;
		}
	}
#endif
#if 0
	if (w->xv_port != -1) {
		XvAttribute		*at;
		int k, attributes;
		at = XvQueryPortAttributes(dpy, w->xv_port, &attributes);
		for (k = 0; k < attributes; k++) {
			if (strcmp(at[k].name, "XV_COLORSPACE") == 0) {
				dprint("set attr XV_COLORSPACE %d\n",
						XvSetPortAttribute(dpy, w->xv_port,
						XInternAtom(dpy, "XV_COLORSPACE", True),
						1));
				int value;
				XvGetPortAttribute(dpy, w->xv_port,
						XInternAtom(dpy, "XV_COLORSPACE", True),
						&value);
				dprint("get attr XV_COLORSPACE %d\n", value);
			}
		}
		if (at)
			XFree(at);
	}
#endif
	return 0;
done:
	return -1;
}

#define CLAMP(x) ((x) > 255 ? 255 : ((x) < 0 ? 0 : (x)))

static void video_window_copy_buffer(struct video_window_state *w, void *source)
{
	unsigned int y, x;

	//printf("%d %d\n", w->yuv_format, w->dest_guid);
	if (w->yuv_format == YUV_FORMAT_UYVY_422 && (w->dest_guid == GUID_YUV12_PLANAR || w->dest_guid == GUID_I420_PLANAR)) {
		unsigned int u = w->dest_guid == GUID_I420_PLANAR ? 1 : 2;
		unsigned int v = w->dest_guid == GUID_YUV12_PLANAR ? 1 : 2;
		char *src = source;
		char *dy = w->yuv_image->data + w->yuv_image->offsets[0];
		char *du = w->yuv_image->data + w->yuv_image->offsets[u];
		char *dv = w->yuv_image->data + w->yuv_image->offsets[v];
		unsigned int width = w->yuv_image->width;
		unsigned int height = w->yuv_image->height;
		for (y = 0; y < height; y++) {
			if (y & 1) {
				// odd lines, copy Y only
				for (x = 0; x < width; x++) {
					src++;
					dy[x] = *src++;
				}
			} else {
				// even lines, copy U, Y, V, Y
				for (x = 0; x < width; x++) {
					(x & 1 ? dv : du)[x/2] = *src++;
					dy[x] = *src++;
				}
				du += w->yuv_image->pitches[u];
				dv += w->yuv_image->pitches[v];
			}
			dy += w->yuv_image->pitches[0];
		}
	} else if (w->yuv_format == YUV_FORMAT_YUY2_422 && (w->dest_guid == GUID_YUV12_PLANAR || w->dest_guid == GUID_I420_PLANAR)) {
		unsigned int u = w->dest_guid == GUID_I420_PLANAR ? 1 : 2;
		unsigned int v = w->dest_guid == GUID_I420_PLANAR ? 2 : 1;
		char *src = source;
		char *dy = w->yuv_image->data + w->yuv_image->offsets[0];
		char *du = w->yuv_image->data + w->yuv_image->offsets[u];
		char *dv = w->yuv_image->data + w->yuv_image->offsets[v];
		unsigned int width = w->yuv_image->width;
		unsigned int height = w->yuv_image->height;
		for (y = 0; y < height; y++) {
			if (y & 1) {
				// odd lines, copy Y only
				for (x = 0; x < width; x++) {
					dy[x] = *src++;
					src++;
				}
			} else {
				// even lines, copy Y, U, Y, V
				for (x = 0; x < width; x++) {
					dy[x] = *src++;
					(x & 1 ? dv : du)[x/2] = *src++;
				}
				du += w->yuv_image->pitches[u];
				dv += w->yuv_image->pitches[v];
			}
			dy += w->yuv_image->pitches[0];
		}
	} else if (w->yuv_format == YUV_FORMAT_UYVY_422 && w->dest_guid == GUID_RGBA_PACKED) {
		// Convert UYVY to RGBA
		__u32 *src = source;
		unsigned int width = w->image->width;
		unsigned int height = w->image->height;
		for (y = 0; y < height; y++) {
			__u32 *dest = (__u32 *) (w->image->data + y * w->image->bytes_per_line);
			for (x = 0 ; x < width; x += 2) {
				int y0 = ((*src >>  8) & 0xff) - 16;
				int y1 = ((*src >> 24) & 0xff) - 16;
				int  u = ((*src >>  0) & 0xff) - 128;
				int  v = ((*src >> 16) & 0xff) - 128;
				int r,g,b;
				src++;

				r = (298*y0         + 409*v + 128) >> 8;
				g = (298*y0 - 100*u - 208*v + 128) >> 8;
				b = (298*y0 + 516*u         + 128) >> 8;
				*dest++ = (CLAMP(r) << 16) | (CLAMP(g) << 8) | (CLAMP(b) << 0);

				r = (298*y1         + 409*v + 128) >> 8;
				g = (298*y1 - 100*u - 208*v + 128) >> 8;
				b = (298*y1 + 516*u         + 128) >> 8;
				*dest++ = (CLAMP(r) << 16) | (CLAMP(g) << 8) | (CLAMP(b) << 0);

			}
		}
	} else if (w->yuv_format == YUV_FORMAT_YUY2_422 && w->dest_guid == GUID_RGBA_PACKED) {
		// Convert YUY2 to RGBA
		__u32 *src = source;
		unsigned int width = w->image->width;
		unsigned int height = w->image->height;
		for (y = 0; y < height; y++) {
			__u32 *dest = (__u32 *) (w->image->data + y * w->image->bytes_per_line);
			for (x = 0 ; x < width; x += 2) {
				int y0 = (*src >>  0) & 0xff;
				int y1 = (*src >> 16) & 0xff;
				int  u = (*src >>  8) & 0xff;
				int  v = (*src >> 24) & 0xff;
				int r,g,b;
				src++;

				r = (298*y0         + 409*v + 128) >> 8;
				g = (298*y0 - 100*u - 208*v + 128) >> 8;
				b = (298*y0 + 516*u         + 128) >> 8;
				*dest++ = (CLAMP(r) << 16) | (CLAMP(g) << 8) | (CLAMP(b) << 0);

				r = (298*y1         + 409*v + 128) >> 8;
				g = (298*y1 - 100*u - 208*v + 128) >> 8;
				b = (298*y1 + 516*u         + 128) >> 8;
				*dest++ = (CLAMP(r) << 16) | (CLAMP(g) << 8) | (CLAMP(b) << 0);
			}
		}
	} else {
		memcpy(w->yuv_image->data, source, w->yuv_image->data_size);
	}
}

#ifdef ENABLE_COLORPICKER
// picker tool - click on window to print YUV info
static int pick_x = -1, pick_y = -1;
#endif


static int video_window_check(struct video_window_state *w)
{
	XEvent			event;
#ifdef ENABLE_COLORPICKER
	while (XCheckWindowEvent(w->dpy, w->window, ButtonPressMask, &event)) {
		printf("type: %d %d %d,%d\n", event.type,
				event.xbutton.button,
				event.xbutton.x, 
				event.xbutton.y);
		pick_x = event.xbutton.x;
		pick_y = event.xbutton.y;
	}
#endif
	if (XCheckTypedWindowEvent(w->dpy, w->window, ClientMessage, &event)) {
		return -1;
	}
	return 0;
}

static int video_window_update(struct video_window_state *w)
{
	int _x, _y;
	unsigned int	_w, _h, _bw, _d;
	Window _dw;
	
	XGetGeometry(w->dpy, w->window, &_dw, &_x, &_y, &_w, &_h, &_bw, &_d);
	if (w->xv_port != -1) {
		XvShmPutImage(w->dpy, w->xv_port, w->window, w->gc, w->yuv_image,
			0, 0, w->yuv_image->width, w->yuv_image->height,
			0, 0, _w, _h, True);
	} else {
		XShmPutImage(w->dpy, w->window, w->gc, w->image,
			0, 0, 0, 0, w->image->width, w->image->height,
			True);
	}
	//XFlush(dpy);

	return video_window_check(w);
}


#if 0
// rotated copy YUV422 - unfinished
static void rotcpy(void *dest, void *src, unsigned int w, unsigned int h)
{
	// rotate 90 deg
	// w and h are original width and height
	// attempt to write to dest linearly
	int i, j;

	for (j = 0; j < w; j++) {
		unsigned char *line = dest + j*h*2;
		for (i = 0; i < h; i++) {
			line[i*2+1] = *(unsigned char*)(src + 
		}
	}
}
#endif

static void *preview_thread(void *env)
{
	struct stream *strm = (struct stream*) env;
	struct dev *dev = strm->dev;
	int strmidx = strm - dev->strm;
	int rotate = 0;
	unsigned int frames = 0;
	struct timeval t0, t1;

	pthread_mutex_lock(&strm->mutex); // unlocked by get_param
	get_param(strm, 0, 9, 0x80, &rotate);

	pthread_mutex_lock(&strm->mutex);
	dprint("%s: started\n", __func__);

	unsigned int	yuv_width = 640;
	unsigned int	yuv_height = 480;
	struct video_window_state window_state = { .xv_port = -1 };

	unsigned int		i;
	int			vd;
	struct v4l2_requestbuffers req;
	void			*buffer[NUM_BUFS] = {};
	size_t			length[NUM_BUFS] = {};
	struct v4l2_format	fmt = {};
	int			decoded_preview = 0;
	unsigned int pixelformat = 0;

	gettimeofday(&t0, NULL);
	gettimeofday(&t1, NULL);
full_restart: // strm->mutex must be locked before jumping to this label
	strm->restart = 0;
	vd = strm->fd;
	if (vd == -1) {
		goto done;
	}

	if (fmt.type == 0) {
		memset(&fmt, 0, sizeof(fmt));
		fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		if (ioctl(vd, VIDIOC_G_FMT, &fmt) < 0) {
			perror("VIDIOC_G_FMT");
			goto done;
		}
		yuv_width = fmt.fmt.pix.width;
		yuv_height = fmt.fmt.pix.height;
		pixelformat = fmt.fmt.pix.pixelformat;

		if (decoded_preview && pixelformat != V4L2_PIX_FMT_NV12) {
			// decoded preview requires NV12
			pixelformat = V4L2_PIX_FMT_NV12;
		}

		if (fmt.fmt.pix.pixelformat != pixelformat) {
			memset(&fmt, 0, sizeof(fmt));
			fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
			fmt.fmt.pix.width = yuv_width;
			fmt.fmt.pix.height = yuv_height;
			fmt.fmt.pix.pixelformat = pixelformat;
			if (ioctl(vd, VIDIOC_S_FMT, &fmt) < 0) {
				perror("VIDIOC_S_FMT");
				goto done;
			}
			yuv_width = fmt.fmt.pix.width;
			yuv_height = fmt.fmt.pix.height;
			pixelformat = fmt.fmt.pix.pixelformat;

		}
	} else { // use existing format
		if (ioctl(vd, VIDIOC_S_FMT, &fmt) < 0) {
			perror("VIDIOC_S_FMT");
			goto done;
		}
	}
	dprint("preview %s %dx%d size %d bpl %d\n",
			pixelformat == V4L2_PIX_FMT_NV12 ? "NV12" :
			pixelformat == V4L2_PIX_FMT_UYVY ? "UYVY" : "????",
			fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.sizeimage, fmt.fmt.pix.bytesperline);

	if (window_state.xv_port == -1) {
		if (strm->previewing && video_window_init(&window_state,
			      !rotate ? yuv_width : yuv_height,
			      !rotate ? yuv_height : yuv_width,
			      pixelformat == V4L2_PIX_FMT_UYVY ? YUV_FORMAT_UYVY_422 : YUV_FORMAT_YUV_420,
			      dev->preview_xid) < 0) {
			goto done;
		}
	}

	memset(&req, 0, sizeof(req));
	req.count = NUM_BUFS;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		dprint("req .count=%i .type=%x .memory=%x\n", req.count, req.type, req.memory);
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QUERYBUF, &buf) < 0) {
			perror("VIDIOC_QUERYBUF");
			goto done;
		}
		if (buffer[i]) {
			if (munmap(buffer[i], length[i]) < 0)
				perror("munmap");
		}
		length[i] = buf.length;
		buffer[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vd, buf.m.offset);
		if (buffer[i] == MAP_FAILED) {
			perror("mmap");
			goto done;
		}
	}

	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			goto done;
		}
	}
	if (ioctl(vd, VIDIOC_STREAMON, &fmt.type) < 0) {
		perror("VIDIOC_STREAMON");
		goto done;
	}
	strm->errors = 0; // reset error counts
	pthread_mutex_unlock(&strm->mutex);

	while (active_not_restart(strm)) {
		struct pollfd fds = { .fd = vd, .events = POLLIN };
		struct v4l2_buffer buf;
		int rc;

		rc = poll(&fds, 1, 1000/*ms*/);

		if (rc == 0) {
			// first frame timeout could be USB PHY problem?
			if (frames == 0 || strm->errors > 0)
				strm->errors++;
			dprint("preview poll timeout, errs=%d\n", strm->errors);
			// reset USB PHY if stream stopped accidentally, reopen & restart
			check_stream_stopped(strm);
			continue;
		}

		if (rc == 0 || (rc == -1 && errno == EINTR) ||
		    !(fds.revents & POLLIN)) {
			if (strm->previewing && video_window_check(&window_state) < 0)
				break; // window closed
			continue;
		}

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;

		if (ioctl(vd, VIDIOC_DQBUF, &buf) < 0) {
			if (errno == EAGAIN)
				continue;
			perror("VIDIOC_DQBUF");
			break;
		}
		if (0) printf("idx = %d buf.sequence = %u ts = %lu.%06lu size = %d  flags=%x\n", buf.index, buf.sequence,
				buf.timestamp.tv_sec,
				buf.timestamp.tv_usec,
				buf.bytesused,
				buf.flags);

		if (decoded_preview) {
#ifdef ENABLE_DECODED_PREVIEW
			decode(window_state.yuv_image, buffer[buf.index], buf.bytesused);
#endif
		} else if (strm->previewing) {
			if (rotate) {
				//rotcpy(window_state.yuv_image->data, buffer[buf.index], yuv_width, yuv_height);
			} else {
				video_window_copy_buffer(&window_state, buffer[buf.index]);
				//memcpy(window_state.yuv_image->data, buffer[buf.index], buf.bytesused);
			}
		}
		if (buf.bytesused < fmt.fmt.pix.sizeimage || (buf.flags & V4L2_BUF_FLAG_ERROR)) {
			// sometimes the driver gives us incomplete frames
			dprint("incomplete frame? %6d %6d %02x %02x %02x %02x\n", buf.bytesused, fmt.fmt.pix.sizeimage,
					(unsigned char)window_state.yuv_image->data[0],
					(unsigned char)window_state.yuv_image->data[1],
					(unsigned char)window_state.yuv_image->data[2],
					(unsigned char)window_state.yuv_image->data[3]);
			if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
				perror("VIDIOC_QBUF");
				break;
			}
			if (frames > 0) {
				strm->errors++;
			}
			continue;
		}
		if (0 && pixelformat == V4L2_PIX_FMT_UYVY) {
			// test pixel range
			int luma_min = 256, luma_max = -1;
			int chroma_min = 256, chroma_max = -1;
			unsigned int i;
			unsigned char *p = buffer[buf.index];
			for (i = 0; i < buf.bytesused; i+=2) {
				if (luma_min > p[i+1]) luma_min = p[i+1];
				if (luma_max < p[i+1]) luma_max = p[i+1];
				if (chroma_min > p[i]) chroma_min = p[i];
				if (chroma_max < p[i]) chroma_max = p[i];
			}
			printf("range %d-%d %d-%d\n", luma_min, luma_max, chroma_min, chroma_max);
		}
#ifdef ENABLE_COLORPICKER
		if (pick_x != -1 && pick_y != -1 && pixelformat == V4L2_PIX_FMT_UYVY) {
			unsigned char *p = buffer[buf.index] + pick_y * fmt.fmt.pix.bytesperline;
			unsigned char *pa = buffer[buf.index] + (pick_y-1) * fmt.fmt.pix.bytesperline;
			unsigned char *pb = buffer[buf.index] + (pick_y+1) * fmt.fmt.pix.bytesperline;
			unsigned int xy = pick_x * 2 + 1;
			unsigned int xu = (pick_x&~1)*2;
			unsigned int xv = (pick_x&~1)*2+2;
			unsigned char y_min = 16, y_max = 235, c_min = 16, c_max = 240;
			unsigned int // center left   right     above    below
				y = p[xy] + p[xy-4] + p[xy+4] + pa[xy] + pb[xy] - 5*y_min,
				u = p[xu] + p[xu-4] + p[xu+4] + pa[xu] + pb[xu] - 5*c_min,
				v = p[xv] + p[xv-4] + p[xv+4] + pa[xv] + pb[xv] - 5*c_min;
			double y_scale = 1. / ((y_max-y_min) * 5), c_scale = 1./((c_max-c_min)*5);
			// NOTE: these are printed in 0.0-1.0 range, and NOT gamma corrected
			printf("%.3f %.3f %.3f\n", y*y_scale, u*c_scale, v*c_scale);
		}
#endif
		if (frames == 0) {
			// set start time to actual first frame
			gettimeofday(&t0, NULL);
		}

		if (strm->callback) {
			strm->callback(buffer[buf.index], buf.bytesused, dev->board_num, strmidx);
		}
		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			break;
		}
		if (strm->previewing && video_window_update(&window_state) < 0) {
			// window closing
			dprint("preview window closed\n");
			break;
		}
		frames++;
		strm->errors = 0; // reset error count on successful frame
	}

	pthread_mutex_lock(&strm->mutex);
	if (strm->active && strm->restart) {
		// we can get here due to errors after the file descriptor is reopened
		dprint("Preview stream restarting\n");

		// Send the PREVIEW_RESTART command so the device knows to continue
		//set_xu_control(UVC_XU_COMMAND, UVC_XU_COMMAND_PREVIEW_RESTART, strm->dev);
		// not really needed since displaying old frames isn't wanted nor necessary

		goto full_restart;
	}

	gettimeofday(&t1, NULL);
	stop_stream(strm);
	if (ioctl(vd, VIDIOC_STREAMOFF, &fmt.type) < 0) {
		perror("VIDIOC_STREAMOFF");
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		munmap(buffer[i], length[i]);
	}
	req.count = 0;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		goto done;
	}
done:
	dprint("%s: stopped, %d frames, %f fps\n", __func__, frames,
		frames / ((t1.tv_sec - t0.tv_sec) + 1e-6*(t1.tv_usec - t0.tv_usec)));

	if (strm->previewing) {
		video_window_cleanup(&window_state);
	}

	if (strm->active) {
		strm->active = 0;
		pthread_detach(pthread_self());
	}
	pthread_mutex_unlock(&strm->mutex);
	return NULL;
}


// play raw frames, or stream from a file, or convert MP4 to TS
static void *playback_thread(void *env)
{
	struct stream *strm = (struct stream*) env;
	struct dev *dev = strm->dev;
	int strmidx = strm - dev->strm;
	struct v4l2_requestbuffers req;
	int vd;
	void			*buffer[NUM_BUFS] = {};
	size_t			length[NUM_BUFS] = {};
	struct v4l2_format	fmt;
	unsigned int i;
	unsigned long long	total = 0;
	unsigned int bufs_queued = 0;
	struct timeval t0, t1;
	unsigned int frames = 0;

	dprint("%s: started strmidx=%d\n", __func__, strmidx);
	memset(&t0, 0, sizeof(struct timeval));
	memset(&t1, 0, sizeof(struct timeval));
	pthread_mutex_lock(&strm->mutex);

	vd = strm->fd;
	if (vd == -1) {
		//perror(videodev);
		goto done;
	}

	memset(&fmt, 0, sizeof(fmt));
	fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	if (ioctl(vd, VIDIOC_G_FMT, &fmt) < 0) {
		perror("VIDIOC_S_FMT");
		goto done;
	}
	if (strm->playback_mode == MID2263_RECMODE_UYVY) {
		fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY;
		dprint("%s: UYVY %ux%u(%u)\n", __func__,
				fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.bytesperline);
	} else {
		fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
		// the image resolution doesn't matter here
	}
	if (ioctl(vd, VIDIOC_S_FMT, &fmt) < 0) {
		perror("VIDIOC_S_FMT");
		goto done;
	}
	if (ioctl(vd, VIDIOC_G_FMT, &fmt) < 0) {
		perror("VIDIOC_S_FMT");
		goto done;
	}
	dprint("%s: %s %ux%u(%u)\n", __func__, fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_UYVY ? "UYVY" : "MJPEG",
			fmt.fmt.pix.width, fmt.fmt.pix.height, fmt.fmt.pix.bytesperline);


	memset(&req, 0, sizeof(req));
	req.count = NUM_BUFS;
	req.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		dprint("req .count=%i .type=%x .memory=%x\n", req.count, req.type, req.memory);
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QUERYBUF, &buf) < 0) {
			perror("VIDIOC_QUERYBUF");
			goto done;
		}
		if (buffer[i]) {
			if (munmap(buffer[i], length[i]) < 0)
				perror("munmap");
		}
		length[i] = buf.length;
		buffer[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vd, buf.m.offset);
		if (buffer[i] == MAP_FAILED) {
			perror("mmap");
			goto done;
		}
	}

	pthread_mutex_unlock(&strm->mutex);

	gettimeofday(&t0, NULL);

	while (active_not_restart(strm) && !feof(strm->rec_file)) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
		buf.memory = V4L2_MEMORY_MMAP;
		if (bufs_queued < req.count) {
			buf.index = bufs_queued;
		} else {
			struct pollfd fds = { .fd = vd, .events = POLLIN };
			int rc = poll(&fds, 1, 1000/*ms*/);

			if (rc == 0)
				dprint("playback poll timeout\n");
			if (rc == 0 || (rc == -1 && errno == EINTR) ||
			    !(fds.revents & POLLIN))
				continue;

			if (ioctl(vd, VIDIOC_DQBUF, &buf) < 0) {
				if (errno == EAGAIN)
					continue;
				perror("VIDIOC_DQBUF");
				break;
			}
		}

		buf.length = fmt.fmt.pix.sizeimage;
		if (strm->playback_mode == MID2263_RECMODE_UYVY) {
			buf.bytesused = fread(buffer[buf.index], 1, buf.length, strm->rec_file);
		} else {
			buf.bytesused = fread(buffer[buf.index], 1, buf.length, strm->rec_file);
		}
		dprint("%s: buf index=%u bytesused=%u length=%u\n", __func__,
				buf.index, buf.bytesused, buf.length);

		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			break;
		}
		if (bufs_queued < req.count) {
			bufs_queued++;
			if (bufs_queued == req.count) {
				dprint("%s: streamon\n", __func__);
				if (ioctl(vd, VIDIOC_STREAMON, &fmt.type) < 0) {
					perror("VIDIOC_STREAMON");
					pthread_mutex_lock(&strm->mutex);
					goto done;
				}
			}
		}
	}
	if (strm->active) {
		// end of file reached

		if (bufs_queued < req.count) {
			// streamon if we haven't already
			if (ioctl(vd, VIDIOC_STREAMON, &fmt.type) < 0) {
				perror("VIDIOC_STREAMON");
				pthread_mutex_lock(&strm->mutex);
				goto done;
			}
		}

		// need some way to wait until the device is actually done playing
	}

	pthread_mutex_lock(&strm->mutex);
	gettimeofday(&t1, NULL);
	if (ioctl(vd, VIDIOC_STREAMOFF, &fmt.type) < 0) {
		perror("VIDIOC_STREAMOFF");
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		munmap(buffer[i], length[i]);
	}
	req.count = 0;
	req.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		goto done;
	}

done:
	pthread_mutex_unlock(&strm->mutex);
	dprint("%s: stopped, %llu bytes, %f fps\n", __func__, total,
		frames / ((t1.tv_sec - t0.tv_sec) + 1e-6*(t1.tv_usec - t0.tv_usec)));
	return NULL;
}


static void *audio_preview_thread(void *env)
{
	struct stream *strm = (struct stream *) env;
	struct dev *dev = strm->dev;
	//snd_pcm_info_t *info = NULL;
	snd_pcm_t *pcm_play = NULL;
	snd_pcm_hw_params_t *hwparams = NULL;
	snd_pcm_sw_params_t *swparams = NULL;
	snd_pcm_uframes_t period = 1024;
	unsigned int pcm_rate;
	unsigned int buffer_time = 500000; /* ring buffer length in us */
	snd_pcm_uframes_t buffer_size;
	int rc = 0;
	//void *buffer = NULL;
	int card = -1;
	int index = 0;
	struct ringbuffer *rb = NULL;
	struct stream *strm_audcap = GetAudioCapStrm(dev->board_num);
	if (strm_audcap) {
		rb = &strm_audcap->ringbuffer_pcm[IDX_RB_AUDPREVIEW];
		PutStrm(strm_audcap);
	} else {
		return NULL;
	}
	pthread_mutex_lock(&strm->mutex);
	rc = get_xu_control(UVC_XU_AUDIO_SAMPLERATE, (int *)&pcm_rate, dev);
	if (rc < 0) {
		perror("UVC_XU_AUDIO_SAMPLERATE");
		goto done;
	}
	dprint("%s: samplerate=%u\n", __func__, pcm_rate);

	for (snd_card_next(&card); card != -1; snd_card_next(&card)) {
		char *name;
		char *longname;

		rc = snd_card_get_name(card, &name);
		if (rc < 0)
			continue;
		rc = snd_card_get_longname(card, &longname);
		if (rc < 0)
			continue;
		if (strcmp(name, cardname) == 0) {
			if (index == dev->board_num) {
				dprint("%s: found card %s %s\n", __func__, name, longname);
				break;
			}
			index++;
		}
	}
	if (card == -1) {
		dprint("%s: alsa card not found\n", __func__);
		goto done;
	}


	rc = snd_pcm_hw_params_malloc(&hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_malloc(&swparams);
	if (rc < 0) goto done;
	rc = snd_pcm_open(&pcm_play, "default", SND_PCM_STREAM_PLAYBACK, 0);
	if (rc < 0) {
		dprint("%s: alsa snd_pcm_open default rc=%d\n", __func__, rc);
		goto done;
	}
	rc = snd_pcm_hw_params_any(pcm_play, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_resample(pcm_play, hwparams, 1);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_access(pcm_play, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_format(pcm_play, hwparams, SND_PCM_FORMAT_S16_LE);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_channels(pcm_play, hwparams, 2);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_near(pcm_play, hwparams, &pcm_rate, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_period_size_near(pcm_play, hwparams, &period, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_buffer_time_near(pcm_play, hwparams, &buffer_time, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size);
	if (rc < 0) goto done;
	//printf("buffer size %u\n", buffer_size);
	rc = snd_pcm_hw_params(pcm_play, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_current(pcm_play, swparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_start_threshold(pcm_play, swparams, 2 * period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_avail_min(pcm_play, swparams, period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params(pcm_play, swparams);
	if (rc < 0) goto done;

	snd_pcm_start(pcm_play);
	if (rc < 0) goto done;
	dprint("%s: started!\n", __func__);
	strm->audio_previewing = 1;
	while (strm->active && rc != -ENODEV) {
		void * buf;
		pthread_mutex_unlock(&strm->mutex);
		// wait on ringbuffer
		pthread_mutex_lock(&rb->lock);
		pthread_cond_wait(&rb->cond, &rb->lock);
		// while not empty, process frames
		while (rb->head != rb->tail) {
			unsigned int length = ringbuffer_get_len(rb);
			buf = ringbuffer_get_buf(rb, length);
			if (buf == NULL)
				break;
			if (length <= 0 || length > RB_SIZE_AUDIO_PCM) {
				printf("invalid length!\n");
				break;
			}
			// shouldn't need to unlock the ringbuffer here
			//pthread_mutex_unlock(&rb->lock);
			rc = snd_pcm_writei(pcm_play, buf, length/4);
			if (rc == -EPIPE) {
				snd_pcm_prepare(pcm_play);
			} else if (rc < 0) {
				dprint("%s: snd_pcm_writei rc=%d %s\n", __func__, rc, snd_strerror(rc));
				rc = -1;
				//pthread_mutex_lock(&rb->lock);
				break;
			}
			//pthread_mutex_lock(&rb->lock);
			rb->tail += length;
		}
		pthread_mutex_unlock(&rb->lock);
		if (rc == -1)
			break;
#ifdef DBG
		struct timeval tv = {};
		gettimeofday(&tv, NULL);
		if (last != 0) {
			printf("%llu\n", tv.tv_sec * 1000000 + tv.tv_usec - last);
		}
		last = tv.tv_sec * 1000000 + tv.tv_usec;
		fwrite(buf, rc * 4, 1, dbg);
#endif
#if 0
		{
			unsigned char *p = buf;
			fprintf(stderr, "%04x %04x %04x %04x %04x %04x\n",
				p[0] | (p[1] << 8),
				p[2] | (p[3] << 8),
				p[4] | (p[5] << 8),
				p[6] | (p[7] << 8),
				p[8] | (p[9] << 8),
				p[10] | (p[11] << 8));
		}
#endif
		pthread_mutex_lock(&strm->mutex);
	}
done:
#ifdef DBG
	if (dbg) fclose(dbg);
#endif
	dprint("%s: stopped\n", __func__);
	pthread_mutex_unlock(&strm->mutex);

	if (rc < 0) {
		dprint("%s: pcm error %d: %s\n", __func__, rc, snd_strerror(rc));
	}

	if (pcm_play) {
		snd_pcm_drop(pcm_play);
		snd_pcm_close(pcm_play);
	}
	if (hwparams) {
		snd_pcm_hw_params_free(hwparams);
	}
	if (swparams) {
		snd_pcm_sw_params_free(swparams);
	}
	return NULL;
}
// reads audio from PCM, save data to either 1 or 2 FIFOs
// depending if preview is required and/or record AVI with PCM
// audio
static void *audio_buffer_thread(void *env)
{
	struct stream *strm = (struct stream *) env;
	struct dev *dev = strm->dev;
	snd_pcm_info_t *info = NULL;
	snd_pcm_t *pcm_cap = NULL;
	snd_pcm_hw_params_t *hwparams = NULL;
	snd_pcm_sw_params_t *swparams = NULL;
	snd_pcm_uframes_t period = 1024;
	unsigned int pcm_rate;
	unsigned int buffer_time = 500000; /* ring buffer length in us */
	snd_pcm_uframes_t buffer_size;
	int rc = 0;
	void *buffer = NULL;
	int card = -1;
	int index = 0;

	pthread_mutex_lock(&strm->mutex);
	rc = get_xu_control(UVC_XU_AUDIO_SAMPLERATE, (int *)&pcm_rate, dev);
	if (rc < 0) {
		perror("UVC_XU_AUDIO_SAMPLERATE");
		goto done;
	}
	dprint("%s: samplerate=%u\n", __func__, pcm_rate);

	for (snd_card_next(&card); card != -1; snd_card_next(&card)) {
		char *name;
		char *longname;

		rc = snd_card_get_name(card, &name);
		if (rc < 0)
			continue;
		rc = snd_card_get_longname(card, &longname);
		if (rc < 0)
			continue;
		if (strcmp(name, cardname) == 0) {
			if (index == dev->board_num) {
				dprint("%s: found card %s %s\n", __func__, name, longname);
				break;
			}
			index++;
		}
	}
	if (card == -1) {
		dprint("%s: alsa card not found\n", __func__);
		goto done;
	}
	char name[16];
	sprintf(name, "hw:%d,0", card);
	rc = snd_pcm_open(&pcm_cap, name, SND_PCM_STREAM_CAPTURE, 0);
	if (rc < 0) {
		sprintf(name, "plughw:%d,0", card);
		rc = snd_pcm_open(&pcm_cap, name, SND_PCM_STREAM_CAPTURE, 0);
		if (rc < 0) {
			sprintf(name, "dsnoop:%d,0", card);
			rc = snd_pcm_open(&pcm_cap, name, SND_PCM_STREAM_CAPTURE, 0);
			if (rc < 0) {
				dprint("%s: alsa snd_pcm_open %s rc=%d\n", __func__, name, rc);
				goto done;
			}
		}
	}
restart:
	if (info == NULL) {
		rc = snd_pcm_info_malloc(&info);
		if (rc < 0) goto done;
	}
	rc = snd_pcm_info(pcm_cap, info);
	if (rc < 0) goto done;
	dprint("%s: found pcm %s %s\n", __func__,
		snd_pcm_name(pcm_cap),
		snd_pcm_info_get_id(info));
	rc = snd_pcm_hw_params_malloc(&hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_malloc(&swparams);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_any(pcm_cap, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_resample(pcm_cap, hwparams, 1);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_access(pcm_cap, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_format(pcm_cap, hwparams, SND_PCM_FORMAT_S16_LE);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_channels(pcm_cap, hwparams, 2);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_near(pcm_cap, hwparams, &pcm_rate, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_period_size_near(pcm_cap, hwparams, &period, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_buffer_time_near(pcm_cap, hwparams, &buffer_time, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size);
	if (rc < 0) goto done;
	//printf("buffer size %u\n", buffer_size);
	rc = snd_pcm_hw_params(pcm_cap, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_current(pcm_cap, swparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_start_threshold(pcm_cap, swparams, 2 * period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_avail_min(pcm_cap, swparams, period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params(pcm_cap, swparams);
	if (rc < 0) goto done;

	if (buffer == NULL) {
		buffer = malloc(period * 4);
		if (!buffer) {
			dprint("%s: failed to allocate audio buffer\n", __func__);
			goto done;
		}
	}

	rc = snd_pcm_start(pcm_cap);
	if (rc < 0) goto done;

	dprint("%s: started\n", __func__);
#ifdef DBG
	long long last = 0;
	FILE *dbg = fopen("demo.pcm", "w");
#endif
	while (strm->active && rc != -ENODEV) {
		pthread_mutex_unlock(&strm->mutex);
		rc = snd_pcm_readi(pcm_cap, buffer, period);
		//printf("period %lu, rc %d\n", period, rc);
		if (rc == -EPIPE) {
			snd_pcm_prepare(pcm_cap);
		} else if (rc < 0) {
			int new_pcm_rate;
			dprint("%s: snd_pcm_readi rc=%d %s\n", __func__, rc, snd_strerror(rc));
			rc = get_xu_control(UVC_XU_AUDIO_SAMPLERATE, &new_pcm_rate, dev);
			if (rc >= 0 && new_pcm_rate != (int)pcm_rate) {
				pcm_rate = new_pcm_rate;
				dprint("%s: new samplerate=%u\n", __func__, pcm_rate);
				snd_pcm_drop(pcm_cap);
				snd_pcm_close(pcm_cap);
				goto restart;
			}
			break;
		} else {
#ifdef DBG
			struct timeval tv = {};
			gettimeofday(&tv, NULL);

			if (last != 0) {
				printf("%llu\n", tv.tv_sec * 1000000 + tv.tv_usec - last);
			}
			last = tv.tv_sec * 1000000 + tv.tv_usec;
			fwrite(buffer, rc * 4, 1, dbg);
#endif
#if 0
			{
				unsigned char *p = buffer;
			fprintf(stderr, "size:%d, %04x %04x %04x %04x %04x %04x\n",
				rc,
					p[0] | (p[1] << 8),
					p[2] | (p[3] << 8),
					p[4] | (p[5] << 8),
					p[6] | (p[7] << 8),
					p[8] | (p[9] << 8),
					p[10] | (p[11] << 8));
			}
#endif
			//  check if audio record running
			if (strm->audio_recording && strm->ringbuffer_pcm[IDX_RB_RECORD].buf != NULL)
				ringbuffer_audio_add(strm, IDX_RB_RECORD, buffer, rc*4);
			// check if audio preview running.
			if (strm->audio_previewing && strm->ringbuffer_pcm[IDX_RB_AUDPREVIEW].buf != NULL)
				ringbuffer_audio_add(strm, IDX_RB_AUDPREVIEW, buffer, rc*4);

		}
		pthread_mutex_lock(&strm->mutex);
	}
done:
#ifdef DBG
	if (dbg) fclose(dbg);
#endif
	dprint("%s: stopped\n", __func__);
	pthread_mutex_unlock(&strm->mutex);
	if (rc < 0) {
		dprint("%s: pcm error %d: %s\n", __func__, rc, snd_strerror(rc));
	}
	if (info) {
		snd_pcm_info_free(info);
	}
	if (pcm_cap) {
		snd_pcm_drop(pcm_cap);
		snd_pcm_close(pcm_cap);
	}
	if (swparams) {
		snd_pcm_sw_params_free(swparams);
	}
	if (buffer) {
		free(buffer);
	}

	return NULL;
}

static void interpolate_line(unsigned char *pdst, unsigned char *line, int len)
{
	int j;
	for (j = 0; j < len; j++) {
		pdst[j] = (line[j] + line[j+len]) / 2;
	}
	return;
}


static void *preview_3364_thread(void *env)
{
	struct stream *strm = (struct stream*) env;
	struct dev *dev = strm->dev;
	int strmidx = strm - dev->strm;

	pthread_mutex_lock(&strm->mutex);
	dprint("%s: started\n", __func__);

	unsigned int	yuv_width;
	unsigned int	yuv_height;
	struct video_window_state window_state = { .xv_port = -1 };

	unsigned int		i;
	int			vd;
	struct v4l2_requestbuffers req;
	void			*buffer[NUM_BUFS];
	size_t			length[NUM_BUFS];
	struct v4l2_format	fmt;

	for (i = 0; i < NUM_BUFS; i++) {
		buffer[i] = NULL;
		length[i] = 0;
	}
	vd = strm->fd;
	if (vd == -1) {
		goto done;
	}
	memset(&fmt, 0, sizeof(fmt));
	fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	if (ioctl(vd, VIDIOC_G_FMT, &fmt) < 0) {
		perror("VIDIOC_G_FMT");
		goto done;
	}

	int locked;

	do {
		if (GetVideoInfo((int*)&yuv_width, (int*)&yuv_height, NULL, NULL, &locked, dev) < 0) {
			dprint("Failed to get video info\n");
			goto done;
		}
		if (!strm->active)
			goto done;
		if (!locked) {
			pthread_mutex_unlock(&strm->mutex);
			dprint("waiting for lock\n");
			sleep(1);
			pthread_mutex_lock(&strm->mutex);
		}
	} while (!locked);

	//printf("width %d, height %d\n", yuv_width, yuv_height);
	if (fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_UYVY ||
	    fmt.fmt.pix.width != yuv_width ||
	    fmt.fmt.pix.height != yuv_height) {
		//printf("setting UYVY fmt %dx%d\n", yuv_width, yuv_height);
		memset(&fmt, 0, sizeof(fmt));
		fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		fmt.fmt.pix.width = yuv_width;
		fmt.fmt.pix.height = yuv_height;
		fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//UYVY;
		if (ioctl(vd, VIDIOC_S_FMT, &fmt) < 0) {
			perror("VIDIOC_S_FMT");
			goto done;
		}
	}

	memset(&req, 0, sizeof(req));
	req.count = NUM_BUFS;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		dprint("req .count=%i .type=%x .memory=%x\n", req.count, req.type, req.memory);
		goto done;
	}

	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QUERYBUF, &buf) < 0) {
			perror("VIDIOC_QUERYBUF");
			goto done;
		}
		length[i] = buf.length;
		buffer[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, vd, buf.m.offset);
		if (buffer[i] == MAP_FAILED) {
			perror("mmap");
			goto done;
		}
	}
	if (strm->previewing && video_window_init(&window_state, yuv_width, yuv_height, YUV_FORMAT_YUY2_422, dev->preview_xid) < 0) {
		goto done;
	}

	for (i = 0; i < req.count; i++) {
		struct v4l2_buffer buf;

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;
		buf.index = i;
		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			goto done;
		}
	}
	if (ioctl(vd, VIDIOC_STREAMON, &fmt.type) < 0) {
		perror("VIDIOC_STREAMON");
		goto done;
	}

	while (strm->active) {
		struct pollfd fds = { .fd = vd, .events = POLLIN };
		struct v4l2_buffer buf;
		int rc;

		pthread_mutex_unlock(&strm->mutex);
		rc = poll(&fds, 1, 1000/*ms*/);
		pthread_mutex_lock(&strm->mutex);

		if (rc == 0)
			dprint("preview hd poll timeout\n");
		if (rc == 0 || (rc == -1 && errno == EINTR) ||
		    !(fds.revents & POLLIN)) {
			if (strm->previewing && video_window_check(&window_state) < 0)
				break; // window closed
			continue;
		}

		memset(&buf, 0, sizeof(buf));
		buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_MMAP;

		if (ioctl(vd, VIDIOC_DQBUF, &buf) < 0) {
			if (errno == EAGAIN)
				continue;
			perror("VIDIOC_DQBUF");
			break;
		}

		if (strm->previewing) {
			//printf("preview mode %d, interlaced %d\n", strm->fullsize_preview_mode, strm->is_interlaced);
			if (!strm->is_interlaced || strm->fullsize_preview_mode == MID3364_FIELD_SEQ) {
				video_window_copy_buffer(&window_state, buffer[buf.index]);
				//memcpy(window_state.yuv_image->data, buffer[buf.index], buf.bytesused);
			} else {
				int i;
				unsigned char *pfield2, *pfield1;
				unsigned char *pdst;
				int linelen = yuv_width*2;
				pfield1 = (unsigned char *) buffer[buf.index] + yuv_width*yuv_height;
				pfield2 = (unsigned char *) buffer[buf.index];
				pdst = (unsigned char *) window_state.yuv_image->data;
				// is_interlaced
				if (strm->fullsize_preview_mode == MID3364_INTERLACE) {
					for (i = 0; i < (int)yuv_height/2; i++) {
						memcpy(pdst, pfield1, linelen);
						pdst += linelen;
						memcpy(pdst, pfield2, linelen);
						pdst += linelen;
						pfield1 += linelen;
						pfield2 += linelen;
					}
				} else {
					unsigned char *pfield;
					pfield = (strm->fullsize_preview_mode == MID3364_TOP_FIELD) ? pfield1 : pfield2;
					for (i = 0; i <= (int) ((yuv_height/2) - 1); i++) {
						// copy line
						memcpy(pdst, pfield, linelen);
						// interpolate line
						pdst += linelen;
						if ( i < (int) ((yuv_height / 2 ) - 1))
							interpolate_line(pdst, pfield, linelen);
						else
							memcpy(pdst, pfield, linelen);

						pdst += linelen;
						pfield += linelen;
					}
				}
			}
		}
		if (buf.bytesused < fmt.fmt.pix.sizeimage) {
			// sometimes the driver gives us incomplete frames
			dprint("incomplete frame? %6d %6d %02x %02x %02x %02x\n", buf.bytesused, fmt.fmt.pix.sizeimage,
					(unsigned char)window_state.yuv_image->data[0],
					(unsigned char)window_state.yuv_image->data[1],
					(unsigned char)window_state.yuv_image->data[2],
					(unsigned char)window_state.yuv_image->data[3]);

			if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
				perror("VIDIOC_QBUF");
				break;
			}
			continue;
		}

		if (strm->callback) {
			strm->callback(buffer[buf.index], buf.bytesused, dev->board_num, strmidx);
		}
		if (ioctl(vd, VIDIOC_QBUF, &buf) < 0) {
			perror("VIDIOC_QBUF");
			break;
		}
		//fprintf(stdout, "timestamp %lu:%03lu\n", buf.timestamp.tv_sec, buf.timestamp.tv_usec / 1000);

		if (strm->previewing && video_window_update(&window_state) < 0) {
			// window closing
			dprint("preview window closed\n");
			break;
		}
	}

	if (ioctl(vd, VIDIOC_STREAMOFF, &fmt.type) < 0) {
		perror("VIDIOC_STREAMOFF");
		goto done;
	}
	for (i = 0; i < req.count; i++) {
		if (length[i] != 0)
			munmap(buffer[i], length[i]);
	}
	req.count = 0;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_MMAP;
	if (ioctl(vd, VIDIOC_REQBUFS, &req) < 0) {
		perror("VIDIOC_REQBUFS");
		goto done;
	}
done:
	dprint("%s: stopped\n", __func__);

	if (strm->previewing) {
		video_window_cleanup(&window_state);
	}

	if (strm->active) {
		strm->active = 0;
		pthread_detach(pthread_self());
	}
	pthread_mutex_unlock(&strm->mutex);
	return NULL;
}

static void *log_thread(void *env)
{
	struct stream *strm = (struct stream *)env;
	int rc = 0;

	while (strm->fd >= 0) {
		char tmp[61] = {};

		rc = get_xu_control_data(UVC_XU_LOG_DATA, (__u8 *)tmp, strm->dev);
		if (rc < 0)
			break;
		if (tmp[0]) {
			fprintf(stderr, "%s", tmp);
		} else {
			fflush(stderr);
			ms_sleep(10);
		}
	}
	return NULL;
}

/* Overlay code *********************************************************/



struct RLE_image {
    unsigned char *data;
    int len; // data length
    int w;
    int h;
    int run; // current run length
    int rgb; // current run color
};

static int png_alpha = 0;

#define MAX_RUN (255 + 16)

int encodeRLE_init(struct RLE_image *rle, int w, int h, int rgb)
{
    rle->w = w;
    rle->h = h;
    rle->len = 10;
    rle->data = malloc(rle->len + w * h);
    if (rle->data) {
        memcpy(rle->data, "RLE ", 4);
        rle->data[rle->len-6] = w & 0xff;
        rle->data[rle->len-5] = (w >> 8) & 0xff;
        rle->data[rle->len-4] = h & 0xff;
        rle->data[rle->len-3] = (h >> 8) & 0xff;

        // convert RGB888 to RGB565
        rgb = ((rgb & 0xf80000) >> 8) |
              ((rgb & 0x00fc00) >> 5) |
              ((rgb & 0x0000f8) >> 3);
        rle->data[rle->len-2] = rgb & 0xff;
        rle->data[rle->len-1] = (rgb >> 8) & 0xff;
    }
    rle->run = 0;
    rle->rgb = 0;
    return rle->data ? 0 : -1;
}

static inline void finish_run(struct RLE_image *rle, int rgb, int run)
{
    if (rle->run > 15) {
        rle->data[rle->len++] = rle->rgb;
        rle->data[rle->len++] = rle->run - 16;
    } else if (rle->run > 0) {
        rle->data[rle->len++] = rle->rgb | rle->run;
    }
    rle->run = run;
    rle->rgb = rgb;
}

void encodeRLE_rgba(struct RLE_image *rle, unsigned char *data)
{
    int i = 0;
    int x;

    for (x = 0; x < rle->w; ++x) {
        int rgb = 0;
        unsigned char alpha = 0;

        rgb += (int)data[i++] * (0x10000 * 299 / 1000);   // red
        rgb += (int)data[i++] * (0x10000 * 587 / 1000);   // green
        rgb += (int)data[i++] * (0x10000 * 114 / 1000);   // blue
        rgb >>= 17; // rgb now 0x00..0x7f
        alpha = data[i++];   // alpha

        if (png_alpha) {
            if (alpha <= 0x10) {
                rgb = 0;
            } else {
                rgb = (rgb << 1) & 0xf0;
                if (rgb == 0) rgb = 0x10;
            }
        } else if (rgb <= 0xf || alpha == 0) {
            rgb = (alpha >> 1) & 0x70;
        } else {
            rgb = (rgb & 0x70) + 0x80;
        }
        if (rle->rgb != rgb || rle->run == MAX_RUN)
            finish_run(rle, rgb, 1);
        else
            rle->run++;
    }
    if (rle->rgb != 0)
        finish_run(rle, 0, 0);
}

void encodeRLE_done(struct RLE_image *rle)
{
    finish_run(rle, 0, 0);
    rle->data = realloc(rle->data, rle->len);
}



/* PNG image functions ********************************************************/
#ifdef MAGICKWAND_MAJOR_VERSION

#include <alloca.h>
#include <png.h>

struct io_ptr {
    unsigned char *src;
    png_size_t len;
};

/* We need some custom io functions to read a png file from memory */
static void user_read_data(png_structp png_ptr, png_bytep data, png_size_t length)
{
    struct io_ptr *iop = png_get_io_ptr(png_ptr);

    if (iop->len == 0)
        return;
    if (iop->len < length)
        length = iop->len;
    memcpy(data, iop->src, length);
    iop->src += length;
    iop->len -= length;
}

static void user_error_fn(png_structp png_ptr, png_const_charp error_msg)
{
    (void)png_ptr;
    fprintf(stderr, "png error: %s\n", error_msg);
}

static void user_warning_fn(png_structp png_ptr, png_const_charp warning_msg)
{
    (void)png_ptr;
    fprintf(stderr, "png warning: %s\n", warning_msg);
}

static int loadPNG_RLE(const unsigned char *src, unsigned int len, struct RLE_image *rle)
{
    struct io_ptr iop = { (void*)src + 8, len };
    png_structp png_ptr;
    png_infop info_ptr;
    png_uint_32 width = 0;
    png_uint_32 height = 0;
    int bitDepth = 0;
    int colorType = -1;
    unsigned int y;
    png_bytep data;

    if (len < 8 || !png_check_sig((unsigned char*)src, 8))
        return -1;

    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, user_error_fn, user_warning_fn);
    if (!png_ptr)
        return -1;
    info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return -1;
    }

    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return -1;
    }
    png_set_read_fn(png_ptr, &iop, user_read_data);

    png_set_sig_bytes(png_ptr, 8);

    png_read_info(png_ptr, info_ptr);
    if (png_get_IHDR(png_ptr, info_ptr,
                      &width,
                      &height,
                      &bitDepth,
                      &colorType,
                      NULL, NULL, NULL) == 0) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return -1;
    }
    if (0) fprintf(stderr, "png width=%u height=%u bitDepth=%d colorType=%d\n",
              (unsigned int)width, (unsigned int)height, bitDepth, colorType);
    if (colorType != PNG_COLOR_TYPE_RGB_ALPHA && colorType != PNG_COLOR_TYPE_RGB && colorType != PNG_COLOR_TYPE_GA) {
        dprint("unsupported png width=%d height=%d bitDepth=%d colorType=%d\n",
              width, height, bitDepth, colorType);
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        return -1;
    }

    data = alloca(png_get_rowbytes(png_ptr, info_ptr));
    if (!data) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return -1;
    }

    if (encodeRLE_init(rle, width, height, 0) != 0) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        return -1;
    }
    png_bytep tmp = NULL;
    if (colorType == PNG_COLOR_TYPE_GA)
	    tmp = malloc(width * 4);

    for (y = 0; y < height; ++y) {
        png_read_row(png_ptr, data, NULL);
        if (colorType == PNG_COLOR_TYPE_RGB_ALPHA) {
            encodeRLE_rgba(rle, data);
        } else if (colorType == PNG_COLOR_TYPE_RGB) {
		printf("RGB not done\n");
            //encodeRLE_rgb(rle, data);
        } else if (colorType == PNG_COLOR_TYPE_GA) {
		unsigned int x;
		// gray/alpha byte pairs
		for (x = 0; x < width; x++) {
			tmp[x*4+0] = data[x*2];
			tmp[x*4+1] = data[x*2];
			tmp[x*4+2] = data[x*2];
			tmp[x*4+3] = data[x*2+1];
		}
		encodeRLE_rgba(rle, tmp);
	}
    }
    free(tmp);
    encodeRLE_done(rle);
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    return 0;
}
#endif

/* Overlay API functions ******************************************************/

MID2263_API int S2263_OverlayText(const char *text, unsigned int mode, int x, int y, 
	int devid, int strmidx)
{
	unsigned char *data;
	unsigned int length;
	int rc;
	int hdr_len = 6;

	length = hdr_len + strlen(text);
	data = malloc(length);
	if (!data) return -ENOMEM;
	memcpy(data, "TEXT", 4);
	data[4] = mode & 0xff;
	data[5] = (mode >> 8) & 0xff;
	dprint("mode = %02x%02x\n", data[5], data[4]);
	memcpy(data + hdr_len, text, length - hdr_len);

	rc = S2263_OverlayData(data, length, x, y, 0, devid, strmidx, 0);
	free(data);
	return rc;
}

#ifdef MAGICKWAND_MAJOR_VERSION
MID2263_API int S2263_OverlayStyledText(const char *text, const char *font_name, int font_size, 
	unsigned int style, int x, int y, int alpha, 
	int devid, int strmidx, int channel)
{
	int rc;

        MagickWand *wand = NULL;
        MagickWand *c_wand = NULL;
        DrawingWand *d_wand = NULL;
        PixelWand *p_wand = NULL;
        double *fm = NULL;
        unsigned char *pixels;
        int width, height;
	int font_bold = style & OVERLAY_STYLE_BOLD;
	int font_italic = style & OVERLAY_STYLE_ITALIC;
	int font_underline = style & OVERLAY_STYLE_UNDERLINE;
	int font_shadow = style & OVERLAY_STYLE_SHADOW;
	int font_outline = style & OVERLAY_STYLE_OUTLINE;
	int font_alpha = alpha;
	int font_y;
        double pad = (font_shadow ? 2 : 
                font_outline ? 1+(double)font_size/8 : 1);

        int i;
        static int initialized = 0;

        if (!initialized) {
                MagickWandGenesis();
                initialized = 1;
        }

        wand = NewMagickWand();
        d_wand = NewDrawingWand();
        p_wand = NewPixelWand();

        PixelSetColor(p_wand, "none");
        MagickNewImage(wand, 1, 1, p_wand);

        DrawSetFont(d_wand, font_name);
        DrawSetFontSize(d_wand, font_size);
        DrawSetTextAntialias(d_wand, MagickTrue);
        DrawSetStrokeWidth(d_wand, pad);

	DrawSetFontWeight(d_wand, font_bold ? 900 : 100);
        DrawSetFontStyle(d_wand, font_italic ? ItalicStyle : NormalStyle);
        // Underline bug: Stroke width ignored? Looks bad
        DrawSetTextDecoration(d_wand, font_underline ? UnderlineDecoration : NoDecoration);

        fm = MagickQueryFontMetrics(wand, d_wand, text);
        if (!fm) {
                dprint("Failed to initialize font\n");
                return -1;
        }
        //printf("metrics: %g %g %g %g %g %g %g %g %g %g %g %g %g\n",
        //      fm[0], fm[1], fm[2], fm[3], fm[4], fm[5], fm[6], 
        //      fm[7], fm[8], fm[9], fm[10], fm[11], fm[12]);
        width = (int)fm[4] + 3 * pad + (font_shadow ? 5 : 0);
        height = (int)fm[5] + 2 * pad + (font_shadow ? 5 : 0);
        font_y = (int)(fm[5] + fm[3] - pad);
        free(fm);
        fm = NULL;

        MagickRemoveImage(wand);
        MagickNewImage(wand, width, height, p_wand);

        PixelSetColor(p_wand, "white");
        DrawSetFillColor(d_wand, p_wand);


        if (font_outline) {
                PixelSetColor(p_wand, "black");
                PixelSetAlpha(p_wand, (double)font_outline/8);
                DrawSetStrokeColor(d_wand, p_wand);
                DrawAnnotation(d_wand, pad, font_y, (unsigned char*) text);
        }

        PixelSetColor(p_wand, "none");
        DrawSetStrokeColor(d_wand, p_wand);
        DrawAnnotation(d_wand, pad, font_y, (unsigned char*) text);
        MagickDrawImage(wand, d_wand);

	if (font_shadow) {
                MagickResetImagePage(wand, "");
                c_wand = CloneMagickWand(wand);
                PixelSetColor(p_wand, "none");
                PixelSetAlpha(p_wand, 1.0);
                MagickSetImageBackgroundColor(wand, p_wand);
                MagickShadowImage(wand, 70, 4, 5, 5);
                MagickCompositeImage(wand, c_wand, OverCompositeOp,
#if MAGICKWAND_CHECK_VERSION(7,0,0)
				MagickTrue,
#endif
				5, 5);

                DestroyMagickWand(c_wand);
        }

        c_wand = NewMagickWand();
        PixelSetColor(p_wand, "none");
        PixelSetAlpha(p_wand, (double)font_alpha/8);
        MagickNewImage(c_wand, width, height, p_wand);
        MagickCompositeImage(c_wand, wand, OverCompositeOp,
#if MAGICKWAND_CHECK_VERSION(7,0,0)
			MagickTrue,
#endif
			0, 0);

        pixels = malloc(width * height * 4);
        if (!pixels) {
                dprint("Failed to allocate buffer\n");
                return -1;
        }

        MagickExportImagePixels(c_wand, 0, 0, width, height, "RGBA",
                CharPixel, pixels);

        MagickRemoveImage(wand);
        DestroyMagickWand(wand);
        MagickRemoveImage(c_wand);
        DestroyMagickWand(c_wand);
        DestroyDrawingWand(d_wand);
        DestroyPixelWand(p_wand);

        struct RLE_image rle;
        if (encodeRLE_init(&rle, width, height, 0) != 0) {
                dprint("Failed to allocate memory\n");
                return -1;
        }

        for (i = 0; i < height; i++)
                encodeRLE_rgba(&rle, pixels + i*width*4);

        encodeRLE_done(&rle);
        free(pixels);

	rc = S2263_OverlayData(rle.data, rle.len, x, y, 0, devid, strmidx, channel);
	free(rle.data);
	return rc;
}
#endif

MID2263_API int S2263_OverlayFile(const char *filename, int x, int y, int alpha, 
	int devid, int strmidx, int channel)
{
	unsigned char *data = NULL;
	long length;
	int rc = -1;
	int fd;
	struct stat st;

	fd = open(filename, O_RDONLY);
	if (fd < 0) {
		rc = fd;
		dprint("Failed to open %s\n", filename);
		goto done;
	}
	rc = fstat(fd, &st);
	if (rc < 0) {
		dprint("Failed fstat on %s\n", filename);
		goto done;
	}
	if (st.st_size > 4 * 1024 * 1024) {
		rc = -1;
		dprint("File too big %s\n", filename);
		goto done;
	}
	length = st.st_size;
	data = malloc(length);
	if (!data) {
		rc = -ENOMEM;
		dprint("Failed to allocate memory, length=%u\n", length);
		goto done;
	}
	rc = read(fd, data, length);
	if (rc < 0) {
		dprint("Failed to read file data\n");
		goto done;
	}

	rc = S2263_OverlayData(data, length, x, y, alpha, devid, strmidx, channel);
done:
	if (fd >= 0) close(fd);
	free(data);
	return rc;
}


#ifdef USE_ASYNC_TRANSFER
static void overlay_transfer_cb(struct libusb_transfer *xfer)
{
	printf("%s: len=%d/%d\n", __func__, xfer->actual_length, xfer->length);
	switch (xfer->status) {
	case LIBUSB_TRANSFER_COMPLETED: break;
	case LIBUSB_TRANSFER_ERROR: dprint("Overlay transfer failed\n"); break;
	case LIBUSB_TRANSFER_TIMED_OUT: dprint("Overlay transfer timed out\n"); break;
	case LIBUSB_TRANSFER_CANCELLED: dprint("Overlay transfer cancelled\n"); break;
	case LIBUSB_TRANSFER_STALL: dprint("Overlay transfer stalled\n"); break;
	case LIBUSB_TRANSFER_NO_DEVICE: dprint("Overlay transfer device disconnected\n"); break;
	case LIBUSB_TRANSFER_OVERFLOW: dprint("Overlay transfer overflow\n"); break;
	}
}
#endif


MID2263_API int S2263_OverlayData(const unsigned char *data, unsigned int length, 
	int x, int y, int alpha, int devid, int strmidx, int channel)
{
	struct stream *strm = strmidx != STRM_OUT ? GetStrm(devid, strmidx) : GetOutputStrm(devid);
	int rc = 0;
	void *needs_free = NULL;

	if (!strm)
		return -ENODEV;

#ifdef MAGICKWAND_MAJOR_VERSION
	if (length >= 4 && strmidx < 2 &&
		data[0] == 0x89 &&
		data[1] == 0x50 &&
		data[2] == 0x4e &&
		data[3] == 0x47 && 0) { // PNG image - convert on host
		struct RLE_image rle;

		png_alpha = strm->dev->fw_ver >= 3587 ? alpha < 9 : 1;
		if (loadPNG_RLE(data, length, &rle) < 0) {
			png_alpha = 0;
			dprint("PNG error %p len=%u\n", data, length);
			PutStrm(strm);
			return -EINVAL;
		}
		if (png_alpha && strm->dev->fw_ver >= 3587) {
			// new code to use embedded alpha
			png_alpha = 0;
			rle.data[3] = 'A'; // RLEA
		}
		//printf("PNG %p len=%u -> %u\n", data, length, rle.len);
		data = rle.data;
		length = rle.len;
		needs_free = (void*)data;
	}
#endif

	struct overlay_header ovl;
	ovl.length = length;
	ovl.id  = (strmidx << 4) | channel;
	ovl.alpha = alpha;
	ovl.x = htole16(x);
	ovl.y = htole16(y);

	const unsigned char *orig_data = data;

	if (strm->dev->usb_dev) {
		struct dev *dev = strm->dev;
		int rc = libusb_claim_interface(dev->usb_dev, dev->usb_iface);
		if (rc == 0) {
			unsigned char *mem;

			if (length < 4) // special case for OverlayMove & OverlayShow
				length = 0; // no data to send

			int len = 2 + sizeof(ovl) + length;
#ifndef USE_ASYNC_TRANSFER
			if ((len % dev->usb_ep_max_packet_size) == 0) {
				//dprintf("overlay len=%d  NEEDS ZLP (wMaxPacketSize=%d)\n", len, dev->usb_ep_max_packet_size);
				// NOTE: This is to prevent even multiple of wMaxPacketSize transfers
				// from combining with the next transfer (causing it to be lost.)
				// Firmware will remove extra byte when it exceeds ovl.length
				len++;
			}
#endif

			mem = malloc(len);
			if (mem) {
				mem[0] = 2;
				mem[1] = UVC_STREAM_EOF;
				memcpy(mem + 2, &ovl, sizeof(ovl));
				memcpy(mem + 2 + sizeof(ovl), data, length);
				//dprint("strmidx=%d ovl.id=%x %d,%d alpha=%d\n", strmidx, ovl.id, ovl.x, ovl.y, ovl.alpha);
#ifndef USE_ASYNC_TRANSFER
				int xfer = 0;
				// NOTE: this does not handle ZLP for even multiple of wMaxPacketSize transfers
				rc = libusb_bulk_transfer(dev->usb_dev, dev->usb_ep,
					mem, len,
					&xfer, 1000/*ms timeout*/);
				if (rc)
					dprint("libusb_bulk_transfer len=%d xfer=%d rc=%d\n", len, xfer, rc);

				free(mem);
#else
				// FIXME: this should send the ZLP but in testing it sends incomplete
				// transfers and the callback isn't executed...
				// NOTE: transfers are asynchronous, and errors won't be reported back.
				struct libusb_transfer *xfer = libusb_alloc_transfer(0);

				if (!xfer) {
					rc = -ENOMEM;
				} else {
					libusb_fill_bulk_transfer(xfer,
						dev->usb_dev, // dev_handle
						dev->usb_ep, // endpoint,
						mem, // buffer
						len, // length
						overlay_transfer_cb, // callback
						NULL, // user_data,
						1000); // timeout in ms
					xfer->flags |=	LIBUSB_TRANSFER_SHORT_NOT_OK |
							LIBUSB_TRANSFER_FREE_BUFFER |
							LIBUSB_TRANSFER_FREE_TRANSFER |
							LIBUSB_TRANSFER_ADD_ZERO_PACKET;
					printf("xfer %p %x %x %x %d %d %d %d %p %p %p %d\n",
						xfer->dev_handle,
						xfer->flags,
						xfer->endpoint,
						xfer->type,
						xfer->timeout,
						xfer->status,
						xfer->length,
						xfer->actual_length,
						xfer->callback,
						xfer->user_data,
						xfer->buffer,
						xfer->num_iso_packets);
					rc = libusb_submit_transfer(xfer);
				}

				if (rc != 0) {
					dprint("%s: failed to submit transfer rc=%d\n", __func__, rc);
					if (xfer) libusb_free_transfer(xfer);
					free(mem);
				}
#endif
			} else {
				dprint("%s: failed to allocate memory\n", __func__);
				rc = -ENOMEM;
			}
			libusb_release_interface(dev->usb_dev, dev->usb_iface);
			goto done;
		}
	}

	for(;;) {
		// send header
		rc = set_xu_control_data(UVC_XU_OVERLAY_HEADER, (__u8*) &ovl, strm->dev);
		if (rc < 0)
			dprint("%s: overlay header rc=%d %s\n", __func__, rc, strerror(errno));

		if (length < 4)
			break; // no data to send

		int ctrl_id = UVC_XU_OVERLAY_DATA60;
#define MAX_SIZE 60
		// send max size chunks
		while (rc >= 0 && length >= MAX_SIZE) {
			rc = set_xu_control_data(ctrl_id, data, strm->dev);
			if (rc < 0)
				dprint("%s: overlay data rc=%d %s\n", __func__, rc, strerror(errno));
			data += MAX_SIZE;
			length -= MAX_SIZE;
		}
		// pad leftovers
		if (rc >= 0 && length > 0) {
			unsigned char tmp[MAX_SIZE];
			memcpy(tmp, data, length);
			memset(tmp + length, 0, MAX_SIZE - length);
			rc = set_xu_control_data(ctrl_id, tmp, strm->dev);
			if (rc < 0)
				dprint("%s: overlay data rc=%d %s\n", __func__, rc, strerror(errno));
		}
		if (strm->dev->fw_ver < SRAY_XU_OVERLAY_CHECK_VERSION)
			break; // before check was added

		// check to see if overlay was fully received
		int recvd = 0;
		rc = get_xu_control(UVC_XU_OVERLAY_CHECK, &recvd, strm->dev);
		// recvd will be zero if fully received
		if (rc < 0 || recvd == 0) {
			break;
		} else {
			dprint("overlay received %d/%d bytes, resending...\n", recvd, ovl.length);
		}

		// retry, device did not completely receive overlay data (seen on raspberry pi)
		data = orig_data;
		length = ovl.length;
	}

	if (rc == 0 && png_alpha) { // PNG (update alpha as needed on fw < 3587)
		unsigned char tmp[61] = {}; // overlay data may access beyond allocated length (lint check)
		png_alpha = 0;
		S2263_OverlayData(tmp, 2, x, y, alpha, devid, strmidx, channel);
	}

	//dprint("overlay length=%d ch=%d id=%d alpha=%d x=%d y=%d\n",
	//	ovl.length, ovl.id >> 4, ovl.id&15, alpha, x, y);
done:
	free(needs_free); // free allocated data as needed
	PutStrm(strm);
	return rc;
}

MID2263_API int S2263_OverlayMove(int x, int y, int alpha, int devid, int strmidx, int channel)
{
	return S2263_OverlayData(NULL, 3, x, y, alpha, devid, strmidx, channel);
}

MID2263_API int S2263_OverlayShow(int visible, int devid, int strmidx, int channel)
{
	if (visible < 0 || visible > 2)
		return -EINVAL;
	return S2263_OverlayData(NULL, visible, 0, 0, 0, devid, strmidx, channel);
}

MID2263_API int S2263_SetClock(const struct timeval *tv, int devid)
{
	struct stream *strm = GetStrm(devid, 0);
	if (!strm)
		return -ENODEV;
	struct clock {
		 __u8 year;   // minus 1900, similar to struct tm
		 __u8 month;  // 1-12
		 __u8 day;    // 1-31
		 __u8 hour;   // 0-23
		 __u8 minute; // 0-59
		 __u8 second; // 0-59
	} c;
	struct tm tm;
	time_t t = tv ? tv->tv_sec : time(NULL);
	localtime_r(&t, &tm);
	c.year = tm.tm_year; // year - 1900
	c.month = tm.tm_mon+1;
	c.day = tm.tm_mday;
	c.hour = tm.tm_hour;
	c.minute = tm.tm_min;
	c.second = tm.tm_sec;

	int ret = set_xu_control_data(UVC_XU_CLOCK, (__u8*)&c, strm->dev);
	PutStrm(strm);
	return ret;
}


MID2263_API int S2263_SetMP4FastStart(int max_minutes, int interval_seconds, int devid, int strmidx)
{
	struct stream *strm = GetStrm(devid, strmidx);
	int rc = 0;

	if (!strm)
		return -ENODEV;
	if (max_minutes < 0 || interval_seconds < 0) {
		rc = -EINVAL;
	} else {
		strm->mp4_max_minutes = max_minutes;
		strm->mp4_interval_seconds = interval_seconds;
	}

	PutStrm(strm);
	return rc;
}

static int s3364_get_xudata(int id, __u8 *buf, int devid)
{
	struct stream *strm = Get3364Strm(devid);
	int rc;
	if (!strm)
		return -ENODEV;
	if (!buf) {
		PutStrm(strm);
		return -EINVAL;
	}
	memset(buf, 0, 10);
	rc = get_xu3364_control_data(id, buf, strm->dev);
	PutStrm(strm);
	return rc;
}


static int s3364_set_xudata(int id, __u8 *buf, int devid)
{
	struct stream *strm = Get3364Strm(devid);
	int rc;
	if (!strm)
		return -ENODEV;
	if (!buf) {
		PutStrm(strm);
		return -EINVAL;
	}
	rc = set_xu3364_control_data(id, buf, strm->dev);
	PutStrm(strm);
	return rc;
}



MID2263_API int S3364_GetSerialNumberMfg(int *serial_number, int devid)
{
	int rc;
	__u8 buf[10];

	rc = s3364_get_xudata(XU3364_SERIAL_NUM, buf, devid);

	if (rc < 0)
		return rc;

	if (!serial_number) {
		dprint("%s: serial_number is NULL\n", __func__);
		return -EINVAL;
	}

	*serial_number = buf[0] + (buf[1] << 8) + (buf[2] << 16) + (buf[3] << 24);
	return rc;
}


MID2263_API int S3364_GetUserData(unsigned char *data, int devid)
{
	int rc;
	__u8 buf[10];
	rc = s3364_get_xudata(XU3364_EEPROM_USER, buf, devid);
	memcpy(data, buf, 10);
	return rc;
}


MID2263_API int S3364_SetUserData(unsigned char *data, int devid)
{
	int rc;
	__u8 buf[10];
	memcpy(buf, data, 10);
	rc = s3364_set_xudata(XU3364_EEPROM_USER, buf, devid);
	return rc;
}


MID2263_API int S3364_GetBoardType(int *type, int devid)
{
	int rc;
	__u8 buf[10];
	rc = s3364_get_xudata(XU3364_EXTRA_INFO, buf, devid);
	if (rc < 0)
		return rc;
	*type = buf[1];
	return rc;
}


MID2263_API int S3364_GetUsbSpeed(int *speed, int devid)
{
	int rc;
	__u8 buf[10];

	rc = s3364_get_xudata(XU3364_EXTRA_INFO, buf, devid);
	*speed = buf[0];
	return rc;
}


MID2263_API int S3364_SetUsbSpeed(int *speed, int devid)
{
	int rc;
	__u8 buf[10];

	rc = s3364_get_xudata(XU3364_EXTRA_INFO, buf, devid);
	*speed = buf[0];
	return rc;
}



MID2263_API int S3364_SetVideoInterlaced(int bInterlaced, int devid)
{
	int rc;
	__u8 buf[10];
	struct stream *strm;

	buf[0] = 0;
	buf[1] = bInterlaced;
	rc = s3364_set_xudata(XU3364_SET_FORMAT, buf, devid);

	strm = Get3364Strm(devid);
	if (!strm)
		return -ENODEV;
	strm->is_interlaced = bInterlaced;
	PutStrm(strm);

	return rc;
}



MID2263_API int S3364_SetVideoSize(int w, int h, int devid)
{
	struct stream *strm;
	struct v4l2_format fmt = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE };

	strm = Get3364Strm(devid);
	if (!strm)
		return -ENODEV;

	fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
	fmt.fmt.pix.width = w;
	fmt.fmt.pix.height = h;

	(void) xioctl(strm->fd, VIDIOC_S_FMT, &fmt);
	PutStrm(strm);
	return 0;
}


MID2263_API int S3364_SetPreviewInterlaceMode(MID3364_INTERLACE_MODE mode, int devid)
{
	struct stream *strm = Get3364Strm(devid);

	if (!strm)
		return -ENODEV;
	//printf("set HD preview interlace mode %x\n", mode);
	if ((unsigned)mode > MID3364_INTERLACE_MODE_LAST)
		return -EINVAL;

	strm->fullsize_preview_mode = mode;
	PutStrm(strm);
	return 0;
}

MID2263_API int S3364_GetPreviewInterlaceMode(MID3364_INTERLACE_MODE *mode, int devid)
{
	struct stream *strm = Get3364Strm(devid);

	if (!strm)
		return -ENODEV;

	*mode = (MID3364_INTERLACE_MODE) strm->fullsize_preview_mode;
	PutStrm(strm);
	return 0;
}


MID2263_API int S3364_StartFullSizePreview(int devid)
{
	struct stream *strm = Get3364Strm(devid);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	if (strm->active) {
		PutStrm(strm);
		return -EBUSY;
	}
	rc = -pthread_create(
		&strm->thread,
		NULL,
		preview_3364_thread,
		strm);
	strm->active = (rc == 0);
	strm->previewing = strm->active;
	PutStrm(strm);
	return rc;
}


MID2263_API int S3364_StopStream(int devid)
{
	struct stream *strm = Get3364Strm(devid);
	int rc = 0;
	if (!strm)
		return -ENODEV;
	strm->active = 0;
	if (strm->recording) {
		if (strm->rec_file) {
			strm->ringbuffer.exit = 1;
			pthread_cond_signal(&strm->ringbuffer.cond);
		}
		pthread_mutex_unlock(&strm->mutex);
		pthread_join(strm->thread, NULL);
		if (strm->rec_file)
			pthread_join(strm->file_thread, NULL);
		pthread_mutex_lock(&strm->mutex);
		if (strm->rec_file) {
			fclose(strm->rec_file);
			strm->rec_file = NULL;
			free(strm->ringbuffer.buf);
			strm->ringbuffer.buf = NULL;
		}
		strm->recording = 0;
		rb_simple_destroy(&strm->rb_audio);
	} else if (strm->previewing) {

		pthread_mutex_unlock(&strm->mutex);
		pthread_join(strm->thread, NULL);
		pthread_mutex_lock(&strm->mutex);
		strm->previewing = 0;
	}
	PutStrm(strm);//lint !e456 // execution path with different mutex lock state
	return rc;
}


MID2263_API int S3364_StopFullSizePreview(int devid)
{
	return S3364_StopStream(devid);
}



#if 0
// original audio preview thread. saved for reference only.
static void *audio_preview_thread_orig(void *env)
{
	struct stream *strm = (struct stream *) env;
	struct dev *dev = strm->dev;
	snd_pcm_info_t *info = NULL;
	snd_pcm_t *pcm_cap = NULL;
	snd_pcm_t *pcm_play = NULL;
	snd_pcm_hw_params_t *hwparams = NULL;
	snd_pcm_sw_params_t *swparams = NULL;
	snd_pcm_uframes_t period = 1024;
	unsigned int pcm_rate;
	unsigned int buffer_time = 500000; /* ring buffer length in us */
	snd_pcm_uframes_t buffer_size;
	int rc = 0;
	void *buffer = NULL;
	int card = -1;
	int index = 0;

	pthread_mutex_lock(&strm->mutex);


	rc = get_xu_control(UVC_XU_AUDIO_SAMPLERATE, (int *)&pcm_rate, dev);
	if (rc < 0) {
		perror("UVC_XU_AUDIO_SAMPLERATE");
		goto done;
	}
	dprint("%s: samplerate=%u\n", __func__, pcm_rate);

	for (snd_card_next(&card); card != -1; snd_card_next(&card)) {
		char *name;
		char *longname;

		rc = snd_card_get_name(card, &name);
		if (rc < 0)
			continue;
		rc = snd_card_get_longname(card, &longname);
		if (rc < 0)
			continue;
		if (strcmp(name, cardname) == 0) {
			if (index == dev->board_num) {
				dprint("%s: found card %s %s\n", __func__, name, longname);
				break;
			}
			index++;
		}
	}
	if (card == -1) {
		dprint("%s: alsa card not found\n", __func__);
		goto done;
	}

	char name[16];
	sprintf(name, "hw:%d,0", card);
	rc = snd_pcm_open(&pcm_cap, name, SND_PCM_STREAM_CAPTURE, 0);
	if (rc < 0) {
		sprintf(name, "plughw:%d,0", card);
		rc = snd_pcm_open(&pcm_cap, name, SND_PCM_STREAM_CAPTURE, 0);
		if (rc < 0) {
			sprintf(name, "dsnoop:%d,0", card);
			rc = snd_pcm_open(&pcm_cap, name, SND_PCM_STREAM_CAPTURE, 0);
			if (rc < 0) {
				dprint("%s: alsa snd_pcm_open %s rc=%d\n", __func__, name, rc);
				goto done;
			}
		}
	}

restart:

	if (info == NULL) {
		rc = snd_pcm_info_malloc(&info);
		if (rc < 0) goto done;
	}
	rc = snd_pcm_info(pcm_cap, info);
	if (rc < 0) goto done;

	dprint("%s: found pcm %s %s\n", __func__,
		snd_pcm_name(pcm_cap),
		snd_pcm_info_get_id(info));

	rc = snd_pcm_hw_params_malloc(&hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_malloc(&swparams);
	if (rc < 0) goto done;

	rc = snd_pcm_hw_params_any(pcm_cap, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_resample(pcm_cap, hwparams, 1);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_access(pcm_cap, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_format(pcm_cap, hwparams, SND_PCM_FORMAT_S16_LE);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_channels(pcm_cap, hwparams, 2);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_near(pcm_cap, hwparams, &pcm_rate, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_period_size_near(pcm_cap, hwparams, &period, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_buffer_time_near(pcm_cap, hwparams, &buffer_time, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size);
	if (rc < 0) goto done;
	printf("buffer size %lu\n", buffer_size);
	rc = snd_pcm_hw_params(pcm_cap, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_current(pcm_cap, swparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_start_threshold(pcm_cap, swparams, 2 * period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_avail_min(pcm_cap, swparams, period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params(pcm_cap, swparams);
	if (rc < 0) goto done;

	if (buffer == NULL) {
		buffer = malloc(period * 4);
		if (!buffer) {
			dprint("%s: failed to allocate audio buffer\n", __func__);
			goto done;
		}
	}

	rc = snd_pcm_open(&pcm_play, "default", SND_PCM_STREAM_PLAYBACK, 0);
	if (rc < 0) {
		dprint("%s: alsa snd_pcm_open default rc=%d\n", __func__, rc);
		goto done;
	}

	rc = snd_pcm_hw_params_any(pcm_play, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_resample(pcm_play, hwparams, 1);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_access(pcm_play, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_format(pcm_play, hwparams, SND_PCM_FORMAT_S16_LE);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_channels(pcm_play, hwparams, 2);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_rate_near(pcm_play, hwparams, &pcm_rate, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_period_size_near(pcm_play, hwparams, &period, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_set_buffer_time_near(pcm_play, hwparams, &buffer_time, NULL);
	if (rc < 0) goto done;
	rc = snd_pcm_hw_params_get_buffer_size(hwparams, &buffer_size);
	if (rc < 0) goto done;
	//printf("buffer size %u\n", buffer_size);
	rc = snd_pcm_hw_params(pcm_play, hwparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_current(pcm_play, swparams);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_start_threshold(pcm_play, swparams, 2 * period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params_set_avail_min(pcm_play, swparams, period);
	if (rc < 0) goto done;
	rc = snd_pcm_sw_params(pcm_play, swparams);
	if (rc < 0) goto done;

	rc = snd_pcm_start(pcm_cap);
	if (rc < 0) goto done;
	snd_pcm_start(pcm_play);
	if (rc < 0) goto done;

	dprint("%s: started\n", __func__);
#ifdef DBG
	long long last = 0;
	FILE *dbg = fopen("demo.pcm", "w");
#endif
	while (strm->active && rc != -ENODEV) {
		pthread_mutex_unlock(&strm->mutex);

		rc = snd_pcm_readi(pcm_cap, buffer, period);

		if (rc == -EPIPE) {
			snd_pcm_prepare(pcm_cap);
		} else if (rc < 0) {
			int new_pcm_rate;
			dprint("%s: snd_pcm_readi rc=%d %s\n", __func__, rc, snd_strerror(rc));

			rc = get_xu_control(UVC_XU_AUDIO_SAMPLERATE, &new_pcm_rate, dev);
			if (rc >= 0 && new_pcm_rate != (int)pcm_rate) {
				pcm_rate = new_pcm_rate;
				dprint("%s: new samplerate=%u\n", __func__, pcm_rate);
				snd_pcm_drop(pcm_cap);
				snd_pcm_close(pcm_cap);
				snd_pcm_drop(pcm_play);
				snd_pcm_close(pcm_play);
				goto restart;
			}
			break;
		} else {
#ifdef DBG
			struct timeval tv = {};
			gettimeofday(&tv, NULL);

			if (last != 0) {
				printf("%llu\n", tv.tv_sec * 1000000 + tv.tv_usec - last);
			}
			last = tv.tv_sec * 1000000 + tv.tv_usec;
			fwrite(buffer, rc * 4, 1, dbg);
#endif
#if 0
			{
				unsigned char *p = buffer;
			fprintf(stderr, "%04x %04x %04x %04x %04x %04x\n",
					p[0] | (p[1] << 8),
					p[2] | (p[3] << 8),
					p[4] | (p[5] << 8),
					p[6] | (p[7] << 8),
					p[8] | (p[9] << 8),
					p[10] | (p[11] << 8));
			}
#endif
			rc = snd_pcm_writei(pcm_play, buffer, rc);
			if (rc == -EPIPE) {
				snd_pcm_prepare(pcm_play);
			} else if (rc < 0) {
				dprint("%s: snd_pcm_writei rc=%d %s\n", __func__, rc, snd_strerror(rc));
				break;
			}
		}
		pthread_mutex_lock(&strm->mutex);
	}
done:
#ifdef DBG
	if (dbg) fclose(dbg);
#endif
	dprint("%s: stopped\n", __func__);
	pthread_mutex_unlock(&strm->mutex);

	if (rc < 0) {
		dprint("%s: pcm error %d: %s\n", __func__, rc, snd_strerror(rc));
	}

	if (info) {
		snd_pcm_info_free(info);
	}
	if (pcm_cap) {
		snd_pcm_drop(pcm_cap);
		snd_pcm_close(pcm_cap);
	}
	if (pcm_play) {
		snd_pcm_drop(pcm_play);
		snd_pcm_close(pcm_play);
	}
	if (hwparams) {
		snd_pcm_hw_params_free(hwparams);
	}
	if (swparams) {
		snd_pcm_sw_params_free(swparams);
	}
	if (buffer) {
		free(buffer);
	}

	return NULL;
}
#endif

