Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

298 lines
11 KiB

using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg;
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.OutputFormat;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
namespace ErsatzTV.Core.FFmpeg;
public class FFmpegLibraryProcessService : IFFmpegProcessService
{
private readonly FFmpegProcessService _ffmpegProcessService;
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
private readonly ILogger<FFmpegLibraryProcessService> _logger;
public FFmpegLibraryProcessService(
FFmpegProcessService ffmpegProcessService,
FFmpegPlaybackSettingsCalculator playbackSettingsCalculator,
IFFmpegStreamSelector ffmpegStreamSelector,
ILogger<FFmpegLibraryProcessService> logger)
{
_ffmpegProcessService = ffmpegProcessService;
_playbackSettingsCalculator = playbackSettingsCalculator;
_ffmpegStreamSelector = ffmpegStreamSelector;
_logger = logger;
}
public async Task<Process> ForPlayoutItem(
string ffmpegPath,
bool saveReports,
Channel channel,
MediaVersion videoVersion,
MediaVersion audioVersion,
string videoPath,
string audioPath,
DateTimeOffset start,
DateTimeOffset finish,
DateTimeOffset now,
Option<ChannelWatermark> globalWatermark,
VaapiDriver vaapiDriver,
string vaapiDevice,
bool hlsRealtime,
FillerKind fillerKind,
TimeSpan inPoint,
TimeSpan outPoint,
long ptsOffset,
Option<int> targetFramerate)
{
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
channel.StreamingMode,
channel.FFmpegProfile,
videoVersion,
videoStream,
maybeAudioStream,
start,
now,
inPoint,
outPoint,
hlsRealtime,
targetFramerate);
var ffmpegVideoStream = new VideoStream(
videoStream.Index,
videoStream.Codec,
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.RFrameRate);
var inputFiles = new List<InputFile>
{
new(videoVersion.MediaFiles.Head().Path, new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegVideoStream })
};
foreach (MediaStream audioStream in maybeAudioStream)
{
inputFiles.Head().Streams
.Add(new AudioStream(audioStream.Index, audioStream.Codec, audioStream.Channels));
}
// TODO: need formats for these codecs
string videoFormat = playbackSettings.VideoCodec switch
{
"libx265" or "hevc_nvenc" or "hevc_qsv" or "hevc_vaapi" or "hevc_videotoolbox" => VideoFormat.Hevc,
"libx264" or "h264_nvenc" or "h264_qsv" or "h264_vaapi" or "h264_videotoolbox" => VideoFormat.H264,
"mpeg2video" => VideoFormat.Mpeg2Video,
"copy" => VideoFormat.Copy,
_ => throw new ArgumentOutOfRangeException($"unexpected video codec {playbackSettings.VideoCodec}")
};
HardwareAccelerationMode hwAccel = playbackSettings.HardwareAcceleration switch
{
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
_ => HardwareAccelerationMode.None
};
OutputFormatKind outputFormat = channel.StreamingMode == StreamingMode.HttpLiveStreamingSegmenter
? OutputFormatKind.Hls
: OutputFormatKind.MpegTs;
Option<string> hlsPlaylistPath = outputFormat == OutputFormatKind.Hls
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8")
: Option<string>.None;
Option<string> hlsSegmentTemplate = outputFormat == OutputFormatKind.Hls
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
: Option<string>.None;
var desiredState = new FrameState(
saveReports,
hwAccel,
VaapiDriverName(hwAccel, vaapiDriver),
VaapiDeviceName(hwAccel, vaapiDevice),
playbackSettings.RealtimeOutput,
false,
playbackSettings.StreamSeek,
finish - now,
videoFormat,
ffmpegVideoStream.PixelFormat,
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height))
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)),
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
playbackSettings.VideoTrackTimeScale,
playbackSettings.Deinterlace,
playbackSettings.AudioCodec,
playbackSettings.AudioChannels,
playbackSettings.AudioBitrate,
playbackSettings.AudioBufferSize,
playbackSettings.AudioSampleRate,
videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None,
playbackSettings.NormalizeLoudness,
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
"ErsatzTV",
channel.Name,
maybeAudioStream.Map(s => Optional(s.Language)).Flatten(),
outputFormat,
hlsPlaylistPath,
hlsSegmentTemplate,
ptsOffset);
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
return GetProcess(ffmpegPath, inputFiles, desiredState);
}
public Task<Process> ForError(
string ffmpegPath,
Channel channel,
Option<TimeSpan> duration,
string errorMessage,
bool hlsRealtime,
long ptsOffset) =>
_ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset);
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
{
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
var desiredState = FrameState.Concat(saveReports, channel.Name, resolution);
var inputFiles = new List<InputFile>
{
new ConcatInputFile($"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}", resolution)
};
return GetProcess(ffmpegPath, inputFiles, desiredState);
}
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) =>
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host);
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) =>
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile);
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) =>
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile);
public Task<Either<BaseError, string>> GenerateSongImage(
string ffmpegPath,
Option<string> subtitleFile,
Channel channel,
Option<ChannelWatermark> globalWatermark,
MediaVersion videoVersion,
string videoPath,
bool boxBlur,
Option<string> watermarkPath,
ChannelWatermarkLocation watermarkLocation,
int horizontalMarginPercent,
int verticalMarginPercent,
int watermarkWidthPercent) =>
_ffmpegProcessService.GenerateSongImage(
ffmpegPath,
subtitleFile,
channel,
globalWatermark,
videoVersion,
videoPath,
boxBlur,
watermarkPath,
watermarkLocation,
horizontalMarginPercent,
verticalMarginPercent,
watermarkWidthPercent);
private Process GetProcess(string ffmpegPath, IList<InputFile> inputFiles, FrameState desiredState)
{
var pipelineBuilder = new PipelineBuilder(inputFiles, FileSystemLayout.FFmpegReportsFolder, _logger);
FFmpegPipeline pipeline = pipelineBuilder.Build(desiredState);
IEnumerable<string> loggedSteps = pipeline.PipelineSteps.Map(ps => ps.GetType().Name);
IEnumerable<string> loggedVideoFilters = pipeline.VideoFilterSteps.Map(vf => vf.GetType().Name);
IEnumerable<string> loggedAudioFilters = pipeline.AudioFilterSteps.Map(af => af.GetType().Name);
_logger.LogDebug(
"FFmpeg pipeline {PipelineSteps}, {AudioFilters}, {VideoFilters}",
loggedSteps,
loggedAudioFilters,
loggedVideoFilters
);
IList<EnvironmentVariable> environmentVariables = CommandGenerator.GenerateEnvironmentVariables(pipeline.PipelineSteps);
IList<string> arguments = CommandGenerator.GenerateArguments(inputFiles, pipeline.PipelineSteps);
var startInfo = new ProcessStartInfo
{
FileName = ffmpegPath,
RedirectStandardOutput = true,
RedirectStandardError = false,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
if (environmentVariables.Any())
{
_logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables);
}
foreach ((string key, string value) in environmentVariables)
{
startInfo.EnvironmentVariables[key] = value;
}
foreach (string argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
return new Process
{
StartInfo = startInfo
};
}
private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver)
{
if (accelerationMode == HardwareAccelerationMode.Vaapi)
{
switch (driver)
{
case VaapiDriver.i965:
return "i965";
case VaapiDriver.iHD:
return "iHD";
case VaapiDriver.RadeonSI:
return "radeonsi";
}
}
return Option<string>.None;
}
private static Option<string> VaapiDeviceName(HardwareAccelerationMode accelerationMode, string vaapiDevice)
{
return accelerationMode == HardwareAccelerationMode.Vaapi ? vaapiDevice : Option<string>.None;
}
}