diff options
Diffstat (limited to 'MediaBrowser.Api/Playback/BaseStreamingService.cs')
| -rw-r--r-- | MediaBrowser.Api/Playback/BaseStreamingService.cs | 297 |
1 files changed, 185 insertions, 112 deletions
diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index c016f67ab..31a81de73 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -13,7 +13,6 @@ using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using System; @@ -123,7 +122,11 @@ namespace MediaBrowser.Api.Playback var outputFileExtension = GetOutputFileExtension(state); - return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + (outputFileExtension ?? string.Empty).ToLower()); + var data = GetCommandLineArguments("dummy\\dummy", state, false); + + data += "-" + (state.Request.DeviceId ?? string.Empty); + + return Path.Combine(folder, data.GetMD5().ToString("N") + (outputFileExtension ?? string.Empty).ToLower()); } protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); @@ -138,14 +141,9 @@ namespace MediaBrowser.Api.Playback { var time = request.StartTimeTicks; - if (time.HasValue) + if (time.HasValue && time.Value > 0) { - var seconds = TimeSpan.FromTicks(time.Value).TotalSeconds; - - if (seconds > 0) - { - return string.Format("-ss {0}", seconds.ToString(UsCulture)); - } + return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value)); } return string.Empty; @@ -319,7 +317,7 @@ namespace MediaBrowser.Api.Playback switch (qualitySetting) { case EncodingQuality.HighSpeed: - param = "-preset ultrafast"; + param = "-preset superfast"; break; case EncodingQuality.HighQuality: param = "-preset superfast"; @@ -350,16 +348,16 @@ namespace MediaBrowser.Api.Playback var profileScore = 0; string crf; + var qmin = "0"; + var qmax = "50"; switch (qualitySetting) { case EncodingQuality.HighSpeed: - crf = "12"; - profileScore = 2; + crf = "10"; break; case EncodingQuality.HighQuality: - crf = "8"; - profileScore = 1; + crf = "6"; break; case EncodingQuality.MaxQuality: crf = "4"; @@ -371,14 +369,17 @@ namespace MediaBrowser.Api.Playback if (isVc1) { profileScore++; - // Max of 2 - profileScore = Math.Min(profileScore, 2); } + // Max of 2 + profileScore = Math.Min(profileScore, 2); + // http://www.webmproject.org/docs/encoder-parameters/ - param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1}", + param = string.Format("-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}", profileScore.ToString(UsCulture), - crf); + crf, + qmin, + qmax); } else if (string.Equals(videoCodec, "mpeg4", StringComparison.OrdinalIgnoreCase)) @@ -469,11 +470,11 @@ namespace MediaBrowser.Api.Playback /// </summary> /// <param name="state">The state.</param> /// <param name="outputVideoCodec">The output video codec.</param> - /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="allowTimeStampCopy">if set to <c>true</c> [allow time stamp copy].</param> /// <returns>System.String.</returns> protected string GetOutputSizeParam(StreamState state, string outputVideoCodec, - CancellationToken cancellationToken) + bool allowTimeStampCopy = true) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ @@ -562,11 +563,14 @@ namespace MediaBrowser.Api.Playback if (state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream) { - var subParam = GetTextSubtitleParam(state, cancellationToken); + var subParam = GetTextSubtitleParam(state); filters.Add(subParam); - output += " -copyts"; + if (allowTimeStampCopy) + { + output += " -copyts"; + } } if (filters.Count > 0) @@ -581,12 +585,10 @@ namespace MediaBrowser.Api.Playback /// Gets the text subtitle param. /// </summary> /// <param name="state">The state.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <returns>System.String.</returns> - protected string GetTextSubtitleParam(StreamState state, - CancellationToken cancellationToken) + protected string GetTextSubtitleParam(StreamState state) { - var seconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds; + var seconds = Math.Round(TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds); if (state.SubtitleStream.IsExternal) { @@ -604,17 +606,17 @@ namespace MediaBrowser.Api.Playback } } - // TODO: Perhaps also use original_size=1920x800 + // TODO: Perhaps also use original_size=1920x800 ?? return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB", subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"), charsetParam, - Math.Round(seconds).ToString(UsCulture)); + seconds.ToString(UsCulture)); } return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB", state.MediaPath.Replace('\\', '/').Replace(":/", "\\:/"), state.InternalSubtitleStreamOffset.ToString(UsCulture), - Math.Round(seconds).ToString(UsCulture)); + seconds.ToString(UsCulture)); } /// <summary> @@ -623,7 +625,7 @@ namespace MediaBrowser.Api.Playback /// <param name="state">The state.</param> /// <param name="outputVideoCodec">The output video codec.</param> /// <returns>System.String.</returns> - protected string GetInternalGraphicalSubtitleParam(StreamState state, string outputVideoCodec) + protected string GetGraphicalSubtitleParam(StreamState state, string outputVideoCodec) { var outputSizeParam = string.Empty; @@ -632,7 +634,7 @@ namespace MediaBrowser.Api.Playback // Add resolution params, if specified if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) { - outputSizeParam = GetOutputSizeParam(state, outputVideoCodec, CancellationToken.None).TrimEnd('"'); + outputSizeParam = GetOutputSizeParam(state, outputVideoCodec).TrimEnd('"'); outputSizeParam = "," + outputSizeParam.Substring(outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase)); } @@ -772,6 +774,11 @@ namespace MediaBrowser.Api.Playback return "copy"; } + protected virtual bool SupportsThrottling + { + get { return false; } + } + /// <summary> /// Gets the input argument. /// </summary> @@ -779,6 +786,19 @@ namespace MediaBrowser.Api.Playback /// <returns>System.String.</returns> protected string GetInputArgument(StreamState state) { + if (state.InputProtocol == MediaProtocol.File && + state.RunTimeTicks.HasValue && + state.VideoType == VideoType.VideoFile && + !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) + { + if (state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && state.IsInputVideo) + { + var url = "http://localhost:" + ServerConfigurationManager.Configuration.HttpServerPortNumber.ToString(UsCulture) + "/mediabrowser/videos/" + state.Request.Id + "/stream?static=true&Throttle=true&mediaSourceId=" + state.Request.MediaSourceId; + + return string.Format("\"{0}\"", url); + } + } + var protocol = state.InputProtocol; var inputPath = new[] { state.MediaPath }; @@ -794,6 +814,81 @@ namespace MediaBrowser.Api.Playback return MediaEncoder.GetInputArgument(inputPath, protocol); } + private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) + { + if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath)) + { + state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); + } + + if (string.IsNullOrEmpty(state.MediaPath)) + { + var checkCodecs = false; + + if (string.Equals(state.ItemType, typeof(LiveTvChannel).Name)) + { + var streamInfo = await LiveTvManager.GetChannelStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false); + + state.LiveTvStreamId = streamInfo.Id; + + if (!string.IsNullOrEmpty(streamInfo.Path)) + { + state.MediaPath = streamInfo.Path; + state.InputProtocol = MediaProtocol.File; + + await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false); + } + else if (!string.IsNullOrEmpty(streamInfo.Url)) + { + state.MediaPath = streamInfo.Url; + state.InputProtocol = MediaProtocol.Http; + } + + AttachMediaStreamInfo(state, streamInfo.MediaStreams, state.VideoRequest, state.RequestedUrl); + checkCodecs = true; + } + + else if (string.Equals(state.ItemType, typeof(LiveTvVideoRecording).Name) || + string.Equals(state.ItemType, typeof(LiveTvAudioRecording).Name)) + { + var streamInfo = await LiveTvManager.GetRecordingStream(state.Request.Id, cancellationTokenSource.Token).ConfigureAwait(false); + + state.LiveTvStreamId = streamInfo.Id; + + if (!string.IsNullOrEmpty(streamInfo.Path)) + { + state.MediaPath = streamInfo.Path; + state.InputProtocol = MediaProtocol.File; + + await Task.Delay(1500, cancellationTokenSource.Token).ConfigureAwait(false); + } + else if (!string.IsNullOrEmpty(streamInfo.Url)) + { + state.MediaPath = streamInfo.Url; + state.InputProtocol = MediaProtocol.Http; + } + + AttachMediaStreamInfo(state, streamInfo.MediaStreams, state.VideoRequest, state.RequestedUrl); + checkCodecs = true; + } + + var videoRequest = state.VideoRequest; + + if (videoRequest != null && checkCodecs) + { + if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream)) + { + state.OutputVideoCodec = "copy"; + } + + if (state.AudioStream != null && CanStreamCopyAudio(videoRequest, state.AudioStream, state.SupportedAudioCodecs)) + { + state.OutputAudioCodec = "copy"; + } + } + } + } + /// <summary> /// Starts the FFMPEG. /// </summary> @@ -811,10 +906,7 @@ namespace MediaBrowser.Api.Playback Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - if (state.VideoType == VideoType.Iso && state.IsoType.HasValue && IsoManager.CanMount(state.MediaPath)) - { - state.IsoMount = await IsoManager.Mount(state.MediaPath, cancellationTokenSource.Token).ConfigureAwait(false); - } + await AcquireResources(state, cancellationTokenSource).ConfigureAwait(false); var commandLineArgs = GetCommandLineArguments(outputPath, state, true); @@ -849,7 +941,6 @@ namespace MediaBrowser.Api.Playback ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, TranscodingJobType, process, - state.Request.StartTimeTicks, state.Request.DeviceId, state, cancellationTokenSource); @@ -866,7 +957,7 @@ namespace MediaBrowser.Api.Playback var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine); await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false); - process.Exited += (sender, args) => OnFfMpegProcessExited(process, state); + process.Exited += (sender, args) => OnFfMpegProcessExited(process, state, outputPath); try { @@ -892,18 +983,6 @@ namespace MediaBrowser.Api.Playback { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } - - // Allow a small amount of time to buffer a little - if (state.IsInputVideo) - { - await Task.Delay(500, cancellationTokenSource.Token).ConfigureAwait(false); - } - - // This is arbitrary, but add a little buffer time when internet streaming - if (state.InputProtocol != MediaProtocol.File) - { - await Task.Delay(3000, cancellationTokenSource.Token).ConfigureAwait(false); - } } private async void StartStreamingLog(StreamState state, Stream source, Stream target) @@ -1061,7 +1140,8 @@ namespace MediaBrowser.Api.Playback // Make sure we don't request a bitrate higher than the source var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value; - return Math.Min(currentBitrate, request.AudioBitRate.Value); + return request.AudioBitRate.Value; + //return Math.Min(currentBitrate, request.AudioBitRate.Value); } return null; @@ -1091,8 +1171,16 @@ namespace MediaBrowser.Api.Playback /// </summary> /// <param name="process">The process.</param> /// <param name="state">The state.</param> - private void OnFfMpegProcessExited(Process process, StreamState state) + /// <param name="outputPath">The output path.</param> + private void OnFfMpegProcessExited(Process process, StreamState state, string outputPath) { + var job = ApiEntryPoint.Instance.GetTranscodingJob(outputPath, TranscodingJobType); + + if (job != null) + { + job.HasExited = true; + } + Logger.Debug("Disposing stream resources"); state.Dispose(); @@ -1126,13 +1214,13 @@ namespace MediaBrowser.Api.Playback return state.VideoRequest.Framerate.Value; } - var maxrate = state.VideoRequest.MaxFramerate ?? 23.97602; + var maxrate = state.VideoRequest.MaxFramerate; - if (state.VideoStream != null) + if (maxrate.HasValue && state.VideoStream != null) { var contentRate = state.VideoStream.AverageFrameRate ?? state.VideoStream.RealFrameRate; - if (contentRate.HasValue && contentRate.Value > maxrate) + if (contentRate.HasValue && contentRate.Value > maxrate.Value) { return maxrate; } @@ -1330,8 +1418,6 @@ namespace MediaBrowser.Api.Playback ParseParams(request); } - var user = AuthorizationRequestFilterAttribute.GetCurrentUser(Request, UserManager); - var url = Request.PathInfo; if (string.IsNullOrEmpty(request.AudioCodec)) @@ -1353,13 +1439,10 @@ namespace MediaBrowser.Api.Playback var item = LibraryManager.GetItemById(request.Id); - if (user != null && item.GetPlayAccess(user) != PlayAccess.Full) - { - throw new ArgumentException(string.Format("{0} is not allowed to play media.", user.Name)); - } - List<MediaStream> mediaStreams = null; + state.ItemType = item.GetType().Name; + if (item is ILiveTvRecording) { var recording = await LiveTvManager.GetInternalRecording(request.Id, cancellationToken).ConfigureAwait(false); @@ -1376,16 +1459,8 @@ namespace MediaBrowser.Api.Playback mediaStreams = source.MediaStreams; - if (string.IsNullOrWhiteSpace(path) && string.IsNullOrWhiteSpace(mediaUrl)) - { - var streamInfo = await LiveTvManager.GetRecordingStream(request.Id, cancellationToken).ConfigureAwait(false); - - state.LiveTvStreamId = streamInfo.Id; - mediaStreams = streamInfo.MediaStreams; - - path = streamInfo.Path; - mediaUrl = streamInfo.Url; - } + // Just to prevent this from being null and causing other methods to fail + state.MediaPath = string.Empty; if (!string.IsNullOrEmpty(path)) { @@ -1397,17 +1472,20 @@ namespace MediaBrowser.Api.Playback state.MediaPath = mediaUrl; state.InputProtocol = MediaProtocol.Http; } - - state.RunTimeTicks = recording.RunTimeTicks; + else + { + // No media info, so this is probably needed + state.DeInterlace = true; + } if (recording.RecordingInfo.Status == RecordingStatus.InProgress) { - await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + state.ReadInputAtNativeFramerate = true; } - state.ReadInputAtNativeFramerate = recording.RecordingInfo.Status == RecordingStatus.InProgress; + state.RunTimeTicks = recording.RunTimeTicks; + state.OutputAudioSync = "1000"; - state.DeInterlace = true; state.InputVideoSync = "-1"; state.InputAudioSync = "1"; state.InputContainer = recording.Container; @@ -1418,40 +1496,27 @@ namespace MediaBrowser.Api.Playback state.VideoType = VideoType.VideoFile; state.IsInputVideo = string.Equals(channel.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); - - var streamInfo = await LiveTvManager.GetChannelStream(request.Id, cancellationToken).ConfigureAwait(false); - - state.LiveTvStreamId = streamInfo.Id; - mediaStreams = streamInfo.MediaStreams; - - if (!string.IsNullOrEmpty(streamInfo.Path)) - { - state.MediaPath = streamInfo.Path; - state.InputProtocol = MediaProtocol.File; - - await Task.Delay(1000, cancellationToken).ConfigureAwait(false); - } - else if (!string.IsNullOrEmpty(streamInfo.Url)) - { - state.MediaPath = streamInfo.Url; - state.InputProtocol = MediaProtocol.Http; - } + mediaStreams = new List<MediaStream>(); state.ReadInputAtNativeFramerate = true; state.OutputAudioSync = "1000"; state.DeInterlace = true; state.InputVideoSync = "-1"; state.InputAudioSync = "1"; + + // Just to prevent this from being null and causing other methods to fail + state.MediaPath = string.Empty; } else if (item is IChannelMediaItem) { - var source = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false); + var mediaSource = await GetChannelMediaInfo(request.Id, request.MediaSourceId, cancellationToken).ConfigureAwait(false); state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase); - state.InputProtocol = source.Protocol; - state.MediaPath = source.Path; + state.InputProtocol = mediaSource.Protocol; + state.MediaPath = mediaSource.Path; state.RunTimeTicks = item.RunTimeTicks; - state.RemoteHttpHeaders = source.RequiredHttpHeaders; - mediaStreams = source.MediaStreams; + state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; + state.InputBitrate = mediaSource.Bitrate; + mediaStreams = mediaSource.MediaStreams; } else { @@ -1465,6 +1530,7 @@ namespace MediaBrowser.Api.Playback state.MediaPath = mediaSource.Path; state.InputProtocol = mediaSource.Protocol; state.InputContainer = mediaSource.Container; + state.InputBitrate = mediaSource.Bitrate; if (item is Video) { @@ -1488,16 +1554,23 @@ namespace MediaBrowser.Api.Playback state.RunTimeTicks = mediaSource.RunTimeTicks; } - if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase)) + // If it's a wtv and we don't have media info, we will probably need to deinterlace + if (string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) && + mediaStreams.Count == 0) { state.DeInterlace = true; } + if (state.InputProtocol == MediaProtocol.Rtmp) + { + state.ReadInputAtNativeFramerate = true; + } + var videoRequest = request as VideoStreamRequest; AttachMediaStreamInfo(state, mediaStreams, videoRequest, url); - state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 10; + state.SegmentLength = state.ReadInputAtNativeFramerate ? 5 : 7; state.HlsListSize = state.ReadInputAtNativeFramerate ? 100 : 1440; var container = Path.GetExtension(state.RequestedUrl); @@ -1574,6 +1647,8 @@ namespace MediaBrowser.Api.Playback { state.AudioStream = GetMediaStream(mediaStreams, null, MediaStreamType.Audio, true); } + + state.AllMediaStreams = mediaStreams; } private async Task<MediaSourceInfo> GetChannelMediaInfo(string id, @@ -1609,7 +1684,10 @@ namespace MediaBrowser.Api.Playback // Can't stream copy if we're burning in subtitles if (request.SubtitleStreamIndex.HasValue) { - return false; + if (request.SubtitleMethod == SubtitleDeliveryMethod.Encode) + { + return false; + } } // Source and target codecs must match @@ -1886,7 +1964,8 @@ namespace MediaBrowser.Api.Playback state.TargetPacketLength, state.TranscodeSeekInfo, state.IsTargetAnamorphic - ); + + ).FirstOrDefault() ?? string.Empty; } foreach (var item in responseHeaders) @@ -1911,12 +1990,6 @@ namespace MediaBrowser.Api.Playback /// <param name="videoRequest">The video request.</param> private void EnforceResolutionLimit(StreamState state, VideoStreamRequest videoRequest) { - // If enabled, allow whatever the client asks for - if (ServerConfigurationManager.Configuration.AllowVideoUpscaling) - { - return; - } - // Switch the incoming params to be ceilings rather than fixed values videoRequest.MaxWidth = videoRequest.MaxWidth ?? videoRequest.Width; videoRequest.MaxHeight = videoRequest.MaxHeight ?? videoRequest.Height; @@ -1925,7 +1998,7 @@ namespace MediaBrowser.Api.Playback videoRequest.Height = null; } - protected string GetInputModifier(StreamState state) + protected string GetInputModifier(StreamState state, bool genPts = true) { var inputModifier = string.Empty; @@ -1945,9 +2018,9 @@ namespace MediaBrowser.Api.Playback inputModifier += " " + GetFastSeekCommandLineParameter(state.Request); inputModifier = inputModifier.Trim(); - if (state.VideoRequest != null) + if (state.VideoRequest != null && genPts) { - inputModifier += " -fflags genpts"; + inputModifier += " -fflags +genpts"; } if (!string.IsNullOrEmpty(state.InputAudioSync)) |
