├── cooldance.ogg ├── .gitignore ├── big-buck-bunny_trailer.webm ├── gstreamer-tutorial-lca2k18.pdf ├── network-clocks ├── Makefile ├── README.md ├── netclock-server.c └── playback-sync.c ├── README.md ├── Makefile ├── command-lines.txt ├── test-rtsp-uri.c └── playback.c /cooldance.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaytan/gst-tutorial-lca2018/HEAD/cooldance.ogg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | playback 2 | test-rtsp-uri 3 | network-clocks/netclock-server 4 | network-clocks/playback-sync 5 | 6 | -------------------------------------------------------------------------------- /big-buck-bunny_trailer.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaytan/gst-tutorial-lca2018/HEAD/big-buck-bunny_trailer.webm -------------------------------------------------------------------------------- /gstreamer-tutorial-lca2k18.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaytan/gst-tutorial-lca2018/HEAD/gstreamer-tutorial-lca2k18.pdf -------------------------------------------------------------------------------- /network-clocks/Makefile: -------------------------------------------------------------------------------- 1 | 2 | TARGET=playback-sync 3 | TARGET2=netclock-server 4 | 5 | CFLAGS=-Wall -O0 -g `pkg-config --cflags gstreamer-1.0 gstreamer-net-1.0 gstreamer-pbutils-1.0` 6 | LDFLAGS=`pkg-config --libs gstreamer-1.0 gstreamer-net-1.0 gstreamer-pbutils-1.0` 7 | 8 | all: $(TARGET) $(TARGET2) 9 | 10 | $(TARGET): $(TARGET).c 11 | gcc -o $@ $< $(CFLAGS) $(LDFLAGS) 12 | 13 | $(TARGET2): $(TARGET2).c 14 | gcc -o $@ $< $(CFLAGS) $(LDFLAGS) 15 | 16 | clean: 17 | rm -f $(TARGET) $(TARGET2) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains some examples for the GStreamer Tutorial 2 | I am presenting at Linux.conf.au 2018 in Sydney. 3 | 4 | To build the code, install some pre-requisites 5 | 6 | Fedora: 7 | dnf install gstreamer-tools gstreamer1-devel gstreamer1-plugins-\\* gstreamer1-libav gstreamer1-rtsp-server gstreamer1-rtsp-server-devel 8 | 9 | Debian: 10 | apt-get install gstreamer1.0-tools libgstreamer1.0-dev gstreamer1.0-plugins-\\* gstreamer1.0-libav libgstrtspserver-1.0-0 libgstrtspserver-1.0-dev 11 | -------------------------------------------------------------------------------- /network-clocks/README.md: -------------------------------------------------------------------------------- 1 | To use this example, run ./netclock-server 2 | 3 | Every second, it will print a base time. Copy that base time, and 4 | use it in several instances of the ./playback-sync app to play 5 | a file or URI synchronised across multiple instances like this: 6 | 7 | ./playback-sync -c netclock-host-IP -p netclock-host-port -b base-time 8 | 9 | providing the values for the netclock-server IP address, port and selected 10 | base time. Use the same base time on each player to get synchronised playback 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=$(shell pkg-config --cflags gstreamer-1.0 gstreamer-plugins-base-1.0 gstreamer-rtsp-server-1.0) 2 | LDFLAGS=$(shell pkg-config --libs gstreamer-1.0 gstreamer-plugins-base-1.0 gstreamer-rtsp-server-1.0) 3 | 4 | all: playback test-rtsp-uri network-clocks 5 | 6 | playback: playback.c 7 | $(CC) -o playback playback.c $(CFLAGS) $(LDFLAGS) 8 | 9 | test-rtsp-uri: test-rtsp-uri.c 10 | $(CC) -o test-rtsp-uri test-rtsp-uri.c $(CFLAGS) $(LDFLAGS) 11 | 12 | network-clocks: 13 | make -C network-clocks 14 | 15 | .PHONY: network-clocks 16 | -------------------------------------------------------------------------------- /command-lines.txt: -------------------------------------------------------------------------------- 1 | gst-inspect-1.0 2 | gst-inspect-1.0 identity 3 | 4 | gst-launch-1.0 audiotestsrc ! audioconvert ! autoaudiosink 5 | gst-launch-1.0 videotestsrc ! videoconvert ! autovideosink 6 | 7 | gst-typefind-1.0 big-buck-bunny_trailer.webm 8 | gst-launch-1.0 -vm audiotestsrc num-buffers=2 ! fakesink silent=false 9 | 10 | gst-launch-1.0 \ 11 | filesrc location=cooldance.ogg ! oggdemux name=d \ 12 | d. ! queue ! vorbisdec ! audioconvert ! audioresample ! autoaudiosink \ 13 | d. ! queue ! theoradec ! videoconvert ! videoscale ! autovideosink 14 | 15 | gst-launch-1.0 playbin uri=https://gstreamer.freedesktop.org/media/incoming/Pixar%20-%20Geri\'s%20Game.avi 16 | gst-launch-1.0 playbin uri=file:///$PWD/big-buck-bunny_trailer.webm video-sink="glupload ! gleffects_sobel ! glimagesink" 17 | 18 | gst-launch-1.0 -e v4l2src ! videoconvert ! x264enc ! mp4mux ! filesink location=test.mp4 19 | 20 | gst-launch-1.0 videotestsrc ! avenc_mpeg2video ! mpegvideoparse ! rtpmpvpay ! udpsink 21 | gst-launch-1.0 udpsrc caps="application/x-rtp,clock-rate=90000,payload=32" ! rtpjitterbuffer ! rtpmpvdepay ! decodebin ! autovideosink 22 | 23 | -------------------------------------------------------------------------------- /network-clocks/netclock-server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | gboolean print_time (gpointer user_data) 6 | { 7 | GstClock *clock = GST_CLOCK (user_data); 8 | g_print("Base time %" G_GUINT64_FORMAT "\r", 9 | gst_clock_get_time (clock)); 10 | 11 | return G_SOURCE_CONTINUE; 12 | } 13 | 14 | gint 15 | main (gint argc, gchar * argv[]) 16 | { 17 | GMainLoop *loop; 18 | GstClock *clock; 19 | GstNetTimeProvider *net_clock; 20 | int clock_port = 0; 21 | 22 | gst_init (&argc, &argv); 23 | 24 | if (argc > 1) 25 | clock_port = atoi (argv[1]); 26 | 27 | loop = g_main_loop_new (NULL, FALSE); 28 | 29 | clock = gst_system_clock_obtain (); 30 | net_clock = gst_net_time_provider_new (clock, NULL, clock_port); 31 | 32 | g_object_get (net_clock, "port", &clock_port, NULL); 33 | 34 | g_print ("Published network clock on port %u\n", clock_port); 35 | 36 | g_timeout_add_seconds (1, print_time, clock); 37 | g_main_loop_run (loop); 38 | 39 | /* cleanup */ 40 | gst_object_unref (clock); 41 | g_main_loop_unref (loop); 42 | 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /test-rtsp-uri.c: -------------------------------------------------------------------------------- 1 | /* GStreamer 2 | * Copyright (C) 2008 Wim Taymans 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Library General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Library General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Library General Public 15 | * License along with this library; if not, write to the 16 | * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, 17 | * Boston, MA 02110-1301, USA. 18 | */ 19 | 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | #define DEFAULT_RTSP_PORT "8554" 26 | 27 | static char *port = (char *) DEFAULT_RTSP_PORT; 28 | 29 | static GOptionEntry entries[] = { 30 | {"port", 'p', 0, G_OPTION_ARG_STRING, &port, 31 | "Port to listen on (default: " DEFAULT_RTSP_PORT ")", "PORT"}, 32 | {NULL} 33 | }; 34 | 35 | 36 | static gboolean 37 | timeout (GstRTSPServer * server) 38 | { 39 | GstRTSPSessionPool *pool; 40 | 41 | pool = gst_rtsp_server_get_session_pool (server); 42 | gst_rtsp_session_pool_cleanup (pool); 43 | g_object_unref (pool); 44 | 45 | return TRUE; 46 | } 47 | 48 | #if 0 49 | static gboolean 50 | remove_map (GstRTSPServer * server) 51 | { 52 | GstRTSPMountPoints *mounts; 53 | 54 | g_print ("removing /test mount point\n"); 55 | mounts = gst_rtsp_server_get_mount_points (server); 56 | gst_rtsp_mount_points_remove_factory (mounts, "/test"); 57 | g_object_unref (mounts); 58 | 59 | return FALSE; 60 | } 61 | #endif 62 | 63 | int 64 | main (int argc, gchar * argv[]) 65 | { 66 | GMainLoop *loop; 67 | GstRTSPServer *server; 68 | GstRTSPMountPoints *mounts; 69 | GOptionContext *optctx; 70 | GError *error = NULL; 71 | gint i; 72 | 73 | optctx = g_option_context_new (" - Test RTSP Server, URI"); 74 | g_option_context_add_main_entries (optctx, entries, NULL); 75 | g_option_context_add_group (optctx, gst_init_get_option_group ()); 76 | if (!g_option_context_parse (optctx, &argc, &argv, &error)) { 77 | g_printerr ("Error parsing options: %s\n", error->message); 78 | g_option_context_free (optctx); 79 | g_clear_error (&error); 80 | return -1; 81 | } 82 | g_option_context_free (optctx); 83 | 84 | if (argc < 2) { 85 | g_printerr ("Please pass an URI or file as argument!\n"); 86 | return -1; 87 | } 88 | 89 | loop = g_main_loop_new (NULL, FALSE); 90 | 91 | /* create a server instance */ 92 | server = gst_rtsp_server_new (); 93 | g_object_set (server, "service", port, NULL); 94 | 95 | /* get the mount points for this server, every server has a default object 96 | * that be used to map uri mount points to media factories */ 97 | mounts = gst_rtsp_server_get_mount_points (server); 98 | 99 | for (i = 1; i < argc; i++) { 100 | GstRTSPMediaFactoryURI *factory; 101 | gchar *uri; 102 | 103 | /* make a URI media factory for a test stream. */ 104 | factory = gst_rtsp_media_factory_uri_new (); 105 | 106 | /* when using GStreamer as a client, one can use the gst payloader, 107 | * which is more efficient when there is no payloader for the 108 | * compressed format */ 109 | /* g_object_set (factory, "use-gstpay", TRUE, NULL); */ 110 | 111 | /* check if URI is valid, otherwise convert filename to URI if it's 112 | * a file */ 113 | if (gst_uri_is_valid (argv[i])) { 114 | uri = g_strdup (argv[i]); 115 | } else if (g_file_test (argv[i], G_FILE_TEST_EXISTS)) { 116 | uri = gst_filename_to_uri (argv[i], NULL); 117 | } else { 118 | g_printerr ("Unrecognised command line argument '%s'.\n" 119 | "Please pass an URI or file as argument!\n", argv[i]); 120 | return -1; 121 | } 122 | 123 | gst_rtsp_media_factory_uri_set_uri (factory, uri); 124 | g_free (uri); 125 | 126 | /* if you want multiple clients to see the same video, set the 127 | * shared property to TRUE */ 128 | gst_rtsp_media_factory_set_shared ( GST_RTSP_MEDIA_FACTORY (factory), TRUE); 129 | 130 | gst_rtsp_media_factory_set_retransmission_time ( 131 | GST_RTSP_MEDIA_FACTORY (factory), 400 * GST_MSECOND); 132 | 133 | /* attach the test factory to the /test url */ 134 | if (i == 1) { 135 | gst_rtsp_mount_points_add_factory (mounts, "/test", 136 | GST_RTSP_MEDIA_FACTORY (factory)); 137 | g_print ("Attached stream at /test\n"); 138 | } else { 139 | gchar *mp = g_strdup_printf ("/test%d", i-1); 140 | gst_rtsp_mount_points_add_factory (mounts, mp, 141 | GST_RTSP_MEDIA_FACTORY (factory)); 142 | g_print ("Attached stream at %s\n", mp); 143 | g_free (mp); 144 | } 145 | } 146 | 147 | /* don't need the ref to the mapper anymore */ 148 | g_object_unref (mounts); 149 | 150 | /* attach the server to the default maincontext */ 151 | if (gst_rtsp_server_attach (server, NULL) == 0) 152 | goto failed; 153 | 154 | /* do session cleanup every 2 seconds */ 155 | g_timeout_add_seconds (2, (GSourceFunc) timeout, server); 156 | 157 | #if 0 158 | /* remove the mount point after 10 seconds, new clients won't be able to use 159 | * the /test url anymore */ 160 | g_timeout_add_seconds (10, (GSourceFunc) remove_map, server); 161 | #endif 162 | 163 | /* start serving */ 164 | g_print ("stream(s) ready on rtsp://127.0.0.1:%s/test and /test[1 to n]\n", port); 165 | g_main_loop_run (loop); 166 | 167 | return 0; 168 | 169 | /* ERRORS */ 170 | failed: 171 | { 172 | g_print ("failed to attach the server\n"); 173 | return -1; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /playback.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | typedef struct 9 | { 10 | GMainLoop *loop; 11 | GstElement *playbin; 12 | guint bus_watch; 13 | guint io_watch_id; 14 | } GlobalData; 15 | 16 | static gboolean handle_bus_msg (GstBus * bus, GstMessage * msg, 17 | GlobalData * data); 18 | static gboolean io_callback (GIOChannel * io, GIOCondition condition, 19 | GlobalData * data); 20 | 21 | static GstElement * 22 | create_element (const gchar * type, const gchar * name) 23 | { 24 | GstElement *e; 25 | 26 | e = gst_element_factory_make (type, name); 27 | if (!e) { 28 | g_print ("Failed to create element %s\n", type); 29 | exit (1); 30 | } 31 | 32 | return e; 33 | } 34 | 35 | static gchar * 36 | canonicalise_uri (const gchar * in) 37 | { 38 | if (gst_uri_is_valid (in)) 39 | return g_strdup (in); 40 | 41 | return gst_filename_to_uri (in, NULL); 42 | } 43 | 44 | int 45 | main (int argc, char *argv[]) 46 | { 47 | GlobalData data; 48 | GIOChannel *io = NULL; 49 | GstBus *bus; 50 | gchar *uri; 51 | 52 | /* Initialize GStreamer */ 53 | gst_init (&argc, &argv); 54 | 55 | if (argc < 2) { 56 | g_print ("Usage: %s \n", argv[0]); 57 | g_print ("When running, pressing 'q' quits the application\n" 58 | "'f' seeks backwards 10 seconds\n" 59 | "'g' seeks forwards 10 seconds\n" 60 | "For this trivial example, you need to press enter after each command\n"); 61 | return 1; 62 | } 63 | 64 | /* Build the pipeline */ 65 | data.playbin = create_element ("playbin", NULL); 66 | 67 | /* Make sure the input filename or uri is a uri */ 68 | uri = canonicalise_uri (argv[1]); 69 | 70 | /* Set the uri property on playbin */ 71 | g_object_set (data.playbin, "uri", uri, NULL); 72 | 73 | /* Connect to the bus to receive callbacks */ 74 | bus = gst_element_get_bus (data.playbin); 75 | 76 | data.bus_watch = gst_bus_add_watch (bus, (GstBusFunc) handle_bus_msg, &data); 77 | 78 | gst_object_unref (bus); 79 | 80 | /* Set up the main loop */ 81 | data.loop = g_main_loop_new (NULL, FALSE); 82 | 83 | /* Start playing */ 84 | gst_element_set_state (data.playbin, GST_STATE_PLAYING); 85 | g_print ("Now playing %s\n", uri); 86 | 87 | g_free (uri); 88 | 89 | /* Listen to stdin input */ 90 | io = g_io_channel_unix_new (fileno (stdin)); 91 | data.io_watch_id = g_io_add_watch (io, G_IO_IN, (GIOFunc) (io_callback), 92 | &data); 93 | g_io_channel_unref (io); 94 | 95 | /* Run the mainloop until it is exited by the message handler */ 96 | g_main_loop_run (data.loop); 97 | 98 | /* Clean everything up before exiting */ 99 | g_source_remove (data.bus_watch); 100 | g_source_remove (data.io_watch_id); 101 | gst_element_set_state (data.playbin, GST_STATE_NULL); 102 | gst_object_unref (data.playbin); 103 | g_main_loop_unref (data.loop); 104 | 105 | return 0; 106 | } 107 | 108 | static gboolean 109 | handle_bus_msg (GstBus * bus, GstMessage * msg, GlobalData * data) 110 | { 111 | /* Wait until error or EOS */ 112 | switch (GST_MESSAGE_TYPE (msg)) { 113 | case GST_MESSAGE_EOS:{ 114 | g_print ("Finished playback. Exiting.\n"); 115 | g_main_loop_quit (data->loop); 116 | break; 117 | } 118 | case GST_MESSAGE_ERROR:{ 119 | GError *err = NULL; 120 | gchar *dbg_info = NULL; 121 | 122 | gst_message_parse_error (msg, &err, &dbg_info); 123 | g_printerr ("ERROR from element %s: %s\n", 124 | GST_OBJECT_NAME (msg->src), err->message); 125 | g_printerr ("Debugging info: %s\n", (dbg_info) ? dbg_info : "none"); 126 | g_print ("Exiting.\n"); 127 | g_error_free (err); 128 | g_free (dbg_info); 129 | 130 | g_main_loop_quit (data->loop); 131 | break; 132 | } 133 | case GST_MESSAGE_TAG:{ 134 | GstTagList *tags; 135 | gchar *value; 136 | 137 | gst_message_parse_tag (msg, &tags); 138 | 139 | g_print ("Found tags\n"); 140 | if (gst_tag_list_get_string (tags, GST_TAG_ARTIST, &value)) { 141 | g_print ("Artist: %s\n", value); 142 | g_free (value); 143 | } 144 | 145 | if (gst_tag_list_get_string (tags, GST_TAG_TITLE, &value)) { 146 | g_print ("Title: %s\n", value); 147 | g_free (value); 148 | } 149 | 150 | if (gst_tag_list_get_string (tags, GST_TAG_ALBUM, &value)) { 151 | g_print ("Album: %s\n", value); 152 | g_free (value); 153 | } 154 | 155 | gst_tag_list_free (tags); 156 | break; 157 | } 158 | case GST_MESSAGE_ASYNC_DONE:{ 159 | GstPad *video_pad; 160 | GstCaps *caps; 161 | GstStructure *s; 162 | 163 | g_signal_emit_by_name (data->playbin, "get-video-pad", 0, &video_pad); 164 | if (video_pad) { 165 | gint width, height; 166 | gint par_n, par_d; 167 | 168 | caps = gst_pad_get_current_caps (video_pad); 169 | s = gst_caps_get_structure (caps, 0); 170 | 171 | gst_structure_get_int (s, "width", &width); 172 | gst_structure_get_int (s, "height", &height); 173 | par_n = par_d = 1; 174 | gst_structure_get_fraction (s, "pixel-aspect-ratio", &par_n, &par_d); 175 | 176 | width = width * par_n / par_d; 177 | g_print ("Video size: %dx%d\n", width, height); 178 | gst_caps_unref (caps); 179 | gst_object_unref (video_pad); 180 | } 181 | 182 | break; 183 | } 184 | default: 185 | /* Ignore messages we don't know about */ 186 | break; 187 | } 188 | 189 | return TRUE; 190 | } 191 | 192 | static void 193 | seek (GlobalData * data, gboolean forward) 194 | { 195 | gint64 position; 196 | 197 | if (!gst_element_query_position (data->playbin, GST_FORMAT_TIME, &position)) 198 | return; 199 | 200 | if (forward) 201 | position += 10 * GST_SECOND; 202 | else if (position > 10 * GST_SECOND) 203 | position -= 10 * GST_SECOND; 204 | else 205 | position = 0; 206 | 207 | gst_element_seek_simple (data->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, 208 | position); 209 | } 210 | 211 | static gboolean 212 | io_callback (GIOChannel * io, GIOCondition condition, GlobalData * data) 213 | { 214 | gchar in; 215 | GError *error = NULL; 216 | 217 | switch (g_io_channel_read_chars (io, &in, 1, NULL, &error)) { 218 | case G_IO_STATUS_NORMAL: 219 | switch (in) { 220 | case 'q': 221 | g_main_loop_quit (data->loop); 222 | break; 223 | case 'f': 224 | seek (data, FALSE); 225 | break; 226 | case 'g': 227 | seek (data, TRUE); 228 | break; 229 | } 230 | break; 231 | case G_IO_STATUS_AGAIN: 232 | break; 233 | case G_IO_STATUS_ERROR: 234 | g_printerr ("stdin IO error: %s\n", error->message); 235 | g_error_free (error); 236 | g_main_loop_quit (data->loop); 237 | return FALSE; 238 | default: 239 | return FALSE; 240 | } 241 | 242 | return TRUE; 243 | } 244 | -------------------------------------------------------------------------------- /network-clocks/playback-sync.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | static gchar *clock_host = NULL; 11 | static gint clock_port = 0; 12 | static GstClockTime base_time = GST_CLOCK_TIME_NONE; 13 | 14 | static GOptionEntry opt_entries[] = { 15 | {"clock-host", 'c', 0, G_OPTION_ARG_STRING, &clock_host, 16 | "Network clock provider host IP", NULL}, 17 | {"clock-port", 'p', 0, G_OPTION_ARG_INT, &clock_port, 18 | "Network clock provider port", NULL}, 19 | {"base-time", 'b', 0, G_OPTION_ARG_INT64, &base_time, 20 | "Playback base time to sync to", NULL}, 21 | }; 22 | 23 | enum 24 | { 25 | PLAY_FLAGS_VIDEO = 0x1, 26 | PLAY_FLAGS_AUDIO = 0x2, 27 | PLAY_FLAGS_SUBTITLES = 0x4, 28 | PLAY_FLAGS_VISUALISATIONS = 0x8, 29 | PLAY_FLAGS_DOWNLOAD = 0x80 30 | }; 31 | 32 | typedef struct 33 | { 34 | GMainLoop *loop; 35 | GstElement *playbin; 36 | guint bus_watch; 37 | guint io_watch_id; 38 | 39 | gboolean buffering; 40 | gboolean is_live; 41 | } GlobalData; 42 | 43 | static gboolean handle_bus_msg (GstBus * bus, GstMessage * msg, 44 | GlobalData * data); 45 | static gboolean io_callback (GIOChannel * io, GIOCondition condition, 46 | GlobalData * data); 47 | 48 | static GstElement * 49 | create_element (const gchar * type, const gchar * name) 50 | { 51 | GstElement *e; 52 | 53 | e = gst_element_factory_make (type, name); 54 | if (!e) { 55 | g_print ("Failed to create element %s\n", type); 56 | exit (1); 57 | } 58 | 59 | return e; 60 | } 61 | 62 | static gchar * 63 | canonicalise_uri (const gchar * in) 64 | { 65 | if (gst_uri_is_valid (in)) 66 | return g_strdup (in); 67 | 68 | return gst_filename_to_uri (in, NULL); 69 | } 70 | 71 | int 72 | main (int argc, char *argv[]) 73 | { 74 | GOptionContext *opt_ctx; 75 | GstClock *net_clock; 76 | GError *err = NULL; 77 | GlobalData data; 78 | GIOChannel *io = NULL; 79 | GstBus *bus; 80 | gchar *uri; 81 | GstStateChangeReturn sret; 82 | gint flags; 83 | 84 | /* Initialize GStreamer */ 85 | opt_ctx = g_option_context_new ("- Network clock playback"); 86 | g_option_context_add_main_entries (opt_ctx, opt_entries, NULL); 87 | g_option_context_add_group (opt_ctx, gst_init_get_option_group ()); 88 | if (!g_option_context_parse (opt_ctx, &argc, &argv, &err)) 89 | g_error ("Error parsing options: %s", err->message); 90 | g_clear_error (&err); 91 | g_option_context_free (opt_ctx); 92 | 93 | if (argc < 2 || clock_host == NULL || clock_port == 0) { 94 | g_print ("Usage: %s -c netclock-host-IP -p netclock-host-port -b base-time \n", argv[0]); 95 | g_print ("When running, pressing 'q' quits the application\n" 96 | "'f' seeks backwards 10 seconds\n" 97 | "'g' seeks forwards 10 seconds\n" 98 | "'a' switches to the next audio track\n" 99 | "'d' enables/disables subtitles\n" 100 | "'s' switches to the next subtitle track\n" 101 | "'v' enables/disables visualisations\n" 102 | "For this trivial example, you need to press enter after each command\n"); 103 | return 1; 104 | } 105 | 106 | net_clock = gst_net_client_clock_new (NULL, clock_host, clock_port, 0); 107 | /* Wait until the local clock synchronises to the master */ 108 | gst_clock_wait_for_sync (net_clock, GST_CLOCK_TIME_NONE); 109 | g_print ("Network clock is synched to master\n"); 110 | 111 | /* Build the pipeline */ 112 | data.playbin = create_element ("playbin", "playbin"); 113 | 114 | /* Tell the pipeline to always use this clock, and disable 115 | * automatic selection */ 116 | gst_pipeline_use_clock (GST_PIPELINE (data.playbin), net_clock); 117 | 118 | /* We can release the clock now - the pipeline has a handle already */ 119 | gst_object_unref (GST_OBJECT (net_clock)); 120 | 121 | /* If a base-time was supplied, pass that to the pipeline */ 122 | if (base_time != GST_CLOCK_TIME_NONE) { 123 | gst_element_set_start_time(GST_ELEMENT (data.playbin), GST_CLOCK_TIME_NONE); 124 | gst_element_set_base_time (GST_ELEMENT (data.playbin), base_time); 125 | } 126 | 127 | /* Make everyone try and play with 100ms latency */ 128 | gst_pipeline_set_latency (GST_PIPELINE (data.playbin), 100 * GST_MSECOND); 129 | 130 | /* Make sure the input filename or uri is a uri */ 131 | uri = canonicalise_uri (argv[1]); 132 | 133 | /* Set the uri property on playbin */ 134 | g_object_set (data.playbin, "uri", uri, NULL); 135 | 136 | /* Set the playbin download flag */ 137 | g_object_get (data.playbin, "flags", &flags, NULL); 138 | flags |= PLAY_FLAGS_DOWNLOAD; 139 | g_object_set (data.playbin, "flags", flags, NULL); 140 | 141 | /* Connect to the bus to receive callbacks */ 142 | bus = gst_element_get_bus (data.playbin); 143 | 144 | data.bus_watch = gst_bus_add_watch (bus, (GstBusFunc) handle_bus_msg, &data); 145 | 146 | gst_object_unref (bus); 147 | 148 | /* Set up the main loop */ 149 | data.loop = g_main_loop_new (NULL, FALSE); 150 | 151 | /* Start playing */ 152 | sret = gst_element_set_state (data.playbin, GST_STATE_PLAYING); 153 | 154 | g_print ("Now playing %s\n", uri); 155 | g_free (uri); 156 | 157 | switch (sret) { 158 | case GST_STATE_CHANGE_FAILURE: 159 | /* ignore, there will be an error message on the bus */ 160 | break; 161 | case GST_STATE_CHANGE_NO_PREROLL: 162 | g_print ("Pipeline is live.\n"); 163 | data.is_live = TRUE; 164 | break; 165 | case GST_STATE_CHANGE_ASYNC: 166 | g_print ("Prerolling...\r"); 167 | break; 168 | default: 169 | break; 170 | } 171 | 172 | /* Listen to stdin input */ 173 | io = g_io_channel_unix_new (fileno (stdin)); 174 | data.io_watch_id = g_io_add_watch (io, G_IO_IN, (GIOFunc) (io_callback), 175 | &data); 176 | g_io_channel_unref (io); 177 | 178 | /* Run the mainloop until it is exited by the message handler */ 179 | g_main_loop_run (data.loop); 180 | 181 | /* Clean everything up before exiting */ 182 | g_source_remove (data.bus_watch); 183 | g_source_remove (data.io_watch_id); 184 | gst_element_set_state (data.playbin, GST_STATE_NULL); 185 | gst_object_unref (data.playbin); 186 | g_main_loop_unref (data.loop); 187 | 188 | return 0; 189 | } 190 | 191 | static gboolean 192 | handle_bus_msg (GstBus * bus, GstMessage * msg, GlobalData * data) 193 | { 194 | /* Wait until error or EOS */ 195 | switch (GST_MESSAGE_TYPE (msg)) { 196 | case GST_MESSAGE_EOS:{ 197 | g_print ("Finished playback. Exiting.\n"); 198 | g_main_loop_quit (data->loop); 199 | break; 200 | } 201 | case GST_MESSAGE_ERROR:{ 202 | GError *err = NULL; 203 | gchar *dbg_info = NULL; 204 | 205 | gst_message_parse_error (msg, &err, &dbg_info); 206 | g_printerr ("ERROR from element %s: %s\n", 207 | GST_OBJECT_NAME (msg->src), err->message); 208 | g_printerr ("Debugging info: %s\n", (dbg_info) ? dbg_info : "none"); 209 | g_print ("Exiting.\n"); 210 | g_error_free (err); 211 | g_free (dbg_info); 212 | 213 | g_main_loop_quit (data->loop); 214 | break; 215 | } 216 | case GST_MESSAGE_TAG:{ 217 | GstTagList *tags; 218 | gchar *value; 219 | 220 | gst_message_parse_tag (msg, &tags); 221 | 222 | g_print ("Found tags\n"); 223 | if (gst_tag_list_get_string (tags, GST_TAG_ARTIST, &value)) { 224 | g_print ("Artist: %s\n", value); 225 | g_free (value); 226 | } 227 | 228 | if (gst_tag_list_get_string (tags, GST_TAG_TITLE, &value)) { 229 | g_print ("Title: %s\n", value); 230 | g_free (value); 231 | } 232 | 233 | if (gst_tag_list_get_string (tags, GST_TAG_ALBUM, &value)) { 234 | g_print ("Album: %s\n", value); 235 | g_free (value); 236 | } 237 | 238 | gst_tag_list_free (tags); 239 | break; 240 | } 241 | case GST_MESSAGE_ASYNC_DONE:{ 242 | GstPad *video_pad; 243 | GstCaps *caps; 244 | GstStructure *s; 245 | 246 | g_print ("Prerolled.\r"); 247 | 248 | g_signal_emit_by_name (data->playbin, "get-video-pad", 0, &video_pad); 249 | if (video_pad) { 250 | gint width, height; 251 | gint par_n, par_d; 252 | 253 | caps = gst_pad_get_current_caps (video_pad); 254 | s = gst_caps_get_structure (caps, 0); 255 | 256 | gst_structure_get_int (s, "width", &width); 257 | gst_structure_get_int (s, "height", &height); 258 | par_n = par_d = 1; 259 | gst_structure_get_fraction (s, "pixel-aspect-ratio", &par_n, &par_d); 260 | 261 | width = width * par_n / par_d; 262 | g_print ("Video size: %dx%d\n", width, height); 263 | gst_caps_unref (caps); 264 | gst_object_unref (video_pad); 265 | } 266 | 267 | break; 268 | } 269 | case GST_MESSAGE_BUFFERING:{ 270 | gint percent; 271 | 272 | if (!data->buffering) 273 | g_print ("\n"); 274 | 275 | gst_message_parse_buffering (msg, &percent); 276 | g_print ("Buffering... %d%% \r", percent); 277 | 278 | /* no state management needed for live pipelines */ 279 | if (data->is_live) 280 | break; 281 | 282 | if (percent == 100) { 283 | /* a 100% message means buffering is done */ 284 | if (data->buffering) { 285 | data->buffering = FALSE; 286 | gst_element_set_state (data->playbin, GST_STATE_PLAYING); 287 | } 288 | } else { 289 | /* buffering... */ 290 | if (!data->buffering) { 291 | gst_element_set_state (data->playbin, GST_STATE_PAUSED); 292 | data->buffering = TRUE; 293 | } 294 | } 295 | break; 296 | } 297 | case GST_MESSAGE_CLOCK_LOST:{ 298 | /* Clock-lost means the pipeline wants to select a new clock, 299 | * which is done by pausing/playing */ 300 | g_print ("Clock lost, selecting a new one\n"); 301 | gst_element_set_state (data->playbin, GST_STATE_PAUSED); 302 | gst_element_set_state (data->playbin, GST_STATE_PLAYING); 303 | break; 304 | } 305 | case GST_MESSAGE_LATENCY: 306 | g_print ("Redistribute latency...\n"); 307 | gst_bin_recalculate_latency (GST_BIN (data->playbin)); 308 | break; 309 | case GST_MESSAGE_REQUEST_STATE:{ 310 | GstState state; 311 | gchar *name; 312 | 313 | name = gst_object_get_path_string (GST_MESSAGE_SRC (msg)); 314 | 315 | gst_message_parse_request_state (msg, &state); 316 | 317 | g_print ("Setting state to %s as requested by %s...\n", 318 | gst_element_state_get_name (state), name); 319 | 320 | gst_element_set_state (data->playbin, state); 321 | g_free (name); 322 | break; 323 | } 324 | case GST_MESSAGE_WARNING:{ 325 | GError *err; 326 | gchar *dbg = NULL; 327 | 328 | gst_message_parse_warning (msg, &err, &dbg); 329 | g_printerr ("WARNING %s\n", err->message); 330 | if (dbg != NULL) 331 | g_printerr ("WARNING debug information: %s\n", dbg); 332 | g_error_free (err); 333 | g_free (dbg); 334 | break; 335 | } 336 | case GST_MESSAGE_STATE_CHANGED: { 337 | if (GST_MESSAGE_SRC (msg) == GST_OBJECT_CAST (data->playbin)) { 338 | GstState old, new, pending; 339 | gst_message_parse_state_changed (msg, &old, &new, &pending); 340 | if (old == GST_STATE_PAUSED && new == GST_STATE_PLAYING) { 341 | g_print ("Reached playing. Base time is %" G_GUINT64_FORMAT "\n", 342 | gst_element_get_base_time (GST_ELEMENT (data->playbin))); 343 | } 344 | } 345 | break; 346 | } 347 | default:{ 348 | if (gst_is_missing_plugin_message (msg)) { 349 | gchar *desc; 350 | 351 | desc = gst_missing_plugin_message_get_description (msg); 352 | g_print ("Missing plugin: %s\n", desc); 353 | g_free (desc); 354 | } 355 | /* Ignore messages we don't know about */ 356 | break; 357 | } 358 | } 359 | 360 | return TRUE; 361 | } 362 | 363 | static void 364 | seek (GlobalData * data, gboolean forward) 365 | { 366 | gint64 position; 367 | 368 | if (!gst_element_query_position (data->playbin, GST_FORMAT_TIME, &position)) 369 | return; 370 | 371 | if (forward) 372 | position += 10 * GST_SECOND; 373 | else if (position > 10 * GST_SECOND) 374 | position -= 10 * GST_SECOND; 375 | else 376 | position = 0; 377 | 378 | gst_element_seek_simple (data->playbin, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, 379 | position); 380 | } 381 | 382 | static void 383 | next_audio (GlobalData * data) 384 | { 385 | gint current, count; 386 | 387 | /* Switch to the next audio track */ 388 | g_object_get (data->playbin, 389 | "current-audio", ¤t, "n-audio", &count, NULL); 390 | current += 1; 391 | if (current >= count) 392 | current = 0; 393 | 394 | g_object_set (data->playbin, "current-audio", current, NULL); 395 | g_print ("Now playing audio track %d of %d\n", current, count); 396 | } 397 | 398 | static void 399 | toggle_subtitle (GlobalData * data) 400 | { 401 | gint flags; 402 | 403 | g_object_get (data->playbin, "flags", &flags, NULL); 404 | if (flags & PLAY_FLAGS_SUBTITLES) { 405 | g_print ("Disabling subtitles\n"); 406 | flags &= ~PLAY_FLAGS_SUBTITLES; 407 | } else { 408 | g_print ("Enabling subtitles\n"); 409 | flags |= PLAY_FLAGS_SUBTITLES; 410 | } 411 | 412 | g_object_set (data->playbin, "flags", flags, NULL); 413 | } 414 | 415 | static void 416 | next_subtitle (GlobalData * data) 417 | { 418 | gint flags; 419 | gint current, count; 420 | 421 | /* Switch to the next subtitle track */ 422 | g_object_get (data->playbin, 423 | "current-text", ¤t, "n-text", &count, NULL); 424 | current += 1; 425 | if (current >= count) 426 | current = 0; 427 | g_object_set (data->playbin, "current-text", current, NULL); 428 | 429 | /* Make sure subtitles are enabled */ 430 | g_object_get (data->playbin, "flags", &flags, NULL); 431 | flags |= PLAY_FLAGS_SUBTITLES; 432 | g_object_set (data->playbin, "flags", flags, NULL); 433 | g_print ("Now showing subtitles track %d of %d\n", current, count); 434 | } 435 | 436 | static void 437 | toggle_vis (GlobalData * data) 438 | { 439 | gint flags; 440 | 441 | g_object_get (data->playbin, "flags", &flags, NULL); 442 | if (flags & PLAY_FLAGS_VISUALISATIONS) { 443 | g_print ("Disabling visualisations\n"); 444 | flags &= ~PLAY_FLAGS_VISUALISATIONS; 445 | } else { 446 | g_print ("Enabling visualisations\n"); 447 | flags |= PLAY_FLAGS_VISUALISATIONS; 448 | } 449 | 450 | g_object_set (data->playbin, "flags", flags, NULL); 451 | } 452 | 453 | static gboolean 454 | io_callback (GIOChannel * io, GIOCondition condition, GlobalData * data) 455 | { 456 | gchar in; 457 | GError *error = NULL; 458 | 459 | switch (g_io_channel_read_chars (io, &in, 1, NULL, &error)) { 460 | case G_IO_STATUS_NORMAL: 461 | switch (in) { 462 | case 'q': 463 | g_main_loop_quit (data->loop); 464 | break; 465 | case 'f': 466 | seek (data, FALSE); 467 | break; 468 | case 'g': 469 | seek (data, TRUE); 470 | break; 471 | case 'a': 472 | next_audio (data); 473 | break; 474 | case 'd': 475 | toggle_subtitle (data); 476 | break; 477 | case 's': 478 | next_subtitle (data); 479 | break; 480 | case 'v': 481 | toggle_vis (data); 482 | break; 483 | } 484 | break; 485 | case G_IO_STATUS_AGAIN: 486 | break; 487 | case G_IO_STATUS_ERROR: 488 | g_printerr ("stdin IO error: %s\n", error->message); 489 | g_error_free (error); 490 | g_main_loop_quit (data->loop); 491 | return FALSE; 492 | default: 493 | return FALSE; 494 | } 495 | 496 | return TRUE; 497 | } 498 | --------------------------------------------------------------------------------