From 76c2e448d9ecfe3c7794250282322edc0beaf7b0 Mon Sep 17 00:00:00 2001 From: waterliu99 Date: Sun, 16 Aug 2020 23:22:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96m3u8=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E5=BE=85=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/JT1078.Hls.Test/M3U8_Test.cs | 31 ++-- src/JT1078.Hls/M3U8FileManage.cs | 225 +++++++++++---------------- src/JT1078.Hls/Options/M3U8Option.cs | 14 +- 4 files changed, 107 insertions(+), 164 deletions(-) diff --git a/.gitignore b/.gitignore index 2b2e424..92bb157 100644 --- a/.gitignore +++ b/.gitignore @@ -332,3 +332,4 @@ ASALocalRun/ /src/JT1078.Flv.Test/H264/JT1078_4.txt /src/JT1078.Flv.Test/H264/JT1078_5.txt /doc/tools/FlvAnalyzer.exe +/doc/tools/EasyICE_2.7.0.2 diff --git a/src/JT1078.Hls.Test/M3U8_Test.cs b/src/JT1078.Hls.Test/M3U8_Test.cs index de5ed30..8d12acf 100644 --- a/src/JT1078.Hls.Test/M3U8_Test.cs +++ b/src/JT1078.Hls.Test/M3U8_Test.cs @@ -21,21 +21,22 @@ namespace JT1078.Hls.Test 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(); + TSEncoder tSEncoder = new TSEncoder(); + var m3u8Manage = new M3U8FileManage(new Options.M3U8Option { HlsFileDirectory = hls_file_directory, M3U8Filepath = m3u8_filepath }, tSEncoder); + + 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) + { + m3u8Manage.CreateTsData(fullpackage); + m3u8Manage.CreateM3U8File(); + } + } } catch (Exception ex) { diff --git a/src/JT1078.Hls/M3U8FileManage.cs b/src/JT1078.Hls/M3U8FileManage.cs index 1282db2..e0d3552 100644 --- a/src/JT1078.Hls/M3U8FileManage.cs +++ b/src/JT1078.Hls/M3U8FileManage.cs @@ -3,7 +3,9 @@ using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; +using System.Threading; using JT1078.Hls.MessagePack; using JT1078.Hls.Options; using JT1078.Protocol; @@ -18,9 +20,9 @@ namespace JT1078.Hls { private TSEncoder tSEncoder; public readonly M3U8Option m3U8Option; - ArrayPool arrayPool = ArrayPool.Create(); - byte[] fileData; - int fileIndex = 0; + + public ConcurrentQueue tsFileInfoQueue = new ConcurrentQueue(); + public ConcurrentQueue tsDelFileNameQueue = new ConcurrentQueue(); ConcurrentDictionary TsFirst1078PackageDic = new ConcurrentDictionary(); @@ -28,37 +30,26 @@ namespace JT1078.Hls { this.tSEncoder = tSEncoder; this.m3U8Option = m3U8Option; - fileData = arrayPool.Rent(2500000); - //AppendM3U8Start(m3U8Option.TsFileMaxSecond, m3U8Option.TsFileCount); - } - /// - /// 创建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; - //} } - - private byte[] CreateTsData(JT1078Package jt1078Package, bool isNeedHeader,Span span) { + public void CreateTsData(JT1078Package jt1078Package) { + string tsFileDirectory = m3U8Option.HlsFileDirectory; + string tsNewFileName = $"{m3U8Option.TsFileCount}.ts"; + string fileName= Path.Combine(tsFileDirectory, tsNewFileName); var buff = TSArrayPool.Rent(jt1078Package.Bodies.Length + 2048); TSMessagePackWriter tSMessagePackWriter = new TSMessagePackWriter(buff); if (TsFirst1078PackageDic.TryGetValue(jt1078Package.SIM, out var firstTimespan)) { + var pes = tSEncoder.CreatePES(jt1078Package, 188); + tSMessagePackWriter.WriteArray(pes); + CreateTsFile(fileName, tSMessagePackWriter.FlushAndGetArray()); if ((jt1078Package.Timestamp - firstTimespan) > 10 * 1000) { //按设定的时间(默认为10秒)切分ts文件 + TsFirst1078PackageDic.TryRemove(jt1078Package.SIM, out var _); + tsFileInfoQueue.Enqueue(new TsFileInfo { FileName= tsNewFileName, Duration= (jt1078Package.Timestamp - firstTimespan)/1000.0 }); + m3U8Option.TsFileCount++; } - var pes = tSEncoder.CreatePES(jt1078Package, 188); - tSMessagePackWriter.WriteArray(pes); } else { var sdt = tSEncoder.CreateSDT(jt1078Package); @@ -67,143 +58,105 @@ namespace JT1078.Hls tSMessagePackWriter.WriteArray(pat); var pmt = tSEncoder.CreatePMT(jt1078Package); tSMessagePackWriter.WriteArray(pmt); - var pes = tSEncoder.CreatePES(jt1078Package, 188); + var pes = tSEncoder.CreatePES(jt1078Package); tSMessagePackWriter.WriteArray(pes); + CreateTsFile(fileName, tSMessagePackWriter.FlushAndGetArray()); + TsFirst1078PackageDic.TryAdd(jt1078Package.SIM, jt1078Package.Timestamp); } - return buff; } - public void CreateM3U8File(JT1078Package fullpackage,byte[] data) + public void CreateM3U8File() { //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)) + if (tsFileInfoQueue.TryDequeue(out var tsFileInfo)) { - 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)) + string firstTsIndex = string.Empty; + StringBuilder sb = new StringBuilder(); + List fileHeader = new List() { + "#EXTM3U",//开始 + "#EXT-X-VERSION:3",//版本号 + "#EXT-X-ALLOW-CACHE:NO",//是否允许cache + $"#EXT-X-TARGETDURATION:{m3U8Option.TsFileMaxSecond}",//第一个TS分片的序列号 + $"#EXT-X-MEDIA-SEQUENCE:firstTsIndex" + }; + Queue fileBody = new Queue(); + if (!File.Exists(m3U8Option.M3U8Filepath)) File.Create(m3U8Option.M3U8Filepath); + using(FileStream fs=new FileStream(m3U8Option.M3U8Filepath,FileMode.Open,FileAccess.Read,FileShare.ReadWrite)) + using (StreamReader sr = new StreamReader(fs)) { 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); - } + if (fileHeader.Contains(text)) continue; + if (text.StartsWith("#EXT-X-MEDIA-SEQUENCE")) continue; + fileBody.Enqueue(text); + } + if (fileBody.Count >= m3U8Option.TsFileCapacity * 2) + { + var deleteTsFileName_extraInfo = fileBody.Dequeue(); + var deleteTsFileName = fileBody.Dequeue(); + tsDelFileNameQueue.Enqueue(deleteTsFileName); + + fileBody.Enqueue($"#EXTINF:{tsFileInfo.Duration},"); + fileBody.Enqueue(tsFileInfo.FileName); + + firstTsIndex = fileBody.ElementAt(1).Replace(".ts", ""); } + else { + fileBody.Enqueue($"#EXTINF:{tsFileInfo.Duration},"); + fileBody.Enqueue(tsFileInfo.FileName); + + firstTsIndex = fileBody.ElementAt(1).Replace(".ts", ""); + } + fileHeader.ForEach((m) => { + if (m.Contains("firstTsIndex")) m = m.Replace("firstTsIndex", firstTsIndex); + sb.AppendLine(m); + }); + fileBody.ToList().ForEach(m => { + sb.AppendLine(m); + }); + } + using (FileStream fs = new FileStream(m3U8Option.M3U8Filepath, FileMode.Create,FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter sw = new StreamWriter(fs)) + { + sw.Write(sb.ToString()); } - AppendTsToM3u8( tsRealSecond, ts_name, sb, false); - } - else - { - AppendTsToM3u8(tsRealSecond, ts_name, sb); } } /// /// 创建ts文件 /// - /// ts文件路径 + /// ts文件路径 /// 文件内容 - public void CreateTsFile(string ts_filepath, byte[] data) + private void CreateTsFile(string fileName, byte[] data) { - File.Delete(ts_filepath); - using (var fileStream = new FileStream(ts_filepath, FileMode.CreateNew, FileAccess.Write)) + var fileDirectory = fileName.Substring(0, fileName.LastIndexOf("\\")); + if (!Directory.Exists(fileDirectory)) Directory.CreateDirectory(fileDirectory); + using (var fileStream = new FileStream(fileName, FileMode.Append, FileAccess.Write,FileShare.ReadWrite, data.Length)) { fileStream.Write(data,0,data.Length); } } + + /// + /// 添加结束标识 + /// 直播流用不到 + /// + /// + //public void AppendM3U8End() + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("#EXT-X-ENDLIST"); //m3u8文件结束符 表示视频已经结束 有这个标志同时也说明当前流是一个非直播流 + // //#EXT-X-PLAYLIST-TYPE:VOD/Live //VOD表示当前视频流不是一个直播流,而是点播流(也就是视频的全部ts文件已经生成) + //} + } + /// + /// ts文件信息 + /// + public class TsFileInfo { + public string FileName { get; set; } + public double Duration { get; set; } } } diff --git a/src/JT1078.Hls/Options/M3U8Option.cs b/src/JT1078.Hls/Options/M3U8Option.cs index b35bb77..131edb7 100644 --- a/src/JT1078.Hls/Options/M3U8Option.cs +++ b/src/JT1078.Hls/Options/M3U8Option.cs @@ -18,22 +18,10 @@ namespace JT1078.Hls.Options /// public int TsFileMaxSecond { get; set; } = 10; /// - /// 生成的ts的文件数 + /// 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; }