@@ -1,6 +1,6 @@ | |||||
language: csharp | language: csharp | ||||
solution: JT1078.sln | solution: JT1078.sln | ||||
dotnet: 2.2.101 | |||||
dotnet: 3.0.100 | |||||
os: linux | os: linux | ||||
mono: none | mono: none | ||||
dist: trusty2 | 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' "> | <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' "> | ||||
<PackageReference Include="System.Memory" Version="4.5.3" /> | <PackageReference Include="System.Memory" Version="4.5.3" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | |||||
<ProjectReference Include="..\JT1078.Protocol\JT1078.Protocol.csproj" /> | |||||
</ItemGroup> | |||||
</Project> | </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> | /// </summary> | ||||
public byte[] Bodies{ get; set; } | public byte[] Bodies{ get; set; } | ||||
public string GetKey() | |||||
{ | |||||
return $"{SIM}_{LogicChannelNumber.ToString()}"; | |||||
} | |||||
} | } | ||||
} | } |