$ 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="[31mbroken[0m"
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="[31mbroken[0m"
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(×tampstr[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