@@ -9,6 +9,7 @@ using Microsoft.Extensions.Hosting; | |||||
using Microsoft.Extensions.Logging; | using Microsoft.Extensions.Logging; | ||||
using NLog.Extensions.Logging; | using NLog.Extensions.Logging; | ||||
using System; | using System; | ||||
using System.IO; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace JT1078.Gateway.TestNormalHosting | namespace JT1078.Gateway.TestNormalHosting | ||||
@@ -33,6 +34,8 @@ namespace JT1078.Gateway.TestNormalHosting | |||||
.ConfigureServices((hostContext, services) => | .ConfigureServices((hostContext, services) => | ||||
{ | { | ||||
services.AddMemoryCache(); | services.AddMemoryCache(); | ||||
services.AddScoped<FileSystemWatcher>(); | |||||
services.AddSingleton<HLSRequestManager>(); | |||||
services.AddSingleton<ILoggerFactory, LoggerFactory>(); | services.AddSingleton<ILoggerFactory, LoggerFactory>(); | ||||
services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); | services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); | ||||
services.AddSingleton<FlvEncoder>(); | services.AddSingleton<FlvEncoder>(); | ||||
@@ -19,6 +19,6 @@ | |||||
"TsFileCapacity": 10, | "TsFileCapacity": 10, | ||||
"TsFileMaxSecond": 10, | "TsFileMaxSecond": 10, | ||||
"M3U8FileName": "live.m3u8", | "M3U8FileName": "live.m3u8", | ||||
"HlsFileDirectory":"www/root/demo" | |||||
"HlsFileDirectory":"wwwroot/demo" | |||||
} | } | ||||
} | } |
@@ -1 +1,7 @@ | |||||
m3u8 demo | |||||
#EXTM3U | |||||
#EXT-X-VERSION:3 | |||||
#EXT-X-TARGETDURATION:7 | |||||
#EXT-X-MEDIA-SEQUENCE:0 | |||||
#EXTINF:7.200667, | |||||
demo0.ts | |||||
#EXT-X-ENDLIST |
@@ -0,0 +1,34 @@ | |||||
<html> | |||||
<head></head> | |||||
<body> | |||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> | |||||
<!-- Or if you want a more recent alpha version --> | |||||
<!-- <script src="https://cdn.jsdelivr.net/npm/hls.js@alpha"></script> --> | |||||
<!-- video:play() failed because the user didn't interact with the document first //https://www.jianshu.com/p/06179ca12dfa --> | |||||
<video autoplay muted id="video"></video> | |||||
<script> | |||||
var video = document.getElementById('video'); | |||||
var videoSrc = 'demo.m3u8'; | |||||
// | |||||
// First check for native browser HLS support | |||||
// | |||||
if (video.canPlayType('application/vnd.apple.mpegurl')) { | |||||
video.src = videoSrc; | |||||
video.addEventListener('loadedmetadata', function() { | |||||
video.play(); | |||||
}); | |||||
// | |||||
// If no native HLS support, check if hls.js is supported | |||||
// | |||||
} else if (Hls.isSupported()) { | |||||
var hls = new Hls(); | |||||
hls.loadSource(videoSrc); | |||||
hls.attachMedia(video); | |||||
hls.on(Hls.Events.MANIFEST_PARSED, function() { | |||||
video.play(); | |||||
}); | |||||
} | |||||
</script> | |||||
</body> | |||||
</html> | |||||
@@ -0,0 +1,26 @@ | |||||
#EXTM3U | |||||
#EXT-X-VERSION:3 | |||||
#EXT-X-ALLOW-CACHE:NO | |||||
#EXT-X-TARGETDURATION:10 | |||||
#EXT-X-MEDIA-SEQUENCE:7 | |||||
#EXTINF:10.04, | |||||
7.ts | |||||
#EXTINF:10.041, | |||||
8.ts | |||||
#EXTINF:10.08, | |||||
9.ts | |||||
#EXTINF:10.001, | |||||
10.ts | |||||
#EXTINF:10.12, | |||||
11.ts | |||||
#EXTINF:10.04, | |||||
12.ts | |||||
#EXTINF:10.001, | |||||
13.ts | |||||
#EXTINF:10.04, | |||||
14.ts | |||||
#EXTINF:10.04, | |||||
15.ts | |||||
#EXTINF:10.041, | |||||
16.ts |
@@ -0,0 +1,155 @@ | |||||
using JT1078.Gateway.Configurations; | |||||
using JT1078.Gateway.Extensions; | |||||
using JT1078.Gateway.Metadata; | |||||
using JT1078.Gateway.Sessions; | |||||
using Microsoft.Extensions.Caching.Memory; | |||||
using Microsoft.Extensions.Logging; | |||||
using Microsoft.Extensions.Options; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Net; | |||||
using System.Security.Principal; | |||||
using System.Text; | |||||
namespace JT1078.Gateway | |||||
{ | |||||
/// <summary> | |||||
/// Hls请求管理 | |||||
/// </summary> | |||||
public class HLSRequestManager | |||||
{ | |||||
private const string m3u8Mime = "application/x-mpegURL"; | |||||
private const string tsMime = "video/MP2T"; | |||||
private readonly JT1078Configuration Configuration; | |||||
private readonly JT1078HttpSessionManager HttpSessionManager; | |||||
private readonly JT1078SessionManager SessionManager; | |||||
private readonly ILogger Logger; | |||||
private IMemoryCache memoryCache; | |||||
private FileSystemWatcher fileSystemWatcher; | |||||
public HLSRequestManager( | |||||
IMemoryCache memoryCache, | |||||
IOptions<JT1078Configuration> jT1078ConfigurationAccessor, | |||||
JT1078HttpSessionManager httpSessionManager, | |||||
JT1078SessionManager sessionManager, | |||||
FileSystemWatcher fileSystemWatcher, | |||||
ILoggerFactory loggerFactory) | |||||
{ | |||||
this.memoryCache = memoryCache; | |||||
this.fileSystemWatcher = fileSystemWatcher; | |||||
HttpSessionManager = httpSessionManager; | |||||
SessionManager = sessionManager; | |||||
Configuration = jT1078ConfigurationAccessor.Value; | |||||
Logger = loggerFactory.CreateLogger<HLSRequestManager>(); | |||||
} | |||||
/// <summary> | |||||
/// 处理hls实时视频请求 | |||||
/// </summary> | |||||
/// <param name="context"></param> | |||||
/// <param name="principal"></param> | |||||
public async void HandleHlsRequest(HttpListenerContext context, IPrincipal principal) { | |||||
if (context.Request.QueryString.Count < 2) | |||||
{ | |||||
context.Http404(); | |||||
return; | |||||
} | |||||
string sim = context.Request.QueryString.Get("sim");//终端sim卡号 | |||||
string channelNo = context.Request.QueryString.Get("channelNo");//通道号 | |||||
string key = $"{sim}_{channelNo}"; | |||||
string filename = Path.GetFileName(context.Request.Url.AbsolutePath.ToString()); | |||||
string filepath = Path.Combine(Configuration.HlsRootDirectory, key, filename); | |||||
if (!File.Exists(filepath)) | |||||
{ | |||||
if (filename.ToLower().Contains("m3u8")) | |||||
{ | |||||
fileSystemWatcher = new FileSystemWatcher(); | |||||
fileSystemWatcher.Path = Path.Combine(Configuration.HlsRootDirectory, key); | |||||
fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite; //NotifyFilters.CreateTime | |||||
fileSystemWatcher.Filter = "*.m3u8"; // Only watch text files. | |||||
fileSystemWatcher.Changed += (sender, arg) => | |||||
{ | |||||
if (context.Response.ContentLength64 != 0) return; | |||||
//wwwroot\1234_2\live.m3u8 | |||||
var key = arg.FullPath.Replace(arg.Name, "").Substring(arg.FullPath.Replace(arg.Name, "").IndexOf("\\")).Replace("\\", ""); | |||||
var sim = key.Split("_")[0]; | |||||
var channel = int.Parse(key.Split("_")[1]); | |||||
try | |||||
{ | |||||
using (FileStream sr = new FileStream(arg.FullPath, FileMode.Open)) | |||||
{ | |||||
context.Response.ContentType = m3u8Mime; | |||||
context.Response.StatusCode = (int)HttpStatusCode.OK; | |||||
context.Response.ContentLength64 = sr.Length; | |||||
sr.CopyTo(context.Response.OutputStream); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logger.LogError(ex, $"{context.Request.Url}"); | |||||
} | |||||
finally | |||||
{ | |||||
context.Response.OutputStream.Close(); | |||||
context.Response.Close(); | |||||
} | |||||
}; | |||||
fileSystemWatcher.EnableRaisingEvents = true; // Begin watching. | |||||
} | |||||
else | |||||
{ | |||||
context.Http404(); | |||||
return; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
try | |||||
{ | |||||
using (FileStream sr = new FileStream(filepath, FileMode.Open)) | |||||
{ | |||||
if (filename.ToLower().Contains("m3u8")) | |||||
{ | |||||
context.Response.ContentType = m3u8Mime; | |||||
} | |||||
else | |||||
{ | |||||
context.Response.ContentType = tsMime; | |||||
} | |||||
context.Response.StatusCode = (int)HttpStatusCode.OK; | |||||
context.Response.ContentLength64 = sr.Length; | |||||
await sr.CopyToAsync(context.Response.OutputStream); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logger.LogError(ex, $"{context.Request.Url}"); | |||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||||
} | |||||
finally | |||||
{ | |||||
context.Response.OutputStream.Close(); | |||||
context.Response.Close(); | |||||
} | |||||
} | |||||
var jT1078HttpContext = new JT1078HttpContext(context, principal); | |||||
jT1078HttpContext.Sim = sim; | |||||
jT1078HttpContext.ChannelNo = int.Parse(channelNo); | |||||
HttpSessionManager.TryAdd(jT1078HttpContext); | |||||
//如果过了30s,还未收到浏览器请求,则移除掉session | |||||
memoryCache.Set(key, DateTime.Now, new MemoryCacheEntryOptions | |||||
{ | |||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10) | |||||
}.RegisterPostEvictionCallback((key, value, reason, state) => | |||||
{ | |||||
if (reason != EvictionReason.Expired) return; | |||||
//当清空httpssion时,同时清除tcpsseion | |||||
var removeSim = key.ToString().Split('_')[0]; | |||||
//移除httpsession | |||||
HttpSessionManager.TryRemoveBySim(removeSim); | |||||
//移除tcpsession | |||||
SessionManager.RemoveByTerminalPhoneNo(removeSim); | |||||
})); | |||||
} | |||||
} | |||||
} |
@@ -53,6 +53,18 @@ | |||||
协调器Coordinator主机登录密码 | 协调器Coordinator主机登录密码 | ||||
</summary> | </summary> | ||||
</member> | </member> | ||||
<member name="T:JT1078.Gateway.HLSRequestManager"> | |||||
<summary> | |||||
Hls请求管理 | |||||
</summary> | |||||
</member> | |||||
<member name="M:JT1078.Gateway.HLSRequestManager.HandleHlsRequest(System.Net.HttpListenerContext,System.Security.Principal.IPrincipal)"> | |||||
<summary> | |||||
处理hls实时视频请求 | |||||
</summary> | |||||
<param name="context"></param> | |||||
<param name="principal"></param> | |||||
</member> | |||||
<member name="T:JT1078.Gateway.JT1078CoordinatorHttpClient"> | <member name="T:JT1078.Gateway.JT1078CoordinatorHttpClient"> | ||||
<summary> | <summary> | ||||
协调器客户端 | 协调器客户端 | ||||
@@ -28,24 +28,24 @@ namespace JT1078.Gateway | |||||
private readonly IJT1078Authorization authorization; | private readonly IJT1078Authorization authorization; | ||||
private IMemoryCache memoryCache; | |||||
private HttpListener listener; | private HttpListener listener; | ||||
private JT1078HttpSessionManager SessionManager; | private JT1078HttpSessionManager SessionManager; | ||||
private readonly HLSRequestManager hLSRequestManager; | |||||
private FileSystemWatcher watcher; | |||||
public JT1078HttpServer( | public JT1078HttpServer( | ||||
IMemoryCache memoryCache, | |||||
IOptions<JT1078Configuration> jT1078ConfigurationAccessor, | IOptions<JT1078Configuration> jT1078ConfigurationAccessor, | ||||
IJT1078Authorization authorization, | IJT1078Authorization authorization, | ||||
JT1078HttpSessionManager sessionManager, | JT1078HttpSessionManager sessionManager, | ||||
HLSRequestManager hLSRequestManager, | |||||
ILoggerFactory loggerFactory) | ILoggerFactory loggerFactory) | ||||
{ | { | ||||
Logger = loggerFactory.CreateLogger<JT1078TcpServer>(); | Logger = loggerFactory.CreateLogger<JT1078TcpServer>(); | ||||
Configuration = jT1078ConfigurationAccessor.Value; | Configuration = jT1078ConfigurationAccessor.Value; | ||||
this.authorization = authorization; | this.authorization = authorization; | ||||
this.SessionManager = sessionManager; | this.SessionManager = sessionManager; | ||||
this.memoryCache = memoryCache; | |||||
this.hLSRequestManager = hLSRequestManager; | |||||
} | } | ||||
public Task StartAsync(CancellationToken cancellationToken) | public Task StartAsync(CancellationToken cancellationToken) | ||||
@@ -74,14 +74,18 @@ namespace JT1078.Gateway | |||||
var context = await listener.GetContextAsync(); | var context = await listener.GetContextAsync(); | ||||
try | try | ||||
{ | { | ||||
if (authorization.Authorization(context,out var principal)) | |||||
{ | |||||
await ProcessRequestAsync(context, principal); | |||||
} | |||||
else | |||||
await Task.Run(async () => | |||||
{ | { | ||||
await context.Http401(); | |||||
} | |||||
IPrincipal principal=null; | |||||
if (context.Request.RawUrl.Contains(".ts")||authorization.Authorization(context, out principal)) | |||||
{ | |||||
await ProcessRequestAsync(context, principal); | |||||
} | |||||
else | |||||
{ | |||||
await context.Http401(); | |||||
} | |||||
}); | |||||
} | } | ||||
catch (Exception ex) | catch (Exception ex) | ||||
{ | { | ||||
@@ -93,9 +97,6 @@ namespace JT1078.Gateway | |||||
return Task.CompletedTask; | return Task.CompletedTask; | ||||
} | } | ||||
private const string m3u8Mime = "application/x-mpegURL"; | |||||
private const string tsMime = "video/MP2T"; | |||||
private async ValueTask ProcessRequestAsync(HttpListenerContext context, IPrincipal principal) | private async ValueTask ProcessRequestAsync(HttpListenerContext context, IPrincipal principal) | ||||
{ | { | ||||
if(context.Request.RawUrl.StartsWith("/favicon.ico")) | if(context.Request.RawUrl.StartsWith("/favicon.ico")) | ||||
@@ -103,65 +104,19 @@ namespace JT1078.Gateway | |||||
context.Http404(); | context.Http404(); | ||||
return; | return; | ||||
} | } | ||||
if (context.Request.RawUrl.EndsWith(".m3u8") || context.Request.RawUrl.EndsWith(".ts")) | |||||
if (context.Request.RawUrl.Contains(".m3u8") || context.Request.RawUrl.Contains(".ts")) | |||||
{ | { | ||||
var uri = new Uri(context.Request.RawUrl); | |||||
string url = uri.AbsolutePath; | |||||
var queryParams = uri.Query.Substring(1, uri.Query.Length - 1).Split('&'); | |||||
if (queryParams.Length < 2) | |||||
{ | |||||
context.Http404(); | |||||
return; | |||||
} | |||||
string key = $"{queryParams[0].Split('=')[1]}_{queryParams[1].Split('=')[1]}";//默认queryParams第一个参数是终端号,第二个参数是通道号 | |||||
memoryCache.GetOrCreate(key, (cacheEntry) => { | |||||
cacheEntry.SetSlidingExpiration(TimeSpan.FromSeconds(20)); | |||||
cacheEntry.RegisterPostEvictionCallback((key, value, reason, state) => { | |||||
//当清空httpssion时,同时清除tcpsseion | |||||
}); | |||||
return DateTime.Now; | |||||
}); | |||||
string filename = Path.GetFileName(url); | |||||
string filepath = Path.Combine(Configuration.HlsRootDirectory, key, filename); | |||||
if (!File.Exists(filepath)) | |||||
{ | |||||
context.Http404(); | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
using (FileStream sr = new FileStream(filepath, FileMode.Open)) | |||||
{ | |||||
context.Response.ContentLength64 = sr.Length; | |||||
await sr.CopyToAsync(context.Response.OutputStream); | |||||
} | |||||
string ext = Path.GetExtension(filename); | |||||
if (ext == ".m3u8") | |||||
{ | |||||
context.Response.ContentType = m3u8Mime; | |||||
} | |||||
else if (ext == ".ts") | |||||
{ | |||||
context.Response.ContentType = tsMime; | |||||
} | |||||
context.Response.StatusCode = (int)HttpStatusCode.OK; | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
Logger.LogError(ex, $"{context.Request.RawUrl}"); | |||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||||
} | |||||
finally | |||||
{ | |||||
context.Response.OutputStream.Close(); | |||||
context.Response.Close(); | |||||
} | |||||
hLSRequestManager.HandleHlsRequest(context, principal); | |||||
return; | return; | ||||
} | } | ||||
if (Logger.IsEnabled(LogLevel.Trace)) | if (Logger.IsEnabled(LogLevel.Trace)) | ||||
{ | { | ||||
Logger.LogTrace($"[http RequestTraceIdentifier]:{context.Request.RequestTraceIdentifier.ToString()}-{context.Request.RemoteEndPoint.ToString()}"); | Logger.LogTrace($"[http RequestTraceIdentifier]:{context.Request.RequestTraceIdentifier.ToString()}-{context.Request.RemoteEndPoint.ToString()}"); | ||||
} | } | ||||
if (Logger.IsEnabled(LogLevel.Trace)) | |||||
{ | |||||
Logger.LogTrace($"[http RequestTraceIdentifier]:{context.Request.RequestTraceIdentifier.ToString()}-{context.Request.RemoteEndPoint.ToString()}"); | |||||
} | |||||
string sim = context.Request.QueryString.Get("sim"); | string sim = context.Request.QueryString.Get("sim"); | ||||
string channel = context.Request.QueryString.Get("channel"); | string channel = context.Request.QueryString.Get("channel"); | ||||
if(string.IsNullOrEmpty(sim) || string.IsNullOrEmpty(channel)) | if(string.IsNullOrEmpty(sim) || string.IsNullOrEmpty(channel)) | ||||