Repository: a1q123456/Harmonic Branch: master Commit: dba877459e5b Files: 226 Total size: 429.0 KB Directory structure: gitextract_v87mtvnp/ ├── .github/ │ └── workflows/ │ └── dotnetcore.yml ├── .gitignore ├── Harmonic/ │ ├── Buffers/ │ │ └── ByteBuffer.cs │ ├── Controllers/ │ │ ├── Living/ │ │ │ ├── LivingController.cs │ │ │ └── LivingStream.cs │ │ ├── NeverRegisterAttribute.cs │ │ ├── Record/ │ │ │ ├── RecordController.cs │ │ │ └── RecordStream.cs │ │ ├── RtmpController.cs │ │ ├── WebSocketController.cs │ │ └── WebSocketPlayController.cs │ ├── Harmonic.csproj │ ├── Hosting/ │ │ ├── IStartup.cs │ │ ├── RtmpServer.cs │ │ ├── RtmpServerBuilder.cs │ │ ├── RtmpServerOptions.cs │ │ └── WebSocketOptions.cs │ ├── Networking/ │ │ ├── Amf/ │ │ │ ├── Common/ │ │ │ │ ├── Amf3Object.cs │ │ │ │ ├── TypeRegisterState.cs │ │ │ │ ├── Undefined.cs │ │ │ │ └── Unsupported.cs │ │ │ ├── Data/ │ │ │ │ ├── IDynamicObject.cs │ │ │ │ ├── IExternalizable.cs │ │ │ │ └── Message.cs │ │ │ └── Serialization/ │ │ │ ├── Amf0/ │ │ │ │ ├── Amf0CommonValues.cs │ │ │ │ ├── Amf0Reader.cs │ │ │ │ ├── Amf0Type.cs │ │ │ │ ├── Amf0Writer.cs │ │ │ │ └── SerializationContext.cs │ │ │ ├── Amf3/ │ │ │ │ ├── Amf3Array.cs │ │ │ │ ├── Amf3ClassTraits.cs │ │ │ │ ├── Amf3CommonValues.cs │ │ │ │ ├── Amf3Dictionary.cs │ │ │ │ ├── Amf3Reader.cs │ │ │ │ ├── Amf3Type.cs │ │ │ │ ├── Amf3Writer.cs │ │ │ │ ├── Amf3Xml.cs │ │ │ │ ├── SerializationContext.cs │ │ │ │ └── Vector.cs │ │ │ └── Attributes/ │ │ │ ├── ClassFieldAttribute.cs │ │ │ └── TypedObjectAttribute.cs │ │ ├── ConnectionInformation.cs │ │ ├── Flv/ │ │ │ ├── Data/ │ │ │ │ ├── AacPacketType.cs │ │ │ │ ├── AudioData.cs │ │ │ │ ├── CodecId.cs │ │ │ │ ├── FlvAudioData.cs │ │ │ │ ├── FlvVideoData.cs │ │ │ │ ├── FrameType.cs │ │ │ │ ├── SoundFormat.cs │ │ │ │ ├── SoundRate.cs │ │ │ │ ├── SoundSize.cs │ │ │ │ └── SoundType.cs │ │ │ ├── FlvDemuxer.cs │ │ │ └── FlvMuxer.cs │ │ ├── Rtmp/ │ │ │ ├── ChunkStreamContext.cs │ │ │ ├── Data/ │ │ │ │ ├── ChunkBasicHeader.cs │ │ │ │ ├── ChunkHeader.cs │ │ │ │ ├── ChunkHeaderType.cs │ │ │ │ ├── Message.cs │ │ │ │ ├── MessageHeader.cs │ │ │ │ ├── MessageType.cs │ │ │ │ ├── SharedObjectMessage.cs │ │ │ │ └── UserControlMessageEvents.cs │ │ │ ├── Exceptions/ │ │ │ │ └── UnknownMessageReceivedException.cs │ │ │ ├── HandshakeContext.cs │ │ │ ├── IOPipeLine.cs │ │ │ ├── MessageReadingState.cs │ │ │ ├── Messages/ │ │ │ │ ├── AbortMessage.cs │ │ │ │ ├── AcknowledgementMessage.cs │ │ │ │ ├── AggregateMessage.cs │ │ │ │ ├── AmfEncodingVersion.cs │ │ │ │ ├── AudioMessage.cs │ │ │ │ ├── Commands/ │ │ │ │ │ ├── CallCommandMessage.cs │ │ │ │ │ ├── CommandMessage.cs │ │ │ │ │ ├── CommandMessageFactory.cs │ │ │ │ │ ├── ConnectCommandMessage.cs │ │ │ │ │ ├── CreateStreamCommandMessage.cs │ │ │ │ │ ├── DeleteStreamCommandMessage.cs │ │ │ │ │ ├── OnStatusCommandMessage.cs │ │ │ │ │ ├── PauseCommandMessage.cs │ │ │ │ │ ├── Play2CommandMessage.cs │ │ │ │ │ ├── PlayCommandMessage.cs │ │ │ │ │ ├── PublishCommandMessage.cs │ │ │ │ │ ├── ReceiveAudioCommandMessage.cs │ │ │ │ │ ├── ReceiveVideoCommandMessage.cs │ │ │ │ │ ├── ReturnResultCommandMessage.cs │ │ │ │ │ └── SeekCommandMessage.cs │ │ │ │ ├── ControlMessage.cs │ │ │ │ ├── DataMessage.cs │ │ │ │ ├── SetChunkSizeMessage.cs │ │ │ │ ├── SetPeerBandwidthMessage.cs │ │ │ │ ├── UserControlMessages/ │ │ │ │ │ ├── PingRequestMessage.cs │ │ │ │ │ ├── PingResponseMessage.cs │ │ │ │ │ ├── SetBufferLengthMessage.cs │ │ │ │ │ ├── StreamBeginMessage.cs │ │ │ │ │ ├── StreamDryMessage.cs │ │ │ │ │ ├── StreamEofMessage.cs │ │ │ │ │ ├── StreamIsRecordedMessage.cs │ │ │ │ │ ├── UserControlMessage.cs │ │ │ │ │ └── UserControlMessageFactory.cs │ │ │ │ ├── VideoMessage.cs │ │ │ │ └── WindowAcknowledgementSizeMessage.cs │ │ │ ├── NetConnection.cs │ │ │ ├── NetStream.cs │ │ │ ├── RtmpChunkStream.cs │ │ │ ├── RtmpControlChunkStream.cs │ │ │ ├── RtmpControlMessageStream.cs │ │ │ ├── RtmpMessageStream.cs │ │ │ ├── RtmpSession.cs │ │ │ ├── Serialization/ │ │ │ │ ├── OptionalArgumentAttribute.cs │ │ │ │ ├── RtmpCommandAttribute.cs │ │ │ │ ├── RtmpMessageAttribute.cs │ │ │ │ ├── SerializationContext.cs │ │ │ │ └── UserControlMessageAttribute.cs │ │ │ ├── Streaming/ │ │ │ │ ├── PublishingType.cs │ │ │ │ └── PublishingTypeNameAttribute.cs │ │ │ ├── Supervisor.cs │ │ │ └── WriteState.cs │ │ ├── Utils/ │ │ │ ├── NetworkBitConverter.cs │ │ │ └── StreamHelper.cs │ │ └── WebSocket/ │ │ └── WebSocketSession.cs │ ├── Rpc/ │ │ ├── CommandObjectAttribute.cs │ │ ├── FromCommandObjectAttribute.cs │ │ ├── FromOptionalArgumentAttribute.cs │ │ ├── RpcMethodAttribute.cs │ │ └── RpcService.cs │ └── Service/ │ ├── PublisherSessionService.cs │ ├── RecordService.cs │ └── RecordServiceConfiguration.cs ├── Harmonic.sln ├── LICENSE ├── README.md ├── RoadMap.md ├── UnitTest/ │ ├── TestAmf0Reader.cs │ ├── TestAmf0Writer.cs │ ├── TestAmf3Reader.cs │ ├── TestAmf3Writer.cs │ ├── TestUnlimitedBuffer.cs │ └── UnitTest.csproj ├── demo/ │ ├── MyLivingController.cs │ ├── MyLivingStream.cs │ ├── Program.cs │ ├── StartUp.cs │ └── demo.csproj ├── docs/ │ ├── README.md │ ├── api.md │ └── rpc.md └── samples/ ├── amf0/ │ ├── boolean/ │ │ ├── false.amf0 │ │ └── true.amf0 │ ├── misc/ │ │ ├── array.amf0 │ │ ├── date.amf0 │ │ ├── ecmaarray.amf0 │ │ ├── longstring.amf0 │ │ ├── null.amf0 │ │ ├── object.amf0 │ │ ├── packet.amf0 │ │ ├── undefined.amf0 │ │ └── xml.amf0 │ ├── number/ │ │ ├── 1.1261843717924092.amf0 │ │ ├── 2.3763213699559538.amf0 │ │ ├── 3.498957015368982.amf0 │ │ ├── 3.9231228517554273.amf0 │ │ ├── 4.141123418573091.amf0 │ │ ├── 4.485998361678176.amf0 │ │ ├── 5.442560101247932.amf0 │ │ ├── 7.560779119365773.amf0 │ │ ├── 9.719564819564491.amf0 │ │ └── 9.844079468164518.amf0 │ └── string/ │ ├── fkqudskxxb.amf0 │ ├── grkogrokmq.amf0 │ ├── gymtsavnng.amf0 │ ├── karxrlzavl.amf0 │ ├── lbwkjydfuv.amf0 │ ├── qsqwosrxcl.amf0 │ ├── rpwhhjwary.amf0 │ ├── sjceuhcjfa.amf0 │ ├── vrouinfvzr.amf0 │ └── vsyrigrfbn.amf0 └── amf3/ ├── boolean/ │ ├── false.amf3 │ └── true.amf3 ├── intenger/ │ ├── 56.amf3 │ ├── 57.amf3 │ ├── 60.amf3 │ ├── 67.amf3 │ ├── 72.amf3 │ ├── 73.amf3 │ ├── 75.amf3 │ ├── 78.amf3 │ ├── 82.amf3 │ └── 98.amf3 ├── misc/ │ ├── array.amf3 │ ├── bytearray.amf3 │ ├── date.amf3 │ ├── dictionary.amf3 │ ├── externalizable.amf3 │ ├── null.amf3 │ ├── object.amf3 │ ├── packet.amf3 │ ├── undefined.amf3 │ ├── vector_any_object.amf3 │ ├── vector_double.amf3 │ ├── vector_int.amf3 │ ├── vector_typted_object.amf3 │ ├── vector_uint.amf3 │ ├── xml.amf3 │ └── xml_document.amf3 ├── number/ │ ├── 0.05806697191443333.amf3 │ ├── 3.962148410082559.amf3 │ ├── 4.465764800567858.amf3 │ ├── 6.863435764713296.amf3 │ ├── 7.645173446829178.amf3 │ ├── 8.451623695104308.amf3 │ ├── 8.518697602984554.amf3 │ ├── 8.85002823631796.amf3 │ ├── 9.838871036292584.amf3 │ └── 9.98509389093438.amf3 └── string/ ├── aoxqmkvbxa.amf3 ├── bghnwadduz.amf3 ├── cmaljzrwgc.amf3 ├── cuyerozwyf.amf3 ├── dfjfucqvpr.amf3 ├── fxxcsjosdu.amf3 ├── korbgwizge.amf3 ├── psvigwvvpx.amf3 ├── ubteltbaku.amf3 └── vqayztgtuf.amf3 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/dotnetcore.yml ================================================ name: .NET Core on: [push] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: dotnet-version: 2.2.108 - name: Build with dotnet run: dotnet pack --configuration Release - name: Nuget Push run: dotnet nuget push Harmonic/bin/Release/*.nupkg -k ${{ secrets.NUGET_TOKEN }} -s https://api.nuget.org/v3/index.json ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Publish results publish/ # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ .vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # .NET Core project.lock.json project.fragment.lock.json artifacts/ **/Properties/launchSettings.json *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Typescript v1 declaration files typings/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Telerik's JustMock configuration file *.jmconfig ================================================ FILE: Harmonic/Buffers/ByteBuffer.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Sources; namespace Harmonic.Buffers { public class ByteBuffer : IDisposable { private List _buffers = new List(); private int _bufferEnd = 0; private int _bufferStart = 0; private readonly int _maxiumBufferSize = 0; private event Action _memoryUnderLimit; private event Action _dataWritten; private object _sync = new object(); private ArrayPool _arrayPool; public int BufferSegmentSize { get; } public int Length { get { return _buffers.Count * BufferSegmentSize - BufferBytesAvailable() - _bufferStart; } } public ByteBuffer(int bufferSegmentSize = 1024, int maxiumBufferSize = -1, ArrayPool arrayPool = null) { if (bufferSegmentSize == 0) { throw new ArgumentOutOfRangeException(); } BufferSegmentSize = bufferSegmentSize; _maxiumBufferSize = maxiumBufferSize; if (arrayPool != null) { _arrayPool = arrayPool; } else { _arrayPool = ArrayPool.Shared; } _buffers.Add(_arrayPool.Rent(bufferSegmentSize)); } private int BufferBytesAvailable() { return BufferSegmentSize - _bufferEnd; } private void AddNewBufferSegment() { var arr = _arrayPool.Rent(BufferSegmentSize); Debug.Assert(_buffers.IndexOf(arr) == -1); _buffers.Add(arr); _bufferEnd = 0; } public void WriteToBuffer(byte data) { if (Length > _maxiumBufferSize && _maxiumBufferSize >= 0) { throw new InvalidOperationException("buffer length exceeded"); } lock (_sync) { int available = BufferBytesAvailable(); byte[] buffer = null; if (available == 0) { AddNewBufferSegment(); buffer = _buffers.Last(); } else { buffer = _buffers.Last(); } buffer[_bufferEnd] = data; _bufferEnd += 1; } } private void WriteToBufferNoCheck(ReadOnlySpan bytes) { lock (_sync) { var requiredLength = bytes.Length; int available = BufferBytesAvailable(); if (available < requiredLength) { var bytesIndex = 0; do { var buffer = _buffers.Last(); var seq = bytes.Slice(bytesIndex, Math.Min(available, requiredLength)); seq.CopyTo(buffer.AsSpan(_bufferEnd)); _bufferEnd += seq.Length; requiredLength -= seq.Length; available -= seq.Length; bytesIndex += seq.Length; if (available == 0) { AddNewBufferSegment(); available = BufferBytesAvailable(); } } while (requiredLength != 0); } else { var buffer = _buffers.Last(); bytes.CopyTo(buffer.AsSpan(_bufferEnd)); _bufferEnd += bytes.Length; } } _dataWritten?.Invoke(); } class _source : IValueTaskSource { private static readonly Action CallbackCompleted = _ => { Debug.Assert(false, "Should not be invoked"); }; private List cb = new List(); private ValueTaskSourceStatus status = ValueTaskSourceStatus.Pending; private ExecutionContext executionContext; private object scheduler; private object state; private Action continuation; public _source() { } public void Cancel() { status = ValueTaskSourceStatus.Canceled; } public void Success() { status = ValueTaskSourceStatus.Succeeded; var previousContinuation = Interlocked.CompareExchange(ref this.continuation, CallbackCompleted, null); if (previousContinuation != null) { // Async work completed, continue with... continuation ExecutionContext ec = executionContext; if (ec == null) { InvokeContinuation(previousContinuation, this.state, forceAsync: false); } else { // This case should be relatively rare, as the async Task/ValueTask method builders // use the awaiter's UnsafeOnCompleted, so this will only happen with code that // explicitly uses the awaiter's OnCompleted instead. executionContext = null; ExecutionContext.Run(ec, runState => { var t = (Tuple<_source, Action, object>)runState; t.Item1.InvokeContinuation(t.Item2, t.Item3, forceAsync: false); }, Tuple.Create(this, previousContinuation, this.state)); } } } public void GetResult(short token) { return; } public ValueTaskSourceStatus GetStatus(short token) { return status; } public void OnCompleted(Action continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) { if ((flags & ValueTaskSourceOnCompletedFlags.FlowExecutionContext) != 0) { this.executionContext = ExecutionContext.Capture(); } if ((flags & ValueTaskSourceOnCompletedFlags.UseSchedulingContext) != 0) { SynchronizationContext sc = SynchronizationContext.Current; if (sc != null && sc.GetType() != typeof(SynchronizationContext)) { this.scheduler = sc; } else { TaskScheduler ts = TaskScheduler.Current; if (ts != TaskScheduler.Default) { this.scheduler = ts; } } } // Remember current state this.state = state; // Remember continuation to be executed on completed (if not already completed, in case of which // continuation will be set to CallbackCompleted) var previousContinuation = Interlocked.CompareExchange(ref this.continuation, continuation, null); if (previousContinuation != null) { if (!ReferenceEquals(previousContinuation, CallbackCompleted)) { throw new InvalidOperationException(); } // Lost the race condition and the operation has now already completed. // We need to invoke the continuation, but it must be asynchronously to // avoid a stack dive. However, since all of the queueing mechanisms flow // ExecutionContext, and since we're still in the same context where we // captured it, we can just ignore the one we captured. executionContext = null; this.state = null; // we have the state in "state"; no need for the one in UserToken InvokeContinuation(continuation, state, forceAsync: true); } cb.Add(() => continuation(state)); } private void InvokeContinuation(Action continuation, object state, bool forceAsync) { if (continuation == null) return; object scheduler = this.scheduler; this.scheduler = null; if (scheduler != null) { if (scheduler is SynchronizationContext sc) { sc.Post(s => { var t = (Tuple, object>)s; t.Item1(t.Item2); }, Tuple.Create(continuation, state)); } else { Debug.Assert(scheduler is TaskScheduler, $"Expected TaskScheduler, got {scheduler}"); Task.Factory.StartNew(continuation, state, CancellationToken.None, TaskCreationOptions.DenyChildAttach, (TaskScheduler)scheduler); } } else if (forceAsync) { ThreadPool.QueueUserWorkItem(continuation, state, preferLocal: true); } else { continuation(state); } } } public ValueTask WriteToBufferAsync(ReadOnlyMemory bytes) { lock (_sync) { if (Length + bytes.Length > _maxiumBufferSize && _maxiumBufferSize >= 0) { var source = new _source(); Action ac = null; ac = () => { _memoryUnderLimit -= ac; WriteToBufferNoCheck(bytes.Span); source.Success(); }; _memoryUnderLimit += ac; return new ValueTask(source, 0); } } WriteToBufferNoCheck(bytes.Span); return default; } public void WriteToBuffer(ReadOnlySpan bytes) { while (Length + bytes.Length > _maxiumBufferSize && _maxiumBufferSize >= 0) { Thread.Yield(); } WriteToBufferNoCheck(bytes); } private void TakeOutMemoryNoCheck(Span buffer) { lock (_sync) { var discardBuffers = new List(); bool prevDiscarded = false; if (Length < buffer.Length && _maxiumBufferSize >= 0) { throw new InvalidProgramException(); } foreach (var b in _buffers) { if (buffer.Length == 0) { break; } var start = 0; var end = BufferSegmentSize; var isFirst = b == _buffers.First() || prevDiscarded; var isLast = b == _buffers.Last(); if (isFirst) { start = _bufferStart; } if (isLast) { end = _bufferEnd; } var length = end - start; var needToCopy = Math.Min(buffer.Length, length); b.AsSpan(start, needToCopy).CopyTo(buffer); start += needToCopy; if (isFirst) { _bufferStart += needToCopy; } if (end - start == 0) { if (isFirst) { _bufferStart = 0; } if (isLast) { _bufferEnd = 0; } discardBuffers.Add(b); prevDiscarded = true; } else { prevDiscarded = false; } buffer = buffer.Slice(needToCopy); } //Console.WriteLine(Length); Debug.Assert(buffer.Length == 0 || _maxiumBufferSize < 0); while (discardBuffers.Any()) { var b = discardBuffers.First(); _arrayPool.Return(b); discardBuffers.Remove(b); _buffers.Remove(b); } if (!_buffers.Any()) { AddNewBufferSegment(); } } if (Length <= _maxiumBufferSize && _maxiumBufferSize >= 0) { _memoryUnderLimit?.Invoke(); } } public ValueTask TakeOutMemoryAsync(Memory buffer, CancellationToken ct = default) { lock (_sync) { if (buffer.Length > Length && _maxiumBufferSize >= 0) { var source = new _source(); var reg = ct.Register(() => { source.Cancel(); }); Action ac = null; ac = () => { if (buffer.Length <= Length) { _dataWritten -= ac; reg.Dispose(); TakeOutMemoryNoCheck(buffer.Span); source.Success(); } }; _dataWritten += ac; return new ValueTask(source, 0); } } TakeOutMemoryNoCheck(buffer.Span); return default; } public void TakeOutMemory(Span buffer) { while (buffer.Length > Length && _maxiumBufferSize >= 0) { Thread.Yield(); } TakeOutMemoryNoCheck(buffer); } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { foreach (var buffer in _buffers) { _arrayPool.Return(buffer); } _buffers.Clear(); } disposedValue = true; } } // ~UnlimitedBuffer() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Controllers/Living/LivingController.cs ================================================ using Harmonic.Networking.Rtmp; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Controllers.Living { public class LivingController : RtmpController { [RpcMethod("createStream")] public uint CreateStream() { var stream = RtmpSession.CreateNetStream(); return stream.MessageStream.MessageStreamId; } } } ================================================ FILE: Harmonic/Controllers/Living/LivingStream.cs ================================================ using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Rtmp; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Messages.UserControlMessages; using Harmonic.Networking.Rtmp.Streaming; using Harmonic.Rpc; using Harmonic.Service; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Harmonic.Controllers.Living { public class LivingStream : NetStream { private List _cleanupActions = new List(); private PublishingType _publishingType; private PublisherSessionService _publisherSessionService = null; public DataMessage FlvMetadata = null; public AudioMessage AACConfigureRecord = null; public VideoMessage AVCConfigureRecord = null; public event Action OnVideoMessage; public event Action OnAudioMessage; private RtmpChunkStream _videoChunkStream = null; private RtmpChunkStream _audioChunkStream = null; public LivingStream(PublisherSessionService publisherSessionService) { _publisherSessionService = publisherSessionService; } [RpcMethod("play")] public async Task Play( [FromOptionalArgument] string streamName, [FromOptionalArgument] double start = -1, [FromOptionalArgument] double duration = -1, [FromOptionalArgument] bool reset = false) { var publisher = _publisherSessionService.FindPublisher(streamName); if (publisher == null) { throw new KeyNotFoundException(); } var resetData = new AmfObject { {"level", "status" }, {"code", "NetStream.Play.Reset" }, {"description", "Resetting and playing stream." }, {"details", streamName } }; var resetStatus = RtmpSession.CreateCommandMessage(); resetStatus.InfoObject = resetData; await MessageStream.SendMessageAsync(ChunkStream, resetStatus); var startData = new AmfObject { {"level", "status" }, {"code", "NetStream.Play.Start" }, {"description", "Started playing." }, {"details", streamName } }; var startStatus = RtmpSession.CreateCommandMessage(); startStatus.InfoObject = startData; await MessageStream.SendMessageAsync(ChunkStream, startStatus); var flvMetadata = RtmpSession.CreateData(); flvMetadata.MessageHeader = (MessageHeader)publisher.FlvMetadata.MessageHeader.Clone(); flvMetadata.Data = publisher.FlvMetadata.Data; await MessageStream.SendMessageAsync(ChunkStream, flvMetadata); _videoChunkStream = RtmpSession.CreateChunkStream(); _audioChunkStream = RtmpSession.CreateChunkStream(); if (publisher.AACConfigureRecord != null) { await MessageStream.SendMessageAsync(_audioChunkStream, publisher.AACConfigureRecord.Clone() as AudioMessage); } if (publisher.AVCConfigureRecord != null) { await MessageStream.SendMessageAsync(_videoChunkStream, publisher.AVCConfigureRecord.Clone() as VideoMessage); } publisher.OnAudioMessage += SendAudio; publisher.OnVideoMessage += SendVideo; _cleanupActions.Add(() => { publisher.OnVideoMessage -= SendVideo; publisher.OnAudioMessage -= SendAudio; }); } private async void SendVideo(VideoMessage message) { var video = message.Clone() as VideoMessage; try { await MessageStream.SendMessageAsync(_videoChunkStream, video); } catch { foreach (var a in _cleanupActions) { a(); } RtmpSession.Close(); } } private async void SendAudio(AudioMessage message) { var audio = message.Clone(); try { await MessageStream.SendMessageAsync(_audioChunkStream, audio as AudioMessage); } catch { foreach (var a in _cleanupActions) { a(); } RtmpSession.Close(); } } [RpcMethod(Name = "publish")] public void Publish([FromOptionalArgument] string publishingName, [FromOptionalArgument] string publishingType) { if (string.IsNullOrEmpty(publishingName)) { throw new InvalidOperationException("empty publishing name"); } if (!PublishingHelpers.IsTypeSupported(publishingType)) { throw new InvalidOperationException($"not supported publishing type {publishingType}"); } _publishingType = PublishingHelpers.PublishingTypes[publishingType]; _publisherSessionService.RegisterPublisher(publishingName, this); RtmpSession.SendControlMessageAsync(new StreamBeginMessage() { StreamID = MessageStream.MessageStreamId }); var onStatus = RtmpSession.CreateCommandMessage(); MessageStream.RegisterMessageHandler(HandleDataMessage); MessageStream.RegisterMessageHandler(HandleAudioMessage); MessageStream.RegisterMessageHandler(HandleVideoMessage); onStatus.InfoObject = new AmfObject { {"level", "status" }, {"code", "NetStream.Publish.Start" }, {"description", "Stream is now published." }, {"details", publishingName } }; MessageStream.SendMessageAsync(ChunkStream, onStatus); } private void HandleDataMessage(DataMessage msg) { FlvMetadata = msg; } private void HandleAudioMessage(AudioMessage audioData) { if (AACConfigureRecord == null && audioData.Data.Length >= 2) { AACConfigureRecord = audioData; return; } OnAudioMessage?.Invoke(audioData); } private void HandleVideoMessage(VideoMessage videoData) { if (AVCConfigureRecord == null && videoData.Data.Length >= 2) { AVCConfigureRecord = videoData; } OnVideoMessage?.Invoke(videoData); } #region Disposable Support private bool disposedValue = false; protected override void Dispose(bool disposing) { if (!disposedValue) { base.Dispose(disposing); _publisherSessionService.RemovePublisher(this); _videoChunkStream?.Dispose(); _audioChunkStream?.Dispose(); disposedValue = true; } } #endregion } } ================================================ FILE: Harmonic/Controllers/NeverRegisterAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Controllers { [AttributeUsage(AttributeTargets.Class)] public class NeverRegisterAttribute : Attribute { } } ================================================ FILE: Harmonic/Controllers/Record/RecordController.cs ================================================ using Harmonic.Networking.Rtmp.Messages; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Controllers.Record { public class RecordController : RtmpController { [RpcMethod("createStream")] public uint CreateStream() { var stream = RtmpSession.CreateNetStream(); return stream.MessageStream.MessageStreamId; } } } ================================================ FILE: Harmonic/Controllers/Record/RecordStream.cs ================================================ using Harmonic.Networking.Flv; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Rtmp; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Messages.UserControlMessages; using Harmonic.Networking.Rtmp.Streaming; using Harmonic.Networking.Utils; using Harmonic.Rpc; using Harmonic.Service; using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using Harmonic.Networking.Flv.Data; namespace Harmonic.Controllers.Record { public class RecordStream : NetStream { private PublishingType _publishingType; private FileStream _recordFile = null; private FileStream _recordFileData = null; private RecordService _recordService = null; private DataMessage _metaData = null; private uint _currentTimestamp = 0; private SemaphoreSlim _playLock = new SemaphoreSlim(1); private int _playing = 0; private AmfObject _keyframes = null; private List _keyframeTimes; private List _keyframeFilePositions; private long _bufferMs = -1; private RtmpChunkStream VideoChunkStream { get; set; } = null; private RtmpChunkStream AudioChunkStream { get; set; } = null; private bool _disposed = false; private CancellationTokenSource _playCts; protected override async void Dispose(bool disposing) { base.Dispose(disposing); if (!_disposed) { _disposed = true; if (_recordFileData != null) { try { var filePath = _recordFileData.Name; using (var recordFile = new FileStream(filePath.Substring(0, filePath.Length - 5) + ".flv", FileMode.OpenOrCreate)) { recordFile.SetLength(0); recordFile.Seek(0, SeekOrigin.Begin); await recordFile.WriteAsync(FlvMuxer.MultiplexFlvHeader(true, true)); var metaData = _metaData.Data[1] as Dictionary; metaData["duration"] = ((double)_currentTimestamp) / 1000; metaData["keyframes"] = _keyframes; _metaData.MessageHeader.MessageLength = 0; var dataTagLen = FlvMuxer.MultiplexFlv(_metaData).Length; var offset = recordFile.Position + dataTagLen; for (int i = 0; i < _keyframeFilePositions.Count; i++) { _keyframeFilePositions[i] = (double)_keyframeFilePositions[i] + offset; } await recordFile.WriteAsync(FlvMuxer.MultiplexFlv(_metaData)); _recordFileData.Seek(0, SeekOrigin.Begin); await _recordFileData.CopyToAsync(recordFile); _recordFileData.Dispose(); File.Delete(filePath); } } catch (Exception e) { Console.WriteLine(e); } } _recordFile?.Dispose(); } } public RecordStream(RecordService recordService) { _recordService = recordService; } [RpcMethod(Name = "publish")] public async Task Publish([FromOptionalArgument] string streamName, [FromOptionalArgument] string publishingType) { if (string.IsNullOrEmpty(streamName)) { throw new InvalidOperationException("empty publishing name"); } if (!PublishingHelpers.IsTypeSupported(publishingType)) { throw new InvalidOperationException($"not supported publishing type {publishingType}"); } _publishingType = PublishingHelpers.PublishingTypes[publishingType]; await RtmpSession.SendControlMessageAsync(new StreamIsRecordedMessage() { StreamID = MessageStream.MessageStreamId }); await RtmpSession.SendControlMessageAsync(new StreamBeginMessage() { StreamID = MessageStream.MessageStreamId }); var onStatus = RtmpSession.CreateCommandMessage(); MessageStream.RegisterMessageHandler(HandleData); MessageStream.RegisterMessageHandler(HandleAudioMessage); MessageStream.RegisterMessageHandler(HandleVideoMessage); MessageStream.RegisterMessageHandler(HandleUserControlMessage); onStatus.InfoObject = new AmfObject { {"level", "status" }, {"code", "NetStream.Publish.Start" }, {"description", "Stream is now published." }, {"details", streamName } }; await MessageStream.SendMessageAsync(ChunkStream, onStatus); _recordFileData = new FileStream(_recordService.GetRecordFilename(streamName) + ".data", FileMode.OpenOrCreate); _recordFileData.SetLength(0); _keyframes = new AmfObject(); _keyframeTimes = new List(); _keyframeFilePositions = new List(); _keyframes.Add("times", _keyframeTimes); _keyframes.Add("filepositions", _keyframeFilePositions); } private void HandleUserControlMessage(UserControlMessage msg) { if (msg.UserControlEventType == UserControlEventType.SetBufferLength) { _bufferMs = (msg as SetBufferLengthMessage).BufferMilliseconds; } } private async void HandleAudioMessage(AudioMessage message) { try { _currentTimestamp = Math.Max(_currentTimestamp, message.MessageHeader.Timestamp); await SaveMessage(message); } catch { RtmpSession.Close(); } } private async void HandleVideoMessage(VideoMessage message) { try { _currentTimestamp = Math.Max(_currentTimestamp, message.MessageHeader.Timestamp); var head = message.Data.Span[0]; var data = FlvDemuxer.DemultiplexVideoData(message); if (data.FrameType == FrameType.KeyFrame) { _keyframeTimes.Add((double)message.MessageHeader.Timestamp / 1000); _keyframeFilePositions.Add((double)_recordFileData.Position); } await SaveMessage(message); } catch { RtmpSession.Close(); } } private void HandleData(DataMessage message) { try { _metaData = message; _metaData.Data.RemoveAt(0); } catch { RtmpSession.Close(); } } [RpcMethod("seek")] public async Task Seek([FromOptionalArgument] double milliSeconds) { var resetData = new AmfObject { {"level", "status" }, {"code", "NetStream.Seek.Notify" }, {"description", "Seeking stream." }, {"details", "seek" } }; var resetStatus = RtmpSession.CreateCommandMessage(); resetStatus.InfoObject = resetData; await MessageStream.SendMessageAsync(ChunkStream, resetStatus); _playCts?.Cancel(); while (_playing == 1) { await Task.Yield(); } var cts = new CancellationTokenSource(); _playCts?.Dispose(); _playCts = cts; await SeekAndPlay(milliSeconds, cts.Token); } [RpcMethod("play")] public async Task Play( [FromOptionalArgument] string streamName, [FromOptionalArgument] double start = -1, [FromOptionalArgument] double duration = -1, [FromOptionalArgument] bool reset = false) { _recordFile = new FileStream(_recordService.GetRecordFilename(streamName) + ".flv", FileMode.Open, FileAccess.Read); await FlvDemuxer.AttachStream(_recordFile); var resetData = new AmfObject { {"level", "status" }, {"code", "NetStream.Play.Reset" }, {"description", "Resetting and playing stream." }, {"details", streamName } }; var resetStatus = RtmpSession.CreateCommandMessage(); resetStatus.InfoObject = resetData; await MessageStream.SendMessageAsync(ChunkStream, resetStatus); var startData = new AmfObject { {"level", "status" }, {"code", "NetStream.Play.Start" }, {"description", "Started playing." }, {"details", streamName } }; var startStatus = RtmpSession.CreateCommandMessage(); startStatus.InfoObject = startData; await MessageStream.SendMessageAsync(ChunkStream, startStatus); var bandwidthLimit = new WindowAcknowledgementSizeMessage() { WindowSize = 500 * 1024 }; await RtmpSession.ControlMessageStream.SendMessageAsync(RtmpSession.ControlChunkStream, bandwidthLimit); VideoChunkStream = RtmpSession.CreateChunkStream(); AudioChunkStream = RtmpSession.CreateChunkStream(); var cts = new CancellationTokenSource(); _playCts?.Dispose(); _playCts = cts; start = Math.Max(start, 0); await SeekAndPlay(start / 1000, cts.Token); } [RpcMethod("pause")] public async Task Pause([FromOptionalArgument] bool isPause, [FromOptionalArgument] double milliseconds) { if (isPause) { _playCts?.Cancel(); while (_playing == 1) { await Task.Yield(); } } else { var cts = new CancellationTokenSource(); _playCts?.Dispose(); _playCts = cts; await SeekAndPlay(milliseconds, cts.Token); } } private async Task StartPlayNoLock(CancellationToken ct) { while (_recordFile.Position < _recordFile.Length && !ct.IsCancellationRequested) { while (_bufferMs != -1 && _currentTimestamp >= _bufferMs) { await Task.Yield(); } await PlayRecordFileNoLock(ct); } } private Task ReadMessage(CancellationToken ct) { return FlvDemuxer.DemultiplexFlvAsync(ct); } private async Task SeekAndPlay(double milliSeconds, CancellationToken ct) { await _playLock.WaitAsync(); Interlocked.Exchange(ref _playing, 1); try { _recordFile.Seek(9, SeekOrigin.Begin); FlvDemuxer.SeekNoLock(milliSeconds, _metaData == null ? null : _metaData.Data[2] as Dictionary, ct); await StartPlayNoLock(ct); } catch (Exception e) { Console.WriteLine(e); } finally { Interlocked.Exchange(ref _playing, 0); _playLock.Release(); } } private async Task PlayRecordFileNoLock(CancellationToken ct) { var message = await ReadMessage(ct); if (message is AudioMessage) { await MessageStream.SendMessageAsync(AudioChunkStream, message); } else if (message is VideoMessage) { await MessageStream.SendMessageAsync(VideoChunkStream, message); } else if (message is DataMessage data) { data.Data.Insert(0, "@setDataFrame"); _metaData = data; await MessageStream.SendMessageAsync(ChunkStream, data); } _currentTimestamp = Math.Max(_currentTimestamp, message.MessageHeader.Timestamp); } private async Task SaveMessage(Message message) { await _recordFileData.WriteAsync(FlvMuxer.MultiplexFlv(message)); } } } ================================================ FILE: Harmonic/Controllers/RtmpController.cs ================================================ using Harmonic.Networking.Flv; using Harmonic.Networking.Rtmp; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Controllers { public abstract class RtmpController { public RtmpMessageStream MessageStream { get; internal set; } = null; public RtmpChunkStream ChunkStream { get; internal set; } = null; public RtmpSession RtmpSession { get; internal set; } = null; private FlvMuxer _flvMuxer = null; private FlvDemuxer _flvDemuxer = null; public FlvMuxer FlvMuxer { get { if (_flvMuxer == null) { _flvMuxer = new FlvMuxer(); } return _flvMuxer; } } public FlvDemuxer FlvDemuxer { get { if (_flvDemuxer == null) { _flvDemuxer = new FlvDemuxer(RtmpSession.IOPipeline.Options.MessageFactories); } return _flvDemuxer; } } [RpcMethod("deleteStream")] public void DeleteStream([FromOptionalArgument] double streamId) { RtmpSession.DeleteNetStream((uint)streamId); } } } ================================================ FILE: Harmonic/Controllers/WebSocketController.cs ================================================ using Harmonic.Networking.Flv; using Harmonic.Networking.Rtmp; using Harmonic.Networking.WebSocket; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Text; using System.Threading.Tasks; namespace Harmonic.Controllers { public abstract class WebSocketController { public string StreamName { get; internal set; } public NameValueCollection Query { get; internal set; } public WebSocketSession Session { get; internal set; } private FlvMuxer _flvMuxer = null; private FlvDemuxer _flvDemuxer = null; public FlvMuxer FlvMuxer { get { if (_flvMuxer == null) { _flvMuxer = new FlvMuxer(); } return _flvMuxer; } } public FlvDemuxer FlvDemuxer { get { if (_flvDemuxer == null) { _flvDemuxer = new FlvDemuxer(Session.Options.MessageFactories); } return _flvDemuxer; } } public abstract Task OnConnect(); public abstract void OnMessage(string msg); } } ================================================ FILE: Harmonic/Controllers/WebSocketPlayController.cs ================================================ using Harmonic.Networking; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Service; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Harmonic.Controllers { public class WebSocketPlayController : WebSocketController, IDisposable { private RecordService _recordService = null; private PublisherSessionService _publisherSessionService = null; private List _cleanupActions = new List(); private FileStream _recordFile = null; private SemaphoreSlim _playLock = new SemaphoreSlim(1); private int _playing = 0; private long _playRangeTo = 0; public WebSocketPlayController(PublisherSessionService publisherSessionService, RecordService recordService) { _publisherSessionService = publisherSessionService; _recordService = recordService; } public override async Task OnConnect() { var publisher = _publisherSessionService.FindPublisher(StreamName); if (publisher != null) { _cleanupActions.Add(() => { publisher.OnAudioMessage -= SendAudio; publisher.OnVideoMessage -= SendVideo; }); var metadata = (Dictionary)publisher.FlvMetadata.Data.Last(); var hasAudio = metadata.ContainsKey("audiocodecid"); var hasVideo = metadata.ContainsKey("videocodecid"); await Session.SendFlvHeaderAsync(hasAudio, hasVideo); await Session.SendMessageAsync(publisher.FlvMetadata); if (hasAudio) { await Session.SendMessageAsync(publisher.AACConfigureRecord); } if (hasVideo) { await Session.SendMessageAsync(publisher.AVCConfigureRecord); } publisher.OnAudioMessage += SendAudio; publisher.OnVideoMessage += SendVideo; } // play record else { _recordFile = new FileStream(_recordService.GetRecordFilename(StreamName) + ".flv", FileMode.Open, FileAccess.Read); var fromStr = Query.Get("from"); long from = 0; if (fromStr != null) { from = long.Parse(fromStr); } var toStr = Query.Get("to"); _playRangeTo = -1; if (toStr != null) { _playRangeTo = long.Parse(toStr); } var header = new byte[9]; await _recordFile.ReadBytesAsync(header); await Session.SendRawDataAsync(header); from = Math.Max(from, 9); _recordFile.Seek(from, SeekOrigin.Begin); await PlayRecordFile(); } } private async Task PlayRecordFile() { Interlocked.Exchange(ref _playing, 1); var buffer = new byte[512]; int bytesRead; do { await _playLock.WaitAsync(); bytesRead = await _recordFile.ReadAsync(buffer); await Session.SendRawDataAsync(buffer); _playLock.Release(); if (_playRangeTo < _recordFile.Position && _playRangeTo != -1) { break; } } while (bytesRead != 0); Interlocked.Exchange(ref _playing, 0); } private void SendVideo(VideoMessage message) { Session.SendMessageAsync(message); } private void SendAudio(AudioMessage message) { Session.SendMessageAsync(message); } public override void OnMessage(string msg) { } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { foreach (var c in _cleanupActions) { c(); } _recordFile?.Dispose(); } disposedValue = true; } } // ~WebSocketPlayController() // { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Harmonic.csproj ================================================  Harmonic 0.0.2 kyokunnodenwa kyokunnodenwa netcoreapp2.2 latest false true TRACE full true false ================================================ FILE: Harmonic/Hosting/IStartup.cs ================================================ using System; using Autofac; namespace Harmonic.Hosting { public interface IStartup { void ConfigureServices(ContainerBuilder builder); } } ================================================ FILE: Harmonic/Hosting/RtmpServer.cs ================================================ using Autofac; using Harmonic.Networking.Rtmp; using Harmonic.Service; using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Fleck; using Harmonic.Networking.WebSocket; namespace Harmonic.Hosting { public class RtmpServer { private readonly Socket _listener; private ManualResetEvent _allDone = new ManualResetEvent(false); private readonly RtmpServerOptions _options; private WebSocketServer _webSocketServer = null; private WebSocketOptions _webSocketOptions = null; public bool Started { get; private set; } = false; internal RtmpServer(RtmpServerOptions options, WebSocketOptions webSocketOptions) { _options = options; _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.NoDelay = true; _listener.Bind(options.RtmpEndPoint); _listener.Listen(128); _webSocketOptions = webSocketOptions; if (webSocketOptions != null) { _webSocketServer = new WebSocketServer($"{(options.Cert == null ? "ws" : "wss")}://{webSocketOptions.BindEndPoint.ToString()}"); } } public Task StartAsync(CancellationToken ct = default) { if (Started) { throw new InvalidOperationException("already started"); } _webSocketServer?.Start(c => { var session = new WebSocketSession(c, _webSocketOptions); c.OnOpen = session.HandleOpen; c.OnClose = session.HandleClose; c.OnMessage = session.HandleMessage; }); if (_webSocketServer != null) { CancellationTokenRegistration reg = default; reg = ct.Register(() => { reg.Dispose(); _webSocketServer.Dispose(); _webSocketServer = new WebSocketServer(_webSocketOptions.BindEndPoint.ToString()); }); } Started = true; var ret = new TaskCompletionSource(); var t = new Thread(o => { try { while (!ct.IsCancellationRequested) { try { _allDone.Reset(); _listener.BeginAccept(new AsyncCallback(ar => { AcceptCallback(ar, ct); }), _listener); while (!_allDone.WaitOne(1)) { ct.ThrowIfCancellationRequested(); } } catch (OperationCanceledException) { throw; } catch (Exception e) { Console.WriteLine(e.ToString()); } } } catch (OperationCanceledException) { } finally { ret.SetResult(1); } }); t.Start(); return ret.Task; } private async void AcceptCallback(IAsyncResult ar, CancellationToken ct) { Socket listener = (Socket)ar.AsyncState; Socket client = listener.EndAccept(ar); client.NoDelay = true; // Signal the main thread to continue. _allDone.Set(); IOPipeLine pipe = null; try { pipe = new IOPipeLine(client, _options); await pipe.StartAsync(ct); } catch (TimeoutException) { client.Close(); } catch (Exception e) { Console.WriteLine("{0} Message: {1}", e.GetType().ToString(), e.Message); Console.WriteLine(e.StackTrace); client.Close(); } finally { pipe?.Dispose(); } } } } ================================================ FILE: Harmonic/Hosting/RtmpServerBuilder.cs ================================================ using Harmonic.Controllers; using Harmonic.Controllers.Living; using Harmonic.Controllers.Record; using Harmonic.Networking.Rtmp; using System; using System.Reflection; using System.Security.Cryptography.X509Certificates; namespace Harmonic.Hosting { public class RtmpServerBuilder { private IStartup _startup = null; private X509Certificate2 _cert = null; private bool _useWebSocket = false; private bool _useSsl = false; private WebSocketOptions _websocketOptions = null; private RtmpServerOptions _options = null; public RtmpServerBuilder UseStartup() where T: IStartup, new() { _startup = new T(); return this; } public RtmpServerBuilder UseSsl(X509Certificate2 cert) { _useSsl = true; _cert = cert; return this; } public RtmpServerBuilder UseWebSocket(Action conf) { _useWebSocket = true; _websocketOptions = new WebSocketOptions(); conf(_websocketOptions); return this; } public RtmpServerBuilder UseHarmonic(Action config) { _options = new RtmpServerOptions(); config(_options); return this; } public RtmpServer Build() { _options = _options ?? new RtmpServerOptions(); _options.Startup = _startup; var types = Assembly.GetCallingAssembly().GetTypes(); var registerInternalControllers = true; _websocketOptions._serverOptions = _options; foreach (var type in types) { var neverRegister = type.GetCustomAttribute(); if (neverRegister != null) { continue; } if (typeof(NetStream).IsAssignableFrom(type) && !type.IsAbstract) { _options.RegisterStream(type); } else if (typeof(RtmpController).IsAssignableFrom(type) && !type.IsAbstract) { _options.RegisterController(type); } if (typeof(LivingController).IsAssignableFrom(type)) { registerInternalControllers = false; } if (_useWebSocket) { if (typeof(WebSocketController).IsAssignableFrom(type) && !type.IsAbstract) { _websocketOptions.RegisterController(type); } if (typeof(WebSocketPlayController).IsAssignableFrom(type)) { registerInternalControllers = false; } } } if (registerInternalControllers) { _options.RegisterController(); _options.RegisterStream(); _options.RegisterStream(); _options.RegisterController(); if (_useWebSocket) { _websocketOptions.RegisterController(); } } if (_useSsl) { _options.Cert = _cert; } _options.CleanupRpcRegistration(); _options.BuildContainer(); var ret = new RtmpServer(_options, _websocketOptions); return ret; } } } ================================================ FILE: Harmonic/Hosting/RtmpServerOptions.cs ================================================ using Autofac; using Harmonic.Controllers; using Harmonic.Controllers.Living; using Harmonic.Networking.Rtmp; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Messages.UserControlMessages; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Rpc; using Harmonic.Service; using System; using System.Collections.Generic; using System.Net; using System.Reflection; using System.Text; using System.Security.Cryptography.X509Certificates; namespace Harmonic.Hosting { public class RtmpServerOptions { internal Dictionary _messageFactories = new Dictionary(); public IReadOnlyDictionary MessageFactories => _messageFactories; public delegate Message MessageFactory(MessageHeader header, Networking.Rtmp.Serialization.SerializationContext context, out int consumed); private Dictionary _registeredControllers = new Dictionary(); internal ContainerBuilder _builder = null; private RpcService _rpcService = null; internal IStartup _startup = null; internal IStartup Startup { get { return _startup; } set { _startup = value; _builder = new ContainerBuilder(); _startup.ConfigureServices(_builder); RegisterCommonServices(_builder); } } internal IContainer ServiceContainer { get; private set; } internal ILifetimeScope ServerLifetime { get; private set; } internal IReadOnlyDictionary RegisteredControllers => _registeredControllers; internal IPEndPoint RtmpEndPoint { get; set; } = new IPEndPoint(IPAddress.Any, 1935); internal bool UseUdp { get; set; } = true; internal bool UseWebsocket { get; set; } = true; internal X509Certificate2 Cert { get; set; } internal RtmpServerOptions() { var userControlMessageFactory = new UserControlMessageFactory(); var commandMessageFactory = new CommandMessageFactory(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage((MessageHeader header, SerializationContext context, out int consumed) => { consumed = 0; return new DataMessage(header.MessageType == MessageType.Amf0Data ? AmfEncodingVersion.Amf0 : AmfEncodingVersion.Amf3); }); RegisterMessage(); RegisterMessage(); RegisterMessage(userControlMessageFactory.Provide); RegisterMessage(commandMessageFactory.Provide); _rpcService = new RpcService(); } internal void BuildContainer() { ServiceContainer = _builder.Build(); ServerLifetime = ServiceContainer.BeginLifetimeScope(); } public void RegisterMessage(MessageFactory factory) where T : Message { var tType = typeof(T); var attr = tType.GetCustomAttribute(); if (attr == null) { throw new InvalidOperationException(); } foreach (var messageType in attr.MessageTypes) { _messageFactories.Add(messageType, factory); } } public void RegisterMessage() where T : Message, new() { var tType = typeof(T); var attr = tType.GetCustomAttribute(); if (attr == null) { throw new InvalidOperationException(); } foreach (var messageType in attr.MessageTypes) { _messageFactories.Add(messageType, (MessageHeader a, SerializationContext b, out int c) => { c = 0; return new T(); }); } } internal void RegisterController(Type controllerType, string appName = null) { if (!typeof(RtmpController).IsAssignableFrom(controllerType)) { throw new InvalidOperationException("controllerType must inherit from AbstractController"); } var name = appName ?? controllerType.Name.Replace("Controller", ""); _registeredControllers.Add(name.ToLower(), controllerType); _rpcService.RegeisterController(controllerType); _builder.RegisterType(controllerType).AsSelf(); } internal void RegisterStream(Type streamType) { if (!typeof(NetStream).IsAssignableFrom(streamType)) { throw new InvalidOperationException("streamType must inherit from NetStream"); } _rpcService.RegeisterController(streamType); _builder.RegisterType(streamType).AsSelf(); } internal void CleanupRpcRegistration() { _rpcService.CleanupRegistration(); } private void RegisterCommonServices(ContainerBuilder builder) { builder.Register(c => new RecordServiceConfiguration()) .AsSelf(); builder.Register(c => new RecordService(c.Resolve())) .AsSelf() .InstancePerLifetimeScope(); builder.Register(c => new PublisherSessionService()) .AsSelf() .InstancePerLifetimeScope(); builder.Register(c => _rpcService) .AsSelf() .SingleInstance(); } internal void RegisterController(string appName = null) where T : RtmpController { RegisterController(typeof(T), appName); } internal void RegisterStream() where T : NetStream { RegisterStream(typeof(T)); } } } ================================================ FILE: Harmonic/Hosting/WebSocketOptions.cs ================================================ using Autofac; using Harmonic.Controllers; using System; using System.Collections.Generic; using System.Net; using System.Text; using System.Text.RegularExpressions; namespace Harmonic.Hosting { public class WebSocketOptions { public IPEndPoint BindEndPoint { get; set; } public Regex UrlMapping { get; set; } = new Regex(@"/(?[a-zA-Z0-9]+)/(?[a-zA-Z0-9\.]+)", RegexOptions.IgnoreCase); internal Dictionary _controllers = new Dictionary(); internal RtmpServerOptions _serverOptions = null; internal void RegisterController() where T: WebSocketController { RegisterController(typeof(T)); } internal void RegisterController(Type controllerType) { if (!typeof(WebSocketController).IsAssignableFrom(controllerType)) { throw new ArgumentException("controller not inherit from WebSocketController"); } _controllers.Add(controllerType.Name.Replace("Controller", "").ToLower(), controllerType); _serverOptions._builder.RegisterType(controllerType).AsSelf(); } } } ================================================ FILE: Harmonic/Networking/Amf/Common/Amf3Object.cs ================================================ using Harmonic.Networking.Amf.Data; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; namespace Harmonic.Networking.Amf.Common { public class AmfObject : IDynamicObject, IEnumerable { private Dictionary _fields = new Dictionary(); private Dictionary _dynamicFields = new Dictionary(); public bool IsAnonymous { get => GetType() == typeof(AmfObject); } public bool IsDynamic { get => _dynamicFields.Any(); } public IReadOnlyDictionary DynamicFields { get => _dynamicFields; } public IReadOnlyDictionary Fields { get => _fields; } public AmfObject() { } public AmfObject(Dictionary values) { _fields = values; } public void Add(string memberName, object member) { _fields.Add(memberName, member); } public void AddDynamic(string memberName, object member) { _dynamicFields.Add(memberName, member); } public IEnumerator GetEnumerator() { return ((IEnumerable)Fields).GetEnumerator(); } } } ================================================ FILE: Harmonic/Networking/Amf/Common/TypeRegisterState.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Common { class TypeRegisterState { public Type Type { get; set; } public Dictionary> Members { get; set; } } } ================================================ FILE: Harmonic/Networking/Amf/Common/Undefined.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Common { public class Undefined { } } ================================================ FILE: Harmonic/Networking/Amf/Common/Unsupported.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Common { public class Unsupported { } } ================================================ FILE: Harmonic/Networking/Amf/Data/IDynamicObject.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Data { public interface IDynamicObject { IReadOnlyDictionary DynamicFields { get; } void AddDynamic(string key, object data); } } ================================================ FILE: Harmonic/Networking/Amf/Data/IExternalizable.cs ================================================ using Harmonic.Buffers; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Data { public interface IExternalizable { bool TryDecodeData(Span buffer, out int consumed); bool TryEncodeData(ByteBuffer buffer); } } ================================================ FILE: Harmonic/Networking/Amf/Data/Message.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Data { public class Message { public string TargetUri { get; set; } public string ResponseUri { get; set; } public object Content { get; set; } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf0/Amf0CommonValues.cs ================================================ using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml; namespace Harmonic.Networking.Amf.Serialization.Amf0 { public static class Amf0CommonValues { public static readonly int TIMEZONE_LENGTH = 2; public static readonly int MARKER_LENGTH = 1; public static readonly int STRING_HEADER_LENGTH = sizeof(ushort); public static readonly int LONG_STRING_HEADER_LENGTH = sizeof(uint); } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf0/Amf0Reader.cs ================================================ using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Data; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Amf.Serialization.Attributes; using Harmonic.Networking.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Xml; namespace Harmonic.Networking.Amf.Serialization.Amf0 { public class Amf0Reader { public readonly IReadOnlyDictionary TypeLengthMap = new Dictionary() { { Amf0Type.Number, 8 }, { Amf0Type.Boolean, sizeof(byte) }, { Amf0Type.String, Amf0CommonValues.STRING_HEADER_LENGTH }, { Amf0Type.LongString, Amf0CommonValues.LONG_STRING_HEADER_LENGTH }, { Amf0Type.Object, /* object marker*/ Amf0CommonValues.MARKER_LENGTH - /* utf8-empty */Amf0CommonValues.STRING_HEADER_LENGTH - /* object end marker */Amf0CommonValues.MARKER_LENGTH }, { Amf0Type.Null, 0 }, { Amf0Type.Undefined, 0 }, { Amf0Type.Reference, sizeof(ushort) }, { Amf0Type.EcmaArray, sizeof(uint) }, { Amf0Type.StrictArray, sizeof(uint) }, { Amf0Type.Date, 10 }, { Amf0Type.Unsupported, 0 }, { Amf0Type.XmlDocument, 0 }, { Amf0Type.TypedObject, /* object marker*/ Amf0CommonValues.MARKER_LENGTH - /* class name */ Amf0CommonValues.STRING_HEADER_LENGTH - /* at least on character for class name */ 1 - /* utf8-empty */Amf0CommonValues.STRING_HEADER_LENGTH - /* object end marker */Amf0CommonValues.MARKER_LENGTH }, { Amf0Type.AvmPlusObject, 0 }, { Amf0Type.ObjectEnd, 0 } }; private delegate bool ReadDataHandler(Span buffer, out T data, out int consumedLength); private delegate bool ReadDataHandler(Span buffer, out object data, out int consumedLength); private List _registeredTypes = new List(); public IReadOnlyList RegisteredTypes { get; } private IReadOnlyDictionary _readDataHandlers; private Dictionary _registeredTypeStates = new Dictionary(); private List _referenceTable = new List(); private Amf3.Amf3Reader _amf3Reader = new Amf3.Amf3Reader(); public bool StrictMode { get; set; } = true; public Amf0Reader() { var readDataHandlers = new Dictionary { [Amf0Type.Number] = OutValueTypeEraser(TryGetNumber), [Amf0Type.Boolean] = OutValueTypeEraser(TryGetBoolean), [Amf0Type.String] = OutValueTypeEraser(TryGetString), [Amf0Type.Object] = OutValueTypeEraser(TryGetObject), [Amf0Type.Null] = OutValueTypeEraser(TryGetNull), [Amf0Type.Undefined] = OutValueTypeEraser(TryGetUndefined), [Amf0Type.Reference] = OutValueTypeEraser(TryGetReference), [Amf0Type.EcmaArray] = OutValueTypeEraser>(TryGetEcmaArray), [Amf0Type.StrictArray] = OutValueTypeEraser>(TryGetStrictArray), [Amf0Type.Date] = OutValueTypeEraser(TryGetDate), [Amf0Type.LongString] = OutValueTypeEraser(TryGetLongString), [Amf0Type.Unsupported] = OutValueTypeEraser(TryGetUnsupported), [Amf0Type.XmlDocument] = OutValueTypeEraser(TryGetXmlDocument), [Amf0Type.TypedObject] = OutValueTypeEraser(TryGetTypedObject), [Amf0Type.AvmPlusObject] = OutValueTypeEraser(TryGetAvmPlusObject) }; _readDataHandlers = readDataHandlers; } public void ResetReference() { _referenceTable.Clear(); } public void RegisterType() where T : new() { var type = typeof(T); var props = type.GetProperties(); var fields = props.Where(p => p.CanWrite && Attribute.GetCustomAttribute(p, typeof(ClassFieldAttribute)) != null).ToList(); var members = fields.ToDictionary(p => ((ClassFieldAttribute)Attribute.GetCustomAttribute(p, typeof(ClassFieldAttribute))).Name ?? p.Name, p => new Action(p.SetValue)); if (members.Keys.Where(s => string.IsNullOrEmpty(s)).Any()) { throw new InvalidOperationException("Field name cannot be empty or null"); } string mapedName = null; var attr = type.GetCustomAttribute(); if (attr != null) { mapedName = attr.Name; } var typeName = mapedName == null ? type.Name : mapedName; var state = new TypeRegisterState() { Members = members, Type = type }; _registeredTypes.Add(type); _registeredTypeStates.Add(typeName, state); _amf3Reader.RegisterTypedObject(typeName, state); } public void RegisterIExternalizableForAvmPlus() where T : IExternalizable, new() { _amf3Reader.RegisterExternalizable(); } private ReadDataHandler OutValueTypeEraser(ReadDataHandler handler) { return (Span b, out object d, out int c) => { var ret = handler(b, out var n, out c); d = n; return ret; }; } private bool TryReadHeader(Span buffer, out KeyValuePair header, out int consumed) { header = default; consumed = 0; if (!TryGetStringImpl(buffer, Amf0.Amf0CommonValues.STRING_HEADER_LENGTH, out var headerName, out var nameConsumed)) { return false; } buffer = buffer.Slice(nameConsumed); if (buffer.Length < 1) { return false; } var mustUnderstand = buffer[0]; buffer = buffer.Slice(1); if (buffer.Length < sizeof(uint)) { return false; } buffer = buffer.Slice(sizeof(uint)); if (!TryGetValue(buffer, out _, out var headerValue, out var valueConsumed)) { return false; } header = new KeyValuePair(headerName, headerValue); consumed = nameConsumed + 1 + sizeof(uint) + valueConsumed; return true; } public bool TryGetMessage(Span buffer, out Message message, out int consumed) { message = default; consumed = default; if (!TryGetStringImpl(buffer, Amf0CommonValues.STRING_HEADER_LENGTH, out var targetUri, out var targetUriConsumed)) { return false; } buffer = buffer.Slice(targetUriConsumed); if (!TryGetStringImpl(buffer, Amf0CommonValues.STRING_HEADER_LENGTH, out var responseUri, out var responseUriConsumed)) { return false; } buffer = buffer.Slice(responseUriConsumed); if (buffer.Length < sizeof(uint)) { return false; } var messageLength = NetworkBitConverter.ToUInt32(buffer); if (messageLength >= 0 && buffer.Length < messageLength) { return false; } if (messageLength == 0 && StrictMode) { return true; } buffer = buffer.Slice(sizeof(uint)); if (!TryGetValue(buffer, out _, out var content, out var contentConsumed)) { return false; } consumed = targetUriConsumed + responseUriConsumed + sizeof(uint) + contentConsumed; message = new Message() { TargetUri = targetUri, ResponseUri = responseUri, Content = content }; return true; } public bool TryGetPacket(Span buffer, out List> headers, out List messages, out int consumed) { headers = default; messages = default; consumed = 0; if (buffer.Length < 1) { return false; } var version = NetworkBitConverter.ToUInt16(buffer); buffer = buffer.Slice(sizeof(ushort)); consumed += sizeof(ushort); var headerCount = NetworkBitConverter.ToUInt16(buffer); buffer = buffer.Slice(sizeof(ushort)); consumed += sizeof(ushort); headers = new List>(); messages = new List(); for (int i = 0; i < headerCount; i++) { if (!TryReadHeader(buffer, out var header, out var headerConsumed)) { return false; } headers.Add(header); buffer = buffer.Slice(headerConsumed); consumed += headerConsumed; } var messageCount = NetworkBitConverter.ToUInt16(buffer); buffer = buffer.Slice(sizeof(ushort)); consumed += sizeof(ushort); for (int i = 0; i < messageCount; i++) { if (!TryGetMessage(buffer, out var message, out var messageConsumed)) { return false; } messages.Add(message); consumed += messageConsumed; } return true; } public bool TryDescribeData(Span buffer, out Amf0Type type, out int consumedLength) { type = default; consumedLength = default; if (buffer.Length < Amf0CommonValues.MARKER_LENGTH) { return false; } var marker = (Amf0Type)buffer[0]; if (!TypeLengthMap.TryGetValue(marker, out var bytesNeed)) { return false; } if (buffer.Length - Amf0CommonValues.MARKER_LENGTH < bytesNeed) { return false; } type = marker; consumedLength = (int)bytesNeed + Amf0CommonValues.MARKER_LENGTH; return true; } public bool TryGetNumber(Span buffer, out double value, out int bytesConsumed) { value = default; bytesConsumed = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Number) { return false; } value = NetworkBitConverter.ToDouble(buffer.Slice(Amf0CommonValues.MARKER_LENGTH)); bytesConsumed = length; return true; } public bool TryGetBoolean(Span buffer, out bool value, out int bytesConsumed) { value = default; bytesConsumed = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Boolean) { return false; } value = buffer[1] != 0; bytesConsumed = length; return true; } public bool TryGetString(Span buffer, out string value, out int bytesConsumed) { value = default; bytesConsumed = default; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (type != Amf0Type.String) { return false; } if (!TryGetStringImpl(buffer.Slice(Amf0CommonValues.MARKER_LENGTH), Amf0CommonValues.STRING_HEADER_LENGTH, out value, out bytesConsumed)) { return false; } bytesConsumed += Amf0CommonValues.MARKER_LENGTH; _referenceTable.Add(value); return true; } private bool TryGetObjectImpl(Span objectBuffer, out Dictionary value, out int bytesConsumed) { value = default; bytesConsumed = default; var obj = new Dictionary(); _referenceTable.Add(obj); var consumed = 0; while (true) { if (!TryGetStringImpl(objectBuffer, Amf0CommonValues.STRING_HEADER_LENGTH, out var key, out var keyLength)) { return false; } consumed += keyLength; objectBuffer = objectBuffer.Slice(keyLength); if (!TryGetValue(objectBuffer, out var dataType, out var data, out var valueLength)) { return false; } consumed += valueLength; objectBuffer = objectBuffer.Slice(valueLength); if (!key.Any() && dataType == Amf0Type.ObjectEnd) { break; } obj.Add(key, data); } value = obj; bytesConsumed = consumed; return true; } public bool TryGetObject(Span buffer, out AmfObject value, out int bytesConsumed) { value = default; bytesConsumed = default; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (type == Amf0Type.Null) { if (!TryGetNull(buffer, out _, out bytesConsumed)) { return false; } value = null; return true; } if (type != Amf0Type.Object) { return false; } var objectBuffer = buffer.Slice(Amf0CommonValues.MARKER_LENGTH); if (!TryGetObjectImpl(objectBuffer, out var obj, out var consumed)) { return false; } value = new AmfObject(obj); bytesConsumed = consumed + Amf0CommonValues.MARKER_LENGTH; return true; } public bool TryGetNull(Span buffer, out object value, out int bytesConsumed) { value = default; bytesConsumed = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Null) { return false; } value = null; bytesConsumed = Amf0CommonValues.MARKER_LENGTH; return true; } public bool TryGetUndefined(Span buffer, out Undefined value, out int consumedLength) { value = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Undefined) { return false; } value = new Undefined(); consumedLength = Amf0CommonValues.MARKER_LENGTH; return true; } private bool TryGetReference(Span buffer, out object value, out int consumedLength) { var index = 0; value = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Reference) { return false; } index = NetworkBitConverter.ToUInt16(buffer.Slice(Amf0CommonValues.MARKER_LENGTH, sizeof(ushort))); consumedLength = Amf0CommonValues.MARKER_LENGTH + sizeof(ushort); if (_referenceTable.Count <= index) { return false; } value = _referenceTable[index]; return true; } private bool TryGetKeyValuePair(Span buffer, out KeyValuePair value, out bool kvEnd, out int consumed) { value = default; kvEnd = default; consumed = 0; if (!TryGetStringImpl(buffer, Amf0CommonValues.STRING_HEADER_LENGTH, out var key, out var keyLength)) { return false; } consumed += keyLength; if (buffer.Length - keyLength < 0) { return false; } buffer = buffer.Slice(keyLength); if (!TryGetValue(buffer, out var elementType, out var element, out var valueLength)) { return false; } consumed += valueLength; value = new KeyValuePair(key, element); kvEnd = !key.Any() && elementType == Amf0Type.ObjectEnd; return true; } public bool TryGetEcmaArray(Span buffer, out Dictionary value, out int consumedLength) { value = default; consumedLength = default; int consumed = 0; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (type != Amf0Type.EcmaArray) { return false; } var obj = new Dictionary(); _referenceTable.Add(obj); var elementCount = NetworkBitConverter.ToUInt32(buffer.Slice(Amf0CommonValues.MARKER_LENGTH, sizeof(uint))); var arrayBodyBuffer = buffer.Slice(Amf0CommonValues.MARKER_LENGTH + sizeof(uint)); consumed = Amf0CommonValues.MARKER_LENGTH + sizeof(uint); if (StrictMode) { for (int i = 0; i < elementCount; i++) { if (!TryGetKeyValuePair(arrayBodyBuffer, out var kv, out _, out var kvConsumed)) { return false; } arrayBodyBuffer = arrayBodyBuffer.Slice(kvConsumed); consumed += kvConsumed; obj.Add(kv.Key, kv.Value); } if (!TryGetStringImpl(arrayBodyBuffer, Amf0CommonValues.STRING_HEADER_LENGTH, out var emptyStr, out var emptyStrConsumed)) { return false; } if (emptyStr.Any()) { return false; } consumed += emptyStrConsumed; arrayBodyBuffer = arrayBodyBuffer.Slice(emptyStrConsumed); if (!TryDescribeData(arrayBodyBuffer, out var objEndType, out var objEndConsumed)) { return false; } if (objEndType != Amf0Type.ObjectEnd) { return false; } consumed += objEndConsumed; } else { while (true) { if (!TryGetKeyValuePair(arrayBodyBuffer, out var kv, out var isEnd, out var kvConsumed)) { return false; } arrayBodyBuffer = arrayBodyBuffer.Slice(kvConsumed); consumed += kvConsumed; if (isEnd) { break; } obj.Add(kv.Key, kv.Value); } } value = obj; consumedLength = consumed; return true; } public bool TryGetStrictArray(Span buffer, out List array, out int consumedLength) { array = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (type != Amf0Type.StrictArray) { return false; } var obj = new List(); _referenceTable.Add(obj); var elementCount = NetworkBitConverter.ToUInt32(buffer.Slice(Amf0CommonValues.MARKER_LENGTH, sizeof(uint))); int consumed = Amf0CommonValues.MARKER_LENGTH + sizeof(uint); var arrayBodyBuffer = buffer.Slice(consumed); var elementBodyBuffer = arrayBodyBuffer; System.Diagnostics.Debug.WriteLine(elementCount); for (uint i = 0; i < elementCount; i++) { if (!TryGetValue(elementBodyBuffer, out _, out var element, out var bufferConsumed)) { return false; } obj.Add(element); if (elementBodyBuffer.Length - bufferConsumed < 0) { return false; } elementBodyBuffer = elementBodyBuffer.Slice(bufferConsumed); consumed += bufferConsumed; } array = obj; consumedLength = consumed; return true; } public bool TryGetDate(Span buffer, out DateTime value, out int consumendLength) { value = default; consumendLength = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Date) { return false; } var timestamp = NetworkBitConverter.ToDouble(buffer.Slice(Amf0CommonValues.MARKER_LENGTH)); value = DateTimeOffset.FromUnixTimeMilliseconds((long)timestamp).LocalDateTime; consumendLength = length; return true; } public bool TryGetLongString(Span buffer, out string value, out int consumedLength) { value = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (type != Amf0Type.LongString) { return false; } if (!TryGetStringImpl(buffer.Slice(Amf0CommonValues.MARKER_LENGTH), Amf0CommonValues.LONG_STRING_HEADER_LENGTH, out value, out consumedLength)) { return false; } consumedLength += Amf0CommonValues.MARKER_LENGTH; return true; } internal bool TryGetStringImpl(Span buffer, int lengthOfLengthField, out string value, out int consumedLength) { value = default; consumedLength = default; var stringLength = 0; if (lengthOfLengthField == Amf0CommonValues.STRING_HEADER_LENGTH) { stringLength = (int)NetworkBitConverter.ToUInt16(buffer); } else { stringLength = (int)NetworkBitConverter.ToUInt32(buffer); } if (buffer.Length - lengthOfLengthField < stringLength) { return false; } value = Encoding.UTF8.GetString(buffer.Slice(lengthOfLengthField, stringLength)); consumedLength = lengthOfLengthField + stringLength; return true; } private bool TryGetUnsupported(Span buffer, out Unsupported value, out int consumedLength) { value = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out var length)) { return false; } if (type != Amf0Type.Unsupported) { return false; } value = new Unsupported(); consumedLength = Amf0CommonValues.MARKER_LENGTH; return true; } public bool TryGetXmlDocument(Span buffer, out XmlDocument value, out int consumedLength) { value = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (!TryGetStringImpl(buffer.Slice(Amf0CommonValues.MARKER_LENGTH), Amf0CommonValues.LONG_STRING_HEADER_LENGTH, out var str, out consumedLength)) { return false; } value = new XmlDocument(); value.LoadXml(str); consumedLength += Amf0CommonValues.MARKER_LENGTH; return true; } public bool TryGetTypedObject(Span buffer, out object value, out int consumedLength) { value = default; consumedLength = default; if (!TryDescribeData(buffer, out var type, out _)) { return true; } if (type != Amf0Type.TypedObject) { return false; } var consumed = Amf0CommonValues.MARKER_LENGTH; if (!TryGetStringImpl(buffer.Slice(Amf0CommonValues.MARKER_LENGTH), Amf0CommonValues.STRING_HEADER_LENGTH, out var className, out var stringLength)) { return false; } consumed += stringLength; var objectBuffer = buffer.Slice(consumed); if (!TryGetObjectImpl(objectBuffer, out var dict, out var objectConsumed)) { return false; } consumed += objectConsumed; if (!_registeredTypeStates.TryGetValue(className, out var state)) { return false; } var objectType = state.Type; var obj = Activator.CreateInstance(objectType); if (state.Members.Keys.Except(dict.Keys).Any()) { return false; } else if (dict.Keys.Except(state.Members.Keys).Any()) { return false; } foreach ((var name, var setter) in state.Members) { setter(obj, dict[name]); } value = obj; consumedLength = consumed; return true; } public bool TryGetAvmPlusObject(Span buffer, out object value, out int consumed) { value = default; consumed = default; if (!TryDescribeData(buffer, out var type, out _)) { return false; } if (type != Amf0Type.AvmPlusObject) { return false; } buffer = buffer.Slice(Amf0CommonValues.MARKER_LENGTH); if (!_amf3Reader.TryGetValue(buffer, out value, out consumed)) { return false; } consumed += Amf0CommonValues.MARKER_LENGTH; return true; } public bool TryGetValue(Span objectBuffer, out Amf0Type objectType, out object data, out int valueLength) { data = default; valueLength = default; objectType = default; if (!TryDescribeData(objectBuffer, out var type, out var length)) { return false; } if (type == Amf0Type.ObjectEnd) { objectType = type; valueLength = Amf0CommonValues.MARKER_LENGTH; return true; } if (!_readDataHandlers.TryGetValue(type, out var handler)) { return false; } if (!handler(objectBuffer, out data, out valueLength)) { return false; } objectType = type; return true; } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf0/Amf0Type.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf0 { public enum Amf0Type { Number, Boolean, String, Object, Moveclip, Null, Undefined, Reference, EcmaArray, ObjectEnd, StrictArray, Date, LongString, Unsupported, Recordset, XmlDocument, TypedObject, AvmPlusObject } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf0/Amf0Writer.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Serialization.Attributes; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.IO; using System.IO.Pipelines; using System.Linq; using System.Reflection; using System.Text; using System.Xml; namespace Harmonic.Networking.Amf.Serialization.Amf0 { public class Amf0Writer { private delegate void GetBytesHandler(T value, SerializationContext context); private delegate void GetBytesHandler(object value, SerializationContext context); private IReadOnlyDictionary _getBytesHandlers = null; private ArrayPool _arrayPool = ArrayPool.Shared; public Amf0Writer() { var getBytesHandlers = new Dictionary(); getBytesHandlers[typeof(double)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(int)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(short)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(long)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(uint)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(ushort)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(ulong)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(float)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(DateTime)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(string)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(XmlDocument)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(Unsupported)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(Undefined)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(bool)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(object)] = GetBytesWrapper(WriteTypedBytes); getBytesHandlers[typeof(AmfObject)] = GetBytesWrapper(WriteBytes); getBytesHandlers[typeof(Dictionary)] = GetBytesWrapper>(WriteBytes); getBytesHandlers[typeof(List)] = GetBytesWrapper>(WriteBytes); _getBytesHandlers = getBytesHandlers; } private GetBytesHandler GetBytesWrapper(GetBytesHandler handler) { return (object v, SerializationContext context) => { if (v is T tv) { handler(tv, context); } else { handler((T)Convert.ChangeType(v, typeof(T)), context); } }; } public void WriteAvmPlusBytes(SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf0Type.AvmPlusObject); } private void WriteStringBytesImpl(string str, SerializationContext context, out bool isLongString, bool marker = false, bool forceLongString = false) { var bytesNeed = 0; var headerLength = 0; var bodyLength = 0; bodyLength = Encoding.UTF8.GetByteCount(str); bytesNeed += bodyLength; if (bodyLength > ushort.MaxValue || forceLongString) { headerLength = Amf0CommonValues.LONG_STRING_HEADER_LENGTH; isLongString = true; if (marker) { context.Buffer.WriteToBuffer((byte)Amf0Type.LongString); } } else { isLongString = false; headerLength = Amf0CommonValues.STRING_HEADER_LENGTH; if (marker) { context.Buffer.WriteToBuffer((byte)Amf0Type.String); } } bytesNeed += headerLength; var bufferBackend = _arrayPool.Rent(bytesNeed); try { var buffer = bufferBackend.AsSpan(0, bytesNeed); if (isLongString) { NetworkBitConverter.TryGetBytes((uint)bodyLength, buffer); } else { var contractRet = NetworkBitConverter.TryGetBytes((ushort)bodyLength, buffer); Contract.Assert(contractRet); } Encoding.UTF8.GetBytes(str, buffer.Slice(headerLength)); context.Buffer.WriteToBuffer(buffer); } finally { _arrayPool.Return(bufferBackend); } } public void WriteBytes(string str, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH; var refIndex = context.ReferenceTable.IndexOf(str); if (refIndex != -1) { WriteReferenceIndexBytes((ushort)refIndex, context); return; } WriteStringBytesImpl(str, context, out var isLongString, true); context.ReferenceTable.Add(str); } public void WriteBytes(double val, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH + sizeof(double); var bufferBackend = _arrayPool.Rent(bytesNeed); try { var buffer = bufferBackend.AsSpan(0, bytesNeed); buffer[0] = (byte)Amf0Type.Number; var contractRet = NetworkBitConverter.TryGetBytes(val, buffer.Slice(Amf0CommonValues.MARKER_LENGTH)); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(buffer); } finally { _arrayPool.Return(bufferBackend); } } public void WriteBytes(bool val, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH + sizeof(byte); context.Buffer.WriteToBuffer((byte)Amf0Type.Boolean); context.Buffer.WriteToBuffer((byte)(val ? 1 : 0)); } public void WriteBytes(Undefined value, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH; var bufferBackend = _arrayPool.Rent(bytesNeed); context.Buffer.WriteToBuffer((byte)Amf0Type.Undefined); } public void WriteBytes(Unsupported value, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH; context.Buffer.WriteToBuffer((byte)Amf0Type.Unsupported); } private void WriteReferenceIndexBytes(ushort index, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH + sizeof(ushort); var backend = _arrayPool.Rent(bytesNeed); try { var buffer = backend.AsSpan(0, bytesNeed); buffer[0] = (byte)Amf0Type.Reference; var contractRet = NetworkBitConverter.TryGetBytes(index, buffer.Slice(Amf0CommonValues.MARKER_LENGTH)); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(buffer); } finally { _arrayPool.Return(backend); } } private void WriteObjectEndBytes(SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH; context.Buffer.WriteToBuffer((byte)Amf0Type.ObjectEnd); } public void WriteBytes(DateTime dateTime, SerializationContext context) { var bytesNeed = Amf0CommonValues.MARKER_LENGTH + sizeof(double) + sizeof(short); var backend = _arrayPool.Rent(bytesNeed); try { var buffer = backend.AsSpan(0, bytesNeed); buffer.Slice(0, bytesNeed).Clear(); buffer[0] = (byte)Amf0Type.Date; var dof = new DateTimeOffset(dateTime); var timestamp = (double)dof.ToUnixTimeMilliseconds(); var contractRet = NetworkBitConverter.TryGetBytes(timestamp, buffer.Slice(Amf0CommonValues.MARKER_LENGTH)); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(buffer); } finally { _arrayPool.Return(backend); } } public void WriteBytes(XmlDocument xml, SerializationContext context) { string content = null; using (var stringWriter = new StringWriter()) using (var xmlTextWriter = XmlWriter.Create(stringWriter)) { xml.WriteTo(xmlTextWriter); xmlTextWriter.Flush(); content = stringWriter.GetStringBuilder().ToString(); } context.Buffer.WriteToBuffer((byte)Amf0Type.XmlDocument); WriteStringBytesImpl(content, context, out _, forceLongString: true); } public void WriteNullBytes(SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf0Type.Null); } public void WriteValueBytes(object value, SerializationContext context) { var valueType = value != null ? value.GetType() : typeof(object); if (!_getBytesHandlers.TryGetValue(valueType, out var handler)) { throw new InvalidOperationException(); } handler(value, context); } // strict array public void WriteBytes(List value, SerializationContext context) { if (value == null) { WriteNullBytes(context); return; } var bytesNeed = Amf0CommonValues.MARKER_LENGTH + sizeof(uint); var refIndex = context.ReferenceTable.IndexOf(value); if (refIndex >= 0) { WriteReferenceIndexBytes((ushort)refIndex, context); return; } context.ReferenceTable.Add(value); context.Buffer.WriteToBuffer((byte)Amf0Type.StrictArray); var countBuffer = _arrayPool.Rent(sizeof(uint)); try { var contractRet = NetworkBitConverter.TryGetBytes((uint)value.Count, countBuffer); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(countBuffer.AsSpan(0, sizeof(uint))); } finally { _arrayPool.Return(countBuffer); } foreach (var data in value) { WriteValueBytes(data, context); } } // ecma array public void WriteBytes(Dictionary value, SerializationContext context) { if (value == null) { WriteNullBytes(context); return; } var refIndex = context.ReferenceTable.IndexOf(value); if (refIndex >= 0) { WriteReferenceIndexBytes((ushort)refIndex, context); return; } context.Buffer.WriteToBuffer((byte)Amf0Type.EcmaArray); context.ReferenceTable.Add(value); var countBuffer = _arrayPool.Rent(sizeof(uint)); try { var contractRet = NetworkBitConverter.TryGetBytes((uint)value.Count, countBuffer); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(countBuffer.AsSpan(0, sizeof(uint))); } finally { _arrayPool.Return(countBuffer); } foreach ((var key, var data) in value) { WriteStringBytesImpl(key, context, out _); WriteValueBytes(data, context); } WriteStringBytesImpl("", context, out _); WriteObjectEndBytes(context); } public void WriteTypedBytes(object value, SerializationContext context) { if (value == null) { WriteNullBytes(context); return; } var refIndex = context.ReferenceTable.IndexOf(value); if (refIndex >= 0) { WriteReferenceIndexBytes((ushort)refIndex, context); return; } context.Buffer.WriteToBuffer((byte)Amf0Type.TypedObject); context.ReferenceTable.Add(value); var valueType = value.GetType(); var className = valueType.Name; var clsAttr = (TypedObjectAttribute)Attribute.GetCustomAttribute(valueType, typeof(TypedObjectAttribute)); if (clsAttr != null && clsAttr.Name != null) { className = clsAttr.Name; } WriteStringBytesImpl(className, context, out _); var props = valueType.GetProperties(); foreach (var prop in props) { var attr = (ClassFieldAttribute)Attribute.GetCustomAttribute(prop, typeof(ClassFieldAttribute)); if (attr != null) { WriteStringBytesImpl(attr.Name ?? prop.Name, context, out _); WriteValueBytes(prop.GetValue(value), context); } } WriteStringBytesImpl("", context, out _); WriteObjectEndBytes(context); } public void WriteBytes(AmfObject value, SerializationContext context) { if (value == null) { WriteNullBytes(context); return; } var refIndex = context.ReferenceTable.IndexOf(value); if (refIndex >= 0) { WriteReferenceIndexBytes((ushort)refIndex, context); return; } context.Buffer.WriteToBuffer((byte)Amf0Type.Object); context.ReferenceTable.Add(value); foreach (var field in value.Fields) { WriteStringBytesImpl(field.Key, context, out _); WriteValueBytes(field.Value, context); } foreach (var field in value.DynamicFields) { WriteStringBytesImpl(field.Key, context, out _); WriteValueBytes(field.Value, context); } WriteStringBytesImpl("", context, out _); WriteObjectEndBytes(context); } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf0/SerializationContext.cs ================================================ using Harmonic.Buffers; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf0 { public class SerializationContext : IDisposable { public ByteBuffer Buffer { get; private set; } public List ReferenceTable { get; set; } = new List(); public int MessageLength => Buffer.Length; private bool _disposeBuffer = true; public SerializationContext() { Buffer = new ByteBuffer(); } public SerializationContext(ByteBuffer buffer) { Buffer = buffer; _disposeBuffer = false; } public void GetMessage(Span buffer) { ReferenceTable.Clear(); Buffer.TakeOutMemory(buffer); } public void Dispose() { if (_disposeBuffer) { ((IDisposable)Buffer).Dispose(); } } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3Array.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class Amf3Array { public Dictionary SparsePart { get; set; } = new Dictionary(); public List DensePart { get; set; } = new List(); public object this[string key] { get { return SparsePart[key]; } set { SparsePart[key] = value; } } public object this[int index] { get { return DensePart[index]; } set { DensePart[index] = value; } } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3ClassTraits.cs ================================================ using System; using System.Linq; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public enum Amf3ClassType { Anonymous, Typed, Externalizable } public class Amf3ClassTraits : IEquatable { public bool IsDynamic { get; set; } = false; public Amf3ClassType ClassType { get; set; } public string ClassName { get; set; } public List Members { get; set; } = new List(); public override bool Equals(object obj) { if (obj is Amf3ClassTraits traits) { Equals(traits); } return base.Equals(obj); } public bool Equals(Amf3ClassTraits traits) { return traits.ClassType == ClassType && traits.ClassName == ClassName && traits.Members.SequenceEqual(Members); } public override int GetHashCode() { var hash = new HashCode(); hash.Add(ClassType); hash.Add(ClassName); foreach (var member in Members) { hash.Add(member); } return hash.ToHashCode(); } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3CommonValues.cs ================================================ using System; using System.Linq; using System.Buffers; using System.Collections.Generic; using System.Text; using System.Xml; using System.Dynamic; using System.IO; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public static class Amf3CommonValues { public static readonly int MARKER_LENGTH = 1; } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3Dictionary.cs ================================================ using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class Amf3Dictionary : Dictionary { public bool WeakKeys { get; set; } = false; public Amf3Dictionary() : base() { } public Amf3Dictionary(IDictionary dictionary) : base(dictionary) { } public Amf3Dictionary(IEnumerable> collection) : base(collection) { } public Amf3Dictionary(IEqualityComparer comparer) : base(comparer) { } public Amf3Dictionary(int capacity) : base(capacity) { } public Amf3Dictionary(IDictionary dictionary, IEqualityComparer comparer) : base(dictionary, comparer) { } public Amf3Dictionary(IEnumerable> collection, IEqualityComparer comparer) : base(collection, comparer) { } public Amf3Dictionary(int capacity, IEqualityComparer comparer) : base(capacity, comparer) { } protected Amf3Dictionary(SerializationInfo info, StreamingContext context) : base(info, context) { } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3Reader.cs ================================================ using Harmonic.Networking.Amf.Common; using System; using System.Linq; using System.Collections.Generic; using System.Text; using System.Xml; using Harmonic.Networking.Utils; using System.Buffers; using Harmonic.Networking.Amf.Data; using System.Reflection; using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Serialization.Attributes; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class Amf3Reader { private delegate bool ReaderHandler(Span buffer, out T value, out int consumed); private delegate bool ReaderHandler(Span buffer, out object value, out int consumed); private List _objectReferenceTable = new List(); private List _stringReferenceTable = new List(); private List _objectTraitsReferenceTable = new List(); private Dictionary _readerHandlers = new Dictionary(); private Dictionary _registeredTypedObejectStates = new Dictionary(); private List _registeredTypes = new List(); private Dictionary _registeredExternalizable = new Dictionary(); private readonly IReadOnlyList _supportedTypes = null; private MemoryPool _memoryPool = MemoryPool.Shared; public IReadOnlyList RegisteredTypes { get => _registeredTypes; } public Amf3Reader() { var supportedTypes = new List() { Amf3Type.Undefined , Amf3Type.Null , Amf3Type.False , Amf3Type.True, Amf3Type.Integer , Amf3Type.Double , Amf3Type.String , Amf3Type.Xml , Amf3Type.XmlDocument , Amf3Type.Date , Amf3Type.Array , Amf3Type.Object , Amf3Type.ByteArray , Amf3Type.VectorObject , Amf3Type.VectorDouble , Amf3Type.VectorInt , Amf3Type.VectorUInt , Amf3Type.Dictionary }; _supportedTypes = supportedTypes; var readerHandlers = new Dictionary { [Amf3Type.Undefined] = ReaderHandlerWrapper(TryGetUndefined), [Amf3Type.Null] = ReaderHandlerWrapper(TryGetNull), [Amf3Type.True] = ReaderHandlerWrapper(TryGetTrue), [Amf3Type.False] = ReaderHandlerWrapper(TryGetFalse), [Amf3Type.Double] = ReaderHandlerWrapper(TryGetDouble), [Amf3Type.Integer] = ReaderHandlerWrapper(TryGetUInt29), [Amf3Type.String] = ReaderHandlerWrapper(TryGetString), [Amf3Type.Xml] = ReaderHandlerWrapper(TryGetXml), [Amf3Type.XmlDocument] = ReaderHandlerWrapper(TryGetXmlDocument), [Amf3Type.Date] = ReaderHandlerWrapper(TryGetDate), [Amf3Type.ByteArray] = ReaderHandlerWrapper(TryGetByteArray), [Amf3Type.VectorDouble] = ReaderHandlerWrapper>(TryGetVectorDouble), [Amf3Type.VectorInt] = ReaderHandlerWrapper>(TryGetVectorInt), [Amf3Type.VectorUInt] = ReaderHandlerWrapper>(TryGetVectorUint), [Amf3Type.VectorObject] = ReaderHandlerWrapper(TryGetVectorObject), [Amf3Type.Array] = ReaderHandlerWrapper(TryGetArray), [Amf3Type.Object] = ReaderHandlerWrapper(TryGetObject), [Amf3Type.Dictionary] = ReaderHandlerWrapper>(TryGetDictionary) }; _readerHandlers = readerHandlers; } public void ResetReference() { _objectReferenceTable.Clear(); _objectTraitsReferenceTable.Clear(); _stringReferenceTable.Clear(); } private ReaderHandler ReaderHandlerWrapper(ReaderHandler handler) { return (Span b, out object value, out int consumed) => { value = default; consumed = default; if (handler(b, out var data, out consumed)) { value = data; return true; } return false; }; } internal void RegisterTypedObject(string mappedName, TypeRegisterState state) { _registeredTypedObejectStates.Add(mappedName, state); } public void RegisterTypedObject() where T: new() { var type = typeof(T); var props = type.GetProperties(); var fields = props.Where(p => p.CanWrite && Attribute.GetCustomAttribute(p, typeof(ClassFieldAttribute)) != null).ToList(); var members = fields.ToDictionary(p => ((ClassFieldAttribute)Attribute.GetCustomAttribute(p, typeof(ClassFieldAttribute))).Name ?? p.Name, p => new Action(p.SetValue)); if (members.Keys.Where(s => string.IsNullOrEmpty(s)).Any()) { throw new InvalidOperationException("Field name cannot be empty or null"); } string mapedName = null; var attr = type.GetCustomAttribute(); if (attr != null) { mapedName = attr.Name; } var typeName = mapedName == null ? type.Name : mapedName; var state = new TypeRegisterState() { Members = members, Type = type }; _registeredTypes.Add(type); _registeredTypedObejectStates.Add(typeName, state); } public void RegisterExternalizable() where T : IExternalizable, new() { var type = typeof(T); string mapedName = null; var attr = type.GetCustomAttribute(); if (attr != null) { mapedName = attr.Name; } var typeName = mapedName == null ? type.Name : mapedName; _registeredExternalizable.Add(typeName, type); } public bool TryDescribeData(Span buffer, out Amf3Type type) { type = default; if (buffer.Length < Amf3CommonValues.MARKER_LENGTH) { return false; } var typeMark = (Amf3Type)buffer[0]; if (!_supportedTypes.Contains(typeMark)) { return false; } type = typeMark; return true; } public bool DataIsType(Span buffer, Amf3Type type) { if (!TryDescribeData(buffer, out var dataType)) { return false; } return dataType == type; } public bool TryGetUndefined(Span buffer, out Undefined value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Undefined)) { return false; } value = new Undefined(); consumed = Amf3CommonValues.MARKER_LENGTH; return true; } public bool TryGetNull(Span buffer, out object value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Null)) { return false; } value = null; consumed = Amf3CommonValues.MARKER_LENGTH; return true; } public bool TryGetTrue(Span buffer, out bool value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.True)) { return false; } value = true; consumed = Amf3CommonValues.MARKER_LENGTH; return true; } public bool TryGetBoolean(Span buffer, out bool value, out int consumed) { if (DataIsType(buffer, Amf3Type.True)) { consumed = Amf3CommonValues.MARKER_LENGTH; value = true; return true; } else if (DataIsType(buffer, Amf3Type.False)) { consumed = Amf3CommonValues.MARKER_LENGTH; value = false; return true; } value = default; consumed = default; return false; } public bool TryGetFalse(Span buffer, out bool value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.False)) { return false; } value = false; consumed = Amf3CommonValues.MARKER_LENGTH; return true; } public bool TryGetUInt29(Span buffer, out uint value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Integer)) { return false; } var dataBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); if (!TryGetU29Impl(dataBuffer, out value, out var dataConsumed)) { return false; } consumed = Amf3CommonValues.MARKER_LENGTH + dataConsumed; return true; } private bool TryGetU29Impl(Span dataBuffer, out uint value, out int consumed) { value = default; consumed = default; var bytesNeed = 0; for (int i = 0; i < 4; i++) { bytesNeed++; if (dataBuffer.Length < bytesNeed) { return false; } var hasBytes = ((dataBuffer[i] >> 7 & 0x01) == 0x01); if (!hasBytes) { break; } } switch (bytesNeed) { case 3: case 4: dataBuffer[2] = (byte)(0x7F & dataBuffer[2]); dataBuffer[1] = (byte)(0x7F & dataBuffer[1]); dataBuffer[0] = (byte)(0x7F & dataBuffer[0]); dataBuffer[2] = (byte)(dataBuffer[1] << 7 | dataBuffer[2]); dataBuffer[1] = (byte)(dataBuffer[0] << 6 | (dataBuffer[1] >> 1)); dataBuffer[0] = (byte)(dataBuffer[0] >> 2); break; case 2: dataBuffer[1] = (byte)(0x7F & dataBuffer[1]); dataBuffer[1] = (byte)(dataBuffer[0] << 7 | dataBuffer[1]); dataBuffer[0] = (byte)(0x7F & dataBuffer[0]); dataBuffer[0] = (byte)(dataBuffer[0] >> 1); break; } using (var mem = _memoryPool.Rent(sizeof(uint))) { var buffer = mem.Memory.Span; buffer.Clear(); dataBuffer.Slice(0, bytesNeed).CopyTo(buffer.Slice(sizeof(uint) - bytesNeed)); value = NetworkBitConverter.ToUInt32(buffer); consumed = bytesNeed; return true; } } public bool TryGetDouble(Span buffer, out double value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Double)) { return false; } value = NetworkBitConverter.ToDouble(buffer.Slice(Amf3CommonValues.MARKER_LENGTH)); consumed = Amf3CommonValues.MARKER_LENGTH + sizeof(double); return true; } public bool TryGetString(Span buffer, out string value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.String)) { return false; } var objectBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); TryGetStringImpl(objectBuffer, _stringReferenceTable, out var str, out var strConsumed); value = str; consumed = Amf3CommonValues.MARKER_LENGTH + strConsumed; return true; } private bool TryGetStringImpl(Span objectBuffer, List referenceTable, out string value, out int consumed) where T : class { value = default; consumed = default; if (!TryGetU29Impl(objectBuffer, out var header, out int headerLen)) { return false; } if (!TryGetReference(header, _stringReferenceTable, out var headerData, out string refValue, out var isRef)) { return false; } if (isRef) { value = refValue; consumed = headerLen; return true; } var strLen = (int)headerData; if (objectBuffer.Length - headerLen < strLen) { return false; } value = Encoding.UTF8.GetString(objectBuffer.Slice(headerLen, strLen)); consumed = headerLen + strLen; if (value.Any()) { referenceTable.Add(value as T); } return true; } public bool TryGetXmlDocument(Span buffer, out XmlDocument value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.XmlDocument)) { return false; } var objectBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); TryGetStringImpl(objectBuffer, _objectReferenceTable, out var str, out var strConsumed); var xml = new XmlDocument(); xml.LoadXml(str); value = xml; consumed = Amf3CommonValues.MARKER_LENGTH + strConsumed; return true; } public bool TryGetDate(Span buffer, out DateTime value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Date)) { return false; } var objectBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); if (!TryGetU29Impl(objectBuffer, out var header, out var headerLength)) { return false; } if (!TryGetReference(header, _objectReferenceTable, out var headerData, out DateTime refValue, out var isRef)) { return false; } if (isRef) { value = refValue; consumed = Amf3CommonValues.MARKER_LENGTH + headerLength; return true; } var timestamp = NetworkBitConverter.ToDouble(objectBuffer.Slice(headerLength)); value = DateTimeOffset.FromUnixTimeMilliseconds((long)(timestamp)).LocalDateTime; consumed = Amf3CommonValues.MARKER_LENGTH + headerLength + sizeof(double); _objectReferenceTable.Add(value); return true; } public bool TryGetArray(Span buffer, out Amf3Array value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Array)) { return false; } if (!TryGetU29Impl(buffer.Slice(Amf3CommonValues.MARKER_LENGTH), out var header, out var headerConsumed)) { return false; } if (!TryGetReference(header, _objectReferenceTable, out var headerData, out Amf3Array refValue, out var isRef)) { return false; } if (isRef) { value = refValue; consumed = Amf3CommonValues.MARKER_LENGTH + headerConsumed; return true; } var arrayConsumed = 0; var arrayBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH + headerConsumed); var denseItemCount = (int)headerData; if (!TryGetStringImpl(arrayBuffer, _stringReferenceTable, out var key, out var keyConsumed)) { return false; } var array = new Amf3Array(); _objectReferenceTable.Add(array); if (key.Any()) { do { arrayBuffer = arrayBuffer.Slice(keyConsumed); arrayConsumed += keyConsumed; if (!TryGetValue(arrayBuffer, out var item, out var itemConsumed)) { return false; } arrayConsumed += itemConsumed; arrayBuffer = arrayBuffer.Slice(itemConsumed); array.SparsePart.Add(key, item); if (!TryGetStringImpl(arrayBuffer, _stringReferenceTable, out key, out keyConsumed)) { return false; } } while (key.Any()); } arrayConsumed += keyConsumed; arrayBuffer = arrayBuffer.Slice(keyConsumed); for (int i = 0; i < denseItemCount; i++) { if (!TryGetValue(arrayBuffer, out var item, out var itemConsumed)) { return false; } array.DensePart.Add(item); arrayConsumed += itemConsumed; arrayBuffer = arrayBuffer.Slice(itemConsumed); } value = array; consumed = Amf3CommonValues.MARKER_LENGTH + headerConsumed + arrayConsumed; return true; } public bool TryGetObject(Span buffer, out object value, out int consumed) { value = default; consumed = 0; if (!DataIsType(buffer, Amf3Type.Object)) { return false; } consumed = Amf3CommonValues.MARKER_LENGTH; if (!TryGetU29Impl(buffer.Slice(Amf3CommonValues.MARKER_LENGTH), out var header, out var headerLength)) { return false; } consumed += headerLength; if (!TryGetReference(header, _objectReferenceTable, out var headerData, out object refValue, out var isRef)) { return false; } if (isRef) { value = refValue; return true; } Amf3ClassTraits traits = null; if ((header & 0x02) == 0x00) { var referenceIndex = (int)((header >> 2) & 0x3FFFFFFF); if (_objectTraitsReferenceTable.Count <= referenceIndex) { return false; } if (_objectTraitsReferenceTable[referenceIndex] is Amf3ClassTraits obj) { traits = obj; } else { return false; } } else { traits = new Amf3ClassTraits(); var dataBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH + headerLength); if ((header & 0x04) == 0x04) { traits.ClassType = Amf3ClassType.Externalizable; if (!TryGetStringImpl(dataBuffer, _stringReferenceTable, out var extClassName, out int extClassNameConsumed)) { return false; } consumed += extClassNameConsumed; traits.ClassName = extClassName; var externailzableBuffer = dataBuffer.Slice(extClassNameConsumed); if (!_registeredExternalizable.TryGetValue(extClassName, out var extType)) { return false; } var extObj = Activator.CreateInstance(extType) as IExternalizable; if (!extObj.TryDecodeData(externailzableBuffer, out var extConsumed)) { return false; } value = extObj; consumed = Amf3CommonValues.MARKER_LENGTH + headerLength + extClassNameConsumed + extConsumed; return true; } if (!TryGetStringImpl(dataBuffer, _stringReferenceTable, out var className, out int classNameConsumed)) { return false; } dataBuffer = dataBuffer.Slice(classNameConsumed); consumed += classNameConsumed; if (className.Any()) { traits.ClassType = Amf3ClassType.Typed; traits.ClassName = className; } else { traits.ClassType = Amf3ClassType.Anonymous; } if ((header & 0x08) == 0x08) { traits.IsDynamic = true; } var memberCount = header >> 4; for (int i = 0; i < memberCount; i++) { if (!TryGetStringImpl(dataBuffer, _stringReferenceTable, out var key, out var keyConsumed)) { return false; } traits.Members.Add(key); dataBuffer = dataBuffer.Slice(keyConsumed); consumed += keyConsumed; } _objectTraitsReferenceTable.Add(traits); } object deserailziedObject = null; var valueBuffer = buffer.Slice(consumed); if (traits.ClassType == Amf3ClassType.Typed) { if (!_registeredTypedObejectStates.TryGetValue(traits.ClassName, out var state)) { return false; } var classType = state.Type; if (!traits.Members.OrderBy(m => m).SequenceEqual(state.Members.Keys.OrderBy(p => p))) { return false; } deserailziedObject = Activator.CreateInstance(classType); _objectReferenceTable.Add(deserailziedObject); foreach (var member in traits.Members) { if (!TryGetValue(valueBuffer, out var data, out var dataConsumed)) { return false; } valueBuffer = valueBuffer.Slice(dataConsumed); consumed += dataConsumed; state.Members[member](deserailziedObject, data); } } else { var obj = new AmfObject(); _objectReferenceTable.Add(obj); foreach (var member in traits.Members) { if (!TryGetValue(valueBuffer, out var data, out var dataConsumed)) { return false; } valueBuffer = valueBuffer.Slice(dataConsumed); consumed += dataConsumed; obj.Add(member, data); } deserailziedObject = obj; } if (traits.IsDynamic) { var dynamicObject = deserailziedObject as IDynamicObject; if (dynamicObject == null) { return false; } if (!TryGetStringImpl(valueBuffer, _stringReferenceTable, out var key, out var keyConsumed)) { return false; } consumed += keyConsumed; valueBuffer = valueBuffer.Slice(keyConsumed); while (key.Any()) { if (!TryGetValue(valueBuffer, out var data, out var dataConsumed)) { return false; } valueBuffer = valueBuffer.Slice(dataConsumed); consumed += dataConsumed; dynamicObject.AddDynamic(key, data); if (!TryGetStringImpl(valueBuffer, _stringReferenceTable, out key, out keyConsumed)) { return false; } valueBuffer = valueBuffer.Slice(keyConsumed); consumed += keyConsumed; } } value = deserailziedObject; return true; } public bool TryGetXml(Span buffer, out Amf3Xml value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Xml)) { return false; } var objectBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); TryGetStringImpl(objectBuffer, _objectReferenceTable, out var str, out var strConsumed); var xml = new Amf3Xml(); xml.LoadXml(str); value = xml; consumed = Amf3CommonValues.MARKER_LENGTH + strConsumed; return true; } public bool TryGetByteArray(Span buffer, out byte[] value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.ByteArray)) { return false; } var objectBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); if (!TryGetU29Impl(objectBuffer, out var header, out int headerLen)) { return false; } if (!TryGetReference(header, _objectReferenceTable, out var headerData, out byte[] refValue, out var isRef)) { return false; } if (isRef) { value = refValue; consumed = Amf3CommonValues.MARKER_LENGTH + headerLen; return true; } var arrayLen = (int)headerData; if (objectBuffer.Length - headerLen < arrayLen) { return false; } value = new byte[arrayLen]; objectBuffer.Slice(headerLen, arrayLen).CopyTo(value); _objectReferenceTable.Add(value); consumed = Amf3CommonValues.MARKER_LENGTH + headerLen + arrayLen; return true; } public bool TryGetValue(Span buffer, out object value, out int consumed) { value = default; consumed = default; if (!TryDescribeData(buffer, out var type)) { return false; } if (!_readerHandlers.TryGetValue(type, out var handler)) { return false; } if (!handler(buffer, out value, out consumed)) { return false; } return true; } public bool TryGetVectorInt(Span buffer, out Vector value, out int consumed) { value = default; consumed = Amf3CommonValues.MARKER_LENGTH; if (!DataIsType(buffer, Amf3Type.VectorInt)) { return false; } buffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); if (!ReadVectorHeader(ref buffer, ref value, ref consumed, out var itemCount, out var isFixedSize, out var isRef)) { return false; } if (isRef) { return true; } var vector = new Vector { IsFixedSize = isFixedSize }; _objectReferenceTable.Add(vector); for (int i = 0; i < itemCount; i++) { if (!TryGetIntVectorData(ref buffer, vector, ref consumed)) { return false; } } value = vector; return true; } public bool TryGetVectorUint(Span buffer, out Vector value, out int consumed) { value = default; consumed = Amf3CommonValues.MARKER_LENGTH; if (!DataIsType(buffer, Amf3Type.VectorUInt)) { return false; } buffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); if (!ReadVectorHeader(ref buffer, ref value, ref consumed, out var itemCount, out var isFixedSize, out var isRef)) { return false; } if (isRef) { return true; } var vector = new Vector { IsFixedSize = isFixedSize }; _objectReferenceTable.Add(vector); for (int i = 0; i < itemCount; i++) { if (!TryGetUIntVectorData(ref buffer, vector, ref consumed)) { return false; } } value = vector; return true; } public bool TryGetVectorDouble(Span buffer, out Vector value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.VectorDouble)) { return false; } buffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); if (!ReadVectorHeader(ref buffer, ref value, ref consumed, out var itemCount, out var isFixedSize, out var isRef)) { return false; } if (isRef) { return true; } var vector = new Vector() { IsFixedSize = isFixedSize }; _objectReferenceTable.Add(vector); for (int i = 0; i < itemCount; i++) { if (!TryGetDoubleVectorData(ref buffer, vector, ref consumed)) { return false; } } value = vector; return true; } private bool TryGetIntVectorData(ref Span buffer, Vector vector, ref int consumed) { var value = NetworkBitConverter.ToInt32(buffer); vector.Add(value); consumed += sizeof(int); buffer = buffer.Slice(sizeof(int)); return true; } private bool TryGetUIntVectorData(ref Span buffer, Vector vector, ref int consumed) { var value = NetworkBitConverter.ToUInt32(buffer); vector.Add(value); consumed += sizeof(uint); buffer = buffer.Slice(sizeof(uint)); return true; } private bool TryGetDoubleVectorData(ref Span buffer, Vector vector, ref int consumed) { var value = NetworkBitConverter.ToDouble(buffer); vector.Add(value); consumed += sizeof(double); buffer = buffer.Slice(sizeof(double)); return true; } private bool TryGetReference(uint header, List referenceTable, out uint headerData, out T value, out bool isRef) { isRef = default; value = default; headerData = header >> 1; if ((header & 0x01) == 0x00) { var referenceIndex = (int)headerData; if (referenceTable.Count <= referenceIndex) { return false; } if (referenceTable[referenceIndex] is T refObject) { value = refObject; isRef = true; return true; } else { return false; } } isRef = false; return true; } private bool ReadVectorHeader(ref Span buffer, ref T value, ref int consumed, out int itemCount, out bool isFixedSize, out bool isRef) { isFixedSize = default; itemCount = default; isRef = default; if (!TryGetU29Impl(buffer, out var header, out var headerLength)) { return false; } if (!TryGetReference(header, _objectReferenceTable, out var headerData, out T refValue, out isRef)) { return false; } if (isRef) { value = refValue; consumed = Amf3CommonValues.MARKER_LENGTH + headerLength; return true; } itemCount = (int)headerData; var objectBuffer = buffer.Slice(headerLength); if (objectBuffer.Length < sizeof(byte)) { return false; } isFixedSize = objectBuffer[0] == 0x01; buffer = objectBuffer.Slice(sizeof(byte)); consumed = Amf3CommonValues.MARKER_LENGTH + headerLength + sizeof(byte); return true; } private bool ReadVectorTypeName(ref Span typeNameBuffer, out string typeName, out int typeNameConsumed) { typeName = default; typeNameConsumed = default; if (!TryGetStringImpl(typeNameBuffer, _stringReferenceTable, out typeName, out typeNameConsumed)) { return false; } typeNameBuffer = typeNameBuffer.Slice(typeNameConsumed); return true; } public bool TryGetVectorObject(Span buffer, out object value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.VectorObject)) { return false; } buffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH); int arrayConsumed = 0; if (!ReadVectorHeader(ref buffer, ref value, ref arrayConsumed, out var itemCount, out var isFixedSize, out var isRef)) { return false; } if (isRef) { consumed = arrayConsumed; return true; } if (!ReadVectorTypeName(ref buffer, out var typeName, out var typeNameConsumed)) { return false; } var arrayBodyBuffer = buffer; object resultVector = null; Type elementType = null; Action addAction = null; if (typeName == "*") { elementType = typeof(object); var v = new Vector(); _objectReferenceTable.Add(v); v.IsFixedSize = isFixedSize; resultVector = v; addAction = v.Add; } else { if (!_registeredTypedObejectStates.TryGetValue(typeName, out var state)) { return false; } elementType = state.Type; var vectorType = typeof(Vector<>).MakeGenericType(elementType); resultVector = Activator.CreateInstance(vectorType); _objectReferenceTable.Add(resultVector); vectorType.GetProperty("IsFixedSize").SetValue(resultVector, isFixedSize); var addMethod = vectorType.GetMethod("Add"); addAction = o => addMethod.Invoke(resultVector, new object[] { o }); } for (int i = 0; i < itemCount; i++) { if (!TryGetValue(arrayBodyBuffer, out var item, out var itemConsumed)) { return false; } addAction(item); arrayBodyBuffer = arrayBodyBuffer.Slice(itemConsumed); arrayConsumed += itemConsumed; } value = resultVector; consumed = typeNameConsumed + arrayConsumed; return true; } public bool TryGetDictionary(Span buffer, out Amf3Dictionary value, out int consumed) { value = default; consumed = default; if (!DataIsType(buffer, Amf3Type.Dictionary)) { return false; } if (!TryGetU29Impl(buffer.Slice(Amf3CommonValues.MARKER_LENGTH), out var header, out var headerLength)) { return false; } if (!TryGetReference(header, _objectReferenceTable, out var headerData, out Amf3Dictionary refValue, out var isRef)) { return false; } if (isRef) { value = refValue; consumed = Amf3CommonValues.MARKER_LENGTH + headerLength; return true; } var itemCount = (int)headerData; var dictConsumed = 0; if (buffer.Length - Amf3CommonValues.MARKER_LENGTH - headerLength < sizeof(byte)) { return false; } var weakKeys = buffer[Amf3CommonValues.MARKER_LENGTH + headerLength] == 0x01; var dictBuffer = buffer.Slice(Amf3CommonValues.MARKER_LENGTH + headerLength + /* weak key flag */ sizeof(byte)); var dict = new Amf3Dictionary() { WeakKeys = weakKeys }; _objectReferenceTable.Add(dict); for (int i = 0; i < itemCount; i++) { if (!TryGetValue(dictBuffer, out var key, out var keyConsumed)) { return false; } dictBuffer = dictBuffer.Slice(keyConsumed); dictConsumed += keyConsumed; if (!TryGetValue(dictBuffer, out var data, out var dataConsumed)) { return false; } dictBuffer = dictBuffer.Slice(dataConsumed); dict.Add(key, data); dictConsumed += dataConsumed; } value = dict; consumed = Amf3CommonValues.MARKER_LENGTH + headerLength + dictConsumed + /* weak key flag */ sizeof(byte); return true; } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3Type.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public enum Amf3Type : byte { Undefined, Null, False, True, Integer, Double, String, XmlDocument, Date, Array, Object, Xml, ByteArray, VectorInt, VectorUInt, VectorDouble, VectorObject, Dictionary } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3Writer.cs ================================================ using Harmonic.Networking.Amf.Common; using System; using System.Linq; using System.Buffers; using System.Collections.Generic; using System.Text; using System.Xml; using System.IO; using Harmonic.Buffers; using Harmonic.Networking.Utils; using Harmonic.Networking.Amf.Data; using System.Diagnostics.Contracts; using System.Reflection; using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Serialization.Attributes; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class Amf3Writer { private delegate void WriteHandler(T value, SerializationContext context); private delegate void WriteHandler(object value, SerializationContext context); private ArrayPool _arrayPool = ArrayPool.Shared; private Dictionary _writeHandlers = new Dictionary(); public static readonly uint U29MAX = 0x1FFFFFFF; private MethodInfo _writeVectorTMethod = null; private MethodInfo _writeDictionaryTMethod = null; public Amf3Writer() { var writeHandlers = new Dictionary { [typeof(int)] = WriteHandlerWrapper(WriteBytes), [typeof(uint)] = WriteHandlerWrapper(WriteBytes), [typeof(long)] = WriteHandlerWrapper(WriteBytes), [typeof(ulong)] = WriteHandlerWrapper(WriteBytes), [typeof(short)] = WriteHandlerWrapper(WriteBytes), [typeof(ushort)] = WriteHandlerWrapper(WriteBytes), [typeof(double)] = WriteHandlerWrapper(WriteBytes), [typeof(Undefined)] = WriteHandlerWrapper(WriteBytes), [typeof(object)] = WriteHandlerWrapper(WriteBytes), [typeof(DateTime)] = WriteHandlerWrapper(WriteBytes), [typeof(XmlDocument)] = WriteHandlerWrapper(WriteBytes), [typeof(Amf3Xml)] = WriteHandlerWrapper(WriteBytes), [typeof(bool)] = WriteHandlerWrapper(WriteBytes), [typeof(Memory)] = WriteHandlerWrapper>(WriteBytes), [typeof(string)] = WriteHandlerWrapper(WriteBytes), [typeof(Vector)] = WriteHandlerWrapper>(WriteBytes), [typeof(Vector)] = WriteHandlerWrapper>(WriteBytes), [typeof(Vector)] = WriteHandlerWrapper>(WriteBytes), [typeof(Vector<>)] = WrapVector, [typeof(Amf3Dictionary<,>)] = WrapDictionary }; _writeHandlers = writeHandlers; Action, SerializationContext> method = WriteBytes; _writeVectorTMethod = method.Method.GetGenericMethodDefinition(); Action, SerializationContext> dictMethod = WriteBytes; _writeDictionaryTMethod = dictMethod.Method.GetGenericMethodDefinition(); } private void WrapVector(object value, SerializationContext context) { var valueType = value.GetType(); var contractRet = valueType.IsGenericType; Contract.Assert(contractRet); var defination = valueType.GetGenericTypeDefinition(); Contract.Assert(defination == typeof(Vector<>)); var vectorT = valueType.GetGenericArguments().First(); _writeVectorTMethod.MakeGenericMethod(vectorT).Invoke(this, new object[] { value, context }); } private void WrapDictionary(object value, SerializationContext context) { var valueType = value.GetType(); var contractRet = valueType.IsGenericType; Contract.Assert(contractRet); var defination = valueType.GetGenericTypeDefinition(); Contract.Assert(defination == typeof(Amf3Dictionary<,>)); var tKey = valueType.GetGenericArguments().First(); var tValue = valueType.GetGenericArguments().Last(); _writeDictionaryTMethod.MakeGenericMethod(tKey, tValue).Invoke(this, new object[] { value, context }); } private WriteHandler WriteHandlerWrapper(WriteHandler handler) { return (object obj, SerializationContext context) => { if (obj is T tObj) { handler(tObj, context); } else { handler((T)Convert.ChangeType(obj, typeof(T)), context); } }; } private string XmlToString(XmlDocument xml) { using (var stringWriter = new StringWriter()) using (var xmlTextWriter = XmlWriter.Create(stringWriter)) { xml.WriteTo(xmlTextWriter); xmlTextWriter.Flush(); return stringWriter.GetStringBuilder().ToString(); } } public void WriteBytes(Undefined value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Undefined); } public void WriteBytes(bool value, SerializationContext context) { if (value) { context.Buffer.WriteToBuffer((byte)Amf3Type.True); } else { context.Buffer.WriteToBuffer((byte)Amf3Type.False); } } private void WriteU29BytesImpl(uint value, SerializationContext context) { var length = 0; if (value <= 0x7F) { length = 1; } else if (value <= 0x3FFF) { length = 2; } else if (value <= 0x1FFFFF) { length = 3; } else if (value <= U29MAX) { length = 4; } else { throw new ArgumentOutOfRangeException(); } var arr = ArrayPool.Shared.Rent(4); try { NetworkBitConverter.TryGetBytes(value, arr); switch (length) { case 4: context.Buffer.WriteToBuffer((byte)(arr[0] << 2 | ((arr[1]) >> 6) | 0x80)); context.Buffer.WriteToBuffer((byte)(arr[1] << 1 | ((arr[2]) >> 7) | 0x80)); context.Buffer.WriteToBuffer((byte)(arr[2] | 0x80)); context.Buffer.WriteToBuffer(arr[3]); break; case 3: context.Buffer.WriteToBuffer((byte)(arr[1] << 2 | ((arr[2]) >> 6) | 0x80)); context.Buffer.WriteToBuffer((byte)(arr[2] << 1 | ((arr[3]) >> 7) | 0x80)); context.Buffer.WriteToBuffer((byte)(arr[3] & 0x7F)); break; case 2: context.Buffer.WriteToBuffer((byte)(arr[2] << 1 | ((arr[3]) >> 7) | 0x80)); context.Buffer.WriteToBuffer((byte)(arr[3] & 0x7F)); break; case 1: context.Buffer.WriteToBuffer((byte)(arr[3])); break; default: throw new ApplicationException(); } } finally { ArrayPool.Shared.Return(arr); } } public void WriteBytes(uint value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Integer); WriteU29BytesImpl(value, context); } public void WriteBytes(double value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Double); var backend = _arrayPool.Rent(sizeof(double)); try { var contractRet = NetworkBitConverter.TryGetBytes(value, backend); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(backend.AsSpan(0, sizeof(double))); } finally { _arrayPool.Return(backend); } } private void WriteStringBytesImpl(string value, SerializationContext context, List referenceTable) { if (value is T tValue) { var refIndex = referenceTable.IndexOf(tValue); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { var byteCount = (uint)Encoding.UTF8.GetByteCount(value); var header = (byteCount << 1) | 0x01; WriteU29BytesImpl(header, context); var backend = _arrayPool.Rent((int)byteCount); try { Encoding.UTF8.GetBytes(value, backend); context.Buffer.WriteToBuffer(backend.AsSpan(0, (int)byteCount)); } finally { _arrayPool.Return(backend); } if (value.Any()) { referenceTable.Add(tValue); } } } else { Contract.Assert(false); } } public void WriteBytes(string value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.String); WriteStringBytesImpl(value, context, context.StringReferenceTable); } public void WriteBytes(XmlDocument xml, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.XmlDocument); var content = XmlToString(xml); WriteStringBytesImpl(content, context, context.ObjectReferenceTable); } public void WriteBytes(DateTime dateTime, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Date); var refIndex = context.ObjectReferenceTable.IndexOf(dateTime); uint header = 0; if (refIndex >= 0) { header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } context.ObjectReferenceTable.Add(dateTime); var timeOffset = new DateTimeOffset(dateTime); var timestamp = timeOffset.ToUnixTimeMilliseconds(); header = 0x01; WriteU29BytesImpl(header, context); var backend = _arrayPool.Rent(sizeof(double)); try { var contractRet = NetworkBitConverter.TryGetBytes(timestamp, backend); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(backend.AsSpan(0, sizeof(double))); } finally { _arrayPool.Return(backend); } } public void WriteBytes(Amf3Xml xml, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Xml); var content = XmlToString(xml); WriteStringBytesImpl(content, context, context.ObjectReferenceTable); } public void WriteBytes(Memory value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.ByteArray); uint header = 0; var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } header = ((uint)value.Length << 1) | 0x01; WriteU29BytesImpl(header, context); context.Buffer.WriteToBuffer(value.Span); context.ObjectReferenceTable.Add(value); } public void WriteValueBytes(object value, SerializationContext context) { if (value == null) { WriteBytes((object)null, context); return; } var valueType = value.GetType(); if (_writeHandlers.TryGetValue(valueType, out var handler)) { handler(value, context); } else { if (valueType.IsGenericType) { var genericDefination = valueType.GetGenericTypeDefinition(); if (genericDefination != typeof(Vector<>) && genericDefination != typeof(Amf3Dictionary<,>)) { throw new NotSupportedException(); } if (_writeHandlers.TryGetValue(genericDefination, out handler)) { handler(value, context); } } else { WriteBytes(value, context); } } } public void WriteBytes(object value, SerializationContext context) { uint header = 0; if (value == null) { context.Buffer.WriteToBuffer((byte)Amf3Type.Null); return; } else { context.Buffer.WriteToBuffer((byte)Amf3Type.Object); } var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } var objType = value.GetType(); string attrTypeName = null; var classAttr = objType.GetCustomAttribute(); if (classAttr != null) { attrTypeName = classAttr.Name; } var traits = new Amf3ClassTraits(); var memberValues = new List(); if (value is AmfObject amf3Object) { if (amf3Object.IsAnonymous) { traits.ClassName = ""; traits.ClassType = Amf3ClassType.Anonymous; } else { traits.ClassName = attrTypeName ?? objType.Name; traits.ClassType = Amf3ClassType.Typed; } traits.IsDynamic = amf3Object.IsDynamic; traits.Members = new List(amf3Object.Fields.Keys); memberValues = new List(amf3Object.Fields.Keys.Select(k => amf3Object.Fields[k])); } else if (value is IExternalizable) { traits.ClassName = attrTypeName ?? objType.Name; traits.ClassType = Amf3ClassType.Externalizable; } else { traits.ClassName = attrTypeName ?? objType.Name; traits.ClassType = Amf3ClassType.Typed; var props = objType.GetProperties(); foreach (var prop in props) { var attr = (ClassFieldAttribute)Attribute.GetCustomAttribute(prop, typeof(ClassFieldAttribute)); if (attr != null) { traits.Members.Add(attr.Name ?? prop.Name); memberValues.Add(prop.GetValue(value)); } } traits.IsDynamic = value is IDynamicObject; } context.ObjectReferenceTable.Add(value); var traitRefIndex = context.ObjectTraitsReferenceTable.IndexOf(traits); if (traitRefIndex >= 0) { header = ((uint)traitRefIndex << 2) | 0x01; WriteU29BytesImpl(header, context); } else { if (traits.ClassType == Amf3ClassType.Externalizable) { header = 0x07; WriteU29BytesImpl(header, context); WriteStringBytesImpl(traits.ClassName, context, context.StringReferenceTable); var extObj = value as IExternalizable; extObj.TryEncodeData(context.Buffer); return; } else { header = 0x03; if (traits.IsDynamic) { header |= 0x08; } var memberCount = (uint)traits.Members.Count; header |= memberCount << 4; WriteU29BytesImpl(header, context); WriteStringBytesImpl(traits.ClassName, context, context.StringReferenceTable); foreach (var memberName in traits.Members) { WriteStringBytesImpl(memberName, context, context.StringReferenceTable); } } context.ObjectTraitsReferenceTable.Add(traits); } foreach (var memberValue in memberValues) { WriteValueBytes(memberValue, context); } if (traits.IsDynamic) { var amf3Obj = value as IDynamicObject; foreach ((var key, var item) in amf3Obj.DynamicFields) { WriteStringBytesImpl(key, context, context.StringReferenceTable); WriteValueBytes(item, context); } WriteStringBytesImpl("", context, context.StringReferenceTable); } } public void WriteBytes(Vector value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.VectorUInt); var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { context.ObjectReferenceTable.Add(value); var header = ((uint)value.Count << 1) | 0x01; WriteU29BytesImpl(header, context); context.Buffer.WriteToBuffer(value.IsFixedSize ? (byte)0x01 : (byte)0x00); var buffer = _arrayPool.Rent(sizeof(uint)); try { foreach (var i in value) { var contractRet = NetworkBitConverter.TryGetBytes(i, buffer); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(buffer.AsSpan(0, sizeof(uint))); } } finally { _arrayPool.Return(buffer); } } } public void WriteBytes(Vector value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.VectorInt); var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { context.ObjectReferenceTable.Add(value); var header = ((uint)value.Count << 1) | 0x01; WriteU29BytesImpl(header, context); context.Buffer.WriteToBuffer(value.IsFixedSize ? (byte)0x01 : (byte)0x00); var buffer = _arrayPool.Rent(sizeof(int)); try { foreach (var i in value) { var contractRet = NetworkBitConverter.TryGetBytes(i, buffer); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(buffer.AsSpan(0, sizeof(int))); } } finally { _arrayPool.Return(buffer); } return; } } public void WriteBytes(Vector value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.VectorDouble); var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { context.ObjectReferenceTable.Add(value); var header = ((uint)value.Count << 1) | 0x01; WriteU29BytesImpl(header, context); context.Buffer.WriteToBuffer(value.IsFixedSize ? (byte)0x01 : (byte)0x00); var buffer = _arrayPool.Rent(sizeof(double)); try { foreach (var i in value) { var contractRet = NetworkBitConverter.TryGetBytes(i, buffer); Contract.Assert(contractRet); context.Buffer.WriteToBuffer(buffer.AsSpan(0, sizeof(double))); } } finally { _arrayPool.Return(buffer); } return; } } public void WriteBytes(Vector value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.VectorObject); var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { context.ObjectReferenceTable.Add(value); var header = ((uint)value.Count << 1) | 0x01; WriteU29BytesImpl(header, context); context.Buffer.WriteToBuffer(value.IsFixedSize ? (byte)0x01 : (byte)0x00); var tType = typeof(T); string typeName = tType.Name; var attr = tType.GetCustomAttribute(); if (attr != null) { typeName = attr.Name; } var className = typeof(T) == typeof(object) ? "*" : typeName; WriteStringBytesImpl(className, context, context.StringReferenceTable); foreach (var i in value) { WriteValueBytes(i, context); } return; } } public void WriteBytes(Amf3Array value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Array); var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { context.ObjectReferenceTable.Add(value); var header = ((uint)value.DensePart.Count << 1) | 0x01; WriteU29BytesImpl(header, context); foreach ((var key, var item) in value.SparsePart) { WriteStringBytesImpl(key, context, context.StringReferenceTable); WriteValueBytes(item, context); } WriteStringBytesImpl("", context, context.StringReferenceTable); foreach (var i in value.DensePart) { WriteValueBytes(i, context); } return; } } public void WriteBytes(Amf3Dictionary value, SerializationContext context) { context.Buffer.WriteToBuffer((byte)Amf3Type.Dictionary); var refIndex = context.ObjectReferenceTable.IndexOf(value); if (refIndex >= 0) { var header = (uint)refIndex << 1; WriteU29BytesImpl(header, context); return; } else { context.ObjectReferenceTable.Add(value); var header = (uint)value.Count << 1 | 0x01; WriteU29BytesImpl(header, context); context.Buffer.WriteToBuffer((byte)(value.WeakKeys ? 0x01 : 0x00)); foreach ((var key, var item) in value) { WriteValueBytes(key, context); WriteValueBytes(item, context); } return; } } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Amf3Xml.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Xml; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class Amf3Xml : XmlDocument { public Amf3Xml() : base() { } public Amf3Xml(XmlNameTable nt) : base(nt) { } protected internal Amf3Xml(XmlImplementation imp) : base(imp) { } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/SerializationContext.cs ================================================ using Harmonic.Buffers; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class SerializationContext : IDisposable { public ByteBuffer Buffer { get; private set; } public List ObjectReferenceTable { get; set; } = new List(); public List StringReferenceTable { get; set; } = new List(); public List ObjectTraitsReferenceTable { get; set; } = new List(); public int MessageLength => Buffer.Length; private bool _disposeBuffer = true; public SerializationContext() { Buffer = new ByteBuffer(); } public SerializationContext(ByteBuffer buffer) { Buffer = buffer; _disposeBuffer = false; } public void Dispose() { if (_disposeBuffer) { ((IDisposable)Buffer).Dispose(); } } public void GetMessage(Span buffer) { ObjectReferenceTable.Clear(); StringReferenceTable.Clear(); ObjectTraitsReferenceTable.Clear(); Buffer.TakeOutMemory(buffer); } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Amf3/Vector.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Linq; namespace Harmonic.Networking.Amf.Serialization.Amf3 { public class Vector : List, IEquatable> { private List _data = new List(); public bool IsFixedSize { get; set; } = false; public new void Add(T item) { if (IsFixedSize) { throw new NotSupportedException(); } ((List)this).Add(item); } public override bool Equals(object obj) { if (obj is Vector en) { return IsFixedSize == en.IsFixedSize && en.SequenceEqual(this); } return base.Equals(obj); } public bool Equals(List other) { return other.SequenceEqual(this); } public override int GetHashCode() { var hash = new HashCode(); foreach (var d in _data) { hash.Add(d); } hash.Add(IsFixedSize); return hash.ToHashCode(); } } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Attributes/ClassFieldAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Serialization.Attributes { [AttributeUsage(AttributeTargets.Property)] public class ClassFieldAttribute : Attribute { public string Name { get; set; } = null; } } ================================================ FILE: Harmonic/Networking/Amf/Serialization/Attributes/TypedObjectAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Amf.Attributes { [AttributeUsage(AttributeTargets.Class)] public class TypedObjectAttribute : Attribute { public string Name { get; set; } = null; } } ================================================ FILE: Harmonic/Networking/ConnectionInformation.cs ================================================ using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Networking { public class ConnectionInformation { public string App { get; set; } public string Flashver { get; set; } public string SwfUrl { get; set; } public string TcUrl { get; set; } public bool Fpad { get; set; } public int AudioCodecs { get; set; } public int VideoCodecs { get; set; } int VideoFunction { get; set; } public string PageUrl { get; set; } public AmfEncodingVersion AmfEncodingVersion { get; set; } } } ================================================ FILE: Harmonic/Networking/Flv/Data/AacPacketType.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum AacPacketType { SequenceHeader, Raw } } ================================================ FILE: Harmonic/Networking/Flv/Data/AudioData.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public class AudioData { public AacPacketType? AacPacketType { get; set; } = null; public ReadOnlyMemory Data { get; set; } } } ================================================ FILE: Harmonic/Networking/Flv/Data/CodecId.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum CodecId { Jpeg = 1, H263, ScreenVideo, Vp6, Vp6WithAlpha, ScreenVideo2, Avc } } ================================================ FILE: Harmonic/Networking/Flv/Data/FlvAudioData.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public class FlvAudioData { public SoundFormat SoundFormat { get; set; } public SoundRate SoundRate { get; set; } public SoundSize SoundSize { get; set; } public SoundType SoundType { get; set; } public AudioData AudioData { get; set; } } } ================================================ FILE: Harmonic/Networking/Flv/Data/FlvVideoData.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public class FlvVideoData { public FrameType FrameType { get; set; } public CodecId CodecId { get; set; } public ReadOnlyMemory VideoData { get; set; } } } ================================================ FILE: Harmonic/Networking/Flv/Data/FrameType.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum FrameType { KeyFrame = 1, InterFrame, DisposableInterFrame, GeneratedKeyFrame, VideoInfoOrCommandFrame } } ================================================ FILE: Harmonic/Networking/Flv/Data/SoundFormat.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum SoundFormat { PcmPE, Adpcm, Mp3, PcmLE, Nellymonser16k, Nellymonser8k, Nellymonser, G711ALawPcm, G711MuLawPcm, Aac = 10, Speex, Mp38k = 14, DeviceSpecificSound } } ================================================ FILE: Harmonic/Networking/Flv/Data/SoundRate.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum SoundRate { Hz5Dot5, Hz11, Hz22, Hz44 } } ================================================ FILE: Harmonic/Networking/Flv/Data/SoundSize.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum SoundSize { Snd8Bit, Snd16Bit } } ================================================ FILE: Harmonic/Networking/Flv/Data/SoundType.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv.Data { public enum SoundType { SndMono, SndStereo } } ================================================ FILE: Harmonic/Networking/Flv/FlvDemuxer.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Flv.Data; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static Harmonic.Hosting.RtmpServerOptions; namespace Harmonic.Networking.Flv { public class FlvDemuxer { private Amf0Reader _amf0Reader = new Amf0Reader(); private Amf3Reader _amf3Reader = new Amf3Reader(); private ArrayPool _arrayPool = ArrayPool.Shared; private Stream _stream = null; private IReadOnlyDictionary _factories = null; public FlvDemuxer(IReadOnlyDictionary factories) { _factories = factories; } public async Task AttachStream(Stream stream, bool disposeOld = false) { if (disposeOld) { _stream?.Dispose(); } var headerBuffer = new byte[9]; await stream.ReadBytesAsync(headerBuffer); _stream = stream; return headerBuffer; } public void SeekNoLock(double milliseconds, Dictionary metaData, CancellationToken ct = default) { if (metaData == null) { return; } var seconds = milliseconds / 1000; var keyframes = metaData["keyframes"] as AmfObject; var times = keyframes.Fields["times"] as List; var idx = times.FindIndex(t => ((double)t) >= seconds); if (idx == -1) { return; } var filePositions = keyframes.Fields["filepositions"] as List; var pos = (double)filePositions[idx]; _stream.Seek((int)(pos - 4), SeekOrigin.Begin); } private async Task ReadHeader(CancellationToken ct = default) { byte[] headerBuffer = null; byte[] timestampBuffer = null; try { headerBuffer = _arrayPool.Rent(15); timestampBuffer = _arrayPool.Rent(4); await _stream.ReadBytesAsync(headerBuffer.AsMemory(0, 15), ct); var type = (MessageType)headerBuffer[4]; var length = NetworkBitConverter.ToUInt24(headerBuffer.AsSpan(5, 3)); headerBuffer.AsSpan(8, 3).CopyTo(timestampBuffer.AsSpan(1)); timestampBuffer[0] = headerBuffer[11]; var timestamp = NetworkBitConverter.ToInt32(timestampBuffer.AsSpan(0, 4)); var streamId = NetworkBitConverter.ToUInt24(headerBuffer.AsSpan(12, 3)); var header = new MessageHeader() { MessageLength = length, MessageStreamId = streamId, MessageType = type, Timestamp = (uint)timestamp }; return header; } finally { if (headerBuffer != null) { _arrayPool.Return(headerBuffer); } if (timestampBuffer != null) { _arrayPool.Return(timestampBuffer); } } } public FlvAudioData DemultiplexAudioData(AudioMessage message) { var head = message.Data.Span[0]; var soundFormat = (SoundFormat)(head >> 4); var soundRate = (SoundRate)((head & 0x0C) >> 2); var soundSize = (SoundSize)(head & 0x02); var soundType = (SoundType)(head & 0x01); var ret = new FlvAudioData(); ret.SoundFormat = soundFormat; ret.SoundRate = soundRate; ret.SoundSize = soundSize; ret.SoundType = soundType; ret.AudioData = new AudioData(); if (soundFormat == SoundFormat.Aac) { ret.AudioData.AacPacketType = (AacPacketType)message.Data.Span[1]; ret.AudioData.Data = message.Data.Slice(2); } ret.AudioData.Data = message.Data.Slice(1); return ret; } public FlvVideoData DemultiplexVideoData(VideoMessage message) { var ret = new FlvVideoData(); var head = message.Data.Span[0]; ret.FrameType = (FrameType)(head >> 4); ret.CodecId = (CodecId)(head & 0x0F); ret.VideoData = message.Data.Slice(1); return ret; } public async Task DemultiplexFlvAsync(CancellationToken ct = default) { byte[] bodyBuffer = null; try { var header = await ReadHeader(ct); bodyBuffer = _arrayPool.Rent((int)header.MessageLength); if (!_factories.TryGetValue(header.MessageType, out var factory)) { throw new InvalidOperationException(); } await _stream.ReadBytesAsync(bodyBuffer.AsMemory(0, (int)header.MessageLength), ct); var context = new Networking.Rtmp.Serialization.SerializationContext() { Amf0Reader = _amf0Reader, Amf3Reader = _amf3Reader, ReadBuffer = bodyBuffer.AsMemory(0, (int)header.MessageLength) }; var message = factory(header, context, out var consumed); context.ReadBuffer = context.ReadBuffer.Slice(consumed); message.MessageHeader = header; message.Deserialize(context); _amf0Reader.ResetReference(); _amf3Reader.ResetReference(); return message; } finally { if (bodyBuffer != null) { _arrayPool.Return(bodyBuffer); } } } } } ================================================ FILE: Harmonic/Networking/Flv/FlvMuxer.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Utils; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Flv { public class FlvMuxer { private Amf0Writer _amf0Writer = new Amf0Writer(); private Amf3Writer _amf3Writer = new Amf3Writer(); public byte[] MultiplexFlvHeader(bool hasAudio, bool hasVideo) { var header = new byte[13]; header[0] = 0x46; header[1] = 0x4C; header[2] = 0x56; header[3] = 0x01; byte audioFlag = 0x01 << 2; byte videoFlag = 0x01; byte typeFlag = 0x00; if (hasAudio) typeFlag |= audioFlag; if (hasVideo) typeFlag |= videoFlag; header[4] = typeFlag; NetworkBitConverter.TryGetBytes(9, header.AsSpan(5)); return header; } public byte[] MultiplexFlv(Message data) { var dataBuffer = new ByteBuffer(); var buffer = new byte[4]; if (data.MessageHeader.MessageLength == 0) { var messageBuffer = new ByteBuffer(); var context = new Networking.Rtmp.Serialization.SerializationContext() { Amf0Writer = _amf0Writer, Amf3Writer = _amf3Writer, WriteBuffer = messageBuffer }; data.Serialize(context); var length = messageBuffer.Length; data.MessageHeader.MessageLength = (uint)length; var bodyBuffer = new byte[length]; messageBuffer.TakeOutMemory(bodyBuffer); dataBuffer.WriteToBuffer((byte)data.MessageHeader.MessageType); NetworkBitConverter.TryGetUInt24Bytes(data.MessageHeader.MessageLength, buffer); dataBuffer.WriteToBuffer(buffer.AsSpan(0, 3)); NetworkBitConverter.TryGetBytes(data.MessageHeader.Timestamp, buffer); dataBuffer.WriteToBuffer(buffer.AsSpan(1, 3)); dataBuffer.WriteToBuffer(buffer.AsSpan(0, 1)); buffer.AsSpan().Clear(); dataBuffer.WriteToBuffer(buffer.AsSpan(0, 3)); dataBuffer.WriteToBuffer(bodyBuffer); } else { dataBuffer.WriteToBuffer((byte)data.MessageHeader.MessageType); NetworkBitConverter.TryGetUInt24Bytes(data.MessageHeader.MessageLength, buffer); dataBuffer.WriteToBuffer(buffer.AsSpan(0, 3)); NetworkBitConverter.TryGetBytes(data.MessageHeader.Timestamp, buffer); dataBuffer.WriteToBuffer(buffer.AsSpan(1, 3)); dataBuffer.WriteToBuffer(buffer.AsSpan(0, 1)); buffer.AsSpan().Clear(); dataBuffer.WriteToBuffer(buffer.AsSpan(0, 3)); var context = new Networking.Rtmp.Serialization.SerializationContext() { Amf0Writer = _amf0Writer, Amf3Writer = _amf3Writer, WriteBuffer = dataBuffer }; data.Serialize(context); } NetworkBitConverter.TryGetBytes((data.MessageHeader.MessageLength + 11), buffer); dataBuffer.WriteToBuffer(buffer); var rawData = new byte[dataBuffer.Length]; dataBuffer.TakeOutMemory(rawData); return rawData; } } } ================================================ FILE: Harmonic/Networking/Rtmp/ChunkStreamContext.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using static Harmonic.Networking.Rtmp.IOPipeLine; namespace Harmonic.Networking.Rtmp { class ChunkStreamContext : IDisposable { private ArrayPool _arrayPool = ArrayPool.Shared; internal ChunkHeader _processingChunk = null; internal int ReadMinimumBufferSize { get => (ReadChunkSize + TYPE0_SIZE) * 4; } internal Dictionary _previousWriteMessageHeader = new Dictionary(); internal Dictionary _previousReadMessageHeader = new Dictionary(); internal Dictionary _incompleteMessageState = new Dictionary(); internal uint? ReadWindowAcknowledgementSize { get; set; } = null; internal uint? WriteWindowAcknowledgementSize { get; set; } = null; internal int ReadChunkSize { get; set; } = 128; internal long ReadUnAcknowledgedSize = 0; internal long WriteUnAcknowledgedSize = 0; internal uint _writeChunkSize = 128; internal readonly int EXTENDED_TIMESTAMP_LENGTH = 4; internal readonly int TYPE0_SIZE = 11; internal readonly int TYPE1_SIZE = 7; internal readonly int TYPE2_SIZE = 3; internal RtmpSession _rtmpSession = null; internal Amf0Reader _amf0Reader = new Amf0Reader(); internal Amf0Writer _amf0Writer = new Amf0Writer(); internal Amf3Reader _amf3Reader = new Amf3Reader(); internal Amf3Writer _amf3Writer = new Amf3Writer(); private IOPipeLine _ioPipeline = null; private SemaphoreSlim _sync = new SemaphoreSlim(1); internal LimitType? PreviousLimitType { get; set; } = null; public ChunkStreamContext(IOPipeLine stream) { _rtmpSession = new RtmpSession(stream); _ioPipeline = stream; _ioPipeline.NextProcessState = ProcessState.FirstByteBasicHeader; _ioPipeline._bufferProcessors.Add(ProcessState.ChunkMessageHeader, ProcessChunkMessageHeader); _ioPipeline._bufferProcessors.Add(ProcessState.CompleteMessage, ProcessCompleteMessage); _ioPipeline._bufferProcessors.Add(ProcessState.ExtendedTimestamp, ProcessExtendedTimestamp); _ioPipeline._bufferProcessors.Add(ProcessState.FirstByteBasicHeader, ProcessFirstByteBasicHeader); } public void Dispose() { ((IDisposable)_rtmpSession).Dispose(); } internal async Task MultiplexMessageAsync(uint chunkStreamId, Message message) { if (!message.MessageHeader.MessageStreamId.HasValue) { throw new InvalidOperationException("cannot send message that has not attached to a message stream"); } byte[] buffer = null; uint length = 0; using (var writeBuffer = new ByteBuffer()) { var context = new Serialization.SerializationContext() { Amf0Reader = _amf0Reader, Amf0Writer = _amf0Writer, Amf3Reader = _amf3Reader, Amf3Writer = _amf3Writer, WriteBuffer = writeBuffer }; message.Serialize(context); length = (uint)writeBuffer.Length; Debug.Assert(length != 0); buffer = _arrayPool.Rent((int)length); writeBuffer.TakeOutMemory(buffer); } try { message.MessageHeader.MessageLength = length; Debug.Assert(message.MessageHeader.MessageLength != 0); if (message.MessageHeader.MessageType == 0) { message.MessageHeader.MessageType = message.GetType().GetCustomAttribute().MessageTypes.First(); } Debug.Assert(message.MessageHeader.MessageType != 0); Task ret = null; // chunking bool isFirstChunk = true; _rtmpSession.AssertStreamId(message.MessageHeader.MessageStreamId.Value); for (int i = 0; i < message.MessageHeader.MessageLength;) { _previousWriteMessageHeader.TryGetValue(chunkStreamId, out var prevHeader); var chunkHeaderType = SelectChunkType(message.MessageHeader, prevHeader, isFirstChunk); isFirstChunk = false; GenerateBasicHeader(chunkHeaderType, chunkStreamId, out var basicHeader, out var basicHeaderLength); GenerateMesesageHeader(chunkHeaderType, message.MessageHeader, prevHeader, out var messageHeader, out var messageHeaderLength); _previousWriteMessageHeader[chunkStreamId] = (MessageHeader)message.MessageHeader.Clone(); var headerLength = basicHeaderLength + messageHeaderLength; var bodySize = (int)(length - i >= _writeChunkSize ? _writeChunkSize : length - i); var chunkBuffer = _arrayPool.Rent(headerLength + bodySize); await _sync.WaitAsync(); try { basicHeader.AsSpan(0, basicHeaderLength).CopyTo(chunkBuffer); messageHeader.AsSpan(0, messageHeaderLength).CopyTo(chunkBuffer.AsSpan(basicHeaderLength)); _arrayPool.Return(basicHeader); _arrayPool.Return(messageHeader); buffer.AsSpan(i, bodySize).CopyTo(chunkBuffer.AsSpan(headerLength)); i += bodySize; var isLastChunk = message.MessageHeader.MessageLength - i == 0; long offset = 0; long totalLength = headerLength + bodySize; long currentSendSize = totalLength; while (offset != (headerLength + bodySize)) { if (WriteWindowAcknowledgementSize.HasValue && Interlocked.Read(ref WriteUnAcknowledgedSize) + headerLength + bodySize > WriteWindowAcknowledgementSize.Value) { currentSendSize = Math.Min(WriteWindowAcknowledgementSize.Value, currentSendSize); //var delayCount = 0; while (currentSendSize + Interlocked.Read(ref WriteUnAcknowledgedSize) >= WriteWindowAcknowledgementSize.Value) { await Task.Delay(1); } } var tsk = _ioPipeline.SendRawData(chunkBuffer.AsMemory((int)offset, (int)currentSendSize)); offset += currentSendSize; totalLength -= currentSendSize; if (WriteWindowAcknowledgementSize.HasValue) { Interlocked.Add(ref WriteUnAcknowledgedSize, currentSendSize); } if (isLastChunk) { ret = tsk; } } if (isLastChunk) { if (message.MessageHeader.MessageType == MessageType.SetChunkSize) { var setChunkSize = message as SetChunkSizeMessage; _writeChunkSize = setChunkSize.ChunkSize; } else if (message.MessageHeader.MessageType == MessageType.SetPeerBandwidth) { var m = message as SetPeerBandwidthMessage; ReadWindowAcknowledgementSize = m.WindowSize; } else if (message.MessageHeader.MessageType == MessageType.WindowAcknowledgementSize) { var m = message as WindowAcknowledgementSizeMessage; WriteWindowAcknowledgementSize = m.WindowSize; } } } finally { _sync.Release(); _arrayPool.Return(chunkBuffer); } } Debug.Assert(ret != null); await ret; } finally { _arrayPool.Return(buffer); } } private void GenerateMesesageHeader(ChunkHeaderType chunkHeaderType, MessageHeader header, MessageHeader prevHeader, out byte[] buffer, out int length) { var timestamp = header.Timestamp; switch (chunkHeaderType) { case ChunkHeaderType.Type0: buffer = _arrayPool.Rent(TYPE0_SIZE + EXTENDED_TIMESTAMP_LENGTH); NetworkBitConverter.TryGetUInt24Bytes(timestamp >= 0xFFFFFF ? 0xFFFFFF : timestamp, buffer.AsSpan(0, 3)); NetworkBitConverter.TryGetUInt24Bytes(header.MessageLength, buffer.AsSpan(3, 3)); NetworkBitConverter.TryGetBytes((byte)header.MessageType, buffer.AsSpan(6, 1)); NetworkBitConverter.TryGetBytes(header.MessageStreamId.Value, buffer.AsSpan(7, 4), true); length = TYPE0_SIZE; break; case ChunkHeaderType.Type1: buffer = _arrayPool.Rent(TYPE1_SIZE + EXTENDED_TIMESTAMP_LENGTH); timestamp = timestamp - prevHeader.Timestamp; NetworkBitConverter.TryGetUInt24Bytes(timestamp >= 0xFFFFFF ? 0xFFFFFF : timestamp, buffer.AsSpan(0, 3)); NetworkBitConverter.TryGetUInt24Bytes(header.MessageLength, buffer.AsSpan(3, 3)); NetworkBitConverter.TryGetBytes((byte)header.MessageType, buffer.AsSpan(6, 1)); length = TYPE1_SIZE; break; case ChunkHeaderType.Type2: buffer = _arrayPool.Rent(TYPE2_SIZE + EXTENDED_TIMESTAMP_LENGTH); timestamp = timestamp - prevHeader.Timestamp; NetworkBitConverter.TryGetUInt24Bytes(timestamp >= 0xFFFFFF ? 0xFFFFFF : timestamp, buffer.AsSpan(0, 3)); length = TYPE2_SIZE; break; case ChunkHeaderType.Type3: buffer = _arrayPool.Rent(EXTENDED_TIMESTAMP_LENGTH); length = 0; break; default: throw new ArgumentException(); } if (timestamp >= 0xFFFFFF) { NetworkBitConverter.TryGetBytes(timestamp, buffer.AsSpan(length, EXTENDED_TIMESTAMP_LENGTH)); length += EXTENDED_TIMESTAMP_LENGTH; } } private void GenerateBasicHeader(ChunkHeaderType chunkHeaderType, uint chunkStreamId, out byte[] buffer, out int length) { byte fmt = (byte)chunkHeaderType; if (chunkStreamId >= 2 && chunkStreamId <= 63) { buffer = _arrayPool.Rent(1); buffer[0] = (byte)((byte)(fmt << 6) | chunkStreamId); length = 1; } else if (chunkStreamId >= 64 && chunkStreamId <= 319) { buffer = _arrayPool.Rent(2); buffer[0] = (byte)(fmt << 6); buffer[1] = (byte)(chunkStreamId - 64); length = 2; } else if (chunkStreamId >= 320 && chunkStreamId <= 65599) { buffer = _arrayPool.Rent(3); buffer[0] = (byte)((fmt << 6) | 1); buffer[1] = (byte)((chunkStreamId - 64) & 0xff); buffer[2] = (byte)((chunkStreamId - 64) >> 8); length = 3; } else { throw new NotSupportedException(); } } private ChunkHeaderType SelectChunkType(MessageHeader messageHeader, MessageHeader prevHeader, bool isFirstChunk) { if (prevHeader == null) { return ChunkHeaderType.Type0; } if (!isFirstChunk) { return ChunkHeaderType.Type3; } long currentTimestamp = messageHeader.Timestamp; long prevTimesatmp = prevHeader.Timestamp; if (currentTimestamp - prevTimesatmp < 0) { return ChunkHeaderType.Type0; } if (messageHeader.MessageType == prevHeader.MessageType && messageHeader.MessageLength == prevHeader.MessageLength && messageHeader.MessageStreamId == prevHeader.MessageStreamId && messageHeader.Timestamp != prevHeader.Timestamp) { return ChunkHeaderType.Type2; } else if (messageHeader.MessageStreamId == prevHeader.MessageStreamId) { return ChunkHeaderType.Type1; } else { return ChunkHeaderType.Type0; } } private void FillHeader(ChunkHeader header) { if (!_previousReadMessageHeader.TryGetValue(header.ChunkBasicHeader.ChunkStreamId, out var prevHeader) && header.ChunkBasicHeader.RtmpChunkHeaderType != ChunkHeaderType.Type0) { throw new InvalidOperationException(); } switch (header.ChunkBasicHeader.RtmpChunkHeaderType) { case ChunkHeaderType.Type1: header.MessageHeader.Timestamp += prevHeader.Timestamp; header.MessageHeader.MessageStreamId = prevHeader.MessageStreamId; break; case ChunkHeaderType.Type2: header.MessageHeader.Timestamp += prevHeader.Timestamp; header.MessageHeader.MessageLength = prevHeader.MessageLength; header.MessageHeader.MessageType = prevHeader.MessageType; header.MessageHeader.MessageStreamId = prevHeader.MessageStreamId; break; case ChunkHeaderType.Type3: header.MessageHeader.Timestamp = prevHeader.Timestamp; header.MessageHeader.MessageLength = prevHeader.MessageLength; header.MessageHeader.MessageType = prevHeader.MessageType; header.MessageHeader.MessageStreamId = prevHeader.MessageStreamId; break; } } public bool ProcessFirstByteBasicHeader(ReadOnlySequence buffer, ref int consumed) { if (buffer.Length - consumed < 1) { return false; } var header = new ChunkHeader() { ChunkBasicHeader = new ChunkBasicHeader(), MessageHeader = new MessageHeader() }; _processingChunk = header; var arr = _arrayPool.Rent(1); buffer.Slice(consumed, 1).CopyTo(arr); consumed += 1; var basicHeader = arr[0]; _arrayPool.Return(arr); header.ChunkBasicHeader.RtmpChunkHeaderType = (ChunkHeaderType)(basicHeader >> 6); header.ChunkBasicHeader.ChunkStreamId = (uint)basicHeader & 0x3F; if (header.ChunkBasicHeader.ChunkStreamId != 0 && header.ChunkBasicHeader.ChunkStreamId != 0x3F) { if (header.ChunkBasicHeader.RtmpChunkHeaderType == ChunkHeaderType.Type3) { FillHeader(header); _ioPipeline.NextProcessState = ProcessState.CompleteMessage; return true; } } _ioPipeline.NextProcessState = ProcessState.ChunkMessageHeader; return true; } private bool ProcessChunkMessageHeader(ReadOnlySequence buffer, ref int consumed) { int bytesNeed = 0; switch (_processingChunk.ChunkBasicHeader.ChunkStreamId) { case 0: bytesNeed = 1; break; case 0x3F: bytesNeed = 2; break; } switch (_processingChunk.ChunkBasicHeader.RtmpChunkHeaderType) { case ChunkHeaderType.Type0: bytesNeed += TYPE0_SIZE; break; case ChunkHeaderType.Type1: bytesNeed += TYPE1_SIZE; break; case ChunkHeaderType.Type2: bytesNeed += TYPE2_SIZE; break; } if (buffer.Length - consumed <= bytesNeed) { return false; } byte[] arr = null; if (_processingChunk.ChunkBasicHeader.ChunkStreamId == 0) { arr = _arrayPool.Rent(1); buffer.Slice(consumed, 1).CopyTo(arr); consumed += 1; _processingChunk.ChunkBasicHeader.ChunkStreamId = (uint)arr[0] + 64; _arrayPool.Return(arr); } else if (_processingChunk.ChunkBasicHeader.ChunkStreamId == 0x3F) { arr = _arrayPool.Rent(2); buffer.Slice(consumed, 2).CopyTo(arr); consumed += 2; _processingChunk.ChunkBasicHeader.ChunkStreamId = (uint)arr[1] * 256 + arr[0] + 64; _arrayPool.Return(arr); } var header = _processingChunk; switch (header.ChunkBasicHeader.RtmpChunkHeaderType) { case ChunkHeaderType.Type0: arr = _arrayPool.Rent(TYPE0_SIZE); buffer.Slice(consumed, TYPE0_SIZE).CopyTo(arr); consumed += TYPE0_SIZE; header.MessageHeader.Timestamp = NetworkBitConverter.ToUInt24(arr.AsSpan(0, 3)); header.MessageHeader.MessageLength = NetworkBitConverter.ToUInt24(arr.AsSpan(3, 3)); header.MessageHeader.MessageType = (MessageType)arr[6]; header.MessageHeader.MessageStreamId = NetworkBitConverter.ToUInt32(arr.AsSpan(7, 4), true); break; case ChunkHeaderType.Type1: arr = _arrayPool.Rent(TYPE1_SIZE); buffer.Slice(consumed, TYPE1_SIZE).CopyTo(arr); consumed += TYPE1_SIZE; header.MessageHeader.Timestamp = NetworkBitConverter.ToUInt24(arr.AsSpan(0, 3)); header.MessageHeader.MessageLength = NetworkBitConverter.ToUInt24(arr.AsSpan(3, 3)); header.MessageHeader.MessageType = (MessageType)arr[6]; break; case ChunkHeaderType.Type2: arr = _arrayPool.Rent(TYPE2_SIZE); buffer.Slice(consumed, TYPE2_SIZE).CopyTo(arr); consumed += TYPE2_SIZE; header.MessageHeader.Timestamp = NetworkBitConverter.ToUInt24(arr.AsSpan(0, 3)); break; } if (arr != null) { _arrayPool.Return(arr); } FillHeader(header); if (header.MessageHeader.Timestamp == 0x00FFFFFF) { _ioPipeline.NextProcessState = ProcessState.ExtendedTimestamp; } else { _ioPipeline.NextProcessState = ProcessState.CompleteMessage; } return true; } private bool ProcessExtendedTimestamp(ReadOnlySequence buffer, ref int consumed) { if (buffer.Length - consumed < 4) { return false; } var arr = _arrayPool.Rent(4); buffer.Slice(consumed, 4).CopyTo(arr); consumed += 4; var extendedTimestamp = NetworkBitConverter.ToUInt32(arr.AsSpan(0, 4)); _processingChunk.ExtendedTimestamp = extendedTimestamp; _processingChunk.MessageHeader.Timestamp = extendedTimestamp; _ioPipeline.NextProcessState = ProcessState.CompleteMessage; return true; } private bool ProcessCompleteMessage(ReadOnlySequence buffer, ref int consumed) { var header = _processingChunk; if (!_incompleteMessageState.TryGetValue(header.ChunkBasicHeader.ChunkStreamId, out var state)) { state = new MessageReadingState() { CurrentIndex = 0, MessageLength = header.MessageHeader.MessageLength, Body = _arrayPool.Rent((int)header.MessageHeader.MessageLength) }; _incompleteMessageState.Add(header.ChunkBasicHeader.ChunkStreamId, state); } var bytesNeed = (int)(state.RemainBytes >= ReadChunkSize ? ReadChunkSize : state.RemainBytes); if (buffer.Length - consumed < bytesNeed) { return false; } if (_previousReadMessageHeader.TryGetValue(header.ChunkBasicHeader.ChunkStreamId, out var prevHeader)) { if (prevHeader.MessageStreamId != header.MessageHeader.MessageStreamId) { // inform user previous message will never be received prevHeader = null; } } _previousReadMessageHeader[_processingChunk.ChunkBasicHeader.ChunkStreamId] = (MessageHeader)_processingChunk.MessageHeader.Clone(); _processingChunk = null; buffer.Slice(consumed, bytesNeed).CopyTo(state.Body.AsSpan(state.CurrentIndex)); consumed += bytesNeed; state.CurrentIndex += bytesNeed; if (state.IsCompleted) { _incompleteMessageState.Remove(header.ChunkBasicHeader.ChunkStreamId); try { var context = new Serialization.SerializationContext() { Amf0Reader = _amf0Reader, Amf0Writer = _amf0Writer, Amf3Reader = _amf3Reader, Amf3Writer = _amf3Writer, ReadBuffer = state.Body.AsMemory(0, (int)state.MessageLength) }; if (header.MessageHeader.MessageType == MessageType.AggregateMessage) { var agg = new AggregateMessage() { MessageHeader = header.MessageHeader }; agg.Deserialize(context); foreach (var message in agg.Messages) { if (!_ioPipeline.Options.MessageFactories.TryGetValue(message.Header.MessageType, out var factory)) { continue; } var msgContext = new Serialization.SerializationContext() { Amf0Reader = context.Amf0Reader, Amf3Reader = context.Amf3Reader, Amf0Writer = context.Amf0Writer, Amf3Writer = context.Amf3Writer, ReadBuffer = context.ReadBuffer.Slice(message.DataOffset, (int)message.DataLength) }; try { var msg = factory(header.MessageHeader, msgContext, out var factoryConsumed); msg.MessageHeader = header.MessageHeader; msg.Deserialize(msgContext); context.Amf0Reader.ResetReference(); context.Amf3Reader.ResetReference(); _rtmpSession.MessageArrived(msg); } catch (NotSupportedException) { } } } else { if (_ioPipeline.Options._messageFactories.TryGetValue(header.MessageHeader.MessageType, out var factory)) { try { var message = factory(header.MessageHeader, context, out var factoryConsumed); message.MessageHeader = header.MessageHeader; context.ReadBuffer = context.ReadBuffer.Slice(factoryConsumed); message.Deserialize(context); context.Amf0Reader.ResetReference(); context.Amf3Reader.ResetReference(); _rtmpSession.MessageArrived(message); } catch (NotSupportedException) { } } } } finally { _arrayPool.Return(state.Body); } } _ioPipeline.NextProcessState = ProcessState.FirstByteBasicHeader; return true; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/ChunkBasicHeader.cs ================================================ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; namespace Harmonic.Networking.Rtmp.Data { class ChunkBasicHeader { public ChunkHeaderType RtmpChunkHeaderType { get; set; } public uint ChunkStreamId { get; set; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/ChunkHeader.cs ================================================ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace Harmonic.Networking.Rtmp.Data { class ChunkHeader { public ChunkBasicHeader ChunkBasicHeader { get; set; } public MessageHeader MessageHeader { get; set; } public uint ExtendedTimestamp { get; set; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/ChunkHeaderType.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Data { public enum ChunkHeaderType : byte { // Timestampe + Message Length + Message Type Id + Message Stream Id + Type0 = 0, // Timestamp Delta + Message Length + Message Type Id Type1 = 1, // Timestamp Delta Type2 = 2, // Nothing Type3 = 3 } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/Message.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using System; using System.Buffers; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace Harmonic.Networking.Rtmp.Data { public abstract class Message { protected ArrayPool _arrayPool = ArrayPool.Shared; public MessageHeader MessageHeader { get; internal set; } = new MessageHeader(); internal Message() { } public abstract void Deserialize(SerializationContext context); public abstract void Serialize(SerializationContext context); } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/MessageHeader.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Data { public class MessageHeader: ICloneable { public uint Timestamp { get; set; } public uint MessageLength { get; internal set; } public MessageType MessageType { get; internal set; } = 0; public uint? MessageStreamId { get; internal set; } = null; public object Clone() { return MemberwiseClone(); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/MessageType.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Data { public enum MessageType : byte { #region Protocol Control Messages SetChunkSize = 1, AbortMessage = 2, Acknowledgement = 3, WindowAcknowledgementSize = 5, SetPeerBandwidth = 6, #endregion UserControlMessages = 4, #region Rtmp Command Messages Amf0Command = 20, Amf3Command = 17, Amf0Data = 18, Amf3Data = 15, Amf0SharedObjectMessage = 19, Amf3SharedObjectMessage = 16, AudioMessage = 8, VideoMessage = 9, AggregateMessage = 22, #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/SharedObjectMessage.cs ================================================ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace Harmonic.Networking.Rtmp.Data { public class SharedObjectMessage { public string SharedObjectName { get; set; } public UInt16 CurrentVersion { get; set; } // TBD } } ================================================ FILE: Harmonic/Networking/Rtmp/Data/UserControlMessageEvents.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Data { public enum UserControlMessageEvents : UInt16 { StreamBegin, StreamEOF, StreamDry, SetBufferLength, StreamIsRecorded, PingRequest, PingResponse } } ================================================ FILE: Harmonic/Networking/Rtmp/Exceptions/UnknownMessageReceivedException.cs ================================================ using Harmonic.Networking.Rtmp.Data; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Exceptions { public class UnknownMessageReceivedException : Exception { public MessageHeader Header { get; set; } public UnknownMessageReceivedException(MessageHeader header) { Header = header; } } } ================================================ FILE: Harmonic/Networking/Rtmp/HandshakeContext.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Harmonic.Networking.Rtmp { sealed class HandshakeContext : IDisposable { private uint _readerTimestampEpoch = 0; private uint _writerTimestampEpoch = 0; private ArrayPool _arrayPool = ArrayPool.Shared; private Random _random = new Random(); private byte[] _s1Data = null; private IOPipeLine _ioPipeline = null; public HandshakeContext(IOPipeLine ioPipeline) { _ioPipeline = ioPipeline; _ioPipeline._bufferProcessors.Add(ProcessState.HandshakeC0C1, ProcessHandshakeC0C1); _ioPipeline._bufferProcessors.Add(ProcessState.HandshakeC2, ProcessHandshakeC2); } public void Dispose() { if (_s1Data != null) { _arrayPool.Return(_s1Data); _s1Data = null; } } private bool ProcessHandshakeC0C1(ReadOnlySequence buffer, ref int consumed) { if (buffer.Length - consumed < 1537) { return false; } var arr = _arrayPool.Rent(1537); try { buffer.Slice(consumed, 9).CopyTo(arr); consumed += 9; var version = arr[0]; if (version < 3) { throw new NotSupportedException(); } if (version > 31) { throw new ProtocolViolationException(); } _readerTimestampEpoch = NetworkBitConverter.ToUInt32(arr.AsSpan(1, 4)); _writerTimestampEpoch = 0; _s1Data = _arrayPool.Rent(1528); _random.NextBytes(_s1Data.AsSpan(0, 1528)); // s0s1 arr.AsSpan().Clear(); arr[0] = 3; NetworkBitConverter.TryGetBytes(_writerTimestampEpoch, arr.AsSpan(1, 4)); _s1Data.AsSpan(0, 1528).CopyTo(arr.AsSpan(9)); _ = _ioPipeline.SendRawData(arr.AsMemory(0, 1537)); // s2 NetworkBitConverter.TryGetBytes(_readerTimestampEpoch, arr.AsSpan(0, 4)); NetworkBitConverter.TryGetBytes((uint)0, arr.AsSpan(4, 4)); _ = _ioPipeline.SendRawData(arr.AsMemory(0, 1536)); buffer.Slice(consumed, 1528).CopyTo(arr.AsSpan(8)); consumed += 1528; _ioPipeline.NextProcessState = ProcessState.HandshakeC2; return true; } finally { _arrayPool.Return(arr); } } private bool ProcessHandshakeC2(ReadOnlySequence buffer, ref int consumed) { if (buffer.Length - consumed < 1536) { return false; } var arr = _arrayPool.Rent(1536); try { buffer.Slice(consumed, 1536).CopyTo(arr); consumed += 1536; var s1Timestamp = NetworkBitConverter.ToUInt32(arr.AsSpan(0, 4)); if (s1Timestamp != _writerTimestampEpoch) { throw new ProtocolViolationException(); } if (!arr.AsSpan(8, 1528).SequenceEqual(_s1Data.AsSpan(0, 1528))) { throw new ProtocolViolationException(); } _ioPipeline.OnHandshakeSuccessful(); return true; } finally { _arrayPool.Return(_s1Data); _arrayPool.Return(arr); _s1Data = null; Dispose(); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/IOPipeLine.cs ================================================ using Harmonic.Networking; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Exceptions; using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.IO.Pipelines; using System.Collections.ObjectModel; using System.Collections.Concurrent; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Utils; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Buffers; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using System.Reflection; using Harmonic.Networking.Rtmp.Messages.UserControlMessages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Hosting; using System.Linq; using System.Diagnostics; namespace Harmonic.Networking.Rtmp { enum ProcessState { HandshakeC0C1, HandshakeC2, FirstByteBasicHeader, ChunkMessageHeader, ExtendedTimestamp, CompleteMessage } // TBD: retransfer bytes when acknowledgement not received class IOPipeLine : IDisposable { internal delegate bool BufferProcessor(ReadOnlySequence buffer, ref int consumed); private SemaphoreSlim _writerSignal = new SemaphoreSlim(0); private Socket _socket; private ArrayPool _arrayPool = ArrayPool.Shared; private readonly int _resumeWriterThreshole; internal Dictionary _bufferProcessors; private ConcurrentQueue _writerQueue = new ConcurrentQueue(); internal ProcessState NextProcessState { get; set; } = ProcessState.HandshakeC0C1; internal ChunkStreamContext ChunkStreamContext { get; set; } = null; private HandshakeContext _handshakeContext = null; public RtmpServerOptions Options { get; set; } = null; public IOPipeLine(Socket socket, RtmpServerOptions options, int resumeWriterThreshole = 65535) { _socket = socket; _resumeWriterThreshole = resumeWriterThreshole; _bufferProcessors = new Dictionary(); Options = options; _handshakeContext = new HandshakeContext(this); } public Task StartAsync(CancellationToken ct = default) { var d = PipeOptions.Default; var opt = new PipeOptions( d.Pool, d.ReaderScheduler, d.WriterScheduler, _resumeWriterThreshole, d.ResumeWriterThreshold, d.MinimumSegmentSize, d.UseSynchronizationContext); var pipe = new Pipe(opt); var t1 = Producer(_socket, pipe.Writer, ct); var t2 = Consumer(pipe.Reader, ct); var t3 = Writer(ct); ct.Register(() => { ChunkStreamContext?.Dispose(); ChunkStreamContext = null; }); var tcs = new TaskCompletionSource(); Action setException = t => { tcs.TrySetException(t.Exception.InnerException); }; Action setCanceled = _ => { tcs.TrySetCanceled(); }; Action setResult = t => { tcs.TrySetResult(1); }; t1.ContinueWith(setException, TaskContinuationOptions.OnlyOnFaulted); t2.ContinueWith(setException, TaskContinuationOptions.OnlyOnFaulted); t3.ContinueWith(setException, TaskContinuationOptions.OnlyOnFaulted); t1.ContinueWith(setCanceled, TaskContinuationOptions.OnlyOnCanceled); t2.ContinueWith(setCanceled, TaskContinuationOptions.OnlyOnCanceled); t3.ContinueWith(setCanceled, TaskContinuationOptions.OnlyOnCanceled); t1.ContinueWith(setResult, TaskContinuationOptions.OnlyOnRanToCompletion); t2.ContinueWith(setResult, TaskContinuationOptions.OnlyOnRanToCompletion); t3.ContinueWith(setResult, TaskContinuationOptions.OnlyOnRanToCompletion); return tcs.Task; } internal void OnHandshakeSuccessful() { _handshakeContext = null; _bufferProcessors.Clear(); ChunkStreamContext = new ChunkStreamContext(this); } #region Sender internal async Task SendRawData(ReadOnlyMemory data) { var tcs = new TaskCompletionSource(); var buffer = _arrayPool.Rent(data.Length); data.CopyTo(buffer); _writerQueue.Enqueue(new WriteState() { Buffer = buffer, TaskSource = tcs, Length = data.Length }); _writerSignal.Release(); await tcs.Task; } private async Task Writer(CancellationToken ct) { while (!ct.IsCancellationRequested && !disposedValue) { await _writerSignal.WaitAsync(ct); if (_writerQueue.TryDequeue(out var data)) { Debug.Assert(data != null); Debug.Assert(_socket != null); Debug.Assert((data.Buffer[0] & 0x3F) < 10); await _socket.SendAsync(data.Buffer.AsMemory(0, data.Length), SocketFlags.None, ct); _arrayPool.Return(data.Buffer); data.TaskSource?.SetResult(1); } else { Debug.Assert(false); } } } #endregion #region Receiver private async Task Producer(Socket s, PipeWriter writer, CancellationToken ct = default) { while (!ct.IsCancellationRequested && !disposedValue) { var memory = writer.GetMemory(ChunkStreamContext == null ? 1536 : ChunkStreamContext.ReadMinimumBufferSize); var bytesRead = await s.ReceiveAsync(memory, SocketFlags.None); if (bytesRead == 0) { break; } writer.Advance(bytesRead); var result = await writer.FlushAsync(ct); if (result.IsCompleted || result.IsCanceled) { break; } } writer.Complete(); } private async Task Consumer(PipeReader reader, CancellationToken ct = default) { while (!ct.IsCancellationRequested && !disposedValue) { var result = await reader.ReadAsync(ct); var buffer = result.Buffer; int consumed = 0; while (true) { if (!_bufferProcessors[NextProcessState](buffer, ref consumed)) { break; } } buffer = buffer.Slice(consumed); reader.AdvanceTo(buffer.Start, buffer.End); if (ChunkStreamContext != null) { ChunkStreamContext.ReadUnAcknowledgedSize += consumed; if (ChunkStreamContext.ReadWindowAcknowledgementSize.HasValue) { if (ChunkStreamContext.ReadUnAcknowledgedSize >= ChunkStreamContext.ReadWindowAcknowledgementSize) { ChunkStreamContext._rtmpSession.Acknowledgement((uint)ChunkStreamContext.ReadUnAcknowledgedSize); ChunkStreamContext.ReadUnAcknowledgedSize -= 0; } } } if (result.IsCompleted || result.IsCanceled) { break; } } // Mark the PipeReader as complete reader.Complete(); } internal void Disconnect() { _socket.Close(); Dispose(); } #endregion #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { _handshakeContext?.Dispose(); ChunkStreamContext?.Dispose(); _socket.Dispose(); _writerSignal.Dispose(); } disposedValue = true; } } // ~IOPipeline() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/MessageReadingState.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp { class MessageReadingState { public uint MessageLength; public byte[] Body; public int CurrentIndex; public long RemainBytes { get => MessageLength - CurrentIndex; } public bool IsCompleted { get => RemainBytes == 0; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/AbortMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.AbortMessage)] public class AbortMessage : ControlMessage { public uint AbortedChunkStreamId { get; set; } public AbortMessage() : base() { } public override void Deserialize(SerializationContext context) { AbortedChunkStreamId = NetworkBitConverter.ToUInt32(context.ReadBuffer.Span); } public override void Serialize(SerializationContext context) { var buffer = _arrayPool.Rent(sizeof(uint)); try { NetworkBitConverter.TryGetBytes(AbortedChunkStreamId, buffer); context.WriteBuffer.WriteToBuffer(buffer); } finally { _arrayPool.Return(buffer); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/AcknowledgementMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.Acknowledgement)] public class AcknowledgementMessage : ControlMessage { public uint BytesReceived { get; set; } public AcknowledgementMessage() : base() { } public override void Deserialize(SerializationContext context) { BytesReceived = NetworkBitConverter.ToUInt32(context.ReadBuffer.Span); } public override void Serialize(SerializationContext context) { var buffer = _arrayPool.Rent(sizeof(uint)); try { NetworkBitConverter.TryGetBytes(BytesReceived, buffer); context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, sizeof(uint))); } finally { _arrayPool.Return(buffer); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/AggregateMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { internal class MessageData { public MessageHeader Header { get; set; } public int DataOffset { get; set; } public uint DataLength { get; set; } } [RtmpMessage(MessageType.AggregateMessage)] internal class AggregateMessage : Message { public List Messages { get; set; } = new List(); public byte[] MessageBuffer { get; set; } = null; public AggregateMessage() : base() { } private MessageData DeserializeMessage(Span buffer, out int consumed) { consumed = 0; var header = new MessageHeader(); header.MessageType = (MessageType)buffer[0]; buffer = buffer.Slice(sizeof(byte)); consumed += sizeof(byte); header.MessageLength = NetworkBitConverter.ToUInt24(buffer); buffer = buffer.Slice(3); consumed += 3; header.Timestamp = NetworkBitConverter.ToUInt32(buffer); buffer = buffer.Slice(sizeof(uint)); consumed += sizeof(uint); header.MessageStreamId = header.MessageStreamId; // Override message stream id buffer = buffer.Slice(3); consumed += 3; var offset = consumed; consumed += (int)header.MessageLength; header.Timestamp += MessageHeader.Timestamp; return new MessageData() { Header = header, DataOffset = offset, DataLength = header.MessageLength }; } public override void Deserialize(SerializationContext context) { var spanBuffer = context.ReadBuffer.Span; while (spanBuffer.Length != 0) { Messages.Add(DeserializeMessage(spanBuffer, out var consumed)); spanBuffer = spanBuffer.Slice(consumed + /* back pointer */ 4); } } public override void Serialize(SerializationContext context) { int bytesNeed = (int)(Messages.Count * 11 + Messages.Sum(m => m.DataLength)); var buffer = _arrayPool.Rent(bytesNeed); try { var span = buffer.AsSpan(0, bytesNeed); int consumed = 0; foreach (var message in Messages) { span[0] = (byte)message.Header.MessageType; span = span.Slice(sizeof(byte)); NetworkBitConverter.TryGetUInt24Bytes((uint)message.Header.MessageLength, span); span = span.Slice(3); NetworkBitConverter.TryGetBytes(message.Header.Timestamp, span); span = span.Slice(4); NetworkBitConverter.TryGetUInt24Bytes((uint)MessageHeader.MessageStreamId, span); span = span.Slice(3); MessageBuffer.AsSpan(consumed, (int)message.Header.MessageLength).CopyTo(span); consumed += (int)message.Header.MessageLength; span = span.Slice((int)message.Header.MessageLength); } context.WriteBuffer.WriteToBuffer(span); } finally { _arrayPool.Return(buffer); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/AmfEncodingVersion.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { public enum AmfEncodingVersion { Amf0, Amf3 } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/AudioMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using System; using System.Collections.Generic; using System.Net; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.AudioMessage)] public sealed class AudioMessage : Message, ICloneable { public ReadOnlyMemory Data { get; private set; } public object Clone() { var ret = new AudioMessage { MessageHeader = (MessageHeader)MessageHeader.Clone() }; ret.MessageHeader.MessageStreamId = null; ret.Data = Data; return ret; } public override void Deserialize(SerializationContext context) { // TODO: optimize performance var data = new byte[context.ReadBuffer.Length]; context.ReadBuffer.Span.Slice(0, (int)MessageHeader.MessageLength).CopyTo(data); Data = data; } public override void Serialize(SerializationContext context) { context.WriteBuffer.WriteToBuffer(Data.Span.Slice(0, Data.Length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/CallCommandMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Networking.Rtmp.Messages.Commands { public abstract class CallCommandMessage : CommandMessage { public CallCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/CommandMessage.cs ================================================ using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpMessage(MessageType.Amf3Command, MessageType.Amf0Command)] public abstract class CommandMessage : Message { public AmfEncodingVersion AmfEncodingVersion { get; set; } public virtual string ProcedureName { get; set; } public double TranscationID { get; set; } public virtual AmfObject CommandObject { get; set; } public CommandMessage(AmfEncodingVersion encoding) : base() { AmfEncodingVersion = encoding; MessageHeader.MessageType = encoding == AmfEncodingVersion.Amf0 ? MessageType.Amf0Command : MessageType.Amf3Command; } public void DeserializeAmf0(SerializationContext context) { var buffer = context.ReadBuffer.Span; if (!context.Amf0Reader.TryGetNumber(buffer, out var txid, out var consumed)) { throw new InvalidOperationException(); } TranscationID = txid; buffer = buffer.Slice(consumed); context.Amf0Reader.TryGetObject(buffer, out var commandObj, out consumed); CommandObject = commandObj; buffer = buffer.Slice(consumed); var optionArguments = GetType().GetProperties().Where(p => p.GetCustomAttribute() != null).ToList(); var i = 0; while (buffer.Length > 0) { if (!context.Amf0Reader.TryGetValue(buffer, out _, out var optArg, out consumed)) { break; } buffer = buffer.Slice(consumed); optionArguments[i].SetValue(this, optArg); i++; if (i >= optionArguments.Count) { break; } } } public void DeserializeAmf3(SerializationContext context) { var buffer = context.ReadBuffer.Span; if (!context.Amf3Reader.TryGetDouble(buffer, out var txid, out var consumed)) { throw new InvalidOperationException(); } TranscationID = txid; buffer = buffer.Slice(consumed); context.Amf3Reader.TryGetObject(buffer, out var commandObj, out consumed); CommandObject = commandObj as AmfObject; buffer = buffer.Slice(consumed); var optionArguments = GetType().GetProperties().Where(p => p.GetCustomAttribute() != null).ToList(); var i = 0; while (buffer.Length > 0) { context.Amf0Reader.TryGetValue(buffer, out _, out var optArg, out _); optionArguments[i].SetValue(this, optArg); } } public void SerializeAmf0(SerializationContext context) { using (var writeContext = new Amf.Serialization.Amf0.SerializationContext(context.WriteBuffer)) { if (ProcedureName == null) { ProcedureName = GetType().GetCustomAttribute().Name; } Debug.Assert(!string.IsNullOrEmpty(ProcedureName)); context.Amf0Writer.WriteBytes(ProcedureName, writeContext); context.Amf0Writer.WriteBytes(TranscationID, writeContext); context.Amf0Writer.WriteValueBytes(CommandObject, writeContext); var optionArguments = GetType().GetProperties().Where(p => p.GetCustomAttribute() != null).ToList(); foreach (var optionArgument in optionArguments) { context.Amf0Writer.WriteValueBytes(optionArgument.GetValue(this), writeContext); } } } public void SerializeAmf3(SerializationContext context) { using (var writeContext = new Amf.Serialization.Amf3.SerializationContext(context.WriteBuffer)) { if (ProcedureName == null) { ProcedureName = GetType().GetCustomAttribute().Name; } Debug.Assert(!string.IsNullOrEmpty(ProcedureName)); context.Amf3Writer.WriteBytes(ProcedureName, writeContext); context.Amf3Writer.WriteBytes(TranscationID, writeContext); context.Amf3Writer.WriteValueBytes(CommandObject, writeContext); var optionArguments = GetType().GetProperties().Where(p => p.GetCustomAttribute() != null).ToList(); foreach (var optionArgument in optionArguments) { context.Amf3Writer.WriteValueBytes(optionArgument.GetValue(this), writeContext); } } } public sealed override void Deserialize(SerializationContext context) { if (AmfEncodingVersion == AmfEncodingVersion.Amf0) { DeserializeAmf0(context); } else { DeserializeAmf3(context); } } public sealed override void Serialize(SerializationContext context) { if (AmfEncodingVersion == AmfEncodingVersion.Amf0) { SerializeAmf0(context); } else { SerializeAmf3(context); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/CommandMessageFactory.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Net; using System.Reflection; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { public class CommandMessageFactory { public Dictionary _messageFactories = new Dictionary(); public CommandMessageFactory() { RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); RegisterMessage(); } public void RegisterMessage() where T: CommandMessage { var tType = typeof(T); var attr = tType.GetCustomAttribute(); if (attr == null) { throw new InvalidOperationException(); } _messageFactories.Add(attr.Name, tType); } public Message Provide(MessageHeader header, SerializationContext context, out int consumed) { string name = null; bool amf3 = false; if (header.MessageType == MessageType.Amf0Command) { if (!context.Amf0Reader.TryGetString(context.ReadBuffer.Span, out name, out consumed)) { throw new ProtocolViolationException(); } } else if (header.MessageType == MessageType.Amf3Command) { amf3 = true; if (!context.Amf3Reader.TryGetString(context.ReadBuffer.Span, out name, out consumed)) { throw new ProtocolViolationException(); } } else { throw new InvalidOperationException(); } if (!_messageFactories.TryGetValue(name, out var t)) { throw new NotSupportedException(); } var ret = (CommandMessage)Activator.CreateInstance(t, amf3 ? AmfEncodingVersion.Amf3 : AmfEncodingVersion.Amf0); ret.ProcedureName = name; return ret; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/ConnectCommandMessage.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "connect")] public class ConnectCommandMessage : CommandMessage { [OptionalArgument] public object UserArguments { get; set; } public ConnectCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/CreateStreamCommandMessage.cs ================================================ using System; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "createStream")] public class CreateStreamCommandMessage : CommandMessage { public CreateStreamCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/DeleteStreamCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "deleteStream")] public class DeleteStreamCommandMessage : CommandMessage { [OptionalArgument] public double StreamID { get; set; } public DeleteStreamCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/OnStatusCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "onStatus")] public class OnStatusCommandMessage : CommandMessage { [OptionalArgument] public object InfoObject { get; set; } public OnStatusCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/PauseCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "pause")] public class PauseCommandMessage : CommandMessage { [OptionalArgument] public bool IsPause { get; set; } [OptionalArgument] public double MilliSeconds { get; set; } public PauseCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/Play2CommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "play2")] public class Play2CommandMessage : CommandMessage { [OptionalArgument] public object Parameters { get; set; } public Play2CommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/PlayCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "play")] public class PlayCommandMessage : CommandMessage { [OptionalArgument] public string StreamName { get; set; } [OptionalArgument] public double Start { get; set; } [OptionalArgument] public double Duration { get; set; } [OptionalArgument] public bool Reset { get; set; } public PlayCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/PublishCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "publish")] public class PublishCommandMessage : CommandMessage { [OptionalArgument] public string PublishingName { get; set; } [OptionalArgument] public string PublishingType { get; set; } public PublishCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/ReceiveAudioCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "receiveAudio")] public class ReceiveAudioCommandMessage : CommandMessage { [OptionalArgument] public bool IsReceive { get; set; } public ReceiveAudioCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/ReceiveVideoCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "receiveVideo")] public class ReceiveVideoCommandMessage : CommandMessage { [OptionalArgument] public bool IsReceive { get; set; } public ReceiveVideoCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/ReturnResultCommandMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Networking.Rtmp.Messages.Commands { public class ReturnResultCommandMessage : CallCommandMessage { [OptionalArgument] public object ReturnValue { get; set; } private bool _success = true; public bool IsSuccess { get { return _success; } set { if (value) { ProcedureName = "_result"; } else { ProcedureName = "_error"; } _success = value; } } public ReturnResultCommandMessage(AmfEncodingVersion encoding) : base(encoding) { IsSuccess = true; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/Commands/SeekCommandMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.Commands { [RtmpCommand(Name = "seek")] public class SeekCommandMessage : CommandMessage { [OptionalArgument] public double MilliSeconds { get; set; } public SeekCommandMessage(AmfEncodingVersion encoding) : base(encoding) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/ControlMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { public abstract class ControlMessage : Message { internal ControlMessage() : base() { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/DataMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Net; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.Amf0Data, MessageType.Amf3Data)] public class DataMessage : Message { public List Data { get; set; } public DataMessage(AmfEncodingVersion encoding) : base() { MessageHeader.MessageType = encoding == AmfEncodingVersion.Amf0 ? MessageType.Amf0Data : MessageType.Amf3Data; } public override void Deserialize(SerializationContext context) { Data = new List(); var span = context.ReadBuffer.Span; if (MessageHeader.MessageType == MessageType.Amf0Data) { while (span.Length != 0) { if (!context.Amf0Reader.TryGetValue(span, out _, out var data, out var consumed)) { throw new ProtocolViolationException(); } Data.Add(data); span = span.Slice(consumed); } } else { while (span.Length != 0) { if (!context.Amf3Reader.TryGetValue(span, out var data, out var consumed)) { throw new ProtocolViolationException(); } Data.Add(data); span = span.Slice(consumed); } } } public override void Serialize(SerializationContext context) { if (MessageHeader.MessageType == MessageType.Amf0Data) { var sc = new Amf.Serialization.Amf0.SerializationContext(context.WriteBuffer); foreach (var data in Data) { context.Amf0Writer.WriteValueBytes(data, sc); } } else { var sc = new Amf.Serialization.Amf3.SerializationContext(context.WriteBuffer); foreach (var data in Data) { context.Amf3Writer.WriteValueBytes(data, sc); } } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/SetChunkSizeMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.SetChunkSize)] public class SetChunkSizeMessage : ControlMessage { public uint ChunkSize { get; set; } public SetChunkSizeMessage() : base() { } public override void Deserialize(SerializationContext context) { var chunkSize = NetworkBitConverter.ToInt32(context.ReadBuffer.Span); ChunkSize = (uint)chunkSize; } public override void Serialize(SerializationContext context) { var buffer = _arrayPool.Rent(sizeof(uint)); try { NetworkBitConverter.TryGetBytes(ChunkSize, buffer); context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, sizeof(uint))); } finally { _arrayPool.Return(buffer); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/SetPeerBandwidthMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; namespace Harmonic.Networking.Rtmp.Messages { public enum LimitType : byte { Hard, Soft, Dynamic } [RtmpMessage(MessageType.SetPeerBandwidth)] public class SetPeerBandwidthMessage : ControlMessage { public uint WindowSize { get; set; } public LimitType LimitType { get; set; } public SetPeerBandwidthMessage() : base() { } public override void Deserialize(SerializationContext context) { WindowSize = NetworkBitConverter.ToUInt32(context.ReadBuffer.Span); LimitType = (LimitType)context.ReadBuffer.Span.Slice(sizeof(uint))[0]; } public override void Serialize(SerializationContext context) { var buffer = _arrayPool.Rent(sizeof(uint) + sizeof(byte)); try { NetworkBitConverter.TryGetBytes(WindowSize, buffer); buffer.AsSpan(sizeof(uint))[0] = (byte)LimitType; context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, sizeof(uint) + sizeof(byte))); } finally { _arrayPool.Return(buffer); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingRequestMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.PingRequest)] public class PingRequestMessage : UserControlMessage { public uint Timestamp { get; set; } public PingRequestMessage() : base() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); Timestamp = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(Timestamp, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingResponseMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.PingResponse)] public class PingResponseMessage : UserControlMessage { public uint Timestamp { get; set; } public PingResponseMessage() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); Timestamp = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(Timestamp, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/SetBufferLengthMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.SetBufferLength)] public class SetBufferLengthMessage : UserControlMessage { public uint StreamID { get; set; } public uint BufferMilliseconds { get; set; } public SetBufferLengthMessage() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); StreamID = NetworkBitConverter.ToUInt32(span); span = span.Slice(sizeof(uint)); BufferMilliseconds = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(StreamID, span); span = span.Slice(sizeof(uint)); NetworkBitConverter.TryGetBytes(BufferMilliseconds, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamBeginMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.StreamBegin)] public class StreamBeginMessage : UserControlMessage { public uint StreamID { get; set; } public StreamBeginMessage() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); StreamID = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(StreamID, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamDryMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.StreamDry)] public class StreamDryMessage : UserControlMessage { public uint StreamID { get; set; } public StreamDryMessage() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); StreamID = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(StreamID, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamEofMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.StreamEof)] public class StreamEofMessage : UserControlMessage { public uint StreamID { get; set; } public StreamEofMessage() : base() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); StreamID = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(StreamID, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamIsRecordedMessage.cs ================================================ using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { [UserControlMessage(Type = UserControlEventType.StreamIsRecorded)] public class StreamIsRecordedMessage : UserControlMessage { public uint StreamID { get; set; } public StreamIsRecordedMessage() : base() { } public override void Deserialize(SerializationContext context) { var span = context.ReadBuffer.Span; var eventType = (UserControlEventType)NetworkBitConverter.ToUInt16(span); span = span.Slice(sizeof(ushort)); Contract.Assert(eventType == UserControlEventType.StreamIsRecorded); StreamID = NetworkBitConverter.ToUInt32(span); } public override void Serialize(SerializationContext context) { var length = sizeof(ushort) + sizeof(uint); var buffer = _arrayPool.Rent(length); try { var span = buffer.AsSpan(); NetworkBitConverter.TryGetBytes((ushort)UserControlEventType.StreamBegin, span); span = span.Slice(sizeof(ushort)); NetworkBitConverter.TryGetBytes(StreamID, span); } finally { _arrayPool.Return(buffer); } context.WriteBuffer.WriteToBuffer(buffer.AsSpan(0, length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { public enum UserControlEventType : ushort { StreamBegin, StreamEof, StreamDry, SetBufferLength, StreamIsRecorded, PingRequest, PingResponse } [RtmpMessage(MessageType.UserControlMessages)] public abstract class UserControlMessage : ControlMessage { public UserControlEventType UserControlEventType { get; set; } public UserControlMessage() : base() { } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessageFactory.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; using System; using System.Collections.Generic; using System.Reflection; using System.Text; namespace Harmonic.Networking.Rtmp.Messages.UserControlMessages { public class UserControlMessageFactory { public Dictionary _messageFactories = new Dictionary(); public void RegisterMessage() where T: UserControlMessage, new() { var tType = typeof(T); var attr = tType.GetCustomAttribute(); if (attr == null) { throw new InvalidOperationException(); } _messageFactories.Add(attr.Type, tType); } public Message Provide(MessageHeader header, SerializationContext context, out int consumed) { var type = (UserControlEventType)NetworkBitConverter.ToUInt16(context.ReadBuffer.Span); if (!_messageFactories.TryGetValue(type, out var t)) { throw new NotSupportedException(); } consumed = 0; return (Message)Activator.CreateInstance(t); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/VideoMessage.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Text; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.VideoMessage)] public sealed class VideoMessage : Message, ICloneable { public ReadOnlyMemory Data { get; private set; } public object Clone() { var ret = new VideoMessage { MessageHeader = (MessageHeader)MessageHeader.Clone() }; ret.MessageHeader.MessageStreamId = null; ret.Data = Data; return ret; } public override void Deserialize(SerializationContext context) { // TODO: optimize performance var data = new byte[context.ReadBuffer.Length]; context.ReadBuffer.Span.Slice(0, (int)MessageHeader.MessageLength).CopyTo(data); Data = data; } public override void Serialize(SerializationContext context) { context.WriteBuffer.WriteToBuffer(Data.Span.Slice(0, Data.Length)); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Messages/WindowAcknowledgementSizeMessage.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.Text; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Networking.Utils; namespace Harmonic.Networking.Rtmp.Messages { [RtmpMessage(MessageType.WindowAcknowledgementSize)] public class WindowAcknowledgementSizeMessage : ControlMessage { public uint WindowSize { get; set; } public WindowAcknowledgementSizeMessage() : base() { } public override void Deserialize(SerializationContext context) { WindowSize = NetworkBitConverter.ToUInt32(context.ReadBuffer.Span); } public override void Serialize(SerializationContext context) { var arr = ArrayPool.Shared.Rent(sizeof(uint)); try { NetworkBitConverter.TryGetBytes(WindowSize, arr); context.WriteBuffer.WriteToBuffer(arr.AsSpan(0, sizeof(uint))); } finally { ArrayPool.Shared.Return(arr); } } } } ================================================ FILE: Harmonic/Networking/Rtmp/NetConnection.cs ================================================ using Autofac; using Harmonic.Controllers; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Serialization; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Harmonic.Networking.Rtmp { public class NetConnection : IDisposable { private RtmpSession _rtmpSession = null; private RtmpChunkStream _rtmpChunkStream = null; private Dictionary _netStreams = new Dictionary(); private RtmpControlMessageStream _controlMessageStream = null; public IReadOnlyDictionary NetStreams { get => _netStreams; } private RtmpController _controller; private bool _connected = false; private object _streamsLock = new object(); private RtmpController Controller { get { return _controller; } set { if (_controller != null) { throw new InvalidOperationException("already have an controller"); } _controller = value ?? throw new InvalidOperationException("controller cannot be null"); _controller.MessageStream = _controlMessageStream; _controller.ChunkStream = _rtmpChunkStream; _controller.RtmpSession = _rtmpSession; } } internal NetConnection(RtmpSession rtmpSession) { _rtmpSession = rtmpSession; _rtmpChunkStream = _rtmpSession.CreateChunkStream(); _controlMessageStream = _rtmpSession.ControlMessageStream; _controlMessageStream.RegisterMessageHandler(CommandHandler); } private void CommandHandler(CommandMessage command) { if (command.ProcedureName == "connect") { Connect(command); _connected = true; } else if (command.ProcedureName == "close") { Close(); _connected = false; } else if (_controller != null && _connected) { _rtmpSession.CommandHandler(_controller, command); } else { _rtmpSession.Close(); } } public void Connect(CommandMessage command) { var commandObj = command.CommandObject; _rtmpSession.ConnectionInformation = new Networking.ConnectionInformation(); var props = _rtmpSession.ConnectionInformation.GetType().GetProperties(); foreach (var prop in props) { var sb = new StringBuilder(prop.Name); sb[0] = char.ToLower(sb[0]); var asPropName = sb.ToString(); if (commandObj.Fields.ContainsKey(asPropName)) { var commandObjectValue = commandObj.Fields[asPropName]; if (commandObjectValue.GetType() == prop.PropertyType) { prop.SetValue(_rtmpSession.ConnectionInformation, commandObjectValue); } } } if (_rtmpSession.FindController(_rtmpSession.ConnectionInformation.App, out var controllerType)) { Controller = _rtmpSession.IOPipeline.Options.ServerLifetime.Resolve(controllerType) as RtmpController; } else { _rtmpSession.Close(); return; } AmfObject param = new AmfObject { { "code", "NetConnection.Connect.Success" }, { "description", "Connection succeeded." }, { "level", "status" }, }; var msg = _rtmpSession.CreateCommandMessage(); msg.CommandObject = new AmfObject { { "capabilities", 255.00 }, { "fmsVer", "FMS/4,5,1,484" }, { "mode", 1.0 } }; msg.ReturnValue = param; msg.IsSuccess = true; msg.TranscationID = command.TranscationID; _rtmpSession.ControlMessageStream.SendMessageAsync(_rtmpChunkStream, msg); } public void Close() { _rtmpSession.Close(); } internal void MessageStreamDestroying(NetStream stream) { lock (_streamsLock) { _netStreams.Remove(stream.MessageStream.MessageStreamId); } } internal void AddMessageStream(uint id, NetStream stream) { lock (_streamsLock) { _netStreams.Add(id, stream); } } internal void RemoveMessageStream(uint id) { lock (_streamsLock) { _netStreams.Remove(id); } } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { lock (_streamsLock) { while (_netStreams.Any()) { (_, var stream) = _netStreams.First(); if (stream is IDisposable disp) { disp.Dispose(); } } } _rtmpChunkStream.Dispose(); } disposedValue = true; } } // ~NetConnection() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/NetStream.cs ================================================ using Harmonic.Controllers; using Harmonic.Networking.Amf.Common; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp { public abstract class NetStream : RtmpController, IDisposable { [RpcMethod("deleteStream")] public void DeleteStream() { Dispose(); } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { MessageStream.RtmpSession.NetConnection.MessageStreamDestroying(this); } disposedValue = true; } } // ~NetStream() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/RtmpChunkStream.cs ================================================ using Harmonic.Networking.Rtmp.Data; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Harmonic.Networking.Rtmp { public class RtmpChunkStream : IDisposable { internal RtmpSession RtmpSession { get; set; } = null; public uint ChunkStreamId { get; protected set; } internal RtmpChunkStream(RtmpSession rtmpSession, uint chunkStreamId) { ChunkStreamId = chunkStreamId; RtmpSession = rtmpSession; } internal RtmpChunkStream(RtmpSession rtmpSession) { RtmpSession = rtmpSession; ChunkStreamId = rtmpSession.MakeUniqueChunkStreamId(); } protected RtmpChunkStream() { } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { RtmpSession.ChunkStreamDestroyed(this); } disposedValue = true; } } // ~RtmpChunkStream() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/RtmpControlChunkStream.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Harmonic.Networking.Rtmp { class RtmpControlChunkStream : RtmpChunkStream { private static readonly uint CONTROL_CSID = 2; internal RtmpControlChunkStream(RtmpSession rtmpSession) : base() { ChunkStreamId = CONTROL_CSID; RtmpSession = rtmpSession; } } } ================================================ FILE: Harmonic/Networking/Rtmp/RtmpControlMessageStream.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Networking.Rtmp { public class RtmpControlMessageStream : RtmpMessageStream { private static readonly uint CONTROL_MSID = 0; internal RtmpControlMessageStream(RtmpSession rtmpSession) : base(rtmpSession, CONTROL_MSID) { } } } ================================================ FILE: Harmonic/Networking/Rtmp/RtmpMessageStream.cs ================================================ using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Serialization; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Harmonic.Networking.Rtmp { public class RtmpMessageStream : IDisposable { public uint MessageStreamId { get; private set; } internal RtmpSession RtmpSession { get; } private Dictionary> _messageHandlers = new Dictionary>(); internal RtmpMessageStream(RtmpSession rtmpSession, uint messageStreamId) { MessageStreamId = messageStreamId; RtmpSession = rtmpSession; } internal RtmpMessageStream(RtmpSession rtmpSession) { MessageStreamId = rtmpSession.MakeUniqueMessageStreamId(); RtmpSession = rtmpSession; } private void AttachMessage(Message message) { message.MessageHeader.MessageStreamId = MessageStreamId; } public virtual Task SendMessageAsync(RtmpChunkStream chunkStream, Message message) { AttachMessage(message); return RtmpSession.SendMessageAsync(chunkStream.ChunkStreamId, message); } internal void RegisterMessageHandler(Action handler) where T: Message { var attr = typeof(T).GetCustomAttribute(); if (attr == null || !attr.MessageTypes.Any()) { throw new InvalidOperationException("unsupported message type"); } foreach (var messageType in attr.MessageTypes) { if (_messageHandlers.ContainsKey(messageType)) { throw new InvalidOperationException("message type already registered"); } _messageHandlers[messageType] = m => { handler(m as T); }; } } protected void RemoveMessageHandler(MessageType messageType) { _messageHandlers.Remove(messageType); } internal void MessageArrived(Message message) { if (_messageHandlers.TryGetValue(message.MessageHeader.MessageType, out var handler)) { handler(message); } } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { RtmpSession.MessageStreamDestroying(this); } disposedValue = true; } } // ~RtmpMessageStream() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/RtmpSession.cs ================================================ using Autofac; using Harmonic.Controllers; using Harmonic.Networking.Rtmp.Data; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Threading; namespace Harmonic.Networking.Rtmp { public class RtmpSession : IDisposable { internal IOPipeLine IOPipeline { get; set; } = null; private Dictionary _messageStreams = new Dictionary(); private Random _random = new Random(); internal RtmpControlChunkStream ControlChunkStream { get; } public RtmpControlMessageStream ControlMessageStream { get; } public NetConnection NetConnection { get; } private RpcService _rpcService = null; public ConnectionInformation ConnectionInformation { get; internal set; } private object _allocCsidLocker = new object(); private SortedList _allocatedCsid = new SortedList(); internal RtmpSession(IOPipeLine ioPipeline) { IOPipeline = ioPipeline; ControlChunkStream = new RtmpControlChunkStream(this); ControlMessageStream = new RtmpControlMessageStream(this); _messageStreams.Add(ControlMessageStream.MessageStreamId, ControlMessageStream); NetConnection = new NetConnection(this); ControlMessageStream.RegisterMessageHandler(HandleSetChunkSize); ControlMessageStream.RegisterMessageHandler(HandleWindowAcknowledgementSize); ControlMessageStream.RegisterMessageHandler(HandleSetPeerBandwidth); ControlMessageStream.RegisterMessageHandler(HandleAcknowledgement); _rpcService = ioPipeline.Options.ServerLifetime.Resolve(); } private void HandleAcknowledgement(AcknowledgementMessage ack) { Interlocked.Add(ref IOPipeline.ChunkStreamContext.WriteUnAcknowledgedSize, ack.BytesReceived * -1); } internal void AssertStreamId(uint msid) { Debug.Assert(_messageStreams.ContainsKey(msid)); } internal uint MakeUniqueMessageStreamId() { // TBD use uint.MaxValue return (uint)_random.Next(1, 20); } internal uint MakeUniqueChunkStreamId() { // TBD make csid unique lock (_allocCsidLocker) { var next = _allocatedCsid.Any() ? _allocatedCsid.Last().Key : 2; if (uint.MaxValue == next) { for (uint i = 0; i < uint.MaxValue; i++) { if (!_allocatedCsid.ContainsKey(i)) { _allocatedCsid.Add(i, i); return i; } } throw new InvalidOperationException("too many chunk stream"); } next += 1; _allocatedCsid.Add(next, next); return next; } } public T CreateNetStream() where T: NetStream { var ret = IOPipeline.Options.ServerLifetime.Resolve(); ret.MessageStream = CreateMessageStream(); ret.RtmpSession = this; ret.ChunkStream = CreateChunkStream(); ret.MessageStream.RegisterMessageHandler(c => CommandHandler(ret, c)); NetConnection.AddMessageStream(ret.MessageStream.MessageStreamId, ret); return ret; } public void DeleteNetStream(uint id) { if (NetConnection.NetStreams.TryGetValue(id, out var stream)) { if (stream is IDisposable disp) { disp.Dispose(); } NetConnection.RemoveMessageStream(id); } } public T CreateCommandMessage() where T: CommandMessage { var ret = Activator.CreateInstance(typeof(T), ConnectionInformation.AmfEncodingVersion); return ret as T; } public T CreateData() where T : DataMessage { var ret = Activator.CreateInstance(typeof(T), ConnectionInformation.AmfEncodingVersion); return ret as T; } internal void CommandHandler(RtmpController controller, CommandMessage command) { MethodInfo method = null; object[] arguments = null; try { _rpcService.PrepareMethod(controller, command, out method, out arguments); var result = method.Invoke(controller, arguments); if (result != null) { var resType = method.ReturnType; if (resType.IsGenericType && resType.GetGenericTypeDefinition() == typeof(Task<>)) { var tsk = result as Task; tsk.ContinueWith(t => { var taskResult = resType.GetProperty("Result").GetValue(result); var retCommand = new ReturnResultCommandMessage(command.AmfEncodingVersion); retCommand.IsSuccess = true; retCommand.TranscationID = command.TranscationID; retCommand.CommandObject = null; retCommand.ReturnValue = taskResult; _ = controller.MessageStream.SendMessageAsync(controller.ChunkStream, retCommand); }, TaskContinuationOptions.OnlyOnRanToCompletion); tsk.ContinueWith(t => { var exception = tsk.Exception; var retCommand = new ReturnResultCommandMessage(command.AmfEncodingVersion); retCommand.IsSuccess = false; retCommand.TranscationID = command.TranscationID; retCommand.CommandObject = null; retCommand.ReturnValue = exception.Message; _ = controller.MessageStream.SendMessageAsync(controller.ChunkStream, retCommand); }, TaskContinuationOptions.OnlyOnFaulted); } else if (resType == typeof(Task)) { var tsk = result as Task; tsk.ContinueWith(t => { var exception = tsk.Exception; var retCommand = new ReturnResultCommandMessage(command.AmfEncodingVersion); retCommand.IsSuccess = false; retCommand.TranscationID = command.TranscationID; retCommand.CommandObject = null; retCommand.ReturnValue = exception.Message; _ = controller.MessageStream.SendMessageAsync(controller.ChunkStream, retCommand); }, TaskContinuationOptions.OnlyOnFaulted); } else if (resType != typeof(void)) { var retCommand = new ReturnResultCommandMessage(command.AmfEncodingVersion); retCommand.IsSuccess = true; retCommand.TranscationID = command.TranscationID; retCommand.CommandObject = null; retCommand.ReturnValue = result; _ = controller.MessageStream.SendMessageAsync(controller.ChunkStream, retCommand); } } } catch (Exception e) { var retCommand = new ReturnResultCommandMessage(command.AmfEncodingVersion); retCommand.IsSuccess = false; retCommand.TranscationID = command.TranscationID; retCommand.CommandObject = null; retCommand.ReturnValue = e.Message; _ = controller.MessageStream.SendMessageAsync(controller.ChunkStream, retCommand); return; } } internal bool FindController(string appName, out Type controllerType) { return IOPipeline.Options.RegisteredControllers.TryGetValue(appName.ToLower(), out controllerType); } public void Close() { IOPipeline.Disconnect(); } private RtmpMessageStream CreateMessageStream() { var stream = new RtmpMessageStream(this); MessageStreamCreated(stream); return stream; } public RtmpChunkStream CreateChunkStream() { return new RtmpChunkStream(this); } internal void ChunkStreamDestroyed(RtmpChunkStream rtmpChunkStream) { lock (_allocCsidLocker) { _allocatedCsid.Remove(rtmpChunkStream.ChunkStreamId); } } internal Task SendMessageAsync(uint chunkStreamId, Message message) { return IOPipeline.ChunkStreamContext.MultiplexMessageAsync(chunkStreamId, message); } internal void MessageStreamCreated(RtmpMessageStream messageStream) { _messageStreams[messageStream.MessageStreamId] = messageStream; } internal void MessageStreamDestroying(RtmpMessageStream messageStream) { _messageStreams.Remove(messageStream.MessageStreamId); } internal void MessageArrived(Message message) { if (_messageStreams.TryGetValue(message.MessageHeader.MessageStreamId.Value, out var stream)) { stream.MessageArrived(message); } else { Console.WriteLine($"Warning: aborted message stream id: {message.MessageHeader.MessageStreamId}"); } } internal void Acknowledgement(uint bytesReceived) { _ = ControlMessageStream.SendMessageAsync(ControlChunkStream, new AcknowledgementMessage() { BytesReceived = bytesReceived }); } private void HandleSetPeerBandwidth(SetPeerBandwidthMessage message) { if (IOPipeline.ChunkStreamContext.WriteWindowAcknowledgementSize.HasValue && message.LimitType == LimitType.Soft && message.WindowSize > IOPipeline.ChunkStreamContext.WriteWindowAcknowledgementSize) { return; } if (IOPipeline.ChunkStreamContext.PreviousLimitType.HasValue && message.LimitType == LimitType.Dynamic && IOPipeline.ChunkStreamContext.PreviousLimitType != LimitType.Hard) { return; } IOPipeline.ChunkStreamContext.PreviousLimitType = message.LimitType; IOPipeline.ChunkStreamContext.WriteWindowAcknowledgementSize = message.WindowSize; SendControlMessageAsync(new WindowAcknowledgementSizeMessage() { WindowSize = message.WindowSize }); } private void HandleWindowAcknowledgementSize(WindowAcknowledgementSizeMessage message) { IOPipeline.ChunkStreamContext.ReadWindowAcknowledgementSize = message.WindowSize; } private void HandleSetChunkSize(SetChunkSizeMessage setChunkSize) { IOPipeline.ChunkStreamContext.ReadChunkSize = (int)setChunkSize.ChunkSize; } public Task SendControlMessageAsync(Message message) { if (message.MessageHeader.MessageType == MessageType.WindowAcknowledgementSize) { IOPipeline.ChunkStreamContext.WriteWindowAcknowledgementSize = ((WindowAcknowledgementSizeMessage)message).WindowSize; } return ControlMessageStream.SendMessageAsync(ControlChunkStream, message); } #region IDisposable Support private bool disposedValue = false; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { NetConnection.Dispose(); ControlChunkStream.Dispose(); ControlMessageStream.Dispose(); } disposedValue = true; } } // ~RtmpSession() { // Dispose(false); // } public void Dispose() { Dispose(true); // GC.SuppressFinalize(this); } #endregion } } ================================================ FILE: Harmonic/Networking/Rtmp/Serialization/OptionalArgumentAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Serialization { [AttributeUsage(AttributeTargets.Property)] public class OptionalArgumentAttribute : Attribute { } } ================================================ FILE: Harmonic/Networking/Rtmp/Serialization/RtmpCommandAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Serialization { [AttributeUsage(AttributeTargets.Class)] public class RtmpCommandAttribute : Attribute { public string Name { get; set; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Serialization/RtmpMessageAttribute.cs ================================================ using Harmonic.Networking.Rtmp.Data; using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Harmonic.Networking.Rtmp.Serialization { [AttributeUsage(AttributeTargets.Class)] public class RtmpMessageAttribute : Attribute { public RtmpMessageAttribute(params MessageType[] messageTypes) { MessageTypes = messageTypes.ToList(); } internal List MessageTypes { get; set; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Serialization/SerializationContext.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Serialization { public class SerializationContext { public Amf3Reader Amf3Reader { get; internal set; } = null; public Amf3Writer Amf3Writer { get; internal set; } = null; public Amf0Reader Amf0Reader { get; internal set; } = null; public Amf0Writer Amf0Writer { get; internal set; } = null; public ByteBuffer WriteBuffer { get; internal set; } = null; public Memory ReadBuffer { get; internal set; } = null; } } ================================================ FILE: Harmonic/Networking/Rtmp/Serialization/UserControlMessageAttribute.cs ================================================ using Harmonic.Networking.Rtmp.Messages.UserControlMessages; using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Serialization { public class UserControlMessageAttribute : Attribute { public UserControlEventType Type { get; set; } } } ================================================ FILE: Harmonic/Networking/Rtmp/Streaming/PublishingType.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp.Streaming { public enum PublishingType { [PublishingTypeName("")] None, [PublishingTypeName("live")] Live, [PublishingTypeName("record")] Record, [PublishingTypeName("append")] Append } } ================================================ FILE: Harmonic/Networking/Rtmp/Streaming/PublishingTypeNameAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace Harmonic.Networking.Rtmp.Streaming { [AttributeUsage(AttributeTargets.Field)] public class PublishingTypeNameAttribute : Attribute { public string Name { get; set; } public PublishingTypeNameAttribute(string name) { Name = name; } } public static class PublishingHelpers { public static IReadOnlyDictionary PublishingTypes { get; } static PublishingHelpers() { var types = new Dictionary(); var enumType = typeof(PublishingType); var members = Enum.GetNames(enumType).Select(n => enumType.GetMember(n).First()).ToArray(); foreach (var member in members) { var name = member.GetCustomAttribute().Name; types.Add(name, (PublishingType)Enum.Parse(enumType, member.Name)); } PublishingTypes = types; } public static bool IsTypeSupported(string type) { return PublishingTypes.ContainsKey(type); } } } ================================================ FILE: Harmonic/Networking/Rtmp/Supervisor.cs ================================================ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Harmonic.Hosting; namespace Harmonic.NetWorking { public class Supervisor { class SessionState { public CancellationTokenSource CancellationTokenSource; public DateTime LastPing; public IStreamSession Session; } private Thread thread = null; private readonly RtmpServer server = null; private CancellationToken cancellationToken = default; private TimeSpan pingInterval = default; private TimeSpan responseThreshole = default; private Dictionary sessionStates = new Dictionary(); public Supervisor(RtmpServer server) { this.server = server; } public void StartAsync(TimeSpan pingInterval, TimeSpan responseThreshole, CancellationToken ct = default) { if (thread != null) { throw new InvalidOperationException("already started"); } cancellationToken = ct; this.pingInterval = pingInterval; this.responseThreshole = responseThreshole; thread = new Thread(ThreadEntry); //thread.Start(); } private void ThreadEntry() { try { while (!cancellationToken.IsCancellationRequested) { cancellationToken.ThrowIfCancellationRequested(); foreach (var kv in server.connectedSessions) { if (!sessionStates.TryGetValue(kv.Key, out var sessionState)) { sessionState = new SessionState() { LastPing = DateTime.Now, Session = kv.Value }; sessionStates.Add(kv.Key, sessionState); } var session = kv.Value; if (DateTime.Now - sessionState.LastPing >= pingInterval && sessionState.CancellationTokenSource == null) { sessionState.CancellationTokenSource = new CancellationTokenSource(); sessionState.CancellationTokenSource.CancelAfter((int)responseThreshole.TotalMilliseconds); var pingTask = session.PingAsync(sessionState.CancellationTokenSource.Token); pingTask.ContinueWith(tsk => { sessionState.CancellationTokenSource.Dispose(); sessionState.CancellationTokenSource = null; sessionState.LastPing = DateTime.Now; }, TaskContinuationOptions.OnlyOnRanToCompletion); pingTask.ContinueWith(tsk => { sessionState.Session.Disconnect(new ExceptionalEventArgs("pingpong timeout")); sessionState.CancellationTokenSource.Dispose(); sessionState.CancellationTokenSource = null; }, TaskContinuationOptions.OnlyOnCanceled); } } Thread.Sleep(1); cancellationToken.ThrowIfCancellationRequested(); } } catch { } } } } ================================================ FILE: Harmonic/Networking/Rtmp/WriteState.cs ================================================ using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace Harmonic.Networking.Rtmp { class WriteState { public byte[] Buffer; public int Length; public TaskCompletionSource TaskSource = null; } } ================================================ FILE: Harmonic/Networking/Utils/NetworkBitConverter.cs ================================================ using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace Harmonic.Networking.Utils { public static class NetworkBitConverter { private static MemoryPool _memoryPool = MemoryPool.Shared; public static int ToInt32(Span buffer, bool littleEndian = false) { if (!littleEndian) { buffer.Slice(0, sizeof(int)).Reverse(); } return BitConverter.ToInt32(buffer); } public static uint ToUInt32(Span buffer, bool littleEndian = false) { if (!littleEndian) { buffer.Slice(0, sizeof(uint)).Reverse(); } return BitConverter.ToUInt32(buffer); } public static ulong ToUInt64(Span buffer, bool littleEndian = false) { if (!littleEndian) { buffer.Slice(0, sizeof(ulong)).Reverse(); } return BitConverter.ToUInt64(buffer); } public static ushort ToUInt16(Span buffer, bool littleEndian = false) { if (!littleEndian) { buffer.Slice(0, sizeof(ushort)).Reverse(); } return BitConverter.ToUInt16(buffer); } public static uint ToUInt24(ReadOnlySpan buffer, bool littleEndian = false) { using (var owner = _memoryPool.Rent(4)) { var memory = owner.Memory.Slice(0, 4); memory.Span.Clear(); buffer.CopyTo(memory.Span.Slice(1)); if (!littleEndian) { memory.Span.Reverse(); } return BitConverter.ToUInt32(memory.Span); } } public static double ToDouble(Span buffer, bool littleEndian = false) { if (!littleEndian) { buffer.Slice(0, sizeof(double)).Reverse(); } return BitConverter.ToDouble(buffer); } public static bool TryGetUInt24Bytes(uint value, Span buffer, bool littleEndian = false) { if (buffer.Length < 3) { return false; } using (var owner = _memoryPool.Rent(4)) { if (!BitConverter.TryWriteBytes(owner.Memory.Span, value)) { return false; } var valueSpan = owner.Memory.Span.Slice(0, 3); if (!littleEndian) { valueSpan.Reverse(); } valueSpan.CopyTo(buffer); } return true; } public static bool TryGetBytes(int value, Span buffer, bool littleEndian = false) { if (!BitConverter.TryWriteBytes(buffer, value)) { return false; } if (!littleEndian) { buffer.Slice(0, sizeof(int)).Reverse(); } return true; } public static bool TryGetBytes(double value, Span buffer, bool littleEndian = false) { if (!BitConverter.TryWriteBytes(buffer, value)) { return false; } if (!littleEndian) { buffer.Slice(0, sizeof(double)).Reverse(); } return true; } public static bool TryGetBytes(uint value, Span buffer, bool littleEndian = false) { if (!BitConverter.TryWriteBytes(buffer, value)) { return false; } if (!littleEndian) { buffer.Slice(0, sizeof(uint)).Reverse(); } return true; } public static bool TryGetBytes(byte value, Span buffer) { if (buffer.Length < 1) { return false; } buffer[0] = value; return true; } public static bool TryGetBytes(ushort value, Span buffer, bool littleEndian = false) { if (!BitConverter.TryWriteBytes(buffer, value)) { return false; } if (!littleEndian) { buffer.Slice(0, sizeof(ushort)).Reverse(); } return true; } public static bool TryGetBytes(ulong value, Span buffer, bool littleEndian = false) { if (!BitConverter.TryWriteBytes(buffer, value)) { return false; } if (!littleEndian) { buffer.Slice(0, sizeof(ulong)).Reverse(); } return true; } } } ================================================ FILE: Harmonic/Networking/Utils/StreamHelper.cs ================================================ using System; using System.Buffers; using System.IO; using System.Threading; using System.Threading.Tasks; namespace Harmonic.Networking { static class StreamHelper { public static byte[] ReadBytes(this Stream stream, int count) { if (stream == null) throw new ArgumentNullException(nameof(stream)); var result = new byte[count]; var bytesRead = 0; while (count > 0) { var n = stream.Read(result, bytesRead, count); if (n == 0) break; bytesRead += n; count -= n; } if (bytesRead != result.Length) { throw new EndOfStreamException(); } return result; } public static async Task ReadBytesAsync(this Stream stream, Memory buffer, CancellationToken ct = default) { int count = buffer.Length; var offset = 0; while (count != 0) { var n = await stream.ReadAsync(buffer.Slice(offset, count), ct); ct.ThrowIfCancellationRequested(); if (n == 0) { break; } offset += n; count -= n; } if (offset != buffer.Length) { throw new EndOfStreamException(); } } } } ================================================ FILE: Harmonic/Networking/WebSocket/WebSocketSession.cs ================================================ using Autofac; using Fleck; using Harmonic.Buffers; using Harmonic.Controllers; using Harmonic.Hosting; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Rtmp.Messages; using Harmonic.Networking.Rtmp.Serialization; using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Harmonic.Networking.Utils; using Harmonic.Networking.Rtmp.Data; using System.Linq; using Harmonic.Networking.Flv; using System.Threading.Tasks; using System.Web; namespace Harmonic.Networking.WebSocket { public class WebSocketSession { private IWebSocketConnection _webSocketConnection = null; private WebSocketOptions _options = null; private WebSocketController _controller = null; private FlvMuxer _flvMuxer = null; public RtmpServerOptions Options => _options._serverOptions; public WebSocketSession(IWebSocketConnection connection, WebSocketOptions options) { _webSocketConnection = connection; _options = options; _flvMuxer = new FlvMuxer(); } public Task SendRawDataAsync(byte[] data) { return _webSocketConnection.Send(data); } public void Close() { _webSocketConnection.Close(); } public void SendString(string str) { _webSocketConnection.Send(str); } internal void HandleOpen() { try { var path = _webSocketConnection.ConnectionInfo.Path; var match = _options.UrlMapping.Match(path); var streamName = match.Groups["streamName"].Value; var controllerName = match.Groups["controller"].Value; var query = ""; var idx = path.IndexOf('?'); if (idx != -1) { query = path.Substring(idx); } if (!_options._controllers.TryGetValue(controllerName.ToLower(), out var controllerType)) { _webSocketConnection.Close(); } _controller = _options._serverOptions.ServerLifetime.Resolve(controllerType) as WebSocketController; _controller.Query = HttpUtility.ParseQueryString(query); _controller.StreamName = streamName; _controller.Session = this; _controller.OnConnect().ContinueWith(_ => { _webSocketConnection.Close(); }, TaskContinuationOptions.OnlyOnFaulted); ; } catch { _webSocketConnection.Close(); } } public Task SendFlvHeaderAsync(bool hasAudio, bool hasVideo) { return SendRawDataAsync(_flvMuxer.MultiplexFlvHeader(hasAudio, hasVideo)); } public Task SendMessageAsync(Message data) { return SendRawDataAsync(_flvMuxer.MultiplexFlv(data)); } internal void HandleClose() { if (_controller is IDisposable disp) { disp.Dispose(); } _controller = null; } internal void HandleMessage(string msg) { _controller?.OnMessage(msg); } } } ================================================ FILE: Harmonic/Rpc/CommandObjectAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Networking.Rtmp { [AttributeUsage(AttributeTargets.Parameter)] public class CommandObjectAttribute : Attribute { } } ================================================ FILE: Harmonic/Rpc/FromCommandObjectAttribute.cs ================================================ using System; namespace Harmonic.Rpc { [AttributeUsage(AttributeTargets.Parameter)] public class FromCommandObjectAttribute : Attribute { public FromCommandObjectAttribute(string key = null) { Key = key; } public string Key { get; set; } = null; } } ================================================ FILE: Harmonic/Rpc/FromOptionalArgumentAttribute.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Rpc { [AttributeUsage(AttributeTargets.Parameter)] public class FromOptionalArgumentAttribute : Attribute { } } ================================================ FILE: Harmonic/Rpc/RpcMethodAttribute.cs ================================================ using System; namespace Harmonic.Rpc { public class RpcMethodAttribute : Attribute { public string Name { get; set; } = null; public RpcMethodAttribute(string name = null) { Name = name; } } } ================================================ FILE: Harmonic/Rpc/RpcService.cs ================================================ using Harmonic.Controllers; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Rtmp; using Harmonic.Networking.Rtmp.Messages.Commands; using Harmonic.Networking.Rtmp.Serialization; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; namespace Harmonic.Rpc { internal class RpcParameter { public bool IsOptional; public bool IsFromCommandObject; public bool IsCommandObject; public bool IsFromOptionalArgument; public int OptionalArgumentIndex; public string CommandObjectKey; public Type ParameterType; } internal class RpcMethod { public string MethodName; public MethodInfo Method; public List Parameters = new List(); } internal class RpcService { public Dictionary> Controllers = new Dictionary>(); public void PrepareMethod(T instance, CommandMessage command, out MethodInfo methodInfo, out object[] callArguments) where T: RtmpController { if (!Controllers.TryGetValue(instance.GetType(), out var methods)) { throw new EntryPointNotFoundException(); } foreach (var method in methods) { if (method.MethodName != command.ProcedureName) { continue; } var arguments = new object[method.Parameters.Count]; var i = 0; foreach (var para in method.Parameters) { if (para.IsCommandObject) { arguments[i] = command.CommandObject; i++; } else if (para.IsFromCommandObject) { var commandObj = command.CommandObject; object val = null; if (!commandObj.Fields.TryGetValue(para.CommandObjectKey, out val) && !commandObj.DynamicFields.TryGetValue(para.CommandObjectKey, out val)) { if (para.IsOptional) { arguments[i] = Type.Missing; i++; } else { break; } } if (para.ParameterType.IsAssignableFrom(val.GetType())) { arguments[i] = val; i++; } else { if (para.IsOptional) { arguments[i] = Type.Missing; i++; } else { break; } } } else if (para.IsFromOptionalArgument) { var optionArguments = command.GetType().GetProperties().Where(p => p.GetCustomAttribute() != null).ToList(); if (para.OptionalArgumentIndex >= optionArguments.Count) { if (para.IsOptional) { arguments[i] = Type.Missing; i++; } else { break; } } else { arguments[i] = optionArguments[i].GetValue(command); i++; } } } if (i == arguments.Length) { methodInfo = method.Method; callArguments = arguments; return; } } throw new EntryPointNotFoundException(); } internal void CleanupRegistration() { foreach (var controller in Controllers) { var gps = controller.Value.GroupBy(m => m.MethodName).Where(gp => gp.Count() > 1); var hiddenMethods = new List(); foreach (var gp in gps) { hiddenMethods.AddRange(gp.Where(m => m.Method.DeclaringType != controller.Key)); } foreach (var m in hiddenMethods) { controller.Value.Remove(m); } } } internal void RegeisterController(Type controllerType) { var methods = controllerType.GetMethods(); foreach (var method in methods) { var attr = method.GetCustomAttribute(); if (attr != null) { var rpcMethod = new RpcMethod(); var methodName = attr.Name ?? method.Name; var parameters = method.GetParameters(); bool canInvoke = false; var optArgIndex = 0; foreach (var para in parameters) { var fromCommandObject = para.GetCustomAttribute(); var fromOptionalArg = para.GetCustomAttribute(); var commandObject = para.GetCustomAttribute(); if (fromCommandObject == null && fromOptionalArg == null && commandObject == null) { break; } canInvoke = true; if (fromCommandObject != null) { var name = fromCommandObject.Key ?? para.Name; rpcMethod.Parameters.Add(new RpcParameter() { CommandObjectKey = name, IsFromCommandObject = true, ParameterType = para.ParameterType, IsOptional = para.IsOptional }); } else if (fromOptionalArg != null) { rpcMethod.Parameters.Add(new RpcParameter() { OptionalArgumentIndex = optArgIndex, IsFromOptionalArgument = true, ParameterType = para.ParameterType, IsOptional = para.IsOptional }); optArgIndex++; } else if (commandObject != null && para.ParameterType.IsAssignableFrom(typeof(AmfObject))) { rpcMethod.Parameters.Add(new RpcParameter() { IsCommandObject = true, IsOptional = para.IsOptional }); } } if (canInvoke || !parameters.Any()) { rpcMethod.Method = method; rpcMethod.MethodName = methodName; if (!Controllers.TryGetValue(controllerType, out var mapping)) { Controllers.Add(controllerType, new List()); } Controllers[controllerType].Add(rpcMethod); } } } } } } ================================================ FILE: Harmonic/Service/PublisherSessionService.cs ================================================ using System; using System.Linq; using System.Collections.Generic; using Harmonic.Controllers.Living; using Harmonic.Networking.Rtmp.Messages; namespace Harmonic.Service { public class PublisherSessionService { private Dictionary _pathMapToSession = new Dictionary(); private Dictionary _sessionMapToPath = new Dictionary(); internal void RegisterPublisher(string publishingName, LivingStream session) { if (_pathMapToSession.ContainsKey(publishingName)) { throw new InvalidOperationException("request instance is publishing"); } if (_sessionMapToPath.ContainsKey(session)) { throw new InvalidOperationException("request session is publishing"); } _pathMapToSession.Add(publishingName, session); _sessionMapToPath.Add(session, publishingName); } internal void RemovePublisher(LivingStream session) { if (_sessionMapToPath.TryGetValue(session, out var publishingName)) { _sessionMapToPath.Remove(session); _pathMapToSession.Remove(publishingName); } } public LivingStream FindPublisher(string publishingName) { if (_pathMapToSession.TryGetValue(publishingName, out var session)) { return session; } return null; } } } ================================================ FILE: Harmonic/Service/RecordService.cs ================================================ using System; using System.Collections.Generic; using System.IO; using System.Text; namespace Harmonic.Service { public class RecordService { private RecordServiceConfiguration _configuration; public RecordService(RecordServiceConfiguration configuration) { _configuration = configuration; } public string GetRecordFilename(string streamName) { return Path.Combine(_configuration.RecordPath, _configuration.FilenameFormat.Replace("{streamName}", streamName)); } } } ================================================ FILE: Harmonic/Service/RecordServiceConfiguration.cs ================================================ using System; using System.Collections.Generic; using System.Text; namespace Harmonic.Service { public class RecordServiceConfiguration { public virtual string RecordPath { get; set; } = @"Record"; public virtual string FilenameFormat { get; set; } = @"recorded-{streamName}"; } } ================================================ FILE: Harmonic.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.352 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Harmonic", "Harmonic\Harmonic.csproj", "{687B914B-94EB-417A-992B-0B1EFD0623C2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest", "UnitTest\UnitTest.csproj", "{A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "demo", "demo\demo.csproj", "{986D043B-34A6-4DE9-949E-98472E65EC18}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {687B914B-94EB-417A-992B-0B1EFD0623C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Debug|x86.ActiveCfg = Debug|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Debug|x86.Build.0 = Debug|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Release|Any CPU.Build.0 = Release|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Release|x86.ActiveCfg = Release|Any CPU {687B914B-94EB-417A-992B-0B1EFD0623C2}.Release|x86.Build.0 = Release|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Debug|x86.ActiveCfg = Debug|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Debug|x86.Build.0 = Debug|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Release|Any CPU.Build.0 = Release|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Release|x86.ActiveCfg = Release|Any CPU {A79FD57F-233E-4DB9-8A29-79F24ADE5EE8}.Release|x86.Build.0 = Release|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Debug|Any CPU.Build.0 = Debug|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Debug|x86.ActiveCfg = Debug|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Debug|x86.Build.0 = Debug|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Release|Any CPU.ActiveCfg = Release|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Release|Any CPU.Build.0 = Release|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Release|x86.ActiveCfg = Release|Any CPU {986D043B-34A6-4DE9-949E-98472E65EC18}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {62AAD21C-2BDF-4259-A92E-C9A9BB35716B} EndGlobalSection EndGlobal ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 a1q123456 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ master is not a stable branch, you may want to see the latest [tag](https://github.com/a1q123456/rtmp-sharp-server/tree/v0.0.1) # Harmonic A high performance RTMP live streaming application framework # Getting started ## Code Program.cs ```csharp using Harmonic.Hosting; using System; using System.Net; namespace demo { class Program { static void Main(string[] args) { RtmpServer server = new RtmpServerBuilder() .UseStartup() .Build(); var tsk = server.StartAsync(); tsk.Wait(); } } } ``` StartUp.cs ```csharp using Autofac; using Harmonic.Hosting; namespace demo { class Startup : IStartup { public void ConfigureServices(ContainerBuilder builder) { } } } ``` Build a server like this to support websocket-flv transmission ```csharp RtmpServer server = new RtmpServerBuilder() .UseStartup() .UseWebSocket(c => { c.BindEndPoint = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 8080); }) .Build(); ``` ## push video file using ffmpeg ```bash ffmpeg -i test.mp4 -f flv -vcodec h264 -acodec aac "rtmp://127.0.0.1/living/streamName" ``` ## play rtmp stream using ffplay ```bash ffplay "rtmp://127.0.0.1/living/streamName" ``` ## play flv stream using [flv.js](https://github.com/Bilibili/flv.js) by websocket ```html ``` # Dive in deep You can view docs [here](docs/README.md) ================================================ FILE: RoadMap.md ================================================ # Harmonic a high performance RTMP streaming application framework # Road map - [x] Rtmp muxer/demuxer - [x] Amf0 serializer/deserializer - [x] Amf3 serialzier/deserializer - [x] Rtmp message serializier/deserializer - [x] Live Streamingseeking - [x] Flv on websocket support - [x] Unittest - [x] Flv muxer/demuxer - [x] Video recorder - [x] Video file playing - [x] websocket-flv seeking support - [ ] Rtmp on Udp support ================================================ FILE: UnitTest/TestAmf0Reader.cs ================================================ using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Data; using Harmonic.Networking.Amf.Serialization.Amf0; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace UnitTest { [TestClass] public class TestAmf0Reader { [TestMethod] public void TestReadNumber() { var reader = new Amf0Reader(); var files = Directory.GetFiles("../../../../samples/amf0/number"); foreach (var file in files) { var value = double.Parse(Path.GetFileNameWithoutExtension(file)); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetNumber(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadString() { var reader = new Amf0Reader(); var files = Directory.GetFiles("../../../../samples/amf0/string"); foreach (var file in files) { var value = Path.GetFileNameWithoutExtension(file); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetString(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadBoolean() { var reader = new Amf0Reader(); var files = Directory.GetFiles("../../../../samples/amf0/boolean"); foreach (var file in files) { var value = bool.Parse(Path.GetFileNameWithoutExtension(file)); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetBoolean(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadArray() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/array.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); var arrayData = new List { 1.0d, 2.0d, 3.0d, 4.0d, "a", "asdf", "eee" }; Assert.IsTrue(reader.TryGetStrictArray(data, out var dataRead, out var consumed)); Assert.IsTrue(arrayData.SequenceEqual(dataRead)); Assert.AreEqual(consumed, data.Length); } } [TestMethod] public void TestReadDate() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/date.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetDate(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead.Year, 2019); Assert.AreEqual(dataRead.Month, 2); Assert.AreEqual(dataRead.Day, 11); Assert.AreEqual(consumed, data.Length); } } [TestMethod] public void TestReadLongString() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/longstring.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetLongString(data, out var dataRead, out var consumed)); Assert.AreEqual(string.Concat(Enumerable.Repeat("abc", 32767)), dataRead); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadNull() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/null.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetNull(data, out var dataRead, out var consumed)); Assert.AreEqual(null, dataRead); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadNull2() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/null.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetObject(data, out var dataRead, out var consumed)); Assert.AreEqual(null, dataRead); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadXml() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/xml.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetXmlDocument(data, out var dataRead, out var consumed)); Assert.AreNotEqual(dataRead.GetElementsByTagName("a").Count, 0); Assert.AreNotEqual(dataRead.GetElementsByTagName("b").Count, 0); Assert.IsNotNull(dataRead.GetElementsByTagName("b")[0].Attributes["value"], "1"); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadUndefined() { var reader = new Amf0Reader(); using (var f = new FileStream("../../../../samples/amf0/misc/undefined.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetUndefined(data, out var dataRead, out var consumed)); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadEcmaArray() { var reader = new Amf0Reader(); // pyamf has a bug about element count of ecma array // https://github.com/hydralabs/pyamf/blob/master/pyamf/amf0.py#L567 reader.StrictMode = false; using (var f = new FileStream("../../../../samples/amf0/misc/ecmaarray.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetEcmaArray(data, out var dataRead, out var consumed)); Assert.IsTrue(dataRead.SequenceEqual(new Dictionary() { ["a"] = 1.0d, ["b"] = "a", ["c"] = "a" })); Assert.AreEqual(consumed, data.Length); } } [TestMethod] public void TestReadObject() { var reader = new Amf0Reader(); // pyamf has a bug about element count of ecma array // https://github.com/hydralabs/pyamf/blob/master/pyamf/amf0.py#L567 reader.StrictMode = false; using (var f = new FileStream("../../../../samples/amf0/misc/object.amf0", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetObject(data, out var dataRead, out var consumed)); Assert.IsTrue(dataRead.Fields.SequenceEqual(new Dictionary() { ["a"] = "b", ["c"] = 1.0 })); Assert.AreEqual(consumed, data.Length); } } [TestMethod] public void TestPacket() { var reader = new Amf0Reader(); reader.RegisterType(); reader.StrictMode = false; using (var file = new FileStream("../../../../samples/amf0/misc/packet.amf0", FileMode.Open)) { var data = new byte[file.Length]; file.Read(data); Assert.IsTrue(reader.TryGetPacket(data, out var headers, out var messages, out var consumed)); Assert.AreEqual(consumed, file.Length); } } } } ================================================ FILE: UnitTest/TestAmf0Writer.cs ================================================ using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Data; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Attributes; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Xml; namespace UnitTest { [TestClass] public class TestAmf0Writer { [TestMethod] public void TestNumber() { var random = new Random(); var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { for (int i = 0; i < 1000; i++) { var num = random.NextDouble() * 10 - 5; writer.WriteBytes(num, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetNumber(buffer, out var readValue, out var consumed); Assert.AreEqual(num, readValue); Assert.AreEqual(buffer.Length, consumed); } } } [TestMethod] public void TestString() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { for (int i = 0; i < 1000; i++) { var val = Guid.NewGuid().ToString(); writer.WriteBytes(val, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetString(buffer, out var readValue, out var consumed); Assert.AreEqual(val, readValue); Assert.AreEqual(buffer.Length, consumed); } } } [TestMethod] public void TestLongString() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { for (int i = 0; i < 1000; i++) { var val = string.Concat(Enumerable.Repeat(Guid.NewGuid().ToString(), 2000)); writer.WriteBytes(val, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetLongString(buffer, out var readValue, out var consumed); Assert.AreEqual(val, readValue); Assert.AreEqual(buffer.Length, consumed); } } } [TestMethod] public void TestDate() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); var date = DateTime.Now; using (var sc = new SerializationContext()) { writer.WriteBytes(date, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetDate(buffer, out var val, out var consumed)); Assert.AreEqual(val.Date, date.Date); Assert.AreEqual(val.Hour, date.Hour); Assert.AreEqual(val.Minute, date.Minute); Assert.AreEqual(val.Second, date.Second); Assert.AreEqual(val.Millisecond, date.Millisecond); Assert.AreEqual(val.Kind, date.Kind); Assert.AreEqual(consumed, buffer.Length); } } [TestMethod] public void TestBoolean() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { writer.WriteBytes(true, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetBoolean(buffer, out var val, out var consumed)); Assert.IsTrue(val); Assert.AreEqual(consumed, buffer.Length); writer.WriteBytes(false, sc); buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetBoolean(buffer, out val, out consumed)); Assert.IsFalse(val); Assert.AreEqual(consumed, buffer.Length); } } [TestMethod] public void TestArray() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); var array = new List() { 1, 3.0, "string", new DateTime(2019, 2, 11), false, new List() { null, 3, "string2", "string2" } }; using (var sc = new SerializationContext()) { writer.WriteBytes(array, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetStrictArray(buffer, out var val, out var consumed)); Assert.IsTrue((double)val[0] == 1.0); Assert.IsTrue((double)val[1] == 3.0); Assert.IsTrue((string)val[2] == "string"); Assert.IsTrue((DateTime)val[3] == new DateTime(2019, 2, 11)); Assert.IsTrue((bool)val[4] == false); var e5 = (List)val[5]; Assert.IsTrue(e5[0] == null); Assert.IsTrue((double)e5[1] == 3.0); Assert.IsTrue((string)e5[2] == "string2"); Assert.IsTrue((string)e5[3] == "string2"); } } [TestMethod] public void TestEcmaArray() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); var array = new Dictionary() { ["1"] = 1.0, ["2"] = 2.0, ["3"] = "a", ["4"] = "a" }; using (var sc = new SerializationContext()) { writer.WriteBytes(array, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); // EcmaMarker:byte + ElementCount: uint + // StringLength: ushort + StringContent: byte * 1 + NumberMarker: byte + Number: double + // StringLength: ushort + StringContent: byte * 1 + NumberMarker: byte + Number: double // StringLength: ushort + StringContent: byte * 1 + StringMarker: byte + StringLength: ushort + StringContent: byte * 1 + // StringLength: ushort + StringContent: byte * 1 + ReferenceMarker: byte + ReferenceIndex: ushort + // StringLength: ushort + StringConent: byte * 0 + ObjectEndMarker: byte Assert.AreEqual(buffer.Length, 45); Assert.IsTrue(reader.TryGetEcmaArray(buffer, out var readData, out var consumed)); Assert.IsTrue(readData.SequenceEqual(array)); } } [TestMethod] public void TestNull() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { writer.WriteNullBytes(sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetNull(buffer, out var nullObj, out var consunmed)); Assert.IsNull(nullObj); Assert.AreEqual(consunmed, buffer.Length); } } [TestMethod] public void TestUndefined() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { writer.WriteBytes(new Undefined(), sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetUndefined(buffer, out var ud, out var consunmed)); Assert.IsNotNull(ud); Assert.AreEqual(consunmed, buffer.Length); } } [TestMethod] public void TestXml() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); using (var sc = new SerializationContext()) { var xml = new XmlDocument(); var elem = xml.CreateElement("price"); xml.AppendChild(elem); writer.WriteBytes(xml, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetXmlDocument(buffer, out var ud, out var consunmed)); Assert.IsNotNull(ud); Assert.AreNotEqual(ud.GetElementsByTagName("price").Count, 0); Assert.AreEqual(consunmed, buffer.Length); } } [TestMethod] public void TestObject() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); object nullVal = null; var refVal = new List() { 1, 2, "test" }; var obj = new AmfObject { { "c" , 1.0}, {"test" , false}, {"test2" , nullVal}, {"test3" , new Undefined()}, {"test4" , refVal}, {"test5" , "test"}, {"test6" , refVal } }; using (var sc = new SerializationContext()) { writer.WriteBytes(obj, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); // test reference table is working Assert.AreEqual(buffer.Length, 97); Assert.IsTrue(reader.TryGetObject(buffer, out var readObj, out var consumed)); Assert.AreEqual(consumed, buffer.Length); } } [TypedObject(Name = "Another.Name")] class TypedClass : IDynamicObject { [ClassField] public double c { get; set; } [ClassField] public bool test { get; set; } [ClassField] public object test2 { get; set; } [ClassField] public Undefined test3 { get; set; } [ClassField] public List test4 { get; set; } [ClassField] public string test5 { get; set; } [ClassField] public List test6 { get; set; } private Dictionary _dynamicFields = new Dictionary(); public IReadOnlyDictionary DynamicFields => _dynamicFields; public void AddDynamic(string key, object data) { _dynamicFields.Add(key, data); } } [TestMethod] public void TestTypedObject() { var writer = new Amf0Writer(); var reader = new Amf0Reader(); reader.RegisterType(); object nullVal = null; var refVal = new List() { 1, 2, "test" }; var obj = new TypedClass() { c = 1.0, test = false, test2 = nullVal, test3 = new Undefined(), test4 = refVal, test5 = "test", test6 = refVal }; using (var sc = new SerializationContext()) { writer.WriteTypedBytes(obj, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetTypedObject(buffer, out var readObj, out var consumed)); Assert.AreEqual(consumed, buffer.Length); } } } } ================================================ FILE: UnitTest/TestAmf3Reader.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Data; using Harmonic.Networking.Amf.Serialization.Amf0; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Amf.Serialization.Attributes; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace UnitTest { [TypedObject(Name = "TestClass")] public class TestCls : IDynamicObject, IEquatable { [ClassField(Name = "t1")] public double T1 { get; set; } [ClassField(Name = "t2")] public string T2 { get; set; } [ClassField(Name = "t3")] public string T3 { get; set; } [ClassField] public Vector t4 { get; set; } private Dictionary _dynamicFields = new Dictionary(); public IReadOnlyDictionary DynamicFields => _dynamicFields; public void AddDynamic(string key, object data) { _dynamicFields.Add(key, data); } public bool Equals(TestCls other) { return T1 == other.T1 && T2 == other.T2 && T3 == other.T3 && (t4 != null ? t4.Equals(other.t4) : t4 == other.t4) && _dynamicFields.SequenceEqual(other._dynamicFields); } public override bool Equals(object obj) { if (obj is TestCls to) { return Equals(to); } return base.Equals(obj); } } public class iexternalizable : IExternalizable { public double v1; public int v2; public bool TryEncodeData(ByteBuffer buffer) { var b1 = BitConverter.GetBytes(v1); var b2 = BitConverter.GetBytes(v2); buffer.WriteToBuffer(b1); buffer.WriteToBuffer(b2); return true; } public bool TryDecodeData(Span buffer, out int consumed) { v1 = BitConverter.ToDouble(buffer); v2 = BitConverter.ToInt32(buffer.Slice(sizeof(double))); consumed = sizeof(double) + sizeof(int); return true; } } [TypedObject(Name = "flex.messaging.messages.RemotingMessage")] public class RemotingMessage : IDynamicObject { private Dictionary _dynamicFields = new Dictionary(); [ClassField(Name = "body")] public object Body { get; set; } [ClassField(Name = "clientId")] public object ClientId { get; set; } [ClassField(Name = "destination")] public object Destination { get; set; } [ClassField(Name = "headers")] public object Headers { get; set; } [ClassField(Name = "messageId")] public object MessageId { get; set; } [ClassField(Name = "operation")] public object Operation { get; set; } [ClassField(Name = "source")] public object Source { get; set; } [ClassField(Name = "timeToLive")] public object TimeToLive { get; set; } [ClassField(Name = "timestamp")] public object Timestamp { get; set; } public IReadOnlyDictionary DynamicFields { get => _dynamicFields; } public void AddDynamic(string key, object data) { _dynamicFields.Add(key, data); } } [TestClass] public class TestAmf3Reader { [TestMethod] public void TestReadNumber() { var reader = new Amf3Reader(); var files = Directory.GetFiles("../../../../samples/amf3/number"); foreach (var file in files) { var value = double.Parse(Path.GetFileNameWithoutExtension(file)); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetDouble(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadInteger() { var reader = new Amf3Reader(); var files = Directory.GetFiles("../../../../samples/amf3/intenger"); foreach (var file in files) { var value = uint.Parse(Path.GetFileNameWithoutExtension(file)); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetUInt29(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadString() { var reader = new Amf3Reader(); var files = Directory.GetFiles("../../../../samples/amf3/string"); foreach (var file in files) { var value = Path.GetFileNameWithoutExtension(file); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetString(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadBoolean() { var reader = new Amf3Reader(); var files = Directory.GetFiles("../../../../samples/amf3/boolean"); foreach (var file in files) { var value = bool.Parse(Path.GetFileNameWithoutExtension(file)); using (var f = new FileStream(file, FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetBoolean(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead, value); Assert.AreEqual(consumed, f.Length); } } } [TestMethod] public void TestReadPacket1() { var reader = new Amf0Reader(); reader.RegisterType(); reader.StrictMode = false; using (var file = new FileStream("../../../../samples/amf3/misc/packet.amf3", FileMode.Open)) { var data = new byte[file.Length]; file.Read(data); Assert.IsTrue(reader.TryGetPacket(data, out var headers, out var messages, out var consumed)); Assert.AreEqual(consumed, file.Length); } } [TestMethod] public void TestUndefined() { var reader = new Amf3Reader(); reader.RegisterTypedObject(); using (var file = new FileStream("../../../../samples/amf3/misc/undefined.amf3", FileMode.Open)) { var data = new byte[file.Length]; file.Read(data); Assert.IsTrue(reader.TryGetUndefined(data, out var value, out var consumed)); Assert.AreEqual(consumed, file.Length); } } [TestMethod] public void TestNull() { var reader = new Amf3Reader(); reader.RegisterTypedObject(); using (var file = new FileStream("../../../../samples/amf3/misc/null.amf3", FileMode.Open)) { var data = new byte[file.Length]; file.Read(data); Assert.IsTrue(reader.TryGetNull(data, out var value, out var consumed)); Assert.AreEqual(consumed, file.Length); } } [TestMethod] public void TestDate() { var reader = new Amf3Reader(); reader.RegisterTypedObject(); using (var file = new FileStream("../../../../samples/amf3/misc/date.amf3", FileMode.Open)) { var data = new byte[file.Length]; file.Read(data); Assert.IsTrue(reader.TryGetDate(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead.Year, 2019); Assert.AreEqual(dataRead.Month, 2); Assert.AreEqual(dataRead.Day, 11); Assert.AreEqual(consumed, file.Length); } } [TestMethod] public void TestReadObject() { var reader = new Amf3Reader(); using (var file = new FileStream("../../../../samples/amf3/misc/object.amf3", FileMode.Open)) { var data = new byte[file.Length]; file.Read(data); Assert.IsTrue(reader.TryGetObject(data, out var dataRead, out var consumed)); var obj = (AmfObject)dataRead; Assert.IsTrue(obj.Fields.SequenceEqual(new Dictionary() { ["t1"] = 1.0, ["t2"] = "aaa", ["t3"] = "aac" })); Assert.AreEqual(obj.DynamicFields["td"], "aacf"); var td2 = (Dictionary)obj.DynamicFields["td2"]; var keyList = td2.Keys.ToList(); var key0 = (string)keyList[0]; var key1 = (double)keyList[1]; var key2 = (Vector)keyList[2]; var key3 = (Vector)keyList[3]; var v0 = (double)td2[key0]; var v1 = (Vector)td2[key1]; var v2 = (Vector)td2[key2]; var v3 = (Vector)td2[key3]; Assert.AreEqual(key0, "test"); Assert.AreEqual(key1, 3); Assert.AreEqual(key2[0], 3.0); Assert.AreEqual(key2[1], 4.0); Assert.AreEqual(key2[2], 5.0); Assert.AreEqual(key3[0], (uint)32); Assert.AreEqual(key3[1], (uint)43); Assert.AreEqual(key3[2], (uint)54); Assert.AreEqual(v0, 1); Assert.AreEqual(v1[0], 2); Assert.AreEqual(v1[1], 3); Assert.AreEqual(v1[2], 4); Assert.AreEqual(v2[0], 2); Assert.AreEqual(v2[1], 3); Assert.AreEqual(v2[2], 4); Assert.AreEqual(v3[0], 2); Assert.AreEqual(v3[1], 3); Assert.AreEqual(v3[2], 4); Assert.AreEqual(consumed, file.Length); } } [TestMethod] public void TestReadXml() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/xml.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetXml(data, out var dataRead, out var consumed)); Assert.AreNotEqual(dataRead.GetElementsByTagName("a").Count, 0); Assert.AreNotEqual(dataRead.GetElementsByTagName("b").Count, 0); Assert.IsNotNull(dataRead.GetElementsByTagName("b")[0].Attributes["value"], "1"); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadXmlDocument() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/xml_document.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetXmlDocument(data, out var dataRead, out var consumed)); Assert.AreNotEqual(dataRead.GetElementsByTagName("a").Count, 0); Assert.AreEqual(dataRead.GetElementsByTagName("a")[0].Attributes["value"].Value, "1"); Assert.AreNotEqual(dataRead.GetElementsByTagName("b").Count, 0); Assert.AreEqual(dataRead.GetElementsByTagName("b")[0].FirstChild.Value, "2"); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadArray() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/array.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetArray(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead[0], 1.0); Assert.AreEqual(dataRead[1], "aa"); var v = (Vector)dataRead["aa"]; Assert.AreEqual(v[0], 1); Assert.AreEqual(v[1], 2); Assert.AreEqual(v[2], 3); Assert.IsInstanceOfType(dataRead["bb"], typeof(Dictionary)); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadDictionary() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/dictionary.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetDictionary(data, out var dataRead, out var consumed)); var keys = dataRead.Keys.ToList(); var k0 = keys[0]; var k2 = (AmfObject)keys[1]; var k1 = keys[2]; var v0 = dataRead[k0]; var v1 = (Vector)dataRead[k1]; var v2 = (Vector)dataRead[k2]; Assert.AreEqual(k0, "test"); Assert.AreEqual(k1, 3.0); Assert.AreEqual(k2.Fields["t1"], 1.0); Assert.AreEqual(k2.Fields["t2"], "aaa"); Assert.AreEqual(k2.Fields["t3"], "aac"); Assert.AreEqual(v0, 1.0); Assert.AreEqual(v1[0], 2); Assert.AreEqual(v1[1], 3); Assert.AreEqual(v1[2], 4); Assert.AreEqual(v2[0], 2); Assert.AreEqual(v2[1], 3); Assert.AreEqual(v2[2], 4); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadVectorInt() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/vector_int.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetVectorInt(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead[0], 1); Assert.AreEqual(dataRead[1], 2); Assert.AreEqual(dataRead[2], 3); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadVectorUint() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/vector_uint.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetVectorUint(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead[0], (uint)1); Assert.AreEqual(dataRead[1], (uint)2); Assert.AreEqual(dataRead[2], (uint)3); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadVectorDouble() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/vector_double.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetVectorDouble(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead[0], (double)1); Assert.AreEqual(dataRead[1], (double)2); Assert.AreEqual(dataRead[2], (double)3); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadVectorTyped() { var reader = new Amf3Reader(); reader.RegisterTypedObject(); using (var f = new FileStream("../../../../samples/amf3/misc/vector_typted_object.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetVectorObject(data, out var dataRead, out var consumed)); var v = (Vector)dataRead; var t = v[0]; Assert.AreEqual(t.T1, 1.0); Assert.AreEqual(t.T2, "aaa"); Assert.AreEqual(t.T3, "aac"); Assert.AreEqual(t.t4[0], 1); Assert.AreEqual(t.t4[1], 2); Assert.AreEqual(t.t4[2], 3); var t2 = v[1]; Assert.AreEqual(t2.T1, 1.0); Assert.AreEqual(t2.T2, "aaa"); Assert.AreEqual(t2.T3, "aac"); Assert.AreEqual(t2.t4[0], 1); Assert.AreEqual(t2.t4[1], 2); Assert.AreEqual(t2.t4[2], 3); Assert.AreEqual(consumed, f.Length); } } [TestMethod] public void TestReadVectorAny() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/vector_any_object.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetVectorObject(data, out var dataRead, out var consumed)); var v = (Vector)dataRead; var obj = (AmfObject)v[0]; Assert.AreEqual(obj.Fields["t1"], 1.0); Assert.AreEqual(obj.Fields["t2"], "aaa"); Assert.AreEqual(obj.Fields["t3"], "aac"); Assert.AreEqual(v[1], 2.0); Assert.AreEqual(consumed, data.Length); } } [TestMethod] public void TestReadByteArray() { var reader = new Amf3Reader(); using (var f = new FileStream("../../../../samples/amf3/misc/bytearray.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetByteArray(data, out var dataRead, out var consumed)); Assert.AreEqual(dataRead[0], (byte)1); Assert.AreEqual(dataRead[1], (byte)2); Assert.AreEqual(dataRead[2], (byte)3); Assert.AreEqual(consumed, data.Length); } } [TestMethod] public void TestReadExternalizable() { var reader = new Amf3Reader(); reader.RegisterExternalizable(); using (var f = new FileStream("../../../../samples/amf3/misc/externalizable.amf3", FileMode.Open)) { var data = new byte[f.Length]; f.Read(data); Assert.IsTrue(reader.TryGetObject(data, out var dataRead, out var consumed)); var ie = (iexternalizable)dataRead; Assert.AreEqual(ie.v1, 3.14); Assert.AreEqual(ie.v2, 333); Assert.AreEqual(consumed, data.Length); } } } } ================================================ FILE: UnitTest/TestAmf3Writer.cs ================================================ using Harmonic.Buffers; using Harmonic.Networking.Amf.Attributes; using Harmonic.Networking.Amf.Common; using Harmonic.Networking.Amf.Data; using Harmonic.Networking.Amf.Serialization.Amf3; using Harmonic.Networking.Amf.Serialization.Attributes; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Xml; namespace UnitTest { [TestClass] public class TestAmf3Writer { [TestMethod] public void TestDouble() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); var random = new Random(); using (var sc = new SerializationContext()) { for (int i = 0; i < 1000; i++) { var value = random.NextDouble(); writer.WriteBytes(value, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetDouble(buffer, out var readValue, out var consumed); Assert.AreEqual(readValue, value); Assert.AreEqual(consumed, buffer.Length); } } } [TestMethod] public void TestInteger() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); var backend = new byte[5]; using (var sc = new SerializationContext()) { for (int i = 0; i <= Amf3Writer.U29MAX; i += 0xFF) { var value = (uint)i; writer.WriteBytes(value, sc); var buffer = backend.AsSpan(0, sc.MessageLength); buffer.Clear(); sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetUInt29(buffer, out var readValue, out var consumed)); Assert.AreEqual(readValue, value); Assert.AreEqual(consumed, buffer.Length); } } } [TestMethod] public void TestBoolean() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); using (var sc = new SerializationContext()) { writer.WriteBytes(true, sc); var buffer = new byte[sc.MessageLength]; ; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetBoolean(buffer, out var readVal, out var consumed)); Assert.AreEqual(buffer.Length, consumed); Assert.IsTrue(readVal); writer.WriteBytes(false, sc); sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetBoolean(buffer, out readVal, out consumed)); Assert.AreEqual(buffer.Length, consumed); Assert.IsFalse(readVal); } } [TestMethod] public void TestNull() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); using (var sc = new SerializationContext()) { writer.WriteBytes((object)null, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetNull(buffer, out var readVal, out var consumed)); Assert.IsNull(readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestUndefined() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); using (var sc = new SerializationContext()) { writer.WriteBytes(new Harmonic.Networking.Amf.Common.Undefined(), sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetUndefined(buffer, out var readVal, out var consumed)); Assert.IsNotNull(readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestArray() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); var arr = new Amf3Array(); arr["a"] = (uint)1; arr["b"] = 2.1; arr["d"] = null; arr.DensePart.Add(1); arr.DensePart.Add(1.2); using (var sc = new SerializationContext()) { writer.WriteBytes(arr, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetArray(buffer, out var readVal, out var consumed)); Assert.AreEqual(arr["a"], readVal["a"]); Assert.AreEqual(arr["b"], readVal["b"]); Assert.AreEqual(arr["d"], readVal["d"]); Assert.AreEqual(1.0, readVal[0]); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestByteArray() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); using (var sc = new SerializationContext()) { var arr = new byte[] { 1, 2, 3 }; writer.WriteBytes(arr, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetByteArray(buffer, out var readVal, out var consumed)); Assert.IsTrue(arr.SequenceEqual(readVal)); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestDate() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); using (var sc = new SerializationContext()) { var date = DateTime.Now; writer.WriteBytes(date, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetDate(buffer, out var readVal, out var consumed)); Assert.AreEqual(date.Year, readVal.Year); Assert.AreEqual(date.Month, readVal.Month); Assert.AreEqual(date.Day, readVal.Day); Assert.AreEqual(date.Hour, readVal.Hour); Assert.AreEqual(date.Minute, readVal.Minute); Assert.AreEqual(date.Second, readVal.Second); Assert.AreEqual(date.Millisecond, readVal.Millisecond); } } [TestMethod] public void TestDictionary() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); var dict = new Amf3Dictionary(); dict.Add("ss", 1.0); dict.Add("sd", new Vector() { 1, 2 }); dict.Add(new Vector() { 1, 2 }, "sd"); using (var sc = new SerializationContext()) { writer.WriteBytes(dict, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetDictionary(buffer, out var readVal, out var consumed)); Assert.AreEqual(dict["ss"], readVal["ss"]); Assert.AreEqual(dict["sd"], readVal["sd"]); Assert.AreEqual(dict[new Vector() { 1, 2 }], readVal[new Vector() { 1, 2 }]); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestIExternalizable() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); reader.RegisterExternalizable(); var ext = new iexternalizable() { v1 = 0.1, v2 = 1 }; using (var sc = new SerializationContext()) { writer.WriteBytes(ext, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetObject(buffer, out var readVal, out var consumed)); var val = (iexternalizable)readVal; Assert.AreEqual(val.v1, ext.v1); Assert.AreEqual(val.v2, ext.v2); Assert.AreEqual(buffer.Length, consumed); } } public class TestCls2: IEquatable { [ClassField] public double t1 {get;set;} public bool Equals(TestCls2 other) { return t1 == other.t1; } public override bool Equals(object obj) { if (obj is TestCls2 obj2) { return Equals(obj2); } return base.Equals(obj); } public override int GetHashCode() { return HashCode.Combine(t1); } } [TestMethod] public void TestObject() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); var obj = new AmfObject { { "t1", (uint)2 }, { "t2", 3.1 } }; obj.AddDynamic("t3", new Vector() { 2, 3, 4 }); using (var sc = new SerializationContext()) { writer.WriteBytes(obj, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetObject(buffer, out var readVal, out var consumed)); var readObj = (AmfObject)readVal; Assert.AreEqual(readObj.Fields["t1"], (uint)2); Assert.AreEqual(readObj.Fields["t2"], 3.1); Assert.AreEqual(readObj.DynamicFields["t3"], new Vector() { 2, 3, 4 }); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestObject2() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); reader.RegisterTypedObject(); var obj = new TestCls2() { t1 = 3.5 }; using (var sc = new SerializationContext()) { writer.WriteBytes(obj, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetObject(buffer, out var readVal, out var consumed)); Assert.AreEqual(obj, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestObject3() { var reader = new Amf3Reader(); var writer = new Amf3Writer(); reader.RegisterTypedObject(); var t = new TestCls() { T1 = 3.3, T2 = "abc", T3 = "abd", t4 = new Vector() { 2000, 30000, 400000 } }; using (var sc = new SerializationContext()) { writer.WriteBytes(t, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetObject(buffer, out var readVal, out var consumed)); Assert.AreEqual(t, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestXmlDocument() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); using (var sc = new SerializationContext()) { var xml = new XmlDocument(); var elem = xml.CreateElement("price"); xml.AppendChild(elem); writer.WriteBytes(xml, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetXmlDocument(buffer, out var ud, out var consunmed)); Assert.IsNotNull(ud); Assert.AreNotEqual(ud.GetElementsByTagName("price").Count, 0); Assert.AreEqual(consunmed, buffer.Length); } } [TestMethod] public void TestXml() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); using (var sc = new SerializationContext()) { var xml = new Amf3Xml(); var elem = xml.CreateElement("price"); xml.AppendChild(elem); writer.WriteBytes(xml, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetXml(buffer, out var ud, out var consunmed)); Assert.IsNotNull(ud); Assert.AreNotEqual(ud.GetElementsByTagName("price").Count, 0); Assert.AreEqual(consunmed, buffer.Length); } } [TestMethod] public void TestVectorUint() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); using (var sc = new SerializationContext()) { var v = new Vector() { 2, 3, 4 }; writer.WriteBytes(v, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetVectorUint(buffer, out var readVal, out var consumed); Assert.AreEqual(v, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestVectorInt() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); using (var sc = new SerializationContext()) { var v = new Vector() { 2, 3, 4 }; writer.WriteBytes(v, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetVectorInt(buffer, out var readVal, out var consumed); Assert.AreEqual(v, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestVectorDouble() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); using (var sc = new SerializationContext()) { var v = new Vector() { 2, 3, 4 }; writer.WriteBytes(v, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetVectorDouble(buffer, out var readVal, out var consumed); Assert.AreEqual(v, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestVectorTypedObject() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); reader.RegisterTypedObject(); var t = new TestCls() { T1 = 3.3, T2 = "abc", T3 = "abd", t4 = new Vector() { 2000, 30000, 400000 } }; t.AddDynamic("t5", new Vector() { new TestCls { T1 = 5.6 } }); using (var sc = new SerializationContext()) { var v = new Vector() { t, t, t }; writer.WriteBytes(v, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetVectorObject(buffer, out var readVal, out var consumed); Assert.IsTrue(readVal.GetType().GetGenericArguments().First() == typeof(TestCls)); Assert.AreEqual(v, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestVectorAnyObject() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); reader.RegisterTypedObject(); var t = new TestCls() { T1 = 3.3, T2 = "abc", T3 = "abd", t4 = new Vector() { 2000, 30000, 400000 } }; using (var sc = new SerializationContext()) { var v = new Vector() { t, 3.2, 4.5 }; writer.WriteBytes(v, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); reader.TryGetVectorObject(buffer, out var readVal, out var consumed); Assert.IsTrue(readVal.GetType().GetGenericArguments().First() == typeof(object)); Assert.AreEqual(v, readVal); Assert.AreEqual(buffer.Length, consumed); } } [TestMethod] public void TestString() { var writer = new Amf3Writer(); var reader = new Amf3Reader(); using (var sc = new SerializationContext()) { for (int i = 0; i < 1000; i++) { var str = Guid.NewGuid().ToString(); writer.WriteBytes(str, sc); var buffer = new byte[sc.MessageLength]; sc.GetMessage(buffer); Assert.IsTrue(reader.TryGetString(buffer, out var readVal, out var consumed)); Assert.AreEqual(str, readVal); Assert.AreEqual(buffer.Length, consumed); } } } } } ================================================ FILE: UnitTest/TestUnlimitedBuffer.cs ================================================ using Harmonic.Buffers; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Buffers; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace UnitTest { [TestClass] public class TestUnlimitedBuffer { [TestMethod] public void TestBufferSegmentSize() { var random = new Random(); Assert.ThrowsException(() => new ByteBuffer(0)); for (int i = 0; i < 10000; i++) { var size = random.Next(1, 500); var len1 = random.Next(0, 100); var len2 = random.Next(0, 200); var buffer = new ByteBuffer(size); var bytes1 = new byte[len1]; var bytes2 = new byte[len2]; random.NextBytes(bytes1); random.NextBytes(bytes2); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(bytes2); var length = buffer.Length; Assert.AreEqual(length, len1 + len2); var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, len1).SequenceEqual(bytes1)); Assert.IsTrue(outBuffer.AsSpan(len1, len2).SequenceEqual(bytes2)); } } [TestMethod] public void TestBufferLengthDifferentBufferSegmentSize() { var random = new Random(); for (int i = 0; i < 1000; i++) { var len1 = random.Next(0, 3000); var len2 = random.Next(0, 3000); var bytes1 = new byte[len1]; var bytes2 = new byte[len2]; random.NextBytes(bytes1); random.NextBytes(bytes2); var buffer = new ByteBuffer(random.Next(10, 3000)); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(bytes2); Assert.AreEqual(len1 + len2, buffer.Length); } } [TestMethod] public void TestBufferLength() { var random = new Random(); for (int i = 0; i < 1000; i++) { var len1 = random.Next(0, 3000); var len2 = random.Next(0, 3000); var bytes1 = new byte[len1]; var bytes2 = new byte[len2]; random.NextBytes(bytes1); random.NextBytes(bytes2); var buffer = new ByteBuffer(); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(bytes2); Assert.AreEqual(len1 + len2, buffer.Length); } } [TestMethod] public void TestWriteToBuffer4DifferentBufferSegmentSize() { for (int i = 0; i < 10000; i++) { var random = new Random(); var buffer = new ByteBuffer(random.Next(1, 3000)); var bytes1 = new byte[1024]; var data = (byte)random.Next(byte.MinValue, byte.MaxValue); random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(data); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, 1024).SequenceEqual(bytes1)); Assert.AreEqual(outBuffer[1024], data); } } [TestMethod] public void TestWriteToBuffer5DifferentBufferSegmentSize() { for (int i = 0; i < 10000; i++) { var random = new Random(); var buffer = new ByteBuffer(512); var bytes1 = new byte[4096]; var data = (byte)random.Next(byte.MinValue, byte.MaxValue); random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(data); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); var test1 = new byte[2]; var test2 = new byte[3]; var test3 = new byte[512]; var test4 = new byte[1024]; buffer.TakeOutMemory(test1.AsSpan()); buffer.TakeOutMemory(test2.AsSpan()); buffer.TakeOutMemory(test3); buffer.TakeOutMemory(test4); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(test1.AsSpan().SequenceEqual(bytes1.AsSpan(0, 2))); Assert.IsTrue(test2.AsSpan().SequenceEqual(bytes1.AsSpan(2, 3))); Assert.IsTrue(test3.AsSpan().SequenceEqual(bytes1.AsSpan(5, 512))); Assert.IsTrue(test4.AsSpan().SequenceEqual(bytes1.AsSpan(517, 1024))); Assert.IsTrue(outBuffer.AsSpan(0, 4096 - 5 - 512 - 1024).SequenceEqual(bytes1.AsSpan(517 + 1024))); } } [TestMethod] public void TestParalleWriteAndRead() { var random = new Random(); var buffer = new ByteBuffer(512, 35767); var th1 = new Thread(() => { byte i = 0; while (true) { var arr = new byte[new Random().Next(256, 512)]; for (var j = 0; j < arr.Length; j++) { arr[j] = i; i++; if (i > 100) { i = 0; } } buffer.WriteToBuffer(arr); } }); th1.IsBackground = true; th1.Start(); var th2 = new Thread(() => { while (true) { var arr = new byte[new Random().Next(129, 136)]; if (buffer.Length >= arr.Length) { buffer.TakeOutMemory(arr); for (int i = 1; i < arr.Length; i++) { Assert.IsTrue(arr[i] - arr[i - 1] == 1 || arr[i - 1] - arr[i] == 100); } } } }); th2.IsBackground = true; th2.Start(); Thread.Sleep(TimeSpan.FromSeconds(30)); } [TestMethod] public void TestAsyncWriteAndRead() { var buffer = new ByteBuffer(512, 35767); short c = 0; Func th1 = async () => { byte i = 0; while (c < short.MaxValue) { var arr = new byte[new Random().Next(256, 512)]; for (var j = 0; j < arr.Length; j++) { arr[j] = i; i++; if (i > 100) { i = 0; } } await buffer.WriteToBufferAsync(arr); c++; } }; Func th2 = async () => { while (c < short.MaxValue) { var arr = new byte[new Random().Next(129, 136)]; if (buffer.Length >= arr.Length) { await buffer.TakeOutMemoryAsync(arr); for (int i = 1; i < arr.Length; i++) { Assert.IsTrue(arr[i] - arr[i - 1] == 1 || arr[i - 1] - arr[i] == 100); } } } }; var t = th1(); th2(); t.Wait(); } [TestMethod] public void TestWriteToBuffer3DifferentBufferSegmentSize() { for (int i = 0; i < 10000; i++) { var random = new Random(); var buffer = new ByteBuffer(random.Next(1, 3000)); var bytes1 = new byte[3]; var data = (byte)random.Next(byte.MinValue, byte.MaxValue); random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(data); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, 3).SequenceEqual(bytes1)); Assert.AreEqual(outBuffer[3], data); } } [TestMethod] public void TestWriteToBuffer4() { var buffer = new ByteBuffer(); var random = new Random(); var bytes1 = new byte[1024]; var data = (byte)random.Next(byte.MinValue, byte.MaxValue); random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(data); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, 1024).SequenceEqual(bytes1)); Assert.AreEqual(outBuffer[1024], data); } [TestMethod] public void TestWriteToBuffer3() { var buffer = new ByteBuffer(); var random = new Random(); var bytes1 = new byte[3]; var data = (byte)random.Next(byte.MinValue, byte.MaxValue); random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(data); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, 3).SequenceEqual(bytes1)); Assert.AreEqual(outBuffer[3], data); } [TestMethod] public void TestWriteToBuffer1DifferentBufferSegmentSize() { for (int i = 0; i < 10000; i++) { var random = new Random(); var buffer = new ByteBuffer(random.Next(1, 3000)); var bytes1 = new byte[3]; var bytes2 = new byte[7]; random.NextBytes(bytes1); random.NextBytes(bytes2); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(bytes2); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, 3).SequenceEqual(bytes1)); Assert.IsTrue(outBuffer.AsSpan(3, 7).SequenceEqual(bytes2)); } } [TestMethod] public void TestWriteToBuffer1() { var buffer = new ByteBuffer(); var random = new Random(); var bytes1 = new byte[3]; var bytes2 = new byte[7]; random.NextBytes(bytes1); random.NextBytes(bytes2); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(bytes2); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); Assert.IsTrue(outBuffer.AsSpan(0, 3).SequenceEqual(bytes1)); Assert.IsTrue(outBuffer.AsSpan(3, 7).SequenceEqual(bytes2)); } [TestMethod] public void TestWriteToBuffer2() { var buffer = new ByteBuffer(); var random = new Random(); var bytes1 = new byte[4000]; var bytes2 = new byte[3001]; random.NextBytes(bytes1); random.NextBytes(bytes2); buffer.WriteToBuffer(bytes1); buffer.WriteToBuffer(bytes2); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); var seq = outBuffer.AsSpan(0, 4000); var seq2 = outBuffer.AsSpan(4000, 3001).ToArray(); Assert.IsTrue(seq.SequenceEqual(bytes1)); Assert.IsTrue(seq2.SequenceEqual(bytes2)); outBuffer.AsSpan().Clear(); buffer.TakeOutMemory(outBuffer); Assert.IsFalse(outBuffer.Any(b => b != 0)); } [TestMethod] public void TestClearAndCopyToDifferentBufferSegmentSize() { var random = new Random(); var buffer = new ByteBuffer(random.Next(1, 3000)); var bytes1 = new byte[4000]; random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); outBuffer.AsSpan().Clear(); buffer.TakeOutMemory(outBuffer); Assert.IsFalse(outBuffer.Any(b => b != 0)); } [TestMethod] public void TestClearAndCopyTo() { var buffer = new ByteBuffer(); var random = new Random(); var bytes1 = new byte[4000]; random.NextBytes(bytes1); buffer.WriteToBuffer(bytes1); var length = buffer.Length; var outBuffer = ArrayPool.Shared.Rent(length); buffer.TakeOutMemory(outBuffer); outBuffer.AsSpan().Clear(); buffer.TakeOutMemory(outBuffer); Assert.IsFalse(outBuffer.Any(b => b != 0)); } } } ================================================ FILE: UnitTest/UnitTest.csproj ================================================  netcoreapp2.2 false true ================================================ FILE: demo/MyLivingController.cs ================================================ using Harmonic.Controllers; using Harmonic.Controllers.Living; using Harmonic.Rpc; using System; using System.Collections.Generic; using System.Text; namespace demo { [NeverRegister] class MyLivingController : LivingController { [RpcMethod("createStream")] public new uint CreateStream() { var stream = RtmpSession.CreateNetStream(); return stream.MessageStream.MessageStreamId; } } } ================================================ FILE: demo/MyLivingStream.cs ================================================ using Harmonic.Controllers; using Harmonic.Controllers.Living; using Harmonic.Rpc; using Harmonic.Service; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace demo { [NeverRegister] public class MyLivingStream : LivingStream { public MyLivingStream(PublisherSessionService publisherSessionService) : base(publisherSessionService) { } [RpcMethod("play")] public new async Task Play( [FromOptionalArgument] string streamName, [FromOptionalArgument] double start = -1, [FromOptionalArgument] double duration = -1, [FromOptionalArgument] bool reset = false) { // Do some check or other stuff await base.Play(streamName, start, duration, reset); } } } ================================================ FILE: demo/Program.cs ================================================ using Harmonic.Hosting; using System; using System.Net; namespace demo { class Program { static void Main(string[] args) { RtmpServer server = new RtmpServerBuilder() .UseStartup() .UseWebSocket(c => { c.BindEndPoint = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 8080); }) .Build(); var tsk = server.StartAsync(); tsk.Wait(); } } } ================================================ FILE: demo/StartUp.cs ================================================ using Autofac; using Harmonic.Hosting; using System; using System.Collections.Generic; using System.Text; namespace demo { class Startup : IStartup { public void ConfigureServices(ContainerBuilder builder) { } } } ================================================ FILE: demo/demo.csproj ================================================  Exe netcoreapp2.2 ================================================ FILE: docs/README.md ================================================ # Harmonic A high performance RTMP live streaming application framework # Usage Program.cs ```csharp using Harmonic.Hosting; using System; using System.Net; namespace demo { class Program { static void Main(string[] args) { RtmpServer server = new RtmpServerBuilder() .UseStartup() .Build(); var tsk = server.StartAsync(); tsk.Wait(); } } } ``` StartUp.cs ```csharp using Autofac; using Harmonic.Hosting; namespace demo { class Startup : IStartup { public void ConfigureServices(ContainerBuilder builder) { } } } ``` Build a server like this to support websocket-flv transmission ```csharp RtmpServer server = new RtmpServerBuilder() .UseStartup() .UseWebSocket(c => { c.BindEndPoint = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 8080); }) .Build(); ``` # Scalability Harmonic will scan your assembly and try to find classes that inherit from `RtmpController` or `WebSocketController` then register them into Harmonic, and map controller by url `rtmp://
//` for rtmp and `ws://
//`. the controller_name is controller class's name then remove the `Controller` suffix, for example `Living` is controller_name of `LivingController`. once Harmonic found any class that inherts from `RtmpController` or `WebSocketController`, it will never register `RtmpController` and `WebSocketController`. You can also inherit builtin classes `LivingController` or `WebSocketPlayController`, when Harmonic found a class that inherit from them, it will not register `LivingController` and `WebSocketPlayController`. When you want to custom streaming logic, you can create a class that inherits from `LivingController` or `WebSocketPlayController`. ```csharp public class MyLivingController : LivingController { [RpcMethod("createStream")] public new uint CreateStream() { var stream = RtmpSession.CreateNetStream(); return stream.MessageStream.MessageStreamId; } } public class MyLivingStream : LivingStream { [RpcMethod(Name = "publish")] public void Publish([FromOptionalArgument] string publishingName, [FromOptionalArgument] string publishingType) { if (...) { } // your logic base.Publish(publishingName, publishingType); } } ``` ## RtmpController and WebSocketController RtmpController and WebSocketController are two abstract basic controller, they are intended for serving video on rtmp protocol and websocket protocol. When a controller class inherit from RtmpController, it will become an rtmp controller, it will working on rtmp protocol, and supports every rtmp features. When a controller class inherit from WebSocketController, it will become a websocket controller, it can only send flv header and tags. ## Recording The `RecordController` can record video, by default, it will save flv files into `working_dir/Record`. You can overrite the recording configuration by register you own configure class in `StartUp` class ```csharp class MyRecordConfiguration: RecordServiceConfiguration { public override string RecordPath { get; set; } = @"MyRecordPath"; public override string FilenameFormat { get; set; } = @"recorded-{streamName}"; }; class Startup : IStartup { public void ConfigureServices(ContainerBuilder builder) { builder.Register(c => new MyRecordConfiguration()).As(); } } ``` ## Websocket websocket protocol and rtmp protocol are running on two different controllers, so when you push vide to url: `rtmp://127.0.0.1/living/a`, the corresponding playing url for websocket is `ws://127.0.0.1/websocketplay/a` ## Internal Controllers ### LivingController LivingController provides a simple living service, it recieves video or audio data and broadcast data to other plays. ### RecordController RecordController supports video recording, and can be configured. ### WebsocketPlayController WebsocketPlayController supports two modes: lving mode and vod mode. when stream name in url is not in living, this controller will try to find a stream in recording folder, then play it. ## Internal Classes ### NetConnection NetConnection is responsible for managing all NetStreams, process some control messages, rpc support, handle `connect`, `createStream` and another command messages. ### NetStream NetStream is created by NetConnection, it reperents a logic stream, all of RtmpController is NetStream. ### MessageStream MessageStream repersents a logic rtmp stream. Every message must be sent on a specific MessageStream. ### ChunkStream Message will be break into chunks before sending to peer. chunks must be send on a ChunkStream, in a ChunkStream, every chunk must be sent one by one. that means you can't send a message concurrently on one ChunkStream, but message can be sent concurrently on some different ChunkStream. ### RtmpSession RtmpSession is a bridge from NetStream to RtmpServer, controllers can access to it's own RtmpSession property to send message, or close connection or something else. ## Rpc See [rpc-docs](rpc.md) ## Api See [api-docs](api.md) ## Dependency injection in controllers Harmonic uses autofac as the DI framework, you can register you own service in `StartUp`, and use it in your controller ```csharp class Startup : IStartup { public void ConfigureServices(ContainerBuilder builder) { builder.Register(c => new MyService()).AsSelf(); } } class MyController: LivingController { public MyController(MyService service) { //... } } ``` ## Custom message To add your own custom message, you need to write a message class, then rengister the message class when you call `UseHarmonic` for example: ```csharp static void Main(string[] args) { RtmpServer server = new RtmpServerBuilder() .UseStartup() .UseWebSocket(c => { c.BindEndPoint = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 8080); }) .UseHarmonic(c => { c.RegisterMessage(); }) .Build(); var tsk = server.StartAsync(); tsk.Wait(); } ``` # Test ## push video file using ffmpeg ```bash ffmpeg -i test.mp4 -f flv -vcodec h264 -acodec aac "rtmp://127.0.0.1/living/streamName" ``` ## play rtmp stream using ffplay ```bash ffplay "rtmp://127.0.0.1/living/streamName" ``` ## play flv stream using [flv.js](https://github.com/Bilibili/flv.js) by websocket ```html ``` ``` ================================================ FILE: docs/api.md ================================================ # NetConnection ## IReadOnlyDictionary NetStreams { get; } access all NetStreams that is managed by this NetConnection # RtmpSession ## RtmpControlMessageStream ControlMessageStream { get; }; get the ControlMessageStream, it will be useful when you intend to send a controll message. ## NetConnection NetConnection { get; } get the NetConnection instance. ## ConnectionInformation ConnectionInformation { get; } get the connection information, the data that was sent when peer call the `connect` command. ## T CreateNetStream() where T: NetStream create a netstream(usally a sub controller), then you can send message on it ## void DeleteNetStream(uint id) destroy a message stream ## T CreateCommandMessage() where T: CommandMessage create a command message, it will create a command message using the amf encoding the `ConnectionInformation` provided. ## T CreateData() where T : DataMessage same as CreateCommandMessage ## Close() close connection ## RtmpChunkStream CreateChunkStream() when you want to send messages independently, you can create a chunkstream, different chunk stream can send message concurrently and using independent timestamp. ## Task SendControlMessageAsync(Message message) send a control message using RtmpControlMessageStream and ControlChunkStream ================================================ FILE: docs/rpc.md ================================================ # Ways to map rpc method in a RtmpController ## Attributes ### RpcMethodAttribute marks a method that can be invoked by Rpc service. ### CommandObjectAttribute map a parameter that is an whole rtmp CommandObject, the parameter has this attribute must be a AmfObject ### FromCommandObjectAttribute map a parameter that is presents in rtmp CommandObject, when you specificed the Key property, the rpc service will extract an object from CommandObject by key you specificed as method argument, when you not specificed a Key property, Rpc service will use parameter's name instead to find a proper object. ### FromOptionalArgumentAttribute map a parameter that is presetns in rtmp CommandArgument field, the order of object in CommandArgument field is the order your parameter which has this attribute to call the method. the Name filed is the name that in the command message. if you did't specificed a name, Rpc service will use the method's name. ## About return value if your method returns a value, the rpc service will return it by invoking `_result`. if your method throws an expection, the rpc service will return the exception message to peer by invoking `_error`. if your method returns a Task, rpc service will wait to task completes, then returns the result of the task. if your method returns void, rpc service will send nothing to peer. if you wish to return multiple data, or the return rules not satisifies your requirements, you can make your method return void or `Task`(just a Task), and call `_result` or `_error` in the method ================================================ FILE: samples/amf0/boolean/true.amf0 ================================================  ================================================ FILE: samples/amf0/misc/null.amf0 ================================================  ================================================ FILE: samples/amf0/misc/undefined.amf0 ================================================  ================================================ FILE: samples/amf3/boolean/false.amf3 ================================================  ================================================ FILE: samples/amf3/boolean/true.amf3 ================================================  ================================================ FILE: samples/amf3/intenger/56.amf3 ================================================ 8 ================================================ FILE: samples/amf3/intenger/57.amf3 ================================================ 9 ================================================ FILE: samples/amf3/intenger/60.amf3 ================================================ < ================================================ FILE: samples/amf3/intenger/67.amf3 ================================================ C ================================================ FILE: samples/amf3/intenger/72.amf3 ================================================ H ================================================ FILE: samples/amf3/intenger/73.amf3 ================================================ I ================================================ FILE: samples/amf3/intenger/75.amf3 ================================================ K ================================================ FILE: samples/amf3/intenger/78.amf3 ================================================ N ================================================ FILE: samples/amf3/intenger/82.amf3 ================================================ R ================================================ FILE: samples/amf3/intenger/98.amf3 ================================================ b ================================================ FILE: samples/amf3/misc/bytearray.amf3 ================================================  ================================================ FILE: samples/amf3/misc/null.amf3 ================================================  ================================================ FILE: samples/amf3/misc/xml.amf3 ================================================ - ================================================ FILE: samples/amf3/misc/xml_document.amf3 ================================================ 52 ================================================ FILE: samples/amf3/number/0.05806697191443333.amf3 ================================================ ?BR ================================================ FILE: samples/amf3/number/3.962148410082559.amf3 ================================================ @zݙ ================================================ FILE: samples/amf3/number/4.465764800567858.amf3 ================================================ @rD ================================================ FILE: samples/amf3/number/6.863435764713296.amf3 ================================================ @t(N ================================================ FILE: samples/amf3/number/7.645173446829178.amf3 ================================================ @Y ================================================ FILE: samples/amf3/number/8.451623695104308.amf3 ================================================ @ ;8$ ================================================ FILE: samples/amf3/number/8.518697602984554.amf3 ================================================ @! r ================================================ FILE: samples/amf3/number/8.85002823631796.amf3 ================================================ @!6S ================================================ FILE: samples/amf3/number/9.838871036292584.amf3 ================================================ @#$f ================================================ FILE: samples/amf3/number/9.98509389093438.amf3 ================================================ @#^9 ================================================ FILE: samples/amf3/string/aoxqmkvbxa.amf3 ================================================ aoxqmkvbxa ================================================ FILE: samples/amf3/string/bghnwadduz.amf3 ================================================ bghnwadduz ================================================ FILE: samples/amf3/string/cmaljzrwgc.amf3 ================================================ cmaljzrwgc ================================================ FILE: samples/amf3/string/cuyerozwyf.amf3 ================================================ cuyerozwyf ================================================ FILE: samples/amf3/string/dfjfucqvpr.amf3 ================================================ dfjfucqvpr ================================================ FILE: samples/amf3/string/fxxcsjosdu.amf3 ================================================ fxxcsjosdu ================================================ FILE: samples/amf3/string/korbgwizge.amf3 ================================================ korbgwizge ================================================ FILE: samples/amf3/string/psvigwvvpx.amf3 ================================================ psvigwvvpx ================================================ FILE: samples/amf3/string/ubteltbaku.amf3 ================================================ ubteltbaku ================================================ FILE: samples/amf3/string/vqayztgtuf.amf3 ================================================ vqayztgtuf