using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.MediaInfo; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MediaBrowser.Api.Playback.Hls { /// /// Class BaseHlsService /// public abstract class BaseHlsService : BaseStreamingService { protected override string GetOutputFilePath(StreamState state) { var folder = ApplicationPaths.EncodedMediaCachePath; var outputFileExtension = GetOutputFileExtension(state); return Path.Combine(folder, GetCommandLineArguments("dummy\\dummy", state, false).GetMD5() + (outputFileExtension ?? string.Empty).ToLower()); } /// /// Initializes a new instance of the class. /// /// The app paths. /// The user manager. /// The library manager. /// The iso manager. /// The media encoder. protected BaseHlsService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder) : base(appPaths, userManager, libraryManager, isoManager, mediaEncoder) { } /// /// 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) { var state = GetState(request); return ProcessRequestAsync(state).Result; } /// /// Processes the request async. /// /// The state. /// Task{System.Object}. public async Task ProcessRequestAsync(StreamState state) { if (!state.VideoRequest.VideoBitRate.HasValue) { throw new ArgumentException("A video bitrate is required"); } if (!state.Request.AudioBitRate.HasValue) { throw new ArgumentException("An audio bitrate is required"); } var playlist = GetOutputFilePath(state); var isPlaylistNewlyCreated = false; // If the playlist doesn't already exist, startup ffmpeg if (!File.Exists(playlist)) { isPlaylistNewlyCreated = true; await StartFfMpeg(state, playlist).ConfigureAwait(false); } else { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); } if (isPlaylistNewlyCreated) { await WaitForMinimumSegmentCount(playlist, 3).ConfigureAwait(false); } var audioBitrate = GetAudioBitrateParam(state) ?? 0; var videoBitrate = GetVideoBitrateParam(state) ?? 0; var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate); try { return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); } finally { ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls); } } private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate) { var builder = new StringBuilder(); builder.AppendLine("#EXTM3U"); // Pad a little to satisfy the apple hls validator var paddedBitrate = Convert.ToInt32(bitrate * 1.05); // 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 //builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=64000"); //playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "-low/stream.m3u8"); //builder.AppendLine(playlistUrl); return builder.ToString(); } private 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 = new FileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) { 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; } protected void ExtendHlsTimer(string itemId, string playlistId) { var normalizedPlaylistId = playlistId.Replace("-low", string.Empty); foreach (var playlist in Directory.EnumerateFiles(ApplicationPaths.EncodedMediaCachePath, "*.m3u8") .Where(i => i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) .ToList()) { ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls); // Avoid implicitly captured closure var playlist1 = playlist; Task.Run(async () => { // This is an arbitrary time period corresponding to when the request completes. await Task.Delay(30000).ConfigureAwait(false); ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist1, TranscodingJobType.Hls); }); } } /// /// 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 probeSize = GetProbeSizeArgument(state.Item); var args = string.Format("{0} {1} {2} -i {3}{4} -threads 0 {5} {6} -sc_threshold 0 {7} -hls_time 10 -start_number 0 -hls_list_size 1440 \"{8}\"", probeSize, GetUserAgentParam(state.Item), GetFastSeekCommandLineParameter(state.Request), GetInputArgument(state.Item, state.IsoMount), GetSlowSeekCommandLineParameter(state.Request), GetMapArgs(state), GetVideoArguments(state, performSubtitleConversions), GetAudioArguments(state), outputPath ).Trim(); if (state.Item is Video) { var lowBitratePath = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath) + "-low.m3u8"); var lowBitrateParams = string.Format(" -threads 0 -vn -codec:a:{1} libmp3lame -ac 2 -ab 32000 -hls_time 10 -start_number 0 -hls_list_size 1440 \"{0}\"", lowBitratePath, state.AudioStream.Index); args += " " + lowBitrateParams; } return args; } } }