mirror of https://github.com/ErsatzTV/ErsatzTV.git
14 changed files with 242 additions and 2 deletions
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using YamlDotNet.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public class MpegTsScript |
||||
{ |
||||
[YamlMember(Alias = "name", ApplyNamingConventions = false)] |
||||
public string Name { get; set; } |
||||
|
||||
[YamlMember(Alias = "linux_script", ApplyNamingConventions = false)] |
||||
public string LinuxScript { get; set; } |
||||
|
||||
[YamlMember(Alias = "windows_script", ApplyNamingConventions = false)] |
||||
public string WindowsScript { get; set; } |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
using CliWrap; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg; |
||||
|
||||
public interface IMpegTsScriptService |
||||
{ |
||||
Task RefreshScripts(); |
||||
|
||||
List<MpegTsScript> GetScripts(); |
||||
|
||||
Task<Option<Command>> Execute(MpegTsScript script, Channel channel, string hlsUrl, string ffmpegPath); |
||||
} |
||||
@ -0,0 +1,135 @@
@@ -0,0 +1,135 @@
|
||||
using System.Collections.Concurrent; |
||||
using System.Runtime.InteropServices; |
||||
using CliWrap; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using Microsoft.Extensions.Logging; |
||||
using Scriban; |
||||
using Scriban.Runtime; |
||||
using YamlDotNet.Serialization; |
||||
using YamlDotNet.Serialization.NamingConventions; |
||||
|
||||
namespace ErsatzTV.Infrastructure.FFmpeg; |
||||
|
||||
public class MpegTsScriptService( |
||||
ILocalFileSystem localFileSystem, |
||||
ITempFilePool tempFilePool, |
||||
ILogger<MpegTsScriptService> logger) : IMpegTsScriptService |
||||
{ |
||||
private static readonly ConcurrentDictionary<string, MpegTsScript> Scripts = new(); |
||||
|
||||
public async Task RefreshScripts() |
||||
{ |
||||
foreach (string folder in localFileSystem.ListSubdirectories(FileSystemLayout.MpegTsScriptsFolder)) |
||||
{ |
||||
string definition = Path.Combine(folder, "mpegts.yml"); |
||||
if (!Scripts.ContainsKey(folder) && localFileSystem.FileExists(definition)) |
||||
{ |
||||
Option<MpegTsScript> maybeScript = FromYaml(await localFileSystem.ReadAllText(definition)); |
||||
foreach (var script in maybeScript) |
||||
{ |
||||
Scripts[folder] = script; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
public List<MpegTsScript> GetScripts() => Scripts.Values.ToList(); |
||||
|
||||
public async Task<Option<Command>> Execute(MpegTsScript script, Channel channel, string hlsUrl, string ffmpegPath) |
||||
{ |
||||
string scriptFolder = string.Empty; |
||||
foreach (KeyValuePair<string, MpegTsScript> kvp in Scripts.Where(kvp => kvp.Value == script)) |
||||
{ |
||||
scriptFolder = kvp.Key; |
||||
} |
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
{ |
||||
string scriptInput = Path.Combine(scriptFolder, script.WindowsScript); |
||||
if (File.Exists(scriptInput)) |
||||
{ |
||||
Option<string> maybeScript = await GetTemplatedScript( |
||||
scriptInput, |
||||
hlsUrl, |
||||
channel.Name, |
||||
ffmpegPath); |
||||
foreach (string finalScript in maybeScript) |
||||
{ |
||||
string fileName = tempFilePool.GetNextTempFile(TempFileCategory.MpegTsScript); |
||||
await File.WriteAllTextAsync(fileName, finalScript); |
||||
return Cli.Wrap("pwsh").WithArguments(["-File", fileName]); |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
string scriptInput = Path.Combine(scriptFolder, script.LinuxScript); |
||||
if (File.Exists(scriptInput)) |
||||
{ |
||||
Option<string> maybeScript = await GetTemplatedScript( |
||||
scriptInput, |
||||
hlsUrl, |
||||
channel.Name, |
||||
ffmpegPath); |
||||
foreach (string finalScript in maybeScript) |
||||
{ |
||||
string fileName = tempFilePool.GetNextTempFile(TempFileCategory.MpegTsScript); |
||||
await File.WriteAllTextAsync(fileName, finalScript); |
||||
return Cli.Wrap("bash").WithArguments([fileName]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return []; |
||||
} |
||||
|
||||
private async Task<Option<string>> GetTemplatedScript( |
||||
string fileName, |
||||
string hlsUrl, |
||||
string channelName, |
||||
string ffmpegPath) |
||||
{ |
||||
string script = await localFileSystem.ReadAllText(fileName); |
||||
try |
||||
{ |
||||
var data = new Dictionary<string, string> |
||||
{ |
||||
["HlsUrl"] = hlsUrl, |
||||
["ChannelName"] = channelName, |
||||
["FFmpegPath"] = ffmpegPath |
||||
}; |
||||
|
||||
var scriptObject = new ScriptObject(); |
||||
scriptObject.Import(data, renamer: member => member.Name); |
||||
var context = new TemplateContext { MemberRenamer = member => member.Name }; |
||||
context.PushGlobal(scriptObject); |
||||
return await Template.Parse(script).RenderAsync(context); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
logger.LogWarning(ex, "Failed to render mpegts script as scriban template"); |
||||
return Option<string>.None; |
||||
} |
||||
} |
||||
|
||||
private Option<MpegTsScript> FromYaml(string yaml) |
||||
{ |
||||
try |
||||
{ |
||||
IDeserializer deserializer = new DeserializerBuilder() |
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance) |
||||
.Build(); |
||||
|
||||
return deserializer.Deserialize<MpegTsScript>(yaml); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
logger.LogWarning(ex, "Failed to load mpegts script YAML definition"); |
||||
return Option<MpegTsScript>.None; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
name: "Default" |
||||
linux_script: "run.sh" |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
#! /bin/sh |
||||
|
||||
"{{ FFmpegPath }}" -nostdin -threads 1 -hide_banner -loglevel error -nostats -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i "{{ HlsUrl }}" -map 0 -c copy -metadata service_provider="ErsatzTV" -metadata service_name="{{ ChannelName }}" -f mpegts pipe:1 |
||||
Loading…
Reference in new issue