using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls { /// /// Class BaseHlsService /// public abstract class BaseHlsService : BaseStreamingService { protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager) { } protected override string GetOutputFilePath(StreamState state) { var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; var outputFileExtension = GetOutputFileExtension(state); return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + (outputFileExtension ?? string.Empty).ToLower()); } /// /// Gets the audio arguments. /// /// The state. /// System.String. protected abstract string GetAudioArguments(StreamState state); /// /// Gets the video arguments. /// /// The state. /// if set to true [perform subtitle conversion]. /// System.String. protected abstract string GetVideoArguments(StreamState state, bool performSubtitleConversion); /// /// Gets the segment file extension. /// /// The state. /// System.String. protected abstract string GetSegmentFileExtension(StreamState state); /// /// Gets the type of the transcoding job. /// /// The type of the transcoding job. protected override TranscodingJobType TranscodingJobType { get { return TranscodingJobType.Hls; } } /// /// Processes the request. /// /// The request. /// System.Object. protected object ProcessRequest(StreamRequest request) { return ProcessRequestAsync(request).Result; } private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1); /// /// Processes the request async. /// /// The request. /// Task{System.Object}. /// /// A video bitrate is required /// or /// An audio bitrate is required /// private async Task ProcessRequestAsync(StreamRequest request) { var state = GetState(request, CancellationToken.None).Result; if (!state.VideoRequest.VideoBitRate.HasValue && (string.IsNullOrEmpty(state.VideoRequest.VideoCodec) || !string.Equals(state.VideoRequest.VideoCodec, "copy", StringComparison.OrdinalIgnoreCase))) { state.Dispose(); throw new ArgumentException("A video bitrate is required"); } if (!state.Request.AudioBitRate.HasValue && (string.IsNullOrEmpty(state.Request.AudioCodec) || !string.Equals(state.Request.AudioCodec, "copy", StringComparison.OrdinalIgnoreCase))) { state.Dispose(); throw new ArgumentException("An audio bitrate is required"); } var playlist = GetOutputFilePath(state); if (File.Exists(playlist)) { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); } else { await FfmpegStartLock.WaitAsync().ConfigureAwait(false); try { if (File.Exists(playlist)) { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); } else { // If the playlist doesn't already exist, startup ffmpeg try { await StartFfMpeg(state, playlist).ConfigureAwait(false); } catch { state.Dispose(); throw; } } await WaitForMinimumSegmentCount(playlist, GetSegmentWait()).ConfigureAwait(false); } finally { FfmpegStartLock.Release(); } } int audioBitrate; int videoBitrate; GetPlaylistBitrates(state, out audioBitrate, out videoBitrate); var appendBaselineStream = false; var baselineStreamBitrate = 64000; var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; if (hlsVideoRequest != null) { appendBaselineStream = hlsVideoRequest.AppendBaselineStream; baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate; } var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate); try { return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); } finally { ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); } } /// /// Gets the segment wait. /// /// System.Int32. protected int GetSegmentWait() { var minimumSegmentCount = 3; var quality = GetQualitySetting(); if (quality == EncodingQuality.HighSpeed || quality == EncodingQuality.HighQuality) { minimumSegmentCount = 2; } return minimumSegmentCount; } /// /// Gets the playlist bitrates. /// /// The state. /// The audio bitrate. /// The video bitrate. protected void GetPlaylistBitrates(StreamState state, out int audioBitrate, out int videoBitrate) { var audioBitrateParam = GetAudioBitrateParam(state); var videoBitrateParam = GetVideoBitrateParamValue(state); if (!audioBitrateParam.HasValue) { if (state.AudioStream != null) { audioBitrateParam = state.AudioStream.BitRate; } } if (!videoBitrateParam.HasValue) { if (state.VideoStream != null) { videoBitrateParam = state.VideoStream.BitRate; } } audioBitrate = audioBitrateParam ?? 0; videoBitrate = videoBitrateParam ?? 0; } private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, bool includeBaselineStream, int baselineStreamBitrate) { var builder = new StringBuilder(); builder.AppendLine("#EXTM3U"); // Pad a little to satisfy the apple hls validator var paddedBitrate = Convert.ToInt32(bitrate * 1.15); // Main stream builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture)); var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8"); builder.AppendLine(playlistUrl); // Low bitrate stream if (includeBaselineStream) { builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + baselineStreamBitrate.ToString(UsCulture)); playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "-low/stream.m3u8"); builder.AppendLine(playlistUrl); } return builder.ToString(); } protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount) { while (true) { string fileText; // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true)) { using (var reader = new StreamReader(fileStream)) { fileText = await reader.ReadToEndAsync().ConfigureAwait(false); } } if (CountStringOccurrences(fileText, "#EXTINF:") >= segmentCount) { break; } await Task.Delay(25).ConfigureAwait(false); } } /// /// Count occurrences of strings. /// /// The text. /// The pattern. /// System.Int32. private static int CountStringOccurrences(string text, string pattern) { // Loop through all instances of the string 'text'. var count = 0; var i = 0; while ((i = text.IndexOf(pattern, i, StringComparison.OrdinalIgnoreCase)) != -1) { i += pattern.Length; count++; } return count; } /// /// Gets the command line arguments. /// /// The output path. /// The state. /// if set to true [perform subtitle conversions]. /// System.String. protected override string GetCommandLineArguments(string outputPath, StreamState state, bool performSubtitleConversions) { var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream; var itsOffsetMs = hlsVideoRequest == null ? 0 : ((GetHlsVideoStream)state.VideoRequest).TimeStampOffsetMs; var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture)); var threads = GetNumberOfThreads(state, false); var inputModifier = GetInputModifier(state); // If performSubtitleConversions is true we're actually starting ffmpeg var startNumberParam = performSubtitleConversions ? GetStartNumber(state).ToString(UsCulture) : "0"; var args = string.Format("{0} {1} -i {2}{3} -map_metadata -1 -threads {4} {5} {6} -sc_threshold 0 {7} -hls_time {8} -start_number {9} -hls_list_size {10} \"{11}\"", itsOffset, inputModifier, GetInputArgument(state), GetSlowSeekCommandLineParameter(state.Request), threads, GetMapArgs(state), GetVideoArguments(state, performSubtitleConversions), GetAudioArguments(state), state.SegmentLength.ToString(UsCulture), startNumberParam, state.HlsListSize.ToString(UsCulture), outputPath ).Trim(); if (hlsVideoRequest != null) { if (hlsVideoRequest.AppendBaselineStream && state.IsInputVideo) { var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8"); var bitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? 64000; var lowBitrateParams = string.Format(" -threads {0} -vn -codec:a:0 libmp3lame -ac 2 -ab {1} -hls_time {2} -start_number {3} -hls_list_size {4} \"{5}\"", threads, bitrate / 2, state.SegmentLength.ToString(UsCulture), startNumberParam, state.HlsListSize.ToString(UsCulture), lowBitratePath); args += " " + lowBitrateParams; } } return args; } protected virtual int GetStartNumber(StreamState state) { return 0; } } }