media server: how to create resource urls?

classic Classic list List threaded Threaded
15 messages Options
Reply | Threaded
Open this post in threaded view
|

media server: how to create resource urls?

bergstr
Hello,

I am hacking together a media server for my android app, based on android MediaStore. I can browse the contents ok, but am now faced with the question of how to create proper resource URIs that can be passed to the control point/renderer and will be handled correctly when invoked from there. Not sure if I missed reading some important docs (not much into reading when I'm in hacking mode), so beat me if I have. Of course, if someone has a helpful hint, I'd much prefer that.

thanks,
chris
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Michael Pujos
First you will need an http server to serve files but you know that.
The good news is that Cling 2.0 uses Jetty which is super powerful and just plain awesome for that purpose.

You will have to devise a scheme for mapping  MediaStore Uris to stream URL paths.

for example if you a have an mp3 audio item mediastore Uri whose path is /external/audio/media/3604
you could generate a stream URL:  http://<ip address>:<port>/mediastore/external/audio/media/3604.mp3

Then you have a Jetty Servlet handle the /mediastore context path, that handle the servlet path (/external/audio/media/3604.mp3), can easily reconstruct mediastore Uri from it, make a query to the ContentResolver to find the real path of the file, and forward actual file serving to  the DefaultServlet.

Another scheme is for example to pass the encoded filename to serve, in the stream URL path:
http://<ip address>:<port>/filesystem/<URL encoded filename>

The first scheme is preferable though as it allow to access all of an item metadata, and you do not have to deal with complex path encoding, which some device do not like (bugs). And there's some security concerns as well.

For <ip address> simply use the ip address of the nework interface which you receive the Browse request.

If you're not familiar with Jetty and servlets, that's the first thing to learn.
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

bergstr
Hi and thanks for your reply,

So I haven't missed out on something obvious. As this lies outside of UPnP, Cling doesnt provide anything...

I was already headed along scheme 2 (registered the DefautServlet into a dedicated context path), but will reconsider.

Thanks alot
chris
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Michael Pujos
A few more recommendations for broad compatibility:

- avoid URL path having special characters percent encoded (eg %20, etc).

- avoid having a query part in URL. Avoid :

http://.../blargh.mp3?param1=foo&param2=bar

- although not strictly required, it is a good idea to put a file extension at the end of the url path that correspond to the content of the stream
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

bergstr
cool. Thanks.
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

bergstr
In reply to this post by Michael Pujos
unfortunately, it looks to me like cling is protecting the jetty instance by all means. I thought I could simply override the AsyncServletStreamServerImpl#init method and register my servlets through the ServletContainerAdapter#registerServlet method, but thats reserved for the UPnP servlet. So I will have to hack my way through to the jetty server proper rather ruthlessly, it seems.. might end up copying and modifying the entire JettyServletContainer. That thing has public static methods, which isn't encouraging, but they seem to be called during tests only.
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Christian Bauer
Administrator
You are not supposed to register your servlet on the default standalone Jetty instance of Cling.

You are supposed to have your own instance of Jetty, with whatever bootstrap/setup/configuration you need, and then let Cling register its servlet on it. In fact, Cling doesn't care that you are running Jetty, it needs a servlet 3.0 container. In your UpnpServiceConfiguration:

    @Override
    public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
        return new org.fourthline.cling.transport.impl.AsyncServletStreamServerImpl(
            new org.fourthline.cling.transport.impl.AsyncServletStreamServerConfigurationImpl(
                [Provide your implementation of ServletContainerAdapter],
                [Provide the TCP port your servlet container is listening on]
            )
        );
    }

The ServletContainerAdapter is well documented.
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

bergstr
yeah, thats what I meant by "copying and modifying the entire JettyServletContainer".

Since we're at it, could you tell me whether there is any reason not to maintain only one ServletContextHandler at the default "/", and register all servlets into their respective sub-paths (e.g., the UPnP servlet would go into "/upnp/*")?

thanks.
chris
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Christian Bauer
Administrator
My servlet context is mine, your is yours. My context doesn't need sessions for example. I have no idea what your context needs.

Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Christian Bauer
Administrator
In reply to this post by bergstr
Also, there will probably be several servlets for a UPnP service at some point, when the bridge is made functional again. All should be under the same context that has nothing to do with any other contexts on that servlet container.

Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

bergstr
understood. Any recommendation on whether to also maintain separate connectors? After all, there are different requirements: the server port should remain the same so that resource URLs are stable, while the client can choose any available port..
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Christian Bauer
Administrator
The addConnector() of the adapter is simply called for every discovered network address. The implications should be clear of either implementing or ignoring it. Whatever Cling thinks the network addresses are should be the same as your HTTP server's addresses or discovery won't work.

Don't know what client ports have to do with server configuration…

Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

bergstr
my question was this:

- the media server listens on a give port for client (renderer) requests for media resources.
- the UPnP client has its own requirements.

it might make sense to also configure the ports separately
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

Christian Bauer
Administrator
That's not necessary, it's all HTTP… but you can configure it any way you like.
Reply | Threaded
Open this post in threaded view
|

Re: media server: how to create resource urls?

helge79
tl;dr at the bottom ;)

Sorry for replying to this old thread, but it better fits in here than starting a new thread. I have the same requirement as the OP and thought about using Jetty because it's already available. So I registered my own configuration based on AndroidUpnpServiceConfiguration and have overridden createStreamServer() to provide my Jetty Instance which is mainly a 1:1 copy of  Cling's JettyServletContainer except the following methods.

    @Override
    synchronized public int addConnector(String host, int port) throws IOException {
        Connector[] connectors = server.getConnectors();
        if (connectors != null) {
            for (Connector connector : connectors) {
                if (connector.getPort() == port && connector.getHost().equals(host)) {
                    log.info("Connector on " + host + ":" + port + " is already existing at " + host + ":" + connector.getLocalPort());
                    return connector.getLocalPort();
                }
            }
        }

        SocketConnector connector = new SocketConnector();
        connector.setHost(host);
        connector.setPort(port);

        // Open immediately so we can get the assigned local port
        connector.open();

        // Only add if open() succeeded
        server.addConnector(connector);

        log.info("add connector on " + host + ":" + connector.getLocalPort());

        return connector.getLocalPort();
    }

    @Override
    synchronized public void registerServlet(String contextPath, Servlet servlet) {
        log.info("Registering servlet " + servlet.getClass() + " under context path: " + contextPath);

        ServletContextHandler servletContextHandler = (ServletContextHandler)server.getHandler();
        if (servletContextHandler == null) {
            servletContextHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
            servletContextHandler.setContextPath("/");
            servletContextHandler.setServletHandler(new ServletHandler());

            server.setHandler(servletContextHandler);
        }

        servletContextHandler.getServletHandler().addServletWithMapping(new ServletHolder(servlet), contextPath + "/*");
    }

In my main activity I create the singleton Jetty instance, get the current host based on an instance of Cling's AndroidNetworkAddressFactory, add the connector, register my servlet and start the server. Afterwards Cling registers its servlet at my Jetty instance. Because I initiated the Jetty instance I know about its host and its port, so I can create the resource URLs in SetAVTransportURI.

    private String host = null;
    private int port = 0;
    [...]
    public void onCreate(Bundle savedInstanceState) {
        [..]
        AndroidNetworkAddressFactory factory = new AndroidNetworkAddressFactory(0);
        if (factory.hasUsableNetwork()) {
            Iterator<InetAddress> addresses = factory.getBindAddresses();
            while (addresses.hasNext()) {
                host = addresses.next().getHostAddress();
                try {
                    port = MyJettyContainer.INSTANCE.addConnector(host, port);
                    break;
                } catch (IOException e) {
                    // TODO: react appropriately
                }
            }
        }

        MyJettyContainer.INSTANCE.registerServlet("/test", new FileServlet());
        MyJettyContainer.INSTANCE.startIfNotRunning();
    }

This is running fine so far. However I am really sure this is not the correct way to do it, because I would not get notified about any changes in network (Wifi on/off, possible change of IP address). From the sources I can see AndroidRouter is detecting network changes and would call addConnector() again. I assume this is working however I haven't found any reliable information if it is allowed to add connectors to a running Jetty instance.

tl;dr

Does Cling provide some kind of hook I can use to detect network changes? Basically I just want to have my servlet run on the same host and port as Cling does. So is the above still valid to let Cling register its handles at my Jetty instance or is there any other proposed way to do it? Or is it even better to use an own HttpServer implementation as done by sash?


Thanks in advance and kind regards,
Helge