mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2025-07-18 18:42:46 +00:00
Improved mDNS response
This commit is contained in:
commit
1d5adff0d5
@ -27,6 +27,7 @@
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.limelight.Game"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/title_activity_game"
|
||||
android:parentActivityName="com.limelight.Connection"
|
||||
|
62
LuaScripts/NALParser.lua
Normal file
62
LuaScripts/NALParser.lua
Normal file
@ -0,0 +1,62 @@
|
||||
-- H264 NAL Parser
|
||||
-- Version: 1.0
|
||||
-- Cameron Gutman
|
||||
|
||||
-- NAL header
|
||||
local nal_start = ProtoField.bytes("nal.start", "H264 NAL Start Sequence") -- 4 Byte Start
|
||||
local nal_type = ProtoField.uint8("nal.type", "H264 NAL Type") -- 1 byte NAL type
|
||||
local nal_data = ProtoField.bytes("nal.data", "H264 NAL Data") -- variable byte NAL data
|
||||
|
||||
p_h264raw = Proto("h264raw", "H264 Raw NAL Parser")
|
||||
p_h264raw.fields = {
|
||||
nal_start,
|
||||
nal_type,
|
||||
nal_data
|
||||
}
|
||||
|
||||
function p_h264raw.dissector(buf, pkt, root)
|
||||
pkt.cols.protocol = p_h264raw.name
|
||||
subtree = root:add(p_h264raw, buf(0))
|
||||
|
||||
local i = 0
|
||||
local data_start = -1
|
||||
while i < buf:len do
|
||||
-- Make sure we have a potential start sequence and type
|
||||
if buf:len() - i < 5 then
|
||||
-- We need more data
|
||||
pkt.desegment_len = DESEGMENT_ONE_MORE_SEGMENT
|
||||
pkt.desegment_offset = 0
|
||||
return
|
||||
end
|
||||
|
||||
-- Check for start sequence
|
||||
start = buf(i, 4):uint()
|
||||
if start == 1 then
|
||||
if data_start ~= -1 then
|
||||
-- End the last NAL
|
||||
subtree:add(nal_data, buf(data_start, i-data_start))
|
||||
end
|
||||
-- This is the start of a NAL
|
||||
subtree:add(nal_start, buf(i, 4))
|
||||
i = i + 4
|
||||
-- Next byte is NAL type
|
||||
subtree:add(nal_type, buf(i, 1), buf(i, 1):uint8())
|
||||
i = i + 1
|
||||
-- Data begins here
|
||||
data_start = i
|
||||
else
|
||||
-- This must be a data byte
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function p_h264raw.init()
|
||||
end
|
||||
|
||||
|
||||
local udp_dissector_table = DissectorTable.get("rtp.pt")
|
||||
udp_dissector_table:add(96, p_h264raw)
|
||||
|
||||
|
||||
|
@ -35,8 +35,15 @@ or to a theme attribute in the form "<code>?[<i>package</i>:][<i>type</i>:]<i>na
|
||||
public static final int ic_launcher=0x7f020000;
|
||||
}
|
||||
public static final class id {
|
||||
<<<<<<< HEAD
|
||||
public static final int mDNSResultView=0x7f080000;
|
||||
public static final int surfaceView=0x7f080001;
|
||||
=======
|
||||
public static final int hostTextView=0x7f080002;
|
||||
public static final int pairButton=0x7f080000;
|
||||
public static final int statusButton=0x7f080001;
|
||||
public static final int surfaceView=0x7f080003;
|
||||
>>>>>>> d7062ac2e0b306c42144c84690a3735c5878e11d
|
||||
}
|
||||
public static final class layout {
|
||||
public static final int activity_connection=0x7f030000;
|
||||
|
@ -8,6 +8,15 @@
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context=".Connection" >
|
||||
|
||||
<Button
|
||||
android:id="@+id/pairButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/statusButton"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="Pair with PC"/>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<ListView
|
||||
android:id="@+id/mDNSResultView"
|
||||
android:layout_width="fill_parent"
|
||||
@ -15,5 +24,24 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true" >
|
||||
</ListView>
|
||||
=======
|
||||
<EditText
|
||||
android:id="@+id/hostTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:ems="10"
|
||||
android:hint="IP address of GeForce PC" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/statusButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/hostTextView"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:text="Start Streaming Steam!" />
|
||||
>>>>>>> d7062ac2e0b306c42144c84690a3735c5878e11d
|
||||
|
||||
</RelativeLayout>
|
||||
|
@ -1,10 +1,18 @@
|
||||
package com.limelight;
|
||||
|
||||
import java.io.IOException;
|
||||
<<<<<<< HEAD
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
=======
|
||||
import java.net.SocketException;
|
||||
>>>>>>> d7062ac2e0b306c42144c84690a3735c5878e11d
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.NvHTTP;
|
||||
import com.limelight.nvstream.NvmDNS;
|
||||
|
||||
import android.os.Bundle;
|
||||
@ -13,24 +21,28 @@ import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class Connection extends Activity {
|
||||
private Button statusButton;
|
||||
private Button statusButton, pairButton;
|
||||
private TextView hostText;
|
||||
private SharedPreferences prefs;
|
||||
|
||||
private static final String DEFAULT_HOST = "35.0.113.120";
|
||||
private static final String DEFAULT_HOST = "192.168.1.240";
|
||||
public static final String HOST_KEY = "hostText";
|
||||
|
||||
<<<<<<< HEAD
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
=======
|
||||
>>>>>>> d7062ac2e0b306c42144c84690a3735c5878e11d
|
||||
@Override
|
||||
public void onPause() {
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
@ -65,8 +77,14 @@ public class Connection extends Activity {
|
||||
setContentView(R.layout.activity_connection);
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
// this.statusButton = (Button) findViewById(R.id.statusButton);
|
||||
// this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||
=======
|
||||
this.statusButton = (Button) findViewById(R.id.statusButton);
|
||||
this.pairButton = (Button) findViewById(R.id.pairButton);
|
||||
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||
>>>>>>> d7062ac2e0b306c42144c84690a3735c5878e11d
|
||||
|
||||
//prefs = getPreferences(0);
|
||||
//this.hostText.setText(prefs.getString(Connection.HOST_KEY, Connection.DEFAULT_HOST));
|
||||
@ -78,7 +96,65 @@ public class Connection extends Activity {
|
||||
intent.putExtra("host", Connection.this.hostText.getText().toString());
|
||||
Connection.this.startActivity(intent);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
});*/
|
||||
=======
|
||||
});
|
||||
|
||||
this.pairButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View arg0) {
|
||||
Toast.makeText(Connection.this, "Pairing...", Toast.LENGTH_LONG).show();
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String macAddress;
|
||||
try {
|
||||
macAddress = NvConnection.getMacAddressString();
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (macAddress == null) {
|
||||
System.out.println("Couldn't find a MAC address");
|
||||
return;
|
||||
}
|
||||
|
||||
NvHTTP httpConn = new NvHTTP(hostText.getText().toString(), macAddress);
|
||||
|
||||
String message;
|
||||
try {
|
||||
if (httpConn.getPairState()) {
|
||||
message = "Already paired";
|
||||
}
|
||||
else {
|
||||
int session = httpConn.getSessionId();
|
||||
if (session == 0) {
|
||||
message = "Pairing was declined by the target";
|
||||
}
|
||||
else {
|
||||
message = "Pairing was successful";
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
message = e.getMessage();
|
||||
} catch (XmlPullParserException e) {
|
||||
message = e.getMessage();
|
||||
}
|
||||
|
||||
final String toastMessage = message;
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(Connection.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
>>>>>>> d7062ac2e0b306c42144c84690a3735c5878e11d
|
||||
}
|
||||
|
||||
}
|
||||
|
31
src/com/limelight/nvstream/NvApp.java
Normal file
31
src/com/limelight/nvstream/NvApp.java
Normal file
@ -0,0 +1,31 @@
|
||||
package com.limelight.nvstream;
|
||||
|
||||
public class NvApp {
|
||||
private String appName;
|
||||
private int appId;
|
||||
private boolean isRunning;
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
}
|
||||
|
||||
public void setIsRunning(String isRunning) {
|
||||
this.isRunning = isRunning.equals("1");
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public boolean getIsRunning() {
|
||||
return this.isRunning;
|
||||
}
|
||||
}
|
@ -2,7 +2,11 @@ package com.limelight.nvstream;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -34,6 +38,33 @@ public class NvConnection {
|
||||
this.threadPool = new ThreadPoolExecutor(1, 1, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
|
||||
}
|
||||
|
||||
public static String getMacAddressString() throws SocketException {
|
||||
Enumeration<NetworkInterface> ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||
|
||||
while (ifaceList.hasMoreElements()) {
|
||||
NetworkInterface iface = ifaceList.nextElement();
|
||||
|
||||
/* Look for the first non-loopback interface to use as the MAC address.
|
||||
* We don't require the interface to be up to avoid having to repair when
|
||||
* connecting over different interfaces */
|
||||
if (!iface.isLoopback()) {
|
||||
byte[] macAddress = iface.getHardwareAddress();
|
||||
if (macAddress.length == 6) {
|
||||
StringBuilder addrStr = new StringBuilder();
|
||||
for (int i = 0; i < macAddress.length; i++) {
|
||||
addrStr.append(String.format("%02x", macAddress[i]));
|
||||
if (i != macAddress.length - 1) {
|
||||
addrStr.append(':');
|
||||
}
|
||||
}
|
||||
return addrStr.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
@ -159,7 +190,7 @@ public class NvConnection {
|
||||
|
||||
private void startSteamBigPicture() throws XmlPullParserException, IOException
|
||||
{
|
||||
NvHTTP h = new NvHTTP(host, "b0:ee:45:57:5d:5f");
|
||||
NvHTTP h = new NvHTTP(host, getMacAddressString());
|
||||
|
||||
if (!h.getPairState())
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Stack;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
@ -11,19 +12,18 @@ import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
public class NvHTTP {
|
||||
private String host;
|
||||
private String macAddress;
|
||||
|
||||
public static final int PORT = 47989;
|
||||
public String baseUrl;
|
||||
|
||||
public NvHTTP(String host, String macAddress)
|
||||
{
|
||||
this.host = host;
|
||||
public NvHTTP(String host, String macAddress) {
|
||||
this.macAddress = macAddress;
|
||||
this.baseUrl = "http://" + host + ":" + PORT;
|
||||
}
|
||||
|
||||
private String getXmlString(InputStream in, String attribute) throws XmlPullParserException, IOException
|
||||
{
|
||||
private String getXmlString(InputStream in, String tagname)
|
||||
throws XmlPullParserException, IOException {
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
@ -32,21 +32,19 @@ public class NvHTTP {
|
||||
int eventType = xpp.getEventType();
|
||||
Stack<String> currentTag = new Stack<String>();
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT)
|
||||
{
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case (XmlPullParser.START_TAG):
|
||||
currentTag.push(xpp.getName());
|
||||
for (int i = 0; i < xpp.getAttributeCount(); i++)
|
||||
{
|
||||
if (xpp.getAttributeName(i).equals(attribute))
|
||||
return xpp.getAttributeValue(i);
|
||||
}
|
||||
} else if (eventType == XmlPullParser.END_TAG) {
|
||||
break;
|
||||
case (XmlPullParser.END_TAG):
|
||||
currentTag.pop();
|
||||
} else if (eventType == XmlPullParser.TEXT) {
|
||||
if (currentTag.peek().equals(attribute)) {
|
||||
break;
|
||||
case (XmlPullParser.TEXT):
|
||||
if (currentTag.peek().equals(tagname)) {
|
||||
return xpp.getText();
|
||||
}
|
||||
break;
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
@ -54,43 +52,86 @@ public class NvHTTP {
|
||||
return null;
|
||||
}
|
||||
|
||||
private InputStream openHttpConnection(String url) throws IOException
|
||||
{
|
||||
private InputStream openHttpConnection(String url) throws IOException {
|
||||
return new URL(url).openConnection().getInputStream();
|
||||
}
|
||||
|
||||
public String getAppVersion() throws XmlPullParserException, IOException
|
||||
{
|
||||
InputStream in = openHttpConnection("http://"+host+":"+PORT+"/appversion");
|
||||
public String getAppVersion() throws XmlPullParserException, IOException {
|
||||
InputStream in = openHttpConnection(baseUrl + "/appversion");
|
||||
return getXmlString(in, "appversion");
|
||||
}
|
||||
|
||||
public boolean getPairState() throws IOException, XmlPullParserException
|
||||
{
|
||||
InputStream in = openHttpConnection("http://"+host+":"+PORT+"/pairstate?mac="+macAddress);
|
||||
public boolean getPairState() throws IOException, XmlPullParserException {
|
||||
InputStream in = openHttpConnection(baseUrl + "/pairstate?mac=" + macAddress);
|
||||
String paired = getXmlString(in, "paired");
|
||||
return Integer.valueOf(paired) != 0;
|
||||
}
|
||||
|
||||
public int getSessionId() throws IOException, XmlPullParserException
|
||||
{
|
||||
InputStream in = openHttpConnection("http://"+host+":"+PORT+"/pair?mac="+macAddress+"&devicename=ANDROID");
|
||||
public int getSessionId() throws IOException, XmlPullParserException {
|
||||
/* Pass the model (minus spaces) as the device name */
|
||||
String deviceName = android.os.Build.MODEL;
|
||||
deviceName = deviceName.replace(" ", "");
|
||||
InputStream in = openHttpConnection(baseUrl + "/pair?mac=" + macAddress
|
||||
+ "&devicename=" + deviceName);
|
||||
String sessionId = getXmlString(in, "sessionid");
|
||||
return Integer.valueOf(sessionId);
|
||||
return Integer.parseInt(sessionId);
|
||||
}
|
||||
|
||||
public int getSteamAppId(int sessionId) throws IOException, XmlPullParserException
|
||||
{
|
||||
InputStream in = openHttpConnection("http://"+host+":"+PORT+"/applist?session="+sessionId);
|
||||
String appId = getXmlString(in, "ID");
|
||||
return Integer.valueOf(appId);
|
||||
public int getSteamAppId(int sessionId) throws IOException,
|
||||
XmlPullParserException {
|
||||
LinkedList<NvApp> appList = getAppList(sessionId);
|
||||
for (NvApp app : appList) {
|
||||
if (app.getAppName().equals("Steam")) {
|
||||
return app.getAppId();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public LinkedList<NvApp> getAppList(int sessionId) throws IOException, XmlPullParserException {
|
||||
InputStream in = openHttpConnection(baseUrl + "/applist?session=" + sessionId);
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
|
||||
xpp.setInput(new InputStreamReader(in));
|
||||
int eventType = xpp.getEventType();
|
||||
LinkedList<NvApp> appList = new LinkedList<NvApp>();
|
||||
Stack<String> currentTag = new Stack<String>();
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case (XmlPullParser.START_TAG):
|
||||
currentTag.push(xpp.getName());
|
||||
if (xpp.getName().equals("App")) {
|
||||
appList.addLast(new NvApp());
|
||||
}
|
||||
break;
|
||||
case (XmlPullParser.END_TAG):
|
||||
currentTag.pop();
|
||||
break;
|
||||
case (XmlPullParser.TEXT):
|
||||
NvApp app = appList.getLast();
|
||||
if (currentTag.peek().equals("AppTitle")) {
|
||||
app.setAppName(xpp.getText());
|
||||
} else if (currentTag.peek().equals("ID")) {
|
||||
app.setAppId(xpp.getText());
|
||||
} else if (currentTag.peek().equals("IsRunning")) {
|
||||
app.setIsRunning(xpp.getText());
|
||||
}
|
||||
break;
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
|
||||
// Returns gameSession XML attribute
|
||||
public int launchApp(int sessionId, int appId) throws IOException, XmlPullParserException
|
||||
{
|
||||
InputStream in = openHttpConnection("http://"+host+":"+PORT+"/launch?session="+sessionId+"&appid="+appId);
|
||||
public int launchApp(int sessionId, int appId) throws IOException,
|
||||
XmlPullParserException {
|
||||
InputStream in = openHttpConnection(baseUrl + "/launch?session="
|
||||
+ sessionId + "&appid=" + appId);
|
||||
String gameSession = getXmlString(in, "gamesession");
|
||||
return Integer.valueOf(gameSession);
|
||||
return Integer.parseInt(gameSession);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,5 @@
|
||||
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;
|
||||
@ -12,11 +8,16 @@ 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.AvDepacketizer;
|
||||
import com.limelight.nvstream.av.AvRtpPacket;
|
||||
|
||||
import jlibrtp.DataFrame;
|
||||
import jlibrtp.Participant;
|
||||
import jlibrtp.RTPAppIntf;
|
||||
import jlibrtp.RTPSession;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
@ -24,87 +25,278 @@ import android.media.MediaCodec.BufferInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.view.Surface;
|
||||
|
||||
public class NvVideoStream implements RTPAppIntf {
|
||||
public class NvVideoStream {
|
||||
public static final int RTP_PORT = 47998;
|
||||
public static final int RTCP_PORT = 47999;
|
||||
public static final int FIRST_FRAME_PORT = 47996;
|
||||
|
||||
private static final int FRAME_RATE = 60;
|
||||
private ByteBuffer[] decoderInputBuffers = null;
|
||||
private MediaCodec decoder;
|
||||
private ByteBuffer[] videoDecoderInputBuffers, audioDecoderInputBuffers;
|
||||
private MediaCodec videoDecoder, audioDecoder;
|
||||
|
||||
private int frameIndex = 0;
|
||||
private LinkedBlockingQueue<AvRtpPacket> packets = new LinkedBlockingQueue<AvRtpPacket>();
|
||||
|
||||
private InputStream getFirstFrame(String host) throws UnknownHostException, IOException
|
||||
private RTPSession session;
|
||||
private DatagramSocket rtp;
|
||||
|
||||
private AvBufferPool pool = new AvBufferPool(1500);
|
||||
|
||||
private AvDepacketizer depacketizer = new AvDepacketizer();
|
||||
|
||||
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);
|
||||
|
||||
audioDecoder = MediaCodec.createDecoderByType("audio/mp4a-latm");
|
||||
MediaFormat audioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 48000, 2);
|
||||
|
||||
videoDecoder.configure(videoFormat, surface, null, 0);
|
||||
audioDecoder.configure(audioFormat, null, null, 0);
|
||||
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
videoDecoder.start();
|
||||
audioDecoder.start();
|
||||
|
||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||
audioDecoderInputBuffers = audioDecoder.getInputBuffers();
|
||||
}
|
||||
|
||||
public void startVideoStream(final String host, final Surface surface)
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
decoder = MediaCodec.createDecoderByType("video/avc");
|
||||
MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 1280, 720);
|
||||
// Start the receive thread early to avoid missing
|
||||
// early packets
|
||||
startReceiveThread();
|
||||
|
||||
decoder.configure(mediaFormat, surface, null, 0);
|
||||
decoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
decoder.start();
|
||||
decoderInputBuffers = decoder.getInputBuffers();
|
||||
// Start the keepalive ping to keep the stream going
|
||||
startUdpPingThread();
|
||||
|
||||
int inputIndex = decoder.dequeueInputBuffer(-1);
|
||||
if (inputIndex >= 0)
|
||||
{
|
||||
ByteBuffer buf = decoderInputBuffers[inputIndex];
|
||||
// Start the depacketizer thread to deal with the RTP data
|
||||
startDepacketizerThread();
|
||||
|
||||
buf.clear();
|
||||
buf.put(firstFrame);
|
||||
// Start decoding the data we're receiving
|
||||
startDecoderThread();
|
||||
|
||||
decoder.queueInputBuffer(inputIndex,
|
||||
0, firstFrame.length,
|
||||
0, 0);
|
||||
frameIndex++;
|
||||
// Start playing back audio data
|
||||
startAudioPlaybackThread();
|
||||
|
||||
// Read the first frame to start the UDP video stream
|
||||
try {
|
||||
readFirstFrame(host);
|
||||
} catch (IOException e2) {
|
||||
e2.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
final RTPSession session = new RTPSession(rtp, rtcp);
|
||||
session.addParticipant(new Participant(host, RTP_PORT, RTCP_PORT));
|
||||
//session.RTPSessionRegister(NvVideoStream.this, null, null);
|
||||
// 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 (;;)
|
||||
{
|
||||
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;
|
||||
|
||||
case AvDecodeUnit.TYPE_AAC:
|
||||
{
|
||||
int inputIndex = audioDecoder.dequeueInputBuffer(0);
|
||||
if (inputIndex == -4)
|
||||
{
|
||||
ByteBuffer buf = audioDecoderInputBuffers[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);
|
||||
}
|
||||
|
||||
audioDecoder.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
|
||||
@ -128,65 +320,26 @@ public class NvVideoStream implements RTPAppIntf {
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// Receive thread
|
||||
private void startAudioPlaybackThread()
|
||||
{
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
byte[] packet = new byte[1500];
|
||||
|
||||
// Send PING every 100 ms
|
||||
for (;;)
|
||||
{
|
||||
DatagramPacket dp = new DatagramPacket(packet, 0, packet.length);
|
||||
|
||||
try {
|
||||
rtp.receive(dp);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
break;
|
||||
}
|
||||
|
||||
System.out.println("in receiveData");
|
||||
int inputIndex = decoder.dequeueInputBuffer(-1);
|
||||
if (inputIndex >= 0)
|
||||
{
|
||||
ByteBuffer buf = decoderInputBuffers[inputIndex];
|
||||
NvVideoPacket nvVideo = new NvVideoPacket(dp.getData());
|
||||
|
||||
buf.clear();
|
||||
buf.put(nvVideo.data);
|
||||
|
||||
System.out.println(nvVideo);
|
||||
if (nvVideo.length == 0xc803) {
|
||||
decoder.queueInputBuffer(inputIndex,
|
||||
0, nvVideo.length,
|
||||
0, 0);
|
||||
frameIndex++;
|
||||
} else {
|
||||
decoder.queueInputBuffer(inputIndex,
|
||||
0, 0,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
BufferInfo info = new BufferInfo();
|
||||
System.out.println("dequeuing outputbuffer");
|
||||
int outIndex = decoder.dequeueOutputBuffer(info, -1);
|
||||
System.out.println("done dequeuing output buffer");
|
||||
System.out.println("Waiting for audio");
|
||||
int outIndex = audioDecoder.dequeueOutputBuffer(info, -1);
|
||||
System.out.println("Got audio");
|
||||
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");
|
||||
//decoderOutputFormat = decoder.getOutputFormat();
|
||||
System.out.println("New output Format: " + decoder.getOutputFormat());
|
||||
System.out.println("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||
System.out.println("Try again later");
|
||||
@ -195,9 +348,7 @@ public class NvVideoStream implements RTPAppIntf {
|
||||
break;
|
||||
}
|
||||
if (outIndex >= 0) {
|
||||
System.out.println("releasing output buffer");
|
||||
decoder.releaseOutputBuffer(outIndex, true);
|
||||
System.out.println("output buffer released");
|
||||
audioDecoder.releaseOutputBuffer(outIndex, true);
|
||||
}
|
||||
|
||||
}
|
||||
@ -205,52 +356,30 @@ public class NvVideoStream implements RTPAppIntf {
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(DataFrame frame, Participant participant) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEvent(int type, Participant[] participant) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int frameSize(int payloadType) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the presentation time for frame N, in microseconds.
|
||||
*/
|
||||
private static long computePresentationTime(int frameIndex) {
|
||||
return 132 + frameIndex * 1000000 / FRAME_RATE;
|
||||
}
|
||||
|
||||
class NvVideoPacket {
|
||||
byte[] preamble;
|
||||
short length;
|
||||
byte[] extra;
|
||||
byte[] data;
|
||||
|
||||
public NvVideoPacket(byte[] payload)
|
||||
private void outputDisplayLoop()
|
||||
{
|
||||
ByteBuffer bb = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
preamble = new byte[12+16];
|
||||
extra = new byte[38];
|
||||
|
||||
bb.get(preamble);
|
||||
length = bb.getShort();
|
||||
bb.get(extra);
|
||||
data = new byte[length];
|
||||
|
||||
if (bb.remaining() + length <= payload.length)
|
||||
bb.get(data);
|
||||
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);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return "";//String.format("Length: %d | %02x %02x %02x %02x %02x %02x %02x %02x",
|
||||
//length, data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
src/com/limelight/nvstream/av/AvBufferDescriptor.java
Normal file
21
src/com/limelight/nvstream/av/AvBufferDescriptor.java
Normal file
@ -0,0 +1,21 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class AvBufferDescriptor {
|
||||
public byte[] data;
|
||||
public int offset;
|
||||
public int length;
|
||||
|
||||
public AvBufferDescriptor(byte[] data, int offset, int length)
|
||||
{
|
||||
this.data = data;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public AvBufferDescriptor(AvBufferDescriptor desc)
|
||||
{
|
||||
this.data = desc.data;
|
||||
this.offset = desc.offset;
|
||||
this.length = desc.length;
|
||||
}
|
||||
}
|
30
src/com/limelight/nvstream/av/AvBufferPool.java
Normal file
30
src/com/limelight/nvstream/av/AvBufferPool.java
Normal file
@ -0,0 +1,30 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class AvBufferPool {
|
||||
private LinkedList<byte[]> bufferList = new LinkedList<byte[]>();
|
||||
private int bufferSize;
|
||||
|
||||
public AvBufferPool(int size)
|
||||
{
|
||||
this.bufferSize = size;
|
||||
}
|
||||
|
||||
public synchronized byte[] allocate()
|
||||
{
|
||||
if (bufferList.isEmpty())
|
||||
{
|
||||
return new byte[bufferSize];
|
||||
}
|
||||
else
|
||||
{
|
||||
return bufferList.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void free(byte[] buffer)
|
||||
{
|
||||
//bufferList.addFirst(buffer);
|
||||
}
|
||||
}
|
42
src/com/limelight/nvstream/av/AvDecodeUnit.java
Normal file
42
src/com/limelight/nvstream/av/AvDecodeUnit.java
Normal file
@ -0,0 +1,42 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AvDecodeUnit {
|
||||
public static final int TYPE_UNKNOWN = 0;
|
||||
public static final int TYPE_H264 = 1;
|
||||
public static final int TYPE_AAC = 2;
|
||||
|
||||
private int type;
|
||||
private List<AvBufferDescriptor> bufferList;
|
||||
private int dataLength;
|
||||
private int flags;
|
||||
|
||||
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()
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
public int getFlags()
|
||||
{
|
||||
return flags;
|
||||
}
|
||||
|
||||
public List<AvBufferDescriptor> getBufferList()
|
||||
{
|
||||
return bufferList;
|
||||
}
|
||||
|
||||
public int getDataLength()
|
||||
{
|
||||
return dataLength;
|
||||
}
|
||||
}
|
317
src/com/limelight/nvstream/av/AvDepacketizer.java
Normal file
317
src/com/limelight/nvstream/av/AvDepacketizer.java
Normal file
@ -0,0 +1,317 @@
|
||||
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> avcNalDataChain = null;
|
||||
private int avcNalDataLength = 0;
|
||||
private LinkedList<AvBufferDescriptor> aacNalDataChain = null;
|
||||
private int aacNalDataLength = 0;
|
||||
private int currentlyDecoding;
|
||||
|
||||
// Sequencing state
|
||||
private short lastSequenceNumber;
|
||||
|
||||
private LinkedBlockingQueue<AvDecodeUnit> decodedUnits = new LinkedBlockingQueue<AvDecodeUnit>();
|
||||
|
||||
private void reassembleAacNal()
|
||||
{
|
||||
// This is the start of a new AAC NAL
|
||||
if (aacNalDataChain != null && aacNalDataLength != 0)
|
||||
{
|
||||
System.out.println("Assembling AAC NAL: "+aacNalDataLength);
|
||||
|
||||
/*AvBufferDescriptor header = aacNalDataChain.getFirst();
|
||||
for (int i = 0; i < header.length; i++)
|
||||
System.out.printf("%02x ", header.data[header.offset+i]);
|
||||
System.out.println();*/
|
||||
|
||||
// Construct the AAC decode unit
|
||||
AvDecodeUnit du = new AvDecodeUnit(AvDecodeUnit.TYPE_AAC, aacNalDataChain, aacNalDataLength, 0);
|
||||
decodedUnits.add(du);
|
||||
|
||||
// Clear old state
|
||||
aacNalDataChain = null;
|
||||
aacNalDataLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void reassembleAvcNal()
|
||||
{
|
||||
// This is the start of a new NAL
|
||||
if (avcNalDataChain != null && avcNalDataLength != 0)
|
||||
{
|
||||
int flags = 0;
|
||||
|
||||
// Check if this is a special NAL unit
|
||||
AvBufferDescriptor header = avcNalDataChain.getFirst();
|
||||
AvBufferDescriptor specialSeq = NAL.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, avcNalDataChain, avcNalDataLength, flags);
|
||||
decodedUnits.add(du);
|
||||
|
||||
// Clear old state
|
||||
avcNalDataChain = null;
|
||||
avcNalDataLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void addInputData(AvPacket packet)
|
||||
{
|
||||
AvBufferDescriptor location = packet.getNewPayloadDescriptor();
|
||||
|
||||
while (location.length != 0)
|
||||
{
|
||||
// Remember the start of the NAL data in this packet
|
||||
int start = location.offset;
|
||||
|
||||
// Check for a special sequence
|
||||
AvBufferDescriptor specialSeq = NAL.getSpecialSequenceDescriptor(location);
|
||||
if (specialSeq != null)
|
||||
{
|
||||
if (NAL.isAvcStartSequence(specialSeq))
|
||||
{
|
||||
// We're decoding H264 now
|
||||
currentlyDecoding = AvDecodeUnit.TYPE_H264;
|
||||
|
||||
// Check if it's the end of the last frame
|
||||
if (NAL.isAvcFrameStart(specialSeq))
|
||||
{
|
||||
// Reassemble any pending AVC NAL
|
||||
reassembleAvcNal();
|
||||
|
||||
// Setup state for the new NAL
|
||||
avcNalDataChain = new LinkedList<AvBufferDescriptor>();
|
||||
avcNalDataLength = 0;
|
||||
}
|
||||
}
|
||||
else if (NAL.isAacStartSequence(specialSeq))
|
||||
{
|
||||
// We're decoding AAC now
|
||||
currentlyDecoding = AvDecodeUnit.TYPE_AAC;
|
||||
|
||||
// Reassemble any pending AAC NAL
|
||||
reassembleAacNal();
|
||||
|
||||
// Setup state for the new NAL
|
||||
aacNalDataChain = new LinkedList<AvBufferDescriptor>();
|
||||
aacNalDataLength = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not either sequence we want
|
||||
//currentlyDecoding = AvDecodeUnit.TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
// Skip the start sequence
|
||||
location.length -= specialSeq.length;
|
||||
location.offset += specialSeq.length;
|
||||
}
|
||||
|
||||
// Move to the next special sequence
|
||||
while (location.length != 0)
|
||||
{
|
||||
specialSeq = NAL.getSpecialSequenceDescriptor(location);
|
||||
|
||||
// Check if this should end the current NAL
|
||||
if (specialSeq != null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This byte is part of the NAL data
|
||||
location.offset++;
|
||||
location.length--;
|
||||
}
|
||||
}
|
||||
|
||||
AvBufferDescriptor data = new AvBufferDescriptor(location.data, start, location.offset-start);
|
||||
|
||||
if (currentlyDecoding == AvDecodeUnit.TYPE_H264 && avcNalDataChain != null)
|
||||
{
|
||||
// Add a buffer descriptor describing the NAL data in this packet
|
||||
avcNalDataChain.add(data);
|
||||
avcNalDataLength += location.offset-start;
|
||||
}
|
||||
else if (currentlyDecoding == AvDecodeUnit.TYPE_AAC && aacNalDataChain != null)
|
||||
{
|
||||
// Add a buffer descriptor describing the NAL data in this packet
|
||||
aacNalDataChain.add(data);
|
||||
aacNalDataLength += location.offset-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 &&
|
||||
(short)(lastSequenceNumber + 1) != seq)
|
||||
{
|
||||
System.out.println("Received OOS data (expected "+(lastSequenceNumber + 1)+", got "+seq+")");
|
||||
|
||||
// Reset the depacketizer state
|
||||
currentlyDecoding = AvDecodeUnit.TYPE_UNKNOWN;
|
||||
avcNalDataChain = null;
|
||||
avcNalDataLength = 0;
|
||||
aacNalDataChain = null;
|
||||
aacNalDataLength = 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 NAL {
|
||||
|
||||
// This assumes that the buffer passed in is already a special sequence
|
||||
public static boolean isAvcStartSequence(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);
|
||||
}
|
||||
|
||||
// This assumes that the buffer passed in is already a special sequence
|
||||
public static boolean isAacStartSequence(AvBufferDescriptor specialSeq)
|
||||
{
|
||||
if (specialSeq.length != 3)
|
||||
return false;
|
||||
|
||||
// The start sequence is 00 00 03
|
||||
return (specialSeq.data[specialSeq.offset+specialSeq.length-1] == 0x03);
|
||||
}
|
||||
|
||||
// This assumes that the buffer passed in is already a special sequence
|
||||
public static boolean isAvcFrameStart(AvBufferDescriptor specialSeq)
|
||||
{
|
||||
if (specialSeq.length != 4)
|
||||
return false;
|
||||
|
||||
// The frame start sequence is 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;
|
||||
}
|
||||
}
|
15
src/com/limelight/nvstream/av/AvPacket.java
Normal file
15
src/com/limelight/nvstream/av/AvPacket.java
Normal file
@ -0,0 +1,15 @@
|
||||
package com.limelight.nvstream.av;
|
||||
|
||||
public class AvPacket {
|
||||
private AvBufferDescriptor buffer;
|
||||
|
||||
public AvPacket(AvBufferDescriptor rtpPayload)
|
||||
{
|
||||
buffer = new AvBufferDescriptor(rtpPayload);
|
||||
}
|
||||
|
||||
public AvBufferDescriptor getNewPayloadDescriptor()
|
||||
{
|
||||
return new AvBufferDescriptor(buffer.data, buffer.offset+56, buffer.length-56);
|
||||
}
|
||||
}
|
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