$ git clone http://tcclient.ion.nu/tc_client.git
commit 0b5f12f1f24a295fa26404cec45f0b04bd3048d9
Author: Alicia <...>
Date:   Thu Dec 1 21:23:19 2016 +0100

    tc_client-gtk: added support for integrated youtube videos.

diff --git a/ChangeLog b/ChangeLog
index 6da69e9..a11c51b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -11,6 +11,7 @@ tc_client-gtk: handle failure to connect to pulseaudio more gracefully.
 tc_client-gtk: rewrote the audio mixer.
 tc_client-gtk: display the channel name in the window title.
 tc_client-gtk: added a setting to control /disablesnapshots and /enablesnapshots.
+tc_client-gtk: added support for integrated youtube videos.
 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 3faa68a..4ca6e7f 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/config.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 libcamera.a
+TC_CLIENT_GTK_OBJ=utilities/gtk/camviewer.o utilities/gtk/userlist.o utilities/gtk/media.o utilities/gtk/compat.o utilities/gtk/config.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
 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
@@ -43,6 +43,10 @@ ifdef SWSCALE_LIBS
     endif
     endif
   endif
+  ifdef AVFORMAT_LIBS
+    CFLAGS+=-DHAVE_AVFORMAT=1
+    CONFINFO+=|  Will enable integrated youtube videos
+  endif
   ifdef PULSE_LIBS
     CONFINFO+=|  Will enable outgoing mic support
     CFLAGS+=-DHAVE_PULSEAUDIO=1 $(PULSE_CFLAGS)
@@ -129,7 +133,7 @@ cursedchat: $(CURSEDCHAT_OBJ)
  $(CC) $(LDFLAGS) $^ $(LIBS) $(READLINE_LIBS) $(CURSES_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) $(LIBX11_LIBS) $(PULSE_LIBS) -o $@
+ $(CC) $(LDFLAGS) $(TC_CLIENT_GTK_OBJ) $(LIBS) $(GTK_LIBS) $(AVCODEC_LIBS) $(AVUTIL_LIBS) $(SWSCALE_LIBS) $(AVRESAMPLE_LIBS) $(SWRESAMPLE_LIBS) $(AVFORMAT_LIBS) $(AO_LIBS) $(LIBV4L2_LIBS) $(LIBX11_LIBS) $(PULSE_LIBS) -o $@
 
 camplaceholder.gif: utilities/gtk/gencamplaceholder.sh utilities/gtk/camplaceholder.xcf utilities/gtk/spinnerdot.xcf
  utilities/gtk/gencamplaceholder.sh
@@ -146,7 +150,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/config.c utilities/gtk/gui.c utilities/gtk/logging.c utilities/gtk/postproc.c utilities/gtk/inputhistory.c utilities/gtk/main.h 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/postproc.h utilities/gtk/inputhistory.h gtkgui.glade
+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/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/config.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/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 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/configure b/configure
index 659666c..8e2ad2e 100755
--- a/configure
+++ b/configure
@@ -137,6 +137,7 @@ else
 fi
 
 testpkgconfig libswscale SWSCALE
+testpkgconfig libavformat AVFORMAT
 
 printf 'Checking for libavutil... '
 avutillibs="`pkg-config --libs libavutil 2> /dev/null`"
diff --git a/dist/appimage.sh b/dist/appimage.sh
index 475f962..200d001 100755
--- a/dist/appimage.sh
+++ b/dist/appimage.sh
@@ -15,7 +15,7 @@ mkdir -p deps/src
   cd ffmpeg-3.1.4
   mkdir -p build
   cd build
-  ../configure --prefix="${rootdir}/deps" --enable-gpl --disable-programs --disable-doc --disable-htmlpages --disable-manpages --disable-podpages --disable-txtpages --disable-avdevice --disable-avformat --disable-postproc --disable-avfilter --disable-encoders --enable-encoder="flv,nellymoser,speex" --disable-decoders --enable-decoder="flv,nellymoser,speex" --disable-parsers --disable-bsfs --disable-devices --disable-sdl --disable-libxcb --disable-xlib --enable-static --disable-shared
+  ../configure --prefix="${rootdir}/deps" --enable-gpl --disable-programs --disable-doc --disable-htmlpages --disable-manpages --disable-podpages --disable-txtpages --disable-avdevice --disable-postproc --disable-avfilter --disable-encoders --enable-encoder="flv,nellymoser,speex" --disable-decoders --enable-decoder="flv,nellymoser,speex,h264,mpeg4,vp8,vp9,aac" --disable-protocols --enable-protocol="tls_gnutls,https" --disable-demuxers --enable-demuxer="webm_dash_manifest,m4v,matroska,aac,h264,mpegvideo" --disable-muxers --disable-parsers --disable-bsfs --disable-devices --disable-sdl --disable-libxcb --disable-xlib --enable-gnutls --enable-static --disable-shared
   make
   make install
 )
diff --git a/dist/w32.sh b/dist/w32.sh
index 5adfde5..405360a 100755
--- a/dist/w32.sh
+++ b/dist/w32.sh
@@ -64,5 +64,5 @@ echo "cd bin" > "tc_client-${version}-w32/TC Client.cmd"
 echo "start tc_client-gtk" >> "tc_client-${version}-w32/TC Client.cmd"
 # Some good defaults
 echo 'youtubecmd: http://youtube.com/watch?v=%i' > "tc_client-${version}-w32/bin/config.txt"
-echo 'youtuberadio_cmd: True' >> "tc_client-${version}-w32/bin/config.txt"
+echo 'youtuberadio_embed: True' >> "tc_client-${version}-w32/bin/config.txt"
 zip -r "tc_client-${version}-w32.zip" "tc_client-${version}-w32"
diff --git a/gtkgui.glade b/gtkgui.glade
index e5bfeed..a6dfe1b 100644
--- a/gtkgui.glade
+++ b/gtkgui.glade
@@ -938,6 +938,30 @@
                             <property name="position">0</property>
                           </packing>
                         </child>
+                        <child>
+                          <object class="GtkRadioButton" id="youtuberadio_embed">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="xalign">0</property>
+                            <property name="draw_indicator">True</property>
+                            <property name="group">youtuberadio_none</property>
+                            <child>
+                              <object class="GtkLabel" id="label37">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="label" translatable="yes">Play youtube video next to the cameras</property>
+                                <property name="wrap">True</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
                         <child>
                           <object class="GtkRadioButton" id="youtuberadio_cmd">
                             <property name="visible">True</property>
@@ -961,7 +985,7 @@
                           <packing>
                             <property name="expand">False</property>
                             <property name="fill">True</property>
-                            <property name="position">1</property>
+                            <property name="position">2</property>
                           </packing>
                         </child>
                         <child>
@@ -972,7 +996,7 @@
                           <packing>
                             <property name="expand">False</property>
                             <property name="fill">True</property>
-                            <property name="position">2</property>
+                            <property name="position">3</property>
                           </packing>
                         </child>
                         <child>
@@ -986,7 +1010,7 @@
                           <packing>
                             <property name="expand">False</property>
                             <property name="fill">True</property>
-                            <property name="position">3</property>
+                            <property name="position">4</property>
                           </packing>
                         </child>
                       </object>
diff --git a/testbuilds.sh b/testbuilds.sh
index 545dc6b..925f9a6 100755
--- a/testbuilds.sh
+++ b/testbuilds.sh
@@ -1,14 +1,13 @@
 #!/bin/sh
 # Utility to make sure that changes did not break the project in certain configurations
-echo "WARNING: this tool is for developers, not users. You have 8 seconds to press Ctrl+C to abort."
-echo
-echo "What it does is to configure and build the project with different features enabled and disabled to make sure that improvements didn't also break stuff in certain cases. This *will* erase anything you've already compiled."
-sleep 8
-make clean > /dev/null 2> /dev/null
-printf "Without mic support, with gtk+-2.x, without RTMP_DEBUG: "
+rm -rf testbuilds
+mkdir -p testbuilds
+cd testbuilds
+
+printf "Without mic support, with gtk+-2.x, without RTMP_DEBUG, without youtube support: "
 res="broken"
 while true; do
-  ./configure > /dev/null 2> /dev/null || break
+  ../configure > /dev/null 2> /dev/null || break
   sed -i -e '/^GTK_/d' config.mk
   echo "GTK_LIBS=`pkg-config --libs gtk+-2.0 2> /dev/null`" >> config.mk
   echo "GTK_CFLAGS=`pkg-config --cflags gtk+-2.0`" >> config.mk
@@ -16,6 +15,7 @@ while true; do
   if ! grep -q '^AVCODEC_LIBS' config.mk; then res="libavcodec not found, can't test"; break; fi
   if ! grep -q '^SWSCALE_LIBS' config.mk; then res="libswscale not found, can't test"; break; fi
   sed -i -e '/^AO_LIBS/d' config.mk
+  sed -i -e '/^AVFORMAT_LIBS/d' config.mk
   echo 'CFLAGS+=-Werror' >> config.mk
   if ! make utils > /dev/null 2> /dev/null; then
     sed -i -e 's/-Werror[^ ]*//g' config.mk
@@ -31,15 +31,16 @@ mv camviewer camviewer.gtk2 > /dev/null 2> /dev/null
 mv tc_client-gtk tc_client-gtk.gtk2 > /dev/null 2> /dev/null
 
 make clean > /dev/null 2> /dev/null
-printf "With mic support, with gtk+-3.x, with RTMP_DEBUG: "
+printf "With mic support, with gtk+-3.x, with RTMP_DEBUG, with youtube support: "
 res="broken"
 while true; do
-  ./configure > /dev/null 2> /dev/null || break
+  ../configure > /dev/null 2> /dev/null || break
   if ! grep -q 'GTK_LIBS=.*-lgtk-3' config.mk; then res="gtk+-3.x not found, can't test"; break; fi
   if ! grep -q '^AO_LIBS' config.mk; then res="libao not found, can't test"; break; fi
   if ! grep -q 'RESAMPLE_LIBS' config.mk; then res="lib(av|sw)resample not found, can't test"; break; fi
   if ! grep -q '^AVCODEC_LIBS' config.mk; then res="libavcodec not found, can't test"; break; fi
   if ! grep -q '^SWSCALE_LIBS' config.mk; then res="libswscale not found, can't test"; break; fi
+  if ! grep -q '^AVFORMAT_LIBS' config.mk; then res="libavformat not found, can't test"; break; fi
   echo 'CFLAGS+=-DRTMP_DEBUG=1 -Werror' >> config.mk
   if ! make utils > /dev/null 2> /dev/null; then
     sed -i -e 's/-Werror[^ ]*//g' config.mk
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index 5423f16..fc56916 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -59,6 +59,7 @@
 #include "logging.h"
 #include "../stringutils.h"
 #include "inputhistory.h"
+#include "playmedia.h"
 #include "main.h"
 
 struct viddata
@@ -161,6 +162,12 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     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)
@@ -206,7 +213,7 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     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);
-    g_object_unref(oldpixbuf);
+    if(oldpixbuf){g_object_unref(oldpixbuf);}
     return 1;
   }
   if(!strncmp(buf, "Audio: ", 7))
@@ -342,6 +349,10 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     if(!space){return 1;}
     if(space[-1]==':')
     {
+      char nickbuf[space-nick];
+      memcpy(nickbuf, nick, &space[-1]-nick);
+      nickbuf[&space[-1]-nick]=0;
+      struct user* user=finduser(nickbuf);
       if(config_get_bool("soundradio_cmd"))
       {
 #ifdef _WIN32
@@ -356,48 +367,61 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
         }
 #endif
       }
-      if(!strncmp(space, " /mbs youTube ", 14) && config_get_bool("youtuberadio_cmd"))
+      if(user && user->ismod && !strncmp(space, " /mbs youTube ", 14))
       {
-#ifndef _WIN32
-        if(!fork())
-#else
-        char* spacetmp=space;
-        space=strdup(space);
-#endif
+        if(config_get_bool("youtuberadio_cmd"))
         {
-// TODO: store the PID and make sure it's dead before starting a new video? and upon /mbc?
-// TODO: only play videos from mods?
-          char* id=&space[14];
-          char* offset=strchr(id, ' ');
-          if(!offset){_exit(1);}
-          offset[0]=0;
-          offset=&offset[1];
-          char* end=strchr(offset, ' '); // Ignore any additional arguments after the offset (the modbot utility includes the video title here)
-          if(end){end[0]=0;}
-          // 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])
+          #ifndef _WIN32
+          if(!fork())
+          #else
+          char* spacetmp=space;
+          space=strdup(space);
+          #endif
           {
-            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];
+// TODO: store the PID and make sure it's dead before starting a new video? and upon /mbc?
+            char* id=&space[14];
+            char* offset=strchr(id, ' ');
+            if(!offset){_exit(1);}
+            offset[0]=0;
+            offset=&offset[1];
+            char* end=strchr(offset, ' '); // Ignore any additional arguments after the offset (the modbot utility includes the video title here)
+            if(end){end[0]=0;}
+            // 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];
+            }
+            #ifdef _WIN32
+            w32_runcmd(cmd);
+            free(space);
+            space=spacetmp;
+            #else
+            execlp("sh", "sh", "-c", cmd, (char*)0);
+            _exit(0);
+            #endif
           }
-#ifdef _WIN32
-          w32_runcmd(cmd);
-          free(space);
-          space=spacetmp;
-#else
-          execlp("sh", "sh", "-c", cmd, (char*)0);
-          _exit(0);
-#endif
         }
+#ifdef HAVE_AVFORMAT
+        else if(config_get_bool("youtuberadio_embed")){media_play(&space[6]);}
+      }
+      else if(user && user->ismod && !strncmp(space, " /mbsk youTube ", 15) && config_get_bool("youtuberadio_embed"))
+      {
+        media_seek(strtol(&space[15], 0, 0));
+      }
+      else if(user && user->ismod && !strncmp(space, " /mbc ", 6) && config_get_bool("youtuberadio_embed"))
+      {
+        media_close();
+#endif
       }
       // Handle incoming PMs
       else if(!strncmp(space, " /msg ", 6))
@@ -510,8 +534,6 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x)
     idend[0]=0;
     camera_remove(nick, 1); // Remove any duplicates
     struct camera* cam=camera_new(nick, id);
-    cam->vctx=avcodec_alloc_context3(data->vdecoder);
-    avcodec_open2(cam->vctx, data->vdecoder, 0);
     updatescaling(0, 0, 1);
     gtk_widget_show_all(cam->box);
     return 1;
@@ -755,6 +777,20 @@ void sendmessage(GtkEntry* entry, void* x)
     sendingmsg=0;
     return;
   }
+#ifdef HAVE_AVFORMAT
+  else if(!strncmp(msg, "/mbs ", 5) && config_get_bool("youtuberadio_embed"))
+  {
+    media_play(&msg[5]);
+  }
+  else if(!strncmp(msg, "/mbsk ", 6) && config_get_bool("youtuberadio_embed"))
+  {
+    media_seek(strtol(&msg[6], 0, 0));
+  }
+  else if(!strncmp(msg, "/mbc ", 5) && config_get_bool("youtuberadio_embed"))
+  {
+    media_close();
+  }
+#endif
   else if(msg[0]!='/') // If we're in a PM tab, send messages as PMs
   {
     GtkNotebook* tabs=GTK_NOTEBOOK(gtk_builder_get_object(gui, "tabs"));
diff --git a/utilities/gtk/gui.c b/utilities/gtk/gui.c
index 47da826..7778df0 100644
--- a/utilities/gtk/gui.c
+++ b/utilities/gtk/gui.c
@@ -79,6 +79,10 @@ void settings_reset(GtkBuilder* gui)
   logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
   gtk_entry_set_text(GTK_ENTRY(logpath), config_get_str("logpath_pm"));
   // Youtube
+#ifdef HAVE_AVFORMAT
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_embed"));
+  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(option), config_get_bool("youtuberadio_embed"));
+#endif
   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"));
@@ -141,10 +145,14 @@ void savesettings(GtkButton* button, GtkBuilder* gui)
   logpath=GTK_WIDGET(gtk_builder_get_object(gui, "logpath_pm"));
   config_set("logpath_pm", gtk_entry_get_text(GTK_ENTRY(logpath)));
   // Youtube
+#ifdef HAVE_AVFORMAT
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_embed"));
+  config_set("youtuberadio_embed", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(option))?"True":"False");
+#endif
   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");
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
+  config_set("youtuberadio_cmd", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(option))?"True":"False");
   // Cameras
   option=GTK_WIDGET(gtk_builder_get_object(gui, "camdownonjoin"));
   config_set("camdownonjoin", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(option))?"True":"False");
@@ -1002,6 +1010,10 @@ void gui_init(char frombuild)
   // Youtube
   option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_cmd"));
   g_signal_connect(option, "toggled", G_CALLBACK(toggle_youtubecmd), gui);
+  #ifndef HAVE_AVFORMAT
+  option=GTK_WIDGET(gtk_builder_get_object(gui, "youtuberadio_embed"));
+  gtk_widget_destroy(option);
+  #endif
   // Misc
   option=GTK_WIDGET(gtk_builder_get_object(gui, "camdownonjoin"));
 
diff --git a/utilities/gtk/main.h b/utilities/gtk/main.h
index 8443ba6..55a46f0 100644
--- a/utilities/gtk/main.h
+++ b/utilities/gtk/main.h
@@ -15,6 +15,7 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 extern char* nickname;
+extern gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer x);
 extern void togglecam(GtkCheckMenuItem* item, void* x);
 #ifdef HAVE_PULSEAUDIO
 void togglemic(GtkCheckMenuItem* item, void* x);
diff --git a/utilities/gtk/media.c b/utilities/gtk/media.c
index 2e9b693..0378838 100644
--- a/utilities/gtk/media.c
+++ b/utilities/gtk/media.c
@@ -165,10 +165,16 @@ struct camera* camera_new(const char* nick, const char* id)
   ++camcount;
   cams=realloc(cams, sizeof(struct camera)*camcount);
   struct camera* cam=&cams[camcount-1];
+  cam->vctx=0;
 #if defined(HAVE_AVRESAMPLE) || defined(HAVE_SWRESAMPLE)
   cam->actx=0;
   cam->samples=0;
   cam->samplecount=0;
+  #ifdef HAVE_AVRESAMPLE
+  cam->resamplectx=0;
+  #else
+  cam->swrctx=0;
+  #endif
 #endif
   cam->nick=strdup(nick);
   cam->id=strdup(id);
diff --git a/utilities/gtk/media.h b/utilities/gtk/media.h
index bb0757c..e58d101 100644
--- a/utilities/gtk/media.h
+++ b/utilities/gtk/media.h
@@ -16,6 +16,8 @@
 */
 #ifndef MEDIA_H
 #define MEDIA_H
+#include <glib.h>
+#include <gtk/gtk.h>
 #include <libavcodec/avcodec.h>
 #ifdef HAVE_AVRESAMPLE
   #include <libavresample/avresample.h>
diff --git a/utilities/gtk/playmedia.c b/utilities/gtk/playmedia.c
new file mode 100644
index 0000000..89ff815
--- /dev/null
+++ b/utilities/gtk/playmedia.c
@@ -0,0 +1,239 @@
+#ifdef HAVE_AVFORMAT
+#include <stdio.h>
+#include <unistd.h>
+#include <libavformat/avformat.h>
+#include <libavformat/avio.h>
+#include "media.h"
+#include "main.h"
+#include "playmedia.h"
+
+struct frame
+{
+  unsigned long int duration;
+  unsigned long int pts;
+  void* buf;
+  unsigned int size;
+};
+
+struct framequeue
+{
+  struct frame* frames;
+  unsigned int size;
+  unsigned int len;
+};
+
+struct playmediadata
+{
+  char* id;
+  int fd;
+  long int seek;
+};
+
+static GThread* playingmedia=0;
+static long int playmedia_seek=-1;
+
+static void queue_push(struct framequeue* queue, AVPacket* packet, unsigned long int duration, unsigned long int pts)
+{
+  if(queue->len==queue->size)
+  {
+    queue->size+=10;
+    queue->frames=realloc(queue->frames, sizeof(struct frame)*queue->size);
+  }
+  queue->frames[queue->len].duration=duration;
+  queue->frames[queue->len].pts=pts;
+  queue->frames[queue->len].buf=malloc(packet->size);
+  queue->frames[queue->len].size=packet->size;
+  // Prefer pts (presentation timestamp) when available (more reliable than the provided duration)
+  if(queue->len && pts!=AV_NOPTS_VALUE && queue->frames[queue->len-1].pts!=AV_NOPTS_VALUE)
+  {
+    queue->frames[queue->len-1].duration=pts-queue->frames[queue->len-1].pts;
+  }
+  memcpy(queue->frames[queue->len].buf, packet->data, packet->size);
+  ++queue->len;
+}
+
+static void queue_pop(struct framequeue* queue)
+{
+  free(queue->frames[0].buf);
+  usleep(queue->frames[0].duration);
+  --queue->len;
+  memmove(queue->frames, &queue->frames[1], sizeof(struct frame)*queue->len);
+}
+
+static void queue_empty(struct framequeue* queue)
+{
+  unsigned int i;
+  for(i=0; i<queue->len; ++i)
+  {
+    free(queue->frames[i].buf);
+  }
+  free(queue->frames);
+  queue->frames=0;
+  queue->len=0;
+  queue->size=0;
+}
+
+// TODO: Handle soundcloud
+// TODO: Handle /mbsp (start paused), /mbpl (resume), /mbpa (pause)
+void* playmedia(void* data)
+{
+  struct playmediadata* pmd=data;
+  int fd=pmd->fd;
+  playmedia_seek=pmd->seek;
+  // Get the URL for the actual file
+  if(strchr(pmd->id, '\'')){free(pmd->id); free(pmd); return 0;}
+  char cmd[strlen("youtube-dl -f 'best[height<=480]' -g -- ''0")+strlen(pmd->id)];
+  strcpy(cmd, "youtube-dl -f 'best[height<=480]' -g -- '");
+  strcat(cmd, pmd->id);
+  strcat(cmd, "'");
+  free(pmd->id);
+  free(pmd);
+  FILE* ytdl=popen(cmd, "r");
+  if(!ytdl){return 0;}
+  char url[2048];
+  fgets(url, 2048, ytdl);
+  pclose(ytdl);
+  char* end;
+  if((end=strchr(url, '\n'))){end[0]=0;}
+  if((end=strchr(url, '\r'))){end[0]=0;}
+  if(!url[0]){return 0;}
+
+  av_register_all();
+  avformat_network_init();
+  AVDictionary* options=0;
+  av_dict_set(&options, "user_agent", "tc_client-gtk (using libavformat)", 0);
+  AVFormatContext* avfctx=0;
+  if(avformat_open_input(&avfctx, url, 0, &options)){return 0;}
+  avformat_find_stream_info(avfctx, 0);
+  AVCodec* vcodec=0;
+  AVCodec* acodec=0;
+  int stream_v=av_find_best_stream(avfctx, AVMEDIA_TYPE_VIDEO, -1, -1, &vcodec, 0);
+  int stream_a=av_find_best_stream(avfctx, AVMEDIA_TYPE_AUDIO, -1, -1, &acodec, 0);
+
+  // Ask the main thread to set up the camera for us
+  write(fd, "Starting media stream for YouTube (media)\n", 42);
+  struct camera* cam;
+  unsigned int i=0;
+  while(!(cam=camera_find("media")) && i<30){++i; usleep(1000);}
+  if(!cam)
+  {
+    avformat_close_input(&avfctx);
+    return 0;
+  }
+  // Set up video decoder
+  cam->vctx=avcodec_alloc_context3(vcodec);
+  avcodec_parameters_to_context(cam->vctx, avfctx->streams[stream_v]->codecpar);
+  avcodec_open2(cam->vctx, vcodec, 0);
+  // Set up audio decoder+resampler
+  cam->actx=avcodec_alloc_context3(acodec);
+  avcodec_parameters_to_context(cam->actx, avfctx->streams[stream_a]->codecpar);
+  avcodec_open2(cam->actx, acodec, 0);
+  cam->samplerate=cam->actx->sample_rate;
+  // TODO: Figure out why planar formats don't resample well without pretending they're non-planar (sample case: PXj51mQHz5I)
+  //  For now, just pass off the first planar channel as non-planar single-channel
+  enum AVSampleFormat samplefmt=av_get_alt_sample_fmt(cam->actx->sample_fmt, 0);
+  int chanlayout=cam->actx->channel_layout;
+  if(samplefmt!=cam->actx->sample_fmt){chanlayout=AV_CH_FRONT_CENTER;}
+  #ifdef HAVE_AVRESAMPLE
+    cam->resamplectx=avresample_alloc_context();
+    av_opt_set_int(cam->resamplectx, "in_channel_layout", chanlayout, 0);
+    av_opt_set_int(cam->resamplectx, "in_sample_fmt", samplefmt, 0);
+    av_opt_set_int(cam->resamplectx, "in_sample_rate", cam->actx->sample_rate, 0);
+    av_opt_set_int(cam->resamplectx, "out_channel_layout", AV_CH_FRONT_CENTER, 0);
+    av_opt_set_int(cam->resamplectx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
+    av_opt_set_int(cam->resamplectx, "out_sample_rate", SAMPLERATE_OUT, 0);
+    avresample_open(cam->resamplectx);
+  #elif defined(HAVE_SWRESAMPLE)
+    cam->swrctx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, SAMPLERATE_OUT, chanlayout, samplefmt, cam->actx->sample_rate, 0, 0);
+    swr_init(cam->swrctx);
+  #endif
+
+  AVPacket packet={0};
+  struct framequeue vqueue={0,0,0};
+  struct framequeue aqueue={0,0,0};
+  while(playingmedia && av_read_frame(avfctx, &packet)==0)
+  {
+    unsigned long int num=avfctx->streams[packet.stream_index]->time_base.num;
+    unsigned long int den=avfctx->streams[packet.stream_index]->time_base.den;
+    unsigned long int pts=packet.pts;
+    if(pts!=AV_NOPTS_VALUE){pts=num*1000000*pts/den;}
+    if(packet.stream_index==stream_v)
+    {
+      queue_push(&vqueue, &packet, num*1000000*packet.duration/den, pts);
+    }
+    else if(packet.stream_index==stream_a)
+    {
+      queue_push(&aqueue, &packet, num*1000000*packet.duration/den, pts);
+    }
+    // TODO: Handle video-only and audio-only?
+    if(aqueue.len<2 || vqueue.len<2){continue;}
+    if(aqueue.frames[0].duration>vqueue.frames[0].duration)
+    {
+      aqueue.frames[0].duration-=vqueue.frames[0].duration;
+      dprintf(fd, "Video: media %u\n", vqueue.frames[0].size+1);
+      write(fd, "\x22", 1);
+      write(fd, vqueue.frames[0].buf, vqueue.frames[0].size);
+      queue_pop(&vqueue);
+    }else{
+      vqueue.frames[0].duration-=aqueue.frames[0].duration;
+      dprintf(fd, "Audio: media %u\n", aqueue.frames[0].size+1);
+      write(fd, "\x22", 1);
+      write(fd, aqueue.frames[0].buf, aqueue.frames[0].size);
+      queue_pop(&aqueue);
+    }
+
+    if(playmedia_seek!=-1)
+    {
+      // Convert tinychat's milliseconds to whatever libavformat uses (microseconds)
+      av_seek_frame(avfctx, -1, playmedia_seek*AV_TIME_BASE/1000, AVSEEK_FLAG_ANY);
+      playmedia_seek=-1;
+      queue_empty(&vqueue);
+      queue_empty(&aqueue);
+    }
+  }
+  write(fd, "VideoEnd: media\n", 16);
+  queue_empty(&vqueue);
+  queue_empty(&aqueue);
+  playingmedia=0;
+  avformat_close_input(&avfctx);
+  return 0;
+}
+
+void media_play(const char* args)
+{
+  const char* id=strchr(args, ' ');
+  if(!id){return;}
+  id=&id[1];
+  const char* timestampstr=strchr(id, ' ');
+  if(!timestampstr){return;}
+  long int timestamp=strtol(&timestampstr[1], 0, 0);
+  int p[2];
+  pipe(p);
+  GIOChannel* iochannel=g_io_channel_unix_new(p[0]);
+  g_io_channel_set_encoding(iochannel, 0, 0);
+  g_io_add_watch(iochannel, G_IO_IN, handledata, 0);
+
+  struct playmediadata* pmd=malloc(sizeof(struct playmediadata));
+  pmd->id=strndup(id, timestampstr-id);
+  pmd->fd=p[1];
+  pmd->seek=(timestamp?timestamp:-1);
+  media_close();
+  playingmedia=g_thread_new("media", playmedia, pmd);
+}
+
+void media_seek(long int ms)
+{
+  playmedia_seek=ms;
+}
+
+void media_close(void)
+{
+  if(playingmedia)
+  {
+    GThread* t=playingmedia;
+    playingmedia=0;
+    g_thread_join(t);
+    camera_remove("media", 0);
+  }
+}
+#endif
diff --git a/utilities/gtk/playmedia.h b/utilities/gtk/playmedia.h
new file mode 100644
index 0000000..e0607e7
--- /dev/null
+++ b/utilities/gtk/playmedia.h
@@ -0,0 +1,6 @@
+#ifdef HAVE_AVFORMAT
+extern void* playmedia(void* data);
+extern void media_play(const char* args);
+extern void media_seek(long int ms);
+extern void media_close(void);
+#endif