/* * LimitedRateStreamingSource.java * * Copyright � 1998-2011 Research In Motion Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Note: For the sake of simplicity, this sample application may not leverage * resource bundles and resource strings. However, it is STRONGLY recommended * that application developers make use of the localization features available * within the BlackBerry development platform to ensure a seamless application * experience across a variety of languages and geographies. For more information * on localizing your application, please refer to the BlackBerry Java Development * Environment Development Guide associated with this release. */ package com.rim.samples.device.bufferedplaybackdemo; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.microedition.io.Connector; import javax.microedition.io.ContentConnection; import javax.microedition.io.file.FileConnection; import javax.microedition.media.Control; import javax.microedition.media.protocol.ContentDescriptor; import javax.microedition.media.protocol.DataSource; import javax.microedition.media.protocol.SourceStream; import net.rim.device.api.io.SharedInputStream; import net.rim.device.api.system.Application; import net.rim.device.api.ui.component.TextField; /** * The data source used by the BufferedPlayback's media player. */ public final class LimitedRateStreamingSource extends DataSource { /** The max size to be read from the stream at one time */ private static final int READ_CHUNK = 512; // bytes /** A reference to the field which displays the load status */ private TextField _loadStatusField; /** A reference to the field which displays the player status */ private TextField _playStatusField; /** * The minimum number of bytes that must be buffered before the media file * will begin playing. */ private int _startBuffer = 200000; /** The maximum size (in bytes) of a single read */ private int _readLimit = 32000; /** * The minimum forward byte buffer which must be maintained in order for the * video to keep playing. If the forward buffer falls below this number, the * playback will pause until the buffer increases. */ private int _pauseBytes = 64000; /** * The minimum forward byte buffer required to resume playback after a * pause. */ private int _resumeBytes = 128000; /** The stream connection over which media content is passed */ private ContentConnection _contentConnection; /** An input stream shared between several readers */ private SharedInputStream _readAhead; /** A stream to the buffered resource */ private LimitedRateSourceStream _feedToPlayer; /** The MIME type of the remote media file */ private String _forcedContentType; /** A counter for the total number of buffered bytes */ private volatile int _totalRead; /** A flag used to tell the connection thread to stop */ private volatile boolean _stop; /** * A flag used to indicate that the initial buffering is complete. In other * words, that the current buffer is larger than the defined start buffer * size. */ private volatile boolean _bufferingComplete; /** A flag used to indicate that the remote file download is complete */ private volatile boolean _downloadComplete; /** The thread which retrieves the remote media file */ private ConnectionThread _loaderThread; /** The local save file into which the remote file is written */ private FileConnection _saveFile; /** A stream for the local save file */ private OutputStream _saveStream; /** * Constructor * * @param locator * The locator that describes the DataSource. */ LimitedRateStreamingSource(final String locator) { super(locator); } /** * Open a connection to the locator * * @throws IOException * Thrown if the firewall disallows a connection that is not * btspp or comm or if save file could not be created */ public void connect() throws IOException { // Open the connection to the remote file _contentConnection = (ContentConnection) Connector .open(getLocator(), Connector.READ); // Cache a reference to the locator final String locator = getLocator(); // Report status System.out.println("Loading: " + locator); System.out.println("Size: " + _contentConnection.getLength()); // The name of the remote file begins after the last forward slash final int filenameStart = locator.lastIndexOf('/'); // The file name ends at the first instance of a semicolon int paramStart = locator.indexOf(';'); // If there is no semicolon, the file name ends at the end of the line if (paramStart < 0) { paramStart = locator.length(); } // Extract the file name final String filename = locator.substring(filenameStart, paramStart); System.out.println("Filename: " + filename); // Open a local save file with the same name as the remote file _saveFile = (FileConnection) Connector.open( "file:///SDCard/blackberry/music" + filename, Connector.READ_WRITE); // If the file doesn't already exist, create it if (!_saveFile.exists()) { _saveFile.create(); } // Open the file for writing _saveFile.setReadable(true); // Open a shared input stream to the local save file to // allow many simultaneous readers. final SharedInputStream fileStream = SharedInputStream.getSharedInputStream(_saveFile .openInputStream()); // Begin reading at the beginning of the file fileStream.setCurrentPosition(0); // If the local file is smaller than the remote file... if (_saveFile.fileSize() < _contentConnection.getLength()) { // Did not get the entire file, set the system to try again _saveFile.setWritable(true); // A non-null save stream is used as a flag later to indicate that // the file download was incomplete. _saveStream = _saveFile.openOutputStream(); // Use a new shared input stream for buffered reading _readAhead = SharedInputStream.getSharedInputStream(_contentConnection .openInputStream()); } else { // The download is complete _downloadComplete = true; // We can use the initial input stream to read the buffered media _readAhead = fileStream; // We can close the remote connection _contentConnection.close(); } if (_forcedContentType != null) { // Use the user-defined content type if it is set _feedToPlayer = new LimitedRateSourceStream(_readAhead, _forcedContentType); } else { // Otherwise, use the MIME types of the remote file _feedToPlayer = new LimitedRateSourceStream(_readAhead, _contentConnection .getType()); } } /** * Destroy and close all existing connections */ public void disconnect() { try { if (_saveStream != null) { // Destroy the stream to the local save file _saveStream.close(); _saveStream = null; } // Close the local save file _saveFile.close(); if (_readAhead != null) { // Close the reader stream _readAhead.close(); _readAhead = null; } // Close the remote file connection _contentConnection.close(); // Close the stream to the player _feedToPlayer.close(); } catch (final Exception e) { BufferedPlayback.errorDialog(e.toString()); } } /** * Returns the content type of the remote file * * @return The content type of the remote file */ public String getContentType() { return _feedToPlayer.getContentDescriptor().getContentType(); } /** * Returns a stream to the buffered resource * * @return A stream to the buffered resource */ public SourceStream[] getStreams() { return new SourceStream[] { _feedToPlayer }; } /** * Starts the connection thread used to download the remote file */ public void start() throws IOException { // If the save stream is null, we have already completely downloaded // the file. if (_saveStream != null) { // Open the connection thread to finish downloading the file _loaderThread = new ConnectionThread(); _loaderThread.start(); } } /** * Stop the connection thread */ public void stop() throws IOException { // Set the boolean flag to stop the thread _stop = true; } /** * @see javax.microedition.media.Controllable#getControl(String) */ public Control getControl(final String controlType) { // No implemented Controls return null; } /** * @see javax.microedition.media.Controllable#getControls() */ public Control[] getControls() { // No implemented Controls return null; } /** * Force the lower level stream to a given content type. Must be called * before the connect function in order to work. * * @param contentType * The content type to use. */ void setContentType(final String contentType) { _forcedContentType = contentType; } /** * A stream to the buffered media resource */ private final class LimitedRateSourceStream implements SourceStream { /** A stream to the local copy of the remote resource */ private final SharedInputStream _baseSharedStream; /** Describes the content type of the media file */ private final ContentDescriptor _contentDescriptor; /** * Constructor. Creates a LimitedRateSourceStream from the given * InputStream. * * @param inputStream * The input stream used to create a new reader. * @param contentType * The content type of the remote file. */ LimitedRateSourceStream(final InputStream inputStream, final String contentType) { _baseSharedStream = SharedInputStream.getSharedInputStream(inputStream); _contentDescriptor = new ContentDescriptor(contentType); } /** * Returns the content descriptor for this stream * * @return The content descriptor for this stream */ public ContentDescriptor getContentDescriptor() { return _contentDescriptor; } /** * Returns the length provided by the connection * * @return long The length provided by the connection */ public long getContentLength() { return _contentConnection.getLength(); } /** * Returns the seek type of the stream */ public int getSeekType() { return SEEKABLE_TO_START; } /** * Returns the maximum size (in bytes) of a single read */ public int getTransferSize() { return _readLimit; } /** * Writes bytes from the buffer into a byte array for playback * * @param bytes * The buffer into which the data is read * @param off * The start offset in array b at which the data is written * @param len * The maximum number of bytes to read * @return the total number of bytes read into the buffer, or -1 if * there is no more data because the end of the stream has been * reached. * @throws IOException * Thrown if a read error occurs */ public int read(final byte[] bytes, final int off, final int len) throws IOException { System.out.println("Read Request for: " + len + " bytes"); // Limit bytes read to the readLimit int readLength = len; if (readLength > getReadLimit()) { readLength = getReadLimit(); } // The number of available byes in the buffer int available; // A boolean flag indicating that the thread should pause // until the buffer has increased sufficiently. boolean paused = 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(bytes, off, len); } else if (_bufferingComplete) { if (paused && available > getResumeBytes()) { // If the video is paused due to buffering, but the // number of available byes is sufficiently high, // resume playback of the media. System.out .println("Resuming - Available: " + available); updatePlayStatus("Resuming " + available + " Bytes"); paused = false; return _baseSharedStream.read(bytes, off, readLength); } else if (!paused && (available > getPauseBytes() || available > readLength)) { // We have enough information for this media playback if (available < getPauseBytes()) { // If the buffer is now insufficient, set the // pause flag. paused = true; updatePlayStatus("Pausing " + available + " Bytes"); } System.out.println("Reading: " + readLength + " - Available: " + available); return _baseSharedStream.read(bytes, off, readLength); } else if (!paused) { // Set pause until loaded enough to resume paused = true; updatePlayStatus("Pausing " + available + " Bytes"); } } else { // We are not ready to start yet, try sleeping to allow the // buffer to increase. try { Thread.sleep(500); } catch (final Exception e) { BufferedPlayback .errorDialog("Thread.sleep(long) threw " + e.toString()); } } } } /** * @see javax.microedition.media.protocol.SourceStream#seek(long) */ public long seek(final long where) throws IOException { _baseSharedStream.setCurrentPosition((int) where); return _baseSharedStream.getCurrentPosition(); } /** * @see javax.microedition.media.protocol.SourceStream#tell() */ public long tell() { return _baseSharedStream.getCurrentPosition(); } /** * Close the stream * * @throws IOException * Thrown if the stream could not be closed */ void close() throws IOException { _baseSharedStream.close(); } /** * @see javax.microedition.media.Controllable#getControl(String) */ public Control getControl(final String controlType) { // No implemented controls return null; } /** * @see javax.microedition.media.Controllable#getControls() */ public Control[] getControls() { // No implemented controls return null; } } /** * A thread which downloads the remote file and writes it to the local file */ private final class ConnectionThread extends Thread { /** * Download the remote media file, then write it to the local file. * * @see java.lang.Thread#run() */ public void run() { try { final byte[] data = new byte[READ_CHUNK]; int len = 0; updateLoadStatus("Buffering"); // Until we reach the end of the file while (-1 != (len = _readAhead.read(data))) { _totalRead += len; updateLoadStatus(_totalRead + " Bytes"); if (!_bufferingComplete && _totalRead > getStartBuffer()) { // We have enough of a buffer to begin playback _bufferingComplete = true; System.out.println("Initial Buffering Complete"); updateLoadStatus("Buffering Complete"); } if (_stop) { // Stop reading return; } } System.out.println("Downloading Complete"); updateLoadStatus("Done " + _totalRead + " Bytes"); System.out.println("Total Read: " + _totalRead); // If the downloaded data is not the same size // as the remote file, something is wrong. if (_totalRead != _contentConnection.getLength()) { System.err.println("* Unable to Download entire file *"); } _downloadComplete = true; _readAhead.setCurrentPosition(0); // Write downloaded data to the local file while (-1 != (len = _readAhead.read(data))) { _saveStream.write(data); } } catch (final Exception e) { BufferedPlayback.errorDialog(e.toString()); } } } /** * Gets the minimum forward byte buffer which must be maintained in order * for the video to keep playing. * * @return The pause byte buffer. */ int getPauseBytes() { return _pauseBytes; } /** * Sets the minimum forward buffer which must be maintained in order for the * video to keep playing. * * @param pauseBytes * The new pause byte buffer */ void setPauseBytes(final int pauseBytes) { _pauseBytes = pauseBytes; } /** * Gets the maximum size (in bytes) of a single read * * @return The maximum size (in bytes) of a single read */ int getReadLimit() { return _readLimit; } /** * Sets the maximum size (in bytes) of a single read * * @param readLimit * The new maximum size (in bytes) of a single read */ void setReadLimit(final int readLimit) { _readLimit = readLimit; } /** * Gets the minimum forward byte buffer required to resume playback after a * pause. * * @return The resume byte buffer */ int getResumeBytes() { return _resumeBytes; } /** * Sets the minimum forward byte buffer required to resume playback after a * pause. * * @param resumeBytes * The new resume byte buffer */ void setResumeBytes(final int resumeBytes) { _resumeBytes = resumeBytes; } /** * Gets the minimum number of bytes that must be buffered before the media * file will begin playing. * * @return The start byte buffer */ int getStartBuffer() { return _startBuffer; } /** * Sets the minimum number of bytes that must be buffered before the media * file will begin playing. * * @param startBuffer * The new start byte buffer */ void setStartBuffer(final int startBuffer) { _startBuffer = startBuffer; } /** * Gets a reference to the text field where load status updates are written. * * @return The load status text field */ TextField getLoadStatus() { return _loadStatusField; } /** * Sets a reference to the text field where load status updates are written. * * @param loadStatus * The new load status text field */ void setLoadStatus(final TextField loadStatus) { _loadStatusField = loadStatus; } /** * Gets a reference to the text field where player status updates are * written. * * @return The player status text field */ TextField getPlayStatus() { return _playStatusField; } /** * Sets a reference to the text field where player status updates are * written. * * @param playStatus * The new player status text field */ void setPlayStatus(final TextField playStatus) { _playStatusField = playStatus; } /** * Update the player status field * * @param text * The new message to be displayed */ void updatePlayStatus(final String text) { updateStatus(getPlayStatus(), text); } /** * Update the load status field * * @param text * The new message to be displayed */ void updateLoadStatus(final String text) { updateStatus(getLoadStatus(), text); } /** * Update a given status field * * @param field * The field to be updated * @param text * The message to be displayed in the field */ void updateStatus(final TextField field, final String text) { synchronized (Application.getEventLock()) { field.setText(text); } } }