|
|
@@ -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<byte> arrayPool = ArrayPool<byte>.Create(); |
|
|
|
byte[] fileData; |
|
|
|
int fileIndex = 0; |
|
|
|
|
|
|
|
public ConcurrentQueue<TsFileInfo> tsFileInfoQueue = new ConcurrentQueue<TsFileInfo>(); |
|
|
|
public ConcurrentQueue<string> tsDelFileNameQueue = new ConcurrentQueue<string>(); |
|
|
|
|
|
|
|
ConcurrentDictionary<string, ulong> TsFirst1078PackageDic = new ConcurrentDictionary<string, ulong>(); |
|
|
|
|
|
|
@@ -28,37 +30,26 @@ namespace JT1078.Hls |
|
|
|
{ |
|
|
|
this.tSEncoder = tSEncoder; |
|
|
|
this.m3U8Option = m3U8Option; |
|
|
|
fileData = arrayPool.Rent(2500000); |
|
|
|
//AppendM3U8Start(m3U8Option.TsFileMaxSecond, m3U8Option.TsFileCount); |
|
|
|
} |
|
|
|
/// <summary> |
|
|
|
/// 创建m3u8文件 和 ts文件 |
|
|
|
/// </summary> |
|
|
|
/// <param name="jt1078Package"></param> |
|
|
|
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<byte> 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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary> |
|
|
|
/// 添加结束标识 |
|
|
|
/// </summary> |
|
|
|
/// <param name="filepath"></param> |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary> |
|
|
|
/// m3u8追加ts文件 |
|
|
|
/// </summary> |
|
|
|
/// <param name="filepath"></param> |
|
|
|
/// <param name="tsRealSecond"></param> |
|
|
|
/// <param name="tsName"></param> |
|
|
|
/// <param name="sb"></param> |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary> |
|
|
|
/// 更新m3u8文件 |
|
|
|
/// </summary> |
|
|
|
/// <param name="filepath"></param> |
|
|
|
/// <param name="tsRealSecond"></param> |
|
|
|
/// <param name="count"></param> |
|
|
|
/// <param name="tsName"></param> |
|
|
|
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<string> fileHeader = new List<string>() { |
|
|
|
"#EXTM3U",//开始 |
|
|
|
"#EXT-X-VERSION:3",//版本号 |
|
|
|
"#EXT-X-ALLOW-CACHE:NO",//是否允许cache |
|
|
|
$"#EXT-X-TARGETDURATION:{m3U8Option.TsFileMaxSecond}",//第一个TS分片的序列号 |
|
|
|
$"#EXT-X-MEDIA-SEQUENCE:firstTsIndex" |
|
|
|
}; |
|
|
|
Queue<string> fileBody = new Queue<string>(); |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary> |
|
|
|
/// 创建ts文件 |
|
|
|
/// </summary> |
|
|
|
/// <param name="ts_filepath">ts文件路径</param> |
|
|
|
/// <param name="fileName">ts文件路径</param> |
|
|
|
/// <param name="data">文件内容</param> |
|
|
|
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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary> |
|
|
|
/// 添加结束标识 |
|
|
|
/// 直播流用不到 |
|
|
|
/// </summary> |
|
|
|
/// <param name="filepath"></param> |
|
|
|
//public void AppendM3U8End() |
|
|
|
//{ |
|
|
|
// StringBuilder sb = new StringBuilder(); |
|
|
|
// sb.AppendLine("#EXT-X-ENDLIST"); //m3u8文件结束符 表示视频已经结束 有这个标志同时也说明当前流是一个非直播流 |
|
|
|
// //#EXT-X-PLAYLIST-TYPE:VOD/Live //VOD表示当前视频流不是一个直播流,而是点播流(也就是视频的全部ts文件已经生成) |
|
|
|
//} |
|
|
|
} |
|
|
|
/// <summary> |
|
|
|
/// ts文件信息 |
|
|
|
/// </summary> |
|
|
|
public class TsFileInfo { |
|
|
|
public string FileName { get; set; } |
|
|
|
public double Duration { get; set; } |
|
|
|
} |
|
|
|
} |