| 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 | */ |