diff --git a/CHANGELOG.md b/CHANGELOG.md index 680b1bd0e..cdfce7c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 +- Fix some more hls.js warnins by adding codec information to multi-variant playlists ### Changed - Filler presets: use separate text fields for `hours`, `minutes` and `seconds` duration diff --git a/ErsatzTV.Application/Channels/ChannelStreamingSpecsViewModel.cs b/ErsatzTV.Application/Channels/ChannelStreamingSpecsViewModel.cs new file mode 100644 index 000000000..a6a46aa97 --- /dev/null +++ b/ErsatzTV.Application/Channels/ChannelStreamingSpecsViewModel.cs @@ -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); diff --git a/ErsatzTV.Application/Channels/Mapper.cs b/ErsatzTV.Application/Channels/Mapper.cs index 4f702abf4..75f9256d7 100644 --- a/ErsatzTV.Application/Channels/Mapper.cs +++ b/ErsatzTV.Application/Channels/Mapper.cs @@ -49,8 +49,14 @@ internal static class Mapper internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) => new(resolution.Height, resolution.Width); - internal static ResolutionAndBitrateViewModel ProjectToViewModel(Resolution resolution, int bitrate) => - new(resolution.Height, resolution.Width, bitrate); + internal static ChannelStreamingSpecsViewModel ProjectToSpecsViewModel(Channel channel) => + 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) { diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelResolutionAndBitrate.cs b/ErsatzTV.Application/Channels/Queries/GetChannelResolutionAndBitrate.cs deleted file mode 100644 index 2b7cd74f7..000000000 --- a/ErsatzTV.Application/Channels/Queries/GetChannelResolutionAndBitrate.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ErsatzTV.Application.Channels; - -public record GetChannelResolutionAndBitrate(string ChannelNumber) : IRequest>; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelStreamingSpecs.cs b/ErsatzTV.Application/Channels/Queries/GetChannelStreamingSpecs.cs new file mode 100644 index 000000000..28fb05c8e --- /dev/null +++ b/ErsatzTV.Application/Channels/Queries/GetChannelStreamingSpecs.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Channels; + +public record GetChannelStreamingSpecs(string ChannelNumber) : IRequest>; diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelResolutionAndBitrateHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelStreamingSpecsHandler.cs similarity index 55% rename from ErsatzTV.Application/Channels/Queries/GetChannelResolutionAndBitrateHandler.cs rename to ErsatzTV.Application/Channels/Queries/GetChannelStreamingSpecsHandler.cs index 3c32f6b17..ad6021dab 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelResolutionAndBitrateHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelStreamingSpecsHandler.cs @@ -5,11 +5,11 @@ using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Channels; -public class GetChannelResolutionAndBitrateHandler(IDbContextFactory dbContextFactory) - : IRequestHandler> +public class GetChannelStreamingSpecsHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> { - public async Task> Handle( - GetChannelResolutionAndBitrate request, + public async Task> Handle( + GetChannelStreamingSpecs request, CancellationToken cancellationToken) { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); @@ -20,8 +20,6 @@ public class GetChannelResolutionAndBitrateHandler(IDbContextFactory .ThenInclude(ff => ff.Resolution) .SelectOneAsync(c => c.Number, c => c.Number == request.ChannelNumber, cancellationToken); - return maybeChannel.Map(c => Mapper.ProjectToViewModel( - c.FFmpegProfile.Resolution, - (int)((c.FFmpegProfile.VideoBitrate * 1000 + c.FFmpegProfile.AudioBitrate * 1000) * 1.2))); + return maybeChannel.Map(Mapper.ProjectToSpecsViewModel); } } diff --git a/ErsatzTV.Application/Channels/ResolutionAndBitrateViewModel.cs b/ErsatzTV.Application/Channels/ResolutionAndBitrateViewModel.cs deleted file mode 100644 index 9bd7f8b39..000000000 --- a/ErsatzTV.Application/Channels/ResolutionAndBitrateViewModel.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ErsatzTV.Application.Channels; - -public record ResolutionAndBitrateViewModel(int Height, int Width, int Bitrate); diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index 5ec74d881..769363605 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -14,7 +14,7 @@ - + diff --git a/ErsatzTV/Controllers/IptvController.cs b/ErsatzTV/Controllers/IptvController.cs index 26f2e1f14..de78f2e9c 100644 --- a/ErsatzTV/Controllers/IptvController.cs +++ b/ErsatzTV/Controllers/IptvController.cs @@ -316,14 +316,41 @@ public class IptvController : ControllerBase "Failed to return ffmpeg multi-variant playlist; falling back to generated playlist"); } - Option maybeResolutionAndBitrate = - await _mediator.Send(new GetChannelResolutionAndBitrate(channelNumber)); + Option maybeStreamingSpecs = + await _mediator.Send(new GetChannelStreamingSpecs(channelNumber)); string resolution = string.Empty; var bitrate = "10000000"; - foreach (ResolutionAndBitrateViewModel res in maybeResolutionAndBitrate) + foreach (ChannelStreamingSpecsViewModel streamingSpecs in maybeStreamingSpecs) { - resolution = $",RESOLUTION={res.Width}x{res.Height}"; - bitrate = res.Bitrate.ToString(CultureInfo.InvariantCulture); + string videoCodec = streamingSpecs.VideoFormat switch + { + 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 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 diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 4b298f4b3..2698b65d1 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -57,7 +57,7 @@ - +