$ git clone http://tcclient.ion.nu/tc_client.git
commit e2b0a27f9138fdc09288835943f526cf6221dafb
Author: Alicia <...>
Date:   Sat Dec 17 10:40:23 2016 +0100

    tc_client-gtk: added support for viewing and approving greenroom cameras.

diff --git a/ChangeLog b/ChangeLog
index 9ea198b..bc4dad3 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -25,6 +25,7 @@ tc_client-gtk: added an icon to mark moderators in the user list.
 tc_client-gtk: disable the input field and the broadcast menu when in "lurker" mode.
 tc_client-gtk: added an option to hide join/quit/nickname notifications.
 tc_client-gtk: made the user list sorted.
+tc_client-gtk: added support for viewing and approving greenroom cameras.
 dist/appimage.sh: fix audio in appimages by building ffmpeg with support for nellymoser and speex, and depending on the system's libao and libpulse instead of including it in the appimage.
 libcamera(escapi): handle failure to open camera more gracefully.
 irchack: pass along "<user> cammed up" notifications.
diff --git a/Makefile b/Makefile
index 7116b3d..e667f06 100644
--- a/Makefile
+++ b/Makefile
@@ -16,7 +16,7 @@ IRCHACK_OBJ=utilities/irchack/irchack.o utilities/compat.o
 MODBOT_OBJ=utilities/modbot/modbot.o utilities/list.o utilities/modbot/queue.o utilities/compat.o
 CAMVIEWER_OBJ=utilities/camviewer/camviewer.o utilities/compat.o utilities/compat_av.o libcamera.a
 CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o utilities/compat.o utilities/list.o
-TC_CLIENT_GTK_OBJ=utilities/gtk/camviewer.o utilities/gtk/userlist.o utilities/gtk/media.o utilities/gtk/compat.o utilities/gtk/configfile.o utilities/gtk/gui.o utilities/stringutils.o utilities/gtk/logging.o utilities/gtk/postproc.o utilities/compat.o utilities/compat_av.o utilities/gtk/inputhistory.o utilities/gtk/playmedia.o libcamera.a
+TC_CLIENT_GTK_OBJ=utilities/gtk/camviewer.o utilities/gtk/userlist.o utilities/gtk/media.o utilities/gtk/compat.o utilities/gtk/configfile.o utilities/gtk/gui.o utilities/stringutils.o utilities/gtk/logging.o utilities/gtk/postproc.o utilities/compat.o utilities/compat_av.o utilities/gtk/inputhistory.o utilities/gtk/playmedia.o utilities/gtk/greenroom.o libcamera.a
 LIBCAMERA_OBJ=utilities/libcamera/camera.o utilities/libcamera/camera_img.o
 UTILS=irchack modbot
 CONFINFO=|Will enable the IRC utility irchack|Will enable the bot utility modbot
@@ -153,7 +153,7 @@ SOURCES+=utilities/irchack/irchack.c
 SOURCES+=utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/modbot/commands.html
 SOURCES+=utilities/camviewer/camviewer.c
 SOURCES+=utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h
-SOURCES+=utilities/gtk/camviewer.c utilities/gtk/userlist.c utilities/gtk/media.c utilities/gtk/compat.c utilities/gtk/configfile.c utilities/gtk/gui.c utilities/gtk/logging.c utilities/gtk/postproc.c utilities/gtk/inputhistory.c utilities/gtk/playmedia.c utilities/gtk/main.h utilities/gtk/userlist.h utilities/gtk/media.h utilities/gtk/compat.h utilities/gtk/configfile.h utilities/gtk/gui.h utilities/gtk/logging.h utilities/gtk/postproc.h utilities/gtk/inputhistory.h utilities/gtk/playmedia.h gtkgui.glade
+SOURCES+=utilities/gtk/camviewer.c utilities/gtk/userlist.c utilities/gtk/media.c utilities/gtk/compat.c utilities/gtk/configfile.c utilities/gtk/gui.c utilities/gtk/logging.c utilities/gtk/postproc.c utilities/gtk/inputhistory.c utilities/gtk/playmedia.c utilities/gtk/greenroom.c utilities/gtk/main.h utilities/gtk/userlist.h utilities/gtk/media.h utilities/gtk/compat.h utilities/gtk/configfile.h utilities/gtk/gui.h utilities/gtk/logging.h utilities/gtk/postproc.h utilities/gtk/inputhistory.h utilities/gtk/playmedia.h utilities/gtk/greenroom.h gtkgui.glade
 SOURCES+=utilities/gtk/gencamplaceholder.sh utilities/gtk/camplaceholder.xcf utilities/gtk/spinnerdot.xcf utilities/gtk/modicon.xcf
 SOURCES+=utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h utilities/stringutils.c utilities/stringutils.h utilities/compat_av.c utilities/compat_av.h
 SOURCES+=utilities/libcamera/camera.c utilities/libcamera/camera.h utilities/libcamera/camera_v4l2.c utilities/libcamera/camera_v4l2.h utilities/libcamera/camera_img.c utilities/libcamera/camera_img.h utilities/libcamera/camera_escapi.cpp utilities/libcamera/camera_escapi.h utilities/libcamera/camera_x11.c utilities/libcamera/camera_x11.h
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index d05b45a..d42cbef 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -60,6 +60,7 @@
 #include "../stringutils.h"
 #include "inputhistory.h"
 #include "playmedia.h"
+#include "greenroom.h"
 #include "main.h"
 
 struct viddata
@@ -75,8 +76,8 @@ int tc_client_in[2];
 const char* channel=0;
 const char* mycolor=0;
 char* nickname=0;
+char hasgreenroom=0;
 char frombuild=0; // Running from the build directory
-#define TC_CLIENT (frombuild?"./tc_client":"tc_client")
 #ifdef _WIN32
   PROCESS_INFORMATION coreprocess={.hProcess=0};
 #endif
@@ -143,9 +144,9 @@ void printchat(const char* text, const char* color, unsigned int offset, const c
 }
 
 unsigned int cameventsource=0;
-char buf[1024];
 gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
 {
+  static char buf[1024];
   gsize r;
   unsigned int i;
   for(i=0; i<1023; ++i)
@@ -160,14 +161,6 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     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]);
-    if(!cam){return 1;}
-    if(!cam->vctx)
-    {
-      cam->vctx=avcodec_alloc_context3(data->vdecoder);
-      avcodec_open2(cam->vctx, data->vdecoder, 0);
-    }
     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)
@@ -176,6 +169,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     av_init_packet(&pkt);
     unsigned char databuf[size+4];
     pkt.data=databuf;
+    pkt.size=size;
     unsigned char frameinfo;
     g_io_channel_read_chars(iochannel, (gchar*)&frameinfo, 1, 0, 0);
 //   printf("Frametype-frame: %x\n", ((unsigned int)frameinfo&0xf0)/16);
@@ -187,33 +181,10 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
       pos+=r;
     }
     if((frameinfo&0xf)!=2){return 1;} // Not FLV1, get data but discard it
+    // Find the camera representation for the given ID
+    struct camera* cam=camera_find(&buf[7]);
     if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
-    pkt.size=size;
-    int gotframe;
-    avcodec_send_packet(cam->vctx, &pkt);
-    gotframe=avcodec_receive_frame(cam->vctx, cam->frame);
-    if(gotframe){return 1;}
-
-    if(cam->placeholder) // Remove the placeholder animation if it has it
-    {
-      g_source_remove(cam->placeholder);
-      cam->placeholder=0;
-    }
-    // Scale and convert to RGB24 format
-    unsigned int bufsize=av_image_get_buffer_size(AV_PIX_FMT_RGB24, camsize_scale.width, camsize_scale.height, 1);
-    unsigned char* buf=malloc(bufsize);
-    cam->dstframe->data[0]=buf;
-    cam->dstframe->linesize[0]=camsize_scale.width*3;
-    struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, camsize_scale.width, camsize_scale.height, AV_PIX_FMT_RGB24, SWS_BICUBIC, 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);
-    postprocess(&cam->postproc, cam->dstframe->data[0], camsize_scale.width, camsize_scale.height);
-
-    GdkPixbuf* oldpixbuf=gtk_image_get_pixbuf(GTK_IMAGE(cam->cam));
-    GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, camsize_scale.width, camsize_scale.height, cam->dstframe->linesize[0], freebuffer, 0);
-    volume_indicator(gdkframe, cam);
-    gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), gdkframe);
-    if(oldpixbuf){g_object_unref(oldpixbuf);}
+    camera_decode(cam, &pkt, camsize_scale.width, camsize_scale.height);
     return 1;
   }
   if(!strncmp(buf, "Audio: ", 7))
@@ -242,23 +213,29 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     struct camera* cam=camera_find(&buf[7]);
     if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
     if(!cam->actx && camera_init_audio(cam, frameinfo)){return 1;}
-    int gotframe;
-    avcodec_send_packet(cam->actx, &pkt);
-    gotframe=avcodec_receive_frame(cam->actx, cam->frame);
-    if(gotframe){return 1;}
-    camera_calcvolume(cam, (float*)cam->frame->data[0], cam->frame->nb_samples);
-    unsigned int samplecount=cam->frame->nb_samples*SAMPLERATE_OUT/cam->samplerate;
-    int16_t outbuf[samplecount];
-    void* outdata[]={outbuf, 0};
-  #ifdef HAVE_AVRESAMPLE
-    int outlen=avresample_convert(cam->resamplectx, (void*)outdata, samplecount*sizeof(uint8_t), samplecount, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples);
-  #else
-    int outlen=swr_convert(cam->swrctx, (void*)outdata, samplecount, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
-  #endif
-    if(outlen>0){camera_playsnd(cam, outbuf, outlen);}
+    mic_decode(cam, &pkt);
 #endif
     return 1;
   }
+  if(!strncmp(buf, "Nickname of connection ", 23))
+  {
+    char* nick=strstr(&buf[23], ": ");
+    if(nick)
+    {
+      nick[0]=0;
+      if(greenroom_gotnick(&buf[23], &nick[2]))
+      {
+        char buf[strlen(&nick[2])+strlen(" is waiting in the greenroom0")];
+        strcpy(buf, &nick[2]);
+        strcat(buf, " is waiting in the greenroom");
+        printchat(buf, 0, 0, 0);
+      }else{
+        nick[0]=':';
+        printchat(buf, 0, 0, 0);
+      }
+    }
+    return 1;
+  }
   if(!strncmp(buf, "Currently online: ", 18))
   {
     printchat(buf, 0, 0, 0);
@@ -279,6 +256,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     unsigned int length=strlen(&buf[15]);
     nickname=malloc(length+strlen("guest-")+1);
     sprintf(nickname, "guest-%s", &(buf[15]));
+    if(hasgreenroom){greenroom_join(&buf[15]);}
     return 1;
   }
   if(!strncmp(buf, "Captcha: ", 9))
@@ -479,6 +457,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
           free(nickname);
           nickname=strdup(&space[21]);
         }
+        greenroom_changenick(nick, &space[21]);
       }
     }
     free(color);
@@ -530,14 +509,14 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     if(!idend){return 1;}
     idend[0]=0;
     camera_remove(nick, 1); // Remove any duplicates
-    struct camera* cam=camera_new(nick, id);
+    struct camera* cam=camera_new(nick, id, CAMFLAG_NONE);
     updatescaling(0, 0, 1);
     gtk_widget_show_all(cam->box);
     return 1;
   }
   if(!strcmp(buf, "Starting outgoing media stream"))
   {
-    struct camera* cam=camera_new(nickname, "out");
+    struct camera* cam=camera_new(nickname, "out", CAMFLAG_NONE);
     cam->vctx=avcodec_alloc_context3(data->vencoder);
     cam->vctx->pix_fmt=AV_PIX_FMT_YUV420P;
     cam->vctx->time_base.num=1;
@@ -577,6 +556,11 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     gui_disableinputs();
     return 1;
   }
+  if(!strcmp(buf, "Channel has greenroom"))
+  {
+    hasgreenroom=1;
+    return 1;
+  }
   if(!strcmp(buf, "Server disconnected"))
   {
     printchat(buf, 0, 0, 0);
@@ -831,7 +815,8 @@ void sendmessage(GtkEntry* entry, void* x)
      !strcmp(msg, "/camup") ||
      !strcmp(msg, "/camdown") ||
      !strncmp(msg, "/video ", 7) ||
-     !strncmp(msg, "/topic ", 7))
+     !strncmp(msg, "/topic ", 7) ||
+     !strncmp(msg, "/getnick ", 9))
   {
     gtk_entry_set_text(entry, "");
     sendingmsg=0;
@@ -981,6 +966,7 @@ void captcha_done(GtkWidget* button, void* x)
   gtk_widget_hide(GTK_WIDGET(gtk_builder_get_object(gui, "captcha")));
   gtk_widget_show(GTK_WIDGET(gtk_builder_get_object(gui, "main")));
   write(tc_client_in[1], "\n", 1);
+  if(greenroompipe_in[1]>-1){write(greenroompipe_in[1], "\n", 1);}
 }
 
 #ifndef _WIN32
diff --git a/utilities/gtk/greenroom.c b/utilities/gtk/greenroom.c
new file mode 100644
index 0000000..77c6e68
--- /dev/null
+++ b/utilities/gtk/greenroom.c
@@ -0,0 +1,362 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2016  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 <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#ifndef NO_PRCTL
+  #include <sys/prctl.h>
+#endif
+#include <glib.h>
+#include <gtk/gtk.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
+#include "compat.h"
+#include "gui.h"
+#include "main.h"
+#include "configfile.h"
+#include "userlist.h"
+#include "greenroom.h"
+
+int greenroompipe[2];
+int greenroompipe_in[2]={-1,-1};
+// TODO: Handle outgoing cam, sending to greenroom if we don't have the broadcast password yet
+// TODO: Option to show greenroom as non-mod
+
+struct greenmap
+{
+  const char* id;
+  const char* nick;
+  const char* camid;
+};
+static struct greenmap* users=0;
+static unsigned int usercountgr=0;
+static GtkWidget* menu=0;
+static GtkWidget* menuitem=0;
+
+static void greenroom_updatecount(void)
+{
+  // Ignore any leftover users that we didn't find the nickname for
+  unsigned int count=0;
+  unsigned int i;
+  for(i=0; i<usercountgr; ++i){count+=!!users[i].nick;}
+  if(count)
+  {
+    char buf[snprintf(0, 0, "Greenroom (%u)", count)+1];
+    sprintf(buf, "Greenroom (%u)", count);
+    gtk_menu_item_set_label(GTK_MENU_ITEM(menuitem), buf);
+  }else{
+    gtk_menu_item_set_label(GTK_MENU_ITEM(menuitem), "Greenroom");
+  }
+}
+
+static void greenroom_remove(const char* id, char cam)
+{
+  unsigned int i;
+  for(i=0; i<usercountgr; ++i)
+  {
+    const char* str=(cam?users[i].camid:users[i].id);
+    if(str && !strcmp(str, id))
+    {
+      free((void*)users[i].id);
+      free((void*)users[i].nick);
+      free((void*)users[i].camid);
+      --usercountgr;
+      memmove(&users[i], &users[i+1], sizeof(struct greenmap)*(usercountgr-i));
+      greenroom_updatecount();
+      return;
+    }
+  }
+}
+
+static void greenroom_allow(GtkWidget* menuitem, void* x)
+{
+  unsigned int i;
+  for(i=0; i<usercountgr; ++i)
+  {
+    if(!strcmp(users[i].id, x))
+    {
+      dprintf(tc_client_in[1], "/allow %s\n", users[i].nick);
+    }
+  }
+}
+
+static gboolean greenroom_handleline(GIOChannel* iochannel, GIOCondition condition, gpointer x)
+{
+  static char buf[1024];
+  gsize r;
+  unsigned int i;
+  for(i=0; i<1023; ++i)
+  {
+    g_io_channel_read_chars(iochannel, &buf[i], 1, &r, 0);
+    if(r<1){printf("No more data\n"); gtk_main_quit(); return 0;}
+    if(buf[i]=='\r'||buf[i]=='\n'){break;}
+  }
+  buf[i]=0;
+  char* space=strchr(buf, ' ');
+  if(!strncmp(buf, "Video: ", 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;}
+    // 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;
+    pkt.size=size;
+    unsigned char frameinfo;
+    g_io_channel_read_chars(iochannel, (gchar*)&frameinfo, 1, 0, 0);
+    unsigned int pos=0;
+    while(pos<size)
+    {
+      g_io_channel_read_chars(iochannel, (gchar*)pkt.data+pos, size-pos, &r, 0);
+      pos+=r;
+    }
+    if((frameinfo&0xf)!=2){return 1;} // Not FLV1, get data but discard it
+    // Find the camera representation for the given ID
+    struct camera* cam=camera_find(&buf[7]);
+    if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
+    camera_decode(cam, &pkt, 160, 120);
+    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;
+    g_io_channel_read_chars(iochannel, (gchar*)&frameinfo, 1, 0, 0);
+    --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)
+    {
+      g_io_channel_read_chars(iochannel, (gchar*)pkt.data+pos, size-pos, &r, 0);
+      pos+=r;
+    }
+#ifdef HAVE_LIBAO
+    if(gtk_widget_is_visible(menu)) // Only play audio when looking at the greenroom
+    {
+      // 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;}
+      if(!cam->actx && camera_init_audio(cam, frameinfo)){return 1;}
+      mic_decode(cam, &pkt);
+    }
+#endif
+    return 1;
+  }
+  if(!strncmp(buf, "VideoEnd: ", 10))
+  {
+    // TODO: Deal with potential collisions with cams in the main channel
+    camera_remove(&buf[10], 0);
+    greenroom_remove(&buf[10], 1);
+    return 1;
+  }
+  // For both "currently on cam" and "cammed up", add to a list and reply with a "/getnick", whose response will complete the item and then we can announce it
+  if(space && menu && !strcmp(space, " cammed up"))
+  {
+    // Find out who this cam belongs to
+    space[0]=0;
+    dprintf(tc_client_in[1], "/getnick %s\n", buf);
+    ++usercountgr;
+    users=realloc(users, sizeof(struct greenmap)*usercountgr);
+    users[usercountgr-1].id=strdup(buf);
+    users[usercountgr-1].nick=0;
+    users[usercountgr-1].camid=0;
+    return 1;
+  }
+  if(menu && !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], "/getnick %s\n", user);
+      ++usercountgr;
+      users=realloc(users, sizeof(struct greenmap)*usercountgr);
+      users[usercountgr-1].id=strdup(user);
+      users[usercountgr-1].nick=0;
+      users[usercountgr-1].camid=0;
+    }
+    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_remove(nick, 1); // Remove any duplicates
+    struct camera* cam=camera_new(nick, id, CAMFLAG_GREENROOM);
+    unsigned int i;
+    for(i=0; i<usercountgr; ++i)
+    {
+      if(!strcmp(users[i].id, nick))
+      {
+        cam->label=gtk_label_new(users[i].nick);
+        users[i].camid=strdup(id);
+      }
+    }
+    // Add it to the greenroom menu
+    cam->box=gtk_menu_item_new();
+    GtkWidget* box=gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+    gtk_box_pack_start(GTK_BOX(box), cam->cam, 0, 0, 0);
+    gtk_box_pack_start(GTK_BOX(box), cam->label, 0, 0, 0);
+    gtk_container_add(GTK_CONTAINER(cam->box), box);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), cam->box);
+    g_signal_connect(cam->box, "activate", G_CALLBACK(greenroom_allow), cam->nick);
+    gtk_widget_show_all(cam->box);
+    greenroom_updatecount();
+    return 1;
+  }
+  if(!strncmp(buf, "Captcha: ", 9))
+  {
+    // If we're a mod, don't bother with the captcha (since we're only here to look at cams)
+    if(user_ismod(nickname))
+    {
+      write(greenroompipe_in[1], "\n", 1);
+    }else{
+      gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(gui, "captcha")));
+      char link[snprintf(0,0,"Captcha: <a href=\"%s\">%s</a>", &buf[9], &buf[9])+1];
+      sprintf(link, "Captcha: <a href=\"%s\">%s</a>", &buf[9], &buf[9]);
+      gtk_label_set_markup(GTK_LABEL(gtk_builder_get_object(gui, "captcha_link")), link);
+    }
+    return 1;
+  }
+  if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
+  {
+    space=strchr(&buf[8], ' ');
+    if(!strcmp(space, " left the channel"))
+    {
+      space[0]=0;
+      camera_remove(&buf[8], 1);
+      greenroom_remove(&buf[8], 0);
+    }
+    return 1;
+  }
+  return 1;
+}
+
+char greenroom_gotnick(const char* id, const char* nick)
+{
+  unsigned int i;
+  for(i=0; i<usercountgr; ++i)
+  {
+    if(!strcmp(users[i].id, id))
+    {
+      free((void*)users[i].nick); // just in case
+      users[i].nick=strdup(nick);
+      dprintf(greenroompipe_in[1], "/opencam %s\n", id);
+      return 1;
+    }
+  }
+  return 0;
+}
+
+void greenroom_join(const char* id)
+{
+  // Only add greenroom menu if mod (or optionally see greenroom anyway?)
+  if(user_ismod(nickname))
+  {
+    menuitem=gtk_menu_item_new_with_label("Greenroom");
+    gtk_menu_shell_append(GTK_MENU_SHELL(gtk_builder_get_object(gui, "menubar")), menuitem);
+    menu=gtk_menu_new();
+    gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
+    gtk_widget_show_all(menuitem);
+    gtk_widget_show_all(menu);
+  }
+#ifdef _WIN32
+  char cmd[strlen("./tc_client --greenroom --cookies tinychat_no_account.cookie  0")+strlen(channel)+strlen(id)];
+  strcpy(cmd, "./tc_client --greenroom ");
+  if(config_get_bool("storecookies"))
+  {
+    strcat(cmd, "--cookies tinychat_no_account.cookie");
+  }
+  strcat(cmd, channel);
+  strcat(cmd, " ");
+  strcat(cmd, id);
+  w32_runcmdpipes(cmd, greenroompipe_in, greenroompipe, coreprocess);
+#else
+  pipe(greenroompipe);
+  pipe(greenroompipe_in);
+  if(!fork())
+  {
+    prctl(PR_SET_PDEATHSIG, SIGHUP);
+    close(greenroompipe[0]);
+    close(greenroompipe_in[1]);
+    dup2(greenroompipe[1], 1);
+    dup2(greenroompipe_in[0], 0);
+    if(config_get_bool("storecookies"))
+    {
+      const char* home=getenv("HOME");
+
+      char filename[strlen(home)+strlen("/.config/tc_client-gtk.cookies/no_account0")];
+      sprintf(filename, "%s/.config", home);
+      mkdir(filename, 0700);
+      sprintf(filename, "%s/.config/tc_client-gtk.cookies", home);
+      mkdir(filename, 0700);
+      sprintf(filename, "%s/.config/tc_client-gtk.cookies/no_account", home);
+      execlp(TC_CLIENT, TC_CLIENT, "--greenroom", "--cookies", filename, channel, id, (char*)0);
+    }else{
+      execlp(TC_CLIENT, TC_CLIENT, "--greenroom", channel, id, (char*)0);
+    }
+    _exit(1);
+  }
+#endif
+  GIOChannel* tcchannel=g_io_channel_unix_new(greenroompipe[0]);
+  g_io_channel_set_encoding(tcchannel, 0, 0);
+  g_io_add_watch(tcchannel, G_IO_IN, greenroom_handleline, 0);
+}
+
+void greenroom_changenick(const char* from, const char* to)
+{
+  unsigned int i;
+  for(i=0; i<usercountgr; ++i)
+  {
+    if(users[i].nick && !strcmp(users[i].nick, from))
+    {
+      free((void*)users[i].nick);
+      users[i].nick=strdup(to);
+      struct camera* cam=camera_find(users[i].camid);
+      if(cam){gtk_label_set_text(GTK_LABEL(cam->label), to);}
+    }
+  }
+}
diff --git a/utilities/gtk/greenroom.h b/utilities/gtk/greenroom.h
new file mode 100644
index 0000000..98d12d0
--- /dev/null
+++ b/utilities/gtk/greenroom.h
@@ -0,0 +1,20 @@
+/*
+    tc_client-gtk, a graphical user interface for tc_client
+    Copyright (C) 2016  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 greenroompipe_in[2];
+extern char greenroom_gotnick(const char* id, const char* nick);
+extern void greenroom_join(const char* id);
+extern void greenroom_changenick(const char* from, const char* to);
diff --git a/utilities/gtk/main.h b/utilities/gtk/main.h
index 55a46f0..3624296 100644
--- a/utilities/gtk/main.h
+++ b/utilities/gtk/main.h
@@ -14,7 +14,12 @@
     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/>.
 */
+#define TC_CLIENT (frombuild?"./tc_client":"tc_client")
+extern int tc_client_in[2];
+extern const char* channel;
 extern char* nickname;
+extern char hasgreenroom;
+extern char frombuild;
 extern gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x);
 extern void togglecam(GtkCheckMenuItem* item, void* x);
 #ifdef HAVE_PULSEAUDIO
diff --git a/utilities/gtk/media.c b/utilities/gtk/media.c
index 353c5e8..b3d89c2 100644
--- a/utilities/gtk/media.c
+++ b/utilities/gtk/media.c
@@ -160,7 +160,7 @@ struct camera* camera_findbynick(const char* nick)
   return 0;
 }
 
-struct camera* camera_new(const char* nick, const char* id)
+struct camera* camera_new(const char* nick, const char* id, unsigned char flags)
 {
   ++camcount;
   cams=realloc(cams, sizeof(struct camera)*camcount);
@@ -181,19 +181,26 @@ struct camera* camera_new(const char* nick, const char* id)
   cam->frame=av_frame_alloc();
   cam->dstframe=av_frame_alloc();
   cam->cam=gtk_image_new();
-  cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-  gtk_box_set_homogeneous(GTK_BOX(cam->box), 0);
-  // Wrap cam image in an event box to catch (right) clicks
-  GtkWidget* eventbox=gtk_event_box_new();
-  gtk_container_add(GTK_CONTAINER(eventbox), cam->cam);
-  gtk_event_box_set_above_child(GTK_EVENT_BOX(eventbox), 1);
-  cam->label=gtk_label_new(cam->nick);
-  gtk_box_pack_start(GTK_BOX(cam->box), eventbox, 0, 0, 0);
-  gtk_box_pack_start(GTK_BOX(cam->box), cam->label, 0, 0, 0);
-  g_signal_connect(eventbox, "button-release-event", G_CALLBACK(gui_show_cam_menu), cam->id);
-  cam->placeholder=g_timeout_add(100, camplaceholder_update, cam->id);
+  if(flags&CAMFLAG_GREENROOM)
+  {
+    cam->box=0;
+    cam->placeholder=0;
+  }else{
+    cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+    gtk_box_set_homogeneous(GTK_BOX(cam->box), 0);
+    // Wrap cam image in an event box to catch (right) clicks
+    GtkWidget* eventbox=gtk_event_box_new();
+    gtk_container_add(GTK_CONTAINER(eventbox), cam->cam);
+    gtk_event_box_set_above_child(GTK_EVENT_BOX(eventbox), 1);
+    cam->label=gtk_label_new(cam->nick);
+    gtk_box_pack_start(GTK_BOX(cam->box), eventbox, 0, 0, 0);
+    gtk_box_pack_start(GTK_BOX(cam->box), cam->label, 0, 0, 0);
+    g_signal_connect(eventbox, "button-release-event", G_CALLBACK(gui_show_cam_menu), cam->id);
+    cam->placeholder=g_timeout_add(100, camplaceholder_update, cam->id);
+  }
   cam->volume=0;
   cam->volumeold=1024;
+  cam->flags=flags;
   // Initialize postprocessing values
   postproc_init(&cam->postproc);
   return cam;
@@ -451,7 +458,13 @@ const char* camselect_file(void)
 
 void updatescaling(unsigned int width, unsigned int height, char changedcams)
 {
-  if(!camcount){return;}
+  unsigned int boxcount=0;
+  unsigned int i;
+  for(i=0; i<camcount; ++i)
+  {
+    if(!cams[i].flags&CAMFLAG_GREENROOM){++boxcount;}
+  }
+  if(!boxcount){return;}
   if(!width){width=gtk_widget_get_allocated_width(GTK_WIDGET(gtk_builder_get_object(gui, "main")));}
   if(!height){height=gtk_widget_get_allocated_height(GTK_WIDGET(gtk_builder_get_object(gui, "camerascroll")));}
 
@@ -461,11 +474,11 @@ void updatescaling(unsigned int width, unsigned int height, char changedcams)
   camsize_scale.height=1;
   unsigned int rowcount=1;
   unsigned int rows;
-  for(rows=1; rows<=camcount; ++rows)
+  for(rows=1; rows<=boxcount; ++rows)
   {
     struct size scale;
-    unsigned int cams_per_row=camcount/rows;
-    if(camcount%rows){++cams_per_row;}
+    unsigned int cams_per_row=boxcount/rows;
+    if(boxcount%rows){++cams_per_row;}
     scale.width=width/cams_per_row;
     // 3/4 ratio
     scale.height=scale.width*3/4;
@@ -484,11 +497,11 @@ void updatescaling(unsigned int width, unsigned int height, char changedcams)
     }else if(scale.width<camsize_scale.width){break;} // Only getting smaller from here, use the last one that increased
   }
 
-  unsigned int i;
   if(rowcount!=camrowcount || changedcams) // Changed the number of rows, shuffle everything around to fit. Or added/removed a camera, in which case we need to shuffle things around anyway
   {
     for(i=0; i<camcount; ++i)
     {
+      if(cams[i].flags&CAMFLAG_GREENROOM){continue;}
       g_object_ref(cams[i].box); // Increase reference counts so that they are not deallocated while they are temporarily detached from the rows
       GtkContainer* parent=GTK_CONTAINER(gtk_widget_get_parent(cams[i].box));
       if(parent){gtk_container_remove(parent, cams[i].box);}
@@ -503,11 +516,14 @@ void updatescaling(unsigned int width, unsigned int height, char changedcams)
       gtk_widget_set_halign(camrows[i], GTK_ALIGN_CENTER);
       gtk_widget_show(camrows[i]);
     }
-    unsigned int cams_per_row=camcount/camrowcount;
-    if(camcount%camrowcount){++cams_per_row;}
+    unsigned int cams_per_row=boxcount/camrowcount;
+    if(boxcount%camrowcount){++cams_per_row;}
+    unsigned int index=0;
     for(i=0; i<camcount; ++i)
     {
-      gtk_box_pack_start(GTK_BOX(camrows[i/cams_per_row]), cams[i].box, 0, 0, 0);
+      if(cams[i].flags&CAMFLAG_GREENROOM){continue;}
+      gtk_box_pack_start(GTK_BOX(camrows[index/cams_per_row]), cams[i].box, 0, 0, 0);
+      ++index;
       g_object_unref(cams[i].box); // Decrease reference counts once they're attached again
     }
   }
@@ -517,6 +533,7 @@ void updatescaling(unsigned int width, unsigned int height, char changedcams)
   // Rescale current images to fit
   for(i=0; i<camcount; ++i)
   {
+    if(cams[i].flags&CAMFLAG_GREENROOM){continue;}
     GdkPixbuf* pixbuf=gtk_image_get_pixbuf(GTK_IMAGE(cams[i].cam));
     if(!pixbuf){continue;}
     GdkPixbuf* old=pixbuf;
@@ -699,3 +716,58 @@ void volume_indicator(GdkPixbuf* frame, struct camera* cam)
   }
   ++cam->volumeold;
 }
+
+void camera_decode(struct camera* cam, AVPacket* pkt, unsigned int width, unsigned int height)
+{
+  if(!cam->vctx)
+  {
+    AVCodec* codec=avcodec_find_decoder(AV_CODEC_ID_FLV1);
+    cam->vctx=avcodec_alloc_context3(codec);
+    avcodec_open2(cam->vctx, codec, 0);
+  }
+  int gotframe;
+  avcodec_send_packet(cam->vctx, pkt);
+  gotframe=avcodec_receive_frame(cam->vctx, cam->frame);
+  if(gotframe){return;}
+
+  if(cam->placeholder) // Remove the placeholder animation if it has it
+  {
+    g_source_remove(cam->placeholder);
+    cam->placeholder=0;
+  }
+  // Scale and convert to RGB24 format
+  unsigned int bufsize=av_image_get_buffer_size(AV_PIX_FMT_RGB24, width, height, 1);
+  unsigned char* buf=malloc(bufsize);
+  cam->dstframe->data[0]=buf;
+  cam->dstframe->linesize[0]=width*3;
+  struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, width, height, AV_PIX_FMT_RGB24, SWS_BICUBIC, 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);
+  postprocess(&cam->postproc, cam->dstframe->data[0], width, height);
+
+  GdkPixbuf* oldpixbuf=gtk_image_get_pixbuf(GTK_IMAGE(cam->cam));
+  GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, width, height, cam->dstframe->linesize[0], freebuffer, 0);
+  volume_indicator(gdkframe, cam);
+  gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), gdkframe);
+  if(oldpixbuf){g_object_unref(oldpixbuf);}
+}
+
+#ifdef HAVE_LIBAO
+void mic_decode(struct camera* cam, AVPacket* pkt)
+{
+  int gotframe;
+  avcodec_send_packet(cam->actx, pkt);
+  gotframe=avcodec_receive_frame(cam->actx, cam->frame);
+  if(gotframe){return;}
+  camera_calcvolume(cam, (float*)cam->frame->data[0], cam->frame->nb_samples);
+  unsigned int samplecount=cam->frame->nb_samples*SAMPLERATE_OUT/cam->samplerate;
+  int16_t outbuf[samplecount];
+  void* outdata[]={outbuf, 0};
+#ifdef HAVE_AVRESAMPLE
+  int outlen=avresample_convert(cam->resamplectx, (void*)outdata, samplecount*sizeof(uint8_t), samplecount, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples);
+#else
+  int outlen=swr_convert(cam->swrctx, (void*)outdata, samplecount, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
+#endif
+  if(outlen>0){camera_playsnd(cam, outbuf, outlen);}
+}
+#endif
diff --git a/utilities/gtk/media.h b/utilities/gtk/media.h
index e58d101..c205428 100644
--- a/utilities/gtk/media.h
+++ b/utilities/gtk/media.h
@@ -27,6 +27,8 @@
 #include "../libcamera/camera.h"
 #include "postproc.h"
 #define SAMPLERATE_OUT 11025 // 11025 is the most common input sample rate, and it is more CPU-efficient to keep it that way than upsampling or downsampling it when converting from flt to s16
+#define CAMFLAG_NONE      0
+#define CAMFLAG_GREENROOM 1
 struct camera
 {
   AVFrame* frame;
@@ -50,6 +52,7 @@ struct camera
   unsigned int samplerate;
   float volume;
   unsigned int volumeold;
+  unsigned char flags;
 };
 struct size
 {
@@ -75,7 +78,7 @@ extern gboolean audiomixer(void* p);
 extern void camera_remove(const char* id, char isnick);
 extern struct camera* camera_find(const char* id);
 extern struct camera* camera_findbynick(const char* nick);
-extern struct camera* camera_new(const char* nick, const char* id);
+extern struct camera* camera_new(const char* nick, const char* id, unsigned char flags);
 extern char camera_init_audio(struct camera* cam, uint8_t frameinfo);
 extern void camera_cleanup(void);
 extern void freebuffer(guchar* pixels, gpointer data);
@@ -94,4 +97,8 @@ extern void* audiothread_in(void* fdp);
 extern gboolean mic_encode(GIOChannel* iochannel, GIOCondition condition, gpointer datap);
 extern void camera_calcvolume(struct camera* cam, float* samples, unsigned int samplecount);
 extern void volume_indicator(GdkPixbuf* frame, struct camera* cam);
+extern void camera_decode(struct camera* cam, AVPacket* pkt, unsigned int width, unsigned int height);
+#ifdef HAVE_LIBAO
+extern void mic_decode(struct camera* cam, AVPacket* pkt);
+#endif
 #endif
diff --git a/utilities/gtk/userlist.c b/utilities/gtk/userlist.c
index 3873c74..d04593a 100644
--- a/utilities/gtk/userlist.c
+++ b/utilities/gtk/userlist.c
@@ -156,3 +156,10 @@ void userlist_sort(void)
     gtk_box_reorder_child(GTK_BOX(userlistwidget), userlist[i].item, position);
   }
 }
+
+char user_ismod(const char* nick)
+{
+  struct user* user=finduser(nick);
+  if(!user){return 0;}
+  return user->ismod;
+}
diff --git a/utilities/gtk/userlist.h b/utilities/gtk/userlist.h
index ebcf7b8..0315ca8 100644
--- a/utilities/gtk/userlist.h
+++ b/utilities/gtk/userlist.h
@@ -37,3 +37,4 @@ extern void renameuser(const char* old, const char* newnick);
 extern void removeuser(const char* nick);
 extern void usersetmod(const char* nick, char mod);
 extern void userlist_sort(void);
+extern char user_ismod(const char* nick);