Closed jhhuang closed 3 years ago
What exact RTSP camera model do you have?
DeviceInfo: "manufacturer":"Dahua", "modle":"IPC-T12D-V2" I fixed the issue by sending RTCP report packet and OptionsCommand every 5sec.
Can you submit pull request?
Sorry, It's used for a test project ,so it's poor coded. I can paste it here, if you wanted.
Yes, please paste the code.
` package com.alexvas.rtsp;
import android.util.Log;
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Timer; import java.util.TimerTask;
public class RTCPSender extends TimerTask { private static final String TAG = RTCPSender.class.getSimpleName(); String remoteRtcpHost; InetAddress remoteIp; int localRtcpPort; int reportingPeriod; DatagramSocket rtcpSocket; Timer timer; RtspClient rtspClient; long lastRtspReq = 0;
// stats variables
int highSeqNb;
int numPktsExpected; // number of RTP packets expected since the last RTCP packet
int numPktsLost; // number of RTP packets lost since the last RTCP packet
int lastHighSeqNb; // last highest Seq number received
int lastCumLost; // last cumulative packets lost
float lastFractionLost; // last fraction lost
SocketStuff socketStuff;
private OutputStream outputStream;
public RTCPSender(String remoteHost, int localRtcpPort, int remoteRtcpPort, int reportingPeriod, RtspClient rtspClient, SocketStuff socketStuff) {
this.remoteRtcpHost = remoteHost;
this.localRtcpPort = localRtcpPort;
this.reportingPeriod = reportingPeriod;
this.rtspClient = rtspClient;
this.socketStuff = socketStuff;
}
public void start() {
try {
this.remoteIp = InetAddress.getByName(remoteRtcpHost);
// // // bind UDP port for sending and receiving RTCP packets rtcpSocket = new DatagramSocket(localRtcpPort); rtcpSocket.setReuseAddress(true); // timer = new Timer(RTCPSender.class.getSimpleName(), false); timer.scheduleAtFixedRate(this, reportingPeriod, reportingPeriod); } catch (IOException e) { Log.e(TAG, "Error while starting RTCP sender thread", e); } }
public void stop() {
if (timer != null)
timer.cancel();
synchronized (rtcpSocket) {
rtcpSocket.close();
}
socketStuff = null;
}
@Override
public void run() {
sendReport();
}
public void setStats(int seqNb) {
this.highSeqNb = seqNb;
}
protected void sendReport() {
// compute stats for this period
//numPktsExpected = statHighSeqNum - lastHighSeqNb;
//numPktsLost = statCumLost - lastCumLost;
//lastFractionLost = randomGenerator.nextInt(10)/10.0f;
//lastHighSeqNb = highSeqNb;
//lastFractionLost = numPktsExpected == 0 ? 0f : (float)numPktsLost / numPktsExpected;
//lastCumLost = statCumLost;
RTCPpacket rtcp_packet = new RTCPpacket(0, 0, highSeqNb);//lastFractionLost, statCumLost, highSeqNb);
int packetLength = rtcp_packet.getLength();
byte[] packetBits = new byte[packetLength];
rtcp_packet.getpacket(packetBits);
try {
// send RTCP report packet
synchronized (rtcpSocket) {
DatagramPacket dp = new DatagramPacket(packetBits, packetLength, remoteIp, localRtcpPort);
rtcpSocket.send(dp);
Log.d(TAG, "Sent RTCP report at seq number {}" + highSeqNb);
}
} catch (IOException e) {
Log.e(TAG, "Error while sending RTCP packet", e);
}
try {
// also send a request to keep RTSP connection alive
long now = System.currentTimeMillis();
if (now - lastRtspReq > 10000) {
rtspClient.sendOptionsCommand(socketStuff.outputStream, socketStuff.request, ++socketStuff.cSeq, socketStuff.userAgent, socketStuff.authToken);
lastRtspReq = now;
}
} catch (IOException e) {
Log.d(TAG, "Error while sending RTSP keep-alive request", e);
}
}
public static class SocketStuff {
InputStream inputStream;
OutputStream outputStream;
String request;
int cSeq;
String userAgent;
String authToken;
public SocketStuff(InputStream inputStream, OutputStream outputStream, String request, int cSeq, String userAgent, String authToken) {
this.inputStream = inputStream;
this.outputStream = outputStream;
this.request = request;
this.cSeq = cSeq;
this.userAgent = userAgent;
this.authToken = authToken;
}
}
} `
`package com.alexvas.rtsp;
import android.net.Uri; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.Nullable;
import com.alexvas.utils.NetUtils; import com.alexvas.utils.VideoCodecUtils;
import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean;
//OPTIONS rtsp://10.0.1.145:88/videoSub RTSP/1.0 //CSeq: 1 //User-Agent: Lavf58.29.100 //exclude //RTSP/1.0 200 OK //CSeq: 1 //Date: Fri, Jan 03 2020 22:03:07 GMT //Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
//DESCRIBE rtsp://10.0.1.145:88/videoSub RTSP/1.0 //Accept: application/sdp //CSeq: 2 //User-Agent: Lavf58.29.100 // //RTSP/1.0 401 Unauthorized //CSeq: 2 //Date: Fri, Jan 03 2020 22:03:07 GMT //WWW-Authenticate: Digest realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130"
//DESCRIBE rtsp://10.0.1.145:88/videoSub RTSP/1.0 //Accept: application/sdp //CSeq: 3 //User-Agent: Lavf58.29.100 //Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:88/videoSub", response="4f062baec1c813ae3db15e3a14111d3d" // //RTSP/1.0 200 OK //CSeq: 3 //Date: Fri, Jan 03 2020 22:03:07 GMT //Content-Base: rtsp://10.0.1.145:65534/videoSub/ //Content-Type: application/sdp //Content-Length: 495 // //v=0 //o=- 1578088972261172 1 IN IP4 10.0.1.145 //s=IP Camera Video //i=videoSub //t=0 0 //a=tool:LIVE555 Streaming Media v2014.02.10 //a=type:broadcast //a=control:* //a=range:npt=0- //a=x-qt-text-nam:IP Camera Video //a=x-qt-text-inf:videoSub //m=video 0 RTP/AVP 96 //c=IN IP4 0.0.0.0 //b=AS:96 //a=rtpmap:96 H264/90000 //a=fmtp:96 packetization-mode=1;profile-level-id=420020;sprop-parameter-sets=Z0IAIJWoFAHmQA==,aM48gA== //a=control:track1 //m=audio 0 RTP/AVP 0 //c=IN IP4 0.0.0.0 //b=AS:64 //a=control:track2 //SETUP rtsp://10.0.1.145:65534/videoSub/track1 RTSP/1.0 //Transport: RTP/AVP/UDP;unicast;client_port=27452-27453 //CSeq: 4 //User-Agent: Lavf58.29.100 //Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:65534/videoSub/track1", response="1fbc50b24d582c9331dd5e89f3102a06" // //RTSP/1.0 200 OK //CSeq: 4 //Date: Fri, Jan 03 2020 22:03:07 GMT //Transport: RTP/AVP;unicast;destination=10.0.1.53;source=10.0.1.145;client_port=27452-27453;server_port=6972-6973 //Session: 1F91B1B6;timeout=65
//SETUP rtsp://10.0.1.145:65534/videoSub/track2 RTSP/1.0 //Transport: RTP/AVP/UDP;unicast;client_port=27454-27455 //CSeq: 5 //User-Agent: Lavf58.29.100 //Session: 1F91B1B6 //Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:65534/videoSub/track2", response="ad779abe070c096eff1012e7c70c986a" // //RTSP/1.0 200 OK //CSeq: 5 //Date: Fri, Jan 03 2020 22:03:07 GMT //Transport: RTP/AVP;unicast;destination=10.0.1.53;source=10.0.1.145;client_port=27454-27455;server_port=6974-6975 //Session: 1F91B1B6;timeout=65
//PLAY rtsp://10.0.1.145:65534/videoSub/ RTSP/1.0 //Range: npt=0.000- //CSeq: 6 //User-Agent: Lavf58.29.100 //Session: 1F91B1B6 //Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:65534/videoSub/", response="bb52eb6938dd4e50c4fac50363ffded0" // //RTSP/1.0 200 OK //CSeq: 6 //Date: Fri, Jan 03 2020 22:03:07 GMT //Range: npt=0.000- //Session: 1F91B1B6 //RTP-Info: url=rtsp://10.0.1.145:65534/videoSub/track1;seq=42731;rtptime=2690581590,url=rtsp://10.0.1.145:65534/videoSub/track2;seq=34051;rtptime=3328043318
// https://www.ietf.org/rfc/rfc2326.txt public class RtspClient {
public static final int VIDEO_CODEC_H264 = 0;
public static final int VIDEO_CODEC_H265 = 1;
public static final int AUDIO_CODEC_AAC = 0;
private static final String TAG = RtspClient.class.getSimpleName();
static final String TAG_DEBUG = TAG + " DBG";
private static final int rtpRcvPort = 5678;
private static final boolean DEBUG = true;
private static final String CRLF = "\r\n";
// Size of buffer for reading from the connection
private final static int MAX_LINE_SIZE = 4098;
private static boolean debug;
private final @NonNull
Socket rtspSocket;
private final @NonNull
String uriRtsp;
private final @NonNull
AtomicBoolean exitFlag;
private final @NonNull
RtspClientListener listener;
// private boolean sendOptionsCommand;
private boolean requestVideo;
private boolean requestAudio;
private @Nullable
String username;
private @Nullable
String password;
private @Nullable
String userAgent;
private RTCPSender rtcpThread;
private RtspClient(@NonNull RtspClient.Builder builder) {
rtspSocket = builder.rtspSocket;
uriRtsp = builder.uriRtsp;
exitFlag = builder.exitFlag;
listener = builder.listener;
requestVideo = builder.requestVideo;
requestAudio = builder.requestAudio;
username = builder.username;
password = builder.password;
debug = builder.debug;
userAgent = builder.userAgent;
}
@Nullable
private static String getUriForSetup(@NonNull String uriRtsp, @Nullable Track track) {
if (track == null || TextUtils.isEmpty(track.request))
return null;
String uriRtspSetup = uriRtsp;
if (track.request.startsWith("rtsp://") || track.request.startsWith("rtsps://")) {
// Absolute URL
uriRtspSetup = track.request;
} else {
// Relative URL
if (!track.request.startsWith("/")) {
track.request = "/" + track.request;
}
uriRtspSetup += track.request;
}
return uriRtspSetup;
}
private static void checkExitFlag(@NonNull AtomicBoolean exitFlag) throws InterruptedException {
if (exitFlag.get())
throw new InterruptedException();
}
private static void checkStatusCode(int code) throws IOException {
switch (code) {
case 200:
break;
case 401:
throw new UnauthorizedException();
default:
throw new IOException("Invalid status code " + code);
}
}
private static void readRtpData(
@NonNull InputStream inputStream,
@NonNull SdpInfo sdpInfo,
@NonNull AtomicBoolean exitFlag,
@NonNull RtspClientListener listener)
throws IOException {
byte[] data = new byte[15000]; // Usually not bigger than MTU
// Read 1000 RTP packets
final VideoRtpParser videoParser = new VideoRtpParser();
final AacParser audioParser = (sdpInfo.audioTrack != null ? new AacParser(sdpInfo.audioTrack.mode) : null);
byte[] nalUnitSps = (sdpInfo.videoTrack != null ? sdpInfo.videoTrack.sps : null);
byte[] nalUnitPps = (sdpInfo.videoTrack != null ? sdpInfo.videoTrack.pps : null);
BufferedInputStream buffInputStream = new BufferedInputStream(inputStream);
while (!exitFlag.get()) {
RtpParser.RtpHeader header = RtpParser.readHeader(buffInputStream);
if (header == null) {
buffInputStream.reset();
readResponseStatusCode(buffInputStream);
readResponseHeaders(buffInputStream);
Log.e(TAG, "header == null");
continue;
}
NetUtils.readData(buffInputStream, data, 0, header.payloadSize);
// Video
if (sdpInfo.videoTrack != null && header.payloadType == sdpInfo.videoTrack.payloadType) {
byte[] nalUnit = videoParser.processRtpPacketAndGetNalUnit(data, header.payloadSize);
if (nalUnit != null) {
int type = VideoCodecUtils.getH264NalUnitType(nalUnit, 0, nalUnit.length);
// Log.i(TAG, "NAL u: " + VideoCodecUtils.getH264NalUnitTypeString(type)); switch (type) { case VideoCodecUtils.NAL_SPS: nalUnitSps = nalUnit; // Looks like there is NAL_IDR_SLICE as well. Send it now. if (nalUnit.length > 100) listener.onRtspVideoNalUnitReceived(nalUnit, 0, nalUnit.length, (long) (header.timeStamp 11.111111)); break; case VideoCodecUtils.NAL_PPS: nalUnitPps = nalUnit; // Looks like there is NAL_IDR_SLICE as well. Send it now. if (nalUnit.length > 100) listener.onRtspVideoNalUnitReceived(nalUnit, 0, nalUnit.length, (long) (header.timeStamp 11.111111)); break; case VideoCodecUtils.NAL_IDR_SLICE: // Combine IDR with SPS/PPS if (nalUnitSps != null && nalUnitPps != null) { // byte[] nalUnitSppPpsIdr = new byte[nalUnitSps.length + nalUnitPps.length + nalUnit.length]; // System.arraycopy(nalUnitSps, 0, nalUnitSppPpsIdr, 0, nalUnitSps.length); // System.arraycopy(nalUnitPps, 0, nalUnitSppPpsIdr, nalUnitSps.length, nalUnitPps.length); // System.arraycopy(nalUnit, 0, nalUnitSppPpsIdr, nalUnitSps.length + nalUnitPps.length, nalUnit.length); // listener.onRtspNalUnitReceived(nalUnitSppPpsIdr, 0, nalUnitSppPpsIdr.length, System.currentTimeMillis()); byte[] nalUnitSppPps = new byte[nalUnitSps.length + nalUnitPps.length]; System.arraycopy(nalUnitSps, 0, nalUnitSppPps, 0, nalUnitSps.length); System.arraycopy(nalUnitPps, 0, nalUnitSppPps, nalUnitSps.length, nalUnitPps.length); listener.onRtspVideoNalUnitReceived(nalUnitSppPps, 0, nalUnitSppPps.length, (long) (header.timeStamp 11.111111)); // listener.onRtspNalUnitReceived(nalUnitSppPps, 0, nalUnitSppPps.length, System.currentTimeMillis() / 10); // Send it only once nalUnitSps = null; nalUnitPps = null; } // listener.onRtspNalUnitReceived(nalUnitSps, 0, nalUnitSps.length, System.currentTimeMillis()); // listener.onRtspNalUnitReceived(nalUnitPps, 0, nalUnitPps.length, System.currentTimeMillis()); // listener.onRtspNalUnitReceived(nalUnit, 0, nalUnit.length, System.currentTimeMillis()); // break; default: listener.onRtspVideoNalUnitReceived(nalUnit, 0, nalUnit.length, (long) (header.timeStamp 11.111111)); // listener.onRtspNalUnitReceived(nalUnit, 0, nalUnit.length, System.currentTimeMillis() / 10); } }
// Audio
} else if (sdpInfo.audioTrack != null && header.payloadType == sdpInfo.audioTrack.payloadType) {
if (audioParser != null) {
byte[] sample = audioParser.processRtpPacketAndGetSample(data, header.payloadSize);
if (sample != null)
listener.onRtspAudioSampleReceived(sample, 0, sample.length, (long) (header.timeStamp * 11.111111));
}
// Unknown
} else {
// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml
if (DEBUG && header.payloadType >= 96 && header.payloadType <= 127)
Log.w(TAG, "Invalid RTP payload type " + header.payloadType);
}
}
}
public static void sendOptionsCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken)
throws IOException {
if (DEBUG)
Log.v(TAG, "sendOptionsCommand(request=\"" + request + "\", cSeq=" + cSeq + "\", userAgent=" + userAgent + "\", authToken=" + authToken + ")");
outputStream.write(("OPTIONS " + request + " RTSP/1.0" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
public static void sendDescribeCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken)
throws IOException {
if (DEBUG)
Log.v(TAG, "sendDescribeCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
outputStream.write(("DESCRIBE " + request + " RTSP/1.0" + CRLF).getBytes());
outputStream.write(("Accept: application/sdp" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private static void sendSetupCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken,
@Nullable String session,
@NonNull String interleaved)
throws IOException {
if (DEBUG)
Log.v(TAG, "sendSetupCommand(request=\"" + request + "\", cSeq=" + cSeq + "\", userAgent=" + userAgent + "\", authToken=" + authToken
+ "\", session=" + session + "\", interleaved=" + interleaved + ")");
outputStream.write(("SETUP " + request + " RTSP/1.0" + CRLF).getBytes());
int rtcpPort = rtpRcvPort + 1;
outputStream.write(("Transport: RTP/AVP/TCP;unicast;" + "client_port=" + rtpRcvPort + "-" + rtcpPort + ";interleaved=" + interleaved + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
if (session != null)
outputStream.write(("Session: " + session + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private static void sendPlayCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken,
@NonNull String session)
throws IOException {
if (DEBUG)
Log.v(TAG, "sendPlayCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
outputStream.write(("PLAY " + request + " RTSP/1.0" + CRLF).getBytes());
outputStream.write(("Range: npt=0.000-" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
outputStream.write(("Session: " + session + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
public static int readResponseStatusCode(@NonNull InputStream inputStream) throws IOException {
// String line = readLine(inputStream); // if (debug) // Log.d(TAG_DEBUG, "" + line); String line; while (!TextUtils.isEmpty(line = readLine(inputStream))) { if (debug) Log.d(TAG_DEBUG, "" + line); //noinspection ConstantConditions int indexRtsp = line.indexOf("RTSP/1.0 "); // 9 characters if (indexRtsp >= 0) { int indexCode = line.indexOf(' ', indexRtsp + 9); String code = line.substring(indexRtsp + 9, indexCode); try { return Integer.parseInt(code); } catch (NumberFormatException e) { // Does not fulfill standard "RTSP/1.1 200 Ok" token // Continue search for } } } if (debug) Log.d(TAG_DEBUG, "" + line); return -1; }
@NonNull
public static ArrayList<Pair<String, String>> readResponseHeaders(@NonNull InputStream inputStream) throws IOException {
ArrayList<Pair<String, String>> headers = new ArrayList<>();
String line;
while (true) {
line = readLine(inputStream);
if (!TextUtils.isEmpty(line)) {
if (debug)
Log.d(TAG_DEBUG, "" + line);
if (CRLF.equals(line)) {
return headers;
} else {
String[] pairs = TextUtils.split(line, ":");
if (pairs.length == 2) {
headers.add(Pair.create(pairs[0].trim(), pairs[1].trim()));
}
}
} else {
break;
}
}
return headers;
}
/**
* Get a list of tracks from SDP. Usually contains video and audio track only.
*
* @return array of 2 tracks. First is video track, second audio track.
*/
@NonNull
private static Track[] getTracksFromDescribeParams(@NonNull List<Pair<String, String>> params) {
Track[] tracks = new Track[2];
Track currentTrack = null;
for (Pair<String, String> param : params) {
switch (param.first) {
case "m":
// m=video 0 RTP/AVP 96
if (param.second.startsWith("video")) {
currentTrack = new VideoTrack();
tracks[0] = currentTrack;
// m=audio 0 RTP/AVP 97
} else if (param.second.startsWith("audio")) {
currentTrack = new AudioTrack();
tracks[1] = currentTrack;
} else {
currentTrack = null;
}
if (currentTrack != null) {
// m=<media> <port>/<number of ports> <proto> <fmt> ...
String[] values = TextUtils.split(param.second, " ");
currentTrack.payloadType = (values.length > 3 ? Integer.parseInt(values[3]) : -1);
if (currentTrack.payloadType == -1)
Log.e(TAG, "Failed to get payload type from \"m=" + param.second + "\"");
}
break;
case "a":
// a=control:trackID=1
if (currentTrack != null) {
if (param.second.startsWith("control:")) {
currentTrack.request = param.second.substring(8);
// a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=Z01AKZpmBkCb8uAtQEBAQXpw,aO48gA==
// a=fmtp:97 streamtype=5; profile-level-id=15; mode=AAC-hbr; config=1408; sizeLength=13; indexLength=3; indexDeltaLength=3; profile=1; bitrate=32000;
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
// a=fmtp:96 streamtype=5; profile-level-id=14; mode=AAC-lbr; config=1388; sizeLength=6; indexLength=2; indexDeltaLength=2; constantDuration=1024; maxDisplacement=5
} else if (param.second.startsWith("fmtp:")) {
// Video
if (currentTrack instanceof VideoTrack) {
updateVideoTrackFromDescribeParam((VideoTrack) tracks[0], param);
// Audio
} else {
updateAudioTrackFromDescribeParam((AudioTrack) tracks[1], param);
}
// a=rtpmap:96 H264/90000
// a=rtpmap:97 mpeg4-generic/16000/1
// a=rtpmap:97 G726-32/8000
} else if (param.second.startsWith("rtpmap:")) {
// Video
if (currentTrack instanceof VideoTrack) {
String[] values = TextUtils.split(param.second, " ");
if (values.length > 1) {
values = TextUtils.split(values[1], "/");
if (values.length > 0) {
switch (values[0].toLowerCase()) {
case "h264":
((VideoTrack) tracks[0]).videoCodec = VIDEO_CODEC_H264;
break;
case "h265":
((VideoTrack) tracks[0]).videoCodec = VIDEO_CODEC_H265;
break;
default:
Log.w(TAG, "Unknown video codec \"" + values[0] + "\"");
}
Log.i(TAG, "Video: " + values[0]);
}
}
// Audio
} else {
String[] values = TextUtils.split(param.second, " ");
if (values.length > 1) {
AudioTrack track = ((AudioTrack) tracks[1]);
values = TextUtils.split(values[1], "/");
if (values.length > 1) {
if ("mpeg4-generic".equals(values[0].toLowerCase())) {
track.audioCodec = AUDIO_CODEC_AAC;
} else {
Log.w(TAG, "Unknown audio codec \"" + values[0] + "\"");
}
track.sampleRateHz = Integer.parseInt(values[1]);
// If no channels specified, use mono, e.g. "a=rtpmap:97 MPEG4-GENERIC/8000"
track.channels = values.length > 2 ? Integer.parseInt(values[2]) : 1;
Log.i(TAG, "Audio: " + (track.audioCodec == AUDIO_CODEC_AAC ? "AAC LC" : "n/a") + ", sample rate: " + track.sampleRateHz + " Hz, channels: " + track.channels);
}
}
}
}
}
break;
}
}
return tracks;
}
// Pair first - name, e.g. "a"; second - value, e.g "cliprect:0,0,1920,1080"
@NonNull
private static List<Pair<String, String>> getDescribeParams(@NonNull String text) {
ArrayList<Pair<String, String>> list = new ArrayList<>();
String[] params = TextUtils.split(text, "\r\n");
for (String param : params) {
int i = param.indexOf('=');
if (i > 0) {
String name = param.substring(0, i).trim();
String value = param.substring(i + 1);
list.add(Pair.create(name, value));
}
}
return list;
}
@NonNull
private static SdpInfo getSdpInfoFromDescribeParams(@NonNull List<Pair<String, String>> params) {
SdpInfo sdpInfo = new SdpInfo();
Track[] tracks = getTracksFromDescribeParams(params);
sdpInfo.videoTrack = ((VideoTrack) tracks[0]);
sdpInfo.audioTrack = ((AudioTrack) tracks[1]);
for (Pair<String, String> param : params) {
switch (param.first) {
case "s":
sdpInfo.sessionName = param.second;
break;
case "i":
sdpInfo.sessionDescription = param.second;
break;
}
}
return sdpInfo;
}
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
@Nullable
private static List<Pair<String, String>> getSdpAParams(@NonNull Pair<String, String> param) {
if (param.first.equals("a") && param.second.startsWith("fmtp:")) { //
String value = param.second.substring(8).trim(); // fmtp can be '96' (2 chars) and '127' (3 chars)
String[] paramsA = TextUtils.split(value, ";");
// streamtype=5
// profile-level-id=1
// mode=AAC-hbr
ArrayList<Pair<String, String>> retParams = new ArrayList<>();
for (String paramA : paramsA) {
paramA = paramA.trim();
// sprop-parameter-sets=Z0LAKIyNQDwBEvLAPCIRqA==,aM48gA==
int i = paramA.indexOf("=");
if (i != -1)
retParams.add(
Pair.create(
paramA.substring(0, i),
paramA.substring(i + 1)));
}
return retParams;
} else {
Log.e(TAG, "Not a valid fmtp");
}
return null;
}
private static void updateVideoTrackFromDescribeParam(@NonNull VideoTrack videoTrack, @NonNull Pair<String, String> param) {
// a=fmtp:96 packetization-mode=1;profile-level-id=42C028;sprop-parameter-sets=Z0LAKIyNQDwBEvLAPCIRqA==,aM48gA==;
// a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=Z01AKZpmBkCb8uAtQEBAQXpw,aO48gA==
// a=fmtp:99 sprop-parameter-sets=Z0LgKdoBQBbpuAgIMBA=,aM4ySA==;packetization-mode=1;profile-level-id=42e029
List<Pair<String, String>> params = getSdpAParams(param);
if (params != null) {
for (Pair<String, String> pair : params) {
switch (pair.first) {
case "sprop-parameter-sets": {
String[] paramsSpsPps = TextUtils.split(pair.second, ",");
if (paramsSpsPps.length > 1) {
byte[] sps = Base64.decode(paramsSpsPps[0], Base64.NO_WRAP);
byte[] pps = Base64.decode(paramsSpsPps[1], Base64.NO_WRAP);
byte[] nalSps = new byte[sps.length + 4];
byte[] nalPps = new byte[pps.length + 4];
// Add 00 00 00 01 NAL unit header
nalSps[0] = 0;
nalSps[1] = 0;
nalSps[2] = 0;
nalSps[3] = 1;
System.arraycopy(sps, 0, nalSps, 4, sps.length);
nalPps[0] = 0;
nalPps[1] = 0;
nalPps[2] = 0;
nalPps[3] = 1;
System.arraycopy(pps, 0, nalPps, 4, pps.length);
videoTrack.sps = nalSps;
videoTrack.pps = nalPps;
}
}
break;
}
}
}
}
private static void updateAudioTrackFromDescribeParam(@NonNull AudioTrack audioTrack, @NonNull Pair<String, String> param) {
// a=fmtp:96 streamtype=5; profile-level-id=14; mode=AAC-lbr; config=1388; sizeLength=6; indexLength=2; indexDeltaLength=2; constantDuration=1024; maxDisplacement=5
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
List<Pair<String, String>> params = getSdpAParams(param);
if (params != null) {
for (Pair<String, String> pair : params) {
switch (pair.first) {
case "mode":
audioTrack.mode = pair.second;
break;
}
}
}
}
private static int getHeaderContentLength(@NonNull ArrayList<Pair<String, String>> headers) {
String length = getHeader(headers, "content-length");
if (!TextUtils.isEmpty(length)) {
try {
//noinspection ConstantConditions
return Integer.parseInt(length);
} catch (NumberFormatException ignored) {
}
}
return -1;
}
@Nullable
private static Pair<String, String> getHeaderWwwAuthenticateDigestRealmAndNonce(@NonNull ArrayList<Pair<String, String>> headers) {
for (Pair<String, String> head : headers) {
String h = head.first.toLowerCase();
// WWW-Authenticate: Digest realm="AXIS_00408CEF081C", nonce="00054cecY7165349339ae05f7017797d6b0aaad38f6ff45", stale=FALSE
// WWW-Authenticate: Basic realm="AXIS_00408CEF081C"
// WWW-Authenticate: Digest realm="Login to 4K049EBPAG1D7E7", nonce="de4ccb15804565dc8a4fa5b115695f4f"
if ("www-authenticate".equals(h) && head.second.toLowerCase().startsWith("digest")) {
String v = head.second.substring(7).trim();
int begin, end;
begin = v.indexOf("realm=");
begin = v.indexOf('"', begin) + 1;
end = v.indexOf('"', begin);
String digestRealm = v.substring(begin, end);
begin = v.indexOf("nonce=");
begin = v.indexOf('"', begin) + 1;
end = v.indexOf('"', begin);
String digestNonce = v.substring(begin, end);
return Pair.create(digestRealm, digestNonce);
}
}
return null;
}
@Nullable
private static String getHeaderWwwAuthenticateBasicRealm(@NonNull ArrayList<Pair<String, String>> headers) {
for (Pair<String, String> head : headers) {
// Session: ODgyODg3MjQ1MDczODk3NDk4Nw
String h = head.first.toLowerCase();
String v = head.second.toLowerCase();
// WWW-Authenticate: Digest realm="AXIS_00408CEF081C", nonce="00054cecY7165349339ae05f7017797d6b0aaad38f6ff45", stale=FALSE
// WWW-Authenticate: Basic realm="AXIS_00408CEF081C"
if ("www-authenticate".equals(h) && v.startsWith("basic")) {
v = v.substring(6).trim();
// realm=
// AXIS_00408CEF081C
String[] tokens = TextUtils.split(v, "\"");
if (tokens.length > 2)
return tokens[1];
}
}
return null;
}
//v=0 //o=- 1542237507365806 1542237507365806 IN IP4 10.0.1.111 //s=Media Presentation //e=NONE //b=AS:50032 //t=0 0 //a=control:* //a=range:npt=0.000000- //m=video 0 RTP/AVP 96 //c=IN IP4 0.0.0.0 //b=AS:50000 //a=framerate:25.0 //a=transform:1.000000,0.000000,0.000000;0.000000,1.000000,0.000000;0.000000,0.000000,1.000000 //a=control:trackID=1 //a=rtpmap:96 H264/90000 //a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=Z01AKZpmBkCb8uAtQEBAQXpw,aO48gA== //m=audio 0 RTP/AVP 97 //c=IN IP4 0.0.0.0 //b=AS:32 //a=control:trackID=2 //a=rtpmap:97 G726-32/8000
// v=0 // o=- 14190294250618174561 14190294250618174561 IN IP4 127.0.0.1 // s=IP Webcam // c=IN IP4 0.0.0.0 // t=0 0 // a=range:npt=now- // a=control:* // m=video 0 RTP/AVP 96 // a=rtpmap:96 H264/90000 // a=control:h264 // a=fmtp:96 packetization-mode=1;profile-level-id=42C028;sprop-parameter-sets=Z0LAKIyNQDwBEvLAPCIRqA==,aM48gA==; // a=cliprect:0,0,1920,1080 // a=framerate:30.0 // a=framesize:96 1080-1920
// Basic authentication
@NonNull
private static String getBasicAuthHeader(@Nullable String username, @Nullable String password) {
String auth = (username == null ? "" : username) + ":" + (password == null ? "" : password);
return "Basic " + new String(Base64.encode(auth.getBytes(StandardCharsets.ISO_8859_1), Base64.NO_WRAP));
}
// Digest authentication
@Nullable
private static String getDigestAuthHeader(
@Nullable String username,
@Nullable String password,
@NonNull String method,
@NonNull String digestUri,
@NonNull String realm,
@NonNull String nonce) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] ha1;
if (username == null)
username = "";
if (password == null)
password = "";
// calc A1 digest
md.update(username.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(realm.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(password.getBytes(StandardCharsets.ISO_8859_1));
ha1 = md.digest();
// calc A2 digest
md.reset();
md.update(method.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(digestUri.getBytes(StandardCharsets.ISO_8859_1));
byte[] ha2 = md.digest();
// calc response
md.update(getHexStringFromBytes(ha1).getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(nonce.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
// TODO add support for more secure version of digest auth
//md.update(nc.getBytes(StandardCharsets.ISO_8859_1));
//md.update((byte) ':');
//md.update(cnonce.getBytes(StandardCharsets.ISO_8859_1));
//md.update((byte) ':');
//md.update(qop.getBytes(StandardCharsets.ISO_8859_1));
//md.update((byte) ':');
md.update(getHexStringFromBytes(ha2).getBytes(StandardCharsets.ISO_8859_1));
String response = getHexStringFromBytes(md.digest());
// Log.d("username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"", // userName, digestRealm, digestNonce, digestUri, response);
return "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + digestUri + "\", response=\"" + response + "\"";
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@NonNull
private static String getHexStringFromBytes(@NonNull byte[] bytes) {
StringBuilder buf = new StringBuilder();
for (byte b : bytes)
buf.append(String.format("%02x", b));
return buf.toString();
}
@NonNull
private static String readContentAsText(@NonNull InputStream inputStream, int length) throws IOException {
if (length <= 0)
return "";
byte[] b = new byte[length];
int read = readData(inputStream, b, 0, length);
return new String(b, 0, read);
}
@Nullable
private static String readLine(@NonNull InputStream inputStream) throws IOException {
byte[] bufferLine = new byte[MAX_LINE_SIZE];
int offset = 0;
int readBytes;
do {
// Didn't find "\r\n" within 4K bytes
if (offset >= MAX_LINE_SIZE) {
throw new NoResponseHeadersException();
}
// Read 1 byte
readBytes = inputStream.read(bufferLine, offset, 1);
if (readBytes == 1) {
// Check for EOL
// Some cameras like Linksys WVC200 do not send \n instead of \r\n
if (offset > 0 && /*bufferLine[offset-1] == '\r' &&*/ bufferLine[offset] == '\n') {
// Found empty EOL. End of header section
if (offset == 1)
return "";//break;
// Found EOL. Add to array.
return new String(bufferLine, 0, offset - 1);
} else {
offset++;
}
}
} while (readBytes > 0);
return null;
}
private static int readData(@NonNull InputStream inputStream, @NonNull byte[] buffer, int offset, int length) throws IOException {
if (DEBUG)
Log.v(TAG, "readData(offset=" + offset + ", length=" + length + ")");
int readBytes;
int totalReadBytes = 0;
do {
readBytes = inputStream.read(buffer, offset + totalReadBytes, length - totalReadBytes);
if (readBytes > 0)
totalReadBytes += readBytes;
} while (readBytes >= 0 && totalReadBytes < length);
return totalReadBytes;
}
private static void dumpHeaders(@NonNull ArrayList<Pair<String, String>> headers) {
if (DEBUG) {
for (Pair<String, String> head : headers) {
Log.d(TAG, head.first + ": " + head.second);
}
}
}
@Nullable
private static String getHeader(@NonNull ArrayList<Pair<String, String>> headers, @NonNull String header) {
for (Pair<String, String> head : headers) {
// Session: ODgyODg3MjQ1MDczODk3NDk4Nw
String h = head.first.toLowerCase();
if (header.toLowerCase().equals(h)) {
return head.second;
}
}
// Not found
return null;
}
public void execute() {
if (DEBUG)
Log.v(TAG, "execute()");
listener.onRtspConnecting();
try {
InputStream inputStream = rtspSocket.getInputStream();
final OutputStream outputStream = debug ?
new LoggerOutputStream(rtspSocket.getOutputStream()) :
new BufferedOutputStream(rtspSocket.getOutputStream());
SdpInfo sdpInfo = new SdpInfo();
int cSeq = 0;
ArrayList<Pair<String, String>> headers;
int status;
String authToken = null;
Pair<String, String> digestRealmNonce = null;
// OPTIONS rtsp://10.0.1.78:8080/video/h264 RTSP/1.0 // CSeq: 1 // User-Agent: Lavf58.29.100
// RTSP/1.0 200 OK // CSeq: 1 // Public: OPTIONS, DESCRIBE, SETUP, PLAY, GET_PARAMETER, SET_PARAMETER, TEARDOWN // if (sendOptionsCommand) { checkExitFlag(exitFlag); sendOptionsCommand(outputStream, uriRtsp, ++cSeq, userAgent, authToken); status = readResponseStatusCode(inputStream); headers = readResponseHeaders(inputStream); dumpHeaders(headers); // Try once again with credentials if (status == 401) { digestRealmNonce = getHeaderWwwAuthenticateDigestRealmAndNonce(headers); if (digestRealmNonce == null) { String basicRealm = getHeaderWwwAuthenticateBasicRealm(headers); if (TextUtils.isEmpty(basicRealm)) { throw new IOException("Unknown authentication type"); } // Basic auth authToken = getBasicAuthHeader(username, password); } else { // Digest auth authToken = getDigestAuthHeader(username, password, "OPTIONS", uriRtsp, digestRealmNonce.first, digestRealmNonce.second); } checkExitFlag(exitFlag); final String token = authToken; sendOptionsCommand(outputStream, uriRtsp, ++cSeq, userAgent, authToken); status = readResponseStatusCode(inputStream); headers = readResponseHeaders(inputStream); dumpHeaders(headers); } if (DEBUG) Log.i(TAG, "OPTIONS status: " + status); checkStatusCode(status);
// DESCRIBE rtsp://10.0.1.78:8080/video/h264 RTSP/1.0 // Accept: application/sdp // CSeq: 2 // User-Agent: Lavf58.29.100
// RTSP/1.0 200 OK // CSeq: 2 // Content-Type: application/sdp // Content-Length: 364 // // v=0 // t=0 0 // a=range:npt=now- // m=video 0 RTP/AVP 96 // a=rtpmap:96 H264/90000 // a=fmtp:96 packetization-mode=1;sprop-parameter-sets=Z0KAH9oBABhpSCgwMDaFCag=,aM4G4g== // a=control:trackID=1 // m=audio 0 RTP/AVP 96 // a=rtpmap:96 mpeg4-generic/48000/1 // a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1188 // a=control:trackID=2 checkExitFlag(exitFlag);
sendDescribeCommand(outputStream, uriRtsp, ++cSeq, userAgent, authToken);
status = readResponseStatusCode(inputStream);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
// Try once again with credentials. OPTIONS command can be accepted without authentication.
if (status == 401) {
digestRealmNonce = getHeaderWwwAuthenticateDigestRealmAndNonce(headers);
if (digestRealmNonce == null) {
String basicRealm = getHeaderWwwAuthenticateBasicRealm(headers);
if (TextUtils.isEmpty(basicRealm)) {
throw new IOException("Unknown authentication type");
}
// Basic auth
authToken = getBasicAuthHeader(username, password);
} else {
// Digest auth
authToken = getDigestAuthHeader(username, password, "DESCRIBE", uriRtsp, digestRealmNonce.first, digestRealmNonce.second);
}
checkExitFlag(exitFlag);
sendDescribeCommand(outputStream, uriRtsp, ++cSeq, userAgent, authToken);
status = readResponseStatusCode(inputStream);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
}
if (DEBUG)
Log.i(TAG, "DESCRIBE status: " + status);
checkStatusCode(status);
int contentLength = getHeaderContentLength(headers);
if (contentLength > 0) {
String content = readContentAsText(inputStream, contentLength);
if (debug)
Log.i(TAG_DEBUG, "content: " + content);
try {
List<Pair<String, String>> params = getDescribeParams(content);
sdpInfo = getSdpInfoFromDescribeParams(params);
Log.i(TAG_DEBUG, sdpInfo.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
// SETUP rtsp://10.0.1.78:8080/video/h264/trackID=1 RTSP/1.0 // Transport: RTP/AVP/TCP;unicast;interleaved=0-1 // CSeq: 3 // User-Agent: Lavf58.29.100
// RTSP/1.0 200 OK // CSeq: 3 // Transport: RTP/AVP/TCP;unicast;interleaved=0-1 // Session: Mzk5MzY2MzUwMTg3NTc2Mzc5NQ;timeout=30 String session = null; for (int i = 0; i < 2; i++) { // i=0 - video track, i=1 - audio track checkExitFlag(exitFlag); Track track = (i == 0 ? (requestVideo ? sdpInfo.videoTrack : null) : (requestAudio ? sdpInfo.audioTrack : null)); if (track != null) { String uriRtspSetup = getUriForSetup(uriRtsp, track); if (uriRtspSetup == null) { Log.e(TAG, "Failed to get RTSP URI for SETUP"); continue; } if (digestRealmNonce != null) authToken = getDigestAuthHeader(username, password, "SETUP", uriRtspSetup, digestRealmNonce.first, digestRealmNonce.second); sendSetupCommand(outputStream, uriRtspSetup, ++cSeq, userAgent, authToken, session, (i == 0 ? "0-1" /video/ : "2-3" /audio/)); status = readResponseStatusCode(inputStream); if (DEBUG) Log.i(TAG, "SETUP status: " + status); checkStatusCode(status); headers = readResponseHeaders(inputStream); dumpHeaders(headers); session = getHeader(headers, "Session"); if (!TextUtils.isEmpty(session)) { // ODgyODg3MjQ1MDczODk3NDk4Nw;timeout=30 String[] params = TextUtils.split(session, ";"); session = params[0]; } if (DEBUG) Log.d(TAG, "SETUP session: " + session); if (TextUtils.isEmpty(session)) throw new IOException("Failed to get RTSP session"); } }
Uri uri = Uri.parse(uriRtsp);
Log.d(TAG, "uri.getHost() == " + uri.getHost() + " uri.getPort() == " + uri.getPort());
RTCPSender.SocketStuff socketStuff = new RTCPSender.SocketStuff(inputStream, outputStream, uriRtsp, ++cSeq, userAgent, authToken);
rtcpThread = new RTCPSender(uri.getHost(), rtpRcvPort + 1, 0, 5000, this, socketStuff);
rtcpThread.start();
if (TextUtils.isEmpty(session))
throw new IOException("Failed to get any media track");
// PLAY rtsp://10.0.1.78:8080/video/h264 RTSP/1.0 // Range: npt=0.000- // CSeq: 5 // User-Agent: Lavf58.29.100 // Session: Mzk5MzY2MzUwMTg3NTc2Mzc5NQ
// RTSP/1.0 200 OK // CSeq: 5 // RTP-Info: url=/video/h264;seq=56 // Session: Mzk5MzY2MzUwMTg3NTc2Mzc5NQ;timeout=30 checkExitFlag(exitFlag); if (digestRealmNonce != null) authToken = getDigestAuthHeader(username, password, "PLAY", uriRtsp /?/, digestRealmNonce.first, digestRealmNonce.second); sendPlayCommand(outputStream, uriRtsp, ++cSeq, userAgent, authToken, session); status = readResponseStatusCode(inputStream); if (DEBUG) Log.i(TAG, "PLAY status: " + status); checkStatusCode(status); headers = readResponseHeaders(inputStream); dumpHeaders(headers);
listener.onRtspConnected(sdpInfo);
if (sdpInfo.videoTrack != null) {
// Blocking call unless exitFlag set to true or thread.interrupt() called.
readRtpData(inputStream, sdpInfo, exitFlag, listener);
} else {
listener.onRtspFailed("No tracks found. RTSP server issue.");
}
Log.i(TAG, "onRtspDisconnected 1 ");
listener.onRtspDisconnected();
rtcpThread.stop();
rtcpThread = null;
} catch (UnauthorizedException e) {
e.printStackTrace();
listener.onRtspFailedUnauthorized();
} catch (InterruptedException e) {
// Thread interrupted. Expected behavior.
Log.i(TAG, "onRtspDisconnected 2 " + e.toString());
listener.onRtspDisconnected();
} catch (Exception e) {
e.printStackTrace();
listener.onRtspFailed(e.getMessage());
}
try {
rtspSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public interface RtspClientListener {
void onRtspConnecting();
void onRtspConnected(@NonNull SdpInfo sdpInfo);
void onRtspVideoNalUnitReceived(@NonNull byte[] data, int offset, int length, long timestamp);
void onRtspAudioSampleReceived(@NonNull byte[] data, int offset, int length, long timestamp);
void onRtspDisconnected();
void onRtspFailedUnauthorized();
void onRtspFailed(@Nullable String message);
}
public static class SdpInfo {
/**
* Session name (RFC 2327). In most cases RTSP server name.
*/
public @Nullable
String sessionName;
/**
* Session description (RFC 2327).
*/
public @Nullable
String sessionDescription;
public @Nullable
VideoTrack videoTrack;
public @Nullable
AudioTrack audioTrack;
@Override
public String toString() {
return "SdpInfo{" +
"sessionName='" + sessionName + '\'' +
", sessionDescription='" + sessionDescription + '\'' +
", videoTrack=" + videoTrack +
", audioTrack=" + audioTrack +
'}';
}
}
public abstract static class Track {
public String request;
public int payloadType;
}
public static class VideoTrack extends Track {
public int videoCodec = VIDEO_CODEC_H264;
public @Nullable
byte[] sps; // Both H.264 and H.265
public @Nullable
byte[] pps; // Both H.264 and H.265
// public @Nullable byte[] vps; // H.265 only // public @Nullable byte[] sei; // H.265 only }
public static class AudioTrack extends Track {
public int audioCodec = AUDIO_CODEC_AAC;
public int sampleRateHz; // 16000, 8000
public int channels; // 1 - mono, 2 - stereo
public String mode; // AAC-lbr, AAC-hbr
}
private static class UnauthorizedException extends IOException {
UnauthorizedException() {
super("Unauthorized");
}
}
private final static class NoResponseHeadersException extends IOException {
private static final long serialVersionUID = 1L;
}
public static class Builder {
private static final String DEFAULT_USER_AGENT = "Lavf58.29.100";
private final @NonNull
Socket rtspSocket;
private final @NonNull
String uriRtsp;
private final @NonNull
AtomicBoolean exitFlag;
private final @NonNull
RtspClientListener listener;
// private boolean sendOptionsCommand = true;
private boolean requestVideo = true;
private boolean requestAudio = true;
private boolean debug = false;
private @Nullable
String username = null;
private @Nullable
String password = null;
private @Nullable
String userAgent = DEFAULT_USER_AGENT;
public Builder(
@NonNull Socket rtspSocket,
@NonNull String uriRtsp,
@NonNull AtomicBoolean exitFlag,
@NonNull RtspClientListener listener) {
this.rtspSocket = rtspSocket;
this.uriRtsp = uriRtsp;
this.exitFlag = exitFlag;
this.listener = listener;
}
@NonNull
public Builder withDebug(boolean debug) {
this.debug = debug;
return this;
}
@NonNull
public Builder withCredentials(@Nullable String username, @Nullable String password) {
this.username = username;
this.password = password;
return this;
}
@NonNull
public Builder withUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
return this;
}
// @NonNull // public Builder sendOptionsCommand(boolean sendOptionsCommand) { // this.sendOptionsCommand = sendOptionsCommand; // return this; // }
@NonNull
public Builder requestVideo(boolean requestVideo) {
this.requestVideo = requestVideo;
return this;
}
@NonNull
public Builder requestAudio(boolean requestAudio) {
this.requestAudio = requestAudio;
return this;
}
@NonNull
public RtspClient build() {
return new RtspClient(this);
}
}
}
class LoggerOutputStream extends BufferedOutputStream { private boolean logging = true;
public LoggerOutputStream(@NonNull OutputStream out) {
super(out);
}
public synchronized void setLogging(boolean logging) {
this.logging = logging;
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
if (logging)
Log.i(RtspClient.TAG_DEBUG, new String(b, off, len));
}
}
static RtpHeader readHeader(BufferedInputStream buffInputStream) throws IOException {
// 24 01 00 1c 80 c8 00 06 7f 1d d2 c4
// 24 01 00 1c 80 c8 00 06 13 9b cf 60
buffInputStream.mark(RTP_HEADER_SIZE);
byte[] header = new byte[RTP_HEADER_SIZE];
// Skip 4 bytes (TCP only). No those bytes in UDP.
NetUtils.readData(buffInputStream, header, 0, 4);
if (DEBUG && header[0] == 0x24)
Log.d(TAG, header[1] == 0 ? "RTP packet" : "RTCP packet");
int packetSize = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF);
if (DEBUG)
Log.d(TAG, "Packet size: " + packetSize);
if(header[0] != 0x24){
return null;
}
if (NetUtils.readData(buffInputStream, header, 0, header.length) == header.length) {
return RtpHeader.parseData(header, packetSize);
} else {
return null;
}
}`
just take care of RTCPSender
Fixed in 1.2.8 version.
Hello Alex. While looking for RTSP libraries to read text from Camcorder, I found library-client-rtsp.aar and an example for xamain, it works. But I was not successful using xamarin android. Do you have any suggestions?
OnRtspFailed event occurs, and "message" parameter is null
It just worked for about one minute, and it would lost connection becasuse of no send Receiver Report packet to the ipcamera.Please Check it!