您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

249 行
10 KiB

  1. using System;
  2. using System.Buffers;
  3. using System.Collections.Concurrent;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading;
  9. using JT1078.Hls.MessagePack;
  10. using JT1078.Hls.Options;
  11. using JT1078.Protocol;
  12. using JT1078.Protocol.Extensions;
  13. namespace JT1078.Hls
  14. {
  15. /// <summary>
  16. /// m3u8文件管理
  17. /// </summary>
  18. public class M3U8FileManage
  19. {
  20. private TSEncoder tSEncoder;
  21. private M3U8Option m3U8Option;
  22. ConcurrentDictionary<string, TsFileInfo> curTsFileInfoDic = new ConcurrentDictionary<string, TsFileInfo>();//当前文件信息
  23. ConcurrentDictionary<string, Queue<TsFileInfo>> tsFileInfoQueueDic = new ConcurrentDictionary<string, Queue<TsFileInfo>>();
  24. /// <summary>
  25. ///
  26. /// </summary>
  27. /// <param name="m3U8Option"></param>
  28. public M3U8FileManage(M3U8Option m3U8Option):this(m3U8Option, new TSEncoder())
  29. {
  30. }
  31. /// <summary>
  32. ///
  33. /// </summary>
  34. /// <param name="m3U8Option"></param>
  35. /// <param name="tSEncoder"></param>
  36. public M3U8FileManage(M3U8Option m3U8Option, TSEncoder tSEncoder)
  37. {
  38. this.tSEncoder = tSEncoder;
  39. this.m3U8Option = m3U8Option;
  40. }
  41. /// <summary>
  42. /// 生成ts和m3u8文件
  43. /// </summary>
  44. /// <param name="jt1078Package"></param>
  45. public void CreateTsData(JT1078Package jt1078Package)
  46. {
  47. string key = jt1078Package.GetKey();
  48. //string hlsFileDirectory = m3U8Option.HlsFileDirectory;
  49. //string m3u8FileName = Path.Combine(hlsFileDirectory, key, m3U8Option.M3U8FileName);
  50. var buff = TSArrayPool.Rent(jt1078Package.Bodies.Length + 1024);
  51. TSMessagePackWriter tSMessagePackWriter = new TSMessagePackWriter(buff);
  52. try
  53. {
  54. var curTsFileInfo = CreateTsFileInfo(key);
  55. if (!curTsFileInfo.IsCreateTsFile)
  56. {
  57. var pes = tSEncoder.CreatePES(jt1078Package);
  58. tSMessagePackWriter.WriteArray(pes);
  59. CreateTsFile(curTsFileInfo.FileName,key, tSMessagePackWriter.FlushAndGetArray());
  60. curTsFileInfo.Duration = (jt1078Package.Timestamp - curTsFileInfo.TsFirst1078PackageTimeStamp) / 1000.0;
  61. //按设定的时间(默认为10秒)切分ts文件
  62. if (curTsFileInfo.Duration > (m3U8Option.TsFileMaxSecond-1))
  63. {
  64. var tsFileInfoQueue = ManageTsFileInfo(key, curTsFileInfo);
  65. CreateM3U8File(curTsFileInfo, tsFileInfoQueue);
  66. var newTsFileInfo = new TsFileInfo { IsCreateTsFile = true, Duration = 0, TsFileSerialNo = ++curTsFileInfo.TsFileSerialNo };
  67. curTsFileInfoDic.TryUpdate(key, newTsFileInfo, curTsFileInfo);
  68. }
  69. }
  70. else
  71. {
  72. curTsFileInfo.IsCreateTsFile = false;
  73. curTsFileInfo.TsFirst1078PackageTimeStamp = jt1078Package.Timestamp;
  74. curTsFileInfo.FileName = $"{curTsFileInfo.TsFileSerialNo}.ts";
  75. var sdt = tSEncoder.CreateSDT();
  76. tSMessagePackWriter.WriteArray(sdt);
  77. var pat = tSEncoder.CreatePAT();
  78. tSMessagePackWriter.WriteArray(pat);
  79. var pmt = tSEncoder.CreatePMT();
  80. tSMessagePackWriter.WriteArray(pmt);
  81. var pes = tSEncoder.CreatePES(jt1078Package);
  82. tSMessagePackWriter.WriteArray(pes);
  83. CreateTsFile(curTsFileInfo.FileName,key, tSMessagePackWriter.FlushAndGetArray());
  84. }
  85. }
  86. finally
  87. {
  88. TSArrayPool.Return(buff);
  89. }
  90. }
  91. /// <summary>
  92. /// 维护TS文件信息队列
  93. /// </summary>
  94. /// <param name="key"></param>
  95. /// <param name="curTsFileInfo"></param>
  96. /// <returns></returns>
  97. private Queue<TsFileInfo> ManageTsFileInfo(string key, TsFileInfo curTsFileInfo)
  98. {
  99. if (tsFileInfoQueueDic.TryGetValue(key, out var tsFileInfoQueue))
  100. {
  101. if (tsFileInfoQueue.Count >= m3U8Option.TsFileCapacity)
  102. {
  103. var deleteTsFileInfo = tsFileInfoQueue.Dequeue();
  104. var deleteTsFileName = Path.Combine(m3U8Option.HlsFileDirectory, key, deleteTsFileInfo.FileName);
  105. if (File.Exists(deleteTsFileName)) File.Delete(deleteTsFileName);
  106. }
  107. tsFileInfoQueue.Enqueue(curTsFileInfo);
  108. }
  109. else
  110. {
  111. tsFileInfoQueue = new Queue<TsFileInfo>(new List<TsFileInfo> { curTsFileInfo });
  112. tsFileInfoQueueDic.TryAdd(key, tsFileInfoQueue);
  113. }
  114. return tsFileInfoQueue;
  115. }
  116. /// <summary>
  117. /// 创建M3U8文件
  118. /// </summary>
  119. /// <param name="curTsFileInfo">当前ts文件信息</param>
  120. /// <param name="tsFileInfoQueue">ts文件信息队列</param>
  121. private void CreateM3U8File(TsFileInfo curTsFileInfo, Queue<TsFileInfo> tsFileInfoQueue)
  122. {
  123. //ecode_slice_header error 以非关键帧开始生成的ts,通过ffplay播放会出现报错信息
  124. StringBuilder sb = new StringBuilder();
  125. sb.AppendLine("#EXTM3U");//开始
  126. sb.AppendLine("#EXT-X-VERSION:3");//版本号
  127. sb.AppendLine("#EXT-X-ALLOW-CACHE:NO");//是否允许cache
  128. sb.AppendLine($"#EXT-X-TARGETDURATION:{m3U8Option.TsFileMaxSecond}");//第一个TS分片的序列号
  129. sb.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{(curTsFileInfo.TsFileSerialNo - m3U8Option.TsFileCapacity > 0 ? (curTsFileInfo.TsFileSerialNo - m3U8Option.TsFileCapacity+1) : 0)}"); //默认第一个文件为0
  130. sb.AppendLine();
  131. for (int i = 0; i < tsFileInfoQueue.Count; i++)
  132. {
  133. var tsFileInfo = tsFileInfoQueue.ElementAt(i);
  134. sb.AppendLine($"#EXTINF:{tsFileInfo.Duration},");
  135. sb.AppendLine($"{tsFileInfo.FileName}?sim={tsFileInfo.Sim}&channelNo={tsFileInfo.ChannelNo}");
  136. }
  137. string m3u8FileName = Path.Combine(m3U8Option.HlsFileDirectory,$"{curTsFileInfo.Sim}_{curTsFileInfo.ChannelNo}", m3U8Option.M3U8FileName);
  138. using (FileStream fs = new FileStream(m3u8FileName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
  139. {
  140. var buffer = Encoding.UTF8.GetBytes(sb.ToString());
  141. fs.Write(buffer,0, buffer.Length);
  142. }
  143. }
  144. /// <summary>
  145. /// 创建TS文件信息
  146. /// </summary>
  147. /// <param name="key"></param>
  148. /// <returns></returns>
  149. private TsFileInfo CreateTsFileInfo(string key)
  150. {
  151. if (!curTsFileInfoDic.TryGetValue(key, out var curTsFileInfo))
  152. {
  153. curTsFileInfo = new TsFileInfo()
  154. {
  155. Sim = key.Split('_')[0],
  156. ChannelNo = key.Split('_')[1]
  157. };
  158. curTsFileInfoDic.TryAdd(key, curTsFileInfo);
  159. }
  160. else {
  161. curTsFileInfo.Sim = key.Split('_')[0];
  162. curTsFileInfo.ChannelNo = key.Split('_')[1];
  163. }
  164. return curTsFileInfo;
  165. }
  166. /// <summary>
  167. /// 创建TS文件
  168. /// </summary>
  169. /// <param name="fileName">ts文件路径</param>
  170. /// <param name="key">终端号_通道号(用作目录)</param>
  171. /// <param name="data">文件内容</param>
  172. private void CreateTsFile(string fileName,string key, byte[] data)
  173. {
  174. string tsFileName = Path.Combine(m3U8Option.HlsFileDirectory, key, fileName);
  175. using (var fileStream = new FileStream(tsFileName, FileMode.Append, FileAccess.Write))
  176. {
  177. fileStream.Write(data,0,data.Length);
  178. }
  179. }
  180. /// <summary>
  181. /// 添加结束标识
  182. /// 直播流用不到
  183. /// </summary>
  184. public void AppendM3U8End()
  185. {
  186. StringBuilder sb = new StringBuilder();
  187. sb.AppendLine("#EXT-X-ENDLIST");
  188. //m3u8文件结束符 表示视频已经结束 有这个标志同时也说明当前流是一个非直播流
  189. //#EXT-X-PLAYLIST-TYPE:VOD/Live //VOD表示当前视频流不是一个直播流,而是点播流(也就是视频的全部ts文件已经生成)
  190. }
  191. /// <summary>
  192. /// 停止观看直播时清零数据
  193. /// </summary>
  194. /// <param name="sim"></param>
  195. /// <param name="channelNo"></param>
  196. public void Clear(string sim,int channelNo)
  197. {
  198. var key = $"{sim}_{channelNo}";
  199. curTsFileInfoDic.TryRemove(key, out _);
  200. tsFileInfoQueueDic.TryRemove(key, out _);
  201. var directory = Path.Combine(m3U8Option.HlsFileDirectory, key);
  202. if (Directory.Exists(directory)) Directory.Delete(directory);
  203. }
  204. /// <summary>
  205. /// TS文件信息
  206. /// </summary>
  207. internal class TsFileInfo
  208. {
  209. /// <summary>
  210. /// 设备手机号
  211. /// </summary>
  212. public string Sim { get; set; }
  213. /// <summary>
  214. /// 设备逻辑通道号
  215. /// </summary>
  216. public string ChannelNo { get; set; }
  217. /// <summary>
  218. /// ts文件名
  219. /// </summary>
  220. public string FileName { get; set; } = "0.ts";
  221. /// <summary>
  222. /// 持续时间
  223. /// </summary>
  224. public double Duration { get; set; } = 0;
  225. /// <summary>
  226. /// 当前ts文件序号
  227. /// </summary>
  228. public int TsFileSerialNo { get; set; } = 0;
  229. /// <summary>
  230. /// 是否创建ts文件
  231. /// </summary>
  232. public bool IsCreateTsFile { get; set; } = true;
  233. /// <summary>
  234. /// ts文件第一个jt1078包的时间戳
  235. /// </summary>
  236. public ulong TsFirst1078PackageTimeStamp { get; set; } = 0;
  237. }
  238. }
  239. }