BililiveRecorder/BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs

307 lines
11 KiB
C#
Raw Normal View History

2021-02-08 16:51:19 +08:00
using System;
using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Pipeline;
namespace BililiveRecorder.Flv.Writer
{
public class FlvProcessingContextWriter : IFlvProcessingContextWriter, IDisposable
{
private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
2021-03-03 19:04:37 +08:00
private readonly IFlvTagWriter tagWriter;
2021-02-23 18:03:37 +08:00
private bool disposedValue;
2021-02-08 16:51:19 +08:00
2021-03-03 19:04:37 +08:00
private WriterState state = WriterState.EmptyFileOrNotOpen;
2021-02-08 16:51:19 +08:00
private Tag? nextScriptTag = null;
private Tag? nextAudioHeaderTag = null;
private Tag? nextVideoHeaderTag = null;
private ScriptTagBody? lastScriptBody = null;
2021-02-23 18:03:37 +08:00
private double lastDuration;
public event EventHandler<FileClosedEventArgs>? FileClosed;
2021-02-08 16:51:19 +08:00
public Action<ScriptTagBody>? BeforeScriptTagWrite { get; set; }
public Action<ScriptTagBody>? BeforeScriptTagRewrite { get; set; }
2021-03-03 19:04:37 +08:00
public FlvProcessingContextWriter(IFlvTagWriter tagWriter)
2021-02-08 16:51:19 +08:00
{
2021-03-03 19:04:37 +08:00
this.tagWriter = tagWriter ?? throw new ArgumentNullException(nameof(tagWriter));
2021-02-08 16:51:19 +08:00
}
public async Task WriteAsync(FlvProcessingContext context)
{
if (this.state == WriterState.Invalid)
throw new InvalidOperationException("FlvProcessingContextWriter is in a invalid state.");
// TODO disk speed detection
//if (!await this.semaphoreSlim.WaitAsync(1000 * 5).ConfigureAwait(false))
//{
// this.state = WriterState.Invalid;
// throw new InvalidOperationException("WriteAsync Wait timed out.");
//}
await this.semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
foreach (var item in context.Output)
{
try
{
await this.WriteSingleActionAsync(item).ConfigureAwait(false);
}
catch (Exception)
{
this.state = WriterState.Invalid;
throw;
}
}
}
finally
{
this.semaphoreSlim.Release();
}
2021-02-23 18:03:37 +08:00
// Dispose tags
foreach (var action in context.Output)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
2021-02-08 16:51:19 +08:00
}
#region Flv Writer Implementation
private Task WriteSingleActionAsync(PipelineAction action) => action switch
{
PipelineNewFileAction _ => this.OpenNewFile(),
PipelineScriptAction scriptAction => this.WriteScriptTag(scriptAction),
PipelineHeaderAction headerAction => this.WriteHeaderTags(headerAction),
PipelineDataAction dataAction => this.WriteDataTags(dataAction),
2021-03-03 19:04:37 +08:00
PipelineEndAction endAction => this.WriteEndTag(endAction),
2021-02-08 16:51:19 +08:00
PipelineLogAlternativeHeaderAction logAlternativeHeaderAction => this.WriteAlternativeHeader(logAlternativeHeaderAction),
_ => Task.CompletedTask,
};
2021-03-03 19:04:37 +08:00
private Task WriteAlternativeHeader(PipelineLogAlternativeHeaderAction logAlternativeHeaderAction) =>
this.tagWriter.WriteAlternativeHeaders(logAlternativeHeaderAction.Tags);
2021-02-08 16:51:19 +08:00
2021-02-23 18:03:37 +08:00
private Task OpenNewFile()
2021-02-08 16:51:19 +08:00
{
2021-02-23 18:03:37 +08:00
this.CloseCurrentFileImpl();
2021-02-08 16:51:19 +08:00
// delay open until write
this.state = WriterState.EmptyFileOrNotOpen;
2021-02-23 18:03:37 +08:00
return Task.CompletedTask;
2021-02-08 16:51:19 +08:00
}
private Task WriteScriptTag(PipelineScriptAction scriptAction)
{
if (scriptAction.Tag != null)
this.nextScriptTag = scriptAction.Tag;
// delay writing
return Task.CompletedTask;
}
private Task WriteHeaderTags(PipelineHeaderAction headerAction)
{
if (headerAction.AudioHeader != null)
this.nextAudioHeaderTag = headerAction.AudioHeader;
if (headerAction.VideoHeader != null)
this.nextVideoHeaderTag = headerAction.VideoHeader;
// delay writing
return Task.CompletedTask;
}
2021-02-23 18:03:37 +08:00
private void CloseCurrentFileImpl()
2021-02-08 16:51:19 +08:00
{
2021-02-23 18:03:37 +08:00
var eventArgs = new FileClosedEventArgs
{
2021-03-03 19:04:37 +08:00
FileSize = this.tagWriter.FileSize,
2021-02-23 18:03:37 +08:00
Duration = this.lastDuration,
2021-03-03 19:04:37 +08:00
State = this.tagWriter.State,
2021-02-23 18:03:37 +08:00
};
2021-03-03 19:04:37 +08:00
if (this.tagWriter.CloseCurrentFile())
{
this.lastDuration = 0d;
FileClosed?.Invoke(this, eventArgs);
}
2021-02-08 16:51:19 +08:00
}
private async Task OpenNewFileImpl()
{
2021-02-23 18:03:37 +08:00
this.CloseCurrentFileImpl();
2021-02-08 16:51:19 +08:00
2021-03-03 19:04:37 +08:00
await this.tagWriter.CreateNewFile().ConfigureAwait(false);
2021-02-08 16:51:19 +08:00
this.state = WriterState.BeforeScript;
}
private async Task RewriteScriptTagImpl(double duration)
{
2021-03-03 19:04:37 +08:00
if (this.lastScriptBody is null)
2021-02-08 16:51:19 +08:00
return;
2021-02-23 18:03:37 +08:00
var value = this.lastScriptBody.GetMetadataValue();
2021-03-03 19:04:37 +08:00
if (value is not null)
2021-02-23 18:03:37 +08:00
value["duration"] = (ScriptDataNumber)duration;
2021-02-08 16:51:19 +08:00
this.BeforeScriptTagRewrite?.Invoke(this.lastScriptBody);
2021-03-03 19:04:37 +08:00
await this.tagWriter.OverwriteMetadata(this.lastScriptBody).ConfigureAwait(false);
2021-02-08 16:51:19 +08:00
}
private async Task WriteScriptTagImpl()
{
if (this.nextScriptTag is null)
throw new InvalidOperationException("No script tag availible");
if (this.nextScriptTag.ScriptData is null)
throw new InvalidOperationException("ScriptData is null");
this.lastScriptBody = this.nextScriptTag.ScriptData;
2021-02-23 18:03:37 +08:00
var value = this.lastScriptBody.GetMetadataValue();
2021-03-03 19:04:37 +08:00
if (value is not null)
2021-02-23 18:03:37 +08:00
value["duration"] = (ScriptDataNumber)0;
2021-02-08 16:51:19 +08:00
this.BeforeScriptTagWrite?.Invoke(this.lastScriptBody);
2021-03-03 19:04:37 +08:00
await this.tagWriter.WriteTag(this.nextScriptTag).ConfigureAwait(false);
2021-02-08 16:51:19 +08:00
this.state = WriterState.BeforeHeader;
}
private async Task WriteHeaderTagsImpl()
{
if (this.nextVideoHeaderTag is null)
throw new InvalidOperationException("No video header tag availible");
if (this.nextAudioHeaderTag is null)
throw new InvalidOperationException("No audio header tag availible");
2021-03-03 19:04:37 +08:00
await this.tagWriter.WriteTag(this.nextVideoHeaderTag).ConfigureAwait(false);
await this.tagWriter.WriteTag(this.nextAudioHeaderTag).ConfigureAwait(false);
2021-02-08 16:51:19 +08:00
this.state = WriterState.Writing;
}
private async Task WriteDataTags(PipelineDataAction dataAction)
{
switch (this.state)
{
case WriterState.EmptyFileOrNotOpen:
await this.OpenNewFileImpl().ConfigureAwait(false);
await this.WriteScriptTagImpl().ConfigureAwait(false);
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
break;
case WriterState.BeforeScript:
await this.WriteScriptTagImpl().ConfigureAwait(false);
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
break;
case WriterState.BeforeHeader:
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
break;
case WriterState.Writing:
break;
default:
throw new InvalidOperationException($"Can't write data tag with current state ({this.state})");
}
foreach (var tag in dataAction.Tags)
2021-03-03 19:04:37 +08:00
await this.tagWriter.WriteTag(tag).ConfigureAwait(false);
2021-02-08 16:51:19 +08:00
var duration = dataAction.Tags[dataAction.Tags.Count - 1].Timestamp / 1000d;
2021-02-23 18:03:37 +08:00
this.lastDuration = duration;
2021-02-08 16:51:19 +08:00
await this.RewriteScriptTagImpl(duration).ConfigureAwait(false);
}
2021-03-03 19:04:37 +08:00
private async Task WriteEndTag(PipelineEndAction endAction)
{
switch (this.state)
{
case WriterState.EmptyFileOrNotOpen:
await this.OpenNewFileImpl().ConfigureAwait(false);
await this.WriteScriptTagImpl().ConfigureAwait(false);
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
break;
case WriterState.BeforeScript:
await this.WriteScriptTagImpl().ConfigureAwait(false);
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
break;
case WriterState.BeforeHeader:
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
break;
case WriterState.Writing:
break;
default:
throw new InvalidOperationException($"Can't write data tag with current state ({this.state})");
}
await this.tagWriter.WriteTag(endAction.Tag).ConfigureAwait(false);
}
2021-02-08 16:51:19 +08:00
#endregion
#region IDisposable
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
2021-02-23 18:03:37 +08:00
// dispose managed state (managed objects)
2021-03-03 19:04:37 +08:00
this.tagWriter.Dispose();
2021-02-08 16:51:19 +08:00
}
2021-02-23 18:03:37 +08:00
// free unmanaged resources (unmanaged objects) and override finalizer
// set large fields to null
2021-02-08 16:51:19 +08:00
this.disposedValue = true;
}
}
2021-02-23 18:03:37 +08:00
// override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
2021-02-08 16:51:19 +08:00
// ~FlvProcessingContextWriter()
// {
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: false);
// }
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}
#endregion
}
internal enum WriterState
{
/// <summary>
/// Invalid
/// </summary>
Invalid,
/// <summary>
/// 未开文件、空文件、还未写入 FLV Header
/// </summary>
EmptyFileOrNotOpen,
/// <summary>
/// 已写入 FLV Header、还未写入 Script Tag
/// </summary>
BeforeScript,
/// <summary>
/// 已写入 Script Tag、还未写入 音视频 Header
/// </summary>
BeforeHeader,
/// <summary>
/// 已写入音视频 Header、正常写入数据
/// </summary>
Writing,
}
}