2018-03-24 04:58:13 +08:00
|
|
|
|
using NLog;
|
|
|
|
|
using System;
|
2018-03-12 18:57:20 +08:00
|
|
|
|
using System.Collections.Generic;
|
2018-03-19 01:05:02 +08:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
2018-03-12 18:57:20 +08:00
|
|
|
|
|
|
|
|
|
namespace BililiveRecorder.FlvProcessor
|
|
|
|
|
{
|
2018-03-24 02:27:58 +08:00
|
|
|
|
// TODO: 重构 Tag 解析流程
|
|
|
|
|
// TODO: 添加测试
|
|
|
|
|
// 注:下载时会按照 4 11 N bytes 下载
|
2018-10-24 13:33:43 +08:00
|
|
|
|
public class FlvStreamProcessor : IFlvStreamProcessor
|
2018-03-12 18:57:20 +08:00
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
|
|
|
|
|
2018-03-24 11:07:43 +08:00
|
|
|
|
internal const uint SEC_TO_MS = 1000; // 1 second = 1000 ms
|
2018-03-19 17:35:27 +08:00
|
|
|
|
internal const int MIN_BUFFER_SIZE = 1024 * 2;
|
2018-03-19 16:51:35 +08:00
|
|
|
|
internal static readonly byte[] FLV_HEADER_BYTES = new byte[]
|
|
|
|
|
{
|
|
|
|
|
0x46, // F
|
|
|
|
|
0x4c, // L
|
|
|
|
|
0x56, // V
|
|
|
|
|
0x01, // Version 1
|
|
|
|
|
0x05, // bit 00000 1 0 1 (have video and audio)
|
|
|
|
|
0x00, // ---
|
|
|
|
|
0x00, // |
|
|
|
|
|
0x00, // |
|
|
|
|
|
0x09, // total of 9 bytes
|
2018-03-19 17:35:27 +08:00
|
|
|
|
// 0x00, // ---
|
|
|
|
|
// 0x00, // |
|
|
|
|
|
// 0x00, // |
|
|
|
|
|
// 0x00, // the "0th" tag has a length of 0
|
2018-03-19 16:51:35 +08:00
|
|
|
|
};
|
2018-03-13 13:56:44 +08:00
|
|
|
|
|
2018-10-24 13:33:43 +08:00
|
|
|
|
public IFlvMetadata Metadata { get; set; } = null;
|
2018-03-19 01:05:02 +08:00
|
|
|
|
public event TagProcessedEvent TagProcessed;
|
2018-03-19 16:51:35 +08:00
|
|
|
|
public event StreamFinalizedEvent StreamFinalized;
|
2018-10-24 13:33:43 +08:00
|
|
|
|
public Func<string> GetFileName { get; set; }
|
2018-03-12 18:57:20 +08:00
|
|
|
|
|
2018-10-24 13:33:43 +08:00
|
|
|
|
public uint Clip_Past { get; set; } = 90;
|
|
|
|
|
public uint Clip_Future { get; set; } = 30;
|
2018-03-29 11:39:44 +08:00
|
|
|
|
private readonly bool _noClip = false;
|
2018-03-19 17:35:27 +08:00
|
|
|
|
|
2018-03-19 01:05:02 +08:00
|
|
|
|
private bool _headerParsed = false;
|
2018-10-24 13:33:43 +08:00
|
|
|
|
private readonly List<IFlvTag> HTags = new List<IFlvTag>();
|
|
|
|
|
private readonly List<IFlvTag> Tags = new List<IFlvTag>();
|
2018-03-19 01:05:02 +08:00
|
|
|
|
private readonly MemoryStream _buffer = new MemoryStream();
|
|
|
|
|
private readonly MemoryStream _data = new MemoryStream();
|
2018-10-24 13:33:43 +08:00
|
|
|
|
private IFlvTag currentTag = null;
|
|
|
|
|
private readonly object _writelock = new object();
|
2018-03-19 16:51:35 +08:00
|
|
|
|
private bool Finallized = false;
|
|
|
|
|
|
|
|
|
|
private readonly FileStream _fs;
|
2018-03-13 13:56:44 +08:00
|
|
|
|
|
2018-03-24 08:34:57 +08:00
|
|
|
|
public int LasttimeRemovedTimestamp { get; private set; } = 0;
|
2018-03-24 04:58:13 +08:00
|
|
|
|
public int MaxTimeStamp { get; private set; } = 0;
|
2018-03-19 17:35:27 +08:00
|
|
|
|
public int BaseTimeStamp { get; private set; } = 0;
|
|
|
|
|
public int TagVideoCount { get; private set; } = 0;
|
|
|
|
|
public int TagAudioCount { get; private set; } = 0;
|
|
|
|
|
private bool hasOffset = false;
|
2018-03-19 16:51:35 +08:00
|
|
|
|
|
2018-03-29 11:39:44 +08:00
|
|
|
|
public FlvStreamProcessor(string path, bool noclip)
|
2018-03-12 18:57:20 +08:00
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
_noClip = noclip;
|
|
|
|
|
if (path == null)
|
2018-03-19 16:51:35 +08:00
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
// 不保存到硬盘,只在内存储存
|
|
|
|
|
_fs = null;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite);
|
|
|
|
|
if (!_fs.CanSeek)
|
|
|
|
|
{
|
|
|
|
|
_fs.Dispose();
|
|
|
|
|
try { File.Delete(path); } catch (Exception) { }
|
|
|
|
|
throw new NotSupportedException("Target File Cannot Seek");
|
|
|
|
|
}
|
2018-03-19 16:51:35 +08:00
|
|
|
|
}
|
2018-03-12 18:57:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-18 18:55:28 +08:00
|
|
|
|
public void AddBytes(byte[] data)
|
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
lock (_writelock)
|
2018-10-24 13:33:43 +08:00
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
_AddBytes(data);
|
2018-10-24 13:33:43 +08:00
|
|
|
|
}
|
2018-03-19 01:05:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void _AddBytes(byte[] data)
|
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
if (Finallized)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Stream File Already Closed");
|
|
|
|
|
}
|
|
|
|
|
else if (!_headerParsed)
|
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
if (data[4] != FLV_HEADER_BYTES[4])
|
|
|
|
|
{
|
|
|
|
|
// 七牛 直播云 的高端 FLV 头
|
|
|
|
|
logger.Debug("FLV头[4]的值是 {0}", data[4]);
|
|
|
|
|
data[4] = FLV_HEADER_BYTES[4];
|
|
|
|
|
}
|
2018-03-19 16:51:35 +08:00
|
|
|
|
var r = new bool[FLV_HEADER_BYTES.Length];
|
|
|
|
|
for (int i = 0; i < FLV_HEADER_BYTES.Length; i++)
|
2018-10-24 13:33:43 +08:00
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
r[i] = data[i] == FLV_HEADER_BYTES[i];
|
2018-10-24 13:33:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-19 16:51:35 +08:00
|
|
|
|
bool succ = r.All(x => x);
|
|
|
|
|
if (!succ)
|
2018-10-24 13:33:43 +08:00
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
throw new NotSupportedException("Not FLV Stream or Not Supported"); // TODO: custom Exception.
|
2018-10-24 13:33:43 +08:00
|
|
|
|
}
|
2018-03-19 16:51:35 +08:00
|
|
|
|
|
|
|
|
|
_headerParsed = true;
|
|
|
|
|
_AddBytes(data.Skip(FLV_HEADER_BYTES.Length).ToArray());
|
|
|
|
|
}
|
|
|
|
|
else if (currentTag == null)
|
2018-03-19 01:05:02 +08:00
|
|
|
|
{
|
2018-03-22 00:40:25 +08:00
|
|
|
|
_buffer.Write(data, 0, data.Length);
|
2018-03-19 01:05:02 +08:00
|
|
|
|
if (_buffer.Position >= MIN_BUFFER_SIZE)
|
|
|
|
|
{
|
2018-03-22 00:40:25 +08:00
|
|
|
|
_ParseTag(_buffer.GetBuffer().Take((int)_buffer.Position).ToArray());
|
2018-03-19 01:05:02 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_WriteTagData(data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void _WriteTagData(byte[] data)
|
|
|
|
|
{
|
|
|
|
|
int toRead = Math.Min(data.Length, (currentTag.TagSize - (int)_data.Position));
|
|
|
|
|
_data.Write(data, 0, toRead);
|
|
|
|
|
if ((int)_data.Position == currentTag.TagSize)
|
|
|
|
|
{
|
|
|
|
|
currentTag.Data = _data.ToArray();
|
|
|
|
|
_data.SetLength(0); // reset data buffer
|
|
|
|
|
_TagCreated(currentTag);
|
|
|
|
|
currentTag = null;
|
|
|
|
|
_AddBytes(data.Skip(toRead).ToArray());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 13:33:43 +08:00
|
|
|
|
private void _TagCreated(IFlvTag tag)
|
2018-03-19 01:05:02 +08:00
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
if (Metadata == null)
|
2018-03-19 01:05:02 +08:00
|
|
|
|
{
|
2018-03-19 16:51:35 +08:00
|
|
|
|
if (tag.TagType == TagType.DATA)
|
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
_fs?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
|
|
|
|
|
_fs?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
|
|
|
|
|
2018-10-24 13:33:43 +08:00
|
|
|
|
Metadata = new FlvMetadata(tag.Data);
|
2018-03-19 16:51:35 +08:00
|
|
|
|
|
|
|
|
|
// TODO: 添加录播姬标记、录制信息
|
|
|
|
|
|
|
|
|
|
tag.Data = Metadata.ToBytes();
|
2018-03-24 08:41:29 +08:00
|
|
|
|
tag.WriteTo(_fs);
|
2018-03-19 16:51:35 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("onMetaData not found");
|
|
|
|
|
}
|
2018-03-19 01:05:02 +08:00
|
|
|
|
}
|
2018-03-19 16:51:35 +08:00
|
|
|
|
else
|
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
if (!hasOffset)
|
2018-03-19 17:35:27 +08:00
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
if (tag.TagType == TagType.VIDEO)
|
|
|
|
|
{
|
|
|
|
|
TagVideoCount++;
|
|
|
|
|
if (TagVideoCount < 2)
|
2018-03-19 17:35:27 +08:00
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
logger.Trace("第一个 Video Tag 时间戳 {0} ms", tag.TimeStamp);
|
2018-03-24 08:34:57 +08:00
|
|
|
|
HTags.Add(tag);
|
2018-03-19 17:35:27 +08:00
|
|
|
|
}
|
2018-03-24 04:58:13 +08:00
|
|
|
|
else
|
2018-03-19 17:35:27 +08:00
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
BaseTimeStamp = tag.TimeStamp;
|
2018-03-19 17:35:27 +08:00
|
|
|
|
hasOffset = true;
|
2018-03-24 04:58:13 +08:00
|
|
|
|
logger.Trace("重设时间戳 {0} 毫秒", BaseTimeStamp);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (tag.TagType == TagType.AUDIO)
|
|
|
|
|
{
|
|
|
|
|
TagAudioCount++;
|
|
|
|
|
if (TagAudioCount < 2)
|
|
|
|
|
{
|
|
|
|
|
logger.Trace("第一个 Audio Tag 时间戳 {0} ms", tag.TimeStamp);
|
2018-03-24 08:34:57 +08:00
|
|
|
|
HTags.Add(tag);
|
2018-03-24 04:58:13 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2018-03-19 17:35:27 +08:00
|
|
|
|
BaseTimeStamp = tag.TimeStamp;
|
2018-03-24 04:58:13 +08:00
|
|
|
|
hasOffset = true;
|
|
|
|
|
logger.Trace("重设时间戳 {0} 毫秒", BaseTimeStamp);
|
2018-03-19 17:35:27 +08:00
|
|
|
|
}
|
2018-03-24 04:58:13 +08:00
|
|
|
|
}
|
2018-03-19 17:35:27 +08:00
|
|
|
|
}
|
2018-03-19 01:05:02 +08:00
|
|
|
|
|
2018-03-24 04:58:13 +08:00
|
|
|
|
|
2018-03-23 06:57:22 +08:00
|
|
|
|
if (hasOffset)
|
|
|
|
|
{
|
|
|
|
|
tag.TimeStamp -= BaseTimeStamp; // 修复时间戳
|
2018-03-24 04:58:13 +08:00
|
|
|
|
if (tag.TimeStamp < 0)
|
2018-10-24 13:33:43 +08:00
|
|
|
|
{
|
2018-03-24 04:58:13 +08:00
|
|
|
|
tag.TimeStamp = 0;
|
2018-10-24 13:33:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-23 06:57:22 +08:00
|
|
|
|
MaxTimeStamp = Math.Max(MaxTimeStamp, tag.TimeStamp);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
tag.TimeStamp = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-29 11:39:44 +08:00
|
|
|
|
// 如果没有禁用 Clip 功能
|
|
|
|
|
if (!_noClip)
|
2018-03-24 08:34:57 +08:00
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
Tags.Add(tag); // Clip 缓存
|
|
|
|
|
|
|
|
|
|
// 移除过旧的数据
|
|
|
|
|
if (MaxTimeStamp - LasttimeRemovedTimestamp > 800)
|
|
|
|
|
{
|
|
|
|
|
LasttimeRemovedTimestamp = MaxTimeStamp;
|
|
|
|
|
int max_remove_index = Tags.FindLastIndex(x => x.IsVideoKeyframe && ((MaxTimeStamp - x.TimeStamp) > (Clip_Past * SEC_TO_MS)));
|
|
|
|
|
if (max_remove_index > 0)
|
2018-10-24 13:33:43 +08:00
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
Tags.RemoveRange(0, max_remove_index);
|
2018-10-24 13:33:43 +08:00
|
|
|
|
}
|
2018-03-29 11:39:44 +08:00
|
|
|
|
// Tags.RemoveRange(0, max_remove_index + 1 - 1);
|
|
|
|
|
// 给将来的备注:这里是故意 + 1 - 1 的,因为要保留选中的那个关键帧, + 1 就把关键帧删除了
|
|
|
|
|
}
|
2018-03-24 08:34:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-19 17:35:27 +08:00
|
|
|
|
// 写入硬盘
|
2018-03-24 08:41:29 +08:00
|
|
|
|
tag.WriteTo(_fs);
|
2018-03-19 16:51:35 +08:00
|
|
|
|
|
|
|
|
|
TagProcessed?.Invoke(this, new TagProcessedArgs() { Tag = tag });
|
2018-03-24 04:58:13 +08:00
|
|
|
|
} // if (Metadata == null) else
|
2018-03-19 16:51:35 +08:00
|
|
|
|
}
|
2018-03-19 01:05:02 +08:00
|
|
|
|
|
|
|
|
|
private void _ParseTag(byte[] data)
|
|
|
|
|
{
|
2018-03-22 00:40:25 +08:00
|
|
|
|
_buffer.Position = 0;
|
|
|
|
|
_buffer.SetLength(0);
|
2018-03-19 17:35:27 +08:00
|
|
|
|
byte[] b = new byte[4];
|
2018-03-19 01:05:02 +08:00
|
|
|
|
_buffer.Write(data, 0, data.Length);
|
|
|
|
|
long dataLen = _buffer.Position;
|
|
|
|
|
_buffer.Position = 0;
|
2018-10-24 13:33:43 +08:00
|
|
|
|
IFlvTag tag = new FlvTag();
|
2018-03-19 01:05:02 +08:00
|
|
|
|
|
2018-03-19 17:35:27 +08:00
|
|
|
|
// Previous Tag Size
|
|
|
|
|
_buffer.Read(b, 0, 4);
|
|
|
|
|
b = new byte[4];
|
|
|
|
|
|
2018-03-19 01:05:02 +08:00
|
|
|
|
// TagType UI8
|
|
|
|
|
tag.TagType = (TagType)_buffer.ReadByte();
|
2018-03-25 15:14:27 +08:00
|
|
|
|
// Debug.Write(string.Format("Tag Type: {0}\n", tag.TagType));
|
2018-03-19 01:05:02 +08:00
|
|
|
|
|
|
|
|
|
// DataSize UI24
|
|
|
|
|
_buffer.Read(b, 1, 3);
|
2018-03-24 08:58:46 +08:00
|
|
|
|
tag.TagSize = BitConverter.ToInt32(b.ToBE(), 0);
|
2018-03-19 01:05:02 +08:00
|
|
|
|
|
|
|
|
|
// Timestamp UI24
|
|
|
|
|
_buffer.Read(b, 1, 3);
|
|
|
|
|
// TimestampExtended UI8
|
|
|
|
|
_buffer.Read(b, 0, 1);
|
|
|
|
|
tag.TimeStamp = BitConverter.ToInt32(b.ToBE(), 0);
|
|
|
|
|
|
|
|
|
|
// StreamID UI24
|
|
|
|
|
_buffer.Read(tag.StreamId, 0, 3);
|
|
|
|
|
|
|
|
|
|
currentTag = tag;
|
|
|
|
|
byte[] rest = _buffer.GetBuffer().Skip((int)_buffer.Position).Take((int)(dataLen - _buffer.Position)).ToArray();
|
|
|
|
|
_buffer.Position = 0;
|
|
|
|
|
|
|
|
|
|
_AddBytes(rest);
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-24 13:33:43 +08:00
|
|
|
|
public IFlvClipProcessor Clip()
|
2018-03-19 01:05:02 +08:00
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
// 如果禁用 clip 功能 或者 已经结束处理了
|
|
|
|
|
if (_noClip || Finallized)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2018-03-21 20:56:56 +08:00
|
|
|
|
lock (_writelock)
|
|
|
|
|
{
|
2018-03-29 13:02:43 +08:00
|
|
|
|
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d, Clip_Future);
|
2018-10-24 13:33:43 +08:00
|
|
|
|
return new FlvClipProcessor(Metadata, HTags, new List<IFlvTag>(Tags.ToArray()), Clip_Future);
|
2018-03-21 20:56:56 +08:00
|
|
|
|
}
|
2018-03-29 11:39:44 +08:00
|
|
|
|
}
|
2018-03-18 18:55:28 +08:00
|
|
|
|
}
|
2018-03-13 13:21:01 +08:00
|
|
|
|
|
2018-03-19 16:51:35 +08:00
|
|
|
|
public void FinallizeFile()
|
2018-03-13 13:21:01 +08:00
|
|
|
|
{
|
2018-03-21 20:56:56 +08:00
|
|
|
|
if (!Finallized)
|
2018-10-24 13:33:43 +08:00
|
|
|
|
{
|
2018-03-21 20:56:56 +08:00
|
|
|
|
lock (_writelock)
|
|
|
|
|
{
|
2018-03-26 02:56:56 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Metadata.Meta["duration"] = MaxTimeStamp / 1000.0;
|
|
|
|
|
Metadata.Meta["lasttimestamp"] = (double)MaxTimeStamp;
|
|
|
|
|
byte[] metadata = Metadata.ToBytes();
|
|
|
|
|
|
2018-03-29 11:39:44 +08:00
|
|
|
|
|
2018-03-26 02:56:56 +08:00
|
|
|
|
// 13 for FLV header & "0th" tag size
|
|
|
|
|
// 11 for 1st tag header
|
2018-03-29 11:39:44 +08:00
|
|
|
|
_fs?.Seek(13 + 11, SeekOrigin.Begin);
|
|
|
|
|
_fs?.Write(metadata, 0, metadata.Length);
|
2018-03-26 02:56:56 +08:00
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
logger.Error(ex, "保存录制文件时出错");
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2018-03-29 11:39:44 +08:00
|
|
|
|
_fs?.Close();
|
2018-03-26 02:56:56 +08:00
|
|
|
|
_buffer.Close();
|
|
|
|
|
_data.Close();
|
|
|
|
|
Tags.Clear();
|
2018-03-19 16:51:35 +08:00
|
|
|
|
|
2018-03-26 02:56:56 +08:00
|
|
|
|
Finallized = true;
|
2018-03-19 16:51:35 +08:00
|
|
|
|
|
2018-03-26 02:56:56 +08:00
|
|
|
|
StreamFinalized?.Invoke(this, new StreamFinalizedArgs() { StreamProcessor = this });
|
|
|
|
|
}
|
2018-03-21 20:56:56 +08:00
|
|
|
|
}
|
2018-10-24 13:33:43 +08:00
|
|
|
|
}
|
2018-03-19 16:51:35 +08:00
|
|
|
|
}
|
2018-03-13 13:21:01 +08:00
|
|
|
|
|
2018-03-12 18:57:20 +08:00
|
|
|
|
#region IDisposable Support
|
|
|
|
|
private bool disposedValue = false; // To detect redundant calls
|
|
|
|
|
|
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
|
|
|
{
|
|
|
|
|
if (!disposedValue)
|
|
|
|
|
{
|
|
|
|
|
if (disposing)
|
|
|
|
|
{
|
2018-03-21 20:56:56 +08:00
|
|
|
|
_buffer.Dispose();
|
|
|
|
|
_data.Dispose();
|
2018-03-29 11:39:44 +08:00
|
|
|
|
_fs?.Dispose();
|
2018-03-12 18:57:20 +08:00
|
|
|
|
}
|
2018-03-21 20:56:56 +08:00
|
|
|
|
Tags.Clear();
|
2018-03-12 18:57:20 +08:00
|
|
|
|
disposedValue = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-21 20:56:56 +08:00
|
|
|
|
public void Dispose()
|
2018-03-12 18:57:20 +08:00
|
|
|
|
{
|
|
|
|
|
Dispose(true);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|