diff --git a/MediaBrowser.Api/ApiService.cs b/MediaBrowser.Api/ApiService.cs index 382d236ae7..5945bf25ad 100644 --- a/MediaBrowser.Api/ApiService.cs +++ b/MediaBrowser.Api/ApiService.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Serialization; +using MediaBrowser.Api.Transcoding; using MediaBrowser.Controller; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Users; namespace MediaBrowser.Api { @@ -13,6 +12,44 @@ namespace MediaBrowser.Api /// public static class ApiService { + /// + /// Holds the list of active transcoding jobs + /// + private static List CurrentTranscodingJobs = new List(); + + /// + /// Finds an active transcoding job + /// + public static TranscodingJob GetTranscodingJob(string outputPath) + { + lock (CurrentTranscodingJobs) + { + return CurrentTranscodingJobs.FirstOrDefault(j => j.OutputFile.Equals(outputPath, StringComparison.OrdinalIgnoreCase)); + } + } + + /// + /// Removes a transcoding job from the active list + /// + public static void RemoveTranscodingJob(TranscodingJob job) + { + lock (CurrentTranscodingJobs) + { + CurrentTranscodingJobs.Remove(job); + } + } + + /// + /// Adds a transcoding job to the active list + /// + public static void AddTranscodingJob(TranscodingJob job) + { + lock (CurrentTranscodingJobs) + { + CurrentTranscodingJobs.Add(job); + } + } + public static BaseItem GetItemById(string id) { Guid guid = string.IsNullOrEmpty(id) ? Guid.Empty : new Guid(id); diff --git a/MediaBrowser.Api/HttpHandlers/AudioHandler.cs b/MediaBrowser.Api/HttpHandlers/AudioHandler.cs index 8ebaf04bcd..76a48308b9 100644 --- a/MediaBrowser.Api/HttpHandlers/AudioHandler.cs +++ b/MediaBrowser.Api/HttpHandlers/AudioHandler.cs @@ -1,4 +1,9 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using MediaBrowser.Api.Transcoding; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net.Handlers; using MediaBrowser.Controller; using MediaBrowser.Model.Entities; @@ -7,11 +12,11 @@ namespace MediaBrowser.Api.HttpHandlers { public class AudioHandler : StaticFileHandler { - private BaseItem _LibraryItem; + private Audio _LibraryItem; /// /// Gets the library item that will be played, if any /// - private BaseItem LibraryItem + private Audio LibraryItem { get { @@ -21,7 +26,7 @@ namespace MediaBrowser.Api.HttpHandlers if (!string.IsNullOrEmpty(id)) { - _LibraryItem = Kernel.Instance.GetItemById(Guid.Parse(id)); + _LibraryItem = Kernel.Instance.GetItemById(Guid.Parse(id)) as Audio; } } @@ -33,13 +38,282 @@ namespace MediaBrowser.Api.HttpHandlers { get { - if (LibraryItem != null) + return TranscodedPath; + } + } + + private string _TranscodedPath; + /// + /// Gets the library item that will be played, if any + /// + private string TranscodedPath + { + get + { + if (_TranscodedPath == null) + { + string originalMediaPath = LibraryItem == null ? base.Path : LibraryItem.Path; + + if (!RequiresTranscoding()) + { + _TranscodedPath = originalMediaPath; + } + else + { + string outputPath = GetOutputFilePath(originalMediaPath); + + // Find the job in the list + TranscodingJob job = ApiService.GetTranscodingJob(outputPath); + + if (job == null && !File.Exists(outputPath)) + { + job = GetNewTranscodingJob(originalMediaPath, outputPath); + job.Start(); + } + + if (job != null) + { + job.WaitForExit(); + } + + _TranscodedPath = outputPath; + } + } + + return _TranscodedPath; + } + } + + public string AudioFormat + { + get + { + string val = QueryString["audiobitrate"]; + + if (string.IsNullOrEmpty(val)) + { + return "mp3"; + } + + return val; + } + } + + public int? AudioBitRate + { + get + { + string val = QueryString["audiobitrate"]; + + if (string.IsNullOrEmpty(val)) + { + return null; + } + + return int.Parse(val); + } + } + + public int? NumAudioChannels + { + get + { + string val = QueryString["audiochannels"]; + + if (string.IsNullOrEmpty(val)) + { + return null; + } + + return int.Parse(val); + } + } + + public int? AudioSampleRate + { + get + { + string val = QueryString["audiosamplerate"]; + + if (string.IsNullOrEmpty(val)) { - return LibraryItem.Path; + return 44100; } - return base.Path; + return int.Parse(val); } } + + private static string _StreamsDirectory = null; + /// + /// Gets the folder path to where transcodes will be cached + /// + public static string StreamsDirectory + { + get + { + if (_StreamsDirectory == null) + { + _StreamsDirectory = System.IO.Path.Combine(ApplicationPaths.ProgramDataPath, "streams"); + + if (!Directory.Exists(_StreamsDirectory)) + { + Directory.CreateDirectory(_StreamsDirectory); + } + } + + return _StreamsDirectory; + } + } + + private static string _FFMpegDirectory = null; + /// + /// Gets the folder path to ffmpeg + /// + public static string FFMpegDirectory + { + get + { + if (_FFMpegDirectory == null) + { + _FFMpegDirectory = System.IO.Path.Combine(ApplicationPaths.ProgramDataPath, "ffmpeg"); + + if (!Directory.Exists(_FFMpegDirectory)) + { + Directory.CreateDirectory(_FFMpegDirectory); + + // Extract ffmpeg + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MediaBrowser.Api.ffmpeg.ffmpeg.exe")) + { + using (FileStream fileStream = new FileStream(FFMpegPath, FileMode.Create)) + { + stream.CopyTo(fileStream); + } + } + } + } + + return _FFMpegDirectory; + } + } + + private static string FFMpegPath + { + get + { + return System.IO.Path.Combine(FFMpegDirectory, "ffmpeg.exe"); + } + } + + private string GetOutputFilePath(string input) + { + string hash = Kernel.GetMD5(input).ToString(); + + if (AudioBitRate.HasValue) + { + hash += "_ab" + AudioBitRate; + } + if (NumAudioChannels.HasValue) + { + hash += "_ac" + NumAudioChannels; + } + if (AudioSampleRate.HasValue) + { + hash += "_ar" + AudioSampleRate; + } + + string filename = hash + "." + AudioFormat.ToLower(); + + return System.IO.Path.Combine(StreamsDirectory, filename); + } + + /// + /// Determines whether or not the original file requires transcoding + /// + private bool RequiresTranscoding() + { + // Only support skipping transcoding for library items + if (LibraryItem == null) + { + return true; + } + + // If it's not in the same format, we need to transcode + if (!LibraryItem.Path.EndsWith(AudioFormat, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // If the bitrate is greater than our desired bitrate, we need to transcode + if (AudioBitRate.HasValue) + { + if (AudioBitRate.Value < LibraryItem.BitRate) + { + return true; + } + } + + // If the number of channels is greater than our desired channels, we need to transcode + if (NumAudioChannels.HasValue) + { + if (NumAudioChannels.Value < LibraryItem.Channels) + { + return true; + } + } + + // If the sample rate is greater than our desired sample rate, we need to transcode + if (AudioSampleRate.HasValue) + { + if (AudioSampleRate.Value < LibraryItem.SampleRate) + { + return true; + } + } + + // Yay + return false; + } + + /// + /// Creates a new transcoding job + /// + private TranscodingJob GetNewTranscodingJob(string input, string output) + { + return new TranscodingJob() + { + InputFile = input, + OutputFile = output, + TranscoderPath = FFMpegPath, + Arguments = GetAudioArguments(input, output) + }; + } + + /// + /// Creates arguments to pass to ffmpeg + /// + private string GetAudioArguments(string input, string output) + { + List audioTranscodeParams = new List(); + + if (AudioBitRate.HasValue) + { + audioTranscodeParams.Add("-ab " + AudioBitRate.Value); + } + + if (NumAudioChannels.HasValue) + { + audioTranscodeParams.Add("-ac " + NumAudioChannels.Value); + } + + if (AudioSampleRate.HasValue) + { + audioTranscodeParams.Add("-ar " + AudioSampleRate.Value); + } + + audioTranscodeParams.Add("-f " + AudioFormat); + + return "-i \"" + input + "\" -vn " + string.Join(" ", audioTranscodeParams.ToArray()) + " \"" + output + "\""; + } } } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index 33c209cd43..bbb8dcbb43 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -66,6 +66,7 @@ + @@ -84,7 +85,9 @@ - + + + xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData\Plugins\$(ProjectName)\" /y diff --git a/MediaBrowser.Api/Transcoding/TranscodingJob.cs b/MediaBrowser.Api/Transcoding/TranscodingJob.cs new file mode 100644 index 0000000000..e504fec095 --- /dev/null +++ b/MediaBrowser.Api/Transcoding/TranscodingJob.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using MediaBrowser.Common.Logging; + +namespace MediaBrowser.Api.Transcoding +{ + /// + /// Represents an active transcoding job + /// + public class TranscodingJob + { + public string InputFile { get; set; } + public string OutputFile { get; set; } + public string TranscoderPath { get; set; } + public string Arguments { get; set; } + + public TranscoderJobStatus Status { get; private set; } + + /// + /// Starts the job + /// + public void Start() + { + ApiService.AddTranscodingJob(this); + + ProcessStartInfo startInfo = new ProcessStartInfo(); + + startInfo.CreateNoWindow = true; + + startInfo.UseShellExecute = false; + + startInfo.FileName = TranscoderPath; + startInfo.WorkingDirectory = Path.GetDirectoryName(TranscoderPath); + startInfo.Arguments = Arguments; + + Logger.LogInfo("TranscodingJob.Start: " + TranscoderPath + " " + Arguments); + + Process process = new Process(); + + process.StartInfo = startInfo; + + process.EnableRaisingEvents = true; + + process.Start(); + + process.Exited += process_Exited; + } + + void process_Exited(object sender, EventArgs e) + { + ApiService.RemoveTranscodingJob(this); + + Process process = sender as Process; + + // If it terminated with an error + if (process.ExitCode != 0) + { + Status = TranscoderJobStatus.Error; + + // Delete this since it won't be valid + if (File.Exists(OutputFile)) + { + File.Delete(OutputFile); + } + } + else + { + Status = TranscoderJobStatus.Completed; + } + + process.Dispose(); + } + + /// + /// Provides a helper to wait for the job to exit + /// + public void WaitForExit() + { + while (true) + { + TranscoderJobStatus status = Status; + + if (status == TranscoderJobStatus.Completed || status == TranscoderJobStatus.Error) + { + break; + } + + Thread.Sleep(500); + } + } + } + + public enum TranscoderJobStatus + { + Queued, + Started, + Completed, + Error + } +} diff --git a/MediaBrowser.Api/ffmpeg/ffmpeg.exe.REMOVED.git-id b/MediaBrowser.Api/ffmpeg/ffmpeg.exe.REMOVED.git-id new file mode 100644 index 0000000000..cefa9ca782 --- /dev/null +++ b/MediaBrowser.Api/ffmpeg/ffmpeg.exe.REMOVED.git-id @@ -0,0 +1 @@ +a9ba5e8a56932043f5fe75db9b4f3b29fe210dbf \ No newline at end of file