chiark / gitweb /
1ccc6eb884f1ebcb3d4cba1a48b0b568b77930ac
[disorder] / plugins / tracklength-gstreamer.c
1 /*
2  * This file is part of DisOrder.
3  * Copyright (C) 2018 Mark Wooding
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
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/>.
17  */
18 /** @file plugins/tracklength-gstreamer.c
19  * @brief Plugin to compute track lengths using GStreamer
20  */
21 #include "tracklength.h"
22
23 #include "speaker-protocol.h"
24
25 /* Ugh.  It turns out that libxml tries to define a function called
26  * `attribute', and it's included by GStreamer for some unimaginable reason.
27  * So undefine it here.  We'll want GCC attributes for special effects, but
28  * can take care of ourselves.
29  */
30 #undef attribute
31
32 #include <glib.h>
33 #include <gst/gst.h>
34 #include <gst/app/gstappsink.h>
35 #include <gst/audio/audio.h>
36
37 /* The only application we have for `attribute' is declaring function
38  * arguments as being unused, because we have a lot of callback functions
39  * which are meant to comply with an externally defined interface.
40  */
41 #ifdef __GNUC__
42 #  define UNUSED __attribute__((unused))
43 #endif
44
45 #define END ((void *)0)
46
47 static int inited_gstreamer = 0;
48
49 enum { ST_PAUSE, ST_PAUSED, ST_PLAY, ST_EOS, ST_BOTCHED = -1 };
50
51 struct tracklength_state {
52   const char *path;
53   int state;
54   GMainLoop *loop;
55   GstElement *pipeline;
56   GstElement *sink;
57 };
58
59 static void decoder_pad_arrived(GstElement *elt, GstPad *pad, gpointer p) {
60   /* The `decodebin' element dreamed up a pad.  If it's for audio output,
61    * attach it to a dummy sink so that it doesn't become sad.
62    */
63
64   struct tracklength_state *state = p;
65   GstCaps *caps = 0;
66   GstStructure *s;
67   gchar *padname = 0;
68   guint i, n;
69
70 #ifdef HAVE_GSTREAMER_0_10
71   caps = gst_pad_get_caps(pad); if(!caps) goto end;
72 #else
73   caps = gst_pad_get_current_caps(pad); if(!caps) goto end;
74 #endif
75   for(i = 0, n = gst_caps_get_size(caps); i < n; i++) {
76     s = gst_caps_get_structure(caps, i);
77     if(!strncmp(gst_structure_get_name(s), "audio/", 6)) goto match;
78   }
79   goto end;
80
81 match:
82   padname = gst_pad_get_name(pad);
83   if(!padname) {
84     disorder_error(0, "error checking `%s': "
85                    "failed to get GStreamer pad name (out of memory?)",
86                    state->path);
87     goto botched;
88   }
89   if(!gst_element_link_pads(elt, padname, state->sink, "sink")) {
90     disorder_error(0, "error checking `%s': "
91                    "failed to link GStreamer elements `%s' and `sink'",
92                    state->path, GST_OBJECT_NAME(elt));
93     goto botched;
94   }
95   goto end;
96
97 botched:
98   state->state = ST_BOTCHED;
99   g_main_loop_quit(state->loop);
100 end:
101   if(caps) gst_caps_unref(caps);
102   if(padname) g_free(padname);
103 }
104
105 static void bus_message(GstBus UNUSED *bus, GstMessage *msg, gpointer p) {
106   /* Our bus sent us a message.  If it's interesting, maybe switch state, and
107    * wake the main loop up.
108    */
109
110   struct tracklength_state *state = p;
111   GstState newstate;
112
113   switch(GST_MESSAGE_TYPE(msg)) {
114   case GST_MESSAGE_ERROR:
115     /* An error.  Mark the operation as botched and wake up the main loop. */
116     disorder_error(0, "error checking `%s': %s", state->path,
117                    gst_structure_get_string(gst_message_get_structure(msg),
118                                             "debug"));
119     state->state = ST_BOTCHED;
120     g_main_loop_quit(state->loop);
121     break;
122   case GST_MESSAGE_STATE_CHANGED:
123     /* A state change.  If we've reached `PAUSED', then notify the main loop.
124      * This turns out to be a point at which the duration becomes available.
125      * If not, then the main loop will set us playing.
126      */
127     gst_message_parse_state_changed(msg, 0, &newstate, 0);
128     if(state->state == ST_PAUSE && newstate == GST_STATE_PAUSED &&
129        GST_MESSAGE_SRC(msg) == GST_OBJECT(state->pipeline)) {
130       g_main_loop_quit(state->loop);
131       state->state = ST_PAUSED;
132     }
133     break;
134   case GST_MESSAGE_EOS:
135     /* The end of the stream.  Wake up the loop, and set the state to mark this
136      * as being the end of the line.
137      */
138     state->state = ST_EOS;
139     g_main_loop_quit(state->loop);
140     break;
141   case GST_MESSAGE_DURATION_CHANGED:
142     /* Something thinks it knows a duration.  Wake up the main loop just in
143      * case.
144      */
145     break;
146   default:
147     /* Something else happened.  Whatevs. */
148     break;
149   }
150 }
151
152 static int query_track_length(struct tracklength_state *state,
153                               long *length_out)
154 {
155   /* Interrogate the pipeline to find the track length.  Return zero on
156    * success, or -1 on failure.  This is annoying and nonportable.
157    */
158
159   gint64 t;
160 #ifdef HAVE_GSTREAMER_0_10
161   GstFormat fmt = GST_FORMAT_TIME;
162 #endif
163
164 #ifdef HAVE_GSTREAMER_0_10
165   if(!gst_element_query_duration(state->pipeline, &fmt, &t) ||
166      fmt != GST_FORMAT_TIME)
167     return -1;
168 #else
169   if(!gst_element_query_duration(state->pipeline, GST_FORMAT_TIME, &t))
170     return -1;
171 #endif
172   *length_out = (t + 500000000)/1000000000;
173   return 0;
174 }
175
176 long disorder_tracklength(const char UNUSED *track, const char *path) {
177   /* Discover the length of a track. */
178
179   struct tracklength_state state;
180   GstElement *source, *decode, *sink;
181   GstBus *bus = 0;
182   int running = 0;
183   long length = -1;
184
185   /* Fill in the state structure. */
186   state.path = path;
187   state.state = ST_PAUSE;
188   state.pipeline = 0;
189   state.loop = 0;
190
191   /* Set up the GStreamer machinery. */
192   if(!inited_gstreamer) gst_init(0, 0);
193
194   /* Create the necessary pipeline elements. */
195   source = gst_element_factory_make("filesrc", "file");
196   decode = gst_element_factory_make("decodebin", "decode");
197   sink = state.sink = gst_element_factory_make("fakesink", "sink");
198   state.pipeline = gst_pipeline_new("pipe");
199   if(!source || !decode || !sink) {
200     disorder_error(0, "failed to create GStreamer elements: "
201                    "need base and good plugins");
202     goto end;
203   }
204   g_object_set(source, "location", path, END);
205
206   /* Add the elements to the pipeline.  It will take over responsibility for
207    * them.
208    */
209   gst_bin_add_many(GST_BIN(state.pipeline), source, decode, sink, END);
210
211   /* Link the elements together as far as we can.  Arrange to link the decoder
212    * onto our (dummy) sink when it's ready to produce output.
213    */
214   if(!gst_element_link(source, decode)) {
215     disorder_error(0, "error checking `%s': "
216                    "failed to link GStreamer elements `file' and `decode'",
217                    path);
218     goto end;
219   }
220   g_signal_connect(decode, "pad-added",
221                    G_CALLBACK(decoder_pad_arrived), &state);
222
223   /* Fetch the bus and listen for messages. */
224   bus = gst_pipeline_get_bus(GST_PIPELINE(state.pipeline));
225   gst_bus_add_signal_watch(bus);
226   g_signal_connect(bus, "message", G_CALLBACK(bus_message), &state);
227
228   /* Turn the handle until we have an answer.  The message handler will wake us
229    * up if: the pipeline reports that the duration has changed (suggesting that
230    * something might know what it is); we successfully reach the initial
231    * `paused' state; or we hit the end of the stream (by which point we ought
232    * to know where we are).
233    */
234   state.loop = g_main_loop_new(0, FALSE);
235   gst_element_set_state(state.pipeline, GST_STATE_PAUSED); running = 1;
236   for(;;) {
237     g_main_loop_run(state.loop);
238     if(!query_track_length(&state, &length)) goto end;
239     switch(state.state) {
240     case ST_BOTCHED: goto end;
241     case ST_EOS:
242       disorder_error(0, "error checking `%s': "
243                      "failed to fetch duration from GStreamer pipeline",
244                      path);
245       goto end;
246     case ST_PAUSED:
247       gst_element_set_state(state.pipeline, GST_STATE_PLAYING);
248       state.state = ST_PLAY;
249       break;
250     }
251   }
252
253 end:
254   /* Well, it's too late to worry about anything now. */
255   if(running) gst_element_set_state(state.pipeline, GST_STATE_NULL);
256   if(bus) {
257     gst_bus_remove_signal_watch(bus);
258     gst_object_unref(bus);
259   }
260   if(state.pipeline)
261     gst_object_unref(state.pipeline);
262   else {
263     if(source) gst_object_unref(source);
264     if(decode) gst_object_unref(decode);
265     if(sink) gst_object_unref(sink);
266   }
267   if(state.loop) g_main_loop_unref(state.loop);
268   return length;
269 }
270
271 #ifdef STANDALONE
272 int main(int argc, char *argv[]) {
273   int i;
274
275   for(i = 1; i < argc; i++)
276     printf("%s: %ld\n", argv[i], disorder_tracklength(0, argv[i]));
277   return (0);
278 }
279 #endif
280
281 /*
282 Local Variables:
283 c-basic-offset:2
284 comment-column:40
285 fill-column:79
286 End:
287 */