The DVB Service Information API

One of the more useful parts of a transport stream is the service information (SI). DVB service information may go far beyond simply describing the structure of the stream. For instance, schedule information, detailed information about the elements of a service, language information and network information is all available.

Using the service information API, an application can access to most of the tables that are carried in a DVB transport stream. The only table that can't be accessed using the service information API is the Application Information Table (AIT). This table is added by MHP for application signalling, and can't be accessed using this API for a mixture of security and functionality reasons. Instead, the application listing and launching API provides access to the contents of the AIT.

The service information API has a single major class, the org.dvb.si.SIDatabase class. This represents the entire database of service information that the receiver has access to on a given network interface. Since a receiver may have more than one network interface, there may be more than one SIDatabase object. The getSIDatabase() method will return references to all of the available databases. An application may then use the various methods offered by the SIDatabase (below) to get information from the various tables in a specific database.

public class SIDatabase {

  public static SIDatabase[] getSIDatabase();

  public SIRequest retrieveSIBouquets();

  public SIRequest retrieveActualSINetwork();
  public SIRequest retrieveSINetworks();

  public SIRequest
    retrieveActualSITransportStream();
  public SIRequest
    retrieveSITransportStreamDescription();

  public SIRequest retrieveActualSIServices();
  public SIRequest retrieveSIServices();
  public SIRequest retrieveSIService();

  public SIRequeszt retrievePMTServices();
  public SIRequest retrievePMTService();

  public SIRequest
    retrievePMTElementaryStreams();

  public SIRequest retrieveSITimeFromTDT();
  public SIRequest retrieveSITimeFromTOT();
}

This is not the full interface, and does not show all the parameters for these methods due to the large size of this class. Given that we will discuss some of the common parameters below, the full description of the SIDatabase class is available in a separate window so that you can use it as a reference when we discuss these arguments.

Since the receiver may cache SI data to improve performance, the application can choose whether it wants the cached data, the most up-to-date data from the stream or simply whichever is availble (e.g. the receiver will check the cache first, then wait for the data from the stream if it's not in the cache). This allows the application to trade off speed for accuracy, and given that service information may be broadcast at a low bit rate, this feature is extremely useful.

One implication of this, however, is that retrieving data from the SI database may take some time, since the receiver may have to wait for the information to be broadcast before it can return it. For this reason, nearly every method in the SIDatabase class is asynchronous.

Let's examine one of these methods. Given the size of the interface, we won't examine every method, because that's what the href="http://www.mhp.org/technical_essen/pdf_and_other_files/tam527r18.zip">javadoc of the MHP specification is for:

  public SIRequest retrieveSIService(
    short retrieveMode,
    Object appData,
    SIRetrievalListener listener,
    org.davic.net.dvb.DvbLocator dvbLocator,
    short[] someDescriptorTags)
    throws SIIllegalArgumentException;

First of all, these methods all throw the same exception - an SIIllegalArgumentException. This is thrown if any of the arguments to the method is not valid.

The retrieveMode parameter indicates where the API should get the data from. This can take one of three values - FROM_CACHE_ONLY (retrieve the data from the cache, or fail if it's not cached), FROM_CACHE_OR_STREAM (retrieve it from the cache, or wait for it to be broadcast in the stream if it's not cached), or FROM_STREAM_ONLY (ignore the cache and always wait for the data to be broadcast in the stream).

The appData parameter is an application-specific object that identifies the request to the application. Since all of the method calls that retrieve data from the SI database are asynchronous, the contents of this parameter are passed back in the event that signals completion of the request. This allows an application to user the same listener for multiple requests while being able to identify which result is associated with which request.

The next paramter, the listener parameter indicates which object should be notified of the completion of this request. By passing this at the time of the request, rather than registering a specific listener separately, the application has much more control over directing the results of SI requests to the appropriate object within the application. It adds an extra parameter to every method call, but generally this overhead is worth the tradeoff.

These three parameters are common to every method in the SIDatabase class that retrieves information from the database. Thus, every request will follow the same basic model:

The application makes a request to the SI database.

The application makes a request to the SI database.

The SI database returns an SIRequest object that uniquely identifies that query.

The SI database returns an SIRequest object that uniquely identifies that query.

A SISuccessfulRetrieveEvent tells the application that the data is ready.

The SISuccessfulRetrieveEvent tells the application that the data is ready.

Calling getResult() on the event returns an iterator containig the results.

Calling getResult() on the event returns an iterator containig the results.

Each result is represented by an object that implements the SIInformation interface.

Each result is represented by an object that implements the SIInformation interface.

Applications can call nextElement() on the iterator to get the next result.

Applications can call nextElement() on the iterator  to get the next result.

  1. The application makes a request to the SIDatabase object for the service information that it wants.
  2. When an application issues a request to the SI database, the method returns an SIRequest object. This uniquely identifies the request that has just been made, and allows the application to cancel the request should it wish to do so using the cancel() method. It also allows the application to determine if the request will be satisfied from the cache, or whether the information will be retrieved from the stream.
  3. When the information is available, the SI database generates an org.dvb.si.SISuccessfulRetrieveEvent and uses this to notify the application that the request has succeeded. This event contains references both to the SIRequest object that was returned by the requesting method and to the object passed in the appData argument to the request. The first of these allows the application to identify exactly which request this result relates to, while the second is more often used to provide a general filtering mechanism for certain types of request.

    This may be used where the application uses the results of one request to issue another request of the same type, but for different information. By using this value, the application can tell whether a given result is for the first request or the second without having to use separate listener objects for the two requests.

  4. Once the application receives this event, it will call the getResult() method. This returns an SIIterator object that contains all the values that have been returned by the request.
  5. This SIIterator will contain zero or more objects which implement the SIInformation interface. These actually represent elements of the SI database - each object will contain the contents of one row from a particular SI table. This interface has various subclasses, corresponding to the various tables that are in the SI database.
  6. To get the next result from the SI query, the applisation can call the nextElement() method on the SIIterator object. This behaves just like every other iterator, and so this will return a null reference when there are no more results.

The possible subclasses of SIInformation that could be returned in the contents of the SIIterator are:

  • SIEvent
  • SINetwork
  • SIBouquet
  • SITransportStream
  • SITransportStreamBAT
  • SITransportStreamDescription
  • SITransportStreamNIT
  • SIService
  • SITime
  • PMTService
  • PMTElementaryStream

Each iterator will contain objects of only one of these classes - no request will return objects of more than one type.

We can never be sure that an SI request will succeed, even if the information should be available. This may be caused by other applications - for instance, the receiver may have tuned to a new transport stream that contains different SI. Alternatively, it may be that an SI table is not being broadcast, or that the information that's being broadcast does not contain the full information. While a number of the DVB SI tables are mandatory and must be broadcast in a DVB service, unfortunately the SI specification fails to say that these tables actually have to contain anything.

We've already seen that the SISuccessfulRetrieveEvent gets generated when an SI database query succeeds. An SILackOfResourcesEvent indicates that a request can't be completed because of resource limitations, while a SIRequestCancelledEvent indicates that the request was cancelled.

The other events - SITableNotFoundEvent, SIObjectNotInTableEvent and SINotInCacheEvent indicate that the requested information couldn't be found. The last of these events is generated only if the request specified that only the cache should be checked (by using FROM_CACHE_ONLY as the retrieval mode, and a query that allowed the database to search the broadcast SI as well as the cached data may return a successful result.

The code example below shows how the SI API gets used in practise. It's not as scary as it looks - most of this code is actually comments. The first part of the example actually performs the request:

// get a reference to an SIDatabase.  Since the receiver
// probably only has one network interface, we will take
// a shortcut and simply use the first one in the array,
// rather than querying every database
SIDatabase[] databases = SIDatabase.getSIDatabase();
SIDatabase database = databases[0];

// Now we issue a request for the data we want.  In this
// example, we want to retrieve the Program Map Table for
// the current service in this case.
//
// The first three arguments are the standard ones
// described above (retrieval mode, application-specific
// request data and the listener to be notified when the
// request completes).
//
// The next argument gives the locator of the service
// that we're requesting the information for (
// as an org.davic.net.dvb.DvbLocator), while the final
// argument is an array of 'short' values that lists the
// descriptors that we're specifically interested in.  We
// leave this null to indicate that we don't care about
// retrieving any descriptors (an array of one element
// with the value -1 would indicate that we want
// information about all descriptors.  Other values are
// also possible)
SIRetrievalListener listener;
listener = new MyListener();
database.retrievePMTService(
SIInformation.FROM_CACHE_OR_STREAM,
  new Integer (1),
  listener,
  current_service_locator,
  null);

Now, we need a listener. This class shows the listener that we're using in the code sample we've just seen:

public class MyListener
  implements org.dvb.si.SIRetrievalListener {

  public void postRetrievalEvent(
    SIRetrievalEvent event) {

    // first, check that the requests actually
    // succeeded.  This request would probably
    // only fail due to resource limitations,
    // since a stream that didn't contain this
    // information would be hopelessly messed up
    if (event instanceof SISuccessfulRetrieveEvent) {

      // now we check the application-specific data to
      // see what  kind of request this is.  The reason
      // why we do this becomes apparent further down in
      // the code, when we issue another request (for a
      // different type of data) that also uses this
      // object as a listener
      Integer appData = (Integer) event.getAppData();

      // Cast the event to the correct class so that we 
      // can access all the methods in it.
      SISuccessfulRetrieveEvent ev;
      ev = (SISuccessfulRetrieveEvent) event;
      
      if (appData.intValue() == 1) {
        // since its value is 1, we've got a result
        // from the original request

        // get the iterator that contains the results of
        // the request
        SIIterator myIterator = ev.getResult();

        // now loop through the iterator, checking every
        // element and using the results as we want to
        while (myIterator.hasMoreElements()) {

          // the information that we care about is
          // returned in a set of PMTService objects
          PMTService service =
            (PMTService) myIterator.nextElement();

          // get the information about the service that
          // is contained in the PMTService object.

          // now get the information for the elementary
          // streams.  This involves issuing another
          // request to the database.  This  time, we
          // don't pass in any application-specific data,
          // and we use the current object as the
          // listener for the request notification.
          //
          // In this case, we don't make the request
          // directly to the SIDatabase.  The PMTService
          // class allows us to issue the request via
          // that class as well, and the method used for
          // that request is simpler and takes fewer
          // arguments.
          //
          // The first three arguments are the same as
          // for all other requests, while the final
          // argument is again an array of 'short'
          // integers describing the descriptors that
          // we're interested in.
          service.retrievePMTElementaryStreams (
            SIInformation.FROM_CACHE_OR_STREAM,
            new Integer(2),
            this,
            null);
        }
        else {
          // here is where we would handle the
          // results from the second request that
          // we issued (for the elementary streams
          // from the service).
        }
      }
    }
  }

  //other methods of the class go here
}

As we can see from this example, we re-use the listener to handle more than one type of request. The application-specific data that gets passed to the request is then used when the request completes to decide what processing is needed to actually get at the data we need. This allows an application to build one class which acts as a listener for all events relating to SI queries.

There's a lot more that we haven't covered here, but the basic approach is the same in all cases.