diff --git a/ErsatzTV.Core.Tests/Streaming/DirectStreamSessionTrackerTests.cs b/ErsatzTV.Core.Tests/Streaming/DirectStreamSessionTrackerTests.cs index 8b104b9ab..48649ab23 100644 --- a/ErsatzTV.Core.Tests/Streaming/DirectStreamSessionTrackerTests.cs +++ b/ErsatzTV.Core.Tests/Streaming/DirectStreamSessionTrackerTests.cs @@ -71,4 +71,28 @@ public class DirectStreamSessionTrackerTests sessions.Single().ChannelNumber.ShouldBe("2"); sessions.Single().StreamingMode.ShouldBe(StreamingMode.HttpLiveStreamingDirect); } + + [Test] + public void Should_Not_Orphan_Session_When_Last_Registration_Is_Removed_During_Register() + { + DirectStreamSessionTracker tracker = null; + IDisposable existingSession = null; + + tracker = new TestDirectStreamSessionTracker(() => existingSession?.Dispose()); + existingSession = tracker.Register("1", StreamingMode.TransportStream); + + using IDisposable newSession = tracker.Register("1", StreamingMode.HttpLiveStreamingDirect); + + tracker.IsActive("1").ShouldBeTrue(); + tracker.GetViewerCount("1").ShouldBe(1); + + IReadOnlyCollection sessions = tracker.GetActiveSessions("1"); + sessions.Count.ShouldBe(1); + sessions.Single().StreamingMode.ShouldBe(StreamingMode.HttpLiveStreamingDirect); + } + + private sealed class TestDirectStreamSessionTracker(Action onRegisteringSession) : DirectStreamSessionTracker + { + protected override void OnRegisteringSession() => onRegisteringSession(); + } } diff --git a/ErsatzTV.Core/Streaming/DirectStreamSessionTracker.cs b/ErsatzTV.Core/Streaming/DirectStreamSessionTracker.cs index 7e228bd89..e0c8bc18e 100644 --- a/ErsatzTV.Core/Streaming/DirectStreamSessionTracker.cs +++ b/ErsatzTV.Core/Streaming/DirectStreamSessionTracker.cs @@ -15,11 +15,17 @@ public class DirectStreamSessionTracker : IDirectStreamSessionTracker ConcurrentDictionary channelSessions = _sessions.GetOrAdd(channelNumber, _ => new ConcurrentDictionary()); + OnRegisteringSession(); + channelSessions.TryAdd(session.Id, session); return new Registration(this, session); } + protected virtual void OnRegisteringSession() + { + } + public bool IsActive(string channelNumber) => GetViewerCount(channelNumber) > 0; public int GetViewerCount(string channelNumber) => @@ -43,10 +49,6 @@ public class DirectStreamSessionTracker : IDirectStreamSessionTracker } channelSessions.TryRemove(session.Id, out _); - if (channelSessions.IsEmpty) - { - _sessions.TryRemove(session.ChannelNumber, out _); - } } private sealed class Registration(DirectStreamSessionTracker tracker, DirectStreamSession session) : IDisposable diff --git a/ErsatzTV.Tests/Controllers/TrackedFileStreamResultTests.cs b/ErsatzTV.Tests/Controllers/TrackedFileStreamResultTests.cs index 6f4b24523..e05fafba4 100644 --- a/ErsatzTV.Tests/Controllers/TrackedFileStreamResultTests.cs +++ b/ErsatzTV.Tests/Controllers/TrackedFileStreamResultTests.cs @@ -1,10 +1,12 @@ using ErsatzTV.Controllers; using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Streaming; using ErsatzTV.Core.Streaming; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using NSubstitute; using NUnit.Framework; using Shouldly; @@ -78,9 +80,27 @@ public class TrackedFileStreamResultTests tracker.GetViewerCount("1").ShouldBe(0); } - private static ActionContext GetActionContext() + [Test] + public async Task Should_Not_Track_Session_For_Head_Request() + { + IDirectStreamSessionTracker tracker = Substitute.For(); + var stream = new BlockingReadStream(); + TrackedFileStreamResult result = new( + stream, + "video/mp2t", + tracker, + "1", + StreamingMode.TransportStream); + + await result.ExecuteResultAsync(GetActionContext(HttpMethods.Head)); + + tracker.DidNotReceive().Register(Arg.Any(), Arg.Any()); + } + + private static ActionContext GetActionContext(string method = "GET") { var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = method; httpContext.RequestServices = new ServiceCollection() .AddLogging() .AddControllers() diff --git a/ErsatzTV/Controllers/TrackedFileStreamResult.cs b/ErsatzTV/Controllers/TrackedFileStreamResult.cs index 6d01c51d4..8f13bfc29 100644 --- a/ErsatzTV/Controllers/TrackedFileStreamResult.cs +++ b/ErsatzTV/Controllers/TrackedFileStreamResult.cs @@ -1,5 +1,6 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Streaming; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers; @@ -13,6 +14,12 @@ public class TrackedFileStreamResult( { public override async Task ExecuteResultAsync(ActionContext context) { + if (HttpMethods.IsHead(context.HttpContext.Request.Method)) + { + await base.ExecuteResultAsync(context); + return; + } + using IDisposable registration = directStreamSessionTracker.Register(channelNumber, streamingMode); await base.ExecuteResultAsync(context);