shithub: rtmp

Download patch

ref: 74213ea11514c175161cc1a61d48a429845d4624
author: Sigrid Solveig Haflínudóttir <ftrvxmtrx@gmail.com>
date: Tue Jul 20 11:31:54 EDT 2021

first, nothing to see yet

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,1 @@
+This is partially based on librtmp which is LGPLv2.1, same goes for this project.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,3 @@
+# rtmp
+
+RTMP streaming for Plan 9. WIP.
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,12 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin/video
+TARG=rtmp
+
+OFILES=\
+	rtmp.$O\
+
+default:V: all
+
+</sys/src/cmd/mkone
+#LDFLAGS=-p
--- /dev/null
+++ b/rtmp.c
@@ -1,0 +1,510 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <bio.h>
+#include <libsec.h>
+
+enum {
+	Port = 1935,
+
+	Anum,
+	Aarr = 8,
+	Aend,
+	Alstr = 12,
+
+	Faudio = 8,
+	Fvideo = 9,
+	Fscript = 18,
+
+	EncH264 = 7,
+	EncAAC = 10,
+
+	FlKey = 1<<0,
+	FlHdr = 1<<1,
+
+	Sigsz = 1536,
+};
+
+typedef struct Frame Frame;
+typedef struct IVF IVF;
+
+struct Frame {
+	u64int ts;
+	u8int *buf;
+	int bufsz;
+	int sz;
+};
+
+struct IVF {
+	u16int w, h;
+	u32int tbdenum, tbnum;
+};
+
+int mainstacksize = 65536;
+
+static u8int *
+amfi16(u8int *p, u8int *e, s16int i)
+{
+	if(p == nil)
+		return nil;
+	if(e-p < 2){
+		werrstr("buffer short");
+		return nil;
+	}
+	*p++ = i >> 8;
+	*p++ = i;
+
+	return p;
+}
+
+static u8int *
+amfi24(u8int *p, u8int *e, s32int i)
+{
+	if(p == nil)
+		return nil;
+	if(e-p < 3){
+		werrstr("buffer short");
+		return nil;
+	}
+	*p++ = i >> 16;
+	*p++ = i >> 8;
+	*p++ = i;
+
+	return p;
+}
+
+static u8int *
+amfi32(u8int *p, u8int *e, s32int i)
+{
+	if(p == nil)
+		return nil;
+	if(e-p < 4){
+		werrstr("buffer short");
+		return nil;
+	}
+	*p++ = i >> 24;
+	*p++ = i >> 16;
+	*p++ = i >> 8;
+	*p++ = i;
+
+	return p;
+}
+
+static u8int *
+amfnum(u8int *p, u8int *e, double v)
+{
+	union {
+		double v;
+		u64int u;
+	}x;
+
+	if(p == nil)
+		return nil;
+	if(p+8 > e){
+		werrstr("buffer short");
+		return nil;
+	}
+	x.v = v;
+	*p++ = x.u >> 56;
+	*p++ = x.u >> 48;
+	*p++ = x.u >> 40;
+	*p++ = x.u >> 32;
+	*p++ = x.u >> 24;
+	*p++ = x.u >> 16;
+	*p++ = x.u >> 8;
+	*p++ = x.u;
+
+	return p;
+}
+
+static u8int *
+amfkvnum(u8int *p, u8int *e, char *name, double v)
+{
+	int n;
+
+	if(p == nil)
+		return nil;
+	if((n = strlen(name)) > 0xffff){
+		werrstr("string too long");
+		return nil;
+	}
+	if(p+2+n+8 > e){
+		werrstr("buffer short");
+		return nil;
+	}
+	p = amfi16(p, e, n);
+	p = (u8int*)memmove(p, name, n) + n;
+
+	return amfnum(p, e, v);
+}
+
+static u8int *
+amfstr(u8int *p, u8int *e, char *s)
+{
+	int n;
+
+	if(p == nil)
+		return nil;
+	n = strlen(s);
+	if(p+1+4+n > e){
+		werrstr("string too long");
+		return nil;
+	}
+	*p++ = Alstr;
+
+	return (u8int*)memmove(amfi32(p, e, n), s, n) + n;
+}
+
+static u8int *
+amfarr(u8int *p, u8int *e)
+{
+	if(p == nil)
+		return nil;
+	if(p == e){
+		werrstr("buffer short");
+		return nil;
+	}
+	*p++ = Aarr;
+
+	return p;
+}
+
+static u8int *
+amfend(u8int *p, u8int *e)
+{
+	return amfi24(p, e, Aend);
+}
+
+static u8int *
+flvscript(u8int *p, u8int *e, int w, int h, int audio)
+{
+	u8int *psz, *d, *p0;
+	int stream;
+	u32int ts;
+
+	if(p+16 > e){
+		werrstr("buffer short");
+		return nil;
+	}
+
+	/* FIXME ever need to change these? */
+	stream = 0;
+	ts = 0;
+
+	p0 = p;
+	*p++ = Fvideo;
+	psz = p;
+	p = amfi24(p, e, 0); /* sz set later */
+	p = amfi24(p, e, ts);
+	*p++ = ts>>24;
+	p = amfi24(p, e, stream);
+
+	d = p;
+	p = amfstr(p, e, "onMetaData");
+	p = amfarr(p, e);
+	p = amfi32(p, e, audio ? 5 : 4);
+	p = amfkvnum(p, e, "duration", 0.0);
+	p = amfkvnum(p, e, "width", w);
+	p = amfkvnum(p, e, "height", h);
+	p = amfkvnum(p, e, "videocodecid", EncH264);
+	if(audio)
+		p = amfkvnum(p, e, "audiocodecid", EncAAC);
+	p = amfend(p, e);
+	amfi24(psz, e, p-d);
+
+	return amfi32(p, e, p-p0);
+}
+
+static u8int *
+flvdata(u8int *p, u8int *e, u32int pts, u32int dts, void *data, int sz, int type, int fl)
+{
+	u8int *p0, *psz, *d;
+	int stream;
+
+	/* FIXME ever need to change these? */
+	stream = 0;
+
+	assert(type == Faudio || type == Fvideo);
+	p0 = p;
+	*p++ = type;
+	psz = p;
+	p = amfi24(p, e, 0); /* size to be set later */
+	p = amfi24(p, e, dts);
+	*p++ = dts >> 24;
+	p = amfi24(p, e, stream);
+
+	d = p;
+	if(type == Faudio){
+		*p++ = (EncAAC<<4) | 0x0f;
+		*p++ = (fl & FlHdr) ? 0 : 1;
+	}
+	if(type == Fvideo){
+		*p++ = ((fl & FlKey) ? 0x10 : 0x20) | EncH264;
+		*p++ = (fl & FlHdr) ? 0 : 1;
+		pts = ((fl & FlHdr) || pts < dts) ? 0 : (pts - dts);
+		p = amfi24(p, e, pts);
+		if((fl & FlHdr) == 0)
+			p = amfi32(p, e, sz);
+	}
+	p = (u8int*)memmove(p, data, sz) + sz;
+	amfi24(psz, e, p-d);
+
+	return amfi32(p, e, p-p0);
+}
+
+static int
+Bu16le(Biobuf *b, u16int *o)
+{
+	int x;
+
+	x = Bgetc(b);
+	x |= Bgetc(b)<<8;
+	*o = x;
+	if(x < 0)
+		werrstr("failed to read 2 bytes");
+
+	return x < 0 ? -1 : 0;
+}
+
+static int
+Bu32le(Biobuf *b, u32int *o)
+{
+	int x, i;
+
+	*o = 0;
+	for(i = 0; i < 4; *o |= x<<(i*8), i++){
+		if((x = Bgetc(b)) < 0){
+			werrstr("failed to read 4 bytes");
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+static int
+Bu64le(Biobuf *b, u64int *o)
+{
+	int x, i;
+
+	*o = 0;
+	for(i = 0; i < 8; *o |= x<<(i*8), i++){
+		if((x = Bgetc(b)) < 0){
+			werrstr("failed to read 8 bytes");
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+static int
+ivfopen(Biobuf *v, IVF *o)
+{
+	u16int hlen;
+	u8int b[6];
+
+	if(Bread(v, b, 6) != 6 || Bu16le(v, &hlen) < 0 ||
+	   hlen < 0x20 || memcmp(b, "DKIF", 4) != 0 ||
+	   Bread(v, b, 4) != 4){
+		werrstr("invalid header");
+		goto err;
+	}
+	if(memcmp(b, "AVC1", 4) != 0){
+		werrstr("not H.264");
+		goto err;
+	}
+	if(Bu16le(v, &o->w) < 0 ||
+	   Bu16le(v, &o->h) < 0 ||
+	   Bu32le(v, &o->tbdenum) < 0 ||
+	   Bu32le(v, &o->tbnum) < 0){
+		werrstr("invalid data");
+		goto err;
+	}
+	if(Bseek(v, hlen, 0) != hlen){
+		werrstr("broken stream");
+		goto err;
+	}
+
+	return 0;
+err:
+	werrstr("ivf: %r");
+	return -1;
+}
+
+static int
+ivfread(Biobuf *v, Frame *f)
+{
+	u8int *buf;
+	u64int ts;
+	u32int sz;
+	int n;
+
+	if(Bu32le(v, &sz) < 0 || Bu64le(v, &ts) < 0 || (int)sz < 0){
+		/* eof */
+		f->sz = 0;
+		return 0;
+	}
+	buf = f->buf;
+	if(sz > f->bufsz){
+		if((buf = realloc(f->buf, sz)) == nil){
+			werrstr("frame is too big: %d bytes", sz);
+			goto err;
+		}
+		f->buf = buf;
+	}
+	if((n = Bread(v, buf, sz)) != sz){
+		werrstr("short read (%d < %d)", n, sz);
+		goto err;
+	}
+	f->buf = buf;
+	f->sz = sz;
+	f->ts = ts;
+
+	return 0;
+err:
+	werrstr("ivf: %r");
+	return -1;
+}
+
+static int
+handshake(int f, char *path)
+{
+	u8int cl[1+Sigsz], sv[1+Sigsz];
+
+	cl[0] = 3; /* no encryption */
+	memset(cl+1, 0, 8);
+	prng(cl+1+8, Sigsz-8);
+	if(write(f, cl, sizeof(cl)) != sizeof(cl))
+		goto err;
+	if(readn(f, sv, sizeof(sv)) != sizeof(sv))
+		goto err;
+	if(cl[0] != sv[0]){
+		werrstr("expected %d (no encryption), got %d", cl[0], sv[0]);
+		goto err;
+	}
+	if(write(f, sv+1, Sigsz) != Sigsz)
+		goto err;
+	if(readn(f, sv+1, Sigsz) != Sigsz)
+		goto err;
+	if(memcmp(cl, sv, sizeof(cl)) != 0){
+		werrstr("signature mismatch");
+		goto err;
+	}
+
+	return f;
+
+err:
+	werrstr("handshake: %r");
+	close(f);
+	return -1;
+}
+
+static int
+rtmpdial(char *url)
+{
+	char *s, *e, *path;
+	int f, port, ctl;
+
+	if(memcmp(url, "rtmp://", 7) != 0){
+		werrstr("invalid url");
+		goto err;
+	}
+	s = url + 7;
+	if((e = strpbrk(s, ":/")) == nil){
+		werrstr("no path");
+		goto err;
+	}
+	port = 1935;
+	if(*e == ':'){
+		if((port = strtol(e+1, &path, 10)) < 1 || path == e+1 || *path != '/'){
+			werrstr("invalid port");
+			goto err;
+		}
+	}else{
+		path = e;
+	}
+
+	s = smprint("tcp!%.*s!%d", (int)(e-s), s, port);
+	f = dial(s, nil, nil, &ctl);
+	free(s);
+	if(f < 0)
+		goto err;
+
+	return handshake(f, path);
+err:
+	werrstr("rtmpdial: %r");
+	return -1;
+}
+
+static void
+usage(void)
+{
+	fprint(2, "usage: %s [-a AUDIO] -v VIDEO [URL]\n", argv0);
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Biobuf *a, *v, o;
+	u8int *b, *p, *e;
+	int bufsz, fd;
+	Frame f;
+	IVF ivf;
+
+	a = nil;
+	v = nil;
+	ARGBEGIN{
+	case 'a':
+		if((a = Bopen(EARGF(usage()), OREAD)) == nil)
+			sysfatal("%r");
+		break;
+	case 'v':
+		if((v = Bopen(EARGF(usage()), OREAD)) == nil)
+			sysfatal("%r");
+		break;
+	default:
+		usage();
+	}ARGEND
+
+	if(argc != 1)
+		usage();
+	if(v == nil)
+		sysfatal("no video specified");
+	if(ivfopen(v, &ivf) != 0)
+		sysfatal("%r");
+	srand(time(nil));
+	if((fd = rtmpdial(argv[0])) < 0 || Binit(&o, fd, OWRITE) < 0)
+		sysfatal("%r");
+
+	bufsz = 65536;
+	if((b = malloc(bufsz)) == nil)
+		sysfatal("memory");
+	e = b + bufsz;
+
+	if((p = flvscript(b, e, ivf.w, ivf.h, 0)) == nil || Bwrite(&o, b, p-b) < 0)
+		sysfatal("%r");
+
+	memset(&f, 0, sizeof(f));
+	for(;;){
+		if(ivfread(v, &f) != 0)
+			sysfatal("%r");
+		if(f.sz == 0)
+			break;
+		if(bufsz < f.sz+64){
+			free(b);
+			bufsz *= 2;
+			if((b = malloc(bufsz)) == nil)
+				sysfatal("memory");
+			e = b + bufsz;
+		}
+		if((p = flvdata(b, e, f.ts, f.ts, f.buf, f.sz, Fvideo, FlHdr)) == nil)
+			sysfatal("video: flvdata: %r");
+		if(Bwrite(&o, b, p-b) < 0)
+			sysfatal("%r");
+		Bflush(&o);
+	}
+
+	threadexitsall(nil);
+}