From 85849c93964161c15cc0cec7e68bb4917ee93bcd Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@musicpd.org>
Date: Mon, 21 Sep 2020 13:01:51 +0200
Subject: [PATCH] decoder/plugin: add method protocols()

Similar to commit 4e2a551f30c1e1db13933d15c44d9186a2f37959 but for
decoder plugins.  This is tailored for the FFmpeg decoder plugin which
implements some protocols (e.g. RTSP) as demuxer plugin.
---
 src/CommandLine.cxx           |  4 +++
 src/decoder/DecoderPlugin.cxx | 14 +++++++++
 src/decoder/DecoderPlugin.hxx | 30 ++++++++++++++++++
 src/decoder/Thread.cxx        | 57 +++++++++++++++++++++++++++++++++++
 src/ls.cxx                    | 16 +++++++++-
 test/run_decoder.cxx          |  7 ++++-
 6 files changed, 126 insertions(+), 2 deletions(-)

diff --git a/src/CommandLine.cxx b/src/CommandLine.cxx
index 1413d5374..96d5be4b1 100644
--- a/src/CommandLine.cxx
+++ b/src/CommandLine.cxx
@@ -154,6 +154,10 @@ static void version()
 			for (; *suffixes != nullptr; ++suffixes)
 				printf(" %s", *suffixes);
 
+		if (plugin.protocols != nullptr)
+			for (const auto &i : plugin.protocols())
+				printf(" %s", i.c_str());
+
 		printf("\n");
 	});
 
diff --git a/src/decoder/DecoderPlugin.cxx b/src/decoder/DecoderPlugin.cxx
index d7e6de357..bbb3966fc 100644
--- a/src/decoder/DecoderPlugin.cxx
+++ b/src/decoder/DecoderPlugin.cxx
@@ -18,10 +18,24 @@
  */
 
 #include "DecoderPlugin.hxx"
+#include "util/StringCompare.hxx"
 #include "util/StringUtil.hxx"
 
+#include <algorithm>
 #include <cassert>
 
+bool
+DecoderPlugin::SupportsUri(const char *uri) const noexcept
+{
+	if (protocols != nullptr) {
+		const auto p = protocols();
+		return std::any_of(p.begin(), p.end(), [uri](const auto &schema)
+			{ return StringStartsWithIgnoreCase(uri, schema.c_str()); } );
+	}
+
+	return false;
+}
+
 bool
 DecoderPlugin::SupportsSuffix(const char *suffix) const noexcept
 {
diff --git a/src/decoder/DecoderPlugin.hxx b/src/decoder/DecoderPlugin.hxx
index da4409e47..c09aa5032 100644
--- a/src/decoder/DecoderPlugin.hxx
+++ b/src/decoder/DecoderPlugin.hxx
@@ -23,6 +23,8 @@
 #include "util/Compiler.h"
 
 #include <forward_list>  // IWYU pragma: export
+#include <set>
+#include <string>
 
 struct ConfigBlock;
 class InputStream;
@@ -50,6 +52,16 @@ struct DecoderPlugin {
 	 */
 	void (*finish)() noexcept = nullptr;
 
+	/**
+	 * Return a set of supported protocols.
+	 */
+	std::set<std::string> (*protocols)() noexcept = nullptr;
+
+	/**
+	 * Decode an URI with a protocol listed in protocols().
+	 */
+	void (*uri_decode)(DecoderClient &client, const char *uri) = nullptr;
+
 	/**
 	 * Decode a stream (data read from an #InputStream object).
 	 *
@@ -143,6 +155,14 @@ struct DecoderPlugin {
 		return copy;
 	}
 
+	constexpr auto WithProtocols(std::set<std::string> (*_protocols)() noexcept,
+				     void (*_uri_decode)(DecoderClient &client, const char *uri)) noexcept {
+		auto copy = *this;
+		copy.protocols = _protocols;
+		copy.uri_decode = _uri_decode;
+		return copy;
+	}
+
 	constexpr auto WithSuffixes(const char *const*_suffixes) noexcept {
 		auto copy = *this;
 		copy.suffixes = _suffixes;
@@ -183,6 +203,13 @@ struct DecoderPlugin {
 		stream_decode(client, is);
 	}
 
+	/**
+	 * Decode an URI which is supported (check SupportsUri()).
+	 */
+	void UriDecode(DecoderClient &client, const char *uri) const {
+		uri_decode(client, uri);
+	}
+
 	/**
 	 * Decode a file.
 	 */
@@ -218,6 +245,9 @@ struct DecoderPlugin {
 		return container_scan(path, tnum);
 	}
 
+	gcc_pure
+	bool SupportsUri(const char *uri) const noexcept;
+
 	/**
 	 * Does the plugin announce the specified file name suffix?
 	 */
diff --git a/src/decoder/Thread.cxx b/src/decoder/Thread.cxx
index 9167cd421..d4d85edb4 100644
--- a/src/decoder/Thread.cxx
+++ b/src/decoder/Thread.cxx
@@ -46,6 +46,42 @@
 
 static constexpr Domain decoder_thread_domain("decoder_thread");
 
+/**
+ * Decode a URI with the given decoder plugin.
+ *
+ * Caller holds DecoderControl::mutex.
+ */
+static bool
+DecoderUriDecode(const DecoderPlugin &plugin,
+		 DecoderBridge &bridge, const char *uri)
+{
+	assert(plugin.uri_decode != nullptr);
+	assert(bridge.stream_tag == nullptr);
+	assert(bridge.decoder_tag == nullptr);
+	assert(uri != nullptr);
+	assert(bridge.dc.state == DecoderState::START);
+
+	FormatDebug(decoder_thread_domain, "probing plugin %s", plugin.name);
+
+	if (bridge.dc.command == DecoderCommand::STOP)
+		throw StopDecoder();
+
+	{
+		const ScopeUnlock unlock(bridge.dc.mutex);
+
+		FormatThreadName("decoder:%s", plugin.name);
+
+		plugin.UriDecode(bridge, uri);
+
+		SetThreadName("decoder");
+	}
+
+	assert(bridge.dc.state == DecoderState::START ||
+	       bridge.dc.state == DecoderState::DECODE);
+
+	return bridge.dc.state != DecoderState::START;
+}
+
 /**
  * Decode a stream with the given decoder plugin.
  *
@@ -236,6 +272,24 @@ MaybeLoadReplayGain(DecoderBridge &bridge, InputStream &is)
 	LoadReplayGain(bridge, is);
 }
 
+/**
+ * Try decoding a URI.
+ *
+ * DecoderControl::mutex is not be locked by caller.
+ */
+static bool
+TryUriDecode(DecoderBridge &bridge, const char *uri)
+{
+	return decoder_plugins_try([&bridge, uri](const DecoderPlugin &plugin){
+		if (!plugin.SupportsUri(uri))
+			return false;
+
+		std::unique_lock<Mutex> lock(bridge.dc.mutex);
+		bridge.Reset();
+		return DecoderUriDecode(plugin, bridge, uri);
+	});
+}
+
 /**
  * Try decoding a stream.
  *
@@ -244,6 +298,9 @@ MaybeLoadReplayGain(DecoderBridge &bridge, InputStream &is)
 static bool
 decoder_run_stream(DecoderBridge &bridge, const char *uri)
 {
+	if (TryUriDecode(bridge, uri))
+		return true;
+
 	DecoderControl &dc = bridge.dc;
 
 	auto input_stream = bridge.OpenUri(uri);
diff --git a/src/ls.cxx b/src/ls.cxx
index 022ccb370..5a1536c3d 100644
--- a/src/ls.cxx
+++ b/src/ls.cxx
@@ -21,6 +21,8 @@
 #include "ls.hxx"
 #include "input/Registry.hxx"
 #include "input/InputPlugin.hxx"
+#include "decoder/DecoderList.hxx"
+#include "decoder/DecoderPlugin.hxx"
 #include "client/Response.hxx"
 #include "util/UriExtract.hxx"
 
@@ -39,6 +41,11 @@ void print_supported_uri_schemes_to_fp(FILE *fp)
 			protocols.emplace(uri);
 		});
 
+	decoder_plugins_for_each([&protocols](const auto &plugin){
+		if (plugin.protocols != nullptr)
+			protocols.merge(plugin.protocols());
+	});
+
 	for (const auto& protocol : protocols) {
 		fprintf(fp, " %s", protocol.c_str());
 	}
@@ -54,6 +61,11 @@ print_supported_uri_schemes(Response &r)
 			protocols.emplace(uri);
 		});
 
+	decoder_plugins_for_each_enabled([&protocols](const auto &plugin){
+		if (plugin.protocols != nullptr)
+			protocols.merge(plugin.protocols());
+	});
+
 	for (const auto& protocol : protocols) {
 		r.Format("handler: %s\n", protocol.c_str());
 	}
@@ -68,5 +80,7 @@ uri_supported_scheme(const char *uri) noexcept
 		if (plugin->SupportsUri(uri))
 			return true;
 
-	return false;
+	return decoder_plugins_try([uri](const auto &plugin){
+		return plugin.SupportsUri(uri);
+	});
 }
diff --git a/test/run_decoder.cxx b/test/run_decoder.cxx
index 1147e4faf..a7e275302 100644
--- a/test/run_decoder.cxx
+++ b/test/run_decoder.cxx
@@ -204,7 +204,12 @@ try {
 	}
 
 	MyDecoderClient client(c.seek_where);
-	if (plugin->file_decode != nullptr) {
+	if (plugin->SupportsUri(c.uri)) {
+		try {
+			plugin->UriDecode(client, c.uri);
+		} catch (StopDecoder) {
+		}
+	} else if (plugin->file_decode != nullptr) {
 		try {
 			plugin->FileDecode(client, FromNarrowPath(c.uri));
 		} catch (StopDecoder) {