diff --git a/include/output_rtltcp.h b/include/output_rtltcp.h
new file mode 100644
index 00000000..59ac76e2
--- /dev/null
+++ b/include/output_rtltcp.h
@@ -0,0 +1,31 @@
+/** @file
+    rtl_tcp output for rtl_433 raw data.
+
+    Copyright (C) 2022 Christian Zuckschwerdt
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+*/
+
+#ifndef INCLUDE_OUTPUT_RTLTCP_H_
+#define INCLUDE_OUTPUT_RTLTCP_H_
+
+#include "raw_output.h"
+
+#include <stdint.h>
+
+struct r_cfg;
+
+/** Construct rtl_tcp data output.
+
+    @param host the server host to bind
+    @param port the server port to bind
+    @param cfg the r_api config to use
+    @return The initialized rtltcp output instance.
+            You must release this object with raw_output_free once you're done with it.
+*/
+struct raw_output *raw_output_rtltcp_create(char const *host, char const *port, struct r_cfg *cfg);
+
+#endif /* INCLUDE_OUTPUT_RTLTCP_H_ */
diff --git a/include/r_api.h b/include/r_api.h
index 299ac9d5..2f7a1ee0 100644
--- a/include/r_api.h
+++ b/include/r_api.h
@@ -85,6 +85,8 @@ void add_trigger_output(struct r_cfg *cfg, char *param);
 
 void add_null_output(struct r_cfg *cfg, char *param);
 
+void add_rtltcp_output(struct r_cfg *cfg, char *param);
+
 void start_outputs(struct r_cfg *cfg, char const *const *well_known);
 
 void add_sr_dumper(struct r_cfg *cfg, char const *spec, int overwrite);
diff --git a/include/raw_output.h b/include/raw_output.h
new file mode 100644
index 00000000..c8d9157b
--- /dev/null
+++ b/include/raw_output.h
@@ -0,0 +1,28 @@
+/** @file
+    Raw I/Q data output handler.
+
+    Copyright (C) 2022 Christian Zuckschwerdt
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+*/
+
+#ifndef INCLUDE_RAW_OUTPUT_H_
+#define INCLUDE_RAW_OUTPUT_H_
+
+#include <stdint.h>
+
+struct raw_output;
+
+typedef struct raw_output {
+    void (*output_frame)(struct raw_output *output, uint8_t const *data, uint32_t len);
+    void (*output_free)(struct raw_output *output);
+} raw_output_t;
+
+void raw_output_frame(struct raw_output *output, uint8_t const *data, uint32_t len);
+
+void raw_output_free(struct raw_output *output);
+
+#endif /* INCLUDE_RAW_OUTPUT_H_ */
diff --git a/include/rtl_433.h b/include/rtl_433.h
index 7fe352ee..30b785c0 100644
--- a/include/rtl_433.h
+++ b/include/rtl_433.h
@@ -94,6 +94,7 @@ typedef struct r_cfg {
     uint16_t num_r_devices;
     list_t data_tags;
     list_t output_handler;
+    list_t raw_handler;
     struct dm_state *demod;
     char const *sr_filename;
     int sr_execopen;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index b12bb25f..a30688fc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -25,6 +25,7 @@ add_library(r_433 STATIC
     output_file.c
     output_influx.c
     output_mqtt.c
+    output_rtltcp.c
     output_trigger.c
     output_udp.c
     pulse_analyzer.c
@@ -33,6 +34,7 @@ add_library(r_433 STATIC
     pulse_detect_fsk.c
     r_api.c
     r_util.c
+    raw_output.c
     rfraw.c
     samp_grab.c
     sdr.c
diff --git a/src/output_rtltcp.c b/src/output_rtltcp.c
new file mode 100644
index 00000000..03040624
--- /dev/null
+++ b/src/output_rtltcp.c
@@ -0,0 +1,557 @@
+/** @file
+    rtl_tcp output for rtl_433 raw data.
+
+    Copyright (C) 2022 Christian Zuckschwerdt
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+*/
+
+#include "output_rtltcp.h"
+
+#include "rtl_433.h"
+#include "r_api.h"
+#include "r_util.h"
+#include "fatal.h"
+#include "compat_pthread.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <signal.h>
+
+#include <limits.h>
+// gethostname() needs _XOPEN_SOURCE 500 on unistd.h
+#ifndef _XOPEN_SOURCE
+#define _XOPEN_SOURCE 500
+#endif
+
+#ifndef _MSC_VER
+#include <unistd.h>
+#endif
+
+#ifdef _WIN32
+    #if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0600)
+    #undef _WIN32_WINNT
+    #define _WIN32_WINNT 0x0600   /* Needed to pull in 'struct sockaddr_storage' */
+    #endif
+
+    #include <winsock2.h>
+    #include <ws2tcpip.h>
+#else
+    #include <sys/types.h>
+    #include <sys/socket.h>
+    #include <sys/select.h>
+    #include <netdb.h>
+    #include <netinet/in.h>
+
+    #define SOCKET          int
+    #define INVALID_SOCKET  (-1)
+    #define closesocket(x)  close(x)
+#endif
+
+#include <time.h>
+
+#ifdef _WIN32
+    #define _POSIX_HOST_NAME_MAX  128
+    #define perror(str)           ws2_perror(str)
+
+    static void ws2_perror(const char *str)
+    {
+        if (str && *str)
+            fprintf(stderr, "%s: ", str);
+        fprintf(stderr, "Winsock error %d.\n", WSAGetLastError());
+    }
+#endif
+#ifdef ESP32
+    #include <tcpip_adapter.h>
+    #define _POSIX_HOST_NAME_MAX 128
+    #define gai_strerror strerror
+#endif
+
+#ifdef _MSC_VER
+#include <BaseTsd.h>
+typedef SSIZE_T ssize_t;
+#endif
+
+// MSG_NOSIGNAL is Linux and most BSDs only, not macOS or Windows
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif
+
+/* rtl_tcp server */
+
+// Only available if Threads are enabled.
+// Currently serves a maximum of 1 client connection.
+// A data backing of max_clients+1 slots is needed to write a data slot
+// when each client is blocking a different data slot.
+// Should use a global ring buffer or shared memory for sendfile() someday.
+
+#ifdef THREADS
+
+#define DATA_SLOTS 2
+typedef struct rtltcp_server {
+    struct sockaddr_storage addr;
+    socklen_t addr_len;
+    SOCKET sock;
+    int client_count; ///< number of connected clients
+
+    int data_recent;            ///< the data slot with most recent data, -1 otherwise
+    int data_inuse[DATA_SLOTS]; ///< data slot is in use, 0 otherwise
+    void *data_buf[DATA_SLOTS]; ///< data slot memory of data_size bytes, NULL otherwise
+    int data_size[DATA_SLOTS];  ///< data slot data_buf size, 0 otherwise
+    int data_len[DATA_SLOTS];   ///< data slot valid bytes in data_buf, 0 otherwise
+
+    pthread_t thread;
+    pthread_mutex_t lock; ///< lock for data slots
+    pthread_cond_t cond;  ///< wait for data slots
+    r_cfg_t *cfg;
+    struct raw_output *output;
+} rtltcp_server_t;
+
+static ssize_t send_all(int sockfd, void const *buf, size_t len, int flags)
+{
+    size_t sent = 0;
+    while (sent < len) {
+        ssize_t ret = send(sockfd, (uint8_t *)buf + sent, len - sent, flags);
+        if (ret < 0)
+            return ret;
+        sent += (size_t)ret;
+    }
+    return sent;
+}
+
+static void send_header(SOCKET sock)
+{
+    uint8_t msg[] = {'R', 'T', 'L', '0', 0, 0, 0, 0, 0, 0, 0, 0};
+    send_all(sock, msg, sizeof(msg), MSG_NOSIGNAL); // ignore SIGPIPE
+}
+
+#define RTLTCP_SET_FREQ 0x01
+#define RTLTCP_SET_SAMPLE_RATE 0x02
+#define RTLTCP_SET_GAIN_MODE 0x03
+#define RTLTCP_SET_GAIN 0x04
+#define RTLTCP_SET_FREQ_CORRECTION 0x05
+#define RTLTCP_SET_IF_TUNER_GAIN 0x06
+#define RTLTCP_SET_TEST_MODE 0x07
+#define RTLTCP_SET_AGC_MODE 0x08
+#define RTLTCP_SET_DIRECT_SAMPLING 0x09
+#define RTLTCP_SET_OFFSET_TUNING 0x0a
+#define RTLTCP_SET_RTL_XTAL 0x0b
+#define RTLTCP_SET_TUNER_XTAL 0x0c
+#define RTLTCP_SET_TUNER_GAIN_BY_ID 0x0d
+#define RTLTCP_SET_BIAS_TEE 0x0e
+
+/*
+E.g. initialization from Gqrx:
+- RTLTCP_SET_GAIN_MODE  with 1
+- RTLTCP_SET_AGC_MODE   with 0
+- RTLTCP_SET_DIRECT_SAMPLING  with 0
+- RTLTCP_SET_OFFSET_TUNING  with 0
+- RTLTCP_SET_BIAS_TEE  with 0
+- RTLTCP_SET_SAMPLE_RATE  with 250000
+- RTLTCP_SET_FREQ  with 52000000
+- RTLTCP_SET_GAIN  with 0
+- RTLTCP_SET_GAIN  with 0
+- RTLTCP_SET_FREQ  with 433968000
+*/
+
+static int parse_command(r_cfg_t *cfg, uint8_t const *buf, int len)
+{
+    UNUSED(cfg);
+
+    if (len < 5)
+        return 0;
+    int cmd = buf[0];
+    unsigned arg = (unsigned)buf[1] << 24 | buf[2] << 16 | buf[3] << 8 | buf[4];
+    // fprintf(stderr, "CMD: %d with %u (%d) %02x %02x %02x %02x\n", cmd, arg, (int)arg, buf[1], buf[2], buf[3], buf[4]);
+    len -= 5;
+
+    switch (cmd) {
+    case RTLTCP_SET_FREQ:
+        fprintf(stderr, "rtl_tcp received command SET_FREQ with %u\n", arg);
+        // set_center_freq(cfg, arg);
+        break;
+    case RTLTCP_SET_SAMPLE_RATE:
+        fprintf(stderr, "rtl_tcp received command SET_SAMPLE_RATE with %u\n", arg);
+        // set_sample_rate(cfg, arg);
+        break;
+    case RTLTCP_SET_GAIN_MODE:
+        fprintf(stderr, "rtl_tcp received command SET_GAIN_MODE with %u\n", arg);
+        break;
+    case RTLTCP_SET_GAIN:
+        fprintf(stderr, "rtl_tcp received command SET_GAIN with %u\n", arg);
+        break;
+    case RTLTCP_SET_FREQ_CORRECTION:
+        fprintf(stderr, "rtl_tcp received command SET_FREQ_CORRECTION with %u\n", arg);
+        // set_freq_correction(cfg, (int)arg);
+        break;
+    case RTLTCP_SET_IF_TUNER_GAIN:
+        fprintf(stderr, "rtl_tcp received command SET_IF_TUNER_GAIN with %u\n", arg);
+        break;
+    case RTLTCP_SET_TEST_MODE:
+        fprintf(stderr, "rtl_tcp received command SET_TEST_MODE with %u\n", arg);
+        break;
+    case RTLTCP_SET_AGC_MODE:
+        fprintf(stderr, "rtl_tcp received command SET_AGC_MODE with %u\n", arg);
+        break;
+    case RTLTCP_SET_DIRECT_SAMPLING:
+        fprintf(stderr, "rtl_tcp received command SET_DIRECT_SAMPLING with %u\n", arg);
+        break;
+    case RTLTCP_SET_OFFSET_TUNING:
+        fprintf(stderr, "rtl_tcp received command SET_OFFSET_TUNING with %u\n", arg);
+        break;
+    case RTLTCP_SET_RTL_XTAL:
+        fprintf(stderr, "rtl_tcp received command SET_RTL_XTAL with %u\n", arg);
+        break;
+    case RTLTCP_SET_TUNER_XTAL:
+        fprintf(stderr, "rtl_tcp received command SET_TUNER_XTAL with %u\n", arg);
+        break;
+    case RTLTCP_SET_TUNER_GAIN_BY_ID:
+        fprintf(stderr, "rtl_tcp received command SET_TUNER_GAIN_BY_ID with %u\n", arg);
+        break;
+    case RTLTCP_SET_BIAS_TEE:
+        fprintf(stderr, "rtl_tcp received command SET_BIAS_TEE with %u\n", arg);
+        break;
+    default:
+        fprintf(stderr, "rtl_tcp received unknown command %d with %u\n", cmd, arg);
+        break;
+    }
+
+    return 5;
+}
+
+// event handler to broadcast to all our sockets
+static void rtltcp_broadcast_send(rtltcp_server_t *srv, uint8_t const *data, int len)
+{
+    // fprintf(stderr, "%s: %d byte frame\n", __func__, len);
+    pthread_mutex_lock(&srv->lock);
+    if (srv->client_count <= 0) {
+        pthread_mutex_unlock(&srv->lock);
+        return; // no clients, do nothing
+    }
+
+    // find a free slot
+    int slot = 0;
+    for (; slot < DATA_SLOTS; ++slot) {
+        if (srv->data_inuse[slot] == 0) {
+            break;
+        }
+    }
+    if (slot >= DATA_SLOTS) {
+        fprintf(stderr, "%s: all data slots in use!\n", __func__);
+        return; // this should never happen
+    }
+
+    // (re-)allocate slot buffer if needed
+    if (srv->data_buf[slot] == NULL || srv->data_size[slot] < len) {
+        //fprintf(stderr, "%s: allocating buffer of %d bytes for rtl_tcp\n", __func__, len);
+        free(srv->data_buf[slot]);
+        srv->data_buf[slot] = malloc(len);
+        if (!srv->data_buf[slot]) {
+            FATAL_MALLOC("rtltcp_broadcast_send()");
+        }
+        srv->data_size[slot] = len;
+    }
+
+    // transfer data to the buffer slot
+    memcpy(srv->data_buf[slot], data, len);
+    srv->data_len[slot] = len;
+    srv->data_recent = slot;
+    pthread_mutex_unlock(&srv->lock);
+    pthread_cond_signal(&srv->cond);
+    // perhaps broadcast if we want to support multiple clients
+    //int pthread_cond_broadcast(&srv->cond);
+}
+
+static THREAD_RETURN THREAD_CALL accept_thread(void *arg)
+{
+    rtltcp_server_t *srv = arg;
+
+    // Start listening for clients, waits for an incoming connection
+    listen(srv->sock, 1);
+    //fprintf(stderr, "rtl_tcp listening...\n");
+
+    for (;;) {
+        // Accept actual connection from the client
+        struct sockaddr_storage addr = {0};
+        unsigned addr_len = sizeof(addr);
+        int sock = accept(srv->sock, (struct sockaddr *)&addr, &addr_len);
+
+        if (sock < 0) {
+            perror("ERROR on accept");
+            continue;
+        }
+
+        // Prevent SIGPIPE per file descriptor, supported on MacOS and most BSDs
+#ifdef SO_NOSIGPIPE
+        int opt = 1;
+        if (setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &opt, sizeof(opt)) == -1) {
+            perror("setsockopt");
+            continue;
+        }
+#endif
+
+        char host[INET6_ADDRSTRLEN] = {0};
+        char port[NI_MAXSERV]       = {0};
+
+        int err = getnameinfo((struct sockaddr *)&srv->addr, srv->addr_len,
+                host, sizeof(host), port, sizeof(port), NI_NUMERICHOST | NI_NUMERICSERV);
+        if (err != 0) {
+            fprintf(stderr, "failed to convert address to string (code=%d)\n", err);
+            continue;
+        }
+        fprintf(stderr, "rtl_tcp client connected from %s port %s\n", host, port);
+
+        pthread_mutex_lock(&srv->lock);
+        srv->client_count += 1;
+        pthread_mutex_unlock(&srv->lock);
+        int slot = -1; // data sent in previous loop
+
+        send_header(sock);
+
+        // Client loop
+        for (;;) {
+            // Read available commands
+            int abort = 0;
+            for (;;) {
+                fd_set fds;
+                FD_ZERO(&fds);
+                FD_SET(sock, &fds);
+                struct timeval timeout = {0};
+
+                int ready = select(sock + 1, &fds, NULL, NULL, &timeout);
+                if (ready <= 0)
+                    break;
+
+                uint8_t buf[128] = {0};
+                ssize_t len = recv(sock, buf, sizeof(buf), 0);
+                //fprintf(stderr, "rtl_tcp recv %zd bytes (%d)\n", len, ready);
+                if (len <= 0) {
+                    abort = 1;
+                    break;
+                }
+                int pos = 0;
+                while (pos + 5 <= len) {
+                    pos += parse_command(srv->cfg, &buf[pos], (int)len - pos);
+                }
+            }
+            if (abort) {
+                break;
+            }
+
+            // Send frames
+            fd_set fds;
+            FD_ZERO(&fds);
+            FD_SET(sock, &fds);
+            struct timeval timeout = {0};
+
+            int ready = select(sock + 1, NULL, &fds, NULL, &timeout);
+            if (ready <= 0) {
+                fprintf(stderr, "rtl_tcp send not ready for write?\n");
+                break;
+            }
+
+            pthread_mutex_lock(&srv->lock);
+            if (srv->data_recent < 0 || srv->data_recent == slot)
+                pthread_cond_wait(&srv->cond, &srv->lock);
+            // Maybe timeout to check recv()
+            // pthread_cond_timedwait(&srv->cond, &srv->lock, const struct timespec *abstime);
+
+            // Get data and mark as in use
+            slot = srv->data_recent;
+            srv->data_inuse[slot] += 1;
+            void const *data = srv->data_buf[slot];
+            int data_len     = srv->data_len[slot];
+            pthread_mutex_unlock(&srv->lock);
+
+            send_all(sock, data, data_len, MSG_NOSIGNAL); // ignore SIGPIPE
+
+            // Mark data as done
+            pthread_mutex_lock(&srv->lock);
+            srv->data_inuse[slot] -= 1;
+            pthread_mutex_unlock(&srv->lock);
+        }
+
+        pthread_mutex_lock(&srv->lock);
+        srv->client_count -= 1;
+        pthread_mutex_unlock(&srv->lock);
+
+        fprintf(stderr, "rtl_tcp client disconnected from %s port %s\n", host, port);
+        close(sock);
+    }
+    return NULL;
+}
+
+static int rtltcp_server_start(rtltcp_server_t *srv, char const *host, char const *port, r_cfg_t *cfg, struct raw_output *output)
+{
+    if (!host || !port)
+        return -1;
+
+    struct addrinfo hints, *res, *res0;
+    int error;
+    SOCKET sock;
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family   = PF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
+    hints.ai_flags    = AI_ADDRCONFIG;
+    error             = getaddrinfo(host, port, &hints, &res0);
+    if (error) {
+        fprintf(stderr, "%s\n", gai_strerror(error));
+        return -1;
+    }
+    sock = INVALID_SOCKET;
+    for (res = res0; res; res = res->ai_next) {
+        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+        if (sock >= 0) {
+            srv->sock = sock;
+            memset(&srv->addr, 0, sizeof(srv->addr));
+            memcpy(&srv->addr, res->ai_addr, res->ai_addrlen);
+            srv->addr_len = res->ai_addrlen;
+            break; // success
+        }
+    }
+    freeaddrinfo(res0);
+    if (sock == INVALID_SOCKET) {
+        perror("socket");
+        return -1;
+    }
+
+    if (bind(sock, (struct sockaddr *)&srv->addr, srv->addr_len) < 0) {
+        perror("error on binding");
+        return -1;
+    }
+
+    srv->cfg     = cfg;
+    srv->output  = output;
+
+    srv->data_recent = -1;
+
+    char address[INET6_ADDRSTRLEN] = {0};
+    char portstr[NI_MAXSERV] = {0};
+
+    int err = getnameinfo((struct sockaddr *)&srv->addr, srv->addr_len,
+            address, sizeof(address), portstr, sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV);
+    if (err != 0) {
+        fprintf(stderr, "failed to convert address to string (code=%d)\n", err);
+        return -1;
+    }
+    fprintf(stderr, "Starting rtl_tcp server on address %s %s\n", address, portstr);
+
+    pthread_mutex_init(&srv->lock, NULL);
+    pthread_cond_init(&srv->cond, NULL);
+
+#ifndef _WIN32
+    // Block all signals from the worker thread
+    sigset_t sigset;
+    sigset_t oldset;
+    sigfillset(&sigset);
+    pthread_sigmask(SIG_SETMASK, &sigset, &oldset);
+#endif
+    int r = pthread_create(&srv->thread, NULL, accept_thread, srv);
+#ifndef _WIN32
+    pthread_sigmask(SIG_SETMASK, &oldset, NULL);
+#endif
+    if (r) {
+        fprintf(stderr, "%s: error in pthread_create, rc: %d\n", __func__, r);
+    }
+
+    return r;
+}
+
+static int rtltcp_server_stop(rtltcp_server_t *srv)
+{
+    if (!srv)
+        return 0;
+
+    fprintf(stderr, "Stopping rtl_tcp server...\n");
+    // thread is likely blocking in accept, recv, or send
+    int r = pthread_cancel(srv->thread);
+    if (r) {
+        fprintf(stderr, "%s: error in pthread_cancel, rc: %d\n", __func__, r);
+    }
+    pthread_mutex_destroy(&srv->lock);
+    pthread_cond_destroy(&srv->cond);
+
+    srv->client_count = 0;
+    for (int slot = 0; slot < DATA_SLOTS; ++slot) {
+        free(srv->data_buf[slot]);
+        srv->data_buf[slot] = NULL;
+        srv->data_inuse[slot] = 0;
+    }
+
+    // close server socket
+    int ret = 0;
+    if (srv->sock != INVALID_SOCKET) {
+        ret = closesocket(srv->sock);
+        srv->sock = INVALID_SOCKET;
+    }
+
+#ifdef _WIN32
+    WSACleanup();
+#endif
+
+    return ret;
+}
+
+/* rtl_tcp raw output */
+
+typedef struct raw_output_rtltcp {
+    struct raw_output output;
+    rtltcp_server_t server;
+} raw_output_rtltcp_t;
+
+static void raw_output_rtltcp_frame(raw_output_t *output, uint8_t const *data, uint32_t len)
+{
+    raw_output_rtltcp_t *rtltcp = (raw_output_rtltcp_t *)output;
+
+    rtltcp_broadcast_send(&rtltcp->server, data, len);
+}
+
+static void raw_output_rtltcp_free(raw_output_t *output)
+{
+    raw_output_rtltcp_t *rtltcp = (raw_output_rtltcp_t *)output;
+
+    if (!rtltcp)
+        return;
+
+    rtltcp_server_stop(&rtltcp->server);
+
+    free(rtltcp);
+}
+
+struct raw_output *raw_output_rtltcp_create(const char *host, const char *port, r_cfg_t *cfg)
+{
+    raw_output_rtltcp_t *rtltcp = calloc(1, sizeof(raw_output_rtltcp_t));
+    if (!rtltcp) {
+        WARN_CALLOC("raw_output_rtltcp_create()");
+        return NULL; // NOTE: returns NULL on alloc failure.
+    }
+
+    rtltcp->output.output_frame  = raw_output_rtltcp_frame;
+    rtltcp->output.output_free   = raw_output_rtltcp_free;
+
+    int ret = rtltcp_server_start(&rtltcp->server, host, port, cfg, &rtltcp->output);
+    if (ret != 0) {
+        exit(1);
+    }
+
+    return &rtltcp->output;
+}
+
+#else
+
+struct raw_output *raw_output_rtltcp_create(const char *host, const char *port, r_cfg_t *cfg)
+{
+    UNUSED(host);
+    UNUSED(port);
+    UNUSED(cfg);
+    fprintf(stderr, "\nWARNING: rtl_tcp not available in this build!\n\n");
+    return NULL;
+}
+
+#endif
diff --git a/src/r_api.c b/src/r_api.c
index b2c4a24f..0e04c603 100644
--- a/src/r_api.c
+++ b/src/r_api.c
@@ -34,6 +34,7 @@
 #include "output_mqtt.h"
 #include "output_influx.h"
 #include "output_trigger.h"
+#include "output_rtltcp.h"
 #include "write_sigrok.h"
 #include "mongoose.h"
 #include "compat_time.h"
@@ -223,6 +224,8 @@ void r_free_cfg(r_cfg_t *cfg)
 
     free(cfg->devices);
 
+    list_free_elems(&cfg->raw_handler, (list_elem_free_fn)raw_output_free);
+
     list_free_elems(&cfg->output_handler, (list_elem_free_fn)data_output_free);
 
     list_free_elems(&cfg->data_tags, (list_elem_free_fn)data_tag_free);
@@ -999,6 +1002,16 @@ void add_null_output(r_cfg_t *cfg, char *param)
     list_push(&cfg->output_handler, NULL);
 }
 
+void add_rtltcp_output(r_cfg_t *cfg, char *param)
+{
+    char *host = "localhost";
+    char *port = "1234";
+    hostport_param(param, &host, &port);
+    fprintf(stderr, "rtl_tcp server at %s port %s\n", host, port);
+
+    list_push(&cfg->raw_handler, raw_output_rtltcp_create(host, port, cfg));
+}
+
 void add_sr_dumper(r_cfg_t *cfg, char const *spec, int overwrite)
 {
     // create channels
diff --git a/src/raw_output.c b/src/raw_output.c
new file mode 100644
index 00000000..4c191a88
--- /dev/null
+++ b/src/raw_output.c
@@ -0,0 +1,30 @@
+/** @file
+    Raw I/Q data output handler.
+
+    Copyright (C) 2022 Christian Zuckschwerdt
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+*/
+
+#include "raw_output.h"
+
+#include <stdint.h>
+
+/* generic raw_output */
+
+void raw_output_frame(struct raw_output *output, uint8_t const *data, uint32_t len)
+{
+    if (!output)
+        return;
+    output->output_frame(output, data, len);
+}
+
+void raw_output_free(struct raw_output *output)
+{
+    if (!output)
+        return;
+    output->output_free(output);
+}
diff --git a/src/rtl_433.c b/src/rtl_433.c
index 138eaacf..1f784556 100644
--- a/src/rtl_433.c
+++ b/src/rtl_433.c
@@ -40,6 +40,7 @@
 #include "pulse_demod.h"
 #include "rfraw.h"
 #include "data.h"
+#include "raw_output.h"
 #include "r_util.h"
 #include "optparse.h"
 #include "abuf.h"
@@ -365,6 +366,12 @@ static void sdr_callback(unsigned char *iq_buf, uint32_t len, void *ctx)
     char time_str[LOCAL_TIME_BUFLEN];
     unsigned long n_samples;
 
+    // do this here and not in sdr_handler so realtime replay can use rtl_tcp output
+    for (void **iter = cfg->raw_handler.elems; iter && *iter; ++iter) {
+        raw_output_t *output = *iter;
+        raw_output_frame(output, iq_buf, len);
+    }
+
     if ((cfg->bytes_to_read > 0) && (cfg->bytes_to_read <= len)) {
         len = cfg->bytes_to_read;
         cfg->exit_async = 1;
@@ -1144,6 +1151,9 @@ static void parse_conf_option(r_cfg_t *cfg, int opt, char *arg)
         else if (strncmp(arg, "null", 4) == 0) {
             add_null_output(cfg, arg_param(arg));
         }
+        else if (strncmp(arg, "rtl_tcp", 7) == 0) {
+            add_rtltcp_output(cfg, arg_param(arg));
+        }
         else {
             fprintf(stderr, "Invalid output format %s\n", arg);
             usage(1);
diff --git a/vs15/rtl_433.vcxproj b/vs15/rtl_433.vcxproj
index 40bb4200..b921ee2b 100644
--- a/vs15/rtl_433.vcxproj
+++ b/vs15/rtl_433.vcxproj
@@ -116,6 +116,7 @@ COPY ..\..\libusb\MS64\dll\libusb*.dll $(TargetDir)</Command>
     <ClInclude Include="..\include\output_file.h" />
     <ClInclude Include="..\include\output_influx.h" />
     <ClInclude Include="..\include\output_mqtt.h" />
+    <ClInclude Include="..\include\output_rtltcp.h" />
     <ClInclude Include="..\include\output_trigger.h" />
     <ClInclude Include="..\include\output_udp.h" />
     <ClInclude Include="..\include\pulse_analyzer.h" />
@@ -126,6 +127,7 @@ COPY ..\..\libusb\MS64\dll\libusb*.dll $(TargetDir)</Command>
     <ClInclude Include="..\include\r_device.h" />
     <ClInclude Include="..\include\r_private.h" />
     <ClInclude Include="..\include\r_util.h" />
+    <ClInclude Include="..\include\raw_output.h" />
     <ClInclude Include="..\include\rfraw.h" />
     <ClInclude Include="..\include\rtl_433.h" />
     <ClInclude Include="..\include\rtl_433_devices.h" />
@@ -157,6 +159,7 @@ COPY ..\..\libusb\MS64\dll\libusb*.dll $(TargetDir)</Command>
     <ClCompile Include="..\src\output_file.c" />
     <ClCompile Include="..\src\output_influx.c" />
     <ClCompile Include="..\src\output_mqtt.c" />
+    <ClCompile Include="..\src\output_rtltcp.c" />
     <ClCompile Include="..\src\output_trigger.c" />
     <ClCompile Include="..\src\output_udp.c" />
     <ClCompile Include="..\src\pulse_analyzer.c" />
@@ -165,6 +168,7 @@ COPY ..\..\libusb\MS64\dll\libusb*.dll $(TargetDir)</Command>
     <ClCompile Include="..\src\pulse_detect_fsk.c" />
     <ClCompile Include="..\src\r_api.c" />
     <ClCompile Include="..\src\r_util.c" />
+    <ClCompile Include="..\src\raw_output.c" />
     <ClCompile Include="..\src\rfraw.c" />
     <ClCompile Include="..\src\rtl_433.c" />
     <ClCompile Include="..\src\samp_grab.c" />
diff --git a/vs15/rtl_433.vcxproj.filters b/vs15/rtl_433.vcxproj.filters
index 856cef4c..9fa44ca8 100644
--- a/vs15/rtl_433.vcxproj.filters
+++ b/vs15/rtl_433.vcxproj.filters
@@ -86,6 +86,9 @@
     <ClInclude Include="..\include\output_mqtt.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\include\output_rtltcp.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\include\output_trigger.h">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -116,6 +119,9 @@
     <ClInclude Include="..\include\r_util.h">
       <Filter>Header Files</Filter>
     </ClInclude>
+    <ClInclude Include="..\include\raw_output.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
     <ClInclude Include="..\include\rfraw.h">
       <Filter>Header Files</Filter>
     </ClInclude>
@@ -205,6 +211,9 @@
     <ClCompile Include="..\src\output_mqtt.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\output_rtltcp.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\output_trigger.c">
       <Filter>Source Files</Filter>
     </ClCompile>
@@ -229,6 +238,9 @@
     <ClCompile Include="..\src\r_util.c">
       <Filter>Source Files</Filter>
     </ClCompile>
+    <ClCompile Include="..\src\raw_output.c">
+      <Filter>Source Files</Filter>
+    </ClCompile>
     <ClCompile Include="..\src\rfraw.c">
       <Filter>Source Files</Filter>
     </ClCompile>