$ git clone http://tcclient.ion.nu/tc_client.git
commit 55feb9deed08bf6dd7a18b0e57246ea8ed42247f
Author: Alicia <...>
Date: Tue Apr 7 06:49:01 2015 +0200
Version 0.29
diff --git a/ChangeLog b/ChangeLog
index 99ac586..e64b4f4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,18 @@
+0.29:
+Added testbuilds.sh to automatically try building the project with different configurations to verify that the latest changes did not break it in some cases.
+Check for endianness macros in the configure script and if they are missing/broken, test endianness and add our own macros.
+If getkey() returns null, print a descriptive message and exit (instead of segfaulting)
+Added optional code to record incoming RTMP packets, add -DRTMP_DEBUG=1 to CFLAGS to enable.
+Added compatibility hacks for apple systems (building onto android ones)
+Handle /help without sending it as a message.
+camviewer: create a new pipe for showing our own cam instead of mixing it in with tc_client's output.
+camviewer: remove associated cameras when a user leaves (in case there was no explicit message about ending the stream)
+camviewer: support older versions of libav which had parts of libavutil in libavcore instead.
+Forked camviewer into tc_client-gtk to keep camviewer as a simpler demo while tc_client-gtk develops into a more complete client interface.
+tc_client-gtk: added a menubar and moved the broadcasting option to there instead of as a button.
+tc_client-gtk: scale cams to fit/fill into the window.
+tc_client-gtk: added a text view for chat, an input field and a user list, switched to a glade file instead of building the GUI manually from code.
+modbot: added a !syncvid command to synchronize video position (or to see a video that was started while having youtube videos disabled in the flash client)
0.28:
Delete streams when they stop, to allow reusing stream IDs.
Added a /camdown command.
diff --git a/Makefile b/Makefile
index 72a37d8..19a173a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.28
+VERSION=0.29
CFLAGS=-g3 -Wall $(shell curl-config --cflags)
LIBS=-g3 $(shell curl-config --libs)
ifneq ($(wildcard config.mk),)
@@ -9,12 +9,13 @@ IRCHACK_OBJ=utilities/irchack/irchack.o utilities/compat.o
MODBOT_OBJ=utilities/modbot/modbot.o utilities/list.o utilities/modbot/queue.o
CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o 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
UTILS=irchack modbot
ifdef GTK_LIBS
ifdef AVCODEC_LIBS
ifdef AVUTIL_LIBS
ifdef SWSCALE_LIBS
- UTILS+=camviewer
+ UTILS+=camviewer tc_client-gtk
CFLAGS+=$(GTK_CFLAGS) $(AVCODEC_CFLAGS) $(AVUTIL_CFLAGS) $(SWSCALE_CFLAGS)
ifdef AO_LIBS
ifdef AVRESAMPLE_LIBS
@@ -55,8 +56,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 $@
+
clean:
- rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) tc_client irchack modbot camviewer cursedchat
+ rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) $(TC_CLIENT_GTK_OBJ) tc_client irchack modbot camviewer cursedchat tc_client-gtk
tarball:
- tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/modbot/commands.html utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h configure
+ tar -cJf tc_client-$(VERSION).tar.xz --transform='s|^|tc_client-$(VERSION)/|' Makefile client.c amfparser.c rtmp.c numlist.c amfwriter.c idlist.c colors.c endian.c media.c amfparser.h rtmp.h numlist.h amfwriter.h idlist.h colors.h endian.h media.h LICENSE README ChangeLog crossbuild.sh testbuilds.sh utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/queue.c utilities/modbot/queue.h utilities/modbot/commands.html utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h utilities/gtk/camviewer.c utilities/gtk/userlist.c utilities/gtk/media.c utilities/gtk/compat.c utilities/gtk/userlist.h utilities/gtk/media.h utilities/gtk/compat.h utilities/compat.c utilities/compat.h utilities/list.c utilities/list.h gtkgui.glade configure
diff --git a/client.c b/client.c
index b259d22..1f55248 100644
--- a/client.c
+++ b/client.c
@@ -229,8 +229,14 @@ void usage(const char* me)
"-u, --user <user> Username of tinychat account to use.\n"
"-p, --pass <pass> Password of tinychat account to use.\n"
"-c, --color <value> Color to use in chat.\n"
+#ifdef RTMP_DEBUG
+ " --rtmplog <file> Write RTMP input to file.\n"
+#endif
,me);
}
+#ifdef RTMP_DEBUG
+extern int rtmplog;
+#endif
int main(int argc, char** argv)
{
@@ -262,6 +268,9 @@ int main(int argc, char** argv)
++i;
currentcolor=atoi(argv[i]);
}
+#ifdef RTMP_DEBUG
+ else if(!strcmp(argv[i], "--rtmplog")){++i; rtmplog=open(argv[i], O_WRONLY|O_CREAT|O_TRUNC, 0600); if(rtmplog<0){perror("rtmplog: open");}}
+#endif
else if(!channel){channel=argv[i];}
else if(!nickname){nickname=strdup(argv[i]);}
else if(!password){password=argv[i];}
@@ -437,6 +446,7 @@ int main(int argc, char** argv)
"/camdown = close the broadcasting stream\n"
"/video <length> = send a <length> bytes long encoded frame, send the frame data after this line\n");
fflush(stdout);
+ continue;
}
else if(!strncmp(buf, "/color", 6) && (!buf[6]||buf[6]==' '))
{
@@ -642,6 +652,7 @@ int main(int argc, char** argv)
char* id=amfin->items[amfin->itemcount-1].string.string;
printf("Connection ID: %s\n", id);
char* key=getkey(id, channel);
+ if(!key){printf("Failed to get channel key\n"); return 1;}
amfinit(&amf, 3);
amfstring(&amf, "cauth");
diff --git a/configure b/configure
index 166d010..78aaae3 100755
--- a/configure
+++ b/configure
@@ -151,4 +151,30 @@ else
echo no
fi
+# TODO: handle crosscompiling better
+printf 'Checking if endianness macros work... '
+echo '#include <stdio.h>' > endiantest.c
+echo 'int main(){' >> endiantest.c
+echo '#if defined(__ORDER_LITTLE_ENDIAN__) && defined(__ORDER_BIG_ENDIAN__) && defined(__BYTE_ORDER__)' >> endiantest.c
+echo ' if(__ORDER_LITTLE_ENDIAN__==__ORDER_BIG_ENDIAN__){return 1;}' >> endiantest.c
+echo ' if(__BYTE_ORDER__!=__ORDER_LITTLE_ENDIAN__ && __BYTE_ORDER__!=__ORDER_BIG_ENDIAN__){return 1;}' >> endiantest.c
+echo ' return 0;' >> endiantest.c
+echo '#else' >> endiantest.c
+echo ' return 1;' >> endiantest.c
+echo '#endif' >> endiantest.c
+echo '}' >> endiantest.c
+"$CC" endiantest.c -o endiantest > /dev/null 2> /dev/null
+if ./endiantest 2> /dev/null; then
+ echo yes
+else
+ echo no
+ printf 'Checking endianness and writing our own macros... '
+ echo '#include <stdio.h>' > endiantest.c
+ echo 'int main(){int num=1;char* x=(char*)#if(x[0]){printf("CFLAGS+=-D__ORDER_LITTLE_ENDIAN__=1 -D__ORDER_BIG_ENDIAN__=2 -D__BYTE_ORDER__=1\n");}else{printf("CFLAGS+=-D__ORDER_LITTLE_ENDIAN__=1 -D__ORDER_BIG_ENDIAN__=2 -D__BYTE_ORDER__=2\n");}return 0;}' >> endiantest.c
+ "$CC" endiantest.c -o endiantest > /dev/null 2> /dev/null
+ ./endiantest >> config.mk
+ echo done
+fi
+rm -f endiantest.c endiantest
+
echo Done
diff --git a/gtkgui.glade b/gtkgui.glade
index 8a12566..9661ca8 100644
--- a/gtkgui.glade
+++ b/gtkgui.glade
@@ -2,48 +2,11 @@
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.0"/>
- <object class="GtkWindow" id="channelpasswordwindow">
- <property name="can_focus">False</property>
- <property name="title" translatable="yes">Channel password?</property>
- <signal name="destroy" handler="gtk_main_quit" swapped="no"/>
- <child>
- <object class="GtkBox" id="box4">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkEntry" id="channelpassword">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="visibility">False</property>
- <property name="input_purpose">password</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="channelpasswordbutton">
- <property name="label" translatable="yes">Enter</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
<object class="GtkWindow" id="main">
<property name="can_focus">False</property>
<property name="title" translatable="yes">tc_client</property>
- <property name="default_width">300</property>
- <property name="default_height">400</property>
+ <property name="default_width">200</property>
+ <property name="default_height">300</property>
<signal name="destroy" handler="gtk_main_quit" swapped="no"/>
<child>
<object class="GtkBox" id="vbox1">
@@ -75,28 +38,6 @@
</child>
</object>
</child>
- <child>
- <object class="GtkMenuItem" id="menuitem1">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Options</property>
- <property name="use_underline">True</property>
- <child type="submenu">
- <object class="GtkMenu" id="menu1">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkMenuItem" id="menuitem_options_settings">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Settings</property>
- <property name="use_underline">True</property>
- </object>
- </child>
- </object>
- </child>
- </object>
- </child>
</object>
<packing>
<property name="expand">False</property>
@@ -113,6 +54,8 @@
<object class="GtkScrolledWindow" id="camerascroll">
<property name="visible">True</property>
<property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">never</property>
<child>
<object class="GtkViewport" id="viewport1">
<property name="visible">True</property>
@@ -199,7 +142,6 @@
<object class="GtkEntry" id="inputfield">
<property name="visible">True</property>
<property name="can_focus">True</property>
- <property name="has_focus">True</property>
</object>
<packing>
<property name="expand">False</property>
@@ -223,526 +165,4 @@
</object>
</child>
</object>
- <object class="GtkWindow" id="settings">
- <property name="can_focus">False</property>
- <property name="title" translatable="yes">tc_client settings</property>
- <property name="default_width">350</property>
- <property name="default_height">200</property>
- <signal name="delete-event" handler="gtk_widget_hide_on_delete" swapped="no"/>
- <child>
- <object class="GtkBox" id="box1">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkNotebook" id="pager">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="tab_pos">left</property>
- <property name="scrollable">True</property>
- <child>
- <object class="GtkScrolledWindow" id="scrolledwindow1">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="shadow_type">in</property>
- <child>
- <object class="GtkViewport" id="viewport2">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkBox" id="box2">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkCheckButton" id="enable_logging">
- <property name="label" translatable="yes">Enable logging</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="halign">start</property>
- <property name="xalign">0</property>
- <property name="draw_indicator">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label3">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Path for channel: (%h=home directory, %c=channelname)</property>
- <property name="wrap">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="logpath_channel">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label4">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Path for PMs: (%h=home directory, %c=channelname, %n=nickname)</property>
- <property name="wrap">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="logpath_pm">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">4</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- </child>
- </object>
- </child>
- <child type="tab">
- <object class="GtkLabel" id="label1">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Logging</property>
- </object>
- <packing>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <object class="GtkScrolledWindow" id="scrolledwindow2">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="shadow_type">in</property>
- <child>
- <object class="GtkViewport" id="viewport3">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkBox" id="box3">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkRadioButton" id="soundradio_none">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="halign">start</property>
- <property name="xalign">0</property>
- <property name="active">True</property>
- <property name="draw_indicator">True</property>
- <child>
- <object class="GtkLabel" id="label14">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Do not play notification sounds</property>
- <property name="wrap">True</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkRadioButton" id="soundradio_cmd">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="halign">start</property>
- <property name="xalign">0</property>
- <property name="draw_indicator">True</property>
- <property name="group">soundradio_none</property>
- <child>
- <object class="GtkLabel" id="label13">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Run a command to play sounds:</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="GtkEntry" id="soundcmd">
- <property name="visible">True</property>
- <property name="sensitive">False</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">1</property>
- </packing>
- </child>
- <child type="tab">
- <object class="GtkLabel" id="label2">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Sound</property>
- </object>
- <packing>
- <property name="position">1</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- <child>
- <object class="GtkScrolledWindow" id="scrolledwindow3">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="shadow_type">in</property>
- <child>
- <object class="GtkViewport" id="viewport4">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <child>
- <object class="GtkBox" id="box5">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkRadioButton" id="youtuberadio_none">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="halign">start</property>
- <property name="xalign">0</property>
- <property name="active">True</property>
- <property name="draw_indicator">True</property>
- <child>
- <object class="GtkLabel" id="label12">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Do not play youtube videos</property>
- <property name="wrap">True</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkRadioButton" id="youtuberadio_cmd">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">False</property>
- <property name="halign">start</property>
- <property name="xalign">0</property>
- <property name="active">True</property>
- <property name="draw_indicator">True</property>
- <property name="group">youtuberadio_none</property>
- <child>
- <object class="GtkLabel" id="label11">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Run a command to play youtube videos (%i=video ID, %t=offset in milliseconds):</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="GtkEntry" id="youtubecmd">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label15">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Example: youtube-dl -o - -- '%i' | mplayer -ss "`expr '%t' / 1000`" -</property>
- <property name="wrap">True</property>
- <property name="selectable">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- </child>
- </object>
- <packing>
- <property name="position">2</property>
- </packing>
- </child>
- <child type="tab">
- <object class="GtkLabel" id="label10">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Youtube</property>
- </object>
- <packing>
- <property name="position">2</property>
- <property name="tab_fill">False</property>
- </packing>
- </child>
- </object>
- <packing>
- <property name="expand">True</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="save_settings">
- <property name="label" translatable="yes">OK</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- <property name="halign">end</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">False</property>
- <property name="position">1</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
- <object class="GtkWindow" id="startwindow">
- <property name="can_focus">False</property>
- <property name="title" translatable="yes">tc_client</property>
- <property name="resizable">False</property>
- <signal name="destroy" handler="gtk_main_quit" swapped="no"/>
- <child>
- <object class="GtkBox" id="box6">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkExpander" id="expander1">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <child>
- <object class="GtkBox" id="box7">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="orientation">vertical</property>
- <child>
- <object class="GtkLabel" id="label8">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Username:</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="acc_username">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label9">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Password:</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="acc_password">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="visibility">False</property>
- <property name="input_purpose">password</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkCheckButton" id="start_rememberacc">
- <property name="label" translatable="yes">Remember account</property>
- <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>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">4</property>
- </packing>
- </child>
- </object>
- </child>
- <child type="label">
- <object class="GtkLabel" id="label5">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="label" translatable="yes">Account</property>
- </object>
- </child>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">0</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label6">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Nickname:</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">1</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="start_nick">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">2</property>
- </packing>
- </child>
- <child>
- <object class="GtkLabel" id="label7">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="halign">start</property>
- <property name="label" translatable="yes">Channel:</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">3</property>
- </packing>
- </child>
- <child>
- <object class="GtkEntry" id="start_channel">
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">4</property>
- </packing>
- </child>
- <child>
- <object class="GtkCheckButton" id="start_rememberchan">
- <property name="label" translatable="yes">Remember nickname and channel</property>
- <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>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">5</property>
- </packing>
- </child>
- <child>
- <object class="GtkButton" id="connectbutton">
- <property name="label" translatable="yes">Connect</property>
- <property name="visible">True</property>
- <property name="can_focus">True</property>
- <property name="receives_default">True</property>
- </object>
- <packing>
- <property name="expand">False</property>
- <property name="fill">True</property>
- <property name="position">6</property>
- </packing>
- </child>
- </object>
- </child>
- </object>
</interface>
diff --git a/media.c b/media.c
index 7e2e166..b224911 100644
--- a/media.c
+++ b/media.c
@@ -52,6 +52,7 @@ void stream_start(const char* nick, int sock) // called upon privmsg "/opencam .
amfnull(&amf);
amfsend(&amf, sock);
printf("Starting media stream for %s (%u)\n", nick, userid);
+ fflush(stdout);
}
void streamout_start(unsigned int id, int sock) // called upon privmsg "/camup"
diff --git a/numlist.c b/numlist.c
index e39aad6..47635f1 100644
--- a/numlist.c
+++ b/numlist.c
@@ -31,7 +31,7 @@ char* fromnumlist(char* in, size_t* outlen)
++len;
x=&x[1];
}
-#ifdef __ANDROID__
+#if defined(__ANDROID__) || defined(__APPLE__)
*outlen=len;
unsigned char* string=malloc(len+1);
#else
@@ -46,7 +46,7 @@ char* fromnumlist(char* in, size_t* outlen)
}
string[len]=0;
-#ifdef __ANDROID__
+#if defined(__ANDROID__) || defined(__APPLE__)
return string;
#else
iconv_t cd=iconv_open("", "utf-16");
@@ -66,7 +66,7 @@ char* fromnumlist(char* in, size_t* outlen)
char* tonumlist(char* i_in, size_t len)
{
-#ifdef __ANDROID__
+#if defined(__ANDROID__) || defined(__APPLE__)
#define in i_in
#else
iconv_t cd=iconv_open("utf-16le", "");
diff --git a/rtmp.c b/rtmp.c
index 2956d12..b17b704 100644
--- a/rtmp.c
+++ b/rtmp.c
@@ -34,6 +34,9 @@ struct chunk
struct chunk* chunks=0;
unsigned int chunkcount=0;
unsigned int chunksize_in=128;
+#ifdef RTMP_DEBUG
+int rtmplog=-1;
+#endif
size_t fullread(int fd, void* buf, size_t len)
{
@@ -42,6 +45,12 @@ size_t fullread(int fd, void* buf, size_t len)
{
size_t r=read(fd, buf, remaining);
if(r<1){return 0;}
+#ifdef RTMP_DEBUG
+ if(rtmplog>-1)
+ {
+ write(rtmplog, buf, r);
+ }
+#endif
remaining-=r;
buf+=r;
}
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index a11142b..174c60a 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -21,7 +21,11 @@
#include <sys/prctl.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
-#include <libavutil/imgutils.h>
+#if LIBAVUTIL_VERSION_MAJOR>50 || (LIBAVUTIL_VERSION_MAJOR==50 && LIBAVUTIL_VERSION_MINOR>37)
+ #include <libavutil/imgutils.h>
+#else
+ #include <libavcore/imgutils.h>
+#endif
#ifdef HAVE_SOUND
// TODO: use libavresample instead if available
#if HAVE_SOUND==avresample
@@ -118,6 +122,28 @@ void camera_playsnd(struct viddata* data, struct camera* cam, short* samples, un
}
#endif
+void camera_remove(struct viddata* data, const char* nick)
+{
+ unsigned int i;
+ for(i=0; i<data->camcount; ++i)
+ {
+ if(!strcmp(data->cams[i].id, nick))
+ {
+ gtk_widget_destroy(data->cams[i].box);
+ av_frame_free(&data->cams[i].frame);
+ avcodec_free_context(&data->cams[i].vctx);
+#ifdef HAVE_SOUND
+ avcodec_free_context(&data->cams[i].actx);
+#endif
+ free(data->cams[i].id);
+ free(data->cams[i].nick);
+ --data->camcount;
+ memmove(&data->cams[i], &data->cams[i+1], (data->camcount-i)*sizeof(struct camera));
+ break;
+ }
+ }
+}
+
char buf[1024];
gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
{
@@ -151,6 +177,13 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
dprintf(tc_client_in[1], "/opencam %s\n", buf);
return 1;
}
+ // Make sure the cam goes away when a user leaves
+ else if(space && !strcmp(space, " left the channel"))
+ {
+ space[0]=0;
+ camera_remove(data, buf);
+ return 1;
+ }
if(!strncmp(buf, "Starting media stream for ", 26))
{
char* nick=&buf[26];
@@ -204,23 +237,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
}
if(!strncmp(buf, "VideoEnd: ", 10))
{
- for(i=0; i<data->camcount; ++i)
- {
- if(!strcmp(data->cams[i].id, &buf[10]))
- {
- gtk_widget_destroy(data->cams[i].box);
- av_frame_free(&data->cams[i].frame);
- avcodec_free_context(&data->cams[i].vctx);
-#ifdef HAVE_SOUND
- avcodec_free_context(&data->cams[i].actx);
-#endif
- free(data->cams[i].id);
- free(data->cams[i].nick);
- --data->camcount;
- memmove(&data->cams[i], &data->cams[i+1], (data->camcount-i)*sizeof(struct camera));
- break;
- }
- }
+ camera_remove(data, &buf[10]);
return 1;
}
if(!strncmp(buf, "Audio: ", 7))
@@ -350,12 +367,16 @@ void togglecam(GtkButton* button, struct viddata* data)
dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
return;
}
+ // Set up a second pipe to be handled by handledata() to avoid overlap with tc_client's output
+ int campipe[2];
+ pipe(campipe);
dprintf(tc_client_in[1], "/camup\n");
gtk_button_set_label(button, "Stop broadcasting");
// printf("Camming up!\n");
camproc=fork();
if(!camproc)
{
+ close(campipe[0]);
prctl(PR_SET_PDEATHSIG, SIGHUP);
unsigned int delay=500000;
// Set up camera
@@ -413,15 +434,19 @@ packet.size=0;
write(tc_client_in[1], &frameinfo, 1);
write(tc_client_in[1], packet.data, packet.size);
// Also send the packet to our main thread so we can see ourselves
- dprintf(tc_client[1], "Video: out %i\n", packet.size+1);
- write(tc_client[1], &frameinfo, 1);
- write(tc_client[1], packet.data, packet.size);
+ dprintf(campipe[1], "Video: out %i\n", packet.size+1);
+ write(campipe[1], &frameinfo, 1);
+ write(campipe[1], packet.data, packet.size);
av_free_packet(&packet);
}
sws_freeContext(swsctx);
_exit(0);
}
+ close(campipe[1]);
+ GIOChannel* channel=g_io_channel_unix_new(campipe[0]);
+ g_io_channel_set_encoding(channel, 0, 0);
+ g_io_add_watch(channel, G_IO_IN, handledata, data);
}
#endif
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
new file mode 100644
index 0000000..c90ca37
--- /dev/null
+++ b/utilities/gtk/camviewer.c
@@ -0,0 +1,721 @@
+/*
+ tc_client-gtk, a graphical user interface for tc_client
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <ctype.h>
+#include <libavcodec/avcodec.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
+#ifdef HAVE_SOUND
+ // Use libavresample if available, otherwise fall back on libswresample
+ #if HAVE_SOUND==avresample
+ #include <libavutil/opt.h>
+ #include <libavresample/avresample.h>
+ #else
+ #include <libswresample/swresample.h>
+ #endif
+ #include <ao/ao.h>
+#endif
+#include <gtk/gtk.h>
+#include <gdk/gdkkeysyms.h>
+#ifdef HAVE_V4L2
+ #include <libv4l2.h>
+ #include <linux/videodev2.h>
+#endif
+#include "userlist.h"
+#include "media.h"
+#include "compat.h"
+
+struct viddata
+{
+ GtkWidget* box;
+ AVCodec* vdecoder;
+ AVCodec* vencoder;
+ AVCodec* adecoder;
+ int scalewidth;
+ int scaleheight;
+#ifdef HAVE_SOUND
+ int audiopipe;
+ #if HAVE_SOUND==avresample
+ AVAudioResampleContext* resamplectx;
+ #else
+ SwrContext* swrctx;
+ #endif
+#endif
+ GtkTextBuffer* buffer; // TODO: struct buffer array, for PMs
+ GtkAdjustment* scroll;
+};
+
+int tc_client[2];
+int tc_client_in[2];
+
+char autoscroll_before(GtkAdjustment* scroll)
+{
+ // Figure out if we're at the bottom and should autoscroll with new content
+ int upper=gtk_adjustment_get_upper(scroll);
+ int size=gtk_adjustment_get_page_size(scroll);
+ int value=gtk_adjustment_get_value(scroll);
+ return (value+size==upper);
+}
+
+void autoscroll_after(GtkAdjustment* scroll)
+{
+ while(gtk_events_pending()){gtk_main_iteration();} // Make sure the textview's new size affects scroll's "upper" value first
+ int upper=gtk_adjustment_get_upper(scroll);
+ int size=gtk_adjustment_get_page_size(scroll);
+ gtk_adjustment_set_value(scroll, upper-size);
+}
+
+void updatescaling(struct viddata* data, unsigned int width, unsigned int height)
+{
+// TODO: Move updatescaling into media.c?
+ if(!camcount){return;}
+ if(!width){width=gtk_widget_get_allocated_width(data->box);}
+ if(!height){height=gtk_widget_get_allocated_height(data->box);}
+ data->scalewidth=width/camcount;
+ // 3/4 ratio
+ data->scaleheight=data->scalewidth*3/4;
+ unsigned int i;
+ unsigned int labelsize=0;
+ for(i=0; i<camcount; ++i)
+ {
+ if(gtk_widget_get_allocated_height(cams[i].label)>labelsize)
+ labelsize=gtk_widget_get_allocated_height(cams[i].label);
+ }
+ // Fit by height
+ if(height<data->scaleheight+labelsize)
+ {
+ data->scaleheight=height-labelsize;
+ data->scalewidth=data->scaleheight*4/3;
+ }
+ if(data->scalewidth<8){data->scalewidth=8;}
+ if(data->scaleheight<1){data->scaleheight=1;}
+ // TODO: wrapping and stuff
+ // Rescale current images to fit
+ for(i=0; i<camcount; ++i)
+ {
+ GdkPixbuf* pixbuf=gtk_image_get_pixbuf(GTK_IMAGE(cams[i].cam));
+ if(!pixbuf){continue;}
+ pixbuf=gdk_pixbuf_scale_simple(pixbuf, data->scalewidth, data->scaleheight, GDK_INTERP_BILINEAR);
+ gtk_image_set_from_pixbuf(GTK_IMAGE(cams[i].cam), pixbuf);
+// TODO: figure out/fix the "static" noise that seems to happen here
+ }
+}
+
+void printchat(struct viddata* data, const char* text)
+{
+ char bottom=autoscroll_before(data->scroll);
+ // Insert new content
+ GtkTextIter end;
+ gtk_text_buffer_get_end_iter(data->buffer, &end);
+ gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
+ gtk_text_buffer_insert(data->buffer, &end, text, -1);
+ if(bottom){autoscroll_after(data->scroll);}
+}
+
+char buf[1024];
+gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
+{
+ int fd=g_io_channel_unix_get_fd(channel);
+ struct viddata* data=datap;
+ unsigned int i;
+ for(i=0; i<1023; ++i)
+ {
+ if(read(fd, &buf[i], 1)<1){printf("No more data\n"); gtk_main_quit(); return 0;}
+ if(buf[i]=='\r'||buf[i]=='\n'){break;}
+ }
+ buf[i]=0;
+ if(!strncmp(buf, "Currently online: ", 18))
+ {
+ printchat(data, buf);
+ char* next=&buf[16];
+ while(next)
+ {
+ char* nick=&next[2];
+ next=strstr(nick, ", ");
+ if(next){next[0]=0;}
+ adduser(nick);
+ }
+ return 1;
+ }
+ // Start streams once we're properly connected
+ if(!strncmp(buf, "Currently on cam: ", 18))
+ {
+ printchat(data, buf);
+ 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], "/opencam %s\n", user);
+ }
+ return 1;
+ }
+ char* space=strchr(buf, ' ');
+ // Timestamped events
+ if(buf[0]=='['&&isdigit(buf[1])&&isdigit(buf[2])&&buf[3]==':'&&isdigit(buf[4])&&isdigit(buf[5])&&buf[6]==']'&&buf[7]==' ')
+ {
+ char* nick=&buf[8];
+ space=strchr(nick, ' ');
+ if(!space){return 1;}
+// if(space[-1]==':') // TODO: Handle messages specifically, e.g. handle /msg
+ // Remove escape codes and pick up the text color while we're at it
+ char* color=0;
+ char* esc;
+ char* escend;
+ while((esc=strchr(buf, '\x1b'))&&(escend=strchr(esc, 'm')))
+ {
+ escend[0]=0;
+ if(!color && strcmp(&esc[1], "[0")){color=strdup(&esc[1]);}
+ memmove(esc, &escend[1], strlen(&escend[1])+1);
+ }
+ char bottom=autoscroll_before(data->scroll);
+ // Insert new content
+ GtkTextIter end;
+ gtk_text_buffer_get_end_iter(data->buffer, &end);
+ gtk_text_buffer_insert(data->buffer, &end, "\n", -1);
+ int startnum=gtk_text_iter_get_offset(&end);
+ gtk_text_buffer_insert(data->buffer, &end, buf, -1);
+ if(color) // Set color if there was one
+ {
+ GtkTextIter start;
+ gtk_text_buffer_get_iter_at_offset(data->buffer, &start, startnum+8); // 8 to skip timestamps
+ gtk_text_buffer_apply_tag_by_name(data->buffer, color, &start, &end);
+ }
+ if(bottom){autoscroll_after(data->scroll);}
+ if(space[-1]!=':') // Not a message
+ {
+ if(!strcmp(space, " entered the channel"))
+ {
+ space[0]=0;
+ adduser(nick);
+ }
+ else if(!strcmp(space, " left the channel"))
+ {
+ space[0]=0;
+ removeuser(nick);
+ camera_removebynick(nick);
+ }
+ else if(!strncmp(space, " changed nickname to ", 21))
+ {
+ space[0]=0;
+ renameuser(nick, &space[21]);
+ struct camera* cam=camera_findbynick(nick);
+ if(cam)
+ {
+ free(cam->nick);
+ cam->nick=strdup(&space[21]);
+ gtk_label_set_text(GTK_LABEL(cam->label), cam->nick);
+ }
+ }
+ }
+ return 1;
+ }
+ if(space && !strcmp(space, " is a moderator."))
+ {
+ space[0]=0;
+ struct user* user=finduser(buf);
+ if(user)
+ {
+ user->ismod=1;
+ renameuser(buf, buf); // Update the userlist label
+ }
+ return 1;
+ }
+ if(space && !strcmp(space, " is no longer a moderator."))
+ {
+ space[0]=0;
+ struct user* user=finduser(buf);
+ if(user){user->ismod=0;}
+ return 1;
+ }
+ // Start a stream when someone cams up
+ if(space && !strcmp(space, " cammed up"))
+ {
+ space[0]=0;
+ dprintf(tc_client_in[1], "/opencam %s\n", buf);
+ 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_removebynick(nick); // Remove any duplicates
+ struct camera* cam=camera_new();
+ cam->frame=av_frame_alloc();
+ cam->dstframe=av_frame_alloc();
+ cam->nick=strdup(nick);
+ cam->id=strdup(id);
+ cam->vctx=avcodec_alloc_context3(data->vdecoder);
+ avcodec_open2(cam->vctx, data->vdecoder, 0);
+#ifdef HAVE_SOUND
+ cam->actx=avcodec_alloc_context3(data->adecoder);
+ avcodec_open2(cam->actx, data->adecoder, 0);
+ cam->samples=0;
+#endif
+ cam->cam=gtk_image_new();
+ cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_set_homogeneous(GTK_BOX(cam->box), 0);
+ gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
+ cam->label=gtk_label_new(cam->nick);
+ gtk_box_pack_start(GTK_BOX(cam->box), cam->label, 0, 0, 0);
+ gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
+ gtk_widget_show_all(cam->box);
+ updatescaling(data, 0, 0);
+ while(gtk_events_pending()){gtk_main_iteration();} // Make sure the label gets its size before we calculate scaling
+ updatescaling(data, 0, 0);
+ return 1;
+ }
+ if(!strcmp(buf, "Starting outgoing media stream"))
+ {
+ struct camera* cam=camera_new();
+ cam->frame=av_frame_alloc();
+ cam->dstframe=av_frame_alloc();
+ cam->nick=strdup("You");
+ cam->id=strdup("out");
+ cam->vctx=avcodec_alloc_context3(data->vdecoder);
+ avcodec_open2(cam->vctx, data->vdecoder, 0);
+ cam->actx=0;
+ cam->cam=gtk_image_new();
+ cam->box=gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_pack_start(GTK_BOX(cam->box), cam->cam, 0, 0, 0);
+ cam->label=gtk_label_new(cam->nick);
+ gtk_box_pack_start(GTK_BOX(cam->box), cam->label, 0, 0, 0);
+ gtk_box_pack_start(GTK_BOX(data->box), cam->box, 0, 0, 0);
+ gtk_widget_show_all(cam->box);
+ updatescaling(data, 0, 0);
+ while(gtk_events_pending()){gtk_main_iteration();} // Make sure the label gets its size before we calculate scaling
+ updatescaling(data, 0, 0);
+ return 1;
+ }
+ if(!strncmp(buf, "VideoEnd: ", 10))
+ {
+ camera_remove(&buf[10]);
+ updatescaling(data, 0, 0);
+ 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;
+ read(fd, &frameinfo, 1);
+ --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)
+ {
+ pos+=read(fd, pkt.data+pos, size-pos);
+ }
+#ifdef HAVE_SOUND
+ // 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;}
+ int gotframe;
+ avcodec_decode_audio4(cam->actx, cam->frame, &gotframe, &pkt);
+ if(!gotframe){return 1;}
+ #if HAVE_SOUND==avresample
+ int outlen=avresample_convert(data->resamplectx, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples, cam->frame->data, cam->frame->linesize[0], cam->frame->nb_samples);
+ #else
+ int outlen=swr_convert(data->resamplectx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
+ #endif
+ camera_playsnd(data->audiopipe, cam, (short*)cam->frame->data[0], outlen);
+#endif
+ return 1;
+ }
+ if(strncmp(buf, "Video: ", 7)){printf("Got '%s'\n", buf); fflush(stdout); return 1;} // Ignore anything else that isn't video
+ 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]);
+ 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;
+ unsigned char frameinfo;
+ read(fd, &frameinfo, 1);
+// printf("Frametype-frame: %x\n", ((unsigned int)frameinfo&0xf0)/16);
+// printf("Frametype-codec: %x\n", (unsigned int)frameinfo&0xf);
+ unsigned int pos=0;
+ while(pos<size)
+ {
+ pos+=read(fd, pkt.data+pos, size-pos);
+ }
+ if((frameinfo&0xf)!=2){return 1;} // Not FLV1, get data but discard it
+ if(!cam){printf("No cam found with ID '%s'\n", &buf[7]); return 1;}
+ pkt.size=size;
+ int gotframe;
+ avcodec_decode_video2(cam->vctx, cam->frame, &gotframe, &pkt);
+ if(!gotframe){return 1;}
+
+ // Scale and convert to RGB24 format
+ unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, data->scalewidth, data->scaleheight);
+ unsigned char buf[bufsize];
+ cam->dstframe->data[0]=buf;
+ cam->dstframe->linesize[0]=data->scalewidth*3;
+ struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, data->scalewidth, data->scaleheight, AV_PIX_FMT_RGB24, 0, 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);
+
+ GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, data->scalewidth, data->scaleheight, cam->dstframe->linesize[0], 0, 0);
+ gtk_image_set_from_pixbuf(GTK_IMAGE(cam->cam), gdkframe);
+ // Make sure it gets redrawn in time
+ gdk_window_process_updates(gtk_widget_get_window(cam->cam), 1);
+
+ return 1;
+}
+
+#ifdef HAVE_SOUND
+void audiothread(int fd)
+{
+ ao_initialize();
+ ao_sample_format samplefmt;
+ samplefmt.bits=16;
+ samplefmt.rate=22050;
+ samplefmt.channels=1;
+ samplefmt.byte_format=AO_FMT_NATIVE; // I'm guessing libavcodec decodes it to native
+ samplefmt.matrix=0;
+ ao_option clientname={.key="client_name", .value="tc_client/camviewer", .next=0};
+ ao_device* dev=ao_open_live(ao_default_driver_id(), &samplefmt, &clientname);
+ char buf[2048];
+ size_t len;
+ while((len=read(fd, buf, 2048))>0)
+ {
+ ao_play(dev, buf, len);
+ }
+ ao_close(dev);
+}
+#endif
+
+#ifdef HAVE_V4L2
+pid_t camproc=0;
+void togglecam(GtkCheckMenuItem* item, struct viddata* data)
+{
+ if(!gtk_check_menu_item_get_active(item))
+ {
+ kill(camproc, SIGINT);
+ camproc=0;
+ dprintf(tc_client_in[1], "/camdown\n");
+ dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
+ return;
+ }
+ // Set up a second pipe to be handled by handledata() to avoid overlap with tc_client's output
+ int campipe[2];
+ pipe(campipe);
+ dprintf(tc_client_in[1], "/camup\n");
+// printf("Camming up!\n");
+ camproc=fork();
+ if(!camproc)
+ {
+ close(campipe[0]);
+ prctl(PR_SET_PDEATHSIG, SIGHUP);
+ unsigned int delay=500000;
+ // Set up camera
+ int fd=v4l2_open("/dev/video0", O_RDWR);
+ struct v4l2_format fmt;
+ fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
+ fmt.fmt.pix.width=320;
+ fmt.fmt.pix.height=240;
+ fmt.fmt.pix.pixelformat=V4L2_PIX_FMT_RGB24;
+ fmt.fmt.pix.field=V4L2_FIELD_NONE;
+ fmt.fmt.pix.bytesperline=fmt.fmt.pix.width*3;
+ fmt.fmt.pix.sizeimage=fmt.fmt.pix.bytesperline*fmt.fmt.pix.height;
+ v4l2_ioctl(fd, VIDIOC_S_FMT, &fmt);
+ AVCodecContext* ctx=avcodec_alloc_context3(data->vencoder);
+ ctx->width=fmt.fmt.pix.width;
+ ctx->height=fmt.fmt.pix.height;
+ ctx->pix_fmt=PIX_FMT_YUV420P;
+ ctx->time_base.num=1;
+ ctx->time_base.den=10;
+ avcodec_open2(ctx, data->vencoder, 0);
+ AVFrame* frame=av_frame_alloc();
+ frame->format=PIX_FMT_RGB24;
+ frame->width=fmt.fmt.pix.width;
+ frame->height=fmt.fmt.pix.height;
+ av_image_alloc(frame->data, frame->linesize, ctx->width, ctx->height, frame->format, 1);
+ AVPacket packet;
+ packet.buf=0;
+ packet.data=0;
+ packet.size=0;
+ packet.dts=AV_NOPTS_VALUE;
+ packet.pts=AV_NOPTS_VALUE;
+
+ // Set up frame for conversion from the camera's format to a format the encoder can use
+ AVFrame* dstframe=av_frame_alloc();
+ dstframe->format=ctx->pix_fmt;
+ dstframe->width=ctx->width;
+ dstframe->height=ctx->height;
+ av_image_alloc(dstframe->data, dstframe->linesize, ctx->width, ctx->height, ctx->pix_fmt, 1);
+
+ struct SwsContext* swsctx=sws_getContext(frame->width, frame->height, PIX_FMT_RGB24, frame->width, frame->height, AV_PIX_FMT_YUV420P, 0, 0, 0, 0);
+
+ while(1)
+ {
+ usleep(delay);
+ if(delay>100000){delay-=50000;}
+ v4l2_read(fd, frame->data[0], fmt.fmt.pix.sizeimage);
+ int gotpacket;
+ sws_scale(swsctx, (const uint8_t*const*)frame->data, frame->linesize, 0, frame->height, dstframe->data, dstframe->linesize);
+ av_init_packet(&packet);
+ packet.data=0;
+packet.size=0;
+ avcodec_encode_video2(ctx, &packet, dstframe, &gotpacket);
+ unsigned char frameinfo=0x22; // Note: differentiating between keyframes and non-keyframes seems to break stuff, so let's just go with all being interframes (1=keyframe, 2=interframe, 3=disposable interframe)
+ dprintf(tc_client_in[1], "/video %i\n", packet.size+1);
+ write(tc_client_in[1], &frameinfo, 1);
+ write(tc_client_in[1], packet.data, packet.size);
+ // Also send the packet to our main thread so we can see ourselves
+ dprintf(campipe[1], "Video: out %i\n", packet.size+1);
+ write(campipe[1], &frameinfo, 1);
+ write(campipe[1], packet.data, packet.size);
+
+ av_free_packet(&packet);
+ }
+ sws_freeContext(swsctx);
+ _exit(0);
+ }
+ close(campipe[1]);
+ GIOChannel* channel=g_io_channel_unix_new(campipe[0]);
+ g_io_channel_set_encoding(channel, 0, 0);
+ g_io_add_watch(channel, G_IO_IN, handledata, data);
+}
+#endif
+
+gboolean handleresize(GtkWidget* widget, GdkEventConfigure* event, struct viddata* data)
+{
+ char bottom=autoscroll_before(data->scroll);
+ if(event->width!=gtk_widget_get_allocated_width(data->box))
+ {
+ updatescaling(data, event->width, 0);
+ }
+ if(bottom){autoscroll_after(data->scroll);}
+ return 0;
+}
+
+void handleresizepane(GObject* obj, GParamSpec* spec, struct viddata* data)
+{
+ char bottom=autoscroll_before(data->scroll);
+ updatescaling(data, 0, gtk_paned_get_position(GTK_PANED(obj)));
+ if(bottom){autoscroll_after(data->scroll);}
+}
+
+gboolean inputkeys(GtkWidget* widget, GdkEventKey* event, void* data)
+{
+ if(event->keyval==GDK_KEY_Up || event->keyval==GDK_KEY_Down){return 1;}
+ if(event->keyval==GDK_KEY_Tab)
+ {
+ // Tab completion
+ int cursor=gtk_editable_get_position(GTK_EDITABLE(widget));;
+ GtkEntryBuffer* buf=gtk_entry_get_buffer(GTK_ENTRY(widget));
+ const char* text=gtk_entry_buffer_get_text(buf);
+ unsigned int namestart=0;
+ unsigned int i;
+ for(i=0; i<cursor; ++i)
+ {
+ if(text[i]==' '){namestart=i+1;}
+ }
+ const char* matches[usercount];
+ unsigned int matchcount=0;
+ unsigned int commonlen=128;
+ for(i=0; i<usercount; ++i)
+ {
+ if(!strncmp(&text[namestart], userlist[i].nick, cursor-namestart))
+ {
+ unsigned int j;
+ for(j=0; j<matchcount; ++j)
+ {
+ if(strncmp(matches[j], userlist[i].nick, commonlen))
+ {
+ for(commonlen=0; userlist[i].nick[commonlen] && matches[j][commonlen] && userlist[i].nick[commonlen]==matches[j][commonlen]; ++commonlen);
+ }
+ }
+ matches[matchcount]=userlist[i].nick;
+ ++matchcount;
+ }
+ }
+ if(matchcount==1)
+ {
+ gtk_entry_buffer_insert_text(buf, cursor, &matches[0][cursor-namestart], -1);
+ cursor+=strlen(&matches[0][cursor-namestart]);
+ if(!namestart){gtk_entry_buffer_insert_text(buf, cursor, ": ", -1); cursor+=2;}
+ gtk_editable_set_position(GTK_EDITABLE(widget), cursor);
+ }
+ else if(matchcount>1)
+ {
+ gtk_entry_buffer_insert_text(buf, cursor, &matches[0][cursor-namestart], commonlen+namestart-cursor);
+ cursor=namestart+commonlen;
+ gtk_editable_set_position(GTK_EDITABLE(widget), cursor);
+ }
+ return 1;
+ }
+ return 0;
+}
+
+void sendmessage(GtkEntry* entry, struct viddata* data)
+{
+ const char* msg=gtk_entry_get_text(entry);
+ char text[strlen("[00:00] ")+strlen("You: ")+strlen(msg)+1];
+ time_t timestamp=time(0);
+ struct tm* t=localtime(×tamp);
+ sprintf(text, "[%02i:%02i] ", t->tm_hour, t->tm_min);
+ sprintf(&text[8], "You: %s", msg);
+ printchat(data, text);
+ dprintf(tc_client_in[1], "%s\n", msg);
+ gtk_entry_set_text(entry, "");
+}
+
+int main(int argc, char** argv)
+{
+ struct viddata data={0,0,0,0,0};
+ avcodec_register_all();
+ data.vdecoder=avcodec_find_decoder(AV_CODEC_ID_FLV1);
+ data.adecoder=avcodec_find_decoder(AV_CODEC_ID_NELLYMOSER);
+
+#ifdef HAVE_SOUND
+ #if HAVE_SOUND==avresample
+ data.resamplectx=avresample_alloc_context();
+ av_opt_set_int(data.resamplectx, "in_channel_layout", AV_CH_FRONT_CENTER, 0);
+ av_opt_set_int(data.resamplectx, "in_sample_fmt", AV_SAMPLE_FMT_FLT, 0);
+ // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
+ av_opt_set_int(data.resamplectx, "in_sample_rate", 11025, 0);
+ av_opt_set_int(data.resamplectx, "out_channel_layout", AV_CH_FRONT_CENTER, 0);
+ av_opt_set_int(data.resamplectx, "out_sample_fmt", AV_SAMPLE_FMT_S16, 0);
+ av_opt_set_int(data.resamplectx, "out_sample_rate", 22050, 0);
+ avresample_open(data.resamplectx);
+ #else
+ data.resamplectx=swr_alloc_set_opts(0, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_S16, 22050, AV_CH_FRONT_CENTER, AV_SAMPLE_FMT_FLT, 11025, 0, 0);
+ swr_init(data.swrctx);
+ #endif
+ int audiopipe[2];
+ pipe(audiopipe);
+ data.audiopipe=audiopipe[1];
+ if(!fork())
+ {
+ prctl(PR_SET_PDEATHSIG, SIGHUP);
+ close(audiopipe[1]);
+ audiothread(audiopipe[0]);
+ _exit(0);
+ }
+ close(audiopipe[0]);
+#endif
+
+ gtk_init(&argc, &argv);
+ GtkBuilder* gui=gtk_builder_new_from_file("gtkgui.glade");
+ gtk_builder_connect_signals(gui, 0);
+
+#ifdef HAVE_V4L2
+ GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast_camera"));
+ g_signal_connect(item, "toggled", G_CALLBACK(togglecam), &data);
+ data.vencoder=avcodec_find_encoder(AV_CODEC_ID_FLV1);
+#else
+ GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast"));
+ gtk_widget_destroy(item);
+#endif
+
+ data.box=GTK_WIDGET(gtk_builder_get_object(gui, "cambox"));
+ userlistwidget=GTK_WIDGET(gtk_builder_get_object(gui, "userlistbox"));
+ GtkWidget* chatview=GTK_WIDGET(gtk_builder_get_object(gui, "chatview"));
+ data.scroll=gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(gtk_builder_get_object(gui, "chatscroll")));
+
+ data.buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(chatview));
+ #define colormap(code, color) gtk_text_buffer_create_tag(data.buffer, code, "foreground", color, (char*)0)
+ colormap("[31", "#821615");
+ colormap("[31;1", "#c53332");
+ colormap("[33", "#a08f23");
+ //colormap("[33", "#a78901");
+ colormap("[33;1", "#919104");
+ colormap("[32;1", "#7bb224");
+ //colormap("[32;1", "#7db257");
+ colormap("[32", "#487d21");
+ colormap("[36", "#00a990");
+ colormap("[34;1", "#32a5d9");
+ //colormap("[34;1", "#1d82eb");
+ colormap("[34", "#1965b6");
+ colormap("[35", "#5c1a7a");
+ colormap("[35;1", "#9d5bb5");
+ //colormap("[35;1", "#c356a3");
+ //colormap("[35;1", "#b9807f");
+
+ GtkWidget* panes=GTK_WIDGET(gtk_builder_get_object(gui, "vpaned"));
+ g_signal_connect(panes, "notify::position", (GCallback)handleresizepane, &data);
+
+ GtkWidget* inputfield=GTK_WIDGET(gtk_builder_get_object(gui, "inputfield"));
+ g_signal_connect(inputfield, "activate", (GCallback)sendmessage, &data);
+ g_signal_connect(inputfield, "key-press-event", (GCallback)inputkeys, &data);
+
+ GtkWidget* window=GTK_WIDGET(gtk_builder_get_object(gui, "main"));
+ g_signal_connect(window, "configure-event", (GCallback)handleresize, &data);
+ gtk_widget_show_all(window);
+
+ pipe(tc_client);
+ pipe(tc_client_in);
+ if(!fork())
+ {
+ prctl(PR_SET_PDEATHSIG, SIGHUP);
+ close(tc_client[0]);
+ close(tc_client_in[1]);
+ dup2(tc_client[1], 1);
+ dup2(tc_client_in[0], 0);
+ argv[0]="./tc_client";
+ execv("./tc_client", argv);
+ }
+ close(tc_client_in[0]);
+ GIOChannel* tcchannel=g_io_channel_unix_new(tc_client[0]);
+ g_io_channel_set_encoding(tcchannel, 0, 0);
+ unsigned int channel_id=g_io_add_watch(tcchannel, G_IO_IN, handledata, &data);
+
+ gtk_main();
+
+ g_source_remove(channel_id);
+ g_io_channel_shutdown(tcchannel, 0, 0);
+ camera_cleanup();
+#ifdef HAVE_SOUND
+ #if HAVE_SOUND==avresample
+ avresample_free(&data.resamplectx);
+ #else
+ swr_free(&data.swrctx);
+ #endif
+#endif
+ return 0;
+}
diff --git a/utilities/gtk/compat.c b/utilities/gtk/compat.c
new file mode 100644
index 0000000..4b45d2a
--- /dev/null
+++ b/utilities/gtk/compat.c
@@ -0,0 +1,93 @@
+/*
+ tc_client-gtk, a graphical user interface for tc_client
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#include <gtk/gtk.h>
+#include "compat.h"
+
+#if GTK_MAJOR_VERSION<3
+ GtkWidget* gtk_box_new(int vertical, int spacing)
+ {
+ if(vertical)
+ {
+ return gtk_vbox_new(1, spacing);
+ }else{
+ return gtk_hbox_new(1, spacing);
+ }
+ }
+ int gtk_widget_get_allocated_width(GtkWidget* widget)
+ {
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(widget, &alloc);
+ return alloc.width;
+ }
+ int gtk_widget_get_allocated_height(GtkWidget* widget)
+ {
+ GtkAllocation alloc;
+ gtk_widget_get_allocation(widget, &alloc);
+ return alloc.height;
+ }
+ char* newline(char* line)
+ {
+ unsigned int i;
+ for(i=0; line[i] && line[i]!='\n' && line[i]!='\r'; ++i);
+ return &line[i];
+ }
+ // Hack to let us load a glade GUI designed for gtk+-3.x
+ GtkBuilder* gtk_builder_new_from_file(const char* filename)
+ {
+ struct stat st;
+ if(stat(filename, &st)){return 0;}
+ char buf[st.st_size+10];
+ int f=open(filename, O_RDONLY);
+ read(f, buf, st.st_size);
+ close(f);
+ buf[st.st_size]=0;
+ char* pos;
+ if((pos=strstr(buf, "<requires ")))
+ {
+ char* end=newline(pos);
+ memmove(pos, end, strlen(end)+1);
+ }
+ char* orientation;
+ while((orientation=strstr(buf, "<property name=\"orientation\">")))
+ {
+ char dir=toupper(orientation[29]);
+ pos=newline(orientation);
+ memmove(orientation, pos, strlen(pos)+1);
+ pos=orientation;
+ while(pos>buf && strncmp(pos, "class=\"Gtk", 10)){--pos;}
+ if(pos>buf)
+ {
+ memmove(&pos[11], &pos[10], strlen(&pos[10])+1);
+ pos[10]=dir;
+ }
+ }
+ while((pos=strstr(buf, "class=\"GtkBox\"")) || (pos=strstr(buf, "class=\"GtkPaned\"")))
+ {
+ memmove(&pos[11], &pos[10], strlen(&pos[10])+1);
+ pos[10]='H'; // Default is horizontal
+ }
+ GtkBuilder* gui=gtk_builder_new();
+ GError* error=0;
+ if(!gtk_builder_add_from_string(gui, buf, -1, &error)){g_error("%s\n", error->message);}
+ return gui;
+ }
+#endif
diff --git a/utilities/gtk/compat.h b/utilities/gtk/compat.h
new file mode 100644
index 0000000..88004d2
--- /dev/null
+++ b/utilities/gtk/compat.h
@@ -0,0 +1,24 @@
+/*
+ tc_client-gtk, a graphical user interface for tc_client
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+#if GTK_MAJOR_VERSION<3
+ #define GTK_ORIENTATION_HORIZONTAL 0
+ #define GTK_ORIENTATION_VERTICAL 1
+ extern GtkWidget* gtk_box_new(int vertical, int spacing);
+ extern int gtk_widget_get_allocated_width(GtkWidget* widget);
+ extern int gtk_widget_get_allocated_height(GtkWidget* widget);
+ extern GtkBuilder* gtk_builder_new_from_file(const char* filename);
+#endif
diff --git a/utilities/gtk/media.c b/utilities/gtk/media.c
new file mode 100644
index 0000000..e7fbb24
--- /dev/null
+++ b/utilities/gtk/media.c
@@ -0,0 +1,141 @@
+/*
+ tc_client-gtk, a graphical user interface for tc_client
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdlib.h>
+#include <string.h>
+#include <gtk/gtk.h>
+#include <libavcodec/avcodec.h>
+#include "media.h"
+struct camera* cams=0;
+unsigned int camcount=0;
+
+#ifdef HAVE_SOUND
+// Experimental mixer, not sure if it really works
+void camera_playsnd(int audiopipe, struct camera* cam, short* samples, unsigned int samplecount)
+{
+ if(cam->samples)
+ {
+// int sources=1;
+ unsigned int i;
+ for(i=0; i<camcount; ++i)
+ {
+ if(!cams[i].samples){continue;}
+ if(cam==&cams[i]){continue;}
+ unsigned j;
+ for(j=0; j<cam->samplecount && j<cams[i].samplecount; ++j)
+ {
+ cam->samples[j]+=cams[i].samples[j];
+ }
+ free(cams[i].samples);
+ cams[i].samples=0;
+// ++sources;
+ }
+ write(audiopipe, cam->samples, cam->samplecount*sizeof(short));
+ free(cam->samples);
+// printf("Mixed sound from %i sources (cam: %p)\n", sources, cam);
+ }
+ cam->samples=malloc(samplecount*sizeof(short));
+ memcpy(cam->samples, samples, samplecount*sizeof(short));
+ cam->samplecount=samplecount;
+}
+#endif
+
+void camera_remove(const char* id)
+{
+ unsigned int i;
+ for(i=0; i<camcount; ++i)
+ {
+ if(!strcmp(cams[i].id, id))
+ {
+ gtk_widget_destroy(cams[i].box);
+ av_frame_free(&cams[i].frame);
+ avcodec_free_context(&cams[i].vctx);
+#ifdef HAVE_SOUND
+ avcodec_free_context(&cams[i].actx);
+#endif
+ free(cams[i].id);
+ free(cams[i].nick);
+ --camcount;
+ memmove(&cams[i], &cams[i+1], (camcount-i)*sizeof(struct camera));
+ break;
+ }
+ }
+}
+
+void camera_removebynick(const char* nick)
+{
+ unsigned int i;
+ for(i=0; i<camcount; ++i)
+ {
+ if(!strcmp(cams[i].nick, nick))
+ {
+ gtk_widget_destroy(cams[i].box);
+ av_frame_free(&cams[i].frame);
+ avcodec_free_context(&cams[i].vctx);
+#ifdef HAVE_SOUND
+ avcodec_free_context(&cams[i].actx);
+#endif
+ free(cams[i].id);
+ free(cams[i].nick);
+ --camcount;
+ memmove(&cams[i], &cams[i+1], (camcount-i)*sizeof(struct camera));
+ break;
+ }
+ }
+}
+
+struct camera* camera_find(const char* id)
+{
+ unsigned int i;
+ for(i=0; i<camcount; ++i)
+ {
+ if(!strcmp(cams[i].id, id)){return &cams[i];}
+ }
+ return 0;
+}
+
+struct camera* camera_findbynick(const char* nick)
+{
+ unsigned int i;
+ for(i=0; i<camcount; ++i)
+ {
+ if(!strcmp(cams[i].nick, nick)){return &cams[i];}
+ }
+ return 0;
+}
+
+struct camera* camera_new(void)
+{
+ ++camcount;
+ cams=realloc(cams, sizeof(struct camera)*camcount);
+ return &cams[camcount-1];
+}
+
+void camera_cleanup(void)
+{
+ unsigned int i;
+ for(i=0; i<camcount; ++i)
+ {
+ av_frame_free(&cams[i].frame);
+ avcodec_free_context(&cams[i].vctx);
+#ifdef HAVE_SOUND
+ avcodec_free_context(&cams[i].actx);
+#endif
+ free(cams[i].id);
+ free(cams[i].nick);
+ }
+ free(cams);
+}
diff --git a/utilities/gtk/media.h b/utilities/gtk/media.h
new file mode 100644
index 0000000..159e998
--- /dev/null
+++ b/utilities/gtk/media.h
@@ -0,0 +1,42 @@
+/*
+ tc_client-gtk, a graphical user interface for tc_client
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+struct camera
+{
+ AVFrame* frame;
+ AVFrame* dstframe;
+ GtkWidget* cam;
+ AVCodecContext* vctx;
+ AVCodecContext* actx;
+ short* samples;
+ unsigned int samplecount;
+ char* id;
+ char* nick;
+ GtkWidget* box; // holds label and cam
+ GtkWidget* label;
+};
+extern struct camera* cams;
+extern unsigned int camcount;
+
+#ifdef HAVE_SOUND
+extern void camera_playsnd(int audiopipe, struct camera* cam, short* samples, unsigned int samplecount);
+#endif
+extern void camera_remove(const char* nick);
+extern void camera_removebynick(const char* nick);
+extern struct camera* camera_find(const char* id);
+extern struct camera* camera_findbynick(const char* nick);
+extern struct camera* camera_new(void);
+extern void camera_cleanup(void);
diff --git a/utilities/gtk/userlist.c b/utilities/gtk/userlist.c
new file mode 100644
index 0000000..89d1df5
--- /dev/null
+++ b/utilities/gtk/userlist.c
@@ -0,0 +1,84 @@
+/*
+ camviewer, a sample application to view tinychat cam streams
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+#include <stdlib.h>
+#include <string.h>
+#include <gtk/gtk.h>
+#include "userlist.h"
+
+struct user* userlist=0;
+unsigned int usercount=0;
+GtkWidget* userlistwidget=0;
+
+struct user* finduser(const char* nick)
+{
+ unsigned int i;
+ for(i=0; i<usercount; ++i)
+ {
+ if(!strcmp(userlist[i].nick, nick)){return &userlist[i];}
+ }
+ return 0;
+}
+
+struct user* adduser(const char* nick)
+{
+ struct user* user=finduser(nick);
+ if(user){return user;} // User already existed (this might happen when running /names)
+ ++usercount;
+ userlist=realloc(userlist, sizeof(struct user)*usercount);
+ userlist[usercount-1].nick=strdup(nick);
+ userlist[usercount-1].label=gtk_label_new(nick); // TODO: some kind of menubutton for actions?
+#if GTK_MAJOR_VERSION>=3
+ gtk_widget_set_halign(userlist[usercount-1].label, GTK_ALIGN_START);
+#endif
+ userlist[usercount-1].ismod=0;
+ gtk_box_pack_start(GTK_BOX(userlistwidget), userlist[usercount-1].label, 0, 0, 0);
+ gtk_widget_show(userlist[usercount-1].label);
+ return &userlist[usercount-1];
+}
+
+void renameuser(const char* old, const char* newnick)
+{
+ struct user* user=finduser(old);
+ if(!user){return;}
+ free(user->nick);
+ user->nick=strdup(newnick);
+ if(user->ismod)
+ {
+ char newlabel[strlen(newnick)+2];
+ newlabel[0]='@';
+ strcpy(&newlabel[1], newnick);
+ gtk_label_set_text(GTK_LABEL(user->label), newlabel);
+ }else{
+ gtk_label_set_text(GTK_LABEL(user->label), newnick);
+ }
+}
+
+void removeuser(const char* nick)
+{
+ unsigned int i;
+ for(i=0; i<usercount; ++i)
+ {
+ if(!strcmp(userlist[i].nick, nick))
+ {
+ free(userlist[i].nick);
+ gtk_widget_destroy(userlist[i].label);
+ --usercount;
+ memmove(&userlist[i], &userlist[i+1], (usercount-i)*sizeof(struct user));
+ return;
+ }
+ }
+}
diff --git a/utilities/gtk/userlist.h b/utilities/gtk/userlist.h
new file mode 100644
index 0000000..98b886e
--- /dev/null
+++ b/utilities/gtk/userlist.h
@@ -0,0 +1,32 @@
+/*
+ camviewer, a sample application to view tinychat cam streams
+ Copyright (C) 2015 alicia@ion.nu
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, version 3 of the License.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+struct user
+{
+ char* nick;
+ GtkWidget* label;
+// unsigned int id; // hm, tc_client doesn't share IDs other than in guestnicks, this might be useful for a ban-after-they-left situation
+ char ismod;
+};
+
+extern struct user* userlist;
+extern unsigned int usercount;
+extern GtkWidget* userlistwidget;
+
+extern struct user* finduser(const char* nick);
+extern struct user* adduser(const char* nick);
+extern void renameuser(const char* old, const char* newnick);
+extern void removeuser(const char* nick);
diff --git a/utilities/modbot/commands.html b/utilities/modbot/commands.html
index c9477b2..b7b8325 100644
--- a/utilities/modbot/commands.html
+++ b/utilities/modbot/commands.html
@@ -16,6 +16,7 @@
<tr><td>!wrongrequest</td><td>undo the last request you made</td></tr>
<tr><td>!requestedby</td><td>see who requested the current video</td></tr>
<tr><td>!modstats</td><td>get a percentage of how often there are mods in the channel (aside from modbot)</td></tr>
+ <tr><td>!syncvid</td><td>synchronize video position (or see a video that was started while having youtube videos disabled in the flash client)</td></tr>
<tr><th colspan="2">Mod commands:</th></tr>
<tr><td>!playnext</td><td>play the next video in queue without approving it (to see if it's ok)</td></tr>
<tr><td>!approve</td><td>mark the currently playing video as good, or if none is playing the next in queue</td></tr>
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index 86ed03b..bb193b8 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -499,6 +499,16 @@ int main(int argc, char** argv)
}
say(pm, "The channel has had mods %u%% of the time for the past %u %s\n", hasmods, session, timeformat);
}
+ else if(!strcmp(msg, "!syncvid"))
+ {
+ if(playing)
+ {
+ space[0]=0;
+ say(0, "/priv %s /mbs youTube %s %u\n", nick, playing, (time(0)-started)*1000);
+ }else{
+ say(pm, "Nothing is playing\n");
+ }
+ }
else if(list_contains(&mods, nick)) // Mods-only commands
{
if(!strcmp(msg, "!playnext"))