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