hawkeye camera support(not tested)

This commit is contained in:
meelstorm
2025-08-19 12:45:50 +02:00
parent f80b275ad8
commit 6ef6aced8d
170 changed files with 20190 additions and 72 deletions

View File

@@ -0,0 +1,401 @@
using System;
using System.Collections.Generic;
/*
* CJOCh264bitstream.cpp
*
* Created on: Aug 23, 2014
* Author: Jordi Cenzano (www.jordicenzano.name)
*/
/*
* CJOCh264bitstream.h
*
* Created on: Aug 23, 2014
* Author: Jordi Cenzano (www.jordicenzano.name)
*/
//! h264 bitstream class
/*!
It is used to create the h264 bit oriented stream, it contains different functions that helps you to create the h264 compliant stream (bit oriented, exp golomb coder)
*/
namespace Inspectron.HawkEye.RTSP.Server
{
public class CJOCh264bitstream : System.IDisposable
{
private const int BUFFER_SIZE_BITS = 24; //! Buffer size in bits used for emulation prevention
//C++ TO C# CONVERTER NOTE: The following #define macro was replaced in-line:
//ORIGINAL LINE: #define BUFFER_SIZE_BYTES (24/8)
private const int H264_EMULATION_PREVENTION_BYTE = 0x03; //! Emulation prevention byte
/*! Buffer */
private byte[] m_buffer = new byte[BUFFER_SIZE_BITS];
/*! Bit buffer index */
private int m_nLastbitinbuffer;
/*! Starting byte indicator */
private int m_nStartingbyte;
/*! Pointer to output file */
//private FILE m_pOutFile;
//Byte Array used for output
private List<byte> m_pOutFile;
//! Clears the buffer
private void clearbuffer()
{
//C++ TO C# CONVERTER TODO TASK: The memory management function 'memset' has no equivalent in C#:
//memset(m_buffer, 0, sizeof(byte) * BUFFER_SIZE_BITS);
System.Array.Clear(m_buffer, 0, BUFFER_SIZE_BITS);
m_nLastbitinbuffer = 0;
m_nStartingbyte = 0;
}
//! Returns the nNumbit value (1 or 0) of lval
/*!
\param lval number to extract the nNumbit value
\param nNumbit Bit position that we want to know if its 1 or 0 (from 0 to 63)
\return bit value (1 or 0)
*/
private static int getbitnum(uint lval, int nNumbit)
{
int lrc = 0;
uint lmask = (uint) Math.Pow((uint)2,(uint)nNumbit);
if ((lval & lmask) > 0)
{
lrc = 1;
}
return lrc;
}
//! Adds 1 bit to the end of h264 bitstream
/*!
\param nVal bit to add at the end of h264 bitstream
*/
private void addbittostream(int nVal)
{
if (m_nLastbitinbuffer >= BUFFER_SIZE_BITS)
{
//Must be aligned, no need to do dobytealign();
savebufferbyte();
}
//Use circular buffer of BUFFER_SIZE_BYTES
int nBytePos = (m_nStartingbyte + (m_nLastbitinbuffer / 8)) % (24 / 8);
//The first bit to add is on the left
int nBitPosInByte = 7 - m_nLastbitinbuffer % 8;
//Get the byte value from buffer
int nValTmp = m_buffer[nBytePos];
//Change the bit
if (nVal > 0)
{
nValTmp = (nValTmp | (int) Math.Pow(2,nBitPosInByte));
}
else
{
nValTmp = (nValTmp & ~((int) Math.Pow(2,nBitPosInByte)));
}
//Save the new byte value to the buffer
m_buffer[nBytePos] = (byte) nValTmp;
m_nLastbitinbuffer++;
}
//! Adds 8 bit to the end of h264 bitstream (it is optimized for byte aligned situations)
/*!
\param nVal byte to add at the end of h264 bitstream (from 0 to 255)
*/
private void addbytetostream(int nVal)
{
if (m_nLastbitinbuffer >= BUFFER_SIZE_BITS)
{
//Must be aligned, no need to do dobytealign();
savebufferbyte();
}
//Used circular buffer of BUFFER_SIZE_BYTES
int nBytePos = (m_nStartingbyte + (m_nLastbitinbuffer / 8)) % (24 / 8);
//The first bit to add is on the left
int nBitPosInByte = 7 - m_nLastbitinbuffer % 8;
//Check if it is byte aligned
if (nBitPosInByte != 7)
{
throw new System.Exception("Error: inserting not aligment byte");
}
//Add all byte to buffer
m_buffer[nBytePos] = (byte) nVal;
m_nLastbitinbuffer = m_nLastbitinbuffer + 8;
}
//! Save all buffer to file
/*!
\param bemulationprevention Indicates if it will insert the emulation prevention byte or not (when it is needed)
*/
private void savebufferbyte(bool bemulationprevention = true)
{
bool bemulationpreventionexecuted = false;
if (m_pOutFile == null)
{
throw new System.Exception("Error: out file is NULL");
}
//Check if the last bit in buffer is multiple of 8
if ((m_nLastbitinbuffer % 8) != 0)
{
throw new System.Exception("Error: Save to file must be byte aligned");
}
if ((m_nLastbitinbuffer / 8) <= 0)
{
throw new System.Exception("Error: NO bytes to save");
}
if (bemulationprevention == true)
{
//Emulation prevention will be used:
/*As per h.264 spec,
rbsp_data shouldn't contain
- 0x 00 00 00
- 0x 00 00 01
- 0x 00 00 02
- 0x 00 00 03
rbsp_data shall be in the following way
- 0x 00 00 03 00
- 0x 00 00 03 01
- 0x 00 00 03 02
- 0x 00 00 03 03
*/
//Check if emulation prevention is needed (emulation prevention is byte align defined)
if ( (m_buffer[((m_nStartingbyte + 0) % (24 / 8))] == 0x00)
&& (m_buffer[((m_nStartingbyte + 1) % (24 / 8))] == 0x00)
&& ((m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x00)
|| (m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x01)
|| (m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x02)
|| (m_buffer[((m_nStartingbyte + 2) % (24 / 8))] == 0x03)))
{
int nbuffersaved = 0;
byte cEmulationPreventionByte = H264_EMULATION_PREVENTION_BYTE;
//Save 1st byte
fwrite(m_buffer[((m_nStartingbyte + nbuffersaved) % (24 / 8))], 1, 1, m_pOutFile);
nbuffersaved++;
//Save 2st byte
fwrite(m_buffer[((m_nStartingbyte + nbuffersaved) % (24 / 8))], 1, 1, m_pOutFile);
nbuffersaved++;
//Save emulation prevention byte
fwrite(cEmulationPreventionByte, 1, 1, m_pOutFile);
//Save the rest of bytes (usually 1)
while (nbuffersaved < (24 / 8))
{
fwrite(m_buffer[((m_nStartingbyte + nbuffersaved) % (24 / 8))], 1, 1, m_pOutFile);
nbuffersaved++;
}
//All bytes in buffer are saved, so clear the buffer
clearbuffer();
bemulationpreventionexecuted = true;
}
}
if (bemulationpreventionexecuted == false)
{
//No emulation prevention was used
//Save the oldest byte in buffer
fwrite(m_buffer[m_nStartingbyte], 1, 1, m_pOutFile);
//Move the index
m_buffer[m_nStartingbyte] = 0;
m_nStartingbyte++;
m_nStartingbyte = m_nStartingbyte % (24 / 8);
m_nLastbitinbuffer = m_nLastbitinbuffer - 8;
}
}
//! Constructor
/*!
\param pOutBinaryFile The output file pointer
*/
public CJOCh264bitstream(List<byte> pOutBinaryFile)
{
clearbuffer();
//C++ TO C# CONVERTER TODO TASK: C# does not have an equivalent to pointers to variables (in C#, the variable no longer points to the original when the original variable is re-assigned):
//ORIGINAL LINE: m_pOutFile = pOutBinaryFile;
m_pOutFile = pOutBinaryFile;
}
//! Destructor
public virtual void Dispose()
{
close();
}
//! Add 4 bytes to h264 bistream without taking into acount the emulation prevention. Used to add the NAL header to the h264 bistream
/*!
\param nVal The 32b value to add
\param bDoAlign Indicates if the function will insert 0 in order to create a byte aligned stream before adding nVal 4 bytes to stream. If you try to call this function and the stream is not byte aligned an exception will be thrown
*/
public void add4bytesnoemulationprevention(uint nVal, bool bDoAlign = false)
{
//Used to add NAL header stream
//Remember: NAL header is byte oriented
if (bDoAlign == true)
{
dobytealign();
}
if ((m_nLastbitinbuffer % 8) != 0)
{
throw new System.Exception("Error: Save to file must be byte aligned");
}
while (m_nLastbitinbuffer != 0)
{
savebufferbyte();
}
byte cbyte = (byte)((nVal & 0xFF000000) >> 24);
fwrite(cbyte, 1, 1, m_pOutFile);
cbyte = (byte)((nVal & 0x00FF0000) >> 16);
fwrite(cbyte, 1, 1, m_pOutFile);
cbyte = (byte)((nVal & 0x0000FF00) >> 8);
fwrite(cbyte, 1, 1, m_pOutFile);
cbyte = (byte)(nVal & 0x000000FF);
fwrite(cbyte, 1, 1, m_pOutFile);
}
//! Adds nNumbits of lval to the end of h264 bitstream
/*!
\param nVal value to add at the end of the h264 stream (only the LAST nNumbits will be added)
\param nNumbits number of bits of lval that will be added to h264 stream (counting from left)
*/
//Public functions
public void addbits(uint lval, int nNumbits)
{
if ((nNumbits <= 0) || (nNumbits > 64))
{
throw new System.Exception("Error: numbits must be between 1 ... 64");
}
int nBit = 0;
int n = nNumbits - 1;
while (n >= 0)
{
nBit = getbitnum(lval, n);
n--;
addbittostream(nBit);
}
}
//! Adds lval to the end of h264 bitstream using exp golomb coding for unsigned values
/*!
\param nVal value to add at the end of the h264 stream
*/
public void addexpgolombunsigned(uint lval)
{
//it implements unsigned exp golomb coding
uint lvalint = lval + 1;
int nnumbits = (int)(Math.Log(lvalint,2) + 1);
for (int n = 0; n < (nnumbits - 1); n++)
{
addbits(0, 1);
}
addbits(lvalint, nnumbits);
}
//! Adds lval to the end of h264 bitstream using exp golomb coding for signed values
/*!
\param nVal value to add at the end of the h264 stream
*/
public void addexpgolombsigned(int lval)
{
//it implements a signed exp golomb coding
uint lvalint = (uint)(Math.Abs(lval) * 2 - 1);
if (lval <= 0)
{
lvalint = (uint)(2 * Math.Abs(lval));
}
addexpgolombunsigned(lvalint);
}
//! Adds 0 to the end of h264 bistream in order to leave a byte aligned stream (It will insert seven 0 maximum)
public void dobytealign()
{
//Check if the last bit in buffer is multiple of 8
int nr = m_nLastbitinbuffer % 8;
if ((nr % 8) != 0)
{
m_nLastbitinbuffer = m_nLastbitinbuffer + (8 - nr);
}
}
//! Adds cByte (8 bits) to the end of h264 bitstream. This function it is optimized in byte aligned streams.
/*!
\param cByte value to add at the end of the h264 stream (from 0 to 255)
*/
public void addbyte(byte cByte)
{
//Byte alignment optimization
if ((m_nLastbitinbuffer % 8) == 0)
{
addbytetostream(cByte);
}
else
{
addbits(cByte, 8);
}
}
//! Close the h264 stream saving to disk the last remaing bits in buffer
public void close()
{
//Flush the data in stream buffer
dobytealign();
while (m_nLastbitinbuffer != 0)
{
savebufferbyte();
}
}
// 'writing' to memory
private void fwrite(byte b, int x, int y, List<byte>data)
{
data.Add(b);
}
}
}

View File

@@ -0,0 +1,498 @@
/* * CJOCh264encoder.cpp
*
* Created on: Aug 17, 2014
* Author: Jordi Cenzano (www.jordicenzano.name)
*/
/*
* CJOCh264encoder.h
*
* Created on: Aug 17, 2014
* Author: Jordi Cenzano (www.jordicenzano.name)
*/
//C++ TO C# CONVERTER NOTE: The following #define macro was replaced in-line:
//ORIGINAL LINE: #define BUFFER_SIZE_BYTES (24/8)
//! h264 encoder class
/*!
It is used to create the h264 compliant stream
*/
using System.Collections.Generic;
namespace Inspectron.HawkEye.RTSP.Server
{
public class CJOCh264encoder : CJOCh264bitstream
{
/**
* Allowed sample formats
*/
public enum enSampleFormat
{
SAMPLE_FORMAT_YUV420p //!< SAMPLE_FORMAT_YUV420p
}
public List<byte> m_pOutFile = null;
public byte[] sps = null;
public byte[] pps = null;
public byte[] nal = null;
/*!Set the used Y macroblock size for I PCM in YUV420p */
private const int MACROBLOCK_Y_WIDTH = 16;
private const int MACROBLOCK_Y_HEIGHT = 16;
/*!Set time base in Hz */
private const int TIME_SCALE_IN_HZ = 27000000;
/*!Pointer to pixels */
private class YUV420p_frame_t
{
public byte[] pYCbCr;
}
/*! Frame */
private class frame_t
{
public enSampleFormat sampleformat; //!< Sample format
public uint nYwidth; //!< Y (luminance) block width in pixels
public uint nYheight; //!< Y (luminance) block height in pixels
public uint nCwidth; //!< C (Crominance) block width in pixels
public uint nCheight; //!< C (Crominance) block height in pixels
public uint nYmbwidth; //!< Y (luminance) macroblock width in pixels
public uint nYmbheight; //!< Y (luminance) macroblock height in pixels
public uint nCmbwidth; //!< Y (Crominance) macroblock width in pixels
public uint nCmbheight; //!< Y (Crominance) macroblock height in pixels
public YUV420p_frame_t yuv420pframe = new YUV420p_frame_t(); //!< Pointer to current frame data
public uint nyuv420pframesize; //!< Size in bytes of yuv420pframe
}
/*! The frame var*/
private frame_t m_frame = new frame_t();
/*! The frames per second var*/
private uint m_nFps;
/*! Number of frames sent to the output */
private uint m_lNumFramesAdded;
//! Frees the frame yuv420pframe allocated memory
//Free the allocated video frame mem
private void free_video_src_frame()
{
if (m_frame.yuv420pframe.pYCbCr != null)
{
//C++ TO C# CONVERTER TODO TASK: The memory management function 'free' has no equivalent in C#:
// free(m_frame.yuv420pframe.pYCbCr);
}
//C++ TO C# CONVERTER TODO TASK: The memory management function 'memset' has no equivalent in C#:
// memset(m_frame, 0, sizeof(frame_t));
}
//! Allocs the frame yuv420pframe memory according to the frame properties
//Alloc mem to store a video frame
private void alloc_video_src_frame()
{
if (m_frame.yuv420pframe.pYCbCr != null)
{
throw new System.Exception("Error: null values in frame");
}
uint nYsize = m_frame.nYwidth * m_frame.nYheight;
uint nCsize = m_frame.nCwidth * m_frame.nCheight;
m_frame.nyuv420pframesize = nYsize + nCsize + nCsize;
m_frame.yuv420pframe.pYCbCr = new byte[m_frame.nyuv420pframesize];
if (m_frame.yuv420pframe.pYCbCr == null)
{
throw new System.Exception("Error: memory alloc");
}
}
//! Creates SPS NAL and add it to the output
/*!
\param nImW Frame width in pixels
\param nImH Frame height in pixels
\param nMbW macroblock width in pixels
\param nMbH macroblock height in pixels
\param nFps frames x second (tipical values are: 25, 30, 50, etc)
\param nSARw Indicates the horizontal size of the sample aspect ratio (tipical values are:1, 4, 16, etc)
\param nSARh Indicates the vertical size of the sample aspect ratio (tipical values are:1, 3, 9, etc)
*/
//Creates and saves the NAL SPS (including VUI) (one per file)
private void create_sps(uint nImW, uint nImH, uint nMbW, uint nMbH, uint nFps, uint nSARw, uint nSARh)
{
add4bytesnoemulationprevention(0x000001); // NAL header
addbits(0x0, 1); // forbidden_bit
addbits(0x3, 2); // nal_ref_idc
addbits(0x7, 5); // nal_unit_type : 7 ( SPS )
addbits(0x42, 8); // profile_idc = baseline ( 0x42 )
addbits(0x0, 1); // constraint_set0_flag
addbits(0x0, 1); // constraint_set1_flag
addbits(0x0, 1); // constraint_set2_flag
addbits(0x0, 1); // constraint_set3_flag
addbits(0x0, 1); // constraint_set4_flag
addbits(0x0, 1); // constraint_set5_flag
addbits(0x0, 2); // reserved_zero_2bits /* equal to 0 */
addbits(0x0a, 8); // level_idc: 3.1 (0x0a)
addexpgolombunsigned(0); // seq_parameter_set_id
addexpgolombunsigned(0); // log2_max_frame_num_minus4
addexpgolombunsigned(0); // pic_order_cnt_type
addexpgolombunsigned(0); // log2_max_pic_order_cnt_lsb_minus4
addexpgolombunsigned(0); // max_num_refs_frames
addbits(0x0, 1); // gaps_in_frame_num_value_allowed_flag
uint nWinMbs = nImW / nMbW;
addexpgolombunsigned(nWinMbs - 1); // pic_width_in_mbs_minus_1
uint nHinMbs = nImH / nMbH;
addexpgolombunsigned(nHinMbs - 1); // pic_height_in_map_units_minus_1
addbits(0x1, 1); // frame_mbs_only_flag
addbits(0x0, 1); // direct_8x8_interfernce
addbits(0x0, 1); // frame_cropping_flag
// addbits(0x1, 1); // vui_parameter_present
addbits(0x0, 1); // vui_parameter_present
//VUI parameters (AR, timming)
// addbits(0x1, 1); //aspect_ratio_info_present_flag
// addbits(0xFF, 8); //aspect_ratio_idc = Extended_SAR
//AR
// addbits(nSARw, 16); //sar_width
// addbits(nSARh, 16); //sar_height
// addbits(0x0, 1); //overscan_info_present_flag
// addbits(0x0, 1); //video_signal_type_present_flag
// addbits(0x0, 1); //chroma_loc_info_present_flag
// addbits(0x1, 1); //timing_info_present_flag
// uint nnum_units_in_tick = TIME_SCALE_IN_HZ / (2 * nFps);
// addbits(nnum_units_in_tick, 32); //num_units_in_tick
// addbits(TIME_SCALE_IN_HZ, 32); //time_scale
// addbits(0x1, 1); //fixed_frame_rate_flag
// addbits(0x0, 1); //nal_hrd_parameters_present_flag
// addbits(0x0, 1); //vcl_hrd_parameters_present_flag
// addbits(0x0, 1); //pic_struct_present_flag
// addbits(0x0, 1); //bitstream_restriction_flag
//END VUI
//BUG? addbits(0x0, 1); // frame_mbs_only_flag
addbits(0x1, 1); // rbsp stop bit
dobytealign();
}
//! Creates PPS NAL and add it to the output
//Creates and saves the NAL PPS (one per file)
private void create_pps()
{
add4bytesnoemulationprevention(0x000001); // NAL header
addbits(0x0, 1); // forbidden_bit
addbits(0x3, 2); // nal_ref_idc
addbits(0x8, 5); // nal_unit_type : 8 ( PPS )
addexpgolombunsigned(0); // pic_parameter_set_id
addexpgolombunsigned(0); // seq_parameter_set_id
addbits(0x0, 1); // entropy_coding_mode_flag
addbits(0x0, 1); // bottom_field_pic_order_in frame_present_flag
addexpgolombunsigned(0); // nun_slices_groups_minus1
addexpgolombunsigned(0); // num_ref_idx10_default_active_minus
addexpgolombunsigned(0); // num_ref_idx11_default_active_minus
addbits(0x0, 1); // weighted_pred_flag
addbits(0x0, 2); // weighted_bipred_idc
addexpgolombsigned(0); // pic_init_qp_minus26
addexpgolombsigned(0); // pic_init_qs_minus26
addexpgolombsigned(0); // chroma_qp_index_offset
addbits(0x0, 1); //deblocking_filter_present_flag
addbits(0x0, 1); // constrained_intra_pred_flag
addbits(0x0, 1); //redundant_pic_ent_present_flag
addbits(0x1, 1); // rbsp stop bit
dobytealign();
}
//! Creates Slice NAL and add it to the output
/*!
\param lFrameNum number of frame
*/
//Creates and saves the NAL SLICE (one per frame)
//H264 Spec Section 7.3.3 Slice Header Syntax
private void create_slice_header(uint lFrameNum)
{
add4bytesnoemulationprevention(0x000001); // NAL header
addbits(0x0, 1); // forbidden_bit
addbits(0x3, 2); // nal_ref_idc
addbits(0x5, 5); // nal_unit_type : 5 ( Coded slice of an IDR picture )
addexpgolombunsigned(0); // first_mb_in_slice
addexpgolombunsigned(7); // slice_type
addexpgolombunsigned(0); // pic_param_set_id
byte cFrameNum = 0; // (byte)(lFrameNum % 16); // H264 Spec says "If the current picture is an IDR picture, frame_num shall be equal to 0. "
// Also any maths here must relate to the value of log2_max_frame_num_minus4 in the SPS
addbits(cFrameNum, 4); // frame_num ( numbits = v = log2_max_frame_num_minus4 + 4)
// idr_pic_id range is 0..65535. All slices in the same IDR must have the same pic_id. Spec says if there are two
// IDRs back to back they must have different idr_pic_id values
uint lidr_pic_id = lFrameNum % 65536;
addexpgolombunsigned(lidr_pic_id); // idr_pic_id
addbits(0x0, 4); // pic_order_cnt_lsb (numbits = v = log2_max_fpic_order_cnt_lsb_minus4 + 4)
// nal_ref_idc != 0. Insert dec_ref_pic_marking
addbits(0x0, 1); // no_output_of_prior_pics_flag
addbits(0x0, 1); // long_term_reference_flag
addexpgolombsigned(0); //slice_qp_delta
//Probably NOT byte aligned!!!
}
//! Creates macroblock header and add it to the output
//Creates and saves the macroblock header(one per macroblock)
private void create_macroblock_header()
{
addexpgolombunsigned(25); // mb_type (I_PCM)
}
//! Creates the slice footer and add it to the output
//Creates and saves the SLICE footer (one per SLICE)
private void create_slice_footer()
{
addbits(0x1, 1); // rbsp stop bit
}
//! Creates SPS NAL and add it to the output
/*!
\param nYpos First vertical macroblock pixel inside the frame
\param nYpos nXpos horizontal macroblock pixel inside the frame
*/
//Creates & saves a macroblock (coded INTRA 16x16)
private void create_macroblock(uint nYpos, uint nXpos)
{
uint x;
uint y;
create_macroblock_header();
dobytealign();
//Y
uint nYsize = m_frame.nYwidth * m_frame.nYheight;
for (y = nYpos * m_frame.nYmbheight; y < (nYpos + 1) * m_frame.nYmbheight; y++)
{
for (x = nXpos * m_frame.nYmbwidth; x < (nXpos + 1) * m_frame.nYmbwidth; x++)
{
addbyte(m_frame.yuv420pframe.pYCbCr[(y * m_frame.nYwidth + x)]);
}
}
//Cb
uint nCsize = m_frame.nCwidth * m_frame.nCheight;
for (y = nYpos * m_frame.nCmbheight; y < (nYpos + 1) * m_frame.nCmbheight; y++)
{
for (x = nXpos * m_frame.nCmbwidth; x < (nXpos + 1) * m_frame.nCmbwidth; x++)
{
addbyte(m_frame.yuv420pframe.pYCbCr[nYsize + (y * m_frame.nCwidth + x)]);
}
}
//Cr
for (y = nYpos * m_frame.nCmbheight; y < (nYpos + 1) * m_frame.nCmbheight; y++)
{
for (x = nXpos * m_frame.nCmbwidth; x < (nXpos + 1) * m_frame.nCmbwidth; x++)
{
addbyte(m_frame.yuv420pframe.pYCbCr[nYsize + nCsize + (y * m_frame.nCwidth + x)]);
}
}
}
//! Constructor
/*!
\param pOutFile The output file pointer
*/
//Private functions
//Contructor
public CJOCh264encoder(List<byte> pOutFile) : base(pOutFile)
{
m_lNumFramesAdded = 0;
//C++ TO C# CONVERTER TODO TASK: The memory management function 'memset' has no equivalent in C#:
//memset(m_frame, 0, sizeof(frame_t));
m_nFps = 25;
m_pOutFile = pOutFile;
}
//! Destructor
//Destructor
public override void Dispose()
{
free_video_src_frame();
base.Dispose();
}
//! Initializes the coder
/*!
\param nImW Frame width in pixels
\param nImH Frame height in pixels
\param nFps Desired frames per second of the output file (typical values are: 25, 30, 50, etc)
\param SampleFormat Sample format if the input file. In this implementation only SAMPLE_FORMAT_YUV420p is allowed
\param nSARw Indicates the horizontal size of the sample aspect ratio (typical values are:1, 4, 16, etc)
\param nSARh Indicates the vertical size of the sample aspect ratio (typical values are:1, 3, 9, etc)
*/
//public functions
//Initilizes the h264 coder (mini-coder)
public void IniCoder(uint nImW, uint nImH, uint nImFps, CJOCh264encoder.enSampleFormat SampleFormat, uint nSARw = 1, uint nSARh = 1)
{
m_lNumFramesAdded = 0;
if (SampleFormat != enSampleFormat.SAMPLE_FORMAT_YUV420p)
{
throw new System.Exception("Error: SAMPLE FORMAT not allowed. Only yuv420p is allowed in this version");
}
free_video_src_frame();
//Ini vars
m_frame.sampleformat = SampleFormat;
m_frame.nYwidth = nImW;
m_frame.nYheight = nImH;
if (SampleFormat == enSampleFormat.SAMPLE_FORMAT_YUV420p)
{
//Set macroblock Y size
m_frame.nYmbwidth = MACROBLOCK_Y_WIDTH;
m_frame.nYmbheight = MACROBLOCK_Y_HEIGHT;
//Set macroblock C size (in YUV420 is 1/2 of Y)
m_frame.nCmbwidth = MACROBLOCK_Y_WIDTH / 2;
m_frame.nCmbheight = MACROBLOCK_Y_HEIGHT / 2;
//Set C size
m_frame.nCwidth = m_frame.nYwidth / 2;
m_frame.nCheight = m_frame.nYheight / 2;
//In this implementation only picture sizes multiples of macroblock size (16x16) are allowed
if (((nImW % MACROBLOCK_Y_WIDTH) != 0) || ((nImH % MACROBLOCK_Y_HEIGHT) != 0))
{
throw new System.Exception("Error: size not allowed. Only multiples of macroblock are allowed (macroblock size is: 16x16)");
}
}
m_nFps = nImFps;
//Alloc mem for 1 frame
alloc_video_src_frame();
//Create h264 SPS & PPS
create_sps(m_frame.nYwidth, m_frame.nYheight, m_frame.nYmbwidth, m_frame.nYmbheight, nImFps, nSARw, nSARh);
close(); // Flush data to the List<byte>
sps = m_pOutFile.ToArray();
m_pOutFile.Clear();
create_pps();
close(); // Flush data to the List<byte>
pps = m_pOutFile.ToArray();
m_pOutFile.Clear();
}
//! Returns the frame pointer
/*!
\return Frame pointer ready to fill with frame pixels data (the format to fill the data is indicated by SampleFormat parameter when the coder is initialized
*/
//Returns the frame pointer to load the video frame
public byte[] GetFramePtr()
{
if (m_frame.yuv420pframe.pYCbCr == null)
{
throw new System.Exception("Error: video frame is null (not initialized)");
}
return m_frame.yuv420pframe.pYCbCr;
}
//! Returns the allocated frame memory in bytes
/*!
\return The allocated memory to store the frame data
*/
//Returns the the allocated size for video frame
public uint GetFrameSize()
{
return m_frame.nyuv420pframesize;
}
//! It codes the frame that is in frame memory a it saves the coded data to disc
//Codifies & save the video frame (it only uses 16x16 intra PCM -> NO COMPRESSION!)
public void CodeAndSaveFrame()
{
m_pOutFile.Clear();
//The slice header is not byte aligned, so the first macroblock header is not byte aligned
create_slice_header(m_lNumFramesAdded);
//Loop over macroblock size
uint y;
uint x;
for (y = 0; y < m_frame.nYheight / m_frame.nYmbheight; y++)
{
for (x = 0; x < m_frame.nYwidth / m_frame.nYmbwidth; x++)
{
create_macroblock(y, x);
}
}
create_slice_footer();
dobytealign();
m_lNumFramesAdded++;
// flush
close();
nal = m_pOutFile.ToArray();
}
//! Returns number of coded frames
/*!
\return The number of coded frames
*/
//Returns the number of codified frames
public uint GetSavedFrames()
{
return m_lNumFramesAdded;
}
//! Flush all data and save the trailing bits
//Closes the h264 coder saving the last bits in the buffer
public void CloseCoder()
{
close();
}
}
}

View File

@@ -0,0 +1,34 @@
namespace Inspectron.HawkEye.RTSP.Server
{
public static class RTPPacketUtil
{
public static void WriteHeader(byte[] rtp_packet, int rtp_version, int rtp_padding, int rtp_extension, int rtp_csrc_count, int rtp_marker, int rtp_payload_type)
{
rtp_packet[0] = (byte)((rtp_version << 6) | (rtp_padding << 5) | (rtp_extension << 4) | rtp_csrc_count);
rtp_packet[1] = (byte)((rtp_marker << 7) | (rtp_payload_type & 0x7F));
}
public static void WriteSequenceNumber(byte[] rtp_packet, uint empty_sequence_id)
{
rtp_packet[2] = ((byte)((empty_sequence_id >> 8) & 0xFF));
rtp_packet[3] = ((byte)((empty_sequence_id >> 0) & 0xFF));
}
public static void WriteTS(byte[] rtp_packet, uint ts)
{
rtp_packet[4] = ((byte)((ts >> 24) & 0xFF));
rtp_packet[5] = ((byte)((ts >> 16) & 0xFF));
rtp_packet[6] = ((byte)((ts >> 8) & 0xFF));
rtp_packet[7] = ((byte)((ts >> 0) & 0xFF));
}
public static void WriteSSRC(byte[] rtp_packet, uint ssrc)
{
rtp_packet[8] = ((byte)((ssrc >> 24) & 0xFF));
rtp_packet[9] = ((byte)((ssrc >> 16) & 0xFF));
rtp_packet[10] = ((byte)((ssrc >> 8) & 0xFF));
rtp_packet[11] = ((byte)((ssrc >> 0) & 0xFF));
}
}
}

View File

@@ -0,0 +1,800 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using Inspectron.HawkEye.RTSP.Messages;
// RTSP Server Example (c) Roger Hardiman, 2016, 2018
// Released uder the MIT Open Source Licence
//
// Re-uses some code from the Multiplexer example of SharpRTSP
//
// This example simulates a live RTSP video stream, for example a CCTV Camera
// It creates a Video Source (a test card) that creates a YUV Image
// The image is then encoded as H264 data using a very basic H264 Encoder
// The H264 data (the NALs) are sent to the RTSP clients
// Video is sent in UDP Mode or TCP Mode (ie RTP over RTSP mode)
// The Tiny H264 Encoder is a 100% .NET encoder which is lossless and creates large bitstreams as
// there is no compression. It is limited to 128x96 resolution. However it makes it easy to write a quick
// demo without needing native APIs or cross compiled C libraries for H264
namespace Inspectron.HawkEye.RTSP.Server
{
public class RtspServer : IDisposable
{
const int h264_width = 192; // Tiny needs 128x96
const int h264_height = 128;
const int h264_fps = 25;
const uint global_ssrc = 0x4321FADE; // 8 hex digits
private TcpListener _RTSPServerListener;
private ManualResetEvent _Stopping;
private Thread _ListenTread;
private TestCard video_source = null;
private SimpleH264Encoder h264_encoder = null;
//private TinyH264Encoder h264_encoder = null;
byte[] raw_sps = null;
byte[] raw_pps = null;
List<RTSPConnection> rtsp_list = new List<RTSPConnection>(); // list of RTSP Listeners
Random rnd = new Random();
int session_handle = 1;
Authentication auth = null;
/// <summary>
/// Initializes a new instance of the <see cref="RTSPServer"/> class.
/// </summary>
/// <param name="aPortNumber">A numero port.</param>
/// <param name="username">username.</param>
/// <param name="password">password.</param>
public RtspServer(int portNumber, String username, String password)
{
if (portNumber < System.Net.IPEndPoint.MinPort || portNumber > System.Net.IPEndPoint.MaxPort)
throw new ArgumentOutOfRangeException("aPortNumber", portNumber, "Port number must be between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort");
Contract.EndContractBlock();
if (String.IsNullOrEmpty(username) == false
&& String.IsNullOrEmpty(password) == false) {
String realm = "SharpRTSPServer";
auth = new Authentication(username,password,realm,Authentication.Type.Digest);
} else {
auth = null;
}
RtspUtils.RegisterUri();
_RTSPServerListener = new TcpListener(IPAddress.Any, portNumber);
}
/// <summary>
/// Starts the listen.
/// </summary>
public void StartListen()
{
_RTSPServerListener.Start();
_Stopping = new ManualResetEvent(false);
_ListenTread = new Thread(new ThreadStart(AcceptConnection));
_ListenTread.Start();
// Initialise the H264 encoder
h264_encoder = new SimpleH264Encoder(h264_width, h264_height, h264_fps);
//h264_encoder = new TinyH264Encoder(); // hard coded to 192x128
// Start the VideoSource
video_source = new TestCard(h264_width, h264_height, h264_fps);
video_source.ReceivedYUVFrame += video_source_ReceivedYUVFrame;
}
/// <summary>
/// Accepts the connection.
/// </summary>
private void AcceptConnection()
{
try
{
while (!_Stopping.WaitOne(0))
{
// Wait for an incoming TCP Connection
TcpClient oneClient = _RTSPServerListener.AcceptTcpClient();
Console.WriteLine("Connection from " + oneClient.Client.RemoteEndPoint.ToString());
// Hand the incoming TCP connection over to the RTSP classes
var rtsp_socket = new RtspTcpTransport(oneClient);
RtspListener newListener = new RtspListener(rtsp_socket);
newListener.MessageReceived += RTSP_Message_Received;
//RTSPDispatcher.Instance.AddListener(newListener);
// Add the RtspListener to the RTSPConnections List
lock (rtsp_list) {
RTSPConnection new_connection = new RTSPConnection();
new_connection.listener = newListener;
new_connection.client_hostname = newListener.RemoteAdress.Split(':')[0];
new_connection.ssrc = global_ssrc;
new_connection.time_since_last_rtsp_keepalive = DateTime.UtcNow;
new_connection.video_time_since_last_rtcp_keepalive = DateTime.UtcNow;
rtsp_list.Add(new_connection);
}
newListener.Start();
}
}
catch (SocketException error)
{
// _logger.Warn("Got an error listening, I have to handle the stopping which also throw an error", error);
}
catch (Exception error)
{
// _logger.Error("Got an error listening...", error);
throw;
}
}
public void StopListen()
{
_RTSPServerListener.Stop();
_Stopping.Set();
_ListenTread.Join();
}
#region IDisposable Membres
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
StopListen();
_Stopping.Dispose();
}
}
#endregion
// Process each RTSP message that is received
private void RTSP_Message_Received(object sender, RtspChunkEventArgs e)
{
// Cast the 'sender' and 'e' into the RTSP Listener (the Socket) and the RTSP Message
RtspListener listener = sender as RtspListener;
RtspMessage message = e.Message as RtspMessage;
Console.WriteLine("RTSP message received " + message);
// Check if the RTSP Message has valid authentication (validating against username,password,realm and nonce)
if (auth != null) {
bool authorized = false;
if (message.Headers.ContainsKey("Authorization") == true ) {
// The Header contained Authorization
// Check the message has the correct Authorization
// If it does not have the correct Authorization then close the RTSP connection
authorized = auth.IsValid(message);
if (authorized == false) {
// Send a 401 Authentication Failed reply, then close the RTSP Socket
RtspResponse authorization_response = (e.Message as RtspRequest).CreateResponse();
authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader());
authorization_response.ReturnCode = 401;
listener.SendMessage(authorization_response);
lock (rtsp_list) {
foreach (RTSPConnection connection in rtsp_list.ToArray()){
if (connection.listener == listener) {
rtsp_list.Remove(connection);
}
}
}
listener.Dispose();
return;
}
}
if ((message.Headers.ContainsKey("Authorization") == false)){
// Send a 401 Authentication Failed with extra info in WWW-Authenticate
// to tell the Client if we are using Basic or Digest Authentication
RtspResponse authorization_response = (e.Message as RtspRequest).CreateResponse();
authorization_response.AddHeader("WWW-Authenticate: " + auth.GetHeader()); // 'Basic' or 'Digest'
authorization_response.ReturnCode = 401;
listener.SendMessage(authorization_response);
return;
}
}
// Update the RTSP Keepalive Timeout
// We could check that the message is GET_PARAMETER or OPTIONS for a keepalive but instead we will update the timer on any message
lock (rtsp_list)
{
foreach (RTSPConnection connection in rtsp_list)
{
if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress))
{
// found the connection
connection.time_since_last_rtsp_keepalive = DateTime.UtcNow;
break;
}
}
}
// Handle OPTIONS message
if (message is RtspRequestOptions)
{
// Create the reponse to OPTIONS
RtspResponse options_response = (e.Message as RtspRequestOptions).CreateResponse();
listener.SendMessage(options_response);
}
// Handle DESCRIBE message
if (message is RtspRequestDescribe)
{
String requested_url = (message as RtspRequestDescribe).RtspUri.ToString();
Console.WriteLine("Request for " + requested_url);
// TODO. Check the requsted_url is valid. In this example we accept any RTSP URL
// Make the Base64 SPS and PPS
raw_sps = h264_encoder.GetRawSPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
raw_pps = h264_encoder.GetRawPPS(); // no 0x00 0x00 0x00 0x01 or 32 bit size header
String sps_str = Convert.ToBase64String(raw_sps);
String pps_str = Convert.ToBase64String(raw_pps);
StringBuilder sdp = new StringBuilder();
// Generate the SDP
// The sprop-parameter-sets provide the SPS and PPS for H264 video
// The packetization-mode defines the H264 over RTP payloads used but is Optional
sdp.Append("v=0\n");
sdp.Append("o=user 123 0 IN IP4 0.0.0.0\n");
sdp.Append("s=SharpRTSP Test Camera\n");
sdp.Append("m=video 0 RTP/AVP 96\n");
sdp.Append("c=IN IP4 0.0.0.0\n");
sdp.Append("a=control:trackID=0\n");
sdp.Append("a=rtpmap:96 H264/90000\n");
sdp.Append("a=fmtp:96 profile-level-id=42A01E; sprop-parameter-sets=" + sps_str + "," + pps_str + ";\n");
byte[] sdp_bytes = Encoding.ASCII.GetBytes(sdp.ToString());
// Create the reponse to DESCRIBE
// This must include the Session Description Protocol (SDP)
RtspResponse describe_response = (e.Message as RtspRequestDescribe).CreateResponse();
describe_response.AddHeader("Content-Base: " + requested_url);
describe_response.AddHeader("Content-Type: application/sdp");
describe_response.Data = sdp_bytes;
describe_response.AdjustContentLength();
listener.SendMessage(describe_response);
}
// Handle SETUP message
if (message is RtspRequestSetup)
{
//
var setupMessage = message as RtspRequestSetup;
// Check the RTSP transport
// If it is UDP or Multicast, create the sockets
// If it is RTP over RTSP we send data via the RTSP Listener
// FIXME client may send more than one possible transport.
// very rare
RtspTransport transport = setupMessage.GetTransports()[0];
// Construct the Transport: reply from the Server to the client
RtspTransport transport_reply = new RtspTransport();
transport_reply.SSrc = global_ssrc.ToString("X8"); // Convert to Hex, padded to 8 characters
if (transport.LowerTransport == RtspTransport.LowerTransportType.TCP)
{
// RTP over RTSP mode}
transport_reply.LowerTransport = RtspTransport.LowerTransportType.TCP;
transport_reply.Interleaved = new PortCouple(transport.Interleaved.First, transport.Interleaved.Second);
}
UDPSocket udp_pair = null;
if (transport.LowerTransport == RtspTransport.LowerTransportType.UDP
&& transport.IsMulticast == false)
{
Boolean udp_supported = true;
if (udp_supported) {
// RTP over UDP mode
// Create a pair of UDP sockets - One is for the Video, one is for the RTCP
udp_pair = new UDPSocket(50000, 51000); // give a range of 500 pairs (1000 addresses) to try incase some address are in use
udp_pair.DataReceived += (object local_sender, RtspChunkEventArgs local_e) => {
// RTCP data received
Console.WriteLine("RTCP data received " + local_sender.ToString() + " " + local_e.ToString());
};
udp_pair.Start(); // start listening for data on the UDP ports
// Pass the Port of the two sockets back in the reply
transport_reply.LowerTransport = RtspTransport.LowerTransportType.UDP;
transport_reply.IsMulticast = false;
transport_reply.ClientPort = new PortCouple(udp_pair.data_port,udp_pair.control_port);
} else {
transport_reply = null;
}
}
if (transport.LowerTransport == RtspTransport.LowerTransportType.UDP
&& transport.IsMulticast == true)
{
// RTP over Multicast UDP mode}
// Create a pair of UDP sockets in Multicast Mode
// Pass the Ports of the two sockets back in the reply
transport_reply.LowerTransport = RtspTransport.LowerTransportType.UDP;
transport_reply.IsMulticast = true;
transport_reply.Port = new PortCouple(7000, 7001); // FIX
// for now until implemented
transport_reply = null;
}
if (transport_reply != null)
{
// Update the session with transport information
String copy_of_session_id = "";
lock (rtsp_list)
{
foreach (RTSPConnection connection in rtsp_list)
{
if (connection.listener.RemoteAdress.Equals(listener.RemoteAdress)) {
// ToDo - Check the Track ID to determine if this is a SETUP for the Video Stream
// or a SETUP for an Audio Stream.
// In the SDP the H264 video track is TrackID 0
// found the connection
// Add the transports to the connection
connection.video_client_transport = transport;
connection.video_transport_reply = transport_reply;
// If we are sending in UDP mode, add the UDP Socket pair and the Client Hostname
connection.video_udp_pair = udp_pair;
connection.video_session_id = session_handle.ToString();
session_handle++;
// Copy the Session ID
copy_of_session_id = connection.video_session_id;
break;
}
}
}
RtspResponse setup_response = setupMessage.CreateResponse();
setup_response.Headers[RtspHeaderNames.Transport] = transport_reply.ToString();
setup_response.Session = copy_of_session_id;
listener.SendMessage(setup_response);
}
else
{
RtspResponse setup_response = setupMessage.CreateResponse();
// unsuported transport
setup_response.ReturnCode = 461;
listener.SendMessage(setup_response);
}
}
// Handle PLAY message (Sent with a Session ID)
if (message is RtspRequestPlay)
{
lock (rtsp_list)
{
// Search for the Session in the Sessions List. Change the state to "PLAY"
bool session_found = false;
foreach (RTSPConnection connection in rtsp_list)
{
if (message.Session == connection.video_session_id) /* OR AUDIO_SESSION_ID */
{
// found the session
session_found = true;
connection.play = true; // ACTUALLY YOU COULD PAUSE JUST THE VIDEO (or JUST THE AUDIO)
string range = "npt=0-"; // Playing the 'video' from 0 seconds until the end
string rtp_info = "url="+((RtspRequestPlay)message).RtspUri+";seq=" + connection.video_sequence_number; // TODO Add rtptime +";rtptime="+session.rtp_initial_timestamp;
// Send the reply
RtspResponse play_response = (e.Message as RtspRequestPlay).CreateResponse();
play_response.AddHeader("Range: " + range);
play_response.AddHeader("RTP-Info: " + rtp_info);
listener.SendMessage(play_response);
break;
}
}
if (session_found == false) {
// Session ID was not found in the list of Sessions. Send a 454 error
RtspResponse play_failed_response = (e.Message as RtspRequestPlay).CreateResponse();
play_failed_response.ReturnCode = 454; // Session Not Found
listener.SendMessage(play_failed_response);
}
}
}
// Handle PAUSE message (Sent with a Session ID)
if (message is RtspRequestPause)
{
lock (rtsp_list)
{
// Search for the Session in the Sessions List. Change the state of "PLAY"
foreach (RTSPConnection connection in rtsp_list)
{
if (message.Session == connection.video_session_id /* OR AUDIO SESSION ID */)
{
// found the session
connection.play = false; // COULD HAVE PLAY/PAUSE FOR VIDEO AND AUDIO
break;
}
}
}
// ToDo - only send back the OK response if the Session in the RTSP message was found
RtspResponse pause_response = (e.Message as RtspRequestPause).CreateResponse();
listener.SendMessage(pause_response);
}
// Handle GET_PARAMETER message, often used as a Keep Alive
if (message is RtspRequestGetParameter)
{
// Create the reponse to GET_PARAMETER
RtspResponse getparameter_response = (e.Message as RtspRequestGetParameter).CreateResponse();
listener.SendMessage(getparameter_response);
}
// Handle TEARDOWN (sent with a Session ID)
if (message is RtspRequestTeardown)
{
lock (rtsp_list)
{
// Search for the Session in the Sessions List.
foreach (RTSPConnection connection in rtsp_list.ToArray()) // Convert to ToArray so we can delete from the rtp_list
{
if (message.Session == connection.video_session_id) // SHOULD HAVE AN AUDIO TEARDOWN AS WELL
{
// If this is UDP, close the transport
// For TCP there is no transport to close (as RTP packets were interleaved into the RTSP connection)
if (connection.video_udp_pair != null) {
connection.video_udp_pair.Stop();
connection.video_udp_pair = null;
}
rtsp_list.Remove(connection);
// Close the RTSP socket
listener.Dispose();
}
}
}
}
}
// The 'Camera' (YUV TestCard) has generated a YUV image.
// If there are RTSP clients connected then Compress the Video Frame (with H264) and send it to the client
void video_source_ReceivedYUVFrame(uint timestamp_ms, int width, int height, byte[] yuv_data)
{
DateTime now = DateTime.UtcNow;
int current_rtp_play_count = 0;
int current_rtp_count = 0;
int timeout_in_seconds = 70; // must have a RTSP message every 70 seconds or we will close the connection
lock (rtsp_list) {
current_rtp_count = rtsp_list.Count;
foreach (RTSPConnection connection in rtsp_list.ToArray()) { // Convert to Array to allow us to delete from rtsp_list
// RTSP Timeout (clients receiving RTP video over the RTSP session
// do not need to send a keepalive (so we check for Socket write errors)
Boolean sending_rtp_via_tcp = false;
if ((connection.video_client_transport != null) &&
(connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.TCP))
{
sending_rtp_via_tcp = true;
}
if (sending_rtp_via_tcp == false && ((now - connection.time_since_last_rtsp_keepalive).TotalSeconds > timeout_in_seconds)) {
Console.WriteLine("Removing session " + connection.video_session_id + " due to TIMEOUT");
connection.play = false; // stop sending data
if (connection.video_udp_pair != null)
{
connection.video_udp_pair.Stop();
connection.video_udp_pair = null;
}
connection.listener.Dispose();
rtsp_list.Remove(connection);
continue;
}
else if (connection.play) current_rtp_play_count++;
}
}
// Take the YUV image and encode it into a H264 NAL
// This returns a NAL with no headers (no 00 00 00 01 header and no 32 bit sizes)
Console.WriteLine(current_rtp_count + " RTSP clients connected. " + current_rtp_play_count + " RTSP clients in PLAY mode");
if (current_rtp_play_count == 0) return;
// Compress the video (YUV to H264)
byte[] raw_video_nal = h264_encoder.CompressFrame(yuv_data);
Boolean isKeyframe = true; // SimpleH264encoder and TinyH24encoder only emit keyframes
List<byte[]> nal_array = new List<byte[]>();
// We may want to add the SPS and PPS to the H264 stream as in-band data.
// This may be of use if the client did not parse the SPS/PPS in the SDP
// or if the H264 encoder changes properties (eg a new resolution or framerate which
// gives a new SPS or PPS).
// Also looking towards H265, the VPS/SPS/PPS do not need to be in the SDP so would be added here.
Boolean add_sps_pps_to_keyframe = true;
if (add_sps_pps_to_keyframe && isKeyframe) {
nal_array.Add(raw_sps);
nal_array.Add(raw_pps);
}
// add the rest of the NALs
nal_array.Add(raw_video_nal);
UInt32 rtp_timestamp = timestamp_ms * 90; // 90kHz clock
// Build a list of 1 or more RTP packets
// The last packet will have the M bit set to '1'
List<byte[]> rtp_packets = new List<byte[]>();
for(int x = 0; x < nal_array.Count; x++) {
byte[] raw_nal = nal_array[x];
Boolean last_nal = false;
if (x == nal_array.Count - 1) {
last_nal = true; // last NAL in our nal_array
}
// The H264 Payload could be sent as one large RTP packet (assuming the receiver can handle it)
// or as a Fragmented Data, split over several RTP packets with the same Timestamp.
bool fragmenting = false;
int packetMTU = 65500;
if (raw_nal.Length > packetMTU) fragmenting = true;
if (fragmenting == false)
{
// Put the whole NAL into one RTP packet.
// Note some receivers will have maximum buffers and be unable to handle large RTP packets.
// Also with RTP over RTSP there is a limit of 65535 bytes for the RTP packet.
byte[] rtp_packet = new byte[12 + raw_nal.Length]; // 12 is header size when there are no CSRCs or extensions
// Create an single RTP fragment
// RTP Packet Header
// 0 - Version, P, X, CC, M, PT and Sequence Number
//32 - Timestamp. H264 uses a 90kHz clock
//64 - SSRC
//96 - CSRCs (optional)
//nn - Extension ID and Length
//nn - Extension header
int rtp_version = 2;
int rtp_padding = 0;
int rtp_extension = 0;
int rtp_csrc_count = 0;
int rtp_marker = (last_nal == true ? 1 : 0); // set to 1 if the last NAL in the array
int rtp_payload_type = 96;
RTPPacketUtil.WriteHeader(rtp_packet, rtp_version, rtp_padding, rtp_extension, rtp_csrc_count, rtp_marker, rtp_payload_type);
UInt32 empty_sequence_id = 0;
RTPPacketUtil.WriteSequenceNumber(rtp_packet, empty_sequence_id);
RTPPacketUtil.WriteTS(rtp_packet, rtp_timestamp);
UInt32 empty_ssrc = 0;
RTPPacketUtil.WriteSSRC(rtp_packet, empty_ssrc);
// Now append the raw NAL
System.Array.Copy(raw_nal, 0, rtp_packet, 12, raw_nal.Length);
rtp_packets.Add(rtp_packet);
}
else
{
int data_remaining = raw_nal.Length;
int nal_pointer = 0;
int start_bit = 1;
int end_bit = 0;
// consume first byte of the raw_nal. It is used in the FU header
byte first_byte = raw_nal[0];
nal_pointer++;
data_remaining--;
while (data_remaining > 0)
{
int payload_size = Math.Min(packetMTU, data_remaining);
if (data_remaining - payload_size == 0) end_bit = 1;
byte[] rtp_packet = new byte[12 + 2 + payload_size]; // 12 is header size. 2 bytes for FU-A header. Then payload
// RTP Packet Header
// 0 - Version, P, X, CC, M, PT and Sequence Number
//32 - Timestamp. H264 uses a 90kHz clock
//64 - SSRC
//96 - CSRCs (optional)
//nn - Extension ID and Length
//nn - Extension header
int rtp_version = 2;
int rtp_padding = 0;
int rtp_extension = 0;
int rtp_csrc_count = 0;
int rtp_marker = (last_nal == true ? 1 : 0); // Marker set to 1 on last packet
int rtp_payload_type = 96;
RTPPacketUtil.WriteHeader(rtp_packet, rtp_version, rtp_padding, rtp_extension, rtp_csrc_count, rtp_marker, rtp_payload_type);
UInt32 empty_sequence_id = 0;
RTPPacketUtil.WriteSequenceNumber(rtp_packet, empty_sequence_id);
RTPPacketUtil.WriteTS(rtp_packet, rtp_timestamp);
UInt32 empty_ssrc = 0;
RTPPacketUtil.WriteSSRC(rtp_packet, empty_ssrc);
// Now append the Fragmentation Header (with Start and End marker) and part of the raw_nal
byte f_bit = 0;
byte nri = (byte)((first_byte >> 5) & 0x03); // Part of the 1st byte of the Raw NAL (NAL Reference ID)
byte type = 28; // FU-A Fragmentation
rtp_packet[12] = (byte)((f_bit << 7) + (nri << 5) + type);
rtp_packet[13] = (byte)((start_bit << 7) + (end_bit << 6) + (0 << 5) + (first_byte & 0x1F));
System.Array.Copy(raw_nal, nal_pointer, rtp_packet, 14, payload_size);
nal_pointer = nal_pointer + payload_size;
data_remaining = data_remaining - payload_size;
rtp_packets.Add(rtp_packet);
start_bit = 0;
}
}
}
lock (rtsp_list)
{
// Go through each RTSP connection and output the NAL on the Video Session
foreach (RTSPConnection connection in rtsp_list.ToArray()) // ToArray makes a temp copy of the list.
// This lets us delete items in the foreach
// eg when there is Write Error
{
// Only process Sessions in Play Mode
if (connection.play == false) continue;
String connection_type = "";
if (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.TCP) connection_type = "TCP";
if (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.UDP
&& connection.video_client_transport.IsMulticast == false) connection_type = "UDP";
if (connection.video_client_transport.LowerTransport == RtspTransport.LowerTransportType.UDP
&& connection.video_client_transport.IsMulticast == true) connection_type = "Multicast";
Console.WriteLine("Sending video session " + connection.video_session_id + " " + connection_type + " Timestamp(ms)=" + timestamp_ms + ". RTP timestamp=" + rtp_timestamp + ". Sequence="+ connection.video_sequence_number);
// There could be more than 1 RTP packet (if the data is fragmented)
Boolean write_error = false;
foreach (byte[] rtp_packet in rtp_packets)
{
// Add the specific data for each transmission
RTPPacketUtil.WriteSequenceNumber(rtp_packet, connection.video_sequence_number);
connection.video_sequence_number++;
// Add the specific SSRC for each transmission
RTPPacketUtil.WriteSSRC(rtp_packet, connection.ssrc);
// Send as RTP over RTSP (Interleaved)
if (connection.video_transport_reply.LowerTransport == RtspTransport.LowerTransportType.TCP)
{
int video_channel = connection.video_transport_reply.Interleaved.First; // second is for RTCP status messages)
object state = new object();
try
{
// send the whole NAL. With RTP over RTSP we do not need to Fragment the NAL (as we do with UDP packets or Multicast)
//session.listener.BeginSendData(video_channel, rtp_packet, new AsyncCallback(session.listener.EndSendData), state);
connection.listener.SendData(video_channel, rtp_packet);
}
catch
{
Console.WriteLine("Error writing to listener " + connection.listener.RemoteAdress);
write_error = true;
break; // exit out of foreach loop
}
}
// Send as RTP over UDP
if (connection.video_transport_reply.LowerTransport == RtspTransport.LowerTransportType.UDP && connection.video_transport_reply.IsMulticast == false)
{
try
{
// send the whole NAL. ** We could fragment the RTP packet into smaller chuncks that fit within the MTU
// Send to the IP address of the Client
// Send to the UDP Port the Client gave us in the SETUP command
connection.video_udp_pair.Write_To_Data_Port(rtp_packet,connection.client_hostname,connection.video_client_transport.ClientPort.First);
}
catch (Exception e)
{
Console.WriteLine("UDP Write Exception " + e.ToString());
Console.WriteLine("Error writing to listener " + connection.listener.RemoteAdress);
write_error = true;
break; // exit out of foreach loop
}
}
// TODO. Add Multicast
}
if (write_error)
{
Console.WriteLine("Removing session " + connection.video_session_id + " due to write error");
connection.play = false; // stop sending data
if (connection.video_udp_pair != null) {
connection.video_udp_pair.Stop();
connection.video_udp_pair = null;
}
connection.listener.Dispose();
rtsp_list.Remove(connection); // remove the session. It is dead
}
}
}
}
public class RTSPConnection
{
public RtspListener listener = null; // The RTSP client connection
public bool play = false; // set to true when Session is in Play mode
public DateTime time_since_last_rtsp_keepalive = DateTime.UtcNow; // Time since last RTSP message received - used to spot dead UDP clients
public UInt32 ssrc = 0x12345678; // SSRC value used with this client connection
public String client_hostname = ""; // Client Hostname/IP Address
public String video_session_id = ""; // RTSP Session ID used with this client connection
public UInt16 video_sequence_number = 1; // 16 bit RTP packet sequence number used with this client connection
public RtspTransport video_client_transport; // Transport: string from the client to the server
public RtspTransport video_transport_reply; // Transport: reply from the server to the client
public UDPSocket video_udp_pair = null; // Pair of UDP sockets (data and control) used when sending via UDP
public DateTime video_time_since_last_rtcp_keepalive = DateTime.UtcNow; // Time since last RTCP message received - used to spot dead UDP clients
// TODO - Add Audio
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
// Simple H264 Encoder
// Written by Jordi Cenzano (www.jordicenzano.name)
//
// Ported to C# by Roger Hardiman www.rjh.org.uk
// This is a very simple lossless H264 encoder. No compression is used and so the output NAL data is as
// large as the input YUV data.
// It is used for a quick example of H264 encoding in pure .Net without needing OS specific APIs
// or cross compiled C libraries.
//
// SimpleH264Encoder can use any image Width or Height
namespace Inspectron.HawkEye.RTSP.Server
{
public class SimpleH264Encoder
{
CJOCh264encoder h264encoder = null;
uint width = 0;
uint height = 0;
List<byte> nal = new List<byte>();
// Constuctor
public SimpleH264Encoder(uint width, uint height, uint fps)
{
// We have the ability to set the aspect ratio (SAR).
// For now we set to 1:1
uint SARw = 1;
uint SARh = 1;
// Initialise H264 encoder. The original C++ code writes to a file. In this port it writes to a List<byte>
h264encoder = new CJOCh264encoder(nal);
h264encoder.IniCoder(width, height, fps, CJOCh264encoder.enSampleFormat.SAMPLE_FORMAT_YUV420p, SARw, SARh);
this.width = width;
this.height = height;
// NAL array will contain SPS and PPS
}
// Raw SPS with no Size Header and no 00 00 00 01 headers
public byte[] GetRawSPS()
{
byte[] sps_with_header = h264encoder.sps;
byte[] sps = new byte[sps_with_header.Length - 4];
System.Array.Copy(sps_with_header, 4, sps, 0, sps.Length);
return sps;
}
public byte[] GetRawPPS()
{
byte[] pps_with_header = h264encoder.pps;
byte[] pps = new byte[pps_with_header.Length - 4];
System.Array.Copy(pps_with_header, 4, pps, 0, pps.Length);
return pps;
}
public byte[] CompressFrame(byte[] yuv_data)
{
byte[] image = h264encoder.GetFramePtr();
// copy over the YUV image
System.Array.Copy(yuv_data, image, image.Length);
// // HACK. Set the YUV pixels all to 127
// for (int hack = 0; hack < image.Length; hack++) image[hack] = 127;
h264encoder.CodeAndSaveFrame();
// Get the NAL (which has the 00 00 00 01 header)
byte[] nal_with_header = h264encoder.nal;
byte[] nal = new byte[nal_with_header.Length - 4];
System.Array.Copy(nal_with_header, 4, nal, 0, nal.Length);
return nal;
}
public void ChangeAnnexBto32BitSize(byte[] data)
{
if (data.Length < 4) return;
// change data from 0x00 0x00 0x00 0x01 format to 32 bit size
int len = data.Length - 4;// subtract Annex B header size
if (BitConverter.IsLittleEndian)
{
data[0] = (byte)((len >> 24) & 0xFF);
data[1] = (byte)((len >> 16) & 0xFF);
data[2] = (byte)((len >> 8) & 0xFF);
data[3] = (byte)((len << 0) & 0xFF);
}
else
{
data[0] = (byte)((len >> 0) & 0xFF);
data[1] = (byte)((len >> 8) & 0xFF);
data[2] = (byte)((len >> 16) & 0xFF);
data[3] = (byte)((len >> 24) & 0xFF);
}
}
}
}

View File

@@ -0,0 +1,209 @@
using System;
using System.Diagnostics;
using System.Globalization;
// (c) Roger Hardiman 2016
// This class uses a System Timer to generate a YUV image at regular intervals
// The ReceivedYUVFrame event is fired for each new YUV image
namespace Inspectron.HawkEye.RTSP.Server
{
public class TestCard
{
// Events that applications can receive
public event ReceivedYUVFrameHandler ReceivedYUVFrame;
// Delegated functions (essentially the function prototype)
public delegate void ReceivedYUVFrameHandler(uint timestamp, int width, int height, byte[] data);
// Local variables
private System.Timers.Timer frame_timer;
private int fps = 0;
private Stopwatch stopwatch;
private byte[] yuv_frame = null;
private int x_position = 0;
private int y_position = 0;
private int width = 0;
private int height = 0;
private Object generate_lock = new Object();
private long count = 0;
// ASCII Font
// Created by Roger Hardiman using an online generation tool
// http://www.riyas.org/2013/12/online-led-matrix-font-generator-with.html
byte[] ascii_0 = { 0x00, 0x3c, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3c };
byte[] ascii_1 = { 0x00, 0x08, 0x18, 0x28, 0x08, 0x08, 0x08, 0x3e };
byte[] ascii_2 = { 0x00, 0x3e, 0x42, 0x02, 0x0c, 0x30, 0x40, 0x7e };
byte[] ascii_3 = { 0x00, 0x7c, 0x02, 0x02, 0x3c, 0x02, 0x02, 0x7c };
byte[] ascii_4 = { 0x00, 0x0c, 0x14, 0x24, 0x44, 0x7e, 0x04, 0x04 };
byte[] ascii_5 = { 0x00, 0x7e, 0x40, 0x40, 0x7c, 0x02, 0x02, 0x7c };
byte[] ascii_6 = { 0x00, 0x3e, 0x40, 0x40, 0x7c, 0x42, 0x42, 0x3c };
byte[] ascii_7 = { 0x00, 0x7e, 0x02, 0x02, 0x04, 0x08, 0x10, 0x20 };
byte[] ascii_8 = { 0x00, 0x3c, 0x42, 0x42, 0x3c, 0x42, 0x42, 0x3c };
byte[] ascii_9 = { 0x00, 0x3c, 0x42, 0x42, 0x3c, 0x02, 0x02, 0x3e };
byte[] ascii_colon = { 0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00 };
byte[] ascii_space = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
byte[] ascii_dot = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00 };
// Constructor
public TestCard(int width, int height, int fps)
{
this.width = width;
this.height = height;
this.fps = fps;
// YUV size
int y_size = width * height;
int u_size = (width >> 1) * (height >> 1);
int v_size = (width >> 1) * (height >> 1);
yuv_frame = new byte[y_size + u_size + v_size];
// Set all values to 127
for (int x = 0; x < yuv_frame.Length; x++)
{
yuv_frame[x] = 127;
}
stopwatch = new Stopwatch();
stopwatch.Start();
// Start timer. The Timer will generate each YUV frame
frame_timer = new System.Timers.Timer();
frame_timer.Interval = 1; // on first pass timer will fire straight away (cannot have zero interval)
frame_timer.AutoReset = false; // do not restart timer after the time has elapsed
frame_timer.Elapsed += (object sender, System.Timers.ElapsedEventArgs e) =>
{
// send a frame
Send_YUV_Frame();
count++;
// Some CPU cycles will have been used in Sending the YUV Frame.
// Compute the delay required (the Timer Interval) before sending the next YUV frame
long time_for_next_tick_ms = (count * 1000) / fps;
long time_to_wait = time_for_next_tick_ms - stopwatch.ElapsedMilliseconds;
if (time_to_wait <= 0) time_to_wait = 1; // cannot have negative or zero intervals
frame_timer.Interval = time_to_wait;
frame_timer.Start();
};
frame_timer.Start();
}
// Dispose
public void Disconnect()
{
// Stop the frame timer
frame_timer.Stop();
frame_timer.Dispose();
}
private void Send_YUV_Frame()
{
lock (generate_lock)
{
// Get the current time
DateTime now_utc = DateTime.UtcNow;
DateTime now_local = now_utc.ToLocalTime();
long timestamp_ms = ((long)(now_utc.Ticks / TimeSpan.TicksPerMillisecond));
// Generate the String to write
char[] overlay = null;
if (width >= 96)
{
// Need 12 characters of 8x8 pixels. 12*8 = 96
// HH:MM:SS.mmm
String overlay_str = now_local.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture); // do not replace : or . by local formats
overlay = overlay_str.ToCharArray();
}
else
{
// Min for most video formats is 16x16, enough for 2 characters
String overlay_str = now_local.ToString("ss", CultureInfo.InvariantCulture); // do not replace : or . by local formats
overlay = overlay_str.ToCharArray();
}
// process each character
int start_row = ((height / 2) - 4); // start 4 pixels above the centre row (4 is half the font height)
for (int c = 0; c < overlay.Length; c++)
{
byte[] font = ascii_space;
if (overlay[c] == '0') font = ascii_0;
if (overlay[c] == '1') font = ascii_1;
if (overlay[c] == '2') font = ascii_2;
if (overlay[c] == '3') font = ascii_3;
if (overlay[c] == '4') font = ascii_4;
if (overlay[c] == '5') font = ascii_5;
if (overlay[c] == '6') font = ascii_6;
if (overlay[c] == '7') font = ascii_7;
if (overlay[c] == '8') font = ascii_8;
if (overlay[c] == '9') font = ascii_9;
if (overlay[c] == ' ') font = ascii_space;
if (overlay[c] == ':') font = ascii_colon;
if (overlay[c] == '.') font = ascii_dot;
// process the font character
for (int rows = 0; rows < 8; rows++)
{
int y_plane_pos = (start_row * width) + (rows * width) + (c * 8);
byte row_byte = font[rows];
// bit shift the row byte into individual pixels where the font On/Off maps to Y intensity 50 or 200
for (int bits = 0; bits < 8; bits++)
{
if ((row_byte & 0x80) == 0x80)
{
// Pixel On
yuv_frame[y_plane_pos] = 200;
}
else
{
yuv_frame[y_plane_pos] = 50;
}
y_plane_pos++;
row_byte = (byte)(row_byte << 1); // shift up so the next 'bit' to process is the most significant bit
}
}
}
// Toggle the pixel value
byte pixel_value = yuv_frame[(y_position * width) + x_position];
// change brightness of pixel
if (pixel_value > 128) pixel_value = 30;
else pixel_value = 230;
yuv_frame[(y_position * width) + x_position] = pixel_value;
// move the x and y position
x_position = x_position + 5;
if (x_position >= width)
{
x_position = 0;
y_position = y_position + 1;
}
if (y_position >= height)
{
y_position = 0;
}
// fire the Event
if (ReceivedYUVFrame != null)
{
ReceivedYUVFrame((uint)stopwatch.ElapsedMilliseconds, width, height, yuv_frame);
}
}
}
}
}

View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
// Tiny H264 Encoder
// World's Smallest h.264 Encoder, by Ben Mesander.
// https://cardinalpeak.com/blog/worlds-smallest-h-264-encoder/
//
// Ported to C# by Roger Hardiman www.rjh.org.uk
// Input: YUV image that must be 128x96
// Output: H264 NAL
//
// This is a very simple lossless H264 encoder. No compression is used and so the output NAL data is as
// large as the input YUV data.
// It is used for a quick example of H264 encoding in pure .Net without needing OS specific APIs
// or cross compiled C libraries.
//
// The H264 SPS/PPS data includes the image size. As the SPS/PPS is hard coded in this example the YUV
// image size must be 128 x 96
namespace Inspectron.HawkEye.RTSP.Server
{
public class TinyH264Encoder
{
int width = 0;
int height = 0;
int uv_width = 0;
int uv_height = 0;
int y_size = 0;
int u_size = 0;
int v_size = 0;
byte[] sps = { 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2 };
//byte[] sps_b = { 0x00, 0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2 }; // Annex B
//byte[] sps32 = { 0x00, 0x00, 0x00, 0x07, 0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2 }; // 32 bit size
byte[] pps = { 0x68, 0xce, 0x38, 0x80 };
//byte[] pps_b = { 0x00, 0x00, 0x00, 0x01, 0x68, 0xce, 0x38, 0x80 }; // Annex B
//byte[] pps32 = { 0x00, 0x00, 0x00, 0x04, 0x68, 0xce, 0x38, 0x80 }; // 32 bit size
byte[] slice_header = { 0x05, 0x88, 0x84, 0x21, 0xa0 };
//byte[] slice_header_b = { 0x00, 0x00, 0x00, 0x01, 0x05, 0x88, 0x84, 0x21, 0xa0 };
//byte[] slice_header_32 = { 0x00, 0x00, 0x00, 0x00, 0x05, 0x88, 0x84, 0x21, 0xa0 }; // must replace size bytes
byte[] slice_end = { 0x80 };
byte[] macroblock_header = { 0x0d, 0x00 };
List<byte> nal = new List<byte>();
// Constuctor
public TinyH264Encoder()
{
this.width = 128; // Hard coded size that is embedded in the SPS/PPS data
this.height = 96; // Hard coded size that is embedded in the SPS/PPS data
this.uv_width = width >> 1;
this.uv_height = height >> 1;
this.y_size = width * height;
this.u_size = (width >> 1) * (height >> 1);
this.v_size = (width >> 1) * (height >> 1);
}
public byte[] GetRawSPS()
{
return sps.ToArray();
}
public byte[] GetRawPPS()
{
return pps.ToArray();
}
public byte[] CompressFrame(byte[] yuv_data)
{
// we can only do 128 x 96
if (width != 128) return null;
if (height != 96) return null;
// check size
if (yuv_data.Length < (y_size + u_size + v_size))
{
// the yuv image is too small.
return null;
}
nal.Clear();
// Slice Header
foreach (byte b in slice_header) nal.Add(b);
// Add each macro block
for (int i = 0; i < (height / 16); i++) {
for (int j = 0; j < (width / 16); j++) {
macroblock(i, j, yuv_data);
}
}
// Add slice end
foreach (byte b in slice_end) nal.Add(b);
byte[] nal_array = nal.ToArray();
return nal_array;
}
/* Write a macroblock's worth of YUV data in I_PCM mode */
private void macroblock(int i, int j, byte[] frame)
{
int x, y;
if (!((i == 0) && (j == 0)))
{
foreach (byte b in macroblock_header) nal.Add(b);
}
for (x = i * 16; x < ((i + 1) * 16); x++)
{
for (y = j * 16; y < ((j + 1) * 16); y++)
{
nal.Add(frame[(x * width) + y]);
}
}
for (x = i * 8; x < (i + 1) * 8; x++)
{
for (y = j * 8; y < (j + 1) * 8; y++)
{
nal.Add(frame[y_size + (x * uv_width) + y]);
}
}
for (x = i * 8; x < (i + 1) * 8; x++)
{
for (y = j * 8; y < (j + 1) * 8; y++)
{
nal.Add(frame[y_size + u_size + (x * uv_width) + y]);
}
}
}
public void ChangeAnnexBto32BitSize(byte[] data)
{
if (data.Length < 4) return;
// change data from 0x00 0x00 0x00 0x01 format to 32 bit size
int len = data.Length - 4;// subtract Annex B header size
if (BitConverter.IsLittleEndian)
{
data[0] = (byte)((len >> 24) & 0xFF);
data[1] = (byte)((len >> 16) & 0xFF);
data[2] = (byte)((len >> 8) & 0xFF);
data[3] = (byte)((len << 0) & 0xFF);
}
else
{
data[0] = (byte)((len >> 0) & 0xFF);
data[1] = (byte)((len >> 8) & 0xFF);
data[2] = (byte)((len >> 16) & 0xFF);
data[3] = (byte)((len >> 24) & 0xFF);
}
}
}
}