/* * @(#)RawBufferParser.java 1.37 02/08/21 * * Copyright (c) 1996-2002 Sun Microsystems, Inc. All rights reserved. */ package com.sun.media.parser; import java.io.IOException; import java.awt.Dimension; import java.util.Vector; import javax.media.*; import javax.media.Buffer; import javax.media.protocol.*; import javax.media.format.*; import com.sun.media.*; import com.sun.media.rtp.Depacketizer; /** * Parser for a raw stream of buffers from a PushBufferDataSource. */ public class RawBufferParser extends RawStreamParser { static final String NAMEBUFFER = "Raw video/audio buffer stream parser"; private boolean started = false; // For comparing formats. static AudioFormat mpegAudio = new AudioFormat(AudioFormat.MPEG_RTP); static VideoFormat mpegVideo = new VideoFormat(VideoFormat.MPEG_RTP); static VideoFormat jpegVideo = new VideoFormat(VideoFormat.JPEG_RTP); static VideoFormat h261Video = new VideoFormat(VideoFormat.H261_RTP); static VideoFormat h263Video = new VideoFormat(VideoFormat.H263_RTP); static VideoFormat h263_1998Video = new VideoFormat(VideoFormat.H263_1998_RTP); public String getName() { return NAMEBUFFER; } public void setSource(DataSource source) throws IOException, IncompatibleSourceException { if (!(source instanceof PushBufferDataSource)) { throw new IncompatibleSourceException("DataSource not supported: " + source); } else { streams = ((PushBufferDataSource) source).getStreams(); } if ( streams == null) { throw new IOException("Got a null stream from the DataSource"); } if (streams.length == 0) { throw new IOException("Got a empty stream array from the DataSource"); } if (!supports(streams)) throw new IncompatibleSourceException("DataSource not supported: " + source); this.source = source; this.streams = streams; } /** * Override this if the Parser has additional requirements * from the PushSourceStream */ protected boolean supports(SourceStream[] streams) { return ( (streams[0] != null) && (streams[0] instanceof PushBufferStream) ); } /** * Opens the plug-in software or hardware component and acquires * necessary resources. If all the needed resources could not be * acquired, it throws a ResourceUnavailableException. Data should not * be passed into the plug-in without first calling this method. */ public void open() { if (tracks != null) return; tracks = new Track[streams.length]; for (int i = 0; i < streams.length; i++) { tracks[i] = new FrameTrack(this, (PushBufferStream)streams[i], 1); } } public void close() { if (source != null) { try { source.stop(); // stop every tracks, so that readFrame() can be released. // close every tracks to unblock the transfer handlers. for (int i = 0; i < tracks.length; i++) { ((FrameTrack)tracks[i]).stop(); ((FrameTrack)tracks[i]).close(); } source.disconnect(); } catch (Exception e) { // Internal error? } source = null; } started = false; } /** */ public Track [] getTracks() { for (int i = 0; i < tracks.length; i++) ((FrameTrack)tracks[i]).parse(); return tracks; } /** * Start the parser. */ public void start() throws IOException { for (int i = 0; i < tracks.length; i++) ((FrameTrack)tracks[i]).start(); source.start(); started = true; } /** * Stop the parser. */ public void stop() { try { source.stop(); // stop each of the tracks, so that readFrame can be released for (int i = 0; i < tracks.length; i++) ((FrameTrack)tracks[i]).stop(); } catch (Exception e) { // Internal errors? } started = false; } /** * Resets the state of the plug-in. Typically at end of media or when media * is repositioned. */ public void reset() { for (int i = 0; i < tracks.length; i++) ((FrameTrack)tracks[i]).reset(); } boolean isRTPFormat(Format fmt) { return fmt != null && fmt.getEncoding() != null && (fmt.getEncoding().endsWith("rtp") || fmt.getEncoding().endsWith("RTP")); } /**************************************************************** * Track class ****************************************************************/ final int [] h261Widths = {176, 352}; final int [] h261Heights = {144, 288}; final int [] h263Widths = {0, 128, 176, 352, 704, 1408,0,0}; final int [] h263Heights = {0, 96, 144, 288, 576, 1152,0,0}; final float MPEGRateTbl[] = { 0.0f, 23.976f, 24.f, 25.f, 29.97f, 30.f, 50.f, 59.94f, 60.f }; public static int[][] MPASampleTbl = { { 22050, 24000, 16000, 0 }, // MPEG 2 { 44100, 48000, 32000, 0 } // MPEG 1 }; class FrameTrack implements Track, BufferTransferHandler { Demultiplexer parser; PushBufferStream pbs; boolean enabled = true; CircularBuffer bufferQ; Format format = null; TrackListener listener; boolean stopped = true; boolean closed = false; boolean keyFrameFound = false; boolean checkDepacketizer = false; Depacketizer depacketizer = null; Object keyFrameLock = new Object(); public FrameTrack(Demultiplexer parser, PushBufferStream pbs, int numOfBufs) { this.pbs = pbs; format = pbs.getFormat(); if (source instanceof com.sun.media.protocol.DelegateDataSource || !isRTPFormat(format)) { keyFrameFound = true; } bufferQ = new CircularBuffer(numOfBufs); pbs.setTransferHandler(this); //System.err.println("Track format is " + format); } public Format getFormat() { return format; } public void setEnabled(boolean t) { if (t) pbs.setTransferHandler(this); else pbs.setTransferHandler(null); enabled = t; } public boolean isEnabled() { return enabled; } public Time getDuration() { return parser.getDuration(); } public Time getStartTime() { return new Time(0); } public void setTrackListener(TrackListener l) { listener = l; } /** * Peek into the data stream to parse the data format. */ public void parse() { try { synchronized (keyFrameLock) { while (!keyFrameFound) keyFrameLock.wait(); } } catch (Exception e) { } } private Depacketizer findDepacketizer(String name, Format input) { Class cls; Object obj; try { cls = BasicPlugIn.getClassForName(name); obj = cls.newInstance(); if (!(obj instanceof Depacketizer)) return null; Depacketizer dpktizer = (Depacketizer)obj; if (dpktizer.setInputFormat(input) == null) return null; dpktizer.open(); return dpktizer; } catch (Exception e) { } catch (Error e) { } return null; } private boolean findKeyFrame(Buffer buf) { if (!checkDepacketizer) { // Check to see if there's a depacketizer associated // with the format. If so, we'll use it for parsing // the input. Vector pnames = PlugInManager.getPlugInList(buf.getFormat(), null, Depacketizer.DEPACKETIZER); if (pnames.size() != 0) { depacketizer = findDepacketizer((String)pnames.elementAt(0), buf.getFormat()); } checkDepacketizer = true; } Format fmt = buf.getFormat(); if (fmt == null) return false; if (fmt.getEncoding() == null) { synchronized (keyFrameLock) { keyFrameFound = true; keyFrameLock.notifyAll(); } return true; } boolean rtn = true; if (jpegVideo.matches(fmt)) rtn = findJPEGKey(buf); else if (h261Video.matches(fmt)) rtn = findH261Key(buf); else if (h263Video.matches(fmt)) rtn = findH263Key(buf); else if (h263_1998Video.matches(fmt)) rtn = findH263_1998Key(buf); else if (mpegVideo.matches(fmt)) rtn = findMPEGKey(buf); else if (mpegAudio.matches(fmt)) rtn = findMPAKey(buf); else if (depacketizer != null) { fmt = depacketizer.parse(buf); if (fmt != null) { // Found the format. We are done with // the depacketizer. format = fmt; buf.setFormat(format); depacketizer.close(); depacketizer = null; } else rtn = false; } if (rtn) { synchronized (keyFrameLock) { keyFrameFound = true; keyFrameLock.notifyAll(); } } return keyFrameFound; } /** * Parse the RTP/JPEG stream * Code taken from com.sun.media.codec.video.jpeg.RTPDePacketizer. */ public boolean findJPEGKey(Buffer b) { if ((b.getFlags() & Buffer.FLAG_RTP_MARKER) == 0) return false; int width, height; byte data[]; data = (byte[])b.getData(); width = (data[b.getOffset() + 6] & 0xff) * 8; height = (data[b.getOffset() + 7] & 0xff) * 8; format = new VideoFormat(VideoFormat.JPEG_RTP, new Dimension(width, height), ((VideoFormat)format).getMaxDataLength(), ((VideoFormat)format).getDataType(), ((VideoFormat)format).getFrameRate()); b.setFormat(format); return true; } /** * Parse the RTP/H261 stream * Code taken from com.sun.media.codec.video.h261.NativeDecoder. */ public boolean findH261Key(Buffer b) { int width, height, payloadLen, offset, skipBytes; byte data[]; if ((data = (byte[])b.getData()) == null) return false; offset = b.getOffset(); // Get to the actual h261 compressed payload skipBytes = 4; if ( (data[offset+skipBytes] != 0) || (data[offset+skipBytes+1] != 1) || ((data[offset+skipBytes+2] & 0xfc) != 0) ) { return false; } int s = (data[offset+skipBytes+3]>>3)&0x01; width = h261Widths[s]; height = h261Heights[s]; format = new VideoFormat(VideoFormat.H261_RTP, new Dimension(width, height), ((VideoFormat)format).getMaxDataLength(), ((VideoFormat)format).getDataType(), ((VideoFormat)format).getFrameRate()); b.setFormat(format); return true; } /** * Parse the RTP/H263 stream * Code taken from com.ibm.media.codec.video.h263.JavaDecoder. */ public boolean findH263Key(Buffer b) { int width, height, payloadLen, offset; byte data[]; if ((data = (byte[])b.getData()) == null) return false; payloadLen = getH263PayloadHeaderLength(data, b.getOffset()); offset = b.getOffset(); if ((data[offset+payloadLen] != 0) || (data[offset+payloadLen+1] != 0) || ((data[offset+payloadLen+2] & 0xfc) != 0x80)) return false; int s = (data[offset+payloadLen+4] >> 2) & 0x7; width = h263Widths[s]; height = h263Heights[s]; format = new VideoFormat(VideoFormat.H263_RTP, new Dimension(width, height), ((VideoFormat)format).getMaxDataLength(), ((VideoFormat)format).getDataType(), ((VideoFormat)format).getFrameRate()); b.setFormat(format); return true; } int getH263PayloadHeaderLength(byte[] input,int offset) { int l = 0; byte b = input[offset]; if ( (b & 0x80) != 0) { //mode B or C if ((b & 0x40) != 0) //mode C l = 12; else //mode B l = 8; } else { //mode A l = 4; } return l; } /** * Parse the RTP/H263-1998 stream * Code taken from com.sun.media.codec.video.h263.NativeDecoder. */ public boolean findH263_1998Key(Buffer b) { int width, height, payloadLen, offset; byte data[]; int s = -1; int picOffset = -1; if ((data = (byte[])b.getData()) == null) return false; offset = b.getOffset(); // 2 bytes for H263-1998 header + pLen from header payloadLen = 2 + (((data[offset] & 0x01) << 5) | ((data[offset+1] & 0xf8) >> 3)); if ( (data[offset] & 0x02) != 0) { // Video Redundancy present payloadLen++; } picOffset = -1; if (payloadLen > 5) { // Use PIC header in payload header if ( ((data[offset] & 0x02) == 0x02) && ((data[offset+3] & 0xfc) == 0x80)) { picOffset = offset + 3; } else if ((data[offset+2] & 0xfc) == 0x80) { picOffset = offset + 2; } } else if ( ((data[offset] & 0x04) == 0x04) && ((data[offset+payloadLen] & 0xfc) == 0x80)) { picOffset = offset + payloadLen; } if (picOffset < 0) return false; s = (data[picOffset+2] >> 2) & 0x7; if (s == 7) { // Extended PTYPE, picture size is in the extension // if UFEP = 001 if (((data[picOffset+3] >> 1) & 0x07) == 1) { s = ((data[picOffset+3] << 2) & 0x04) | ((data[picOffset+4] >> 6) & 0x03); } else { return false; // picture type not present } } if (s < 0) return false; width = h263Widths[s]; height = h263Heights[s]; format = new VideoFormat(VideoFormat.H263_1998_RTP, new Dimension(width, height), ((VideoFormat)format).getMaxDataLength(), ((VideoFormat)format).getDataType(), ((VideoFormat)format).getFrameRate()); b.setFormat(format); return true; } /** * Parse the RTP/MPEG video stream * Code taken from com.sun.media.codec.video.mpeg.DePacketizer. */ public boolean findMPEGKey(Buffer b) { int width, height; float frameRate; byte data[]; if ((data = (byte[])b.getData()) == null) { return false; } int off = b.getOffset(); if (b.getLength() < 12) { return false; // can't contain MPEG sequence header } if ((data[off+2] & 0x20) != 0x20) { return false; // doesn't contain MPEG sequence header } if (data[off+4] != 0 || data[off+5] != 0 || data[off+6] != 1 || (data[off+7] & 0xff) != 0xb3) { return false; // doesn't start with MPEG sequence header } int frix = (data[off+11] & 0x0f); if (frix == 0 || frix > 8) { return false; // not a valid frame rate } width = ((data[off+8] & 0xff) << 4) | ((data[off+9] & 0xf0) >> 4); height = ((data[off+9] & 0x0f) << 8) | (data[off+10] & 0xff); frameRate = MPEGRateTbl[frix]; format = new VideoFormat(VideoFormat.MPEG_RTP, new Dimension(width, height), ((VideoFormat)format).getMaxDataLength(), ((VideoFormat)format).getDataType(), frameRate); b.setFormat(format); return true; } /** * Parse the RTP/MPEG audio stream * Code taken from com.sun.media.codec.audio.mpa.DePacketizer. */ public boolean findMPAKey(Buffer b) { int channels; double sampleRate; byte data[]; if ((data = (byte[])b.getData()) == null) return false; int off = b.getOffset(); if (b.getLength() < 8) return false; // doesn't contain MPA header if (data[off+2] != 0 || data[off+3] != 0) return false; // frame continuation off += 4; // skip RTP header to get to MPA header if ((data[off] & 0xff) != 0xff || (data[off+1] & 0xf6) <= 0xf0 || (data[off+2] & 0xf0) == 0xf0 || (data[off+2] & 0x0c) == 0x0c || (data[off+3] & 0x03) == 0x02) return false; // doesn't start with a valid MPA header int id = (data[off+1] >> 3) & 1; // MPEG1 or MPEG2 int six = (data[off+2] >> 2) & 3; channels = (((data[off+3] >> 6) & 3) == 3) ? 1 : 2; sampleRate = MPASampleTbl[id][six]; format = new AudioFormat(AudioFormat.MPEG_RTP, sampleRate, 16, channels, AudioFormat.LITTLE_ENDIAN, AudioFormat.SIGNED); b.setFormat(format); return true; } public void stop(){ // we basically need to ensure that readFrame will return // immediately.and also make sure that if it is called in // the stopped state, it returns w/o blocking. synchronized (bufferQ){ stopped = true; bufferQ.notifyAll(); } } public void start(){ // we need to ensure that readFrame is returned to its // original state and does not return w/o blocking. synchronized (bufferQ){ stopped = false; if (source instanceof CaptureDevice) { // Flush the buffer Q. while (bufferQ.canRead()) { bufferQ.read(); bufferQ.readReport(); } } bufferQ.notifyAll(); } } public void close() { // Unblock the transfer handlers. setEnabled(false); synchronized (bufferQ) { closed = true; bufferQ.notifyAll(); } } public void reset() { } public void readFrame(Buffer buffer) { // Retrieve a filled buffer. if (stopped) { buffer.setDiscard(true); buffer.setFormat(format); return; } Buffer filled; synchronized (bufferQ) { while (!bufferQ.canRead()) { try { bufferQ.wait(); if (stopped){ buffer.setDiscard(true); buffer.setFormat(format); return; } } catch (Exception e) {} } filled = bufferQ.read(); } // Copy all the attributes from filled to buffer. Object hdr = buffer.getHeader(); buffer.copy(filled, true); filled.setHeader(hdr); //if ((buffer.getFlags() & Buffer.FLAG_RTP_MARKER) != 0) //System.err.println("RBP: TS: " + buffer.getTimeStamp()); // Update the saved format. format = filled.getFormat(); synchronized (bufferQ) { bufferQ.readReport(); bufferQ.notifyAll(); } } public int mapTimeToFrame(Time t) { return -1; } public Time mapFrameToTime(int frameNumber) { return new Time(0); } public void transferData(PushBufferStream pbs) { // Retrieve an empty buffer for the PSS to write into. Buffer buffer; synchronized (bufferQ) { while (!bufferQ.canWrite() && !closed) { try { bufferQ.wait(); } catch (Exception e) {} } // If source is null, the data source has been closed. if (closed) return; buffer = bufferQ.getEmptyBuffer(); } try { pbs.read(buffer); } catch (IOException e) { buffer.setDiscard(true); } // Until we find the first key frame, we won't // add it to the buffer queue. if (!keyFrameFound && !findKeyFrame(buffer)) { synchronized (bufferQ) { // Discard that buffer. bufferQ.writeReport(); bufferQ.read(); bufferQ.readReport(); } return; } // Put the filled buffer back to the queue for consumption. synchronized (bufferQ) { bufferQ.writeReport(); bufferQ.notifyAll(); } } } }