|
|
|
@ -665,7 +665,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService |
|
|
|
string vaapiDevice, |
|
|
|
string vaapiDevice, |
|
|
|
Option<int> qsvExtraHardwareFrames) |
|
|
|
Option<int> qsvExtraHardwareFrames) |
|
|
|
{ |
|
|
|
{ |
|
|
|
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateErrorSettings( |
|
|
|
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings( |
|
|
|
channel.StreamingMode, |
|
|
|
channel.StreamingMode, |
|
|
|
channel.FFmpegProfile, |
|
|
|
channel.FFmpegProfile, |
|
|
|
hlsRealtime); |
|
|
|
hlsRealtime); |
|
|
|
@ -726,57 +726,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService |
|
|
|
playbackSettings.NormalizeColors, |
|
|
|
playbackSettings.NormalizeColors, |
|
|
|
playbackSettings.Deinterlace); |
|
|
|
playbackSettings.Deinterlace); |
|
|
|
|
|
|
|
|
|
|
|
OutputFormatKind outputFormat = OutputFormatKind.MpegTs; |
|
|
|
|
|
|
|
switch (channel.StreamingMode) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
case StreamingMode.HttpLiveStreamingSegmenter: |
|
|
|
|
|
|
|
outputFormat = OutputFormatKind.Hls; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<string> hlsPlaylistPath = outputFormat is OutputFormatKind.Hls or OutputFormatKind.HlsMp4 |
|
|
|
|
|
|
|
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8") |
|
|
|
|
|
|
|
: Option<string>.None; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
long nowSeconds = now.ToUnixTimeSeconds(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<string> hlsSegmentTemplate = outputFormat switch |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
OutputFormatKind.Hls => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"), |
|
|
|
|
|
|
|
OutputFormatKind.HlsMp4 => Path.Combine( |
|
|
|
|
|
|
|
FileSystemLayout.TranscodeFolder, |
|
|
|
|
|
|
|
channel.Number, |
|
|
|
|
|
|
|
$"live_{nowSeconds}_%06d.m4s"), |
|
|
|
|
|
|
|
_ => Option<string>.None |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<string> hlsInitTemplate = outputFormat switch |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
OutputFormatKind.HlsMp4 => $"{nowSeconds}_init.mp4", |
|
|
|
|
|
|
|
_ => 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}"; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string videoPath = _localFileSystem.GetCustomOrDefaultFile( |
|
|
|
string videoPath = _localFileSystem.GetCustomOrDefaultFile( |
|
|
|
FileSystemLayout.ResourcesCacheFolder, |
|
|
|
FileSystemLayout.ResourcesCacheFolder, |
|
|
|
"background.png"); |
|
|
|
"background.png"); |
|
|
|
@ -802,6 +751,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService |
|
|
|
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); |
|
|
|
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); |
|
|
|
_logger.LogDebug("HW accel mode: {HwAccel}", hwAccel); |
|
|
|
_logger.LogDebug("HW accel mode: {HwAccel}", hwAccel); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var hlsOptions = GetHlsOptions(channel, now, ptsOffset, audioFormat); |
|
|
|
|
|
|
|
|
|
|
|
var ffmpegState = new FFmpegState( |
|
|
|
var ffmpegState = new FFmpegState( |
|
|
|
channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel, |
|
|
|
channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel, |
|
|
|
HardwareAccelerationMode.None, // no hw accel decode since errors loop
|
|
|
|
HardwareAccelerationMode.None, // no hw accel decode since errors loop
|
|
|
|
@ -816,11 +767,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService |
|
|
|
None, |
|
|
|
None, |
|
|
|
None, |
|
|
|
None, |
|
|
|
None, |
|
|
|
None, |
|
|
|
outputFormat, |
|
|
|
hlsOptions.OutputFormat, |
|
|
|
hlsPlaylistPath, |
|
|
|
hlsOptions.HlsPlaylistPath, |
|
|
|
hlsSegmentTemplate, |
|
|
|
hlsOptions.HlsSegmentTemplate, |
|
|
|
hlsInitTemplate, |
|
|
|
hlsOptions.HlsInitTemplate, |
|
|
|
hlsSegmentOptions, |
|
|
|
hlsOptions.HlsSegmentOptions, |
|
|
|
ptsOffset, |
|
|
|
ptsOffset, |
|
|
|
Option<int>.None, |
|
|
|
Option<int>.None, |
|
|
|
qsvExtraHardwareFrames, |
|
|
|
qsvExtraHardwareFrames, |
|
|
|
@ -862,6 +813,136 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService |
|
|
|
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, None, pipeline); |
|
|
|
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, None, pipeline); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<Command> Slug( |
|
|
|
|
|
|
|
string ffmpegPath, |
|
|
|
|
|
|
|
Channel channel, |
|
|
|
|
|
|
|
DateTimeOffset now, |
|
|
|
|
|
|
|
TimeSpan duration, |
|
|
|
|
|
|
|
bool hlsRealtime, |
|
|
|
|
|
|
|
TimeSpan ptsOffset) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings( |
|
|
|
|
|
|
|
channel.StreamingMode, |
|
|
|
|
|
|
|
channel.FFmpegProfile, |
|
|
|
|
|
|
|
hlsRealtime); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Resolution desiredResolution = channel.FFmpegProfile.Resolution; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string audioFormat = playbackSettings.AudioFormat switch |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3, |
|
|
|
|
|
|
|
FFmpegProfileAudioFormat.AacLatm => AudioFormat.AacLatm, |
|
|
|
|
|
|
|
_ => AudioFormat.Aac |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var audioState = new AudioState( |
|
|
|
|
|
|
|
audioFormat, |
|
|
|
|
|
|
|
playbackSettings.AudioChannels, |
|
|
|
|
|
|
|
playbackSettings.AudioBitrate, |
|
|
|
|
|
|
|
playbackSettings.AudioBufferSize, |
|
|
|
|
|
|
|
playbackSettings.AudioSampleRate, |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
AudioFilter.None, |
|
|
|
|
|
|
|
playbackSettings.TargetLoudness); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
string videoFormat = GetVideoFormat(playbackSettings); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var desiredState = new FrameState( |
|
|
|
|
|
|
|
playbackSettings.RealtimeOutput, |
|
|
|
|
|
|
|
InfiniteLoop: false, |
|
|
|
|
|
|
|
videoFormat, |
|
|
|
|
|
|
|
GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile), |
|
|
|
|
|
|
|
VideoPreset.Unset, |
|
|
|
|
|
|
|
channel.FFmpegProfile.AllowBFrames, |
|
|
|
|
|
|
|
new PixelFormatYuv420P(), |
|
|
|
|
|
|
|
new FrameSize(desiredResolution.Width, desiredResolution.Height), |
|
|
|
|
|
|
|
new FrameSize(desiredResolution.Width, desiredResolution.Height), |
|
|
|
|
|
|
|
Option<FrameSize>.None, |
|
|
|
|
|
|
|
channel.FFmpegProfile.PadMode is FilterMode.HardwareIfPossible |
|
|
|
|
|
|
|
? FFmpegFilterMode.HardwareIfPossible |
|
|
|
|
|
|
|
: FFmpegFilterMode.Software, |
|
|
|
|
|
|
|
IsAnamorphic: false, |
|
|
|
|
|
|
|
Option<FrameRate>.None, |
|
|
|
|
|
|
|
playbackSettings.VideoBitrate, |
|
|
|
|
|
|
|
playbackSettings.VideoBufferSize, |
|
|
|
|
|
|
|
playbackSettings.VideoTrackTimeScale, |
|
|
|
|
|
|
|
playbackSettings.NormalizeColors, |
|
|
|
|
|
|
|
playbackSettings.Deinterlace); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var frameRate = playbackSettings.FrameRate.IfNone(new FrameRate("24")); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var ffmpegVideoStream = new VideoStream( |
|
|
|
|
|
|
|
0, |
|
|
|
|
|
|
|
VideoFormat.GeneratedImage, |
|
|
|
|
|
|
|
string.Empty, |
|
|
|
|
|
|
|
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
|
|
|
|
|
|
|
|
ColorParams.Default, |
|
|
|
|
|
|
|
desiredState.PaddedSize, |
|
|
|
|
|
|
|
MaybeSampleAspectRatio: "1:1", |
|
|
|
|
|
|
|
DisplayAspectRatio: string.Empty, |
|
|
|
|
|
|
|
frameRate, |
|
|
|
|
|
|
|
true, |
|
|
|
|
|
|
|
ScanKind.Progressive); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var videoInputFile = new LavfiInputFile( |
|
|
|
|
|
|
|
$"color=c=black:s={desiredState.PaddedSize.Width}x{desiredState.PaddedSize.Height}:r={frameRate.FrameRateString}:d={duration.TotalSeconds}", |
|
|
|
|
|
|
|
ffmpegVideoStream); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var hlsOptions = GetHlsOptions(channel, now, ptsOffset, audioFormat); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var ffmpegState = new FFmpegState( |
|
|
|
|
|
|
|
channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel, |
|
|
|
|
|
|
|
HardwareAccelerationMode.None, // no hw accel decode since errors loop
|
|
|
|
|
|
|
|
hwAccel, |
|
|
|
|
|
|
|
VaapiDriverName(hwAccel, channel.FFmpegProfile.VaapiDriver), |
|
|
|
|
|
|
|
VaapiDeviceName(hwAccel, channel.FFmpegProfile.VaapiDevice), |
|
|
|
|
|
|
|
playbackSettings.StreamSeek, |
|
|
|
|
|
|
|
duration, |
|
|
|
|
|
|
|
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect, |
|
|
|
|
|
|
|
"ErsatzTV", |
|
|
|
|
|
|
|
channel.Name, |
|
|
|
|
|
|
|
None, |
|
|
|
|
|
|
|
None, |
|
|
|
|
|
|
|
None, |
|
|
|
|
|
|
|
hlsOptions.OutputFormat, |
|
|
|
|
|
|
|
hlsOptions.HlsPlaylistPath, |
|
|
|
|
|
|
|
hlsOptions.HlsSegmentTemplate, |
|
|
|
|
|
|
|
hlsOptions.HlsInitTemplate, |
|
|
|
|
|
|
|
hlsOptions.HlsSegmentOptions, |
|
|
|
|
|
|
|
ptsOffset, |
|
|
|
|
|
|
|
Option<int>.None, |
|
|
|
|
|
|
|
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames), |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
GetTonemapAlgorithm(playbackSettings), |
|
|
|
|
|
|
|
channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var audioInputFile = new NullAudioInputFile(audioState); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder( |
|
|
|
|
|
|
|
hwAccel, |
|
|
|
|
|
|
|
videoInputFile, |
|
|
|
|
|
|
|
audioInputFile, |
|
|
|
|
|
|
|
Option<WatermarkInputFile>.None, |
|
|
|
|
|
|
|
Option<SubtitleInputFile>.None, |
|
|
|
|
|
|
|
Option<ConcatInputFile>.None, |
|
|
|
|
|
|
|
Option<GraphicsEngineInput>.None, |
|
|
|
|
|
|
|
VaapiDisplayName(hwAccel, channel.FFmpegProfile.VaapiDisplay), |
|
|
|
|
|
|
|
VaapiDriverName(hwAccel, channel.FFmpegProfile.VaapiDriver), |
|
|
|
|
|
|
|
VaapiDeviceName(hwAccel, channel.FFmpegProfile.VaapiDevice), |
|
|
|
|
|
|
|
channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel |
|
|
|
|
|
|
|
? FileSystemLayout.TranscodeTroubleshootingFolder |
|
|
|
|
|
|
|
: FileSystemLayout.FFmpegReportsFolder, |
|
|
|
|
|
|
|
FileSystemLayout.FontsCacheFolder, |
|
|
|
|
|
|
|
ffmpegPath); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, None, pipeline); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public async Task<Command> ConcatChannel( |
|
|
|
public async Task<Command> ConcatChannel( |
|
|
|
string ffmpegPath, |
|
|
|
string ffmpegPath, |
|
|
|
bool saveReports, |
|
|
|
bool saveReports, |
|
|
|
@ -1260,4 +1341,67 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService |
|
|
|
|
|
|
|
|
|
|
|
return false; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static HlsOptions GetHlsOptions(Channel channel, DateTimeOffset now, TimeSpan ptsOffset, string audioFormat) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
OutputFormatKind outputFormat = OutputFormatKind.MpegTs; |
|
|
|
|
|
|
|
switch (channel.StreamingMode) |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
case StreamingMode.HttpLiveStreamingSegmenter: |
|
|
|
|
|
|
|
outputFormat = OutputFormatKind.Hls; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<string> hlsPlaylistPath = outputFormat is OutputFormatKind.Hls or OutputFormatKind.HlsMp4 |
|
|
|
|
|
|
|
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8") |
|
|
|
|
|
|
|
: Option<string>.None; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
long nowSeconds = now.ToUnixTimeSeconds(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<string> hlsSegmentTemplate = outputFormat switch |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
OutputFormatKind.Hls => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"), |
|
|
|
|
|
|
|
OutputFormatKind.HlsMp4 => Path.Combine( |
|
|
|
|
|
|
|
FileSystemLayout.TranscodeFolder, |
|
|
|
|
|
|
|
channel.Number, |
|
|
|
|
|
|
|
$"live_{nowSeconds}_%06d.m4s"), |
|
|
|
|
|
|
|
_ => Option<string>.None |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Option<string> hlsInitTemplate = outputFormat switch |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
OutputFormatKind.HlsMp4 => $"{nowSeconds}_init.mp4", |
|
|
|
|
|
|
|
_ => 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}"; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new HlsOptions(outputFormat, hlsPlaylistPath, hlsSegmentTemplate, hlsInitTemplate, hlsSegmentOptions); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private sealed record HlsOptions( |
|
|
|
|
|
|
|
OutputFormatKind OutputFormat, |
|
|
|
|
|
|
|
Option<string> HlsPlaylistPath, |
|
|
|
|
|
|
|
Option<string> HlsSegmentTemplate, |
|
|
|
|
|
|
|
Option<string> HlsInitTemplate, |
|
|
|
|
|
|
|
Option<string> HlsSegmentOptions); |
|
|
|
} |
|
|
|
} |
|
|
|
|