UPnP ATSC Live Video Streaming

2009-04-30 16:15

This article will guide you through the process of setting up live video streaming from a DVB (ATSC) capture card in Linux to a DLNA UPnP Media Renderer that supports MPEG-2 Transport Streams (such as a PS3).

What you need

Mediatomb
Mediatomb is a DLNA/UPnP media server for Linux. You'll want to set up mediatomb to play regular files to your MediaRenderer before attempting the configuration in this guide. It helps to know how the javascript import and transcoding configuration in Mediatomb works, but this guide should be adequate as long as you don't need to do anything fancy.
DVBStreamer
This is the tool that directly talks to your DVB capture card and streams the video. This would be all that you need if your MediaRenderer understands how to listen to a UDP stream. The can't do this (or I couldn't figure out how to do this) so we need one more tool...
NetCat
The swiss-army knife of networking, this tool allows us to take the UDP stream that DVBStreamer gets us and pipe it through a FIFO with a simple script.

Configuring Mediatomb

The tricky bit to get streaming working correctly is getting Mediatomb to do the hard work of talking to the DLNA devices — in a language they understand.

You'll want to set up Mediatomb and get it talking to your UPnP media renderer before trying to do this; you can try to do it all at once, but don't say I didn't warn you.

Firstly, you need to use an import script rather than the built-in import provided by mediatomb for scanning files. The XML config for this will look something like the following:

<scripting script-charset="UTF-8">
    <common-script>/usr/share/mediatomb/js/common.js</common-script>
    <playlist-script>/usr/share/mediatomb/js/playlists.js</playlist-script>
    <virtual-layout type="js">
      <import-script>/etc/mediatomb/import.js</import-script>
    </virtual-layout>
</scripting>

This config goes in the "<import>" tag of the "config.xml" file.

You also need to add a map from a file extension to a mimetype, I used ".live" -> "video/x-dvb-stream", but you could use whatever you like, the mimetype doesn't escape mediatomb, it's just used so we can tell mediatomb to "transcode" the files into something else. Add the following to the "<extension-mimetype>" tag:

<map from="live" to="video/x-dvb-stream"/>

Finally, you need to add a transcoding profile for the "video/x-dvb-stream" mimetype to transcode it into a "video/mpeg" stream. The "<transcoding>" tag should look something like the following:

<transcoding enabled="yes">
  <mimetype-profile-mappings>
      <transcode mimetype="video/x-dvb-stream" using="dvb-stream"/>
  </mimetype-profile-mappings>
  <profiles>
      <profile name="dvb-stream" enabled="yes" type="external">
          <mimetype>video/mpeg</mimetype>
          <accept-url>no</accept-url>
          <first-resource>yes</first-resource>
          <hide-original-resource>yes</hide-original-resource>
          <agent command="/path/to/bin/dvbstream-live" arguments="%in %out"/>
          <buffer size="524288" fill-size="262144" chunk-size="1024"/>
      </profile>
  </profiles>
</transcoding>

Don't worry about the "dvbstream-live" script just yet, we need to get DVBStreamer set up first.

The final piece to the puzzle is the "import.js" script. This is where having some knowledge of javascript and what you want to do helps quite a bit. You should copy the default import.js script from wherever it's installed, and then add the following code:

function videoIsLiveChannel(obj) {
    var pattern = /(.*)\.live/;
    var match = pattern.exec(obj.title);

    if( match ) {
        var container_chain = new Array('video', 'television' );

        obj.title = match[1];

        print("Parsed channel info: " + obj.title);
        addCdsObject(obj, createContainerChain(container_chain));
        return true;
    } else {
        return false;
    }
}

You also need to add the following to the top of the "addVideo" function:

if(videoIsLiveChannel(obj)) {
    return;
}

This tells the import script that if the file to be imported is a ".live" file, we want to add it to the "video/television" section of the UPnP media library, sans ".live" ending. Strictly speaking, adding this to the import script is not necessary, but it makes the listings look a bit nicer.

Setting up DVBStreamer

DVBStreamer is a simple tool that reads from a DVB capture card and can stream (and filter) the broadcast to a "udp://" url. To get DVBStreamer up and running you will need to already have a "channels.conf" file, generated by a program like "dvbscan". Then simply run the "setupdvbstreamer" command with the appropriate options to get dvbstreamer working.

Use the DVBStreamer Wiki to look up how to do some basic tests, stream some video to a file, or use mplayer to watch a UDP stream; essentially, you want to verify that DVBStreamer is working correctly and doing what you expect.

To stream using the scripts below you need to run DVBStreamer as a daemon and set to stream to "udp://localhost:12346". Create a file "dvbstreamer.startup" with a single line:

System Message: ERROR/3 (<string>, line 145)

Cannot find pygments lexer for language "none"

.. sourcecode:: none

    setmrl udp://localhost:12346

Start DVBStreamer like this:

dvbstreamer -f dvbstreamer.startup -d

Now all you need to do is get the scripts to do the "transcoding" and create some ".live" files for your television channels.

The "transcoding" script(s)

There are two scripts that are used by the streaming system, one changes the channel in DVBStreamer so the correct video is streamed, the other pushes the stream from the UDP stream to a file (FIFO) which mediatomb provides as an argument.

Here's the first, the channel changing script. It requires python >=2.5 (and <3.0). It expects the path to a ".live" file as it's first (and only) argument, and connects to the DVBStreamer instance (using the default authentication credentials) to select the DVBStreamer service corresponding to the name of the file. e.g. "/home/media/21-1 WXXI-HD.live" would select the "21-1 WXXI-HD" DVBStreamer service.

#!/usr/bin/env python
from __future__ import with_statement

import sys
import socket
from contextlib import closing
from os import path

class DVBStreamerException(Exception):
    pass

def get_response(streamer):
    resp = []
    while True:
        try:
            resp.append(streamer.recv(4096))
        except socket.error, e:
            if resp and e.args[0] == 11: # temp unavail
                break
            elif e.args[0] == 11:
                pass
            else:
                raise
    if not resp:
        raise DVBStreamerException("Connection closed?")
    resp = ''.join(resp).splitlines()
    if  not resp[-1].startswith('DVBStreamer'):
        return resp + get_response(streamer)

    code = resp[-1].rsplit('/', 1)[-1]
    if int(code.split()[0]) != 0:
        raise DVBStreamerException(resp)
    else:
        return resp[:-1]

def dvbstreamer_cmd(cmd, auth=False, adapter=0):
    with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
        sock.connect(('localhost', 54197+adapter))
        sock.setblocking(0)
        #print "connected, waiting for ready...",
        get_response(sock)
        #print 'got ready: '
        if auth:
            #print 'sending auth...',
            sock.send("auth dvbstreamer control\n")
            get_response(sock)
            #print 'auth success'
        print 'sending cmd: ', cmd.strip()
        sock.send(cmd)
        return get_response(sock)

def main():
    #l = open('/tmp/channel.log', 'w')
    #print >>l, sys.argv
    channel = sys.argv[1]
    if channel.endswith(".live"):
        channel = path.splitext(path.basename(channel))[0]
    #print >>l, channel
    dvbstreamer_cmd("select %s\n" % channel, auth=True)

    #l.close()

if __name__=='__main__':
    main()

Here's the (much simpler) script that calls the above script and hands off to netcat to push the stream to the FIFO.

#!/bin/bash
/path/to/dvbstream-channel "$1"
exec nc -l -u -p 12346 localhost > "$2"

Put these two scripts someplace you can remember and ensure they can be executed by the user running mediatomb.

Wrap-Up

That should be all there is to it, you can download a tarball containing the scripts and config snippets in the article to make life easier.