$ git clone http://tcclient.ion.nu/tc_client.git
commit cc86304e817d91aaa0b59deb1d1883f725994b96
Author: Alicia <...>
Date:   Tue Apr 7 06:49:01 2015 +0200

    Version 0.21

diff --git a/ChangeLog b/ChangeLog
index 4662717..a403f50 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,14 @@
+0.21:
+Renamed streamids to chunkids to better match the specification.
+Added a /close command for mods to close cam/mic streams.
+Added /ban, /banlist and /forgive commands.
+Added a /opencam command to receive cam data, and the 'camviewer' utility to handle the cam data (depends on libavcodec/libswscale and gtk+ 3.x), mostly as a reference.
+Do not echo while entering account password at runtime.
+Announce when someone cams up, and list who is on cam when joining.
+modbot: added !wrongrequest to undo a request.
+modbot: allow entering account password at runtime and without echo.
+modbot: added !skip/!skip <number> to skip videos in the queue.
+irchack: translate between /userinfo and WHOIS.
 0.20:
 Fixed amf0 reading of numbers (type 0)
 Added support for sending format 0 RTMP packets (which include the msgid field)
diff --git a/Makefile b/Makefile
index b364d79..7cdc521 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,41 @@
-VERSION=0.20
+VERSION=0.21
 CFLAGS=-g3 -Wall $(shell curl-config --cflags)
 LIBS=-g3 $(shell curl-config --libs)
 ifneq ($(wildcard config.mk),)
   include config.mk
 endif
+OBJ=client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endian.o media.o
+IRCHACK_OBJ=utilities/irchack/irchack.o
+MODBOT_OBJ=utilities/modbot/modbot.o utilities/modbot/list.o utilities/modbot/queue.o
+CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
+UTILS=irchack modbot
+ifdef GTK_LIBS
+ifdef AVCODEC_LIBS
+ifdef AVUTIL_LIBS
+ifdef SWSCALE_LIBS
+  UTILS+=camviewer
+  CFLAGS+=$(GTK_CFLAGS) $(AVCODEC_CFLAGS) $(AVUTIL_CFLAGS) $(SWSCALE_CFLAGS)
+endif
+endif
+endif
+endif
+
+tc_client: $(OBJ)
+ $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
+
+utils: $(UTILS)
+
+irchack: $(IRCHACK_OBJ)
+ $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
 
-tc_client: client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endian.o
+modbot: $(MODBOT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
 
-utils: irchack modbot
+camviewer: $(CAMVIEWER_OBJ)
+ $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) -o $@
 
 clean:
- rm -f client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endian.o tc_client irchack modbot
+ rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) tc_client irchack modbot camviewer
 
 tarball:
- tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h LICENSE README ChangeLog crossbuild.sh irchack.c modbot.c configure
+ tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c configure
diff --git a/README b/README
index 73890b7..bc658a0 100644
--- a/README
+++ b/README
@@ -1,26 +1,32 @@
 tc_client is a very primitive application, it doesn't implement a user interface on its own,
 you could use it stand-alone but it will be ugly and if someone says something while you are typing a message your input-line will get cut off.
-Better interfaces can be implemented as wrappers that launch tc_client with stdin and stdout as pipes.
+Better interfaces can be implemented as wrappers that launch tc_client with stdin and stdout as pipes. See the bottom of this document for some included applications that run on top of tc_client.
 
 Commands supported by tc_client:
-/msg <nickname> <message> = send a PM, incoming PMs look similar
-/color <on/off>           = enable/disable showing colors with ANSI escapes
-/color <0-15>             = set the color of your outgoing messages
-/colors                   = list the available colors
-/nick <newnick>           = change nickname
-/help                     = list these commands at runtime
+/msg <to> <msg> = send a PM, incoming PMs look similar
+/color <0-15>   = set the color of your outgoing messages
+/color <on/off> = enable/disable showing colors with ANSI escapes
+/color          = see your current color
+/colors         = list the available colors
+/nick <newnick> = change nickname
+/opencam <nick> = see someone's cam/mic (Warning: writes binary data to stdout)
+/close <nick>   = close someone's cam/mic stream (as a mod)
+/ban <nick>     = ban someone
+/banlist        = list who is banned
+/forgive <nick/ID> = unban someone
+/help           = list these commands at runtime
 
-Some things that will probably never change:
-*tc_client can't view people's webcams or listen to mics
+Some things that may never change:
 *tc_client can't stream/broadcast your webcam/mic
 *tc_client itself won't play youtube videos, but applications that rely on tc_client can interpret the /mbs, /mbsk and /mbc commands
 
 Current commands sent by the TC servers that tc_client doesn't know how to handle:
-notice
+notice (some, notice is used for many functions)
 joinsdone
 avons (list of people currently on cam)
 pros
 
 Included applications that rely on tc_client (type 'make utils' to build):
-*irchack = a minimal IRC server that translates between IRC and tc_client's commands
-*modbot  = a bot that handles youtube video requests with a queue and an approval system to keep inappropriate videos from being played. Supports the following commands: !help, !request, !queue (show queue status), for mods only: !playnext (try playing next in queue without marking it yet), !approve, !badvid (Note: modbot depends on youtube-dl to find out the length of videos)
+*irchack   = a minimal IRC server that translates between IRC and tc_client's commands
+*modbot    = a bot that handles youtube video requests with a queue and an approval system to keep inappropriate videos from being played. Supports the following commands: !help, !request, !queue (show queue status), for mods only: !playnext (try playing next in queue without marking it yet), !approve, !badvid (Note: modbot depends on youtube-dl to find out the length of videos)
+*camviewer = an example application for viewing cam streams
diff --git a/amfwriter.c b/amfwriter.c
index 76d06d2..3fc07dc 100644
--- a/amfwriter.c
+++ b/amfwriter.c
@@ -22,10 +22,10 @@
 #include "rtmp.h"
 #include "amfwriter.h"
 
-void amfinit(struct rtmp* msg, unsigned int streamid)
+void amfinit(struct rtmp* msg, unsigned int chunkid)
 {
   msg->type=RTMP_AMF0;
-  msg->streamid=streamid;
+  msg->chunkid=chunkid;
   msg->length=0;
   msg->msgid=0;
   msg->buf=0;
diff --git a/client.c b/client.c
index f2e8bcc..bb2c397 100644
--- a/client.c
+++ b/client.c
@@ -27,13 +27,13 @@
 #include <sys/socket.h>
 #include <locale.h>
 #include <ctype.h>
+#include <termios.h>
 #include <curl/curl.h>
-#include "rtmp.h"
-#include "amfparser.h"
 #include "numlist.h"
-#include "amfwriter.h"
 #include "idlist.h"
 #include "colors.h"
+#include "media.h"
+#include "amfwriter.h"
 
 struct writebuf
 {
@@ -189,7 +189,7 @@ char* getprivfield(char* nick)
   unsigned int id;
   unsigned int privlen;
   for(privlen=0; nick[privlen]&&nick[privlen]!=' '; ++privlen);
-  id=idlist_get((char*)nick);
+  id=idlist_get(nick);
   if(id<0)
   {
     nick[privlen]=0;
@@ -251,10 +251,17 @@ int main(int argc, char** argv)
   setlocale(LC_ALL, "");
   if(account_user && !account_pass) // Only username given, prompt for password
   {
-    fprintf(stderr, "Account password: ");
-    fflush(stderr);
+    struct termios term;
+    tcgetattr(0, &term);
+    term.c_lflag&=~ECHO;
+    tcsetattr(0, TCSANOW, &term);
+    fprintf(stdout, "Account password: ");
+    fflush(stdout);
     account_pass=malloc(128);
     fgets(account_pass, 128, stdin);
+    term.c_lflag|=ECHO;
+    tcsetattr(0, TCSANOW, &term);
+    printf("\n");
     unsigned int i;
     for(i=0; account_pass[i]; ++i)
     {
@@ -349,6 +356,7 @@ int main(int argc, char** argv)
   amfsend(&amf, sock);
   free(modkey);
 
+  char* unban=0;
   struct pollfd pfd[2];
   pfd[0].fd=0;
   pfd[0].events=POLLIN;
@@ -363,7 +371,7 @@ int main(int argc, char** argv)
     if(pfd[0].revents) // Got input, send a privmsg command
     {
       pfd[0].revents=0;
-      unsigned char buf[2048];
+      char buf[2048];
       unsigned int len=0;
       int r;
       while(len<2047)
@@ -379,7 +387,7 @@ int main(int argc, char** argv)
       char* privfield=0;
       if(buf[0]=='/') // Got a command
       {
-        if(!strcmp((char*)buf, "/help"))
+        if(!strcmp(buf, "/help"))
         {
           printf("/help           = print this help text\n"
                  "/color <0-15>   = pick color of your messages\n"
@@ -387,16 +395,21 @@ int main(int argc, char** argv)
                  "/color          = see your current color\n"
                  "/colors         = list the available colors and their numbers\n"
                  "/nick <newnick> = changes your nickname\n"
-                 "/msg <to> <msg> = send a private message\n");
+                 "/msg <to> <msg> = send a private message\n"
+                 "/opencam <nick> = see someone's cam/mic (Warning: writes binary data to stdout)\n"
+                 "/close <nick>   = close someone's cam/mic stream (as a mod)\n"
+                 "/ban <nick>     = ban someone\n"
+                 "/banlist        = list who is banned\n"
+                 "/forgive <nick/ID> = unban someone\n");
           fflush(stdout);
         }
-        else if(!strncmp((char*)buf, "/color", 6) && (!buf[6]||buf[6]==' '))
+        else if(!strncmp(buf, "/color", 6) && (!buf[6]||buf[6]==' '))
         {
           if(buf[6]) // Color specified
           {
-            if(!strcmp((char*)&buf[7], "off")){showcolor=0; continue;}
-            if(!strcmp((char*)&buf[7], "on")){showcolor=1; continue;}
-            currentcolor=atoi((char*)&buf[7]);
+            if(!strcmp(&buf[7], "off")){showcolor=0; continue;}
+            if(!strcmp(&buf[7], "on")){showcolor=1; continue;}
+            currentcolor=atoi(&buf[7]);
             printf("\x1b[%smChanged color\x1b[0m\n", termcolors[currentcolor%16]);
           }else{ // No color specified, state our current color
             printf("\x1b[%smCurrent color: %i\x1b[0m\n", termcolors[currentcolor%16], currentcolor%16);
@@ -404,7 +417,7 @@ int main(int argc, char** argv)
           fflush(stdout);
           continue;
         }
-        else if(!strcmp((char*)buf, "/colors"))
+        else if(!strcmp(buf, "/colors"))
         {
           int i;
           for(i=0; i<16; ++i)
@@ -414,9 +427,9 @@ int main(int argc, char** argv)
           fflush(stdout);
           continue;
         }
-        else if(!strncmp((char*)buf, "/nick ", 6))
+        else if(!strncmp(buf, "/nick ", 6))
         {
-          if((badchar=checknick((char*)&buf[6])))
+          if((badchar=checknick(&buf[6])))
           {
             printf("'%c' is not allowed in nicknames.\n", badchar);
             continue;
@@ -425,40 +438,80 @@ int main(int argc, char** argv)
           amfstring(&amf, "nick");
           amfnum(&amf, 0);
           amfnull(&amf);
-          amfstring(&amf, (char*)&buf[6]);
+          amfstring(&amf, &buf[6]);
           amfsend(&amf, sock);
           continue;
         }
-        else if(!strncmp((char*)buf, "/msg ", 5))
+        else if(!strncmp(buf, "/msg ", 5))
         {
-          privfield=getprivfield((char*)&buf[5]);
+          privfield=getprivfield(&buf[5]);
           if(!privfield){continue;}
         }
-        else if(!strncmp((char*)buf, "/priv ", 6))
+        else if(!strncmp(buf, "/priv ", 6))
         {
-          char* end=strchr((char*)&buf[6], ' ');
+          char* end=strchr(&buf[6], ' ');
           if(!end){continue;}
-          privfield=getprivfield((char*)&buf[6]);
+          privfield=getprivfield(&buf[6]);
           if(!privfield){continue;}
           len=strlen(&end[1]);
           memmove(buf, &end[1], len+1);
         }
-/* While we can get the server to send us video data, we don't know how to handle the data yet.
-        else if(!strncmp((char*)buf, "/cam ", 5))
+        else if(!strncmp(buf, "/opencam ", 9))
+        {
+          stream_start(&buf[9], sock);
+          continue;
+        }
+        else if(!strncmp(buf, "/close ", 7)) // Stop someone's cam/mic broadcast
+        {
+          char nick[strlen(&buf[7])+1];
+          strcpy(nick, &buf[7]);
+          amfinit(&amf, 2);
+          amfstring(&amf, "owner_run");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          sprintf(buf, "_close%s", nick);
+          amfstring(&amf, buf);
+          amfsend(&amf, sock);
+          len=sprintf(buf, "closed: %s", nick);
+        }
+        else if(!strncmp(buf, "/ban ", 5)) // Ban someone
+        {
+          char nick[strlen(&buf[5])+1];
+          strcpy(nick, &buf[5]);
+          amfinit(&amf, 3);
+          amfstring(&amf, "owner_run");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          sprintf(buf, "notice%s%%20was%%20banned%%20by%%20%s%%20(%s)", nick, nickname, account_user);
+          amfstring(&amf, buf);
+          amfsend(&amf, sock);
+          // kick (this does the actual banning)
+          amfinit(&amf, 3);
+          amfstring(&amf, "kick");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, nick);
+          sprintf(buf, "%i", idlist_get(nick));
+          amfstring(&amf, buf);
+          amfsend(&amf, sock);
+          continue;
+        }
+        else if(!strcmp(buf, "/banlist") || !strncmp(buf, "/forgive ", 9))
         {
-          unsigned int id=idlist_get((char*)&buf[5]);
-          camid=malloc(128);
-          sprintf(camid, "%u", id);
+          if(buf[1]=='f') // forgive
+          {
+            free(unban);
+            unban=strdup(&buf[9]);
+          }
           amfinit(&amf, 3);
-          amfstring(&amf, "createStream");
-          amfnum(&amf, 2);
+          amfstring(&amf, "banlist");
+          amfnum(&amf, 0);
           amfnull(&amf);
           amfsend(&amf, sock);
           continue;
         }
-*/
       }
-      char* msg=tonumlist((char*)buf, len);
+      char* msg=tonumlist(buf, len);
       amfinit(&amf, 3);
       amfstring(&amf, "privmsg");
       amfnum(&amf, 0);
@@ -478,205 +531,252 @@ int main(int argc, char** argv)
     // Got data from server
     pfd[1].revents=0;
     // Read the RTMP stream and handle AMF0 packets
-    if(rtmp_get(sock, &rtmp))
+    char rtmpres=rtmp_get(sock, &rtmp);
+    if(!rtmpres){printf("Server disconnected\n"); break;}
+    if(rtmpres==2){continue;} // Not disconnected, but we didn't get a complete chunk yet either
+    if(rtmp.type==RTMP_VIDEO){stream_handledata(&rtmp); continue;}
+    if(rtmp.type!=RTMP_AMF0){printf("Got RTMP type 0x%x\n", rtmp.type); continue;}
+    struct amf* amfin=amf_parse(rtmp.buf, rtmp.length);
+    if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING)
     {
-/* Getting video/cam data, but we don't know how to make use of it yet
-      if(rtmp.type==RTMP_VIDEO)
-      {
-        write(vidf, rtmp.buf, rtmp.length);
-        continue;
-      }
-*/
-      if(rtmp.type!=RTMP_AMF0){printf("Got RTMP type 0x%x\n", rtmp.type); continue;}
-      struct amf* amfin=amf_parse(rtmp.buf, rtmp.length);
-      if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING)
-      {
-//        printf("Got command: '%s'\n", amfin->items[0].string.string);
-        if(!strcmp(amfin->items[0].string.string, "_error"))
-          printamf(amfin);
-      }
-      if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "registered") && amfin->items[amfin->itemcount-1].type==AMF_STRING)
-      {
-        char* id=amfin->items[amfin->itemcount-1].string.string;
-        printf("Guest ID: %s\n", id);
-        char* key=getkey(id, channel);
+//      printf("Got command: '%s'\n", amfin->items[0].string.string);
+      if(!strcmp(amfin->items[0].string.string, "_error"))
+        printamf(amfin);
+    }
+    if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "registered") && amfin->items[amfin->itemcount-1].type==AMF_STRING)
+    {
+      char* id=amfin->items[amfin->itemcount-1].string.string;
+      printf("Connection ID: %s\n", id);
+      char* key=getkey(id, channel);
 
-        amfinit(&amf, 3);
-        amfstring(&amf, "cauth");
-        amfnum(&amf, 0);
-        amfnull(&amf); // Means nothing but is apparently critically important for cauth at least
-        amfstring(&amf, key);
-        amfsend(&amf, sock);
-        free(key);
+      amfinit(&amf, 3);
+      amfstring(&amf, "cauth");
+      amfnum(&amf, 0);
+      amfnull(&amf); // Means nothing but is apparently critically important for cauth at least
+      amfstring(&amf, key);
+      amfsend(&amf, sock);
+      free(key);
 
+      amfinit(&amf, 3);
+      amfstring(&amf, "nick");
+      amfnum(&amf, 0);
+      amfnull(&amf);
+      amfstring(&amf, nickname);
+      amfsend(&amf, sock);
+    }
+    // Items for privmsg: 0=cmd, 2=channel, 3=msg, 4=color/lang, 5=nick
+    else if(amfin->itemcount>5 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "privmsg") && amfin->items[3].type==AMF_STRING && amfin->items[4].type==AMF_STRING && amfin->items[5].type==AMF_STRING)
+    {
+      size_t len;
+      char* msg=fromnumlist(amfin->items[3].string.string, &len);
+      const char* color=(showcolor?resolvecolor(amfin->items[4].string.string):"0");
+      printf("%s \x1b[%sm%s: ", timestamp(), color, amfin->items[5].string.string);
+      fwrite(msg, len, 1, stdout);
+      printf("\x1b[0m\n");
+      if(len==18 && !strncmp(msg, "/userinfo $request", 18))
+      {
+        char* msg;
+        if(account_user)
+        {
+          unsigned int len=strlen("/userinfo \n0")+strlen(account_user);
+          char buf[len+1];
+          sprintf(buf, "/userinfo %s\n", account_user);
+          msg=tonumlist(buf, len);
+        }else{
+          msg=tonumlist("/userinfo tc_client\n", 20); // TODO: include version number?
+        }
         amfinit(&amf, 3);
-        amfstring(&amf, "nick");
+        amfstring(&amf, "privmsg");
         amfnum(&amf, 0);
         amfnull(&amf);
-        amfstring(&amf, nickname);
+        amfstring(&amf, msg);
+        amfstring(&amf, "#0,en");
+        int id=idlist_get(amfin->items[5].string.string);
+        char priv[snprintf(0, 0, "n%i-%s", id, amfin->items[5].string.string)+1];
+        sprintf(priv, "n%i-%s", id, amfin->items[5].string.string);
+        amfstring(&amf, priv);
         amfsend(&amf, sock);
-      }
-      // Items for privmsg: 0=cmd, 2=channel, 3=msg, 4=color/lang, 5=nick
-      else if(amfin->itemcount>5 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "privmsg") && amfin->items[3].type==AMF_STRING && amfin->items[4].type==AMF_STRING && amfin->items[5].type==AMF_STRING)
-      {
-        size_t len;
-        char* msg=fromnumlist(amfin->items[3].string.string, &len);
-        const char* color=(showcolor?resolvecolor(amfin->items[4].string.string):"0");
-        printf("%s \x1b[%sm%s: ", timestamp(), color, amfin->items[5].string.string);
-        fwrite(msg, len, 1, stdout);
-        printf("\x1b[0m\n");
-        if(len==18 && !strncmp(msg, "/userinfo $request", 18))
-        {
-          char* msg;
-          if(account_user)
-          {
-            unsigned int len=strlen("/userinfo \n0")+strlen(account_user);
-            char buf[len+1];
-            sprintf(buf, "/userinfo %s\n", account_user);
-            msg=tonumlist(buf, len);
-          }else{
-            msg=tonumlist("/userinfo tc_client\n", 20); // TODO: include version number?
-          }
-          amfinit(&amf, 3);
-          amfstring(&amf, "privmsg");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, msg);
-          amfstring(&amf, "#0,en");
-          int id=idlist_get(amfin->items[5].string.string);
-          char priv[snprintf(0, 0, "n%i-%s", id, amfin->items[5].string.string)+1];
-          sprintf(priv, "n%i-%s", id, amfin->items[5].string.string);
-          amfstring(&amf, priv);
-          amfsend(&amf, sock);
-          free(msg);
-        }
         free(msg);
-        fflush(stdout);
       }
-      // users on channel entry.  there's also a "joinsdone" command for some reason...
-      else if(amfin->itemcount>3 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "joins"))
+      free(msg);
+      fflush(stdout);
+    }
+    // users on channel entry.  there's also a "joinsdone" command for some reason...
+    else if(amfin->itemcount>3 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "joins"))
+    {
+      printf("Currently online: ");
+      int i;
+      for(i = 3; i < amfin->itemcount-1; i+=2)
       {
-        printf("Currently online: ");
-        int i;
-        for(i = 3; i < amfin->itemcount-1; i+=2)
+        // a "numeric" id precedes each nick, i.e. i is the id, i+1 is the nick
+        if(amfin->items[i].type==AMF_STRING && amfin->items[i+1].type==AMF_STRING)
         {
-          // a "numeric" id precedes each nick, i.e. i is the id, i+1 is the nick
-          if(amfin->items[i].type==AMF_STRING && amfin->items[i+1].type==AMF_STRING)
-          {
-            idlist_add(atoi(amfin->items[i].string.string), amfin->items[i+1].string.string);
-            printf("%s%s", (i==3?"":", "), amfin->items[i+1].string.string);
-          }
+          idlist_add(atoi(amfin->items[i].string.string), amfin->items[i+1].string.string);
+          printf("%s%s", (i==3?"":", "), amfin->items[i+1].string.string);
         }
-        printf("\n");
-        fflush(stdout);
       }
-      // join ("join", 0, "<ID>", "guest-<ID>")
-      else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "join") && amfin->items[2].type==AMF_STRING && amfin->items[3].type==AMF_STRING)
-      {
-        idlist_add(atoi(amfin->items[2].string.string), amfin->items[3].string.string);
-        printf("%s %s entered the channel\n", timestamp(), amfin->items[3].string.string);
-        fflush(stdout);
-      }
-      // part
-      else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "quit") && amfin->items[3].type==AMF_STRING)
+      printf("\n");
+      fflush(stdout);
+    }
+    // join ("join", 0, "<ID>", "guest-<ID>")
+    else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "join") && amfin->items[2].type==AMF_STRING && amfin->items[3].type==AMF_STRING)
+    {
+      idlist_add(atoi(amfin->items[2].string.string), amfin->items[3].string.string);
+      printf("%s %s entered the channel\n", timestamp(), amfin->items[3].string.string);
+      fflush(stdout);
+    }
+    // quit/part
+    else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "quit") && amfin->items[2].type==AMF_STRING)
+    {
+      idlist_remove(amfin->items[2].string.string);
+      printf("%s %s left the channel\n", timestamp(), amfin->items[2].string.string);
+      fflush(stdout);
+    }
+    // nick
+    else if(amfin->itemcount==5 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "nick") && amfin->items[2].type==AMF_STRING && amfin->items[3].type==AMF_STRING)
+    {
+      if(!strcmp(amfin->items[2].string.string, nickname)) // Successfully changed our own nickname
       {
-        idlist_remove(amfin->items[2].string.string);
-        printf("%s %s left the channel\n", timestamp(), amfin->items[2].string.string);
-        fflush(stdout);
+        free(nickname);
+        nickname=strdup(amfin->items[3].string.string);
       }
-      // nick
-      else if(amfin->itemcount==5 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "nick") && amfin->items[2].type==AMF_STRING && amfin->items[3].type==AMF_STRING)
+      idlist_rename(amfin->items[2].string.string, amfin->items[3].string.string);
+      printf("%s %s changed nickname to %s\n", timestamp(), amfin->items[2].string.string, amfin->items[3].string.string);
+      fflush(stdout);
+    }
+    // kick
+    else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "kick") && amfin->items[2].type==AMF_STRING)
+    {
+      if(atoi(amfin->items[2].string.string) == idlist_get(nickname))
       {
-        if(!strcmp(amfin->items[2].string.string, nickname)) // Successfully changed our own nickname
-        {
-          free(nickname);
-          nickname=strdup(amfin->items[3].string.string);
-        }
-        idlist_rename(amfin->items[2].string.string, amfin->items[3].string.string);
-        printf("%s %s changed nickname to %s\n", timestamp(), amfin->items[2].string.string, amfin->items[3].string.string);
+        printf("%s You have been kicked out\n", timestamp());
         fflush(stdout);
       }
-      // kick
-      else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "kick") && amfin->items[2].type==AMF_STRING)
+    }
+    // banned
+    else if(amfin->itemcount==2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "banned"))
+    {
+      printf("%s You are banned from %s\n", timestamp(), channel);
+      fflush(stdout);
+      // When banned and reconnecting, tinychat doesn't disconnect us itself, we need to disconnect
+      close(sock);
+      return 1; // Getting banned is a failure, right?
+    }
+    // from_owner: notices
+    else if(amfin->itemcount==3 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "from_owner") && amfin->items[2].type==AMF_STRING)
+    {
+      if(!strncmp("notice", amfin->items[2].string.string, 6))
       {
-        if(atoi(amfin->items[2].string.string) == idlist_get(nickname))
+        char* notice=strdup(&amfin->items[2].string.string[6]);
+        // replace "%20" with spaces
+        char* space;
+        while((space=strstr(notice, "%20")))
         {
-          printf("%s You have been kicked out\n", timestamp());
-          fflush(stdout);
+          memmove(space, &space[2], strlen(&space[2])+1);
+          space[0]=' ';
         }
-      }
-      // banned
-      else if(amfin->itemcount==2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "banned"))
-      {
-        printf("%s You are banned from %s\n", timestamp(), channel);
+        printf("%s %s\n", timestamp(), notice);
         fflush(stdout);
-        // When banned and reconnecting, tinychat doesn't disconnect us itself, we need to disconnect
-        close(sock);
-        return 1; // Getting banned is a failure, right?
       }
-      // from_owner: notices
-      else if(amfin->itemcount==3 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "from_owner") && amfin->items[2].type==AMF_STRING)
+    }
+    // oper, identifies mods
+    else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "oper") && amfin->items[3].type==AMF_STRING)
+    {
+      idlist_set_op(amfin->items[3].string.string, 1);
+      printf("%s is a moderator.\n", amfin->items[3].string.string);
+      fflush(stdout);
+    }
+    // deop, removes moderator privilege
+    else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "deop") && amfin->items[3].type==AMF_STRING)
+    {
+      idlist_set_op(amfin->items[3].string.string, 0);
+      printf("%s is no longer a moderator.\n", amfin->items[3].string.string);
+      fflush(stdout);
+    }
+    // nickinuse, the nick we wanted to change to is already taken
+    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING &&  amf_comparestrings_c(&amfin->items[0].string, "nickinuse"))
+    {
+      printf("Nick is already in use.\n");
+      fflush(stdout);
+    }
+    // Room topic
+    else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "topic") && amfin->items[2].type==AMF_STRING && strlen(amfin->items[2].string.string) > 0)
+    {
+      printf("Room topic: %s\n", amfin->items[2].string.string);
+      fflush(stdout);
+    }
+    // Get list of banned users
+    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING &&  amf_comparestrings_c(&amfin->items[0].string, "banlist"))
+    {
+      unsigned int i;
+      if(unban) // This is not a response to /banlist but to /forgive
       {
-        if(!strncmp("notice", amfin->items[2].string.string, 6))
+        for(i=2; i+1<amfin->itemcount; i+=2)
         {
-          char* notice=strdup(&amfin->items[2].string.string[6]);
-          // replace "%20" with spaces
-          char* space;
-          while((space=strstr(notice, "%20")))
+          if(amfin->items[i].type!=AMF_STRING || amfin->items[i+1].type!=AMF_STRING){break;}
+          if(!strcmp(amfin->items[i+1].string.string, unban))
           {
-            memmove(space, &space[2], strlen(&space[2])+1);
-            space[0]=' ';
+            free(unban);
+            // A little unnecessary allocation, but the code gets cleaner without leaking
+            unban=strdup(amfin->items[i].string.string);
+            break;
           }
-          printf("%s %s\n", timestamp(), notice);
-          fflush(stdout);
+          // If the nickname isn't found in the banlist we assume it's an ID
         }
+        amfinit(&amf, 3);
+        amfstring(&amf, "forgive");
+        amfnum(&amf, 0);
+        amfnull(&amf);
+        amfstring(&amf, unban);
+        amfsend(&amf, sock);
+        free(unban);
+        unban=0;
+        continue;
       }
-      // oper, identifies mods
-      else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "oper") && amfin->items[3].type==AMF_STRING)
-      {
-        idlist_set_op(amfin->items[3].string.string, 1);
-        printf("%s is a moderator.\n", amfin->items[3].string.string);
-        fflush(stdout);
-      }
-      // deop, removes moderator privilege
-      else if(amfin->itemcount==4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "deop") && amfin->items[3].type==AMF_STRING)
+      printf("Banned users:\n");
+      printf("ID         Nickname\n");
+      for(i=2; i+1<amfin->itemcount; i+=2)
       {
-        idlist_set_op(amfin->items[3].string.string, 0);
-        printf("%s is no longer a moderator.\n", amfin->items[3].string.string);
-        fflush(stdout);
+        if(amfin->items[i].type!=AMF_STRING || amfin->items[i+1].type!=AMF_STRING){break;}
+        unsigned int len=printf("%s", amfin->items[i].string.string);
+        for(;len<10; ++len){printf(" ");}
+        printf(" %s\n", amfin->items[i+1].string.string);
       }
-      // nickinuse, the nick we wanted to change to is already taken
-      else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING &&  amf_comparestrings_c(&amfin->items[0].string, "nickinuse"))
+      printf("Use /forgive <ID> to unban someone\n");
+      fflush(stdout);
+    }
+    // "avons", 0, "ID1", "nick1", "IDn", "nickn"...
+    else if(amfin->itemcount>1 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "avons"))
+    {
+      printf("Currently on cam: ");
+      int i;
+      // a "numeric" id precedes each nick, so we start on 3 and increment by 2
+      for(i = 3; i < amfin->itemcount; i+=2)
       {
-        printf("Nick is already in use.\n");
-        fflush(stdout);
+        if(amfin->items[i].type==AMF_STRING)
+        {
+          printf("%s%s", (i==3?"":", "), amfin->items[i].string.string);
+        }
       }
-      // Room topic
-      else if(amfin->itemcount>2 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "topic") && amfin->items[2].type==AMF_STRING && strlen(amfin->items[2].string.string) > 0)
+      printf("\n");
+      fflush(stdout);
+    }
+    else if(amfin->itemcount>4 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "notice") && amfin->items[2].type==AMF_STRING && amf_comparestrings_c(&amfin->items[2].string, "avon"))
+    {
+      if(amfin->items[4].type==AMF_STRING)
       {
-        printf("Room topic: %s\n", amfin->items[2].string.string);
+        printf("%s cammed up\n", amfin->items[4].string.string);
         fflush(stdout);
       }
-/* More not yet usable cam code, response to trying to open a new stream
-      else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "_result"))
-      {
-        printamf(amfin);
-        if(camid)
-        {
-          amfinit(&amf, 8);
-          amfstring(&amf, "play");
-          amfnum(&amf, 0);
-          amfnull(&amf);
-          amfstring(&amf, camid);
-          amf.msgid=le32(1);
-          amfsend(&amf, sock);
-        }
-      }
-*/
-      // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
-      amf_free(amfin);
     }
-    else{printf("Server disconnected\n"); break;}
+    // Handle results for various requests, haven't seen much of a pattern to it, always successful?
+    else if(amfin->itemcount>0 && amfin->items[0].type==AMF_STRING && amf_comparestrings_c(&amfin->items[0].string, "_result"))
+    {
+      // Creating a new stream worked, now play media (cam/mic) on it (if that's what the result was for)
+      stream_play(amfin, sock);
+    }
+    // else{printf("Unknown command...\n"); printamf(amfin);} // (Debugging)
+    amf_free(amfin);
   }
   free(rtmp.buf);
   close(sock);
diff --git a/configure b/configure
index d45512a..517c8ec 100755
--- a/configure
+++ b/configure
@@ -38,4 +38,44 @@ else
 fi
 rm -f iconvtest iconvtest.c
 
+printf 'Checking for gtk+-3.0... '
+libs="`pkg-config --libs gtk+-3.0`"
+if [ "x$libs" != "x" ]; then
+  echo "GTK_LIBS=${libs}" >> config.mk
+  echo "GTK_CFLAGS=`pkg-config --cflags gtk+-3.0`" >> config.mk
+  echo yes
+else
+  echo no
+fi
+
+printf 'Checking for libavcodec... '
+libs="`pkg-config --libs libavcodec`"
+if [ "x$libs" != "x" ]; then
+  echo "AVCODEC_LIBS=${libs}" >> config.mk
+  echo "AVCODEC_CFLAGS=`pkg-config --cflags libavcodec`" >> config.mk
+  echo yes
+else
+  echo no
+fi
+
+printf 'Checking for libswscale... '
+libs="`pkg-config --libs libswscale`"
+if [ "x$libs" != "x" ]; then
+  echo "SWSCALE_LIBS=${libs}" >> config.mk
+  echo "SWSCALE_CFLAGS=`pkg-config --cflags libswscale`" >> config.mk
+  echo yes
+else
+  echo no
+fi
+
+printf 'Checking for libavutil... '
+libs="`pkg-config --libs libavutil`"
+if [ "x$libs" != "x" ]; then
+  echo "AVUTIL_LIBS=${libs}" >> config.mk
+  echo "AVUTIL_CFLAGS=`pkg-config --cflags libavutil`" >> config.mk
+  echo yes
+else
+  echo no
+fi
+
 echo Done
diff --git a/media.c b/media.c
index b224911..71d54e9 100644
--- a/media.c
+++ b/media.c
@@ -16,11 +16,19 @@
 */
 #include <stdio.h>
 #include <stdlib.h>
-#include <string.h>
+//#include "rtmp.h"
+//#include "amfparser.h"
 #include "endian.h"
 #include "media.h"
 #include "amfwriter.h"
 #include "idlist.h"
+/*
+struct stream
+{
+  unsigned int streamid;
+  unsigned int userid;
+};
+*/
 
 struct stream* streams=0;
 unsigned int streamcount=0;
@@ -35,16 +43,20 @@ char stream_idtaken(unsigned int id)
   return 0;
 }
 
-void stream_start(const char* nick, int sock) // called upon privmsg "/opencam ..."
+void stream_start(const char* nick, int sock) // called upon privmsg "/cam ..."
 {
   unsigned int userid=idlist_get(nick);
+/*
+  unsigned int id=idlist_get((char*)&buf[5]);
+  camid=malloc(128);
+  sprintf(camid, "%u", id);
+*/
   unsigned int streamid=1;
   while(stream_idtaken(streamid)){++streamid;}
   ++streamcount;
   streams=realloc(streams, sizeof(struct stream)*streamcount);
   streams[streamcount-1].userid=userid;
   streams[streamcount-1].streamid=streamid;
-  streams[streamcount-1].outgoing=0;
   struct rtmp amf;
   amfinit(&amf, 3);
   amfstring(&amf, "createStream");
@@ -52,26 +64,6 @@ void stream_start(const char* nick, int sock) // called upon privmsg "/opencam .
   amfnull(&amf);
   amfsend(&amf, sock);
   printf("Starting media stream for %s (%u)\n", nick, userid);
-  fflush(stdout);
-}
-
-void streamout_start(unsigned int id, int sock) // called upon privmsg "/camup"
-{
-  unsigned int streamid=1;
-  while(stream_idtaken(streamid)){++streamid;}
-  ++streamcount;
-  streams=realloc(streams, sizeof(struct stream)*streamcount);
-  streams[streamcount-1].userid=id;
-  streams[streamcount-1].streamid=streamid;
-  streams[streamcount-1].outgoing=1;
-  struct rtmp amf;
-  amfinit(&amf, 3);
-  amfstring(&amf, "createStream");
-  amfnum(&amf, streamid+1);
-  amfnull(&amf);
-  amfsend(&amf, sock);
-  printf("Starting outgoing media stream\n");
-  fflush(stdout);
 }
 
 void stream_play(struct amf* amf, int sock) // called upon _result
@@ -83,13 +75,12 @@ void stream_play(struct amf* amf, int sock) // called upon _result
     {
       struct rtmp amf;
       amfinit(&amf, 8);
-      amfstring(&amf, streams[i].outgoing?"publish":"play");
+      amfstring(&amf, "play");
       amfnum(&amf, 0);
       amfnull(&amf);
       char camid[snprintf(0,0,"%u0", streams[i].userid)];
       sprintf(camid, "%u", streams[i].userid);
       amfstring(&amf, camid);
-      if(streams[i].outgoing){amfstring(&amf, "live");}
       amf.msgid=le32(streams[i].streamid);
       amfsend(&amf, sock);
       return;
@@ -99,103 +90,16 @@ void stream_play(struct amf* amf, int sock) // called upon _result
 
 void stream_handledata(struct rtmp* rtmp)
 {
+rtmp->msgid=1;
   unsigned int i;
   for(i=0; i<streamcount; ++i)
   {
     if(streams[i].streamid!=rtmp->msgid){continue;}
-// fprintf(stderr, "Chunk: chunkid: %u, streamid: %u, userid: %u\n", rtmp->chunkid, rtmp->msgid, streams[i].userid);
-    if(rtmp->type==RTMP_VIDEO)
-    {
-      printf("Video: %u %u\n", streams[i].userid, rtmp->length);
-    }else if(rtmp->type==RTMP_AUDIO){
-      printf("Audio: %u %u\n", streams[i].userid, rtmp->length);
-    }
+    printf("Video: %u %u\n", streams[i].userid, rtmp->length); // TODO: if this becomes permanent we will have to specify a nick or ID or something
+//  write(1, rtmp.buf, rtmp.length);
     fwrite(rtmp->buf, rtmp->length, 1, stdout);
     fflush(stdout);
     return;
   }
   printf("Received media data to unknown stream ID %u\n", rtmp->msgid);
 }
-
-void stream_handlestatus(struct amf* amf, int sock)
-{
-  if(amf->itemcount<3 || amf->items[2].type!=AMF_OBJECT){return;}
-  struct amfobject* obj=&amf->items[2].object;
-  struct amfitem* code=amf_getobjmember(obj, "code");
-  struct amfitem* details=amf_getobjmember(obj, "details");
-  if(!code || !details){return;}
-  if(code->type!=AMF_STRING || details->type!=AMF_STRING){return;}
-  if(!strcmp(code->string.string, "NetStream.Play.Stop"))
-  {
-    unsigned int id=strtoul(details->string.string, 0, 0);
-    unsigned int i;
-    for(i=0; i<streamcount; ++i)
-    {
-      if(streams[i].userid==id)
-      {
-        printf("VideoEnd: %u\n", streams[i].userid);
-        // Delete the stream
-        struct rtmp amf;
-        amfinit(&amf, 3);
-        amfstring(&amf, "deleteStream");
-        amfnum(&amf, 0);
-        amfnull(&amf);
-        amfnum(&amf, streams[i].streamid);
-        amfsend(&amf, sock);
-        // Remove from list of streams
-        --streamcount;
-        memmove(&streams[i], &streams[i+1], sizeof(struct stream)*(streamcount-i));
-        return;
-      }
-    }
-  }
-}
-
-void stream_sendvideo(int sock, void* buf, size_t len)
-{
-  unsigned int i;
-  for(i=0; i<streamcount; ++i)
-  {
-    if(streams[i].outgoing)
-    {
-      struct rtmp msg;
-      msg.type=RTMP_VIDEO;
-      msg.chunkid=6;
-      msg.length=len;
-      msg.msgid=streams[i].streamid;
-      msg.buf=buf;
-      rtmp_send(sock, &msg);
-      return;
-    }
-  }
-}
-
-void stream_stopvideo(int sock)
-{
-  unsigned int i;
-  for(i=0; i<streamcount; ++i)
-  {
-    if(streams[i].outgoing)
-    {
-      struct rtmp amf;
-      // Close the stream
-      amfinit(&amf, 8);
-      amfstring(&amf, "closeStream");
-      amfnum(&amf, 0);
-      amfnull(&amf);
-      amf.msgid=le32(streams[i].streamid);
-      amfsend(&amf, sock);
-      // Delete the stream
-      amfinit(&amf, 3);
-      amfstring(&amf, "deleteStream");
-      amfnum(&amf, 0);
-      amfnull(&amf);
-      amfnum(&amf, streams[i].streamid);
-      amfsend(&amf, sock);
-      // Remove from list of streams
-      --streamcount;
-      memmove(&streams[i], &streams[i+1], sizeof(struct stream)*(streamcount-i));
-      return;
-    }
-  }
-}
diff --git a/media.h b/media.h
index 25195ab..58850d4 100644
--- a/media.h
+++ b/media.h
@@ -20,16 +20,11 @@ struct stream
 {
   unsigned int streamid;
   unsigned int userid;
-  char outgoing;
 };
 
 extern struct stream* streams;
 extern unsigned int streamcount;
 
-extern void stream_start(const char* nick, int sock); // called upon privmsg "/opencam ..."
-extern void streamout_start(unsigned int id, int sock); // called upon privmsg "/camup"
+extern void stream_start(const char* nick, int sock); // called upon privmsg "/cam ..."
 extern void stream_play(struct amf* amf, int sock); // called upon _result
 extern void stream_handledata(struct rtmp* rtmp);
-extern void stream_handlestatus(struct amf* amf, int sock);
-extern void stream_sendvideo(int sock, void* buf, size_t len);
-extern void stream_stopvideo(int sock);
diff --git a/rtmp.c b/rtmp.c
index a1f5ac8..3c5c82b 100644
--- a/rtmp.c
+++ b/rtmp.c
@@ -21,84 +21,123 @@
 #include "endian.h"
 #include "rtmp.h"
 
+struct chunk
+{
+  unsigned int id;
+  unsigned int length;
+  unsigned char type;
+  unsigned int timestamp;
+  unsigned int streamid;
+  unsigned int pos;
+  void* buf;
+};
+struct chunk* chunks=0;
+unsigned int chunkcount=0;
+
+struct chunk* chunk_get(unsigned int id)
+{
+  unsigned int i;
+  for(i=0; i<chunkcount; ++i)
+  {
+    if(chunks[i].id==id){return &chunks[i];}
+  }
+// printf("No chunk found for %u, creating new one\n", id);
+  ++chunkcount;
+  chunks=realloc(chunks, sizeof(struct chunk)*chunkcount);
+  chunks[i].id=id;
+  chunks[i].streamid=0;
+  chunks[i].buf=0;
+  chunks[i].timestamp=0;
+  chunks[i].length=0;
+  return &chunks[i];
+}
+
 char rtmp_get(int sock, struct rtmp* rtmp)
 {
-  static unsigned int length;
-  static unsigned char type;
-  static unsigned int timestamp;
-  // Header format and stream ID
+  // Header format and chunk ID
   unsigned int x=0;
   if(read(sock, &x, 1)<1){return 0;}
-  unsigned int streamid=x&0x3f;
+  unsigned int chunkid=x&0x3f;
   unsigned int fmt=(x&0xc0)>>6;
-  unsigned int msgid=0;
+  struct chunk* chunk=chunk_get(chunkid);
   // Handle extended stream IDs
-  if(streamid<2) // (0=1 extra byte, 1=2 extra bytes)
+  if(chunkid<2) // (0=1 extra byte, 1=2 extra bytes)
   {
-    read(sock, &x, streamid+1);
-    streamid=64+x;
+    read(sock, &x, chunkid+1);
+    chunkid=64+x;
   }
   if(fmt<3)
   {
     // Timestamp
     read(sock, &x, 3);
-    timestamp=x;
+    chunk->timestamp=x;
     if(fmt<2)
     {
       // Length
       x=0;
       read(sock, ((void*)&x)+1, 3);
-      length=be32(x);
+      chunk->length=be32(x);
       // Type
-      read(sock, &type, sizeof(type));
+      read(sock, &chunk->type, sizeof(chunk->type));
       if(fmt<1)
       {
         // Message ID
-        read(sock, &msgid, sizeof(msgid));
+        read(sock, &chunk->streamid, sizeof(chunk->streamid));
       }
     }
   }
   // Extended timestamp
-  if(timestamp==0xffffff)
+  if(chunk->timestamp==0xffffff)
   {
     read(sock, &x, sizeof(x));
-    timestamp=be32(x);
+    chunk->timestamp=be32(x);
   }
 
-  rtmp->type=type;
-  rtmp->streamid=streamid;
-  rtmp->length=length;
-  rtmp->msgid=le32(msgid);
-  free(rtmp->buf);
-  rtmp->buf=malloc(rtmp->length);
-  size_t pos=0;
-  size_t w;
-  // Only read up to 128 bytes at a time and discard the (garbage/RTMP continuation header) bytes in between
-  while(pos<length)
+  if(!chunk->buf)
+  {
+    chunk->buf=malloc(chunk->length);
+    chunk->pos=0;
+  }
+  unsigned int rsize=((chunk->length-chunk->pos>127)?128:(chunk->length-chunk->pos));
+  while(rsize>0)
+  {
+    size_t r=read(sock, chunk->buf+chunk->pos, rsize);;
+    if(r<1){return 0;}
+    rsize-=r;
+    chunk->pos+=r;
+//    if(rsize){printf("Got a short read, %u remaining\n", rsize);}
+  }
+  if(chunk->pos==chunk->length)
   {
-    w=read(sock, rtmp->buf+pos, ((length-pos>127)?128:(length-pos)));
-    if(w<1){break;}
-    pos+=w;
-    if(length-pos>0){read(sock, &w, 1);} // Skip junk once every 128 bytes
+// printf("Got chunk: chunkid=%u, type=%u, length=%u, streamid=%u\n", chunk->id, chunk->type, chunk->length, chunk->streamid);
+    rtmp->type=chunk->type;
+    rtmp->chunkid=chunk->id;
+    rtmp->length=chunk->length;
+    rtmp->msgid=le32(chunk->streamid);
+    free(rtmp->buf);
+    rtmp->buf=chunk->buf;
+    chunk->buf=0;
+    return 1;
   }
-  return 1;
+// printf("Waiting for next part of chunk\n");
+  return 2;
 }
 
 void rtmp_send(int sock, struct rtmp* rtmp)
 {
   // Header format and stream ID
   unsigned int fmt=(rtmp->msgid?0:1);
-  unsigned char basicheader=(rtmp->streamid<64?rtmp->streamid:(rtmp->streamid<256?0:1)) | (fmt<<6);
+  unsigned char basicheader=(rtmp->chunkid<64?rtmp->chunkid:(rtmp->chunkid<256?0:1)) | (fmt<<6);
   write(sock, &basicheader, sizeof(basicheader));
-  if(rtmp->streamid>=64) // Handle large stream IDs
+  if(rtmp->chunkid>=64) // Handle large stream IDs
   {
-    if(rtmp->streamid<256)
+    if(rtmp->chunkid<256)
     {
-      unsigned char streamid=rtmp->streamid-64;
-      write(sock, &streamid, sizeof(streamid));
+      unsigned char chunkid=rtmp->chunkid-64;
+      write(sock, &chunkid, sizeof(chunkid));
     }else{
-      unsigned short streamid=le16(rtmp->streamid-64);
-      write(sock, &streamid, sizeof(streamid));
+      unsigned short chunkid=le16(rtmp->chunkid-64);
+      write(sock, &chunkid, sizeof(chunkid));
     }
   }
   unsigned int x=0;
diff --git a/rtmp.h b/rtmp.h
index f18b2ce..6c03274 100644
--- a/rtmp.h
+++ b/rtmp.h
@@ -27,7 +27,7 @@
 struct rtmp
 {
   unsigned char type;
-  unsigned int streamid;
+  unsigned int chunkid;
   unsigned int length;
   unsigned int msgid;
   void* buf;
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index 174c60a..51e6738 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -16,525 +16,110 @@
 */
 #include <unistd.h>
 #include <fcntl.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/prctl.h>
 #include <libavcodec/avcodec.h>
 #include <libswscale/swscale.h>
-#if LIBAVUTIL_VERSION_MAJOR>50 || (LIBAVUTIL_VERSION_MAJOR==50 && LIBAVUTIL_VERSION_MINOR>37)
-  #include <libavutil/imgutils.h>
-#else
-  #include <libavcore/imgutils.h>
-#endif
-#ifdef HAVE_SOUND
-// TODO: use libavresample instead if available
-  #if HAVE_SOUND==avresample
-    #include <libavutil/opt.h>
-    #include <libavresample/avresample.h>
-  #else
-    #include <libswresample/swresample.h>
-  #endif
-  #include <ao/ao.h>
-#endif
 #include <gtk/gtk.h>
-#ifdef HAVE_V4L2
-  #include <libv4l2.h>
-  #include <linux/videodev2.h>
-#endif
 
-#if GTK_MAJOR_VERSION==2
-  #define GTK_ORIENTATION_HORIZONTAL 0
-  #define GTK_ORIENTATION_VERTICAL 1
-  GtkWidget* gtk_box_new(int vertical, int spacing)
-  {
-    if(vertical)
-    {
-      return gtk_vbox_new(1, spacing);
-    }else{
-      return gtk_hbox_new(1, spacing);
-    }
-  }
-#endif
-
-struct camera
+struct viddata
 {
   AVFrame* frame;
   AVFrame* dstframe;
   GtkWidget* cam;
-  AVCodecContext* vctx;
-  AVCodecContext* actx;
-  short* samples;
-  unsigned int samplecount;
-  char* id;
-  char* nick;
-  GtkWidget* box; // holds label and cam
-};
-
-struct viddata
-{
-  struct camera* cams;
-  unsigned int camcount;
-  GtkWidget* box;
-  AVCodec* vdecoder;
-  AVCodec* vencoder;
-  AVCodec* adecoder;
-#ifdef HAVE_SOUND
-  int audiopipe;
-  #if HAVE_SOUND==avresample
-    AVAudioResampleContext* resamplectx;
-  #else
-    SwrContext* swrctx;
-  #endif
-#endif
+  AVCodecContext* ctx;
+  char* camnick;
 };
 
 int tc_client[2];
 int tc_client_in[2];
 
-#ifdef HAVE_SOUND
-// Experimental mixer, not sure if it really works
-void camera_playsnd(struct viddata* data, struct camera* cam, short* samples, unsigned int samplecount)
-{
-  if(cam->samples)
-  {
-// int sources=1;
-    unsigned int i;
-    for(i=0; i<data->camcount; ++i)
-    {
-      if(!data->cams[i].samples){continue;}
-      if(cam==&data->cams[i]){continue;}
-      unsigned j;
-      for(j=0; j<cam->samplecount && j<data->cams[i].samplecount; ++j)
-      {
-        cam->samples[j]+=data->cams[i].samples[j];
-      }
-      free(data->cams[i].samples);
-      data->cams[i].samples=0;
-// ++sources;
-    }
-    write(data->audiopipe, cam->samples, cam->samplecount*sizeof(short));
-    free(cam->samples);
-// printf("Mixed sound from %i sources (cam: %p)\n", sources, cam);
-  }
-  cam->samples=malloc(samplecount*sizeof(short));
-  memcpy(cam->samples, samples, samplecount*sizeof(short));
-  cam->samplecount=samplecount;
-}
-#endif
-
-void camera_remove(struct viddata* data, const char* nick)
-{
-  unsigned int i;
-  for(i=0; i<data->camcount; ++i)
-  {
-    if(!strcmp(data->cams[i].id, nick))
-    {
-      gtk_widget_destroy(data->cams[i].box);
-      av_frame_free(&data->cams[i].frame);
-      avcodec_free_context(&data->cams[i].vctx);
-#ifdef HAVE_SOUND
-      avcodec_free_context(&data->cams[i].actx);
-#endif
-      free(data->cams[i].id);
-      free(data->cams[i].nick);
-      --data->camcount;
-      memmove(&data->cams[i], &data->cams[i+1], (data->camcount-i)*sizeof(struct camera));
-      break;
-    }
-  }
-}
-
 char buf[1024];
 gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
 {
   struct viddata* data=datap;
-  unsigned int i;
+  int i;
   for(i=0; i<1023; ++i)
   {
     if(read(tc_client[0], &buf[i], 1)<1){printf("No more data\n"); gtk_main_quit(); return 0;}
     if(buf[i]=='\r'||buf[i]=='\n'){break;}
   }
   buf[i]=0;
-  // Start streams once we're properly connected
-  if(!strncmp(buf, "Currently on cam: ", 18))
-  {
-    char* next=&buf[16];
-    while(next)
-    {
-      char* user=&next[2];
-      next=strstr(user, ", ");
-      if(!user[0]){continue;}
-      if(next){next[0]=0;}
-      dprintf(tc_client_in[1], "/opencam %s\n", user);
-    }
-    return 1;
-  }
-  char* space=strchr(buf, ' ');
-  // Start a stream when someone cams up
-  if(space && !strcmp(space, " cammed up"))
-  {
-    space[0]=0;
-    dprintf(tc_client_in[1], "/opencam %s\n", buf);
-    return 1;
-  }
-  // Make sure the cam goes away when a user leaves
-  else if(space && !strcmp(space, " left the channel"))
-  {
-    space[0]=0;
-    camera_remove(data, buf);
-    return 1;
-  }
-  if(!strncmp(buf, "Starting media stream for ", 26))
-  {
-    char* nick=&buf[26];
-    char* id=strstr(nick, " (");
-    if(!id){return 1;}
-    id[0]=0;
-    id=&id[2];
-    char* idend=strchr(id, ')');
-    if(!idend){return 1;}
-    idend[0]=0;
-    ++data->camcount;
-    data->cams=realloc(data->cams, sizeof(struct camera)*data->camcount);
-    struct camera* cam=&data->cams[data->camcount-1];
-    cam->frame=av_frame_alloc();
-    cam->dstframe=av_frame_alloc();
-    cam->nick=strdup(nick);
-    cam->id=strdup(id);
-    cam->vctx=avcodec_alloc_context3(data->vdecoder);
-    avcodec_open2(cam->vctx, data->vdecoder, 0);
-#ifdef HAVE_SOUND
-    cam->actx=avcodec_alloc_context3(data->adecoder);
-    avcodec_open2(cam->actx, data->adecoder, 0);
-    cam->samples=0;
-#endif
-    cam->cam=gtk_image_new();
-    cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-    gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
-    gtk_box_pack_start(GTK_BOX(cam->box), gtk_label_new(cam->nick), 0, 0, 0);
-    gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
-    gtk_widget_show_all(cam->box);
-    return 1;
-  }
-  if(!strcmp(buf, "Starting outgoing media stream"))
-  {
-    ++data->camcount;
-    data->cams=realloc(data->cams, sizeof(struct camera)*data->camcount);
-    struct camera* cam=&data->cams[data->camcount-1];
-    cam->frame=av_frame_alloc();
-    cam->dstframe=av_frame_alloc();
-    cam->nick=strdup("You");
-    cam->id=strdup("out");
-    cam->vctx=avcodec_alloc_context3(data->vdecoder);
-    avcodec_open2(cam->vctx, data->vdecoder, 0);
-    cam->cam=gtk_image_new();
-    cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-    gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
-    gtk_box_pack_start(GTK_BOX(cam->box), gtk_label_new(cam->nick), 0, 0, 0);
-    gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
-    gtk_widget_show_all(cam->box);
-    return 1;
-  }
-  if(!strncmp(buf, "VideoEnd: ", 10))
-  {
-    camera_remove(data, &buf[10]);
-    return 1;
-  }
-  if(!strncmp(buf, "Audio: ", 7))
-  {
-    char* sizestr=strchr(&buf[7], ' ');
-    if(!sizestr){return 1;}
-    sizestr[0]=0;
-    unsigned int size=strtoul(&sizestr[1], 0, 0);
-    if(!size){return 1;}
-    unsigned char frameinfo;
-    read(tc_client[0], &frameinfo, 1);
-    --size; // For the byte we read above
-    AVPacket pkt;
-    av_init_packet(&pkt);
-    unsigned char databuf[size];
-    pkt.data=databuf;
-    pkt.size=size;
-    unsigned int pos=0;
-    while(pos<size)
-    {
-      pos+=read(tc_client[0], pkt.data+pos, size-pos);
-    }
-#ifdef HAVE_SOUND
-    // Find the camera representation for the given ID (for decoder context)
-    struct camera* cam=0;
-    for(i=0; i<data->camcount; ++i)
-    {
-      if(!strcmp(data->cams[i].id, &buf[7])){cam=&data->cams[i]; break;}
-    }
-    if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
-    int gotframe;
-    avcodec_decode_audio4(cam->actx, cam->frame, &gotframe, &pkt);
-    if(!gotframe){return 1;}
-  #if HAVE_SOUND==avresample
-    int outlen=avresample_convert(data->resamplectx, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples);
-  #else
-    int outlen=swr_convert(data->resamplectx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
-  #endif
-    camera_playsnd(data, cam, (short*)cam->frame->data[0], outlen);
-#endif
-    return 1;
-  }
-  if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); fflush(stdout); return 1;} // Ignore anything else that isn't video
+  // Start stream once we're properly connected
+  if(!strncmp(buf, "Connection ID: ", 10)){dprintf(tc_client_in[1], "/opencam %s\n", data->camnick); return 1;}
+  if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); return 1;} // Ignore anything that isn't video
   char* sizestr=strchr(&buf[7], ' ');
-  if(!sizestr){return 1;}
-  sizestr[0]=0;
-  // Find the camera representation for the given ID
-  struct camera* cam=0;
-  for(i=0; i<data->camcount; ++i)
-  {
-    if(!strcmp(data->cams[i].id, &buf[7])){cam=&data->cams[i]; break;}
-  }
   unsigned int size=strtoul(&sizestr[1], 0, 0);
-  if(!size){return 1;}
-  // Mostly ignore the first byte (contains frame type (e.g. keyframe etc.) in 4 bits and codec in the other 4)
+  // Mostly ignore the first byte (contains frame type (e.g. keyframe etc.) in 4 bits and codec in the other 4, but we assume FLV1)
   --size;
   AVPacket pkt;
   av_init_packet(&pkt);
-  unsigned char databuf[size+4];
-  pkt.data=databuf;
-  unsigned char frameinfo;
-  read(tc_client[0], &frameinfo, 1);
-// printf("Frametype-frame: %x\n", ((unsigned int)frameinfo&0xf0)/16);
-// printf("Frametype-codec: %x\n", (unsigned int)frameinfo&0xf);
-  unsigned int pos=0;
-  while(pos<size)
+  pkt.data=malloc(size);
+  read(tc_client[0], pkt.data, 1); // Skip
+// printf("Frametype-frame: %x\n", ((unsigned int)pkt.data[0]&0xf0)/16);
+// printf("Frametype-codec: %x\n", (unsigned int)pkt.data[0]&0xf);
+  if((pkt.data[0]&0xf)!=2) // Not FLV1, get data but discard it
   {
-    pos+=read(tc_client[0], pkt.data+pos, size-pos);
+    read(tc_client[0], pkt.data, size);
+    return 1;
   }
-  if((frameinfo&0xf)!=2){return 1;} // Not FLV1, get data but discard it
-  if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
+  read(tc_client[0], pkt.data, size);
   pkt.size=size;
   int gotframe;
-  avcodec_decode_video2(cam->vctx, cam->frame, &gotframe, &pkt);
+  avcodec_decode_video2(data->ctx, data->frame, &gotframe, &pkt);
+  free(pkt.data);
   if(!gotframe){return 1;}
 
   // Convert to RGB24 format
-  unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, cam->frame->width, cam->frame->height);
+  unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, data->frame->width, data->frame->height);
   unsigned char buf[bufsize];
-  cam->dstframe->data[0]=buf;
-  cam->dstframe->linesize[0]=cam->frame->width*3;
-  struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, cam->frame->width, cam->frame->height, AV_PIX_FMT_RGB24, 0, 0, 0, 0);
-  sws_scale(swsctx, (const uint8_t*const*)cam->frame->data, cam->frame->linesize, 0, cam->frame->height, cam->dstframe->data, cam->dstframe->linesize);
-  sws_freeContext(swsctx);
-
-  GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, cam->frame->width, cam->frame->height, cam->dstframe->linesize[0], 0, 0);
-  gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), gdkframe);
-  // Make sure it gets redrawn in time
-  gdk_window_process_updates(gtk_widget_get_window(cam->cam), 1);
+  data->dstframe->data[0]=buf;
+  data->dstframe->linesize[0]=data->frame->width*3;
+  struct SwsContext* swsctx=sws_getContext(data->frame->width, data->frame->height, data->frame->format, data->frame->width, data->frame->height, AV_PIX_FMT_RGB24, 0, 0, 0, 0);
+  sws_scale(swsctx, (const uint8_t*const*)data->frame->data, data->frame->linesize, 0, data->frame->height, data->dstframe->data, data->dstframe->linesize);
 
+  GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(data->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, data->frame->width, data->frame->height, data->dstframe->linesize[0], 0, 0);
+  gtk_image_set_from_pixbuf(GTK_IMAGE(data->cam), gdkframe);
   g_object_unref(gdkframe);
-  return 1;
-}
-
-#ifdef HAVE_SOUND
-void audiothread(int fd)
-{
-  ao_initialize();
-  ao_sample_format samplefmt;
-  samplefmt.bits=16;
-  samplefmt.rate=22050;
-  samplefmt.channels=1;
-  samplefmt.byte_format=AO_FMT_NATIVE; // I'm guessing libavcodec decodes it to native
-  samplefmt.matrix=0;
-  ao_option clientname={.key="client_name", .value="tc_client/camviewer", .next=0};
-  ao_device* dev=ao_open_live(ao_default_driver_id(), &samplefmt, &clientname);
-  char buf[2048];
-  size_t len;
-  while((len=read(fd, buf, 2048))>0)
-  {
-    ao_play(dev, buf, len);
-  }
-  ao_close(dev);
-}
-#endif
 
-#ifdef HAVE_V4L2
-pid_t camproc=0;
-void togglecam(GtkButton* button, struct viddata* data)
-{
-  if(camproc)
-  {
-    kill(camproc, SIGINT);
-    camproc=0;
-    gtk_button_set_label(button, "Broadcast cam");
-    dprintf(tc_client_in[1], "/camdown\n");
-    dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
-    return;
-  }
-  // Set up a second pipe to be handled by handledata() to avoid overlap with tc_client's output
-  int campipe[2];
-  pipe(campipe);
-  dprintf(tc_client_in[1], "/camup\n");
-  gtk_button_set_label(button, "Stop broadcasting");
-// printf("Camming up!\n");
-  camproc=fork();
-  if(!camproc)
-  {
-    close(campipe[0]);
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
-    unsigned int delay=500000;
-    // Set up camera
-    int fd=v4l2_open("/dev/video0", O_RDWR);
-    struct v4l2_format fmt;
-    fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
-    fmt.fmt.pix.width=320;
-    fmt.fmt.pix.height=240;
-    fmt.fmt.pix.pixelformat=V4L2_PIX_FMT_RGB24;
-    fmt.fmt.pix.field=V4L2_FIELD_NONE;
-    fmt.fmt.pix.bytesperline=fmt.fmt.pix.width*3;
-    fmt.fmt.pix.sizeimage=fmt.fmt.pix.bytesperline*fmt.fmt.pix.height;
-    v4l2_ioctl(fd, VIDIOC_S_FMT, &fmt);
-    AVCodecContext* ctx=avcodec_alloc_context3(data->vencoder);
-    ctx->width=fmt.fmt.pix.width;
-    ctx->height=fmt.fmt.pix.height;
-    ctx->pix_fmt=PIX_FMT_YUV420P;
-    ctx->time_base.num=1;
-    ctx->time_base.den=10;
-    avcodec_open2(ctx, data->vencoder, 0);
-    AVFrame* frame=av_frame_alloc();
-    frame->format=PIX_FMT_RGB24;
-    frame->width=fmt.fmt.pix.width;
-    frame->height=fmt.fmt.pix.height;
-    av_image_alloc(frame->data, frame->linesize, ctx->width, ctx->height, frame->format, 1);
-    AVPacket packet;
-    packet.buf=0;
-    packet.data=0;
-    packet.size=0;
-    packet.dts=AV_NOPTS_VALUE;
-    packet.pts=AV_NOPTS_VALUE;
-
-    // Set up frame for conversion from the camera's format to a format the encoder can use
-    AVFrame* dstframe=av_frame_alloc();
-    dstframe->format=ctx->pix_fmt;
-    dstframe->width=ctx->width;
-    dstframe->height=ctx->height;
-    av_image_alloc(dstframe->data, dstframe->linesize, ctx->width, ctx->height, ctx->pix_fmt, 1);
-
-    struct SwsContext* swsctx=sws_getContext(frame->width, frame->height, PIX_FMT_RGB24, frame->width, frame->height, AV_PIX_FMT_YUV420P, 0, 0, 0, 0);
-
-    while(1)
-    {
-      usleep(delay);
-      if(delay>100000){delay-=50000;}
-      v4l2_read(fd, frame->data[0], fmt.fmt.pix.sizeimage);
-      int gotpacket;
-      sws_scale(swsctx, (const uint8_t*const*)frame->data, frame->linesize, 0, frame->height, dstframe->data, dstframe->linesize);
-      av_init_packet(&packet);
-      packet.data=0;
-packet.size=0;
-      avcodec_encode_video2(ctx, &packet, dstframe, &gotpacket);
-      unsigned char frameinfo=0x22; // Note: differentiating between keyframes and non-keyframes seems to break stuff, so let's just go with all being interframes (1=keyframe, 2=interframe, 3=disposable interframe)
-      dprintf(tc_client_in[1], "/video %i\n", packet.size+1);
-      write(tc_client_in[1], &frameinfo, 1);
-      write(tc_client_in[1], packet.data, packet.size);
-      // Also send the packet to our main thread so we can see ourselves
-      dprintf(campipe[1], "Video: out %i\n", packet.size+1);
-      write(campipe[1], &frameinfo, 1);
-      write(campipe[1], packet.data, packet.size);
-
-      av_free_packet(&packet);
-    }
-    sws_freeContext(swsctx);
-    _exit(0);
-  }
-  close(campipe[1]);
-  GIOChannel* channel=g_io_channel_unix_new(campipe[0]);
-  g_io_channel_set_encoding(channel, 0, 0);
-  g_io_add_watch(channel, G_IO_IN, handledata, data);
+  return 1;
 }
-#endif
 
 int main(int argc, char** argv)
 {
-  struct viddata data={0,0,0,0,0};
+  struct viddata data={av_frame_alloc(),av_frame_alloc()};
+  data.camnick=argv[1];
+  // Init the decoder
   avcodec_register_all();
-  data.vdecoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
-  data.adecoder=avcodec_find_decoder(AV_CODEC_ID_NELLYMOSER);
-
-#ifdef HAVE_SOUND
-  #if HAVE_SOUND==avresample
-  data.resamplectx=avresample_alloc_context();
-  av_opt_set_int(data.resamplectx, "in_channel_layout", AV_CH_FRONT_CENTER, 0);
-  av_opt_set_int(data.resamplectx, "in_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
-  // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
-  av_opt_set_int(data.resamplectx, "in_sample_rate", 11025, 0);
-  av_opt_set_int(data.resamplectx, "out_channel_layout", AV_CH_FRONT_CENTER, 0);
-  av_opt_set_int(data.resamplectx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
-  av_opt_set_int(data.resamplectx, "out_sample_rate", 22050, 0);
-  avresample_open(data.resamplectx);
-  #else
-  data.resamplectx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, 22050, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_FLT, 11025, 0, 0);
-  swr_init(data.swrctx);
-  #endif
-  int audiopipe[2];
-  pipe(audiopipe);
-  data.audiopipe=audiopipe[1];
-  if(!fork())
-  {
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
-    close(audiopipe[1]);
-    audiothread(audiopipe[0]);
-    _exit(0);
-  }
-  close(audiopipe[0]);
-#endif
+  AVCodec* decoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
+  data.ctx=avcodec_alloc_context3(decoder);
+  avcodec_open2(data.ctx, decoder, 0);
 
   gtk_init(&argc, &argv);
   GtkWidget* w=gtk_window_new(GTK_WINDOW_TOPLEVEL);
   g_signal_connect(w, "destroy", gtk_main_quit, 0);
-  data.box=gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
-  gtk_container_add(GTK_CONTAINER(w), data.box);
-#ifdef HAVE_V4L2
-  data.vencoder=avcodec_find_encoder(AV_CODEC_ID_FLV1);
-  GtkWidget* cambutton=gtk_button_new_with_label("Broadcast cam");
-  g_signal_connect(cambutton, "clicked", G_CALLBACK(togglecam), &data);
-  gtk_box_pack_start(GTK_BOX(data.box), cambutton, 0, 0, 0);
-#endif
+  data.cam=gtk_image_new();
+  gtk_container_add(GTK_CONTAINER(w), data.cam);
   gtk_widget_show_all(w);
 
   pipe(tc_client);
   pipe(tc_client_in);
   if(!fork())
   {
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
     close(tc_client[0]);
     close(tc_client_in[1]);
     dup2(tc_client[1], 1);
     dup2(tc_client_in[0], 0);
-    argv[0]="./tc_client";
-    execv("./tc_client", argv);
+    argv[1]="./tc_client";
+    execv("./tc_client", &argv[1]);
   }
+  close(tc_client[1]);
   close(tc_client_in[0]);
   GIOChannel* tcchannel=g_io_channel_unix_new(tc_client[0]);
   g_io_channel_set_encoding(tcchannel, 0, 0);
-  unsigned int channel_id=g_io_add_watch(tcchannel, G_IO_IN, handledata, &data);
+  g_io_add_watch(tcchannel, G_IO_IN, handledata, &data);
 
   gtk_main();

-  g_source_remove(channel_id);
-  g_io_channel_shutdown(tcchannel, 0, 0);
-  unsigned int i;
-  for(i=0; i<data.camcount; ++i)
-  {
-    av_frame_free(&data.cams[i].frame);
-    avcodec_free_context(&data.cams[i].vctx);
-#ifdef HAVE_SOUND
-    avcodec_free_context(&data.cams[i].actx);
-  #if HAVE_SOUND==avresample
-    avresample_free(&data.resamplectx);
-  #else
-    swr_free(&data.swrctx);
-  #endif
-#endif
-    free(data.cams[i].id);
-    free(data.cams[i].nick);
-  }
-  free(data.cams);
+
+  free(data.frame->data[0]);
+  av_frame_free(&data.frame);
   return 0;
 }
diff --git a/utilities/compat.c b/utilities/compat.c
deleted file mode 100644
index e620559..0000000
--- a/utilities/compat.c
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
-    Some compatibility code to work on more limited platforms
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include "compat.h"
-#ifdef __ANDROID__
-// Android has no dprintf, so we make our own
-#include <stdarg.h>
-size_t dprintf(int fd, const char* fmt, ...)
-{
-  va_list va;
-  va_start(va, fmt);
-  int len=vsnprintf(0, 0, fmt, va);
-  va_end(va);
-  char buf[len+1];
-  va_start(va, fmt);
-  vsnprintf(buf, len+1, fmt, va);
-  va_end(va);
-  buf[len]=0;
-  write(fd, buf, len);
-  return len;
-}
-#endif
diff --git a/utilities/compat.h b/utilities/compat.h
deleted file mode 100644
index 2cfb39b..0000000
--- a/utilities/compat.h
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
-    Some compatibility code to work on more limited platforms
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#ifdef __ANDROID__
-#include <stdint.h>
-extern size_t dprintf(int fd, const char* fmt, ...);
-#define mbtowc(x,y,z) 1
-#endif
diff --git a/utilities/cursedchat/buffer.c b/utilities/cursedchat/buffer.c
deleted file mode 100644
index af06a16..0000000
--- a/utilities/cursedchat/buffer.c
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
-    cursedchat, a simple curses interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <string.h>
-#include <stdlib.h>
-#include <curses.h>
-#include "buffer.h"
-
-struct buffer* buffers=0;
-unsigned int buffercount=0;
-unsigned int currentbuf=0;
-
-unsigned int createbuffer(const char* name)
-{
-  ++buffercount;
-  buffers=realloc(buffers, buffercount*sizeof(struct buffer));
-  buffers[buffercount-1].pad=newpad(2048, COLS);
-  scrollok(buffers[buffercount-1].pad, 1);
-  buffers[buffercount-1].name=(name?strdup(name):0);
-  buffers[buffercount-1].scroll=-1;
-  buffers[buffercount-1].seen=1;
-  return buffercount-1;
-}
-
-unsigned int findbuffer(const char* name)
-{
-  unsigned int i;
-  for(i=1; i<buffercount; ++i)
-  {
-    if(!strcmp(buffers[i].name, name)){return i;}
-  }
-  return 0;
-}
-
-void renamebufferunique(unsigned int id)
-{
-  unsigned int i=0;
-  while(i<buffercount)
-  {
-    int len=strlen(buffers[id].name);
-    buffers[id].name=realloc(buffers[id].name, len+2);
-    buffers[id].name[len]='_';
-    buffers[id].name[len+1]=0;
-    for(i=1; i<buffercount; ++i)
-    {
-      if(i!=id && !strcmp(buffers[i].name, buffers[id].name)){break;}
-    }
-  }
-}
diff --git a/utilities/cursedchat/buffer.h b/utilities/cursedchat/buffer.h
deleted file mode 100644
index 79ec1fc..0000000
--- a/utilities/cursedchat/buffer.h
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
-    cursedchat, a simple curses interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-struct buffer
-{
-  WINDOW* pad;
-  char* name;
-  int scroll;
-  char seen;
-};
-
-extern struct buffer* buffers;
-extern unsigned int buffercount;
-extern unsigned int currentbuf;
-
-extern unsigned int createbuffer(const char* name);
-extern unsigned int findbuffer(const char* name);
-extern void renamebufferunique(unsigned int id);
diff --git a/utilities/cursedchat/cursedchat.c b/utilities/cursedchat/cursedchat.c
deleted file mode 100644
index 6e5385c..0000000
--- a/utilities/cursedchat/cursedchat.c
+++ /dev/null
@@ -1,499 +0,0 @@
-/*
-    cursedchat, a simple curses interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-    Copyright (C) 2015  Pamela Hiatt
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <unistd.h>
-#include <stdlib.h>
-#include <poll.h>
-#include <signal.h>
-#include <sys/ioctl.h>
-#include <termios.h>
-#include <locale.h>
-#include <curses.h>
-#include <readline/readline.h>
-#include <readline/history.h>
-#include "../compat.h"
-#include "../list.h"
-#include "buffer.h"
-
-#define HALFSCREEN (LINES>4?(LINES-3)/2:1)
-
-WINDOW* topic;
-char* channeltopic;
-WINDOW* input;
-int to_app;
-struct list userlist={0,0};
-char* nickname=0;
-
-// Translate ANSI escape codes to curses commands and write the text to a window
-void waddansi(WINDOW* w, char* str)
-{
-  while(str[0])
-  {
-    char* esc=strstr(str, "\x1b[");
-    if(esc==str)
-    {
-      str=&str[2];
-      while(str[0]!='m')
-      {
-        if(str[0]=='3'&&str[1]!='m') // Color
-        {
-          unsigned int c=strtoul(&str[1], &str, 10);
-          wattron(w, COLOR_PAIR(c+1));
-        }
-        else if(str[0]=='1') // Bold
-        {
-          wattron(w, A_BOLD);
-          str=&str[1];
-        }
-        else if(str[0]=='0') // Reset
-        {
-          wattroff(w, COLOR_PAIR(1));
-          wattroff(w, A_BOLD);
-          str=&str[1];
-        }
-        else{str=&str[1];}
-      }
-      str=&str[1];
-      continue;
-    }
-    if(esc)
-    {
-      waddnstr(w, str, esc-str);
-      str=esc;
-    }else{
-      waddstr(w, str);
-      return;
-    }
-  }
-}
-
-void drawchat(void)
-{
-  WINDOW* w=buffers[currentbuf].pad;
-  int scroll=buffers[currentbuf].scroll;
-  prefresh(w, (scroll>-1?scroll:getcury(w)-LINES+4), 0, 1, 0, LINES-3, COLS);
-}
-
-void drawtopic(void)
-{
-  werase(topic);
-  unsigned int i;
-  for(i=1; i<buffercount && buffers[i].seen; ++i);
-  if(i<buffercount)
-  {
-    waddstr(topic, "Unread PMs from: ");
-    char first=1;
-    for(i=1; i<buffercount; ++i)
-    {
-      if(!buffers[i].seen)
-      {
-        if(first){first=0;}else{waddstr(topic, ", ");}
-        waddstr(topic, buffers[i].name);
-      }
-    }
-  }
-  else if(currentbuf)
-  {
-    waddstr(topic, "To return to public chat type: /pm");
-  }else{
-    waddstr(topic, channeltopic);
-  }
-  wrefresh(topic);
-}
-
-void gotline(char* line)
-{
-  if(!line){close(to_app); return;} // TODO: handle EOF on stdin better?
-  add_history(line);
-  if(!strcmp(line, "/pm"))
-  {
-    currentbuf=0;
-    drawchat();
-    drawtopic();
-    return;
-  }
-  else if(!strncmp(line, "/pm ", 4))
-  {
-    currentbuf=findbuffer(&line[4]);
-    if(!currentbuf){currentbuf=createbuffer(&line[4]);}
-    buffers[currentbuf].seen=1;
-    drawchat();
-    drawtopic();
-    return;
-  }
-  else if(!strncmp(line, "/buffer ", 8))
-  {
-    unsigned int num=atoi(&line[8]);
-    if(num<0 || num>=buffercount)
-    {
-      wprintw(buffers[currentbuf].pad, "\nInvalid buffer number: %u", num);
-    }else{
-      currentbuf=num;
-      buffers[currentbuf].seen=1;
-      drawtopic();
-    }
-    drawchat();
-    return;
-  }
-  else if(!strcmp(line, "/bufferlist"))
-  {
-    unsigned int i;
-    for(i=0; i<buffercount; ++i)
-    {
-      wprintw(buffers[currentbuf].pad, "\n% 3i: %s", i, i?buffers[i].name:"");
-    }
-    drawchat();
-    return;
-  }
-  else if(!strncmp(line, "/msg ", 5))
-  {
-    char* name=&line[5];
-    char* msg=strchr(name, ' ');
-    if(!msg){return;}
-    msg[0]=0;
-    currentbuf=findbuffer(name);
-    if(!currentbuf){currentbuf=createbuffer(name);}
-    buffers[currentbuf].seen=1;
-    drawtopic();
-    memmove(line, &msg[1], strlen(&msg[1])+1);
-  }
-  else if(!strcmp(line, "/help"))
-  {
-    waddstr(buffers[0].pad, "\nFor cursedchat:\n"
-      "/pm <name>    = switch to the PM buffer for <name>\n"
-      "/pm           = return to the channel/public chat's buffer\n"
-      "/buffer <num> = switch to buffer by number\n"
-      "/bufferlist   = list open buffers, their numbers and associated names\n"
-      "\nFor tc_client (through cursedchat):");
-    write(to_app, line, strlen(line));
-    write(to_app, "\n", 1);
-    return;
-  }
-
-  if(currentbuf) // We're in a PM window, make the message a PM
-  {
-    dprintf(to_app, "/msg %s ", buffers[currentbuf].name);
-  }
-  write(to_app, line, strlen(line));
-  write(to_app, "\n", 1);
-  time_t timestamp=time(0);
-  struct tm* t=localtime(&timestamp);
-  wprintw(buffers[currentbuf].pad, "\n[%02i:%02i] %s: %s", t->tm_hour, t->tm_min, nickname, line);
-  drawchat();
-}
-
-unsigned int bytestochars(const char* buf, unsigned int buflen, unsigned int bytes)
-{
-  unsigned int pos=0;
-  unsigned int i;
-  for(i=0; i<bytes; ++pos)
-  {
-    i+=mbtowc(0,&buf[i],buflen-i);
-  }
-  return pos;
-}
-
-unsigned int charstobytes(const char* buf, unsigned int buflen, unsigned int chars)
-{
-  unsigned int pos;
-  unsigned int i=0;
-  for(pos=0; pos<chars; ++pos)
-  {
-    i+=mbtowc(0,&buf[i],buflen-i);
-  }
-  return i;
-}
-
-int escinput(int a, int byte)
-{
-  char buf[4];
-  read(0, buf, 2);
-  buf[2]=0;
-  if(!strcmp(buf, "[A")||!strcmp(buf, "OA")){rl_get_previous_history(1,27);return 0;}
-  if(!strcmp(buf, "[B")||!strcmp(buf, "OB")){rl_get_next_history(1,27);return 0;}
-  if(!strcmp(buf, "[C")||!strcmp(buf, "OC")){rl_forward(1,27);return 0;}
-  if(!strcmp(buf, "[D")||!strcmp(buf, "OD")){rl_backward(1,27);return 0;}
-  if(!strcmp(buf, "[H")||!strcmp(buf, "OH")){rl_beg_of_line(1,27);return 0;}
-  if(!strcmp(buf, "[F")||!strcmp(buf, "OF")){rl_end_of_line(1,27);return 0;}
-  if(!strcmp(buf, "[3")&&read(0, buf, 1)&&buf[0]=='~'){rl_delete(1,27);return 0;}
-  if(!strcmp(buf, "[5")) // Page up
-  {
-    read(0, buf, 1);
-    struct buffer* b=&buffers[currentbuf];
-    if(b->scroll<0){b->scroll=getcury(b->pad)-LINES+4;}
-    b->scroll-=HALFSCREEN;
-    if(b->scroll<0){b->scroll=0;}
-    drawchat();
-    return 0;
-  }
-  if(!strcmp(buf, "[6")) // Page down
-  {
-    read(0, buf, 1);
-    struct buffer* b=&buffers[currentbuf];
-    if(b->scroll<0){return 0;} // Already at the bottom
-    b->scroll+=HALFSCREEN;
-    if(b->scroll>getcury(b->pad)-LINES+3){b->scroll=-1;}
-    drawchat();
-    return 0;
-  }
-  return 0;
-}
-
-void drawinput(void)
-{
-  werase(input);
-  unsigned int pos=bytestochars(rl_line_buffer, rl_end, rl_point);
-
-  waddstr(input, "> ");
-  int cursor_row=(pos+2)/COLS;
-  int end_row=(rl_end+2)/COLS;
-  // Figure out how much of the buffer to print to not scroll past the cursor
-  unsigned int eol=charstobytes(rl_line_buffer, rl_end, (cursor_row+2)*COLS-3); // -2 for cursor, -1 to avoid wrapping
-  waddnstr(input, rl_line_buffer, eol);
-
-  wmove(input, cursor_row==end_row && cursor_row>0, (pos+2)%COLS); // +2 for prompt
-  wrefresh(input);
-}
-
-void resizechat(int sig)
-{
-  struct winsize size;
-  ioctl(0, TIOCGWINSZ, &size);
-  if(size.ws_row<3){return;} // Too small, would result in negative numbers breaking the chat window
-  resize_term(size.ws_row, size.ws_col);
-  clear();
-  refresh();
-  wresize(topic, 1, COLS);
-  unsigned int i;
-  for(i=0; i<buffercount; ++i)
-  {
-    wresize(buffers[i].pad, buffers[i].pad->_maxy+1, COLS);
-  }
-  wresize(input, 2, COLS);
-  mvwin(input, LINES-2, 0);
-  redrawwin(buffers[currentbuf].pad);
-  redrawwin(topic);
-  redrawwin(input);
-  drawchat();
-  drawtopic();
-  drawinput();
-}
-
-void dontprintmatches(char** matches, int num, int maxlen)
-{
-}
-
-unsigned int completionmatch;
-char* completenicks(const char* text, int state)
-{
-  // text is the word we're completing on, state is the iteration count (one iteration per matching name, until we return 0)
-  if(!state){completionmatch=0;}
-  while(completionmatch<userlist.itemcount)
-  {
-    if(!strncmp(userlist.items[completionmatch], text, strlen(text)))
-    {
-      char* completion=malloc(strlen(userlist.items[completionmatch])+2);
-      strcpy(completion, userlist.items[completionmatch]);
-      // Check if we're on the first word and only add the ":" if we are
-      if(strlen(text)>=rl_point){strcat(completion, ":");}
-      ++completionmatch;
-      return completion;
-    }
-    ++completionmatch;
-  }
-  return 0;
-}
-
-int main(int argc, char** argv)
-{
-  if(argc<3){execv("./tc_client", argv); return 1;}
-  setlocale(LC_ALL, "");
-  WINDOW* w=initscr();
-  signal(SIGWINCH, resizechat);
-  start_color();
-  cbreak();
-  noecho();
-  keypad(w, 1);
-  use_default_colors();
-  topic=newwin(1, COLS, 0, 0);
-  init_pair(1, COLOR_WHITE, COLOR_BLUE);
-
-  // Define colors mapped to ANSI color codes (at least the ones tc_client uses)
-  init_pair(2, COLOR_RED, -1);
-  init_pair(3, COLOR_GREEN, -1);
-  init_pair(4, COLOR_YELLOW, -1);
-  init_pair(5, COLOR_BLUE, -1);
-  init_pair(6, COLOR_MAGENTA, -1);
-  init_pair(7, COLOR_CYAN, -1);
-
-  wbkgd(topic, COLOR_PAIR(1)|' ');
-  createbuffer(0);
-  input=newwin(2, COLS, LINES-2, 0);
-  scrollok(input, 1);
-  rl_initialize();
-  rl_callback_handler_install(0, gotline);
-  rl_bind_key('\x1b', escinput);
-  rl_completion_display_matches_hook=dontprintmatches;
-  rl_completion_entry_function=completenicks;
-  wprintw(input, "> ");
-  wrefresh(topic);
-  wrefresh(input);
-  int app_in[2];
-  int app_out[2];
-  pipe(app_in);
-  pipe(app_out);
-  if(!fork())
-  {
-    close(app_in[1]);
-    close(app_out[0]);
-    dup2(app_in[0],0);
-    dup2(app_out[1],1);
-    argv[0]="./tc_client";
-    execv("./tc_client", argv);
-    _exit(1);
-  }
-  close(app_in[0]);
-  close(app_out[1]);
-  to_app=app_in[1];
-  struct pollfd p[2]={{.fd=0, .events=POLLIN, .revents=0},
-                      {.fd=app_out[0], .events=POLLIN, .revents=0}};
-  while(1)
-  {
-    poll(p, 2, -1);
-    if(p[1].revents) // Getting data from tc_client
-    {
-      p[1].revents=0;
-      char buf[1024];
-      size_t len=0;
-      while(len<1023)
-      {
-        if(read(app_out[0], &buf[len], 1)!=1){len=-1; break;}
-        if(buf[len]=='\r'||buf[len]=='\n'){break;}
-        ++len;
-      }
-      if(len==-1){break;} // Bad read
-      buf[len]=0;
-      unsigned int buffer=0;
-      if(!strncmp(buf, "Room topic: ", 12))
-      {
-        free(channeltopic);
-        channeltopic=strdup(&buf[12]);
-        drawtopic();
-      }
-      else if(!strncmp(buf, "Connection ID: ", 15)) // Our initial nickname is "guest-" plus our connection ID
-      {
-        unsigned int length=strlen(&buf[15]);
-        nickname=malloc(length+strlen("guest-")+1);
-        sprintf(nickname, "guest-%s", &(buf[15]));
-      }
-      else if(!strncmp(buf, "Currently online: ", 18))
-      {
-        // Populate the userlist
-        char* name=&buf[16];
-        while(name)
-        {
-          name=&name[2];
-          char* next=strstr(name, ", ");
-          if(next){next[0]=0;}
-          list_add(&userlist, name);
-          if(next){next[0]=',';}
-          name=next;
-        }
-      }
-      else if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
-      {
-        char* nick=&buf[8];
-        char* msg=strchr(nick, ' ');
-        if(msg[-1]==':')
-        {
-          nick=strchr(nick, 'm')+1;
-          char* nickend=&msg[-1];
-          msg=&msg[1];
-          if(!strncmp(msg, "/msg ", 5)) // message is a PM
-          {
-            char* pm=strchr(&msg[5], ' ');
-            if(!pm){waddstr(buffers[0].pad, "\npm is null!"); continue;}
-            pm=&pm[1];
-            nickend[0]=0;
-            buffer=findbuffer(nick);
-            if(!buffer){buffer=createbuffer(nick);}
-            nickend[0]=':';
-            memmove(msg, pm, strlen(pm)+1);
-            if(buffer!=currentbuf)
-            {
-              buffers[buffer].seen=0;
-              drawtopic();
-            }
-          }
-        }
-        else if(!strncmp(msg, " changed nickname to ", 21))
-        {
-          msg[0]=0;
-          // Update name in userlist
-          list_switch(&userlist, nick, &msg[21]);
-          // If it was us, keep track of the new nickname
-          if(!strcmp(nickname, nick))
-          {
-            free(nickname);
-            nickname=strdup(&msg[21]);
-          }
-          // Prevent duplicate names for buffers, and all the issues that would bring
-          unsigned int i;
-          if((i=findbuffer(&msg[21])))
-          {
-            renamebufferunique(i);
-          }
-          for(i=1; i<buffercount; ++i)
-          {
-            if(!strcmp(buffers[i].name, nick))
-            {
-              free(buffers[i].name);
-              buffers[i].name=strdup(&msg[21]);
-            }
-          }
-          msg[0]=' ';
-        }
-        else if(!strcmp(msg, " entered the channel"))
-        {
-          msg[0]=0;
-          // Add to the userlist
-          list_add(&userlist, nick);
-          msg[0]=' ';
-        }
-        else if(!strcmp(msg, " left the channel"))
-        {
-          msg[0]=0;
-          // Remove from the userlist
-          list_del(&userlist, nick);
-          msg[0]=' ';
-        }
-      }
-      waddstr(buffers[buffer].pad, "\n");
-      waddansi(buffers[buffer].pad, buf);
-      drawchat();
-      wrefresh(input);
-      continue;
-    }
-    if(!p[0].revents){continue;}
-    p[0].revents=0;
-    rl_callback_read_char();
-    drawinput();
-  }
-  rl_callback_handler_remove();
-  endwin();
-  return 0;
-}
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
deleted file mode 100644
index dd65a8d..0000000
--- a/utilities/gtk/camviewer.c
+++ /dev/null
@@ -1,888 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <unistd.h>
-#include <fcntl.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/prctl.h>
-#include <sys/wait.h>
-#include <ctype.h>
-#include <libavcodec/avcodec.h>
-#include <libswscale/swscale.h>
-#if LIBAVUTIL_VERSION_MAJOR>50 || (LIBAVUTIL_VERSION_MAJOR==50 && LIBAVUTIL_VERSION_MINOR>37)
-  #include <libavutil/imgutils.h>
-#else
-  #include <libavcore/imgutils.h>
-#endif
-#ifdef HAVE_SOUND
-  // Use libavresample if available, otherwise fall back on libswresample
-  #if HAVE_SOUND==avresample
-    #include <libavutil/opt.h>
-    #include <libavresample/avresample.h>
-  #else
-    #include <libswresample/swresample.h>
-  #endif
-  #include <ao/ao.h>
-#endif
-#include <gtk/gtk.h>
-#include <gdk/gdkkeysyms.h>
-#ifdef HAVE_V4L2
-  #include <libv4l2.h>
-  #include <linux/videodev2.h>
-#endif
-#include "userlist.h"
-#include "media.h"
-#include "compat.h"
-#include "config.h"
-#include "gui.h"
-#include "logging.h"
-#include "../stringutils.h"
-
-struct viddata
-{
-  GtkWidget* box;
-  AVCodec* vdecoder;
-  AVCodec* vencoder;
-  AVCodec* adecoder;
-  int scalewidth;
-  int scaleheight;
-#ifdef HAVE_SOUND
-  int audiopipe;
-  #if HAVE_SOUND==avresample
-    AVAudioResampleContext* resamplectx;
-  #else
-    SwrContext* swrctx;
-  #endif
-#endif
-  GtkTextBuffer* buffer; // TODO: struct buffer array, for PMs
-  GtkAdjustment* scroll;
-  GtkBuilder* gui;
-};
-
-int tc_client[2];
-int tc_client_in[2];
-const char* channel=0;
-const char* mycolor=0;
-
-void updatescaling(struct viddata* data, unsigned int width, unsigned int height)
-{
-// TODO: Move updatescaling into media.c?
-  if(!camcount){return;}
-  if(!width){width=gtk_widget_get_allocated_width(data->box);}
-  if(!height){height=gtk_widget_get_allocated_height(data->box);}
-  data->scalewidth=width/camcount;
-  // 3/4 ratio
-  data->scaleheight=data->scalewidth*3/4;
-  unsigned int i;
-  unsigned int labelsize=0;
-  for(i=0; i<camcount; ++i)
-  {
-    if(gtk_widget_get_allocated_height(cams[i].label)>labelsize)
-      labelsize=gtk_widget_get_allocated_height(cams[i].label);
-  }
-  // Fit by height
-  if(height<data->scaleheight+labelsize)
-  {
-    data->scaleheight=height-labelsize;
-    data->scalewidth=data->scaleheight*4/3;
-  }
-  if(data->scalewidth<8){data->scalewidth=8;}
-  if(data->scaleheight<1){data->scaleheight=1;}
-  // TODO: wrapping and stuff
-  // Rescale current images to fit
-  for(i=0; i<camcount; ++i)
-  {
-    GdkPixbuf* pixbuf=gtk_image_get_pixbuf(GTK_IMAGE(cams[i].cam));
-    if(!pixbuf){continue;}
-    pixbuf=gdk_pixbuf_scale_simple(pixbuf, data->scalewidth, data->scaleheight, GDK_INTERP_BILINEAR);
-    gtk_image_set_from_pixbuf(GTK_IMAGE(cams[i].cam), pixbuf);
-// TODO: figure out/fix the "static" noise that seems to happen here
-  }
-}
-
-void printchat(struct viddata* data, const char* text)
-{
-  char bottom=autoscroll_before(data->scroll);
-  // Insert new content
-  GtkTextIter end;
-  gtk_text_buffer_get_end_iter(data->buffer, &end);
-  gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
-  gtk_text_buffer_insert(data->buffer, &end, text, -1);
-  if(bottom){autoscroll_after(data->scroll);}
-}
-
-void printchat_color(struct viddata* data, const char* text, const char* color, unsigned int offset)
-{
-  char bottom=autoscroll_before(data->scroll);
-  // Insert new content
-  GtkTextIter end;
-  gtk_text_buffer_get_end_iter(data->buffer, &end);
-  gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
-  int startnum=gtk_text_iter_get_offset(&end);
-  gtk_text_buffer_insert(data->buffer, &end, text, -1);
-  // Set color if there was one
-  GtkTextIter start;
-  gtk_text_buffer_get_iter_at_offset(data->buffer, &start, startnum+offset);
-  gtk_text_buffer_apply_tag_by_name(data->buffer, color, &start, &end);
-  if(bottom){autoscroll_after(data->scroll);}
-}
-
-char buf[1024];
-gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer datap)
-{
-  int fd=g_io_channel_unix_get_fd(iochannel);
-  struct viddata* data=datap;
-  unsigned int i;
-  for(i=0; i<1023; ++i)
-  {
-    if(read(fd, &buf[i], 1)<1){printf("No more data\n"); gtk_main_quit(); return 0;}
-    if(buf[i]=='\r'||buf[i]=='\n'){break;}
-  }
-  buf[i]=0;
-  if(!strncmp(buf, "Currently online: ", 18))
-  {
-    printchat(data, buf);
-    char* next=&buf[16];
-    while(next)
-    {
-      char* nick=&next[2];
-      next=strstr(nick, ", ");
-      if(next){next[0]=0;}
-      adduser(nick);
-    }
-    return 1;
-  }
-  // Start streams once we're properly connected
-  if(!strncmp(buf, "Currently on cam: ", 18))
-  {
-    printchat(data, buf);
-    char* next=&buf[16];
-    while(next)
-    {
-      char* user=&next[2];
-      next=strstr(user, ", ");
-      if(!user[0]){continue;}
-      if(next){next[0]=0;}
-      dprintf(tc_client_in[1], "/opencam %s\n", user);
-    }
-    return 1;
-  }
-  if(!strcmp(buf, "Password required"))
-  {
-    wait(0); // Reap the previous process
-    gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(data->gui, "main")));
-    gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(data->gui, "channelpasswordwindow")));
-    return 1;
-  }
-  if(buf[0]=='/') // For the /help text
-  {
-    printchat(data, buf);
-    return 1;
-  }
-  // Remove escape codes and pick up the text color while we're at it
-  char* color=0;
-  char* esc;
-  char* escend;
-  while((esc=strchr(buf, '\x1b'))&&(escend=strchr(esc, 'm')))
-  {
-    escend[0]=0;
-    if(!color && strcmp(&esc[1], "[0")){color=strdup(&esc[1]);}
-    memmove(esc, &escend[1], strlen(&escend[1])+1);
-  }
-  char* space=strchr(buf, ' ');
-  // Timestamped events
-  if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
-  {
-    char* nick=&buf[8];
-    space=strchr(nick, ' ');
-    if(!space){return 1;}
-    if(space[-1]==':')
-    {
-// TODO: handle /msg (PMs)
-      if(config_get_bool("soundradio_cmd") && !fork())
-      {
-        execlp("sh", "sh", "-c", config_get_str("soundcmd"), (char*)0);
-        _exit(0);
-      }
-      if(!strncmp(space, " /mbs youTube ", 14) && config_get_bool("youtuberadio_cmd") && !fork())
-      {
-// TODO: store the PID and make sure it's dead before starting a new video?
-// TODO: only play videos from mods?
-        char* id=&space[14];
-        char* offset=strchr(id, ' ');
-        if(!offset){_exit(1);}
-        offset[0]=0;
-        offset=&offset[1];
-        // Handle format string
-        const char* fmt=config_get_str("youtubecmd");
-        int len=strlen(fmt)+1;
-        len+=strcount(fmt, "%i")*(strlen(id)-2);
-        len+=strcount(fmt, "%t")*(strlen(id)-2);
-        char cmd[len];
-        cmd[0]=0;
-        while(fmt[0])
-        {
-          if(!strncmp(fmt, "%i", 2)){strcat(cmd, id); fmt=&fmt[2]; continue;}
-          if(!strncmp(fmt, "%t", 2)){strcat(cmd, offset); fmt=&fmt[2]; continue;}
-          for(len=0; fmt[len] && strncmp(&fmt[len], "%i", 2) && strncmp(&fmt[len], "%t", 2); ++len);
-          strncat(cmd, fmt, len);
-          fmt=&fmt[len];
-        }
-        execlp("sh", "sh", "-c", cmd, (char*)0);
-        _exit(0);
-      }
-    }
-// TODO: handle logging PMs
-    if(config_get_bool("enable_logging")){logger_write(buf, channel, 0);}
-    // Insert new content
-    printchat_color(data, buf, color, 8);
-    if(space[-1]!=':') // Not a message
-    {
-      if(!strcmp(space, " entered the channel"))
-      {
-        space[0]=0;
-        adduser(nick);
-      }
-      else if(!strcmp(space, " left the channel"))
-      {
-        space[0]=0;
-        removeuser(nick);
-        camera_removebynick(nick);
-      }
-      else if(!strncmp(space, " changed nickname to ", 21))
-      {
-        space[0]=0;
-        renameuser(nick, &space[21]);
-        struct camera* cam=camera_findbynick(nick);
-        if(cam)
-        {
-          free(cam->nick);
-          cam->nick=strdup(&space[21]);
-          gtk_label_set_text(GTK_LABEL(cam->label), cam->nick);
-        }
-      }
-    }
-    free(color);
-    return 1;
-  }
-  if(!strcmp(buf, "Changed color") || !strncmp(buf, "Current color: ", 15))
-  {
-    printchat_color(data, buf, color, 0);
-    free((void*)mycolor);
-    mycolor=color;
-    return 1;
-  }
-  if(!strncmp(buf, "Color ", 6))
-  {
-    printchat_color(data, buf, color, 0);
-  }
-  free(color);
-  if(space && !strcmp(space, " is a moderator."))
-  {
-    space[0]=0;
-    struct user* user=finduser(buf);
-    if(user)
-    {
-      user->ismod=1;
-      renameuser(buf, buf); // Update the userlist label
-    }
-    return 1;
-  }
-  if(space && !strcmp(space, " is no longer a moderator."))
-  {
-    space[0]=0;
-    struct user* user=finduser(buf);
-    if(user){user->ismod=0;}
-    return 1;
-  }
-  // Start a stream when someone cams up
-  if(space && !strcmp(space, " cammed up"))
-  {
-    space[0]=0;
-    dprintf(tc_client_in[1], "/opencam %s\n", buf);
-    return 1;
-  }
-  if(!strncmp(buf, "Starting media stream for ", 26))
-  {
-    char* nick=&buf[26];
-    char* id=strstr(nick, " (");
-    if(!id){return 1;}
-    id[0]=0;
-    id=&id[2];
-    char* idend=strchr(id, ')');
-    if(!idend){return 1;}
-    idend[0]=0;
-    camera_removebynick(nick); // Remove any duplicates
-    struct camera* cam=camera_new();
-    cam->frame=av_frame_alloc();
-    cam->dstframe=av_frame_alloc();
-    cam->nick=strdup(nick);
-    cam->id=strdup(id);
-    cam->vctx=avcodec_alloc_context3(data->vdecoder);
-    avcodec_open2(cam->vctx, data->vdecoder, 0);
-#ifdef HAVE_SOUND
-    cam->actx=avcodec_alloc_context3(data->adecoder);
-    avcodec_open2(cam->actx, data->adecoder, 0);
-    cam->samples=0;
-#endif
-    cam->cam=gtk_image_new();
-    cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-    gtk_box_set_homogeneous(GTK_BOX(cam->box), 0);
-    gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
-    cam->label=gtk_label_new(cam->nick);
-    gtk_box_pack_start(GTK_BOX(cam->box), cam->label, 0, 0, 0);
-    gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
-    gtk_widget_show_all(cam->box);
-    updatescaling(data, 0, 0);
-    while(gtk_events_pending()){gtk_main_iteration();} // Make sure the label gets its size before we calculate scaling
-    updatescaling(data, 0, 0);
-    return 1;
-  }
-  if(!strcmp(buf, "Starting outgoing media stream"))
-  {
-    struct camera* cam=camera_new();
-    cam->frame=av_frame_alloc();
-    cam->dstframe=av_frame_alloc();
-    cam->nick=strdup("You");
-    cam->id=strdup("out");
-    cam->vctx=avcodec_alloc_context3(data->vdecoder);
-    avcodec_open2(cam->vctx, data->vdecoder, 0);
-    cam->actx=0;
-    cam->cam=gtk_image_new();
-    cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-    gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
-    cam->label=gtk_label_new(cam->nick);
-    gtk_box_pack_start(GTK_BOX(cam->box), cam->label, 0, 0, 0);
-    gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
-    gtk_widget_show_all(cam->box);
-    updatescaling(data, 0, 0);
-    while(gtk_events_pending()){gtk_main_iteration();} // Make sure the label gets its size before we calculate scaling
-    updatescaling(data, 0, 0);
-    return 1;
-  }
-  if(!strncmp(buf, "VideoEnd: ", 10))
-  {
-    camera_remove(&buf[10]);
-    updatescaling(data, 0, 0);
-    return 1;
-  }
-  if(!strncmp(buf, "Audio: ", 7))
-  {
-    char* sizestr=strchr(&buf[7], ' ');
-    if(!sizestr){return 1;}
-    sizestr[0]=0;
-    unsigned int size=strtoul(&sizestr[1], 0, 0);
-    if(!size){return 1;}
-    unsigned char frameinfo;
-    read(fd, &frameinfo, 1);
-    --size; // For the byte we read above
-    AVPacket pkt;
-    av_init_packet(&pkt);
-    unsigned char databuf[size];
-    pkt.data=databuf;
-    pkt.size=size;
-    unsigned int pos=0;
-    while(pos<size)
-    {
-      pos+=read(fd, pkt.data+pos, size-pos);
-    }
-#ifdef HAVE_SOUND
-    // Find the camera representation for the given ID (for decoder context)
-    struct camera* cam=camera_find(&buf[7]);
-    if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
-    int gotframe;
-    avcodec_decode_audio4(cam->actx, cam->frame, &gotframe, &pkt);
-    if(!gotframe){return 1;}
-  #if HAVE_SOUND==avresample
-    int outlen=avresample_convert(data->resamplectx, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples);
-  #else
-    int outlen=swr_convert(data->resamplectx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
-  #endif
-    camera_playsnd(data->audiopipe, cam, (short*)cam->frame->data[0], outlen);
-#endif
-    return 1;
-  }
-  if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); fflush(stdout); return 1;} // Ignore anything else that isn't video
-  char* sizestr=strchr(&buf[7], ' ');
-  if(!sizestr){return 1;}
-  sizestr[0]=0;
-  // Find the camera representation for the given ID
-  struct camera* cam=camera_find(&buf[7]);
-  unsigned int size=strtoul(&sizestr[1], 0, 0);
-  if(!size){return 1;}
-  // Mostly ignore the first byte (contains frame type (e.g. keyframe etc.) in 4 bits and codec in the other 4)
-  --size;
-  AVPacket pkt;
-  av_init_packet(&pkt);
-  unsigned char databuf[size+4];
-  pkt.data=databuf;
-  unsigned char frameinfo;
-  read(fd, &frameinfo, 1);
-// printf("Frametype-frame: %x\n", ((unsigned int)frameinfo&0xf0)/16);
-// printf("Frametype-codec: %x\n", (unsigned int)frameinfo&0xf);
-  unsigned int pos=0;
-  while(pos<size)
-  {
-    pos+=read(fd, pkt.data+pos, size-pos);
-  }
-  if((frameinfo&0xf)!=2){return 1;} // Not FLV1, get data but discard it
-  if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
-  pkt.size=size;
-  int gotframe;
-  avcodec_decode_video2(cam->vctx, cam->frame, &gotframe, &pkt);
-  if(!gotframe){return 1;}
-
-  // Scale and convert to RGB24 format
-  unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, data->scalewidth, data->scaleheight);
-  unsigned char buf[bufsize];
-  cam->dstframe->data[0]=buf;
-  cam->dstframe->linesize[0]=data->scalewidth*3;
-  struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, data->scalewidth, data->scaleheight, AV_PIX_FMT_RGB24, 0, 0, 0, 0);
-  sws_scale(swsctx, (const uint8_t*const*)cam->frame->data, cam->frame->linesize, 0, cam->frame->height, cam->dstframe->data, cam->dstframe->linesize);
-  sws_freeContext(swsctx);
-
-  GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, data->scalewidth, data->scaleheight, cam->dstframe->linesize[0], 0, 0);
-  gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), gdkframe);
-  // Make sure it gets redrawn in time
-  gdk_window_process_updates(gtk_widget_get_window(cam->cam), 1);
-
-  return 1;
-}
-
-#ifdef HAVE_SOUND
-void audiothread(int fd)
-{
-  ao_initialize();
-  ao_sample_format samplefmt;
-  samplefmt.bits=16;
-  samplefmt.rate=22050;
-  samplefmt.channels=1;
-  samplefmt.byte_format=AO_FMT_NATIVE; // I'm guessing libavcodec decodes it to native
-  samplefmt.matrix=0;
-  ao_option clientname={.key="client_name", .value="tc_client/camviewer", .next=0};
-  ao_device* dev=ao_open_live(ao_default_driver_id(), &samplefmt, &clientname);
-  char buf[2048];
-  size_t len;
-  while((len=read(fd, buf, 2048))>0)
-  {
-    ao_play(dev, buf, len);
-  }
-  ao_close(dev);
-}
-#endif
-
-#ifdef HAVE_V4L2
-pid_t camproc=0;
-void togglecam(GtkCheckMenuItem* item, struct viddata* data)
-{
-  if(!gtk_check_menu_item_get_active(item))
-  {
-    kill(camproc, SIGINT);
-    camproc=0;
-    dprintf(tc_client_in[1], "/camdown\n");
-    dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
-    return;
-  }
-  // Set up a second pipe to be handled by handledata() to avoid overlap with tc_client's output
-  int campipe[2];
-  pipe(campipe);
-  dprintf(tc_client_in[1], "/camup\n");
-// printf("Camming up!\n");
-  camproc=fork();
-  if(!camproc)
-  {
-    close(campipe[0]);
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
-    unsigned int delay=500000;
-    // Set up camera
-    int fd=v4l2_open("/dev/video0", O_RDWR);
-    struct v4l2_format fmt;
-    fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
-    fmt.fmt.pix.width=320;
-    fmt.fmt.pix.height=240;
-    fmt.fmt.pix.pixelformat=V4L2_PIX_FMT_RGB24;
-    fmt.fmt.pix.field=V4L2_FIELD_NONE;
-    fmt.fmt.pix.bytesperline=fmt.fmt.pix.width*3;
-    fmt.fmt.pix.sizeimage=fmt.fmt.pix.bytesperline*fmt.fmt.pix.height;
-    v4l2_ioctl(fd, VIDIOC_S_FMT, &fmt);
-    AVCodecContext* ctx=avcodec_alloc_context3(data->vencoder);
-    ctx->width=fmt.fmt.pix.width;
-    ctx->height=fmt.fmt.pix.height;
-    ctx->pix_fmt=PIX_FMT_YUV420P;
-    ctx->time_base.num=1;
-    ctx->time_base.den=10;
-    avcodec_open2(ctx, data->vencoder, 0);
-    AVFrame* frame=av_frame_alloc();
-    frame->format=PIX_FMT_RGB24;
-    frame->width=fmt.fmt.pix.width;
-    frame->height=fmt.fmt.pix.height;
-    av_image_alloc(frame->data, frame->linesize, ctx->width, ctx->height, frame->format, 1);
-    AVPacket packet;
-    packet.buf=0;
-    packet.data=0;
-    packet.size=0;
-    packet.dts=AV_NOPTS_VALUE;
-    packet.pts=AV_NOPTS_VALUE;
-
-    // Set up frame for conversion from the camera's format to a format the encoder can use
-    AVFrame* dstframe=av_frame_alloc();
-    dstframe->format=ctx->pix_fmt;
-    dstframe->width=ctx->width;
-    dstframe->height=ctx->height;
-    av_image_alloc(dstframe->data, dstframe->linesize, ctx->width, ctx->height, ctx->pix_fmt, 1);
-
-    struct SwsContext* swsctx=sws_getContext(frame->width, frame->height, PIX_FMT_RGB24, frame->width, frame->height, AV_PIX_FMT_YUV420P, 0, 0, 0, 0);
-
-    while(1)
-    {
-      usleep(delay);
-      if(delay>100000){delay-=50000;}
-      v4l2_read(fd, frame->data[0], fmt.fmt.pix.sizeimage);
-      int gotpacket;
-      sws_scale(swsctx, (const uint8_t*const*)frame->data, frame->linesize, 0, frame->height, dstframe->data, dstframe->linesize);
-      av_init_packet(&packet);
-      packet.data=0;
-packet.size=0;
-      avcodec_encode_video2(ctx, &packet, dstframe, &gotpacket);
-      unsigned char frameinfo=0x22; // Note: differentiating between keyframes and non-keyframes seems to break stuff, so let's just go with all being interframes (1=keyframe, 2=interframe, 3=disposable interframe)
-      dprintf(tc_client_in[1], "/video %i\n", packet.size+1);
-      write(tc_client_in[1], &frameinfo, 1);
-      write(tc_client_in[1], packet.data, packet.size);
-      // Also send the packet to our main thread so we can see ourselves
-      dprintf(campipe[1], "Video: out %i\n", packet.size+1);
-      write(campipe[1], &frameinfo, 1);
-      write(campipe[1], packet.data, packet.size);
-
-      av_free_packet(&packet);
-    }
-    sws_freeContext(swsctx);
-    _exit(0);
-  }
-  close(campipe[1]);
-  GIOChannel* channel=g_io_channel_unix_new(campipe[0]);
-  g_io_channel_set_encoding(channel, 0, 0);
-  g_io_add_watch(channel, G_IO_IN, handledata, data);
-}
-#endif
-
-gboolean handleresize(GtkWidget* widget, GdkEventConfigure* event, struct viddata* data)
-{
-  char bottom=autoscroll_before(data->scroll);
-  if(event->width!=gtk_widget_get_allocated_width(data->box))
-  {
-    updatescaling(data, event->width, 0);
-  }
-  if(bottom){autoscroll_after(data->scroll);}
-  return 0;
-}
-
-void handleresizepane(GObject* obj, GParamSpec* spec, struct viddata* data)
-{
-  char bottom=autoscroll_before(data->scroll);
-  updatescaling(data, 0, gtk_paned_get_position(GTK_PANED(obj)));
-  if(bottom){autoscroll_after(data->scroll);}
-}
-
-gboolean inputkeys(GtkWidget* widget, GdkEventKey* event, void* data)
-{
-  if(event->keyval==GDK_KEY_Up || event->keyval==GDK_KEY_Down){return 1;}
-  if(event->keyval==GDK_KEY_Tab)
-  {
-    // Tab completion
-    int cursor=gtk_editable_get_position(GTK_EDITABLE(widget));;
-    GtkEntryBuffer* buf=gtk_entry_get_buffer(GTK_ENTRY(widget));
-    const char* text=gtk_entry_buffer_get_text(buf);
-    unsigned int namestart=0;
-    unsigned int i;
-    for(i=0; i<cursor; ++i)
-    {
-      if(text[i]==' '){namestart=i+1;}
-    }
-    const char* matches[usercount];
-    unsigned int matchcount=0;
-    unsigned int commonlen=128;
-    for(i=0; i<usercount; ++i)
-    {
-      if(!strncmp(&text[namestart], userlist[i].nick, cursor-namestart))
-      {
-        unsigned int j;
-        for(j=0; j<matchcount; ++j)
-        {
-          if(strncmp(matches[j], userlist[i].nick, commonlen))
-          {
-            for(commonlen=0; userlist[i].nick[commonlen] && matches[j][commonlen] && userlist[i].nick[commonlen]==matches[j][commonlen]; ++commonlen);
-          }
-        }
-        matches[matchcount]=userlist[i].nick;
-        ++matchcount;
-      }
-    }
-    if(matchcount==1)
-    {
-      gtk_entry_buffer_insert_text(buf, cursor, &matches[0][cursor-namestart], -1);
-      cursor+=strlen(&matches[0][cursor-namestart]);
-      if(!namestart){gtk_entry_buffer_insert_text(buf, cursor, ": ", -1); cursor+=2;}
-      gtk_editable_set_position(GTK_EDITABLE(widget), cursor);
-    }
-    else if(matchcount>1)
-    {
-      gtk_entry_buffer_insert_text(buf, cursor, &matches[0][cursor-namestart], commonlen+namestart-cursor);
-      cursor=namestart+commonlen;
-      gtk_editable_set_position(GTK_EDITABLE(widget), cursor);
-    }
-    return 1;
-  }
-  return 0;
-}
-
-void sendmessage(GtkEntry* entry, struct viddata* data)
-{
-  const char* msg=gtk_entry_get_text(entry);
-  dprintf(tc_client_in[1], "%s\n", msg);
-  // Don't print commands
-  if(!strcmp(msg, "/help") ||
-     !strncmp(msg, "/color ", 7) ||
-     !strcmp(msg, "/color") ||
-     !strcmp(msg, "/colors") ||
-     !strncmp(msg, "/nick ", 6) ||
-//     !strncmp(msg, "/msg ", 5) || // except PM commands
-     !strncmp(msg, "/opencam ", 9) ||
-     !strncmp(msg, "/close ", 7) ||
-     !strncmp(msg, "/ban ", 5) ||
-     !strcmp(msg, "/banlist") ||
-     !strncmp(msg, "/forgive ", 9) ||
-     !strcmp(msg, "/names") ||
-     !strcmp(msg, "/mute") ||
-     !strcmp(msg, "/push2talk") ||
-     !strcmp(msg, "/camup") ||
-     !strcmp(msg, "/camdown") ||
-     !strncmp(msg, "/video ", 7) ||
-     !strncmp(msg, "/topic ", 7))
-  {
-    gtk_entry_set_text(entry, "");
-    return;
-  }
-  char text[strlen("[00:00] ")+strlen("You: ")+strlen(msg)+1];
-  time_t timestamp=time(0);
-  struct tm* t=localtime(&timestamp);
-  sprintf(text, "[%02i:%02i] ", t->tm_hour, t->tm_min);
-  sprintf(&text[8], "You: %s", msg);
-  if(config_get_bool("enable_logging")){logger_write(text, channel, 0);}
-  printchat_color(data, text, mycolor, 8);
-  gtk_entry_set_text(entry, "");
-}
-
-void startsession(GtkButton* button, struct viddata* data)
-{
-  gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(data->gui, "startwindow")));
-  gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(data->gui, "channelpasswordwindow")));
-  gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(data->gui, "main")));
-  const char* nick=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_nick")));
-  channel=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_channel")));
-  const char* chanpass=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "channelpassword")));
-  const char* acc_user=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_username")));
-  const char* acc_pass=gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_password")));
-  pipe(tc_client);
-  pipe(tc_client_in);
-  if(!fork())
-  {
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
-    close(tc_client[0]);
-    close(tc_client_in[1]);
-    dup2(tc_client[1], 1);
-    dup2(tc_client_in[0], 0);
-    if(acc_user[0])
-    {
-      execl("./tc_client", "./tc_client", "-u", acc_user, channel, nick, chanpass, (char*)0);
-    }else{
-      execl("./tc_client", "./tc_client", channel, nick, chanpass, (char*)0);
-    }
-  }
-  if(acc_user[0]){dprintf(tc_client_in[1], "%s\n", acc_pass);}
-  write(tc_client_in[1], "/color\n", 7);
-  GIOChannel* tcchannel=g_io_channel_unix_new(tc_client[0]);
-  g_io_channel_set_encoding(tcchannel, 0, 0);
-  g_io_add_watch(tcchannel, G_IO_IN, handledata, data);
-  // Remember, if asked to
-  char save=0;
-  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(data->gui, "start_rememberchan"))))
-  {
-    config_set("remember_nick", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_nick"))));
-    config_set("remember_chan", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "start_channel"))));
-    config_set("remember_chan_nick", "True");
-    save=1;
-  }
-  else if(config_get_bool("remember_chan_nick")) // Remove previously remembered info
-  {
-    config_set("remember_nick", "");
-    config_set("remember_chan", "");
-    config_set("remember_chan_nick", "False");
-    save=1;
-  }
-  // Same for account
-  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(data->gui, "start_rememberacc"))))
-  {
-    config_set("remember_username", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_username"))));
-    config_set("remember_password", gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(data->gui, "acc_password"))));
-    config_set("remember_acc", "True");
-    save=1;
-  }
-  else if(config_get_bool("remember_acc")) // Remove previously remembered info
-  {
-    config_set("remember_username", "");
-    config_set("remember_password", "");
-    config_set("remember_acc", "False");
-    save=1;
-  }
-  if(save){config_save();}
-}
-
-int main(int argc, char** argv)
-{
-  struct viddata data={0,0,0,0,0};
-  avcodec_register_all();
-  data.vdecoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
-  data.adecoder=avcodec_find_decoder(AV_CODEC_ID_NELLYMOSER);
-
-#ifdef HAVE_SOUND
-  #if HAVE_SOUND==avresample
-  data.resamplectx=avresample_alloc_context();
-  av_opt_set_int(data.resamplectx, "in_channel_layout", AV_CH_FRONT_CENTER, 0);
-  av_opt_set_int(data.resamplectx, "in_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
-  // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
-  av_opt_set_int(data.resamplectx, "in_sample_rate", 11025, 0);
-  av_opt_set_int(data.resamplectx, "out_channel_layout", AV_CH_FRONT_CENTER, 0);
-  av_opt_set_int(data.resamplectx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
-  av_opt_set_int(data.resamplectx, "out_sample_rate", 22050, 0);
-  avresample_open(data.resamplectx);
-  #else
-  data.resamplectx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, 22050, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_FLT, 11025, 0, 0);
-  swr_init(data.swrctx);
-  #endif
-  int audiopipe[2];
-  pipe(audiopipe);
-  data.audiopipe=audiopipe[1];
-  if(!fork())
-  {
-    prctl(PR_SET_PDEATHSIG, SIGHUP);
-    close(audiopipe[1]);
-    audiothread(audiopipe[0]);
-    _exit(0);
-  }
-  close(audiopipe[0]);
-#endif
-
-  gtk_init(&argc, &argv);
-  GtkBuilder* gui=gtk_builder_new_from_file("gtkgui.glade");
-  gtk_builder_connect_signals(gui, 0);
-  data.gui=gui;
-
-#ifdef HAVE_V4L2
-  GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast_camera"));
-  g_signal_connect(item, "toggled", G_CALLBACK(togglecam), &data);
-  data.vencoder=avcodec_find_encoder(AV_CODEC_ID_FLV1);
-#else
-  GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast"));
-  gtk_widget_destroy(item);
-#endif
-
-  item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_options_settings"));
-  g_signal_connect(item, "activate", G_CALLBACK(showsettings), gui);
-  
-  data.box=GTK_WIDGET(gtk_builder_get_object(gui, "cambox"));
-  userlistwidget=GTK_WIDGET(gtk_builder_get_object(gui, "userlistbox"));
-  GtkWidget* chatview=GTK_WIDGET(gtk_builder_get_object(gui, "chatview"));
-  data.scroll=gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(gtk_builder_get_object(gui, "chatscroll")));
-
-  data.buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(chatview));
-  #define colormap(code, color) gtk_text_buffer_create_tag(data.buffer, code, "foreground", color, (char*)0)
-  colormap("[31", "#821615");
-  colormap("[31;1", "#c53332");
-  colormap("[33", "#a08f23");
-  //colormap("[33", "#a78901");
-  colormap("[33;1", "#919104");
-  colormap("[32;1", "#7bb224");
-  //colormap("[32;1", "#7db257");
-  colormap("[32", "#487d21");
-  colormap("[36", "#00a990");
-  colormap("[34;1", "#32a5d9");
-  //colormap("[34;1", "#1d82eb");
-  colormap("[34", "#1965b6");
-  colormap("[35", "#5c1a7a");
-  colormap("[35;1", "#9d5bb5");
-  //colormap("[35;1", "#c356a3");
-  //colormap("[35;1", "#b9807f");
-
-  GtkWidget* panes=GTK_WIDGET(gtk_builder_get_object(gui, "vpaned"));
-  g_signal_connect(panes, "notify::position", G_CALLBACK(handleresizepane), &data);
-
-  GtkWidget* inputfield=GTK_WIDGET(gtk_builder_get_object(gui, "inputfield"));
-  g_signal_connect(inputfield, "activate", G_CALLBACK(sendmessage), &data);
-  g_signal_connect(inputfield, "key-press-event", G_CALLBACK(inputkeys), &data);
-
-  config_load();
-  // Sound
-  GtkWidget* option=GTK_WIDGET(gtk_builder_get_object(gui, "soundradio_cmd"));
-  g_signal_connect(option, "toggled", G_CALLBACK(toggle_soundcmd), gui);
-  // Logging
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "enable_logging"));
-  g_signal_connect(option, "toggled", G_CALLBACK(toggle_logging), gui);
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "save_settings"));
-  g_signal_connect(option, "clicked", G_CALLBACK(savesettings), gui);
-  // Youtube
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
-  g_signal_connect(option, "toggled", G_CALLBACK(toggle_youtubecmd), gui);
-
-  GtkWidget* window=GTK_WIDGET(gtk_builder_get_object(gui, "main"));
-  g_signal_connect(window, "configure-event", G_CALLBACK(handleresize), &data);
-
-  // Start window and channel password window signals
-  GtkWidget* button=GTK_WIDGET(gtk_builder_get_object(gui, "connectbutton"));
-  g_signal_connect(button, "clicked", G_CALLBACK(startsession), &data);
-  button=GTK_WIDGET(gtk_builder_get_object(gui, "channelpasswordbutton"));
-  g_signal_connect(button, "clicked", G_CALLBACK(startsession), &data);
-  button=GTK_WIDGET(gtk_builder_get_object(gui, "channelpassword"));
-  g_signal_connect(button, "activate", G_CALLBACK(startsession), &data);
-  GtkWidget* startwindow=GTK_WIDGET(gtk_builder_get_object(gui, "startwindow"));
-  // Set channel and nick from last session
-  if(config_get_bool("remember_chan_nick"))
-  {
-    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "start_nick")), config_get_str("remember_nick"));
-    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "start_channel")), config_get_str("remember_chan"));
-    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(gui, "start_rememberchan")), 1);
-  }
-  // Set username and password from last session
-  if(config_get_bool("remember_acc"))
-  {
-    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "acc_username")), config_get_str("remember_username"));
-    gtk_entry_set_text(GTK_ENTRY(gtk_builder_get_object(gui, "acc_password")), config_get_str("remember_password"));
-    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(gui, "start_rememberacc")), 1);
-  }
-  gtk_widget_show_all(startwindow);
-
-  gtk_main();

-  camera_cleanup();
-#ifdef HAVE_SOUND
-  #if HAVE_SOUND==avresample
-  avresample_free(&data.resamplectx);
-  #else
-  swr_free(&data.swrctx);
-  #endif
-#endif
-  return 0;
-}
diff --git a/utilities/gtk/compat.c b/utilities/gtk/compat.c
deleted file mode 100644
index 3f2efd4..0000000
--- a/utilities/gtk/compat.c
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <unistd.h>
-#include <fcntl.h>
-#include <string.h>
-#include <ctype.h>
-#include <sys/stat.h>
-#include <gtk/gtk.h>
-#include "compat.h"
-
-#if GTK_MAJOR_VERSION<3
-  GtkWidget* gtk_box_new(int vertical, int spacing)
-  {
-    if(vertical)
-    {
-      return gtk_vbox_new(1, spacing);
-    }else{
-      return gtk_hbox_new(1, spacing);
-    }
-  }
-  int gtk_widget_get_allocated_width(GtkWidget* widget)
-  {
-    GtkAllocation alloc;
-    gtk_widget_get_allocation(widget, &alloc);
-    return alloc.width;
-  }
-  int gtk_widget_get_allocated_height(GtkWidget* widget)
-  {
-    GtkAllocation alloc;
-    gtk_widget_get_allocation(widget, &alloc);
-    return alloc.height;
-  }
-  char* newline(char* line)
-  {
-    unsigned int i;
-    for(i=0; line[i] && line[i]!='\n' && line[i]!='\r'; ++i);
-    return &line[i];
-  }
-  // Hack to let us load a glade GUI designed for gtk+-3.x
-  GtkBuilder* gtk_builder_new_from_file(const char* filename)
-  {
-    struct stat st;
-    if(stat(filename, &st)){return 0;}
-    char buf[st.st_size+10];
-    int f=open(filename, O_RDONLY);
-    read(f, buf, st.st_size);
-    close(f);
-    buf[st.st_size]=0;
-    char* pos;
-    if((pos=strstr(buf, "<requires "))) // Don't require anything
-    {
-      char* end=newline(pos);
-      memmove(pos, end, strlen(end)+1);
-    }
-    // Convert orientation properties into GtkV/GtkH object types
-    char* orientation;
-    while((orientation=strstr(buf, "<property name=\"orientation\">")))
-    {
-      char dir=toupper(orientation[29]);
-      pos=newline(orientation);
-      memmove(orientation, pos, strlen(pos)+1);
-      pos=orientation;
-      while(pos>buf && strncmp(pos, "class=\"Gtk", 10)){--pos;}
-      if(pos>buf)
-      {
-        memmove(&pos[11], &pos[10], strlen(&pos[10])+1);
-        pos[10]=dir;
-      }
-    }
-    // Convert remaining GtkBoxes and GtkPaneds with the default orientation
-    while((pos=strstr(buf, "class=\"GtkBox\"")) || (pos=strstr(buf, "class=\"GtkPaned\"")))
-    {
-      memmove(&pos[11], &pos[10], strlen(&pos[10])+1);
-      pos[10]='H'; // Default is horizontal
-    }
-    GtkBuilder* gui=gtk_builder_new();
-    GError* error=0;
-    if(!gtk_builder_add_from_string(gui, buf, -1, &error)){g_error("%s\n", error->message);}
-    return gui;
-  }
-#endif
diff --git a/utilities/gtk/compat.h b/utilities/gtk/compat.h
deleted file mode 100644
index 88004d2..0000000
--- a/utilities/gtk/compat.h
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#if GTK_MAJOR_VERSION<3
-  #define GTK_ORIENTATION_HORIZONTAL 0
-  #define GTK_ORIENTATION_VERTICAL 1
-  extern GtkWidget* gtk_box_new(int vertical, int spacing);
-  extern int gtk_widget_get_allocated_width(GtkWidget* widget);
-  extern int gtk_widget_get_allocated_height(GtkWidget* widget);
-  extern GtkBuilder* gtk_builder_new_from_file(const char* filename);
-#endif
diff --git a/utilities/gtk/config.c b/utilities/gtk/config.c
deleted file mode 100644
index 7d5a813..0000000
--- a/utilities/gtk/config.c
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include "config.h"
-
-struct configitem
-{
-  const char* name;
-  char* value;
-};
-
-struct configitem* configitems=0;
-unsigned int configitemcount=0;
-
-void config_load(void)
-{
-  const char* home=getenv("HOME");
-  char filename[strlen(home)+strlen("/.config/tc_client-gtk0")];
-  sprintf(filename, "%s/.config/tc_client-gtk", home);
-  FILE* f=fopen(filename, "r");
-  if(!f){return;}
-  char buf[2048];
-  while(fgets(buf, 2048, f))
-  {
-    char* sep=strchr(buf, ':');
-    if(!sep){continue;}
-    sep[0]=0;
-    char* value=&sep[1];
-    while(value[0]==' '){value=&value[1];}
-    while((sep=strchr(value, '\r'))||(sep=strchr(value, '\n'))){sep[0]=0;}
-    ++configitemcount;
-    configitems=realloc(configitems, sizeof(struct configitem)*configitemcount);
-    configitems[configitemcount-1].name=strdup(buf);
-    configitems[configitemcount-1].value=strdup(value);
-  }
-  fclose(f);
-}
-
-void config_save(void)
-{
-  const char* home=getenv("HOME");
-  char filename[strlen(home)+strlen("/.config/tc_client-gtk0")];
-  sprintf(filename, "%s/.config", home);
-  mkdir(filename, 0700);
-  strcat(filename, "/tc_client-gtk");
-  FILE* f=fopen(filename, "w");
-  if(!f){perror("fopen(~/.config/tc_client-gtk)"); return;}
-  unsigned int i;
-  for(i=0; i<configitemcount; ++i)
-  {
-    fprintf(f, "%s: %s\n", configitems[i].name, configitems[i].value);
-  }
-  fclose(f);
-}
-
-char config_get_bool(const char* name)
-{
-  unsigned int i;
-  for(i=0; i<configitemcount; ++i)
-  {
-    if(!strcmp(configitems[i].name, name)){return !strcasecmp(configitems[i].value, "True");}
-  }
-  return 0;
-}
-
-const char* config_get_str(const char* name)
-{
-  unsigned int i;
-  for(i=0; i<configitemcount; ++i)
-  {
-    if(!strcmp(configitems[i].name, name)){return configitems[i].value;}
-  }
-  return "";
-}
-
-int config_get_int(const char* name)
-{
-  unsigned int i;
-  for(i=0; i<configitemcount; ++i)
-  {
-    if(!strcmp(configitems[i].name, name)){return atoi(configitems[i].value);}
-  }
-  return 0;
-}
-
-void config_set(const char* name, const char* value)
-{
-  unsigned int i;
-  for(i=0; i<configitemcount; ++i)
-  {
-    if(!strcmp(configitems[i].name, name))
-    {
-      free(configitems[i].value);
-      configitems[i].value=strdup(value);
-      return;
-    }
-  }
-  ++configitemcount;
-  configitems=realloc(configitems, sizeof(struct configitem)*configitemcount);
-  configitems[configitemcount-1].name=strdup(name);
-  configitems[configitemcount-1].value=strdup(value);
-}
-
-void config_set_int(const char* name, int value)
-{
-  int size=snprintf(0,0, "%i", value);
-  char buf[size+1];
-  sprintf(buf, "%i", value);
-  config_set(name, buf);
-}
diff --git a/utilities/gtk/config.h b/utilities/gtk/config.h
deleted file mode 100644
index a12032c..0000000
--- a/utilities/gtk/config.h
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-extern void config_load(void);
-extern void config_save(void);
-
-extern char config_get_bool(const char* name);
-extern const char* config_get_str(const char* name);
-extern int config_get_int(const char* name);
-
-extern void config_set(const char* name, const char* value);
-extern void config_set_int(const char* name, int value);
diff --git a/utilities/gtk/gui.c b/utilities/gtk/gui.c
deleted file mode 100644
index c59a207..0000000
--- a/utilities/gtk/gui.c
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <gtk/gtk.h>
-#include "gui.h"
-#include "config.h"
-#include "logging.h"
-
-char autoscroll_before(GtkAdjustment* scroll)
-{
-  // Figure out if we're at the bottom and should autoscroll with new content
-  int upper=gtk_adjustment_get_upper(scroll);
-  int size=gtk_adjustment_get_page_size(scroll);
-  int value=gtk_adjustment_get_value(scroll);
-  return (value+size==upper);
-}
-
-void autoscroll_after(GtkAdjustment* scroll)
-{
-  while(gtk_events_pending()){gtk_main_iteration();} // Make sure the textview's new size affects scroll's "upper" value first
-  int upper=gtk_adjustment_get_upper(scroll);
-  int size=gtk_adjustment_get_page_size(scroll);
-  gtk_adjustment_set_value(scroll, upper-size);
-}
-
-void settings_reset(GtkBuilder* gui)
-{
-  // Sound
-  GtkWidget* option=GTK_WIDGET(gtk_builder_get_object(gui, "soundradio_cmd"));
-  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("soundradio_cmd"));
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "soundcmd"));
-  gtk_entry_set_text(GTK_ENTRY(option), config_get_str("soundcmd"));
-  // Logging
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "enable_logging"));
-  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("enable_logging"));
-  GtkWidget* logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_channel"));
-  gtk_entry_set_text(GTK_ENTRY(logpath), config_get_str("logpath_channel"));
-  logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
-  gtk_entry_set_text(GTK_ENTRY(logpath), config_get_str("logpath_pm"));
-  // Youtube
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
-  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("youtuberadio_cmd"));
-  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtubecmd"));
-  gtk_entry_set_text(GTK_ENTRY(option), config_get_str("youtubecmd"));
-}
-
-void showsettings(GtkMenuItem* item, GtkBuilder* gui)
-{
-  settings_reset(gui);
-  GtkWidget* w=GTK_WIDGET(gtk_builder_get_object(gui, "settings"));
-  gtk_widget_show_all(w);
-}
-
-void savesettings(GtkButton* button, GtkBuilder* gui)
-{
-  // Sound
-  GtkWidget* soundcmd=GTK_WIDGET(gtk_builder_get_object(gui, "soundcmd"));
-  config_set("soundcmd", gtk_entry_get_text(GTK_ENTRY(soundcmd)));
-  GtkWidget* soundradio_cmd=GTK_WIDGET(gtk_builder_get_object(gui, "soundradio_cmd"));
-  config_set("soundradio_cmd", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(soundradio_cmd))?"True":"False");
-  // Logging
-  GtkWidget* logging=GTK_WIDGET(gtk_builder_get_object(gui, "enable_logging"));
-  config_set("enable_logging", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(logging))?"True":"False");
-  if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(logging))){logger_close_all();}
-  GtkWidget* logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_channel"));
-  config_set("logpath_channel", gtk_entry_get_text(GTK_ENTRY(logpath)));
-  logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
-  config_set("logpath_pm", gtk_entry_get_text(GTK_ENTRY(logpath)));
-  // Youtube
-  GtkWidget* youtubecmd=GTK_WIDGET(gtk_builder_get_object(gui, "youtubecmd"));
-  config_set("youtubecmd", gtk_entry_get_text(GTK_ENTRY(youtubecmd)));
-  GtkWidget* youtuberadio_cmd=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
-  config_set("youtuberadio_cmd", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(youtuberadio_cmd))?"True":"False");
-
-  config_save();
-  GtkWidget* settings=GTK_WIDGET(gtk_builder_get_object(gui, "settings"));
-  gtk_widget_hide(settings);
-}
-
-void toggle_soundcmd(GtkToggleButton* button, GtkBuilder* gui)
-{
-  GtkWidget* field=GTK_WIDGET(gtk_builder_get_object(gui, "soundcmd"));
-  gtk_widget_set_sensitive(field, gtk_toggle_button_get_active(button));
-}
-
-void toggle_logging(GtkToggleButton* button, GtkBuilder* gui)
-{
-  GtkWidget* field1=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_channel"));
-  GtkWidget* field2=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
-  gtk_widget_set_sensitive(field1, gtk_toggle_button_get_active(button));
-  gtk_widget_set_sensitive(field2, gtk_toggle_button_get_active(button));
-}
-
-void toggle_youtubecmd(GtkToggleButton* button, GtkBuilder* gui)
-{
-  GtkWidget* field=GTK_WIDGET(gtk_builder_get_object(gui, "youtubecmd"));
-  gtk_widget_set_sensitive(field, gtk_toggle_button_get_active(button));
-}
diff --git a/utilities/gtk/gui.h b/utilities/gtk/gui.h
deleted file mode 100644
index e4cfa68..0000000
--- a/utilities/gtk/gui.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <gtk/gtk.h>
-
-extern char autoscroll_before(GtkAdjustment* scroll);
-extern void autoscroll_after(GtkAdjustment* scroll);
-extern void settings_reset(GtkBuilder* gui);
-extern void showsettings(GtkMenuItem* item, GtkBuilder* gui);
-extern void savesettings(GtkButton* button, GtkBuilder* gui);
-extern void toggle_soundcmd(GtkToggleButton* button, GtkBuilder* gui);
-extern void toggle_logging(GtkToggleButton* button, GtkBuilder* gui);
-extern void toggle_youtubecmd(GtkToggleButton* button, GtkBuilder* gui);
diff --git a/utilities/gtk/logging.c b/utilities/gtk/logging.c
deleted file mode 100644
index 6b1a107..0000000
--- a/utilities/gtk/logging.c
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
-#include <time.h>
-#include <sys/stat.h>
-#include "config.h"
-#include "../stringutils.h"
-
-struct logfile
-{
-  FILE* f;
-  const char* path;
-};
-struct logfile* logfiles=0;
-unsigned int logfilecount=0;
-
-void logger_write(const char* line, const char* channel, const char* nick)
-{
-  const char* home=getenv("HOME");
-  const char* path=config_get_str(nick?"logpath_pm":"logpath_channel");
-  int namelen=strlen(path)+1;
-  namelen+=strcount(path, "%h")*(strlen(home)-2);
-  namelen+=strcount(path, "%c")*(strlen(channel)-2);
-  if(nick){namelen+=strcount(path, "%n")*(strlen(nick)-2);}
-  char filename[namelen];
-  filename[0]=0;
-  int len;
-  while(path[0])
-  {
-    if(!strncmp(path, "%h", 2)){strcat(filename, home); path=&path[2]; continue;}
-    if(!strncmp(path, "%c", 2)){strcat(filename, channel); path=&path[2]; continue;}
-    if(nick && !strncmp(path, "%n", 2)){strcat(filename, nick); path=&path[2]; continue;}
-    for(len=0; path[len] && strncmp(&path[len], "%h", 2) && strncmp(&path[len], "%c", 2) && (!nick||strncmp(&path[len], "%n", 2)); ++len);
-    strncat(filename, path, len);
-    path=&path[len];
-  }
-  unsigned int i;
-  for(i=0; i<logfilecount; ++i)
-  {
-    if(!strcmp(filename, logfiles[i].path)){break;}
-  }
-  if(i==logfilecount)
-  {
-    ++logfilecount;
-    logfiles=realloc(logfiles, logfilecount*sizeof(struct logfile));
-    logfiles[i].path=strdup(filename);
-    // Make sure the whole path exists
-    char* sep=filename;
-    while((sep=strchr(sep, '/')))
-    {
-      sep[0]=0;
-printf("Creating '%s' if it doesn't exist yet\n", filename);
-      mkdir(filename, 0700);
-      sep[0]='/';
-      sep=&sep[1];
-    }
-    logfiles[i].f=fopen(logfiles[i].path, "a");
-    if(!logfiles[i].f)
-    {
-      perror("fopen(logfile)");
-      free((void*)logfiles[i].path);
-      --logfilecount;
-      return;
-    }
-    fprintf(logfiles[i].f, "Opening logfile on %ti (TODO: format)\n", time(0));
-  }
-  fprintf(logfiles[i].f, "%s\n", line);
-}
-
-void logger_close_all(void)
-{
-  unsigned int i;
-  for(i=0; i<logfilecount; ++i)
-  {
-    fclose(logfiles[i].f);
-    free((void*)logfiles[i].path);
-  }
-  free(logfiles);
-  logfiles=0;
-  logfilecount=0;
-}
diff --git a/utilities/gtk/logging.h b/utilities/gtk/logging.h
deleted file mode 100644
index 76c1831..0000000
--- a/utilities/gtk/logging.h
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-
-extern void logger_write(const char* line, const char* channel, const char* nick);
-extern void logger_close_all(void);
diff --git a/utilities/gtk/media.c b/utilities/gtk/media.c
deleted file mode 100644
index e7fbb24..0000000
--- a/utilities/gtk/media.c
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <stdlib.h>
-#include <string.h>
-#include <gtk/gtk.h>
-#include <libavcodec/avcodec.h>
-#include "media.h"
-struct camera* cams=0;
-unsigned int camcount=0;
-
-#ifdef HAVE_SOUND
-// Experimental mixer, not sure if it really works
-void camera_playsnd(int audiopipe, struct camera* cam, short* samples, unsigned int samplecount)
-{
-  if(cam->samples)
-  {
-// int sources=1;
-    unsigned int i;
-    for(i=0; i<camcount; ++i)
-    {
-      if(!cams[i].samples){continue;}
-      if(cam==&cams[i]){continue;}
-      unsigned j;
-      for(j=0; j<cam->samplecount && j<cams[i].samplecount; ++j)
-      {
-        cam->samples[j]+=cams[i].samples[j];
-      }
-      free(cams[i].samples);
-      cams[i].samples=0;
-// ++sources;
-    }
-    write(audiopipe, cam->samples, cam->samplecount*sizeof(short));
-    free(cam->samples);
-// printf("Mixed sound from %i sources (cam: %p)\n", sources, cam);
-  }
-  cam->samples=malloc(samplecount*sizeof(short));
-  memcpy(cam->samples, samples, samplecount*sizeof(short));
-  cam->samplecount=samplecount;
-}
-#endif
-
-void camera_remove(const char* id)
-{
-  unsigned int i;
-  for(i=0; i<camcount; ++i)
-  {
-    if(!strcmp(cams[i].id, id))
-    {
-      gtk_widget_destroy(cams[i].box);
-      av_frame_free(&cams[i].frame);
-      avcodec_free_context(&cams[i].vctx);
-#ifdef HAVE_SOUND
-      avcodec_free_context(&cams[i].actx);
-#endif
-      free(cams[i].id);
-      free(cams[i].nick);
-      --camcount;
-      memmove(&cams[i], &cams[i+1], (camcount-i)*sizeof(struct camera));
-      break;
-    }
-  }
-}
-
-void camera_removebynick(const char* nick)
-{
-  unsigned int i;
-  for(i=0; i<camcount; ++i)
-  {
-    if(!strcmp(cams[i].nick, nick))
-    {
-      gtk_widget_destroy(cams[i].box);
-      av_frame_free(&cams[i].frame);
-      avcodec_free_context(&cams[i].vctx);
-#ifdef HAVE_SOUND
-      avcodec_free_context(&cams[i].actx);
-#endif
-      free(cams[i].id);
-      free(cams[i].nick);
-      --camcount;
-      memmove(&cams[i], &cams[i+1], (camcount-i)*sizeof(struct camera));
-      break;
-    }
-  }
-}
-
-struct camera* camera_find(const char* id)
-{
-  unsigned int i;
-  for(i=0; i<camcount; ++i)
-  {
-    if(!strcmp(cams[i].id, id)){return &cams[i];}
-  }
-  return 0;
-}
-
-struct camera* camera_findbynick(const char* nick)
-{
-  unsigned int i;
-  for(i=0; i<camcount; ++i)
-  {
-    if(!strcmp(cams[i].nick, nick)){return &cams[i];}
-  }
-  return 0;
-}
-
-struct camera* camera_new(void)
-{
-  ++camcount;
-  cams=realloc(cams, sizeof(struct camera)*camcount);
-  return &cams[camcount-1];
-}
-
-void camera_cleanup(void)
-{
-  unsigned int i;
-  for(i=0; i<camcount; ++i)
-  {
-    av_frame_free(&cams[i].frame);
-    avcodec_free_context(&cams[i].vctx);
-#ifdef HAVE_SOUND
-    avcodec_free_context(&cams[i].actx);
-#endif
-    free(cams[i].id);
-    free(cams[i].nick);
-  }
-  free(cams);
-}
diff --git a/utilities/gtk/media.h b/utilities/gtk/media.h
deleted file mode 100644
index 159e998..0000000
--- a/utilities/gtk/media.h
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
-    tc_client-gtk, a graphical user interface for tc_client
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-struct camera
-{
-  AVFrame* frame;
-  AVFrame* dstframe;
-  GtkWidget* cam;
-  AVCodecContext* vctx;
-  AVCodecContext* actx;
-  short* samples;
-  unsigned int samplecount;
-  char* id;
-  char* nick;
-  GtkWidget* box; // holds label and cam
-  GtkWidget* label;
-};
-extern struct camera* cams;
-extern unsigned int camcount;
-
-#ifdef HAVE_SOUND
-extern void camera_playsnd(int audiopipe, struct camera* cam, short* samples, unsigned int samplecount);
-#endif
-extern void camera_remove(const char* nick);
-extern void camera_removebynick(const char* nick);
-extern struct camera* camera_find(const char* id);
-extern struct camera* camera_findbynick(const char* nick);
-extern struct camera* camera_new(void);
-extern void camera_cleanup(void);
diff --git a/utilities/gtk/userlist.c b/utilities/gtk/userlist.c
deleted file mode 100644
index 89d1df5..0000000
--- a/utilities/gtk/userlist.c
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-    camviewer, a sample application to view tinychat cam streams
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <stdlib.h>
-#include <string.h>
-#include <gtk/gtk.h>
-#include "userlist.h"
-
-struct user* userlist=0;
-unsigned int usercount=0;
-GtkWidget* userlistwidget=0;
-
-struct user* finduser(const char* nick)
-{
-  unsigned int i;
-  for(i=0; i<usercount; ++i)
-  {
-    if(!strcmp(userlist[i].nick, nick)){return &userlist[i];}
-  }
-  return 0;
-}
-
-struct user* adduser(const char* nick)
-{
-  struct user* user=finduser(nick);
-  if(user){return user;} // User already existed (this might happen when running /names)
-  ++usercount;
-  userlist=realloc(userlist, sizeof(struct user)*usercount);
-  userlist[usercount-1].nick=strdup(nick);
-  userlist[usercount-1].label=gtk_label_new(nick); // TODO: some kind of menubutton for actions?
-#if GTK_MAJOR_VERSION>=3
-  gtk_widget_set_halign(userlist[usercount-1].label, GTK_ALIGN_START);
-#endif
-  userlist[usercount-1].ismod=0;
-  gtk_box_pack_start(GTK_BOX(userlistwidget), userlist[usercount-1].label, 0, 0, 0);
-  gtk_widget_show(userlist[usercount-1].label);
-  return &userlist[usercount-1];
-}
-
-void renameuser(const char* old, const char* newnick)
-{
-  struct user* user=finduser(old);
-  if(!user){return;}
-  free(user->nick);
-  user->nick=strdup(newnick);
-  if(user->ismod)
-  {
-    char newlabel[strlen(newnick)+2];
-    newlabel[0]='@';
-    strcpy(&newlabel[1], newnick);
-    gtk_label_set_text(GTK_LABEL(user->label), newlabel);
-  }else{
-    gtk_label_set_text(GTK_LABEL(user->label), newnick);
-  }
-}
-
-void removeuser(const char* nick)
-{
-  unsigned int i;
-  for(i=0; i<usercount; ++i)
-  {
-    if(!strcmp(userlist[i].nick, nick))
-    {
-      free(userlist[i].nick);
-      gtk_widget_destroy(userlist[i].label);
-      --usercount;
-      memmove(&userlist[i], &userlist[i+1], (usercount-i)*sizeof(struct user));
-      return;
-    }
-  }
-}
diff --git a/utilities/gtk/userlist.h b/utilities/gtk/userlist.h
deleted file mode 100644
index 98b886e..0000000
--- a/utilities/gtk/userlist.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
-    camviewer, a sample application to view tinychat cam streams
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-struct user
-{
-  char* nick;
-  GtkWidget* label;
-//  unsigned int id; // hm, tc_client doesn't share IDs other than in guestnicks, this might be useful for a ban-after-they-left situation
-  char ismod;
-};
-
-extern struct user* userlist;
-extern unsigned int usercount;
-extern GtkWidget* userlistwidget;
-
-extern struct user* finduser(const char* nick);
-extern struct user* adduser(const char* nick);
-extern void renameuser(const char* old, const char* newnick);
-extern void removeuser(const char* nick);
diff --git a/utilities/irchack/irchack.c b/utilities/irchack/irchack.c
index d9fc248..47289c3 100644
--- a/utilities/irchack/irchack.c
+++ b/utilities/irchack/irchack.c
@@ -24,7 +24,25 @@
 #include <sys/socket.h>
 #include <ctype.h>
 #include <signal.h>
-#include "../compat.h"
+
+#ifdef __ANDROID__
+// Android has no dprintf, so we make our own
+#include <stdarg.h>
+size_t dprintf(int fd, const char* fmt, ...)
+{
+  va_list va;
+  va_start(va, fmt);
+  int len=vsnprintf(0, 0, fmt, va);
+  va_end(va);
+  char buf[len+1];
+  va_start(va, fmt);
+  vsnprintf(buf, len+1, fmt, va);
+  va_end(va);
+  buf[len]=0;
+  write(fd, buf, len);
+  return len;
+}
+#endif
 
 // ANSI colors and their IRC equivalents
 struct color{const char* ansi; const char* irc;};
diff --git a/utilities/modbot/commands.html b/utilities/modbot/commands.html
deleted file mode 100644
index b7b8325..0000000
--- a/utilities/modbot/commands.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<html>
-<head>
-  <title>modbot commands</title>
-  <style>
-  <!--
-  th{text-align:left;}
-  -->
-  </style>
-</head>
-<body>
-  modbot is a utility to queue videos to be played, with a list of approved videos that will automatically play when requested. Below is a list of supported commands.<br /><br />
-  <table cellspacing="1" border="0">
-    <tr><th>Command</th><th>Description</th></tr>
-    <tr><td>!request &lt;link/searchterm&gt;</td><td>request a video to be played</td></tr>
-    <tr><td>!queue</td><td>get the number of songs in queue and which (if any) need to  be approved</td></tr>
-    <tr><td>!wrongrequest</td><td>undo the last request you made</td></tr>
-    <tr><td>!requestedby</td><td>see who requested the current video</td></tr>
-    <tr><td>!modstats</td><td>get a percentage of how often there are mods in the channel (aside from modbot)</td></tr>
-    <tr><td>!syncvid</td><td>synchronize video position (or see a video that was started while having youtube videos disabled in the flash client)</td></tr>
-    <tr><th colspan="2">Mod commands:</th></tr>
-    <tr><td>!playnext</td><td>play the next video in queue without approving it (to see if it's ok)</td></tr>
-    <tr><td>!approve</td><td>mark the currently playing video as good, or if none is playing the next in queue</td></tr>
-    <tr><td>!approve &lt;link/searchterm&gt;</td><td>mark the specified video as okay</td></tr>
-    <tr><td>!approve next</td><td>mark the next not yet approved video as okay</td></tr>
-    <tr><td>!approve entire queue</td><td>approve all videos in queue (for playlists)</td></tr>
-    <tr><td>!badvid</td><td>stop playing the current video and mark it as bad</td></tr>
-    <tr><td>!badvid &lt;link/searchterm&gt;</td><td>mark the specified video as bad, preventing it from ever being queued again</td></tr>
-  </table><br />
-  You can also just play videos manually/the good old way and they will be marked as good.
-</body>
-</html>
diff --git a/utilities/list.c b/utilities/modbot/list.c
similarity index 98%
rename from utilities/list.c
rename to utilities/modbot/list.c
index 707a182..bae53df 100644
--- a/utilities/list.c
+++ b/utilities/modbot/list.c
@@ -1,5 +1,5 @@
 /*
-    A simple list implementation
+    modbot, a bot for tc_client that queues and plays videos
     Copyright (C) 2015  alicia@ion.nu
 
     This program is free software: you can redistribute it and/or modify
diff --git a/utilities/list.h b/utilities/modbot/list.h
similarity index 94%
rename from utilities/list.h
rename to utilities/modbot/list.h
index 5e8e7d5..1462584 100644
--- a/utilities/list.h
+++ b/utilities/modbot/list.h
@@ -1,5 +1,5 @@
 /*
-    A simple list implementation
+    modbot, a bot for tc_client that queues and plays videos
     Copyright (C) 2015  alicia@ion.nu
 
     This program is free software: you can redistribute it and/or modify
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index bb193b8..87155b9 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -26,7 +26,7 @@
 #include <stdarg.h>
 #include <time.h>
 #include <termios.h>
-#include "../list.h"
+#include "list.h"
 #include "queue.h"
 
 struct list mods={0,0};
@@ -34,24 +34,9 @@ struct queue queue={0,0};
 struct list goodvids={0,0}; // pre-approved videos
 struct list badvids={0,0}; // not allowed, essentially banned
 char* playing=0;
-char* requester=0;
 time_t started=0;
 int tc_client;
 
-time_t time_with_mods=0;
-time_t time_modcount;
-char havemods=0;
-void timemods(void)
-{
-  time_t now=time(0);
-  if(havemods)
-  {
-    time_with_mods+=now-time_modcount;
-  }
-  time_modcount=now;
-  havemods=(mods.itemcount>1); // Not counting modbot as a mod
-}
-
 void say(const char* pm, const char* fmt, ...)
 {
   va_list va;
@@ -71,51 +56,32 @@ void say(const char* pm, const char* fmt, ...)
   write(tc_client, buf, strlen(buf));
 }
 
-void getvidinfo(const char* vid, const char* type, char* buf, char* errbuf, unsigned int len)
+void getvidinfo(const char* vid, const char* type, char* buf, unsigned int len)
 {
   int out[2];
-  int err[2];
   pipe(out);
-  pipe(err);
   if(!fork())
   {
     close(out[0]);
-    close(err[0]);
     dup2(out[1], 1);
-    dup2(err[1], 2);
     execlp("youtube-dl", "youtube-dl", "--default-search", "auto", type, "--", vid, (char*)0);
     perror("execlp(youtube-dl)");
     _exit(1);
   }
   wait(0);
   close(out[1]);
-  close(err[1]);
-  size_t r;
-  // Read output
-  r=read(out[0], buf, len-1);
-  if(r<0){r=0;}
-  while(r>0 && (buf[r-1]=='\r' || buf[r-1]=='\n')){--r;} // Strip newlines
-  buf[r]=0;
+  len=read(out[0], buf, len-1);
+  if(len<0){len=0;}
+  while(len>0 && (buf[len-1]=='\r' || buf[len-1]=='\n')){--len;} // Strip newlines
+  buf[len]=0;
   close(out[0]);
-  // Read any error messages
-  if(errbuf)
-  {
-    r=read(err[0], errbuf, len-1);
-    if(r<0){r=0;}
-    while(r>0 && (errbuf[r-1]=='\r' || errbuf[r-1]=='\n')){--r;} // Strip newlines
-    errbuf[r]=0;
-    char* newline; // No need for newlines in error messages
-    while((newline=strchr(errbuf, '\n'))){newline[0]=' ';}
-    while((newline=strchr(errbuf, '\r'))){newline[0]=' ';}
-  }
-  close(err[0]);
 }
 
 unsigned int getduration(const char* vid)
 {
   char timebuf[128];
   timebuf[0]=':'; // Sacrifice 1 byte to avoid having to deal with a special case later on, where no ':' is found and we go from the start of the string, but only once
-  getvidinfo(vid, "--get-duration", &timebuf[1], 0, 127);
+  getvidinfo(vid, "--get-duration", &timebuf[1], 127);
   if(!timebuf[1]){printf("Failed to get video duration using youtube-dl, assuming 60s\n"); return 60;} // If using youtube-dl fails, assume videos are 1 minute long
   // youtube-dl prints it out in hh:mm:ss format, convert it to plain seconds
   unsigned int len;
@@ -139,7 +105,7 @@ void playnextvid()
 {
   waitskip=0;
   playing=queue.items[0].video;
-  requester=queue.items[0].requester;
+  free(queue.items[0].requester);
   free(queue.items[0].title);
   --queue.itemcount;
   memmove(queue.items, &queue.items[1], sizeof(struct queueitem)*queue.itemcount);
@@ -152,15 +118,13 @@ void playnextvid()
 void playnext(int x)
 {
   free(playing);
-  free(requester);
   playing=0;
-  requester=0;
   if(queue.itemcount<1){alarm(0); printf("Nothing more to play\n"); return;} // Nothing more to play
   if(!list_contains(&goodvids, queue.items[0].video))
   {
     if(!waitskip)
     {
-      say(0, "Next video (%s, %s) is not yet approved by mods\n", queue.items[0].video, queue.items[0].title);
+      say(0, "Next video (http://youtube.com/watch?v=%s) is not yet approved by mods\n", queue.items[0].video);
       unsigned int i;
       for(i=1; i<queue.itemcount; ++i)
       {
@@ -183,51 +147,6 @@ void playnext(int x)
 
 int main(int argc, char** argv)
 {
-  // Handle arguments (-d, -l, -h additions, the rest are handled by tc_client)
-  char daemon=0;
-  char* logfile=0;
-  char verbose=0;
-  unsigned int i;
-  for(i=1; i<argc; ++i)
-  {
-    if(!strcmp(argv[i], "-d") || !strcmp(argv[i], "--daemon"))
-    {
-      daemon=1;
-      // Remove non-tc_client argument
-      --argc;
-      memmove(&argv[i], &argv[i+1], sizeof(char*)*(argc-i));
-      argv[argc]=0;
-      --i;
-    }
-    else if(i+1<argc && (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--log")))
-    {
-      logfile=argv[i+1];
-      // Remove non-tc_client argument
-      argc-=2;
-      memmove(&argv[i], &argv[i+2], sizeof(char*)*(argc-i));
-      argv[argc]=0;
-      --i;
-    }
-    else if(!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose"))
-    {
-      verbose=1;
-      // Remove non-tc_client argument
-      --argc;
-      memmove(&argv[i], &argv[i+1], sizeof(char*)*(argc-i));
-      argv[argc]=0;
-      --i;
-    }
-    else if(!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help"))
-    {
-      printf("Additional options for modbot:\n"
-             "-d/--daemon     = daemonize after startup\n"
-             "-l/--log <file> = log output into <file>\n"
-             "-v/--verbose    = print/log all incoming messages\n"
-             "\n");
-      execv("./tc_client", argv);
-      return 1;
-    }
-  }
   int in[2];
   int out[2];
   pipe(in);
@@ -249,8 +168,6 @@ int main(int argc, char** argv)
   list_load(&badvids, "badvids.txt");
   char buf[1024];
   int len=0;
-  time_t sessionstart=time(0);
-  time_modcount=sessionstart;
   while(1)
   {
     if(read(out[0], &buf[len], 1)<1){break;}
@@ -283,22 +200,7 @@ int main(int argc, char** argv)
       memmove(esc, &esc[len+1], strlen(&esc[len]));
     }
     len=0;
-    // Note: daemonizing and setting up logging here to avoid interfering with account password entry
-    if(daemon)
-    {
-      if(fork()){return 0;}
-      daemon=0;
-      if(!logfile){logfile="/dev/null";} // Prevent writing to stdout as a daemon
-    }
-    if(logfile)
-    {
-      int f=open(logfile, O_WRONLY|O_CREAT|O_APPEND, 0600);
-      dup2(f, 1);
-      dup2(f, 2);
-      close(f);
-      logfile=0;
-    }
-    if(verbose){printf("Got line '%s'\n", buf); fflush(stdout);}
+    // printf("Got line '%s'\n", buf);
     char* space=strchr(buf, ' ');
     if(!space){continue;}
     if(!strcmp(space, " is a moderator."))
@@ -306,14 +208,12 @@ int main(int argc, char** argv)
       // If there are not-yet-approved videos in the queue when a mod joins, ask them to review them
       space[0]=0;
       list_add(&mods, buf);
-      timemods();
       continue;
     }
     if(!strcmp(space, " is no longer a moderator."))
     {
       space[0]=0;
       list_del(&mods, buf);
-      timemods();
       continue;
     }
     space[0]=0;
@@ -339,24 +239,19 @@ int main(int argc, char** argv)
         {
           char title[256];
           char vid[1024];
-          char viderr[1024];
-          getvidinfo(&msg[9], "--get-id", vid, viderr, 1024);
-          if(!vid[0]) // Nothing found
-          {
-            say(pm, "No video found, sorry (%s)\n", viderr);
-            continue;
-          }
+          getvidinfo(&msg[9], "--get-id", vid, 1024);
+          if(!vid[0]){say(pm, "No video found, sorry\n"); continue;} // Nothing found
           char* plist;
           for(plist=vid; plist[0] && plist[0]!='\r' && plist[0]!='\n'; plist=&plist[1]);
           if(plist[0]) // Link was a playlist, do some trickery to get the title of the first video (instead of getting nothing)
           {
             strcpy(title, "Playlist, starting with ");
             plist[0]=0;
-            getvidinfo(vid, "--get-title", &title[24], 0, 256-24);
+            getvidinfo(vid, "--get-title", &title[24], 256-24);
             plist[0]='\n';
           }else{
             plist=0;
-            getvidinfo(vid, "--get-title", title, 0, 256);
+            getvidinfo(vid, "--get-title", title, 256);
           }
           printf("Requested ID '%s' by '%s'\n", vid, nick);
           // Check if it's already queued and mention which spot it's in, or if it's marked as bad and shouldn't be queued
@@ -402,20 +297,14 @@ int main(int argc, char** argv)
             if(!strcmp(queue.items[i].requester, nick))
             {
               queue_del(&queue, queue.items[i].video);
-              if(!playing && i==0){playnext(0);}
-              i=1; // distinguish from just having reached the front of the queue
               break;
             }
           }
-          if(!i)
-          {
-            say(pm, "I can't find any request by you, sorry\n");
-          }
         }
         else if(!strncmp(msg, "!wrongrequest ", 14))
         {
           char vid[1024];
-          getvidinfo(&msg[14], "--get-id", vid, 0, 1024);
+          getvidinfo(&msg[14], "--get-id", vid, 1024);
           unsigned int i;
           for(i=0; i<queue.itemcount; ++i)
           {
@@ -443,29 +332,22 @@ int main(int argc, char** argv)
           {
             char buf[len];
             buf[0]=0;
-            unsigned int listed=0;
             for(i=0; i<queue.itemcount; ++i)
             {
-              if(listed<5 && !list_contains(&goodvids, queue.items[i].video))
+              if(!list_contains(&goodvids, queue.items[i].video))
               {
                 if(buf[0]){strcat(buf, ", ");}
                 strcat(buf, queue.items[i].video);
                 strcat(buf, " (");
                 strcat(buf, queue.items[i].title);
                 strcat(buf, ")");
-                ++listed;
               }
             }
-            say(pm, "%u video%s in queue, %u of which are not yet approved by mods (%s%s)\n", queue.itemcount, (queue.itemcount==1)?"":"s", notapproved, buf, (listed<notapproved)?", etc.":"");
+            say(pm, "%u videos in queue, %u of which are not yet approved by mods (%s)\n", queue.itemcount, notapproved, buf);
           }else{
-            say(pm, "%u video%s in queue\n", queue.itemcount, (queue.itemcount==1)?"":"s");
+            say(pm, "%u videos in queue\n", queue.itemcount);
           }
         }
-        else if(!strcmp(msg, "!requestedby"))
-        {
-          if(!playing){say(pm, "Nothing is playing\n");}
-          else{say(pm, "%s requested %s\n", requester, playing);}
-        }
         else if(!strcmp(msg, "!time")) // Debugging
         {
           unsigned int remaining=alarm(0);
@@ -474,40 +356,31 @@ int main(int argc, char** argv)
         }
         else if(!strcmp(msg, "!help"))
         {
-          say(pm, "http://tc_client.ion.nu/misc/modbotcommands.html\n");
-        }
-        else if(!strcmp(msg, "!modstats"))
-        {
-          unsigned int session=time(0)-sessionstart;
-          timemods();
-          unsigned int hasmods=time_with_mods*100/session;
-          const char* timeformat="seconds";
-          if(session>=120)
-          {
-            session/=60;
-            timeformat="minutes";
-            if(session>=120)
-            {
-              session/=60;
-              timeformat="hours";
-              if(session>=48)
-              {
-                session/=24;
-                timeformat="days";
-              }
-            }
-          }
-          say(pm, "The channel has had mods %u%% of the time for the past %u %s\n", hasmods, session, timeformat);
-        }
-        else if(!strcmp(msg, "!syncvid"))
-        {
-          if(playing)
-          {
-            space[0]=0;
-            say(0, "/priv %s /mbs youTube %s %u\n", nick, playing, (time(0)-started)*1000);
-          }else{
-            say(pm, "Nothing is playing\n");
-          }
+          say(nick, "The following commands can be used:\n");
+          usleep(500000);
+          say(nick, "!request <link> = request a video to be played\n");
+          usleep(500000);
+          say(nick, "!queue          = get the number of songs in queue and which (if any) need to  be approved\n");
+          usleep(500000);
+          say(nick, "Mod commands:\n"); // TODO: don't bother filling non-mods' chats with these?
+          usleep(500000);
+          say(nick, "!playnext       = play the next video in queue without approving it (to see if it's ok)\n");
+          usleep(500000);
+          say(nick, "!approve        = mark the currently playing video as good, or if none is playing the next in queue\n");
+          usleep(500000);
+          say(nick, "!approve <link> = mark the specified video as okay\n");
+          usleep(500000);
+          say(nick, "!approve next   = mark the next not yet approved video as okay\n");
+          usleep(500000);
+          say(nick, "!approve entire queue = approve all videos in queue (for playlists)\n");
+          usleep(500000);
+          say(nick, "!badvid         = stop playing the current video and mark it as bad\n");
+          usleep(500000);
+          say(nick, "!badvid <link>  = mark the specified video as bad, preventing it from ever being queued again\n");
+          usleep(500000);
+          say(nick, "!wrongrequest   = undo the last request you made\n");
+          usleep(500000);
+          say(nick, "You can also just play videos manually/the good old way and they will be marked as good.\n");
         }
         else if(list_contains(&mods, nick)) // Mods-only commands
         {
@@ -568,13 +441,13 @@ int main(int argc, char** argv)
               {
                 list_save(&goodvids, "goodvids.txt");
                 if(!playing){playnext(0);} // Next in queue just got approved, so play it
-                else{say(pm, "Queue approved. Make sure none of the videos were inappropriate\n");}
+                else{say(pm, "Queue approved\n");}
               }else{
-                say(pm, "The queue is already approved (or empty)\n");
+                say(0, "%s: there is nothing in the queue that isn't already approved, please do not overuse this function\n", nick);
               }
               continue;
             }else{
-              getvidinfo(vid, "--get-id", vidbuf, 0, 256);
+              getvidinfo(vid, "--get-id", vidbuf, 256);
               vid=vidbuf;
             }
             list_add(&goodvids, vid);
@@ -590,24 +463,22 @@ int main(int argc, char** argv)
             char vid[1024];
             if(space && space[1])
             {
-              getvidinfo(&space[1], "--get-id", vid, 0, 256);
+              getvidinfo(&space[1], "--get-id", vid, 256);
             }else{strncpy(vid, playing, 1023); vid[1023]=0;}
-            if(!vid[0]){say(pm, "Video not found, sorry\n"); continue;}
+            if(!vid[0]){say(pm, "Video not found, sorry\n");}
             queue_del(&queue, vid);
             list_del(&goodvids, vid);
             list_add(&badvids, vid);
             list_save(&goodvids, "goodvids.txt");
             list_save(&badvids, "badvids.txt");
-            if(playing && !strcmp(vid, playing)){say(0, "/mbc youTube\n");}
-            say(pm, "Marked '%s' as bad, it will not be allowed into the queue again. You can reverse this by !approve'ing the video by ID/link/name\n", vid);
-            playnext(0);
+            if(!strcmp(vid, playing)){say(0, "/mbc youTube\n"); playnext(0);}
           }
           else if(!strcmp(msg, "!skip") || !strncmp(msg, "!skip ", 6))
           {
             unsigned int num=((msg[5]&&msg[6])?strtoul(&msg[6], 0, 0):1);
             if(num<1){say(pm, "The given value evaluates to 0, please specify the number of videos you would like to skip (or if you do not specify it will default to 1)\n"); continue;}
             if(playing){free(playing); playing=0; --num; say(0, "/mbc youTube\n");}
-            while(num>0&&queue.itemcount>0)
+            while(num>0)
             {
               queue_del(&queue, queue.items[0].video);
               --num;
@@ -625,18 +496,11 @@ int main(int argc, char** argv)
             list_save(&goodvids, "goodvids.txt");
             free(playing);
             playing=strdup(vid);
-            free(requester);
-            requester=strdup(nick);
             unsigned int pos=(end?(strtol(&end[1], 0, 0)/1000):0);
             alarm(getduration(playing)-pos);
             started=time(0)-pos;
           }
-          else if(!strcmp(msg, "/mbc youTube") && playing) // Video cancelled
-          {
-            list_del(&goodvids, playing); // manual /mbc is often used when !badvid should be used, so at least remove it from the list of approved videos
-            list_save(&goodvids, "goodvids.txt");
-            playnext(0);
-          }
+          else if(!strcmp(msg, "/mbc youTube")){playnext(0);} // Video cancelled
           else if(!strncmp(msg, "/mbsk youTube ", 14)) // Seeking
           {
             unsigned int pos=strtol(&msg[14], 0, 0)/1000;
@@ -673,12 +537,6 @@ int main(int argc, char** argv)
             say(0, "/priv %s /mbs youTube %s %u\n", nick, playing, (time(0)-started)*1000);
           }
         }
-        else if(!strcmp(space, " left the channel"))
-        {
-          space[0]=0;
-          list_del(&mods, nick); // Absent people can't be mods
-          timemods();
-        }
       }
     }
   }
diff --git a/utilities/stringutils.c b/utilities/stringutils.c
deleted file mode 100644
index e7d33b4..0000000
--- a/utilities/stringutils.c
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
-    A few simple string utilities
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-#include <string.h>
-
-int strcount(const char* haystack, const char* needle)
-{
-  int c=0;
-  haystack=strstr(haystack, needle);
-  while(haystack)
-  {
-    ++c;
-    haystack=strstr(&haystack[1], needle);
-  }
-  return c;
-}
diff --git a/utilities/stringutils.h b/utilities/stringutils.h
deleted file mode 100644
index c942bfe..0000000
--- a/utilities/stringutils.h
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
-    A few simple string utilities
-    Copyright (C) 2015  alicia@ion.nu
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as published by
-    the Free Software Foundation, version 3 of the License.
-
-    This program 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 Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-*/
-extern int strcount(const char* haystack, const char* needle);