/* * @(#)StreamSource.java * Created: 2005-04-21 * Version: 2-0-alpha * Copyright (c) 2005-2006, University of Manchester All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. Redistributions in binary * form must reproduce the above copyright notice, this list of conditions and * the following disclaimer in the documentation and/or other materials * provided with the distribution. Neither the name of the University of * Manchester nor the names of its contributors may be used to endorse or * promote products derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package javax.media.protocol.recorded; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.FileInputStream; import java.io.IOException; import java.net.DatagramPacket; import java.nio.channels.FileChannel; import java.util.Collections; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rtspd.RTPHeader; import com.compendium.core.db.DBMovies; /** * A Stream to be played back * * @author Andrew G D Rowley * @version 2-0-alpha */ public class StreamSource { static final Logger log = LoggerFactory.getLogger(DBMovies.class); private static final int RTP_PACKET = 0; // The number of usecs per ms private static final int USECS_PER_MS = 1000; // The number of ms per second private static final int MS_PER_SEC = 1000; // The mask for a short from an int private static final int SHORT_MASK = 0x0000FFFF; // The second last item in an array private static final int SECOND_LAST = 2; private static final String EXCEPTION_MESSAGE = "Exception "; // The amount of delay between packets when paused private static final int PAUSE_DELAY = 1000; // The size of an IP address in bytes private static final int IP_ADDRESS_SIZE = 4; // The minimum time before a packet is scheduled private static final long MIN_DELAY = 10; // The maximum time before a packet is dropped private static final long MAX_NEG_DELAY = -500; // The log file private static Log logger = LogFactory.getLog(StreamSource.class.getName()); // The last timestamp seen private long lastPacketTimestamp = 0; // The current packet timestamp private long packetTimestamp = 0; // The current packet group to send private Vector<DatagramPacket> packets = new Vector<DatagramPacket>(0); // True if the end of the file has been reached private boolean qEof = false; // True if there has been an error private boolean qError = false; // True if an RTP packet was found in the file private boolean qFoundRtpPacket = false; // The data channel of the stream file private DataInputStream streamFile = null; // The control channel of the stream file private FileChannel streamFileControl = null; // The first timestamp in this stream private long startTime = 0; // A timer for sending packets at the right time private Timer timer = new Timer(); // The type of the stream private int type = 0; // The length of the current group of packets private Vector<Integer> lengths = new Vector<Integer>(); // The offset of the current packet from the first packet private Vector<Long> offsets = new Vector<Long>(); // The offset of the first sent packet private long firstOffset = 0; // The time at which all streams should start in clock time private long allStartTime = 0; // True if the first packet has been sent private boolean firstPacketSent = false; // The speed at which packets are played back private double scale = 1.0; // A vector of positions of packets in the stream file private Vector<Long> packetPositions = new Vector<Long>(); // A vector of offsets of packets in the stream file private Vector<Long> packetOffsets = new Vector<Long>(); // The current position in the packetPositions vector private int currentPos = 0; // The number of packets sent private int packetCount = 0; // The number of octets sent private int octetCount = 0; // True if the stream has been terminated private boolean terminated = false; // The lowest sequence number in the current block private int lowestSequence = 0; // The datasource to notify of a new packet private DataSource dataSource = null; // The packet data private byte[][] packetData = new byte[50][4096]; // The format of the streams private int rtpFormat = 0; /** * Sets up the stream to be sent * @param dataSource The datasource that this stream will be used in * @param filename The name of the file * */ public StreamSource(DataSource dataSource, String filename) { this.dataSource = dataSource; // Open a connection to the data and process some meta-info openStream(filename); readHeader(); readIndexFile(filename); readPacket(); } /** * Gets the RTP format * @return The rtp format number */ public int getRtpFormat() { return rtpFormat; } /** * Returns after the first packet has been played * */ public synchronized void waitForFirstPacket() { while (!firstPacketSent) { try { wait(); } catch (InterruptedException e) { logger.error(EXCEPTION_MESSAGE, e); } } } // Notifies a reciever that the first packet has been sent private synchronized void notifyFirstPacket() { firstPacketSent = true; notifyAll(); } /** * Handles the case where the timer ticks * * @param task * The task to handle */ public void handleTimeout(TimerTask task) { if (task instanceof UpdateTimer) { // Time to send the next packet{ sendCurrentAndScheduleNextPacket(); } } /** * Starts playback of the stream * @param scale The scale at which to play (1.0 = normal) * @param seek The position in ms to seek from * */ public void play(double scale, long seek) { // Calculate new start time, so that we're aligned w/other streams in // this session if (timer != null) { timer.cancel(); } timer = new Timer(); lastPacketTimestamp = 0; this.scale = scale; // If the scale is 0, we just need to send RTCP packets to say we are // still here if (scale != 0) { // Seek the stream to the starting position long playoutDelay = 0; if (seek != -1) { streamSeek(seek); } // Get the first packet while (!qFoundRtpPacket && !qEof) { readPacket(); if (qError || qEof) { terminateStream(); return; } } // Calculate the initial delay for the first packet playoutDelay = computePlayoutDelay(); // Play the stream if (playoutDelay > MIN_DELAY) { scheduleTimer(playoutDelay); } else { Thread startThread = new Thread() { public void run() { sendCurrentAndScheduleNextPacket(); } }; startThread.start(); } } } /** * Stops the playback of the stream * */ public void teardown() { terminated = true; cancelTimers(); } // Stops the timers private void cancelTimers() { if (timer != null) { timer.cancel(); } } // Searches through the stream for the first packet to play after the given // time private void streamSeek(long seek) { int offsetPos = 0; firstOffset = seek; offsetPos = Collections.binarySearch(packetOffsets, new Long( seek)); if (offsetPos < 0) { offsetPos = (-1 * offsetPos) + 1; } currentPos = offsetPos; try { if ((currentPos >= (packetOffsets.size() - 1)) && (scale < 0)) { currentPos = packetOffsets.size() - SECOND_LAST; int packetType = 1; while (packetType != 0) { long pos = packetPositions.get(currentPos); streamFileControl.position(pos); streamFile.readShort(); packetType = streamFile.readShort() & SHORT_MASK; currentPos--; } } } catch (IOException e) { logger.error(EXCEPTION_MESSAGE, e); qEof = true; } } // Open the stream file private boolean openStream(String filename) { boolean qSuccess = false; // Open the file for reading if (openStreamFile(filename)) { logger.debug("Stream_Source::openStream: opened stream file"); qSuccess = true; } else { logger.debug("Stream_Source::openStream: failed to open " + "stream file\n"); } return qSuccess; } // Actually open the stream file private boolean openStreamFile(String filename) { boolean qSuccess = true; try { FileInputStream stream = new FileInputStream(filename); streamFile = new DataInputStream(stream); streamFileControl = stream.getChannel(); } catch (IOException e) { log.error("Error...", e); qSuccess = false; } return qSuccess; } // Read the header from the stream file private boolean readHeader() { boolean qSuccess = true; try { // Read the start time of the stream long seconds = (streamFile.readInt() & RTPHeader.UINT_TO_LONG_CONVERT); long uSeconds = (streamFile.readInt() & RTPHeader.UINT_TO_LONG_CONVERT); byte addr[] = new byte[IP_ADDRESS_SIZE]; startTime = (seconds * MS_PER_SEC) + (uSeconds / USECS_PER_MS); // Read the sender of the original stream streamFile.read(addr, 0, IP_ADDRESS_SIZE); streamFile.readUnsignedShort(); } catch (IOException e) { logger.error(EXCEPTION_MESSAGE, e); qSuccess = false; } return qSuccess; } /** * Tells the source to read it's index file in preparation for a play * @param filename The name of the file to read * */ public void readIndexFile(String filename) { try { // Open the index file filename += "_index"; DataInputStream indexFile = new DataInputStream( new BufferedInputStream(new FileInputStream(filename))); try { while (true) { long off = indexFile.readLong(); long pos = indexFile.readLong(); packetOffsets.add(new Long(off)); packetPositions.add(new Long(pos)); } } catch (EOFException e) { indexFile.close(); } } catch (IOException e) { logger.error(EXCEPTION_MESSAGE, e); } } // Read a packet from the stream file private void readPacket() { synchronized (packets) { DatagramPacket packet = null; try { long pos = 0; long lastReadTimestamp = -1; lengths.clear(); packets.clear(); offsets.clear(); qError = false; qFoundRtpPacket = false; qEof = false; // Move into the next position if (currentPos >= packetPositions.size() || currentPos < 0) { qEof = true; return; } pos = packetPositions.get(currentPos); streamFileControl.position(pos); currentPos += Double.valueOf(scale).intValue(); lowestSequence = -1; // Read packets while the timestamps are the same while (!qFoundRtpPacket || (lastReadTimestamp == packetTimestamp)) { // Read packet header long offset = 0; byte[] packetBuffer = packetData[packets.size()]; packet = new DatagramPacket(packetBuffer, packetBuffer.length); int length = streamFile.readShort() & RTPHeader.USHORT_TO_INT_CONVERT; type = streamFile.readShort() & RTPHeader.USHORT_TO_INT_CONVERT; offset = streamFile.readInt() & RTPHeader.UINT_TO_LONG_CONVERT; // calculate packet body size and read it streamFile.readFully(packetBuffer, 0, length); // If this is an RTP packet, set it up to be read if (type == RTP_PACKET) { RTPHeader header = new RTPHeader(packetBuffer, 0, length); rtpFormat = header.getPacketType(); packetTimestamp = header.getTimestamp(); if ((packetTimestamp == lastReadTimestamp) || (lastReadTimestamp == -1)) { int sequence = header.getSequence(); lengths.add(new Integer(length)); packets.add(packet); offsets.add(new Long(offset)); lastReadTimestamp = packetTimestamp; if ((sequence < lowestSequence) || (lowestSequence == -1)) { lowestSequence = sequence; } } qFoundRtpPacket = true; } } } catch (EOFException e) { qEof = true; } catch (IOException e) { logger.error(EXCEPTION_MESSAGE, e); qError = true; } } } // Calculate the delay for the next packet private long computePlayoutDelay() { long timeOffset = 0; long delay = 0; // If the scale is stopped, pause by 5 second if (scale == 0) { return PAUSE_DELAY; } // Get the current offset if (allStartTime == 0) { allStartTime = System.currentTimeMillis(); } timeOffset = System.currentTimeMillis() - allStartTime; timeOffset = (long) (timeOffset * scale); // Calculate the delay before sending the next packet delay = (offsets.get(0) - firstOffset) - timeOffset; delay = (long) (delay / scale); return delay; } // Set up a timer to play the next packet private void scheduleTimer(long playoutDelay) { if (!terminated) { timer.schedule(new UpdateTimer(this), playoutDelay); } } // Send the current packet and prepare the next one private void sendCurrentAndScheduleNextPacket() { if (terminated) { return; } // Send out the packet that's waiting sendPacket(); boolean qTimerScheduled = false; // Execute this loop until we've queued up the next packet while (!qTimerScheduled && !qEof && !qError) { // Read the next packet readPacket(); if (qError) { terminateStream(); // We read some data; look at what we have } else if (!qEof) { if (qFoundRtpPacket) { long playoutDelay = 0; // If this packet is the same as the last, send it now if ((packetTimestamp == lastPacketTimestamp) && (scale != 0)) { sendPacket(); continue; } // Work out the delay for the next packet playoutDelay = computePlayoutDelay(); // Schedule a timer for sending the packet if the delay is // greater than 10ms. This stops the loop. if (playoutDelay > MIN_DELAY) { scheduleTimer(playoutDelay); qTimerScheduled = true; } else if (playoutDelay < MAX_NEG_DELAY) { continue; } else { // send it right away. sendPacket(); } } else { // the data we read was an RTCP packet. Ignore it } } else { // Stream has finished. Clean up terminateStream(); } } } // Sends a packet private void sendPacket() { // Notify that the first packet has been sent if (!firstPacketSent) { notifyFirstPacket(); } // If the packet is an RTP packet, play it if (type == RTP_PACKET) { for (int i = 0; (i < lengths.size()) && !terminated; i++) { int length = lengths.get(i); DatagramPacket packet = packets.get(i); try { // Remember items for next time if (lastPacketTimestamp != packetTimestamp) { lastPacketTimestamp = packetTimestamp; } packet.setLength(length); if (!terminated) { dataSource.handleRTPPacket(packet); } packetCount++; octetCount += length - RTPHeader.SIZE; } catch (Exception e) { logger.error(EXCEPTION_MESSAGE, e); } } } } // Stop the stream private void terminateStream() { cancelTimers(); notifyFirstPacket(); logger.debug("Stream Finished"); } /** * Returns the start time of the source * @return The start time */ public long getStartTime() { return startTime; } /** * The current time being played * @return The current time */ public long getCurrentTime() { if (currentPos >= (packetOffsets.size() - 1)) { currentPos = packetOffsets.size() - SECOND_LAST; } return packetOffsets.get(currentPos); } /** * Gets the length of the stream in milliseconds * @return The length og the stream in milliseconds */ public long getDuration() { return packetOffsets.get(packetOffsets.size() - 1); } }