$ git clone http://tcclient.ion.nu/tc_client.git
commit 1b8fe86748867951a561bceb152a261275e95839
Author: Alicia <...>
Date: Thu Jul 2 12:10:25 2015 +0200
tc_client-gtk: added a camera selection (and preview) window when starting to broadcast.
diff --git a/ChangeLog b/ChangeLog
index 95f88d9..b560875 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -3,6 +3,7 @@ Fixed memory alignment in rtmp/amf code (for CPU architectures that are picky ab
Print version info when called with -v/--version (contributed by Jade)
Flush stdout after printing unknown RTMP types, and return upon <1 reads in b_read.
tc_client-gtk: fixed windows compatibility (w32_runcmd and strndup)
+tc_client-gtk: added a camera selection (and preview) window when starting to broadcast.
tc_client-gtk and camviewer: added some compatibility macros for older libav versions.
tc_client-gtk and camviewer: added an abstraction library (libcamera) for cam access.
modbot: added an option (--disable-lists) to disable playlist requests, instead only the first video linked to will be added to the queue.
diff --git a/gtkgui.glade b/gtkgui.glade
index b0f0785..ae7f240 100644
--- a/gtkgui.glade
+++ b/gtkgui.glade
@@ -1,7 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.18.3 -->
+<!-- Generated with glade 3.19.0 -->
<interface>
<requires lib="gtk+" version="3.0"/>
+ <object class="GtkWindow" id="camselection">
+ <property name="can_focus">False</property>
+ <property name="modal">True</property>
+ <child>
+ <object class="GtkBox" id="box8">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkComboBox" id="camselect_combo">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="active">0</property>
+ <property name="has_entry">True</property>
+ <property name="entry_text_column">0</property>
+ <property name="id_column">0</property>
+ <child internal-child="entry">
+ <object class="GtkEntry" id="combobox-entry">
+ <property name="can_focus">False</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="GtkImage" id="camselect_preview">
+ <property name="width_request">320</property>
+ <property name="height_request">240</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stock">gtk-missing-image</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box9">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButton" id="camselect_cancel">
+ <property name="label">gtk-cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="camselect_ok">
+ <property name="label">gtk-ok</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
<object class="GtkWindow" id="channelconfig">
<property name="can_focus">False</property>
<property name="title" translatable="yes">tc_client</property>
diff --git a/utilities/gtk/camviewer.c b/utilities/gtk/camviewer.c
index 29d049d..b536ae4 100644
--- a/utilities/gtk/camviewer.c
+++ b/utilities/gtk/camviewer.c
@@ -524,11 +524,20 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
g_io_channel_read_chars(iochannel, (gchar*)pkt.data+pos, size-pos, &r, 0);
pos+=r;
}
+ unsigned int scalewidth=data->scalewidth;
+ unsigned int scaleheight=data->scaleheight;
if(!strcmp(&buf[7], "out"))
{
- dprintf(tc_client_in[1], "/video %i\n", size+1);
- write(tc_client_in[1], &frameinfo, 1);
- write(tc_client_in[1], pkt.data, size);
+ if(cam)
+ {
+ dprintf(tc_client_in[1], "/video %i\n", size+1);
+ write(tc_client_in[1], &frameinfo, 1);
+ write(tc_client_in[1], pkt.data, size);
+ }else{
+ cam=&campreview;
+ scalewidth=320;
+ scaleheight=240;
+ }
}
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;}
@@ -538,15 +547,15 @@ gboolean handledata(GIOChannel* iochannel, GIOCondition condition, gpointer data
if(!gotframe){return 1;}
// Scale and convert to RGB24 format
- unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, data->scalewidth, data->scaleheight);
+ unsigned int bufsize=avpicture_get_size(PIX_FMT_RGB24, scalewidth, 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);
+ cam->dstframe->linesize[0]=scalewidth*3;
+ struct SwsContext* swsctx=sws_getContext(cam->frame->width, cam->frame->height, cam->frame->format, scalewidth, 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);
+ GdkPixbuf* gdkframe=gdk_pixbuf_new_from_data(cam->dstframe->data[0], GDK_COLORSPACE_RGB, 0, 8, scalewidth, 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);
@@ -577,8 +586,7 @@ void audiothread(int fd)
#endif
#ifdef HAVE_CAM
-pid_t camproc=0;
-unsigned int cameventsource;
+unsigned int cameventsource=0;
void togglecam(GtkCheckMenuItem* item, struct viddata* data)
{
if(!gtk_check_menu_item_get_active(item))
@@ -586,81 +594,20 @@ void togglecam(GtkCheckMenuItem* item, struct viddata* data)
g_source_remove(cameventsource);
kill(camproc, SIGINT);
camproc=0;
- dprintf(tc_client_in[1], "/camdown\n");
- dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
- return;
- }
- unsigned int count;
- char** cams=cam_list(&count);
- if(!count){printf("No camera found\n"); 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
- CAM* cam=cam_open(cams[0]);
- AVCodecContext* ctx=avcodec_alloc_context3(data->vencoder);
- ctx->width=320;
- ctx->height=240;
- cam_resolution(cam, (unsigned int*)&ctx->width, (unsigned int*)&ctx->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=ctx->width;
- frame->height=ctx->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)
+ if(camera_find("out"))
{
- usleep(delay);
- if(delay>100000){delay-=50000;}
- cam_getframe(cam, frame->data[0]);
- 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)
- // Send the packet to our main thread so we can see ourselves (the main thread also sends it to the server)
- 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);
+ dprintf(tc_client_in[1], "/camdown\n");
+ dprintf(tc_client[1], "VideoEnd: out\n"); // Close our local display
}
- sws_freeContext(swsctx);
- _exit(0);
+ return;
}
- close(campipe[1]);
- GIOChannel* channel=g_io_channel_unix_new(campipe[0]);
- g_io_channel_set_encoding(channel, 0, 0);
- cameventsource=g_io_add_watch(channel, G_IO_IN, handledata, data);
+ GtkWidget* window=GTK_WIDGET(gtk_builder_get_object(gui, "camselection"));
+ gtk_widget_show_all(window);
+
+ // Start a cam thread for the selected cam
+ GtkComboBox* combo=GTK_COMBO_BOX(gtk_builder_get_object(gui, "camselect_combo"));
+ GIOChannel* channel=camthread(gtk_combo_box_get_active_id(combo), data->vencoder, 100000);
+ cameventsource=g_io_add_watch(channel, G_IO_IN, handledata, 0);
}
#endif
@@ -961,10 +908,40 @@ int main(int argc, char** argv)
}
gtk_builder_connect_signals(gui, 0);
+ unsigned int i;
#ifdef HAVE_CAM
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);
+ // Set up cam selection and preview
+ campreview.cam=GTK_WIDGET(gtk_builder_get_object(gui, "camselect_preview"));
+ campreview.frame=av_frame_alloc();
+ campreview.dstframe=av_frame_alloc();
+ campreview.vctx=avcodec_alloc_context3(data->vdecoder);
+ avcodec_open2(campreview.vctx, data->vdecoder, 0);
+ GtkComboBox* combo=GTK_COMBO_BOX(gtk_builder_get_object(gui, "camselect_combo"));
+ g_signal_connect(combo, "changed", G_CALLBACK(camselect_change), data->vencoder);
+ // Signals for cancelling
+ item=GTK_WIDGET(gtk_builder_get_object(gui, "camselection"));
+ g_signal_connect(item, "delete-event", G_CALLBACK(camselect_cancel), 0);
+ item=GTK_WIDGET(gtk_builder_get_object(gui, "camselect_cancel"));
+ g_signal_connect(item, "clicked", G_CALLBACK(camselect_cancel), 0);
+ // Signals for switching from preview to streaming
+ item=GTK_WIDGET(gtk_builder_get_object(gui, "camselect_ok"));
+ g_signal_connect(item, "clicked", G_CALLBACK(camselect_accept), data->vencoder);
+ // Populate list of cams
+ unsigned int count;
+ char** cams=cam_list(&count);
+ GtkListStore* list=gtk_list_store_new(1, G_TYPE_STRING);
+ for(i=0; i<count; ++i)
+ {
+ gtk_list_store_insert_with_values(list, 0, -1, 0, cams[i], -1);
+ free(cams[i]);
+ }
+ free(cams);
+ gtk_combo_box_set_model(combo, GTK_TREE_MODEL(list));
+ g_object_unref(list);
+ gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), gtk_cell_renderer_text_new(), 1);
#else
GtkWidget* item=GTK_WIDGET(gtk_builder_get_object(gui, "menuitem_broadcast"));
gtk_widget_destroy(item);
@@ -1029,7 +1006,6 @@ int main(int argc, char** argv)
gtk_widget_destroy(GTK_WIDGET(gtk_builder_get_object(gui, "channel_placeholder")));
}
char buf[256];
- int i;
for(i=0; i<channelcount; ++i)
{
GtkWidget* box=gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
diff --git a/utilities/gtk/media.c b/utilities/gtk/media.c
index 52d61a0..6a4ddb8 100644
--- a/utilities/gtk/media.c
+++ b/utilities/gtk/media.c
@@ -18,10 +18,23 @@
#include <string.h>
#include <gtk/gtk.h>
#include <libavcodec/avcodec.h>
+#ifdef HAVE_CAM
+ #include <sys/prctl.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
+ #include "../libcamera/camera.h"
+#endif
#include "../compat.h"
+#include "gui.h"
#include "media.h"
+struct camera campreview;
struct camera* cams=0;
unsigned int camcount=0;
+pid_t camproc=0;
#if defined(HAVE_AVRESAMPLE) || defined(HAVE_SWRESAMPLE)
// Experimental mixer, not sure if it really works
@@ -140,3 +153,111 @@ void camera_cleanup(void)
}
free(cams);
}
+
+#ifdef HAVE_CAM
+extern unsigned int cameventsource;
+GIOChannel* camthread(const char* name, AVCodec* vencoder, unsigned int delay)
+{
+ if(camproc)
+ {
+ g_source_remove(cameventsource);
+ kill(camproc, SIGINT);
+ usleep(200000); // Give the previous process some time to shut down
+ }
+ // Set up a second pipe to be handled by handledata() to avoid overlap with tc_client's output
+ int campipe[2];
+ pipe(campipe);
+ camproc=fork();
+ if(!camproc)
+ {
+ close(campipe[0]);
+ prctl(PR_SET_PDEATHSIG, SIGHUP);
+ // Set up camera
+ CAM* cam=cam_open(name);
+ AVCodecContext* ctx=avcodec_alloc_context3(vencoder);
+ ctx->width=320;
+ ctx->height=240;
+ cam_resolution(cam, (unsigned int*)&ctx->width, (unsigned int*)&ctx->height);
+ ctx->pix_fmt=PIX_FMT_YUV420P;
+ ctx->time_base.num=1;
+ ctx->time_base.den=10;
+ avcodec_open2(ctx, vencoder, 0);
+ AVFrame* frame=av_frame_alloc();
+ frame->format=PIX_FMT_RGB24;
+ frame->width=ctx->width;
+ frame->height=ctx->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;}
+ cam_getframe(cam, frame->data[0]);
+ 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)
+ // Send the packet to our main thread so we can see ourselves (the main thread also sends it to the server)
+ 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);
+ return channel;
+}
+
+extern GIOFunc handledata;
+void camselect_change(GtkComboBox* combo, AVCodec* vencoder)
+{
+ if(!camproc){return;} // If there isn't a camthread already, it will be started elsewhere
+ GIOChannel* channel=camthread(gtk_combo_box_get_active_id(combo), vencoder, 100000);
+ cameventsource=g_io_add_watch(channel, G_IO_IN, (void*)&handledata, 0);
+}
+
+gboolean camselect_cancel(GtkWidget* widget, void* x1, void* x2)
+{
+ GtkWidget* window=GTK_WIDGET(gtk_builder_get_object(gui, "camselection"));
+ gtk_widget_hide(window);
+ // Note: unchecking the menu item kills the cam thread for us
+ GtkCheckMenuItem* item=GTK_CHECK_MENU_ITEM(gtk_builder_get_object(gui, "menuitem_broadcast_camera"));
+ gtk_check_menu_item_set_active(item, 0);
+ return 1;
+}
+
+extern int tc_client_in[];
+void camselect_accept(GtkWidget* widget, AVCodec* vencoder)
+{
+ GtkWidget* window=GTK_WIDGET(gtk_builder_get_object(gui, "camselection"));
+ gtk_widget_hide(window);
+ // Restart the cam thread with a high initial delay to workaround the bug in the flash client
+ GtkComboBox* combo=GTK_COMBO_BOX(gtk_builder_get_object(gui, "camselect_combo"));
+ GIOChannel* channel=camthread(gtk_combo_box_get_active_id(combo), vencoder, 500000);
+ cameventsource=g_io_add_watch(channel, G_IO_IN, (void*)&handledata, 0);
+ dprintf(tc_client_in[1], "/camup\n");
+}
+#endif
diff --git a/utilities/gtk/media.h b/utilities/gtk/media.h
index bd283e7..1824e80 100644
--- a/utilities/gtk/media.h
+++ b/utilities/gtk/media.h
@@ -28,8 +28,10 @@ struct camera
GtkWidget* box; // holds label and cam
GtkWidget* label;
};
+extern struct camera campreview;
extern struct camera* cams;
extern unsigned int camcount;
+extern pid_t camproc;
#if defined(HAVE_AVRESAMPLE) || defined(HAVE_SWRESAMPLE)
extern void camera_playsnd(int audiopipe, struct camera* cam, short* samples, unsigned int samplecount);
@@ -40,3 +42,9 @@ 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);
+#ifdef HAVE_CAM
+extern GIOChannel* camthread(const char* name, AVCodec* vencoder, unsigned int delay);
+extern void camselect_change(GtkComboBox* combo, AVCodec* vencoder);
+extern gboolean camselect_cancel(GtkWidget* widget, void* x1, void* x2);
+extern void camselect_accept(GtkWidget* widget, AVCodec* vencoder);
+#endif