Commit | Line | Data |
---|---|---|
f8a3bdc8 MW |
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 | */ |