$ git clone http://tcclient.ion.nu/tc_client.git
commit 8dde1f936396b82516544a6df60acd201c4b03d8
Author: Alicia <...>
Date:   Sat Sep 17 18:35:07 2016 +0200

    tc_client-gtk: added a placeholder animation for cameras, shown for audio-only streams and streams that haven't sent any video data yet.

diff --git a/ChangeLog b/ChangeLog
index 474f71e..d2ac7b0 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -26,6 +26,7 @@ tc_client-gtk: un-highlight selected PM tabs even if the sender has left.
 tc_client-gtk: prevent new PM tabs from stopping automatic scrolling.
 tc_client-gtk: if the server disconnects, print the notification and stop any outgoing broadcast.
 tc_client-gtk: optimized incoming audio/video by checking for "Video:" and "Audio:" first when handling lines from the tc_client core.
+tc_client-gtk: added a placeholder animation for cameras, shown for audio-only streams and streams that haven't sent any video data yet.
 tc_client-gtk and camviewer: fixed compatibility with newer libavutil.
 tc_client-gtk and camviewer: added compatibility fallbacks for av_image_get_buffer_size() and av_packet_unref()
 tc_client-gtk and camviewer: specify a scaling algorithm for libswscale (mandatory for older versions of libswscale)
diff --git a/Makefile b/Makefile
index da1da2e..a3efc7c 100644
--- a/Makefile
+++ b/Makefile
@@ -112,8 +112,11 @@ camviewer: $(CAMVIEWER_OBJ)
 cursedchat: $(CURSEDCHAT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) $(READLINE_LIBS) $(CURSES_LIBS) -o $@
 
-tc_client-gtk: $(TC_CLIENT_GTK_OBJ)
- $(CC) $(LDFLAGS) $^ $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(AVRESAMPLE_LIBS) $(SWRESAMPLE_LIBS) $(AO_LIBS) $(LIBV4L2_LIBS) -o $@
+tc_client-gtk: $(TC_CLIENT_GTK_OBJ) camplaceholder.gif
+ $(CC) $(LDFLAGS) $(TC_CLIENT_GTK_OBJ) $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(AVRESAMPLE_LIBS) $(SWRESAMPLE_LIBS) $(AO_LIBS) $(LIBV4L2_LIBS) -o $@
+
+camplaceholder.gif: utilities/gtk/gencamplaceholder.sh utilities/gtk/camplaceholder.xcf utilities/gtk/spinnerdot.xcf
+ utilities/gtk/gencamplaceholder.sh
 
 # Workaround for windows' lack of fork() and inflexibility of having or not having a terminal
 utilities/gtk/camthread.gen.c: utilities/gtk/media.c
@@ -138,7 +141,7 @@ libcamera.a: $(LIBCAMERA_OBJ)
  $(RANLIB) $@
 
 clean:
- rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) $(TC_CLIENT_GTK_OBJ) $(LIBCAMERA_OBJ) tc_client irchack modbot camviewer cursedchat tc_client-gtk
+ rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) $(TC_CLIENT_GTK_OBJ) $(LIBCAMERA_OBJ) tc_client irchack modbot camviewer cursedchat tc_client-gtk camplaceholder.gif
 
 SOURCES=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 testbuilds.sh configure
 SOURCES+=utilities/irchack/irchack.c
@@ -146,6 +149,7 @@ SOURCES+=utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/que
 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/config.c utilities/gtk/gui.c utilities/gtk/logging.c utilities/gtk/inputhistory.c utilities/gtk/userlist.h utilities/gtk/media.h utilities/gtk/compat.h utilities/gtk/config.h utilities/gtk/gui.h utilities/gtk/logging.h utilities/gtk/inputhistory.h gtkgui.glade
+SOURCES+=utilities/gtk/gencamplaceholder.sh utilities/gtk/camplaceholder.xcf utilities/gtk/spinnerdot.xcf
 SOURCES+=utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h utilities/stringutils.c utilities/stringutils.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
 tarball:
diff --git a/utilities/gtk/camplaceholder.xcf b/utilities/gtk/camplaceholder.xcf
new file mode 100644
index 0000000..bfacaca
Binary files /dev/null and b/utilities/gtk/camplaceholder.xcf differ
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index e760e65..bd806ae 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -204,6 +204,11 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
     avcodec_decode_video2(cam->vctx, cam->frame, &gotframe, &pkt);
     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);
@@ -514,6 +519,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
     idend[0]=0;
     camera_removebynick(nick); // Remove any duplicates
     struct camera* cam=camera_new(nick, id);
+    cam->placeholder=g_timeout_add(100, camplaceholder_update, cam->id);
     cam->vctx=avcodec_alloc_context3(data->vdecoder);
     avcodec_open2(cam->vctx, data->vdecoder, 0);
 #if defined(HAVE_AVRESAMPLE) || defined(HAVE_SWRESAMPLE)
@@ -1035,6 +1041,14 @@ int main(int argc, char** argv)
   g_signal_connect(gtk_builder_get_object(gui, "camcolors_flip_vertical"), "toggled", G_CALLBACK(camcolors_toggle_flip), (void*)1);
   // Connect signal for hiding cameras
   g_signal_connect(gtk_builder_get_object(gui, "cam_menu_hide"), "activate", G_CALLBACK(gui_hide_cam), 0);
+  // Load placeholder animation for cameras (no video data yet or mic only)
+  if(frombuild)
+  {
+    camplaceholder=gdk_pixbuf_animation_new_from_file("camplaceholder.gif", 0);
+  }else{
+    camplaceholder=gdk_pixbuf_animation_new_from_file(PREFIX "/share/tc_client/camplaceholder.gif", 0);
+  }
+  camplaceholder_iter=gdk_pixbuf_animation_get_iter(camplaceholder, 0);
   // Populate saved channels
   GtkWidget* startbox=GTK_WIDGET(gtk_builder_get_object(gui, "startbox"));
   int channelcount=config_get_int("channelcount");
diff --git a/utilities/gtk/gencamplaceholder.sh b/utilities/gtk/gencamplaceholder.sh
new file mode 100755
index 0000000..487dd61
--- /dev/null
+++ b/utilities/gtk/gencamplaceholder.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+spinnerpos='+281+341
++270+369
++281+397
++309+408
++337+397
++348+369
++337+341
++309+330'
+{
+  for frame in `seq 1 16`; do
+    echo '( ( utilities/gtk/camplaceholder.xcf -layers Merge )'
+    for spinner in `seq 1 8`; do
+      pos="`echo "$spinnerpos" | sed -n -e "${spinner}p"`"
+      dotframe="`expr '(' "$frame" + "$spinner" '*' 2 ')' '%' 16`"
+      if [ "$dotframe" -gt 11 ]; then dotframe=11; fi # TODO: use identify | wc -l to get the max frame ID?
+      echo "( -geometry "$pos" "utilities/gtk/spinnerdot.xcf[${dotframe}]" ) -composite"
+    done
+    echo ')'
+  done
+  echo '-layers Optimize -delay 100 camplaceholder.gif'
+} | xargs convert
diff --git a/utilities/gtk/media.c b/utilities/gtk/media.c
index 794976a..4076382 100644
--- a/utilities/gtk/media.c
+++ b/utilities/gtk/media.c
@@ -53,6 +53,8 @@ struct size camsize_scale={.width=320, .height=240};
 GtkWidget* cambox;
 GtkWidget** camrows=0;
 unsigned int camrowcount=0;
+GdkPixbufAnimation* camplaceholder=0;
+GdkPixbufAnimationIter* camplaceholder_iter=0;
 
 #if defined(HAVE_AVRESAMPLE) || defined(HAVE_SWRESAMPLE)
 // Experimental mixer, not sure if it really works
@@ -92,6 +94,10 @@ void camera_remove(const char* id)
   {
     if(!strcmp(cams[i].id, id))
     {
+      if(cams[i].placeholder) // Remove the placeholder animation if it has it
+      {
+        g_source_remove(cams[i].placeholder);
+      }
       gtk_widget_destroy(cams[i].box);
       av_frame_free(&cams[i].frame);
       avcodec_free_context(&cams[i].vctx);
@@ -535,3 +541,17 @@ void updatescaling(unsigned int width, unsigned int height, char changedcams)
     g_object_unref(old);
   }
 }
+
+gboolean camplaceholder_update(void* id)
+{
+  struct camera* cam=camera_find(id);
+  GdkPixbuf* oldpixbuf=gtk_image_get_pixbuf(GTK_IMAGE(cam->cam));
+  // Get the current frame of the animation
+  gdk_pixbuf_animation_iter_advance(camplaceholder_iter, 0);
+  GdkPixbuf* frame=gdk_pixbuf_animation_iter_get_pixbuf(camplaceholder_iter);
+  // Scale and replace the current image on camera
+  GdkPixbuf* pixbuf=gdk_pixbuf_scale_simple(frame, camsize_scale.width, camsize_scale.height, GDK_INTERP_BILINEAR);
+  gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), pixbuf);
+  g_object_unref(oldpixbuf);
+  return G_SOURCE_CONTINUE;
+}
diff --git a/utilities/gtk/media.h b/utilities/gtk/media.h
index d2bf77d..b61eaaf 100644
--- a/utilities/gtk/media.h
+++ b/utilities/gtk/media.h
@@ -36,6 +36,7 @@ struct camera
     char flip_horizontal;
     char flip_vertical;
   } postproc;
+  unsigned int placeholder;
 };
 struct size
 {
@@ -54,6 +55,8 @@ extern unsigned int camcount;
 extern struct size camsize_out;
 extern struct size camsize_scale;
 extern GtkWidget* cambox;
+extern GdkPixbufAnimation* camplaceholder;
+extern GdkPixbufAnimationIter* camplaceholder_iter;
 
 #if defined(HAVE_AVRESAMPLE) || defined(HAVE_SWRESAMPLE)
 extern void camera_playsnd(int audiopipe, struct camera* cam, short* samples, unsigned int samplecount);
@@ -73,3 +76,4 @@ extern void camselect_accept(GtkWidget* widget, AVCodec* vencoder);
 extern const char* camselect_file(void);
 extern void camera_postproc(struct camera* cam, unsigned char* buf, unsigned int width, unsigned int height);
 extern void updatescaling(unsigned int width, unsigned int height, char changedcams);
+extern gboolean camplaceholder_update(void* id);
diff --git a/utilities/gtk/spinnerdot.xcf b/utilities/gtk/spinnerdot.xcf
new file mode 100644
index 0000000..8adf0f9
Binary files /dev/null and b/utilities/gtk/spinnerdot.xcf differ