-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a58f6ee
commit 0f2db0d
Showing
4 changed files
with
242 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
samples/LiveStreamingServerNet.RtmpClientPublishDemo/FlvReader.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
using LiveStreamingServerNet.Utilities.Buffers; | ||
using LiveStreamingServerNet.Utilities.Buffers.Contracts; | ||
|
||
namespace LiveStreamingServerNet.RtmpClientPublishDemo | ||
{ | ||
public class FlvReader : IDisposable | ||
{ | ||
private readonly Stream _stream; | ||
private readonly IDataBuffer _buffer; | ||
|
||
public FlvReader(Stream stream) | ||
{ | ||
_stream = stream ?? throw new ArgumentNullException(nameof(stream)); | ||
_buffer = new DataBuffer(); | ||
} | ||
|
||
public async ValueTask<FlvHeader?> ReadHeaderAsync(CancellationToken cancellationToken = default) | ||
{ | ||
try | ||
{ | ||
await _buffer.FromStreamData(_stream, 9, cancellationToken); | ||
|
||
var signature = _buffer.ReadBytes(3); | ||
if (signature[0] != 'F' || signature[1] != 'L' || signature[2] != 'V') | ||
throw new InvalidDataException("Invalid FLV signature."); | ||
|
||
var version = _buffer.ReadByte(); | ||
var flags = _buffer.ReadByte(); | ||
var headerSize = _buffer.ReadInt32BigEndian(); | ||
|
||
var header = new FlvHeader(version, flags, headerSize); | ||
|
||
if (headerSize > 9) | ||
await _buffer.FromStreamData(_stream, headerSize - 9, cancellationToken); | ||
|
||
await _buffer.FromStreamData(_stream, 4, cancellationToken); | ||
var previousTagSize = _buffer.ReadUInt32BigEndian(); | ||
|
||
if (previousTagSize != 0) | ||
throw new InvalidDataException("Invalid PreviousTagSize."); | ||
|
||
return header; | ||
} | ||
catch (EndOfStreamException) | ||
{ | ||
return null; | ||
} | ||
} | ||
|
||
public async ValueTask<FlvTag?> ReadTagAsync(CancellationToken cancellationToken = default) | ||
{ | ||
try | ||
{ | ||
await _buffer.FromStreamData(_stream, FlvTagHeader.Size, cancellationToken); | ||
|
||
var tagHeader = FlvTagHeader.Read(_buffer); | ||
|
||
await _buffer.FromStreamData(_stream, (int)tagHeader.DataSize, cancellationToken); | ||
var payload = _buffer.ToRentedBuffer(); | ||
|
||
await _buffer.FromStreamData(_stream, 4, cancellationToken); | ||
var previousTagSize = _buffer.ReadUInt32BigEndian(); | ||
|
||
if (previousTagSize != (11 + tagHeader.DataSize)) | ||
throw new InvalidDataException("Mismatch in PreviousTagSize."); | ||
|
||
return new FlvTag(tagHeader, payload); | ||
} | ||
catch (EndOfStreamException) | ||
{ | ||
return null; | ||
} | ||
} | ||
|
||
public void Dispose() | ||
{ | ||
_buffer.Dispose(); | ||
} | ||
} | ||
|
||
public enum FlvTagType : byte | ||
{ | ||
Audio = 8, | ||
Video = 9, | ||
ScriptData = 18 | ||
} | ||
|
||
public record struct FlvHeader(byte Version, byte Flags, int HeaderSize) | ||
{ | ||
public bool HasAudio => (Flags & 0x04) != 0; | ||
public bool HasVideo => (Flags & 0x01) != 0; | ||
} | ||
|
||
public record struct FlvTagHeader(FlvTagType TagType, uint DataSize, uint Timestamp) | ||
{ | ||
public const int Size = 11; | ||
|
||
public static FlvTagHeader Read(IDataBuffer dataBuffer) | ||
{ | ||
var tagType = (FlvTagType)dataBuffer.ReadByte(); | ||
var dataSize = dataBuffer.ReadUInt24BigEndian(); | ||
|
||
var timestampLower = dataBuffer.ReadUInt24BigEndian(); | ||
var timestampExtended = dataBuffer.ReadByte(); | ||
var timestamp = ((uint)timestampExtended << 24) | timestampLower; | ||
|
||
dataBuffer.ReadUInt24BigEndian(); | ||
|
||
return new FlvTagHeader(tagType, dataSize, timestamp); | ||
} | ||
} | ||
|
||
public record FlvTag(FlvTagHeader Header, IRentedBuffer Payload); | ||
} |
20 changes: 20 additions & 0 deletions
20
...eamingServerNet.RtmpClientPublishDemo/LiveStreamingServerNet.RtmpClientPublishDemo.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net8.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<IsPackable>false</IsPackable> | ||
<ServerGarbageCollection>false</ServerGarbageCollection> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\src\LiveStreamingServerNet.Rtmp.Client\LiveStreamingServerNet.Rtmp.Client.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
100 changes: 100 additions & 0 deletions
100
samples/LiveStreamingServerNet.RtmpClientPublishDemo/Program.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
using LiveStreamingServerNet.Rtmp.Client; | ||
using LiveStreamingServerNet.Rtmp.Client.Contracts; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Logging; | ||
using System.Diagnostics; | ||
|
||
namespace LiveStreamingServerNet.RtmpClientPublishDemo | ||
{ | ||
public class Program | ||
{ | ||
/// <summary> | ||
/// Connect to rtmp://127.0.0.1/live/demo and publish the stream from input.flv. | ||
/// </summary> | ||
public static async Task Main() | ||
{ | ||
var rtmpUrl = "rtmp://127.0.0.1/live/demo"; | ||
var parsedRtmpUrl = await RtmpUrlParser.ParseAsync(rtmpUrl); | ||
|
||
var serverEndPoint = parsedRtmpUrl.ServerEndPoint; | ||
var information = new Dictionary<string, object> { ["tcUrl"] = parsedRtmpUrl.TcUrl }; | ||
var appName = parsedRtmpUrl.AppName; | ||
var streamName = parsedRtmpUrl.StreamName; | ||
|
||
var rtmpClient = RtmpClientBuilder.Create() | ||
.ConfigureLogging(options => options.AddConsole().SetMinimumLevel(LogLevel.Trace)) | ||
.Build(); | ||
|
||
var logger = rtmpClient.Services.GetRequiredService<ILogger<Program>>(); | ||
|
||
await rtmpClient.ConnectAsync(serverEndPoint, appName, information); | ||
|
||
var rtmpStream = await rtmpClient.CreateStreamAsync(); | ||
|
||
await PublishStreamFromFlvAsync(streamName, rtmpClient, rtmpStream, logger); | ||
} | ||
|
||
private static async Task PublishStreamFromFlvAsync(string streamName, IRtmpClient rtmpClient, IRtmpStream rtmpStream, ILogger<Program> logger) | ||
{ | ||
using var fileStream = new FileStream("input.flv", FileMode.Open, FileAccess.Read, FileShare.Read, 512 * 1024); | ||
using var flvReader = new FlvReader(fileStream); | ||
|
||
rtmpStream.OnUserControlEventReceived += (sender, e) => | ||
logger.LogInformation($"User control event received: {e.EventType}"); | ||
|
||
rtmpStream.OnStatusReceived += (sender, e) => | ||
logger.LogInformation($"Status received: {e.Code}"); | ||
|
||
rtmpStream.Publish.Publish(streamName); | ||
|
||
await Task.WhenAny(SendMediaDataAsync(rtmpStream, flvReader), rtmpClient.UntilStoppedAsync()); | ||
|
||
async Task SendMediaDataAsync(IRtmpStream stream, FlvReader flvReader) | ||
{ | ||
var header = await flvReader.ReadHeaderAsync(); | ||
|
||
if (header == null) | ||
return; | ||
|
||
(uint Timestamp, DateTime Time)? start = null; | ||
|
||
while (true) | ||
{ | ||
var tag = await flvReader.ReadTagAsync(); | ||
|
||
if (tag == null) | ||
return; | ||
|
||
try | ||
{ | ||
if (tag.Header.TagType == FlvTagType.Audio) | ||
{ | ||
await stream.Publish.SendAudioDataAsync(tag.Payload, tag.Header.Timestamp); | ||
} | ||
else if (tag.Header.TagType == FlvTagType.Video) | ||
{ | ||
await stream.Publish.SendVideoDataAsync(tag.Payload, tag.Header.Timestamp); | ||
} | ||
|
||
if (!start.HasValue) | ||
{ | ||
start = (tag.Header.Timestamp, DateTime.UtcNow); | ||
} | ||
|
||
var intendedTime = tag.Header.Timestamp - start.Value.Timestamp; | ||
var elapsedTime = (DateTime.UtcNow - start.Value.Time).TotalMilliseconds; | ||
|
||
var delay = intendedTime - elapsedTime; | ||
|
||
if (delay > 0) | ||
await Task.Delay((int)delay); | ||
} | ||
finally | ||
{ | ||
tag.Payload.Unclaim(); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |