Browse Source

Added audio transcoding

LukePulverenti Luke Pulverenti luke pulverenti 13 years ago
parent
commit
92056c4d3d

+ 39 - 2
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
     /// </summary>
     public static class ApiService
     {
+        /// <summary>
+        /// Holds the list of active transcoding jobs
+        /// </summary>
+        private static List<TranscodingJob> CurrentTranscodingJobs = new List<TranscodingJob>();
+
+        /// <summary>
+        /// Finds an active transcoding job
+        /// </summary>
+        public static TranscodingJob GetTranscodingJob(string outputPath)
+        {
+            lock (CurrentTranscodingJobs)
+            {
+                return CurrentTranscodingJobs.FirstOrDefault(j => j.OutputFile.Equals(outputPath, StringComparison.OrdinalIgnoreCase));
+            }
+        }
+
+        /// <summary>
+        /// Removes a transcoding job from the active list
+        /// </summary>
+        public static void RemoveTranscodingJob(TranscodingJob job)
+        {
+            lock (CurrentTranscodingJobs)
+            {
+                CurrentTranscodingJobs.Remove(job);
+            }
+        }
+
+        /// <summary>
+        /// Adds a transcoding job to the active list
+        /// </summary>
+        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);

+ 280 - 6
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;
         /// <summary>
         /// Gets the library item that will be played, if any
         /// </summary>
-        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;
+        /// <summary>
+        /// Gets the library item that will be played, if any
+        /// </summary>
+        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;
+        /// <summary>
+        /// Gets the folder path to where transcodes will be cached
+        /// </summary>
+        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;
+        /// <summary>
+        /// Gets the folder path to ffmpeg
+        /// </summary>
+        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);
+        }
+
+        /// <summary>
+        /// Determines whether or not the original file requires transcoding
+        /// </summary>
+        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;
+        }
+
+        /// <summary>
+        /// Creates a new transcoding job
+        /// </summary>
+        private TranscodingJob GetNewTranscodingJob(string input, string output)
+        {
+            return new TranscodingJob()
+            {
+                InputFile = input,
+                OutputFile = output,
+                TranscoderPath = FFMpegPath,
+                Arguments = GetAudioArguments(input, output)
+            };
+        }
+
+        /// <summary>
+        /// Creates arguments to pass to ffmpeg
+        /// </summary>
+        private string GetAudioArguments(string input, string output)
+        {
+            List<string> audioTranscodeParams = new List<string>();
+
+            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 + "\"";
+        }
     }
 }

+ 4 - 1
MediaBrowser.Api/MediaBrowser.Api.csproj

@@ -66,6 +66,7 @@
     <Compile Include="ImageProcessor.cs" />
     <Compile Include="Plugin.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
+    <Compile Include="Transcoding\TranscodingJob.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
@@ -84,7 +85,9 @@
   <ItemGroup>
     <None Include="packages.config" />
   </ItemGroup>
-  <ItemGroup />
+  <ItemGroup>
+    <EmbeddedResource Include="ffmpeg\ffmpeg.exe" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <PropertyGroup>
     <PostBuildEvent>xcopy "$(TargetPath)" "$(SolutionDir)\ProgramData\Plugins\$(ProjectName)\" /y</PostBuildEvent>

+ 102 - 0
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
+{
+    /// <summary>
+    /// Represents an active transcoding job
+    /// </summary>
+    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; }
+
+        /// <summary>
+        /// Starts the job
+        /// </summary>
+        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();
+        }
+
+        /// <summary>
+        /// Provides a helper to wait for the job to exit
+        /// </summary>
+        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
+    }
+}

+ 1 - 0
MediaBrowser.Api/ffmpeg/ffmpeg.exe.REMOVED.git-id

@@ -0,0 +1 @@
+a9ba5e8a56932043f5fe75db9b4f3b29fe210dbf