| | 1 | | #nullable disable |
| | 2 | |
|
| | 3 | | #pragma warning disable CS1591 |
| | 4 | |
|
| | 5 | | using System; |
| | 6 | | using System.Globalization; |
| | 7 | | using System.IO; |
| | 8 | | using System.Text; |
| | 9 | | using System.Threading.Tasks; |
| | 10 | | using Microsoft.Extensions.Logging; |
| | 11 | |
|
| | 12 | | namespace MediaBrowser.Controller.MediaEncoding |
| | 13 | | { |
| | 14 | | public class JobLogger |
| | 15 | | { |
| | 16 | | private readonly ILogger _logger; |
| | 17 | |
|
| | 18 | | public JobLogger(ILogger logger) |
| | 19 | | { |
| 0 | 20 | | _logger = logger; |
| 0 | 21 | | } |
| | 22 | |
|
| | 23 | | public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target) |
| | 24 | | { |
| | 25 | | try |
| | 26 | | { |
| | 27 | | using (target) |
| | 28 | | using (reader) |
| | 29 | | { |
| | 30 | | while (!reader.EndOfStream && reader.BaseStream.CanRead) |
| | 31 | | { |
| | 32 | | var line = await reader.ReadLineAsync().ConfigureAwait(false); |
| | 33 | |
|
| | 34 | | ParseLogLine(line, state); |
| | 35 | |
|
| | 36 | | var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line); |
| | 37 | |
|
| | 38 | | // If ffmpeg process is closed, the state is disposed, so don't write to target in that case |
| | 39 | | if (!target.CanWrite) |
| | 40 | | { |
| | 41 | | break; |
| | 42 | | } |
| | 43 | |
|
| | 44 | | await target.WriteAsync(bytes).ConfigureAwait(false); |
| | 45 | |
|
| | 46 | | // Check again, the stream could have been closed |
| | 47 | | if (!target.CanWrite) |
| | 48 | | { |
| | 49 | | break; |
| | 50 | | } |
| | 51 | |
|
| | 52 | | await target.FlushAsync().ConfigureAwait(false); |
| | 53 | | } |
| | 54 | | } |
| | 55 | | } |
| | 56 | | catch (Exception ex) |
| | 57 | | { |
| | 58 | | _logger.LogError(ex, "Error reading ffmpeg log"); |
| | 59 | | } |
| | 60 | | } |
| | 61 | |
|
| | 62 | | private void ParseLogLine(string line, EncodingJobInfo state) |
| | 63 | | { |
| 0 | 64 | | float? framerate = null; |
| 0 | 65 | | double? percent = null; |
| 0 | 66 | | TimeSpan? transcodingPosition = null; |
| 0 | 67 | | long? bytesTranscoded = null; |
| 0 | 68 | | int? bitRate = null; |
| | 69 | |
|
| 0 | 70 | | var parts = line.Split(' '); |
| | 71 | |
|
| 0 | 72 | | var totalMs = state.RunTimeTicks.HasValue |
| 0 | 73 | | ? TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds |
| 0 | 74 | | : 0; |
| | 75 | |
|
| 0 | 76 | | var startMs = state.BaseRequest.StartTimeTicks.HasValue |
| 0 | 77 | | ? TimeSpan.FromTicks(state.BaseRequest.StartTimeTicks.Value).TotalMilliseconds |
| 0 | 78 | | : 0; |
| | 79 | |
|
| 0 | 80 | | for (var i = 0; i < parts.Length; i++) |
| | 81 | | { |
| 0 | 82 | | var part = parts[i]; |
| | 83 | |
|
| 0 | 84 | | if (string.Equals(part, "fps=", StringComparison.OrdinalIgnoreCase) && |
| 0 | 85 | | (i + 1 < parts.Length)) |
| | 86 | | { |
| 0 | 87 | | var rate = parts[i + 1]; |
| | 88 | |
|
| 0 | 89 | | if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) |
| | 90 | | { |
| 0 | 91 | | framerate = val; |
| | 92 | | } |
| | 93 | | } |
| 0 | 94 | | else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase)) |
| | 95 | | { |
| 0 | 96 | | var rate = part.Split('=', 2)[^1]; |
| | 97 | |
|
| 0 | 98 | | if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) |
| | 99 | | { |
| 0 | 100 | | framerate = val; |
| | 101 | | } |
| | 102 | | } |
| 0 | 103 | | else if (state.RunTimeTicks.HasValue && |
| 0 | 104 | | part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) |
| | 105 | | { |
| 0 | 106 | | var time = part.Split('=', 2)[^1]; |
| | 107 | |
|
| 0 | 108 | | if (TimeSpan.TryParse(time, CultureInfo.InvariantCulture, out var val)) |
| | 109 | | { |
| 0 | 110 | | var currentMs = startMs + val.TotalMilliseconds; |
| | 111 | |
|
| 0 | 112 | | percent = 100.0 * currentMs / totalMs; |
| | 113 | |
|
| 0 | 114 | | transcodingPosition = TimeSpan.FromMilliseconds(currentMs); |
| | 115 | | } |
| | 116 | | } |
| 0 | 117 | | else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) |
| | 118 | | { |
| 0 | 119 | | var size = part.Split('=', 2)[^1]; |
| | 120 | |
|
| 0 | 121 | | int? scale = null; |
| 0 | 122 | | if (size.Contains("kb", StringComparison.OrdinalIgnoreCase)) |
| | 123 | | { |
| 0 | 124 | | scale = 1024; |
| 0 | 125 | | size = size.Replace("kb", string.Empty, StringComparison.OrdinalIgnoreCase); |
| | 126 | | } |
| | 127 | |
|
| 0 | 128 | | if (scale.HasValue) |
| | 129 | | { |
| 0 | 130 | | if (long.TryParse(size, CultureInfo.InvariantCulture, out var val)) |
| | 131 | | { |
| 0 | 132 | | bytesTranscoded = val * scale.Value; |
| | 133 | | } |
| | 134 | | } |
| | 135 | | } |
| 0 | 136 | | else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) |
| | 137 | | { |
| 0 | 138 | | var rate = part.Split('=', 2)[^1]; |
| | 139 | |
|
| 0 | 140 | | int? scale = null; |
| 0 | 141 | | if (rate.Contains("kbits/s", StringComparison.OrdinalIgnoreCase)) |
| | 142 | | { |
| 0 | 143 | | scale = 1024; |
| 0 | 144 | | rate = rate.Replace("kbits/s", string.Empty, StringComparison.OrdinalIgnoreCase); |
| | 145 | | } |
| | 146 | |
|
| 0 | 147 | | if (scale.HasValue) |
| | 148 | | { |
| 0 | 149 | | if (float.TryParse(rate, CultureInfo.InvariantCulture, out var val)) |
| | 150 | | { |
| 0 | 151 | | bitRate = (int)Math.Ceiling(val * scale.Value); |
| | 152 | | } |
| | 153 | | } |
| | 154 | | } |
| | 155 | | } |
| | 156 | |
|
| 0 | 157 | | if (framerate.HasValue || percent.HasValue) |
| | 158 | | { |
| 0 | 159 | | state.ReportTranscodingProgress(transcodingPosition, framerate, percent, bytesTranscoded, bitRate); |
| | 160 | | } |
| 0 | 161 | | } |
| | 162 | | } |
| | 163 | | } |