add codec info to multivariant playlist (#2472)

* add codec info to multivariant playlist

* upgrade dependencies
This commit is contained in:
Jason Dove
2025-09-30 13:58:24 -05:00
committed by GitHub
parent 287adc34b5
commit fac0f36d35
10 changed files with 62 additions and 22 deletions
+1
View File
@@ -89,6 +89,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- GIFs flagged to loop forever will loop forever - GIFs flagged to loop forever will loop forever
- GIFs with a specific loop count will loop the specified number of times and then hold the final frame - GIFs with a specific loop count will loop the specified number of times and then hold the final frame
- Note that looping is relative to the start of the content, so this works best with permanent watermarks - Note that looping is relative to the start of the content, so this works best with permanent watermarks
- Fix some more hls.js warnins by adding codec information to multi-variant playlists
### Changed ### Changed
- Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration - Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration
@@ -0,0 +1,11 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Channels;
public record ChannelStreamingSpecsViewModel(
int Height,
int Width,
int Bitrate,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
FFmpegProfileAudioFormat AudioFormat);
+8 -2
View File
@@ -49,8 +49,14 @@ internal static class Mapper
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) => internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
new(resolution.Height, resolution.Width); new(resolution.Height, resolution.Width);
internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) => internal static ChannelStreamingSpecsViewModel ProjectToSpecsViewModel(Channel channel) =>
new(resolution.Height, resolution.Width, bitrate); new(
channel.FFmpegProfile.Resolution.Height,
channel.FFmpegProfile.Resolution.Width,
(int)((channel.FFmpegProfile.VideoBitrate * 1000 + channel.FFmpegProfile.AudioBitrate * 1000) * 1.2),
channel.FFmpegProfile.VideoFormat,
channel.FFmpegProfile.VideoProfile,
channel.FFmpegProfile.AudioFormat);
private static ArtworkContentTypeModel GetLogo(Channel channel) private static ArtworkContentTypeModel GetLogo(Channel channel)
{ {
@@ -1,3 +0,0 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelResolutionAndBitrate(string ChannelNumber) : IRequest<Option<ResolutionAndBitrateViewModel>>;
@@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Channels;
public record GetChannelStreamingSpecs(string ChannelNumber) : IRequest<Option<ChannelStreamingSpecsViewModel>>;
@@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Channels; namespace ErsatzTV.Application.Channels;
public class GetChannelResolutionAndBitrateHandler(IDbContextFactory<TvContext> dbContextFactory) public class GetChannelStreamingSpecsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetChannelResolutionAndBitrate, Option<ResolutionAndBitrateViewModel>> : IRequestHandler<GetChannelStreamingSpecs, Option<ChannelStreamingSpecsViewModel>>
{ {
public async Task<Option<ResolutionAndBitrateViewModel>> Handle( public async Task<Option<ChannelStreamingSpecsViewModel>> Handle(
GetChannelResolutionAndBitrate request, GetChannelStreamingSpecs request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
@@ -20,8 +20,6 @@ public class GetChannelResolutionAndBitrateHandler(IDbContextFactory<TvContext>
.ThenInclude(ff => ff.Resolution) .ThenInclude(ff => ff.Resolution)
.SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber, cancellationToken); .SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber, cancellationToken);
return maybeChannel.Map(c => Mapper.ProjectToViewModel( return maybeChannel.Map(Mapper.ProjectToSpecsViewModel);
c.FFmpegProfile.Resolution,
(int)((c.FFmpegProfile.VideoBitrate * 1000 + c.FFmpegProfile.AudioBitrate * 1000) * 1.2)));
} }
} }
@@ -1,3 +0,0 @@
namespace ErsatzTV.Application.Channels;
public record ResolutionAndBitrateViewModel(int Height, int Width, int Bitrate);
@@ -14,7 +14,7 @@
<PackageReference Include="CliWrap" Version="3.9.0" /> <PackageReference Include="CliWrap" Version="3.9.0" />
<PackageReference Include="Dapper" Version="2.1.66" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" /> <PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.1.7" /> <PackageReference Include="Elastic.Clients.Elasticsearch" Version="9.1.9" />
<PackageReference Include="Jint" Version="4.4.1" /> <PackageReference Include="Jint" Version="4.4.1" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" /> <PackageReference Include="Lucene.Net" Version="4.8.0-beta00017" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" /> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00017" />
+32 -5
View File
@@ -316,14 +316,41 @@ public class IptvController : ControllerBase
"Failed to return ffmpeg multi-variant playlist; falling back to generated playlist"); "Failed to return ffmpeg multi-variant playlist; falling back to generated playlist");
} }
Option<ResolutionAndBitrateViewModel> maybeResolutionAndBitrate = Option<ChannelStreamingSpecsViewModel> maybeStreamingSpecs =
await _mediator.Send(new GetChannelResolutionAndBitrate(channelNumber)); await _mediator.Send(new GetChannelStreamingSpecs(channelNumber));
string resolution = string.Empty; string resolution = string.Empty;
var bitrate = "10000000"; var bitrate = "10000000";
foreach (ResolutionAndBitrateViewModel res in maybeResolutionAndBitrate) foreach (ChannelStreamingSpecsViewModel streamingSpecs in maybeStreamingSpecs)
{ {
resolution = $",RESOLUTION={res.Width}x{res.Height}"; string videoCodec = streamingSpecs.VideoFormat switch
bitrate = res.Bitrate.ToString(CultureInfo.InvariantCulture); {
FFmpegProfileVideoFormat.Av1 => "av01.0.01M.08",
FFmpegProfileVideoFormat.Hevc => "hvc1.1.6.L93.B0",
FFmpegProfileVideoFormat.H264 => "avc1.4D4028",
_ => string.Empty
};
string audioCodec = streamingSpecs.AudioFormat switch
{
FFmpegProfileAudioFormat.Ac3 => "ac-3",
FFmpegProfileAudioFormat.Aac => "mp4a.40.2",
_ => string.Empty
};
List<string> codecStrings = [];
if (!string.IsNullOrWhiteSpace(videoCodec))
{
codecStrings.Add(videoCodec);
}
if (!string.IsNullOrWhiteSpace(audioCodec))
{
codecStrings.Add(audioCodec);
}
string codecs = codecStrings.Count > 0 ? $",CODECS=\"{string.Join(",", codecStrings)}\"" : string.Empty;
resolution = $",RESOLUTION={streamingSpecs.Width}x{streamingSpecs.Height}{codecs}";
bitrate = streamingSpecs.Bitrate.ToString(CultureInfo.InvariantCulture);
} }
return $@"#EXTM3U return $@"#EXTM3U
+1 -1
View File
@@ -57,7 +57,7 @@
<PackageReference Include="MudBlazor" Version="8.12.0" /> <PackageReference Include="MudBlazor" Version="8.12.0" />
<PackageReference Include="NaturalSort.Extension" Version="4.4.0" /> <PackageReference Include="NaturalSort.Extension" Version="4.4.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" /> <PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.8.6" /> <PackageReference Include="Scalar.AspNetCore" Version="2.8.8" />
<PackageReference Include="Serilog" Version="4.3.0" /> <PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />