using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; namespace MediaBrowser.Api.Playback.Hls { /// /// Class BaseHlsService /// public abstract class BaseHlsService : BaseStreamingService { protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer) { } /// /// Gets the audio arguments. /// /// The state. /// System.String. protected abstract string GetAudioArguments(StreamState state); /// /// Gets the video arguments. /// /// The state. /// System.String. protected abstract string GetVideoArguments(StreamState state); /// /// 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. /// if set to true [is live]. /// System.Object. protected object ProcessRequest(StreamRequest request, bool isLive) { return ProcessRequestAsync(request, isLive).Result; } /// /// Processes the request async. /// /// The request. /// if set to true [is live]. /// Task{System.Object}. /// A video bitrate is required /// or /// An audio bitrate is required private async Task ProcessRequestAsync(StreamRequest request, bool isLive) { var cancellationTokenSource = new CancellationTokenSource(); var state = await GetState(request, cancellationTokenSource.Token).ConfigureAwait(false); if (isLive) { state.Request.StartTimeTicks = null; } TranscodingJob job = null; var playlist = state.OutputFilePath; if (!FileSystem.FileExists(playlist)) { await ApiEntryPoint.Instance.TranscodingStartLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { if (!FileSystem.FileExists(playlist)) { // If the playlist doesn't already exist, startup ffmpeg try { job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false); job.IsLiveOutput = isLive; } catch { state.Dispose(); throw; } var waitForSegments = state.SegmentLength >= 10 ? 2 : 3; await WaitForMinimumSegmentCount(playlist, waitForSegments, cancellationTokenSource.Token).ConfigureAwait(false); } } finally { ApiEntryPoint.Instance.TranscodingStartLock.Release(); } } if (isLive) { job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); if (job != null) { ApiEntryPoint.Instance.OnTranscodeEndRequest(job); } return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); } var audioBitrate = state.OutputAudioBitrate ?? 0; var videoBitrate = state.OutputVideoBitrate ?? 0; var appendBaselineStream = false; var baselineStreamBitrate = 64000; var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy; if (hlsVideoRequest != null) { appendBaselineStream = hlsVideoRequest.AppendBaselineStream; baselineStreamBitrate = hlsVideoRequest.BaselineStreamAudioBitRate ?? baselineStreamBitrate; } var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate); job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); if (job != null) { ApiEntryPoint.Instance.OnTranscodeEndRequest(job); } return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary()); } private string GetLivePlaylistText(string path, int segmentLength) { using (var stream = FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { using (var reader = new StreamReader(stream)) { var text = reader.ReadToEnd(); var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(UsCulture); // ffmpeg pads the reported length by a full second text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase); return text; } } } 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 virtual async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken) { Logger.Debug("Waiting for {0} segments in {1}", segmentCount, playlist); while (true) { // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written using (var fileStream = GetPlaylistFileStream(playlist)) { using (var reader = new StreamReader(fileStream)) { var count = 0; while (!reader.EndOfStream) { var line = await reader.ReadLineAsync().ConfigureAwait(false); if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) { count++; if (count >= segmentCount) { Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist); return; } } } await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } } } protected Stream GetPlaylistFileStream(string path) { var tmpPath = path + ".tmp"; try { return FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); } catch (IOException) { return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true); } } protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding) { var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy; var itsOffsetMs = hlsVideoRequest == null ? 0 : hlsVideoRequest.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 isEncoding is true we're actually starting ffmpeg var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0"; var baseUrlParam = string.Empty; if (state.Request is GetLiveHlsStream) { baseUrlParam = string.Format(" -hls_base_url \"{0}/\"", "hls/" + Path.GetFileNameWithoutExtension(outputPath)); } var args = string.Format("{0} {1} {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9}{10} -y \"{11}\"", itsOffset, inputModifier, GetInputArgument(state), threads, GetMapArgs(state), GetVideoArguments(state), GetAudioArguments(state), state.SegmentLength.ToString(UsCulture), startNumberParam, state.HlsListSize.ToString(UsCulture), baseUrlParam, outputPath ).Trim(); if (hlsVideoRequest != null) { if (hlsVideoRequest.AppendBaselineStream) { 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} -y \"{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; } protected override bool CanStreamCopyAudio(VideoStreamRequest request, MediaStream audioStream, List supportedAudioCodecs) { return false; } } }