You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jellyfin/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

948 lines
36 KiB

using MediaBrowser.Common.Net;
11 years ago
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
11 years ago
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
11 years ago
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
11 years ago
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
11 years ago
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
10 years ago
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
11 years ago
namespace MediaBrowser.Api.Playback.Hls
{
/// <summary>
/// Options is needed for chromecast. Threw Head in there since it's related
/// </summary>
[Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
[Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
11 years ago
{
public bool EnableAdaptiveBitrateStreaming { get; set; }
public GetMasterHlsVideoPlaylist()
{
EnableAdaptiveBitrateStreaming = true;
}
11 years ago
}
[Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
[Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
{
public bool EnableAdaptiveBitrateStreaming { get; set; }
public GetMasterHlsAudioPlaylist()
{
EnableAdaptiveBitrateStreaming = true;
}
}
public interface IMasterHlsRequest
{
bool EnableAdaptiveBitrateStreaming { get; set; }
}
[Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
public class GetVariantHlsVideoPlaylist : VideoStreamRequest
{
}
[Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
public class GetVariantHlsAudioPlaylist : StreamRequest
11 years ago
{
}
9 years ago
[Route("/Videos/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")]
public class GetHlsVideoSegment : VideoStreamRequest
{
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
9 years ago
[Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.aac", "GET")]
[Route("/Audio/{Id}/hls1/{PlaylistId}/{SegmentId}.ts", "GET")]
public class GetHlsAudioSegment : StreamRequest
11 years ago
{
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
public class DynamicHlsService : BaseHlsService
{
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, IAuthorizationContext authorizationContext, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer, authorizationContext)
11 years ago
{
NetworkManager = networkManager;
11 years ago
}
10 years ago
protected INetworkManager NetworkManager { get; private set; }
public Task<object> Get(GetMasterHlsVideoPlaylist request)
10 years ago
{
return GetMasterPlaylistInternal(request, "GET");
}
public Task<object> Head(GetMasterHlsVideoPlaylist request)
{
return GetMasterPlaylistInternal(request, "HEAD");
11 years ago
}
public Task<object> Get(GetMasterHlsAudioPlaylist request)
{
return GetMasterPlaylistInternal(request, "GET");
}
public Task<object> Head(GetMasterHlsAudioPlaylist request)
{
return GetMasterPlaylistInternal(request, "HEAD");
}
public Task<object> Get(GetVariantHlsVideoPlaylist request)
{
return GetVariantPlaylistInternal(request, true, "main");
}
public Task<object> Get(GetVariantHlsAudioPlaylist request)
{
return GetVariantPlaylistInternal(request, false, "main");
}
public Task<object> Get(GetHlsVideoSegment request)
{
return GetDynamicSegment(request, request.SegmentId);
}
public Task<object> Get(GetHlsAudioSegment request)
11 years ago
{
10 years ago
return GetDynamicSegment(request, request.SegmentId);
11 years ago
}
private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
11 years ago
{
if ((request.StartTimeTicks ?? 0) > 0)
{
throw new ArgumentException("StartTimeTicks is not allowed.");
}
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
11 years ago
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
11 years ago
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
11 years ago
var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
var segmentExtension = GetSegmentFileExtension(state);
TranscodingJob job = null;
11 years ago
9 years ago
if (FileSystem.FileExists(segmentPath))
11 years ago
{
10 years ago
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
9 years ago
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
11 years ago
}
var transcodingLock = ApiEntryPoint.Instance.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
9 years ago
var released = false;
try
{
9 years ago
if (FileSystem.FileExists(segmentPath))
{
10 years ago
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
9 years ago
released = true;
9 years ago
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
else
{
10 years ago
var startTranscoding = false;
9 years ago
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
10 years ago
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
if (currentTranscodingIndex == null)
{
Logger.Debug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true;
}
else if (requestedIndex < currentTranscodingIndex.Value)
{
Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
startTranscoding = true;
}
else if (requestedIndex - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
10 years ago
{
Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", requestedIndex - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, requestedIndex);
10 years ago
startTranscoding = true;
}
if (startTranscoding)
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
if (currentTranscodingIndex.HasValue)
{
DeleteLastFile(playlistPath, segmentExtension, 0);
}
9 years ago
request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex);
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
}
catch
{
state.Dispose();
throw;
}
10 years ago
//await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
}
else
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job.TranscodingThrottler != null)
{
job.TranscodingThrottler.UnpauseTranscoding();
}
}
}
}
finally
11 years ago
{
9 years ago
if (!released)
{
transcodingLock.Release();
9 years ago
}
}
11 years ago
//Logger.Info("waiting for {0}", segmentPath);
//while (!File.Exists(segmentPath))
//{
// await Task.Delay(50, cancellationToken).ConfigureAwait(false);
//}
11 years ago
Logger.Info("returning {0}", segmentPath);
10 years ago
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
9 years ago
return await GetSegmentResult(state, playlistPath, segmentPath, requestedIndex, job, cancellationToken).ConfigureAwait(false);
}
private const int BufferSize = 81920;
9 years ago
private long GetStartPositionTicks(StreamState state, int requestedIndex)
10 years ago
{
double startSeconds = 0;
9 years ago
var lengths = GetSegmentLengths(state);
10 years ago
for (var i = 0; i < requestedIndex; i++)
{
9 years ago
startSeconds += lengths[requestedIndex];
}
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
return position;
}
10 years ago
9 years ago
private long GetEndPositionTicks(StreamState state, int requestedIndex)
{
double startSeconds = 0;
var lengths = GetSegmentLengths(state);
for (var i = 0; i <= requestedIndex; i++)
{
startSeconds += lengths[requestedIndex];
10 years ago
}
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
return position;
}
9 years ago
private double[] GetSegmentLengths(StreamState state)
{
var result = new List<double>();
var ticks = state.RunTimeTicks ?? 0;
var segmentLengthTicks = TimeSpan.FromSeconds(state.SegmentLength).Ticks;
while (ticks > 0)
{
var length = ticks >= segmentLengthTicks ? segmentLengthTicks : ticks;
result.Add(TimeSpan.FromTicks(length).TotalSeconds);
ticks -= length;
}
return result.ToArray();
}
9 years ago
public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
9 years ago
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited)
{
return null;
}
var file = GetLastTranscodingFile(playlist, segmentExtension, FileSystem);
if (file == null)
{
return null;
}
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, UsCulture);
}
private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, FileSystem);
if (file != null)
{
DeleteFile(file, retryCount);
}
}
private void DeleteFile(FileSystemMetadata file, int retryCount)
{
if (retryCount >= 5)
{
return;
}
10 years ago
try
{
FileSystem.DeleteFile(file.FullName);
}
catch (IOException ex)
{
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
var task = Task.Delay(100);
Task.WaitAll(task);
DeleteFile(file, retryCount + 1);
}
catch (Exception ex)
{
Logger.ErrorException("Error deleting partial stream file(s) {0}", ex, file.FullName);
}
}
private static FileSystemMetadata GetLastTranscodingFile(string playlist, string segmentExtension, IFileSystem fileSystem)
{
var folder = Path.GetDirectoryName(playlist);
var filePrefix = Path.GetFileNameWithoutExtension(playlist) ?? string.Empty;
try
{
9 years ago
return fileSystem.GetFiles(folder)
.Where(i => string.Equals(i.Extension, segmentExtension, StringComparison.OrdinalIgnoreCase) && Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(fileSystem.GetLastWriteTimeUtc)
.FirstOrDefault();
}
catch (IOException)
{
return null;
}
}
protected override int GetStartNumber(StreamState state)
{
return GetStartNumber(state.VideoRequest);
}
private int GetStartNumber(VideoStreamRequest request)
{
var segmentId = "0";
var segmentRequest = request as GetHlsVideoSegment;
if (segmentRequest != null)
{
segmentId = segmentRequest.SegmentId;
}
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
11 years ago
}
private string GetSegmentPath(StreamState state, string playlist, int index)
11 years ago
{
var folder = Path.GetDirectoryName(playlist);
var filename = Path.GetFileNameWithoutExtension(playlist);
return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state));
11 years ago
}
9 years ago
private async Task<object> GetSegmentResult(StreamState state, string playlistPath,
string segmentPath,
int segmentIndex,
TranscodingJob transcodingJob,
CancellationToken cancellationToken)
{
// If all transcoding has completed, just return immediately
9 years ago
if (transcodingJob != null && transcodingJob.HasExited && FileSystem.FileExists(segmentPath))
{
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
var segmentFilename = Path.GetFileName(segmentPath);
while (!cancellationToken.IsCancellationRequested)
{
try
10 years ago
{
using (var fileStream = GetPlaylistFileStream(playlistPath))
10 years ago
{
using (var reader = new StreamReader(fileStream, Encoding.UTF8, true, BufferSize))
10 years ago
{
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
// If it appears in the playlist, it's done
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
{
9 years ago
if (FileSystem.FileExists(segmentPath))
{
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
//break;
}
10 years ago
}
10 years ago
}
}
catch (IOException)
{
// May get an error if the file is locked
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false);
}
private Task<object> GetSegmentResult(StreamState state, string segmentPath, int index, TranscodingJob transcodingJob)
{
9 years ago
var segmentEndingPositionTicks = GetEndPositionTicks(state, index);
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
Path = segmentPath,
FileShare = FileShareMode.ReadWrite,
OnComplete = () =>
{
if (transcodingJob != null)
{
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
10 years ago
ApiEntryPoint.Instance.OnTranscodeEndRequest(transcodingJob);
}
}
});
}
private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
11 years ago
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
10 years ago
if (string.IsNullOrEmpty(request.MediaSourceId))
{
throw new ArgumentException("MediaSourceId is required");
}
var playlistText = string.Empty;
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase))
{
var audioBitrate = state.OutputAudioBitrate ?? 0;
var videoBitrate = state.OutputVideoBitrate ?? 0;
11 years ago
playlistText = GetMasterPlaylistFileText(state, videoBitrate + audioBitrate);
}
11 years ago
10 years ago
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
11 years ago
}
private string GetMasterPlaylistFileText(StreamState state, int totalBitrate)
11 years ago
{
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U");
9 years ago
var isLiveStream = IsLiveStream(state);
11 years ago
var queryStringIndex = Request.RawUrl.IndexOf('?');
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
// Main stream
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
playlistUrl += queryString;
var request = state.Request;
var subtitleStreams = state.MediaSource
.MediaStreams
.Where(i => i.IsTextSubtitleStream)
.ToList();
var subtitleGroup = subtitleStreams.Count > 0 &&
request is GetMasterHlsVideoPlaylist &&
(state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Hls || state.VideoRequest.EnableSubtitlesInManifest) ?
"subs" :
null;
// If we're burning in subtitles then don't add additional subs to the manifest
if (state.SubtitleStream != null && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode)
{
subtitleGroup = null;
}
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
AddSubtitles(state, subtitleStreams, builder);
}
9 years ago
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
{
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
10 years ago
// By default, vary by just 200k
var variation = GetBitrateVariation(totalBitrate);
10 years ago
var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
9 years ago
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
10 years ago
variation *= 2;
newBitrate = totalBitrate - variation;
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
9 years ago
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
11 years ago
return builder.ToString();
}
10 years ago
private string ReplaceBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(UsCulture),
"videobitrate=" + newValue.ToString(UsCulture),
StringComparison.OrdinalIgnoreCase);
}
private void AddSubtitles(StreamState state, IEnumerable<MediaStream> subtitles, StringBuilder builder)
{
var selectedIndex = state.SubtitleStream == null || state.VideoRequest.SubtitleMethod != SubtitleDeliveryMethod.Hls ? (int?)null : state.SubtitleStream.Index;
foreach (var stream in subtitles)
{
const string format = "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"{0}\",DEFAULT={1},FORCED={2},AUTOSELECT=YES,URI=\"{3}\",LANGUAGE=\"{4}\"";
var name = stream.DisplayTitle;
var isDefault = selectedIndex.HasValue && selectedIndex.Value == stream.Index;
var isForced = stream.IsForced;
var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
state.Request.MediaSourceId,
stream.Index.ToString(UsCulture),
30.ToString(UsCulture),
AuthorizationContext.GetAuthorizationInfo(Request).Token);
var line = string.Format(format,
name,
isDefault ? "YES" : "NO",
isForced ? "YES" : "NO",
url,
stream.Language ?? "Unknown");
builder.AppendLine(line);
}
}
private bool EnableAdaptiveBitrateStreaming(StreamState state, bool isLiveStream)
{
// Within the local network this will likely do more harm than good.
if (Request.IsLocal || NetworkManager.IsInLocalNetwork(Request.RemoteIp))
{
return false;
}
var request = state.Request as IMasterHlsRequest;
if (request != null && !request.EnableAdaptiveBitrateStreaming)
{
return false;
}
if (isLiveStream || string.IsNullOrWhiteSpace(state.MediaPath))
{
// Opening live streams is so slow it's not even worth it
return false;
}
10 years ago
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!state.IsOutputVideo)
{
return false;
}
10 years ago
// Having problems in android
return false;
//return state.VideoRequest.VideoBitRate.HasValue;
}
9 years ago
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
{
9 years ago
var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(UsCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(UsCulture);
// tvos wants resolution, codecs, framerate
//if (state.TargetFramerate.HasValue)
//{
// header += string.Format(",FRAME-RATE=\"{0}\"", state.TargetFramerate.Value.ToString(CultureInfo.InvariantCulture));
//}
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
header += string.Format(",SUBTITLES=\"{0}\"", subtitleGroup);
}
builder.AppendLine(header);
builder.AppendLine(url);
}
private int GetBitrateVariation(int bitrate)
{
10 years ago
// By default, vary by just 50k
var variation = 50000;
if (bitrate >= 10000000)
{
variation = 2000000;
}
else if (bitrate >= 5000000)
{
variation = 1500000;
}
else if (bitrate >= 3000000)
{
variation = 1000000;
}
else if (bitrate >= 2000000)
{
variation = 500000;
}
else if (bitrate >= 1000000)
{
variation = 300000;
}
else if (bitrate >= 600000)
{
variation = 200000;
}
10 years ago
else if (bitrate >= 400000)
{
variation = 100000;
}
return variation;
}
private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
11 years ago
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
9 years ago
var segmentLengths = GetSegmentLengths(state);
11 years ago
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U");
9 years ago
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
11 years ago
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(UsCulture));
11 years ago
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
var queryStringIndex = Request.RawUrl.IndexOf('?');
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
//if ((Request.UserAgent ?? string.Empty).IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1)
//{
// queryString = string.Empty;
//}
11 years ago
var index = 0;
9 years ago
foreach (var length in segmentLengths)
11 years ago
{
9 years ago
builder.AppendLine("#EXTINF:" + length.ToString("0.0000", UsCulture) + ",");
11 years ago
9 years ago
builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
11 years ago
name,
index.ToString(UsCulture),
GetSegmentFileExtension(isOutputVideo),
11 years ago
queryString));
index++;
}
builder.AppendLine("#EXT-X-ENDLIST");
var playlistText = builder.ToString();
10 years ago
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
11 years ago
}
protected override string GetAudioArguments(StreamState state)
{
var codec = GetAudioEncoder(state);
if (!state.IsOutputVideo)
{
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
{
return "-acodec copy";
}
var audioTranscodeParams = new List<string>();
audioTranscodeParams.Add("-acodec " + codec);
if (state.OutputAudioBitrate.HasValue)
{
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(UsCulture));
}
if (state.OutputAudioChannels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture));
}
if (state.OutputAudioSampleRate.HasValue)
{
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture));
}
audioTranscodeParams.Add("-vn");
return string.Join(" ", audioTranscodeParams.ToArray());
}
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
11 years ago
{
return "-codec:a:0 copy";
}
var args = "-codec:a:0 " + codec;
var channels = state.OutputAudioChannels;
11 years ago
if (channels.HasValue)
{
args += " -ac " + channels.Value;
}
11 years ago
var bitrate = state.OutputAudioBitrate;
11 years ago
if (bitrate.HasValue)
{
args += " -ab " + bitrate.Value.ToString(UsCulture);
11 years ago
}
args += " " + GetAudioFilterParam(state, true);
11 years ago
return args;
}
protected override string GetVideoArguments(StreamState state)
11 years ago
{
if (!state.IsOutputVideo)
{
return string.Empty;
}
var codec = GetVideoEncoder(state);
11 years ago
10 years ago
var args = "-codec:v:0 " + codec;
if (state.EnableMpegtsM2TsMode)
{
args += " -mpegts_m2ts_mode 1";
}
11 years ago
// See if we can save come cpu cycles by avoiding encoding
9 years ago
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
11 years ago
{
9 years ago
if (state.VideoStream != null && IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
9 years ago
{
args += " -bsf:v h264_mp4toannexb";
}
args += " -flags -global_header";
11 years ago
}
else
{
9 years ago
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
state.SegmentLength.ToString(UsCulture));
11 years ago
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
11 years ago
args += " " + GetVideoQualityParam(state, GetH264Encoder(state)) + keyFrameArg;
11 years ago
//args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
11 years ago
// Add resolution params, if specified
if (!hasGraphicalSubs)
{
args += GetOutputSizeParam(state, codec, EnableCopyTs(state));
}
10 years ago
// This is for internal graphical subs
if (hasGraphicalSubs)
{
args += GetGraphicalSubtitleParam(state, codec);
}
9 years ago
args += " -flags -global_header";
11 years ago
}
if (EnableCopyTs(state) && args.IndexOf("-copyts", StringComparison.OrdinalIgnoreCase) == -1)
{
args += " -copyts";
}
11 years ago
return args;
}
private bool EnableCopyTs(StreamState state)
{
//return state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.VideoRequest.SubtitleMethod == SubtitleDeliveryMethod.Encode;
return true;
}
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
{
var threads = GetNumberOfThreads(state, false);
10 years ago
var inputModifier = GetInputModifier(state, false);
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
var toTimeParam = string.Empty;
9 years ago
var timestampOffsetParam = string.Empty;
if (state.IsOutputVideo && !EnableCopyTs(state) && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && (state.Request.StartTimeTicks ?? 0) > 0)
{
timestampOffsetParam = " -output_ts_offset " + MediaEncoder.GetTimeParameter(state.Request.StartTimeTicks ?? 0);
}
9 years ago
var mapArgs = state.IsOutputVideo ? GetMapArgs(state) : string.Empty;
var enableSplittingOnNonKeyFrames = state.VideoRequest.EnableSplittingOnNonKeyFrames && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase);
// TODO: check libavformat version for 57 50.100 and use -hls_flags split_by_time
var hlsProtocolSupportsSplittingByTime = false;
if (enableSplittingOnNonKeyFrames && !hlsProtocolSupportsSplittingByTime)
{
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
return string.Format("{0} {10} {1} -map_metadata -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} -break_non_keyframes 1 -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
inputModifier,
GetInputArgument(state),
threads,
mapArgs,
GetVideoArguments(state),
GetAudioArguments(state),
state.SegmentLength.ToString(UsCulture),
startNumberParam,
outputPath,
outputTsArg,
toTimeParam
).Trim();
}
10 years ago
var splitByTime = hlsProtocolSupportsSplittingByTime && enableSplittingOnNonKeyFrames;
var splitByTimeArg = splitByTime ? " -hls_flags split_by_time" : "";
return string.Format("{0}{12} {1} -map_metadata -1 -threads {2} {3} {4}{5} {6} -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {7}{8} -start_number {9} -hls_list_size {10} -y \"{11}\"",
10 years ago
inputModifier,
GetInputArgument(state),
10 years ago
threads,
mapArgs,
10 years ago
GetVideoArguments(state),
timestampOffsetParam,
10 years ago
GetAudioArguments(state),
state.SegmentLength.ToString(UsCulture),
splitByTimeArg,
10 years ago
startNumberParam,
state.HlsListSize.ToString(UsCulture),
outputPath,
toTimeParam
10 years ago
).Trim();
}
11 years ago
/// <summary>
/// Gets the segment file extension.
/// </summary>
/// <param name="state">The state.</param>
/// <returns>System.String.</returns>
protected override string GetSegmentFileExtension(StreamState state)
{
return GetSegmentFileExtension(state.IsOutputVideo);
}
protected string GetSegmentFileExtension(bool isOutputVideo)
{
return isOutputVideo ? ".ts" : ".ts";
11 years ago
}
}
9 years ago
}