From b30b458574a151d5e13ecd7d6a2e546fc4f87aa5 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:48:19 +0000 Subject: [PATCH] allow ui and streaming to run on different ports (#1992) * allow ui and streaming to run on different ports * revert global.json change --- CHANGELOG.md | 3 + ...layoutItemProcessByChannelNumberHandler.cs | 6 +- .../ExtractEmbeddedSubtitlesHandler.cs | 10 +- .../Queries/GetSubtitlePathByIdHandler.cs | 6 +- ErsatzTV.Core/FFmpeg/ConcatPlaylist.cs | 4 +- .../FFmpeg/FFmpegLibraryProcessService.cs | 8 +- ErsatzTV.Core/Images/ChannelLogoGenerator.cs | 2 +- ErsatzTV.Core/Settings.cs | 3 +- .../ExternalJsonPlayoutItemProvider.cs | 2 +- ErsatzTV/Program.cs | 32 ++++- ErsatzTV/Properties/launchSettings.json | 4 +- .../RunOnce/EndpointValidatorService.cs | 39 +----- ErsatzTV/Shared/MainLayout.razor | 9 +- ErsatzTV/Startup.cs | 114 ++++++++++++++---- ErsatzTV/appsettings.json | 7 -- docker/Dockerfile | 1 - docker/arm32v7/Dockerfile | 1 - docker/arm64/Dockerfile | 1 - docker/nvidia/Dockerfile | 1 - docker/vaapi/Dockerfile | 1 - 20 files changed, 156 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e2fe171..6a7d7c746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1855,6 +1855,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Use "single-file" deployments for releases - Non-docker releases will have significantly fewer files - It is recommended to empty your installation folder before copying in the latest release. +- Add environment variables to allow ETV to run UI and streaming on separate ports + - `ETV_STREAMING_PORT`: port used for streaming requests, defaults to 8409 + - `ETV_UI_PORT`: port used for admin UI, defaults to 8409 ### Fixed - Fix some cases where Jellyfin artwork would not display diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index b65fecd00..ccba67ac3 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -618,7 +618,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< return new PlayoutItemWithPath( playoutItem, - $"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{pmf.Key}"); + $"http://localhost:{Settings.StreamingPort}/media/plex/{plexMediaSourceId}/{pmf.Key}"); } break; @@ -636,7 +636,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< { return new PlayoutItemWithPath( playoutItem, - $"http://localhost:{Settings.ListenPort}/media/jellyfin/{itemId}"); + $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{itemId}"); } // attempt to remotely stream emby @@ -651,7 +651,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< { return new PlayoutItemWithPath( playoutItem, - $"http://localhost:{Settings.ListenPort}/media/emby/{itemId}"); + $"http://localhost:{Settings.StreamingPort}/media/emby/{itemId}"); } return new PlayoutItemDoesNotExistOnDisk(path); diff --git a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs index 4e4ce661e..abc613398 100644 --- a/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs +++ b/ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs @@ -505,7 +505,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler - $"http://localhost:{Settings.ListenPort}/media/jellyfin/{jellyfinMovie.ItemId}", + $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{jellyfinMovie.ItemId}", JellyfinEpisode jellyfinEpisode => - $"http://localhost:{Settings.ListenPort}/media/jellyfin/{jellyfinEpisode.ItemId}", - EmbyMovie embyMovie => $"http://localhost:{Settings.ListenPort}/media/emby/{embyMovie.ItemId}", - EmbyEpisode embyEpisode => $"http://localhost:{Settings.ListenPort}/media/emby/{embyEpisode.ItemId}", + $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{jellyfinEpisode.ItemId}", + EmbyMovie embyMovie => $"http://localhost:{Settings.StreamingPort}/media/emby/{embyMovie.ItemId}", + EmbyEpisode embyEpisode => $"http://localhost:{Settings.StreamingPort}/media/emby/{embyEpisode.ItemId}", _ => file.Path }; } diff --git a/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs b/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs index a39289907..a1e5ed526 100644 --- a/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs +++ b/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs @@ -79,7 +79,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler s.Path)) { - return $"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{subtitlePath}"; + return $"http://localhost:{Settings.StreamingPort}/media/plex/{plexMediaSourceId}/{subtitlePath}"; } } @@ -120,7 +120,7 @@ public class GetSubtitlePathByIdHandler : IRequestHandler $@"ffconcat version 1.0 -file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode={Mode} -file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode={Mode}"; +file http://localhost:{Settings.StreamingPort}/ffmpeg/stream/{ChannelNumber}?mode={Mode} +file http://localhost:{Settings.StreamingPort}/ffmpeg/stream/{ChannelNumber}?mode={Mode}"; } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 301b5cbb5..a2b254495 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -108,7 +108,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService if (subtitle.SubtitleKind == SubtitleKind.Sidecar) { // proxy to avoid dealing with escaping - subtitle.Path = $"http://localhost:{Settings.ListenPort}/media/subtitle/{subtitle.Id}"; + subtitle.Path = $"http://localhost:{Settings.StreamingPort}/media/subtitle/{subtitle.Id}"; } } @@ -594,7 +594,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height); var concatInputFile = new ConcatInputFile( - $"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}?mode=ts-legacy", + $"http://localhost:{Settings.StreamingPort}/ffmpeg/concat/{channel.Number}?mode=ts-legacy", resolution); IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder( @@ -627,7 +627,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService { var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height); var concatInputFile = new ConcatInputFile( - $"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}?mode=segmenter-v2", + $"http://localhost:{Settings.StreamingPort}/ffmpeg/concat/{channel.Number}?mode=segmenter-v2", resolution); FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateConcatSegmenterSettings( @@ -787,7 +787,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService : $"&access_token={accessToken}"; var concatInputFile = new ConcatInputFile( - $"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter{accessTokenQuery}", + $"http://localhost:{Settings.StreamingPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter{accessTokenQuery}", resolution); IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder( diff --git a/ErsatzTV.Core/Images/ChannelLogoGenerator.cs b/ErsatzTV.Core/Images/ChannelLogoGenerator.cs index 283ca3feb..c3f507db6 100644 --- a/ErsatzTV.Core/Images/ChannelLogoGenerator.cs +++ b/ErsatzTV.Core/Images/ChannelLogoGenerator.cs @@ -19,7 +19,7 @@ public class ChannelLogoGenerator : IChannelLogoGenerator } public static Option GenerateChannelLogoUrl(Channel channel) => - $"http://localhost:{Settings.ListenPort}{GetRoute}?{GetRouteQueryParamName}={channel.WebEncodedName}"; + $"http://localhost:{Settings.StreamingPort}{GetRoute}?{GetRouteQueryParamName}={channel.WebEncodedName}"; public Either GenerateChannelLogo( string text, diff --git a/ErsatzTV.Core/Settings.cs b/ErsatzTV.Core/Settings.cs index 00bf8d468..519cc7201 100644 --- a/ErsatzTV.Core/Settings.cs +++ b/ErsatzTV.Core/Settings.cs @@ -2,5 +2,6 @@ public static class Settings { - public static int ListenPort { get; set; } + public static int UiPort { get; set; } + public static int StreamingPort { get; set; } } diff --git a/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs b/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs index 2aee0503e..4e18c1725 100644 --- a/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs +++ b/ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs @@ -218,7 +218,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider return new PlayoutItemWithPath( GetPlayoutItem(startTime, mediaItem, program), - $"http://localhost:{Settings.ListenPort}/media/plex/{server.Id}/{program.PlexFile}"); + $"http://localhost:{Settings.StreamingPort}/media/plex/{server.Id}/{program.PlexFile}"); } } } diff --git a/ErsatzTV/Program.cs b/ErsatzTV/Program.cs index 9c5fb0473..2599a60d8 100644 --- a/ErsatzTV/Program.cs +++ b/ErsatzTV/Program.cs @@ -131,13 +131,39 @@ public class Program } } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) + private static IHostBuilder CreateHostBuilder(string[] args) + { + string uiPortVariable = Environment.GetEnvironmentVariable("ETV_UI_PORT"); + if (!int.TryParse(uiPortVariable, out int uiPort)) + { + uiPort = 8409; + } + Settings.UiPort = uiPort; + + string streamingPortVariable = Environment.GetEnvironmentVariable("ETV_STREAMING_PORT"); + if (!int.TryParse(streamingPortVariable, out int streamingPort)) + { + streamingPort = 8409; + } + Settings.StreamingPort = streamingPort; + + return Host.CreateDefaultBuilder(args) .ConfigureServices(services => services.AddSingleton(LoggingLevelSwitches)) .ConfigureWebHostDefaults( webBuilder => webBuilder.UseStartup() .UseConfiguration(Configuration) - .UseKestrel(options => options.AddServerHeader = false) + .UseKestrel(options => + { + options.ListenAnyIP(Settings.UiPort); + + if (Settings.StreamingPort != Settings.UiPort) + { + options.ListenAnyIP(Settings.StreamingPort); + } + + options.AddServerHeader = false; + }) .UseContentRoot(BasePath)) .UseSerilog(); + } } diff --git a/ErsatzTV/Properties/launchSettings.json b/ErsatzTV/Properties/launchSettings.json index a1baf9820..b8b4d58b3 100644 --- a/ErsatzTV/Properties/launchSettings.json +++ b/ErsatzTV/Properties/launchSettings.json @@ -5,7 +5,9 @@ "launchBrowser": false, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "DOTNET_ENVIRONMENT": "Development" + "DOTNET_ENVIRONMENT": "Development", + "ETV_STREAMING_PORT": "8409", + "ETV_UI_PORT": "8410" } } } diff --git a/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs b/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs index 72767d7b8..5b812b626 100644 --- a/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs +++ b/ErsatzTV/Services/RunOnce/EndpointValidatorService.cs @@ -1,7 +1,4 @@ -using System.Globalization; -using System.Net; -using System.Text.RegularExpressions; -using ErsatzTV.Core; +using ErsatzTV.Core; namespace ErsatzTV.Services.RunOnce; @@ -20,38 +17,12 @@ public class EndpointValidatorService : BackgroundService { await Task.Yield(); - string urls = _configuration.GetValue("Kestrel:Endpoints:Http:Url"); - if (urls.Split(";").Length > 1) - { - throw new NotSupportedException($"Multiple endpoints are not supported: {urls}"); - } - - const string PATTERN = @"http:\/\/(.*):(\d+)"; - Match match = Regex.Match(urls, PATTERN); - if (match.Success) - { - string hostname = match.Groups[1].Value; - Settings.ListenPort = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); - - // IP address must be 0.0.0.0 or 127.0.0.1 - if (IPAddress.TryParse(hostname, out IPAddress address)) - { - if (!address.Equals(IPAddress.Parse("0.0.0.0")) && !IPAddress.IsLoopback(address)) - { - throw new NotSupportedException($"Endpoint MUST include loopback: {urls}"); - } - } - } - else - { - throw new NotSupportedException($"Invalid endpoint format: {urls}"); - } - string baseUrl = Environment.GetEnvironmentVariable("ETV_BASE_URL"); _logger.LogInformation( - "Server will listen on port {Port} - try UI at {UI}", - Settings.ListenPort, - $"http://localhost:{Settings.ListenPort}{baseUrl}"); + "Server will listen on streaming port {StreamingPort}, UI port {UiPort} - try UI at {UI}", + Settings.StreamingPort, + Settings.UiPort, + $"http://localhost:{Settings.UiPort}{baseUrl}"); } } diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index 3b22b8c3f..4bf090f19 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -73,8 +73,8 @@ @if (SystemStartup.IsDatabaseReady && SystemStartup.IsSearchIndexReady) { - M3U - XMLTV + M3U + XMLTV } @* API *@ @@ -285,4 +285,9 @@ _ => null }; + private string IptvUrl(string path) + { + var uri = new Uri(NavigationManager.Uri); + return $"{uri.Scheme}://{uri.Host}:{Settings.StreamingPort}/iptv/{path}"; + } } \ No newline at end of file diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 872a7d724..eed819d5b 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -556,36 +556,98 @@ public class Startup app.UseResponseCompression(); - app.MapWhen( - ctx => !ctx.Request.Path.StartsWithSegments("/iptv"), - blazor => - { - blazor.UseRouting(); - - if (OidcHelper.IsEnabled) + if (Settings.StreamingPort == Settings.UiPort) + { + app.MapWhen( + ctx => !ctx.Request.Path.StartsWithSegments("/iptv"), + blazor => { - blazor.UseAuthentication(); -#pragma warning disable ASP0001 - blazor.UseAuthorization(); -#pragma warning restore ASP0001 - } + blazor.UseRouting(); - blazor.UseEndpoints( - endpoints => + if (OidcHelper.IsEnabled) { - endpoints.MapControllers(); - endpoints.MapBlazorHub(); - endpoints.MapFallbackToPage("/_Host"); - }); - }); + blazor.UseAuthentication(); +#pragma warning disable ASP0001 + blazor.UseAuthorization(); +#pragma warning restore ASP0001 + } - app.MapWhen( - ctx => ctx.Request.Path.StartsWithSegments("/iptv"), - iptv => - { - iptv.UseRouting(); - iptv.UseEndpoints(endpoints => endpoints.MapControllers()); - }); + blazor.UseEndpoints( + endpoints => + { + endpoints.MapControllers(); + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + }); + + app.MapWhen( + ctx => ctx.Request.Path.StartsWithSegments("/iptv"), + iptv => + { + iptv.UseRouting(); + iptv.UseEndpoints(endpoints => endpoints.MapControllers()); + }); + } + else + { + app.MapWhen( + ctx => ctx.Request.Host.Port == Settings.UiPort, + uiApp => + { + uiApp.UseRouting(); + + uiApp.UseWhen( + c => c.Request.Path.StartsWithSegments("/iptv"), // && !IPAddress.IsLoopback(c.Connection.RemoteIpAddress ?? IPAddress.None), + a => + { + a.Run( + c => + { + c.Response.StatusCode = 404; + return Task.CompletedTask; + }); + }); + + if (OidcHelper.IsEnabled) + { + uiApp.UseAuthentication(); +#pragma warning disable ASP0001 + uiApp.UseAuthorization(); +#pragma warning restore ASP0001 + } + + uiApp.UseEndpoints( + endpoints => + { + endpoints.MapControllers(); + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + }); + + app.MapWhen( + ctx => ctx.Request.Host.Port == Settings.StreamingPort, + streamingApp => + { + streamingApp.UseRouting(); + + streamingApp.UseWhen( + c => !c.Request.Path.StartsWithSegments("/iptv"), + a => + { + a.Run( + c => + { + c.Response.StatusCode = 404; + return Task.CompletedTask; + }); + }); + + streamingApp.UseEndpoints(endpoints => endpoints.MapControllers()); + + }); + } } private static void CustomServices(IServiceCollection services) diff --git a/ErsatzTV/appsettings.json b/ErsatzTV/appsettings.json index 35e2dc02d..85bcf34ea 100644 --- a/ErsatzTV/appsettings.json +++ b/ErsatzTV/appsettings.json @@ -16,13 +16,6 @@ ] }, "AllowedHosts": "*", - "Kestrel": { - "EndPoints": { - "Http": { - "Url": "http://+:8409" - } - } - }, "Trakt": { "ClientId": "5a4e78338b7f177cf980d6a8881dd5a52dee680746864e9e442b42d5f4d4ac82" }, diff --git a/docker/Dockerfile b/docker/Dockerfile index f377cbfa0..06722f591 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,7 +42,6 @@ FROM runtime-base ENV FONTCONFIG_PATH=/etc/fonts RUN fc-cache update WORKDIR /app -EXPOSE 8409 COPY --from=build /app ./ ENV ETV_CONFIG_FOLDER=/config ENV ETV_TRANSCODE_FOLDER=/transcode diff --git a/docker/arm32v7/Dockerfile b/docker/arm32v7/Dockerfile index a91d4b80c..b1888a4e1 100644 --- a/docker/arm32v7/Dockerfile +++ b/docker/arm32v7/Dockerfile @@ -42,7 +42,6 @@ FROM runtime-base ENV FONTCONFIG_PATH=/etc/fonts RUN fc-cache update WORKDIR /app -EXPOSE 8409 COPY --from=build /app ./ ENV ETV_CONFIG_FOLDER=/config ENV ETV_TRANSCODE_FOLDER=/transcode diff --git a/docker/arm64/Dockerfile b/docker/arm64/Dockerfile index 39053fdaf..1762330f8 100644 --- a/docker/arm64/Dockerfile +++ b/docker/arm64/Dockerfile @@ -42,7 +42,6 @@ FROM runtime-base ENV FONTCONFIG_PATH=/etc/fonts RUN fc-cache update WORKDIR /app -EXPOSE 8409 COPY --from=build /app ./ ENV ETV_CONFIG_FOLDER=/config ENV ETV_TRANSCODE_FOLDER=/transcode diff --git a/docker/nvidia/Dockerfile b/docker/nvidia/Dockerfile index 754fb7929..4c2221edb 100644 --- a/docker/nvidia/Dockerfile +++ b/docker/nvidia/Dockerfile @@ -48,7 +48,6 @@ FROM runtime-base ENV FONTCONFIG_PATH=/etc/fonts RUN fc-cache update WORKDIR /app -EXPOSE 8409 COPY --from=build /app ./ ENV ETV_CONFIG_FOLDER=/config ENV ETV_TRANSCODE_FOLDER=/transcode diff --git a/docker/vaapi/Dockerfile b/docker/vaapi/Dockerfile index f1ccbbdaa..26671bf9b 100644 --- a/docker/vaapi/Dockerfile +++ b/docker/vaapi/Dockerfile @@ -42,7 +42,6 @@ FROM runtime-base ENV FONTCONFIG_PATH=/etc/fonts RUN fc-cache update WORKDIR /app -EXPOSE 8409 COPY --from=build /app ./ ENV ETV_CONFIG_FOLDER=/config ENV ETV_TRANSCODE_FOLDER=/transcode