Initial import of libhdhomerun

http://download.silicondust.com/hdhomerun/libhdhomerun_20120405.tgz

Change-Id: Ie1fc20f22827fdcf007d9b2ad93552baf3df398b
diff --git a/libhdhomerun/Makefile b/libhdhomerun/Makefile
new file mode 100755
index 0000000..dfda53f
--- /dev/null
+++ b/libhdhomerun/Makefile
@@ -0,0 +1,59 @@
+
+LIBSRCS += hdhomerun_channels.c
+LIBSRCS += hdhomerun_channelscan.c
+LIBSRCS += hdhomerun_control.c
+LIBSRCS += hdhomerun_debug.c
+LIBSRCS += hdhomerun_device.c
+LIBSRCS += hdhomerun_device_selector.c
+LIBSRCS += hdhomerun_discover.c
+LIBSRCS += hdhomerun_os_posix.c
+LIBSRCS += hdhomerun_pkt.c
+LIBSRCS += hdhomerun_sock_posix.c
+LIBSRCS += hdhomerun_video.c
+
+CC    := $(CROSS_COMPILE)gcc
+STRIP := $(CROSS_COMPILE)strip
+
+CFLAGS += -Wall -O2 -Wmissing-declarations -Wmissing-prototypes -Wstrict-prototypes -Wpointer-arith
+LDFLAGS += -lpthread
+SHARED = -shared -Wl,-soname,libhdhomerun$(LIBEXT)
+
+ifeq ($(OS),Windows_NT)
+  BINEXT := .exe
+  LIBEXT := .dll
+  LDFLAGS += -liphlpapi
+else
+  OS := $(shell uname -s)
+  LIBEXT := .so
+  ifeq ($(OS),Linux)
+    LDFLAGS += -lrt
+  endif
+  ifeq ($(OS),SunOS)
+    LDFLAGS += -lsocket
+  endif
+  ifeq ($(OS),Darwin)
+    CFLAGS += -arch i386 -arch ppc
+    LIBEXT := .dylib
+    SHARED := -dynamiclib -install_name libhdhomerun$(LIBEXT)
+  endif
+endif
+
+all : hdhomerun_config$(BINEXT) libhdhomerun$(LIBEXT)
+
+hdhomerun_config$(BINEXT) : hdhomerun_config.c $(LIBSRCS)
+	$(CC) $(CFLAGS) $+ $(LDFLAGS) -o $@
+	$(STRIP) $@
+
+libhdhomerun$(LIBEXT) : $(LIBSRCS)
+	$(CC) $(CFLAGS) -fPIC -DDLL_EXPORT $(SHARED) $+ $(LDFLAGS) -o $@
+
+clean :
+	-rm -f hdhomerun_config$(BINEXT)
+	-rm -f libhdhomerun$(LIBEXT)
+
+distclean : clean
+
+%:
+	@echo "(ignoring request to make $@)"
+
+.PHONY: all list clean distclean
diff --git a/libhdhomerun/README b/libhdhomerun/README
new file mode 100755
index 0000000..9b5d853
--- /dev/null
+++ b/libhdhomerun/README
@@ -0,0 +1,41 @@
+/*
+ * README
+ *
+ * Copyright © 2005-2009 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+Top level include file: hdhomerun.h
+
+Top level API: hdhomerun_device. See hdhomerun_device.h for documentation.
+
+The hdhomerun_device API should be used rather than the low level control and video APIs required with previous versions.
+
+Additional libraries required:
+- pthread
+- iphlpapi (windows only)
diff --git a/libhdhomerun/hdhomerun.h b/libhdhomerun/hdhomerun.h
new file mode 100755
index 0000000..affd4e2
--- /dev/null
+++ b/libhdhomerun/hdhomerun.h
@@ -0,0 +1,44 @@
+/*
+ * hdhomerun.h
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun_os.h"
+#include "hdhomerun_types.h"
+#include "hdhomerun_pkt.h"
+#include "hdhomerun_sock.h"
+#include "hdhomerun_debug.h"
+#include "hdhomerun_discover.h"
+#include "hdhomerun_control.h"
+#include "hdhomerun_video.h"
+#include "hdhomerun_channels.h"
+#include "hdhomerun_channelscan.h"
+#include "hdhomerun_device.h"
+#include "hdhomerun_device_selector.h"
diff --git a/libhdhomerun/hdhomerun_channels.c b/libhdhomerun/hdhomerun_channels.c
new file mode 100755
index 0000000..c78ebce
--- /dev/null
+++ b/libhdhomerun/hdhomerun_channels.c
@@ -0,0 +1,399 @@
+/*
+ * hdhomerun_channels.c
+ *
+ * Copyright © 2007-2008 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+struct hdhomerun_channel_entry_t {
+	struct hdhomerun_channel_entry_t *next;
+	struct hdhomerun_channel_entry_t *prev;
+	uint32_t frequency;
+	uint16_t channel_number;
+	char name[16];
+};
+
+struct hdhomerun_channel_list_t {
+	struct hdhomerun_channel_entry_t *head;
+	struct hdhomerun_channel_entry_t *tail;
+};
+
+struct hdhomerun_channelmap_range_t {
+	uint16_t channel_range_start;
+	uint16_t channel_range_end;
+	uint32_t frequency;
+	uint32_t spacing;
+};
+
+struct hdhomerun_channelmap_record_t {
+	const char *channelmap;
+	const struct hdhomerun_channelmap_range_t *range_list;
+	const char *channelmap_scan_group;
+	const char *countrycodes;
+};
+
+/* AU antenna channels. Channels {6, 7, 8, 9, 9A} are numbered {5, 6, 7, 8, 9} by the HDHomeRun. */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_au_bcast[] = {
+	{  5,  12, 177500000, 7000000},
+	{ 21,  69, 480500000, 7000000},
+	{  0,   0,         0,       0}
+};
+
+/* EU antenna channels. */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_eu_bcast[] = {
+	{  5,  12, 177500000, 7000000},
+	{ 21,  69, 474000000, 8000000},
+	{  0,   0,         0,       0}
+};
+
+/* EU cable channels. No common standard - use frequency in MHz for channel number. */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_eu_cable[] = {
+	{ 50, 998,  50000000, 1000000},
+	{  0,   0,         0,       0}
+};
+
+/* KR cable channels. */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_kr_cable[] = {
+	{  2,   4,  57000000, 6000000},
+	{  5,   6,  79000000, 6000000},
+	{  7,  13, 177000000, 6000000},
+	{ 14,  22, 123000000, 6000000},
+	{ 23, 153, 219000000, 6000000},
+	{  0,   0,         0,       0}
+};
+
+/* US antenna channels. */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_bcast[] = {
+	{  2,   4,  57000000, 6000000},
+	{  5,   6,  79000000, 6000000},
+	{  7,  13, 177000000, 6000000},
+	{ 14,  69, 473000000, 6000000},
+	{  0,   0,         0,       0}
+};
+
+/* US cable channels. */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_cable[] = {
+	{  2,   4,  57000000, 6000000},
+	{  5,   6,  79000000, 6000000},
+	{  7,  13, 177000000, 6000000},
+	{ 14,  22, 123000000, 6000000},
+	{ 23,  94, 219000000, 6000000},
+	{ 95,  99,  93000000, 6000000},
+	{100, 158, 651000000, 6000000},
+	{  0,   0,         0,       0}
+};
+
+/* US cable channels (HRC). */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_hrc[] = {
+	{  2,   4,  55752700, 6000300},
+	{  5,   6,  79753900, 6000300},
+	{  7,  13, 175758700, 6000300},
+	{ 14,  22, 121756000, 6000300},
+	{ 23,  94, 217760800, 6000300},
+	{ 95,  99,  91754500, 6000300},
+	{100, 158, 649782400, 6000300},
+	{  0,   0,         0,       0}
+};
+
+/* US cable channels (IRC). */
+static const struct hdhomerun_channelmap_range_t hdhomerun_channelmap_range_us_irc[] = {
+	{  2,   4,  57012500, 6000000},
+	{  5,   6,  81012500, 6000000},
+	{  7,  13, 177012500, 6000000},
+	{ 14,  22, 123012500, 6000000},
+	{ 23,  41, 219012500, 6000000},
+	{ 42,  42, 333025000, 6000000},
+	{ 43,  94, 339012500, 6000000},
+	{ 95,  97,  93012500, 6000000},
+	{ 98,  99, 111025000, 6000000},
+	{100, 158, 651012500, 6000000},
+	{  0,   0,         0,       0}
+};
+
+static const struct hdhomerun_channelmap_record_t hdhomerun_channelmap_table[] = {
+	{"au-bcast", hdhomerun_channelmap_range_au_bcast, "au-bcast",               "AU"},
+	{"au-cable", hdhomerun_channelmap_range_eu_cable, "au-cable",               "AU"},
+	{"eu-bcast", hdhomerun_channelmap_range_eu_bcast, "eu-bcast",               "EU PA"},
+	{"eu-cable", hdhomerun_channelmap_range_eu_cable, "eu-cable",               "EU"},
+	{"tw-bcast", hdhomerun_channelmap_range_us_bcast, "tw-bcast",               "TW"},
+	{"tw-cable", hdhomerun_channelmap_range_us_cable, "tw-cable",               "TW"},
+
+	{"kr-bcast", hdhomerun_channelmap_range_us_bcast, "kr-bcast",               "KR"},
+	{"kr-cable", hdhomerun_channelmap_range_kr_cable, "kr-cable",               "KR"},
+	{"us-bcast", hdhomerun_channelmap_range_us_bcast, "us-bcast",               "CA US"},
+	{"us-cable", hdhomerun_channelmap_range_us_cable, "us-cable us-hrc us-irc", "CA PA US"},
+	{"us-hrc",   hdhomerun_channelmap_range_us_hrc  , "us-cable us-hrc us-irc", "CA PA US"},
+	{"us-irc",   hdhomerun_channelmap_range_us_irc,   "us-cable us-hrc us-irc", "CA PA US"},
+
+	{NULL,       NULL,                                NULL,                     NULL}
+};
+
+const char *hdhomerun_channelmap_get_channelmap_from_country_source(const char *countrycode, const char *source)
+{
+	bool_t country_found = FALSE;
+
+	const struct hdhomerun_channelmap_record_t *record = hdhomerun_channelmap_table;
+	while (record->channelmap) {
+		if (!strstr(record->countrycodes, countrycode)) {
+			record++;
+			continue;
+		}
+
+		if (strstr(record->channelmap, source)) {
+			return record->channelmap;
+		}
+
+		country_found = TRUE;
+		record++;
+	}
+
+	if (!country_found) {
+		return hdhomerun_channelmap_get_channelmap_from_country_source("EU", source);
+	}
+
+	return NULL;
+}
+
+const char *hdhomerun_channelmap_get_channelmap_scan_group(const char *channelmap)
+{
+	const struct hdhomerun_channelmap_record_t *record = hdhomerun_channelmap_table;
+	while (record->channelmap) {
+		if (strstr(channelmap, record->channelmap)) {
+			return record->channelmap_scan_group;
+		}
+		record++;
+	}
+
+	return NULL;
+}
+
+uint16_t hdhomerun_channel_entry_channel_number(struct hdhomerun_channel_entry_t *entry)
+{
+	return entry->channel_number;
+}
+
+uint32_t hdhomerun_channel_entry_frequency(struct hdhomerun_channel_entry_t *entry)
+{
+	return entry->frequency;
+}
+
+const char *hdhomerun_channel_entry_name(struct hdhomerun_channel_entry_t *entry)
+{
+	return entry->name;
+}
+
+struct hdhomerun_channel_entry_t *hdhomerun_channel_list_first(struct hdhomerun_channel_list_t *channel_list)
+{
+	return channel_list->head;
+}
+
+struct hdhomerun_channel_entry_t *hdhomerun_channel_list_last(struct hdhomerun_channel_list_t *channel_list)
+{
+	return channel_list->tail;
+}
+
+struct hdhomerun_channel_entry_t *hdhomerun_channel_list_next(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry)
+{
+	return entry->next;
+}
+
+struct hdhomerun_channel_entry_t *hdhomerun_channel_list_prev(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry)
+{
+	return entry->prev;
+}
+
+uint32_t hdhomerun_channel_list_total_count(struct hdhomerun_channel_list_t *channel_list)
+{
+	uint32_t count = 0;
+
+	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
+	while (entry) {
+		count++;
+		entry = hdhomerun_channel_list_next(channel_list, entry);
+	}
+
+	return count;
+}
+
+uint32_t hdhomerun_channel_list_frequency_count(struct hdhomerun_channel_list_t *channel_list)
+{
+	uint32_t count = 0;
+	uint32_t last_frequency = 0;
+
+	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
+	while (entry) {
+		if (entry->frequency != last_frequency) {
+			last_frequency = entry->frequency;
+			count++;
+		}
+
+		entry = hdhomerun_channel_list_next(channel_list, entry);
+	}
+
+	return count;
+}
+
+uint32_t hdhomerun_channel_frequency_round(uint32_t frequency, uint32_t resolution)
+{
+	frequency += resolution / 2;
+	return (frequency / resolution) * resolution;
+}
+
+uint32_t hdhomerun_channel_frequency_round_normal(uint32_t frequency)
+{
+	return hdhomerun_channel_frequency_round(frequency, 125000);
+}
+
+uint32_t hdhomerun_channel_number_to_frequency(struct hdhomerun_channel_list_t *channel_list, uint16_t channel_number)
+{
+	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
+	while (entry) {
+		if (entry->channel_number == channel_number) {
+			return entry->frequency;
+		}
+
+		entry = hdhomerun_channel_list_next(channel_list, entry);
+	}
+
+	return 0;
+}
+
+uint16_t hdhomerun_channel_frequency_to_number(struct hdhomerun_channel_list_t *channel_list, uint32_t frequency)
+{
+	frequency = hdhomerun_channel_frequency_round_normal(frequency);
+
+	struct hdhomerun_channel_entry_t *entry = hdhomerun_channel_list_first(channel_list);
+	while (entry) {
+		if (entry->frequency == frequency) {
+			return entry->channel_number;
+		}
+		if (entry->frequency > frequency) {
+			return 0;
+		}
+
+		entry = hdhomerun_channel_list_next(channel_list, entry);
+	}
+
+	return 0;
+}
+
+static void hdhomerun_channel_list_build_insert(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry)
+{
+	struct hdhomerun_channel_entry_t *prev = NULL;
+	struct hdhomerun_channel_entry_t *next = channel_list->head;
+
+	while (next) {
+		if (next->frequency > entry->frequency) {
+			break;
+		}
+
+		prev = next;
+		next = next->next;
+	}
+
+	entry->prev = prev;
+	entry->next = next;
+
+	if (prev) {
+		prev->next = entry;
+	} else {
+		channel_list->head = entry;
+	}
+
+	if (next) {
+		next->prev = entry;
+	} else {
+		channel_list->tail = entry;
+	}
+}
+
+static void hdhomerun_channel_list_build_range(struct hdhomerun_channel_list_t *channel_list, const char *channelmap, const struct hdhomerun_channelmap_range_t *range)
+{
+	uint16_t channel_number;
+	for (channel_number = range->channel_range_start; channel_number <= range->channel_range_end; channel_number++) {
+		struct hdhomerun_channel_entry_t *entry = (struct hdhomerun_channel_entry_t *)calloc(1, sizeof(struct hdhomerun_channel_entry_t));
+		if (!entry) {
+			return;
+		}
+
+		entry->channel_number = channel_number;
+		entry->frequency = range->frequency + ((uint32_t)(channel_number - range->channel_range_start) * range->spacing);
+		entry->frequency = hdhomerun_channel_frequency_round_normal(entry->frequency);
+		sprintf(entry->name, "%s:%u", channelmap, entry->channel_number);
+
+		hdhomerun_channel_list_build_insert(channel_list, entry);
+	}
+}
+
+static void hdhomerun_channel_list_build_ranges(struct hdhomerun_channel_list_t *channel_list, const struct hdhomerun_channelmap_record_t *record)
+{
+	const struct hdhomerun_channelmap_range_t *range = record->range_list;
+	while (range->frequency) {
+		hdhomerun_channel_list_build_range(channel_list, record->channelmap, range);
+		range++;
+	}
+}
+
+void hdhomerun_channel_list_destroy(struct hdhomerun_channel_list_t *channel_list)
+{
+	while (channel_list->head) {
+		struct hdhomerun_channel_entry_t *entry = channel_list->head;
+		channel_list->head = entry->next;
+		free(entry);
+	}
+
+	free(channel_list);
+}
+
+struct hdhomerun_channel_list_t *hdhomerun_channel_list_create(const char *channelmap)
+{
+	struct hdhomerun_channel_list_t *channel_list = (struct hdhomerun_channel_list_t *)calloc(1, sizeof(struct hdhomerun_channel_list_t));
+	if (!channel_list) {
+		return NULL;
+	}
+
+	const struct hdhomerun_channelmap_record_t *record = hdhomerun_channelmap_table;
+	while (record->channelmap) {
+		if (!strstr(channelmap, record->channelmap)) {
+			record++;
+			continue;
+		}
+
+		hdhomerun_channel_list_build_ranges(channel_list, record);
+		record++;
+	}
+
+	if (!channel_list->head) {
+		free(channel_list);
+		return NULL;
+	}
+
+	return channel_list;
+}
diff --git a/libhdhomerun/hdhomerun_channels.h b/libhdhomerun/hdhomerun_channels.h
new file mode 100755
index 0000000..7772ce8
--- /dev/null
+++ b/libhdhomerun/hdhomerun_channels.h
@@ -0,0 +1,64 @@
+/*
+ * hdhomerun_channels.h
+ *
+ * Copyright © 2007-2008 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hdhomerun_channel_entry_t;
+struct hdhomerun_channel_list_t;
+
+extern LIBTYPE const char *hdhomerun_channelmap_get_channelmap_from_country_source(const char *countrycode, const char *source);
+extern LIBTYPE const char *hdhomerun_channelmap_get_channelmap_scan_group(const char *channelmap);
+
+extern LIBTYPE uint16_t hdhomerun_channel_entry_channel_number(struct hdhomerun_channel_entry_t *entry);
+extern LIBTYPE uint32_t hdhomerun_channel_entry_frequency(struct hdhomerun_channel_entry_t *entry);
+extern LIBTYPE const char *hdhomerun_channel_entry_name(struct hdhomerun_channel_entry_t *entry);
+
+extern LIBTYPE struct hdhomerun_channel_list_t *hdhomerun_channel_list_create(const char *channelmap);
+extern LIBTYPE void hdhomerun_channel_list_destroy(struct hdhomerun_channel_list_t *channel_list);
+
+extern LIBTYPE struct hdhomerun_channel_entry_t *hdhomerun_channel_list_first(struct hdhomerun_channel_list_t *channel_list);
+extern LIBTYPE struct hdhomerun_channel_entry_t *hdhomerun_channel_list_last(struct hdhomerun_channel_list_t *channel_list);
+extern LIBTYPE struct hdhomerun_channel_entry_t *hdhomerun_channel_list_next(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry);
+extern LIBTYPE struct hdhomerun_channel_entry_t *hdhomerun_channel_list_prev(struct hdhomerun_channel_list_t *channel_list, struct hdhomerun_channel_entry_t *entry);
+extern LIBTYPE uint32_t hdhomerun_channel_list_total_count(struct hdhomerun_channel_list_t *channel_list);
+extern LIBTYPE uint32_t hdhomerun_channel_list_frequency_count(struct hdhomerun_channel_list_t *channel_list);
+
+extern LIBTYPE uint32_t hdhomerun_channel_frequency_round(uint32_t frequency, uint32_t resolution);
+extern LIBTYPE uint32_t hdhomerun_channel_frequency_round_normal(uint32_t frequency);
+extern LIBTYPE uint32_t hdhomerun_channel_number_to_frequency(struct hdhomerun_channel_list_t *channel_list, uint16_t channel_number);
+extern LIBTYPE uint16_t hdhomerun_channel_frequency_to_number(struct hdhomerun_channel_list_t *channel_list, uint32_t frequency);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_channelscan.c b/libhdhomerun/hdhomerun_channelscan.c
new file mode 100755
index 0000000..8c1f8db
--- /dev/null
+++ b/libhdhomerun/hdhomerun_channelscan.c
@@ -0,0 +1,350 @@
+/*
+ * hdhomerun_channelscan.c
+ *
+ * Copyright © 2007-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+struct hdhomerun_channelscan_t {
+	struct hdhomerun_device_t *hd;
+	uint32_t scanned_channels;
+	struct hdhomerun_channel_list_t *channel_list;	
+	struct hdhomerun_channel_entry_t *next_channel;
+};
+
+struct hdhomerun_channelscan_t *channelscan_create(struct hdhomerun_device_t *hd, const char *channelmap)
+{
+	struct hdhomerun_channelscan_t *scan = (struct hdhomerun_channelscan_t *)calloc(1, sizeof(struct hdhomerun_channelscan_t));
+	if (!scan) {
+		return NULL;
+	}
+
+	scan->hd = hd;
+
+	scan->channel_list = hdhomerun_channel_list_create(channelmap);
+	if (!scan->channel_list) {
+		free(scan);
+		return NULL;
+	}
+
+	scan->next_channel = hdhomerun_channel_list_last(scan->channel_list);
+	return scan;
+}
+
+void channelscan_destroy(struct hdhomerun_channelscan_t *scan)
+{
+	free(scan);
+}
+
+static int channelscan_find_lock(struct hdhomerun_channelscan_t *scan, uint32_t frequency, struct hdhomerun_channelscan_result_t *result)
+{
+	/* Set channel. */
+	char channel_str[64];
+	sprintf(channel_str, "auto:%ld", (unsigned long)frequency);
+
+	int ret = hdhomerun_device_set_tuner_channel(scan->hd, channel_str);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	/* Wait for lock. */
+	ret = hdhomerun_device_wait_for_lock(scan->hd, &result->status);
+	if (ret <= 0) {
+		return ret;
+	}
+	if (!result->status.lock_supported) {
+		return 1;
+	}
+
+	/* Wait for symbol quality = 100%. */
+	uint64_t timeout = getcurrenttime() + 5000;
+	while (1) {
+		ret = hdhomerun_device_get_tuner_status(scan->hd, NULL, &result->status);
+		if (ret <= 0) {
+			return ret;
+		}
+
+		if (result->status.symbol_error_quality == 100) {
+			return 1;
+		}
+
+		if (getcurrenttime() >= timeout) {
+			return 1;
+		}
+
+		msleep_approx(250);
+	}
+}
+
+static void channelscan_extract_name(struct hdhomerun_channelscan_program_t *program, const char *line)
+{
+	/* Find start of name. */
+	const char *start = strchr(line, ' ');
+	if (!start) {
+		return;
+	}
+	start++;
+
+	start = strchr(start, ' ');
+	if (!start) {
+		return;
+	}
+	start++;
+
+	/* Find end of name. */
+	const char *end = strstr(start, " (");
+	if (!end) {
+		end = strchr(line, 0);
+	}
+
+	if (end <= start) {
+		return;
+	}
+
+	/* Extract name. */
+	size_t length = (size_t)(end - start);
+	if (length > sizeof(program->name) - 1) {
+		length = sizeof(program->name) - 1;
+	}
+
+	strncpy(program->name, start, length);
+	program->name[length] = 0;
+}
+
+static int channelscan_detect_programs(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result, bool_t *pchanged, bool_t *pincomplete)
+{
+	*pchanged = FALSE;
+	*pincomplete = FALSE;
+
+	char *streaminfo;
+	int ret = hdhomerun_device_get_tuner_streaminfo(scan->hd, &streaminfo);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	char *next_line = streaminfo;
+	int program_count = 0;
+
+	while (1) {
+		char *line = next_line;
+
+		next_line = strchr(line, '\n');
+		if (!next_line) {
+			break;
+		}
+		*next_line++ = 0;
+
+		unsigned int transport_stream_id;
+		if (sscanf(line, "tsid=0x%x", &transport_stream_id) == 1) {
+			result->transport_stream_id = transport_stream_id;
+			result->transport_stream_id_detected = TRUE;
+			continue;
+		}
+
+		if (program_count >= HDHOMERUN_CHANNELSCAN_MAX_PROGRAM_COUNT) {
+			continue;
+		}
+
+		struct hdhomerun_channelscan_program_t program;
+		memset(&program, 0, sizeof(program));
+
+		strncpy(program.program_str, line, sizeof(program.program_str));
+		program.program_str[sizeof(program.program_str) - 1] = 0;
+
+		unsigned int program_number;
+		unsigned int virtual_major, virtual_minor;
+		if (sscanf(line, "%u: %u.%u", &program_number, &virtual_major, &virtual_minor) != 3) {
+			if (sscanf(line, "%u: %u", &program_number, &virtual_major) != 2) {
+				continue;
+			}
+			virtual_minor = 0;
+		}
+
+		program.program_number = program_number;
+		program.virtual_major = virtual_major;
+		program.virtual_minor = virtual_minor;
+
+		channelscan_extract_name(&program, line);
+
+		if (strstr(line, "(control)")) {
+			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_CONTROL;
+		} else if (strstr(line, "(encrypted)")) {
+			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_ENCRYPTED;
+		} else if (strstr(line, "(no data)")) {
+			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_NODATA;
+			*pincomplete = TRUE;
+		} else {
+			program.type = HDHOMERUN_CHANNELSCAN_PROGRAM_NORMAL;
+			if ((program.virtual_major == 0) || (program.name[0] == 0)) {
+				*pincomplete = TRUE;
+			}
+		}
+
+		if (memcmp(&result->programs[program_count], &program, sizeof(program)) != 0) {
+			memcpy(&result->programs[program_count], &program, sizeof(program));
+			*pchanged = TRUE;
+		}
+
+		program_count++;
+	}
+
+	if (program_count == 0) {
+		*pincomplete = TRUE;
+	}
+	if (result->program_count != program_count) {
+		result->program_count = program_count;
+		*pchanged = TRUE;
+	}
+
+	return 1;
+}
+
+int channelscan_advance(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result)
+{
+	memset(result, 0, sizeof(struct hdhomerun_channelscan_result_t));
+
+	struct hdhomerun_channel_entry_t *entry = scan->next_channel;
+	if (!entry) {
+		return 0;
+	}
+
+	/* Combine channels with same frequency. */
+	result->frequency = hdhomerun_channel_entry_frequency(entry);
+	strncpy(result->channel_str, hdhomerun_channel_entry_name(entry), sizeof(result->channel_str) - 1);
+	result->channel_str[sizeof(result->channel_str) - 1] = 0;
+
+	while (1) {
+		entry = hdhomerun_channel_list_prev(scan->channel_list, entry);
+		if (!entry) {
+			scan->next_channel = NULL;
+			break;
+		}
+
+		if (hdhomerun_channel_entry_frequency(entry) != result->frequency) {
+			scan->next_channel = entry;
+			break;
+		}
+
+		char *ptr = strchr(result->channel_str, 0);
+		sprintf(ptr, ", %s", hdhomerun_channel_entry_name(entry));
+	}
+
+	return 1;
+}
+
+int channelscan_detect(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result)
+{
+	scan->scanned_channels++;
+
+	/* Find lock. */
+	int ret = channelscan_find_lock(scan, result->frequency, result);
+	if (ret <= 0) {
+		return ret;
+	}
+	if (!result->status.lock_supported) {
+		return 1;
+	}
+
+	/* Detect programs. */
+	result->program_count = 0;
+
+	uint64_t timeout;
+	if (strstr(hdhomerun_device_get_model_str(scan->hd), "atsc")) {
+		timeout = getcurrenttime() + 4000;
+	} else {
+		timeout = getcurrenttime() + 10000;
+	}
+
+	uint64_t complete_time = getcurrenttime() + 1000;
+
+	while (1) {
+		bool_t changed, incomplete;
+		ret = channelscan_detect_programs(scan, result, &changed, &incomplete);
+		if (ret <= 0) {
+			return ret;
+		}
+
+		if (changed) {
+			complete_time = getcurrenttime() + 1000;
+		}
+
+		if (!incomplete && (getcurrenttime() >= complete_time)) {
+			break;
+		}
+
+		if (getcurrenttime() >= timeout) {
+			break;
+		}
+
+		msleep_approx(250);
+	}
+
+	/* Lock => skip overlapping channels. */
+	uint32_t max_next_frequency = result->frequency - 5500000;
+	while (1) {
+		if (!scan->next_channel) {
+			break;
+		}
+
+		if (hdhomerun_channel_entry_frequency(scan->next_channel) <= max_next_frequency) {
+			break;
+		}
+
+		scan->next_channel = hdhomerun_channel_list_prev(scan->channel_list, scan->next_channel);
+	}
+
+	/* Success. */
+	return 1;
+}
+
+uint8_t channelscan_get_progress(struct hdhomerun_channelscan_t *scan)
+{
+	struct hdhomerun_channel_entry_t *entry = scan->next_channel;
+	if (!entry) {
+		return 100;
+	}
+
+	uint32_t channels_remaining = 1;
+	uint32_t frequency = hdhomerun_channel_entry_frequency(entry);
+
+	while (1) {
+		entry = hdhomerun_channel_list_prev(scan->channel_list, entry);
+		if (!entry) {
+			break;
+		}
+
+		if (hdhomerun_channel_entry_frequency(entry) != frequency) {
+			channels_remaining++;
+			frequency = hdhomerun_channel_entry_frequency(entry);
+		}
+	}
+
+	return scan->scanned_channels * 100 / (scan->scanned_channels + channels_remaining);
+}
diff --git a/libhdhomerun/hdhomerun_channelscan.h b/libhdhomerun/hdhomerun_channelscan.h
new file mode 100755
index 0000000..8a1fac2
--- /dev/null
+++ b/libhdhomerun/hdhomerun_channelscan.h
@@ -0,0 +1,53 @@
+/*
+ * hdhomerun_channelscan.h
+ *
+ * Copyright © 2007-2008 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HDHOMERUN_CHANNELSCAN_PROGRAM_NORMAL 0
+#define HDHOMERUN_CHANNELSCAN_PROGRAM_NODATA 1
+#define HDHOMERUN_CHANNELSCAN_PROGRAM_CONTROL 2
+#define HDHOMERUN_CHANNELSCAN_PROGRAM_ENCRYPTED 3
+
+struct hdhomerun_channelscan_t;
+
+extern LIBTYPE struct hdhomerun_channelscan_t *channelscan_create(struct hdhomerun_device_t *hd, const char *channelmap);
+extern LIBTYPE void channelscan_destroy(struct hdhomerun_channelscan_t *scan);
+
+extern LIBTYPE int channelscan_advance(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result);
+extern LIBTYPE int channelscan_detect(struct hdhomerun_channelscan_t *scan, struct hdhomerun_channelscan_result_t *result);
+extern LIBTYPE uint8_t channelscan_get_progress(struct hdhomerun_channelscan_t *scan);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_config.c b/libhdhomerun/hdhomerun_config.c
new file mode 100755
index 0000000..a88268e
--- /dev/null
+++ b/libhdhomerun/hdhomerun_config.c
@@ -0,0 +1,702 @@
+/*
+ * hdhomerun_config.c
+ *
+ * Copyright © 2006-2008 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+/*
+ * The console output format should be set to UTF-8, however in XP and Vista this breaks batch file processing.
+ * Attempting to restore on exit fails to restore if the program is terminated by the user.
+ * Solution - set the output format each printf.
+ */
+#if defined(__WINDOWS__)
+#define printf console_printf
+#define vprintf console_vprintf
+#endif
+
+static const char *appname;
+
+struct hdhomerun_device_t *hd;
+
+static int help(void)
+{
+	printf("Usage:\n");
+	printf("\t%s discover\n", appname);
+	printf("\t%s <id> get help\n", appname);
+	printf("\t%s <id> get <item>\n", appname);
+	printf("\t%s <id> set <item> <value>\n", appname);
+	printf("\t%s <id> scan <tuner> [<filename>]\n", appname);
+	printf("\t%s <id> save <tuner> <filename>\n", appname);
+	printf("\t%s <id> upgrade <filename>\n", appname);
+	return -1;
+}
+
+static void extract_appname(const char *argv0)
+{
+	const char *ptr = strrchr(argv0, '/');
+	if (ptr) {
+		argv0 = ptr + 1;
+	}
+	ptr = strrchr(argv0, '\\');
+	if (ptr) {
+		argv0 = ptr + 1;
+	}
+	appname = argv0;
+}
+
+static bool_t contains(const char *arg, const char *cmpstr)
+{
+	if (strcmp(arg, cmpstr) == 0) {
+		return TRUE;
+	}
+
+	if (*arg++ != '-') {
+		return FALSE;
+	}
+	if (*arg++ != '-') {
+		return FALSE;
+	}
+	if (strcmp(arg, cmpstr) == 0) {
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+static uint32_t parse_ip_addr(const char *str)
+{
+	unsigned long a[4];
+	if (sscanf(str, "%lu.%lu.%lu.%lu", &a[0], &a[1], &a[2], &a[3]) != 4) {
+		return 0;
+	}
+
+	return (uint32_t)((a[0] << 24) | (a[1] << 16) | (a[2] << 8) | (a[3] << 0));
+}
+
+static int discover_print(char *target_ip_str)
+{
+	uint32_t target_ip = 0;
+	if (target_ip_str) {
+		target_ip = parse_ip_addr(target_ip_str);
+		if (target_ip == 0) {
+			fprintf(stderr, "invalid ip address: %s\n", target_ip_str);
+			return -1;
+		}
+	}
+
+	struct hdhomerun_discover_device_t result_list[64];
+	int count = hdhomerun_discover_find_devices_custom(target_ip, HDHOMERUN_DEVICE_TYPE_TUNER, HDHOMERUN_DEVICE_ID_WILDCARD, result_list, 64);
+	if (count < 0) {
+		fprintf(stderr, "error sending discover request\n");
+		return -1;
+	}
+	if (count == 0) {
+		printf("no devices found\n");
+		return 0;
+	}
+
+	int index;
+	for (index = 0; index < count; index++) {
+		struct hdhomerun_discover_device_t *result = &result_list[index];
+		printf("hdhomerun device %08lX found at %u.%u.%u.%u\n",
+			(unsigned long)result->device_id,
+			(unsigned int)(result->ip_addr >> 24) & 0x0FF, (unsigned int)(result->ip_addr >> 16) & 0x0FF,
+			(unsigned int)(result->ip_addr >> 8) & 0x0FF, (unsigned int)(result->ip_addr >> 0) & 0x0FF
+		);
+	}
+
+	return count;
+}
+
+static int cmd_get(const char *item)
+{
+	char *ret_value;
+	char *ret_error;
+	if (hdhomerun_device_get_var(hd, item, &ret_value, &ret_error) < 0) {
+		fprintf(stderr, "communication error sending request to hdhomerun device\n");
+		return -1;
+	}
+
+	if (ret_error) {
+		printf("%s\n", ret_error);
+		return 0;
+	}
+
+	printf("%s\n", ret_value);
+	return 1;
+}
+
+static int cmd_set_internal(const char *item, const char *value)
+{
+	char *ret_error;
+	if (hdhomerun_device_set_var(hd, item, value, NULL, &ret_error) < 0) {
+		fprintf(stderr, "communication error sending request to hdhomerun device\n");
+		return -1;
+	}
+
+	if (ret_error) {
+		printf("%s\n", ret_error);
+		return 0;
+	}
+
+	return 1;
+}
+
+static int cmd_set(const char *item, const char *value)
+{
+	if (strcmp(value, "-") == 0) {
+		char *buffer = NULL;
+		size_t pos = 0;
+
+		while (1) {
+			buffer = (char *)realloc(buffer, pos + 1024);
+			if (!buffer) {
+				fprintf(stderr, "out of memory\n");
+				return -1;
+			}
+
+			size_t size = fread(buffer + pos, 1, 1024, stdin);
+			pos += size;
+
+			if (size < 1024) {
+				break;
+			}
+		}
+
+		buffer[pos] = 0;
+
+		int ret = cmd_set_internal(item, buffer);
+
+		free(buffer);
+		return ret;
+	}
+
+	return cmd_set_internal(item, value);
+}
+
+static volatile sig_atomic_t sigabort_flag = FALSE;
+static volatile sig_atomic_t siginfo_flag = FALSE;
+ 
+static void sigabort_handler(int arg)
+{
+	sigabort_flag = TRUE;
+}
+
+static void siginfo_handler(int arg)
+{
+	siginfo_flag = TRUE;
+}
+
+static void register_signal_handlers(sig_t sigpipe_handler, sig_t sigint_handler, sig_t siginfo_handler)
+{
+#if defined(SIGPIPE)
+	signal(SIGPIPE, sigpipe_handler);
+#endif
+#if defined(SIGINT)
+	signal(SIGINT, sigint_handler);
+#endif
+#if defined(SIGINFO)
+	signal(SIGINFO, siginfo_handler);
+#endif
+}
+
+static void cmd_scan_printf(FILE *fp, const char *fmt, ...)
+{
+	va_list ap;
+	va_start(ap, fmt);
+
+	if (fp) {
+		va_list apc;
+		va_copy(apc, ap);
+
+		vfprintf(fp, fmt, apc);
+		fflush(fp);
+
+		va_end(apc);
+	}
+
+	vprintf(fmt, ap);
+	fflush(stdout);
+
+	va_end(ap);
+}
+
+static int cmd_scan(const char *tuner_str, const char *filename)
+{
+	if (hdhomerun_device_set_tuner_from_str(hd, tuner_str) <= 0) {
+		fprintf(stderr, "invalid tuner number\n");
+		return -1;
+	}
+
+	char *ret_error;
+	if (hdhomerun_device_tuner_lockkey_request(hd, &ret_error) <= 0) {
+		fprintf(stderr, "failed to lock tuner\n");
+		if (ret_error) {
+			fprintf(stderr, "%s\n", ret_error);
+		}
+		return -1;
+	}
+
+	hdhomerun_device_set_tuner_target(hd, "none");
+
+	char *channelmap;
+	if (hdhomerun_device_get_tuner_channelmap(hd, &channelmap) <= 0) {
+		fprintf(stderr, "failed to query channelmap from device\n");
+		return -1;
+	}
+
+	const char *channelmap_scan_group = hdhomerun_channelmap_get_channelmap_scan_group(channelmap);
+	if (!channelmap_scan_group) {
+		fprintf(stderr, "unknown channelmap '%s'\n", channelmap);
+		return -1;
+	}
+
+	if (hdhomerun_device_channelscan_init(hd, channelmap_scan_group) <= 0) {
+		fprintf(stderr, "failed to initialize channel scan\n");
+		return -1;
+	}
+
+	FILE *fp = NULL;
+	if (filename) {
+		fp = fopen(filename, "w");
+		if (!fp) {
+			fprintf(stderr, "unable to create file: %s\n", filename);
+			return -1;
+		}
+	}
+
+	register_signal_handlers(sigabort_handler, sigabort_handler, siginfo_handler);
+
+	int ret = 0;
+	while (!sigabort_flag) {
+		struct hdhomerun_channelscan_result_t result;
+		ret = hdhomerun_device_channelscan_advance(hd, &result);
+		if (ret <= 0) {
+			break;
+		}
+
+		cmd_scan_printf(fp, "SCANNING: %lu (%s)\n",
+			(unsigned long)result.frequency, result.channel_str
+		);
+
+		ret = hdhomerun_device_channelscan_detect(hd, &result);
+		if (ret < 0) {
+			break;
+		}
+		if (ret == 0) {
+			continue;
+		}
+
+		cmd_scan_printf(fp, "LOCK: %s (ss=%u snq=%u seq=%u)\n",
+			result.status.lock_str, result.status.signal_strength,
+			result.status.signal_to_noise_quality, result.status.symbol_error_quality
+		);
+
+		if (result.transport_stream_id_detected) {
+			cmd_scan_printf(fp, "TSID: 0x%04X\n", result.transport_stream_id);
+		}
+
+		int i;
+		for (i = 0; i < result.program_count; i++) {
+			struct hdhomerun_channelscan_program_t *program = &result.programs[i];
+			cmd_scan_printf(fp, "PROGRAM %s\n", program->program_str);
+		}
+	}
+
+	hdhomerun_device_tuner_lockkey_release(hd);
+
+	if (fp) {
+		fclose(fp);
+	}
+	if (ret < 0) {
+		fprintf(stderr, "communication error sending request to hdhomerun device\n");
+	}
+	return ret;
+}
+
+static void cmd_save_print_stats(void)
+{
+	struct hdhomerun_video_stats_t stats;
+	hdhomerun_device_get_video_stats(hd, &stats);
+
+	fprintf(stderr, "%u packets received, %u overflow errors, %u network errors, %u transport errors, %u sequence errors\n",
+		(unsigned int)stats.packet_count, 
+		(unsigned int)stats.overflow_error_count,
+		(unsigned int)stats.network_error_count, 
+		(unsigned int)stats.transport_error_count, 
+		(unsigned int)stats.sequence_error_count
+	);
+}
+
+static int cmd_save(const char *tuner_str, const char *filename)
+{
+	if (hdhomerun_device_set_tuner_from_str(hd, tuner_str) <= 0) {
+		fprintf(stderr, "invalid tuner number\n");
+		return -1;
+	}
+
+	FILE *fp;
+	if (strcmp(filename, "null") == 0) {
+		fp = NULL;
+	} else if (strcmp(filename, "-") == 0) {
+		fp = stdout;
+	} else {
+		fp = fopen(filename, "wb");
+		if (!fp) {
+			fprintf(stderr, "unable to create file %s\n", filename);
+			return -1;
+		}
+	}
+
+	int ret = hdhomerun_device_stream_start(hd);
+	if (ret <= 0) {
+		fprintf(stderr, "unable to start stream\n");
+		if (fp && fp != stdout) {
+			fclose(fp);
+		}
+		return ret;
+	}
+
+	register_signal_handlers(sigabort_handler, sigabort_handler, siginfo_handler);
+
+	struct hdhomerun_video_stats_t stats_old, stats_cur;
+	hdhomerun_device_get_video_stats(hd, &stats_old);
+
+	uint64_t next_progress = getcurrenttime() + 1000;
+
+	while (!sigabort_flag) {
+		uint64_t loop_start_time = getcurrenttime();
+
+		if (siginfo_flag) {
+			fprintf(stderr, "\n");
+			cmd_save_print_stats();
+			siginfo_flag = FALSE;
+		}
+
+		size_t actual_size;
+		uint8_t *ptr = hdhomerun_device_stream_recv(hd, VIDEO_DATA_BUFFER_SIZE_1S, &actual_size);
+		if (!ptr) {
+			msleep_approx(64);
+			continue;
+		}
+
+		if (fp) {
+			if (fwrite(ptr, 1, actual_size, fp) != actual_size) {
+				fprintf(stderr, "error writing output\n");
+				return -1;
+			}
+		}
+
+		if (loop_start_time >= next_progress) {
+			next_progress += 1000;
+			if (loop_start_time >= next_progress) {
+				next_progress = loop_start_time + 1000;
+			}
+
+			/* Windows - indicate activity to suppress auto sleep mode. */
+			#if defined(__WINDOWS__)
+			SetThreadExecutionState(ES_SYSTEM_REQUIRED);
+			#endif
+
+			/* Video stats. */
+			hdhomerun_device_get_video_stats(hd, &stats_cur);
+
+			if (stats_cur.overflow_error_count > stats_old.overflow_error_count) {
+				fprintf(stderr, "o");
+			} else if (stats_cur.network_error_count > stats_old.network_error_count) {
+				fprintf(stderr, "n");
+			} else if (stats_cur.transport_error_count > stats_old.transport_error_count) {
+				fprintf(stderr, "t");
+			} else if (stats_cur.sequence_error_count > stats_old.sequence_error_count) {
+				fprintf(stderr, "s");
+			} else {
+				fprintf(stderr, ".");
+			}
+
+			stats_old = stats_cur;
+			fflush(stderr);
+		}
+
+		int32_t delay = 64 - (int32_t)(getcurrenttime() - loop_start_time);
+		if (delay <= 0) {
+			continue;
+		}
+
+		msleep_approx(delay);
+	}
+
+	if (fp) {
+		fclose(fp);
+	}
+
+	hdhomerun_device_stream_stop(hd);
+
+	fprintf(stderr, "\n");
+	fprintf(stderr, "-- Video statistics --\n");
+	cmd_save_print_stats();
+
+	return 0;
+}
+
+static int cmd_upgrade(const char *filename)
+{
+	FILE *fp = fopen(filename, "rb");
+	if (!fp) {
+		fprintf(stderr, "unable to open file %s\n", filename);
+		return -1;
+	}
+
+	printf("uploading firmware...\n");
+	if (hdhomerun_device_upgrade(hd, fp) <= 0) {
+		fprintf(stderr, "error sending upgrade file to hdhomerun device\n");
+		fclose(fp);
+		return -1;
+	}
+
+	fclose(fp);
+	msleep_minimum(2000);
+
+	printf("upgrading firmware...\n");
+	msleep_minimum(8000);
+
+	printf("rebooting...\n");
+	int count = 0;
+	char *version_str;
+	while (1) {
+		if (hdhomerun_device_get_version(hd, &version_str, NULL) >= 0) {
+			break;
+		}
+
+		count++;
+		if (count > 30) {
+			fprintf(stderr, "error finding device after firmware upgrade\n");
+			return -1;
+		}
+
+		msleep_minimum(1000);
+	}
+
+	printf("upgrade complete - now running firmware %s\n", version_str);
+	return 0;
+}
+
+static int cmd_execute(void)
+{
+	char *ret_value;
+	char *ret_error;
+	if (hdhomerun_device_get_var(hd, "/sys/boot", &ret_value, &ret_error) < 0) {
+		fprintf(stderr, "communication error sending request to hdhomerun device\n");
+		return -1;
+	}
+
+	if (ret_error) {
+		printf("%s\n", ret_error);
+		return 0;
+	}
+
+	char *end = ret_value + strlen(ret_value);
+	char *pos = ret_value;
+
+	while (1) {
+		if (pos >= end) {
+			break;
+		}
+
+		char *eol_r = strchr(pos, '\r');
+		if (!eol_r) {
+			eol_r = end;
+		}
+
+		char *eol_n = strchr(pos, '\n');
+		if (!eol_n) {
+			eol_n = end;
+		}
+
+		char *eol = eol_r;
+		if (eol_n < eol) {
+			eol = eol_n;
+		}
+
+		char *sep = strchr(pos, ' ');
+		if (!sep || sep > eol) {
+			pos = eol + 1;
+			continue;
+		}
+
+		*sep = 0;
+		*eol = 0;
+
+		char *item = pos;
+		char *value = sep + 1;
+
+		printf("set %s \"%s\"\n", item, value);
+
+		cmd_set_internal(item, value);
+
+		pos = eol + 1;
+	}
+
+	return 1;
+}
+
+static int main_cmd(int argc, char *argv[])
+{
+	if (argc < 1) {
+		return help();
+	}
+
+	char *cmd = *argv++; argc--;
+
+	if (contains(cmd, "key")) {
+		if (argc < 2) {
+			return help();
+		}
+		uint32_t lockkey = strtoul(argv[0], NULL, 0);
+		hdhomerun_device_tuner_lockkey_use_value(hd, lockkey);
+
+		cmd = argv[1];
+		argv+=2; argc-=2;
+	}
+
+	if (contains(cmd, "get")) {
+		if (argc < 1) {
+			return help();
+		}
+		return cmd_get(argv[0]);
+	}
+
+	if (contains(cmd, "set")) {
+		if (argc < 2) {
+			return help();
+		}
+		return cmd_set(argv[0], argv[1]);
+	}
+
+	if (contains(cmd, "scan")) {
+		if (argc < 1) {
+			return help();
+		}
+		if (argc < 2) {
+			return cmd_scan(argv[0], NULL);
+		} else {
+			return cmd_scan(argv[0], argv[1]);
+		}
+	}
+
+	if (contains(cmd, "save")) {
+		if (argc < 2) {
+			return help();
+		}
+		return cmd_save(argv[0], argv[1]);
+	}
+
+	if (contains(cmd, "upgrade")) {
+		if (argc < 1) {
+			return help();
+		}
+		return cmd_upgrade(argv[0]);
+	}
+
+	if (contains(cmd, "execute")) {
+		return cmd_execute();
+	}
+
+	return help();
+}
+
+static int main_internal(int argc, char *argv[])
+{
+#if defined(__WINDOWS__)
+	/* Initialize network socket support. */
+	WORD wVersionRequested = MAKEWORD(2, 0);
+	WSADATA wsaData;
+	WSAStartup(wVersionRequested, &wsaData);
+#endif
+
+	extract_appname(argv[0]);
+	argv++;
+	argc--;
+
+	if (argc == 0) {
+		return help();
+	}
+
+	char *id_str = *argv++; argc--;
+	if (contains(id_str, "help")) {
+		return help();
+	}
+	if (contains(id_str, "discover")) {
+		if (argc < 1) {
+			return discover_print(NULL);
+		} else {
+			return discover_print(argv[0]);
+		}
+	}
+
+	/* Device object. */
+	hd = hdhomerun_device_create_from_str(id_str, NULL);
+	if (!hd) {
+		fprintf(stderr, "invalid device id: %s\n", id_str);
+		return -1;
+	}
+
+	/* Device ID check. */
+	uint32_t device_id_requested = hdhomerun_device_get_device_id_requested(hd);
+	if (!hdhomerun_discover_validate_device_id(device_id_requested)) {
+		fprintf(stderr, "invalid device id: %08lX\n", (unsigned long)device_id_requested);
+	}
+
+	/* Connect to device and check model. */
+	const char *model = hdhomerun_device_get_model_str(hd);
+	if (!model) {
+		fprintf(stderr, "unable to connect to device\n");
+		hdhomerun_device_destroy(hd);
+		return -1;
+	}
+
+	/* Command. */
+	int ret = main_cmd(argc, argv);
+
+	/* Cleanup. */
+	hdhomerun_device_destroy(hd);
+
+	/* Complete. */
+	return ret;
+}
+
+int main(int argc, char *argv[])
+{
+	int ret = main_internal(argc, argv);
+	if (ret <= 0) {
+		return 1;
+	}
+	return 0;
+}
diff --git a/libhdhomerun/hdhomerun_control.c b/libhdhomerun/hdhomerun_control.c
new file mode 100755
index 0000000..2873e53
--- /dev/null
+++ b/libhdhomerun/hdhomerun_control.c
@@ -0,0 +1,431 @@
+/*
+ * hdhomerun_control.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+#define HDHOMERUN_CONTROL_CONNECT_TIMEOUT 2500
+#define HDHOMERUN_CONTROL_SEND_TIMEOUT 2500
+#define HDHOMERUN_CONTROL_RECV_TIMEOUT 2500
+#define HDHOMERUN_CONTROL_UPGRADE_TIMEOUT 20000
+
+struct hdhomerun_control_sock_t {
+	uint32_t desired_device_id;
+	uint32_t desired_device_ip;
+	uint32_t actual_device_id;
+	uint32_t actual_device_ip;
+	hdhomerun_sock_t sock;
+	struct hdhomerun_debug_t *dbg;
+	struct hdhomerun_pkt_t tx_pkt;
+	struct hdhomerun_pkt_t rx_pkt;
+};
+
+static void hdhomerun_control_close_sock(struct hdhomerun_control_sock_t *cs)
+{
+	if (cs->sock == HDHOMERUN_SOCK_INVALID) {
+		return;
+	}
+
+	hdhomerun_sock_destroy(cs->sock);
+	cs->sock = HDHOMERUN_SOCK_INVALID;
+}
+
+void hdhomerun_control_set_device(struct hdhomerun_control_sock_t *cs, uint32_t device_id, uint32_t device_ip)
+{
+	hdhomerun_control_close_sock(cs);
+
+	cs->desired_device_id = device_id;
+	cs->desired_device_ip = device_ip;
+	cs->actual_device_id = 0;
+	cs->actual_device_ip = 0;
+}
+
+struct hdhomerun_control_sock_t *hdhomerun_control_create(uint32_t device_id, uint32_t device_ip, struct hdhomerun_debug_t *dbg)
+{
+	struct hdhomerun_control_sock_t *cs = (struct hdhomerun_control_sock_t *)calloc(1, sizeof(struct hdhomerun_control_sock_t));
+	if (!cs) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_control_create: failed to allocate control object\n");
+		return NULL;
+	}
+
+	cs->dbg = dbg;
+	cs->sock = HDHOMERUN_SOCK_INVALID;
+	hdhomerun_control_set_device(cs, device_id, device_ip);
+
+	return cs;
+}
+
+void hdhomerun_control_destroy(struct hdhomerun_control_sock_t *cs)
+{
+	hdhomerun_control_close_sock(cs);
+	free(cs);
+}
+
+static bool_t hdhomerun_control_connect_sock(struct hdhomerun_control_sock_t *cs)
+{
+	if (cs->sock != HDHOMERUN_SOCK_INVALID) {
+		return TRUE;
+	}
+
+	if ((cs->desired_device_id == 0) && (cs->desired_device_ip == 0)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_connect_sock: no device specified\n");
+		return FALSE;
+	}
+	if (hdhomerun_discover_is_ip_multicast(cs->desired_device_ip)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_connect_sock: cannot use multicast ip address for device operations\n");
+		return FALSE;
+	}
+
+	/* Find device. */
+	struct hdhomerun_discover_device_t result;
+	if (hdhomerun_discover_find_devices_custom(cs->desired_device_ip, HDHOMERUN_DEVICE_TYPE_WILDCARD, cs->desired_device_id, &result, 1) <= 0) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_connect_sock: device not found\n");
+		return FALSE;
+	}
+	cs->actual_device_ip = result.ip_addr;
+	cs->actual_device_id = result.device_id;
+
+	/* Create socket. */
+	cs->sock = hdhomerun_sock_create_tcp();
+	if (cs->sock == HDHOMERUN_SOCK_INVALID) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_connect_sock: failed to create socket (%d)\n", hdhomerun_sock_getlasterror());
+		return FALSE;
+	}
+
+	/* Initiate connection. */
+	if (!hdhomerun_sock_connect(cs->sock, cs->actual_device_ip, HDHOMERUN_CONTROL_TCP_PORT, HDHOMERUN_CONTROL_CONNECT_TIMEOUT)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_connect_sock: failed to connect (%d)\n", hdhomerun_sock_getlasterror());
+		hdhomerun_control_close_sock(cs);
+		return FALSE;
+	}
+
+	/* Success. */
+	return TRUE;
+}
+
+uint32_t hdhomerun_control_get_device_id(struct hdhomerun_control_sock_t *cs)
+{
+	if (!hdhomerun_control_connect_sock(cs)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_device_id: connect failed\n");
+		return 0;
+	}
+
+	return cs->actual_device_id;
+}
+
+uint32_t hdhomerun_control_get_device_ip(struct hdhomerun_control_sock_t *cs)
+{
+	if (!hdhomerun_control_connect_sock(cs)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_device_ip: connect failed\n");
+		return 0;
+	}
+
+	return cs->actual_device_ip;
+}
+
+uint32_t hdhomerun_control_get_device_id_requested(struct hdhomerun_control_sock_t *cs)
+{
+	return cs->desired_device_id;
+}
+
+uint32_t hdhomerun_control_get_device_ip_requested(struct hdhomerun_control_sock_t *cs)
+{
+	return cs->desired_device_ip;
+}
+
+uint32_t hdhomerun_control_get_local_addr(struct hdhomerun_control_sock_t *cs)
+{
+	if (!hdhomerun_control_connect_sock(cs)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_local_addr: connect failed\n");
+		return 0;
+	}
+
+	uint32_t addr = hdhomerun_sock_getsockname_addr(cs->sock);
+	if (addr == 0) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_local_addr: getsockname failed (%d)\n", hdhomerun_sock_getlasterror());
+		return 0;
+	}
+
+	return addr;
+}
+
+static bool_t hdhomerun_control_send_sock(struct hdhomerun_control_sock_t *cs, struct hdhomerun_pkt_t *tx_pkt)
+{
+	if (!hdhomerun_sock_send(cs->sock, tx_pkt->start, tx_pkt->end - tx_pkt->start, HDHOMERUN_CONTROL_SEND_TIMEOUT)) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_send_sock: send failed (%d)\n", hdhomerun_sock_getlasterror());
+		hdhomerun_control_close_sock(cs);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static bool_t hdhomerun_control_recv_sock(struct hdhomerun_control_sock_t *cs, struct hdhomerun_pkt_t *rx_pkt, uint16_t *ptype, uint64_t recv_timeout)
+{
+	uint64_t stop_time = getcurrenttime() + recv_timeout;
+	hdhomerun_pkt_reset(rx_pkt);
+
+	while (1) {
+		uint64_t current_time = getcurrenttime();
+		if (current_time >= stop_time) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_recv_sock: timeout\n");
+			hdhomerun_control_close_sock(cs);
+			return FALSE;
+		}
+
+		size_t length = rx_pkt->limit - rx_pkt->end;
+		if (!hdhomerun_sock_recv(cs->sock, rx_pkt->end, &length, stop_time - current_time)) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_recv_sock: recv failed (%d)\n", hdhomerun_sock_getlasterror());
+			hdhomerun_control_close_sock(cs);
+			return FALSE;
+		}
+
+		rx_pkt->end += length;
+
+		int ret = hdhomerun_pkt_open_frame(rx_pkt, ptype);
+		if (ret < 0) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_recv_sock: frame error\n");
+			hdhomerun_control_close_sock(cs);
+			return FALSE;
+		}
+		if (ret > 0) {
+			return TRUE;
+		}
+	}
+}
+
+static int hdhomerun_control_send_recv_internal(struct hdhomerun_control_sock_t *cs, struct hdhomerun_pkt_t *tx_pkt, struct hdhomerun_pkt_t *rx_pkt, uint16_t type, uint64_t recv_timeout)
+{
+	hdhomerun_pkt_seal_frame(tx_pkt, type);
+
+	int i;
+	for (i = 0; i < 2; i++) {
+		if (cs->sock == HDHOMERUN_SOCK_INVALID) {
+			if (!hdhomerun_control_connect_sock(cs)) {
+				hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_send_recv: connect failed\n");
+				return -1;
+			}
+		}
+
+		if (!hdhomerun_control_send_sock(cs, tx_pkt)) {
+			continue;
+		}
+		if (!rx_pkt) {
+			return 1;
+		}
+
+		uint16_t rsp_type;
+		if (!hdhomerun_control_recv_sock(cs, rx_pkt, &rsp_type, recv_timeout)) {
+			continue;
+		}
+		if (rsp_type != type + 1) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_send_recv: unexpected frame type\n");
+			hdhomerun_control_close_sock(cs);
+			continue;
+		}
+
+		return 1;
+	}
+
+	hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_send_recv: failed\n");
+	return -1;
+}
+
+int hdhomerun_control_send_recv(struct hdhomerun_control_sock_t *cs, struct hdhomerun_pkt_t *tx_pkt, struct hdhomerun_pkt_t *rx_pkt, uint16_t type)
+{
+	return hdhomerun_control_send_recv_internal(cs, tx_pkt, rx_pkt, type, HDHOMERUN_CONTROL_RECV_TIMEOUT);
+}
+
+static int hdhomerun_control_get_set(struct hdhomerun_control_sock_t *cs, const char *name, const char *value, uint32_t lockkey, char **pvalue, char **perror)
+{
+	struct hdhomerun_pkt_t *tx_pkt = &cs->tx_pkt;
+	struct hdhomerun_pkt_t *rx_pkt = &cs->rx_pkt;
+
+	/* Request. */
+	hdhomerun_pkt_reset(tx_pkt);
+
+	int name_len = (int)strlen(name) + 1;
+	if (tx_pkt->end + 3 + name_len > tx_pkt->limit) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_set: request too long\n");
+		return -1;
+	}
+	hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_GETSET_NAME);
+	hdhomerun_pkt_write_var_length(tx_pkt, name_len);
+	hdhomerun_pkt_write_mem(tx_pkt, (const void *)name, name_len);
+
+	if (value) {
+		int value_len = (int)strlen(value) + 1;
+		if (tx_pkt->end + 3 + value_len > tx_pkt->limit) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_set: request too long\n");
+			return -1;
+		}
+		hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_GETSET_VALUE);
+		hdhomerun_pkt_write_var_length(tx_pkt, value_len);
+		hdhomerun_pkt_write_mem(tx_pkt, (const void *)value, value_len);
+	}
+
+	if (lockkey != 0) {
+		if (tx_pkt->end + 6 > tx_pkt->limit) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_set: request too long\n");
+			return -1;
+		}
+		hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_GETSET_LOCKKEY);
+		hdhomerun_pkt_write_var_length(tx_pkt, 4);
+		hdhomerun_pkt_write_u32(tx_pkt, lockkey);
+	}
+
+	/* Send/Recv. */
+	if (hdhomerun_control_send_recv_internal(cs, tx_pkt, rx_pkt, HDHOMERUN_TYPE_GETSET_REQ, HDHOMERUN_CONTROL_RECV_TIMEOUT) < 0) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_set: send/recv error\n");
+		return -1;
+	}
+
+	/* Response. */
+	while (1) {
+		uint8_t tag;
+		size_t len;
+		uint8_t *next = hdhomerun_pkt_read_tlv(rx_pkt, &tag, &len);
+		if (!next) {
+			break;
+		}
+
+		switch (tag) {
+		case HDHOMERUN_TAG_GETSET_VALUE:
+			if (pvalue) {
+				*pvalue = (char *)rx_pkt->pos;
+				rx_pkt->pos[len] = 0;
+			}
+			if (perror) {
+				*perror = NULL;
+			}
+			return 1;
+
+		case HDHOMERUN_TAG_ERROR_MESSAGE:
+			rx_pkt->pos[len] = 0;
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_set: %s\n", rx_pkt->pos);
+
+			if (pvalue) {
+				*pvalue = NULL;
+			}
+			if (perror) {
+				*perror = (char *)rx_pkt->pos;
+			}
+
+			return 0;
+		}
+
+		rx_pkt->pos = next;
+	}
+
+	hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_get_set: missing response tags\n");
+	return -1;
+}
+
+int hdhomerun_control_get(struct hdhomerun_control_sock_t *cs, const char *name, char **pvalue, char **perror)
+{
+	return hdhomerun_control_get_set(cs, name, NULL, 0, pvalue, perror);
+}
+
+int hdhomerun_control_set(struct hdhomerun_control_sock_t *cs, const char *name, const char *value, char **pvalue, char **perror)
+{
+	return hdhomerun_control_get_set(cs, name, value, 0, pvalue, perror);
+}
+
+int hdhomerun_control_set_with_lockkey(struct hdhomerun_control_sock_t *cs, const char *name, const char *value, uint32_t lockkey, char **pvalue, char **perror)
+{
+	return hdhomerun_control_get_set(cs, name, value, lockkey, pvalue, perror);
+}
+
+int hdhomerun_control_upgrade(struct hdhomerun_control_sock_t *cs, FILE *upgrade_file)
+{
+	struct hdhomerun_pkt_t *tx_pkt = &cs->tx_pkt;
+	struct hdhomerun_pkt_t *rx_pkt = &cs->rx_pkt;
+	uint32_t sequence = 0;
+
+	/* Upload. */
+	while (1) {
+		uint8_t data[256];
+		size_t length = fread(data, 1, 256, upgrade_file);
+		if (length == 0) {
+			break;
+		}
+
+		hdhomerun_pkt_reset(tx_pkt);
+		hdhomerun_pkt_write_u32(tx_pkt, sequence);
+		hdhomerun_pkt_write_mem(tx_pkt, data, length);
+
+		if (hdhomerun_control_send_recv_internal(cs, tx_pkt, NULL, HDHOMERUN_TYPE_UPGRADE_REQ, 0) < 0) {
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_upgrade: send/recv failed\n");
+			return -1;
+		}
+
+		sequence += (uint32_t)length;
+	}
+
+	if (sequence == 0) {
+		/* No data in file. Error, but no need to close connection. */
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_upgrade: zero length file\n");
+		return 0;
+	}
+
+	/* Execute upgrade. */
+	hdhomerun_pkt_reset(tx_pkt);
+	hdhomerun_pkt_write_u32(tx_pkt, 0xFFFFFFFF);
+
+	if (hdhomerun_control_send_recv_internal(cs, tx_pkt, rx_pkt, HDHOMERUN_TYPE_UPGRADE_REQ, HDHOMERUN_CONTROL_UPGRADE_TIMEOUT) < 0) {
+		hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_upgrade: send/recv failed\n");
+		return -1;
+	}
+
+	/* Check response. */
+	while (1) {
+		uint8_t tag;
+		size_t len;
+		uint8_t *next = hdhomerun_pkt_read_tlv(rx_pkt, &tag, &len);
+		if (!next) {
+			break;
+		}
+
+		switch (tag) {
+		case HDHOMERUN_TAG_ERROR_MESSAGE:
+			rx_pkt->pos[len] = 0;
+			hdhomerun_debug_printf(cs->dbg, "hdhomerun_control_upgrade: %s\n", (char *)rx_pkt->pos);
+			return 0;
+
+		default:
+			break;
+		}
+
+		rx_pkt->pos = next;
+	}
+
+	return 1;
+}
diff --git a/libhdhomerun/hdhomerun_control.h b/libhdhomerun/hdhomerun_control.h
new file mode 100755
index 0000000..9960066
--- /dev/null
+++ b/libhdhomerun/hdhomerun_control.h
@@ -0,0 +1,115 @@
+/*
+ * hdhomerun_control.h
+ *
+ * Copyright © 2006 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hdhomerun_control_sock_t;
+
+/*
+ * Create a control socket.
+ *
+ * This function will not attempt to connect to the device.
+ * The connection will be established when first used.
+ *
+ * uint32_t device_id = 32-bit device id of device. Set to HDHOMERUN_DEVICE_ID_WILDCARD to match any device ID.
+ * uint32_t device_ip = IP address of device. Set to 0 to auto-detect.
+ * struct hdhomerun_debug_t *dbg: Pointer to debug logging object. May be NULL.
+ *
+ * Returns a pointer to the newly created control socket.
+ *
+ * When no longer needed, the socket should be destroyed by calling hdhomerun_control_destroy.
+ */
+extern LIBTYPE struct hdhomerun_control_sock_t *hdhomerun_control_create(uint32_t device_id, uint32_t device_ip, struct hdhomerun_debug_t *dbg);
+extern LIBTYPE void hdhomerun_control_destroy(struct hdhomerun_control_sock_t *cs);
+
+/*
+ * Get the actual device id or ip of the device.
+ *
+ * Returns 0 if the device id cannot be determined.
+ */
+extern LIBTYPE uint32_t hdhomerun_control_get_device_id(struct hdhomerun_control_sock_t *cs);
+extern LIBTYPE uint32_t hdhomerun_control_get_device_ip(struct hdhomerun_control_sock_t *cs);
+extern LIBTYPE uint32_t hdhomerun_control_get_device_id_requested(struct hdhomerun_control_sock_t *cs);
+extern LIBTYPE uint32_t hdhomerun_control_get_device_ip_requested(struct hdhomerun_control_sock_t *cs);
+
+extern LIBTYPE void hdhomerun_control_set_device(struct hdhomerun_control_sock_t *cs, uint32_t device_id, uint32_t device_ip);
+
+/*
+ * Get the local machine IP address used when communicating with the device.
+ *
+ * This function is useful for determining the IP address to use with set target commands.
+ *
+ * Returns 32-bit IP address with native endianness, or 0 on error.
+ */
+extern LIBTYPE uint32_t hdhomerun_control_get_local_addr(struct hdhomerun_control_sock_t *cs);
+
+/*
+ * Low-level communication.
+ */
+extern LIBTYPE int hdhomerun_control_send_recv(struct hdhomerun_control_sock_t *cs, struct hdhomerun_pkt_t *tx_pkt, struct hdhomerun_pkt_t *rx_pkt, uint16_t type);
+
+/*
+ * Get/set a control variable on the device.
+ *
+ * const char *name: The name of var to get/set (c-string). The supported vars is device/firmware dependant.
+ * const char *value: The value to set (c-string). The format is device/firmware dependant.
+
+ * char **pvalue: If provided, the caller-supplied char pointer will be populated with a pointer to the value
+ *		string returned by the device, or NULL if the device returned an error string. The string will remain
+ *		valid until the next call to a control sock function.
+ * char **perror: If provided, the caller-supplied char pointer will be populated with a pointer to the error
+ *		string returned by the device, or NULL if the device returned an value string. The string will remain
+ *		valid until the next call to a control sock function.
+ *
+ * Returns 1 if the operation was successful (pvalue set, perror NULL).
+ * Returns 0 if the operation was rejected (pvalue NULL, perror set).
+ * Returns -1 if a communication error occurs.
+ */
+extern LIBTYPE int hdhomerun_control_get(struct hdhomerun_control_sock_t *cs, const char *name, char **pvalue, char **perror);
+extern LIBTYPE int hdhomerun_control_set(struct hdhomerun_control_sock_t *cs, const char *name, const char *value, char **pvalue, char **perror);
+extern LIBTYPE int hdhomerun_control_set_with_lockkey(struct hdhomerun_control_sock_t *cs, const char *name, const char *value, uint32_t lockkey, char **pvalue, char **perror);
+
+/*
+ * Upload new firmware to the device.
+ *
+ * FILE *upgrade_file: File pointer to read from. The file must have been opened in binary mode for reading.
+ *
+ * Returns 1 if the upload succeeded.
+ * Returns 0 if the upload was rejected.
+ * Returns -1 if an error occurs.
+ */
+extern LIBTYPE int hdhomerun_control_upgrade(struct hdhomerun_control_sock_t *cs, FILE *upgrade_file);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_debug.c b/libhdhomerun/hdhomerun_debug.c
new file mode 100755
index 0000000..9686f61
--- /dev/null
+++ b/libhdhomerun/hdhomerun_debug.c
@@ -0,0 +1,477 @@
+/*
+ * hdhomerun_debug.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+/*
+ * The debug logging includes optional support for connecting to the
+ * Silicondust support server. This option should not be used without
+ * being explicitly enabled by the user. Debug information should be
+ * limited to information useful to diagnosing a problem.
+ *  - Silicondust.
+ */
+
+#include "hdhomerun.h"
+
+#if !defined(HDHOMERUN_DEBUG_HOST)
+#define HDHOMERUN_DEBUG_HOST "debug.silicondust.com"
+#endif
+#if !defined(HDHOMERUN_DEBUG_PORT)
+#define HDHOMERUN_DEBUG_PORT 8002
+#endif
+
+#define HDHOMERUN_DEBUG_CONNECT_RETRY_TIME 30000
+#define HDHOMERUN_DEBUG_CONNECT_TIMEOUT 10000
+#define HDHOMERUN_DEBUG_SEND_TIMEOUT 10000
+
+struct hdhomerun_debug_message_t
+{
+	struct hdhomerun_debug_message_t *next;
+	struct hdhomerun_debug_message_t *prev;
+	char buffer[2048];
+};
+
+struct hdhomerun_debug_t
+{
+	pthread_t thread;
+	volatile bool_t enabled;
+	volatile bool_t terminate;
+	char *prefix;
+
+	pthread_mutex_t print_lock;
+	pthread_mutex_t queue_lock;
+	pthread_mutex_t send_lock;
+
+	struct hdhomerun_debug_message_t *queue_head;
+	struct hdhomerun_debug_message_t *queue_tail;
+	uint32_t queue_depth;
+
+	uint64_t connect_delay;
+
+	char *file_name;
+	FILE *file_fp;
+	hdhomerun_sock_t sock;
+};
+
+static THREAD_FUNC_PREFIX hdhomerun_debug_thread_execute(void *arg);
+
+struct hdhomerun_debug_t *hdhomerun_debug_create(void)
+{
+	struct hdhomerun_debug_t *dbg = (struct hdhomerun_debug_t *)calloc(1, sizeof(struct hdhomerun_debug_t));
+	if (!dbg) {
+		return NULL;
+	}
+
+	dbg->sock = HDHOMERUN_SOCK_INVALID;
+
+	pthread_mutex_init(&dbg->print_lock, NULL);
+	pthread_mutex_init(&dbg->queue_lock, NULL);
+	pthread_mutex_init(&dbg->send_lock, NULL);
+
+	if (pthread_create(&dbg->thread, NULL, &hdhomerun_debug_thread_execute, dbg) != 0) {
+		free(dbg);
+		return NULL;
+	}
+
+	return dbg;
+}
+
+void hdhomerun_debug_destroy(struct hdhomerun_debug_t *dbg)
+{
+	if (!dbg) {
+		return;
+	}
+
+	dbg->terminate = TRUE;
+	pthread_join(dbg->thread, NULL);
+
+	if (dbg->prefix) {
+		free(dbg->prefix);
+	}
+	if (dbg->file_name) {
+		free(dbg->file_name);
+	}
+	if (dbg->file_fp) {
+		fclose(dbg->file_fp);
+	}
+	if (dbg->sock != HDHOMERUN_SOCK_INVALID) {
+		hdhomerun_sock_destroy(dbg->sock);
+	}
+
+	free(dbg);
+}
+
+/* Send lock held by caller */
+static void hdhomerun_debug_close_internal(struct hdhomerun_debug_t *dbg)
+{
+	if (dbg->file_fp) {
+		fclose(dbg->file_fp);
+		dbg->file_fp = NULL;
+	}
+
+	if (dbg->sock != HDHOMERUN_SOCK_INVALID) {
+		hdhomerun_sock_destroy(dbg->sock);
+		dbg->sock = HDHOMERUN_SOCK_INVALID;
+	}
+}
+
+void hdhomerun_debug_close(struct hdhomerun_debug_t *dbg, uint64_t timeout)
+{
+	if (!dbg) {
+		return;
+	}
+
+	if (timeout > 0) {
+		hdhomerun_debug_flush(dbg, timeout);
+	}
+
+	pthread_mutex_lock(&dbg->send_lock);
+	hdhomerun_debug_close_internal(dbg);
+	dbg->connect_delay = 0;
+	pthread_mutex_unlock(&dbg->send_lock);
+}
+
+void hdhomerun_debug_set_filename(struct hdhomerun_debug_t *dbg, const char *filename)
+{
+	if (!dbg) {
+		return;
+	}
+
+	pthread_mutex_lock(&dbg->send_lock);
+
+	if (!filename && !dbg->file_name) {
+		pthread_mutex_unlock(&dbg->send_lock);
+		return;
+	}
+	if (filename && dbg->file_name) {
+		if (strcmp(filename, dbg->file_name) == 0) {
+			pthread_mutex_unlock(&dbg->send_lock);
+			return;
+		}
+	}
+
+	hdhomerun_debug_close_internal(dbg);
+	dbg->connect_delay = 0;
+
+	if (dbg->file_name) {
+		free(dbg->file_name);
+		dbg->file_name = NULL;
+	}
+	if (filename) {
+		dbg->file_name = strdup(filename);
+	}
+
+	pthread_mutex_unlock(&dbg->send_lock);
+}
+
+void hdhomerun_debug_set_prefix(struct hdhomerun_debug_t *dbg, const char *prefix)
+{
+	if (!dbg) {
+		return;
+	}
+
+	pthread_mutex_lock(&dbg->print_lock);
+
+	if (dbg->prefix) {
+		free(dbg->prefix);
+		dbg->prefix = NULL;
+	}
+
+	if (prefix) {
+		dbg->prefix = strdup(prefix);
+	}
+
+	pthread_mutex_unlock(&dbg->print_lock);
+}
+
+void hdhomerun_debug_enable(struct hdhomerun_debug_t *dbg)
+{
+	if (!dbg) {
+		return;
+	}
+
+	dbg->enabled = TRUE;
+}
+
+void hdhomerun_debug_disable(struct hdhomerun_debug_t *dbg)
+{
+	if (!dbg) {
+		return;
+	}
+
+	dbg->enabled = FALSE;
+}
+
+bool_t hdhomerun_debug_enabled(struct hdhomerun_debug_t *dbg)
+{
+	if (!dbg) {
+		return FALSE;
+	}
+
+	return dbg->enabled;
+}
+
+void hdhomerun_debug_flush(struct hdhomerun_debug_t *dbg, uint64_t timeout)
+{
+	if (!dbg) {
+		return;
+	}
+
+	timeout = getcurrenttime() + timeout;
+
+	while (getcurrenttime() < timeout) {
+		pthread_mutex_lock(&dbg->queue_lock);
+		struct hdhomerun_debug_message_t *message = dbg->queue_tail;
+		pthread_mutex_unlock(&dbg->queue_lock);
+
+		if (!message) {
+			return;
+		}
+
+		msleep_approx(10);
+	}
+}
+
+void hdhomerun_debug_printf(struct hdhomerun_debug_t *dbg, const char *fmt, ...)
+{
+	va_list args;
+	va_start(args, fmt);
+	hdhomerun_debug_vprintf(dbg, fmt, args);
+	va_end(args);
+}
+
+void hdhomerun_debug_vprintf(struct hdhomerun_debug_t *dbg, const char *fmt, va_list args)
+{
+	if (!dbg) {
+		return;
+	}
+	if (!dbg->enabled) {
+		return;
+	}
+
+	struct hdhomerun_debug_message_t *message = (struct hdhomerun_debug_message_t *)malloc(sizeof(struct hdhomerun_debug_message_t));
+	if (!message) {
+		return;
+	}
+
+	char *ptr = message->buffer;
+	char *end = message->buffer + sizeof(message->buffer) - 2;
+	*end = 0;
+
+	/*
+	 * Timestamp.
+	 */
+	time_t current_time = time(NULL);
+	ptr += strftime(ptr, end - ptr, "%Y%m%d-%H:%M:%S ", localtime(&current_time));
+	if (ptr > end) {
+		ptr = end;
+	}
+
+	/*
+	 * Debug prefix.
+	 */
+	pthread_mutex_lock(&dbg->print_lock);
+
+	if (dbg->prefix) {
+		int len = snprintf(ptr, end - ptr, "%s ", dbg->prefix);
+		len = (len <= 0) ? 0 : len;
+		ptr += len;
+		if (ptr > end) {
+			ptr = end;
+		}
+	}
+
+	pthread_mutex_unlock(&dbg->print_lock);
+
+	/*
+	 * Message text.
+	 */
+	int len = vsnprintf(ptr, end - ptr, fmt, args);
+	len = (len < 0) ? 0 : len; /* len does not include null */
+	ptr += len;
+	if (ptr > end) {
+		ptr = end;
+	}
+
+	/*
+	 * Force newline.
+	 */
+	if ((ptr[-1] != '\n') && (ptr + 1 <= end)) {
+		*ptr++ = '\n';
+	}
+
+	/*
+	 * Force NULL.
+	 */
+	if (ptr + 1 > end) {
+		ptr = end - 1;
+	}
+	*ptr++ = 0;
+
+	/*
+	 * Enqueue.
+	 */
+	pthread_mutex_lock(&dbg->queue_lock);
+
+	message->prev = NULL;
+	message->next = dbg->queue_head;
+	dbg->queue_head = message;
+	if (message->next) {
+		message->next->prev = message;
+	} else {
+		dbg->queue_tail = message;
+	}
+	dbg->queue_depth++;
+
+	pthread_mutex_unlock(&dbg->queue_lock);
+}
+
+/* Send lock held by caller */
+static bool_t hdhomerun_debug_output_message_file(struct hdhomerun_debug_t *dbg, struct hdhomerun_debug_message_t *message)
+{
+	if (!dbg->file_fp) {
+		uint64_t current_time = getcurrenttime();
+		if (current_time < dbg->connect_delay) {
+			return FALSE;
+		}
+		dbg->connect_delay = current_time + 30*1000;
+
+		dbg->file_fp = fopen(dbg->file_name, "a");
+		if (!dbg->file_fp) {
+			return FALSE;
+		}
+	}
+
+	fprintf(dbg->file_fp, "%s", message->buffer);
+	fflush(dbg->file_fp);
+
+	return TRUE;
+}
+
+/* Send lock held by caller */
+static bool_t hdhomerun_debug_output_message_sock(struct hdhomerun_debug_t *dbg, struct hdhomerun_debug_message_t *message)
+{
+	if (dbg->sock == HDHOMERUN_SOCK_INVALID) {
+		uint64_t current_time = getcurrenttime();
+		if (current_time < dbg->connect_delay) {
+			return FALSE;
+		}
+		dbg->connect_delay = current_time + HDHOMERUN_DEBUG_CONNECT_RETRY_TIME;
+
+		dbg->sock = hdhomerun_sock_create_tcp();
+		if (dbg->sock == HDHOMERUN_SOCK_INVALID) {
+			return FALSE;
+		}
+
+		uint32_t remote_addr = hdhomerun_sock_getaddrinfo_addr(dbg->sock, HDHOMERUN_DEBUG_HOST);
+		if (remote_addr == 0) {
+			hdhomerun_debug_close_internal(dbg);
+			return FALSE;
+		}
+
+		if (!hdhomerun_sock_connect(dbg->sock, remote_addr, HDHOMERUN_DEBUG_PORT, HDHOMERUN_DEBUG_CONNECT_TIMEOUT)) {
+			hdhomerun_debug_close_internal(dbg);
+			return FALSE;
+		}
+	}
+
+	size_t length = strlen(message->buffer);
+	if (!hdhomerun_sock_send(dbg->sock, message->buffer, length, HDHOMERUN_DEBUG_SEND_TIMEOUT)) {
+		hdhomerun_debug_close_internal(dbg);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static bool_t hdhomerun_debug_output_message(struct hdhomerun_debug_t *dbg, struct hdhomerun_debug_message_t *message)
+{
+	pthread_mutex_lock(&dbg->send_lock);
+
+	bool_t ret;
+	if (dbg->file_name) {
+		ret = hdhomerun_debug_output_message_file(dbg, message);
+	} else {
+		ret = hdhomerun_debug_output_message_sock(dbg, message);
+	}
+
+	pthread_mutex_unlock(&dbg->send_lock);
+	return ret;
+}
+
+static void hdhomerun_debug_pop_and_free_message(struct hdhomerun_debug_t *dbg)
+{
+	pthread_mutex_lock(&dbg->queue_lock);
+
+	struct hdhomerun_debug_message_t *message = dbg->queue_tail;
+	dbg->queue_tail = message->prev;
+	if (message->prev) {
+		message->prev->next = NULL;
+	} else {
+		dbg->queue_head = NULL;
+	}
+	dbg->queue_depth--;
+
+	pthread_mutex_unlock(&dbg->queue_lock);
+
+	free(message);
+}
+
+static THREAD_FUNC_PREFIX hdhomerun_debug_thread_execute(void *arg)
+{
+	struct hdhomerun_debug_t *dbg = (struct hdhomerun_debug_t *)arg;
+
+	while (!dbg->terminate) {
+
+		pthread_mutex_lock(&dbg->queue_lock);
+		struct hdhomerun_debug_message_t *message = dbg->queue_tail;
+		uint32_t queue_depth = dbg->queue_depth;
+		pthread_mutex_unlock(&dbg->queue_lock);
+
+		if (!message) {
+			msleep_approx(250);
+			continue;
+		}
+
+		if (queue_depth > 1024) {
+			hdhomerun_debug_pop_and_free_message(dbg);
+			continue;
+		}
+
+		if (!hdhomerun_debug_output_message(dbg, message)) {
+			msleep_approx(250);
+			continue;
+		}
+
+		hdhomerun_debug_pop_and_free_message(dbg);
+	}
+
+	return 0;
+}
diff --git a/libhdhomerun/hdhomerun_debug.h b/libhdhomerun/hdhomerun_debug.h
new file mode 100755
index 0000000..c7831ea
--- /dev/null
+++ b/libhdhomerun/hdhomerun_debug.h
@@ -0,0 +1,64 @@
+/*
+ * hdhomerun_debug.h
+ *
+ * Copyright © 2006 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+/*
+ * The debug logging includes optional support for connecting to the
+ * Silicondust support server. This option should not be used without
+ * being explicitly enabled by the user. Debug information should be
+ * limited to information useful to diagnosing a problem.
+ *  - Silicondust.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hdhomerun_debug_t;
+
+extern LIBTYPE struct hdhomerun_debug_t *hdhomerun_debug_create(void);
+extern LIBTYPE void hdhomerun_debug_destroy(struct hdhomerun_debug_t *dbg);
+
+extern LIBTYPE void hdhomerun_debug_set_prefix(struct hdhomerun_debug_t *dbg, const char *prefix);
+extern LIBTYPE void hdhomerun_debug_set_filename(struct hdhomerun_debug_t *dbg, const char *filename);
+extern LIBTYPE void hdhomerun_debug_enable(struct hdhomerun_debug_t *dbg);
+extern LIBTYPE void hdhomerun_debug_disable(struct hdhomerun_debug_t *dbg);
+extern LIBTYPE bool_t hdhomerun_debug_enabled(struct hdhomerun_debug_t *dbg);
+
+extern LIBTYPE void hdhomerun_debug_flush(struct hdhomerun_debug_t *dbg, uint64_t timeout);
+extern LIBTYPE void hdhomerun_debug_close(struct hdhomerun_debug_t *dbg, uint64_t timeout);
+
+extern LIBTYPE void hdhomerun_debug_printf(struct hdhomerun_debug_t *dbg, const char *fmt, ...);
+extern LIBTYPE void hdhomerun_debug_vprintf(struct hdhomerun_debug_t *dbg, const char *fmt, va_list args);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_device.c b/libhdhomerun/hdhomerun_device.c
new file mode 100755
index 0000000..059c043
--- /dev/null
+++ b/libhdhomerun/hdhomerun_device.c
@@ -0,0 +1,1407 @@
+/*
+ * hdhomerun_device.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+struct hdhomerun_device_t {
+	struct hdhomerun_control_sock_t *cs;
+	struct hdhomerun_video_sock_t *vs;
+	struct hdhomerun_debug_t *dbg;
+	struct hdhomerun_channelscan_t *scan;
+	uint32_t multicast_ip;
+	uint16_t multicast_port;
+	uint32_t device_id;
+	unsigned int tuner;
+	uint32_t lockkey;
+	char name[32];
+	char model[32];
+};
+
+static int hdhomerun_device_set_device_normal(struct hdhomerun_device_t *hd, uint32_t device_id, uint32_t device_ip)
+{
+	if (!hd->cs) {
+		hd->cs = hdhomerun_control_create(0, 0, hd->dbg);
+		if (!hd->cs) {
+			hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_device: failed to create control object\n");
+			return -1;
+		}
+	}
+
+	hdhomerun_control_set_device(hd->cs, device_id, device_ip);
+
+	if ((device_id == 0) || (device_id == HDHOMERUN_DEVICE_ID_WILDCARD)) {
+		device_id = hdhomerun_control_get_device_id(hd->cs);
+	}
+
+	hd->multicast_ip = 0;
+	hd->multicast_port = 0;
+	hd->device_id = device_id;
+	hd->tuner = 0;
+	hd->lockkey = 0;
+
+	sprintf(hd->name, "%08lX-%u", (unsigned long)hd->device_id, hd->tuner);
+	hd->model[0] = 0; /* clear cached model string */
+
+	return 1;
+}
+
+static int hdhomerun_device_set_device_multicast(struct hdhomerun_device_t *hd, uint32_t multicast_ip)
+{
+	if (hd->cs) {
+		hdhomerun_control_destroy(hd->cs);
+		hd->cs = NULL;
+	}
+
+	hd->multicast_ip = multicast_ip;
+	hd->multicast_port = 0;
+	hd->device_id = 0;
+	hd->tuner = 0;
+	hd->lockkey = 0;
+
+	unsigned long ip = multicast_ip;
+	sprintf(hd->name, "%lu.%lu.%lu.%lu", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, (ip >> 0) & 0xFF);
+	sprintf(hd->model, "multicast");
+
+	return 1;
+}
+
+int hdhomerun_device_set_device(struct hdhomerun_device_t *hd, uint32_t device_id, uint32_t device_ip)
+{
+	if ((device_id == 0) && (device_ip == 0)) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_device: device not specified\n");
+		return -1;
+	}
+
+	if (hdhomerun_discover_is_ip_multicast(device_ip)) {
+		return hdhomerun_device_set_device_multicast(hd, device_ip);
+	}
+
+	return hdhomerun_device_set_device_normal(hd, device_id, device_ip);
+}
+
+int hdhomerun_device_set_tuner(struct hdhomerun_device_t *hd, unsigned int tuner)
+{
+	if (hd->multicast_ip != 0) {
+		if (tuner != 0) {
+			hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner: tuner cannot be specified in multicast mode\n");
+			return -1;
+		}
+
+		return 1;
+	}
+
+	hd->tuner = tuner;
+	sprintf(hd->name, "%08lX-%u", (unsigned long)hd->device_id, hd->tuner);
+
+	return 1;
+}
+
+struct hdhomerun_device_t *hdhomerun_device_create(uint32_t device_id, uint32_t device_ip, unsigned int tuner, struct hdhomerun_debug_t *dbg)
+{
+	struct hdhomerun_device_t *hd = (struct hdhomerun_device_t *)calloc(1, sizeof(struct hdhomerun_device_t));
+	if (!hd) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_device_create: failed to allocate device object\n");
+		return NULL;
+	}
+
+	hd->dbg = dbg;
+
+	if ((device_id == 0) && (device_ip == 0) && (tuner == 0)) {
+		return hd;
+	}
+
+	if (hdhomerun_device_set_device(hd, device_id, device_ip) <= 0) {
+		free(hd);
+		return NULL;
+	}
+	if (hdhomerun_device_set_tuner(hd, tuner) <= 0) {
+		free(hd);
+		return NULL;
+	}
+
+	return hd;
+}
+
+void hdhomerun_device_destroy(struct hdhomerun_device_t *hd)
+{
+	if (hd->scan) {
+		channelscan_destroy(hd->scan);
+	}
+
+	if (hd->vs) {
+		hdhomerun_video_destroy(hd->vs);
+	}
+
+	if (hd->cs) {
+		hdhomerun_control_destroy(hd->cs);
+	}
+
+	free(hd);
+}
+
+static bool_t is_hex_char(char c)
+{
+	if ((c >= '0') && (c <= '9')) {
+		return TRUE;
+	}
+	if ((c >= 'A') && (c <= 'F')) {
+		return TRUE;
+	}
+	if ((c >= 'a') && (c <= 'f')) {
+		return TRUE;
+	}
+	return FALSE;
+}
+
+static struct hdhomerun_device_t *hdhomerun_device_create_from_str_device_id(const char *device_str, struct hdhomerun_debug_t *dbg)
+{
+	int i;
+	const char *ptr = device_str;
+	for (i = 0; i < 8; i++) {
+		if (!is_hex_char(*ptr++)) {
+			return NULL;
+		}
+	}
+
+	if (*ptr == 0) {
+		unsigned long device_id;
+		if (sscanf(device_str, "%lx", &device_id) != 1) {
+			return NULL;
+		}
+		return hdhomerun_device_create((uint32_t)device_id, 0, 0, dbg);
+	}
+
+	if (*ptr == '-') {
+		unsigned long device_id;
+		unsigned int tuner;
+		if (sscanf(device_str, "%lx-%u", &device_id, &tuner) != 2) {
+			return NULL;
+		}
+		return hdhomerun_device_create((uint32_t)device_id, 0, tuner, dbg);
+	}
+
+	return NULL;
+}
+
+static struct hdhomerun_device_t *hdhomerun_device_create_from_str_ip_result(unsigned long a[4], unsigned int port, unsigned int tuner, struct hdhomerun_debug_t *dbg)
+{
+	unsigned long device_ip = (a[0] << 24) | (a[1] << 16) | (a[2] << 8) | (a[3] << 0);
+	struct hdhomerun_device_t *hd = hdhomerun_device_create(HDHOMERUN_DEVICE_ID_WILDCARD, (uint32_t)device_ip, tuner, dbg);
+	if (!hd) {
+		return NULL;
+	}
+
+	if (hd->multicast_ip != 0) {
+		hd->multicast_port = (uint16_t)port;
+	}
+
+	return hd;
+}
+
+static struct hdhomerun_device_t *hdhomerun_device_create_from_str_ip(const char *device_str, struct hdhomerun_debug_t *dbg)
+{
+	unsigned long a[4];
+	unsigned int port = 0;
+	unsigned int tuner = 0;
+
+	if (sscanf(device_str, "%lu.%lu.%lu.%lu:%u", &a[0], &a[1], &a[2], &a[3], &port) == 5) {
+		return hdhomerun_device_create_from_str_ip_result(a, port, tuner, dbg);
+	}
+	if (sscanf(device_str, "%lu.%lu.%lu.%lu-%u", &a[0], &a[1], &a[2], &a[3], &tuner) == 5) {
+		return hdhomerun_device_create_from_str_ip_result(a, port, tuner, dbg);
+	}
+	if (sscanf(device_str, "%lu.%lu.%lu.%lu", &a[0], &a[1], &a[2], &a[3]) == 4) {
+		return hdhomerun_device_create_from_str_ip_result(a, port, tuner, dbg);
+	}
+
+	return NULL;
+}
+
+static struct hdhomerun_device_t *hdhomerun_device_create_from_str_dns(const char *device_str, struct hdhomerun_debug_t *dbg)
+{
+#if defined(__CYGWIN__)
+	return NULL;
+#else
+	struct addrinfo hints;
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_INET;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_protocol = IPPROTO_TCP;
+
+	struct addrinfo *sock_info;
+	if (getaddrinfo(device_str, "65001", &hints, &sock_info) != 0) {
+		return NULL;
+	}
+
+	struct sockaddr_in *sock_addr = (struct sockaddr_in *)sock_info->ai_addr;
+	uint32_t device_ip = ntohl(sock_addr->sin_addr.s_addr);
+	freeaddrinfo(sock_info);
+
+	if (device_ip == 0) {
+		return NULL;
+	}
+
+	return hdhomerun_device_create(HDHOMERUN_DEVICE_ID_WILDCARD, (uint32_t)device_ip, 0, dbg);
+#endif
+}
+
+struct hdhomerun_device_t *hdhomerun_device_create_from_str(const char *device_str, struct hdhomerun_debug_t *dbg)
+{
+	struct hdhomerun_device_t *device = hdhomerun_device_create_from_str_device_id(device_str, dbg);
+	if (device) {
+		return device;
+	}
+
+	device = hdhomerun_device_create_from_str_ip(device_str, dbg);
+	if (device) {
+		return device;
+	}
+
+	device = hdhomerun_device_create_from_str_dns(device_str, dbg);
+	if (device) {
+		return device;
+	}
+
+	return NULL;
+}
+
+int hdhomerun_device_set_tuner_from_str(struct hdhomerun_device_t *hd, const char *tuner_str)
+{
+	unsigned int tuner;
+	if (sscanf(tuner_str, "%u", &tuner) == 1) {
+		hdhomerun_device_set_tuner(hd, tuner);
+		return 1;
+	}
+	if (sscanf(tuner_str, "/tuner%u", &tuner) == 1) {
+		hdhomerun_device_set_tuner(hd, tuner);
+		return 1;
+	}
+
+	return -1;
+}
+
+const char *hdhomerun_device_get_name(struct hdhomerun_device_t *hd)
+{
+	return hd->name;
+}
+
+uint32_t hdhomerun_device_get_device_id(struct hdhomerun_device_t *hd)
+{
+	return hd->device_id;
+}
+
+uint32_t hdhomerun_device_get_device_ip(struct hdhomerun_device_t *hd)
+{
+	if (hd->multicast_ip != 0) {
+		return hd->multicast_ip;
+	}
+	if (hd->cs) {
+		return hdhomerun_control_get_device_ip(hd->cs);
+	}
+
+	return 0;
+}
+
+uint32_t hdhomerun_device_get_device_id_requested(struct hdhomerun_device_t *hd)
+{
+	if (hd->multicast_ip != 0) {
+		return 0;
+	}
+	if (hd->cs) {
+		return hdhomerun_control_get_device_id_requested(hd->cs);
+	}
+
+	return 0;
+}
+
+uint32_t hdhomerun_device_get_device_ip_requested(struct hdhomerun_device_t *hd)
+{
+	if (hd->multicast_ip != 0) {
+		return hd->multicast_ip;
+	}
+	if (hd->cs) {
+		return hdhomerun_control_get_device_ip_requested(hd->cs);
+	}
+
+	return 0;
+}
+
+unsigned int hdhomerun_device_get_tuner(struct hdhomerun_device_t *hd)
+{
+	return hd->tuner;
+}
+
+struct hdhomerun_control_sock_t *hdhomerun_device_get_control_sock(struct hdhomerun_device_t *hd)
+{
+	return hd->cs;
+}
+
+struct hdhomerun_video_sock_t *hdhomerun_device_get_video_sock(struct hdhomerun_device_t *hd)
+{
+	if (hd->vs) {
+		return hd->vs;
+	}
+
+	bool_t allow_port_reuse = (hd->multicast_port != 0);
+
+	hd->vs = hdhomerun_video_create(hd->multicast_port, allow_port_reuse, VIDEO_DATA_BUFFER_SIZE_1S * 2, hd->dbg);
+	if (!hd->vs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_video_sock: failed to create video object\n");
+		return NULL;
+	}
+
+	return hd->vs;
+}
+
+uint32_t hdhomerun_device_get_local_machine_addr(struct hdhomerun_device_t *hd)
+{
+	if (hd->cs) {
+		return hdhomerun_control_get_local_addr(hd->cs);
+	}
+
+	return 0;
+}
+
+static uint32_t hdhomerun_device_get_status_parse(const char *status_str, const char *tag)
+{
+	const char *ptr = strstr(status_str, tag);
+	if (!ptr) {
+		return 0;
+	}
+
+	unsigned long value = 0;
+	sscanf(ptr + strlen(tag), "%lu", &value);
+
+	return (uint32_t)value;
+}
+
+static bool_t hdhomerun_device_get_tuner_status_lock_is_bcast(struct hdhomerun_tuner_status_t *status)
+{
+	if (strcmp(status->lock_str, "8vsb") == 0) {
+		return TRUE;
+	}
+	if (strncmp(status->lock_str, "t8", 2) == 0) {
+		return TRUE;
+	}
+	if (strncmp(status->lock_str, "t7", 2) == 0) {
+		return TRUE;
+	}
+	if (strncmp(status->lock_str, "t6", 2) == 0) {
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+uint32_t hdhomerun_device_get_tuner_status_ss_color(struct hdhomerun_tuner_status_t *status)
+{
+	unsigned int ss_yellow_min;
+	unsigned int ss_green_min;
+
+	if (!status->lock_supported) {
+		return HDHOMERUN_STATUS_COLOR_NEUTRAL;
+	}
+
+	if (hdhomerun_device_get_tuner_status_lock_is_bcast(status)) {
+		ss_yellow_min = 50;	/* -30dBmV */
+		ss_green_min = 75;	/* -15dBmV */
+	} else {
+		ss_yellow_min = 80;	/* -12dBmV */
+		ss_green_min = 90;	/* -6dBmV */
+	}
+
+	if (status->signal_strength >= ss_green_min) {
+		return HDHOMERUN_STATUS_COLOR_GREEN;
+	}
+	if (status->signal_strength >= ss_yellow_min) {
+		return HDHOMERUN_STATUS_COLOR_YELLOW;
+	}
+
+	return HDHOMERUN_STATUS_COLOR_RED;
+}
+
+uint32_t hdhomerun_device_get_tuner_status_snq_color(struct hdhomerun_tuner_status_t *status)
+{
+	if (status->signal_to_noise_quality >= 70) {
+		return HDHOMERUN_STATUS_COLOR_GREEN;
+	}
+	if (status->signal_to_noise_quality >= 50) {
+		return HDHOMERUN_STATUS_COLOR_YELLOW;
+	}
+
+	return HDHOMERUN_STATUS_COLOR_RED;
+}
+
+uint32_t hdhomerun_device_get_tuner_status_seq_color(struct hdhomerun_tuner_status_t *status)
+{
+	if (status->symbol_error_quality >= 100) {
+		return HDHOMERUN_STATUS_COLOR_GREEN;
+	}
+
+	return HDHOMERUN_STATUS_COLOR_RED;
+}
+
+int hdhomerun_device_get_tuner_status(struct hdhomerun_device_t *hd, char **pstatus_str, struct hdhomerun_tuner_status_t *status)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_status: device not set\n");
+		return -1;
+	}
+
+	memset(status, 0, sizeof(struct hdhomerun_tuner_status_t));
+
+	char name[32];
+	sprintf(name, "/tuner%u/status", hd->tuner);
+
+	char *status_str;
+	int ret = hdhomerun_control_get(hd->cs, name, &status_str, NULL);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	if (pstatus_str) {
+		*pstatus_str = status_str;
+	}
+
+	if (status) {
+		char *channel = strstr(status_str, "ch=");
+		if (channel) {
+			sscanf(channel + 3, "%31s", status->channel);
+		}
+
+		char *lock = strstr(status_str, "lock=");
+		if (lock) {
+			sscanf(lock + 5, "%31s", status->lock_str);
+		}
+
+		status->signal_strength = (unsigned int)hdhomerun_device_get_status_parse(status_str, "ss=");
+		status->signal_to_noise_quality = (unsigned int)hdhomerun_device_get_status_parse(status_str, "snq=");
+		status->symbol_error_quality = (unsigned int)hdhomerun_device_get_status_parse(status_str, "seq=");
+		status->raw_bits_per_second = hdhomerun_device_get_status_parse(status_str, "bps=");
+		status->packets_per_second = hdhomerun_device_get_status_parse(status_str, "pps=");
+
+		status->signal_present = status->signal_strength >= 45;
+
+		if (strcmp(status->lock_str, "none") != 0) {
+			if (status->lock_str[0] == '(') {
+				status->lock_unsupported = TRUE;
+			} else {
+				status->lock_supported = TRUE;
+			}
+		}
+	}
+
+	return 1;
+}
+
+int hdhomerun_device_get_oob_status(struct hdhomerun_device_t *hd, char **pstatus_str, struct hdhomerun_tuner_status_t *status)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_oob_status: device not set\n");
+		return -1;
+	}
+
+	memset(status, 0, sizeof(struct hdhomerun_tuner_status_t));
+
+	char *status_str;
+	int ret = hdhomerun_control_get(hd->cs, "/oob/status", &status_str, NULL);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	if (pstatus_str) {
+		*pstatus_str = status_str;
+	}
+
+	if (status) {
+		char *channel = strstr(status_str, "ch=");
+		if (channel) {
+			sscanf(channel + 3, "%31s", status->channel);
+		}
+
+		char *lock = strstr(status_str, "lock=");
+		if (lock) {
+			sscanf(lock + 5, "%31s", status->lock_str);
+		}
+
+		status->signal_strength = (unsigned int)hdhomerun_device_get_status_parse(status_str, "ss=");
+		status->signal_to_noise_quality = (unsigned int)hdhomerun_device_get_status_parse(status_str, "snq=");
+		status->signal_present = status->signal_strength >= 45;
+		status->lock_supported = (strcmp(status->lock_str, "none") != 0);
+	}
+
+	return 1;
+}
+
+int hdhomerun_device_get_tuner_vstatus(struct hdhomerun_device_t *hd, char **pvstatus_str, struct hdhomerun_tuner_vstatus_t *vstatus)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_vstatus: device not set\n");
+		return -1;
+	}
+
+	memset(vstatus, 0, sizeof(struct hdhomerun_tuner_vstatus_t));
+
+	char var_name[32];
+	sprintf(var_name, "/tuner%u/vstatus", hd->tuner);
+
+	char *vstatus_str;
+	int ret = hdhomerun_control_get(hd->cs, var_name, &vstatus_str, NULL);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	if (pvstatus_str) {
+		*pvstatus_str = vstatus_str;
+	}
+
+	if (vstatus) {
+		char *vch = strstr(vstatus_str, "vch=");
+		if (vch) {
+			sscanf(vch + 4, "%31s", vstatus->vchannel);
+		}
+
+		char *name = strstr(vstatus_str, "name=");
+		if (name) {
+			sscanf(name + 5, "%31s", vstatus->name);
+		}
+
+		char *auth = strstr(vstatus_str, "auth=");
+		if (auth) {
+			sscanf(auth + 5, "%31s", vstatus->auth);
+		}
+
+		char *cci = strstr(vstatus_str, "cci=");
+		if (cci) {
+			sscanf(cci + 4, "%31s", vstatus->cci);
+		}
+
+		char *cgms = strstr(vstatus_str, "cgms=");
+		if (cgms) {
+			sscanf(cgms + 5, "%31s", vstatus->cgms);
+		}
+
+		if (strncmp(vstatus->auth, "not-subscribed", 14) == 0) {
+			vstatus->not_subscribed = TRUE;
+		}
+
+		if (strncmp(vstatus->auth, "error", 5) == 0) {
+			vstatus->not_available = TRUE;
+		}
+		if (strncmp(vstatus->auth, "dialog", 6) == 0) {
+			vstatus->not_available = TRUE;
+		}
+
+		if (strncmp(vstatus->cci, "protected", 9) == 0) {
+			vstatus->copy_protected = TRUE;
+		}
+		if (strncmp(vstatus->cgms, "protected", 9) == 0) {
+			vstatus->copy_protected = TRUE;
+		}
+	}
+
+	return 1;
+}
+
+int hdhomerun_device_get_tuner_streaminfo(struct hdhomerun_device_t *hd, char **pstreaminfo)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_streaminfo: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/streaminfo", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, pstreaminfo, NULL);
+}
+
+int hdhomerun_device_get_tuner_channel(struct hdhomerun_device_t *hd, char **pchannel)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_channel: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/channel", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, pchannel, NULL);
+}
+
+int hdhomerun_device_get_tuner_vchannel(struct hdhomerun_device_t *hd, char **pvchannel)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_vchannel: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/vchannel", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, pvchannel, NULL);
+}
+
+int hdhomerun_device_get_tuner_channelmap(struct hdhomerun_device_t *hd, char **pchannelmap)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_channelmap: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/channelmap", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, pchannelmap, NULL);
+}
+
+int hdhomerun_device_get_tuner_filter(struct hdhomerun_device_t *hd, char **pfilter)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_filter: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/filter", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, pfilter, NULL);
+}
+
+int hdhomerun_device_get_tuner_program(struct hdhomerun_device_t *hd, char **pprogram)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_program: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/program", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, pprogram, NULL);
+}
+
+int hdhomerun_device_get_tuner_target(struct hdhomerun_device_t *hd, char **ptarget)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_target: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/target", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, ptarget, NULL);
+}
+
+static int hdhomerun_device_get_tuner_plotsample_internal(struct hdhomerun_device_t *hd, const char *name, struct hdhomerun_plotsample_t **psamples, size_t *pcount)
+{
+	char *result;
+	int ret = hdhomerun_control_get(hd->cs, name, &result, NULL);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	struct hdhomerun_plotsample_t *samples = (struct hdhomerun_plotsample_t *)result;
+	*psamples = samples;
+	size_t count = 0;
+
+	while (1) {
+		char *ptr = strchr(result, ' ');
+		if (!ptr) {
+			break;
+		}
+		*ptr++ = 0;
+
+		unsigned long raw;
+		if (sscanf(result, "%lx", &raw) != 1) {
+			break;
+		}
+
+		uint16_t real = (raw >> 12) & 0x0FFF;
+		if (real & 0x0800) {
+			real |= 0xF000;
+		}
+
+		uint16_t imag = (raw >> 0) & 0x0FFF;
+		if (imag & 0x0800) {
+			imag |= 0xF000;
+		}
+
+		samples->real = (int16_t)real;
+		samples->imag = (int16_t)imag;
+		samples++;
+		count++;
+
+		result = ptr;
+	}
+
+	*pcount = count;
+	return 1;
+}
+
+int hdhomerun_device_get_tuner_plotsample(struct hdhomerun_device_t *hd, struct hdhomerun_plotsample_t **psamples, size_t *pcount)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_plotsample: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/plotsample", hd->tuner);
+	return hdhomerun_device_get_tuner_plotsample_internal(hd, name, psamples, pcount);
+}
+
+int hdhomerun_device_get_oob_plotsample(struct hdhomerun_device_t *hd, struct hdhomerun_plotsample_t **psamples, size_t *pcount)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_oob_plotsample: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_device_get_tuner_plotsample_internal(hd, "/oob/plotsample", psamples, pcount);
+}
+
+int hdhomerun_device_get_tuner_lockkey_owner(struct hdhomerun_device_t *hd, char **powner)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_tuner_lockkey_owner: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/lockkey", hd->tuner);
+	return hdhomerun_control_get(hd->cs, name, powner, NULL);
+}
+
+int hdhomerun_device_get_ir_target(struct hdhomerun_device_t *hd, char **ptarget)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_ir_target: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_get(hd->cs, "/ir/target", ptarget, NULL);
+}
+
+int hdhomerun_device_get_lineup_location(struct hdhomerun_device_t *hd, char **plocation)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_lineup_location: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_get(hd->cs, "/lineup/location", plocation, NULL);
+}
+
+int hdhomerun_device_get_version(struct hdhomerun_device_t *hd, char **pversion_str, uint32_t *pversion_num)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_version: device not set\n");
+		return -1;
+	}
+
+	char *version_str;
+	int ret = hdhomerun_control_get(hd->cs, "/sys/version", &version_str, NULL);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	if (pversion_str) {
+		*pversion_str = version_str;
+	}
+
+	if (pversion_num) {
+		unsigned long version_num;
+		if (sscanf(version_str, "%lu", &version_num) != 1) {
+			*pversion_num = 0;
+		} else {
+			*pversion_num = (uint32_t)version_num;
+		}
+	}
+
+	return 1;
+}
+
+int hdhomerun_device_get_supported(struct hdhomerun_device_t *hd, char *prefix, char **pstr)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_channel: device not set\n");
+		return -1;
+	}
+
+	char *features;
+	int ret = hdhomerun_control_get(hd->cs, "/sys/features", &features, NULL);
+	if (ret <= 0) {
+		return ret;
+	}
+
+	if (!prefix) {
+		*pstr = features;
+		return 1;
+	}
+
+	char *ptr = strstr(features, prefix);
+	if (!ptr) {
+		return 0;
+	}
+
+	ptr += strlen(prefix);
+	*pstr = ptr;
+
+	ptr = strchr(ptr, '\n');
+	if (ptr) {
+		*ptr = 0;
+	}
+
+	return 1;
+}
+
+int hdhomerun_device_set_tuner_channel(struct hdhomerun_device_t *hd, const char *channel)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_channel: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/channel", hd->tuner);
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, channel, hd->lockkey, NULL, NULL);
+}
+
+int hdhomerun_device_set_tuner_vchannel(struct hdhomerun_device_t *hd, const char *vchannel)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_vchannel: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/vchannel", hd->tuner);
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, vchannel, hd->lockkey, NULL, NULL);
+}
+
+int hdhomerun_device_set_tuner_channelmap(struct hdhomerun_device_t *hd, const char *channelmap)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_channelmap: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/channelmap", hd->tuner);
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, channelmap, hd->lockkey, NULL, NULL);
+}
+
+int hdhomerun_device_set_tuner_filter(struct hdhomerun_device_t *hd, const char *filter)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_filter: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/filter", hd->tuner);
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, filter, hd->lockkey, NULL, NULL);
+}
+
+static int hdhomerun_device_set_tuner_filter_by_array_append(char **pptr, char *end, uint16_t range_begin, uint16_t range_end)
+{
+	char *ptr = *pptr;
+
+	size_t available = end - ptr;
+	size_t required;
+
+	if (range_begin == range_end) {
+		required = snprintf(ptr, available, "0x%04x ", range_begin) + 1;
+	} else {
+		required = snprintf(ptr, available, "0x%04x-0x%04x ", range_begin, range_end) + 1;
+	}
+
+	if (required > available) {
+		return FALSE;
+	}
+
+	*pptr = strchr(ptr, 0);
+	return TRUE;
+}
+
+int hdhomerun_device_set_tuner_filter_by_array(struct hdhomerun_device_t *hd, unsigned char filter_array[0x2000])
+{
+	char filter[1024];
+	char *ptr = filter;
+	char *end = filter + sizeof(filter);
+
+	uint16_t range_begin = 0xFFFF;
+	uint16_t range_end = 0xFFFF;
+
+	uint16_t i;
+	for (i = 0; i <= 0x1FFF; i++) {
+		if (!filter_array[i]) {
+			if (range_begin == 0xFFFF) {
+				continue;
+			}
+			if (!hdhomerun_device_set_tuner_filter_by_array_append(&ptr, end, range_begin, range_end)) {
+				return 0;
+			}
+			range_begin = 0xFFFF;
+			range_end = 0xFFFF;
+			continue;
+		}
+
+		if (range_begin == 0xFFFF) {
+			range_begin = i;
+			range_end = i;
+			continue;
+		}
+
+		range_end = i;
+	}
+
+	if (range_begin != 0xFFFF) {
+		if (!hdhomerun_device_set_tuner_filter_by_array_append(&ptr, end, range_begin, range_end)) {
+			return 0;
+		}
+	}
+
+	/* Remove trailing space. */
+	if (ptr > filter) {
+		ptr--;
+	}
+	*ptr = 0;
+
+	return hdhomerun_device_set_tuner_filter(hd, filter);
+}
+
+int hdhomerun_device_set_tuner_program(struct hdhomerun_device_t *hd, const char *program)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_program: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/program", hd->tuner);
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, program, hd->lockkey, NULL, NULL);
+}
+
+int hdhomerun_device_set_tuner_target(struct hdhomerun_device_t *hd, const char *target)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_target: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/target", hd->tuner);
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, target, hd->lockkey, NULL, NULL);
+}
+
+static int hdhomerun_device_set_tuner_target_to_local(struct hdhomerun_device_t *hd, const char *protocol)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_target_to_local: device not set\n");
+		return -1;
+	}
+	if (!hd->vs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_tuner_target_to_local: video not initialized\n");
+		return -1;
+	}
+
+	/* Set target. */
+	char target[64];
+	uint32_t local_ip = hdhomerun_control_get_local_addr(hd->cs);
+	uint16_t local_port = hdhomerun_video_get_local_port(hd->vs);
+	sprintf(target, "%s://%u.%u.%u.%u:%u",
+		protocol,
+		(unsigned int)(local_ip >> 24) & 0xFF, (unsigned int)(local_ip >> 16) & 0xFF,
+		(unsigned int)(local_ip >> 8) & 0xFF, (unsigned int)(local_ip >> 0) & 0xFF,
+		(unsigned int)local_port
+	);
+
+	return hdhomerun_device_set_tuner_target(hd, target);
+}
+
+int hdhomerun_device_set_ir_target(struct hdhomerun_device_t *hd, const char *target)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_ir_target: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_set(hd->cs, "/ir/target", target, NULL, NULL);
+}
+
+int hdhomerun_device_set_lineup_location(struct hdhomerun_device_t *hd, const char *location)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_lineup_location: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_set(hd->cs, "/lineup/location", location, NULL, NULL);
+}
+
+int hdhomerun_device_set_sys_dvbc_modulation(struct hdhomerun_device_t *hd, const char *modulation_list)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_sys_dvbc_modulation: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_set(hd->cs, "/sys/dvbc_modulation", modulation_list, NULL, NULL);
+}
+
+int hdhomerun_device_get_var(struct hdhomerun_device_t *hd, const char *name, char **pvalue, char **perror)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_var: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_get(hd->cs, name, pvalue, perror);
+}
+
+int hdhomerun_device_set_var(struct hdhomerun_device_t *hd, const char *name, const char *value, char **pvalue, char **perror)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_set_var: device not set\n");
+		return -1;
+	}
+
+	return hdhomerun_control_set_with_lockkey(hd->cs, name, value, hd->lockkey, pvalue, perror);
+}
+
+int hdhomerun_device_tuner_lockkey_request(struct hdhomerun_device_t *hd, char **perror)
+{
+	if (hd->multicast_ip != 0) {
+		return 1;
+	}
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_tuner_lockkey_request: device not set\n");
+		return -1;
+	}
+
+	uint32_t new_lockkey = random_get32();
+
+	char name[32];
+	sprintf(name, "/tuner%u/lockkey", hd->tuner);
+
+	char new_lockkey_str[64];
+	sprintf(new_lockkey_str, "%u", (unsigned int)new_lockkey);
+
+	int ret = hdhomerun_control_set_with_lockkey(hd->cs, name, new_lockkey_str, hd->lockkey, NULL, perror);
+	if (ret <= 0) {
+		hd->lockkey = 0;
+		return ret;
+	}
+
+	hd->lockkey = new_lockkey;
+	return ret;
+}
+
+int hdhomerun_device_tuner_lockkey_release(struct hdhomerun_device_t *hd)
+{
+	if (hd->multicast_ip != 0) {
+		return 1;
+	}
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_tuner_lockkey_release: device not set\n");
+		return -1;
+	}
+
+	if (hd->lockkey == 0) {
+		return 1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/lockkey", hd->tuner);
+	int ret = hdhomerun_control_set_with_lockkey(hd->cs, name, "none", hd->lockkey, NULL, NULL);
+
+	hd->lockkey = 0;
+	return ret;
+}
+
+int hdhomerun_device_tuner_lockkey_force(struct hdhomerun_device_t *hd)
+{
+	if (hd->multicast_ip != 0) {
+		return 1;
+	}
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_tuner_lockkey_force: device not set\n");
+		return -1;
+	}
+
+	char name[32];
+	sprintf(name, "/tuner%u/lockkey", hd->tuner);
+	int ret = hdhomerun_control_set(hd->cs, name, "force", NULL, NULL);
+
+	hd->lockkey = 0;
+	return ret;
+}
+
+void hdhomerun_device_tuner_lockkey_use_value(struct hdhomerun_device_t *hd, uint32_t lockkey)
+{
+	if (hd->multicast_ip != 0) {
+		return;
+	}
+
+	hd->lockkey = lockkey;
+}
+
+int hdhomerun_device_wait_for_lock(struct hdhomerun_device_t *hd, struct hdhomerun_tuner_status_t *status)
+{
+	/* Delay for SS reading to be valid (signal present). */
+	msleep_minimum(250);
+
+	/* Wait for up to 2.5 seconds for lock. */
+	uint64_t timeout = getcurrenttime() + 2500;
+	while (1) {
+		/* Get status to check for lock. Quality numbers will not be valid yet. */
+		int ret = hdhomerun_device_get_tuner_status(hd, NULL, status);
+		if (ret <= 0) {
+			return ret;
+		}
+
+		if (!status->signal_present) {
+			return 1;
+		}
+		if (status->lock_supported || status->lock_unsupported) {
+			return 1;
+		}
+
+		if (getcurrenttime() >= timeout) {
+			return 1;
+		}
+
+		msleep_approx(250);
+	}
+}
+
+int hdhomerun_device_stream_start(struct hdhomerun_device_t *hd)
+{
+	hdhomerun_device_get_video_sock(hd);
+	if (!hd->vs) {
+		return -1;
+	}
+
+	/* Set target. */
+	if (hd->multicast_ip != 0) {
+		int ret = hdhomerun_video_join_multicast_group(hd->vs, hd->multicast_ip, 0);
+		if (ret <= 0) {
+			return ret;
+		}
+	} else {
+		int ret = hdhomerun_device_set_tuner_target_to_local(hd, HDHOMERUN_TARGET_PROTOCOL_RTP);
+		if (ret == 0) {
+			ret = hdhomerun_device_set_tuner_target_to_local(hd, HDHOMERUN_TARGET_PROTOCOL_UDP);
+		}
+		if (ret <= 0) {
+			return ret;
+		}
+	}
+
+	/* Flush video buffer. */
+	msleep_minimum(64);
+	hdhomerun_video_flush(hd->vs);
+
+	/* Success. */
+	return 1;
+}
+
+uint8_t *hdhomerun_device_stream_recv(struct hdhomerun_device_t *hd, size_t max_size, size_t *pactual_size)
+{
+	if (!hd->vs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_stream_recv: video not initialized\n");
+		return NULL;
+	}
+
+	return hdhomerun_video_recv(hd->vs, max_size, pactual_size);
+}
+
+void hdhomerun_device_stream_flush(struct hdhomerun_device_t *hd)
+{
+	if (!hd->vs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_stream_flush: video not initialized\n");
+		return;
+	}
+
+	hdhomerun_video_flush(hd->vs);
+}
+
+void hdhomerun_device_stream_stop(struct hdhomerun_device_t *hd)
+{
+	if (!hd->vs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_stream_stop: video not initialized\n");
+		return;
+	}
+
+	if (hd->multicast_ip != 0) {
+		hdhomerun_video_leave_multicast_group(hd->vs, hd->multicast_ip, 0);
+	} else {
+		hdhomerun_device_set_tuner_target(hd, "none");
+	}
+}
+
+int hdhomerun_device_channelscan_init(struct hdhomerun_device_t *hd, const char *channelmap)
+{
+	if (hd->scan) {
+		channelscan_destroy(hd->scan);
+	}
+
+	hd->scan = channelscan_create(hd, channelmap);
+	if (!hd->scan) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_channelscan_init: failed to create scan object\n");
+		return -1;
+	}
+
+	return 1;
+}
+
+int hdhomerun_device_channelscan_advance(struct hdhomerun_device_t *hd, struct hdhomerun_channelscan_result_t *result)
+{
+	if (!hd->scan) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_channelscan_advance: scan not initialized\n");
+		return 0;
+	}
+
+	int ret = channelscan_advance(hd->scan, result);
+	if (ret <= 0) { /* Free scan if normal finish or fatal error */
+		channelscan_destroy(hd->scan);
+		hd->scan = NULL;
+	}
+
+	return ret;
+}
+
+int hdhomerun_device_channelscan_detect(struct hdhomerun_device_t *hd, struct hdhomerun_channelscan_result_t *result)
+{
+	if (!hd->scan) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_channelscan_detect: scan not initialized\n");
+		return 0;
+	}
+
+	int ret = channelscan_detect(hd->scan, result);
+	if (ret < 0) { /* Free scan if fatal error */
+		channelscan_destroy(hd->scan);
+		hd->scan = NULL;
+	}
+
+	return ret;
+}
+
+uint8_t hdhomerun_device_channelscan_get_progress(struct hdhomerun_device_t *hd)
+{
+	if (!hd->scan) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_channelscan_get_progress: scan not initialized\n");
+		return 0;
+	}
+
+	return channelscan_get_progress(hd->scan);
+}
+
+const char *hdhomerun_device_get_model_str(struct hdhomerun_device_t *hd)
+{
+	if (*hd->model) {
+		return hd->model;
+	}
+
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_get_model_str: device not set\n");
+		return NULL;
+	}
+
+	char *model_str;
+	int ret = hdhomerun_control_get(hd->cs, "/sys/model", &model_str, NULL);
+	if (ret < 0) {
+		return NULL;
+	}
+	if (ret == 0) {
+		strncpy(hd->model, "hdhomerun_atsc", sizeof(hd->model) - 1);
+		hd->model[sizeof(hd->model) - 1] = 0;
+		return hd->model;
+	}
+
+	strncpy(hd->model, model_str, sizeof(hd->model) - 1);
+	hd->model[sizeof(hd->model) - 1] = 0;
+	return hd->model;
+}
+
+int hdhomerun_device_upgrade(struct hdhomerun_device_t *hd, FILE *upgrade_file)
+{
+	if (!hd->cs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_upgrade: device not set\n");
+		return -1;
+	}
+
+	hdhomerun_control_set(hd->cs, "/tuner0/lockkey", "force", NULL, NULL);
+	hdhomerun_control_set(hd->cs, "/tuner0/channel", "none", NULL, NULL);
+
+	hdhomerun_control_set(hd->cs, "/tuner1/lockkey", "force", NULL, NULL);
+	hdhomerun_control_set(hd->cs, "/tuner1/channel", "none", NULL, NULL);
+
+	return hdhomerun_control_upgrade(hd->cs, upgrade_file);
+}
+
+void hdhomerun_device_debug_print_video_stats(struct hdhomerun_device_t *hd)
+{
+	if (!hdhomerun_debug_enabled(hd->dbg)) {
+		return;
+	}
+
+	if (hd->cs) {
+		char name[32];
+		sprintf(name, "/tuner%u/debug", hd->tuner);
+
+		char *debug_str;
+		char *error_str;
+		int ret = hdhomerun_control_get(hd->cs, name, &debug_str, &error_str);
+		if (ret < 0) {
+			hdhomerun_debug_printf(hd->dbg, "video dev: communication error getting debug stats\n");
+			return;
+		}
+
+		if (error_str) {
+			hdhomerun_debug_printf(hd->dbg, "video dev: %s\n", error_str);
+		} else {
+			hdhomerun_debug_printf(hd->dbg, "video dev: %s\n", debug_str);
+		}
+	}
+
+	if (hd->vs) {
+		hdhomerun_video_debug_print_stats(hd->vs);
+	}
+}
+
+void hdhomerun_device_get_video_stats(struct hdhomerun_device_t *hd, struct hdhomerun_video_stats_t *stats)
+{
+	if (!hd->vs) {
+		hdhomerun_debug_printf(hd->dbg, "hdhomerun_device_stream_flush: video not initialized\n");
+		memset(stats, 0, sizeof(struct hdhomerun_video_stats_t));
+		return;
+	}
+
+	hdhomerun_video_get_stats(hd->vs, stats);
+}
diff --git a/libhdhomerun/hdhomerun_device.h b/libhdhomerun/hdhomerun_device.h
new file mode 100755
index 0000000..51c2b86
--- /dev/null
+++ b/libhdhomerun/hdhomerun_device.h
@@ -0,0 +1,268 @@
+/*
+ * hdhomerun_device.h
+ *
+ * Copyright © 2006-2008 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HDHOMERUN_DEVICE_MAX_TUNE_TO_LOCK_TIME 1500
+#define HDHOMERUN_DEVICE_MAX_LOCK_TO_DATA_TIME 2000
+#define HDHOMERUN_DEVICE_MAX_TUNE_TO_DATA_TIME (HDHOMERUN_DEVICE_MAX_TUNE_TO_LOCK_TIME + HDHOMERUN_DEVICE_MAX_LOCK_TO_DATA_TIME)
+
+#define HDHOMERUN_TARGET_PROTOCOL_UDP "udp"
+#define HDHOMERUN_TARGET_PROTOCOL_RTP "rtp"
+
+/*
+ * Create a device object.
+ *
+ * Typically a device object will be created for each tuner.
+ * It is valid to have multiple device objects communicating with a single HDHomeRun.
+ *
+ * For example, a threaded application that streams video from 4 tuners (2 HDHomeRun devices) and has
+ * GUI feedback to the user of the selected tuner might use 5 device objects: 4 for streaming video
+ * (one per thread) and one for the GUI display that can switch between tuners.
+ *
+ * This function will not attempt to connect to the device. The connection will be established when first used.
+ *
+ * uint32_t device_id = 32-bit device id of device. Set to HDHOMERUN_DEVICE_ID_WILDCARD to match any device ID.
+ * uint32_t device_ip = IP address of device. Set to 0 to auto-detect.
+ * unsigned int tuner = tuner index (0 or 1). Can be changed later by calling hdhomerun_device_set_tuner.
+ * struct hdhomerun_debug_t *dbg: Pointer to debug logging object. May be NULL.
+ *
+ * Returns a pointer to the newly created device object.
+ *
+ * When no longer needed, the socket should be destroyed by calling hdhomerun_device_destroy.
+ *
+ * The hdhomerun_device_create_from_str function creates a device object from the given device_str.
+ * The device_str parameter can be any of the following forms:
+ *     <device id>
+ *     <device id>-<tuner index>
+ *     <ip address>
+ * If the tuner index is not included in the device_str then it is set to zero. Use hdhomerun_device_set_tuner
+ * or hdhomerun_device_set_tuner_from_str to set the tuner.
+ *
+ * The hdhomerun_device_set_tuner_from_str function sets the tuner from the given tuner_str.
+ * The tuner_str parameter can be any of the following forms:
+ *     <tuner index>
+ *     /tuner<tuner index>
+ */
+extern LIBTYPE struct hdhomerun_device_t *hdhomerun_device_create(uint32_t device_id, uint32_t device_ip, unsigned int tuner, struct hdhomerun_debug_t *dbg);
+extern LIBTYPE struct hdhomerun_device_t *hdhomerun_device_create_from_str(const char *device_str, struct hdhomerun_debug_t *dbg);
+extern LIBTYPE void hdhomerun_device_destroy(struct hdhomerun_device_t *hd);
+
+/*
+ * Get the device id, ip, or tuner of the device instance.
+ */
+extern LIBTYPE const char *hdhomerun_device_get_name(struct hdhomerun_device_t *hd);
+extern LIBTYPE uint32_t hdhomerun_device_get_device_id(struct hdhomerun_device_t *hd);
+extern LIBTYPE uint32_t hdhomerun_device_get_device_ip(struct hdhomerun_device_t *hd);
+extern LIBTYPE uint32_t hdhomerun_device_get_device_id_requested(struct hdhomerun_device_t *hd);
+extern LIBTYPE uint32_t hdhomerun_device_get_device_ip_requested(struct hdhomerun_device_t *hd);
+extern LIBTYPE unsigned int hdhomerun_device_get_tuner(struct hdhomerun_device_t *hd);
+
+extern LIBTYPE int hdhomerun_device_set_device(struct hdhomerun_device_t *hd, uint32_t device_id, uint32_t device_ip);
+extern LIBTYPE int hdhomerun_device_set_tuner(struct hdhomerun_device_t *hd, unsigned int tuner);
+extern LIBTYPE int hdhomerun_device_set_tuner_from_str(struct hdhomerun_device_t *hd, const char *tuner_str);
+
+/*
+ * Get the local machine IP address used when communicating with the device.
+ *
+ * This function is useful for determining the IP address to use with set target commands.
+ *
+ * Returns 32-bit IP address with native endianness, or 0 on error.
+ */
+extern LIBTYPE uint32_t hdhomerun_device_get_local_machine_addr(struct hdhomerun_device_t *hd);
+
+/*
+ * Get operations.
+ *
+ * struct hdhomerun_tuner_status_t *status = Pointer to caller supplied status struct to be populated with result.
+ * const char **p<name> = Caller supplied char * to be updated to point to the result string. The string will remain
+ *		valid until another call to a device function.
+ *
+ * Returns 1 if the operation was successful.
+ * Returns 0 if the operation was rejected.
+ * Returns -1 if a communication error occurred.
+ */
+extern LIBTYPE int hdhomerun_device_get_tuner_status(struct hdhomerun_device_t *hd, char **pstatus_str, struct hdhomerun_tuner_status_t *status);
+extern LIBTYPE int hdhomerun_device_get_tuner_vstatus(struct hdhomerun_device_t *hd, char **pvstatus_str, struct hdhomerun_tuner_vstatus_t *vstatus);
+extern LIBTYPE int hdhomerun_device_get_tuner_streaminfo(struct hdhomerun_device_t *hd, char **pstreaminfo);
+extern LIBTYPE int hdhomerun_device_get_tuner_channel(struct hdhomerun_device_t *hd, char **pchannel);
+extern LIBTYPE int hdhomerun_device_get_tuner_vchannel(struct hdhomerun_device_t *hd, char **pvchannel);
+extern LIBTYPE int hdhomerun_device_get_tuner_channelmap(struct hdhomerun_device_t *hd, char **pchannelmap);
+extern LIBTYPE int hdhomerun_device_get_tuner_filter(struct hdhomerun_device_t *hd, char **pfilter);
+extern LIBTYPE int hdhomerun_device_get_tuner_program(struct hdhomerun_device_t *hd, char **pprogram);
+extern LIBTYPE int hdhomerun_device_get_tuner_target(struct hdhomerun_device_t *hd, char **ptarget);
+extern LIBTYPE int hdhomerun_device_get_tuner_plotsample(struct hdhomerun_device_t *hd, struct hdhomerun_plotsample_t **psamples, size_t *pcount);
+extern LIBTYPE int hdhomerun_device_get_tuner_lockkey_owner(struct hdhomerun_device_t *hd, char **powner);
+extern LIBTYPE int hdhomerun_device_get_oob_status(struct hdhomerun_device_t *hd, char **pstatus_str, struct hdhomerun_tuner_status_t *status);
+extern LIBTYPE int hdhomerun_device_get_oob_plotsample(struct hdhomerun_device_t *hd, struct hdhomerun_plotsample_t **psamples, size_t *pcount);
+extern LIBTYPE int hdhomerun_device_get_ir_target(struct hdhomerun_device_t *hd, char **ptarget);
+extern LIBTYPE int hdhomerun_device_get_lineup_location(struct hdhomerun_device_t *hd, char **plocation);
+extern LIBTYPE int hdhomerun_device_get_version(struct hdhomerun_device_t *hd, char **pversion_str, uint32_t *pversion_num);
+extern LIBTYPE int hdhomerun_device_get_supported(struct hdhomerun_device_t *hd, char *prefix, char **pstr);
+
+extern LIBTYPE uint32_t hdhomerun_device_get_tuner_status_ss_color(struct hdhomerun_tuner_status_t *status);
+extern LIBTYPE uint32_t hdhomerun_device_get_tuner_status_snq_color(struct hdhomerun_tuner_status_t *status);
+extern LIBTYPE uint32_t hdhomerun_device_get_tuner_status_seq_color(struct hdhomerun_tuner_status_t *status);
+
+extern LIBTYPE const char *hdhomerun_device_get_model_str(struct hdhomerun_device_t *hd);
+
+/*
+ * Set operations.
+ *
+ * const char *<name> = String to send to device.
+ *
+ * Returns 1 if the operation was successful.
+ * Returns 0 if the operation was rejected.
+ * Returns -1 if a communication error occurred.
+ */
+extern LIBTYPE int hdhomerun_device_set_tuner_channel(struct hdhomerun_device_t *hd, const char *channel);
+extern LIBTYPE int hdhomerun_device_set_tuner_vchannel(struct hdhomerun_device_t *hd, const char *vchannel);
+extern LIBTYPE int hdhomerun_device_set_tuner_channelmap(struct hdhomerun_device_t *hd, const char *channelmap);
+extern LIBTYPE int hdhomerun_device_set_tuner_filter(struct hdhomerun_device_t *hd, const char *filter);
+extern LIBTYPE int hdhomerun_device_set_tuner_filter_by_array(struct hdhomerun_device_t *hd, unsigned char filter_array[0x2000]);
+extern LIBTYPE int hdhomerun_device_set_tuner_program(struct hdhomerun_device_t *hd, const char *program);
+extern LIBTYPE int hdhomerun_device_set_tuner_target(struct hdhomerun_device_t *hd, const char *target);
+extern LIBTYPE int hdhomerun_device_set_ir_target(struct hdhomerun_device_t *hd, const char *target);
+extern LIBTYPE int hdhomerun_device_set_lineup_location(struct hdhomerun_device_t *hd, const char *location);
+extern LIBTYPE int hdhomerun_device_set_sys_dvbc_modulation(struct hdhomerun_device_t *hd, const char *modulation_list);
+
+/*
+ * Get/set a named control variable on the device.
+ *
+ * const char *name: The name of var to get/set (c-string). The supported vars is device/firmware dependant.
+ * const char *value: The value to set (c-string). The format is device/firmware dependant.
+
+ * char **pvalue: If provided, the caller-supplied char pointer will be populated with a pointer to the value
+ *		string returned by the device, or NULL if the device returned an error string. The string will remain
+ *		valid until the next call to a control sock function.
+ * char **perror: If provided, the caller-supplied char pointer will be populated with a pointer to the error
+ *		string returned by the device, or NULL if the device returned an value string. The string will remain
+ *		valid until the next call to a control sock function.
+ *
+ * Returns 1 if the operation was successful (pvalue set, perror NULL).
+ * Returns 0 if the operation was rejected (pvalue NULL, perror set).
+ * Returns -1 if a communication error occurs.
+ */
+extern LIBTYPE int hdhomerun_device_get_var(struct hdhomerun_device_t *hd, const char *name, char **pvalue, char **perror);
+extern LIBTYPE int hdhomerun_device_set_var(struct hdhomerun_device_t *hd, const char *name, const char *value, char **pvalue, char **perror);
+
+/*
+ * Tuner locking.
+ *
+ * The hdhomerun_device_tuner_lockkey_request function is used to obtain a lock
+ * or to verify that the hdhomerun_device object still holds the lock.
+ * Returns 1 if the lock request was successful and the lock was obtained.
+ * Returns 0 if the lock request was rejected.
+ * Returns -1 if a communication error occurs.
+ *
+ * The hdhomerun_device_tuner_lockkey_release function is used to release a
+ * previously held lock. If locking is used then this function must be called
+ * before destroying the hdhomerun_device object.
+ */
+extern LIBTYPE int hdhomerun_device_tuner_lockkey_request(struct hdhomerun_device_t *hd, char **perror);
+extern LIBTYPE int hdhomerun_device_tuner_lockkey_release(struct hdhomerun_device_t *hd);
+extern LIBTYPE int hdhomerun_device_tuner_lockkey_force(struct hdhomerun_device_t *hd);
+
+/*
+ * Intended only for non persistent connections; eg, hdhomerun_config.
+ */
+extern LIBTYPE void hdhomerun_device_tuner_lockkey_use_value(struct hdhomerun_device_t *hd, uint32_t lockkey);
+
+/*
+ * Wait for tuner lock after channel change.
+ *
+ * The hdhomerun_device_wait_for_lock function is used to detect/wait for a lock vs no lock indication
+ * after a channel change.
+ *
+ * It will return quickly if a lock is aquired.
+ * It will return quickly if there is no signal detected.
+ * Worst case it will time out after 1.5 seconds - the case where there is signal but no lock.
+ */
+extern LIBTYPE int hdhomerun_device_wait_for_lock(struct hdhomerun_device_t *hd, struct hdhomerun_tuner_status_t *status);
+
+/*
+ * Stream a filtered program or the unfiltered stream.
+ *
+ * The hdhomerun_device_stream_start function initializes the process and tells the device to start streamin data.
+ *
+ * uint16_t program_number = The program number to filer, or 0 for unfiltered.
+ *
+ * Returns 1 if the oprtation started successfully.
+ * Returns 0 if the operation was rejected.
+ * Returns -1 if a communication error occurs.
+ *
+ * The hdhomerun_device_stream_recv function should be called periodically to receive the stream data.
+ * The buffer can losslessly store 1 second of data, however a more typical call rate would be every 15ms.
+ *
+ * The hdhomerun_device_stream_stop function tells the device to stop streaming data.
+ */
+extern LIBTYPE int hdhomerun_device_stream_start(struct hdhomerun_device_t *hd);
+extern LIBTYPE uint8_t *hdhomerun_device_stream_recv(struct hdhomerun_device_t *hd, size_t max_size, size_t *pactual_size);
+extern LIBTYPE void hdhomerun_device_stream_flush(struct hdhomerun_device_t *hd);
+extern LIBTYPE void hdhomerun_device_stream_stop(struct hdhomerun_device_t *hd);
+
+/*
+ * Channel scan API.
+ */
+extern LIBTYPE int hdhomerun_device_channelscan_init(struct hdhomerun_device_t *hd, const char *channelmap);
+extern LIBTYPE int hdhomerun_device_channelscan_advance(struct hdhomerun_device_t *hd, struct hdhomerun_channelscan_result_t *result);
+extern LIBTYPE int hdhomerun_device_channelscan_detect(struct hdhomerun_device_t *hd, struct hdhomerun_channelscan_result_t *result);
+extern LIBTYPE uint8_t hdhomerun_device_channelscan_get_progress(struct hdhomerun_device_t *hd);
+
+/*
+ * Upload new firmware to the device.
+ *
+ * FILE *upgrade_file: File pointer to read from. The file must have been opened in binary mode for reading.
+ *
+ * Returns 1 if the upload succeeded.
+ * Returns 0 if the upload was rejected.
+ * Returns -1 if an error occurs.
+ */
+extern LIBTYPE int hdhomerun_device_upgrade(struct hdhomerun_device_t *hd, FILE *upgrade_file);
+
+/*
+ * Low level accessor functions. 
+ */
+extern LIBTYPE struct hdhomerun_control_sock_t *hdhomerun_device_get_control_sock(struct hdhomerun_device_t *hd);
+extern LIBTYPE struct hdhomerun_video_sock_t *hdhomerun_device_get_video_sock(struct hdhomerun_device_t *hd);
+
+/*
+ * Debug print internal stats.
+ */
+extern LIBTYPE void hdhomerun_device_debug_print_video_stats(struct hdhomerun_device_t *hd);
+extern LIBTYPE void hdhomerun_device_get_video_stats(struct hdhomerun_device_t *hd, struct hdhomerun_video_stats_t *stats);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_device_selector.c b/libhdhomerun/hdhomerun_device_selector.c
new file mode 100755
index 0000000..f57f125
--- /dev/null
+++ b/libhdhomerun/hdhomerun_device_selector.c
@@ -0,0 +1,348 @@
+/*
+ * hdhomerun_device_selector.c
+ *
+ * Copyright © 2009-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+struct hdhomerun_device_selector_t {
+	struct hdhomerun_device_t **hd_list;
+	size_t hd_count;
+	struct hdhomerun_debug_t *dbg;
+};
+
+struct hdhomerun_device_selector_t *hdhomerun_device_selector_create(struct hdhomerun_debug_t *dbg)
+{
+	struct hdhomerun_device_selector_t *hds = (struct hdhomerun_device_selector_t *)calloc(1, sizeof(struct hdhomerun_device_selector_t));
+	if (!hds) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_device_selector_create: failed to allocate selector object\n");
+		return NULL;
+	}
+
+	hds->dbg = dbg;
+
+	return hds;
+}
+
+void hdhomerun_device_selector_destroy(struct hdhomerun_device_selector_t *hds, bool_t destroy_devices)
+{
+	if (destroy_devices) {
+		size_t index;
+		for (index = 0; index < hds->hd_count; index++) {
+			struct hdhomerun_device_t *entry = hds->hd_list[index];
+			hdhomerun_device_destroy(entry);
+		}
+	}
+
+	if (hds->hd_list) {
+		free(hds->hd_list);
+	}
+
+	free(hds);
+}
+
+LIBTYPE int hdhomerun_device_selector_get_device_count(struct hdhomerun_device_selector_t *hds)
+{
+	return (int)hds->hd_count;
+}
+
+void hdhomerun_device_selector_add_device(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *hd)
+{
+	size_t index;
+	for (index = 0; index < hds->hd_count; index++) {
+		struct hdhomerun_device_t *entry = hds->hd_list[index];
+		if (entry == hd) {
+			return;
+		}
+	}
+
+	hds->hd_list = (struct hdhomerun_device_t **)realloc(hds->hd_list, (hds->hd_count + 1) * sizeof(struct hdhomerun_device_selector_t *));
+	if (!hds->hd_list) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_add_device: failed to allocate device list\n");
+		return;
+	}
+
+	hds->hd_list[hds->hd_count++] = hd;
+}
+
+void hdhomerun_device_selector_remove_device(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *hd)
+{
+	size_t index = 0;
+	while (1) {
+		if (index >= hds->hd_count) {
+			return;
+		}
+
+		struct hdhomerun_device_t *entry = hds->hd_list[index];
+		if (entry == hd) {
+			break;
+		}
+
+		index++;
+	}
+
+	while (index + 1 < hds->hd_count) {
+		hds->hd_list[index] = hds->hd_list[index + 1];
+		index++;
+	}
+
+	hds->hd_list[index] = NULL;
+	hds->hd_count--;
+}
+
+struct hdhomerun_device_t *hdhomerun_device_selector_find_device(struct hdhomerun_device_selector_t *hds, uint32_t device_id, unsigned int tuner_index)
+{
+	size_t index;
+	for (index = 0; index < hds->hd_count; index++) {
+		struct hdhomerun_device_t *entry = hds->hd_list[index];
+		if (hdhomerun_device_get_device_id(entry) != device_id) {
+			continue;
+		}
+		if (hdhomerun_device_get_tuner(entry) != tuner_index) {
+			continue;
+		}
+		return entry;
+	}
+
+	return NULL;
+}
+
+int hdhomerun_device_selector_load_from_file(struct hdhomerun_device_selector_t *hds, char *filename)
+{
+	FILE *fp = fopen(filename, "r");
+	if (!fp) {
+		return 0;
+	}
+
+	while(1) {
+		char device_name[32];
+		if (!fgets(device_name, sizeof(device_name), fp)) {
+			break;
+		}
+
+		struct hdhomerun_device_t *hd = hdhomerun_device_create_from_str(device_name, hds->dbg);
+		if (!hd) {
+			continue;
+		}
+
+		hdhomerun_device_selector_add_device(hds, hd);
+	}
+
+	fclose(fp);
+	return (int)hds->hd_count;
+}
+
+#if defined(__WINDOWS__)
+int hdhomerun_device_selector_load_from_windows_registry(struct hdhomerun_device_selector_t *hds, wchar_t *wsource)
+{
+	HKEY tuners_key;
+	LONG ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Silicondust\\HDHomeRun\\Tuners", 0, KEY_QUERY_VALUE | KEY_ENUMERATE_SUB_KEYS, &tuners_key);
+	if (ret != ERROR_SUCCESS) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_load_from_windows_registry: failed to open tuners registry key (%ld)\n", (long)ret);
+		return 0;
+	}
+
+	DWORD index = 0;
+	while (1) {
+		/* Next tuner device. */
+		wchar_t wdevice_name[32];
+		DWORD size = sizeof(wdevice_name);
+		ret = RegEnumKeyEx(tuners_key, index++, wdevice_name, &size, NULL, NULL, NULL, NULL);
+		if (ret != ERROR_SUCCESS) {
+			break;
+		}
+
+		/* Check device configuation. */
+		HKEY device_key;
+		ret = RegOpenKeyEx(tuners_key, wdevice_name, 0, KEY_QUERY_VALUE, &device_key);
+		if (ret != ERROR_SUCCESS) {
+			hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_load_from_windows_registry: failed to open registry key for %S (%ld)\n", wdevice_name, (long)ret);
+			continue;
+		}
+
+		wchar_t wsource_test[32];
+		size = sizeof(wsource_test);
+		if (RegQueryValueEx(device_key, L"Source", NULL, NULL, (LPBYTE)&wsource_test, &size) != ERROR_SUCCESS) {
+			wsprintf(wsource_test, L"Unknown");
+		}
+
+		RegCloseKey(device_key);
+
+		if (_wcsicmp(wsource_test, wsource) != 0) {
+			continue;
+		}
+
+		/* Create and add device. */
+		char device_name[32];
+		sprintf(device_name, "%S", wdevice_name);
+
+		struct hdhomerun_device_t *hd = hdhomerun_device_create_from_str(device_name, hds->dbg);
+		if (!hd) {
+			hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_load_from_windows_registry: invalid device name '%s' / failed to create device object\n", device_name);
+			continue;
+		}
+
+		hdhomerun_device_selector_add_device(hds, hd);
+	}
+
+	RegCloseKey(tuners_key);
+	return (int)hds->hd_count;
+}
+#endif
+
+static bool_t hdhomerun_device_selector_choose_test(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *test_hd)
+{
+	const char *name = hdhomerun_device_get_name(test_hd);
+
+	/*
+	 * Attempt to aquire lock.
+	 */
+	char *error;
+	int ret = hdhomerun_device_tuner_lockkey_request(test_hd, &error);
+	if (ret > 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s chosen\n", name);
+		return TRUE;
+	}
+	if (ret < 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s communication error\n", name);
+		return FALSE;
+	}
+
+	/*
+	 * In use - check target.
+	 */
+	char *target;
+	ret = hdhomerun_device_get_tuner_target(test_hd, &target);
+	if (ret < 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s communication error\n", name);
+		return FALSE;
+	}
+	if (ret == 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use, failed to read target\n", name);
+		return FALSE;
+	}
+
+	char *ptr = strstr(target, "//");
+	if (ptr) {
+		target = ptr + 2;
+	}
+	ptr = strchr(target, ' ');
+	if (ptr) {
+		*ptr = 0;
+	}
+
+	unsigned long a[4];
+	unsigned long target_port;
+	if (sscanf(target, "%lu.%lu.%lu.%lu:%lu", &a[0], &a[1], &a[2], &a[3], &target_port) != 5) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use, no target set (%s)\n", name, target);
+		return FALSE;
+	}
+
+	uint32_t target_ip = (uint32_t)((a[0] << 24) | (a[1] << 16) | (a[2] << 8) | (a[3] << 0));
+	uint32_t local_ip = hdhomerun_device_get_local_machine_addr(test_hd);
+	if (target_ip != local_ip) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use by %s\n", name, target);
+		return FALSE;
+	}
+
+	/*
+	 * Test local port.
+	 */
+	hdhomerun_sock_t test_sock = hdhomerun_sock_create_udp();
+	if (test_sock == HDHOMERUN_SOCK_INVALID) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use, failed to create test sock\n", name);
+		return FALSE;
+	}
+
+	bool_t inuse = (hdhomerun_sock_bind(test_sock, INADDR_ANY, (uint16_t)target_port, FALSE) == FALSE);
+	hdhomerun_sock_destroy(test_sock);
+
+	if (inuse) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use by local machine\n", name);
+		return FALSE;
+	}
+
+	/*
+	 * Dead local target, force clear lock.
+	 */
+	ret = hdhomerun_device_tuner_lockkey_force(test_hd);
+	if (ret < 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s communication error\n", name);
+		return FALSE;
+	}
+	if (ret == 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use by local machine, dead target, failed to force release lockkey\n", name);
+		return FALSE;
+	}
+
+	hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s in use by local machine, dead target, lockkey force successful\n", name);
+
+	/*
+	 * Attempt to aquire lock.
+	 */
+	ret = hdhomerun_device_tuner_lockkey_request(test_hd, &error);
+	if (ret > 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s chosen\n", name);
+		return TRUE;
+	}
+	if (ret < 0) {
+		hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s communication error\n", name);
+		return FALSE;
+	}
+
+	hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_test: device %s still in use after lockkey force (%s)\n", name, error);
+	return FALSE;
+}
+
+struct hdhomerun_device_t *hdhomerun_device_selector_choose_and_lock(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *prefered)
+{
+	/* Test prefered device first. */
+	if (prefered) {
+		if (hdhomerun_device_selector_choose_test(hds, prefered)) {
+			return prefered;
+		}
+	}
+
+	/* Test other tuners. */
+	size_t index;
+	for (index = 0; index < hds->hd_count; index++) {
+		struct hdhomerun_device_t *entry = hds->hd_list[index];
+		if (entry == prefered) {
+			continue;
+		}
+
+		if (hdhomerun_device_selector_choose_test(hds, entry)) {
+			return entry;
+		}
+	}
+
+	hdhomerun_debug_printf(hds->dbg, "hdhomerun_device_selector_choose_and_lock: no devices available\n");
+	return NULL;
+}
diff --git a/libhdhomerun/hdhomerun_device_selector.h b/libhdhomerun/hdhomerun_device_selector.h
new file mode 100755
index 0000000..2f3587c
--- /dev/null
+++ b/libhdhomerun/hdhomerun_device_selector.h
@@ -0,0 +1,97 @@
+/*
+ * hdhomerun_device_selector.h
+ *
+ * Copyright © 2009 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Create a device selector object for use with dynamic tuner allocation support.
+ * All tuners registered with a specific device selector instance must have the same signal source.
+ * The dbg parameter may be null.
+ */
+extern LIBTYPE struct hdhomerun_device_selector_t *hdhomerun_device_selector_create(struct hdhomerun_debug_t *dbg);
+extern LIBTYPE void hdhomerun_device_selector_destroy(struct hdhomerun_device_selector_t *hds, bool_t destroy_devices);
+
+/*
+ * Get the number of devices in the list.
+ */
+extern LIBTYPE int hdhomerun_device_selector_get_device_count(struct hdhomerun_device_selector_t *hds);
+
+/*
+ * Populate device selector with devices from given source.
+ * Returns the number of devices populated.
+ */
+extern LIBTYPE int hdhomerun_device_selector_load_from_file(struct hdhomerun_device_selector_t *hds, char *filename);
+#if defined(__WINDOWS__)
+extern LIBTYPE int hdhomerun_device_selector_load_from_windows_registry(struct hdhomerun_device_selector_t *hds, wchar_t *wsource);
+#endif
+
+/*
+ * Add/remove a device from the selector list.
+ */
+extern LIBTYPE void hdhomerun_device_selector_add_device(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *hd);
+extern LIBTYPE void hdhomerun_device_selector_remove_device(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *hd);
+
+/*
+ * Find a device in the selector list.
+ */
+extern LIBTYPE struct hdhomerun_device_t *hdhomerun_device_selector_find_device(struct hdhomerun_device_selector_t *hds, uint32_t device_id, unsigned int tuner_index);
+
+/*
+ * Select and lock an available device.
+ * If not null, preference will be given to the prefered device specified.
+ * The device resource lock must be released by the application when no longer needed by
+ * calling hdhomerun_device_tuner_lockkey_release().
+ *
+ * Recommended channel change logic:
+ *
+ * Start (inactive -> active):
+ * - Call hdhomerun_device_selector_choose_and_lock() to choose and lock an available tuner.
+ * 
+ * Stop (active -> inactive):
+ * - Call hdhomerun_device_tuner_lockkey_release() to release the resource lock and allow the tuner
+ *   to be allocated by other computers.
+ *
+ * Channel change (active -> active):
+ * - If the new channel has a different signal source then call hdhomerun_device_tuner_lockkey_release()
+ *   to release the lock on the tuner playing the previous channel, then call
+ *   hdhomerun_device_selector_choose_and_lock() to choose and lock an available tuner.
+ * - If the new channel has the same signal source then call hdhomerun_device_tuner_lockkey_request()
+ *   to refresh the lock. If this function succeeds then the same device can be used. If this fucntion fails
+ *   then call hdhomerun_device_selector_choose_and_lock() to choose and lock an available tuner.
+ */
+extern LIBTYPE struct hdhomerun_device_t *hdhomerun_device_selector_choose_and_lock(struct hdhomerun_device_selector_t *hds, struct hdhomerun_device_t *prefered);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_discover.c b/libhdhomerun/hdhomerun_discover.c
new file mode 100755
index 0000000..0307013
--- /dev/null
+++ b/libhdhomerun/hdhomerun_discover.c
@@ -0,0 +1,439 @@
+/*
+ * hdhomerun_discover.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+#define HDHOMERUN_DISOCVER_MAX_SOCK_COUNT 16
+
+struct hdhomerun_discover_sock_t {
+	hdhomerun_sock_t sock;
+	bool_t detected;
+	uint32_t local_ip;
+	uint32_t subnet_mask;
+};
+
+struct hdhomerun_discover_t {
+	struct hdhomerun_discover_sock_t socks[HDHOMERUN_DISOCVER_MAX_SOCK_COUNT];
+	unsigned int sock_count;
+	struct hdhomerun_pkt_t tx_pkt;
+	struct hdhomerun_pkt_t rx_pkt;
+};
+
+static bool_t hdhomerun_discover_sock_add(struct hdhomerun_discover_t *ds, uint32_t local_ip, uint32_t subnet_mask)
+{
+	unsigned int i;
+	for (i = 1; i < ds->sock_count; i++) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[i];
+
+		if ((dss->local_ip == local_ip) && (dss->subnet_mask == subnet_mask)) {
+			dss->detected = TRUE;
+			return TRUE;
+		}
+	}
+
+	if (ds->sock_count >= HDHOMERUN_DISOCVER_MAX_SOCK_COUNT) {
+		return FALSE;
+	}
+
+	/* Create socket. */
+	hdhomerun_sock_t sock = hdhomerun_sock_create_udp();
+	if (sock == HDHOMERUN_SOCK_INVALID) {
+		return FALSE;
+	}
+
+	/* Bind socket. */
+	if (!hdhomerun_sock_bind(sock, local_ip, 0, FALSE)) {
+		hdhomerun_sock_destroy(sock);
+		return FALSE;
+	}
+
+	/* Write sock entry. */
+	struct hdhomerun_discover_sock_t *dss = &ds->socks[ds->sock_count++];
+	dss->sock = sock;
+	dss->detected = TRUE;
+	dss->local_ip = local_ip;
+	dss->subnet_mask = subnet_mask;
+
+	return TRUE;
+}
+
+struct hdhomerun_discover_t *hdhomerun_discover_create(void)
+{
+	struct hdhomerun_discover_t *ds = (struct hdhomerun_discover_t *)calloc(1, sizeof(struct hdhomerun_discover_t));
+	if (!ds) {
+		return NULL;
+	}
+
+	/* Create a routable socket (always first entry). */
+	if (!hdhomerun_discover_sock_add(ds, 0, 0)) {
+		free(ds);
+		return NULL;
+	}
+
+	/* Success. */
+	return ds;
+}
+
+void hdhomerun_discover_destroy(struct hdhomerun_discover_t *ds)
+{
+	unsigned int i;
+	for (i = 0; i < ds->sock_count; i++) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[i];
+		hdhomerun_sock_destroy(dss->sock);
+	}
+
+	free(ds);
+}
+
+static void hdhomerun_discover_sock_detect(struct hdhomerun_discover_t *ds)
+{
+	unsigned int i;
+	for (i = 1; i < ds->sock_count; i++) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[i];
+		dss->detected = FALSE;
+	}
+
+	struct hdhomerun_local_ip_info_t ip_info_list[HDHOMERUN_DISOCVER_MAX_SOCK_COUNT];
+	int count = hdhomerun_local_ip_info(ip_info_list, HDHOMERUN_DISOCVER_MAX_SOCK_COUNT);
+	if (count < 0) {
+		count = 0;
+	}
+
+	int index;
+	for (index = 0; index < count; index++) {
+		struct hdhomerun_local_ip_info_t *ip_info = &ip_info_list[index];
+		hdhomerun_discover_sock_add(ds, ip_info->ip_addr, ip_info->subnet_mask);
+	}
+
+	struct hdhomerun_discover_sock_t *src = &ds->socks[1];
+	struct hdhomerun_discover_sock_t *dst = &ds->socks[1];
+	count = 1;
+	for (i = 1; i < ds->sock_count; i++) {
+		if (!src->detected) {
+			hdhomerun_sock_destroy(src->sock);
+			src++;
+			continue;
+		}
+		if (dst != src) {
+			*dst = *src;
+		}
+		src++;
+		dst++;
+		count++;
+	}
+
+	ds->sock_count = count;
+}
+
+static bool_t hdhomerun_discover_send_internal(struct hdhomerun_discover_t *ds, struct hdhomerun_discover_sock_t *dss, uint32_t target_ip, uint32_t device_type, uint32_t device_id)
+{
+	struct hdhomerun_pkt_t *tx_pkt = &ds->tx_pkt;
+	hdhomerun_pkt_reset(tx_pkt);
+
+	hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_DEVICE_TYPE);
+	hdhomerun_pkt_write_var_length(tx_pkt, 4);
+	hdhomerun_pkt_write_u32(tx_pkt, device_type);
+	hdhomerun_pkt_write_u8(tx_pkt, HDHOMERUN_TAG_DEVICE_ID);
+	hdhomerun_pkt_write_var_length(tx_pkt, 4);
+	hdhomerun_pkt_write_u32(tx_pkt, device_id);
+	hdhomerun_pkt_seal_frame(tx_pkt, HDHOMERUN_TYPE_DISCOVER_REQ);
+
+	return hdhomerun_sock_sendto(dss->sock, target_ip, HDHOMERUN_DISCOVER_UDP_PORT, tx_pkt->start, tx_pkt->end - tx_pkt->start, 0);
+}
+
+static bool_t hdhomerun_discover_send_wildcard_ip(struct hdhomerun_discover_t *ds, uint32_t device_type, uint32_t device_id)
+{
+	bool_t result = FALSE;
+
+	/*
+	 * Send subnet broadcast using each local ip socket.
+	 * This will work with multiple separate 169.254.x.x interfaces.
+	 */
+	unsigned int i;
+	for (i = 1; i < ds->sock_count; i++) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[i];
+		uint32_t target_ip = dss->local_ip | ~dss->subnet_mask;
+		result |= hdhomerun_discover_send_internal(ds, dss, target_ip, device_type, device_id);
+	}
+
+	/*
+	 * If no local ip sockets then fall back to sending a global broadcast letting the OS choose the interface.
+	 */
+	if (!result) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[0];
+		result = hdhomerun_discover_send_internal(ds, dss, 0xFFFFFFFF, device_type, device_id);
+	}
+
+	return result;
+}
+
+static bool_t hdhomerun_discover_send_target_ip(struct hdhomerun_discover_t *ds, uint32_t target_ip, uint32_t device_type, uint32_t device_id)
+{
+	bool_t result = FALSE;
+
+	/*
+	 * Send targeted packet from any local ip that is in the same subnet.
+	 * This will work with multiple separate 169.254.x.x interfaces.
+	 */
+	unsigned int i;
+	for (i = 1; i < ds->sock_count; i++) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[i];
+		if ((target_ip & dss->subnet_mask) != (dss->local_ip & dss->subnet_mask)) {
+			continue;
+		}
+
+		result |= hdhomerun_discover_send_internal(ds, dss, target_ip, device_type, device_id);
+	}
+
+	/*
+	 * If target IP does not match a local subnet then fall back to letting the OS choose the gateway interface.
+	 */
+	if (!result) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[0];
+		result = hdhomerun_discover_send_internal(ds, dss, target_ip, device_type, device_id);
+	}
+
+	return result;
+}
+
+static bool_t hdhomerun_discover_send(struct hdhomerun_discover_t *ds, uint32_t target_ip, uint32_t device_type, uint32_t device_id)
+{
+	if (target_ip == 0) {
+		return hdhomerun_discover_send_wildcard_ip(ds, device_type, device_id);
+	} else {
+		return hdhomerun_discover_send_target_ip(ds, target_ip, device_type, device_id);
+	}
+}
+
+static bool_t hdhomerun_discover_recv_internal(struct hdhomerun_discover_t *ds, struct hdhomerun_discover_sock_t *dss, struct hdhomerun_discover_device_t *result)
+{
+	struct hdhomerun_pkt_t *rx_pkt = &ds->rx_pkt;
+	hdhomerun_pkt_reset(rx_pkt);
+
+	uint32_t remote_addr;
+	uint16_t remote_port;
+	size_t length = rx_pkt->limit - rx_pkt->end;
+	if (!hdhomerun_sock_recvfrom(dss->sock, &remote_addr, &remote_port, rx_pkt->end, &length, 0)) {
+		return FALSE;
+	}
+
+	rx_pkt->end += length;
+
+	uint16_t type;
+	if (hdhomerun_pkt_open_frame(rx_pkt, &type) <= 0) {
+		return FALSE;
+	}
+	if (type != HDHOMERUN_TYPE_DISCOVER_RPY) {
+		return FALSE;
+	}
+
+	result->ip_addr = remote_addr;
+	result->device_type = 0;
+	result->device_id = 0;
+	result->tuner_count = 0;
+
+	while (1) {
+		uint8_t tag;
+		size_t len;
+		uint8_t *next = hdhomerun_pkt_read_tlv(rx_pkt, &tag, &len);
+		if (!next) {
+			break;
+		}
+
+		switch (tag) {
+		case HDHOMERUN_TAG_DEVICE_TYPE:
+			if (len != 4) {
+				break;
+			}
+			result->device_type = hdhomerun_pkt_read_u32(rx_pkt);
+			break;
+
+		case HDHOMERUN_TAG_DEVICE_ID:
+			if (len != 4) {
+				break;
+			}
+			result->device_id = hdhomerun_pkt_read_u32(rx_pkt);
+			break;
+
+		case HDHOMERUN_TAG_TUNER_COUNT:
+			if (len != 1) {
+				break;
+			}
+			result->tuner_count = hdhomerun_pkt_read_u8(rx_pkt);
+			break;
+
+		default:
+			break;
+		}
+
+		rx_pkt->pos = next;
+	}
+
+	/* Fixup for old firmware. */
+	if (result->tuner_count == 0) {
+		switch (result->device_id >> 20) {
+		case 0x102:
+			result->tuner_count = 1;
+			break;
+
+		case 0x100:
+		case 0x101:
+		case 0x121:
+			result->tuner_count = 2;
+			break;
+
+		default:
+			break;
+		}
+	}
+
+	return TRUE;
+}
+
+static bool_t hdhomerun_discover_recv(struct hdhomerun_discover_t *ds, struct hdhomerun_discover_device_t *result)
+{
+	unsigned int i;
+	for (i = 0; i < ds->sock_count; i++) {
+		struct hdhomerun_discover_sock_t *dss = &ds->socks[i];
+
+		if (hdhomerun_discover_recv_internal(ds, dss, result)) {
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+static struct hdhomerun_discover_device_t *hdhomerun_discover_find_in_list(struct hdhomerun_discover_device_t result_list[], int count, struct hdhomerun_discover_device_t *lookup)
+{
+	int index;
+	for (index = 0; index < count; index++) {
+		struct hdhomerun_discover_device_t *entry = &result_list[index];
+		if (memcmp(lookup, entry, sizeof(struct hdhomerun_discover_device_t)) == 0) {
+			return entry;
+		}
+	}
+
+	return NULL;
+}
+
+int hdhomerun_discover_find_devices(struct hdhomerun_discover_t *ds, uint32_t target_ip, uint32_t device_type, uint32_t device_id, struct hdhomerun_discover_device_t result_list[], int max_count)
+{
+	hdhomerun_discover_sock_detect(ds);
+
+	int count = 0;
+	int attempt;
+	for (attempt = 0; attempt < 2; attempt++) {
+		if (!hdhomerun_discover_send(ds, target_ip, device_type, device_id)) {
+			return -1;
+		}
+
+		uint64_t timeout = getcurrenttime() + 200;
+		while (1) {
+			struct hdhomerun_discover_device_t *result = &result_list[count];
+			memset(result, 0, sizeof(struct hdhomerun_discover_device_t));
+
+			if (!hdhomerun_discover_recv(ds, result)) {
+				if (getcurrenttime() >= timeout) {
+					break;
+				}
+				msleep_approx(10);
+				continue;
+			}
+
+			/* Filter. */
+			if (device_type != HDHOMERUN_DEVICE_TYPE_WILDCARD) {
+				if (device_type != result->device_type) {
+					continue;
+				}
+			}
+			if (device_id != HDHOMERUN_DEVICE_ID_WILDCARD) {
+				if (device_id != result->device_id) {
+					continue;
+				}
+			}
+
+			/* Ensure not already in list. */
+			if (hdhomerun_discover_find_in_list(result_list, count, result)) {
+				continue;
+			}
+
+			/* Add to list. */
+			count++;
+			if (count >= max_count) {
+				return count;
+			}
+		}
+	}
+
+	return count;
+}
+
+int hdhomerun_discover_find_devices_custom(uint32_t target_ip, uint32_t device_type, uint32_t device_id, struct hdhomerun_discover_device_t result_list[], int max_count)
+{
+	if (hdhomerun_discover_is_ip_multicast(target_ip)) {
+		return 0;
+	}
+
+	struct hdhomerun_discover_t *ds = hdhomerun_discover_create();
+	if (!ds) {
+		return -1;
+	}
+
+	int ret = hdhomerun_discover_find_devices(ds, target_ip, device_type, device_id, result_list, max_count);
+
+	hdhomerun_discover_destroy(ds);
+	return ret;
+}
+
+bool_t hdhomerun_discover_validate_device_id(uint32_t device_id)
+{
+	static uint32_t lookup_table[16] = {0xA, 0x5, 0xF, 0x6, 0x7, 0xC, 0x1, 0xB, 0x9, 0x2, 0x8, 0xD, 0x4, 0x3, 0xE, 0x0};
+
+	uint32_t checksum = 0;
+
+	checksum ^= lookup_table[(device_id >> 28) & 0x0F];
+	checksum ^= (device_id >> 24) & 0x0F;
+	checksum ^= lookup_table[(device_id >> 20) & 0x0F];
+	checksum ^= (device_id >> 16) & 0x0F;
+	checksum ^= lookup_table[(device_id >> 12) & 0x0F];
+	checksum ^= (device_id >> 8) & 0x0F;
+	checksum ^= lookup_table[(device_id >> 4) & 0x0F];
+	checksum ^= (device_id >> 0) & 0x0F;
+
+	return (checksum == 0);
+}
+
+bool_t hdhomerun_discover_is_ip_multicast(uint32_t ip_addr)
+{
+	return (ip_addr >= 0xE0000000) && (ip_addr < 0xF0000000);
+}
diff --git a/libhdhomerun/hdhomerun_discover.h b/libhdhomerun/hdhomerun_discover.h
new file mode 100755
index 0000000..2f0d906
--- /dev/null
+++ b/libhdhomerun/hdhomerun_discover.h
@@ -0,0 +1,87 @@
+/*
+ * hdhomerun_discover.h
+ *
+ * Copyright © 2006-2007 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hdhomerun_discover_device_t {
+	uint32_t ip_addr;
+	uint32_t device_type;
+	uint32_t device_id;
+	uint8_t tuner_count;
+};
+
+/*
+ * Find devices.
+ *
+ * The device information is stored in caller-supplied array of hdhomerun_discover_device_t vars.
+ * Multiple attempts are made to find devices.
+ * Execution time is typically 400ms if max_count is not reached.
+ *
+ * Set target_ip to zero to auto-detect the IP address.
+ * Set device_type to HDHOMERUN_DEVICE_TYPE_TUNER to detect HDHomeRun tuner devices.
+ * Set device_id to HDHOMERUN_DEVICE_ID_WILDCARD to detect all device ids.
+ *
+ * Returns the number of devices found.
+ * Retruns -1 on error.
+ */
+extern LIBTYPE int hdhomerun_discover_find_devices_custom(uint32_t target_ip, uint32_t device_type, uint32_t device_id, struct hdhomerun_discover_device_t result_list[], int max_count);
+
+/*
+ * Optional: persistent discover instance available for discover polling use.
+ */
+extern LIBTYPE struct hdhomerun_discover_t *hdhomerun_discover_create(void);
+extern LIBTYPE void hdhomerun_discover_destroy(struct hdhomerun_discover_t *ds);
+extern LIBTYPE int hdhomerun_discover_find_devices(struct hdhomerun_discover_t *ds, uint32_t target_ip, uint32_t device_type, uint32_t device_id, struct hdhomerun_discover_device_t result_list[], int max_count);
+
+/*
+ * Verify that the device ID given is valid.
+ *
+ * The device ID contains a self-check sequence that detects common user input errors including
+ * single-digit errors and two digit transposition errors.
+ *
+ * Returns TRUE if valid.
+ * Returns FALSE if not valid.
+ */
+extern LIBTYPE bool_t hdhomerun_discover_validate_device_id(uint32_t device_id);
+
+/*
+ * Detect if an IP address is multicast.
+ *
+ * Returns TRUE if multicast.
+ * Returns FALSE if zero, unicast, expermental, or broadcast.
+ */
+extern LIBTYPE bool_t hdhomerun_discover_is_ip_multicast(uint32_t ip_addr);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_os.h b/libhdhomerun/hdhomerun_os.h
new file mode 100755
index 0000000..1506e07
--- /dev/null
+++ b/libhdhomerun/hdhomerun_os.h
@@ -0,0 +1,49 @@
+/*
+ * hdhomerun_os.h
+ *
+ * Copyright © 2006-2008 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#if defined(_WIN32) || defined(_WIN64)
+#define __WINDOWS__
+#endif
+
+#if defined(__WINDOWS__)
+#include "hdhomerun_os_windows.h"
+#else
+#include "hdhomerun_os_posix.h"
+#endif
+
+#if !defined(TRUE)
+#define TRUE 1
+#endif
+
+#if !defined(FALSE)
+#define FALSE 0
+#endif
diff --git a/libhdhomerun/hdhomerun_os_posix.c b/libhdhomerun/hdhomerun_os_posix.c
new file mode 100755
index 0000000..25eece8
--- /dev/null
+++ b/libhdhomerun/hdhomerun_os_posix.c
@@ -0,0 +1,105 @@
+/*
+ * hdhomerun_os_posix.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun_os.h"
+
+uint32_t random_get32(void)
+{
+	FILE *fp = fopen("/dev/urandom", "rb");
+	if (!fp) {
+		return (uint32_t)rand();
+	}
+
+	uint32_t Result;
+	if (fread(&Result, 4, 1, fp) != 1) {
+		Result = (uint32_t)rand();
+	}
+
+	fclose(fp);
+	return Result;
+}
+
+uint64_t getcurrenttime(void)
+{
+	static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
+	static uint64_t result = 0;
+	static uint64_t previous_time = 0;
+
+	pthread_mutex_lock(&lock);
+
+#if defined(CLOCK_MONOTONIC)
+	struct timespec tp;
+	clock_gettime(CLOCK_MONOTONIC, &tp);
+	uint64_t current_time = ((uint64_t)tp.tv_sec * 1000) + (tp.tv_nsec / 1000000);
+#else
+	struct timeval t;
+	gettimeofday(&t, NULL);
+	uint64_t current_time = ((uint64_t)t.tv_sec * 1000) + (t.tv_usec / 1000);
+#endif
+
+	if (current_time > previous_time) {
+		result += current_time - previous_time;
+	}
+
+	previous_time = current_time;
+
+	pthread_mutex_unlock(&lock);
+	return result;
+}
+
+void msleep_approx(uint64_t ms)
+{
+	unsigned int delay_s = ms / 1000;
+	if (delay_s > 0) {
+		sleep(delay_s);
+		ms -= delay_s * 1000;
+	}
+
+	unsigned int delay_us = ms * 1000;
+	if (delay_us > 0) {
+		usleep(delay_us);
+	}
+}
+
+void msleep_minimum(uint64_t ms)
+{
+	uint64_t stop_time = getcurrenttime() + ms;
+
+	while (1) {
+		uint64_t current_time = getcurrenttime();
+		if (current_time >= stop_time) {
+			return;
+		}
+
+		msleep_approx(stop_time - current_time);
+	}
+}
diff --git a/libhdhomerun/hdhomerun_os_posix.h b/libhdhomerun/hdhomerun_os_posix.h
new file mode 100755
index 0000000..2969b20
--- /dev/null
+++ b/libhdhomerun/hdhomerun_os_posix.h
@@ -0,0 +1,71 @@
+/*
+ * hdhomerun_os_posix.h
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#define _FILE_OFFSET_BITS 64
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/timeb.h>
+#include <sys/wait.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <pthread.h>
+
+typedef int bool_t;
+typedef void (*sig_t)(int);
+
+#define LIBTYPE
+#define console_vprintf vprintf
+#define console_printf printf
+#define THREAD_FUNC_PREFIX void *
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern LIBTYPE uint32_t random_get32(void);
+extern LIBTYPE uint64_t getcurrenttime(void);
+extern LIBTYPE void msleep_approx(uint64_t ms);
+extern LIBTYPE void msleep_minimum(uint64_t ms);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_os_windows.c b/libhdhomerun/hdhomerun_os_windows.c
new file mode 100755
index 0000000..5f844db
--- /dev/null
+++ b/libhdhomerun/hdhomerun_os_windows.c
@@ -0,0 +1,149 @@
+/*
+ * hdhomerun_os_windows.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun_os.h"
+
+uint32_t random_get32(void)
+{
+	HCRYPTPROV hProv;
+	if (!CryptAcquireContext(&hProv, 0, 0, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) {
+		return (uint32_t)rand();
+	}
+
+	uint32_t Result;
+	CryptGenRandom(hProv, sizeof(Result), (BYTE*)&Result);
+
+	CryptReleaseContext(hProv, 0);
+	return Result;
+}
+
+uint64_t getcurrenttime(void)
+{
+	static pthread_mutex_t lock = INVALID_HANDLE_VALUE;
+	static uint64_t result = 0;
+	static uint32_t previous_time = 0;
+
+	/* Initialization is not thread safe. */
+	if (lock == INVALID_HANDLE_VALUE) {
+		pthread_mutex_init(&lock, NULL);
+	}
+
+	pthread_mutex_lock(&lock);
+
+	uint32_t current_time = GetTickCount();
+
+	if (current_time > previous_time) {
+		result += current_time - previous_time;
+	}
+
+	previous_time = current_time;
+
+	pthread_mutex_unlock(&lock);
+	return result;
+}
+
+void msleep_approx(uint64_t ms)
+{
+	Sleep((DWORD)ms);
+}
+
+void msleep_minimum(uint64_t ms)
+{
+	uint64_t stop_time = getcurrenttime() + ms;
+
+	while (1) {
+		uint64_t current_time = getcurrenttime();
+		if (current_time >= stop_time) {
+			return;
+		}
+
+		msleep_approx(stop_time - current_time);
+	}
+}
+
+int pthread_create(pthread_t *tid, void *attr, LPTHREAD_START_ROUTINE start, void *arg)
+{
+	*tid = CreateThread(NULL, 0, start, arg, 0, NULL);
+	if (!*tid) {
+		return (int)GetLastError();
+	}
+	return 0;
+}
+
+int pthread_join(pthread_t tid, void **value_ptr)
+{
+	while (1) {
+		DWORD ExitCode = 0;
+		if (!GetExitCodeThread(tid, &ExitCode)) {
+			return (int)GetLastError();
+		}
+		if (ExitCode != STILL_ACTIVE) {
+			return 0;
+		}
+	}
+}
+
+void pthread_mutex_init(pthread_mutex_t *mutex, void *attr)
+{
+	*mutex = CreateMutex(NULL, FALSE, NULL);
+}
+
+void pthread_mutex_lock(pthread_mutex_t *mutex)
+{
+	WaitForSingleObject(*mutex, INFINITE);
+}
+
+void pthread_mutex_unlock(pthread_mutex_t *mutex)
+{
+	ReleaseMutex(*mutex);
+}
+
+/*
+ * The console output format should be set to UTF-8, however in XP and Vista this breaks batch file processing.
+ * Attempting to restore on exit fails to restore if the program is terminated by the user.
+ * Solution - set the output format each printf.
+ */
+void console_vprintf(const char *fmt, va_list ap)
+{
+	UINT cp = GetConsoleOutputCP();
+	SetConsoleOutputCP(CP_UTF8);
+	vprintf(fmt, ap);
+	SetConsoleOutputCP(cp);
+}
+
+void console_printf(const char *fmt, ...)
+{
+	va_list ap;
+	va_start(ap, fmt);
+	console_vprintf(fmt, ap);
+	va_end(ap);
+}
diff --git a/libhdhomerun/hdhomerun_os_windows.h b/libhdhomerun/hdhomerun_os_windows.h
new file mode 100755
index 0000000..ff9e1d4
--- /dev/null
+++ b/libhdhomerun/hdhomerun_os_windows.h
@@ -0,0 +1,102 @@
+/*
+ * hdhomerun_os_windows.h
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#define _WINSOCKAPI_
+#include <windows.h>
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#include <wspiapi.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <signal.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/timeb.h>
+
+#if defined(DLL_IMPORT)
+#define LIBTYPE __declspec( dllexport )
+#elif  defined(DLL_EXPORT)
+#define LIBTYPE __declspec( dllimport )
+#else
+#define LIBTYPE
+#endif
+
+typedef int bool_t;
+typedef signed __int8 int8_t;
+typedef signed __int16 int16_t;
+typedef signed __int32 int32_t;
+typedef signed __int64 int64_t;
+typedef unsigned __int8 uint8_t;
+typedef unsigned __int16 uint16_t;
+typedef unsigned __int32 uint32_t;
+typedef unsigned __int64 uint64_t;
+typedef void (*sig_t)(int);
+typedef HANDLE pthread_t;
+typedef HANDLE pthread_mutex_t;
+
+#define va_copy(x, y) x = y
+#define atoll _atoi64
+#define strdup _strdup
+#define strcasecmp _stricmp
+#define snprintf _snprintf
+#define fseeko _fseeki64
+#define ftello _ftelli64
+#define THREAD_FUNC_PREFIX DWORD WINAPI
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern LIBTYPE uint32_t random_get32(void);
+extern LIBTYPE uint64_t getcurrenttime(void);
+extern LIBTYPE void msleep_approx(uint64_t ms);
+extern LIBTYPE void msleep_minimum(uint64_t ms);
+
+extern LIBTYPE int pthread_create(pthread_t *tid, void *attr, LPTHREAD_START_ROUTINE start, void *arg);
+extern LIBTYPE int pthread_join(pthread_t tid, void **value_ptr);
+extern LIBTYPE void pthread_mutex_init(pthread_mutex_t *mutex, void *attr);
+extern LIBTYPE void pthread_mutex_lock(pthread_mutex_t *mutex);
+extern LIBTYPE void pthread_mutex_unlock(pthread_mutex_t *mutex);
+
+/*
+ * The console output format should be set to UTF-8, however in XP and Vista this breaks batch file processing.
+ * Attempting to restore on exit fails to restore if the program is terminated by the user.
+ * Solution - set the output format each printf.
+ */
+extern LIBTYPE void console_vprintf(const char *fmt, va_list ap);
+extern LIBTYPE void console_printf(const char *fmt, ...);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_pkt.c b/libhdhomerun/hdhomerun_pkt.c
new file mode 100755
index 0000000..bc34ba2
--- /dev/null
+++ b/libhdhomerun/hdhomerun_pkt.c
@@ -0,0 +1,245 @@
+/*
+ * hdhomerun_pkt.c
+ *
+ * Copyright © 2005-2006 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+struct hdhomerun_pkt_t *hdhomerun_pkt_create(void)
+{
+	struct hdhomerun_pkt_t *pkt = (struct hdhomerun_pkt_t *)calloc(1, sizeof(struct hdhomerun_pkt_t));
+	if (!pkt) {
+		return NULL;
+	}
+
+	hdhomerun_pkt_reset(pkt);
+
+	return pkt;
+}
+
+void hdhomerun_pkt_destroy(struct hdhomerun_pkt_t *pkt)
+{
+	free(pkt);
+}
+
+void hdhomerun_pkt_reset(struct hdhomerun_pkt_t *pkt)
+{
+	pkt->limit = pkt->buffer + sizeof(pkt->buffer) - 4;
+	pkt->start = pkt->buffer + 1024;
+	pkt->end = pkt->start;
+	pkt->pos = pkt->start;
+}
+
+static uint32_t hdhomerun_pkt_calc_crc(uint8_t *start, uint8_t *end)
+{
+	uint8_t *pos = start;
+	uint32_t crc = 0xFFFFFFFF;
+	while (pos < end) {
+		uint8_t x = (uint8_t)(crc) ^ *pos++;
+		crc >>= 8;
+		if (x & 0x01) crc ^= 0x77073096;
+		if (x & 0x02) crc ^= 0xEE0E612C;
+		if (x & 0x04) crc ^= 0x076DC419;
+		if (x & 0x08) crc ^= 0x0EDB8832;
+		if (x & 0x10) crc ^= 0x1DB71064;
+		if (x & 0x20) crc ^= 0x3B6E20C8;
+		if (x & 0x40) crc ^= 0x76DC4190;
+		if (x & 0x80) crc ^= 0xEDB88320;
+	}
+	return crc ^ 0xFFFFFFFF;
+}
+
+uint8_t hdhomerun_pkt_read_u8(struct hdhomerun_pkt_t *pkt)
+{
+	uint8_t v = *pkt->pos++;
+	return v;
+}
+
+uint16_t hdhomerun_pkt_read_u16(struct hdhomerun_pkt_t *pkt)
+{
+	uint16_t v;
+	v =  (uint16_t)*pkt->pos++ << 8;
+	v |= (uint16_t)*pkt->pos++ << 0;
+	return v;
+}
+
+uint32_t hdhomerun_pkt_read_u32(struct hdhomerun_pkt_t *pkt)
+{
+	uint32_t v;
+	v =  (uint32_t)*pkt->pos++ << 24;
+	v |= (uint32_t)*pkt->pos++ << 16;
+	v |= (uint32_t)*pkt->pos++ << 8;
+	v |= (uint32_t)*pkt->pos++ << 0;
+	return v;
+}
+
+size_t hdhomerun_pkt_read_var_length(struct hdhomerun_pkt_t *pkt)
+{
+	size_t length;
+	
+	if (pkt->pos + 1 > pkt->end) {
+		return (size_t)-1;
+	}
+
+	length = (size_t)*pkt->pos++;
+	if (length & 0x0080) {
+		if (pkt->pos + 1 > pkt->end) {
+			return (size_t)-1;
+		}
+
+		length &= 0x007F;
+		length |= (size_t)*pkt->pos++ << 7;
+	}
+	
+	return length; 
+}
+
+uint8_t *hdhomerun_pkt_read_tlv(struct hdhomerun_pkt_t *pkt, uint8_t *ptag, size_t *plength)
+{
+	if (pkt->pos + 2 > pkt->end) {
+		return NULL;
+	}
+	
+	*ptag = hdhomerun_pkt_read_u8(pkt);
+	*plength = hdhomerun_pkt_read_var_length(pkt);
+
+	if (pkt->pos + *plength > pkt->end) {
+		return NULL;
+	}
+	
+	return pkt->pos + *plength;
+}
+
+void hdhomerun_pkt_write_u8(struct hdhomerun_pkt_t *pkt, uint8_t v)
+{
+	*pkt->pos++ = v;
+
+	if (pkt->pos > pkt->end) {
+		pkt->end = pkt->pos;
+	}
+}
+
+void hdhomerun_pkt_write_u16(struct hdhomerun_pkt_t *pkt, uint16_t v)
+{
+	*pkt->pos++ = (uint8_t)(v >> 8);
+	*pkt->pos++ = (uint8_t)(v >> 0);
+
+	if (pkt->pos > pkt->end) {
+		pkt->end = pkt->pos;
+	}
+}
+
+void hdhomerun_pkt_write_u32(struct hdhomerun_pkt_t *pkt, uint32_t v)
+{
+	*pkt->pos++ = (uint8_t)(v >> 24);
+	*pkt->pos++ = (uint8_t)(v >> 16);
+	*pkt->pos++ = (uint8_t)(v >> 8);
+	*pkt->pos++ = (uint8_t)(v >> 0);
+
+	if (pkt->pos > pkt->end) {
+		pkt->end = pkt->pos;
+	}
+}
+
+void hdhomerun_pkt_write_var_length(struct hdhomerun_pkt_t *pkt, size_t v)
+{
+	if (v <= 127) {
+		*pkt->pos++ = (uint8_t)v;
+	} else {
+		*pkt->pos++ = (uint8_t)(v | 0x80);
+		*pkt->pos++ = (uint8_t)(v >> 7);
+	}
+
+	if (pkt->pos > pkt->end) {
+		pkt->end = pkt->pos;
+	}
+}
+
+void hdhomerun_pkt_write_mem(struct hdhomerun_pkt_t *pkt, const void *mem, size_t length)
+{
+	memcpy(pkt->pos, mem, length);
+	pkt->pos += length;
+
+	if (pkt->pos > pkt->end) {
+		pkt->end = pkt->pos;
+	}
+}
+
+int hdhomerun_pkt_open_frame(struct hdhomerun_pkt_t *pkt, uint16_t *ptype)
+{
+	pkt->pos = pkt->start;
+
+	if (pkt->pos + 4 > pkt->end) {
+		return 0;
+	}
+
+	*ptype = hdhomerun_pkt_read_u16(pkt);
+	size_t length = hdhomerun_pkt_read_u16(pkt);
+	pkt->pos += length;
+
+	if (pkt->pos + 4 > pkt->end) {
+		pkt->pos = pkt->start;
+		return 0;
+	}
+
+	uint32_t calc_crc = hdhomerun_pkt_calc_crc(pkt->start, pkt->pos);
+
+	uint32_t packet_crc;
+	packet_crc =  (uint32_t)*pkt->pos++ << 0;
+	packet_crc |= (uint32_t)*pkt->pos++ << 8;
+	packet_crc |= (uint32_t)*pkt->pos++ << 16;
+	packet_crc |= (uint32_t)*pkt->pos++ << 24;
+	if (calc_crc != packet_crc) {
+		return -1;
+	}
+
+	pkt->start += 4;
+	pkt->end = pkt->start + length;
+	pkt->pos = pkt->start;
+	return 1;
+}
+
+void hdhomerun_pkt_seal_frame(struct hdhomerun_pkt_t *pkt, uint16_t frame_type)
+{
+	size_t length = pkt->end - pkt->start;
+
+	pkt->start -= 4;
+	pkt->pos = pkt->start;
+	hdhomerun_pkt_write_u16(pkt, frame_type);
+	hdhomerun_pkt_write_u16(pkt, (uint16_t)length);
+
+	uint32_t crc = hdhomerun_pkt_calc_crc(pkt->start, pkt->end);
+	*pkt->end++ = (uint8_t)(crc >> 0);
+	*pkt->end++ = (uint8_t)(crc >> 8);
+	*pkt->end++ = (uint8_t)(crc >> 16);
+	*pkt->end++ = (uint8_t)(crc >> 24);
+
+	pkt->pos = pkt->start;
+}
diff --git a/libhdhomerun/hdhomerun_pkt.h b/libhdhomerun/hdhomerun_pkt.h
new file mode 100755
index 0000000..5ac14c7
--- /dev/null
+++ b/libhdhomerun/hdhomerun_pkt.h
@@ -0,0 +1,178 @@
+/*
+ * hdhomerun_pkt.h
+ *
+ * Copyright © 2005-2006 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * The discover protocol (UDP port 65001) and control protocol (TCP port 65001)
+ * both use the same packet based format:
+ *	uint16_t	Packet type
+ *	uint16_t	Payload length (bytes)
+ *	uint8_t[]	Payload data (0-n bytes).
+ *	uint32_t	CRC (Ethernet style 32-bit CRC)
+ *
+ * All variables are big-endian except for the crc which is little-endian.
+ *
+ * Valid values for the packet type are listed below as defines prefixed
+ * with "HDHOMERUN_TYPE_"
+ *
+ * Discovery:
+ *
+ * The payload for a discovery request or reply is a simple sequence of
+ * tag-length-value data:
+ *	uint8_t		Tag
+ *	varlen		Length
+ *	uint8_t[]	Value (0-n bytes)
+ *
+ * The length field can be one or two bytes long.
+ * For a length <= 127 bytes the length is expressed as a single byte. The
+ * most-significant-bit is clear indicating a single-byte length.
+ * For a length >= 128 bytes the length is expressed as a sequence of two bytes as follows:
+ * The first byte is contains the least-significant 7-bits of the length. The
+ * most-significant bit is then set (add 0x80) to indicate that it is a two byte length.
+ * The second byte contains the length shifted down 7 bits.
+ *
+ * A discovery request packet has a packet type of HDHOMERUN_TYPE_DISCOVER_REQ and should
+ * contain two tags: HDHOMERUN_TAG_DEVICE_TYPE and HDHOMERUN_TAG_DEVICE_ID.
+ * The HDHOMERUN_TAG_DEVICE_TYPE value should be set to HDHOMERUN_DEVICE_TYPE_TUNER.
+ * The HDHOMERUN_TAG_DEVICE_ID value should be set to HDHOMERUN_DEVICE_ID_WILDCARD to match
+ * all devices, or to the 32-bit device id number to match a single device.
+ *
+ * The discovery response packet has a packet type of HDHOMERUN_TYPE_DISCOVER_RPY and has the
+ * same format as the discovery request packet with the two tags: HDHOMERUN_TAG_DEVICE_TYPE and
+ * HDHOMERUN_TAG_DEVICE_ID. In the future additional tags may also be returned - unknown tags
+ * should be skipped and not treated as an error.
+ *
+ * Control get/set:
+ *
+ * The payload for a control get/set request is a simple sequence of tag-length-value data
+ * following the same format as for discover packets.
+ *
+ * A get request packet has a packet type of HDHOMERUN_TYPE_GETSET_REQ and should contain
+ * the tag: HDHOMERUN_TAG_GETSET_NAME. The HDHOMERUN_TAG_GETSET_NAME value should be a sequence
+ * of bytes forming a null-terminated string, including the NULL. The TLV length must include
+ * the NULL character so the length field should be set to strlen(str) + 1.
+ *
+ * A set request packet has a packet type of HDHOMERUN_TYPE_GETSET_REQ (same as a get request)
+ * and should contain two tags: HDHOMERUN_TAG_GETSET_NAME and HDHOMERUN_TAG_GETSET_VALUE.
+ * The HDHOMERUN_TAG_GETSET_NAME value should be a sequence of bytes forming a null-terminated
+ * string, including the NULL.
+ * The HDHOMERUN_TAG_GETSET_VALUE value  should be a sequence of bytes forming a null-terminated
+ * string, including the NULL.
+ *
+ * The get and set reply packets have the packet type HDHOMERUN_TYPE_GETSET_RPY and have the same
+ * format as the set request packet with the two tags: HDHOMERUN_TAG_GETSET_NAME and
+ * HDHOMERUN_TAG_GETSET_VALUE. A set request is also implicit get request so the updated value is
+ * returned.
+ *
+ * If the device encounters an error handling the get or set request then the get/set reply packet
+ * will contain the tag HDHOMERUN_TAG_ERROR_MESSAGE. The format of the value is a sequence of
+ * bytes forming a null-terminated string, including the NULL.
+ *
+ * In the future additional tags may also be returned - unknown tags should be skipped and not
+ * treated as an error.
+ *
+ * Security note: The application should not rely on the NULL character being present. The
+ * application should write a NULL character based on the TLV length to protect the application
+ * from a potential attack.
+ *
+ * Firmware Upgrade:
+ *
+ * A firmware upgrade packet has a packet type of HDHOMERUN_TYPE_UPGRADE_REQ and has a fixed format:
+ *	uint32_t	Position in bytes from start of file.
+ *	uint8_t[256]	Firmware data (256 bytes)
+ *
+ * The data must be uploaded in 256 byte chunks and must be uploaded in order.
+ * The position number is in bytes so will increment by 256 each time.
+ *
+ * When all data is uploaded it should be signaled complete by sending another packet of type
+ * HDHOMERUN_TYPE_UPGRADE_REQ with payload of a single uint32_t with the value 0xFFFFFFFF.
+ */
+
+#define HDHOMERUN_DISCOVER_UDP_PORT 65001
+#define HDHOMERUN_CONTROL_TCP_PORT 65001
+
+#define HDHOMERUN_MAX_PACKET_SIZE 1460
+#define HDHOMERUN_MAX_PAYLOAD_SIZE 1452
+
+#define HDHOMERUN_TYPE_DISCOVER_REQ 0x0002
+#define HDHOMERUN_TYPE_DISCOVER_RPY 0x0003
+#define HDHOMERUN_TYPE_GETSET_REQ 0x0004
+#define HDHOMERUN_TYPE_GETSET_RPY 0x0005
+#define HDHOMERUN_TYPE_UPGRADE_REQ 0x0006
+#define HDHOMERUN_TYPE_UPGRADE_RPY 0x0007
+
+#define HDHOMERUN_TAG_DEVICE_TYPE 0x01
+#define HDHOMERUN_TAG_DEVICE_ID 0x02
+#define HDHOMERUN_TAG_GETSET_NAME 0x03
+#define HDHOMERUN_TAG_GETSET_VALUE 0x04
+#define HDHOMERUN_TAG_GETSET_LOCKKEY 0x15
+#define HDHOMERUN_TAG_ERROR_MESSAGE 0x05
+#define HDHOMERUN_TAG_TUNER_COUNT 0x10
+
+#define HDHOMERUN_DEVICE_TYPE_WILDCARD 0xFFFFFFFF
+#define HDHOMERUN_DEVICE_TYPE_TUNER 0x00000001
+#define HDHOMERUN_DEVICE_ID_WILDCARD 0xFFFFFFFF
+
+#define HDHOMERUN_MIN_PEEK_LENGTH 4
+
+struct hdhomerun_pkt_t {
+	uint8_t *pos;
+	uint8_t *start;
+	uint8_t *end;
+	uint8_t *limit;
+	uint8_t buffer[3074];
+};
+
+extern LIBTYPE struct hdhomerun_pkt_t *hdhomerun_pkt_create(void);
+extern LIBTYPE void hdhomerun_pkt_destroy(struct hdhomerun_pkt_t *pkt);
+extern LIBTYPE void hdhomerun_pkt_reset(struct hdhomerun_pkt_t *pkt);
+
+extern LIBTYPE uint8_t hdhomerun_pkt_read_u8(struct hdhomerun_pkt_t *pkt);
+extern LIBTYPE uint16_t hdhomerun_pkt_read_u16(struct hdhomerun_pkt_t *pkt);
+extern LIBTYPE uint32_t hdhomerun_pkt_read_u32(struct hdhomerun_pkt_t *pkt);
+extern LIBTYPE size_t hdhomerun_pkt_read_var_length(struct hdhomerun_pkt_t *pkt);
+extern LIBTYPE uint8_t *hdhomerun_pkt_read_tlv(struct hdhomerun_pkt_t *pkt, uint8_t *ptag, size_t *plength);
+
+extern LIBTYPE void hdhomerun_pkt_write_u8(struct hdhomerun_pkt_t *pkt, uint8_t v);
+extern LIBTYPE void hdhomerun_pkt_write_u16(struct hdhomerun_pkt_t *pkt, uint16_t v);
+extern LIBTYPE void hdhomerun_pkt_write_u32(struct hdhomerun_pkt_t *pkt, uint32_t v);
+extern LIBTYPE void hdhomerun_pkt_write_var_length(struct hdhomerun_pkt_t *pkt, size_t v);
+extern LIBTYPE void hdhomerun_pkt_write_mem(struct hdhomerun_pkt_t *pkt, const void *mem, size_t length);
+
+extern LIBTYPE bool_t hdhomerun_pkt_open_frame(struct hdhomerun_pkt_t *pkt, uint16_t *ptype);
+extern LIBTYPE void hdhomerun_pkt_seal_frame(struct hdhomerun_pkt_t *pkt, uint16_t frame_type);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_sock.h b/libhdhomerun/hdhomerun_sock.h
new file mode 100755
index 0000000..69e1919
--- /dev/null
+++ b/libhdhomerun/hdhomerun_sock.h
@@ -0,0 +1,71 @@
+/*
+ * hdhomerun_sock.h
+ *
+ * Copyright © 2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hdhomerun_local_ip_info_t {
+	uint32_t ip_addr;
+	uint32_t subnet_mask;
+};
+
+extern LIBTYPE int hdhomerun_local_ip_info(struct hdhomerun_local_ip_info_t ip_info_list[], int max_count);
+
+#define HDHOMERUN_SOCK_INVALID -1
+
+typedef int hdhomerun_sock_t;
+
+extern LIBTYPE hdhomerun_sock_t hdhomerun_sock_create_udp(void);
+extern LIBTYPE hdhomerun_sock_t hdhomerun_sock_create_tcp(void);
+extern LIBTYPE void hdhomerun_sock_destroy(hdhomerun_sock_t sock);
+
+extern LIBTYPE int hdhomerun_sock_getlasterror(void);
+extern LIBTYPE uint32_t hdhomerun_sock_getsockname_addr(hdhomerun_sock_t sock);
+extern LIBTYPE uint16_t hdhomerun_sock_getsockname_port(hdhomerun_sock_t sock);
+extern LIBTYPE uint32_t hdhomerun_sock_getpeername_addr(hdhomerun_sock_t sock);
+extern LIBTYPE uint32_t hdhomerun_sock_getaddrinfo_addr(hdhomerun_sock_t sock, const char *name);
+
+extern LIBTYPE bool_t hdhomerun_sock_join_multicast_group(hdhomerun_sock_t sock, uint32_t multicast_ip, uint32_t local_ip);
+extern LIBTYPE bool_t hdhomerun_sock_leave_multicast_group(hdhomerun_sock_t sock, uint32_t multicast_ip, uint32_t local_ip);
+
+extern LIBTYPE bool_t hdhomerun_sock_bind(hdhomerun_sock_t sock, uint32_t local_addr, uint16_t local_port, bool_t allow_reuse);
+extern LIBTYPE bool_t hdhomerun_sock_connect(hdhomerun_sock_t sock, uint32_t remote_addr, uint16_t remote_port, uint64_t timeout);
+
+extern LIBTYPE bool_t hdhomerun_sock_send(hdhomerun_sock_t sock, const void *data, size_t length, uint64_t timeout);
+extern LIBTYPE bool_t hdhomerun_sock_sendto(hdhomerun_sock_t sock, uint32_t remote_addr, uint16_t remote_port, const void *data, size_t length, uint64_t timeout);
+
+extern LIBTYPE bool_t hdhomerun_sock_recv(hdhomerun_sock_t sock, void *data, size_t *length, uint64_t timeout);
+extern LIBTYPE bool_t hdhomerun_sock_recvfrom(hdhomerun_sock_t sock, uint32_t *remote_addr, uint16_t *remote_port, void *data, size_t *length, uint64_t timeout);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/hdhomerun_sock_posix.c b/libhdhomerun/hdhomerun_sock_posix.c
new file mode 100755
index 0000000..52dccc0
--- /dev/null
+++ b/libhdhomerun/hdhomerun_sock_posix.c
@@ -0,0 +1,514 @@
+/*
+ * hdhomerun_sock_posix.c
+ *
+ * Copyright © 2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+/*
+ * Implementation notes:
+ *
+ * API specifies timeout for each operation (or zero for non-blocking).
+ *
+ * It is not possible to rely on the OS socket timeout as this will fail to
+ * detect the command-response situation where data is sent successfully and
+ * the other end chooses not to send a response (other than the TCP ack).
+ *
+ * The select() cannot be used with high socket numbers (typically max 1024)
+ * so the code works as follows:
+ * - Use non-blocking sockets to allow operation without select.
+ * - Use select where safe (low socket numbers).
+ * - Poll with short sleep when select cannot be used safely.
+ */
+
+#include "hdhomerun.h"
+
+#include <net/if.h>
+#include <sys/ioctl.h>
+
+#ifndef SIOCGIFCONF
+#include <sys/sockio.h>
+#endif
+
+#ifndef _SIZEOF_ADDR_IFREQ
+#define _SIZEOF_ADDR_IFREQ(x) sizeof(x)
+#endif
+
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif
+
+int hdhomerun_local_ip_info(struct hdhomerun_local_ip_info_t ip_info_list[], int max_count)
+{
+	int sock = socket(AF_INET, SOCK_DGRAM, 0);
+	if (sock == HDHOMERUN_SOCK_INVALID) {
+		return -1;
+	}
+
+	struct ifconf ifc;
+	size_t ifreq_buffer_size = 1024;
+
+	while (1) {
+		ifc.ifc_len = ifreq_buffer_size;
+		ifc.ifc_buf = (char *)malloc(ifreq_buffer_size);
+		if (!ifc.ifc_buf) {
+			close(sock);
+			return -1;
+		}
+
+		memset(ifc.ifc_buf, 0, ifreq_buffer_size);
+
+		if (ioctl(sock, SIOCGIFCONF, &ifc) != 0) {
+			free(ifc.ifc_buf);
+			close(sock);
+			return -1;
+		}
+
+		if (ifc.ifc_len < ifreq_buffer_size) {
+			break;
+		}
+
+		free(ifc.ifc_buf);
+		ifreq_buffer_size += 1024;
+	}
+
+	char *ptr = ifc.ifc_buf;
+	char *end = ifc.ifc_buf + ifc.ifc_len;
+
+	int count = 0;
+	while (ptr <= end) {
+		struct ifreq *ifr = (struct ifreq *)ptr;
+		ptr += _SIZEOF_ADDR_IFREQ(*ifr);
+
+		if (ioctl(sock, SIOCGIFADDR, ifr) != 0) {
+			continue;
+		}
+
+		struct sockaddr_in *ip_addr_in = (struct sockaddr_in *)&(ifr->ifr_addr);
+		uint32_t ip_addr = ntohl(ip_addr_in->sin_addr.s_addr);
+		if (ip_addr == 0) {
+			continue;
+		}
+
+		if (ioctl(sock, SIOCGIFNETMASK, ifr) != 0) {
+			continue;
+		}
+
+		struct sockaddr_in *subnet_mask_in = (struct sockaddr_in *)&(ifr->ifr_addr);
+		uint32_t subnet_mask = ntohl(subnet_mask_in->sin_addr.s_addr);
+
+		struct hdhomerun_local_ip_info_t *ip_info = &ip_info_list[count++];
+		ip_info->ip_addr = ip_addr;
+		ip_info->subnet_mask = subnet_mask;
+
+		if (count >= max_count) {
+			break;
+		}
+	}
+
+	free(ifc.ifc_buf);
+	close(sock);
+	return count;
+}
+
+hdhomerun_sock_t hdhomerun_sock_create_udp(void)
+{
+	/* Create socket. */
+	hdhomerun_sock_t sock = (hdhomerun_sock_t)socket(AF_INET, SOCK_DGRAM, 0);
+	if (sock == -1) {
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Set non-blocking */
+	if (fcntl(sock, F_SETFL, O_NONBLOCK) != 0) {
+		close(sock);
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Allow broadcast. */
+	int sock_opt = 1;
+	setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char *)&sock_opt, sizeof(sock_opt));
+
+	/* Success. */
+	return sock;
+}
+
+hdhomerun_sock_t hdhomerun_sock_create_tcp(void)
+{
+	/* Create socket. */
+	hdhomerun_sock_t sock = (hdhomerun_sock_t)socket(AF_INET, SOCK_STREAM, 0);
+	if (sock == -1) {
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Set non-blocking */
+	if (fcntl(sock, F_SETFL, O_NONBLOCK) != 0) {
+		close(sock);
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Success. */
+	return sock;
+}
+
+void hdhomerun_sock_destroy(hdhomerun_sock_t sock)
+{
+	close(sock);
+}
+
+int hdhomerun_sock_getlasterror(void)
+{
+	return errno;
+}
+
+uint32_t hdhomerun_sock_getsockname_addr(hdhomerun_sock_t sock)
+{
+	struct sockaddr_in sock_addr;
+	socklen_t sockaddr_size = sizeof(sock_addr);
+
+	if (getsockname(sock, (struct sockaddr *)&sock_addr, &sockaddr_size) != 0) {
+		return 0;
+	}
+
+	return ntohl(sock_addr.sin_addr.s_addr);
+}
+
+uint16_t hdhomerun_sock_getsockname_port(hdhomerun_sock_t sock)
+{
+	struct sockaddr_in sock_addr;
+	socklen_t sockaddr_size = sizeof(sock_addr);
+
+	if (getsockname(sock, (struct sockaddr *)&sock_addr, &sockaddr_size) != 0) {
+		return 0;
+	}
+
+	return ntohs(sock_addr.sin_port);
+}
+
+uint32_t hdhomerun_sock_getpeername_addr(hdhomerun_sock_t sock)
+{
+	struct sockaddr_in sock_addr;
+	socklen_t sockaddr_size = sizeof(sock_addr);
+
+	if (getpeername(sock, (struct sockaddr *)&sock_addr, &sockaddr_size) != 0) {
+		return 0;
+	}
+
+	return ntohl(sock_addr.sin_addr.s_addr);
+}
+
+uint32_t hdhomerun_sock_getaddrinfo_addr(hdhomerun_sock_t sock, const char *name)
+{
+	struct addrinfo hints;
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_INET;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_protocol = IPPROTO_TCP;
+
+	struct addrinfo *sock_info;
+	if (getaddrinfo(name, "", &hints, &sock_info) != 0) {
+		return 0;
+	}
+
+	struct sockaddr_in *sock_addr = (struct sockaddr_in *)sock_info->ai_addr;
+	uint32_t addr = ntohl(sock_addr->sin_addr.s_addr);
+
+	freeaddrinfo(sock_info);
+	return addr;
+}
+
+bool_t hdhomerun_sock_join_multicast_group(hdhomerun_sock_t sock, uint32_t multicast_ip, uint32_t local_ip)
+{
+	struct ip_mreq imr;
+	memset(&imr, 0, sizeof(imr));
+	imr.imr_multiaddr.s_addr  = htonl(multicast_ip);
+	imr.imr_interface.s_addr  = htonl(local_ip);
+
+	if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&imr, sizeof(imr)) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_leave_multicast_group(hdhomerun_sock_t sock, uint32_t multicast_ip, uint32_t local_ip)
+{
+	struct ip_mreq imr;
+	memset(&imr, 0, sizeof(imr));
+	imr.imr_multiaddr.s_addr  = htonl(multicast_ip);
+	imr.imr_interface.s_addr  = htonl(local_ip);
+
+	if (setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (const char *)&imr, sizeof(imr)) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_bind(hdhomerun_sock_t sock, uint32_t local_addr, uint16_t local_port, bool_t allow_reuse)
+{
+	int sock_opt = allow_reuse;
+	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&sock_opt, sizeof(sock_opt));
+
+	struct sockaddr_in sock_addr;
+	memset(&sock_addr, 0, sizeof(sock_addr));
+	sock_addr.sin_family = AF_INET;
+	sock_addr.sin_addr.s_addr = htonl(local_addr);
+	sock_addr.sin_port = htons(local_port);
+
+	if (bind(sock, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static bool_t hdhomerun_sock_wait_for_read_event(hdhomerun_sock_t sock, uint64_t stop_time)
+{
+	uint64_t current_time = getcurrenttime();
+	if (current_time >= stop_time) {
+		return FALSE;
+	}
+
+	if (sock < FD_SETSIZE) {
+		uint64_t timeout = stop_time - current_time;
+		struct timeval t;
+		t.tv_sec = timeout / 1000;
+		t.tv_usec = (timeout % 1000) * 1000;
+
+		fd_set readfds;
+		FD_ZERO(&readfds);
+		FD_SET(sock, &readfds);
+
+		fd_set errorfds;
+		FD_ZERO(&errorfds);
+		FD_SET(sock, &errorfds);
+
+		if (select(sock + 1, &readfds, NULL, &errorfds, &t) <= 0) {
+			return FALSE;
+		}
+		if (!FD_ISSET(sock, &readfds)) {
+			return FALSE;
+		}
+	} else {
+		uint64_t delay = stop_time - current_time;
+		if (delay > 5) {
+			delay = 5;
+		}
+
+		msleep_approx(delay);
+	}
+
+	return TRUE;
+}
+
+static bool_t hdhomerun_sock_wait_for_write_event(hdhomerun_sock_t sock, uint64_t stop_time)
+{
+	uint64_t current_time = getcurrenttime();
+	if (current_time >= stop_time) {
+		return FALSE;
+	}
+
+	if (sock < FD_SETSIZE) {
+		uint64_t timeout = stop_time - current_time;
+		struct timeval t;
+		t.tv_sec = timeout / 1000;
+		t.tv_usec = (timeout % 1000) * 1000;
+
+		fd_set writefds;
+		FD_ZERO(&writefds);
+		FD_SET(sock, &writefds);
+
+		fd_set errorfds;
+		FD_ZERO(&errorfds);
+		FD_SET(sock, &errorfds);
+
+		if (select(sock + 1, NULL, &writefds, &errorfds, &t) <= 0) {
+			return FALSE;
+		}
+		if (!FD_ISSET(sock, &writefds)) {
+			return FALSE;
+		}
+	} else {
+		uint64_t delay = stop_time - current_time;
+		if (delay > 5) {
+			delay = 5;
+		}
+
+		msleep_approx(delay);
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_connect(hdhomerun_sock_t sock, uint32_t remote_addr, uint16_t remote_port, uint64_t timeout)
+{
+	struct sockaddr_in sock_addr;
+	memset(&sock_addr, 0, sizeof(sock_addr));
+	sock_addr.sin_family = AF_INET;
+	sock_addr.sin_addr.s_addr = htonl(remote_addr);
+	sock_addr.sin_port = htons(remote_port);
+
+	if (connect(sock, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) != 0) {
+		if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
+			return FALSE;
+		}
+	}
+
+	uint64_t stop_time = getcurrenttime() + timeout;
+
+	while (1) {
+		if (send(sock, NULL, 0, MSG_NOSIGNAL) == 0) {
+			return TRUE;
+		}
+
+		if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINPROGRESS) && (errno != ENOTCONN)) {
+			return FALSE;
+		}
+
+		if (!hdhomerun_sock_wait_for_write_event(sock, stop_time)) {
+			return FALSE;
+		}
+	}
+}
+
+bool_t hdhomerun_sock_send(hdhomerun_sock_t sock, const void *data, size_t length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+	const uint8_t *ptr = (const uint8_t *)data;
+
+	while (1) {
+		int ret = send(sock, ptr, length, MSG_NOSIGNAL);
+		if (ret <= 0) {
+			if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_write_event(sock, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret < (int)length) {
+			ptr += ret;
+			length -= ret;
+			continue;
+		}
+
+		return TRUE;
+	}
+}
+
+bool_t hdhomerun_sock_sendto(hdhomerun_sock_t sock, uint32_t remote_addr, uint16_t remote_port, const void *data, size_t length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+	const uint8_t *ptr = (const uint8_t *)data;
+
+	while (1) {
+		struct sockaddr_in sock_addr;
+		memset(&sock_addr, 0, sizeof(sock_addr));
+		sock_addr.sin_family = AF_INET;
+		sock_addr.sin_addr.s_addr = htonl(remote_addr);
+		sock_addr.sin_port = htons(remote_port);
+
+		int ret = sendto(sock, ptr, length, 0, (struct sockaddr *)&sock_addr, sizeof(sock_addr));
+		if (ret <= 0) {
+			if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_write_event(sock, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret < (int)length) {
+			ptr += ret;
+			length -= ret;
+			continue;
+		}
+
+		return TRUE;
+	}
+}
+
+bool_t hdhomerun_sock_recv(hdhomerun_sock_t sock, void *data, size_t *length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+
+	while (1) {
+		int ret = recv(sock, data, *length, 0);
+		if (ret < 0) {
+			if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_read_event(sock, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret == 0) {
+			return FALSE;
+		}
+
+		*length = ret;
+		return TRUE;
+	}
+}
+
+bool_t hdhomerun_sock_recvfrom(hdhomerun_sock_t sock, uint32_t *remote_addr, uint16_t *remote_port, void *data, size_t *length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+
+	while (1) {
+		struct sockaddr_in sock_addr;
+		memset(&sock_addr, 0, sizeof(sock_addr));
+		socklen_t sockaddr_size = sizeof(sock_addr);
+
+		int ret = recvfrom(sock, data, *length, 0, (struct sockaddr *)&sock_addr, &sockaddr_size);
+		if (ret < 0) {
+			if ((errno != EAGAIN) && (errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_read_event(sock, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret == 0) {
+			return FALSE;
+		}
+
+		*remote_addr = ntohl(sock_addr.sin_addr.s_addr);
+		*remote_port = ntohs(sock_addr.sin_port);
+		*length = ret;
+		return TRUE;
+	}
+}
diff --git a/libhdhomerun/hdhomerun_sock_windows.c b/libhdhomerun/hdhomerun_sock_windows.c
new file mode 100755
index 0000000..c599694
--- /dev/null
+++ b/libhdhomerun/hdhomerun_sock_windows.c
@@ -0,0 +1,463 @@
+/*
+ * hdhomerun_sock_windows.c
+ *
+ * Copyright © 2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+/*
+ * Implementation notes:
+ *
+ * API specifies timeout for each operation (or zero for non-blocking).
+ *
+ * It is not possible to rely on the OS socket timeout as this will fail to
+ * detect the command-response situation where data is sent successfully and
+ * the other end chooses not to send a response (other than the TCP ack).
+ *
+ * Windows supports select() however native WSA events are used to:
+ * - avoid problems with socket numbers above 1024.
+ * - wait without allowing other events handlers to run (important for use
+ *   with win7 WMC).
+ */
+
+#include "hdhomerun.h"
+#include <windows.h>
+#include <iphlpapi.h>
+
+int hdhomerun_local_ip_info(struct hdhomerun_local_ip_info_t ip_info_list[], int max_count)
+{
+	PIP_ADAPTER_INFO AdapterInfo;
+	ULONG AdapterInfoLength = sizeof(IP_ADAPTER_INFO) * 16;
+
+	while (1) {
+		AdapterInfo = (IP_ADAPTER_INFO *)malloc(AdapterInfoLength);
+		if (!AdapterInfo) {
+			return -1;
+		}
+
+		ULONG LengthNeeded = AdapterInfoLength;
+		DWORD Ret = GetAdaptersInfo(AdapterInfo, &LengthNeeded);
+		if (Ret == NO_ERROR) {
+			break;
+		}
+
+		free(AdapterInfo);
+
+		if (Ret != ERROR_BUFFER_OVERFLOW) {
+			return -1;
+		}
+		if (AdapterInfoLength >= LengthNeeded) {
+			return -1;
+		}
+
+		AdapterInfoLength = LengthNeeded;
+	}
+
+	int count = 0;
+	PIP_ADAPTER_INFO Adapter = AdapterInfo;
+	while (Adapter) {
+		IP_ADDR_STRING *IPAddr = &Adapter->IpAddressList;
+		while (IPAddr) {
+			uint32_t ip_addr = ntohl(inet_addr(IPAddr->IpAddress.String));
+			uint32_t subnet_mask = ntohl(inet_addr(IPAddr->IpMask.String));
+
+			if (ip_addr == 0) {
+				IPAddr = IPAddr->Next;
+				continue;
+			}
+
+			struct hdhomerun_local_ip_info_t *ip_info = &ip_info_list[count++];
+			ip_info->ip_addr = ip_addr;
+			ip_info->subnet_mask = subnet_mask;
+
+			if (count >= max_count) {
+				break;
+			}
+
+			IPAddr = IPAddr->Next;
+		}
+
+		if (count >= max_count) {
+			break;
+		}
+
+		Adapter = Adapter->Next;
+	}
+
+	free(AdapterInfo);
+	return count;
+}
+
+hdhomerun_sock_t hdhomerun_sock_create_udp(void)
+{
+	/* Create socket. */
+	hdhomerun_sock_t sock = (hdhomerun_sock_t)socket(AF_INET, SOCK_DGRAM, 0);
+	if (sock == -1) {
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Set non-blocking */
+	unsigned long mode = 1;
+	if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
+		closesocket(sock);
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Allow broadcast. */
+	int sock_opt = 1;
+	setsockopt(sock, SOL_SOCKET, SO_BROADCAST, (char *)&sock_opt, sizeof(sock_opt));
+
+	/* Success. */
+	return sock;
+}
+
+hdhomerun_sock_t hdhomerun_sock_create_tcp(void)
+{
+	/* Create socket. */
+	hdhomerun_sock_t sock = (hdhomerun_sock_t)socket(AF_INET, SOCK_STREAM, 0);
+	if (sock == -1) {
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Set non-blocking */
+	unsigned long mode = 1;
+	if (ioctlsocket(sock, FIONBIO, &mode) != 0) {
+		closesocket(sock);
+		return HDHOMERUN_SOCK_INVALID;
+	}
+
+	/* Success. */
+	return sock;
+}
+
+void hdhomerun_sock_destroy(hdhomerun_sock_t sock)
+{
+	closesocket(sock);
+}
+
+int hdhomerun_sock_getlasterror(void)
+{
+	return WSAGetLastError();
+}
+
+uint32_t hdhomerun_sock_getsockname_addr(hdhomerun_sock_t sock)
+{
+	struct sockaddr_in sock_addr;
+	int sockaddr_size = sizeof(sock_addr);
+
+	if (getsockname(sock, (struct sockaddr *)&sock_addr, &sockaddr_size) != 0) {
+		return 0;
+	}
+
+	return ntohl(sock_addr.sin_addr.s_addr);
+}
+
+uint16_t hdhomerun_sock_getsockname_port(hdhomerun_sock_t sock)
+{
+	struct sockaddr_in sock_addr;
+	int sockaddr_size = sizeof(sock_addr);
+
+	if (getsockname(sock, (struct sockaddr *)&sock_addr, &sockaddr_size) != 0) {
+		return 0;
+	}
+
+	return ntohs(sock_addr.sin_port);
+}
+
+uint32_t hdhomerun_sock_getpeername_addr(hdhomerun_sock_t sock)
+{
+	struct sockaddr_in sock_addr;
+	int sockaddr_size = sizeof(sock_addr);
+
+	if (getpeername(sock, (struct sockaddr *)&sock_addr, &sockaddr_size) != 0) {
+		return 0;
+	}
+
+	return ntohl(sock_addr.sin_addr.s_addr);
+}
+
+uint32_t hdhomerun_sock_getaddrinfo_addr(hdhomerun_sock_t sock, const char *name)
+{
+	struct addrinfo hints;
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_INET;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_protocol = IPPROTO_TCP;
+
+	struct addrinfo *sock_info;
+	if (getaddrinfo(name, "", &hints, &sock_info) != 0) {
+		return 0;
+	}
+
+	struct sockaddr_in *sock_addr = (struct sockaddr_in *)sock_info->ai_addr;
+	uint32_t addr = ntohl(sock_addr->sin_addr.s_addr);
+
+	freeaddrinfo(sock_info);
+	return addr;
+}
+
+bool_t hdhomerun_sock_join_multicast_group(hdhomerun_sock_t sock, uint32_t multicast_ip, uint32_t local_ip)
+{
+	struct ip_mreq imr;
+	memset(&imr, 0, sizeof(imr));
+	imr.imr_multiaddr.s_addr  = htonl(multicast_ip);
+	imr.imr_interface.s_addr  = htonl(local_ip);
+
+	if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (const char *)&imr, sizeof(imr)) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_leave_multicast_group(hdhomerun_sock_t sock, uint32_t multicast_ip, uint32_t local_ip)
+{
+	struct ip_mreq imr;
+	memset(&imr, 0, sizeof(imr));
+	imr.imr_multiaddr.s_addr  = htonl(multicast_ip);
+	imr.imr_interface.s_addr  = htonl(local_ip);
+
+	if (setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (const char *)&imr, sizeof(imr)) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_bind(hdhomerun_sock_t sock, uint32_t local_addr, uint16_t local_port, bool_t allow_reuse)
+{
+	int sock_opt = allow_reuse;
+	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&sock_opt, sizeof(sock_opt));
+
+	struct sockaddr_in sock_addr;
+	memset(&sock_addr, 0, sizeof(sock_addr));
+	sock_addr.sin_family = AF_INET;
+	sock_addr.sin_addr.s_addr = htonl(local_addr);
+	sock_addr.sin_port = htons(local_port);
+
+	if (bind(sock, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) != 0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_connect(hdhomerun_sock_t sock, uint32_t remote_addr, uint16_t remote_port, uint64_t timeout)
+{
+	/* Connect (non-blocking). */
+	struct sockaddr_in sock_addr;
+	memset(&sock_addr, 0, sizeof(sock_addr));
+	sock_addr.sin_family = AF_INET;
+	sock_addr.sin_addr.s_addr = htonl(remote_addr);
+	sock_addr.sin_port = htons(remote_port);
+
+	if (connect(sock, (struct sockaddr *)&sock_addr, sizeof(sock_addr)) != 0) {
+		if (WSAGetLastError() != WSAEWOULDBLOCK) {
+			return FALSE;
+		}
+	}
+
+	/* Wait for connect to complete (both success and failure will signal). */
+	WSAEVENT wsa_event = WSACreateEvent();
+	if (wsa_event == WSA_INVALID_EVENT) {
+		return FALSE;
+	}
+
+	if (WSAEventSelect(sock, wsa_event, FD_WRITE | FD_CLOSE) == SOCKET_ERROR) {
+		WSACloseEvent(wsa_event);
+		return FALSE;
+	}
+
+	DWORD ret = WaitForSingleObjectEx(wsa_event, (DWORD)timeout, FALSE);
+	WSACloseEvent(wsa_event);
+	if (ret != WAIT_OBJECT_0) {
+		return FALSE;
+	}
+
+	/* Detect success/failure. */
+	wsa_event = WSACreateEvent();
+	if (wsa_event == WSA_INVALID_EVENT) {
+		return FALSE;
+	}
+
+	if (WSAEventSelect(sock, wsa_event, FD_CLOSE) == SOCKET_ERROR) {
+		WSACloseEvent(wsa_event);
+		return FALSE;
+	}
+
+	ret = WaitForSingleObjectEx(wsa_event, 0, FALSE);
+	WSACloseEvent(wsa_event);
+	if (ret == WAIT_OBJECT_0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static bool_t hdhomerun_sock_wait_for_event(hdhomerun_sock_t sock, long event_type, uint64_t stop_time)
+{
+	uint64_t current_time = getcurrenttime();
+	if (current_time >= stop_time) {
+		return FALSE;
+	}
+
+	WSAEVENT wsa_event = WSACreateEvent();
+	if (wsa_event == WSA_INVALID_EVENT) {
+		return FALSE;
+	}
+
+	if (WSAEventSelect(sock, wsa_event, event_type) == SOCKET_ERROR) {
+		WSACloseEvent(wsa_event);
+		return FALSE;
+	}
+
+	DWORD ret = WaitForSingleObjectEx(wsa_event, (DWORD)(stop_time - current_time), FALSE);
+	WSACloseEvent(wsa_event);
+
+	if (ret != WAIT_OBJECT_0) {
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+bool_t hdhomerun_sock_send(hdhomerun_sock_t sock, const void *data, size_t length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+	const uint8_t *ptr = (uint8_t *)data;
+
+	while (1) {
+		int ret = send(sock, (char *)ptr, (int)length, 0);
+		if (ret <= 0) {
+			if (WSAGetLastError() != WSAEWOULDBLOCK) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_event(sock, FD_WRITE | FD_CLOSE, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret < (int)length) {
+			ptr += ret;
+			length -= ret;
+			continue;
+		}
+
+		return TRUE;
+	}
+}
+
+bool_t hdhomerun_sock_sendto(hdhomerun_sock_t sock, uint32_t remote_addr, uint16_t remote_port, const void *data, size_t length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+	const uint8_t *ptr = (uint8_t *)data;
+
+	while (1) {
+		struct sockaddr_in sock_addr;
+		memset(&sock_addr, 0, sizeof(sock_addr));
+		sock_addr.sin_family = AF_INET;
+		sock_addr.sin_addr.s_addr = htonl(remote_addr);
+		sock_addr.sin_port = htons(remote_port);
+
+		int ret = sendto(sock, (char *)ptr, (int)length, 0, (struct sockaddr *)&sock_addr, sizeof(sock_addr));
+		if (ret <= 0) {
+			if (WSAGetLastError() != WSAEWOULDBLOCK) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_event(sock, FD_WRITE | FD_CLOSE, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret < (int)length) {
+			ptr += ret;
+			length -= ret;
+			continue;
+		}
+
+		return TRUE;
+	}
+}
+
+bool_t hdhomerun_sock_recv(hdhomerun_sock_t sock, void *data, size_t *length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+
+	while (1) {
+		int ret = recv(sock, (char *)data, (int)(*length), 0);
+		if (ret < 0) {
+			if (WSAGetLastError() != WSAEWOULDBLOCK) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_event(sock, FD_READ | FD_CLOSE, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret == 0) {
+			return FALSE;
+		}
+
+		*length = ret;
+		return TRUE;
+	}
+}
+
+bool_t hdhomerun_sock_recvfrom(hdhomerun_sock_t sock, uint32_t *remote_addr, uint16_t *remote_port, void *data, size_t *length, uint64_t timeout)
+{
+	uint64_t stop_time = getcurrenttime() + timeout;
+
+	while (1) {
+		struct sockaddr_in sock_addr;
+		memset(&sock_addr, 0, sizeof(sock_addr));
+		int sockaddr_size = sizeof(sock_addr);
+
+		int ret = recvfrom(sock, (char *)data, (int)(*length), 0, (struct sockaddr *)&sock_addr, &sockaddr_size);
+		if (ret < 0) {
+			if (WSAGetLastError() != WSAEWOULDBLOCK) {
+				return FALSE;
+			}
+			if (!hdhomerun_sock_wait_for_event(sock, FD_READ | FD_CLOSE, stop_time)) {
+				return FALSE;
+			}
+			continue;
+		}
+
+		if (ret == 0) {
+			return FALSE;
+		}
+
+		*remote_addr = ntohl(sock_addr.sin_addr.s_addr);
+		*remote_port = ntohs(sock_addr.sin_port);
+		*length = ret;
+		return TRUE;
+	}
+}
diff --git a/libhdhomerun/hdhomerun_types.h b/libhdhomerun/hdhomerun_types.h
new file mode 100755
index 0000000..dd0eb1c
--- /dev/null
+++ b/libhdhomerun/hdhomerun_types.h
@@ -0,0 +1,90 @@
+/*
+ * hdhomerun_types.h
+ *
+ * Copyright © 2008-2009 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#define HDHOMERUN_STATUS_COLOR_NEUTRAL	0xFFFFFFFF
+#define HDHOMERUN_STATUS_COLOR_RED		0xFFFF0000
+#define HDHOMERUN_STATUS_COLOR_YELLOW	0xFFFFFF00
+#define HDHOMERUN_STATUS_COLOR_GREEN	0xFF00C000
+
+struct hdhomerun_device_t;
+struct hdhomerun_device_allocation_t;
+
+struct hdhomerun_tuner_status_t {
+	char channel[32];
+	char lock_str[32];
+	bool_t signal_present;
+	bool_t lock_supported;
+	bool_t lock_unsupported;
+	unsigned int signal_strength;
+	unsigned int signal_to_noise_quality;
+	unsigned int symbol_error_quality;
+	uint32_t raw_bits_per_second;
+	uint32_t packets_per_second;
+};
+
+struct hdhomerun_tuner_vstatus_t {
+	char vchannel[32];
+	char name[32];
+	char auth[32];
+	char cci[32];
+	char cgms[32];
+	bool_t not_subscribed;
+	bool_t not_available;
+	bool_t copy_protected;
+};
+
+struct hdhomerun_channelscan_program_t {
+	char program_str[64];
+	uint16_t program_number;
+	uint16_t virtual_major;
+	uint16_t virtual_minor;
+	uint16_t type;
+	char name[32];
+};
+
+#define HDHOMERUN_CHANNELSCAN_MAX_PROGRAM_COUNT 64
+
+struct hdhomerun_channelscan_result_t {
+	char channel_str[64];
+	uint32_t channelmap;
+	uint32_t frequency;
+	struct hdhomerun_tuner_status_t status;
+	int program_count;
+	struct hdhomerun_channelscan_program_t programs[HDHOMERUN_CHANNELSCAN_MAX_PROGRAM_COUNT];
+	bool_t transport_stream_id_detected;
+	uint16_t transport_stream_id;
+};
+
+struct hdhomerun_plotsample_t {
+	int16_t real;
+	int16_t imag;
+};
diff --git a/libhdhomerun/hdhomerun_video.c b/libhdhomerun/hdhomerun_video.c
new file mode 100755
index 0000000..52b8dca
--- /dev/null
+++ b/libhdhomerun/hdhomerun_video.c
@@ -0,0 +1,395 @@
+/*
+ * hdhomerun_video.c
+ *
+ * Copyright © 2006-2010 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+
+#include "hdhomerun.h"
+
+struct hdhomerun_video_sock_t {
+	pthread_mutex_t lock;
+	struct hdhomerun_debug_t *dbg;
+	hdhomerun_sock_t sock;
+
+	volatile size_t head;
+	volatile size_t tail;
+	uint8_t *buffer;
+	size_t buffer_size;
+	size_t advance;
+
+	pthread_t thread;
+	volatile bool_t terminate;
+
+	volatile uint32_t packet_count;
+	volatile uint32_t transport_error_count;
+	volatile uint32_t network_error_count;
+	volatile uint32_t sequence_error_count;
+	volatile uint32_t overflow_error_count;
+
+	volatile uint32_t rtp_sequence;
+	volatile uint8_t sequence[0x2000];
+};
+
+static THREAD_FUNC_PREFIX hdhomerun_video_thread_execute(void *arg);
+
+struct hdhomerun_video_sock_t *hdhomerun_video_create(uint16_t listen_port, bool_t allow_port_reuse, size_t buffer_size, struct hdhomerun_debug_t *dbg)
+{
+	/* Create object. */
+	struct hdhomerun_video_sock_t *vs = (struct hdhomerun_video_sock_t *)calloc(1, sizeof(struct hdhomerun_video_sock_t));
+	if (!vs) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_video_create: failed to allocate video object\n");
+		return NULL;
+	}
+
+	vs->dbg = dbg;
+	vs->sock = HDHOMERUN_SOCK_INVALID;
+	pthread_mutex_init(&vs->lock, NULL);
+
+	/* Reset sequence tracking. */
+	hdhomerun_video_flush(vs);
+
+	/* Buffer size. */
+	vs->buffer_size = (buffer_size / VIDEO_DATA_PACKET_SIZE) * VIDEO_DATA_PACKET_SIZE;
+	if (vs->buffer_size == 0) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_video_create: invalid buffer size (%lu bytes)\n", (unsigned long)buffer_size);
+		goto error;
+	}
+	vs->buffer_size += VIDEO_DATA_PACKET_SIZE;
+
+	/* Create buffer. */
+	vs->buffer = (uint8_t *)malloc(vs->buffer_size);
+	if (!vs->buffer) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_video_create: failed to allocate buffer (%lu bytes)\n", (unsigned long)vs->buffer_size);
+		goto error;
+	}
+	
+	/* Create socket. */
+	vs->sock = hdhomerun_sock_create_udp();
+	if (vs->sock == HDHOMERUN_SOCK_INVALID) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_video_create: failed to allocate socket\n");
+		goto error;
+	}
+
+	/* Expand socket buffer size. */
+	int rx_size = 1024 * 1024;
+	setsockopt(vs->sock, SOL_SOCKET, SO_RCVBUF, (char *)&rx_size, sizeof(rx_size));
+
+	/* Bind socket. */
+	if (!hdhomerun_sock_bind(vs->sock, INADDR_ANY, listen_port, allow_port_reuse)) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_video_create: failed to bind socket (port %u)\n", listen_port);
+		goto error;
+	}
+
+	/* Start thread. */
+	if (pthread_create(&vs->thread, NULL, &hdhomerun_video_thread_execute, vs) != 0) {
+		hdhomerun_debug_printf(dbg, "hdhomerun_video_create: failed to start thread\n");
+		goto error;
+	}
+
+	/* Success. */
+	return vs;
+
+error:
+	if (vs->sock != HDHOMERUN_SOCK_INVALID) {
+		hdhomerun_sock_destroy(vs->sock);
+	}
+	if (vs->buffer) {
+		free(vs->buffer);
+	}
+	free(vs);
+	return NULL;
+}
+
+void hdhomerun_video_destroy(struct hdhomerun_video_sock_t *vs)
+{
+	vs->terminate = TRUE;
+	pthread_join(vs->thread, NULL);
+
+	hdhomerun_sock_destroy(vs->sock);
+	free(vs->buffer);
+
+	free(vs);
+}
+
+hdhomerun_sock_t hdhomerun_video_get_sock(struct hdhomerun_video_sock_t *vs)
+{
+	return vs->sock;
+}
+
+uint16_t hdhomerun_video_get_local_port(struct hdhomerun_video_sock_t *vs)
+{
+	uint16_t port = hdhomerun_sock_getsockname_port(vs->sock);
+	if (port == 0) {
+		hdhomerun_debug_printf(vs->dbg, "hdhomerun_video_get_local_port: getsockname failed (%d)\n", hdhomerun_sock_getlasterror());
+		return 0;
+	}
+
+	return port;
+}
+
+int hdhomerun_video_join_multicast_group(struct hdhomerun_video_sock_t *vs, uint32_t multicast_ip, uint32_t local_ip)
+{
+	if (!hdhomerun_sock_join_multicast_group(vs->sock, multicast_ip, local_ip)) {
+		hdhomerun_debug_printf(vs->dbg, "hdhomerun_video_join_multicast_group: setsockopt failed (%d)\n", hdhomerun_sock_getlasterror());
+		return -1;
+	}
+
+	return 1;
+}
+
+void hdhomerun_video_leave_multicast_group(struct hdhomerun_video_sock_t *vs, uint32_t multicast_ip, uint32_t local_ip)
+{
+	if (!hdhomerun_sock_leave_multicast_group(vs->sock, multicast_ip, local_ip)) {
+		hdhomerun_debug_printf(vs->dbg, "hdhomerun_video_leave_multicast_group: setsockopt failed (%d)\n", hdhomerun_sock_getlasterror());
+	}
+}
+
+static void hdhomerun_video_stats_ts_pkt(struct hdhomerun_video_sock_t *vs, uint8_t *ptr)
+{
+	uint16_t packet_identifier = ((uint16_t)(ptr[1] & 0x1F) << 8) | (uint16_t)ptr[2];
+	if (packet_identifier == 0x1FFF) {
+		return;
+	}
+
+	bool_t transport_error = ptr[1] >> 7;
+	if (transport_error) {
+		vs->transport_error_count++;
+		vs->sequence[packet_identifier] = 0xFF;
+		return;
+	}
+
+	uint8_t sequence = ptr[3] & 0x0F;
+
+	uint8_t previous_sequence = vs->sequence[packet_identifier];
+	vs->sequence[packet_identifier] = sequence;
+
+	if (previous_sequence == 0xFF) {
+		return;
+	}
+	if (sequence == ((previous_sequence + 1) & 0x0F)) {
+		return;
+	}
+	if (sequence == previous_sequence) {
+		return;
+	}
+
+	vs->sequence_error_count++;
+}
+
+static void hdhomerun_video_parse_rtp(struct hdhomerun_video_sock_t *vs, struct hdhomerun_pkt_t *pkt)
+{
+	pkt->pos += 2;
+	uint32_t rtp_sequence = hdhomerun_pkt_read_u16(pkt);
+	pkt->pos += 8;
+
+	uint32_t previous_rtp_sequence = vs->rtp_sequence;
+	vs->rtp_sequence = rtp_sequence;
+
+	/* Initial case - first packet received. */
+	if (previous_rtp_sequence == 0xFFFFFFFF) {
+		return;
+	}
+
+	/* Normal case - next sequence number. */
+	if (rtp_sequence == ((previous_rtp_sequence + 1) & 0xFFFF)) {
+		return;
+	}
+
+	/* Error case - sequence missed. */
+	vs->network_error_count++;
+
+	/* Restart pid sequence check after packet loss. */
+	int i;
+	for (i = 0; i < 0x2000; i++) {
+		vs->sequence[i] = 0xFF;
+	}
+}
+
+static THREAD_FUNC_PREFIX hdhomerun_video_thread_execute(void *arg)
+{
+	struct hdhomerun_video_sock_t *vs = (struct hdhomerun_video_sock_t *)arg;
+	struct hdhomerun_pkt_t pkt_inst;
+
+	while (!vs->terminate) {
+		struct hdhomerun_pkt_t *pkt = &pkt_inst;
+		hdhomerun_pkt_reset(pkt);
+
+		/* Receive. */
+		size_t length = VIDEO_RTP_DATA_PACKET_SIZE;
+		if (!hdhomerun_sock_recv(vs->sock, pkt->end, &length, 25)) {
+			continue;
+		}
+
+		pkt->end += length;
+
+		if (length == VIDEO_RTP_DATA_PACKET_SIZE) {
+			hdhomerun_video_parse_rtp(vs, pkt);
+			length = (int)(pkt->end - pkt->pos);
+		}
+
+		if (length != VIDEO_DATA_PACKET_SIZE) {
+			/* Data received but not valid - ignore. */
+			continue;
+		}
+
+		pthread_mutex_lock(&vs->lock);
+
+		/* Store in ring buffer. */
+		size_t head = vs->head;
+		uint8_t *ptr = vs->buffer + head;
+		memcpy(ptr, pkt->pos, length);
+
+		/* Stats. */
+		vs->packet_count++;
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 0);
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 1);
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 2);
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 3);
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 4);
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 5);
+		hdhomerun_video_stats_ts_pkt(vs, ptr + TS_PACKET_SIZE * 6);
+
+		/* Calculate new head. */
+		head += length;
+		if (head >= vs->buffer_size) {
+			head -= vs->buffer_size;
+		}
+
+		/* Check for buffer overflow. */
+		if (head == vs->tail) {
+			vs->overflow_error_count++;
+			pthread_mutex_unlock(&vs->lock);
+			continue;
+		}
+
+		vs->head = head;
+
+		pthread_mutex_unlock(&vs->lock);
+	}
+
+	return NULL;
+}
+
+uint8_t *hdhomerun_video_recv(struct hdhomerun_video_sock_t *vs, size_t max_size, size_t *pactual_size)
+{
+	pthread_mutex_lock(&vs->lock);
+
+	size_t head = vs->head;
+	size_t tail = vs->tail;
+
+	if (vs->advance > 0) {
+		tail += vs->advance;
+		if (tail >= vs->buffer_size) {
+			tail -= vs->buffer_size;
+		}
+	
+		vs->tail = tail;
+	}
+
+	if (head == tail) {
+		vs->advance = 0;
+		*pactual_size = 0;
+		pthread_mutex_unlock(&vs->lock);
+		return NULL;
+	}
+
+	size_t size = (max_size / VIDEO_DATA_PACKET_SIZE) * VIDEO_DATA_PACKET_SIZE;
+	if (size == 0) {
+		vs->advance = 0;
+		*pactual_size = 0;
+		pthread_mutex_unlock(&vs->lock);
+		return NULL;
+	}
+
+	size_t avail;
+	if (head > tail) {
+		avail = head - tail;
+	} else {
+		avail = vs->buffer_size - tail;
+	}
+	if (size > avail) {
+		size = avail;
+	}
+	vs->advance = size;
+	*pactual_size = size;
+	uint8_t *result = vs->buffer + tail;
+
+	pthread_mutex_unlock(&vs->lock);
+	return result;
+}
+
+void hdhomerun_video_flush(struct hdhomerun_video_sock_t *vs)
+{
+	pthread_mutex_lock(&vs->lock);
+
+	vs->tail = vs->head;
+	vs->advance = 0;
+
+	vs->rtp_sequence = 0xFFFFFFFF;
+
+	int i;
+	for (i = 0; i < 0x2000; i++) {
+		vs->sequence[i] = 0xFF;
+	}
+
+	vs->packet_count = 0;
+	vs->transport_error_count = 0;
+	vs->network_error_count = 0;
+	vs->sequence_error_count = 0;
+	vs->overflow_error_count = 0;
+
+	pthread_mutex_unlock(&vs->lock);
+}
+
+void hdhomerun_video_debug_print_stats(struct hdhomerun_video_sock_t *vs)
+{
+	struct hdhomerun_video_stats_t stats;
+	hdhomerun_video_get_stats(vs, &stats);
+
+	hdhomerun_debug_printf(vs->dbg, "video sock: pkt=%lu net=%lu te=%lu miss=%lu drop=%lu\n",
+		(unsigned long)stats.packet_count, (unsigned long)stats.network_error_count,
+		(unsigned long)stats.transport_error_count, (unsigned long)stats.sequence_error_count,
+		(unsigned long)stats.overflow_error_count
+	);
+}
+
+void hdhomerun_video_get_stats(struct hdhomerun_video_sock_t *vs, struct hdhomerun_video_stats_t *stats)
+{
+	memset(stats, 0, sizeof(struct hdhomerun_video_stats_t));
+
+	pthread_mutex_lock(&vs->lock);
+
+	stats->packet_count = vs->packet_count;
+	stats->network_error_count = vs->network_error_count;
+	stats->transport_error_count = vs->transport_error_count;
+	stats->sequence_error_count = vs->sequence_error_count;
+	stats->overflow_error_count = vs->overflow_error_count;
+
+	pthread_mutex_unlock(&vs->lock);
+}
diff --git a/libhdhomerun/hdhomerun_video.h b/libhdhomerun/hdhomerun_video.h
new file mode 100755
index 0000000..90dc9d8
--- /dev/null
+++ b/libhdhomerun/hdhomerun_video.h
@@ -0,0 +1,116 @@
+/*
+ * hdhomerun_video.h
+ *
+ * Copyright © 2006 Silicondust USA Inc. <www.silicondust.com>.
+ *
+ * This library is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ * 
+ * As a special exception to the GNU Lesser General Public License,
+ * you may link, statically or dynamically, an application with a
+ * publicly distributed version of the Library to produce an
+ * executable file containing portions of the Library, and
+ * distribute that executable file under terms of your choice,
+ * without any of the additional requirements listed in clause 4 of
+ * the GNU Lesser General Public License.
+ * 
+ * By "a publicly distributed version of the Library", we mean
+ * either the unmodified Library as distributed by Silicondust, or a
+ * modified version of the Library that is distributed under the
+ * conditions defined in the GNU Lesser General Public License.
+ */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hdhomerun_video_sock_t;
+
+struct hdhomerun_video_stats_t {
+	uint32_t packet_count;
+	uint32_t network_error_count;
+	uint32_t transport_error_count;
+	uint32_t sequence_error_count;
+	uint32_t overflow_error_count;
+};
+
+#define TS_PACKET_SIZE 188
+#define VIDEO_DATA_PACKET_SIZE (188 * 7)
+#define VIDEO_DATA_BUFFER_SIZE_1S (20000000 / 8)
+
+#define VIDEO_RTP_DATA_PACKET_SIZE ((188 * 7) + 12)
+
+/*
+ * Create a video/data socket.
+ *
+ * uint16_t listen_port: Port number to listen on. Set to 0 to auto-select.
+ * size_t buffer_size: Size of receive buffer. For 1 second of buffer use VIDEO_DATA_BUFFER_SIZE_1S. 
+ * struct hdhomerun_debug_t *dbg: Pointer to debug logging object. May be NULL.
+ *
+ * Returns a pointer to the newly created control socket.
+ *
+ * When no longer needed, the socket should be destroyed by calling hdhomerun_control_destroy.
+ */
+extern LIBTYPE struct hdhomerun_video_sock_t *hdhomerun_video_create(uint16_t listen_port, bool_t allow_port_reuse, size_t buffer_size, struct hdhomerun_debug_t *dbg);
+extern LIBTYPE void hdhomerun_video_destroy(struct hdhomerun_video_sock_t *vs);
+
+/*
+ * Get the port the socket is listening on.
+ *
+ * Returns 16-bit port with native endianness, or 0 on error.
+ */
+extern LIBTYPE uint16_t hdhomerun_video_get_local_port(struct hdhomerun_video_sock_t *vs);
+
+/*
+ * Join/leave multicast group.
+ */
+extern LIBTYPE int hdhomerun_video_join_multicast_group(struct hdhomerun_video_sock_t *vs, uint32_t multicast_ip, uint32_t local_ip);
+extern LIBTYPE void hdhomerun_video_leave_multicast_group(struct hdhomerun_video_sock_t *vs, uint32_t multicast_ip, uint32_t local_ip);
+
+/*
+ * Read data from buffer.
+ *
+ * size_t max_size: The maximum amount of data to be returned.
+ * size_t *pactual_size: The caller-supplied pactual_size value will be updated to contain the amount
+ *		of data available to the caller.
+ *
+ * Returns a pointer to the data, or NULL if no data is available.
+ * The data will remain valid until another call to hdhomerun_video_recv.
+ *
+ * The amount of data returned will always be a multiple of VIDEO_DATA_PACKET_SIZE (1316).
+ * Attempting to read a single TS frame (188 bytes) will not return data as it is less than
+ * the minimum size.
+ *
+ * The buffer is implemented as a ring buffer. It is possible for this function to return a small
+ * amount of data when more is available due to the wrap-around case.
+ */
+extern LIBTYPE uint8_t *hdhomerun_video_recv(struct hdhomerun_video_sock_t *vs, size_t max_size, size_t *pactual_size);
+
+/*
+ * Flush the buffer.
+ */
+extern LIBTYPE void hdhomerun_video_flush(struct hdhomerun_video_sock_t *vs);
+
+/*
+ * Debug print internal stats.
+ */
+extern LIBTYPE void hdhomerun_video_debug_print_stats(struct hdhomerun_video_sock_t *vs);
+extern LIBTYPE void hdhomerun_video_get_stats(struct hdhomerun_video_sock_t *vs, struct hdhomerun_video_stats_t *stats);
+
+/*
+ * Internal use only.
+ */
+extern LIBTYPE hdhomerun_sock_t hdhomerun_video_get_sock(struct hdhomerun_video_sock_t *vs);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/libhdhomerun/lgpl.txt b/libhdhomerun/lgpl.txt
new file mode 100755
index 0000000..fc8a5de
--- /dev/null
+++ b/libhdhomerun/lgpl.txt
@@ -0,0 +1,165 @@
+		   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions. 
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version. 
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.