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

    Version 0.27

diff --git a/ChangeLog b/ChangeLog
index 9dfa01b..d591221 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+0.27:
+Added some more compatibility hacks for android.
+Added support for video broadcasting (adding /camup and /video <length> followed by the encoded data for each frame)
+crossbuild.sh: build ncurses and readline for the target platform.
+cursedchat: disable readline's match-listing, which does not work well with the curses interface.
+cursedchat: made tab completion work with nicknames.
 0.26:
 modbot: fixed a segfault when trying to skip more videos than what's in the queue.
 modbot: fixed a segfault when receiving a manual /mbc command but no video is playing.
diff --git a/Makefile b/Makefile
index da7c8da..4480abe 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,14 @@
-VERSION=0.26
+VERSION=0.27
 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
+IRCHACK_OBJ=utilities/irchack/irchack.o utilities/compat.o
+MODBOT_OBJ=utilities/modbot/modbot.o utilities/list.o utilities/modbot/queue.o
 CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
-CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o
+CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o utilities/compat.o utilities/list.o
 UTILS=irchack modbot
 ifdef GTK_LIBS
 ifdef AVCODEC_LIBS
@@ -21,6 +21,9 @@ ifdef SWSCALE_LIBS
     CFLAGS+=-DHAVE_SOUND $(SWRESAMPLE_CFLAGS) $(AO_CFLAGS)
   endif
   endif
+  ifdef LIBV4L2_LIBS
+    CFLAGS+=-DHAVE_V4L2 $(LIBV4L2_CFLAGS)
+  endif
 endif
 endif
 endif
@@ -28,7 +31,7 @@ endif
 ifdef CURSES_LIBS
 ifdef READLINE_LIBS
   UTILS+=cursedchat
-  CFLAGS+=$(CURSES_CFLAGS)
+  CFLAGS+=$(CURSES_CFLAGS) $(READLINE_CFLAGS)
 endif
 endif
 
@@ -44,7 +47,7 @@ modbot: $(MODBOT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) -o $@
 
 camviewer: $(CAMVIEWER_OBJ)
- $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(SWRESAMPLE_LIBS) $(AO_LIBS) -o $@
+ $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(SWRESAMPLE_LIBS) $(AO_LIBS) $(LIBV4L2_LIBS) -o $@
 
 cursedchat: $(CURSEDCHAT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) $(READLINE_LIBS) $(CURSES_LIBS) -o $@
@@ -53,4 +56,4 @@ clean:
  rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) tc_client irchack modbot camviewer cursedchat
 
 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 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 utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h 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/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h configure
diff --git a/README b/README
index bc658a0..438e4b5 100644
--- a/README
+++ b/README
@@ -14,19 +14,17 @@ Commands supported by tc_client:
 /ban <nick>     = ban someone
 /banlist        = list who is banned
 /forgive <nick/ID> = unban someone
+/camup          = open an audio/video stream for broadcasting your video
+/video <length> = send a <length> bytes long encoded frame, send the frame data after this line
 /help           = list these commands at runtime
 
-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 (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)
-*camviewer = an example application for viewing cam streams
+*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
+*cursedchat = a curses chat interface
diff --git a/client.c b/client.c
index 3a3b5fa..48af852 100644
--- a/client.c
+++ b/client.c
@@ -120,7 +120,7 @@ char* gethost(char *channel, char *password)
   return host;
 }
 
-char* getkey(char* id, char* channel)
+char* getkey(const char* id, const char* channel)
 {
   char url[strlen("http://tinychat.com/api/captcha/check.php?guest%5Fid=&room=tinychat%5E0")+strlen(id)+strlen(channel)];
   sprintf(url, "http://tinychat.com/api/captcha/check.php?guest%%5Fid=%s&room=tinychat%%5E%s", id, channel);
@@ -142,6 +142,23 @@ char* getkey(char* id, char* channel)
   return key;
 }
 
+char* getbroadcastkey(const char* channel, const char* nick)
+{
+  unsigned int id=idlist_get(nick);
+  char url[strlen("http://tinychat.com/api/broadcast.pw?name=&site=tinychat&nick=&id=0")+128+strlen(channel)+strlen(nick)];
+  sprintf(url, "http://tinychat.com/api/broadcast.pw?name=%s&site=tinychat&nick=%s&id=%u", channel, nick, id);
+  char* response=http_get(url, 0);
+  char* key=strstr(response, " token='");
+
+  if(!key){return 0;}
+  key+=8;
+  char* keyend=strchr(key, '\'');
+  if(!keyend){return 0;}
+  key=strndup(key, keyend-key);
+  free(response);
+  return key;
+}
+
 char* getmodkey(const char* user, const char* pass, const char* channel, char* loggedin)
 {
   // TODO: if possible, do this in a neater way than digging the key out from an HTML page.
@@ -415,7 +432,9 @@ int main(int argc, char** argv)
                  "/forgive <nick/ID> = unban someone\n"
                  "/names          = list everyone that is online\n"
                  "/mute           = temporarily mutes all non-moderator broadcasts\n"
-                 "/push2talk      = puts all non-operators in push2talk mode\n");
+                 "/push2talk      = puts all non-operators in push2talk mode\n"
+                 "/camup          = open an audio/video stream for broadcasting your video\n"
+                 "/video <length> = send a <length> bytes long encoded frame, send the frame data after this line\n");
           fflush(stdout);
         }
         else if(!strncmp(buf, "/color", 6) && (!buf[6]||buf[6]==' '))
@@ -556,6 +575,29 @@ int main(int argc, char** argv)
           amfsend(&amf, sock);
           continue;
         }
+        else if(!strcmp(buf, "/camup"))
+        {
+          // Retrieve and send the key for broadcasting access
+          char* key=getbroadcastkey(channel, nickname);
+          amfinit(&amf, 3);
+          amfstring(&amf, "bauth");
+          amfnum(&amf, 0);
+          amfnull(&amf);
+          amfstring(&amf, key);
+          amfsend(&amf, sock);
+          free(key);
+          // Initiate stream
+          streamout_start(idlist_get(nickname), sock);
+          continue;
+        }
+        else if(!strncmp(buf, "/video ", 7)) // Send video data
+        {
+          size_t len=strtoul(&buf[7],0,10);
+          char buf[len];
+          fullread(0, buf, len);
+          stream_sendvideo(sock, buf, len);
+          continue;
+        }
       }
       char* msg=tonumlist(buf, len);
       amfinit(&amf, 3);
diff --git a/configure b/configure
index 28bb16d..5bff5c5 100755
--- a/configure
+++ b/configure
@@ -132,4 +132,14 @@ else
 fi
 rm -f readlinetest.c readlinetest
 
+printf 'Checking for libv4l2... '
+libs="`pkg-config --libs libv4l2 2> /dev/null`"
+if [ "x$libs" != "x" ]; then
+  echo "LIBV4L2_LIBS=${libs}" >> config.mk
+  echo "LIBV4L2_CFLAGS=`pkg-config --cflags libv4l2`" >> config.mk
+  echo yes
+else
+  echo no
+fi
+
 echo Done
diff --git a/crossbuild.sh b/crossbuild.sh
index 481b401..84893a4 100755
--- a/crossbuild.sh
+++ b/crossbuild.sh
@@ -5,8 +5,35 @@ if [ "$host" = "" ]; then
   if [ "$host" = "" ]; then host='i386-pc-linux-gnu'; fi
   echo "No target host specified (argv[1]), defaulting to ${host}"
 fi
-./configure --host="$host"
 here="`pwd`"
+export PATH="${here}/curlprefix/bin:${here}/ncursesprefix/bin:${PATH}"
+if [ ! -e ncursesprefix ]; then
+  wget -c http://ftp.gnu.org/gnu/ncurses/ncurses-5.9.tar.gz
+  tar -xzf ncurses-5.9.tar.gz
+  cd ncurses-5.9
+  mkdir build
+  cd build
+  # Some hackery to build ncursesw5-config, which seems to get disabled along with --disable-database
+  ../configure --prefix="${here}/ncursesprefix" --host="${host}" --enable-static --disable-shared --with-termlib --enable-widec --without-cxx ac_cv_header_locale_h=no
+  mv Makefile Makefile.tmp
+  ../configure --prefix="${here}/ncursesprefix" --host="${host}" --enable-static --disable-shared --with-termlib --enable-widec --without-cxx --disable-database --with-fallbacks=linux ac_cv_header_locale_h=no
+  mv Makefile.tmp Makefile
+  make
+  make install
+  cd "$here"
+  cp ncursesprefix/lib/libtinfow.a ncursesprefix/lib/libtinfo.a
+fi
+if [ ! -e readlineprefix ]; then
+  wget -c http://ftp.gnu.org/gnu/readline/readline-6.3.tar.gz
+  tar -xzf readline-6.3.tar.gz
+  cd readline-6.3
+  mkdir build
+  cd build
+  ../configure --prefix="${here}/readlineprefix" --host="${host}" --enable-static --disable-shared bash_cv_wcwidth_broken=no
+  make
+  make install
+  cd "$here"
+fi
 if [ ! -e curlprefix ]; then
   wget -c http://curl.haxx.se/download/curl-7.40.0.tar.bz2
   tar -xjf curl-7.40.0.tar.bz2
@@ -18,6 +45,7 @@ if [ ! -e curlprefix ]; then
   make install
   cd "$here"
 fi
+./configure --host="$host" > config.log 2>&1
 if grep -q 'LIBS+=-liconv' config.mk && [ ! -e iconvprefix ]; then
   wget -c http://ftp.gnu.org/gnu/libiconv/libiconv-1.14.tar.gz
   tar -xzf libiconv-1.14.tar.gz
@@ -29,8 +57,9 @@ if grep -q 'LIBS+=-liconv' config.mk && [ ! -e iconvprefix ]; then
   make install
   cd "$here"
 fi
-export PATH="${here}/curlprefix/bin:${PATH}"
 echo "CFLAGS+=-I${here}/iconvprefix/include" >> config.mk
 echo "LDFLAGS+=-L${here}/iconvprefix/lib" >> config.mk
+echo "READLINE_CFLAGS=-I${here}/readlineprefix/include" >> config.mk
+echo "READLINE_LIBS=-L${here}/readlineprefix/lib -lreadline" >> config.mk
 make
 make utils
diff --git a/media.c b/media.c
index a4e7bf3..649c2f7 100644
--- a/media.c
+++ b/media.c
@@ -44,6 +44,7 @@ void stream_start(const char* nick, int sock) // called upon privmsg "/opencam .
   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");
@@ -53,6 +54,24 @@ void stream_start(const char* nick, int sock) // called upon privmsg "/opencam .
   printf("Starting media stream for %s (%u)\n", nick, userid);
 }
 
+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");
+}
+
 void stream_play(struct amf* amf, int sock) // called upon _result
 {
   unsigned int i;
@@ -62,12 +81,13 @@ void stream_play(struct amf* amf, int sock) // called upon _result
     {
       struct rtmp amf;
       amfinit(&amf, 8);
-      amfstring(&amf, "play");
+      amfstring(&amf, streams[i].outgoing?"publish":"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;
@@ -118,3 +138,22 @@ void stream_handlestatus(struct amf* amf)
     }
   }
 }
+
+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;
+    }
+  }
+}
diff --git a/media.h b/media.h
index 728c3dd..71893cf 100644
--- a/media.h
+++ b/media.h
@@ -20,12 +20,15 @@ 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_play(struct amf* amf, int sock); // called upon _result
 extern void stream_handledata(struct rtmp* rtmp);
 extern void stream_handlestatus(struct amf* amf);
+extern void stream_sendvideo(int sock, void* buf, size_t len);
diff --git a/numlist.c b/numlist.c
index b6b15e5..e39aad6 100644
--- a/numlist.c
+++ b/numlist.c
@@ -31,7 +31,12 @@ char* fromnumlist(char* in, size_t* outlen)
     ++len;
     x=&x[1];
   }
+#ifdef __ANDROID__
+  *outlen=len;
+  unsigned char* string=malloc(len+1);
+#else
   unsigned short string[len+1];
+#endif
   int i;
   for(i=0; i<len; ++i)
   {
@@ -41,6 +46,9 @@ char* fromnumlist(char* in, size_t* outlen)
   }
   string[len]=0;
 
+#ifdef __ANDROID__
+  return string;
+#else
   iconv_t cd=iconv_open("", "utf-16");
   char* outbuf=malloc(len*4);
   char* i_out=outbuf;
@@ -53,10 +61,14 @@ char* fromnumlist(char* in, size_t* outlen)
   iconv_close(cd);
   *outlen-=remaining;
   return outbuf;
+#endif
 }
 
 char* tonumlist(char* i_in, size_t len)
 {
+#ifdef __ANDROID__
+  #define in i_in
+#else
   iconv_t cd=iconv_open("utf-16le", "");
   unsigned short in[len+1];
   char* i_out=(char*)in;
@@ -64,6 +76,7 @@ char* tonumlist(char* i_in, size_t len)
   while(outlen>0 && len>0 && iconv(cd, &i_in, &len, &i_out, &outlen)>0);
   iconv_close(cd);
   len=((void*)i_out-(void*)in)/2;
+#endif
 
   char* out=malloc(len*strlen("65535,"));
   out[0]=0;
diff --git a/rtmp.c b/rtmp.c
index 035ff7a..2956d12 100644
--- a/rtmp.c
+++ b/rtmp.c
@@ -84,8 +84,9 @@ char rtmp_get(int sock, struct rtmp* rtmp)
   if(fmt<3)
   {
     // Timestamp
-    fullread(sock, &x, 3);
-    chunk->timestamp=x;
+    x=0;
+    fullread(sock, ((void*)&x)+1, 3);
+    chunk->timestamp=be32(x);
     if(fmt<2)
     {
       // Length
@@ -166,16 +167,17 @@ void rtmp_send(int sock, struct rtmp* rtmp)
   {
     write(sock, &rtmp->msgid, sizeof(rtmp->msgid));
   }
-  // Send 128 bytes at a time separated by 0xc3 (because apparently that's something RTMP requires)
+  // Send 128 bytes at a time separated by a "continuation header", the 0xc3 byte for chunk 3
   void* pos=rtmp->buf;
   unsigned int len=rtmp->length;
+  basicheader|=0xc0; // Turn it into a "continuation header" by setting format to 3
   while(len>0)
   {
     int w;
     if(len>128)
     {
       w=write(sock, pos, 128);
-      w+=write(sock, "\xc3", 1);
+      w+=write(sock, &basicheader, 1);
       len-=128;
     }else{
       w=write(sock, pos, len);
diff --git a/rtmp.h b/rtmp.h
index 6c03274..aa91445 100644
--- a/rtmp.h
+++ b/rtmp.h
@@ -33,5 +33,6 @@ struct rtmp
   void* buf;
 };
 
+extern size_t fullread(int fd, void* buf, size_t len);
 extern char rtmp_get(int sock, struct rtmp* rtmp);
 extern void rtmp_send(int sock, struct rtmp* rtmp);
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index 43a8094..df8ad53 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -16,13 +16,23 @@
 */
 #include <unistd.h>
 #include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
 #include <libavcodec/avcodec.h>
 #include <libswscale/swscale.h>
+#include <libavutil/imgutils.h>
 #ifdef HAVE_SOUND
-#include <libswresample/swresample.h>
-#include <ao/ao.h>
+// TODO: use libavresample instead if available
+  #include <libswresample/swresample.h>
+  #include <ao/ao.h>
 #endif
 #include <gtk/gtk.h>
+#undef HAVE_V4L2 // Not working yet, something is wrong with the frames making the encoding break (for keyframes in particular and I don't know why)
+#ifdef HAVE_V4L2
+  #include <libv4l2.h>
+  #include <linux/videodev2.h>
+#endif
 
 #if GTK_MAJOR_VERSION==2
   #define GTK_ORIENTATION_HORIZONTAL 0
@@ -58,6 +68,7 @@ struct viddata
   unsigned int camcount;
   GtkWidget* box;
   AVCodec* vdecoder;
+//  AVCodec* vencoder;
   AVCodec* adecoder;
 #ifdef HAVE_SOUND
   int audiopipe;
@@ -164,6 +175,27 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     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))
   {
     for(i=0; i<data->camcount; ++i)
@@ -212,7 +244,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     {
       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]); free(pkt.data); return 1;}
+    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;}
@@ -249,7 +281,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
     pos+=read(tc_client[0], 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]); free(pkt.data); return 1;}
+  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);
@@ -295,6 +327,85 @@ void audiothread(int fd)
 }
 #endif
 
+#ifdef HAVE_V4L2
+void camup(GtkWidget* button, struct viddata* data)
+{
+  dprintf(tc_client_in[1], "/camup\n");
+  gtk_widget_destroy(button); // Only once
+// printf("Camming up!\n");
+  if(!fork())
+  {
+    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=0x20;
+// if(packet.flags&AV_PKT_FLAG_KEY){printf("Sending keyframe!\n");}
+      if(packet.flags&AV_PKT_FLAG_KEY){frameinfo|=0x01;}else{frameinfo|=0x02;}
+      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(tc_client[1], "Video: out %i\n", packet.size+1);
+      write(tc_client[1], &frameinfo, 1);
+      write(tc_client[1], packet.data, packet.size);
+
+      av_free_packet(&packet);
+    }
+    sws_freeContext(swsctx);
+    _exit(0);
+  }
+}
+#endif
+
 int main(int argc, char** argv)
 {
   struct viddata data={0,0,0,0,0};
@@ -310,6 +421,7 @@ int main(int argc, char** argv)
   data.audiopipe=audiopipe[1];
   if(!fork())
   {
+    prctl(PR_SET_PDEATHSIG, SIGHUP);
     close(audiopipe[1]);
     audiothread(audiopipe[0]);
     _exit(0);
@@ -322,12 +434,19 @@ int main(int argc, char** argv)
   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(camup), &data);
+  gtk_box_pack_start(GTK_BOX(data.box), cambutton, 0, 0, 0);
+#endif
   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);
@@ -335,7 +454,6 @@ int main(int argc, char** argv)
     argv[0]="./tc_client";
     execv("./tc_client", argv);
   }
-  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);
diff --git a/utilities/compat.c b/utilities/compat.c
new file mode 100644
index 0000000..e620559
--- /dev/null
+++ b/utilities/compat.c
@@ -0,0 +1,35 @@
+/*
+    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
new file mode 100644
index 0000000..2cfb39b
--- /dev/null
+++ b/utilities/compat.h
@@ -0,0 +1,21 @@
+/*
+    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/cursedchat.c b/utilities/cursedchat/cursedchat.c
index 42545f5..8a30f75 100644
--- a/utilities/cursedchat/cursedchat.c
+++ b/utilities/cursedchat/cursedchat.c
@@ -19,9 +19,12 @@
 #include <poll.h>
 #include <signal.h>
 #include <sys/ioctl.h>
+#include <termios.h>
 #include <locale.h>
 #include <curses.h>
 #include <readline/readline.h>
+#include "../compat.h"
+#include "../list.h"
 #include "buffer.h"
 
 #define HALFSCREEN (LINES>4?(LINES-3)/2:1)
@@ -30,6 +33,7 @@ WINDOW* topic;
 char* channeltopic;
 WINDOW* input;
 int to_app;
+struct list userlist={0,0};
 
 // Translate ANSI escape codes to curses commands and write the text to a window
 void waddansi(WINDOW* w, char* str)
@@ -284,6 +288,31 @@ void resizechat(int sig)
   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;}
@@ -313,6 +342,8 @@ int main(int argc, char** argv)
   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);
@@ -358,6 +389,20 @@ int main(int argc, char** argv)
         channeltopic=strdup(&buf[12]);
         drawtopic();
       }
+      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];
@@ -387,6 +432,8 @@ int main(int argc, char** argv)
         else if(!strncmp(msg, " changed nickname to ", 21))
         {
           msg[0]=0;
+          // Update name in userlist
+          list_switch(&userlist, nick, &msg[21]);
           unsigned int i;
           // Prevent duplicate names for buffers, and all the issues that would bring
           if((i=findbuffer(&msg[21])))
@@ -403,6 +450,20 @@ int main(int argc, char** argv)
           }
           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);
diff --git a/utilities/irchack/irchack.c b/utilities/irchack/irchack.c
index 47289c3..d9fc248 100644
--- a/utilities/irchack/irchack.c
+++ b/utilities/irchack/irchack.c
@@ -24,25 +24,7 @@
 #include <sys/socket.h>
 #include <ctype.h>
 #include <signal.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
+#include "../compat.h"
 
 // ANSI colors and their IRC equivalents
 struct color{const char* ansi; const char* irc;};
diff --git a/utilities/modbot/list.c b/utilities/list.c
similarity index 100%
rename from utilities/modbot/list.c
rename to utilities/list.c
diff --git a/utilities/modbot/list.h b/utilities/list.h
similarity index 100%
rename from utilities/modbot/list.h
rename to utilities/list.h
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index e9d1111..3e84a77 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};
@@ -405,7 +405,7 @@ int main(int argc, char** argv)
             }
             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.":"");
           }else{
-            say(pm, "%u videos in queue\n", queue.itemcount);
+            say(pm, "%u video%s in queue\n", queue.itemcount, (queue.itemcount==1)?"":"s");
           }
         }
         else if(!strcmp(msg, "!requestedby"))