RTSP Client in Java: Complete Implementation Guide

RTSP (Real Time Streaming Protocol) is a network control protocol designed for use in entertainment and communications systems to control streaming media servers. Here's a comprehensive guide to building an RTSP client in Java.


RTSP Protocol Overview

Key RTSP Methods

  • DESCRIBE: Get media description
  • SETUP: Establish transport mechanism
  • PLAY: Start playback
  • PAUSE: Pause playback
  • TEARDOWN: End session
  • OPTIONS: Discover server capabilities

RTSP Headers

  • CSeq: Sequence number
  • Session: Session identifier
  • Transport: RTP/RTCP parameters
  • Range: Playback range

Basic RTSP Client Implementation

Example 1: Basic RTSP Client

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
public class BasicRTSPClient {
private static final String RTSP_VERSION = "RTSP/1.0";
private static final String USER_AGENT = "JavaRTSPClient/1.0";
private Socket rtspSocket;
private BufferedReader input;
private PrintWriter output;
private String sessionId;
private int cSeq = 1;
private String serverUrl;
public BasicRTSPClient(String serverUrl) {
this.serverUrl = serverUrl;
}
public boolean connect() throws IOException {
try {
URL url = new URL(serverUrl);
String host = url.getHost();
int port = url.getPort() != -1 ? url.getPort() : 554; // Default RTSP port
rtspSocket = new Socket(host, port);
input = new BufferedReader(new InputStreamReader(rtspSocket.getInputStream()));
output = new PrintWriter(rtspSocket.getOutputStream(), true);
System.out.println("Connected to RTSP server: " + host + ":" + port);
return true;
} catch (Exception e) {
System.err.println("Failed to connect: " + e.getMessage());
return false;
}
}
public RTSPResponse sendRequest(String method, String url, Map<String, String> headers) 
throws IOException {
StringBuilder request = new StringBuilder();
// Request line
request.append(method).append(" ").append(url).append(" ").append(RTSP_VERSION).append("\r\n");
// Headers
headers.put("CSeq", String.valueOf(cSeq++));
headers.put("User-Agent", USER_AGENT);
if (sessionId != null) {
headers.put("Session", sessionId);
}
for (Map.Entry<String, String> header : headers.entrySet()) {
request.append(header.getKey()).append(": ").append(header.getValue()).append("\r\n");
}
// Empty line to end headers
request.append("\r\n");
// Send request
output.print(request.toString());
output.flush();
System.out.println("Sent: " + method + " " + url);
// Read response
return readResponse();
}
private RTSPResponse readResponse() throws IOException {
String statusLine = input.readLine();
if (statusLine == null) {
throw new IOException("Server closed connection");
}
// Parse status line
String[] statusParts = statusLine.split(" ", 3);
if (statusParts.length < 3) {
throw new IOException("Invalid status line: " + statusLine);
}
int statusCode = Integer.parseInt(statusParts[1]);
String statusMessage = statusParts[2];
// Read headers
Map<String, String> headers = new HashMap<>();
String line;
while ((line = input.readLine()) != null && !line.isEmpty()) {
int colonIndex = line.indexOf(':');
if (colonIndex > 0) {
String key = line.substring(0, colonIndex).trim();
String value = line.substring(colonIndex + 1).trim();
headers.put(key, value);
}
}
// Extract session ID if present
if (headers.containsKey("Session")) {
String sessionHeader = headers.get("Session");
// Session header might have timeout: Session: 12345;timeout=60
String[] sessionParts = sessionHeader.split(";");
sessionId = sessionParts[0].trim();
}
return new RTSPResponse(statusCode, statusMessage, headers);
}
public MediaDescription describe(String mediaUrl) throws IOException {
Map<String, String> headers = new HashMap<>();
headers.put("Accept", "application/sdp");
RTSPResponse response = sendRequest("DESCRIBE", mediaUrl, headers);
if (response.statusCode == 200) {
// Read SDP content (if any)
StringBuilder sdpContent = new StringBuilder();
String line;
while (input.ready() && (line = input.readLine()) != null) {
sdpContent.append(line).append("\n");
}
return parseSDP(sdpContent.toString());
} else {
throw new IOException("DESCRIBE failed: " + response.statusCode + " " + response.statusMessage);
}
}
private MediaDescription parseSDP(String sdpContent) {
MediaDescription description = new MediaDescription();
String[] lines = sdpContent.split("\n");
for (String line : lines) {
if (line.startsWith("m=")) {
// Media line: m=video 0 RTP/AVP 96
String[] parts = line.substring(2).split(" ");
if (parts.length >= 4) {
description.mediaType = parts[0];
description.payloadType = Integer.parseInt(parts[3]);
}
} else if (line.startsWith("a=control:")) {
description.controlUrl = line.substring(10);
} else if (line.startsWith("a=rtpmap:")) {
// a=rtpmap:96 H264/90000
String[] parts = line.substring(9).split(" ");
if (parts.length >= 2) {
String[] codecParts = parts[1].split("/");
description.codec = codecParts[0];
if (codecParts.length > 1) {
description.clockRate = Integer.parseInt(codecParts[1]);
}
}
}
}
return description;
}
public SetupResponse setup(String mediaUrl, int clientRtpPort, int clientRtcpPort) 
throws IOException {
Map<String, String> headers = new HashMap<>();
String transport = String.format("RTP/AVP;unicast;client_port=%d-%d", 
clientRtpPort, clientRtcpPort);
headers.put("Transport", transport);
RTSPResponse response = sendRequest("SETUP", mediaUrl, headers);
if (response.statusCode == 200) {
SetupResponse setupResponse = new SetupResponse();
setupResponse.sessionId = sessionId;
// Parse transport response
if (response.headers.containsKey("Transport")) {
String transportHeader = response.headers.get("Transport");
setupResponse.serverRtpPort = parseServerPort(transportHeader, "server_port");
setupResponse.serverRtcpPort = parseServerPort(transportHeader, "server_port") + 1;
}
return setupResponse;
} else {
throw new IOException("SETUP failed: " + response.statusCode + " " + response.statusMessage);
}
}
private int parseServerPort(String transportHeader, String paramName) {
Pattern pattern = Pattern.compile(paramName + "=(\\d+)-(\\d+)");
Matcher matcher = pattern.matcher(transportHeader);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
}
return -1;
}
public boolean play(String mediaUrl, String range) throws IOException {
Map<String, String> headers = new HashMap<>();
if (range != null) {
headers.put("Range", range);
}
RTSPResponse response = sendRequest("PLAY", mediaUrl, headers);
return response.statusCode == 200;
}
public boolean pause(String mediaUrl) throws IOException {
RTSPResponse response = sendRequest("PAUSE", mediaUrl, new HashMap<>());
return response.statusCode == 200;
}
public boolean teardown(String mediaUrl) throws IOException {
RTSPResponse response = sendRequest("TEARDOWN", mediaUrl, new HashMap<>());
// Close connection
if (rtspSocket != null && !rtspSocket.isClosed()) {
rtspSocket.close();
}
return response.statusCode == 200;
}
public void close() throws IOException {
if (rtspSocket != null && !rtspSocket.isClosed()) {
rtspSocket.close();
}
}
// Data classes
public static class RTSPResponse {
public final int statusCode;
public final String statusMessage;
public final Map<String, String> headers;
public RTSPResponse(int statusCode, String statusMessage, Map<String, String> headers) {
this.statusCode = statusCode;
this.statusMessage = statusMessage;
this.headers = headers;
}
@Override
public String toString() {
return "RTSPResponse{" +
"statusCode=" + statusCode +
", statusMessage='" + statusMessage + '\'' +
", headers=" + headers +
'}';
}
}
public static class MediaDescription {
public String mediaType;
public int payloadType;
public String controlUrl;
public String codec;
public int clockRate;
@Override
public String toString() {
return String.format("MediaDescription{type=%s, payload=%d, codec=%s, control=%s}",
mediaType, payloadType, codec, controlUrl);
}
}
public static class SetupResponse {
public String sessionId;
public int serverRtpPort;
public int serverRtcpPort;
@Override
public String toString() {
return String.format("SetupResponse{session=%s, serverRtpPort=%d, serverRtcpPort=%d}",
sessionId, serverRtpPort, serverRtcpPort);
}
}
public static void main(String[] args) {
BasicRTSPClient client = new BasicRTSPClient("rtsp://example.com/media.stream");
try {
// Connect to server
if (!client.connect()) {
return;
}
// Describe media
MediaDescription description = client.describe("rtsp://example.com/media.stream");
System.out.println("Media description: " + description);
// Setup stream (using random ports)
int clientRtpPort = 50000;
int clientRtcpPort = 50001;
SetupResponse setup = client.setup(description.controlUrl, clientRtpPort, clientRtcpPort);
System.out.println("Setup response: " + setup);
// Play stream
if (client.play(description.controlUrl, "npt=0.0-")) {
System.out.println("Playback started");
// Keep playing for 10 seconds
Thread.sleep(10000);
// Pause
client.pause(description.controlUrl);
System.out.println("Playback paused");
// Teardown
client.teardown(description.controlUrl);
System.out.println("Session terminated");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

RTP Packet Processing

Example 2: RTP Packet Receiver

import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.*;
public class RTPReceiver {
private final int rtpPort;
private final int rtcpPort;
private DatagramSocket rtpSocket;
private DatagramSocket rtcpSocket;
private volatile boolean running;
private RTPPacketHandler packetHandler;
public RTPReceiver(int rtpPort, int rtcpPort) {
this.rtpPort = rtpPort;
this.rtcpPort = rtcpPort;
}
public void setPacketHandler(RTPPacketHandler handler) {
this.packetHandler = handler;
}
public void start() throws SocketException {
rtpSocket = new DatagramSocket(rtpPort);
rtcpSocket = new DatagramSocket(rtcpPort);
running = true;
// Start RTP receiver thread
Thread rtpThread = new Thread(this::receiveRTPPackets, "RTP-Receiver");
rtpThread.setDaemon(true);
rtpThread.start();
// Start RTCP receiver thread
Thread rtcpThread = new Thread(this::receiveRTCPPackets, "RTCP-Receiver");
rtcpThread.setDaemon(true);
rtcpThread.start();
System.out.println("RTP receiver started on port " + rtpPort);
System.out.println("RTCP receiver started on port " + rtcpPort);
}
public void stop() {
running = false;
if (rtpSocket != null) {
rtpSocket.close();
}
if (rtcpSocket != null) {
rtcpSocket.close();
}
}
private void receiveRTPPackets() {
byte[] buffer = new byte[4096];
while (running && !rtpSocket.isClosed()) {
try {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
rtpSocket.receive(packet);
RTPPacket rtpPacket = parseRTPPacket(packet.getData(), packet.getLength());
if (rtpPacket != null && packetHandler != null) {
packetHandler.handleRTPPacket(rtpPacket);
}
} catch (IOException e) {
if (running) {
System.err.println("Error receiving RTP packet: " + e.getMessage());
}
}
}
}
private void receiveRTCPPackets() {
byte[] buffer = new byte[4096];
while (running && !rtcpSocket.isClosed()) {
try {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
rtcpSocket.receive(packet);
RTCPPacket rtcpPacket = parseRTCPPacket(packet.getData(), packet.getLength());
if (rtcpPacket != null && packetHandler != null) {
packetHandler.handleRTCPPacket(rtcpPacket);
}
} catch (IOException e) {
if (running) {
System.err.println("Error receiving RTCP packet: " + e.getMessage());
}
}
}
}
private RTPPacket parseRTPPacket(byte[] data, int length) {
if (length < 12) return null; // Minimum RTP header size
ByteBuffer buffer = ByteBuffer.wrap(data, 0, length);
buffer.order(ByteOrder.BIG_ENDIAN);
RTPPacket packet = new RTPPacket();
// First byte: Version (2 bits), Padding (1 bit), Extension (1 bit), CSRC Count (4 bits)
byte firstByte = buffer.get();
packet.version = (firstByte >> 6) & 0x03;
packet.padding = ((firstByte >> 5) & 0x01) == 1;
packet.extension = ((firstByte >> 4) & 0x01) == 1;
packet.csrcCount = firstByte & 0x0F;
// Second byte: Marker (1 bit), Payload Type (7 bits)
byte secondByte = buffer.get();
packet.marker = ((secondByte >> 7) & 0x01) == 1;
packet.payloadType = secondByte & 0x7F;
// Sequence number (16 bits)
packet.sequenceNumber = buffer.getShort() & 0xFFFF;
// Timestamp (32 bits)
packet.timestamp = buffer.getInt();
// SSRC (32 bits)
packet.ssrc = buffer.getInt();
// CSRC list (if any)
packet.csrc = new int[packet.csrcCount];
for (int i = 0; i < packet.csrcCount; i++) {
packet.csrc[i] = buffer.getInt();
}
// Extension header (if present)
if (packet.extension) {
packet.extensionProfile = buffer.getShort() & 0xFFFF;
packet.extensionLength = buffer.getShort() & 0xFFFF;
packet.extensionData = new byte[packet.extensionLength * 4];
buffer.get(packet.extensionData);
}
// Payload
int payloadLength = length - buffer.position();
packet.payload = new byte[payloadLength];
buffer.get(packet.payload);
return packet;
}
private RTCPPacket parseRTCPPacket(byte[] data, int length) {
if (length < 8) return null; // Minimum RTCP header size
RTCPPacket packet = new RTCPPacket();
packet.rawData = Arrays.copyOf(data, length);
// Basic parsing - in practice, you'd parse different RTCP packet types
ByteBuffer buffer = ByteBuffer.wrap(data, 0, length);
buffer.order(ByteOrder.BIG_ENDIAN);
// First byte: Version (2 bits), Padding (1 bit), RC (5 bits)
byte firstByte = buffer.get();
packet.version = (firstByte >> 6) & 0x03;
packet.padding = ((firstByte >> 5) & 0x01) == 1;
packet.reportCount = firstByte & 0x1F;
// Packet Type
packet.packetType = buffer.get() & 0xFF;
// Length
packet.length = buffer.getShort() & 0xFFFF;
// SSRC
packet.ssrc = buffer.getInt();
return packet;
}
// Data classes
public static class RTPPacket {
public int version;
public boolean padding;
public boolean extension;
public int csrcCount;
public boolean marker;
public int payloadType;
public int sequenceNumber;
public int timestamp;
public int ssrc;
public int[] csrc;
public int extensionProfile;
public int extensionLength;
public byte[] extensionData;
public byte[] payload;
@Override
public String toString() {
return String.format(
"RTPPacket{seq=%d, ts=%d, pt=%d, marker=%b, payload=%d bytes}",
sequenceNumber, timestamp, payloadType, marker, 
payload != null ? payload.length : 0
);
}
}
public static class RTCPPacket {
public int version;
public boolean padding;
public int reportCount;
public int packetType;
public int length;
public int ssrc;
public byte[] rawData;
@Override
public String toString() {
return String.format(
"RTCPPacket{type=%d, rc=%d, length=%d}",
packetType, reportCount, length
);
}
}
public interface RTPPacketHandler {
void handleRTPPacket(RTPPacket packet);
void handleRTCPPacket(RTCPPacket packet);
}
}

Complete RTSP Client with RTP Reception

Example 3: Complete Streaming Client

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import javax.sound.sampled.*;
public class RTSPStreamingClient {
private final String rtspUrl;
private final String mediaUrl;
private BasicRTSPClient rtspClient;
private RTPReceiver rtpReceiver;
private MediaPlayer mediaPlayer;
private volatile boolean isPlaying;
public RTSPStreamingClient(String rtspUrl, String mediaUrl) {
this.rtspUrl = rtspUrl;
this.mediaUrl = mediaUrl;
}
public boolean startStream() {
try {
// Initialize RTSP client
rtspClient = new BasicRTSPClient(rtspUrl);
if (!rtspClient.connect()) {
return false;
}
// Describe media
BasicRTSPClient.MediaDescription mediaDesc = rtspClient.describe(mediaUrl);
System.out.println("Media described: " + mediaDesc);
// Find available ports for RTP/RTCP
int[] ports = findAvailablePorts();
int rtpPort = ports[0];
int rtcpPort = ports[1];
// Setup RTP receiver
rtpReceiver = new RTPReceiver(rtpPort, rtcpPort);
setupPacketHandling(mediaDesc);
rtpReceiver.start();
// Setup stream with RTSP server
BasicRTSPClient.SetupResponse setup = rtspClient.setup(
mediaDesc.controlUrl, rtpPort, rtcpPort);
System.out.println("Stream setup: " + setup);
// Start media player based on media type
if ("audio".equals(mediaDesc.mediaType)) {
mediaPlayer = new AudioPlayer(mediaDesc);
} else if ("video".equals(mediaDesc.mediaType)) {
mediaPlayer = new VideoPlayer(mediaDesc);
}
// Start playback
if (rtspClient.play(mediaDesc.controlUrl, "npt=0.0-")) {
isPlaying = true;
System.out.println("Stream playback started");
return true;
}
} catch (Exception e) {
System.err.println("Failed to start stream: " + e.getMessage());
e.printStackTrace();
}
return false;
}
public void stopStream() {
isPlaying = false;
try {
if (rtspClient != null) {
rtspClient.teardown(mediaUrl);
}
if (rtpReceiver != null) {
rtpReceiver.stop();
}
if (mediaPlayer != null) {
mediaPlayer.stop();
}
System.out.println("Stream stopped");
} catch (Exception e) {
System.err.println("Error stopping stream: " + e.getMessage());
}
}
public boolean isPlaying() {
return isPlaying;
}
private int[] findAvailablePorts() throws IOException {
try (DatagramSocket testSocket1 = new DatagramSocket(0);
DatagramSocket testSocket2 = new DatagramSocket(0)) {
int port1 = testSocket1.getLocalPort();
int port2 = testSocket2.getLocalPort();
// Ensure ports are consecutive for RTP/RTCP
if (port2 == port1 + 1) {
return new int[]{port1, port2};
} else {
return new int[]{port1, port1 + 1};
}
}
}
private void setupPacketHandling(BasicRTSPClient.MediaDescription mediaDesc) {
rtpReceiver.setPacketHandler(new RTPReceiver.RTPPacketHandler() {
private final Map<Integer, Long> packetStats = new ConcurrentHashMap<>();
private long lastStatsTime = System.currentTimeMillis();
private int packetCount = 0;
private int lostPackets = 0;
private int lastSequence = -1;
@Override
public void handleRTPPacket(RTPReceiver.RTPPacket packet) {
if (!isPlaying) return;
packetCount++;
// Detect packet loss
if (lastSequence != -1) {
int expectedSequence = (lastSequence + 1) & 0xFFFF;
if (packet.sequenceNumber != expectedSequence) {
int lost = (packet.sequenceNumber - expectedSequence) & 0xFFFF;
lostPackets += lost;
System.out.printf("Packet loss detected: expected %d, got %d (lost: %d)%n",
expectedSequence, packet.sequenceNumber, lost);
}
}
lastSequence = packet.sequenceNumber;
// Process packet based on media type
if (mediaPlayer != null) {
mediaPlayer.processPacket(packet);
}
// Print statistics every 5 seconds
long currentTime = System.currentTimeMillis();
if (currentTime - lastStatsTime >= 5000) {
double packetLossRate = packetCount > 0 ? (double) lostPackets / packetCount * 100 : 0;
System.out.printf("RTP Stats: packets=%d, lost=%d, loss rate=%.2f%%%n",
packetCount, lostPackets, packetLossRate);
packetCount = 0;
lostPackets = 0;
lastStatsTime = currentTime;
}
}
@Override
public void handleRTCPPacket(RTPReceiver.RTCPPacket packet) {
// Handle RTCP packets (sender/receiver reports)
System.out.println("RTCP packet received: " + packet);
}
});
}
// Media player interface
interface MediaPlayer {
void processPacket(RTPReceiver.RTPPacket packet);
void stop();
}
// Audio player implementation (simplified)
static class AudioPlayer implements MediaPlayer {
private final BasicRTSPClient.MediaDescription mediaDesc;
private SourceDataLine audioLine;
private final BlockingQueue<byte[]> audioQueue = new LinkedBlockingQueue<>(100);
private volatile boolean running;
public AudioPlayer(BasicRTSPClient.MediaDescription mediaDesc) {
this.mediaDesc = mediaDesc;
initializeAudio();
}
private void initializeAudio() {
try {
AudioFormat format = new AudioFormat(
mediaDesc.clockRate, // sample rate
16,                 // sample size in bits
1,                  // channels
true,               // signed
false               // big endian
);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
audioLine = (SourceDataLine) AudioSystem.getLine(info);
audioLine.open(format);
audioLine.start();
running = true;
// Start audio playback thread
Thread playbackThread = new Thread(this::playbackLoop, "Audio-Playback");
playbackThread.setDaemon(true);
playbackThread.start();
System.out.println("Audio player initialized");
} catch (Exception e) {
System.err.println("Failed to initialize audio: " + e.getMessage());
}
}
@Override
public void processPacket(RTPReceiver.RTPPacket packet) {
if (audioQueue.size() < 100) { // Prevent queue from growing too large
audioQueue.offer(packet.payload);
}
}
private void playbackLoop() {
while (running) {
try {
byte[] audioData = audioQueue.poll(100, TimeUnit.MILLISECONDS);
if (audioData != null && audioLine != null) {
audioLine.write(audioData, 0, audioData.length);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
@Override
public void stop() {
running = false;
if (audioLine != null) {
audioLine.stop();
audioLine.close();
}
}
}
// Video player implementation (placeholder)
static class VideoPlayer implements MediaPlayer {
private final BasicRTSPClient.MediaDescription mediaDesc;
public VideoPlayer(BasicRTSPClient.MediaDescription mediaDesc) {
this.mediaDesc = mediaDesc;
System.out.println("Video player initialized for codec: " + mediaDesc.codec);
}
@Override
public void processPacket(RTPReceiver.RTPPacket packet) {
// In a real implementation, you would:
// 1. Depacketize H.264/H.265 frames
// 2. Decode using hardware/software decoder
// 3. Render frames on screen
if (packet.marker) {
System.out.println("Video frame completed (marker bit set)");
}
// Simple packet logging
if (packet.sequenceNumber % 100 == 0) {
System.out.println("Video packet: " + packet);
}
}
@Override
public void stop() {
System.out.println("Video player stopped");
}
}
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Usage: java RTSPStreamingClient <rtsp_url> <media_url>");
System.out.println("Example: java RTSPStreamingClient rtsp://server.com rtsp://server.com/media.stream");
return;
}
RTSPStreamingClient client = new RTSPStreamingClient(args[0], args[1]);
// Add shutdown hook for cleanup
Runtime.getRuntime().addShutdownHook(new Thread(client::stopStream));
// Start streaming
if (client.startStream()) {
System.out.println("Stream started successfully. Press Ctrl+C to stop.");
// Keep main thread alive while streaming
while (client.isPlaying()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
} else {
System.err.println("Failed to start stream");
}
}
}

Advanced Features

Example 4: RTSP Client with Authentication

import java.util.*;
import java.util.Base64;
public class AuthenticatedRTSPClient extends BasicRTSPClient {
private String username;
private String password;
private String authHeader;
public AuthenticatedRTSPClient(String serverUrl, String username, String password) {
super(serverUrl);
this.username = username;
this.password = password;
}
@Override
public RTSPResponse sendRequest(String method, String url, Map<String, String> headers) 
throws IOException {
// Add authentication header if available
if (authHeader != null) {
headers.put("Authorization", authHeader);
}
RTSPResponse response = super.sendRequest(method, url, headers);
// Handle 401 Unauthorized
if (response.statusCode == 401 && response.headers.containsKey("WWW-Authenticate")) {
String authChallenge = response.headers.get("WWW-Authenticate");
authHeader = generateAuthHeader(method, url, authChallenge);
// Retry request with authentication
if (authHeader != null) {
headers.put("Authorization", authHeader);
return super.sendRequest(method, url, headers);
}
}
return response;
}
private String generateAuthHeader(String method, String url, String authChallenge) {
if (authChallenge.startsWith("Basic")) {
return generateBasicAuthHeader();
} else if (authChallenge.startsWith("Digest")) {
return generateDigestAuthHeader(method, url, authChallenge);
}
return null;
}
private String generateBasicAuthHeader() {
String credentials = username + ":" + password;
String encoded = Base64.getEncoder().encodeToString(credentials.getBytes());
return "Basic " + encoded;
}
private String generateDigestAuthHeader(String method, String url, String authChallenge) {
// Parse digest challenge
Map<String, String> challengeParams = parseDigestChallenge(authChallenge);
String realm = challengeParams.get("realm");
String nonce = challengeParams.get("nonce");
String qop = challengeParams.get("qop");
// Generate digest response (simplified)
String ha1 = md5(username + ":" + realm + ":" + password);
String ha2 = md5(method + ":" + url);
String response = md5(ha1 + ":" + nonce + ":" + ha2);
return String.format(
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
username, realm, nonce, url, response
);
}
private Map<String, String> parseDigestChallenge(String challenge) {
Map<String, String> params = new HashMap<>();
String[] parts = challenge.substring(7).split(","); // Remove "Digest "
for (String part : parts) {
String[] keyValue = part.trim().split("=", 2);
if (keyValue.length == 2) {
String value = keyValue[1].replace("\"", "");
params.put(keyValue[0].trim(), value);
}
}
return params;
}
private String md5(String input) {
try {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("MD5 calculation failed", e);
}
}
}

Best Practices and Error Handling

Example 5: Robust RTSP Client with Error Recovery

import java.util.concurrent.atomic.*;
public class RobustRTSPClient extends RTSPStreamingClient {
private final AtomicInteger retryCount = new AtomicInteger(0);
private final int maxRetries;
private final long retryDelayMs;
public RobustRTSPClient(String rtspUrl, String mediaUrl, int maxRetries, long retryDelayMs) {
super(rtspUrl, mediaUrl);
this.maxRetries = maxRetries;
this.retryDelayMs = retryDelayMs;
}
@Override
public boolean startStream() {
int attempts = 0;
while (attempts <= maxRetries) {
try {
attempts++;
System.out.printf("Stream attempt %d/%d%n", attempts, maxRetries + 1);
if (super.startStream()) {
retryCount.set(0);
return true;
}
} catch (Exception e) {
System.err.printf("Stream attempt %d failed: %s%n", attempts, e.getMessage());
}
if (attempts <= maxRetries) {
System.out.printf("Retrying in %d ms...%n", retryDelayMs);
try {
Thread.sleep(retryDelayMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
System.err.println("All stream attempts failed");
return false;
}
public void handleNetworkError() {
int currentRetries = retryCount.incrementAndGet();
if (currentRetries <= maxRetries) {
System.out.printf("Network error detected. Retry %d/%d%n", currentRetries, maxRetries);
// Stop current stream
super.stopStream();
// Wait before retry
try {
Thread.sleep(retryDelayMs);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
// Attempt to restart
if (startStream()) {
System.out.println("Stream recovered successfully");
}
} else {
System.err.println("Max retries exceeded. Giving up.");
}
}
public int getRetryCount() {
return retryCount.get();
}
}

Performance Optimization

Key Optimization Techniques

  1. Buffer Management: Use direct buffers for better I/O performance
  2. Thread Pooling: Reuse threads for RTP packet processing
  3. Zero-copy: Minimize data copying between buffers
  4. JNI Integration: Use native libraries for video decoding
  5. Memory Pooling: Reuse byte arrays to reduce GC pressure

Common RTSP Servers for Testing

  • VLC Media Player: Can act as RTSP server
  • FFmpeg: Can stream to RTSP
  • Live555: Popular open-source RTSP server
  • Wowza Streaming Engine: Commercial streaming server

Conclusion

Key Features Implemented

  1. RTSP Protocol Handling: DESCRIBE, SETUP, PLAY, PAUSE, TEARDOWN
  2. RTP/RTCP Reception: Packet parsing and statistics
  3. Media Playback: Audio and video stream handling
  4. Authentication: Basic and Digest authentication support
  5. Error Recovery: Automatic reconnection and retry logic

Use Cases

  • IP Camera Monitoring: Security and surveillance systems
  • Live Streaming: Broadcasting events and conferences
  • Video on Demand: Media distribution systems
  • Video Conferencing: Real-time communication
  • Media Analysis: Processing and analyzing streamed content

Production Considerations

  • Implement proper logging and monitoring
  • Add support for SSL/TLS (RTSPS)
  • Implement congestion control
  • Add support for multiple simultaneous streams
  • Integrate with media processing pipelines

This RTSP client implementation provides a solid foundation for building streaming applications in Java, with extensibility for adding codec-specific processing and advanced features.


Next Steps: Explore integrating with JavaFX for video display, adding H.264/H.265 decoding capabilities, or implementing RTSP server functionality for bidirectional streaming.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper