Files
HawkeyeVision/framework/Inspectron.HawkEye/RTSP/Client/RTSPClient.cs
2025-08-19 12:45:50 +02:00

1129 lines
58 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Inspectron.HawkEye.RTSP.Messages;
namespace Inspectron.HawkEye.RTSP.Client
{
public class RTSPClient
{
private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();
// Events that applications can receive
public event Received_SPS_PPS_Delegate Received_SPS_PPS;
public event Received_VPS_SPS_PPS_Delegate Received_VPS_SPS_PPS;
public event Received_NALs_Delegate Received_NALs;
public event Received_G711_Delegate Received_G711;
public event Received_AMR_Delegate Received_AMR;
public event Received_AAC_Delegate Received_AAC;
// Delegated functions (essentially the function prototype)
public delegate void Received_SPS_PPS_Delegate (byte[] sps, byte[] pps); // H264
public delegate void Received_VPS_SPS_PPS_Delegate(byte[] vps, byte[] sps, byte[] pps); // H265
public delegate void Received_NALs_Delegate (List<byte[]> nal_units); // H264 or H265
public delegate void Received_G711_Delegate (String format, List<byte[]> g711);
public delegate void Received_AMR_Delegate (String format, List<byte[]> amr);
public delegate void Received_AAC_Delegate(String format, List<byte[]> aac, uint ObjectType, uint FrequencyIndex, uint ChannelConfiguration);
public enum RTP_TRANSPORT { UDP, TCP, MULTICAST, UNKNOWN };
public enum MEDIA_REQUEST { VIDEO_ONLY, AUDIO_ONLY, VIDEO_AND_AUDIO };
private enum RTSP_STATUS { WaitingToConnect, Connecting, ConnectFailed, Connected };
Inspectron.HawkEye.RTSP.RtspTcpTransport rtsp_socket = null; // RTSP connection
volatile RTSP_STATUS rtsp_socket_status = RTSP_STATUS.WaitingToConnect;
Inspectron.HawkEye.RTSP.RtspListener rtsp_client = null; // this wraps around a the RTSP tcp_socket stream
RTP_TRANSPORT rtp_transport = RTP_TRANSPORT.UNKNOWN; // Mode, either RTP over UDP or RTP over TCP using the RTSP socket
Inspectron.HawkEye.RTSP.UDPSocket video_udp_pair = null; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode
Inspectron.HawkEye.RTSP.UDPSocket audio_udp_pair = null; // Pair of UDP ports used in RTP over UDP mode or in MULTICAST mode
String url = ""; // RTSP URL (username & password will be stripped out
String username = ""; // Username
String password = ""; // Password
String hostname = ""; // RTSP Server hostname or IP address
int port = 0; // RTSP Server TCP Port number
String session = ""; // RTSP Session
String auth_type = null; // cached from most recent WWW-Authenticate reply
String realm = null; // cached from most recent WWW-Authenticate reply
String nonce = null; // cached from most recent WWW-Authenticate reply
uint ssrc = 12345;
bool client_wants_video = false; // Client wants to receive Video
bool client_wants_audio = false; // Client wants to receive Audio
Uri video_uri = null; // URI used for the Video Track
int video_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value. Bosch use 35)
int video_data_channel = -1; // RTP Channel Number used for the video RTP stream or the UDP port number
int video_rtcp_channel = -1; // RTP Channel Number used for the video RTCP status report messages OR the UDP port number
bool h264_sps_pps_fired = false; // True if the SDP included a sprop-Parameter-Set for H264 video
bool h265_vps_sps_pps_fired = false; // True if the SDP included a sprop-vps, sprop-sps and sprop_pps for H265 video
string video_codec = ""; // Codec used with Payload Types 96..127 (eg "H264")
Uri audio_uri = null; // URI used for the Audio Track
int audio_payload = -1; // Payload Type for the Video. (often 96 which is the first dynamic payload value)
int audio_data_channel = -1; // RTP Channel Number used for the audio RTP stream or the UDP port number
int audio_rtcp_channel = -1; // RTP Channel Number used for the audio RTCP status report messages OR the UDP port number
string audio_codec = ""; // Codec used with Payload Types (eg "PCMA" or "AMR")
bool server_supports_get_parameter = false; // Used with RTSP keepalive
bool server_supports_set_parameter = false; // Used with RTSP keepalive
System.Timers.Timer keepalive_timer = null; // Used with RTSP keepalive
Inspectron.HawkEye.RTSP.H264Payload h264Payload = null;
Inspectron.HawkEye.RTSP.H265Payload h265Payload = null;
Inspectron.HawkEye.RTSP.G711Payload g711Payload = new Inspectron.HawkEye.RTSP.G711Payload();
Inspectron.HawkEye.RTSP.AMRPayload amrPayload = new Inspectron.HawkEye.RTSP.AMRPayload();
Inspectron.HawkEye.RTSP.AACPayload aacPayload = null;
List<Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup> setup_messages = new List<Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup>(); // setup messages still to send
// Constructor
public RTSPClient() {
bool writeLogsToConsole = true;
if (writeLogsToConsole)
{
var config = new NLog.Config.LoggingConfiguration();
// Targets where to log to: Console
var logconsole = new NLog.Targets.ConsoleTarget("logconsole");
// Rules for mapping loggers to targets
config.AddRule(NLog.LogLevel.Debug, NLog.LogLevel.Fatal, logconsole);
// Apply config
NLog.LogManager.Configuration = config;
}
}
public void Connect(String url, RTP_TRANSPORT rtp_transport, MEDIA_REQUEST media_request = MEDIA_REQUEST.VIDEO_AND_AUDIO)
{
Inspectron.HawkEye.RTSP.RtspUtils.RegisterUri();
_logger.Debug("Connecting to " + url);
this.url = url;
// Use URI to extract username and password
// and to make a new URL without the username and password
try {
Uri uri = new Uri(this.url);
hostname = uri.Host;
port = uri.Port;
if (uri.UserInfo.Length > 0) {
username = uri.UserInfo.Split(new char[] {':'})[0];
password = uri.UserInfo.Split(new char[] {':'})[1];
this.url = uri.GetComponents((UriComponents.AbsoluteUri &~ UriComponents.UserInfo),
UriFormat.UriEscaped);
}
} catch {
username = null;
password = null;
}
// We can ask the RTSP server for Video, Audio or both. If we don't want audio we don't need to SETUP the audio channal or receive it
client_wants_video = false;
client_wants_audio = false;
if (media_request == MEDIA_REQUEST.VIDEO_ONLY || media_request == MEDIA_REQUEST.VIDEO_AND_AUDIO) client_wants_video = true;
if (media_request == MEDIA_REQUEST.AUDIO_ONLY || media_request == MEDIA_REQUEST.VIDEO_AND_AUDIO) client_wants_audio = true;
// Connect to a RTSP Server. The RTSP session is a TCP connection
rtsp_socket_status = RTSP_STATUS.Connecting;
try
{
rtsp_socket = new Inspectron.HawkEye.RTSP.RtspTcpTransport(hostname, port);
}
catch
{
rtsp_socket_status = RTSP_STATUS.ConnectFailed;
_logger.Warn("Error - did not connect");
return;
}
if (rtsp_socket.Connected == false)
{
rtsp_socket_status = RTSP_STATUS.ConnectFailed;
_logger.Warn("Error - did not connect");
return;
}
rtsp_socket_status = RTSP_STATUS.Connected;
// Connect a RTSP Listener to the RTSP Socket (or other Stream) to send RTSP messages and listen for RTSP replies
rtsp_client = new Inspectron.HawkEye.RTSP.RtspListener(rtsp_socket);
rtsp_client.AutoReconnect = false;
rtsp_client.MessageReceived += Rtsp_MessageReceived;
rtsp_client.DataReceived += Rtp_DataReceived;
rtsp_client.Start(); // start listening for messages from the server (messages fire the MessageReceived event)
// Check the RTP Transport
// If the RTP transport is TCP then we interleave the RTP packets in the RTSP stream
// If the RTP transport is UDP, we initialise two UDP sockets (one for video, one for RTCP status messages)
// If the RTP transport is MULTICAST, we have to wait for the SETUP message to get the Multicast Address from the RTSP server
this.rtp_transport = rtp_transport;
if (rtp_transport == RTP_TRANSPORT.UDP)
{
video_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
video_udp_pair.DataReceived += Rtp_DataReceived;
video_udp_pair.Start(); // start listening for data on the UDP ports
audio_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
audio_udp_pair.DataReceived += Rtp_DataReceived;
audio_udp_pair.Start(); // start listening for data on the UDP ports
}
if (rtp_transport == RTP_TRANSPORT.TCP)
{
// Nothing to do. Data will arrive in the RTSP Listener
}
if (rtp_transport == RTP_TRANSPORT.MULTICAST)
{
// Nothing to do. Will open Multicast UDP sockets after the SETUP command
}
// Send OPTIONS
// In the Received Message handler we will send DESCRIBE, SETUP and PLAY
Inspectron.HawkEye.RTSP.Messages.RtspRequest options_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions();
options_message.RtspUri = new Uri(this.url);
rtsp_client.SendMessage(options_message);
}
// return true if this connection failed, or if it connected but is no longer connected.
public bool StreamingFinished() {
if (rtsp_socket_status == RTSP_STATUS.ConnectFailed) return true;
if (rtsp_socket_status == RTSP_STATUS.Connected && rtsp_socket.Connected == false) return true;
else return false;
}
public void Pause()
{
if (rtsp_client != null) {
// Send PAUSE
Inspectron.HawkEye.RTSP.Messages.RtspRequest pause_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPause();
pause_message.RtspUri = new Uri(url);
pause_message.Session = session;
if (auth_type != null) {
AddAuthorization(pause_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(pause_message);
}
}
public void Play()
{
if (rtsp_client != null) {
// Send PLAY
Inspectron.HawkEye.RTSP.Messages.RtspRequest play_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay();
play_message.RtspUri = new Uri(url);
play_message.Session = session;
if (auth_type != null) {
AddAuthorization(play_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(play_message);
}
}
public void Stop()
{
if (rtsp_client != null) {
// Send TEARDOWN
Inspectron.HawkEye.RTSP.Messages.RtspRequest teardown_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestTeardown();
teardown_message.RtspUri = new Uri(url);
teardown_message.Session = session;
if (auth_type != null) {
AddAuthorization(teardown_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(teardown_message);
}
// Stop the keepalive timer
if (keepalive_timer != null) keepalive_timer.Stop();
// clear up any UDP sockets
if (video_udp_pair != null) video_udp_pair.Stop();
if (audio_udp_pair != null) audio_udp_pair.Stop();
// Drop the RTSP session
if (rtsp_client != null) {
rtsp_client.Stop();
}
}
int rtp_count = 0; // used for statistics
// RTP packet (or RTCP packet) has been received.
public void Rtp_DataReceived(object sender, Inspectron.HawkEye.RTSP.RtspChunkEventArgs e)
{
Inspectron.HawkEye.RTSP.Messages.RtspData data_received = e.Message as Inspectron.HawkEye.RTSP.Messages.RtspData;
// Check which channel the Data was received on.
// eg the Video Channel, the Video Control Channel (RTCP)
// the Audio Channel or the Audio Control Channel (RTCP)
if (data_received.Channel == video_rtcp_channel || data_received.Channel == audio_rtcp_channel)
{
_logger.Debug("Received a RTCP message on channel " + data_received.Channel);
// RTCP Packet
// - Version, Padding and Receiver Report Count
// - Packet Type
// - Length
// - SSRC
// - payload
// There can be multiple RTCP packets transmitted together. Loop ever each one
long packetIndex = 0;
while (packetIndex < e.Message.Data.Length) {
int rtcp_version = (e.Message.Data[packetIndex+0] >> 6);
int rtcp_padding = (e.Message.Data[packetIndex+0] >> 5) & 0x01;
int rtcp_reception_report_count = (e.Message.Data[packetIndex+0] & 0x1F);
byte rtcp_packet_type = e.Message.Data[packetIndex+1]; // Values from 200 to 207
uint rtcp_length = (uint)(e.Message.Data[packetIndex+2] << 8) + (uint)(e.Message.Data[packetIndex+3]); // number of 32 bit words
uint rtcp_ssrc = (uint)(e.Message.Data[packetIndex+4] << 24) + (uint)(e.Message.Data[packetIndex+5] << 16)
+ (uint)(e.Message.Data[packetIndex+6] << 8) + (uint)(e.Message.Data[packetIndex+7]);
// 200 = SR = Sender Report
// 201 = RR = Receiver Report
// 202 = SDES = Source Description
// 203 = Bye = Goodbye
// 204 = APP = Application Specific Method
// 207 = XR = Extended Reports
_logger.Debug("RTCP Data. PacketType=" + rtcp_packet_type
+ " SSRC=" + rtcp_ssrc);
if (rtcp_packet_type == 200) {
// We have received a Sender Report
// Use it to convert the RTP timestamp into the UTC time
UInt32 ntp_msw_seconds = (uint)(e.Message.Data[packetIndex + 8] << 24) + (uint)(e.Message.Data[packetIndex + 9] << 16)
+ (uint)(e.Message.Data[packetIndex + 10] << 8) + (uint)(e.Message.Data[packetIndex + 11]);
UInt32 ntp_lsw_fractions = (uint)(e.Message.Data[packetIndex + 12] << 24) + (uint)(e.Message.Data[packetIndex + 13] << 16)
+ (uint)(e.Message.Data[packetIndex + 14] << 8) + (uint)(e.Message.Data[packetIndex + 15]);
UInt32 rtp_timestamp = (uint)(e.Message.Data[packetIndex + 16] << 24) + (uint)(e.Message.Data[packetIndex + 17] << 16)
+ (uint)(e.Message.Data[packetIndex + 18] << 8) + (uint)(e.Message.Data[packetIndex + 19]);
double ntp = ntp_msw_seconds + (ntp_lsw_fractions / UInt32.MaxValue);
// NTP Most Signigicant Word is relative to 0h, 1 Jan 1900
// This will wrap around in 2036
DateTime time = new DateTime(1900,1,1,0,0,0,DateTimeKind.Utc);
time = time.AddSeconds((double)ntp_msw_seconds); // adds 'double' (whole&fraction)
_logger.Debug("RTCP time (UTC) for RTP timestamp " + rtp_timestamp + " is " + time);
// Send a Receiver Report
try
{
byte[] rtcp_receiver_report = new byte[8];
int version = 2;
int paddingBit = 0;
int reportCount = 0; // an empty report
int packetType = 201; // Receiver Report
int length = (rtcp_receiver_report.Length/4) - 1; // num 32 bit words minus 1
rtcp_receiver_report[0] = (byte)((version << 6) + (paddingBit << 5) + reportCount);
rtcp_receiver_report[1] = (byte)(packetType);
rtcp_receiver_report[2] = (byte)((length >> 8) & 0xFF);
rtcp_receiver_report[3] = (byte)((length >> 0) & 0XFF);
rtcp_receiver_report[4] = (byte)((ssrc >> 24) & 0xFF);
rtcp_receiver_report[5] = (byte)((ssrc >> 16) & 0xFF);
rtcp_receiver_report[6] = (byte)((ssrc >> 8) & 0xFF);
rtcp_receiver_report[7] = (byte)((ssrc >> 0) & 0xFF);
if (rtp_transport == RTP_TRANSPORT.TCP) {
// Send it over via the RTSP connection
rtsp_client.SendData(video_rtcp_channel,rtcp_receiver_report);
}
if (rtp_transport == RTP_TRANSPORT.UDP || rtp_transport == RTP_TRANSPORT.MULTICAST) {
// Send it via a UDP Packet
_logger.Debug("TODO - Need to implement RTCP over UDP");
}
}
catch
{
_logger.Debug("Error writing RTCP packet");
}
}
packetIndex = packetIndex + ((rtcp_length + 1) * 4);
}
return;
}
if (data_received.Channel == video_data_channel || data_received.Channel == audio_data_channel)
{
// Received some Video or Audio Data on the correct channel.
// RTP Packet Header
// 0 - Version, P, X, CC, M, PT and Sequence Number
//32 - Timestamp
//64 - SSRC
//96 - CSRCs (optional)
//nn - Extension ID and Length
//nn - Extension header
int rtp_version = (e.Message.Data[0] >> 6);
int rtp_padding = (e.Message.Data[0] >> 5) & 0x01;
int rtp_extension = (e.Message.Data[0] >> 4) & 0x01;
int rtp_csrc_count = (e.Message.Data[0] >> 0) & 0x0F;
int rtp_marker = (e.Message.Data[1] >> 7) & 0x01;
int rtp_payload_type = (e.Message.Data[1] >> 0) & 0x7F;
uint rtp_sequence_number = ((uint)e.Message.Data[2] << 8) + (uint)(e.Message.Data[3]);
uint rtp_timestamp = ((uint)e.Message.Data[4] << 24) + (uint)(e.Message.Data[5] << 16) + (uint)(e.Message.Data[6] << 8) + (uint)(e.Message.Data[7]);
uint rtp_ssrc = ((uint)e.Message.Data[8] << 24) + (uint)(e.Message.Data[9] << 16) + (uint)(e.Message.Data[10] << 8) + (uint)(e.Message.Data[11]);
int rtp_payload_start = 4 // V,P,M,SEQ
+ 4 // time stamp
+ 4 // ssrc
+ (4 * rtp_csrc_count); // zero or more csrcs
uint rtp_extension_id = 0;
uint rtp_extension_size = 0;
if (rtp_extension == 1)
{
rtp_extension_id = ((uint)e.Message.Data[rtp_payload_start + 0] << 8) + (uint)(e.Message.Data[rtp_payload_start + 1] << 0);
rtp_extension_size = ((uint)e.Message.Data[rtp_payload_start + 2] << 8) + (uint)(e.Message.Data[rtp_payload_start + 3] << 0) * 4; // units of extension_size is 4-bytes
rtp_payload_start += 4 + (int)rtp_extension_size; // extension header and extension payload
}
_logger.Debug("RTP Data"
+ " V=" + rtp_version
+ " P=" + rtp_padding
+ " X=" + rtp_extension
+ " CC=" + rtp_csrc_count
+ " M=" + rtp_marker
+ " PT=" + rtp_payload_type
+ " Seq=" + rtp_sequence_number
+ " Time (MS)=" + rtp_timestamp / 90 // convert from 90kHZ clock to ms
+ " SSRC=" + rtp_ssrc
+ " Size=" + e.Message.Data.Length);
// Check the payload type in the RTP packet matches the Payload Type value from the SDP
if (data_received.Channel == video_data_channel && rtp_payload_type != video_payload)
{
_logger.Debug("Ignoring this Video RTP payload");
return; // ignore this data
}
// Check the payload type in the RTP packet matches the Payload Type value from the SDP
else if (data_received.Channel == audio_data_channel && rtp_payload_type != audio_payload)
{
_logger.Debug("Ignoring this Audio RTP payload");
return; // ignore this data
}
else if (data_received.Channel == video_data_channel
&& rtp_payload_type == video_payload
&& video_codec.Equals("H264")) {
// H264 RTP Packet
// If rtp_marker is '1' then this is the final transmission for this packet.
// If rtp_marker is '0' we need to accumulate data with the same timestamp
// ToDo - Check Timestamp
// Add the RTP packet to the tempoary_rtp list until we have a complete 'Frame'
byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
List<byte[]> nal_units = h264Payload.Process_H264_RTP_Packet(rtp_payload, rtp_marker); // this will cache the Packets until there is a Frame
if (nal_units == null) {
// we have not passed in enough RTP packets to make a Frame of video
} else {
// If we did not have a SPS and PPS in the SDP then search for the SPS and PPS
// in the NALs and fire the Received_SPS_PPS event.
// We assume the SPS and PPS are in the same Frame.
if (h264_sps_pps_fired == false) {
// Check this frame for SPS and PPS
byte[] sps = null;
byte[] pps = null;
foreach (byte[] nal_unit in nal_units) {
if (nal_unit.Length > 0)
{
int nal_ref_idc = (nal_unit[0] >> 5) & 0x03;
int nal_unit_type = nal_unit[0] & 0x1F;
if (nal_unit_type == 7) sps = nal_unit; // SPS
if (nal_unit_type == 8) pps = nal_unit; // PPS
}
}
if (sps != null && pps != null) {
// Fire the Event
if (Received_SPS_PPS != null)
{
Received_SPS_PPS(sps, pps);
}
h264_sps_pps_fired = true;
}
}
// we have a frame of NAL Units. Write them to the file
if (Received_NALs != null) {
Received_NALs(nal_units);
}
}
}
else if (data_received.Channel == video_data_channel
&& rtp_payload_type == video_payload
&& video_codec.Equals("H265"))
{
// H265 RTP Packet
// If rtp_marker is '1' then this is the final transmission for this packet.
// If rtp_marker is '0' we need to accumulate data with the same timestamp
// Add the RTP packet to the tempoary_rtp list until we have a complete 'Frame'
byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
List<byte[]> nal_units = h265Payload.Process_H265_RTP_Packet(rtp_payload, rtp_marker); // this will cache the Packets until there is a Frame
if (nal_units == null)
{
// we have not passed in enough RTP packets to make a Frame of video
}
else
{
// If we did not have a VPS, SPS and PPS in the SDP then search for the VPS SPS and PPS
// in the NALs and fire the Received_VPS_SPS_PPS event.
// We assume the VPS, SPS and PPS are in the same Frame.
if (h265_vps_sps_pps_fired == false)
{
// Check this frame for VPS, SPS and PPS
byte[] vps = null;
byte[] sps = null;
byte[] pps = null;
foreach (byte[] nal_unit in nal_units)
{
if (nal_unit.Length > 0)
{
int nal_unit_type = (nal_unit[0] >> 1) & 0x3F;
if (nal_unit_type == 32) vps = nal_unit; // VPS
if (nal_unit_type == 33) sps = nal_unit; // SPS
if (nal_unit_type == 34) pps = nal_unit; // PPS
}
}
if (vps != null && sps != null && pps != null)
{
// Fire the Event
if (Received_VPS_SPS_PPS != null)
{
Received_VPS_SPS_PPS(vps, sps, pps);
}
h265_vps_sps_pps_fired = true;
}
}
// we have a frame of NAL Units. Write them to the file
if (Received_NALs != null)
{
Received_NALs(nal_units);
}
}
}
else if (data_received.Channel == audio_data_channel && (rtp_payload_type == 0 || rtp_payload_type == 8 || audio_codec.Equals("PCMA") || audio_codec.Equals("PCMU"))) {
// G711 PCMA or G711 PCMU
byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
List<byte[]> audio_frames = g711Payload.Process_G711_RTP_Packet(rtp_payload, rtp_marker);
if (audio_frames == null) {
// some error
} else {
// Write the audio frames to the file
if (Received_G711 != null) {
Received_G711(audio_codec, audio_frames);
}
}
}
else if (data_received.Channel == audio_data_channel
&& rtp_payload_type == audio_payload
&& audio_codec.Equals("AMR")) {
// AMR
byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
List<byte[]> audio_frames = amrPayload.Process_AMR_RTP_Packet(rtp_payload, rtp_marker);
if (audio_frames == null) {
// some error
} else {
// Write the audio frames to the file
if (Received_AMR != null) {
Received_AMR(audio_codec, audio_frames);
}
}
}
else if (data_received.Channel == audio_data_channel
&& rtp_payload_type == audio_payload
&& audio_codec.Equals("MPEG4-GENERIC")
&& aacPayload != null)
{
// AAC
byte[] rtp_payload = new byte[e.Message.Data.Length - rtp_payload_start]; // payload with RTP header removed
System.Array.Copy(e.Message.Data, rtp_payload_start, rtp_payload, 0, rtp_payload.Length); // copy payload
List<byte[]> audio_frames = aacPayload.Process_AAC_RTP_Packet(rtp_payload, rtp_marker);
if (audio_frames == null) {
// some error
} else {
// Write the audio frames to the file
if (Received_AAC != null) {
Received_AAC(audio_codec, audio_frames, aacPayload.ObjectType, aacPayload.FrequencyIndex, aacPayload.ChannelConfiguration);
}
}
}
else if (data_received.Channel == video_data_channel && rtp_payload_type == 26) {
_logger.Warn("No parser has been written for JPEG RTP packets. Please help write one");
return; // ignore this data
}
else {
_logger.Warn("No parser for RTP payload " + rtp_payload_type);
}
}
}
// RTSP Messages are OPTIONS, DESCRIBE, SETUP, PLAY etc
private void Rtsp_MessageReceived(object sender, Inspectron.HawkEye.RTSP.RtspChunkEventArgs e)
{
Inspectron.HawkEye.RTSP.Messages.RtspResponse message = e.Message as Inspectron.HawkEye.RTSP.Messages.RtspResponse;
_logger.Debug("Received RTSP Message " + message.OriginalRequest.ToString());
// If message has a 401 - Unauthorised Error, then we re-send the message with Authorization
// using the most recently received 'realm' and 'nonce'
if (message.IsOk == false) {
_logger.Debug("Got Error in RTSP Reply " + message.ReturnCode + " " + message.ReturnMessage);
if (message.ReturnCode == 401 && (message.OriginalRequest.Headers.ContainsKey(RtspHeaderNames.Authorization)==true)) {
// the authorization failed.
Stop();
return;
}
// Check if the Reply has an Authenticate header.
if (message.ReturnCode == 401 && message.Headers.ContainsKey(RtspHeaderNames.WWWAuthenticate)) {
// Process the WWW-Authenticate header
// EG: Basic realm="AProxy"
// EG: Digest realm="AXIS_WS_ACCC8E3A0A8F", nonce="000057c3Y810622bff50b36005eb5efeae118626a161bf", stale=FALSE
// EG: Digest realm="IP Camera(21388)", nonce="534407f373af1bdff561b7b4da295354", stale="FALSE"
String www_authenticate = message.Headers[RtspHeaderNames.WWWAuthenticate];
String auth_params = "";
if (www_authenticate.StartsWith("basic",StringComparison.InvariantCultureIgnoreCase)) {
auth_type = "Basic";
auth_params = www_authenticate.Substring(5);
}
if (www_authenticate.StartsWith("digest",StringComparison.InvariantCultureIgnoreCase)) {
auth_type = "Digest";
auth_params = www_authenticate.Substring(6);
}
string[] items = auth_params.Split(new char[] { ',' }); // NOTE, does not handle Commas in Quotes
foreach (string item in items) {
// Split on the = symbol and update the realm and nonce
string[] parts = item.Trim().Split(new char[] {'='},2); // max 2 parts in the results array
if (parts.Count() >= 2 && parts[0].Trim().Equals("realm")) {
realm = parts[1].Trim(new char[] {' ','\"'}); // trim space and quotes
}
else if (parts.Count() >= 2 && parts[0].Trim().Equals("nonce")) {
nonce = parts[1].Trim(new char[] {' ','\"'}); // trim space and quotes
}
}
_logger.Debug("WWW Authorize parsed for " + auth_type + " " + realm + " " + nonce);
}
RtspMessage resend_message = message.OriginalRequest.Clone() as RtspMessage;
if (auth_type != null) {
AddAuthorization(resend_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(resend_message);
return;
}
// If we get a reply to OPTIONS then start the Keepalive Timer and send DESCRIBE
if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions)
{
// Check the capabilities returned by OPTIONS
// The Public: header contains the list of commands the RTSP server supports
// Eg DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD, GET_PARAMETER]}
if (message.Headers.ContainsKey(RtspHeaderNames.Public))
{
string[] parts = message.Headers[RtspHeaderNames.Public].Split(',');
foreach (String part in parts) {
if (part.Trim().ToUpper().Equals("GET_PARAMETER")) server_supports_get_parameter = true;
if (part.Trim().ToUpper().Equals("SET_PARAMETER")) server_supports_set_parameter = true;
}
}
if (keepalive_timer == null)
{
// Start a Timer to send an Keepalive RTSP command every 20 seconds
keepalive_timer = new System.Timers.Timer();
keepalive_timer.Elapsed += Timer_Elapsed;
keepalive_timer.Interval = 20 * 1000;
keepalive_timer.Enabled = true;
// Send DESCRIBE
Inspectron.HawkEye.RTSP.Messages.RtspRequest describe_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestDescribe();
describe_message.RtspUri = new Uri(url);
if (auth_type != null) {
AddAuthorization(describe_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(describe_message);
}
else
{
// If the Keepalive Timer was not null, the OPTIONS reply may have come from a Keepalive
// So no need to generate a DESCRIBE message
// do nothing
}
}
// If we get a reply to DESCRIBE (which was our second command), then prosess SDP and send the SETUP
if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestDescribe)
{
// Got a reply for DESCRIBE
if (message.IsOk == false) {
_logger.Debug("Got Error in DESCRIBE Reply " + message.ReturnCode + " " + message.ReturnMessage);
return;
}
// Examine the SDP
_logger.Debug(System.Text.Encoding.UTF8.GetString(message.Data));
Inspectron.HawkEye.RTSP.Sdp.SdpFile sdp_data;
using (StreamReader sdp_stream = new StreamReader(new MemoryStream(message.Data)))
{
sdp_data = Inspectron.HawkEye.RTSP.Sdp.SdpFile.Read(sdp_stream);
}
// RTP and RTCP 'channels' are used in TCP Interleaved mode (RTP over RTSP)
// These are the channels we request. The camera confirms the channel in the SETUP Reply.
// But, a Panasonic decides to use different channels in the reply.
int next_free_rtp_channel = 0;
int next_free_rtcp_channel = 1;
// Process each 'Media' Attribute in the SDP (each sub-stream)
for (int x = 0; x < sdp_data.Medias.Count; x++)
{
bool audio = (sdp_data.Medias[x].MediaType == Inspectron.HawkEye.RTSP.Sdp.Media.MediaTypes.audio);
bool video = (sdp_data.Medias[x].MediaType == Inspectron.HawkEye.RTSP.Sdp.Media.MediaTypes.video);
if (video && video_payload != -1) continue; // have already matched a video payload. don't match another
if (audio && audio_payload != -1) continue; // have already matched an audio payload. don't match another
if (audio && (client_wants_audio == false)) continue; // client does not want audio from the RTSP server
if (video && (client_wants_video == false)) continue; // client does not want video from the RTSP server
if (audio || video)
{
// search the attributes for control, rtpmap and fmtp
// (fmtp only applies to video)
String control = ""; // the "track" or "stream id"
Inspectron.HawkEye.RTSP.Sdp.AttributFmtp fmtp = null; // holds SPS and PPS in base64 (h264 video)
foreach (Inspectron.HawkEye.RTSP.Sdp.Attribut attrib in sdp_data.Medias[x].Attributs) {
if (attrib.Key.Equals("control")) {
String sdp_control = attrib.Value;
if (sdp_control.ToLower().StartsWith("rtsp://")) {
control = sdp_control; //absolute path
} else {
control = url + "/" + sdp_control; // relative path
}
if (video) video_uri = new Uri(control);
if (audio) audio_uri = new Uri(control);
}
if (attrib.Key.Equals("fmtp")) {
fmtp = attrib as Inspectron.HawkEye.RTSP.Sdp.AttributFmtp;
}
if (attrib.Key.Equals("rtpmap")) {
Inspectron.HawkEye.RTSP.Sdp.AttributRtpMap rtpmap = attrib as Inspectron.HawkEye.RTSP.Sdp.AttributRtpMap;
// Check if the Codec Used (EncodingName) is one we support
String[] valid_video_codecs = {"H264","H265"};
String[] valid_audio_codecs = {"PCMA", "PCMU", "AMR", "MPEG4-GENERIC" /* for aac */}; // Note some are "mpeg4-generic" lower case
if (video && Array.IndexOf(valid_video_codecs,rtpmap.EncodingName.ToUpper()) >= 0) {
// found a valid codec
video_codec = rtpmap.EncodingName.ToUpper();
video_payload = sdp_data.Medias[x].PayloadType;
}
if (audio && Array.IndexOf(valid_audio_codecs,rtpmap.EncodingName.ToUpper()) >= 0) {
audio_codec = rtpmap.EncodingName.ToUpper();
audio_payload = sdp_data.Medias[x].PayloadType;
}
}
}
// Create H264 RTP Parser
if (video && video_codec.Contains("H264"))
{
h264Payload = new Inspectron.HawkEye.RTSP.H264Payload();
}
// If the rtpmap contains H264 then split the fmtp to get the sprop-parameter-sets which hold the SPS and PPS in base64
if (video && video_codec.Contains("H264") && fmtp != null) {
var param = Inspectron.HawkEye.RTSP.Sdp.H264Parameters.Parse(fmtp.FormatParameter);
var sps_pps = param.SpropParameterSets;
if (sps_pps.Count() >= 2) {
byte[] sps = sps_pps[0];
byte[] pps = sps_pps[1];
if (Received_SPS_PPS != null) {
Received_SPS_PPS(sps,pps);
}
h264_sps_pps_fired = true;
}
}
// Create H265 RTP Parser
if (video && video_codec.Contains("H265"))
{
// TODO - check if DONL is being used
bool has_donl = false;
h265Payload = new Inspectron.HawkEye.RTSP.H265Payload(has_donl);
}
// If the rtpmap contains H265 then split the fmtp to get the sprop-vps, sprop-sps and sprop-pps
// The RFC makes the VPS, SPS and PPS OPTIONAL so they may not be present. In which we pass back NULL values
if (video && video_codec.Contains("H265") && fmtp != null)
{
var param = Inspectron.HawkEye.RTSP.Sdp.H265Parameters.Parse(fmtp.FormatParameter);
var vps_sps_pps = param.SpropParameterSets;
if (vps_sps_pps.Count() >= 3)
{
byte[] vps = vps_sps_pps[0];
byte[] sps = vps_sps_pps[1];
byte[] pps = vps_sps_pps[2];
if (Received_VPS_SPS_PPS != null)
{
Received_VPS_SPS_PPS(vps,sps, pps);
}
h265_vps_sps_pps_fired = true;
}
}
// Create AAC RTP Parser
// Example fmtp is "96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1490"
// Example fmtp is ""96 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210"
if (audio && audio_codec.Contains("MPEG4-GENERIC") && fmtp.GetParameter("mode").ToLower().Equals("aac-hbr"))
{
// Extract config (eg 0x1490 or 0x1210)
aacPayload = new Inspectron.HawkEye.RTSP.AACPayload(fmtp.GetParameter("config"));
}
// Send the SETUP RTSP command if we have a matching Payload Decoder
if (video && video_payload == -1) continue;
if (audio && audio_payload == -1) continue;
RtspTransport transport = null;
if (rtp_transport == RTP_TRANSPORT.TCP)
{
// Server interleaves the RTP packets over the RTSP connection
// Example for TCP mode (RTP over RTSP) Transport: RTP/AVP/TCP;interleaved=0-1
if (video) {
video_data_channel = next_free_rtp_channel;
video_rtcp_channel = next_free_rtcp_channel;
}
if (audio) {
audio_data_channel = next_free_rtp_channel;
audio_rtcp_channel = next_free_rtcp_channel;
}
transport = new RtspTransport()
{
LowerTransport = RtspTransport.LowerTransportType.TCP,
Interleaved = new PortCouple(next_free_rtp_channel, next_free_rtcp_channel), // Eg Channel 0 for RTP video data. Channel 1 for RTCP status reports
};
next_free_rtp_channel += 2;
next_free_rtcp_channel += 2;
}
if (rtp_transport == RTP_TRANSPORT.UDP)
{
int rtp_port = 0;
int rtcp_port = 0;
// Server sends the RTP packets to a Pair of UDP Ports (one for data, one for rtcp control messages)
// Example for UDP mode Transport: RTP/AVP;unicast;client_port=8000-8001
if (video) {
video_data_channel = video_udp_pair.data_port; // Used in DataReceived event handler
video_rtcp_channel = video_udp_pair.control_port; // Used in DataReceived event handler
rtp_port = video_udp_pair.data_port;
rtcp_port = video_udp_pair.control_port;
}
if (audio) {
audio_data_channel = audio_udp_pair.data_port; // Used in DataReceived event handler
audio_rtcp_channel = audio_udp_pair.control_port; // Used in DataReceived event handler
rtp_port = audio_udp_pair.data_port;
rtcp_port = audio_udp_pair.control_port;
}
transport = new RtspTransport()
{
LowerTransport = RtspTransport.LowerTransportType.UDP,
IsMulticast = false,
ClientPort = new PortCouple(rtp_port, rtcp_port), // a UDP Port for data (video or audio). a UDP Port for RTCP status reports
};
}
if (rtp_transport == RTP_TRANSPORT.MULTICAST)
{
// Server sends the RTP packets to a Pair of UDP ports (one for data, one for rtcp control messages)
// using Multicast Address and Ports that are in the reply to the SETUP message
// Example for MULTICAST mode Transport: RTP/AVP;multicast
if (video) {
video_data_channel = 0; // we get this information in the SETUP message reply
video_rtcp_channel = 0; // we get this information in the SETUP message reply
}
if (audio) {
audio_data_channel = 0; // we get this information in the SETUP message reply
audio_rtcp_channel = 0; // we get this information in the SETUP message reply
}
transport = new RtspTransport()
{
LowerTransport = RtspTransport.LowerTransportType.UDP,
IsMulticast = true
};
}
// Generate SETUP messages
Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup setup_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup();
setup_message.RtspUri = new Uri(control);
setup_message.AddTransport(transport);
if (auth_type != null) {
AddAuthorization(setup_message,username,password,auth_type,realm,nonce,url);
}
// Add SETUP message to list of mesages to send
setup_messages.Add(setup_message);
}
}
// Send the FIRST SETUP message and remove it from the list of Setup Messages
rtsp_client.SendMessage(setup_messages[0]);
setup_messages.RemoveAt(0);
}
// If we get a reply to SETUP (which was our third command), then we
// (i) check if the Interleaved Channel numbers have been modified by the camera (eg Panasonic cameras)
// (ii) check if we have any more SETUP commands to send out (eg if we are doing SETUP for Video and Audio)
// (iii) send a PLAY command if all the SETUP command have been sent
if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup)
{
// Got Reply to SETUP
if (message.IsOk == false) {
_logger.Debug("Got Error in SETUP Reply " + message.ReturnCode + " " + message.ReturnMessage);
return;
}
_logger.Debug("Got reply from Setup. Session is " + message.Session);
session = message.Session; // Session value used with Play, Pause, Teardown and and additional Setups
if(message.Timeout > 0 && message.Timeout > keepalive_timer.Interval / 1000)
{
keepalive_timer.Interval = message.Timeout * 1000 / 2;
}
// Check the Transport header
if (message.Headers.ContainsKey(RtspHeaderNames.Transport))
{
RtspTransport transport = RtspTransport.Parse(message.Headers[RtspHeaderNames.Transport]);
// Check if Transport header includes Multicast
if (transport.IsMulticast)
{
String multicast_address = transport.Destination;
video_data_channel = transport.Port.First;
video_rtcp_channel = transport.Port.Second;
// Create the Pair of UDP Sockets in Multicast mode
video_udp_pair = new Inspectron.HawkEye.RTSP.UDPSocket(multicast_address, video_data_channel, multicast_address, video_rtcp_channel);
video_udp_pair.DataReceived += Rtp_DataReceived;
video_udp_pair.Start();
// TODO - Need to set audio_udp_pair for Multicast
}
// check if the requested Interleaved channels have been modified by the camera
// in the SETUP Reply (Panasonic have a camera that does this)
if (transport.LowerTransport == RtspTransport.LowerTransportType.TCP) {
if (message.OriginalRequest.RtspUri == video_uri) {
video_data_channel = transport.Interleaved.First;
video_rtcp_channel = transport.Interleaved.Second;
}
if (message.OriginalRequest.RtspUri == audio_uri) {
audio_data_channel = transport.Interleaved.First;
audio_rtcp_channel = transport.Interleaved.Second;
}
}
}
// Check if we have another SETUP command to send, then remote it from the list
if (setup_messages.Count > 0) {
// send the next SETUP message, after adding in the 'session'
Inspectron.HawkEye.RTSP.Messages.RtspRequestSetup next_setup = setup_messages[0];
next_setup.Session = session;
rtsp_client.SendMessage(next_setup);
setup_messages.RemoveAt(0);
}
else {
// Send PLAY
Inspectron.HawkEye.RTSP.Messages.RtspRequest play_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay();
play_message.RtspUri = new Uri(url);
play_message.Session = session;
if (auth_type != null) {
AddAuthorization(play_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(play_message);
}
}
// If we get a reply to PLAY (which was our fourth command), then we should have video being received
if (message.OriginalRequest != null && message.OriginalRequest is Inspectron.HawkEye.RTSP.Messages.RtspRequestPlay)
{
// Got Reply to PLAY
if (message.IsOk == false) {
_logger.Debug("Got Error in PLAY Reply " + message.ReturnCode + " " + message.ReturnMessage);
return;
}
_logger.Debug("Got reply from Play " + message.Command);
}
}
void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
// Send Keepalive message
// The ONVIF Standard uses SET_PARAMETER as "an optional method to keep an RTSP session alive"
// RFC 2326 (RTSP Standard) says "GET_PARAMETER with no entity body may be used to test client or server liveness("ping")"
// This code uses GET_PARAMETER (unless OPTIONS report it is not supported, and then it sends OPTIONS as a keepalive)
if (server_supports_get_parameter) {
Inspectron.HawkEye.RTSP.Messages.RtspRequest getparam_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestGetParameter();
getparam_message.RtspUri = new Uri(url);
getparam_message.Session = session;
if (auth_type != null)
{
AddAuthorization(getparam_message, username, password, auth_type, realm, nonce, url);
}
rtsp_client.SendMessage(getparam_message);
} else {
Inspectron.HawkEye.RTSP.Messages.RtspRequest options_message = new Inspectron.HawkEye.RTSP.Messages.RtspRequestOptions();
options_message.RtspUri = new Uri(url);
if (auth_type != null) {
AddAuthorization(options_message,username,password,auth_type,realm,nonce,url);
}
rtsp_client.SendMessage(options_message);
}
}
// Generate Basic or Digest Authorization
public void AddAuthorization(RtspMessage message, string username, string password,
string auth_type, string realm, string nonce, string url) {
if (username == null || username.Length == 0) return;
if (password == null || password.Length == 0) return;
if (realm == null || realm.Length == 0) return;
if (auth_type.Equals("Digest") && (nonce == null || nonce.Length == 0)) return;
if (auth_type.Equals("Basic")) {
byte[] credentials = System.Text.Encoding.UTF8.GetBytes(username+":"+password);
String credentials_base64 = Convert.ToBase64String(credentials);
String basic_authorization = "Basic " + credentials_base64;
message.Headers.Add(RtspHeaderNames.Authorization, basic_authorization);
return;
}
else if (auth_type.Equals("Digest")) {
string method = message.Method; // DESCRIBE, SETUP, PLAY etc
MD5 md5 = System.Security.Cryptography.MD5.Create();
String hashA1 = CalculateMD5Hash(md5, username+":"+realm+":"+password);
String hashA2 = CalculateMD5Hash(md5, method + ":" + url);
String response = CalculateMD5Hash(md5, hashA1 + ":" + nonce + ":" + hashA2);
const String quote = "\"";
String digest_authorization = "Digest username=" + quote + username + quote +", "
+ "realm=" + quote + realm + quote + ", "
+ "nonce=" + quote + nonce + quote + ", "
+ "uri=" + quote + url + quote + ", "
+ "response=" + quote + response + quote;
message.Headers.Add(RtspHeaderNames.Authorization, digest_authorization);
return;
}
else {
return;
}
}
// MD5 (lower case)
public string CalculateMD5Hash(MD5 md5_session, string input)
{
byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
byte[] hash = md5_session.ComputeHash(inputBytes);
StringBuilder output = new StringBuilder();
for (int i = 0; i < hash.Length; i++) {
output.Append(hash[i].ToString("x2"));
}
return output.ToString();
}
}
}