diff --git a/src/N_m3u8DL-RE.Common/Resource/ResString.cs b/src/N_m3u8DL-RE.Common/Resource/ResString.cs index 34d2040..e5b5b09 100644 --- a/src/N_m3u8DL-RE.Common/Resource/ResString.cs +++ b/src/N_m3u8DL-RE.Common/Resource/ResString.cs @@ -68,6 +68,7 @@ namespace N_m3u8DL_RE.Common.Resource public static string cmd_useSystemProxy { get => GetText("cmd_useSystemProxy"); } public static string cmd_customProxy { get => GetText("cmd_customProxy"); } public static string cmd_liveKeepSegments { get => GetText("cmd_liveKeepSegments"); } + public static string cmd_livePipeMux { get => GetText("cmd_livePipeMux"); } public static string cmd_liveRecordLimit { get => GetText("cmd_liveRecordLimit"); } public static string cmd_taskStartAt { get => GetText("cmd_taskStartAt"); } public static string cmd_liveWaitTime { get => GetText("cmd_liveWaitTime"); } @@ -81,6 +82,8 @@ namespace N_m3u8DL_RE.Common.Resource public static string liveLimitReached { get => GetText("liveLimitReached"); } public static string saveName { get => GetText("saveName"); } public static string taskStartAt { get => GetText("taskStartAt"); } + public static string namedPipeCreated { get => GetText("namedPipeCreated"); } + public static string namedPipeMux { get => GetText("namedPipeMux"); } public static string partMerge { get => GetText("partMerge"); } public static string fetch { get => GetText("fetch"); } public static string ffmpegMerge { get => GetText("ffmpegMerge"); } diff --git a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs index 64de565..bdbae10 100644 --- a/src/N_m3u8DL-RE.Common/Resource/StaticText.cs +++ b/src/N_m3u8DL-RE.Common/Resource/StaticText.cs @@ -22,6 +22,18 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "檢測到新版本,請盡快升級!", enUS: "New version detected!" ), + ["namedPipeCreated"] = new TextContainer + ( + zhCN: "已创建命名管道:", + zhTW: "已創建命名管道:", + enUS: "Named pipe created: " + ), + ["namedPipeMux"] = new TextContainer + ( + zhCN: "通过命名管道混流到", + zhTW: "通過命名管道混流到", + enUS: "Mux with named pipe, to" + ), ["taskStartAt"] = new TextContainer ( zhCN: "程序将等待,直到:", @@ -310,6 +322,12 @@ namespace N_m3u8DL_RE.Common.Resource zhTW: "指定HLS解密IV. 可以是文件, HEX或Base64", enUS: "Set the HLS decryption iv. Can be file, HEX or Base64" ), + ["cmd_livePipeMux"] = new TextContainer + ( + zhCN: "录制直播并开启实时合并时通过管道+ffmpeg实时混流到TS文件", + zhTW: "錄製直播並開啟即時合併時通過管道+ffmpeg即時混流到TS文件", + enUS: "Real-time muxing to TS file through pipeline + ffmpeg (liveRealTimeMerge enabled)" + ), ["cmd_liveKeepSegments"] = new TextContainer ( zhCN: "录制直播并开启实时合并时依然保留分片", diff --git a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs index 86e6d10..4ab2ddb 100644 --- a/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs +++ b/src/N_m3u8DL-RE/CommandLine/CommandInvoker.cs @@ -18,7 +18,7 @@ namespace N_m3u8DL_RE.CommandLine { internal partial class CommandInvoker { - public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20221208"; + public const string VERSION_INFO = "N_m3u8DL-RE (Beta version) 20221210"; [GeneratedRegex("((best|worst)\\d*|all)")] private static partial Regex ForStrRegex(); @@ -76,6 +76,7 @@ namespace N_m3u8DL_RE.CommandLine private readonly static Option LivePerformAsVod = new(new string[] { "--live-perform-as-vod" }, description: ResString.cmd_livePerformAsVod, getDefaultValue: () => false); private readonly static Option LiveRealTimeMerge = new(new string[] { "--live-real-time-merge" }, description: ResString.cmd_liveRealTimeMerge, getDefaultValue: () => false); private readonly static Option LiveKeepSegments = new(new string[] { "--live-keep-segments" }, description: ResString.cmd_liveKeepSegments, getDefaultValue: () => true); + private readonly static Option LivePipeMux = new(new string[] { "--live-pipe-mux" }, description: ResString.cmd_livePipeMux, getDefaultValue: () => false); private readonly static Option LiveRecordLimit = new(new string[] { "--live-record-limit" }, description: ResString.cmd_liveRecordLimit, parseArgument: ParseLiveLimit) { ArgumentHelpName = "HH:mm:ss" }; private readonly static Option LiveWaitTime = new(new string[] { "--live-wait-time" }, description: ResString.cmd_liveWaitTime) { ArgumentHelpName = "SEC" }; @@ -416,6 +417,7 @@ namespace N_m3u8DL_RE.CommandLine LiveRecordLimit = bindingContext.ParseResult.GetValueForOption(LiveRecordLimit), TaskStartAt = bindingContext.ParseResult.GetValueForOption(TaskStartAt), LivePerformAsVod = bindingContext.ParseResult.GetValueForOption(LivePerformAsVod), + LivePipeMux = bindingContext.ParseResult.GetValueForOption(LivePipeMux), UseSystemProxy = bindingContext.ParseResult.GetValueForOption(UseSystemProxy), CustomProxy = bindingContext.ParseResult.GetValueForOption(CustomProxy), LiveWaitTime = bindingContext.ParseResult.GetValueForOption(LiveWaitTime), @@ -485,7 +487,7 @@ namespace N_m3u8DL_RE.CommandLine LogLevel, UILanguage, UrlProcessorArgs, Keys, KeyTextFile, DecryptionBinaryPath, UseShakaPackager, MP4RealTimeDecryption, MuxAfterDone, CustomHLSMethod, CustomHLSKey, CustomHLSIv, UseSystemProxy, CustomProxy, TaskStartAt, - LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LiveRecordLimit, LiveWaitTime, + LivePerformAsVod, LiveRealTimeMerge, LiveKeepSegments, LivePipeMux, LiveRecordLimit, LiveWaitTime, MuxImports, VideoFilter, AudioFilter, SubtitleFilter, DropVideoFilter, DropAudioFilter, DropSubtitleFilter, MoreHelp }; diff --git a/src/N_m3u8DL-RE/CommandLine/MyOption.cs b/src/N_m3u8DL-RE/CommandLine/MyOption.cs index 037d1e9..1bd62f4 100644 --- a/src/N_m3u8DL-RE/CommandLine/MyOption.cs +++ b/src/N_m3u8DL-RE/CommandLine/MyOption.cs @@ -222,5 +222,9 @@ namespace N_m3u8DL_RE.CommandLine public int? LiveWaitTime { get; set; } public bool MuxKeepFiles { get; set; } //public bool LiveWriteHLS { get; set; } = true; + /// + /// See: . + /// + public bool LivePipeMux { get; set; } } } \ No newline at end of file diff --git a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs index 8e47833..2da0ee9 100644 --- a/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs +++ b/src/N_m3u8DL-RE/DownloadManager/SimpleLiveRecordManager2.cs @@ -14,6 +14,9 @@ using N_m3u8DL_RE.Util; using Spectre.Console; using Spectre.Console.Rendering; using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; using System.Text; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; @@ -27,6 +30,7 @@ namespace N_m3u8DL_RE.DownloadManager DownloaderConfig DownloaderConfig; StreamExtractor StreamExtractor; List SelectedSteams; + ConcurrentDictionary PipeSteamNamesDic = new(); List OutputFiles = new(); DateTime NowDateTime; DateTime? PublishDateTime; @@ -183,7 +187,7 @@ namespace N_m3u8DL_RE.DownloadManager bool initDownloaded = false; //是否下载过init文件 ConcurrentDictionary FileDic = new(); List mediaInfos = new(); - FileStream? fileOutputStream = null; + Stream? fileOutputStream = null; WebVttSub currentVtt = new(); //字幕流始终维护一个实例 bool firstSub = true; task.StartTask(); @@ -537,7 +541,30 @@ namespace N_m3u8DL_RE.DownloadManager { Logger.WarnMarkUp($"{Path.GetFileName(output)} => {Path.GetFileName(output = Path.ChangeExtension(output, $"copy" + Path.GetExtension(output)))}"); } - fileOutputStream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + + if (!DownloaderConfig.MyOptions.LivePipeMux || streamSpec.MediaType == MediaType.SUBTITLES) + { + fileOutputStream = new FileStream(output, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + } + else + { + //创建管道 + output = Path.ChangeExtension(output, ".ts"); + var pipeName = $"RE_pipe_{Guid.NewGuid()}"; + fileOutputStream = PipeUtil.CreatePipe(pipeName); + Logger.InfoMarkUp($"{ResString.namedPipeCreated} [cyan]{pipeName.EscapeMarkup()}[/]"); + PipeSteamNamesDic[task.Id] = pipeName; + if (PipeSteamNamesDic.Count == SelectedSteams.Where(x => x.MediaType != MediaType.SUBTITLES).Count()) + { + var names = PipeSteamNamesDic.OrderBy(i => i.Key).Select(k => k.Value).ToArray(); + Logger.WarnMarkUp($"{ResString.namedPipeMux} [deepskyblue1]{Path.GetFileName(output).EscapeMarkup()}[/]"); + var t = PipeUtil.StartPipeMuxAsync(DownloaderConfig.MyOptions.FFmpegBinaryPath!, names, output); + } + + //Windows only + if (OperatingSystem.IsWindows()) + await (fileOutputStream as NamedPipeServerStream)!.WaitForConnectionAsync(); + } } if (streamSpec.MediaType != MediaType.SUBTITLES) @@ -613,16 +640,19 @@ namespace N_m3u8DL_RE.DownloadManager if (fileOutputStream != null) { - //记录所有文件信息 - OutputFiles.Add(new OutputFile() + if (!DownloaderConfig.MyOptions.LivePipeMux) { - Index = task.Id, - FilePath = fileOutputStream.Name, - LangCode = streamSpec.Language, - Description = streamSpec.Name, - Mediainfos = mediaInfos, - MediaType = streamSpec.MediaType, - }); + //记录所有文件信息 + OutputFiles.Add(new OutputFile() + { + Index = task.Id, + FilePath = (fileOutputStream as FileStream)!.Name, + LangCode = streamSpec.Language, + Description = streamSpec.Name, + Mediainfos = mediaInfos, + MediaType = streamSpec.MediaType, + }); + } fileOutputStream.Close(); fileOutputStream.Dispose(); } diff --git a/src/N_m3u8DL-RE/Util/PipeUtil.cs b/src/N_m3u8DL-RE/Util/PipeUtil.cs new file mode 100644 index 0000000..b4b9791 --- /dev/null +++ b/src/N_m3u8DL-RE/Util/PipeUtil.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Diagnostics; +using System.IO.Pipes; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace N_m3u8DL_RE.Util +{ + internal class PipeUtil + { + public static Stream CreatePipe(string pipeName) + { + if (OperatingSystem.IsWindows()) + { + return new NamedPipeServerStream(pipeName, PipeDirection.InOut); + } + else + { + var path = Path.Combine(Path.GetTempPath(), pipeName); + using var p = new Process(); + p.StartInfo = new ProcessStartInfo() + { + FileName = "mkfifo", + Arguments = path, + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + }; + p.Start(); + p.WaitForExit(); + Thread.Sleep(200); + return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + } + } + + public static async Task StartPipeMuxAsync(string binary, string[] pipeNames, string outputPath) + { + return await Task.Run(async () => + { + await Task.Delay(1000); + return StartPipeMux(binary, pipeNames, outputPath); + }); + } + + public static bool StartPipeMux(string binary, string[] pipeNames, string outputPath) + { + string dateString = DateTime.Now.ToString("o"); + StringBuilder command = new StringBuilder("-y -fflags +genpts -loglevel quiet "); + + foreach (var item in pipeNames) + { + if (OperatingSystem.IsWindows()) + command.Append($" -i \"\\\\.\\pipe\\{item}\" "); + else + //command.Append($" -i \"unix://{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{item}")}\" "); + command.Append($" -i \"{Path.Combine(Path.GetTempPath(), item)}\" "); + } + + for (int i = 0; i < pipeNames.Length; i++) + { + command.Append($" -map {i} "); + } + + command.Append(" -strict unofficial -c copy "); + command.Append($" -metadata date=\"{dateString}\" "); + command.Append($" -ignore_unknown -copy_unknown "); + command.Append($" -f mpegts -shortest \"{outputPath}\""); + + using var p = new Process(); + p.StartInfo = new ProcessStartInfo() + { + WorkingDirectory = Environment.CurrentDirectory, + FileName = binary, + Arguments = command.ToString(), + CreateNoWindow = true, + UseShellExecute = false + }; + p.StartInfo.Environment.Add("FFREPORT", "file=ffreport.log:level=42"); + p.Start(); + p.WaitForExit(); + + return p.ExitCode == 0; + } + } +}