Compare commits

...

10 Commits

  1. 4
      .config/dotnet-tools.json
  2. 18
      CHANGELOG.md
  3. 4
      ErsatzTV.Application/ErsatzTV.Application.csproj
  4. 61
      ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs
  5. 39
      ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs
  6. 21
      ErsatzTV.Application/Streaming/NextSessionWorker.cs
  7. 7
      ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs
  8. 9
      ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs
  9. 14
      ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj
  10. 12
      ErsatzTV.Core/ErsatzTV.Core.csproj
  11. 2
      ErsatzTV.Core/FileSystemLayout.cs
  12. 103
      ErsatzTV.Core/Next/Config/ChannelConfig.cs
  13. 64
      ErsatzTV.Core/Next/Playout.cs
  14. 8
      ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
  15. 8
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  16. 2
      ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj
  17. 6
      ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj
  18. 12
      ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj
  19. 6
      ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs
  20. 3
      ErsatzTV.Infrastructure/Streaming/Graphics/Motion/MotionElement.cs
  21. 3
      ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs
  22. 6
      ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj
  23. 8
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  24. 2
      ErsatzTV.Tests/ErsatzTV.Tests.csproj
  25. 3
      ErsatzTV/Controllers/IptvController.cs
  26. 16
      ErsatzTV/ErsatzTV.csproj
  27. 1
      ErsatzTV/Startup.cs

4
.config/dotnet-tools.json

@ -3,11 +3,11 @@ @@ -3,11 +3,11 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2025.3.0.2",
"version": "2026.1.1",
"commands": [
"jb"
],
"rollForward": false
}
}
}
}

18
CHANGELOG.md

@ -4,7 +4,22 @@ All notable changes to this project will be documented in this file. @@ -4,7 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Add channel config overlay system for Next engine
- The channel config schema can be found at https://github.com/ErsatzTV/next/blob/main/schema/channel_config.json
- Config overlays should be created in the config subfolder `next/channel-config-overlays`
- A config file named `default.json` will apply to all channels using the Next engine
- A config file named `{channel_number}.json` (e.g. `1.json`) will apply to the channel with that number
- Channel overlays will override values from the default overlay which will override values from the FFmpeg Profile
### Fixed
- Fix HLS Direct playback when JWT auth is also used
- Use configured ffmpeg path for motion and subtitle graphics elements
- Previously, these elements required ffmpeg to be on PATH
## [26.5.1] - 2026-05-08
### Fixed
- Fix NVIDIA playback on Linux using legacy streaming engine
- Fix cause of unnecessarily large database when using Emby or Jellyfin libraries
- This was caused by orphaned actor and actor artwork records, which will be cleaned up hourly
- When you see logs like `No orphaned actors to delete` and `No orphaned artwork to delete`, you can then reclaim disk space by:
@ -3226,7 +3241,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -3226,7 +3241,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Initial release to facilitate testing outside of Docker.
[Unreleased]: https://github.com/ErsatzTV/legacy/compare/v26.5.0...HEAD
[Unreleased]: https://github.com/ErsatzTV/legacy/compare/v26.5.1...HEAD
[26.5.1]: https://github.com/ErsatzTV/legacy/compare/v26.5.0...v26.5.1
[26.5.0]: https://github.com/ErsatzTV/legacy/compare/v26.4.0...v26.5.0
[26.4.0]: https://github.com/ErsatzTV/legacy/compare/v26.3.0...v26.4.0
[26.3.0]: https://github.com/ErsatzTV/legacy/compare/v26.2.0...v26.3.0

4
ErsatzTV.Application/ErsatzTV.Application.csproj

@ -14,8 +14,8 @@ @@ -14,8 +14,8 @@
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.8" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="4.0.0" />
<PackageReference Include="WebMarkupMin.Core" Version="2.21.0" />

61
ErsatzTV.Application/Playouts/Commands/SyncNextPlayoutHandler.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.CommandLine.Parsing;
using System.Globalization;
using System.IO.Abstractions;
using System.Runtime.InteropServices;
@ -18,6 +19,7 @@ using ErsatzTV.Infrastructure.Data; @@ -18,6 +19,7 @@ using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CommandResult = CliWrap.CommandResult;
using PlayoutItem = ErsatzTV.Core.Domain.PlayoutItem;
using WatermarkLocation = ErsatzTV.FFmpeg.State.WatermarkLocation;
@ -187,6 +189,15 @@ public partial class SyncNextPlayoutHandler( @@ -187,6 +189,15 @@ public partial class SyncNextPlayoutHandler(
.Include(i => i.MediaItem)
.ThenInclude(i => (i as MusicVideo).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as RemoteStream).MediaVersions)
.ThenInclude(mv => mv.MediaFiles)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as RemoteStream).MediaVersions)
.ThenInclude(mv => mv.Streams)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as RemoteStream).RemoteStreamMetadata)
.ThenInclude(em => em.Subtitles)
.AsSplitQuery()
.ToListAsync(cancellationToken);
@ -211,7 +222,8 @@ public partial class SyncNextPlayoutHandler( @@ -211,7 +222,8 @@ public partial class SyncNextPlayoutHandler(
foreach (PlayoutItem playoutItem in group)
{
if (playoutItem.MediaItem is not Episode && playoutItem.MediaItem is not Movie &&
playoutItem.MediaItem is not OtherVideo && playoutItem.MediaItem is not MusicVideo)
playoutItem.MediaItem is not OtherVideo && playoutItem.MediaItem is not MusicVideo &&
playoutItem.MediaItem is not RemoteStream)
{
continue;
}
@ -468,6 +480,53 @@ public partial class SyncNextPlayoutHandler( @@ -468,6 +480,53 @@ public partial class SyncNextPlayoutHandler(
PlayoutItem playoutItem,
CancellationToken cancellationToken)
{
if (playoutItem.MediaItem is RemoteStream remoteStream)
{
if (!string.IsNullOrWhiteSpace(remoteStream.Url))
{
if (remoteStream.Url.StartsWith("rtsp://", StringComparison.OrdinalIgnoreCase))
{
return new Core.Next.Source
{
SourceType = Core.Next.SourceType.Rtsp,
Uri = remoteStream.Url
};
}
return new Core.Next.Source
{
SourceType = Core.Next.SourceType.Http,
Uri = remoteStream.Url,
IsLive = remoteStream.IsLive,
KeepAlive = true,
Reconnect = true
};
}
if (!string.IsNullOrWhiteSpace(remoteStream.Script))
{
var split = CommandLineParser.SplitCommandLine(remoteStream.Script).ToList();
if (split.Count > 0)
{
var source = new Core.Next.Source
{
SourceType = Core.Next.SourceType.Script,
Command = split.Head(),
IsLive = remoteStream.IsLive
};
if (split.Count > 1)
{
source.Args = split.Tail().ToList();
}
return source;
}
}
return Option<Core.Next.Source>.None;
}
string path = await playoutItem.MediaItem.GetLocalPath(
plexPathReplacementService,
jellyfinPathReplacementService,

39
ErsatzTV.Application/Streaming/Commands/StartFFmpegNextSessionHandler.cs

@ -34,7 +34,6 @@ public class StartFFmpegNextSessionHandler( @@ -34,7 +34,6 @@ public class StartFFmpegNextSessionHandler(
ILogger<NextSessionWorker> sessionWorkerLogger)
: IRequestHandler<StartFFmpegNextSession, Either<BaseError, string>>
{
public Task<Either<BaseError, string>> Handle(
StartFFmpegNextSession request,
CancellationToken cancellationToken) =>
@ -282,7 +281,11 @@ public class StartFFmpegNextSessionHandler( @@ -282,7 +281,11 @@ public class StartFFmpegNextSessionHandler(
FFmpegProfileViewModel ffmpegProfile,
CancellationToken cancellationToken)
{
var ffmpeg = new Ffmpeg();
var ffmpeg = new Ffmpeg
{
// next only keeps errors, so always pass the folder
ReportsFolder = FileSystemLayout.FFmpegReportsFolder
};
Option<string> ffmpegPath = await configElementRepository.GetValue<string>(
ConfigElementKey.FFmpegPath,
@ -302,6 +305,10 @@ public class StartFFmpegNextSessionHandler( @@ -302,6 +305,10 @@ public class StartFFmpegNextSessionHandler(
ffmpeg.FfprobePath = path;
}
Option<bool> maybeSaveReports = await configElementRepository.GetValue<bool>(
ConfigElementKey.FFmpegSaveReports,
cancellationToken);
var audioNormalization = new Audio
{
Format = ffmpegProfile.AudioFormat switch
@ -324,6 +331,16 @@ public class StartFFmpegNextSessionHandler( @@ -324,6 +331,16 @@ public class StartFFmpegNextSessionHandler(
};
}
string tonemapAlgorithm = ffmpegProfile.TonemapAlgorithm switch
{
FFmpegProfileTonemapAlgorithm.Clip => "clip",
FFmpegProfileTonemapAlgorithm.Gamma => "gamma",
FFmpegProfileTonemapAlgorithm.Reinhard => "reinhard",
FFmpegProfileTonemapAlgorithm.Mobius => "mobius",
FFmpegProfileTonemapAlgorithm.Hable => "hable",
_ => "linear"
};
var videoNormalization = new Video
{
Format = ffmpegProfile.VideoFormat switch
@ -356,8 +373,22 @@ public class StartFFmpegNextSessionHandler( @@ -356,8 +373,22 @@ public class StartFFmpegNextSessionHandler(
ScalingBehavior.Crop => ScalingMode.Crop,
_ => ScalingMode.ScaleAndPad
},
// TODO: NEXT: more tonemap algorithms
TonemapAlgorithm = "linear",
Deinterlace = ffmpegProfile.DeinterlaceVideo,
Filters = new Filters
{
Tonemap = new TonemapClass
{
Tonemap = tonemapAlgorithm
},
TonemapOpencl = new TonemapOpenclClass
{
Tonemap = tonemapAlgorithm
},
Libplacebo = new LibplaceboClass
{
Tonemapping = tonemapAlgorithm
}
},
VaapiDevice = ffmpegProfile.VaapiDevice,
VaapiDriver = ffmpegProfile.VaapiDriver switch
{

21
ErsatzTV.Application/Streaming/NextSessionWorker.cs

@ -99,9 +99,26 @@ public class NextSessionWorker( @@ -99,9 +99,26 @@ public class NextSessionWorker(
_workingDirectory = fileSystem.Path.Combine(FileSystemLayout.TranscodeFolder, _channelNumber);
_heartbeatFileName = fileSystem.Path.Combine(_workingDirectory, ".heartbeat");
List<string> arguments = ["run", "--output-folder", _workingDirectory, "--number", channelNumber, "-"];
string defaultOverlayFile = fileSystem.Path.Combine(
FileSystemLayout.NextChannelConfigOverlaysFolder,
"default.json");
if (fileSystem.File.Exists(defaultOverlayFile))
{
arguments.Add(defaultOverlayFile);
}
string channelOverlayFile = fileSystem.Path.Combine(
FileSystemLayout.NextChannelConfigOverlaysFolder,
$"{channelNumber}.json");
if (fileSystem.File.Exists(channelOverlayFile))
{
arguments.Add(channelOverlayFile);
}
CommandResult commandResult = await Cli.Wrap(channelBinary)
.WithArguments(
["run", "--output-folder", _workingDirectory, "--number", channelNumber, "-"])
.WithArguments(arguments)
.WithStandardInputPipe(PipeSource.FromString(channelConfig.ToJson()))
.WithStandardOutputPipe(PipeTarget.ToDelegate(l => logger.LogDebug("{Line}", l)))
.WithStandardErrorPipe(PipeTarget.ToDelegate(l => logger.LogDebug("{Line}", l)))

7
ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumber.cs

@ -2,5 +2,10 @@ @@ -2,5 +2,10 @@
namespace ErsatzTV.Application.Streaming;
public record GetHlsPlaylistByChannelNumber(string Scheme, string Host, string ChannelNumber, string Mode)
public record GetHlsPlaylistByChannelNumber(
string Scheme,
string Host,
string ChannelNumber,
string Mode,
string AccessToken)
: IRequest<Either<BaseError, string>>;

9
ErsatzTV.Application/Streaming/Queries/GetHlsPlaylistByChannelNumberHandler.cs

@ -33,11 +33,10 @@ public class GetHlsPlaylistByChannelNumberHandler : @@ -33,11 +33,10 @@ public class GetHlsPlaylistByChannelNumberHandler :
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
DateTimeOffset now = DateTimeOffset.Now;
Validation<BaseError, Parameters> validation = await Validate(dbContext, request, now, cancellationToken);
return await validation.Apply(parameters => GetPlaylist(dbContext, request, parameters, now));
return await validation.Apply(parameters => GetPlaylist(request, parameters, now));
}
private async Task<string> GetPlaylist(
TvContext dbContext,
GetHlsPlaylistByChannelNumber request,
Parameters parameters,
DateTimeOffset now)
@ -69,6 +68,10 @@ public class GetHlsPlaylistByChannelNumberHandler : @@ -69,6 +68,10 @@ public class GetHlsPlaylistByChannelNumberHandler :
};
}
string accessToken = string.IsNullOrWhiteSpace(request.AccessToken)
? string.Empty
: $"&access_token={request.AccessToken}";
long index = GetIndexForChannel(parameters.Channel, parameters.PlayoutItem);
double timeRemaining = Math.Abs((parameters.PlayoutItem.FinishOffset - now).TotalSeconds);
return $@"#EXTM3U
@ -77,7 +80,7 @@ public class GetHlsPlaylistByChannelNumberHandler : @@ -77,7 +80,7 @@ public class GetHlsPlaylistByChannelNumberHandler :
#EXT-X-MEDIA-SEQUENCE:{index}
#EXT-X-DISCONTINUITY
#EXTINF:{timeRemaining:F2},
{request.Scheme}://{request.Host}/{endpoint}/{request.ChannelNumber}{extension}?index={index}{mode}
{request.Scheme}://{request.Host}/{endpoint}/{request.ChannelNumber}{extension}?index={index}{mode}{accessToken}
";
}

14
ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj

@ -9,20 +9,20 @@ @@ -9,20 +9,20 @@
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="6.2.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="6.4.0" />
</ItemGroup>
<ItemGroup>

12
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -16,10 +16,10 @@ @@ -16,10 +16,10 @@
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="LanguageExt.Transformers" Version="4.4.8" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageReference Include="NCalcSync" Version="5.12.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
@ -27,10 +27,10 @@ @@ -27,10 +27,10 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="SkiaSharp" Version="3.119.2" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.2" />
<PackageReference Include="System.CommandLine" Version="2.0.7" />
<PackageReference Include="System.CommandLine" Version="2.0.8" />
<PackageReference Include="Testably.Abstractions" Version="10.2.0" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="17.1.0" />
<PackageReference Include="YamlDotNet" Version="18.0.0" />
</ItemGroup>
<ItemGroup>

2
ErsatzTV.Core/FileSystemLayout.cs

@ -67,6 +67,7 @@ public static class FileSystemLayout @@ -67,6 +67,7 @@ public static class FileSystemLayout
public static readonly string DefaultMpegTsScriptFolder;
public static readonly string NextChannelConfigOverlaysFolder;
public static readonly string NextPlayoutsFolder;
public static readonly string MacOsOldAppDataFolder = Path.Combine(
@ -195,6 +196,7 @@ public static class FileSystemLayout @@ -195,6 +196,7 @@ public static class FileSystemLayout
MpegTsScriptsFolder = Path.Combine(ScriptsFolder, "mpegts");
DefaultMpegTsScriptFolder = Path.Combine(MpegTsScriptsFolder, "default");
NextChannelConfigOverlaysFolder = Path.Combine(AppDataFolder, "next", "channel-config-overlays");
NextPlayoutsFolder = Path.Combine(AppDataFolder, "next", "playouts");
}
}

103
ErsatzTV.Core/Next/Config/ChannelConfig.cs

@ -40,6 +40,9 @@ namespace ErsatzTV.Core.Next.Config @@ -40,6 +40,9 @@ namespace ErsatzTV.Core.Next.Config
[JsonProperty("preferred_filters", NullValueHandling = NullValueHandling.Ignore)]
public List<string> PreferredFilters { get; set; }
[JsonProperty("reports_folder")]
public string ReportsFolder { get; set; }
}
public partial class Normalization
@ -113,6 +116,9 @@ namespace ErsatzTV.Core.Next.Config @@ -113,6 +116,9 @@ namespace ErsatzTV.Core.Next.Config
[JsonProperty("deinterlace", NullValueHandling = NullValueHandling.Ignore)]
public bool? Deinterlace { get; set; }
[JsonProperty("filters", NullValueHandling = NullValueHandling.Ignore)]
public Filters Filters { get; set; }
[JsonProperty("format")]
public VideoFormat? Format { get; set; }
@ -122,9 +128,6 @@ namespace ErsatzTV.Core.Next.Config @@ -122,9 +128,6 @@ namespace ErsatzTV.Core.Next.Config
[JsonProperty("scaling_mode", NullValueHandling = NullValueHandling.Ignore)]
public ScalingMode? ScalingMode { get; set; }
[JsonProperty("tonemap_algorithm")]
public string TonemapAlgorithm { get; set; }
[JsonProperty("vaapi_device")]
public string VaapiDevice { get; set; }
@ -135,6 +138,99 @@ namespace ErsatzTV.Core.Next.Config @@ -135,6 +138,99 @@ namespace ErsatzTV.Core.Next.Config
public long? Width { get; set; }
}
public partial class Filters
{
[JsonProperty("bwdif")]
public BwdifClass Bwdif { get; set; }
[JsonProperty("bwdif_cuda")]
public BwdifCudaClass BwdifCuda { get; set; }
[JsonProperty("deinterlace_qsv")]
public DeinterlaceQsvClass DeinterlaceQsv { get; set; }
[JsonProperty("deinterlace_vaapi")]
public DeinterlaceVaapiClass DeinterlaceVaapi { get; set; }
[JsonProperty("libplacebo")]
public LibplaceboClass Libplacebo { get; set; }
[JsonProperty("tonemap")]
public TonemapClass Tonemap { get; set; }
[JsonProperty("tonemap_opencl")]
public TonemapOpenclClass TonemapOpencl { get; set; }
[JsonProperty("w3fdif")]
public W3FdifClass W3Fdif { get; set; }
[JsonProperty("yadif")]
public YadifClass Yadif { get; set; }
[JsonProperty("yadif_cuda")]
public YadifCudaClass YadifCuda { get; set; }
}
public partial class BwdifClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class BwdifCudaClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class DeinterlaceQsvClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class DeinterlaceVaapiClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class LibplaceboClass
{
[JsonProperty("tonemapping")]
public string Tonemapping { get; set; }
}
public partial class TonemapClass
{
[JsonProperty("tonemap")]
public string Tonemap { get; set; }
}
public partial class TonemapOpenclClass
{
[JsonProperty("tonemap")]
public string Tonemap { get; set; }
}
public partial class W3FdifClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class YadifClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class YadifCudaClass
{
[JsonProperty("mode")]
public string Mode { get; set; }
}
public partial class Playout
{
[JsonProperty("folder")]
@ -173,6 +269,7 @@ namespace ErsatzTV.Core.Next.Config @@ -173,6 +269,7 @@ namespace ErsatzTV.Core.Next.Config
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =

64
ErsatzTV.Core/Next/Playout.cs

@ -99,6 +99,12 @@ namespace ErsatzTV.Core.Next @@ -99,6 +99,12 @@ namespace ErsatzTV.Core.Next
/// A synthetic source produced by an ffmpeg lavfi filter graph.
///
/// A remote source fetched over HTTP(S).
///
/// A live stream pulled from an RTSP server (e.g. an IP camera). Always treated as live: it
/// is never seeked and cannot work ahead.
///
/// An external command whose stdout is an MPEG-TS stream, proxied to ffmpeg over loopback
/// HTTP.
/// </summary>
public partial class Source
{
@ -161,6 +167,8 @@ namespace ErsatzTV.Core.Next @@ -161,6 +167,8 @@ namespace ErsatzTV.Core.Next
/// <summary>
/// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}".
///
/// RTSP URI template, e.g. "rtsp://user:{{PASSWORD}}@camera.lan:554/stream".
/// </summary>
[JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)]
public string Uri { get; set; }
@ -170,6 +178,24 @@ namespace ErsatzTV.Core.Next @@ -170,6 +178,24 @@ namespace ErsatzTV.Core.Next
/// </summary>
[JsonProperty("user_agent")]
public string UserAgent { get; set; }
/// <summary>
/// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to [].
/// </summary>
[JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)]
public List<string> Args { get; set; }
/// <summary>
/// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion.
/// </summary>
[JsonProperty("command", NullValueHandling = NullValueHandling.Ignore)]
public string Command { get; set; }
/// <summary>
/// Whether the content is live and therefore cannot work ahead. Default: false.
/// </summary>
[JsonProperty("is_live")]
public bool? IsLive { get; set; }
}
/// <summary>
@ -307,6 +333,12 @@ namespace ErsatzTV.Core.Next @@ -307,6 +333,12 @@ namespace ErsatzTV.Core.Next
/// A synthetic source produced by an ffmpeg lavfi filter graph.
///
/// A remote source fetched over HTTP(S).
///
/// A live stream pulled from an RTSP server (e.g. an IP camera). Always treated as live: it
/// is never seeked and cannot work ahead.
///
/// An external command whose stdout is an MPEG-TS stream, proxied to ffmpeg over loopback
/// HTTP.
/// </summary>
public partial class PlayoutItemSource
{
@ -369,6 +401,8 @@ namespace ErsatzTV.Core.Next @@ -369,6 +401,8 @@ namespace ErsatzTV.Core.Next
/// <summary>
/// URI template, e.g. "https://example.com/file.mkv?token={{MY_SECRET}}".
///
/// RTSP URI template, e.g. "rtsp://user:{{PASSWORD}}@camera.lan:554/stream".
/// </summary>
[JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)]
public string Uri { get; set; }
@ -378,6 +412,24 @@ namespace ErsatzTV.Core.Next @@ -378,6 +412,24 @@ namespace ErsatzTV.Core.Next
/// </summary>
[JsonProperty("user_agent")]
public string UserAgent { get; set; }
/// <summary>
/// Optional arguments for the command. Supports {{TEMPLATE}} expansion. Defaults to [].
/// </summary>
[JsonProperty("args", NullValueHandling = NullValueHandling.Ignore)]
public List<string> Args { get; set; }
/// <summary>
/// Command that writes an MPEG-TS stream to its stdout. Supports {{TEMPLATE}} expansion.
/// </summary>
[JsonProperty("command", NullValueHandling = NullValueHandling.Ignore)]
public string Command { get; set; }
/// <summary>
/// Whether the content is live and therefore cannot work ahead. Default: false.
/// </summary>
[JsonProperty("is_live")]
public bool? IsLive { get; set; }
}
/// <summary>
@ -435,7 +487,7 @@ namespace ErsatzTV.Core.Next @@ -435,7 +487,7 @@ namespace ErsatzTV.Core.Next
public TimingType TimingType { get; set; }
}
public enum SourceType { Http, Lavfi, Local };
public enum SourceType { Http, Lavfi, Local, Rtsp, Script };
/// <summary>
/// Anchor position within the primary content frame.
@ -500,6 +552,10 @@ namespace ErsatzTV.Core.Next @@ -500,6 +552,10 @@ namespace ErsatzTV.Core.Next
return SourceType.Lavfi;
case "local":
return SourceType.Local;
case "rtsp":
return SourceType.Rtsp;
case "script":
return SourceType.Script;
}
throw new Exception("Cannot unmarshal type SourceType");
}
@ -523,6 +579,12 @@ namespace ErsatzTV.Core.Next @@ -523,6 +579,12 @@ namespace ErsatzTV.Core.Next
case SourceType.Local:
serializer.Serialize(writer, "local");
return;
case SourceType.Rtsp:
serializer.Serialize(writer, "rtsp");
return;
case SourceType.Script:
serializer.Serialize(writer, "script");
return;
}
throw new Exception("Cannot marshal type SourceType");
}

8
ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj

@ -8,14 +8,14 @@ @@ -8,14 +8,14 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0">
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

8
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -12,11 +12,11 @@ @@ -12,11 +12,11 @@
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="Hardware.Info" Version="101.1.1.1" />
<PackageReference Include="Hardware.Info" Version="110.0.0.1" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Lennox.NvEncSharp" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageReference Include="Lennox.NvEncSharp" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.8" />
</ItemGroup>

2
ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj

@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="Dapper" Version="2.1.79" />
<PackageReference Include="EFCore.BulkExtensions.Sqlite" Version="[9.0.2,10)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.15" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.15" />

6
ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj

@ -11,18 +11,18 @@ @@ -11,18 +11,18 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.13.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="10.0.0">
<PackageReference Include="coverlet.collector" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="6.2.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="6.4.0" />
</ItemGroup>
<ItemGroup>

12
ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj

@ -14,12 +14,12 @@ @@ -14,12 +14,12 @@
<ItemGroup>
<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.0" />
<PackageReference Include="CliWrap" Version="3.10.1" />
<PackageReference Include="Dapper" Version="2.1.72" />
<PackageReference Include="Dapper" Version="2.1.79" />
<PackageReference Include="EFCore.BulkExtensions" Version="[9.0.2,10)" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.3.6" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.4.1" />
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="Jint" Version="4.8.0" />
<PackageReference Include="JsonSchema.Net" Version="9.2.0" />
<PackageReference Include="Jint" Version="4.9.2" />
<PackageReference Include="JsonSchema.Net" Version="9.2.1" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00017" />
@ -34,8 +34,8 @@ @@ -34,8 +34,8 @@
<PackageReference Include="Refit.Newtonsoft.Json" Version="10.1.6" />
<PackageReference Include="Refit.Xml" Version="10.1.6" />
<PackageReference Include="RichTextKit.Stbear" Version="0.4.167.3" />
<PackageReference Include="Scriban.Signed" Version="7.1.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Scriban.Signed" Version="7.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4)" />
<PackageReference Include="SkiaSharp" Version="3.119.2" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="TimeZoneConverter" Version="7.2.0" />

6
ErsatzTV.Infrastructure/Streaming/Graphics/GraphicsEngine.cs

@ -27,6 +27,10 @@ public class GraphicsEngine( @@ -27,6 +27,10 @@ public class GraphicsEngine(
ConfigElementKey.FFprobePath,
cancellationToken);
Option<string> ffmpegPath = await configElementRepository.GetValue<string>(
ConfigElementKey.FFmpegPath,
cancellationToken);
var elements = new List<IGraphicsElement>();
foreach (GraphicsElementContext element in context.Elements)
{
@ -54,6 +58,7 @@ public class GraphicsEngine( @@ -54,6 +58,7 @@ public class GraphicsEngine(
new MotionElement(
motionElementDataContext.MotionElement,
ffprobePath,
ffmpegPath,
localStatisticsProvider,
logger));
break;
@ -74,6 +79,7 @@ public class GraphicsEngine( @@ -74,6 +79,7 @@ public class GraphicsEngine(
templateFunctions,
tempFilePool,
subtitleElementContext.SubtitleElement,
ffmpegPath,
variables,
logger);

3
ErsatzTV.Infrastructure/Streaming/Graphics/Motion/MotionElement.cs

@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics; @@ -15,6 +15,7 @@ namespace ErsatzTV.Infrastructure.Streaming.Graphics;
public class MotionElement(
MotionGraphicsElement motionElement,
Option<string> ffprobePath,
Option<string> ffmpegPath,
ILocalStatisticsProvider localStatisticsProvider,
ILogger logger)
: GraphicsElement, IDisposable
@ -193,7 +194,7 @@ public class MotionElement( @@ -193,7 +194,7 @@ public class MotionElement(
_state = MotionElementState.PlayingIn;
Command command = Cli.Wrap("ffmpeg")
Command command = Cli.Wrap(await ffmpegPath.IfNoneAsync("ffmpeg"))
.WithArguments(arguments)
.WithWorkingDirectory(FileSystemLayout.TempFilePoolFolder)
.WithStandardOutputPipe(PipeTarget.ToStream(pipe.Writer.AsStream()));

3
ErsatzTV.Infrastructure/Streaming/Graphics/Subtitle/SubtitleElement.cs

@ -18,6 +18,7 @@ public class SubtitleElement( @@ -18,6 +18,7 @@ public class SubtitleElement(
TemplateFunctions templateFunctions,
ITempFilePool tempFilePool,
SubtitleGraphicsElement subtitleElement,
Option<string> ffmpegPath,
Dictionary<string, object> variables,
ILogger logger)
: GraphicsElement, IDisposable
@ -114,7 +115,7 @@ public class SubtitleElement( @@ -114,7 +115,7 @@ public class SubtitleElement(
"-"
];
Command command = Cli.Wrap("ffmpeg")
Command command = Cli.Wrap(await ffmpegPath.IfNoneAsync("ffmpeg"))
.WithArguments(arguments)
.WithWorkingDirectory(FileSystemLayout.TempFilePoolFolder)
.WithStandardOutputPipe(PipeTarget.ToStream(pipe.Writer.AsStream()));

6
ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj

@ -12,18 +12,18 @@ @@ -12,18 +12,18 @@
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="NUnit" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageReference Include="NUnit.Analyzers" Version="4.13.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="10.0.0">
<PackageReference Include="coverlet.collector" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="6.2.0" />
<PackageReference Include="Testably.Abstractions.Testing" Version="6.4.0" />
</ItemGroup>
<ItemGroup>

8
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -26,16 +26,16 @@ @@ -26,16 +26,16 @@
<PackageReference Include="Humanizer.Core" Version="3.0.10" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR" Version="[12.5.0]" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.7" />
<PackageReference Include="System.CommandLine" Version="2.0.8" />
</ItemGroup>
<ItemGroup>

2
ErsatzTV.Tests/ErsatzTV.Tests.csproj

@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.6.1" />
<PackageReference Include="NUnit.Analyzers" Version="4.13.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

3
ErsatzTV/Controllers/IptvController.cs

@ -275,7 +275,8 @@ public class IptvController : StreamingControllerBase @@ -275,7 +275,8 @@ public class IptvController : StreamingControllerBase
Request.Scheme,
Request.Host.ToString(),
channelNumber,
mode))
mode,
Request.Query["access_token"]))
.Map(r => r.Match<IActionResult>(
playlist => Content(playlist, "application/vnd.apple.mpegurl"),
error => BadRequest(error.Value)));

16
ErsatzTV/ErsatzTV.csproj

@ -31,7 +31,7 @@ @@ -31,7 +31,7 @@
<ItemGroup>
<!-- <PackageReference Include="EntityFrameworkProfiler.Appender" Version="6.0.6049" /> -->
<PackageReference Include="Blazored.FluentValidation" Version="2.2.0" />
<PackageReference Include="BlazorSortable" Version="6.0.1" />
<PackageReference Include="BlazorSortable" Version="8.1.0" />
<PackageReference Include="Chronic.Core" Version="0.4.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.3.1" />
@ -39,23 +39,23 @@ @@ -39,23 +39,23 @@
<PackageReference Include="HtmlSanitizer" Version="9.0.892" />
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.15">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.7">
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="9.4.0" />
<PackageReference Include="NaturalSort.Extension" Version="4.4.1" />
<PackageReference Include="Refit.HttpClientFactory" Version="10.1.6" />
<PackageReference Include="Scalar.AspNetCore" Version="2.14.10" />
<PackageReference Include="Scalar.AspNetCore" Version="2.14.14" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />

1
ErsatzTV/Startup.cs

@ -363,6 +363,7 @@ public class Startup @@ -363,6 +363,7 @@ public class Startup
FileSystemLayout.AudioStreamSelectorScriptsFolder,
FileSystemLayout.MpegTsScriptsFolder,
FileSystemLayout.DefaultMpegTsScriptFolder,
FileSystemLayout.NextChannelConfigOverlaysFolder,
FileSystemLayout.NextPlayoutsFolder,
];

Loading…
Cancel
Save