$ git clone http://tcclient.ion.nu/tc_client.git
commit 5f2908b53e792242ca6006611bb91b4f45d92c22
Author: Alicia <...>
Date: Tue Apr 7 06:49:01 2015 +0200
Version 0.26
diff --git a/ChangeLog b/ChangeLog
index 317ada6..9dfa01b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,9 @@
+0.26:
+modbot: fixed a segfault when trying to skip more videos than what's in the queue.
+modbot: fixed a segfault when receiving a manual /mbc command but no video is playing.
+cursedchat: added PM buffers/windows which can be switched between with "/pm <name>", just "/pm" to return to the channel buffer.
+cursedchat: added /help output specific to cursedchat, in addition to tc_client's output.
+camviewer: added an experimental audio mixer.
0.25:
Handle short reads in the RTMP headers too.
Added the option -c/--color <number> to pick color instead of getting a random color at startup (contributed by Pamela)
diff --git a/Makefile b/Makefile
index 0f7e575..da7c8da 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=0.25
+VERSION=0.26
CFLAGS=-g3 -Wall $(shell curl-config --cflags)
LIBS=-g3 $(shell curl-config --libs)
ifneq ($(wildcard config.mk),)
@@ -8,7 +8,7 @@ OBJ=client.o amfparser.o rtmp.o numlist.o amfwriter.o idlist.o colors.o endian.o
IRCHACK_OBJ=utilities/irchack/irchack.o
MODBOT_OBJ=utilities/modbot/modbot.o utilities/modbot/list.o utilities/modbot/queue.o
CAMVIEWER_OBJ=utilities/camviewer/camviewer.o
-CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o
+CURSEDCHAT_OBJ=utilities/cursedchat/cursedchat.o utilities/cursedchat/buffer.o
UTILS=irchack modbot
ifdef GTK_LIBS
ifdef AVCODEC_LIBS
@@ -53,4 +53,4 @@ clean:
rm -f $(OBJ) $(IRCHACK_OBJ) $(MODBOT_OBJ) $(CAMVIEWER_OBJ) $(CURSEDCHAT_OBJ) tc_client irchack modbot camviewer cursedchat
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/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c 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 utilities/irchack/irchack.c utilities/modbot/modbot.c utilities/modbot/list.c utilities/modbot/list.h utilities/modbot/queue.c utilities/modbot/queue.h utilities/camviewer/camviewer.c utilities/cursedchat/cursedchat.c utilities/cursedchat/buffer.c utilities/cursedchat/buffer.h configure
diff --git a/utilities/camviewer/camviewer.c b/utilities/camviewer/camviewer.c
index 1f9330c..43a8094 100644
--- a/utilities/camviewer/camviewer.c
+++ b/utilities/camviewer/camviewer.c
@@ -45,6 +45,8 @@ struct camera
GtkWidget* cam;
AVCodecContext* vctx;
AVCodecContext* actx;
+ short* samples;
+ unsigned int samplecount;
char* id;
char* nick;
GtkWidget* box; // holds label and cam
@@ -59,12 +61,44 @@ struct viddata
AVCodec* adecoder;
#ifdef HAVE_SOUND
int audiopipe;
+ SwrContext* swrctx;
#endif
};
int tc_client[2];
int tc_client_in[2];
+#ifdef HAVE_SOUND
+// Experimental mixer, not sure if it really works
+void camera_playsnd(struct viddata* data, struct camera* cam, short* samples, unsigned int samplecount)
+{
+ if(cam->samples)
+ {
+// int sources=1;
+ unsigned int i;
+ for(i=0; i<data->camcount; ++i)
+ {
+ if(!data->cams[i].samples){continue;}
+ if(cam==&data->cams[i]){continue;}
+ unsigned j;
+ for(j=0; j<cam->samplecount && j<data->cams[i].samplecount; ++j)
+ {
+ cam->samples[j]+=data->cams[i].samples[j];
+ }
+ free(data->cams[i].samples);
+ data->cams[i].samples=0;
+// ++sources;
+ }
+ write(data->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
+
char buf[1024];
gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
{
@@ -120,6 +154,7 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
#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);
@@ -181,11 +216,8 @@ gboolean handledata(GIOChannel* channel, GIOCondition condition, gpointer datap)
int gotframe;
avcodec_decode_audio4(cam->actx, cam->frame, &gotframe, &pkt);
if(!gotframe){return 1;}
- SwrContext* swrctx=swr_alloc_set_opts(0, 1, AV_SAMPLE_FMT_S16, 22050, 1, cam->frame->format, 11025, 0, 0); // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
- swr_init(swrctx);
- int outlen=swr_convert(swrctx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
- swr_free(&swrctx);
- write(data->audiopipe, cam->frame->data[0], outlen);
+ int outlen=swr_convert(data->swrctx, cam->frame->data, cam->frame->nb_samples, (const uint8_t**)cam->frame->data, cam->frame->nb_samples);
+ camera_playsnd(data, cam, (short*)cam->frame->data[0], outlen);
#endif
return 1;
}
@@ -253,7 +285,6 @@ void audiothread(int fd)
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);
-// TODO: mix sounds, somehow..
char buf[2048];
size_t len;
while((len=read(fd, buf, 2048))>0)
@@ -272,6 +303,8 @@ int main(int argc, char** argv)
data.adecoder=avcodec_find_decoder(AV_CODEC_ID_NELLYMOSER);
#ifdef HAVE_SOUND
+ data.swrctx=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); // TODO: any way to get the sample rate from the frame/decoder? cam->frame->sample_rate seems to be 0
+ swr_init(data.swrctx);
int audiopipe[2];
pipe(audiopipe);
data.audiopipe=audiopipe[1];
@@ -319,6 +352,7 @@ int main(int argc, char** argv)
avcodec_free_context(&data.cams[i].vctx);
#ifdef HAVE_SOUND
avcodec_free_context(&data.cams[i].actx);
+ swr_free(&data.swrctx);
#endif
free(data.cams[i].id);
free(data.cams[i].nick);
diff --git a/utilities/cursedchat/buffer.c b/utilities/cursedchat/buffer.c
new file mode 100644
index 0000000..af06a16
--- /dev/null
+++ b/utilities/cursedchat/buffer.c
@@ -0,0 +1,62 @@
+/*
+ cursedchat, a simple curses 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 <string.h>
+#include <stdlib.h>
+#include <curses.h>
+#include "buffer.h"
+
+struct buffer* buffers=0;
+unsigned int buffercount=0;
+unsigned int currentbuf=0;
+
+unsigned int createbuffer(const char* name)
+{
+ ++buffercount;
+ buffers=realloc(buffers, buffercount*sizeof(struct buffer));
+ buffers[buffercount-1].pad=newpad(2048, COLS);
+ scrollok(buffers[buffercount-1].pad, 1);
+ buffers[buffercount-1].name=(name?strdup(name):0);
+ buffers[buffercount-1].scroll=-1;
+ buffers[buffercount-1].seen=1;
+ return buffercount-1;
+}
+
+unsigned int findbuffer(const char* name)
+{
+ unsigned int i;
+ for(i=1; i<buffercount; ++i)
+ {
+ if(!strcmp(buffers[i].name, name)){return i;}
+ }
+ return 0;
+}
+
+void renamebufferunique(unsigned int id)
+{
+ unsigned int i=0;
+ while(i<buffercount)
+ {
+ int len=strlen(buffers[id].name);
+ buffers[id].name=realloc(buffers[id].name, len+2);
+ buffers[id].name[len]='_';
+ buffers[id].name[len+1]=0;
+ for(i=1; i<buffercount; ++i)
+ {
+ if(i!=id && !strcmp(buffers[i].name, buffers[id].name)){break;}
+ }
+ }
+}
diff --git a/utilities/cursedchat/buffer.h b/utilities/cursedchat/buffer.h
new file mode 100644
index 0000000..79ec1fc
--- /dev/null
+++ b/utilities/cursedchat/buffer.h
@@ -0,0 +1,31 @@
+/*
+ cursedchat, a simple curses 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 buffer
+{
+ WINDOW* pad;
+ char* name;
+ int scroll;
+ char seen;
+};
+
+extern struct buffer* buffers;
+extern unsigned int buffercount;
+extern unsigned int currentbuf;
+
+extern unsigned int createbuffer(const char* name);
+extern unsigned int findbuffer(const char* name);
+extern void renamebufferunique(unsigned int id);
diff --git a/utilities/cursedchat/cursedchat.c b/utilities/cursedchat/cursedchat.c
index fe00302..42545f5 100644
--- a/utilities/cursedchat/cursedchat.c
+++ b/utilities/cursedchat/cursedchat.c
@@ -22,12 +22,13 @@
#include <locale.h>
#include <curses.h>
#include <readline/readline.h>
+#include "buffer.h"
#define HALFSCREEN (LINES>4?(LINES-3)/2:1)
+
WINDOW* topic;
-WINDOW* chat;
+char* channeltopic;
WINDOW* input;
-int chatscroll=-1;
int to_app;
// Translate ANSI escape codes to curses commands and write the text to a window
@@ -75,18 +76,114 @@ void waddansi(WINDOW* w, char* str)
void drawchat(void)
{
- prefresh(chat, (chatscroll>-1?chatscroll:getcury(chat)-LINES+4), 0, 1, 0, LINES-3, COLS);
+ WINDOW* w=buffers[currentbuf].pad;
+ int scroll=buffers[currentbuf].scroll;
+ prefresh(w, (scroll>-1?scroll:getcury(w)-LINES+4), 0, 1, 0, LINES-3, COLS);
+}
+
+void drawtopic(void)
+{
+ werase(topic);
+ unsigned int i;
+ for(i=1; i<buffercount && buffers[i].seen; ++i);
+ if(i<buffercount)
+ {
+ waddstr(topic, "Unread PMs from: ");
+ char first=1;
+ for(i=1; i<buffercount; ++i)
+ {
+ if(!buffers[i].seen)
+ {
+ if(first){first=0;}else{waddstr(topic, ", ");}
+ waddstr(topic, buffers[i].name);
+ }
+ }
+ }
+ else if(currentbuf)
+ {
+ waddstr(topic, "To return to public chat type: /pm");
+ }else{
+ waddstr(topic, channeltopic);
+ }
+ wrefresh(topic);
}
void gotline(char* line)
{
if(!line){close(to_app); return;} // TODO: handle EOF on stdin better?
-// TODO: handle commands (/pm, /help addition)
+ if(!strcmp(line, "/pm"))
+ {
+ currentbuf=0;
+ drawchat();
+ drawtopic();
+ return;
+ }
+ else if(!strncmp(line, "/pm ", 4))
+ {
+ currentbuf=findbuffer(&line[4]);
+ if(!currentbuf){currentbuf=createbuffer(&line[4]);}
+ buffers[currentbuf].seen=1;
+ drawchat();
+ drawtopic();
+ return;
+ }
+ else if(!strncmp(line, "/buffer ", 8))
+ {
+ unsigned int num=atoi(&line[8]);
+ if(num<0 || num>=buffercount)
+ {
+ wprintw(buffers[currentbuf].pad, "\nInvalid buffer number: %u", num);
+ }else{
+ currentbuf=num;
+ buffers[currentbuf].seen=1;
+ drawtopic();
+ }
+ drawchat();
+ return;
+ }
+ else if(!strcmp(line, "/bufferlist"))
+ {
+ unsigned int i;
+ for(i=0; i<buffercount; ++i)
+ {
+ wprintw(buffers[currentbuf].pad, "\n% 3i: %s", i, i?buffers[i].name:"");
+ }
+ drawchat();
+ return;
+ }
+ else if(!strncmp(line, "/msg ", 5))
+ {
+ char* name=&line[5];
+ char* msg=strchr(name, ' ');
+ if(!msg){return;}
+ msg[0]=0;
+ currentbuf=findbuffer(name);
+ if(!currentbuf){currentbuf=createbuffer(name);}
+ buffers[currentbuf].seen=1;
+ drawtopic();
+ memmove(line, &msg[1], strlen(&msg[1])+1);
+ }
+ else if(!strcmp(line, "/help"))
+ {
+ waddstr(buffers[0].pad, "\nFor cursedchat:\n"
+ "/pm <name> = switch to the PM buffer for <name>\n"
+ "/pm = return to the channel/public chat's buffer\n"
+ "/buffer <num> = switch to buffer by number\n"
+ "/bufferlist = list open buffers, their numbers and associated names\n"
+ "\nFor tc_client (through cursedchat):");
+ write(to_app, line, strlen(line));
+ write(to_app, "\n", 1);
+ return;
+ }
+ if(currentbuf) // We're in a PM window, make the message a PM
+ {
+ dprintf(to_app, "/msg %s ", buffers[currentbuf].name);
+ }
write(to_app, line, strlen(line));
write(to_app, "\n", 1);
// TODO: grab user's nick for this
- wprintw(chat, "\n%s: %s", "You", line);
+ wprintw(buffers[currentbuf].pad, "\n%s: %s", "You", line);
drawchat();
}
@@ -127,22 +224,23 @@ int escinput(int a, int byte)
if(!strcmp(buf, "[5")) // Page up
{
read(0, buf, 1);
- if(chatscroll<0){chatscroll=getcury(chat)-LINES+4;}
- chatscroll-=HALFSCREEN; // (LINES-3)/2;
- if(chatscroll<0){chatscroll=0;}
+ struct buffer* b=&buffers[currentbuf];
+ if(b->scroll<0){b->scroll=getcury(b->pad)-LINES+4;}
+ b->scroll-=HALFSCREEN;
+ if(b->scroll<0){b->scroll=0;}
drawchat();
return 0;
}
if(!strcmp(buf, "[6")) // Page down
{
read(0, buf, 1);
- if(chatscroll<0){return 0;} // Already at the bottom
- chatscroll+=HALFSCREEN; // (LINES-3)/2;
- if(chatscroll>getcury(chat)-LINES+3){chatscroll=-1;}
+ struct buffer* b=&buffers[currentbuf];
+ if(b->scroll<0){return 0;} // Already at the bottom
+ b->scroll+=HALFSCREEN;
+ if(b->scroll>getcury(b->pad)-LINES+3){b->scroll=-1;}
drawchat();
return 0;
}
-// wprintw(chat, "\nbuf: %s", buf);
return 0;
}
@@ -168,15 +266,21 @@ void resizechat(int sig)
ioctl(0, TIOCGWINSZ, &size);
if(size.ws_row<3){return;} // Too small, would result in negative numbers breaking the chat window
resize_term(size.ws_row, size.ws_col);
+ clear();
+ refresh();
wresize(topic, 1, COLS);
- wresize(chat, chat->_maxy+1, COLS);
+ unsigned int i;
+ for(i=0; i<buffercount; ++i)
+ {
+ wresize(buffers[i].pad, buffers[i].pad->_maxy+1, COLS);
+ }
wresize(input, 2, COLS);
mvwin(input, LINES-2, 0);
- redrawwin(chat);
+ redrawwin(buffers[currentbuf].pad);
redrawwin(topic);
redrawwin(input);
drawchat();
- wrefresh(topic);
+ drawtopic();
drawinput();
}
@@ -203,8 +307,7 @@ int main(int argc, char** argv)
init_pair(7, COLOR_CYAN, -1);
wbkgd(topic, COLOR_PAIR(1)|' ');
- chat=newpad(2048, COLS);
- scrollok(chat, 1);
+ createbuffer(0);
input=newwin(2, COLS, LINES-2, 0);
scrollok(input, 1);
rl_initialize();
@@ -248,14 +351,61 @@ int main(int argc, char** argv)
}
if(len==-1){break;} // Bad read
buf[len]=0;
+ unsigned int buffer=0;
if(!strncmp(buf, "Room topic: ", 12))
{
- werase(topic);
- waddstr(topic, &buf[12]);
- wrefresh(topic);
+ free(channeltopic);
+ channeltopic=strdup(&buf[12]);
+ drawtopic();
+ }
+ else 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];
+ char* msg=strchr(nick, ' ');
+ if(msg[-1]==':')
+ {
+ nick=strchr(nick, 'm')+1;
+ char* nickend=&msg[-1];
+ msg=&msg[1];
+ if(!strncmp(msg, "/msg ", 5)) // message is a PM
+ {
+ char* pm=strchr(&msg[5], ' ');
+ if(!pm){waddstr(buffers[0].pad, "\npm is null!"); continue;}
+ pm=&pm[1];
+ nickend[0]=0;
+ buffer=findbuffer(nick);
+ if(!buffer){buffer=createbuffer(nick);}
+ nickend[0]=':';
+ memmove(msg, pm, strlen(pm)+1);
+ if(buffer!=currentbuf)
+ {
+ buffers[buffer].seen=0;
+ drawtopic();
+ }
+ }
+ }
+ else if(!strncmp(msg, " changed nickname to ", 21))
+ {
+ msg[0]=0;
+ unsigned int i;
+ // Prevent duplicate names for buffers, and all the issues that would bring
+ if((i=findbuffer(&msg[21])))
+ {
+ renamebufferunique(i);
+ }
+ for(i=1; i<buffercount; ++i)
+ {
+ if(!strcmp(buffers[i].name, nick))
+ {
+ free(buffers[i].name);
+ buffers[i].name=strdup(&msg[21]);
+ }
+ }
+ msg[0]=' ';
+ }
}
- waddstr(chat, "\n");
- waddansi(chat, buf);
+ waddstr(buffers[buffer].pad, "\n");
+ waddansi(buffers[buffer].pad, buf);
drawchat();
wrefresh(input);
continue;
diff --git a/utilities/modbot/modbot.c b/utilities/modbot/modbot.c
index 1041544..e9d1111 100644
--- a/utilities/modbot/modbot.c
+++ b/utilities/modbot/modbot.c
@@ -547,7 +547,7 @@ int main(int argc, char** argv)
unsigned int num=((msg[5]&&msg[6])?strtoul(&msg[6], 0, 0):1);
if(num<1){say(pm, "The given value evaluates to 0, please specify the number of videos you would like to skip (or if you do not specify it will default to 1)\n"); continue;}
if(playing){free(playing); playing=0; --num; say(0, "/mbc youTube\n");}
- while(num>0)
+ while(num>0&&queue.itemcount>0)
{
queue_del(&queue, queue.items[0].video);
--num;
@@ -571,7 +571,7 @@ int main(int argc, char** argv)
alarm(getduration(playing)-pos);
started=time(0)-pos;
}
- else if(!strcmp(msg, "/mbc youTube")) // Video cancelled
+ else if(!strcmp(msg, "/mbc youTube") && playing) // Video cancelled
{
list_del(&goodvids, playing); // manual /mbc is often used when !badvid should be used, so at least remove it from the list of approved videos
list_save(&goodvids, "goodvids.txt");