Saturday, February 21, 2009

Code : Support streaming audio to the media application


The media application on the BlackBerry device is designed to read from the local file system. To stream data from a remote resource to the media application, you must buffer the resource and control the media application's data reads. The best method to accomplish this is to create a custom DataSource and SourceStream implementation that will provide data to the media application.

Buffering data

The following example opens a connection to a remote audio file and buffers an initial amount of data before starting playback. Subsequent read requests from the media application are controlled by limiting the amount of data returned and blocking the read request when the amount of available data is low. When enough data is available, the read request is resumed.

The example also includes support for saving a downloaded audio file to a microSD card and playing back a downloaded file from the microSD card.

Buffering in the SourceStream is done using SharedInputStreams. One thread reads from a connection to the resource, tracking how much data has been read into the stream. The following code shows this thread:

private class ConnectionThread extends Thread {
public void run() {
try {
byte[] data = new byte[READ_CHUNK];
int len = 0;
updateLoadStatus(_resources.getString(

BufferedPlaybackResource.LOAD_BUFFERING));
while (-1 != (len = readAhead.read(data))) {
totalRead += len;
updateLoadStatus(totalRead + _resources.getString(BufferedPlaybackResource.LOAD_UNITS));
if (!bufferingComplete && totalRead > getStartBuffer()) {
bufferingComplete = true;
System.out.println("Initial Buffering Complete");
updateLoadStatus(_resources.getString(

BufferedPlaybackResource.LOAD_BUFFER_COMPLETE));
}
if (_stop) {
return;
}
}
System.out.println("Downloading Complete");
updateLoadStatus(_resources.getString(

BufferedPlaybackResource.LOAD_DONE) + totalRead + _resources.getString(BufferedPlaybackResource.LOAD_UNITS));
System.out.println("Total Read: " + totalRead);
if (totalRead != s.getLength()) {
System.err.println("* Unable to Download entire file *");
}
downloadComplete = true;
// Write download to the local file
readAhead.setCurrentPosition(0);
while (-1 != (len = readAhead.read(data))) {
saveStream.write(data);
}
} catch (Exception e) {
System.err.println(e.toString());
}
}
}

Notice that the application waits until the whole file has been downloaded before saving it to local storage. Writing to storage while downloading the file may slow down the download process. After reading the data, the thread makes the data available to the SharedInputStream.

Reading data

Read requests from the media application are handled by the SourceStream provided by the DataSource. Internally, this SourceStream implementation uses a SharedInputStream based on the stream being read by the thread in the prior example.

The available() method of the SharedInputStream determines how far ahead the stream has been read. When the media application has caught up to the download, the application can block further read requests from the media application until the download is far enough ahead. The goal is to support playback as soon as possible and not stop unless necessary. If stopping is required, try to minimize it by stopping long enough to buffer additional data for playback.

There are four parameters used in the DataSource to define the buffering scheme:

  1. Initial buffer - the amount downloaded before playback starts
  2. Pause mark - defines when reading should be blocked because too little data is available
  3. Restart mark - defines when reading should be allowed again
  4. Limit - defines the amount of data returned at a time

The following example allows these settings to be manually adjusted, while in practice they could be set to correspond to the audio being streamed, and/or automatically adjusted to match network conditions.

The following example shows the read method of the StreamingSource implementation:

public int read(byte[] b, int off, int len) throws IOException {
System.out.println("Read Request for: " + len + " bytes");
// limit bytes read to our readLimit.
int readLength = len;
if (readLength > getReadLimit()) {
readLength = getReadLimit();
}
int available;
boolean restart_pause = false;
for (;;) {
available = _baseSharedStream.available();
if (downloadComplete) {
// Ignore all restrictions if downloading is complete
System.out.println("Complete, Reading: " + len + " - Available: " + available);
return _baseSharedStream.read(b, off, len);
} else if (bufferingComplete) {
if (restart_pause && available > getRestartBytes()) {
// start up again
System.out.println("Restarting - Available: " + available);
updatePlayStatus(_resources.getString(

BufferedPlaybackResource.PLAY_RESTART) + available + _resources.getString(BufferedPlaybackResource.LOAD_UNITS));
restart_pause = false;
return _baseSharedStream.read(b, off, readLength);
} else if (!restart_pause && (available > getPauseBytes() || available > readLength)) {
// we have what is needed
if (available < getPauseBytes()) {
// read this time, but set the pause
restart_pause = true;
updatePlayStatus(_resources.getString(

BufferedPlaybackResource.PLAY_PAUSING) + available + _resources.getString(BufferedPlaybackResource.LOAD_UNITS));
}
System.out.println("Reading: " + readLength + " - Available: " + available);
return _baseSharedStream.read(b, off, readLength);
} else if (!restart_pause) {
// Set pause until loaded enough to restart
restart_pause = true;
updatePlayStatus(_resources.getString(

BufferedPlaybackResource.PLAY_PAUSING) + available + _resources.getString(BufferedPlaybackResource.LOAD_UNITS));
}
} else {
try {
Thread.sleep(100);
} catch (Exception e) {
System.err.println(e.getMessage());
}
}
}
}

The DataSource determines where the data is loaded from during the connect() method. It first opens a connection to the remote resource to get the filename and length and then opens a connection to the local file system. If a file of the same name doesn't exist, or is smaller than the remote file, it is replaced by downloading the file again. Otherwise, the local resource is passed as the resource stream to be read. The details are as follows:

public void connect() throws IOException {
System.out.println("Loading: " + getLocator());
s = (ContentConnection) Connector.open(getLocator(), Connector.READ);
System.out.println("Size: " + s.getLength());
// Open save file location
int filenameStart = getLocator().lastIndexOf('/');
int paramStart = getLocator().indexOf(';');
if (paramStart < 0) {
paramStart = getLocator().length();
}
String filename = getLocator().substring(filenameStart, paramStart);
System.out.println("Filename: " + filename);
saveFile = (FileConnection) Connector.open("file:///SDCard/blackberry/music" + filename, Connector.READ_WRITE);
if (!saveFile.exists()) {
saveFile.create();
}
saveFile.setReadable(true);
SharedInputStream fileStream = SharedInputStream.getSharedInputStream(

saveFile.openInputStream());
fileStream.setCurrentPosition(0);
if (saveFile.fileSize() < s.getLength()) {
// didn't get it all before, download again
saveFile.setWritable(true);
saveStream = saveFile.openOutputStream();
readAhead = SharedInputStream.getSharedInputStream(s.openInputStream());
} else {
downloadComplete = true;
readAhead = fileStream;
s.close();
}
if (forcedContentType != null) {
feedToPlayer = new LimitedRateSourceStream(readAhead, forcedContentType);
} else {
feedToPlayer = new LimitedRateSourceStream(readAhead, s.getType());
}
}

Works on :

  • BlackBerry Device Software 4.2
  • BlackBerry Java Development Environment (JDE) 4.2

1 comment:

  1. I got a serious problem with that example, after couple runs of that program _baseSharedStream.available() returns always zero. I tried to restart my pc, then it works fro the first time and then its the same. Any ideas?

    ReplyDelete

Place your comments here...