chiark / gitweb /
plugins/tracklength-gstreamer.c: Add GStreamer-based tracklength plugin.
[disorder] / plugins / tracklength-gstreamer.c
... / ...
CommitLineData
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
47static int inited_gstreamer = 0;
48
49enum { ST_PAUSE, ST_PAUSED, ST_PLAY, ST_EOS, ST_BOTCHED = -1 };
50
51struct tracklength_state {
52 const char *path;
53 int state;
54 GMainLoop *loop;
55 GstElement *pipeline;
56 GstElement *sink;
57};
58
59static 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
81match:
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
97botched:
98 state->state = ST_BOTCHED;
99 g_main_loop_quit(state->loop);
100end:
101 if(caps) gst_caps_unref(caps);
102 if(padname) g_free(padname);
103}
104
105static 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
152static 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
176long 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
253end:
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
272int 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/*
282Local Variables:
283c-basic-offset:2
284comment-column:40
285fill-column:79
286End:
287*/