@@ -1,6 +1,6 @@ | |||
language: csharp | |||
solution: JT1078.sln | |||
dotnet: 2.2.101 | |||
dotnet: 3.0.100 | |||
os: linux | |||
mono: none | |||
dist: trusty2 | |||
@@ -0,0 +1,20 @@ | |||
using JT1078.Flv.H264; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
using Xunit; | |||
namespace JT1078.Flv.Test.H264 | |||
{ | |||
public class NALUHeaderTest | |||
{ | |||
[Fact] | |||
public void Test1() | |||
{ | |||
NALUHeader header = new NALUHeader(0xc0); | |||
Assert.Equal(1, header.ForbiddenZeroBit); | |||
Assert.Equal(2, header.NalRefIdc); | |||
Assert.Equal(0, header.NalUnitType); | |||
} | |||
} | |||
} |
@@ -0,0 +1,30 @@ | |||
using System.Buffers.Binary; | |||
using Xunit; | |||
using JT1078.Flv.MessagePack; | |||
namespace JT1078.Flv.Test.MessagePack | |||
{ | |||
public class ExpGolombReaderTest | |||
{ | |||
[Fact] | |||
public void Test1() | |||
{ | |||
ExpGolombReader h264GolombReader = new ExpGolombReader(new byte[] { 103, 77, 0, 20, 149, 168, 88, 37, 144, 0 }); | |||
var result = h264GolombReader.ReadSPS(); | |||
Assert.Equal(77, result.profileIdc); | |||
Assert.Equal(0u, result.profileCompat); | |||
Assert.Equal(20, result.levelIdc); | |||
Assert.Equal(352, result.width); | |||
Assert.Equal(288, result.height); | |||
//profileIdc 77 | |||
//profileCompat 0 | |||
//levelIdc 20 | |||
//picOrderCntType 2 | |||
//picWidthInMbsMinus1 21 | |||
//picHeightInMapUnitsMinus1 17 | |||
//frameMbsOnlyFlag 1 | |||
//width 352 | |||
//height 288 | |||
} | |||
} | |||
} |
@@ -0,0 +1,66 @@ | |||
using JT1078.Flv.Extensions; | |||
using JT1078.Flv.MessagePack; | |||
using JT1078.Protocol; | |||
using System; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
namespace JT1078.Flv.H264 | |||
{ | |||
public class H264Demuxer | |||
{ | |||
public const string codecstring = "avc1."; | |||
/// <summary> | |||
/// Expunge any "Emulation Prevention" bytes from a "Raw Byte Sequence Payload" | |||
/// <see cref="https://blog.csdn.net/u011399342/article/details/80472084"/> | |||
/// 防止竞争插入0x03 | |||
/// </summary> | |||
/// <param name="srcBuffer"></param> | |||
/// <returns></returns> | |||
public byte[] DiscardEmulationPreventionBytes(ReadOnlySpan<byte> srcBuffer) | |||
{ | |||
int length = srcBuffer.Length; | |||
List<int> EPBPositions = new List<int>(); | |||
int i = 1; | |||
// Find all `Emulation Prevention Bytes` | |||
while (i < length - 2) | |||
{ | |||
if (srcBuffer[i] == 0 && srcBuffer[i + 1] == 0 && srcBuffer[i + 2] == 0x03) | |||
{ | |||
EPBPositions.Add(i + 2); | |||
i += 2; | |||
} | |||
else | |||
{ | |||
i++; | |||
} | |||
} | |||
// If no Emulation Prevention Bytes were found just return the original | |||
// array | |||
if (EPBPositions.Count == 0) | |||
{ | |||
return srcBuffer.ToArray(); | |||
} | |||
// Create a new array to hold the NAL unit data | |||
int newLength = length - EPBPositions.Count; | |||
byte[] newBuffer = new byte[newLength]; | |||
var sourceIndex = 0; | |||
for (i = 0; i < newLength; sourceIndex++, i++) | |||
{ | |||
if (sourceIndex == EPBPositions[0]) | |||
{ | |||
// Skip this byte | |||
sourceIndex++; | |||
// Remove this position index | |||
EPBPositions.RemoveAt(0); | |||
} | |||
newBuffer[i] = srcBuffer[sourceIndex]; | |||
} | |||
return newBuffer; | |||
} | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
using JT1078.Protocol; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace JT1078.Flv.H264 | |||
{ | |||
public class H264NALU | |||
{ | |||
public readonly static byte[] Start1 = new byte[3] { 0, 0, 1 }; | |||
public readonly static byte[] Start2 = new byte[4] { 0, 0, 0, 1 }; | |||
public byte[] StartCodePrefix { get; set; } | |||
public NALUHeader NALUHeader { get; set; } | |||
public JT1078Package JT1078Package { get; set; } | |||
} | |||
} |
@@ -0,0 +1,25 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace JT1078.Flv.H264 | |||
{ | |||
public struct NALUHeader | |||
{ | |||
public NALUHeader(byte value) | |||
{ | |||
ForbiddenZeroBit = (value & 0x80) >> 7; | |||
NalRefIdc = (value & 0x60) >> 5; | |||
NalUnitType = value & 0x1f; | |||
} | |||
public NALUHeader(ReadOnlySpan<byte> value) | |||
{ | |||
ForbiddenZeroBit = (value[0] & 0x80) >> 7; | |||
NalRefIdc = (value[0] & 0x60) >> 5; | |||
NalUnitType = value[0] & 0x1f; | |||
} | |||
public int ForbiddenZeroBit { get; set; } | |||
public int NalRefIdc { get; set; } | |||
public int NalUnitType { get; set; } | |||
} | |||
} |
@@ -8,5 +8,9 @@ | |||
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' "> | |||
<PackageReference Include="System.Memory" Version="4.5.3" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\JT1078.Protocol\JT1078.Protocol.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@@ -0,0 +1,312 @@ | |||
using System; | |||
using System.Buffers.Binary; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace JT1078.Flv.MessagePack | |||
{ | |||
/// <summary> | |||
/// Exp-Golomb指数哥伦布编码 | |||
/// </summary> | |||
public ref struct ExpGolombReader | |||
{ | |||
public ReadOnlySpan<byte> SrcBuffer { get; } | |||
public int BytesAvailable { get; private set; } | |||
public int Word { get; private set; } | |||
public int BitsAvailable { get; private set; } | |||
public ExpGolombReader(ReadOnlySpan<byte> srcBuffer) | |||
{ | |||
SrcBuffer = srcBuffer; | |||
BytesAvailable = srcBuffer.Length; | |||
Word = 0; | |||
BitsAvailable = 0; | |||
} | |||
public (byte profileIdc,byte levelIdc,uint profileCompat,int width, int height) ReadSPS() | |||
{ | |||
int sarScale = 1; | |||
uint frameCropLeftOffset=0; | |||
uint frameCropRightOffset = 0; | |||
uint frameCropTopOffset = 0; | |||
uint frameCropBottomOffset = 0; | |||
ReadByte(); | |||
//profile_idc | |||
byte profileIdc = ReadByte(); | |||
//constraint_set[0-4]_flag, u(5) | |||
uint profileCompat = ReadBits(5); | |||
//reserved_zero_3bits | |||
SkipBits(3); | |||
//level_idc u(8) | |||
byte levelIdc = ReadByte(); | |||
//seq_parameter_set_id | |||
SkipUEG(); | |||
if (profileIdc == 100 || | |||
profileIdc == 110 || | |||
profileIdc == 122 || | |||
profileIdc == 244 || | |||
profileIdc == 44 || | |||
profileIdc == 83 || | |||
profileIdc == 86 || | |||
profileIdc == 118 || | |||
profileIdc == 128) | |||
{ | |||
uint chromaFormatIdc = ReadUEG(); | |||
if (chromaFormatIdc == 3) | |||
{ | |||
SkipBits(1); // separate_colour_plane_flag | |||
} | |||
SkipUEG(); // bit_depth_luma_minus8 | |||
SkipUEG(); // bit_depth_chroma_minus8 | |||
SkipBits(1); // qpprime_y_zero_transform_bypass_flag | |||
if (ReadBoolean()) | |||
{ // seq_scaling_matrix_present_flag | |||
int scalingListCount = (chromaFormatIdc != 3) ? 8 : 12; | |||
for (int i = 0; i < scalingListCount; i++) | |||
{ | |||
if (ReadBoolean()) | |||
{ // seq_scaling_list_present_flag[ i ] | |||
if (i < 6) | |||
{ | |||
SkipScalingList(16); | |||
} | |||
else | |||
{ | |||
SkipScalingList(64); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
// log2_max_frame_num_minus4 | |||
SkipUEG(); | |||
var picOrderCntType = ReadUEG(); | |||
if (picOrderCntType == 0) | |||
{ | |||
ReadUEG(); //log2_max_pic_order_cnt_lsb_minus4 | |||
} | |||
else if (picOrderCntType == 1) | |||
{ | |||
SkipBits(1); // delta_pic_order_always_zero_flag | |||
SkipEG(); // offset_for_non_ref_pic | |||
SkipEG(); // offset_for_top_to_bottom_field | |||
uint numRefFramesInPicOrderCntCycle = ReadUEG(); | |||
for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) | |||
{ | |||
SkipEG(); // offset_for_ref_frame[ i ] | |||
} | |||
} | |||
SkipUEG(); // max_num_ref_frames | |||
SkipBits(1); // gaps_in_frame_num_value_allowed_flag | |||
uint picWidthInMbsMinus1 = ReadUEG(); | |||
uint picHeightInMapUnitsMinus1 = ReadUEG(); | |||
uint frameMbsOnlyFlag = ReadBits(1); | |||
if (frameMbsOnlyFlag == 0) | |||
{ | |||
SkipBits(1); // mb_adaptive_frame_field_flag | |||
} | |||
this.SkipBits(1); // direct_8x8_inference_flag | |||
if (ReadBoolean()) | |||
{ | |||
// frame_cropping_flag | |||
frameCropLeftOffset = ReadUEG(); | |||
frameCropRightOffset = ReadUEG(); | |||
frameCropTopOffset = ReadUEG(); | |||
frameCropBottomOffset = ReadUEG(); | |||
} | |||
if (ReadBoolean()) | |||
{ | |||
// vui_parameters_present_flag | |||
if (ReadBoolean()) | |||
{ | |||
// aspect_ratio_info_present_flag | |||
byte[] sarRatio=null; | |||
byte aspectRatioIdc = ReadByte(); | |||
switch (aspectRatioIdc) | |||
{ | |||
case 1: sarRatio =new byte[2] { 1, 1 }; break; | |||
case 2: sarRatio =new byte[2] { 12, 11}; break; | |||
case 3: sarRatio =new byte[2] { 10, 11}; break; | |||
case 4: sarRatio =new byte[2] { 16, 11}; break; | |||
case 5: sarRatio =new byte[2] { 40, 33}; break; | |||
case 6: sarRatio =new byte[2] { 24, 11}; break; | |||
case 7: sarRatio =new byte[2] { 20, 11}; break; | |||
case 8: sarRatio =new byte[2] { 32, 11 }; break; | |||
case 9: sarRatio = new byte[2] {80, 33 }; break; | |||
case 10: sarRatio = new byte[2]{18, 11 }; break; | |||
case 11: sarRatio = new byte[2]{15, 11 }; break; | |||
case 12: sarRatio = new byte[2]{64, 33 }; break; | |||
case 13: sarRatio = new byte[2]{160, 99 }; break; | |||
case 14: sarRatio = new byte[2]{4, 3 }; break; | |||
case 15: sarRatio = new byte[2]{3, 2 }; break; | |||
case 16: sarRatio = new byte[2]{ 2, 1 }; break; | |||
case 255: | |||
{ | |||
sarRatio = new byte[2] { (byte)(ReadByte() << 8 | ReadByte()), (byte)(ReadByte() << 8 | ReadByte()) }; | |||
break; | |||
} | |||
} | |||
if (sarRatio != null) | |||
{ | |||
sarScale = sarRatio[0] / sarRatio[1]; | |||
} | |||
} | |||
} | |||
int width= (int)((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale); | |||
int height = (int)(((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag == 1U ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset))); | |||
return (profileIdc, levelIdc, profileCompat,width, height); | |||
} | |||
public void LoadWord() | |||
{ | |||
var position = SrcBuffer.Length - BytesAvailable; | |||
int tmpAvailableBytes = BytesAvailable - 4; | |||
int availableBytes = Math.Min(4, BytesAvailable); | |||
//if (availableBytes == 0) | |||
//{ | |||
// throw new OverflowException("no bytes available"); | |||
//} | |||
ReadOnlySpan<byte> workingBytes=ReadOnlySpan<byte>.Empty; | |||
if (tmpAvailableBytes < 0) | |||
{ | |||
var buffer = new byte[4]; | |||
Array.Copy(SrcBuffer.Slice(position, BytesAvailable).ToArray(), buffer, BytesAvailable); | |||
workingBytes = buffer; | |||
} | |||
else | |||
{ | |||
workingBytes = SrcBuffer.Slice(position, 4); | |||
} | |||
Word = BinaryPrimitives.ReadInt32BigEndian(workingBytes); | |||
// track the amount of this.data that has been processed | |||
BitsAvailable = availableBytes * 8; | |||
BytesAvailable -= availableBytes; | |||
} | |||
public void SkipBits(int count) | |||
{ | |||
if (BitsAvailable > count) | |||
{ | |||
Word <<= count; | |||
BitsAvailable -= count; | |||
} | |||
else | |||
{ | |||
count -= BitsAvailable; | |||
int skipBytes = count >> 3; | |||
count -= (skipBytes >> 3); | |||
LoadWord(); | |||
Word <<= count; | |||
BitsAvailable -= count; | |||
} | |||
} | |||
public uint ReadBits(int size) | |||
{ | |||
var bits = Math.Min(BitsAvailable, size); // :uint | |||
var valu = (uint)Word >> (32 - bits); // :uint | |||
if (size > 32) | |||
{ | |||
throw new OverflowException("Cannot read more than 32 bits at a time"); | |||
} | |||
BitsAvailable -= bits; | |||
if (BitsAvailable > 0) | |||
{ | |||
Word <<= bits; | |||
} | |||
else if (BytesAvailable > 0) | |||
{ | |||
LoadWord(); | |||
} | |||
bits = size - bits; | |||
if (bits > 0) | |||
{ | |||
return ((valu << bits) | ReadBits(bits)); | |||
} | |||
else | |||
{ | |||
return valu; | |||
} | |||
} | |||
public int SkipLZ() | |||
{ | |||
int leadingZeroCount; // :uint | |||
for (leadingZeroCount = 0; leadingZeroCount < this.BitsAvailable; ++leadingZeroCount) | |||
{ | |||
if (0 != (Word & (0x80000000 >> leadingZeroCount))) | |||
{ | |||
// the first bit of working word is 1 | |||
Word <<= leadingZeroCount; | |||
BitsAvailable -= leadingZeroCount; | |||
return leadingZeroCount; | |||
} | |||
} | |||
// we exhausted word and still have not found a 1 | |||
LoadWord(); | |||
return (leadingZeroCount + SkipLZ()); | |||
} | |||
public void SkipUEG() | |||
{ | |||
SkipBits(1 + SkipLZ()); | |||
} | |||
public void SkipEG() | |||
{ | |||
SkipBits(1 + SkipLZ()); | |||
} | |||
public uint ReadUEG() | |||
{ | |||
var clz =SkipLZ(); | |||
return ReadBits(clz + 1) - 1; | |||
} | |||
public int ReadEG() | |||
{ | |||
var valu = (int)ReadUEG(); // :int | |||
if ((0x01 & valu)==1) | |||
{ | |||
// the number is odd if the low order bit is set | |||
return (1 + valu) >> 1; // add 1 to make it even, and divide by 2 | |||
} | |||
else | |||
{ | |||
return -1 * (valu >> 1); // divide by two then make it negative | |||
} | |||
} | |||
public bool ReadBoolean() | |||
{ | |||
return 1 == ReadBits(1); | |||
} | |||
public byte ReadByte() | |||
{ | |||
return (byte)ReadBits(8); | |||
} | |||
public ushort ReadUShort() | |||
{ | |||
return (ushort)ReadBits(16); | |||
} | |||
public uint ReadUInt() | |||
{ | |||
return ReadBits(32); | |||
} | |||
/// <summary> | |||
///Advance the ExpGolomb decoder past a scaling list.The scaling | |||
///list is optionally transmitted as part of a sequence parameter | |||
///set and is not relevant to transmuxing. | |||
///@param count { number} | |||
///the number of entries in this scaling list | |||
///@see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 | |||
/// </summary> | |||
/// <param name="count"></param> | |||
public void SkipScalingList(int count) | |||
{ | |||
int lastScale = 8, | |||
nextScale = 8, | |||
j, | |||
deltaScale; | |||
for (j = 0; j < count; j++) | |||
{ | |||
if (nextScale != 0) | |||
{ | |||
deltaScale = ReadEG(); | |||
nextScale = (lastScale + deltaScale + 256) % 256; | |||
} | |||
lastScale = (nextScale == 0) ? lastScale : nextScale; | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,65 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace JT1078.Flv.Metadata | |||
{ | |||
/// <summary> | |||
/// <code> | |||
///AVCDecoderConfigurationRecord 结构的定义: | |||
///aligned(8) class AVCDecoderConfigurationRecord | |||
///{ | |||
///unsigned int (8) configurationVersion = 1; | |||
///unsigned int (8) AVCProfileIndication; | |||
///unsigned int (8) profile_compatibility; | |||
///unsigned int (8) AVCLevelIndication; | |||
///bit(6) reserved = ‘111111’b; | |||
///unsigned int (2) lengthSizeMinusOne; | |||
///bit(3) reserved = ‘111’b; | |||
///unsigned int (5) numOfSequenceParameterSets; | |||
///for (i=0; i<numOfSequenceParameterSets; i++) { | |||
///unsigned int (16) sequenceParameterSetLength ; | |||
///bit(8*sequenceParameterSetLength) sequenceParameterSetNALUnit; | |||
///} | |||
///unsigned int (8) numOfPictureParameterSets; | |||
///for (i=0; i<numOfPictureParameterSets; i++) { | |||
///unsigned int (16) pictureParameterSetLength; | |||
///bit(8*pictureParameterSetLength) pictureParameterSetNALUnit; | |||
///} | |||
///} | |||
/// </code> | |||
/// </summary> | |||
public class AVCDecoderConfigurationRecord | |||
{ | |||
public byte ConfigurationVersion { get; set; } = 1; | |||
public byte AVCProfileIndication { get; set; } | |||
public byte ProfileCompatibility { get; set; } | |||
public byte AVCLevelIndication { get; set; } | |||
public int LengthSizeMinusOne { get; set; } | |||
public int NumOfSequenceParameterSets { get; set; } | |||
public List<SPSInfo> SPS { get; set; } | |||
public byte[] SPSBuffer { get; set; } | |||
public byte NumOfPictureParameterSets { get; set; } = 1; | |||
public List<PPSInfo> PPS { get; set; } | |||
public byte[] PPSBuffer { get; set; } | |||
#region Just for non-spec-conform encoders ref:org.mp4parser.boxes.iso14496.part15.AvcDecoderConfigurationRecord | |||
public const int LengthSizeMinusOnePaddingBits = 63; | |||
public const int NumberOfSequenceParameterSetsPaddingBits = 7; | |||
public const int ChromaFormatPaddingBits = 31; | |||
public const int BitDepthLumaMinus8PaddingBits = 31; | |||
public const int BitDepthChromaMinus8PaddingBits = 31; | |||
#endregion | |||
public struct SPSInfo | |||
{ | |||
public ushort SequenceParameterSetLength { get; set; } | |||
public byte[] SequenceParameterSetNALUnit { get; set; } | |||
} | |||
public struct PPSInfo | |||
{ | |||
public ushort PictureParameterSetLength { get; set; } | |||
public byte[] PictureParameterSetNALUnit { get; set; } | |||
} | |||
} | |||
} |
@@ -0,0 +1,46 @@ | |||
using JT1078.Protocol.Enums; | |||
using System; | |||
using System.Linq; | |||
using System.Collections.Concurrent; | |||
using System.Collections.Generic; | |||
using System.Text; | |||
namespace JT1078.Protocol | |||
{ | |||
public static class JT1078Demuxer | |||
{ | |||
private readonly static ConcurrentDictionary<string, JT1078Package> JT1078PackageGroupDict = new ConcurrentDictionary<string, JT1078Package>(StringComparer.OrdinalIgnoreCase); | |||
public static JT1078Package Demuxer(JT1078Package jT1078Package) | |||
{ | |||
string cacheKey = jT1078Package.GetKey(); | |||
if (jT1078Package.Label3.SubpackageType == JT1078SubPackageType.分包处理时的第一个包) | |||
{ | |||
JT1078PackageGroupDict.TryRemove(cacheKey, out _); | |||
JT1078PackageGroupDict.TryAdd(cacheKey, jT1078Package); | |||
return default; | |||
} | |||
else if (jT1078Package.Label3.SubpackageType == JT1078SubPackageType.分包处理时的中间包) | |||
{ | |||
if (JT1078PackageGroupDict.TryGetValue(cacheKey, out var tmpPackage)) | |||
{ | |||
tmpPackage.Bodies.Concat(jT1078Package.Bodies).ToArray(); | |||
JT1078PackageGroupDict[cacheKey] = tmpPackage; | |||
} | |||
return default; | |||
} | |||
else if (jT1078Package.Label3.SubpackageType == JT1078SubPackageType.分包处理时的最后一个包) | |||
{ | |||
if (JT1078PackageGroupDict.TryGetValue(cacheKey, out var tmpPackage)) | |||
{ | |||
tmpPackage.Bodies.Concat(jT1078Package.Bodies).ToArray(); | |||
return tmpPackage; | |||
} | |||
return default; | |||
} | |||
else | |||
{ | |||
return jT1078Package; | |||
} | |||
} | |||
} | |||
} |
@@ -86,5 +86,10 @@ namespace JT1078.Protocol | |||
/// 数据体 | |||
/// </summary> | |||
public byte[] Bodies{ get; set; } | |||
public string GetKey() | |||
{ | |||
return $"{SIM}_{LogicChannelNumber.ToString()}"; | |||
} | |||
} | |||
} |