Browse Source

add aac (latm) audio format (#2561)

* add aac (latm) audio format

* update changelog
pull/2562/head
Jason Dove 9 months ago committed by GitHub
parent
commit
a47510fef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 1
      ErsatzTV.Core/Domain/FFmpegProfileAudioFormat.cs
  3. 51
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  4. 4
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  5. 24
      ErsatzTV.FFmpeg/Decoder/DecoderAacLatm.cs
  6. 2
      ErsatzTV.FFmpeg/FFmpegState.cs
  7. 1
      ErsatzTV.FFmpeg/Format/AudioFormat.cs
  8. 2
      ErsatzTV.FFmpeg/InputFile.cs
  9. 9
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  10. 12
      ErsatzTV.FFmpeg/OutputOption/AudioChannelsOutputOption.cs
  11. 7
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  12. 2
      ErsatzTV/Controllers/IptvController.cs
  13. 1
      ErsatzTV/Pages/FFmpegEditor.razor

1
CHANGELOG.md

@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- By default, poster will be added as image with type "poster" and thumbnail will be added as image with type "still" - By default, poster will be added as image with type "poster" and thumbnail will be added as image with type "still"
- Poster will continue to be added as icon by default - Poster will continue to be added as icon by default
- Add buttons to edit Jellyfin and Emby connection information in **Media Sources** > **Jellyfin** and **Media Sources** > **Emby** - Add buttons to edit Jellyfin and Emby connection information in **Media Sources** > **Jellyfin** and **Media Sources** > **Emby**
- Add audio format `aac (latm)` for DVB-C compatibility; `aac` uses ADTS by default which is required in most cases
### Fixed ### Fixed
- Fix NVIDIA startup errors on arm64 - Fix NVIDIA startup errors on arm64

1
ErsatzTV.Core/Domain/FFmpegProfileAudioFormat.cs

@ -9,6 +9,7 @@ public enum FFmpegProfileAudioFormat
Aac = 1, Aac = 1,
Ac3 = 2, Ac3 = 2,
AacLatm = 3,
Copy = 99 Copy = 99
} }

51
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -171,6 +171,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
string audioFormat = playbackSettings.AudioFormat switch string audioFormat = playbackSettings.AudioFormat switch
{ {
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac, FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
FFmpegProfileAudioFormat.AacLatm => AudioFormat.AacLatm,
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3, FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
FFmpegProfileAudioFormat.Copy => AudioFormat.Copy, FFmpegProfileAudioFormat.Copy => AudioFormat.Copy,
_ => throw new ArgumentOutOfRangeException($"unexpected audio format {playbackSettings.VideoFormat}") _ => throw new ArgumentOutOfRangeException($"unexpected audio format {playbackSettings.VideoFormat}")
@ -403,6 +404,27 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_ => Option<string>.None _ => Option<string>.None
}; };
Option<string> hlsSegmentOptions = Option<string>.None;
if (outputFormat is OutputFormatKind.Hls)
{
string options = string.Empty;
if (ptsOffset == TimeSpan.Zero)
{
options += "+initial_discontinuity";
}
if (audioFormat == AudioFormat.AacLatm)
{
options += "+latm";
}
if (!string.IsNullOrWhiteSpace(options))
{
hlsSegmentOptions = $"mpegts_flags={options}";
}
}
FrameSize scaledSize = ffmpegVideoStream.SquarePixelFrameSize( FrameSize scaledSize = ffmpegVideoStream.SquarePixelFrameSize(
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)); new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height));
@ -506,6 +528,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsPlaylistPath, hlsPlaylistPath,
hlsSegmentTemplate, hlsSegmentTemplate,
hlsInitTemplate, hlsInitTemplate,
hlsSegmentOptions,
ptsOffset, ptsOffset,
playbackSettings.ThreadCount, playbackSettings.ThreadCount,
qsvExtraHardwareFrames, qsvExtraHardwareFrames,
@ -583,6 +606,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
string audioFormat = playbackSettings.AudioFormat switch string audioFormat = playbackSettings.AudioFormat switch
{ {
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3, FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
FFmpegProfileAudioFormat.AacLatm => AudioFormat.AacLatm,
_ => AudioFormat.Aac _ => AudioFormat.Aac
}; };
@ -645,6 +669,27 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_ => Option<string>.None _ => Option<string>.None
}; };
Option<string> hlsSegmentOptions = Option<string>.None;
if (outputFormat is OutputFormatKind.Hls)
{
string options = string.Empty;
if (ptsOffset == TimeSpan.Zero)
{
options += "+initial_discontinuity";
}
if (audioFormat == AudioFormat.AacLatm)
{
options += "+latm";
}
if (!string.IsNullOrWhiteSpace(options))
{
hlsSegmentOptions = $"mpegts_options={options}";
}
}
string videoPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png"); string videoPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png");
var videoVersion = BackgroundImageMediaVersion.ForPath(videoPath, desiredResolution); var videoVersion = BackgroundImageMediaVersion.ForPath(videoPath, desiredResolution);
@ -686,6 +731,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsPlaylistPath, hlsPlaylistPath,
hlsSegmentTemplate, hlsSegmentTemplate,
hlsInitTemplate, hlsInitTemplate,
hlsSegmentOptions,
ptsOffset, ptsOffset,
Option<int>.None, Option<int>.None,
qsvExtraHardwareFrames, qsvExtraHardwareFrames,
@ -778,6 +824,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
$"http://localhost:{Settings.StreamingPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter{accessTokenQuery}", $"http://localhost:{Settings.StreamingPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter{accessTokenQuery}",
resolution); resolution);
if (channel.FFmpegProfile.AudioFormat is FFmpegProfileAudioFormat.AacLatm)
{
concatInputFile.AudioFormat = AudioFormat.AacLatm;
}
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder( IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
HardwareAccelerationMode.None, HardwareAccelerationMode.None,
Option<VideoInputFile>.None, Option<VideoInputFile>.None,

4
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -91,6 +91,7 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None,
TimeSpan.Zero, TimeSpan.Zero,
Option<int>.None, Option<int>.None,
Option<int>.None, Option<int>.None,
@ -190,6 +191,7 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None,
TimeSpan.Zero, TimeSpan.Zero,
Option<int>.None, Option<int>.None,
Option<int>.None, Option<int>.None,
@ -347,6 +349,7 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None,
TimeSpan.Zero, TimeSpan.Zero,
Option<int>.None, Option<int>.None,
Option<int>.None, Option<int>.None,
@ -440,6 +443,7 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None,
TimeSpan.Zero, TimeSpan.Zero,
Option<int>.None, Option<int>.None,
Option<int>.None, Option<int>.None,

24
ErsatzTV.FFmpeg/Decoder/DecoderAacLatm.cs

@ -0,0 +1,24 @@
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.InputOption;
namespace ErsatzTV.FFmpeg.Decoder;
public class DecoderAacLatm : IInputOption
{
public EnvironmentVariable[] EnvironmentVariables => [];
public string[] GlobalOptions => [];
public string[] FilterOptions => [];
public string[] OutputOptions => [];
public string[] InputOptions(InputFile inputFile) => ["-c:a", "aac_latm"];
public FrameState NextState(FrameState currentState) => currentState;
public bool AppliesTo(AudioInputFile audioInputFile) => false;
public bool AppliesTo(VideoInputFile videoInputFile) => false;
public bool AppliesTo(ConcatInputFile concatInputFile) => true;
public bool AppliesTo(GraphicsEngineInput graphicsEngineInput) => false;
}

2
ErsatzTV.FFmpeg/FFmpegState.cs

@ -20,6 +20,7 @@ public record FFmpegState(
Option<string> HlsPlaylistPath, Option<string> HlsPlaylistPath,
Option<string> HlsSegmentTemplate, Option<string> HlsSegmentTemplate,
Option<string> HlsInitTemplate, Option<string> HlsInitTemplate,
Option<string> HlsSegmentOptions,
TimeSpan PtsOffset, TimeSpan PtsOffset,
Option<int> ThreadCount, Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames, Option<int> MaybeQsvExtraHardwareFrames,
@ -49,6 +50,7 @@ public record FFmpegState(
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None, Option<string>.None,
Option<string>.None,
TimeSpan.Zero, TimeSpan.Zero,
Option<int>.None, Option<int>.None,
Option<int>.None, Option<int>.None,

1
ErsatzTV.FFmpeg/Format/AudioFormat.cs

@ -4,6 +4,7 @@ public static class AudioFormat
{ {
public const string Aac = "aac"; public const string Aac = "aac";
public const string Ac3 = "ac3"; public const string Ac3 = "ac3";
public const string AacLatm = "aac-latm";
public const string Copy = "copy"; public const string Copy = "copy";
} }

2
ErsatzTV.FFmpeg/InputFile.cs

@ -28,6 +28,8 @@ public record ConcatInputFile(string Url, FrameSize Resolution) : InputFile(
ScanKind.Unknown) ScanKind.Unknown)
}) })
{ {
public Option<string> AudioFormat { get; set; }
public void AddOption(IInputOption option) public void AddOption(IInputOption option)
{ {
if (option.AppliesTo(this)) if (option.AppliesTo(this))

9
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -11,6 +11,7 @@ public class OutputFormatHls : IPipelineStep
private readonly bool _isTroubleshooting; private readonly bool _isTroubleshooting;
private readonly Option<string> _mediaFrameRate; private readonly Option<string> _mediaFrameRate;
private readonly OutputFormatKind _outputFormat; private readonly OutputFormatKind _outputFormat;
private readonly Option<string> _segmentOptions;
private readonly bool _oneSecondGop; private readonly bool _oneSecondGop;
private readonly string _playlistPath; private readonly string _playlistPath;
private readonly string _segmentTemplate; private readonly string _segmentTemplate;
@ -20,6 +21,7 @@ public class OutputFormatHls : IPipelineStep
FrameState desiredState, FrameState desiredState,
Option<string> mediaFrameRate, Option<string> mediaFrameRate,
OutputFormatKind outputFormat, OutputFormatKind outputFormat,
Option<string> segmentOptions,
string segmentTemplate, string segmentTemplate,
Option<string> initTemplate, Option<string> initTemplate,
string playlistPath, string playlistPath,
@ -30,6 +32,7 @@ public class OutputFormatHls : IPipelineStep
_desiredState = desiredState; _desiredState = desiredState;
_mediaFrameRate = mediaFrameRate; _mediaFrameRate = mediaFrameRate;
_outputFormat = outputFormat; _outputFormat = outputFormat;
_segmentOptions = segmentOptions;
_segmentTemplate = segmentTemplate; _segmentTemplate = segmentTemplate;
_initTemplate = initTemplate; _initTemplate = initTemplate;
_playlistPath = playlistPath; _playlistPath = playlistPath;
@ -83,6 +86,11 @@ public class OutputFormatHls : IPipelineStep
break; break;
} }
foreach (string options in _segmentOptions)
{
result.AddRange("-hls_segment_options", options);
}
string pdt = _isTroubleshooting ? string.Empty : "program_date_time+omit_endlist+"; string pdt = _isTroubleshooting ? string.Empty : "program_date_time+omit_endlist+";
if (_isFirstTranscode) if (_isFirstTranscode)
@ -108,7 +116,6 @@ public class OutputFormatHls : IPipelineStep
result.AddRange( result.AddRange(
[ [
"-hls_flags", $"{pdt}append_list+discont_start{independentSegments}", "-hls_flags", $"{pdt}append_list+discont_start{independentSegments}",
"-mpegts_flags", "+initial_discontinuity",
_playlistPath _playlistPath
]); ]);
break; break;

12
ErsatzTV.FFmpeg/OutputOption/AudioChannelsOutputOption.cs

@ -20,15 +20,17 @@ public class AudioChannelsOutputOption : OutputOption
{ {
get get
{ {
if (_sourceChannels != _desiredChannels || _audioFormat == Some(AudioFormat.Aac) && _desiredChannels > 2) if (_sourceChannels != _desiredChannels ||
(_audioFormat == Some(AudioFormat.Aac) || _audioFormat == Some(AudioFormat.AacLatm)) &&
_desiredChannels > 2)
{ {
return new[] return
{ [
"-ac", _desiredChannels.ToString(CultureInfo.InvariantCulture) "-ac", _desiredChannels.ToString(CultureInfo.InvariantCulture)
}; ];
} }
return Array.Empty<string>(); return [];
} }
} }
} }

7
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -162,6 +162,12 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
new EncoderCopyAll() new EncoderCopyAll()
}; };
// ffmpeg below 8 doesn't detect this without an explicit decoder
foreach (string _ in concatInputFile.AudioFormat.Where(af => af == AudioFormat.AacLatm))
{
concatInputFile.AddOption(new DecoderAacLatm());
}
concatInputFile.AddOption(new ReadrateInputOption(1.0)); concatInputFile.AddOption(new ReadrateInputOption(1.0));
SetMetadataServiceProvider(ffmpegState, pipelineSteps); SetMetadataServiceProvider(ffmpegState, pipelineSteps);
@ -384,6 +390,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
desiredState, desiredState,
videoStream.FrameRate, videoStream.FrameRate,
ffmpegState.OutputFormat, ffmpegState.OutputFormat,
ffmpegState.HlsSegmentOptions,
segmentTemplate, segmentTemplate,
ffmpegState.HlsInitTemplate, ffmpegState.HlsInitTemplate,
playlistPath, playlistPath,

2
ErsatzTV/Controllers/IptvController.cs

@ -307,7 +307,7 @@ public class IptvController : StreamingControllerBase
string audioCodec = streamingSpecs.AudioFormat switch string audioCodec = streamingSpecs.AudioFormat switch
{ {
FFmpegProfileAudioFormat.Ac3 => "ac-3", FFmpegProfileAudioFormat.Ac3 => "ac-3",
FFmpegProfileAudioFormat.Aac => "mp4a.40.2", FFmpegProfileAudioFormat.Aac or FFmpegProfileAudioFormat.AacLatm => "mp4a.40.2",
_ => string.Empty _ => string.Empty
}; };

1
ErsatzTV/Pages/FFmpegEditor.razor

@ -242,6 +242,7 @@
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)"> <MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
<MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem> <MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem> <MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.AacLatm">aac (latm)</MudSelectItem>
</MudSelect> </MudSelect>
</MudStack> </MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">

Loading…
Cancel
Save