diff --git a/src/JT1078.Hls.Test/JT1078.Hls.Test.csproj b/src/JT1078.Hls.Test/JT1078.Hls.Test.csproj index 47b3e2b..55d92a8 100644 --- a/src/JT1078.Hls.Test/JT1078.Hls.Test.csproj +++ b/src/JT1078.Hls.Test/JT1078.Hls.Test.csproj @@ -6,11 +6,16 @@ false + + + + + diff --git a/src/JT1078.Hls.Test/M3U8Config.cs b/src/JT1078.Hls.Test/M3U8Config.cs deleted file mode 100644 index e698afb..0000000 --- a/src/JT1078.Hls.Test/M3U8Config.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace JT1078.Hls.Test -{ - /// - /// m3u8配置文件 - /// - public class M3U8Config - { - /// - /// m3u8文件中包含的ts文件数 - /// - public int TsFileCount { get; set; } = 10; - /// - /// 每个ts文件的最大时长 - /// - public int TsFileMaxSecond { get; set; } = 10; - /// - /// m3u8文件中第一个ts文件序号 - /// - public int FirstTsSerialNo { get; set; } = 0; - } -} diff --git a/src/JT1078.Hls.Test/M3U8_Test.cs b/src/JT1078.Hls.Test/M3U8_Test.cs new file mode 100644 index 0000000..ddb0825 --- /dev/null +++ b/src/JT1078.Hls.Test/M3U8_Test.cs @@ -0,0 +1,46 @@ +using JT1078.Protocol; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Xunit; +using JT1078.Protocol.Extensions; + +namespace JT1078.Hls.Test +{ + public class M3U8_Test + { + /// + /// 生成m3u8索引文件 + /// + [Fact] + public void Test4() + { + try + { + var hls_file_directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "H264", "terminalno"); + if (!File.Exists(hls_file_directory)) Directory.CreateDirectory(hls_file_directory); + var m3u8_filepath = Path.Combine(hls_file_directory, "live.m3u8"); + + TSEncoder tSEncoder = new TSEncoder(new M3U8FileManage (new Options.M3U8Option { HlsFileDirectory = hls_file_directory, M3U8Filepath = m3u8_filepath }) ); + var lines = File.ReadAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "H264", "JT1078_3.txt")); + foreach (var line in lines) + { + var data = line.Split(','); + var bytes = data[6].ToHexBytes(); + JT1078Package package = JT1078Serializer.Deserialize(bytes); + JT1078Package fullpackage = JT1078Serializer.Merge(package); + if (fullpackage != null) + { + tSEncoder.CreateM3U8File(fullpackage); + } + } + tSEncoder.AppendM3U8End(); + } + catch (Exception ex) + { + Assert.Throws(() => { }); + } + } + } +} diff --git a/src/JT1078.Hls.Test/TS_Package_Test.cs b/src/JT1078.Hls.Test/TS_Package_Test.cs index 0b135ea..f1a45a0 100644 --- a/src/JT1078.Hls.Test/TS_Package_Test.cs +++ b/src/JT1078.Hls.Test/TS_Package_Test.cs @@ -81,7 +81,7 @@ namespace JT1078.Hls.Test File.Delete(filepath); var lines = File.ReadAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "H264", "JT1078_1.txt")); fileStream = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Write); - TSEncoder tSEncoder = new TSEncoder(); + TSEncoder tSEncoder = new TSEncoder(new M3U8FileManage(new Options.M3U8Option { })); foreach (var line in lines) { var data = line.Split(','); @@ -128,7 +128,7 @@ namespace JT1078.Hls.Test var lines = File.ReadAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "H264", "JT1078_3.txt")); fileStream = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Write); bool isNeedFirstHeadler = true; - TSEncoder tSEncoder = new TSEncoder(); + TSEncoder tSEncoder = new TSEncoder(new M3U8FileManage(new Options.M3U8Option { })); foreach (var line in lines) { var data = line.Split(','); @@ -168,209 +168,6 @@ namespace JT1078.Hls.Test fileStream?.Dispose(); } } - /// - /// 生成m3u8索引文件 - /// - [Fact] - public void Test4() - { - try - { - ArrayPool arrayPool = ArrayPool.Create(); - M3U8Config m3U8Config = new M3U8Config(); - Ts_File_Manage ts_File_Manage = new Ts_File_Manage(); - double file_real_second = m3U8Config.TsFileMaxSecond; - - var lines = File.ReadAllLines(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "H264", "JT1078_3.txt")); - bool isNeedFirstHeadler = true; - TSEncoder tSEncoder = new TSEncoder(); - - ulong init_seconds = 0; - int duration = 0; - int accu_seconds = 0; - - var hls_file_direcotry = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "H264"); - var m3u8Filepath = Path.Combine(hls_file_direcotry, "index.m3u8"); - AppendM3U8Start(m3u8Filepath, m3U8Config.TsFileMaxSecond, m3U8Config.FirstTsSerialNo); - - var fileData= arrayPool.Rent(18888888); - - int fileIndex = 0; - foreach (var line in lines) - { - var data = line.Split(','); - var bytes = data[6].ToHexBytes(); - JT1078Package package = JT1078Serializer.Deserialize(bytes); - JT1078Package fullpackage = JT1078Serializer.Merge(package); - if (fullpackage != null) - { - if (accu_seconds / 1000>= m3U8Config.TsFileMaxSecond) { - //ecode_slice_header error 以非关键帧开始的报错信息 - file_real_second = accu_seconds / 1000.0;//秒 - //生成一个ts文件 - var ts_name = $"{m3U8Config.FirstTsSerialNo}.ts"; - var ts_filepath = Path.Combine(hls_file_direcotry, ts_name); - ts_File_Manage.CreateTsFile(ts_filepath, fileData.AsSpan().Slice(0, fileIndex).ToArray()); - isNeedFirstHeadler = true; - arrayPool.Return(fileData); - fileData = arrayPool.Rent(18888888); - fileIndex = 0; - var media_sequence_no = m3U8Config.FirstTsSerialNo - m3U8Config.TsFileCount; - var del_ts_name=$"{media_sequence_no}.ts"; - var del_ts_filepath = Path.Combine(hls_file_direcotry, del_ts_name); - //更新m3u8文件 - UpdateM3U8File(m3u8Filepath, file_real_second, media_sequence_no+1, del_ts_filepath, del_ts_name,ts_name); - - accu_seconds = 0; - m3U8Config.FirstTsSerialNo = m3U8Config.FirstTsSerialNo + 1; - } - - if (init_seconds == 0) - { - init_seconds = fullpackage.Timestamp; - } - else { - duration =(int)( fullpackage.Timestamp - init_seconds); - init_seconds = fullpackage.Timestamp; - accu_seconds = Convert.ToInt32(accu_seconds) + duration; - } - - if (isNeedFirstHeadler) - { - var sdt = tSEncoder.CreateSDT(fullpackage); - string sdtHEX = sdt.ToHexString(); - sdt.CopyTo(fileData, fileIndex); - fileIndex = sdt.Length; - var pat = tSEncoder.CreatePAT(fullpackage); - string patHEX = pat.ToHexString(); - pat.CopyTo(fileData, fileIndex); - fileIndex = fileIndex + pat.Length; - var pmt = tSEncoder.CreatePMT(fullpackage); - pmt.CopyTo(fileData, fileIndex); - fileIndex = fileIndex + pmt.Length; - var pes = tSEncoder.CreatePES(fullpackage, 18888); - pes.CopyTo(fileData, fileIndex); - fileIndex = fileIndex + pes.Length; - isNeedFirstHeadler = false; - } - else - { - var pes = tSEncoder.CreatePES(fullpackage, 18888); - pes.CopyTo(fileData, fileIndex); - fileIndex = fileIndex + pes.Length; - } - } - } - AppendM3U8End(m3u8Filepath); - } - catch (Exception ex) - { - Assert.Throws(() => { }); - } - } - - private void CreateTsFile(string ts_filepath, byte[] data) - { - using (var fileStream = new FileStream(ts_filepath, FileMode.OpenOrCreate, FileAccess.Write)) - { - fileStream.Write(data); - } - } - - - /// - /// - /// - /// - /// - /// - /// - private void UpdateM3U8File(string m3u8_filepath,double tsRealSecond,int media_sequence_no, string del_ts_filepath,string del_ts_name, string ts_name) { - StringBuilder sb = new StringBuilder(); - if (File.Exists(del_ts_filepath)) - { - //删除最早一个ts文件 - File.Delete(del_ts_filepath); - bool startAppendFileContent = true; - bool isFirstEXTINF = true; - using (StreamReader sr = new StreamReader(m3u8_filepath)) - { - while (!sr.EndOfStream) - { - var text = sr.ReadLine(); - if (text.Length == 0) continue; - if (text.StartsWith("#EXT-X-MEDIA-SEQUENCE")) - { - string media_sequence = $"#EXT-X-MEDIA-SEQUENCE:{media_sequence_no}"; - sb.AppendLine(media_sequence); - continue; - } - if (text.StartsWith("#EXTINF") && isFirstEXTINF) - { - startAppendFileContent = false; - continue; - } - if (text.StartsWith(del_ts_name) && isFirstEXTINF) - { - isFirstEXTINF = false; - startAppendFileContent = true; - continue; - } - if (startAppendFileContent) - { - sb.AppendLine(text); - } - } - } - AppendTsToM3u8(m3u8_filepath, tsRealSecond, ts_name, sb, false); - } - else { - AppendTsToM3u8(m3u8_filepath, tsRealSecond, ts_name, sb); - } - } - /// - /// m3u8追加ts文件 - /// - /// - /// - /// - /// - private void AppendTsToM3u8(string m3u8_filepath, double tsRealSecond, string tsName, StringBuilder sb,bool isAppend=true) { - sb.AppendLine($"#EXTINF:{tsRealSecond},");//extra info,分片TS的信息,如时长,带宽等 - sb.AppendLine($"{tsName}");//文件名 - using (StreamWriter sw = new StreamWriter(m3u8_filepath, isAppend)) - { - sw.WriteLine(sb); - } - } - - private void AppendM3U8Start(string filepath,int fileMaxSecond,int firstTSSerialno) { - if(File.Exists(filepath)) File.Delete(filepath); - StringBuilder sb = new StringBuilder(); - sb.AppendLine("#EXTM3U");//开始 - sb.AppendLine("#EXT-X-VERSION:3");//版本号 - sb.AppendLine("#EXT-X-ALLOW-CACHE:NO");//是否允许cache - - sb.AppendLine($"#EXT-X-TARGETDURATION:{fileMaxSecond}");//每个分片TS的最大的时长 - sb.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{firstTSSerialno}");//第一个TS分片的序列号 - using (StreamWriter sw = new StreamWriter(filepath,true)) - { - sw.WriteLine(sb); - } - } - /// - /// 添加结束标识 - /// - /// - private void AppendM3U8End(string filepath) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("#EXT-X-ENDLIST"); //m3u8文件结束符 表示视频已经结束 有这个标志同时也说明当前流是一个非直播流 - //#EXT-X-PLAYLIST-TYPE:VOD/Live //VOD表示当前视频流不是一个直播流,而是点播流(也就是视频的全部ts文件已经生成) - using (StreamWriter sw = new StreamWriter(filepath,true)) - { - sw.WriteLine(sb); - } - } /// /// diff --git a/src/JT1078.Hls.Test/Ts_File_Manage.cs b/src/JT1078.Hls.Test/Ts_File_Manage.cs deleted file mode 100644 index dc16f6e..0000000 --- a/src/JT1078.Hls.Test/Ts_File_Manage.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace JT1078.Hls.Test -{ - /// - /// ts文件管理 - /// - public class Ts_File_Manage - { - /// - /// 创建ts文件 - /// - /// ts文件路径 - /// 文件内容 - public void CreateTsFile(string ts_filepath, byte[] data) - { - DeleteTsFile(ts_filepath); - using (var fileStream = new FileStream(ts_filepath, FileMode.CreateNew, FileAccess.Write)) - { - fileStream.Write(data); - } - } - - /// - /// 删除ts文件 - /// - /// ts文件路径 - public void DeleteTsFile(string ts_filepath) - { - if (File.Exists(ts_filepath)) File.Delete(ts_filepath); - } - /// - /// ts文件是否存在 - /// - /// - /// - public bool ExistTsFile(string ts_filepath) { - return File.Exists(ts_filepath); - } - } -} diff --git a/src/JT1078.Hls/M3U8FileManage.cs b/src/JT1078.Hls/M3U8FileManage.cs new file mode 100644 index 0000000..8070ead --- /dev/null +++ b/src/JT1078.Hls/M3U8FileManage.cs @@ -0,0 +1,159 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Text; +using JT1078.Hls.Options; +using JT1078.Protocol; +using JT1078.Protocol.Extensions; + +namespace JT1078.Hls +{ + /// + /// m3u8文件管理 + /// + public class M3U8FileManage + { + public readonly M3U8Option m3U8Option; + + public M3U8FileManage(M3U8Option m3U8Option) + { + this.m3U8Option = m3U8Option; + AppendM3U8Start(m3U8Option.TsFileMaxSecond, m3U8Option.TsFileCount); + } + + public void CreateM3U8File(JT1078Package fullpackage,byte[] data) + { + //ecode_slice_header error 以非关键帧开始的报错信息 + //生成一个ts文件 + var ts_name = $"{m3U8Option.TsFileCount}.ts"; + var ts_filepath = Path.Combine(m3U8Option.HlsFileDirectory, ts_name); + CreateTsFile(ts_filepath, data); + + var media_sequence_no = m3U8Option.TsFileCount - m3U8Option.TsFileCapacity; + var del_ts_name = $"{media_sequence_no}.ts"; + //更新m3u8文件 + UpdateM3U8File(m3U8Option.AccumulateSeconds, media_sequence_no + 1, del_ts_name, ts_name); + + m3U8Option.IsNeedFirstHeadler = true; + m3U8Option.AccumulateSeconds = 0; + m3U8Option.TsFileCount = m3U8Option.TsFileCount + 1; + } + + public void AppendM3U8Start(int fileMaxSecond, int firstTSSerialno) + { + if (File.Exists(m3U8Option.M3U8Filepath)) File.Delete(m3U8Option.M3U8Filepath); + StringBuilder sb = new StringBuilder(); + sb.AppendLine("#EXTM3U");//开始 + sb.AppendLine("#EXT-X-VERSION:3");//版本号 + sb.AppendLine("#EXT-X-ALLOW-CACHE:NO");//是否允许cache + + sb.AppendLine($"#EXT-X-TARGETDURATION:{fileMaxSecond}");//每个分片TS的最大的时长 + sb.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{firstTSSerialno}");//第一个TS分片的序列号 + using (StreamWriter sw = new StreamWriter(m3U8Option.M3U8Filepath, true)) + { + sw.WriteLine(sb); + } + } + + /// + /// 添加结束标识 + /// + /// + public void AppendM3U8End() + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("#EXT-X-ENDLIST"); //m3u8文件结束符 表示视频已经结束 有这个标志同时也说明当前流是一个非直播流 + //#EXT-X-PLAYLIST-TYPE:VOD/Live //VOD表示当前视频流不是一个直播流,而是点播流(也就是视频的全部ts文件已经生成) + using (StreamWriter sw = new StreamWriter(m3U8Option.M3U8Filepath, true)) + { + sw.WriteLine(sb); + } + } + + /// + /// m3u8追加ts文件 + /// + /// + /// + /// + /// + public void AppendTsToM3u8(double tsRealSecond, string tsName, StringBuilder sb, bool isAppend = true) + { + sb.AppendLine($"#EXTINF:{tsRealSecond},");//extra info,分片TS的信息,如时长,带宽等 + sb.AppendLine($"{tsName}");//文件名 + using (StreamWriter sw = new StreamWriter(m3U8Option.M3U8Filepath, isAppend)) + { + sw.WriteLine(sb); + } + } + + /// + /// 更新m3u8文件 + /// + /// + /// + /// + /// + public void UpdateM3U8File(double tsRealSecond, int media_sequence_no, string del_ts_name, string ts_name) + { + StringBuilder sb = new StringBuilder(); + var del_ts_filepath = Path.Combine(m3U8Option.HlsFileDirectory, del_ts_name); + if (File.Exists(del_ts_filepath)) + { + //删除最早一个ts文件 + File.Delete(del_ts_filepath); + bool startAppendFileContent = true; + bool isFirstEXTINF = true; + using (StreamReader sr = new StreamReader(m3U8Option.M3U8Filepath)) + { + while (!sr.EndOfStream) + { + var text = sr.ReadLine(); + if (text.Length == 0) continue; + if (text.StartsWith("#EXT-X-MEDIA-SEQUENCE")) + { + string media_sequence = $"#EXT-X-MEDIA-SEQUENCE:{media_sequence_no}"; + sb.AppendLine(media_sequence); + continue; + } + if (text.StartsWith("#EXTINF") && isFirstEXTINF) + { + startAppendFileContent = false; + continue; + } + if (text.StartsWith(del_ts_name) && isFirstEXTINF) + { + isFirstEXTINF = false; + startAppendFileContent = true; + continue; + } + if (startAppendFileContent) + { + sb.AppendLine(text); + } + } + } + AppendTsToM3u8( tsRealSecond, ts_name, sb, false); + } + else + { + AppendTsToM3u8(tsRealSecond, ts_name, sb); + } + } + + /// + /// 创建ts文件 + /// + /// ts文件路径 + /// 文件内容 + public void CreateTsFile(string ts_filepath, byte[] data) + { + File.Delete(ts_filepath); + using (var fileStream = new FileStream(ts_filepath, FileMode.CreateNew, FileAccess.Write)) + { + fileStream.Write(data,0,data.Length); + } + } + } +} diff --git a/src/JT1078.Hls/Options/M3U8Option.cs b/src/JT1078.Hls/Options/M3U8Option.cs new file mode 100644 index 0000000..b35bb77 --- /dev/null +++ b/src/JT1078.Hls/Options/M3U8Option.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Hls.Options +{ + /// + /// m3u8配置文件 + /// + public class M3U8Option + { + /// + /// m3u8文件中默认包含的ts文件数 + /// + public int TsFileCapacity { get; set; } = 10; + /// + /// 每个ts文件的最大时长 + /// + public int TsFileMaxSecond { get; set; } = 10; + /// + /// 生成的ts的文件数 + /// + public int TsFileCount { get; set; } = 0; + /// + /// 1078包的时间戳 毫秒 + /// + public ulong TimestampMilliSecond { get; set; } = 0; + /// + /// 累计时长 如果大于文件时长就存储一个ts文件 + /// + public double AccumulateSeconds { get; set; } = 0; + /// + /// 是否需要头部,每个ts文件都需要头部,重置为true + /// + public bool IsNeedFirstHeadler { get; set; } = true; + /// + /// m3u8文件 + /// + public string M3U8Filepath { get; set; } + /// + /// hls文件路径 + /// + public string HlsFileDirectory { get; set; } + } +} diff --git a/src/JT1078.Hls/TSEncoder.cs b/src/JT1078.Hls/TSEncoder.cs index 4fcdc7c..2b07c7f 100644 --- a/src/JT1078.Hls/TSEncoder.cs +++ b/src/JT1078.Hls/TSEncoder.cs @@ -12,6 +12,8 @@ using System.Collections.Concurrent; using System.Security.Cryptography; using JT1078.Hls.Descriptors; using JT1078.Protocol.Extensions; +using JT1078.Hls.Options; +using System.Buffers; [assembly: InternalsVisibleTo("JT1078.Hls.Test")] @@ -28,12 +30,82 @@ namespace JT1078.Hls private const int FiexdTSLength = 188; private const string ServiceProvider = "JTT1078"; private const string ServiceName = "Koike&TK"; - private const int H264DefaultHZ = 90; - public TSEncoder() + private const int H264DefaultHZ = 90; + + + ArrayPool arrayPool = ArrayPool.Create(); + byte[] fileData; + int fileIndex = 0; + private M3U8FileManage m3U8FileManage; + public TSEncoder(M3U8FileManage m3U8FileManage) { VideoCounter = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.m3U8FileManage = m3U8FileManage; + fileData = arrayPool.Rent(2500000); } private Dictionary VideoCounter; + /// + /// 创建m3u8文件 和 ts文件 + /// + /// + public void CreateM3U8File(JT1078Package jt1078Package) { + CombinedTSData(jt1078Package); + if (m3U8FileManage.m3U8Option.AccumulateSeconds >= m3U8FileManage.m3U8Option.TsFileMaxSecond) { + m3U8FileManage.CreateM3U8File(jt1078Package, fileData.AsSpan().Slice(0,fileIndex).ToArray()); + arrayPool.Return(fileData); + fileData = arrayPool.Rent(2500000); + fileIndex = 0; + } + } + /// + /// m3u8文件 追加结束标识 + /// + public void AppendM3U8End() { + m3U8FileManage.AppendM3U8End(); + } + /// + /// 按 设定的时间(默认为10秒)切分ts文件 + /// + /// + private void CombinedTSData(JT1078Package jt1078Package) { + if (m3U8FileManage.m3U8Option.TimestampMilliSecond == 0) + { + m3U8FileManage.m3U8Option.TimestampMilliSecond = jt1078Package.Timestamp; + } + else + { + int duration = (int)(jt1078Package.Timestamp - m3U8FileManage.m3U8Option.TimestampMilliSecond); + m3U8FileManage.m3U8Option.TimestampMilliSecond = jt1078Package.Timestamp; + m3U8FileManage.m3U8Option.AccumulateSeconds = m3U8FileManage.m3U8Option.AccumulateSeconds + duration / 1000.0; + } + if (m3U8FileManage.m3U8Option.IsNeedFirstHeadler) + { + var sdt = CreateSDT(jt1078Package); + sdt.CopyTo(fileData, fileIndex); + fileIndex = sdt.Length; + + var pat = CreatePAT(jt1078Package); + pat.CopyTo(fileData, fileIndex); + fileIndex = fileIndex + pat.Length; + + var pmt = CreatePMT(jt1078Package); + pmt.CopyTo(fileData, fileIndex); + fileIndex = fileIndex + pmt.Length; + + var pes = CreatePES(jt1078Package, 18888); + pes.CopyTo(fileData, fileIndex); + fileIndex = fileIndex + pes.Length; + + m3U8FileManage.m3U8Option.IsNeedFirstHeadler = false; + } + else + { + var pes = CreatePES(jt1078Package, 18888); + pes.CopyTo(fileData, fileIndex); + fileIndex = fileIndex + pes.Length; + } + } + //private ConcurrentDictionary AudioCounter = new ConcurrentDictionary(); public byte[] CreateSDT(JT1078Package jt1078Package, int minBufferSize = 188) {