chiark / gitweb /
plugins/tracklength-gstreamer.c: Add GStreamer-based tracklength plugin.
authorMark Wooding <mdw@distorted.org.uk>
Tue, 10 Jul 2018 10:40:09 +0000 (11:40 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 10 Jul 2018 21:07:57 +0000 (22:07 +0100)
Now we can have the proper lengths for all supported files.

debian/control
debian/disorder-gstreamer.install
plugins/Makefile.am
plugins/tracklength-gstreamer.c [new file with mode: 0644]

index fd78ad0637af9b935bf747f8b6183ec37a32ee69..a4e71307429c0c5cf4410fe9483745ba6fcedfd2 100644 (file)
@@ -68,7 +68,8 @@ Description: GStreamer-based audio decoder for DisOrder
  .
  This package contains a GStreamer-based audio decoder.  This can (with
  appropriate GStreamer plugins) decode different kinds of audio files, and
- also apply ReplayGain to the decoded audio.
+ also apply ReplayGain to the decoded audio.  It also includes a GStreamer-
+ based track length plugin.
 
 Package: disobedience
 Architecture: any
index d7a5b8d528d4bb4efe6d9ecca3a2fded35e3479f..3f6c8fcba032fdc263c29c68b79cea2f802708ce 100755 (executable)
@@ -1,2 +1,5 @@
+#! /usr/bin/dh-exec
+
+usr/lib/disorder/tracklength-gstreamer.so.0.0.0 => /usr/lib/disorder/tracklength-gstreamer.so
 usr/sbin/disorder-gstdecode
 usr/share/man/man8/disorder-gstdecode.8
index ed189c04dfefa54c48f15b8b0cc2a47870249657..6c79cf61b40f6be2ab4cbb41c409839a695fa76c 100644 (file)
@@ -22,6 +22,7 @@ pkglib_LTLIBRARIES=disorder-tracklength.la fs.la notify.la exec.la shell.la \
                   execraw.la
 AUTOMAKE_OPTIONS=subdir-objects
 AM_CPPFLAGS=-I${top_srcdir}/lib
+AM_CFLAGS=
 
 notify_la_SOURCES=notify.c
 notify_la_LDFLAGS=-module
@@ -44,3 +45,13 @@ execraw_la_LDFLAGS=-module
 
 shell_la_SOURCES=shell.c
 shell_la_LDFLAGS=-module
+
+if GSTDECODE
+AM_CFLAGS+=$(GSTREAMER_CFLAGS)
+pkglib_LTLIBRARIES+=tracklength-gstreamer.la
+tracklength_gstreamer_la_SOURCES=tracklength-gstreamer.c
+tracklength_gstreamer_la_LDFLAGS=-module
+tracklength_gstreamer_la_LIBADD=$(LIBOBJS) \
+       $(GSTREAMER_PLUGINS_BASE_LIBS) \
+       $(GSTREAMER_LIBS)
+endif
diff --git a/plugins/tracklength-gstreamer.c b/plugins/tracklength-gstreamer.c
new file mode 100644 (file)
index 0000000..1ccc6eb
--- /dev/null
@@ -0,0 +1,287 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2018 Mark Wooding
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+/** @file plugins/tracklength-gstreamer.c
+ * @brief Plugin to compute track lengths using GStreamer
+ */
+#include "tracklength.h"
+
+#include "speaker-protocol.h"
+
+/* Ugh.  It turns out that libxml tries to define a function called
+ * `attribute', and it's included by GStreamer for some unimaginable reason.
+ * So undefine it here.  We'll want GCC attributes for special effects, but
+ * can take care of ourselves.
+ */
+#undef attribute
+
+#include <glib.h>
+#include <gst/gst.h>
+#include <gst/app/gstappsink.h>
+#include <gst/audio/audio.h>
+
+/* The only application we have for `attribute' is declaring function
+ * arguments as being unused, because we have a lot of callback functions
+ * which are meant to comply with an externally defined interface.
+ */
+#ifdef __GNUC__
+#  define UNUSED __attribute__((unused))
+#endif
+
+#define END ((void *)0)
+
+static int inited_gstreamer = 0;
+
+enum { ST_PAUSE, ST_PAUSED, ST_PLAY, ST_EOS, ST_BOTCHED = -1 };
+
+struct tracklength_state {
+  const char *path;
+  int state;
+  GMainLoop *loop;
+  GstElement *pipeline;
+  GstElement *sink;
+};
+
+static void decoder_pad_arrived(GstElement *elt, GstPad *pad, gpointer p) {
+  /* The `decodebin' element dreamed up a pad.  If it's for audio output,
+   * attach it to a dummy sink so that it doesn't become sad.
+   */
+
+  struct tracklength_state *state = p;
+  GstCaps *caps = 0;
+  GstStructure *s;
+  gchar *padname = 0;
+  guint i, n;
+
+#ifdef HAVE_GSTREAMER_0_10
+  caps = gst_pad_get_caps(pad); if(!caps) goto end;
+#else
+  caps = gst_pad_get_current_caps(pad); if(!caps) goto end;
+#endif
+  for(i = 0, n = gst_caps_get_size(caps); i < n; i++) {
+    s = gst_caps_get_structure(caps, i);
+    if(!strncmp(gst_structure_get_name(s), "audio/", 6)) goto match;
+  }
+  goto end;
+
+match:
+  padname = gst_pad_get_name(pad);
+  if(!padname) {
+    disorder_error(0, "error checking `%s': "
+                   "failed to get GStreamer pad name (out of memory?)",
+                   state->path);
+    goto botched;
+  }
+  if(!gst_element_link_pads(elt, padname, state->sink, "sink")) {
+    disorder_error(0, "error checking `%s': "
+                   "failed to link GStreamer elements `%s' and `sink'",
+                   state->path, GST_OBJECT_NAME(elt));
+    goto botched;
+  }
+  goto end;
+
+botched:
+  state->state = ST_BOTCHED;
+  g_main_loop_quit(state->loop);
+end:
+  if(caps) gst_caps_unref(caps);
+  if(padname) g_free(padname);
+}
+
+static void bus_message(GstBus UNUSED *bus, GstMessage *msg, gpointer p) {
+  /* Our bus sent us a message.  If it's interesting, maybe switch state, and
+   * wake the main loop up.
+   */
+
+  struct tracklength_state *state = p;
+  GstState newstate;
+
+  switch(GST_MESSAGE_TYPE(msg)) {
+  case GST_MESSAGE_ERROR:
+    /* An error.  Mark the operation as botched and wake up the main loop. */
+    disorder_error(0, "error checking `%s': %s", state->path,
+                   gst_structure_get_string(gst_message_get_structure(msg),
+                                            "debug"));
+    state->state = ST_BOTCHED;
+    g_main_loop_quit(state->loop);
+    break;
+  case GST_MESSAGE_STATE_CHANGED:
+    /* A state change.  If we've reached `PAUSED', then notify the main loop.
+     * This turns out to be a point at which the duration becomes available.
+     * If not, then the main loop will set us playing.
+     */
+    gst_message_parse_state_changed(msg, 0, &newstate, 0);
+    if(state->state == ST_PAUSE && newstate == GST_STATE_PAUSED &&
+       GST_MESSAGE_SRC(msg) == GST_OBJECT(state->pipeline)) {
+      g_main_loop_quit(state->loop);
+      state->state = ST_PAUSED;
+    }
+    break;
+  case GST_MESSAGE_EOS:
+    /* The end of the stream.  Wake up the loop, and set the state to mark this
+     * as being the end of the line.
+     */
+    state->state = ST_EOS;
+    g_main_loop_quit(state->loop);
+    break;
+  case GST_MESSAGE_DURATION_CHANGED:
+    /* Something thinks it knows a duration.  Wake up the main loop just in
+     * case.
+     */
+    break;
+  default:
+    /* Something else happened.  Whatevs. */
+    break;
+  }
+}
+
+static int query_track_length(struct tracklength_state *state,
+                              long *length_out)
+{
+  /* Interrogate the pipeline to find the track length.  Return zero on
+   * success, or -1 on failure.  This is annoying and nonportable.
+   */
+
+  gint64 t;
+#ifdef HAVE_GSTREAMER_0_10
+  GstFormat fmt = GST_FORMAT_TIME;
+#endif
+
+#ifdef HAVE_GSTREAMER_0_10
+  if(!gst_element_query_duration(state->pipeline, &fmt, &t) ||
+     fmt != GST_FORMAT_TIME)
+    return -1;
+#else
+  if(!gst_element_query_duration(state->pipeline, GST_FORMAT_TIME, &t))
+    return -1;
+#endif
+  *length_out = (t + 500000000)/1000000000;
+  return 0;
+}
+
+long disorder_tracklength(const char UNUSED *track, const char *path) {
+  /* Discover the length of a track. */
+
+  struct tracklength_state state;
+  GstElement *source, *decode, *sink;
+  GstBus *bus = 0;
+  int running = 0;
+  long length = -1;
+
+  /* Fill in the state structure. */
+  state.path = path;
+  state.state = ST_PAUSE;
+  state.pipeline = 0;
+  state.loop = 0;
+
+  /* Set up the GStreamer machinery. */
+  if(!inited_gstreamer) gst_init(0, 0);
+
+  /* Create the necessary pipeline elements. */
+  source = gst_element_factory_make("filesrc", "file");
+  decode = gst_element_factory_make("decodebin", "decode");
+  sink = state.sink = gst_element_factory_make("fakesink", "sink");
+  state.pipeline = gst_pipeline_new("pipe");
+  if(!source || !decode || !sink) {
+    disorder_error(0, "failed to create GStreamer elements: "
+                   "need base and good plugins");
+    goto end;
+  }
+  g_object_set(source, "location", path, END);
+
+  /* Add the elements to the pipeline.  It will take over responsibility for
+   * them.
+   */
+  gst_bin_add_many(GST_BIN(state.pipeline), source, decode, sink, END);
+
+  /* Link the elements together as far as we can.  Arrange to link the decoder
+   * onto our (dummy) sink when it's ready to produce output.
+   */
+  if(!gst_element_link(source, decode)) {
+    disorder_error(0, "error checking `%s': "
+                   "failed to link GStreamer elements `file' and `decode'",
+                   path);
+    goto end;
+  }
+  g_signal_connect(decode, "pad-added",
+                   G_CALLBACK(decoder_pad_arrived), &state);
+
+  /* Fetch the bus and listen for messages. */
+  bus = gst_pipeline_get_bus(GST_PIPELINE(state.pipeline));
+  gst_bus_add_signal_watch(bus);
+  g_signal_connect(bus, "message", G_CALLBACK(bus_message), &state);
+
+  /* Turn the handle until we have an answer.  The message handler will wake us
+   * up if: the pipeline reports that the duration has changed (suggesting that
+   * something might know what it is); we successfully reach the initial
+   * `paused' state; or we hit the end of the stream (by which point we ought
+   * to know where we are).
+   */
+  state.loop = g_main_loop_new(0, FALSE);
+  gst_element_set_state(state.pipeline, GST_STATE_PAUSED); running = 1;
+  for(;;) {
+    g_main_loop_run(state.loop);
+    if(!query_track_length(&state, &length)) goto end;
+    switch(state.state) {
+    case ST_BOTCHED: goto end;
+    case ST_EOS:
+      disorder_error(0, "error checking `%s': "
+                     "failed to fetch duration from GStreamer pipeline",
+                     path);
+      goto end;
+    case ST_PAUSED:
+      gst_element_set_state(state.pipeline, GST_STATE_PLAYING);
+      state.state = ST_PLAY;
+      break;
+    }
+  }
+
+end:
+  /* Well, it's too late to worry about anything now. */
+  if(running) gst_element_set_state(state.pipeline, GST_STATE_NULL);
+  if(bus) {
+    gst_bus_remove_signal_watch(bus);
+    gst_object_unref(bus);
+  }
+  if(state.pipeline)
+    gst_object_unref(state.pipeline);
+  else {
+    if(source) gst_object_unref(source);
+    if(decode) gst_object_unref(decode);
+    if(sink) gst_object_unref(sink);
+  }
+  if(state.loop) g_main_loop_unref(state.loop);
+  return length;
+}
+
+#ifdef STANDALONE
+int main(int argc, char *argv[]) {
+  int i;
+
+  for(i = 1; i < argc; i++)
+    printf("%s: %ld\n", argv[i], disorder_tracklength(0, argv[i]));
+  return (0);
+}
+#endif
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+End:
+*/