diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj
index 51f1773613..e0a67ecf11 100644
--- a/MediaBrowser.Api/MediaBrowser.Api.csproj
+++ b/MediaBrowser.Api/MediaBrowser.Api.csproj
@@ -84,14 +84,18 @@
+
+
+
+
+
+
-
-
@@ -104,7 +108,6 @@
-
@@ -136,7 +139,9 @@
-
+
+
+
xcopy "$(TargetPath)" "$(SolutionDir)\MediaBrowser.ServerApplication\CorePlugins\" /y
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs
new file mode 100644
index 0000000000..1591064378
--- /dev/null
+++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs
@@ -0,0 +1,624 @@
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Implementations.HttpServer;
+using MediaBrowser.Common.IO;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Playback
+{
+ ///
+ /// Class BaseStreamingService
+ ///
+ public abstract class BaseStreamingService : BaseRestService
+ {
+ ///
+ /// Gets or sets the application paths.
+ ///
+ /// The application paths.
+ protected IServerApplicationPaths ApplicationPaths { get; set; }
+
+ ///
+ /// Gets the server kernel.
+ ///
+ /// The server kernel.
+ protected Kernel ServerKernel
+ {
+ get { return Kernel as Kernel; }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The app paths.
+ protected BaseStreamingService(IServerApplicationPaths appPaths)
+ {
+ ApplicationPaths = appPaths;
+ }
+
+ ///
+ /// Gets the command line arguments.
+ ///
+ /// The output path.
+ /// The state.
+ /// System.String.
+ protected abstract string GetCommandLineArguments(string outputPath, StreamState state);
+
+ ///
+ /// Gets the type of the transcoding job.
+ ///
+ /// The type of the transcoding job.
+ protected abstract TranscodingJobType TranscodingJobType { get; }
+
+ ///
+ /// Gets the output file extension.
+ ///
+ /// The state.
+ /// System.String.
+ protected virtual string GetOutputFileExtension(StreamState state)
+ {
+ return Path.GetExtension(state.Url);
+ }
+
+ ///
+ /// Gets the output file path.
+ ///
+ /// The state.
+ /// System.String.
+ protected string GetOutputFilePath(StreamState state)
+ {
+ var folder = ApplicationPaths.FFMpegStreamCachePath;
+ return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state).GetMD5() + GetOutputFileExtension(state).ToLower());
+ }
+
+ ///
+ /// The fast seek offset seconds
+ ///
+ private const int FastSeekOffsetSeconds = 1;
+
+ ///
+ /// Gets the fast seek command line parameter.
+ ///
+ /// The request.
+ /// System.String.
+ /// The fast seek command line parameter.
+ protected string GetFastSeekCommandLineParameter(StreamRequest request)
+ {
+ var time = request.StartTimeTicks;
+
+ if (time.HasValue)
+ {
+ var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds;
+
+ if (seconds > 0)
+ {
+ return string.Format("-ss {0}", seconds);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// Gets the slow seek command line parameter.
+ ///
+ /// The request.
+ /// System.String.
+ /// The slow seek command line parameter.
+ protected string GetSlowSeekCommandLineParameter(StreamRequest request)
+ {
+ var time = request.StartTimeTicks;
+
+ if (time.HasValue)
+ {
+ if (TimeSpan.FromTicks(time.Value).TotalSeconds - FastSeekOffsetSeconds > 0)
+ {
+ return string.Format(" -ss {0}", FastSeekOffsetSeconds);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ ///
+ /// Gets the map args.
+ ///
+ /// The state.
+ /// System.String.
+ protected string GetMapArgs(StreamState state)
+ {
+ var args = string.Empty;
+
+ if (state.VideoStream != null)
+ {
+ args += string.Format("-map 0:{0}", state.VideoStream.Index);
+ }
+ else
+ {
+ args += "-map -0:v";
+ }
+
+ if (state.AudioStream != null)
+ {
+ args += string.Format(" -map 0:{0}", state.AudioStream.Index);
+ }
+ else
+ {
+ args += " -map -0:a";
+ }
+
+ if (state.SubtitleStream == null)
+ {
+ args += " -map -0:s";
+ }
+
+ return args;
+ }
+
+ ///
+ /// Determines which stream will be used for playback
+ ///
+ /// All stream.
+ /// Index of the desired.
+ /// The type.
+ /// if set to true [return first if no index].
+ /// MediaStream.
+ private MediaStream GetMediaStream(IEnumerable allStream, int? desiredIndex, MediaStreamType type, bool returnFirstIfNoIndex = true)
+ {
+ var streams = allStream.Where(s => s.Type == type).ToList();
+
+ if (desiredIndex.HasValue)
+ {
+ var stream = streams.FirstOrDefault(s => s.Index == desiredIndex.Value);
+
+ if (stream != null)
+ {
+ return stream;
+ }
+ }
+
+ // Just return the first one
+ return returnFirstIfNoIndex ? streams.FirstOrDefault() : null;
+ }
+
+ ///
+ /// If we're going to put a fixed size on the command line, this will calculate it
+ ///
+ /// The state.
+ /// The output video codec.
+ /// System.String.
+ protected string GetOutputSizeParam(StreamState state, string outputVideoCodec)
+ {
+ // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/
+
+ var assSubtitleParam = string.Empty;
+
+ var request = state.Request;
+
+ if (state.SubtitleStream != null)
+ {
+ if (state.SubtitleStream.Codec.IndexOf("srt", StringComparison.OrdinalIgnoreCase) != -1 || state.SubtitleStream.Codec.IndexOf("subrip", StringComparison.OrdinalIgnoreCase) != -1)
+ {
+ assSubtitleParam = GetTextSubtitleParam((Video)state.Item, state.SubtitleStream, request.StartTimeTicks);
+ }
+ }
+
+ // If fixed dimensions were supplied
+ if (request.Width.HasValue && request.Height.HasValue)
+ {
+ return string.Format(" -vf \"scale={0}:{1}{2}\"", request.Width.Value, request.Height.Value, assSubtitleParam);
+ }
+
+ var isH264Output = outputVideoCodec.Equals("libx264", StringComparison.OrdinalIgnoreCase);
+
+ // If a fixed width was requested
+ if (request.Width.HasValue)
+ {
+ return isH264Output ?
+ string.Format(" -vf \"scale={0}:trunc(ow/a/2)*2{1}\"", request.Width.Value, assSubtitleParam) :
+ string.Format(" -vf \"scale={0}:-1{1}\"", request.Width.Value, assSubtitleParam);
+ }
+
+ // If a max width was requested
+ if (request.MaxWidth.HasValue && !request.MaxHeight.HasValue)
+ {
+ return isH264Output ?
+ string.Format(" -vf \"scale=min(iw\\,{0}):trunc(ow/a/2)*2{1}\"", request.MaxWidth.Value, assSubtitleParam) :
+ string.Format(" -vf \"scale=min(iw\\,{0}):-1{1}\"", request.MaxWidth.Value, assSubtitleParam);
+ }
+
+ // Need to perform calculations manually
+
+ // Try to account for bad media info
+ var currentHeight = state.VideoStream.Height ?? request.MaxHeight ?? request.Height ?? 0;
+ var currentWidth = state.VideoStream.Width ?? request.MaxWidth ?? request.Width ?? 0;
+
+ var outputSize = DrawingUtils.Resize(currentWidth, currentHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+
+ // If we're encoding with libx264, it can't handle odd numbered widths or heights, so we'll have to fix that
+ if (isH264Output)
+ {
+ return string.Format(" -vf \"scale=trunc({0}/2)*2:trunc({1}/2)*2{2}\"", outputSize.Width, outputSize.Height, assSubtitleParam);
+ }
+
+ // Otherwise use -vf scale since ffmpeg will ensure internally that the aspect ratio is preserved
+ return string.Format(" -vf \"scale={0}:-1{1}\"", Convert.ToInt32(outputSize.Width), assSubtitleParam);
+ }
+
+ ///
+ /// Gets the text subtitle param.
+ ///
+ /// The video.
+ /// The subtitle stream.
+ /// The start time ticks.
+ /// System.String.
+ protected string GetTextSubtitleParam(Video video, MediaStream subtitleStream, long? startTimeTicks)
+ {
+ var path = subtitleStream.IsExternal ? GetConvertedAssPath(video, subtitleStream) : GetExtractedAssPath(video, subtitleStream);
+
+ if (string.IsNullOrEmpty(path))
+ {
+ return string.Empty;
+ }
+
+ var param = string.Format(",ass={0}", path);
+
+ if (startTimeTicks.HasValue)
+ {
+ var seconds = Convert.ToInt32(TimeSpan.FromTicks(startTimeTicks.Value).TotalSeconds);
+ param += string.Format(",setpts=PTS-{0}/TB", seconds);
+ }
+
+ return param;
+ }
+
+ ///
+ /// Gets the extracted ass path.
+ ///
+ /// The video.
+ /// The subtitle stream.
+ /// System.String.
+ private string GetExtractedAssPath(Video video, MediaStream subtitleStream)
+ {
+ var path = ServerKernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass");
+
+ if (!File.Exists(path))
+ {
+ var success = ServerKernel.FFMpegManager.ExtractTextSubtitle(video, subtitleStream.Index, path, CancellationToken.None).Result;
+
+ if (!success)
+ {
+ return null;
+ }
+ }
+
+ return path;
+ }
+
+ ///
+ /// Gets the converted ass path.
+ ///
+ /// The video.
+ /// The subtitle stream.
+ /// System.String.
+ private string GetConvertedAssPath(Video video, MediaStream subtitleStream)
+ {
+ var path = ServerKernel.FFMpegManager.GetSubtitleCachePath(video, subtitleStream.Index, ".ass");
+
+ if (!File.Exists(path))
+ {
+ var success = ServerKernel.FFMpegManager.ConvertTextSubtitle(subtitleStream, path, CancellationToken.None).Result;
+
+ if (!success)
+ {
+ return null;
+ }
+ }
+
+ return path;
+ }
+
+ ///
+ /// Gets the internal graphical subtitle param.
+ ///
+ /// The state.
+ /// The output video codec.
+ /// System.String.
+ protected string GetInternalGraphicalSubtitleParam(StreamState state, string outputVideoCodec)
+ {
+ var outputSizeParam = string.Empty;
+
+ var request = state.Request;
+
+ // Add resolution params, if specified
+ if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue)
+ {
+ outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"');
+ outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase));
+ }
+
+ return string.Format(" -filter_complex \"[0:{0}]format=yuva444p,lut=u=128:v=128:y=gammaval(.3)[sub] ; [0:0] [sub] overlay{1}\"", state.SubtitleStream.Index, outputSizeParam);
+ }
+
+ ///
+ /// Gets the number of audio channels to specify on the command line
+ ///
+ /// The request.
+ /// The audio stream.
+ /// System.Nullable{System.Int32}.
+ protected int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream)
+ {
+ if (audioStream.Channels > 2 && request.AudioCodec.HasValue)
+ {
+ if (request.AudioCodec.Value == AudioCodecs.Aac)
+ {
+ // libvo_aacenc currently only supports two channel output
+ return 2;
+ }
+ if (request.AudioCodec.Value == AudioCodecs.Wma)
+ {
+ // wmav2 currently only supports two channel output
+ return 2;
+ }
+ }
+
+ return request.AudioChannels;
+ }
+
+ ///
+ /// Determines whether the specified stream is H264.
+ ///
+ /// The stream.
+ /// true if the specified stream is H264; otherwise, false.
+ protected bool IsH264(MediaStream stream)
+ {
+ return stream.Codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 ||
+ stream.Codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
+ }
+
+ ///
+ /// Gets the name of the output audio codec
+ ///
+ /// The request.
+ /// System.String.
+ protected string GetAudioCodec(StreamRequest request)
+ {
+ var codec = request.AudioCodec;
+
+ if (codec.HasValue)
+ {
+ if (codec == AudioCodecs.Aac)
+ {
+ return "libvo_aacenc";
+ }
+ if (codec == AudioCodecs.Mp3)
+ {
+ return "libmp3lame";
+ }
+ if (codec == AudioCodecs.Vorbis)
+ {
+ return "libvorbis";
+ }
+ if (codec == AudioCodecs.Wma)
+ {
+ return "wmav2";
+ }
+ }
+
+ return "copy";
+ }
+
+ ///
+ /// Gets the name of the output video codec
+ ///
+ /// The request.
+ /// System.String.
+ protected string GetVideoCodec(StreamRequest request)
+ {
+ var codec = request.VideoCodec;
+
+ if (codec.HasValue)
+ {
+ if (codec == VideoCodecs.H264)
+ {
+ return "libx264";
+ }
+ if (codec == VideoCodecs.Vpx)
+ {
+ return "libvpx";
+ }
+ if (codec == VideoCodecs.Wmv)
+ {
+ return "wmv2";
+ }
+ if (codec == VideoCodecs.Theora)
+ {
+ return "libtheora";
+ }
+ }
+
+ return "copy";
+ }
+
+ ///
+ /// Gets the input argument.
+ ///
+ /// The item.
+ /// The iso mount.
+ /// System.String.
+ protected string GetInputArgument(BaseItem item, IIsoMount isoMount)
+ {
+ return isoMount == null ?
+ ServerKernel.FFMpegManager.GetInputArgument(item) :
+ ServerKernel.FFMpegManager.GetInputArgument(item as Video, isoMount);
+ }
+
+ ///
+ /// Starts the FFMPEG.
+ ///
+ /// The state.
+ /// The output path.
+ /// Task.
+ protected async Task StartFFMpeg(StreamState state, string outputPath)
+ {
+ var video = state.Item as Video;
+
+ //if (video != null && video.VideoType == VideoType.Iso &&
+ // video.IsoType.HasValue && Kernel.IsoManager.CanMount(video.Path))
+ //{
+ // IsoMount = await Kernel.IsoManager.Mount(video.Path, CancellationToken.None).ConfigureAwait(false);
+ //}
+
+ 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,
+
+ FileName = ServerKernel.FFMpegManager.FFMpegPath,
+ WorkingDirectory = Path.GetDirectoryName(ServerKernel.FFMpegManager.FFMpegPath),
+ Arguments = GetCommandLineArguments(outputPath, state),
+
+ WindowStyle = ProcessWindowStyle.Hidden,
+ ErrorDialog = false
+ },
+
+ EnableRaisingEvents = true
+ };
+
+ Plugin.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process);
+
+ //Logger.Info(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
+
+ var logFilePath = Path.Combine(Kernel.ApplicationPaths.LogDirectoryPath, "ffmpeg-" + Guid.NewGuid() + ".txt");
+
+ // FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
+ state.LogFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous);
+
+ process.Exited += (sender, args) => OnFFMpegProcessExited(process, state);
+
+ try
+ {
+ process.Start();
+ }
+ catch (Win32Exception ex)
+ {
+ Logger.ErrorException("Error starting ffmpeg", ex);
+
+ Plugin.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType);
+
+ state.LogFileStream.Dispose();
+
+ throw;
+ }
+
+ // 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
+ process.StandardError.BaseStream.CopyToAsync(state.LogFileStream);
+
+ // Wait for the file to exist before proceeeding
+ while (!File.Exists(outputPath))
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Processes the exited.
+ ///
+ /// The process.
+ /// The state.
+ protected void OnFFMpegProcessExited(Process process, StreamState state)
+ {
+ if (state.IsoMount != null)
+ {
+ state.IsoMount.Dispose();
+ state.IsoMount = null;
+ }
+
+ var outputFilePath = GetOutputFilePath(state);
+
+ state.LogFileStream.Dispose();
+
+ int? exitCode = null;
+
+ try
+ {
+ exitCode = process.ExitCode;
+ Logger.Info("FFMpeg exited with code {0} for {1}", exitCode.Value, outputFilePath);
+ }
+ catch
+ {
+ Logger.Info("FFMpeg exited with an error for {0}", outputFilePath);
+ }
+
+ process.Dispose();
+
+ Plugin.Instance.OnTranscodingFinished(outputFilePath, TranscodingJobType);
+
+ if (!exitCode.HasValue || exitCode.Value != 0)
+ {
+ Logger.Info("Deleting partial stream file(s) {0}", outputFilePath);
+
+ try
+ {
+ DeletePartialStreamFiles(outputFilePath);
+ }
+ catch (IOException ex)
+ {
+ Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, outputFilePath);
+ }
+ }
+ else
+ {
+ Logger.Info("FFMpeg completed and exited normally for {0}", outputFilePath);
+ }
+ }
+
+ ///
+ /// Deletes the partial stream files.
+ ///
+ /// The output file path.
+ protected abstract void DeletePartialStreamFiles(string outputFilePath);
+
+ ///
+ /// Gets the state.
+ ///
+ /// The request.
+ /// StreamState.
+ protected StreamState GetState(StreamRequest request)
+ {
+ var item = DtoBuilder.GetItemByClientId(request.Id);
+
+ var media = (IHasMediaStreams)item;
+
+ return new StreamState
+ {
+ Item = item,
+ Request = request,
+ AudioStream = GetMediaStream(media.MediaStreams, request.AudioStreamIndex, MediaStreamType.Audio, true),
+ VideoStream = GetMediaStream(media.MediaStreams, request.VideoStreamIndex, MediaStreamType.Video, true),
+ SubtitleStream = GetMediaStream(media.MediaStreams, request.SubtitleStreamIndex, MediaStreamType.Subtitle, false),
+ Url = Request.PathInfo
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/Progressive/AudioService.cs b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
new file mode 100644
index 0000000000..dcb7d96e30
--- /dev/null
+++ b/MediaBrowser.Api/Playback/Progressive/AudioService.cs
@@ -0,0 +1,86 @@
+using MediaBrowser.Controller;
+using ServiceStack.ServiceHost;
+using System.Collections.Generic;
+
+namespace MediaBrowser.Api.Playback.Progressive
+{
+ ///
+ /// Class GetAudioStream
+ ///
+ [Route("/Audio/{Id}.mp3", "GET")]
+ [Route("/Audio/{Id}.wma", "GET")]
+ [Route("/Audio/{Id}.aac", "GET")]
+ [Route("/Audio/{Id}.flac", "GET")]
+ [Route("/Audio/{Id}.ogg", "GET")]
+ [Route("/Audio/{Id}", "GET")]
+ public class GetAudioStream : StreamRequest
+ {
+
+ }
+
+ ///
+ /// Class AudioService
+ ///
+ public class AudioService : BaseProgressiveStreamingService
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The app paths.
+ public AudioService(IServerApplicationPaths appPaths)
+ : base(appPaths)
+ {
+ }
+
+ ///
+ /// Gets the specified request.
+ ///
+ /// The request.
+ /// System.Object.
+ public object Get(GetAudioStream request)
+ {
+ return ProcessRequest(request);
+ }
+
+ ///
+ /// Gets the command line arguments.
+ ///
+ /// The output path.
+ /// The state.
+ /// System.String.
+ /// Only aac and mp3 audio codecs are supported.
+ protected override string GetCommandLineArguments(string outputPath, StreamState state)
+ {
+ var request = state.Request;
+
+ var audioTranscodeParams = new List();
+
+ if (request.AudioBitRate.HasValue)
+ {
+ audioTranscodeParams.Add("-ab " + request.AudioBitRate.Value);
+ }
+
+ var channels = GetNumAudioChannelsParam(request, state.AudioStream);
+
+ if (channels.HasValue)
+ {
+ audioTranscodeParams.Add("-ac " + channels.Value);
+ }
+
+ if (request.AudioSampleRate.HasValue)
+ {
+ audioTranscodeParams.Add("-ar " + request.AudioSampleRate.Value);
+ }
+
+ const string vn = " -vn";
+
+ return string.Format("{0} -i {1}{2} -threads 0{5} {3} -id3v2_version 3 -write_id3v1 1 \"{4}\"",
+ GetFastSeekCommandLineParameter(request),
+ GetInputArgument(state.Item, state.IsoMount),
+ GetSlowSeekCommandLineParameter(request),
+ string.Join(" ", audioTranscodeParams.ToArray()),
+ outputPath,
+ vn).Trim();
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
new file mode 100644
index 0000000000..bc2d26bcc0
--- /dev/null
+++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs
@@ -0,0 +1,151 @@
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Model.Dto;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Playback.Progressive
+{
+ ///
+ /// Class BaseProgressiveStreamingService
+ ///
+ public abstract class BaseProgressiveStreamingService : BaseStreamingService
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The app paths.
+ protected BaseProgressiveStreamingService(IServerApplicationPaths appPaths)
+ : base(appPaths)
+ {
+ }
+
+ ///
+ /// Gets the output file extension.
+ ///
+ /// The state.
+ /// System.String.
+ protected override string GetOutputFileExtension(StreamState state)
+ {
+ var ext = base.GetOutputFileExtension(state);
+
+ if (!string.IsNullOrEmpty(ext))
+ {
+ return ext;
+ }
+
+ // Try to infer based on the desired video codec
+ if (state.Request.VideoCodec.HasValue)
+ {
+ var video = state.Item as Video;
+
+ if (video != null)
+ {
+ switch (state.Request.VideoCodec.Value)
+ {
+ case VideoCodecs.H264:
+ return ".ts";
+ case VideoCodecs.Theora:
+ return ".ogv";
+ case VideoCodecs.Vpx:
+ return ".webm";
+ case VideoCodecs.Wmv:
+ return ".asf";
+ }
+ }
+ }
+
+ // Try to infer based on the desired audio codec
+ if (state.Request.AudioCodec.HasValue)
+ {
+ var audio = state.Item as Audio;
+
+ if (audio != null)
+ {
+ switch (state.Request.AudioCodec.Value)
+ {
+ case AudioCodecs.Aac:
+ return ".aac";
+ case AudioCodecs.Mp3:
+ return ".mp3";
+ case AudioCodecs.Vorbis:
+ return ".ogg";
+ case AudioCodecs.Wma:
+ return ".wma";
+ }
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the type of the transcoding job.
+ ///
+ /// The type of the transcoding job.
+ protected override TranscodingJobType TranscodingJobType
+ {
+ get { return TranscodingJobType.Progressive; }
+ }
+
+ ///
+ /// Processes the request.
+ ///
+ /// The request.
+ /// Task.
+ protected object ProcessRequest(StreamRequest request)
+ {
+ var state = GetState(request);
+
+ if (request.Static)
+ {
+ return ToStaticFileResult(state.Item.Path);
+ }
+
+ var outputPath = GetOutputFilePath(state);
+
+ if (File.Exists(outputPath) && !Plugin.Instance.HasActiveTranscodingJob(outputPath, TranscodingJobType.Progressive))
+ {
+ return ToStaticFileResult(outputPath);
+ }
+
+ return GetStreamResult(state).Result;
+ }
+
+ ///
+ /// Gets the stream result.
+ ///
+ /// The state.
+ /// Task{System.Object}.
+ private async Task GetStreamResult(StreamState state)
+ {
+ // Use the command line args with a dummy playlist path
+ var outputPath = GetOutputFilePath(state);
+
+ if (!File.Exists(outputPath))
+ {
+ await StartFFMpeg(state, outputPath).ConfigureAwait(false);
+ }
+ else
+ {
+ Plugin.Instance.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
+ }
+
+ return new ProgressiveStreamWriter
+ {
+ Path = outputPath,
+ State = state
+ };
+ }
+
+ ///
+ /// Deletes the partial stream files.
+ ///
+ /// The output file path.
+ protected override void DeletePartialStreamFiles(string outputFilePath)
+ {
+ File.Delete(outputFilePath);
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
new file mode 100644
index 0000000000..9ac93694b3
--- /dev/null
+++ b/MediaBrowser.Api/Playback/Progressive/ProgressiveStreamWriter.cs
@@ -0,0 +1,86 @@
+using MediaBrowser.Common.IO;
+using MediaBrowser.Model.Logging;
+using ServiceStack.Service;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Api.Playback.Progressive
+{
+ public class ProgressiveStreamWriter : IStreamWriter
+ {
+ public string Path { get; set; }
+ public StreamState State { get; set; }
+ public ILogger Logger { get; set; }
+
+ ///
+ /// Writes to.
+ ///
+ /// The response stream.
+ public void WriteTo(Stream responseStream)
+ {
+ var task = WriteToAsync(responseStream);
+
+ Task.WaitAll(task);
+ }
+
+ ///
+ /// Writes to async.
+ ///
+ /// The response stream.
+ /// Task.
+ public async Task WriteToAsync(Stream responseStream)
+ {
+ try
+ {
+ await StreamFile(Path, responseStream).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error streaming media", ex);
+ }
+ finally
+ {
+ Plugin.Instance.OnTranscodeEndRequest(Path, TranscodingJobType.Progressive);
+ }
+ }
+
+ ///
+ /// Streams the file.
+ ///
+ /// The path.
+ /// The output stream.
+ /// Task{System.Boolean}.
+ private async Task StreamFile(string path, Stream outputStream)
+ {
+ var eofCount = 0;
+ long position = 0;
+
+ using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous))
+ {
+ while (eofCount < 15)
+ {
+ await fs.CopyToAsync(outputStream).ConfigureAwait(false);
+
+ var fsPosition = fs.Position;
+
+ var bytesRead = fsPosition - position;
+
+ //Logger.LogInfo("Streamed {0} bytes from file {1}", bytesRead, path);
+
+ if (bytesRead == 0)
+ {
+ eofCount++;
+ await Task.Delay(100).ConfigureAwait(false);
+ }
+ else
+ {
+ eofCount = 0;
+ }
+
+ position = fsPosition;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Api/Streaming/VideoHandler.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
similarity index 55%
rename from MediaBrowser.Api/Streaming/VideoHandler.cs
rename to MediaBrowser.Api/Playback/Progressive/VideoService.cs
index da60297f23..b43b4bfbcb 100644
--- a/MediaBrowser.Api/Streaming/VideoHandler.cs
+++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs
@@ -1,60 +1,59 @@
-using MediaBrowser.Common.IO;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.Logging;
using System;
-using System.Linq;
-using System.Net;
-namespace MediaBrowser.Api.Streaming
+namespace MediaBrowser.Api.Playback.Progressive
{
///
- /// Providers a progressive streaming video api
+ /// Class VideoService
///
- class VideoHandler : BaseProgressiveStreamingHandler