|
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
|
|
using System.IO;
|
|
|
using System.Linq;
|
|
|
using System.Net;
|
|
|
+using System.Runtime.InteropServices;
|
|
|
using System.Text;
|
|
|
using System.Threading;
|
|
|
using System.Threading.Tasks;
|
|
@@ -27,6 +28,8 @@ namespace DotnetRtmpServer.Net.Http
|
|
|
|
|
|
private bool _isClosed;
|
|
|
|
|
|
+ public event AsyncEventHandler<HttpPlayEventArgs> HttpPlay;
|
|
|
+
|
|
|
public HttpServer()
|
|
|
{
|
|
|
if (Config.EnableHlsExtension)
|
|
@@ -143,57 +146,82 @@ namespace DotnetRtmpServer.Net.Http
|
|
|
context.Response.ContentType = "application/octet-stream";
|
|
|
context.Response.StatusCode = (int)HttpStatusCode.OK;
|
|
|
var channelName = path.Replace(".flv", string.Empty);
|
|
|
- var player = new HttpFlvPlayer(channelName, context);
|
|
|
- player.Closed += async(sender, args) =>
|
|
|
+ var appAndStream = channelName.Split('/');
|
|
|
+ if (appAndStream.Length != 2)
|
|
|
{
|
|
|
- lock (_players)
|
|
|
+ Logger.WriteLineWarn($"Request publisher for channel {channelName} does not exist.");
|
|
|
+ await Write404Async(context).ConfigureAwait(false);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var app = appAndStream[0];
|
|
|
+ var stream = appAndStream[1];
|
|
|
+ if (Config.ProxyMode)
|
|
|
+ {
|
|
|
+ channelName = $"proxy/{app}/{stream}";
|
|
|
+ }
|
|
|
+ var player = new HttpFlvPlayer(channelName, context);
|
|
|
+ player.Closed += async (sender, args) =>
|
|
|
{
|
|
|
- if (_players.TryGetValue(player.ChannelName, out var players))
|
|
|
+ lock (_players)
|
|
|
{
|
|
|
- players.TryRemove(player.Id, out _);
|
|
|
- Logger.WriteLineInfo($"One http-flv player for channel:{player.ChannelName} closed.");
|
|
|
- if (players.Count == 0)
|
|
|
+ if (_players.TryGetValue(player.ChannelName, out var players))
|
|
|
{
|
|
|
- _players.TryRemove(player.ChannelName, out _);
|
|
|
+ players.TryRemove(player.Id, out _);
|
|
|
Logger.WriteLineInfo($"One http-flv player for channel:{player.ChannelName} closed.");
|
|
|
+ if (players.Count == 0)
|
|
|
+ {
|
|
|
+ _players.TryRemove(player.ChannelName, out _);
|
|
|
+ Logger.WriteLineInfo($"One http-flv player for channel:{player.ChannelName} closed.");
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- await Task.CompletedTask.ConfigureAwait(false);
|
|
|
- };
|
|
|
- if (_publishers.TryGetValue(player.ChannelName, out var publisher))
|
|
|
- {
|
|
|
- //Send meta data
|
|
|
- await player.SendMetaDataAsync(publisher.VideoConfigureRecord, publisher.AudioConfigureRecord).ConfigureAwait(false);
|
|
|
- if (Config.EnablePublishVideoCache)
|
|
|
+ await Task.CompletedTask.ConfigureAwait(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ //Notify the server create proxy if in proxy mode.
|
|
|
+ if (Config.ProxyMode)
|
|
|
{
|
|
|
- //Fast play
|
|
|
- var videoCaches = publisher.GetVideoCaches();
|
|
|
- foreach (var videoCache in videoCaches)
|
|
|
+ if (HttpPlay != null)
|
|
|
{
|
|
|
- await player.SendVideoDataAsync(videoCache).ConfigureAwait(false);
|
|
|
+ await HttpPlay(this, new HttpPlayEventArgs(app, stream)).ConfigureAwait(false);
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- Logger.WriteLineWarn($"Request publisher for channel {player.ChannelName} does not exist.");
|
|
|
- await player.CloseAsync().ConfigureAwait(false);
|
|
|
- }
|
|
|
|
|
|
- if (!player.IsClosed)
|
|
|
- {
|
|
|
- _players.AddOrUpdate(channelName, _ =>
|
|
|
+ if (_publishers.TryGetValue(player.ChannelName, out var publisher))
|
|
|
{
|
|
|
- var players = new ConcurrentDictionary<ushort, IPlayer>();
|
|
|
- players.TryAdd(player.Id, player);
|
|
|
- return players;
|
|
|
- }, (_, players) =>
|
|
|
+ //Send meta data
|
|
|
+ await player.SendMetaDataAsync(publisher.VideoConfigureRecord, publisher.AudioConfigureRecord).ConfigureAwait(false);
|
|
|
+ if (Config.EnablePublishVideoCache)
|
|
|
+ {
|
|
|
+ //Fast play
|
|
|
+ var videoCaches = publisher.GetVideoCaches();
|
|
|
+ foreach (var videoCache in videoCaches)
|
|
|
+ {
|
|
|
+ await player.SendVideoDataAsync(videoCache).ConfigureAwait(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ Logger.WriteLineWarn($"Request publisher for channel {player.ChannelName} does not exist.");
|
|
|
+ await player.CloseAsync().ConfigureAwait(false);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!player.IsClosed)
|
|
|
{
|
|
|
- players.TryAdd(player.Id,player);
|
|
|
- return players;
|
|
|
- });
|
|
|
- Logger.WriteLineInfo($"Http-flv player:{channelName} registered.");
|
|
|
+ _players.AddOrUpdate(channelName, _ =>
|
|
|
+ {
|
|
|
+ var players = new ConcurrentDictionary<ushort, IPlayer>();
|
|
|
+ players.TryAdd(player.Id, player);
|
|
|
+ return players;
|
|
|
+ }, (_, players) =>
|
|
|
+ {
|
|
|
+ players.TryAdd(player.Id, player);
|
|
|
+ return players;
|
|
|
+ });
|
|
|
+ Logger.WriteLineInfo($"Http-flv player:{channelName} registered.");
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -219,11 +247,25 @@ namespace DotnetRtmpServer.Net.Http
|
|
|
{
|
|
|
var app = sections[0];
|
|
|
var stream = sections[1];
|
|
|
+
|
|
|
+ //Notify the server create proxy if in proxy mode.
|
|
|
+ if (Config.ProxyMode)
|
|
|
+ {
|
|
|
+ if (HttpPlay != null)
|
|
|
+ {
|
|
|
+ await HttpPlay(this, new HttpPlayEventArgs(app, stream)).ConfigureAwait(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
var playFile = sections[2];
|
|
|
- var rtmpChannelName = $"{app}/{stream}";
|
|
|
- if (_publishers.ContainsKey(rtmpChannelName))
|
|
|
+ var channelName = $"proxy/{app}/{stream}";
|
|
|
+ if (_publishers.ContainsKey(channelName))
|
|
|
{
|
|
|
- var playFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "HLS", app, stream, playFile);
|
|
|
+ var baseDir = Config.ProxyMode
|
|
|
+ ?
|
|
|
+ Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "HLS", "Proxy")
|
|
|
+ : Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "HLS");
|
|
|
+ var playFilePath = Path.Combine(baseDir, app, stream, playFile);
|
|
|
if (File.Exists(playFilePath))
|
|
|
{
|
|
|
var tryTimes = 0;
|
|
@@ -250,7 +292,7 @@ namespace DotnetRtmpServer.Net.Http
|
|
|
catch
|
|
|
{
|
|
|
//If the file is being written, wait for 1 second for it end the writing.
|
|
|
- Thread.Sleep(1000);
|
|
|
+ await Task.Delay(1000).ConfigureAwait(false);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -298,28 +340,59 @@ namespace DotnetRtmpServer.Net.Http
|
|
|
if (Config.EnableHlsExtension)
|
|
|
{
|
|
|
//For HLS
|
|
|
+ var app = string.Empty;
|
|
|
+ var stream = string.Empty;
|
|
|
+ var cacheFolder = string.Empty;
|
|
|
+ var streamSource = string.Empty;
|
|
|
+ var supportHls = false;
|
|
|
var appAndStream = publisher.ChannelName.Split('/');
|
|
|
if (appAndStream.Length == 2)
|
|
|
{
|
|
|
- var app = appAndStream[0];
|
|
|
- var stream = appAndStream[1];
|
|
|
- var cacheFolder = Path.Combine(Config.HlsCacheFolder, app, stream);
|
|
|
- var streamSource = $"rtmp://127.0.0.1:{Config.RtmpPort}/{app}/{stream}";
|
|
|
- var producer = new HlsProducer(streamSource, cacheFolder);
|
|
|
- _hlsProducers.AddOrUpdate(publisher.ChannelName, _ => producer, (_, exist) =>
|
|
|
- {
|
|
|
- if (exist != producer)
|
|
|
- {
|
|
|
- exist.Dispose();
|
|
|
- }
|
|
|
+ app = appAndStream[0];
|
|
|
+ stream = appAndStream[1];
|
|
|
+ cacheFolder = Path.Combine(Config.HlsCacheFolder, app, stream);
|
|
|
+ streamSource = $"rtmp://127.0.0.1:{Config.RtmpPort}/{app}/{stream}";
|
|
|
+ supportHls = true;
|
|
|
+ }
|
|
|
+ else if (appAndStream.Length == 3)
|
|
|
+ {
|
|
|
+ app = appAndStream[1];
|
|
|
+ stream = appAndStream[2];
|
|
|
+ cacheFolder = Path.Combine(Config.HlsCacheFolder, "proxy", app, stream);
|
|
|
+ streamSource = $"rtmp://127.0.0.1:{Config.RtmpPort}/{app}/{stream}";
|
|
|
+ supportHls = true;
|
|
|
+ }
|
|
|
|
|
|
+ if (supportHls)
|
|
|
+ {
|
|
|
+ _hlsProducers.AddOrUpdate(publisher.ChannelName, _ =>
|
|
|
+ {
|
|
|
+ var producer = new HlsProducer(publisher.ChannelName, streamSource, cacheFolder);
|
|
|
+ producer.Closed += OnHlsProducerClosedAsync;
|
|
|
+ producer.Start();
|
|
|
return producer;
|
|
|
- });
|
|
|
+ }, (_, exist) => exist);
|
|
|
Logger.WriteLineInfo($"HLS producer for channel:{publisher.ChannelName} added.");
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async Task OnHlsProducerClosedAsync(object sender, EventArgs args)
|
|
|
+ {
|
|
|
+ var producer = (HlsProducer)sender;
|
|
|
+ if (_publishers.ContainsKey(producer.ChannelName))
|
|
|
+ {
|
|
|
+ //Publisher is still there, it could be the producer crashed, so restart it again.
|
|
|
+ producer.Start();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ producer.Closed -= OnHlsProducerClosedAsync;
|
|
|
+ _hlsProducers.TryRemove(producer.ChannelName, out _);
|
|
|
+ await Task.CompletedTask.ConfigureAwait(false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
private async Task OnPublisherAudioDataReceiveAsync(object sender, AudioData e)
|
|
|
{
|