mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-18 18:42:46 +00:00
WIP: Add RTP class. Massive refactoring of NvVideoStream. Rename AvParser to AvDepacketizer. Begin parsing other special NAL sequences.
This commit is contained in:
parent
303ffaa8a3
commit
9e09ca2b7a
@ -1,29 +1,23 @@
|
|||||||
package com.limelight.nvstream;
|
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.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.DatagramPacket;
|
import java.net.DatagramPacket;
|
||||||
import java.net.DatagramSocket;
|
import java.net.DatagramSocket;
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.nvstream.av.AvBufferDescriptor;
|
import com.limelight.nvstream.av.AvBufferDescriptor;
|
||||||
import com.limelight.nvstream.av.AvBufferPool;
|
import com.limelight.nvstream.av.AvBufferPool;
|
||||||
import com.limelight.nvstream.av.AvDecodeUnit;
|
import com.limelight.nvstream.av.AvDecodeUnit;
|
||||||
import com.limelight.nvstream.av.AvPacket;
|
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.Participant;
|
||||||
import jlibrtp.RTPAppIntf;
|
|
||||||
import jlibrtp.RTPSession;
|
import jlibrtp.RTPSession;
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
@ -39,201 +33,281 @@ public class NvVideoStream {
|
|||||||
private ByteBuffer[] videoDecoderInputBuffers = null;
|
private ByteBuffer[] videoDecoderInputBuffers = null;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
|
|
||||||
|
private LinkedBlockingQueue<AvRtpPacket> packets = new LinkedBlockingQueue<AvRtpPacket>();
|
||||||
|
|
||||||
|
private RTPSession session;
|
||||||
|
private DatagramSocket rtp;
|
||||||
|
|
||||||
private AvBufferPool pool = new AvBufferPool(1500);
|
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);
|
Socket s = new Socket(host, FIRST_FRAME_PORT);
|
||||||
return s.getInputStream();
|
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)
|
public void startVideoStream(final String host, final Surface surface)
|
||||||
{
|
{
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
// Setup the decoder context
|
||||||
|
setupDecoders(surface);
|
||||||
|
|
||||||
byte[] firstFrame = new byte[98];
|
// Open RTP sockets and start session
|
||||||
try {
|
try {
|
||||||
System.out.println("VID: Waiting for first frame");
|
setupRtpSession(host);
|
||||||
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);
|
|
||||||
} catch (SocketException e1) {
|
} catch (SocketException e1) {
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
videoDecoder = MediaCodec.createDecoderByType("video/avc");
|
// Start the receive thread early to avoid missing
|
||||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720);
|
// early packets
|
||||||
|
startReceiveThread();
|
||||||
|
|
||||||
videoDecoder.configure(videoFormat, surface, null, 0);
|
// Start the keepalive ping to keep the stream going
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
startUdpPingThread();
|
||||||
videoDecoder.start();
|
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
|
||||||
|
|
||||||
final RTPSession session = new RTPSession(rtp, rtcp);
|
// Start the depacketizer thread to deal with the RTP data
|
||||||
session.addParticipant(new Participant(host, RTP_PORT, RTCP_PORT));
|
startDepacketizerThread();
|
||||||
|
|
||||||
// Ping thread
|
// Start decoding the data we're receiving
|
||||||
new Thread(new Runnable() {
|
startDecoderThread();
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// PING in ASCII
|
|
||||||
final byte[] pingPacket = new byte[] {0x50, 0x49, 0x4E, 0x47};
|
|
||||||
|
|
||||||
// RTP payload type is 127 (dynamic)
|
// Read the first frame to start the UDP video stream
|
||||||
session.payloadType(127);
|
try {
|
||||||
|
readFirstFrame(host);
|
||||||
|
} catch (IOException e2) {
|
||||||
|
e2.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Send PING every 100 ms
|
// Render the frames that are coming out of the decoder
|
||||||
for (;;)
|
outputDisplayLoop();
|
||||||
{
|
}
|
||||||
session.sendData(pingPacket);
|
}).start();
|
||||||
|
}
|
||||||
try {
|
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
|
private void startDecoderThread()
|
||||||
|
{
|
||||||
|
// Decoder thread
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Read the decode units generated from the RTP stream
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
BufferInfo info = new BufferInfo();
|
AvDecodeUnit du;
|
||||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, -1);
|
try {
|
||||||
switch (outIndex) {
|
du = depacketizer.getNextDecodeUnit();
|
||||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
} catch (InterruptedException e) {
|
||||||
System.out.println("Output buffers changed");
|
e.printStackTrace();
|
||||||
break;
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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();
|
}).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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,14 @@ public class AvDecodeUnit {
|
|||||||
private int type;
|
private int type;
|
||||||
private List<AvBufferDescriptor> bufferList;
|
private List<AvBufferDescriptor> bufferList;
|
||||||
private int dataLength;
|
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.type = type;
|
||||||
this.bufferList = bufferList;
|
this.bufferList = bufferList;
|
||||||
this.dataLength = dataLength;
|
this.dataLength = dataLength;
|
||||||
|
this.flags = flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getType()
|
public int getType()
|
||||||
@ -22,6 +24,11 @@ public class AvDecodeUnit {
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getFlags()
|
||||||
|
{
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
public List<AvBufferDescriptor> getBufferList()
|
public List<AvBufferDescriptor> getBufferList()
|
||||||
{
|
{
|
||||||
return bufferList;
|
return bufferList;
|
||||||
|
274
src/com/limelight/nvstream/av/AvDepacketizer.java
Normal file
274
src/com/limelight/nvstream/av/AvDepacketizer.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
32
src/com/limelight/nvstream/av/AvRtpPacket.java
Normal file
32
src/com/limelight/nvstream/av/AvRtpPacket.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user