From f8a3bdc870992858b2e3bf66c33d1a10393de35e Mon Sep 17 00:00:00 2001 Message-Id: From: Mark Wooding Date: Tue, 10 Jul 2018 11:40:09 +0100 Subject: [PATCH] plugins/tracklength-gstreamer.c: Add GStreamer-based tracklength plugin. Organization: Straylight/Edgeware From: Mark Wooding Now we can have the proper lengths for all supported files. --- debian/control | 3 +- debian/disorder-gstreamer.install | 3 + plugins/Makefile.am | 11 ++ plugins/tracklength-gstreamer.c | 287 ++++++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 plugins/tracklength-gstreamer.c diff --git a/debian/control b/debian/control index fd78ad0..a4e7130 100644 --- a/debian/control +++ b/debian/control @@ -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 diff --git a/debian/disorder-gstreamer.install b/debian/disorder-gstreamer.install index d7a5b8d..3f6c8fc 100755 --- a/debian/disorder-gstreamer.install +++ b/debian/disorder-gstreamer.install @@ -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 diff --git a/plugins/Makefile.am b/plugins/Makefile.am index ed189c0..6c79cf6 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -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 index 0000000..1ccc6eb --- /dev/null +++ b/plugins/tracklength-gstreamer.c @@ -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 . + */ +/** @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 +#include +#include +#include + +/* 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: +*/ -- [mdw]