diff --git a/Makefile b/Makefile
index d2c4a799152fb40125889e33c275ff779cd97acd..d5728627f1b3805f7325fe062de5ad47f22cde69 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-TARGETS = probe probe-raw remux thumbnail transcode publish_video simple_live_transcode
+TARGETS = probe probe-raw remux thumbnail transcode publish_video simple_live_transcode live_forward
 
 CFLAGS = -I /usr/include/libxml2
 LDFLAGS= -lcurl -lavcodec -lavformat -lavfilter -lswscale -lavutil -lxml2
@@ -20,5 +20,7 @@ publish_video: publish_video.c *.h util/*.c
 
 simple_live_transcode: simple_live_transcode.c *.h util/*.c
 
+live_forward: live_forward.c *.h util/*.c
+
 clean:
 	rm -f ${TARGETS}
diff --git a/live_forward.c b/live_forward.c
new file mode 100644
index 0000000000000000000000000000000000000000..f3fe35a15a291a4c4b278c7842e1120668f89cdf
--- /dev/null
+++ b/live_forward.c
@@ -0,0 +1,84 @@
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+
+#include "util.h"
+
+int main(int argc, char *argv[])
+{
+	int i, err, canceled;
+	int *idxmap;
+	char *src, *dest;
+	AVFormatContext *demux, *mux;
+	AVPacket pkt;
+	AVStream *stream;
+	AVDictionary *muxopts;
+	if (argc != 5)
+		return 1;
+	av_register_all();
+	avformat_network_init();
+	init_env();
+	init_avlogbuf();
+	memset(&pkt, 0, sizeof(pkt));
+	av_init_packet(&pkt);
+
+	jobid = atoi(argv[1]);
+	src = jstr(jlookup(argv[4], "src"), "rtmp://example.com:99999/notfound");
+	dest = jstr(jlookup(argv[4], "dest"), "rtmp://example.com:99999/notfound");
+	ping_job(jobid, "running", 0);
+
+	demux = 0;
+	if (err = avformat_open_input(&demux, src, 0, 0))
+		job_failed("Opening input stream failed: %s", av_err2str(err));
+	avformat_find_stream_info(demux, 0);
+	err = avformat_alloc_output_context2(&mux,
+			av_guess_format(jstr(jlookup(argv[4], "format"), 0), dest, 0), 0, dest);
+	if (err < 0)
+		job_failed("Error allocating muxer context: %s", av_err2str(err));
+	av_dict_copy(&mux->metadata, demux->metadata, 0);
+	parse_dict(&mux->metadata, jlookup(argv[4], "metadata"));
+	idxmap = malloc(sizeof(int)*demux->nb_streams);
+	for (i = 0; i < demux->nb_streams; i ++)
+	{
+		idxmap[i] = -1;
+		if (demux->streams[i]->codecpar->codec_type != AVMEDIA_TYPE_AUDIO
+				&& demux->streams[i]->codecpar->codec_type != AVMEDIA_TYPE_VIDEO
+				&& demux->streams[i]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE)
+			continue;
+		stream = avformat_new_stream(mux, 0);
+		av_dict_copy(&stream->metadata, mux->streams[i]->metadata, 0);
+		idxmap[i] = stream->index;
+		avcodec_parameters_copy(mux->streams[i]->codecpar,
+				demux->streams[i]->codecpar);
+		mux->streams[i]->codecpar->codec_tag = 0;
+		mux->streams[i]->time_base = demux->streams[i]->time_base;
+	}
+	muxopts = 0;
+	parse_dict(&muxopts, jlookup(argv[4], "options"));
+	if ((err = avformat_write_header(mux, &muxopts)) < 0)
+		job_failed("Writing header failed: %s", av_err2str(err));
+	canceled = 0;
+	while (!canceled && !av_read_frame(demux, &pkt))
+	{
+		if (!checktime(30))
+			canceled = ping_job(jobid, "running", 0);
+		if (pkt.stream_index >= demux->nb_streams
+				|| idxmap[pkt.stream_index] == -1)
+			continue;
+		pkt.stream_index = idxmap[pkt.stream_index];
+		pkt.pts = av_rescale_q_rnd(pkt.pts, demux->streams[pkt.stream_index]->time_base,
+				mux->streams[pkt.stream_index]->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
+		pkt.dts = av_rescale_q_rnd(pkt.dts, demux->streams[pkt.stream_index]->time_base,
+				mux->streams[pkt.stream_index]->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
+		pkt.duration = av_rescale_q(pkt.duration, demux->streams[pkt.stream_index]->time_base,
+				mux->streams[pkt.stream_index]->time_base);
+		pkt.pos = -1;
+		if (err = av_interleaved_write_frame(mux, &pkt))
+			job_failed("Could not write frame: %s", av_err2str(err));
+	}
+	avformat_close_input(&demux);
+	av_interleaved_write_frame(mux, 0);
+	if (err = av_write_trailer(mux))
+		job_failed("Error writing trailer", av_err2str(err));
+	ping_job(jobid, "finished", "{%s, \"log\": \"%s\"}", jescape(get_avlogbuf()));
+	return 0;
+}