WIP: Add RTP class. Massive refactoring of NvVideoStream. Rename AvParser to AvDepacketizer. Begin parsing other special NAL sequences.

This commit is contained in:
Cameron Gutman 2013-10-30 20:14:54 -04:00
parent 303ffaa8a3
commit 9e09ca2b7a
5 changed files with 563 additions and 307 deletions

View File

@ -1,29 +1,23 @@
package com.limelight.nvstream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.nvstream.av.AvBufferDescriptor;
import com.limelight.nvstream.av.AvBufferPool;
import com.limelight.nvstream.av.AvDecodeUnit;
import com.limelight.nvstream.av.AvPacket;
import com.limelight.nvstream.av.AvParser;
import com.limelight.nvstream.av.AvDepacketizer;
import com.limelight.nvstream.av.AvRtpPacket;
import jlibrtp.DataFrame;
import jlibrtp.Participant;
import jlibrtp.RTPAppIntf;
import jlibrtp.RTPSession;
import android.media.MediaCodec;
@ -39,15 +33,72 @@ public class NvVideoStream {
private ByteBuffer[] videoDecoderInputBuffers = null;
private MediaCodec videoDecoder;
private LinkedBlockingQueue<AvRtpPacket> packets = new LinkedBlockingQueue<AvRtpPacket>();
private RTPSession session;
private DatagramSocket rtp;
private AvBufferPool pool = new AvBufferPool(1500);
private AvParser parser = new AvParser();
private AvDepacketizer depacketizer = new AvDepacketizer();
private InputStream getFirstFrame(String host) throws UnknownHostException, IOException
private InputStream openFirstFrameInputStream(String host) throws UnknownHostException, IOException
{
Socket s = new Socket(host, FIRST_FRAME_PORT);
return s.getInputStream();
}
private void readFirstFrame(String host) throws IOException
{
byte[] firstFrame = pool.allocate();
System.out.println("VID: Waiting for first frame");
InputStream firstFrameStream = openFirstFrameInputStream(host);
int offset = 0;
for (;;)
{
int bytesRead = firstFrameStream.read(firstFrame, offset, firstFrame.length-offset);
if (bytesRead == -1)
break;
offset += bytesRead;
}
System.out.println("VID: First frame read ("+offset+" bytes)");
// FIXME: Investigate: putting these NALs into the data stream
// causes the picture to get messed up
//depacketizer.addInputData(new AvPacket(new AvBufferDescriptor(firstFrame, 0, offset)));
}
public void setupRtpSession(String host) throws SocketException
{
DatagramSocket rtcp;
rtp = new DatagramSocket(RTP_PORT);
rtp.setReceiveBufferSize(2097152);
System.out.println("RECV BUF: "+rtp.getReceiveBufferSize());
System.out.println("SEND BUF: "+rtp.getSendBufferSize());
rtcp = new DatagramSocket(RTCP_PORT);
session = new RTPSession(rtp, rtcp);
session.addParticipant(new Participant(host, RTP_PORT, RTCP_PORT));
}
public void setupDecoders(Surface surface)
{
videoDecoder = MediaCodec.createDecoderByType("video/avc");
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720);
videoDecoder.configure(videoFormat, surface, null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
videoDecoder.start();
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
}
public void startVideoStream(final String host, final Surface surface)
{
@ -55,185 +106,208 @@ public class NvVideoStream {
@Override
public void run() {
// Setup the decoder context
setupDecoders(surface);
byte[] firstFrame = new byte[98];
// Open RTP sockets and start session
try {
System.out.println("VID: Waiting for first frame");
InputStream firstFrameStream = getFirstFrame(host);
int offset = 0;
do
{
offset = firstFrameStream.read(firstFrame, offset, firstFrame.length-offset);
} while (offset != firstFrame.length);
System.out.println("VID: First frame read ");
} catch (UnknownHostException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
return;
} catch (IOException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
return;
}
final DatagramSocket rtp, rtcp;
try {
rtp = new DatagramSocket(RTP_PORT);
rtcp = new DatagramSocket(RTCP_PORT);
setupRtpSession(host);
} catch (SocketException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return;
}
videoDecoder = MediaCodec.createDecoderByType("video/avc");
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720);
videoDecoder.configure(videoFormat, surface, null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
videoDecoder.start();
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
final RTPSession session = new RTPSession(rtp, rtcp);
session.addParticipant(new Participant(host, RTP_PORT, RTCP_PORT));
// Start the receive thread early to avoid missing
// early packets
startReceiveThread();
// Ping thread
new Thread(new Runnable() {
@Override
public void run() {
// PING in ASCII
final byte[] pingPacket = new byte[] {0x50, 0x49, 0x4E, 0x47};
// RTP payload type is 127 (dynamic)
session.payloadType(127);
// Send PING every 100 ms
for (;;)
{
session.sendData(pingPacket);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}).start();
// Start the keepalive ping to keep the stream going
startUdpPingThread();
// Decoder thread
new Thread(new Runnable() {
@Override
public void run() {
// Read the decode units generated from the RTP stream
for (;;)
{
AvDecodeUnit du;
try {
du = parser.getNextDecodeUnit();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
switch (du.getType())
{
case AvDecodeUnit.TYPE_H264:
{
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
if (inputIndex >= 0)
{
ByteBuffer buf = videoDecoderInputBuffers[inputIndex];
// Clear old input data
buf.clear();
// Copy data from our buffer list into the input buffer
for (AvBufferDescriptor desc : du.getBufferList())
{
buf.put(desc.data, desc.offset, desc.length);
// Release the buffer back to the buffer pool
pool.free(desc.data);
}
videoDecoder.queueInputBuffer(inputIndex,
0, du.getDataLength(),
0, 0);
}
}
break;
default:
{
System.out.println("Unknown decode unit type");
}
break;
}
}
}
}).start();
// Start the depacketizer thread to deal with the RTP data
startDepacketizerThread();
// Receive thread
new Thread(new Runnable() {
@Override
public void run() {
DatagramPacket packet = new DatagramPacket(pool.allocate(), 1500);
AvBufferDescriptor desc = new AvBufferDescriptor(null, 0, 0);
for (;;)
{
try {
rtp.receive(packet);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return;
}
desc.length = packet.getLength();
desc.offset = packet.getOffset();
desc.data = packet.getData();
// Skip the RTP header
desc.offset += 12;
desc.length -= 12;
// !!! We no longer own the data buffer at this point !!!
parser.addInputData(new AvPacket(desc));
// Get a new buffer from the buffer pool
packet.setData(pool.allocate(), 0, 1500);
}
}
}).start();
// Start decoding the data we're receiving
startDecoderThread();
// Read the first frame to start the UDP video stream
try {
readFirstFrame(host);
} catch (IOException e2) {
e2.printStackTrace();
return;
}
// Render the frames that are coming out of the decoder
outputDisplayLoop();
}
}).start();
}
private void startDecoderThread()
{
// Decoder thread
new Thread(new Runnable() {
@Override
public void run() {
// Read the decode units generated from the RTP stream
for (;;)
{
BufferInfo info = new BufferInfo();
int outIndex = videoDecoder.dequeueOutputBuffer(info, -1);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
System.out.println("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
System.out.println("Output format changed");
System.out.println("New output Format: " + videoDecoder.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
System.out.println("Try again later");
break;
default:
break;
}
if (outIndex >= 0) {
videoDecoder.releaseOutputBuffer(outIndex, true);
}
AvDecodeUnit du;
try {
du = depacketizer.getNextDecodeUnit();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
switch (du.getType())
{
case AvDecodeUnit.TYPE_H264:
{
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
if (inputIndex >= 0)
{
ByteBuffer buf = videoDecoderInputBuffers[inputIndex];
// Clear old input data
buf.clear();
// Copy data from our buffer list into the input buffer
for (AvBufferDescriptor desc : du.getBufferList())
{
buf.put(desc.data, desc.offset, desc.length);
// Release the buffer back to the buffer pool
pool.free(desc.data);
}
videoDecoder.queueInputBuffer(inputIndex,
0, du.getDataLength(),
0, du.getFlags());
}
}
break;
default:
{
System.out.println("Unknown decode unit type");
}
break;
}
}
}
}).start();
}
private void startDepacketizerThread()
{
// This thread lessens the work on the receive thread
// so it can spend more time waiting for data
new Thread(new Runnable() {
@Override
public void run() {
for (;;)
{
AvRtpPacket packet;
try {
packet = packets.take();
} catch (InterruptedException e) {
e.printStackTrace();
return;
}
// !!! We no longer own the data buffer at this point !!!
depacketizer.addInputData(packet);
}
}
}).start();
}
private void startReceiveThread()
{
// Receive thread
new Thread(new Runnable() {
@Override
public void run() {
DatagramPacket packet = new DatagramPacket(pool.allocate(), 1500);
AvBufferDescriptor desc = new AvBufferDescriptor(null, 0, 0);
for (;;)
{
try {
rtp.receive(packet);
} catch (IOException e) {
e.printStackTrace();
return;
}
desc.length = packet.getLength();
desc.offset = packet.getOffset();
desc.data = packet.getData();
// Give the packet to the depacketizer thread
packets.add(new AvRtpPacket(desc));
// Get a new buffer from the buffer pool
packet.setData(pool.allocate(), 0, 1500);
}
}
}).start();
}
private void startUdpPingThread()
{
// Ping thread
new Thread(new Runnable() {
@Override
public void run() {
// PING in ASCII
final byte[] pingPacket = new byte[] {0x50, 0x49, 0x4E, 0x47};
// RTP payload type is 127 (dynamic)
session.payloadType(127);
// Send PING every 100 ms
for (;;)
{
session.sendData(pingPacket);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}).start();
}
private void outputDisplayLoop()
{
for (;;)
{
BufferInfo info = new BufferInfo();
int outIndex = videoDecoder.dequeueOutputBuffer(info, -1);
switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
System.out.println("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
System.out.println("Output format changed");
System.out.println("New output Format: " + videoDecoder.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
System.out.println("Try again later");
break;
default:
break;
}
if (outIndex >= 0) {
videoDecoder.releaseOutputBuffer(outIndex, true);
}
}
}
}

View File

@ -9,12 +9,14 @@ public class AvDecodeUnit {
private int type;
private List<AvBufferDescriptor> bufferList;
private int dataLength;
private int flags;
public AvDecodeUnit(int type, List<AvBufferDescriptor> bufferList, int dataLength)
public AvDecodeUnit(int type, List<AvBufferDescriptor> bufferList, int dataLength, int flags)
{
this.type = type;
this.bufferList = bufferList;
this.dataLength = dataLength;
this.flags = flags;
}
public int getType()
@ -22,6 +24,11 @@ public class AvDecodeUnit {
return type;
}
public int getFlags()
{
return flags;
}
public List<AvBufferDescriptor> getBufferList()
{
return bufferList;

View File

@ -0,0 +1,274 @@
package com.limelight.nvstream.av;
import java.util.LinkedList;
import java.util.concurrent.LinkedBlockingQueue;
import android.media.MediaCodec;
public class AvDepacketizer {
// Current NAL state
private LinkedList<AvBufferDescriptor> nalDataChain = null;
private int nalDataLength = 0;
// Sequencing state
private short lastSequenceNumber;
private LinkedBlockingQueue<AvDecodeUnit> decodedUnits = new LinkedBlockingQueue<AvDecodeUnit>();
private void reassembleNal()
{
// This is the start of a new NAL
if (nalDataChain != null && nalDataLength != 0)
{
int flags = 0;
// Check if this is a special NAL unit
AvBufferDescriptor header = nalDataChain.getFirst();
AvBufferDescriptor specialSeq = H264NAL.getSpecialSequenceDescriptor(header);
if (specialSeq != null)
{
// The next byte after the special sequence is the NAL header
byte nalHeader = specialSeq.data[specialSeq.offset+specialSeq.length];
switch (nalHeader)
{
// SPS and PPS
case 0x67:
case 0x68:
System.out.println("Codec config");
flags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
break;
// IDR
case 0x65:
System.out.println("Reference frame");
flags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
break;
// non-IDR frame
case 0x61:
break;
// Unknown type
default:
System.out.printf("Unknown NAL header: %02x %02x %02x %02x %02x\n",
header.data[header.offset], header.data[header.offset+1],
header.data[header.offset+2], header.data[header.offset+3],
header.data[header.offset+4]);
break;
}
}
else
{
System.out.printf("Invalid NAL: %02x %02x %02x %02x %02x\n",
header.data[header.offset], header.data[header.offset+1],
header.data[header.offset+2], header.data[header.offset+3],
header.data[header.offset+4]);
}
// Construct the H264 decode unit
AvDecodeUnit du = new AvDecodeUnit(AvDecodeUnit.TYPE_H264, nalDataChain, nalDataLength, flags);
decodedUnits.add(du);
// Clear old state
nalDataChain = null;
nalDataLength = 0;
}
}
public void addInputData(AvPacket packet)
{
AvBufferDescriptor location = packet.getNewPayloadDescriptor();
int payloadLength = location.length;
boolean terminateNal = false;
while (location.length != 0)
{
// Remember the start of the NAL data in this packet
int start = location.offset;
// Check for the start sequence
AvBufferDescriptor specialSeq = H264NAL.getSpecialSequenceDescriptor(location);
if (specialSeq != null && H264NAL.isStartSequence(specialSeq))
{
// Reassemble any pending NAL
reassembleNal();
// Setup state for the new NAL
nalDataChain = new LinkedList<AvBufferDescriptor>();
nalDataLength = 0;
// Skip the start sequence
location.length -= specialSeq.length;
location.offset += specialSeq.length;
}
// If there's a NAL assembly in progress, add the current data
if (nalDataChain != null)
{
// FIXME: This is a hack to make parsing full packets
// take less time. We assume if they don't start with
// a NAL start sequence, they're full of NAL data
if (payloadLength == 968)
{
location.offset += location.length;
location.length = 0;
}
else
{
//System.out.println("Using slow parsing case");
while (location.length != 0)
{
specialSeq = H264NAL.getSpecialSequenceDescriptor(location);
// Check if this should end the current NAL
//if (specialSeq != null)
if (specialSeq != null && H264NAL.isStartSequence(specialSeq))
{
//terminateNal = true;
break;
}
else
{
// This byte is part of the NAL data
location.offset++;
location.length--;
}
}
}
int endSub;
// If parsing was finished due to reaching new start sequence,
// remove the last byte from the NAL (since it's the first byte of the
// start of the next one)
if (location.length != 0)
{
endSub = 1;
}
else
{
endSub = 0;
}
// Add a buffer descriptor describing the NAL data in this packet
nalDataChain.add(new AvBufferDescriptor(location.data, start, location.offset-start-endSub));
nalDataLength += location.offset-start-endSub;
// Terminate the NAL if asked
if (terminateNal)
{
reassembleNal();
}
}
else
{
// Otherwise, skip the data
location.offset++;
location.length--;
}
}
}
public void addInputData(AvRtpPacket packet)
{
short seq = packet.getSequenceNumber();
// Toss out the current NAL if we receive a packet that is
// out of sequence
if (lastSequenceNumber != 0 &&
lastSequenceNumber + 1 != seq)
{
System.out.println("Received OOS data (expected "+(lastSequenceNumber + 1)+", got "+seq+")");
nalDataChain = null;
nalDataLength = 0;
}
lastSequenceNumber = seq;
// Pass the payload to the non-sequencing parser
AvBufferDescriptor rtpPayload = packet.getNewPayloadDescriptor();
addInputData(new AvPacket(rtpPayload));
}
public AvDecodeUnit getNextDecodeUnit() throws InterruptedException
{
return decodedUnits.take();
}
}
class H264NAL {
// This assume's that the buffer passed in is already a special sequence
public static boolean isStartSequence(AvBufferDescriptor specialSeq)
{
if (/*specialSeq.length != 3 && */specialSeq.length != 4)
return false;
// The start sequence is 00 00 01 or 00 00 00 01
return (specialSeq.data[specialSeq.offset+specialSeq.length-1] == 0x01);
}
// Returns a buffer descriptor describing the start sequence
public static AvBufferDescriptor getSpecialSequenceDescriptor(AvBufferDescriptor buffer)
{
// NAL start sequence is 00 00 00 01 or 00 00 01
if (buffer.length < 3)
return null;
// 00 00 is magic
if (buffer.data[buffer.offset] == 0x00 &&
buffer.data[buffer.offset+1] == 0x00)
{
// Another 00 could be the end of the special sequence
// 00 00 00 or the middle of 00 00 00 01
if (buffer.data[buffer.offset+2] == 0x00)
{
if (buffer.length >= 4 &&
buffer.data[buffer.offset+3] == 0x01)
{
// It's the AVC start sequence 00 00 00 01
return new AvBufferDescriptor(buffer.data, buffer.offset, 4);
}
else
{
// It's 00 00 00
return new AvBufferDescriptor(buffer.data, buffer.offset, 3);
}
}
else if (buffer.data[buffer.offset+2] == 0x01 ||
buffer.data[buffer.offset+2] == 0x02)
{
// These are easy: 00 00 01 or 00 00 02
return new AvBufferDescriptor(buffer.data, buffer.offset, 3);
}
else if (buffer.data[buffer.offset+2] == 0x03)
{
// 00 00 03 is special because it's a subsequence of the
// NAL wrapping substitute for 00 00 00, 00 00 01, 00 00 02,
// or 00 00 03 in the RBSP sequence. We need to check the next
// byte to see whether it's 00, 01, 02, or 03 (a valid RBSP substitution)
// or whether it's something else
if (buffer.length < 4)
return null;
if (buffer.data[buffer.offset+3] >= 0x00 &&
buffer.data[buffer.offset+3] <= 0x03)
{
// It's not really a special sequence after all
return null;
}
else
{
// It's not a standard replacement so it's a special sequence
return new AvBufferDescriptor(buffer.data, buffer.offset, 3);
}
}
}
return null;
}
}

View File

@ -1,131 +0,0 @@
package com.limelight.nvstream.av;
import java.util.LinkedList;
import java.util.concurrent.LinkedBlockingQueue;
public class AvParser {
// Current NAL state
private LinkedList<AvBufferDescriptor> nalDataChain;
private int nalDataLength;
private LinkedBlockingQueue<AvDecodeUnit> decodedUnits = new LinkedBlockingQueue<AvDecodeUnit>();
private void reassembleNal()
{
// This is the start of a new NAL
if (nalDataChain != null && nalDataLength != 0)
{
// Construct the H264 decode unit
AvDecodeUnit du = new AvDecodeUnit(AvDecodeUnit.TYPE_H264, nalDataChain, nalDataLength);
decodedUnits.add(du);
// Clear old state
nalDataChain = null;
nalDataLength = 0;
}
}
public void addInputData(AvPacket packet)
{
// This payload buffer descriptor belongs to us
AvBufferDescriptor location = packet.getNewPayloadDescriptor();
int payloadLength = location.length;
while (location.length != 0)
{
// Remember the start of the NAL data in this packet
int start = location.offset;
// Check for the start sequence
if (H264NAL.hasStartSequence(location))
{
// Reassemble any pending NAL
reassembleNal();
// Setup state for the new NAL
nalDataChain = new LinkedList<AvBufferDescriptor>();
nalDataLength = 0;
// Skip the start sequence
location.length -= 4;
location.offset += 4;
}
// If there's a NAL assembly in progress, add the current data
if (nalDataChain != null)
{
// FIXME: This is a hack to make parsing full packets
// take less time. We assume if they don't start with
// a NAL start sequence, they're full of NAL data
if (payloadLength == 968)
{
location.offset += location.length;
location.length = 0;
}
else
{
System.out.println("Using slow parsing case");
while (location.length != 0)
{
// Check if this should end the current NAL
if (H264NAL.hasStartSequence(location))
{
break;
}
else
{
// This byte is part of the NAL data
location.offset++;
location.length--;
}
}
}
// Add a buffer descriptor describing the NAL data in this packet
nalDataChain.add(new AvBufferDescriptor(location.data, start, location.offset-start));
nalDataLength += location.offset-start;
}
else
{
// Otherwise, skip the data
location.offset++;
location.length--;
}
}
}
public AvDecodeUnit getNextDecodeUnit() throws InterruptedException
{
return decodedUnits.take();
}
}
class H264NAL {
public static boolean shouldTerminateNal(AvBufferDescriptor buffer)
{
if (buffer.length < 4)
return false;
if (buffer.data[buffer.offset] != 0x00 ||
buffer.data[buffer.offset+1] != 0x00 ||
buffer.data[buffer.offset+2] != 0x00)
{
return false;
}
return true;
}
public static boolean hasStartSequence(AvBufferDescriptor buffer)
{
// NAL start sequence is 00 00 00 01
if (!shouldTerminateNal(buffer))
return false;
if (buffer.data[buffer.offset+3] != 0x01)
return false;
return true;
}
}

View File

@ -0,0 +1,32 @@
package com.limelight.nvstream.av;
import java.nio.ByteBuffer;
public class AvRtpPacket {
private short seqNum;
private AvBufferDescriptor buffer;
public AvRtpPacket(AvBufferDescriptor buffer)
{
this.buffer = new AvBufferDescriptor(buffer);
ByteBuffer bb = ByteBuffer.wrap(buffer.data, buffer.offset, buffer.length);
// Discard the first couple of bytes
bb.getShort();
// Get the sequence number
seqNum = bb.getShort();
}
public short getSequenceNumber()
{
return seqNum;
}
public AvBufferDescriptor getNewPayloadDescriptor()
{
return new AvBufferDescriptor(buffer.data, buffer.offset+12, buffer.length-12);
}
}