using System;
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;
using JT1078.Protocol.Extensions;
namespace JT1078.Hls
{
///
/// m3u8文件管理
///
public class M3U8FileManage
{
private TSEncoder tSEncoder;
private M3U8Option m3U8Option;
ConcurrentDictionary curTsFileInfoDic = new ConcurrentDictionary();//当前文件信息
ConcurrentDictionary> tsFileInfoQueueDic = new ConcurrentDictionary>();
///
///
///
///
public M3U8FileManage(M3U8Option m3U8Option):this(m3U8Option, new TSEncoder())
{
}
///
///
///
///
///
public M3U8FileManage(M3U8Option m3U8Option, TSEncoder tSEncoder)
{
this.tSEncoder = tSEncoder;
this.m3U8Option = m3U8Option;
}
///
/// 生成ts和m3u8文件
///
///
public void CreateTsData(JT1078Package jt1078Package)
{
string key = jt1078Package.GetKey();
//string hlsFileDirectory = m3U8Option.HlsFileDirectory;
//string m3u8FileName = Path.Combine(hlsFileDirectory, key, m3U8Option.M3U8FileName);
var buff = TSArrayPool.Rent(jt1078Package.Bodies.Length + 1024);
TSMessagePackWriter tSMessagePackWriter = new TSMessagePackWriter(buff);
try
{
var curTsFileInfo = CreateTsFileInfo(key);
if (!curTsFileInfo.IsCreateTsFile)
{
var pes = tSEncoder.CreatePES(jt1078Package);
tSMessagePackWriter.WriteArray(pes);
CreateTsFile(curTsFileInfo.FileName,key, tSMessagePackWriter.FlushAndGetArray());
curTsFileInfo.Duration = (jt1078Package.Timestamp - curTsFileInfo.TsFirst1078PackageTimeStamp) / 1000.0;
//按设定的时间(默认为10秒)切分ts文件
if (curTsFileInfo.Duration > (m3U8Option.TsFileMaxSecond-1))
{
var tsFileInfoQueue = ManageTsFileInfo(key, curTsFileInfo);
CreateM3U8File(curTsFileInfo, tsFileInfoQueue);
var newTsFileInfo = new TsFileInfo { IsCreateTsFile = true, Duration = 0, TsFileSerialNo = ++curTsFileInfo.TsFileSerialNo };
curTsFileInfoDic.TryUpdate(key, newTsFileInfo, curTsFileInfo);
}
}
else
{
curTsFileInfo.IsCreateTsFile = false;
curTsFileInfo.TsFirst1078PackageTimeStamp = jt1078Package.Timestamp;
curTsFileInfo.FileName = $"{curTsFileInfo.TsFileSerialNo}.ts";
var sdt = tSEncoder.CreateSDT();
tSMessagePackWriter.WriteArray(sdt);
var pat = tSEncoder.CreatePAT();
tSMessagePackWriter.WriteArray(pat);
var pmt = tSEncoder.CreatePMT();
tSMessagePackWriter.WriteArray(pmt);
var pes = tSEncoder.CreatePES(jt1078Package);
tSMessagePackWriter.WriteArray(pes);
CreateTsFile(curTsFileInfo.FileName,key, tSMessagePackWriter.FlushAndGetArray());
}
}
finally
{
TSArrayPool.Return(buff);
}
}
///
/// 维护TS文件信息队列
///
///
///
///
private Queue ManageTsFileInfo(string key, TsFileInfo curTsFileInfo)
{
if (tsFileInfoQueueDic.TryGetValue(key, out var tsFileInfoQueue))
{
if (tsFileInfoQueue.Count >= m3U8Option.TsFileCapacity)
{
var deleteTsFileInfo = tsFileInfoQueue.Dequeue();
var deleteTsFileName = Path.Combine(m3U8Option.HlsFileDirectory, key, deleteTsFileInfo.FileName);
if (File.Exists(deleteTsFileName)) File.Delete(deleteTsFileName);
}
tsFileInfoQueue.Enqueue(curTsFileInfo);
}
else
{
tsFileInfoQueue = new Queue(new List { curTsFileInfo });
tsFileInfoQueueDic.TryAdd(key, tsFileInfoQueue);
}
return tsFileInfoQueue;
}
///
/// 创建M3U8文件
///
/// 当前ts文件信息
/// ts文件信息队列
private void CreateM3U8File(TsFileInfo curTsFileInfo, Queue tsFileInfoQueue)
{
//ecode_slice_header error 以非关键帧开始生成的ts,通过ffplay播放会出现报错信息
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:{m3U8Option.TsFileMaxSecond}");//第一个TS分片的序列号
sb.AppendLine($"#EXT-X-MEDIA-SEQUENCE:{(curTsFileInfo.TsFileSerialNo - m3U8Option.TsFileCapacity > 0 ? (curTsFileInfo.TsFileSerialNo - m3U8Option.TsFileCapacity+1) : 0)}"); //默认第一个文件为0
sb.AppendLine();
for (int i = 0; i < tsFileInfoQueue.Count; i++)
{
var tsFileInfo = tsFileInfoQueue.ElementAt(i);
sb.AppendLine($"#EXTINF:{tsFileInfo.Duration},");
sb.AppendLine($"{tsFileInfo.FileName}?sim={tsFileInfo.Sim}&channelNo={tsFileInfo.ChannelNo}");
}
string m3u8FileName = Path.Combine(m3U8Option.HlsFileDirectory,$"{curTsFileInfo.Sim}_{curTsFileInfo.ChannelNo}", m3U8Option.M3U8FileName);
using (FileStream fs = new FileStream(m3u8FileName, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
{
var buffer = Encoding.UTF8.GetBytes(sb.ToString());
fs.Write(buffer,0, buffer.Length);
}
}
///
/// 创建TS文件信息
///
///
///
private TsFileInfo CreateTsFileInfo(string key)
{
if (!curTsFileInfoDic.TryGetValue(key, out var curTsFileInfo))
{
curTsFileInfo = new TsFileInfo()
{
Sim = key.Split('_')[0],
ChannelNo = key.Split('_')[1]
};
curTsFileInfoDic.TryAdd(key, curTsFileInfo);
}
else {
curTsFileInfo.Sim = key.Split('_')[0];
curTsFileInfo.ChannelNo = key.Split('_')[1];
}
return curTsFileInfo;
}
///
/// 创建TS文件
///
/// ts文件路径
/// 终端号_通道号(用作目录)
/// 文件内容
private void CreateTsFile(string fileName,string key, byte[] data)
{
string tsFileName = Path.Combine(m3U8Option.HlsFileDirectory, key, fileName);
using (var fileStream = new FileStream(tsFileName, FileMode.Append, FileAccess.Write))
{
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文件已经生成)
}
///
/// 停止观看直播时清零数据
///
///
///
public void Clear(string sim,int channelNo)
{
var key = $"{sim}_{channelNo}";
curTsFileInfoDic.TryRemove(key, out _);
tsFileInfoQueueDic.TryRemove(key, out _);
var directory = Path.Combine(m3U8Option.HlsFileDirectory, key);
if (Directory.Exists(directory)) Directory.Delete(directory);
}
///
/// TS文件信息
///
internal class TsFileInfo
{
///
/// 设备手机号
///
public string Sim { get; set; }
///
/// 设备逻辑通道号
///
public string ChannelNo { get; set; }
///
/// ts文件名
///
public string FileName { get; set; } = "0.ts";
///
/// 持续时间
///
public double Duration { get; set; } = 0;
///
/// 当前ts文件序号
///
public int TsFileSerialNo { get; set; } = 0;
///
/// 是否创建ts文件
///
public bool IsCreateTsFile { get; set; } = true;
///
/// ts文件第一个jt1078包的时间戳
///
public ulong TsFirst1078PackageTimeStamp { get; set; } = 0;
}
}
}