Convert README to restructured text for the benefit of Github
[opo.git] / opo.c
blob0a36703a4bf77d9dbefeb25c5afd735ea5202050
1 /* This file is part of Opo, a small Video Whale
3 Copyright (C) 2011 Douglas Bagnall
5 Opo is free software: you can redistribute it and/or modify it under
6 the terms of the GNU General Public License as published by the Free
7 Software Foundation, either version 3 of the License, or (at your
8 option) any later version.
10 Opo is distributed in the hope that it will be useful, but WITHOUT
11 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
13 License for more details.
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #include <gst/gst.h>
20 #include <gtk/gtk.h>
21 #include <gst/interfaces/xoverlay.h>
22 #include <gdk/gdk.h>
23 #include <gdk/gdkx.h>
24 #include "opo.h"
26 static void gstreamer_stop(GstElement *pipeline);
27 static GstPipeline *gstreamer_start(GMainLoop *loop, window_t *windows);
30 static GstCaps *
31 make_good_caps(void){
32 GstCaps *caps;
33 if (option_autosize && option_content){
34 //automatically find size
35 caps = gst_caps_new_simple("video/x-raw-yuv",
36 NULL);
37 gst_caps_merge(caps, gst_caps_new_simple("video/x-raw-rgb",
38 NULL));
40 else {
41 caps = gst_caps_new_simple("video/x-raw-yuv",
42 "width", G_TYPE_INT, option_screens * option_width,
43 "height", G_TYPE_INT, option_height,
44 NULL);
45 gst_caps_merge(caps, gst_caps_new_simple("video/x-raw-rgb",
46 "width", G_TYPE_INT, option_screens * option_width,
47 "height", G_TYPE_INT, option_height,
48 NULL));
50 return caps;
53 static inline char *
54 attempt_filename_to_uri(char *filename){
55 char *uri;
56 if (g_str_has_prefix(filename, "/")){
57 uri = g_strconcat("file://",
58 option_content,
59 NULL);
61 else {
62 char *cwd = g_get_current_dir();
63 uri = g_strconcat("file://",
64 cwd,
65 "/",
66 option_content,
67 NULL);
68 g_free(cwd);
70 return uri;
73 static void hide_mouse(GtkWidget *widget){
74 GdkWindow *w = GDK_WINDOW(widget->window);
75 GdkDisplay *display = gdk_display_get_default();
76 GdkCursor *cursor = gdk_cursor_new_for_display(display, GDK_BLANK_CURSOR);
77 gdk_window_set_cursor(w, cursor);
78 gdk_cursor_unref (cursor);
82 static void
83 post_tee_pipeline(GstBin *bin, GstElement *tee, GstElement *sink,
84 int crop_left, int crop_right){
85 GstElement *queue = gst_element_factory_make("queue", NULL);
86 GstElement *crop = gst_element_factory_make("videocrop", NULL);
88 g_object_set(G_OBJECT(crop),
89 "top", 0,
90 "bottom", 0,
91 "left", crop_left,
92 "right", crop_right,
93 NULL);
95 gst_bin_add_many(bin,
96 queue,
97 crop,
98 sink,
99 NULL);
101 gst_element_link_many(tee,
102 queue,
103 crop,
104 sink,
105 NULL);
111 static GstBusSyncReply
112 sync_bus_call(GstBus *bus, GstMessage *msg, gpointer data)
114 //g_print("SYNC call with %s\n", GST_MESSAGE_TYPE_NAME(msg));
115 // ignore anything but 'prepare-xwindow-id' element messages
116 if ((GST_MESSAGE_TYPE(msg) != GST_MESSAGE_ELEMENT) ||
117 (! gst_structure_has_name(msg->structure, "prepare-xwindow-id"))){
118 return GST_BUS_PASS;
120 window_t *windows = (window_t *)data;
121 g_print("Got prepare-xwindow-id msg. \n");
122 //connect this one up with the right window.
123 GstElement *sink = GST_ELEMENT(GST_MESSAGE_SRC(msg));
124 int done = 0;
126 g_print("found sink %p\n", sink);
127 for (int i = 0; i < option_screens; i++){
128 const window_t *w = windows + i;
129 if (w->sink == sink){
130 gst_x_overlay_set_xwindow_id(GST_X_OVERLAY(sink), w->xid);
131 g_print("connected sink %d to window %lu\n", i, w->xid);
132 hide_mouse(w->widget);
133 done = 1;
134 break;
138 if (! done){
139 g_print("couldn't find a window for this sink!\n");
142 gst_message_unref(msg);
143 return GST_BUS_DROP;
147 static void
148 set_up_loop(GstElement *source, int flags){
149 g_print("loooooping\n");
150 gint64 end, loop_end;
151 GstFormat nanosec_format = GST_FORMAT_TIME;
152 gst_element_query_duration(source,
153 &nanosec_format,
154 &end
156 if (option_loop_end){
157 loop_end = (gint64)option_loop_end * BILLION;
159 else{
160 loop_end = end - 25 * NS_PER_FRAME;
162 g_print("stream ends at %f seconds\nlooping at %f seconds\n",
163 end / (double)BILLION, loop_end / (double)BILLION);
164 if (!gst_element_seek(source, 1.0, GST_FORMAT_TIME,
165 flags,
166 GST_SEEK_TYPE_SET, NS_PER_FRAME,
167 GST_SEEK_TYPE_SET, loop_end
168 )) {
169 g_print ("Seek failed!\n");
173 static gboolean looping = FALSE;
175 static void
176 manage_state_change(GstMessage *msg, GstElement *pipeline){
177 GstState old_state, new_state, pending_state;
178 gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
179 if (msg->src == GST_OBJECT(pipeline)){
180 //g_print("pipeline state change\n");
181 if (new_state == GST_STATE_PAUSED && ! looping){
182 /* the pipeline is ready for the initial loop to be set*/
183 set_up_loop(pipeline, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_SEGMENT);
184 looping = TRUE;
188 g_print ("Element %s changed state from %s to %s, pending %s.\n",
189 GST_OBJECT_NAME (msg->src),
190 gst_element_state_get_name(old_state),
191 gst_element_state_get_name(new_state),
192 gst_element_state_get_name(pending_state));
196 static gboolean
197 async_bus_call(GstBus *bus, GstMessage *msg, GstElement *pipeline)
199 //g_print("async call with %s\n", GST_MESSAGE_TYPE_NAME(msg));
200 switch(GST_MESSAGE_TYPE(msg)){
201 case GST_MESSAGE_SEGMENT_DONE:
202 set_up_loop(pipeline, GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_SEGMENT);
203 break;
204 case GST_MESSAGE_STATE_CHANGED:
205 manage_state_change(msg, pipeline);
206 break;
207 default:
208 break;
210 return TRUE;
214 static void
215 about_to_finish_cb(GstElement *pipeline, char *uri)
217 g_print("would be starting again with %s\n", uri);
218 g_object_set(G_OBJECT(pipeline),
219 "uri", uri,
220 NULL);
223 static void
224 toggle_fullscreen(GtkWidget *widget){
225 GdkWindowState state = gdk_window_get_state(GDK_WINDOW(widget->window));
226 if (state == GDK_WINDOW_STATE_FULLSCREEN){
227 gtk_window_unfullscreen(GTK_WINDOW(widget));
229 else{
230 gtk_window_fullscreen(GTK_WINDOW(widget));
234 static gboolean
235 key_press_event_cb(GtkWidget *widget, GdkEventKey *event, gpointer data)
237 g_print("got key %c\n", event->keyval);
238 switch (event->keyval){
239 case 'f':
240 toggle_fullscreen(widget);
241 break;
242 case 'q':
243 g_signal_emit_by_name(widget, "destroy");
244 break;
245 default:
246 break;
248 return TRUE;
251 static void
252 destroy_cb(GtkWidget *widget, gpointer data)
254 GMainLoop *loop = (GMainLoop*) data;
255 g_print("Window destroyed\n");
256 g_main_loop_quit(loop);
257 gtk_widget_destroy(widget);
260 static void
261 video_widget_realize_cb(GtkWidget *widget, gpointer data)
263 window_t *w = (window_t *)data;
264 w->xid = GDK_WINDOW_XID(GDK_WINDOW(widget->window));
265 g_print("realised window %d with XID %lu\n", w->id, w->xid);
266 hide_mouse(widget);
269 /*create the X window, but not the xvimagesink*/
270 static void
271 set_up_window(GMainLoop *loop, window_t *w, int screen_no){
272 GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
273 w->widget = window;
274 w->id = screen_no;
275 g_signal_connect(w->widget, "realize", G_CALLBACK(video_widget_realize_cb), w);
277 static const GdkColor black = {0, 0, 0, 0};
278 //XXX need to resize once video size is known
279 gtk_window_set_default_size(GTK_WINDOW(window), option_width, option_height);
281 if (option_fullscreen){
282 gtk_window_fullscreen(GTK_WINDOW(window));
284 int xscreen_no;
285 GdkScreen * xscreen;
286 if (option_x_screens <= 1){
287 xscreen = gdk_screen_get_default();
288 /*Xscreen number might not be 0, but 0 is right assumption for
289 calculations below.*/
290 xscreen_no = 0;
292 else{
293 xscreen_no = screen_no * option_x_screens / option_screens;
294 /* record the display/screen number, so that xvimagesink can be top it later */
295 g_snprintf(w->display, sizeof(w->display), ":0.%d", xscreen_no);
296 xscreen = gdk_display_get_screen(gdk_display_get_default(), xscreen_no);
297 gtk_window_set_screen(GTK_WINDOW(window), xscreen);
299 int x, y;
300 int windows_per_xscreen = option_screens / option_x_screens;
301 int window_no = screen_no % windows_per_xscreen;
302 int monitors = gdk_screen_get_n_monitors(xscreen);
303 if (option_force_multiscreen){
304 /*Ask gtk to find the appropriate monitor (assuming each Xscreen has the
305 same number of monitors). */
306 if (window_no >= monitors){
307 g_print("asking for monitor %d which does not exist! (monitors %d)\n",
308 window_no, monitors);
310 GdkRectangle monitor_shape;
311 gdk_screen_get_monitor_geometry(xscreen, window_no, &monitor_shape);
312 x = monitor_shape.x + 1;
313 y = monitor_shape.y + 1;
315 else {
316 /*simple placement heuristic, places windows evenly across display.
317 This should work with equally sized monitors/projectors, and allows
318 testing on a single monitor.
320 int full_screen_width = gdk_screen_get_width(xscreen);
321 x = window_no * (full_screen_width / windows_per_xscreen) + 1;
322 y = 1;
325 gtk_window_move(GTK_WINDOW(window), x, y);
326 g_print("putting window %d on screen :0.%d at %d,%d\n",
327 screen_no, xscreen_no, x, y);
329 // attach key press signal to key press callback
330 gtk_widget_set_events(window, GDK_KEY_PRESS_MASK);
331 g_signal_connect(G_OBJECT(window), "key-press-event", G_CALLBACK(key_press_event_cb), NULL);
332 g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy_cb), loop);
334 gtk_widget_modify_bg(window, GTK_STATE_NORMAL, &black);
335 gtk_widget_show_all(window);
336 hide_mouse(window);
341 static GstPipeline *
342 uri_pre_tee_pipeline(void){
343 GstPipeline *pipeline;
344 char *uri;
345 if( g_str_has_prefix(option_content, "file://") ||
346 g_str_has_prefix(option_content, "http://") /* || others */
348 uri = option_content;
350 else{
351 uri = attempt_filename_to_uri(option_content);
353 g_print("uri is '%s'\n", uri);
354 pipeline = GST_PIPELINE(gst_element_factory_make("playbin2", NULL));
355 g_object_set(G_OBJECT(pipeline),
356 "uri", uri,
357 "volume", 0.5,
358 NULL);
359 g_signal_connect(pipeline, "about-to-finish",
360 G_CALLBACK(about_to_finish_cb), uri);
361 return pipeline;
364 static GstPipeline *
365 test_pre_tee_pipeline(void){
366 GstPipeline *pipeline;
367 pipeline = GST_PIPELINE(gst_pipeline_new("test_pipeline"));
368 char * src_name = (option_fake) ? "videotestsrc" : "v4l2src";
369 GstElement *src = gst_element_factory_make(src_name, "videosource");
370 if (option_fake == 2){//set some properties for an interesting picture
371 g_object_set(G_OBJECT(src),
372 "pattern", 14, //"zone-plate"
373 "kt2", 0,
374 "kx2", 3,
375 "ky2", 3,
376 "kt", 3,
377 "kxy", 2,
378 NULL);
380 gst_bin_add(GST_BIN(pipeline), src);
381 return pipeline;
384 static GstBin *
385 tee_bin(GMainLoop *loop, window_t *windows){
386 GstBin *bin = GST_BIN(gst_bin_new("teebin"));
387 GstElement *tee = gst_element_factory_make("tee", NULL);
388 gst_bin_add(bin, tee);
389 GstPad *teesink = gst_element_get_pad(tee, "sink");
390 GstPad *ghost = gst_ghost_pad_new("sink", teesink);
391 gst_element_add_pad(GST_ELEMENT(bin), ghost);
393 /* construct the various arms
394 crop _left/_right are amount to cut, not coordinate of cut
396 int input_width = option_screens * option_width;
397 int crop_left = 0;
398 int crop_right = input_width - option_width;
400 int i;
401 for (i = 0; i < option_screens; i++){
402 window_t *w = windows + i;
403 set_up_window(loop, w, i);
404 w->sink = gst_element_factory_make("xvimagesink", NULL);
405 g_object_set(G_OBJECT(w->sink),
406 "display", w->display,
407 NULL);
408 post_tee_pipeline(bin, tee, w->sink, crop_left, crop_right);
409 crop_left += option_width;
410 crop_right -= option_width;
412 return bin;
417 static GstPipeline *
418 gstreamer_start(GMainLoop *loop, window_t *windows)
420 GstPipeline *pipeline;
421 if (option_content){
422 pipeline = uri_pre_tee_pipeline();
424 else{
425 pipeline = test_pre_tee_pipeline();
427 GstBin *teebin = tee_bin(loop, windows);
429 if (option_content) {
430 g_object_set(G_OBJECT(pipeline),
431 "video-sink", GST_ELEMENT(teebin),
432 NULL);
434 else {
435 gst_bin_add(GST_BIN(pipeline), GST_ELEMENT(teebin));
436 GstElement *videosrc = gst_bin_get_by_name(GST_BIN(pipeline), "videosource");
437 GstCaps *caps = make_good_caps();
438 gst_element_link_filtered(videosrc, GST_ELEMENT(teebin), caps);
439 gst_object_unref(caps);
442 GstBus *bus = gst_pipeline_get_bus(pipeline);
443 gst_bus_set_sync_handler(bus, (GstBusSyncHandler)sync_bus_call, windows);
444 gst_bus_add_watch(bus, (GstBusFunc)async_bus_call, pipeline);
446 gst_object_unref(bus);
448 gst_element_set_state(GST_ELEMENT(pipeline), GST_STATE_PLAYING);
450 return pipeline;
454 static void
455 gstreamer_stop(GstElement *pipeline)
457 gst_element_set_state(pipeline, GST_STATE_NULL);
458 gst_object_unref(pipeline);
461 gint main (gint argc, gchar *argv[])
463 //initialise threads before any gtk stuff (because not using gtk_init)
464 g_type_init();
465 g_thread_init(NULL);
466 /*this is more complicated than plain gtk_init/gst_init, so that options from
467 all over can be gathered and presented together.
469 GOptionGroup *gst_opts = gst_init_get_option_group();
470 GOptionGroup *gtk_opts = gtk_get_option_group(TRUE);
471 GOptionContext *ctx = g_option_context_new("...!");
472 g_option_context_add_main_entries(ctx, entries, NULL);
473 g_option_context_add_group(ctx, gst_opts);
474 g_option_context_add_group(ctx, gtk_opts);
475 GError *error = NULL;
476 if (!g_option_context_parse(ctx, &argc, &argv, &error)){
477 g_print ("Error initializing: %s\n", GST_STR_NULL(error->message));
478 exit (1);
480 g_option_context_free(ctx);
481 /*sanitise options*/
482 if (option_x_screens > MAX_X_SCREENS)
483 option_x_screens = MAX_X_SCREENS;
484 if (option_x_screens < MIN_X_SCREENS)
485 option_x_screens = MIN_X_SCREENS;
486 if (option_x_screens > MAX_X_SCREENS)
487 option_screens = MAX_SCREENS;
488 if (option_screens < MIN_SCREENS)
489 option_screens = MIN_SCREENS;
490 if (option_width > MAX_PIXELS)
491 option_width = MAX_PIXELS;
492 if (option_height > MAX_PIXELS)
493 option_height = MAX_PIXELS;
494 /*setting width, height <= 0 makes sizing automatic (default) */
495 if (option_width <= 0){
496 option_width = DEFAULT_WIDTH;
497 option_autosize = 1;
499 if (option_height <= 0){
500 option_height = DEFAULT_HEIGHT;
501 option_autosize = 1;
503 window_t *windows = g_malloc0(sizeof(window_t) * option_screens);
505 GMainLoop *loop = g_main_loop_new(NULL, FALSE);
507 GstPipeline *pipeline = gstreamer_start(loop, windows);
509 g_main_loop_run(loop);
511 gstreamer_stop(GST_ELEMENT(pipeline));
512 return 0;