From c7cc8fad8da2e46cbd33f5516c8368f3227f02fe Mon Sep 17 00:00:00 2001 From: withsalt Date: Tue, 1 Oct 2019 17:24:38 +0800 Subject: [PATCH] init project --- .gitattributes | 63 + .gitignore | 94 +- src/Harmonic/Buffers/ByteBuffer.cs | 445 +++++++ .../Controllers/Living/LivingController.cs | 18 + .../Controllers/Living/LivingStream.cs | 203 +++ .../Controllers/NeverRegisterAttribute.cs | 12 + .../Controllers/Record/RecordController.cs | 18 + .../Controllers/Record/RecordStream.cs | 350 +++++ src/Harmonic/Controllers/RtmpController.cs | 49 + .../Controllers/WebSocketController.cs | 46 + .../Controllers/WebSocketPlayController.cs | 158 +++ src/Harmonic/Harmonic.csproj | 31 + src/Harmonic/Hosting/IStartup.cs | 11 + src/Harmonic/Hosting/RtmpServer.cs | 134 ++ src/Harmonic/Hosting/RtmpServerBuilder.cs | 113 ++ src/Harmonic/Hosting/RtmpServerOptions.cs | 163 +++ src/Harmonic/Hosting/WebSocketOptions.cs | 36 + .../Networking/Amf/Common/Amf3Object.cs | 48 + .../Amf/Common/TypeRegisterState.cs | 12 + .../Networking/Amf/Common/Undefined.cs | 10 + .../Networking/Amf/Common/Unsupported.cs | 10 + .../Networking/Amf/Data/IDynamicObject.cs | 13 + .../Networking/Amf/Data/IExternalizable.cs | 14 + src/Harmonic/Networking/Amf/Data/Message.cs | 13 + .../Serialization/Amf0/Amf0CommonValues.cs | 19 + .../Amf/Serialization/Amf0/Amf0Reader.cs | 838 ++++++++++++ .../Amf/Serialization/Amf0/Amf0Type.cs | 28 + .../Amf/Serialization/Amf0/Amf0Writer.cs | 419 ++++++ .../Amf0/SerializationContext.cs | 42 + .../Amf/Serialization/Amf3/Amf3Array.cs | 38 + .../Amf/Serialization/Amf3/Amf3ClassTraits.cs | 51 + .../Serialization/Amf3/Amf3CommonValues.cs | 17 + .../Amf/Serialization/Amf3/Amf3Dictionary.cs | 31 + .../Amf/Serialization/Amf3/Amf3Reader.cs | 1127 +++++++++++++++++ .../Amf/Serialization/Amf3/Amf3Type.cs | 28 + .../Amf/Serialization/Amf3/Amf3Writer.cs | 700 ++++++++++ .../Amf/Serialization/Amf3/Amf3Xml.cs | 16 + .../Amf3/SerializationContext.cs | 46 + .../Amf/Serialization/Amf3/Vector.cs | 47 + .../Attributes/ClassFieldAttribute.cs | 12 + .../Attributes/TypedObjectAttribute.cs | 12 + .../Networking/ConnectionInformation.cs | 21 + .../Networking/Flv/Data/AacPacketType.cs | 12 + src/Harmonic/Networking/Flv/Data/AudioData.cs | 12 + src/Harmonic/Networking/Flv/Data/CodecId.cs | 17 + .../Networking/Flv/Data/FlvAudioData.cs | 15 + .../Networking/Flv/Data/FlvVideoData.cs | 15 + src/Harmonic/Networking/Flv/Data/FrameType.cs | 15 + .../Networking/Flv/Data/SoundFormat.cs | 23 + src/Harmonic/Networking/Flv/Data/SoundRate.cs | 14 + src/Harmonic/Networking/Flv/Data/SoundSize.cs | 12 + src/Harmonic/Networking/Flv/Data/SoundType.cs | 12 + src/Harmonic/Networking/Flv/FlvDemuxer.cs | 178 +++ src/Harmonic/Networking/Flv/FlvMuxer.cs | 96 ++ .../Networking/Rtmp/ChunkStreamContext.cs | 587 +++++++++ .../Networking/Rtmp/Data/ChunkBasicHeader.cs | 14 + .../Networking/Rtmp/Data/ChunkHeader.cs | 15 + .../Networking/Rtmp/Data/ChunkHeaderType.cs | 18 + src/Harmonic/Networking/Rtmp/Data/Message.cs | 22 + .../Networking/Rtmp/Data/MessageHeader.cs | 19 + .../Networking/Rtmp/Data/MessageType.cs | 36 + .../Rtmp/Data/SharedObjectMessage.cs | 14 + .../Rtmp/Data/UserControlMessageEvents.cs | 17 + .../UnknownMessageReceivedException.cs | 17 + .../Networking/Rtmp/HandshakeContext.cs | 125 ++ src/Harmonic/Networking/Rtmp/IOPipeLine.cs | 268 ++++ .../Networking/Rtmp/MessageReadingState.cs | 21 + .../Networking/Rtmp/Messages/AbortMessage.cs | 39 + .../Rtmp/Messages/AcknowledgementMessage.cs | 40 + .../Rtmp/Messages/AggregateMessage.cs | 99 ++ .../Rtmp/Messages/AmfEncodingVersion.cs | 12 + .../Networking/Rtmp/Messages/AudioMessage.cs | 38 + .../Messages/Commands/CallCommandMessage.cs | 20 + .../Rtmp/Messages/Commands/CommandMessage.cs | 145 +++ .../Commands/CommandMessageFactory.cs | 77 ++ .../Commands/ConnectCommandMessage.cs | 19 + .../Commands/CreateStreamCommandMessage.cs | 16 + .../Commands/DeleteStreamCommandMessage.cs | 19 + .../Commands/OnStatusCommandMessage.cs | 20 + .../Messages/Commands/PauseCommandMessage.cs | 21 + .../Messages/Commands/Play2CommandMessage.cs | 19 + .../Messages/Commands/PlayCommandMessage.cs | 26 + .../Commands/PublishCommandMessage.cs | 21 + .../Commands/ReceiveAudioCommandMessage.cs | 19 + .../Commands/ReceiveVideoCommandMessage.cs | 19 + .../Commands/ReturnResultCommandMessage.cs | 44 + .../Messages/Commands/SeekCommandMessage.cs | 19 + .../Rtmp/Messages/ControlMessage.cs | 13 + .../Networking/Rtmp/Messages/DataMessage.cs | 72 ++ .../Rtmp/Messages/SetChunkSizeMessage.cs | 42 + .../Rtmp/Messages/SetPeerBandwidthMessage.cs | 49 + .../UserControlMessages/PingRequestMessage.cs | 48 + .../PingResponseMessage.cs | 48 + .../SetBufferLengthMessage.cs | 54 + .../UserControlMessages/StreamBeginMessage.cs | 48 + .../UserControlMessages/StreamDryMessage.cs | 48 + .../UserControlMessages/StreamEofMessage.cs | 48 + .../StreamIsRecordedMessage.cs | 48 + .../UserControlMessages/UserControlMessage.cs | 33 + .../UserControlMessageFactory.cs | 37 + .../Networking/Rtmp/Messages/VideoMessage.cs | 41 + .../WindowAcknowledgementSizeMessage.cs | 39 + src/Harmonic/Networking/Rtmp/NetConnection.cs | 200 +++ src/Harmonic/Networking/Rtmp/NetStream.cs | 46 + .../Networking/Rtmp/RtmpChunkStream.cs | 59 + .../Networking/Rtmp/RtmpControlChunkStream.cs | 20 + .../Rtmp/RtmpControlMessageStream.cs | 18 + .../Networking/Rtmp/RtmpMessageStream.cs | 102 ++ src/Harmonic/Networking/Rtmp/RtmpSession.cs | 325 +++++ .../OptionalArgumentAttribute.cs | 11 + .../Serialization/RtmpCommandAttribute.cs | 12 + .../Serialization/RtmpMessageAttribute.cs | 19 + .../Serialization/SerializationContext.cs | 21 + .../UserControlMessageAttribute.cs | 12 + .../Rtmp/Streaming/PublishingType.cs | 18 + .../Streaming/PublishingTypeNameAttribute.cs | 46 + src/Harmonic/Networking/Rtmp/Supervisor.cs | 89 ++ src/Harmonic/Networking/Rtmp/WriteState.cs | 14 + .../Networking/Utils/NetworkBitConverter.cs | 176 +++ src/Harmonic/Networking/Utils/StreamHelper.cs | 57 + .../Networking/WebSocket/WebSocketSession.cs | 110 ++ src/Harmonic/Rpc/CommandObjectAttribute.cs | 11 + .../Rpc/FromCommandObjectAttribute.cs | 14 + .../Rpc/FromOptionalArgumentAttribute.cs | 11 + src/Harmonic/Rpc/RpcMethodAttribute.cs | 14 + src/Harmonic/Rpc/RpcService.cs | 214 ++++ .../Service/PublisherSessionService.cs | 46 + src/Harmonic/Service/RecordService.cs | 23 + .../Service/RecordServiceConfiguration.cs | 12 + src/LiveServer.sln | 31 + src/LiveServer/LiveServer.csproj | 16 + src/LiveServer/LiveServer.csproj.user | 6 + src/LiveServer/Program.cs | 22 + .../PublishProfiles/FolderProfile.pubxml | 15 + .../PublishProfiles/FolderProfile.pubxml.user | 6 + src/LiveServer/StartUp.cs | 16 + 136 files changed, 10278 insertions(+), 84 deletions(-) create mode 100644 .gitattributes create mode 100644 src/Harmonic/Buffers/ByteBuffer.cs create mode 100644 src/Harmonic/Controllers/Living/LivingController.cs create mode 100644 src/Harmonic/Controllers/Living/LivingStream.cs create mode 100644 src/Harmonic/Controllers/NeverRegisterAttribute.cs create mode 100644 src/Harmonic/Controllers/Record/RecordController.cs create mode 100644 src/Harmonic/Controllers/Record/RecordStream.cs create mode 100644 src/Harmonic/Controllers/RtmpController.cs create mode 100644 src/Harmonic/Controllers/WebSocketController.cs create mode 100644 src/Harmonic/Controllers/WebSocketPlayController.cs create mode 100644 src/Harmonic/Harmonic.csproj create mode 100644 src/Harmonic/Hosting/IStartup.cs create mode 100644 src/Harmonic/Hosting/RtmpServer.cs create mode 100644 src/Harmonic/Hosting/RtmpServerBuilder.cs create mode 100644 src/Harmonic/Hosting/RtmpServerOptions.cs create mode 100644 src/Harmonic/Hosting/WebSocketOptions.cs create mode 100644 src/Harmonic/Networking/Amf/Common/Amf3Object.cs create mode 100644 src/Harmonic/Networking/Amf/Common/TypeRegisterState.cs create mode 100644 src/Harmonic/Networking/Amf/Common/Undefined.cs create mode 100644 src/Harmonic/Networking/Amf/Common/Unsupported.cs create mode 100644 src/Harmonic/Networking/Amf/Data/IDynamicObject.cs create mode 100644 src/Harmonic/Networking/Amf/Data/IExternalizable.cs create mode 100644 src/Harmonic/Networking/Amf/Data/Message.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0CommonValues.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Reader.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Type.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Writer.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf0/SerializationContext.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Array.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3ClassTraits.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3CommonValues.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Dictionary.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Reader.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Type.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Writer.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Xml.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/SerializationContext.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Amf3/Vector.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Attributes/ClassFieldAttribute.cs create mode 100644 src/Harmonic/Networking/Amf/Serialization/Attributes/TypedObjectAttribute.cs create mode 100644 src/Harmonic/Networking/ConnectionInformation.cs create mode 100644 src/Harmonic/Networking/Flv/Data/AacPacketType.cs create mode 100644 src/Harmonic/Networking/Flv/Data/AudioData.cs create mode 100644 src/Harmonic/Networking/Flv/Data/CodecId.cs create mode 100644 src/Harmonic/Networking/Flv/Data/FlvAudioData.cs create mode 100644 src/Harmonic/Networking/Flv/Data/FlvVideoData.cs create mode 100644 src/Harmonic/Networking/Flv/Data/FrameType.cs create mode 100644 src/Harmonic/Networking/Flv/Data/SoundFormat.cs create mode 100644 src/Harmonic/Networking/Flv/Data/SoundRate.cs create mode 100644 src/Harmonic/Networking/Flv/Data/SoundSize.cs create mode 100644 src/Harmonic/Networking/Flv/Data/SoundType.cs create mode 100644 src/Harmonic/Networking/Flv/FlvDemuxer.cs create mode 100644 src/Harmonic/Networking/Flv/FlvMuxer.cs create mode 100644 src/Harmonic/Networking/Rtmp/ChunkStreamContext.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/ChunkBasicHeader.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/ChunkHeader.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/ChunkHeaderType.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/Message.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/MessageHeader.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/MessageType.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/SharedObjectMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Data/UserControlMessageEvents.cs create mode 100644 src/Harmonic/Networking/Rtmp/Exceptions/UnknownMessageReceivedException.cs create mode 100644 src/Harmonic/Networking/Rtmp/HandshakeContext.cs create mode 100644 src/Harmonic/Networking/Rtmp/IOPipeLine.cs create mode 100644 src/Harmonic/Networking/Rtmp/MessageReadingState.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/AbortMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/AcknowledgementMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/AggregateMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/AmfEncodingVersion.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/AudioMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/CallCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessageFactory.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/ConnectCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/CreateStreamCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/DeleteStreamCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/OnStatusCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/PauseCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/Play2CommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/PlayCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/PublishCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveAudioCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveVideoCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/ReturnResultCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/Commands/SeekCommandMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/ControlMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/DataMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/SetChunkSizeMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/SetPeerBandwidthMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingRequestMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingResponseMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/SetBufferLengthMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamBeginMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamDryMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamEofMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamIsRecordedMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessageFactory.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/VideoMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/Messages/WindowAcknowledgementSizeMessage.cs create mode 100644 src/Harmonic/Networking/Rtmp/NetConnection.cs create mode 100644 src/Harmonic/Networking/Rtmp/NetStream.cs create mode 100644 src/Harmonic/Networking/Rtmp/RtmpChunkStream.cs create mode 100644 src/Harmonic/Networking/Rtmp/RtmpControlChunkStream.cs create mode 100644 src/Harmonic/Networking/Rtmp/RtmpControlMessageStream.cs create mode 100644 src/Harmonic/Networking/Rtmp/RtmpMessageStream.cs create mode 100644 src/Harmonic/Networking/Rtmp/RtmpSession.cs create mode 100644 src/Harmonic/Networking/Rtmp/Serialization/OptionalArgumentAttribute.cs create mode 100644 src/Harmonic/Networking/Rtmp/Serialization/RtmpCommandAttribute.cs create mode 100644 src/Harmonic/Networking/Rtmp/Serialization/RtmpMessageAttribute.cs create mode 100644 src/Harmonic/Networking/Rtmp/Serialization/SerializationContext.cs create mode 100644 src/Harmonic/Networking/Rtmp/Serialization/UserControlMessageAttribute.cs create mode 100644 src/Harmonic/Networking/Rtmp/Streaming/PublishingType.cs create mode 100644 src/Harmonic/Networking/Rtmp/Streaming/PublishingTypeNameAttribute.cs create mode 100644 src/Harmonic/Networking/Rtmp/Supervisor.cs create mode 100644 src/Harmonic/Networking/Rtmp/WriteState.cs create mode 100644 src/Harmonic/Networking/Utils/NetworkBitConverter.cs create mode 100644 src/Harmonic/Networking/Utils/StreamHelper.cs create mode 100644 src/Harmonic/Networking/WebSocket/WebSocketSession.cs create mode 100644 src/Harmonic/Rpc/CommandObjectAttribute.cs create mode 100644 src/Harmonic/Rpc/FromCommandObjectAttribute.cs create mode 100644 src/Harmonic/Rpc/FromOptionalArgumentAttribute.cs create mode 100644 src/Harmonic/Rpc/RpcMethodAttribute.cs create mode 100644 src/Harmonic/Rpc/RpcService.cs create mode 100644 src/Harmonic/Service/PublisherSessionService.cs create mode 100644 src/Harmonic/Service/RecordService.cs create mode 100644 src/Harmonic/Service/RecordServiceConfiguration.cs create mode 100644 src/LiveServer.sln create mode 100644 src/LiveServer/LiveServer.csproj create mode 100644 src/LiveServer/LiveServer.csproj.user create mode 100644 src/LiveServer/Program.cs create mode 100644 src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml.user create mode 100644 src/LiveServer/StartUp.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index 3e759b7..c52567d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,8 @@ ## 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 @@ -22,19 +19,14 @@ x86/ bld/ [Bb]in/ [Oo]bj/ -[Ll]og/ -# Visual Studio 2015/2017 cache/options directory +# Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ -# Visual Studio 2017 auto generated files -Generated\ Files/ - # MSTest test Results [Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* # NUNIT *.VisualState.xml @@ -45,29 +37,19 @@ TestResult.xml [Rr]eleasePS/ dlldata.c -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core +# DNX project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json - -# StyleCop -StyleCopReport.xml -# Files built by Visual Studio *_i.c *_p.c *_i.h *.ilk *.meta *.obj -*.iobj *.pch *.pdb -*.ipdb *.pgc *.pgd *.rsp @@ -77,7 +59,6 @@ StyleCopReport.xml *.tlh *.tmp *.tmp_proj -*.log *.vspscc *.vssscc .builds @@ -105,9 +86,6 @@ ipch/ *.vspx *.sap -# Visual Studio Trace Files -*.e2e - # TFS 2012 Local Workspace $tf/ @@ -128,14 +106,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - # NCrunch _NCrunch_* .*crunch*.local.xml @@ -167,9 +137,9 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, +# 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 +#*.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to @@ -180,12 +150,12 @@ PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* +**/packages/* # except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ +!**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets @@ -202,7 +172,6 @@ AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt -*.appx # Visual Studio cache files # files ending in .cache can be ignored @@ -217,14 +186,10 @@ ClientBin/ *.dbmdl *.dbproj.schemaview *.jfm -*.pfx *.publishsettings +node_modules/ orleans.codegen.cs -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ @@ -239,19 +204,15 @@ _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak # SQL Server files *.mdf *.ldf -*.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings -*.rptproj.rsuser # Microsoft Fakes FakesAssemblies/ @@ -261,7 +222,6 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat -node_modules/ # Visual Studio 6 build log *.plg @@ -269,9 +229,6 @@ node_modules/ # 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 @@ -296,35 +253,4 @@ paket-files/ # Python Tools for Visual Studio (PTVS) __pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +*.pyc \ No newline at end of file diff --git a/src/Harmonic/Buffers/ByteBuffer.cs b/src/Harmonic/Buffers/ByteBuffer.cs new file mode 100644 index 0000000..3fb76cd --- /dev/null +++ b/src/Harmonic/Buffers/ByteBuffer.cs @@ -0,0 +1,445 @@ +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 + } +} diff --git a/src/Harmonic/Controllers/Living/LivingController.cs b/src/Harmonic/Controllers/Living/LivingController.cs new file mode 100644 index 0000000..0dc6f54 --- /dev/null +++ b/src/Harmonic/Controllers/Living/LivingController.cs @@ -0,0 +1,18 @@ +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; + } + } +} diff --git a/src/Harmonic/Controllers/Living/LivingStream.cs b/src/Harmonic/Controllers/Living/LivingStream.cs new file mode 100644 index 0000000..ce09bd3 --- /dev/null +++ b/src/Harmonic/Controllers/Living/LivingStream.cs @@ -0,0 +1,203 @@ +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 + } +} diff --git a/src/Harmonic/Controllers/NeverRegisterAttribute.cs b/src/Harmonic/Controllers/NeverRegisterAttribute.cs new file mode 100644 index 0000000..26ca944 --- /dev/null +++ b/src/Harmonic/Controllers/NeverRegisterAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Controllers +{ + [AttributeUsage(AttributeTargets.Class)] + public class NeverRegisterAttribute : Attribute + { + + } +} diff --git a/src/Harmonic/Controllers/Record/RecordController.cs b/src/Harmonic/Controllers/Record/RecordController.cs new file mode 100644 index 0000000..5a574e2 --- /dev/null +++ b/src/Harmonic/Controllers/Record/RecordController.cs @@ -0,0 +1,18 @@ +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; + } + } +} diff --git a/src/Harmonic/Controllers/Record/RecordStream.cs b/src/Harmonic/Controllers/Record/RecordStream.cs new file mode 100644 index 0000000..36b6f41 --- /dev/null +++ b/src/Harmonic/Controllers/Record/RecordStream.cs @@ -0,0 +1,350 @@ +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)); + } + } +} diff --git a/src/Harmonic/Controllers/RtmpController.cs b/src/Harmonic/Controllers/RtmpController.cs new file mode 100644 index 0000000..838b14b --- /dev/null +++ b/src/Harmonic/Controllers/RtmpController.cs @@ -0,0 +1,49 @@ +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); + } + } +} diff --git a/src/Harmonic/Controllers/WebSocketController.cs b/src/Harmonic/Controllers/WebSocketController.cs new file mode 100644 index 0000000..49fcfde --- /dev/null +++ b/src/Harmonic/Controllers/WebSocketController.cs @@ -0,0 +1,46 @@ +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); + } +} diff --git a/src/Harmonic/Controllers/WebSocketPlayController.cs b/src/Harmonic/Controllers/WebSocketPlayController.cs new file mode 100644 index 0000000..67452f3 --- /dev/null +++ b/src/Harmonic/Controllers/WebSocketPlayController.cs @@ -0,0 +1,158 @@ +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 + } +} diff --git a/src/Harmonic/Harmonic.csproj b/src/Harmonic/Harmonic.csproj new file mode 100644 index 0000000..83e338b --- /dev/null +++ b/src/Harmonic/Harmonic.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp2.2 + latest + false + + + + true + TRACE + full + true + + + + false + + + + + + + + + + + + + + diff --git a/src/Harmonic/Hosting/IStartup.cs b/src/Harmonic/Hosting/IStartup.cs new file mode 100644 index 0000000..370ad99 --- /dev/null +++ b/src/Harmonic/Hosting/IStartup.cs @@ -0,0 +1,11 @@ +using System; +using Autofac; + +namespace Harmonic.Hosting +{ + public interface IStartup + { + void ConfigureServices(ContainerBuilder builder); + + } +} \ No newline at end of file diff --git a/src/Harmonic/Hosting/RtmpServer.cs b/src/Harmonic/Hosting/RtmpServer.cs new file mode 100644 index 0000000..b97b538 --- /dev/null +++ b/src/Harmonic/Hosting/RtmpServer.cs @@ -0,0 +1,134 @@ +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(); + } + } + } +} diff --git a/src/Harmonic/Hosting/RtmpServerBuilder.cs b/src/Harmonic/Hosting/RtmpServerBuilder.cs new file mode 100644 index 0000000..66e8940 --- /dev/null +++ b/src/Harmonic/Hosting/RtmpServerBuilder.cs @@ -0,0 +1,113 @@ +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; + } + + } +} \ No newline at end of file diff --git a/src/Harmonic/Hosting/RtmpServerOptions.cs b/src/Harmonic/Hosting/RtmpServerOptions.cs new file mode 100644 index 0000000..6eb621f --- /dev/null +++ b/src/Harmonic/Hosting/RtmpServerOptions.cs @@ -0,0 +1,163 @@ +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)); + } + } +} diff --git a/src/Harmonic/Hosting/WebSocketOptions.cs b/src/Harmonic/Hosting/WebSocketOptions.cs new file mode 100644 index 0000000..042c0fc --- /dev/null +++ b/src/Harmonic/Hosting/WebSocketOptions.cs @@ -0,0 +1,36 @@ +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(); + } + + } +} diff --git a/src/Harmonic/Networking/Amf/Common/Amf3Object.cs b/src/Harmonic/Networking/Amf/Common/Amf3Object.cs new file mode 100644 index 0000000..ff67778 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Common/Amf3Object.cs @@ -0,0 +1,48 @@ +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(); + } + } +} diff --git a/src/Harmonic/Networking/Amf/Common/TypeRegisterState.cs b/src/Harmonic/Networking/Amf/Common/TypeRegisterState.cs new file mode 100644 index 0000000..9eac0f7 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Common/TypeRegisterState.cs @@ -0,0 +1,12 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Amf/Common/Undefined.cs b/src/Harmonic/Networking/Amf/Common/Undefined.cs new file mode 100644 index 0000000..2a72579 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Common/Undefined.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Amf.Common +{ + public class Undefined + { + } +} diff --git a/src/Harmonic/Networking/Amf/Common/Unsupported.cs b/src/Harmonic/Networking/Amf/Common/Unsupported.cs new file mode 100644 index 0000000..661d44f --- /dev/null +++ b/src/Harmonic/Networking/Amf/Common/Unsupported.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Amf.Common +{ + public class Unsupported + { + } +} diff --git a/src/Harmonic/Networking/Amf/Data/IDynamicObject.cs b/src/Harmonic/Networking/Amf/Data/IDynamicObject.cs new file mode 100644 index 0000000..85de3e3 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Data/IDynamicObject.cs @@ -0,0 +1,13 @@ +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); + } +} diff --git a/src/Harmonic/Networking/Amf/Data/IExternalizable.cs b/src/Harmonic/Networking/Amf/Data/IExternalizable.cs new file mode 100644 index 0000000..49a707f --- /dev/null +++ b/src/Harmonic/Networking/Amf/Data/IExternalizable.cs @@ -0,0 +1,14 @@ +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); + } +} diff --git a/src/Harmonic/Networking/Amf/Data/Message.cs b/src/Harmonic/Networking/Amf/Data/Message.cs new file mode 100644 index 0000000..685459a --- /dev/null +++ b/src/Harmonic/Networking/Amf/Data/Message.cs @@ -0,0 +1,13 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0CommonValues.cs b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0CommonValues.cs new file mode 100644 index 0000000..7f7491a --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0CommonValues.cs @@ -0,0 +1,19 @@ +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); + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Reader.cs b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Reader.cs new file mode 100644 index 0000000..a0fc42c --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Reader.cs @@ -0,0 +1,838 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Type.cs b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Type.cs new file mode 100644 index 0000000..fe834d4 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Type.cs @@ -0,0 +1,28 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Writer.cs b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Writer.cs new file mode 100644 index 0000000..9cf4326 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf0/Amf0Writer.cs @@ -0,0 +1,419 @@ +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); + } + + + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf0/SerializationContext.cs b/src/Harmonic/Networking/Amf/Serialization/Amf0/SerializationContext.cs new file mode 100644 index 0000000..b5de0df --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf0/SerializationContext.cs @@ -0,0 +1,42 @@ +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(); + } + } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Array.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Array.cs new file mode 100644 index 0000000..abef877 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Array.cs @@ -0,0 +1,38 @@ +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; + } + } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3ClassTraits.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3ClassTraits.cs new file mode 100644 index 0000000..f53919e --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3ClassTraits.cs @@ -0,0 +1,51 @@ +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(); + } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3CommonValues.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3CommonValues.cs new file mode 100644 index 0000000..1268523 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3CommonValues.cs @@ -0,0 +1,17 @@ +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; + + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Dictionary.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Dictionary.cs new file mode 100644 index 0000000..13414f2 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Dictionary.cs @@ -0,0 +1,31 @@ +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) { } + + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Reader.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Reader.cs new file mode 100644 index 0000000..f87d245 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Reader.cs @@ -0,0 +1,1127 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Type.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Type.cs new file mode 100644 index 0000000..f561ee2 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Type.cs @@ -0,0 +1,28 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Writer.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Writer.cs new file mode 100644 index 0000000..45bad7d --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Writer.cs @@ -0,0 +1,700 @@ +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; + } + } + + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Xml.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Xml.cs new file mode 100644 index 0000000..dddf42b --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Amf3Xml.cs @@ -0,0 +1,16 @@ +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) { } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/SerializationContext.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/SerializationContext.cs new file mode 100644 index 0000000..c361142 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/SerializationContext.cs @@ -0,0 +1,46 @@ +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); + } + + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Amf3/Vector.cs b/src/Harmonic/Networking/Amf/Serialization/Amf3/Vector.cs new file mode 100644 index 0000000..035ceda --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Amf3/Vector.cs @@ -0,0 +1,47 @@ +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(); + } + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Attributes/ClassFieldAttribute.cs b/src/Harmonic/Networking/Amf/Serialization/Attributes/ClassFieldAttribute.cs new file mode 100644 index 0000000..ec1c445 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Attributes/ClassFieldAttribute.cs @@ -0,0 +1,12 @@ +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; + } +} diff --git a/src/Harmonic/Networking/Amf/Serialization/Attributes/TypedObjectAttribute.cs b/src/Harmonic/Networking/Amf/Serialization/Attributes/TypedObjectAttribute.cs new file mode 100644 index 0000000..f4d9a97 --- /dev/null +++ b/src/Harmonic/Networking/Amf/Serialization/Attributes/TypedObjectAttribute.cs @@ -0,0 +1,12 @@ +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; + } +} diff --git a/src/Harmonic/Networking/ConnectionInformation.cs b/src/Harmonic/Networking/ConnectionInformation.cs new file mode 100644 index 0000000..1d5ff43 --- /dev/null +++ b/src/Harmonic/Networking/ConnectionInformation.cs @@ -0,0 +1,21 @@ + + +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; } + } +} \ No newline at end of file diff --git a/src/Harmonic/Networking/Flv/Data/AacPacketType.cs b/src/Harmonic/Networking/Flv/Data/AacPacketType.cs new file mode 100644 index 0000000..a202d64 --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/AacPacketType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Flv.Data +{ + public enum AacPacketType + { + SequenceHeader, + Raw + } +} diff --git a/src/Harmonic/Networking/Flv/Data/AudioData.cs b/src/Harmonic/Networking/Flv/Data/AudioData.cs new file mode 100644 index 0000000..f3d6b5b --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/AudioData.cs @@ -0,0 +1,12 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Flv/Data/CodecId.cs b/src/Harmonic/Networking/Flv/Data/CodecId.cs new file mode 100644 index 0000000..8d581d5 --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/CodecId.cs @@ -0,0 +1,17 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Flv/Data/FlvAudioData.cs b/src/Harmonic/Networking/Flv/Data/FlvAudioData.cs new file mode 100644 index 0000000..110716a --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/FlvAudioData.cs @@ -0,0 +1,15 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Flv/Data/FlvVideoData.cs b/src/Harmonic/Networking/Flv/Data/FlvVideoData.cs new file mode 100644 index 0000000..dc58705 --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/FlvVideoData.cs @@ -0,0 +1,15 @@ +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; } + + } +} diff --git a/src/Harmonic/Networking/Flv/Data/FrameType.cs b/src/Harmonic/Networking/Flv/Data/FrameType.cs new file mode 100644 index 0000000..89bd40a --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/FrameType.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Flv.Data +{ + public enum FrameType + { + KeyFrame = 1, + InterFrame, + DisposableInterFrame, + GeneratedKeyFrame, + VideoInfoOrCommandFrame + } +} diff --git a/src/Harmonic/Networking/Flv/Data/SoundFormat.cs b/src/Harmonic/Networking/Flv/Data/SoundFormat.cs new file mode 100644 index 0000000..8ac53df --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/SoundFormat.cs @@ -0,0 +1,23 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Flv/Data/SoundRate.cs b/src/Harmonic/Networking/Flv/Data/SoundRate.cs new file mode 100644 index 0000000..ae36224 --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/SoundRate.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Flv.Data +{ + public enum SoundRate + { + Hz5Dot5, + Hz11, + Hz22, + Hz44 + } +} diff --git a/src/Harmonic/Networking/Flv/Data/SoundSize.cs b/src/Harmonic/Networking/Flv/Data/SoundSize.cs new file mode 100644 index 0000000..5543e83 --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/SoundSize.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Flv.Data +{ + public enum SoundSize + { + Snd8Bit, + Snd16Bit + } +} diff --git a/src/Harmonic/Networking/Flv/Data/SoundType.cs b/src/Harmonic/Networking/Flv/Data/SoundType.cs new file mode 100644 index 0000000..6342994 --- /dev/null +++ b/src/Harmonic/Networking/Flv/Data/SoundType.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Flv.Data +{ + public enum SoundType + { + SndMono, + SndStereo + } +} diff --git a/src/Harmonic/Networking/Flv/FlvDemuxer.cs b/src/Harmonic/Networking/Flv/FlvDemuxer.cs new file mode 100644 index 0000000..9e9af4d --- /dev/null +++ b/src/Harmonic/Networking/Flv/FlvDemuxer.cs @@ -0,0 +1,178 @@ +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); + } + } + } + } +} diff --git a/src/Harmonic/Networking/Flv/FlvMuxer.cs b/src/Harmonic/Networking/Flv/FlvMuxer.cs new file mode 100644 index 0000000..b24d530 --- /dev/null +++ b/src/Harmonic/Networking/Flv/FlvMuxer.cs @@ -0,0 +1,96 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/ChunkStreamContext.cs b/src/Harmonic/Networking/Rtmp/ChunkStreamContext.cs new file mode 100644 index 0000000..69bef2e --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/ChunkStreamContext.cs @@ -0,0 +1,587 @@ +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; + } + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/ChunkBasicHeader.cs b/src/Harmonic/Networking/Rtmp/Data/ChunkBasicHeader.cs new file mode 100644 index 0000000..68e9292 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/ChunkBasicHeader.cs @@ -0,0 +1,14 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/ChunkHeader.cs b/src/Harmonic/Networking/Rtmp/Data/ChunkHeader.cs new file mode 100644 index 0000000..110fe3b --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/ChunkHeader.cs @@ -0,0 +1,15 @@ +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; } + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/ChunkHeaderType.cs b/src/Harmonic/Networking/Rtmp/Data/ChunkHeaderType.cs new file mode 100644 index 0000000..afd5075 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/ChunkHeaderType.cs @@ -0,0 +1,18 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/Message.cs b/src/Harmonic/Networking/Rtmp/Data/Message.cs new file mode 100644 index 0000000..0dd0491 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/Message.cs @@ -0,0 +1,22 @@ +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); + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/MessageHeader.cs b/src/Harmonic/Networking/Rtmp/Data/MessageHeader.cs new file mode 100644 index 0000000..192fbc3 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/MessageHeader.cs @@ -0,0 +1,19 @@ +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(); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/MessageType.cs b/src/Harmonic/Networking/Rtmp/Data/MessageType.cs new file mode 100644 index 0000000..c59ac33 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/MessageType.cs @@ -0,0 +1,36 @@ +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 + + + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/SharedObjectMessage.cs b/src/Harmonic/Networking/Rtmp/Data/SharedObjectMessage.cs new file mode 100644 index 0000000..7ee3048 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/SharedObjectMessage.cs @@ -0,0 +1,14 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/Data/UserControlMessageEvents.cs b/src/Harmonic/Networking/Rtmp/Data/UserControlMessageEvents.cs new file mode 100644 index 0000000..33e9764 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Data/UserControlMessageEvents.cs @@ -0,0 +1,17 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/Exceptions/UnknownMessageReceivedException.cs b/src/Harmonic/Networking/Rtmp/Exceptions/UnknownMessageReceivedException.cs new file mode 100644 index 0000000..4709189 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Exceptions/UnknownMessageReceivedException.cs @@ -0,0 +1,17 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/HandshakeContext.cs b/src/Harmonic/Networking/Rtmp/HandshakeContext.cs new file mode 100644 index 0000000..d1d2815 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/HandshakeContext.cs @@ -0,0 +1,125 @@ +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(); + } + + } + + } +} diff --git a/src/Harmonic/Networking/Rtmp/IOPipeLine.cs b/src/Harmonic/Networking/Rtmp/IOPipeLine.cs new file mode 100644 index 0000000..cb7972f --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/IOPipeLine.cs @@ -0,0 +1,268 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/MessageReadingState.cs b/src/Harmonic/Networking/Rtmp/MessageReadingState.cs new file mode 100644 index 0000000..4725cba --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/MessageReadingState.cs @@ -0,0 +1,21 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/AbortMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/AbortMessage.cs new file mode 100644 index 0000000..fdb37f9 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/AbortMessage.cs @@ -0,0 +1,39 @@ +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); + } + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/AcknowledgementMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/AcknowledgementMessage.cs new file mode 100644 index 0000000..251b604 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/AcknowledgementMessage.cs @@ -0,0 +1,40 @@ +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); + } + + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/AggregateMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/AggregateMessage.cs new file mode 100644 index 0000000..311ae17 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/AggregateMessage.cs @@ -0,0 +1,99 @@ +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); + } + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/AmfEncodingVersion.cs b/src/Harmonic/Networking/Rtmp/Messages/AmfEncodingVersion.cs new file mode 100644 index 0000000..55e890b --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/AmfEncodingVersion.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Rtmp.Messages +{ + public enum AmfEncodingVersion + { + Amf0, + Amf3 + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/AudioMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/AudioMessage.cs new file mode 100644 index 0000000..fa4ab6c --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/AudioMessage.cs @@ -0,0 +1,38 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/CallCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/CallCommandMessage.cs new file mode 100644 index 0000000..dd28c9e --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/CallCommandMessage.cs @@ -0,0 +1,20 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessage.cs new file mode 100644 index 0000000..a38750c --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessage.cs @@ -0,0 +1,145 @@ +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); + } + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessageFactory.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessageFactory.cs new file mode 100644 index 0000000..d6194c9 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/CommandMessageFactory.cs @@ -0,0 +1,77 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/ConnectCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/ConnectCommandMessage.cs new file mode 100644 index 0000000..2ee1d8b --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/ConnectCommandMessage.cs @@ -0,0 +1,19 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/CreateStreamCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/CreateStreamCommandMessage.cs new file mode 100644 index 0000000..85c91b9 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/CreateStreamCommandMessage.cs @@ -0,0 +1,16 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/DeleteStreamCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/DeleteStreamCommandMessage.cs new file mode 100644 index 0000000..e325430 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/DeleteStreamCommandMessage.cs @@ -0,0 +1,19 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/OnStatusCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/OnStatusCommandMessage.cs new file mode 100644 index 0000000..4d6622e --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/OnStatusCommandMessage.cs @@ -0,0 +1,20 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/PauseCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/PauseCommandMessage.cs new file mode 100644 index 0000000..81d665b --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/PauseCommandMessage.cs @@ -0,0 +1,21 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/Play2CommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/Play2CommandMessage.cs new file mode 100644 index 0000000..c72925a --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/Play2CommandMessage.cs @@ -0,0 +1,19 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/PlayCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/PlayCommandMessage.cs new file mode 100644 index 0000000..96c4a3c --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/PlayCommandMessage.cs @@ -0,0 +1,26 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/PublishCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/PublishCommandMessage.cs new file mode 100644 index 0000000..b2b3146 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/PublishCommandMessage.cs @@ -0,0 +1,21 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveAudioCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveAudioCommandMessage.cs new file mode 100644 index 0000000..f43c037 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveAudioCommandMessage.cs @@ -0,0 +1,19 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveVideoCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveVideoCommandMessage.cs new file mode 100644 index 0000000..1bf8c28 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/ReceiveVideoCommandMessage.cs @@ -0,0 +1,19 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/ReturnResultCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/ReturnResultCommandMessage.cs new file mode 100644 index 0000000..7669513 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/ReturnResultCommandMessage.cs @@ -0,0 +1,44 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/Commands/SeekCommandMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/Commands/SeekCommandMessage.cs new file mode 100644 index 0000000..b23f7ad --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/Commands/SeekCommandMessage.cs @@ -0,0 +1,19 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/ControlMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/ControlMessage.cs new file mode 100644 index 0000000..cc26989 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/ControlMessage.cs @@ -0,0 +1,13 @@ +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() + { } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/DataMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/DataMessage.cs new file mode 100644 index 0000000..430667f --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/DataMessage.cs @@ -0,0 +1,72 @@ +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); + } + } + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/SetChunkSizeMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/SetChunkSizeMessage.cs new file mode 100644 index 0000000..8bc0908 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/SetChunkSizeMessage.cs @@ -0,0 +1,42 @@ +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); + } + + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/SetPeerBandwidthMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/SetPeerBandwidthMessage.cs new file mode 100644 index 0000000..c90154f --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/SetPeerBandwidthMessage.cs @@ -0,0 +1,49 @@ +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); + } + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingRequestMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingRequestMessage.cs new file mode 100644 index 0000000..d2e96d6 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingRequestMessage.cs @@ -0,0 +1,48 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingResponseMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingResponseMessage.cs new file mode 100644 index 0000000..d78f288 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/PingResponseMessage.cs @@ -0,0 +1,48 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/SetBufferLengthMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/SetBufferLengthMessage.cs new file mode 100644 index 0000000..1edcbe2 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/SetBufferLengthMessage.cs @@ -0,0 +1,54 @@ +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)); + } + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamBeginMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamBeginMessage.cs new file mode 100644 index 0000000..f6fa5c1 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamBeginMessage.cs @@ -0,0 +1,48 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamDryMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamDryMessage.cs new file mode 100644 index 0000000..a0df2fa --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamDryMessage.cs @@ -0,0 +1,48 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamEofMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamEofMessage.cs new file mode 100644 index 0000000..f32bdb3 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamEofMessage.cs @@ -0,0 +1,48 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamIsRecordedMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamIsRecordedMessage.cs new file mode 100644 index 0000000..97b2f2c --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/StreamIsRecordedMessage.cs @@ -0,0 +1,48 @@ +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)); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessage.cs new file mode 100644 index 0000000..9467412 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessage.cs @@ -0,0 +1,33 @@ +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() + { + } + + } + +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessageFactory.cs b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessageFactory.cs new file mode 100644 index 0000000..0823dd0 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/UserControlMessages/UserControlMessageFactory.cs @@ -0,0 +1,37 @@ +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); + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/VideoMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/VideoMessage.cs new file mode 100644 index 0000000..2debbae --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/VideoMessage.cs @@ -0,0 +1,41 @@ +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)); + } + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Messages/WindowAcknowledgementSizeMessage.cs b/src/Harmonic/Networking/Rtmp/Messages/WindowAcknowledgementSizeMessage.cs new file mode 100644 index 0000000..f7f5d96 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Messages/WindowAcknowledgementSizeMessage.cs @@ -0,0 +1,39 @@ +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); + } + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/NetConnection.cs b/src/Harmonic/Networking/Rtmp/NetConnection.cs new file mode 100644 index 0000000..05b5152 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/NetConnection.cs @@ -0,0 +1,200 @@ +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 + + + } +} diff --git a/src/Harmonic/Networking/Rtmp/NetStream.cs b/src/Harmonic/Networking/Rtmp/NetStream.cs new file mode 100644 index 0000000..3eab300 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/NetStream.cs @@ -0,0 +1,46 @@ +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 + + } +} diff --git a/src/Harmonic/Networking/Rtmp/RtmpChunkStream.cs b/src/Harmonic/Networking/Rtmp/RtmpChunkStream.cs new file mode 100644 index 0000000..02273e8 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/RtmpChunkStream.cs @@ -0,0 +1,59 @@ +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 + + } +} diff --git a/src/Harmonic/Networking/Rtmp/RtmpControlChunkStream.cs b/src/Harmonic/Networking/Rtmp/RtmpControlChunkStream.cs new file mode 100644 index 0000000..21b61d8 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/RtmpControlChunkStream.cs @@ -0,0 +1,20 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/RtmpControlMessageStream.cs b/src/Harmonic/Networking/Rtmp/RtmpControlMessageStream.cs new file mode 100644 index 0000000..5aaf553 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/RtmpControlMessageStream.cs @@ -0,0 +1,18 @@ +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) + { + } + } +} diff --git a/src/Harmonic/Networking/Rtmp/RtmpMessageStream.cs b/src/Harmonic/Networking/Rtmp/RtmpMessageStream.cs new file mode 100644 index 0000000..d40691f --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/RtmpMessageStream.cs @@ -0,0 +1,102 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/RtmpSession.cs b/src/Harmonic/Networking/Rtmp/RtmpSession.cs new file mode 100644 index 0000000..540313a --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/RtmpSession.cs @@ -0,0 +1,325 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/Serialization/OptionalArgumentAttribute.cs b/src/Harmonic/Networking/Rtmp/Serialization/OptionalArgumentAttribute.cs new file mode 100644 index 0000000..09bc00b --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Serialization/OptionalArgumentAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Rtmp.Serialization +{ + [AttributeUsage(AttributeTargets.Property)] + public class OptionalArgumentAttribute : Attribute + { + } +} diff --git a/src/Harmonic/Networking/Rtmp/Serialization/RtmpCommandAttribute.cs b/src/Harmonic/Networking/Rtmp/Serialization/RtmpCommandAttribute.cs new file mode 100644 index 0000000..61325d3 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Serialization/RtmpCommandAttribute.cs @@ -0,0 +1,12 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Serialization/RtmpMessageAttribute.cs b/src/Harmonic/Networking/Rtmp/Serialization/RtmpMessageAttribute.cs new file mode 100644 index 0000000..3b44ba4 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Serialization/RtmpMessageAttribute.cs @@ -0,0 +1,19 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Serialization/SerializationContext.cs b/src/Harmonic/Networking/Rtmp/Serialization/SerializationContext.cs new file mode 100644 index 0000000..9162173 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Serialization/SerializationContext.cs @@ -0,0 +1,21 @@ +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; + + } +} diff --git a/src/Harmonic/Networking/Rtmp/Serialization/UserControlMessageAttribute.cs b/src/Harmonic/Networking/Rtmp/Serialization/UserControlMessageAttribute.cs new file mode 100644 index 0000000..cfea54a --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Serialization/UserControlMessageAttribute.cs @@ -0,0 +1,12 @@ +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; } + } +} diff --git a/src/Harmonic/Networking/Rtmp/Streaming/PublishingType.cs b/src/Harmonic/Networking/Rtmp/Streaming/PublishingType.cs new file mode 100644 index 0000000..48b80f4 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Streaming/PublishingType.cs @@ -0,0 +1,18 @@ +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 + } +} diff --git a/src/Harmonic/Networking/Rtmp/Streaming/PublishingTypeNameAttribute.cs b/src/Harmonic/Networking/Rtmp/Streaming/PublishingTypeNameAttribute.cs new file mode 100644 index 0000000..1dbe8ff --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Streaming/PublishingTypeNameAttribute.cs @@ -0,0 +1,46 @@ +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); + } + } + + +} diff --git a/src/Harmonic/Networking/Rtmp/Supervisor.cs b/src/Harmonic/Networking/Rtmp/Supervisor.cs new file mode 100644 index 0000000..9b89e24 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/Supervisor.cs @@ -0,0 +1,89 @@ +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 + { + + } + } + } +} \ No newline at end of file diff --git a/src/Harmonic/Networking/Rtmp/WriteState.cs b/src/Harmonic/Networking/Rtmp/WriteState.cs new file mode 100644 index 0000000..ce98e52 --- /dev/null +++ b/src/Harmonic/Networking/Rtmp/WriteState.cs @@ -0,0 +1,14 @@ +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; + } +} diff --git a/src/Harmonic/Networking/Utils/NetworkBitConverter.cs b/src/Harmonic/Networking/Utils/NetworkBitConverter.cs new file mode 100644 index 0000000..bbbb7c4 --- /dev/null +++ b/src/Harmonic/Networking/Utils/NetworkBitConverter.cs @@ -0,0 +1,176 @@ +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; + } + } +} diff --git a/src/Harmonic/Networking/Utils/StreamHelper.cs b/src/Harmonic/Networking/Utils/StreamHelper.cs new file mode 100644 index 0000000..6d08796 --- /dev/null +++ b/src/Harmonic/Networking/Utils/StreamHelper.cs @@ -0,0 +1,57 @@ +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(); + } + } + } +} diff --git a/src/Harmonic/Networking/WebSocket/WebSocketSession.cs b/src/Harmonic/Networking/WebSocket/WebSocketSession.cs new file mode 100644 index 0000000..36215b6 --- /dev/null +++ b/src/Harmonic/Networking/WebSocket/WebSocketSession.cs @@ -0,0 +1,110 @@ +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); + } + } +} diff --git a/src/Harmonic/Rpc/CommandObjectAttribute.cs b/src/Harmonic/Rpc/CommandObjectAttribute.cs new file mode 100644 index 0000000..00ba505 --- /dev/null +++ b/src/Harmonic/Rpc/CommandObjectAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Networking.Rtmp +{ + [AttributeUsage(AttributeTargets.Parameter)] + public class CommandObjectAttribute : Attribute + { + } +} diff --git a/src/Harmonic/Rpc/FromCommandObjectAttribute.cs b/src/Harmonic/Rpc/FromCommandObjectAttribute.cs new file mode 100644 index 0000000..4c082f6 --- /dev/null +++ b/src/Harmonic/Rpc/FromCommandObjectAttribute.cs @@ -0,0 +1,14 @@ +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; + } +} diff --git a/src/Harmonic/Rpc/FromOptionalArgumentAttribute.cs b/src/Harmonic/Rpc/FromOptionalArgumentAttribute.cs new file mode 100644 index 0000000..6e6cd38 --- /dev/null +++ b/src/Harmonic/Rpc/FromOptionalArgumentAttribute.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Harmonic.Rpc +{ + [AttributeUsage(AttributeTargets.Parameter)] + public class FromOptionalArgumentAttribute : Attribute + { + } +} diff --git a/src/Harmonic/Rpc/RpcMethodAttribute.cs b/src/Harmonic/Rpc/RpcMethodAttribute.cs new file mode 100644 index 0000000..63b941a --- /dev/null +++ b/src/Harmonic/Rpc/RpcMethodAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Harmonic.Rpc +{ + public class RpcMethodAttribute : Attribute + { + public string Name { get; set; } = null; + public RpcMethodAttribute(string name = null) + { + Name = name; + } + + } +} \ No newline at end of file diff --git a/src/Harmonic/Rpc/RpcService.cs b/src/Harmonic/Rpc/RpcService.cs new file mode 100644 index 0000000..abdf1f7 --- /dev/null +++ b/src/Harmonic/Rpc/RpcService.cs @@ -0,0 +1,214 @@ +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); + } + } + } + } + } +} diff --git a/src/Harmonic/Service/PublisherSessionService.cs b/src/Harmonic/Service/PublisherSessionService.cs new file mode 100644 index 0000000..214986d --- /dev/null +++ b/src/Harmonic/Service/PublisherSessionService.cs @@ -0,0 +1,46 @@ +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; + } + + } +} \ No newline at end of file diff --git a/src/Harmonic/Service/RecordService.cs b/src/Harmonic/Service/RecordService.cs new file mode 100644 index 0000000..9e92592 --- /dev/null +++ b/src/Harmonic/Service/RecordService.cs @@ -0,0 +1,23 @@ +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)); + } + + } +} diff --git a/src/Harmonic/Service/RecordServiceConfiguration.cs b/src/Harmonic/Service/RecordServiceConfiguration.cs new file mode 100644 index 0000000..30673b5 --- /dev/null +++ b/src/Harmonic/Service/RecordServiceConfiguration.cs @@ -0,0 +1,12 @@ +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}"; + } +} diff --git a/src/LiveServer.sln b/src/LiveServer.sln new file mode 100644 index 0000000..fb43013 --- /dev/null +++ b/src/LiveServer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29324.140 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiveServer", "LiveServer\LiveServer.csproj", "{0CB18A48-6D69-4823-937E-59AB249C14BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Harmonic", "Harmonic\Harmonic.csproj", "{7C6859ED-7C6E-4C48-8304-ADC5C38DAABB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0CB18A48-6D69-4823-937E-59AB249C14BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CB18A48-6D69-4823-937E-59AB249C14BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CB18A48-6D69-4823-937E-59AB249C14BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CB18A48-6D69-4823-937E-59AB249C14BB}.Release|Any CPU.Build.0 = Release|Any CPU + {7C6859ED-7C6E-4C48-8304-ADC5C38DAABB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C6859ED-7C6E-4C48-8304-ADC5C38DAABB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C6859ED-7C6E-4C48-8304-ADC5C38DAABB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C6859ED-7C6E-4C48-8304-ADC5C38DAABB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E1F486F8-CEBD-4DED-9F13-095F31384E8E} + EndGlobalSection +EndGlobal diff --git a/src/LiveServer/LiveServer.csproj b/src/LiveServer/LiveServer.csproj new file mode 100644 index 0000000..d796500 --- /dev/null +++ b/src/LiveServer/LiveServer.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.0 + + + + + + + + + + + diff --git a/src/LiveServer/LiveServer.csproj.user b/src/LiveServer/LiveServer.csproj.user new file mode 100644 index 0000000..3e40472 --- /dev/null +++ b/src/LiveServer/LiveServer.csproj.user @@ -0,0 +1,6 @@ + + + + <_LastSelectedProfileId>C:\Users\sysru\Desktop\LiveServer\src\LiveServer\Properties\PublishProfiles\FolderProfile.pubxml + + \ No newline at end of file diff --git a/src/LiveServer/Program.cs b/src/LiveServer/Program.cs new file mode 100644 index 0000000..88ce37a --- /dev/null +++ b/src/LiveServer/Program.cs @@ -0,0 +1,22 @@ +using Harmonic.Hosting; +using System; +using System.Net; + +namespace LiveServer +{ + 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(); + } + } +} diff --git a/src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml b/src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..14e11c1 --- /dev/null +++ b/src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,15 @@ + + + + + FileSystem + Release + Any CPU + netcoreapp3.0 + bin\Release\publish\ + linux-x64 + true + + \ No newline at end of file diff --git a/src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml.user b/src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml.user new file mode 100644 index 0000000..312c6e3 --- /dev/null +++ b/src/LiveServer/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/LiveServer/StartUp.cs b/src/LiveServer/StartUp.cs new file mode 100644 index 0000000..5bc6c33 --- /dev/null +++ b/src/LiveServer/StartUp.cs @@ -0,0 +1,16 @@ +using Autofac; +using Harmonic.Hosting; +using System; +using System.Collections.Generic; +using System.Text; + +namespace LiveServer +{ + class Startup : IStartup + { + public void ConfigureServices(ContainerBuilder builder) + { + + } + } +}