2
0
Эх сурвалжийг харах

Merge pull request #1445 from MediaBrowser/dev

add recording encoding setting
Luke 9 жил өмнө
parent
commit
a0fe620456

+ 1 - 0
MediaBrowser.Model/LiveTv/LiveTvOptions.cs

@@ -8,6 +8,7 @@ namespace MediaBrowser.Model.LiveTv
         public bool EnableMovieProviders { get; set; }
         public string RecordingPath { get; set; }
         public bool EnableAutoOrganize { get; set; }
+        public bool EnableRecordingEncoding { get; set; }
 
         public List<TunerHostInfo> TunerHosts { get; set; }
         public List<ListingsProviderInfo> ListingProviders { get; set; }

+ 50 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs

@@ -0,0 +1,50 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Logging;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+    public class DirectRecorder : IRecorder
+    {
+        private readonly ILogger _logger;
+        private readonly IHttpClient _httpClient;
+        private readonly IFileSystem _fileSystem;
+
+        public DirectRecorder(ILogger logger, IHttpClient httpClient, IFileSystem fileSystem)
+        {
+            _logger = logger;
+            _httpClient = httpClient;
+            _fileSystem = fileSystem;
+        }
+
+        public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken)
+        {
+            var httpRequestOptions = new HttpRequestOptions()
+            {
+                Url = mediaSource.Path
+            };
+
+            httpRequestOptions.BufferContent = false;
+
+            using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
+            {
+                _logger.Info("Opened recording stream from tuner provider");
+
+                using (var output = _fileSystem.GetFileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
+                {
+                    onStarted();
+
+                    _logger.Info("Copying recording stream to file stream");
+
+                    await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+                }
+            }
+        }
+    }
+}

+ 27 - 17
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -764,10 +764,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
                     var duration = recordingEndDate - DateTime.UtcNow;
 
-                    HttpRequestOptions httpRequestOptions = new HttpRequestOptions()
+                    var recorder = await GetRecorder().ConfigureAwait(false);
+
+                    if (recorder is EncodedRecorder)
                     {
-                        Url = mediaStreamInfo.Path
-                    };
+                        recordPath = Path.ChangeExtension(recordPath, ".mp4");
+                    }
 
                     recording.Path = recordPath;
                     recording.Status = RecordingStatus.InProgress;
@@ -776,26 +778,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
 
                     _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
 
-                    httpRequestOptions.BufferContent = false;
                     var durationToken = new CancellationTokenSource(duration);
                     var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
-                    httpRequestOptions.CancellationToken = linkedToken;
+
                     _logger.Info("Writing file to path: " + recordPath);
                     _logger.Info("Opening recording stream from tuner provider");
-                    using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
-                    {
-                        _logger.Info("Opened recording stream from tuner provider");
 
-                        using (var output = _fileSystem.GetFileStream(recordPath, FileMode.Create, FileAccess.Write, FileShare.Read))
-                        {
-                            result.Item2.Release();
-                            isResourceOpen = false;
+                    Action onStarted = () =>
+                    {
+                        result.Item2.Release();
+                        isResourceOpen = false;
+                    };
 
-                            _logger.Info("Copying recording stream to file stream");
-
-                            await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, linkedToken).ConfigureAwait(false);
-                        }
-                    }
+                    await recorder.Record(mediaStreamInfo, recordPath, onStarted, linkedToken).ConfigureAwait(false);
 
                     recording.Status = RecordingStatus.Completed;
                     _logger.Info("Recording completed");
@@ -846,6 +841,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
+        private async Task<IRecorder> GetRecorder()
+        {
+            if (GetConfiguration().EnableRecordingEncoding)
+            {
+                var regInfo = await _security.GetRegistrationStatus("embytvseriesrecordings").ConfigureAwait(false);
+
+                if (regInfo.IsValid)
+                {
+                    return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
+                }
+            }
+
+            return new DirectRecorder(_logger, _httpClient, _fileSystem);
+        }
+
         private async void OnSuccessfulRecording(RecordingInfo recording)
         {
             if (GetConfiguration().EnableAutoOrganize)

+ 255 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -0,0 +1,255 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using CommonIO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+    public class EncodedRecorder : IRecorder
+    {
+        private readonly ILogger _logger;
+        private readonly IFileSystem _fileSystem;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly IApplicationPaths _appPaths;
+        private bool _hasExited;
+        private Stream _logFileStream;
+        private string _targetPath;
+        private Process _process;
+        private readonly IJsonSerializer _json;
+
+        public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IApplicationPaths appPaths, IJsonSerializer json)
+        {
+            _logger = logger;
+            _fileSystem = fileSystem;
+            _mediaEncoder = mediaEncoder;
+            _appPaths = appPaths;
+            _json = json;
+        }
+
+        public async Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken)
+        {
+            _targetPath = targetFile;
+            _fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile));
+
+            var process = new Process
+            {
+                StartInfo = new ProcessStartInfo
+                {
+                    CreateNoWindow = true,
+                    UseShellExecute = false,
+
+                    // Must consume both stdout and stderr or deadlocks may occur
+                    RedirectStandardOutput = true,
+                    RedirectStandardError = true,
+                    RedirectStandardInput = true,
+
+                    FileName = _mediaEncoder.EncoderPath,
+                    Arguments = GetCommandLineArgs(mediaSource, targetFile),
+
+                    WindowStyle = ProcessWindowStyle.Hidden,
+                    ErrorDialog = false
+                },
+
+                EnableRaisingEvents = true
+            };
+
+            _process = process;
+
+            var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
+            _logger.Info(commandLineLogMessage);
+
+            var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
+            _fileSystem.CreateDirectory(Path.GetDirectoryName(logFilePath));
+
+            // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+            _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
+
+            var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
+            await _logFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false);
+
+            process.Exited += (sender, args) => OnFfMpegProcessExited(process);
+
+            process.Start();
+
+            cancellationToken.Register(Stop);
+
+            // MUST read both stdout and stderr asynchronously or a deadlock may occurr
+            process.BeginOutputReadLine();
+
+            // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
+            StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
+
+            // Wait for the file to exist before proceeeding
+			while (!_hasExited)
+            {
+                await Task.Delay(100, cancellationToken).ConfigureAwait(false);
+            }
+        }
+
+        private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile)
+        {
+            string videoArgs;
+            if (EncodeVideo(mediaSource))
+            {
+                var maxBitrate = 25000000;
+                videoArgs = string.Format(
+                        "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync vfr -profile:v high -level 41",
+                        GetOutputSizeParam(),
+                        maxBitrate.ToString(CultureInfo.InvariantCulture));
+            }
+            else
+            {
+                videoArgs = "-codec:v:0 copy";
+            }
+
+            var commandLineArgs = "-fflags +genpts -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
+
+            if (mediaSource.ReadAtNativeFramerate)
+            {
+                commandLineArgs = "-re " + commandLineArgs;
+            }
+
+            commandLineArgs = string.Format(commandLineArgs, mediaSource.Path, targetFile, videoArgs, GetAudioArgs(mediaSource));
+
+            return commandLineArgs;
+        }
+
+        private string GetAudioArgs(MediaSourceInfo mediaSource)
+        {
+            var copyAudio = new[] {"aac", "mp3"};
+            var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
+            if (mediaStreams.Any(i => i.Type == MediaStreamType.Audio && copyAudio.Contains(i.Codec, StringComparer.OrdinalIgnoreCase)))
+            {
+                return "-codec:a:0 copy";
+            }
+
+            var audioChannels = 2;
+            var audioStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
+            if (audioStream != null)
+            {
+                audioChannels = audioStream.Channels ?? audioChannels;
+            }
+            return "-codec:a:0 aac -strict experimental -ab 320000 -ac " + audioChannels.ToString(CultureInfo.InvariantCulture);
+        }
+
+        private bool EncodeVideo(MediaSourceInfo mediaSource)
+        {
+            var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>();
+            return !mediaStreams.Any(i => i.Type == MediaStreamType.Video && string.Equals(i.Codec, "h264", StringComparison.OrdinalIgnoreCase));
+        }
+
+        protected string GetOutputSizeParam()
+        {
+            var filters = new List<string>();
+
+            filters.Add("yadif=0:-1:0");
+
+            var output = string.Empty;
+
+            if (filters.Count > 0)
+            {
+                output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray()));
+            }
+
+            return output;
+        }
+
+        private void Stop()
+        {
+            if (!_hasExited)
+            {
+                try
+                {
+                    _logger.Info("Killing ffmpeg recording process for {0}", _targetPath);
+
+                    //process.Kill();
+                    _process.StandardInput.WriteLine("q");
+
+                    // Need to wait because killing is asynchronous
+                    _process.WaitForExit(5000);
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error killing transcoding job for {0}", ex, _targetPath);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Processes the exited.
+        /// </summary>
+        /// <param name="process">The process.</param>
+        private void OnFfMpegProcessExited(Process process)
+        {
+            _hasExited = true;
+
+            _logger.Debug("Disposing stream resources");
+            DisposeLogStream();
+
+            try
+            {
+                _logger.Info("FFMpeg exited with code {0}", process.ExitCode);
+            }
+            catch
+            {
+                _logger.Error("FFMpeg exited with an error.");
+            }
+        }
+
+        private void DisposeLogStream()
+        {
+            if (_logFileStream != null)
+            {
+                try
+                {
+                    _logFileStream.Dispose();
+                }
+                catch (Exception ex)
+                {
+                    _logger.ErrorException("Error disposing log stream", ex);
+                }
+
+                _logFileStream = null;
+            }
+        }
+
+        private async void StartStreamingLog(Stream source, Stream target)
+        {
+            try
+            {
+                using (var reader = new StreamReader(source))
+                {
+                    while (!reader.EndOfStream)
+                    {
+                        var line = await reader.ReadLineAsync().ConfigureAwait(false);
+
+                        var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
+
+                        await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
+                        await target.FlushAsync().ConfigureAwait(false);
+                    }
+                }
+            }
+            catch (ObjectDisposedException)
+            {
+                // Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
+            }
+            catch (Exception ex)
+            {
+                _logger.ErrorException("Error reading ffmpeg log", ex);
+            }
+        }
+    }
+}

+ 12 - 0
MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+
+namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
+{
+    public interface IRecorder
+    {
+        Task Record(MediaSourceInfo mediaSource, string targetFile, Action onStarted, CancellationToken cancellationToken);
+    }
+}

+ 3 - 0
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -215,8 +215,11 @@
     <Compile Include="Library\Validators\StudiosValidator.cs" />
     <Compile Include="Library\Validators\YearsPostScanTask.cs" />
     <Compile Include="LiveTv\ChannelImageProvider.cs" />
+    <Compile Include="LiveTv\EmbyTV\DirectRecorder.cs" />
     <Compile Include="LiveTv\EmbyTV\EmbyTV.cs" />
+    <Compile Include="LiveTv\EmbyTV\EncodedRecorder.cs" />
     <Compile Include="LiveTv\EmbyTV\EntryPoint.cs" />
+    <Compile Include="LiveTv\EmbyTV\IRecorder.cs" />
     <Compile Include="LiveTv\EmbyTV\ItemDataProvider.cs" />
     <Compile Include="LiveTv\EmbyTV\RecordingHelper.cs" />
     <Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" />