#514 - Support HLS seeking

This commit is contained in:
Luke Pulverenti 2014-07-01 17:13:32 -04:00
parent 970504abdf
commit 3bef6ead9c
16 changed files with 110 additions and 101 deletions

View File

@ -159,7 +159,7 @@ namespace MediaBrowser.Api
return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager));
}
protected IList<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
protected IEnumerable<BaseItem> GetAllLibraryItems(Guid? userId, IUserManager userManager, ILibraryManager libraryManager, string parentId = null)
{
if (!string.IsNullOrEmpty(parentId))
{
@ -169,7 +169,7 @@ namespace MediaBrowser.Api
{
var user = userManager.GetUserById(userId.Value);
return folder.GetRecursiveChildren(user).ToList();
return folder.GetRecursiveChildren(user);
}
return folder.GetRecursiveChildren();
@ -178,7 +178,7 @@ namespace MediaBrowser.Api
{
var user = userManager.GetUserById(userId.Value);
return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user, null);
return userManager.GetUserById(userId.Value).RootFolder.GetRecursiveChildren(user);
}
return libraryManager.RootFolder.GetRecursiveChildren();
@ -239,7 +239,8 @@ namespace MediaBrowser.Api
return name;
}
return libraryManager.RootFolder.GetRecursiveChildren(i => i is Game)
return libraryManager.RootFolder.GetRecursiveChildren()
.OfType<Game>()
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(i =>

View File

@ -381,7 +381,7 @@ namespace MediaBrowser.Api.DefaultTheme
var currentUser1 = user;
var ownedEpisodes = series
.SelectMany(i => i.GetRecursiveChildren(currentUser1, j => j.LocationType != LocationType.Virtual))
.SelectMany(i => i.GetRecursiveChildren(currentUser1).Where(j => j.LocationType != LocationType.Virtual))
.OfType<Episode>()
.ToList();

View File

@ -59,10 +59,11 @@ namespace MediaBrowser.Api.Playback.Hls
/// Processes the request.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="isLive">if set to <c>true</c> [is live].</param>
/// <returns>System.Object.</returns>
protected object ProcessRequest(StreamRequest request)
protected object ProcessRequest(StreamRequest request, bool isLive)
{
return ProcessRequestAsync(request).Result;
return ProcessRequestAsync(request, isLive).Result;
}
private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
@ -70,13 +71,12 @@ namespace MediaBrowser.Api.Playback.Hls
/// Processes the request async.
/// </summary>
/// <param name="request">The request.</param>
/// <param name="isLive">if set to <c>true</c> [is live].</param>
/// <returns>Task{System.Object}.</returns>
/// <exception cref="ArgumentException">
/// A video bitrate is required
/// <exception cref="ArgumentException">A video bitrate is required
/// or
/// An audio bitrate is required
/// </exception>
private async Task<object> ProcessRequestAsync(StreamRequest request)
/// An audio bitrate is required</exception>
private async Task<object> ProcessRequestAsync(StreamRequest request, bool isLive)
{
var cancellationTokenSource = new CancellationTokenSource();
@ -110,7 +110,8 @@ namespace MediaBrowser.Api.Playback.Hls
throw;
}
await WaitForMinimumSegmentCount(playlist, GetSegmentWait(), cancellationTokenSource.Token).ConfigureAwait(false);
var waitCount = isLive ? 1 : GetSegmentWait();
await WaitForMinimumSegmentCount(playlist, waitCount, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
finally
@ -119,6 +120,22 @@ namespace MediaBrowser.Api.Playback.Hls
}
}
if (isLive)
{
//var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
//file = Path.Combine(ServerConfigurationManager.ApplicationPaths.TranscodingTempPath, file);
try
{
return ResultFactory.GetStaticFileResult(Request, playlist, FileShare.ReadWrite);
}
finally
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
}
}
var audioBitrate = state.OutputAudioBitrate ?? 0;
var videoBitrate = state.OutputVideoBitrate ?? 0;
@ -188,16 +205,18 @@ namespace MediaBrowser.Api.Playback.Hls
protected async Task WaitForMinimumSegmentCount(string playlist, int segmentCount, CancellationToken cancellationToken)
{
var count = 0;
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 = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
{
using (var reader = new StreamReader(fileStream))
{
while (true)
{
if (!reader.EndOfStream)
var count = 0;
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync().ConfigureAwait(false);
@ -206,11 +225,12 @@ namespace MediaBrowser.Api.Playback.Hls
count++;
if (count >= segmentCount)
{
Logger.Debug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
return;
}
}
}
await Task.Delay(25, cancellationToken).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
}
@ -229,7 +249,7 @@ namespace MediaBrowser.Api.Playback.Hls
var itsOffsetMs = hlsVideoRequest == null
? 0
: ((GetHlsVideoStream)state.VideoRequest).TimeStampOffsetMs;
: hlsVideoRequest.TimeStampOffsetMs;
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
@ -240,7 +260,15 @@ namespace MediaBrowser.Api.Playback.Hls
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -sc_threshold 0 {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
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} -i {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),
@ -251,6 +279,7 @@ namespace MediaBrowser.Api.Playback.Hls
state.SegmentLength.ToString(UsCulture),
startNumberParam,
state.HlsListSize.ToString(UsCulture),
baseUrlParam,
outputPath
).Trim();

View File

@ -22,11 +22,6 @@ namespace MediaBrowser.Api.Playback.Hls
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetMasterHlsVideoStream : VideoStreamRequest
{
[ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? BaselineStreamAudioBitRate { get; set; }
[ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool AppendBaselineStream { get; set; }
}
[Route("/Videos/{Id}/main.m3u8", "GET")]
@ -35,12 +30,6 @@ namespace MediaBrowser.Api.Playback.Hls
{
}
[Route("/Videos/{Id}/baseline.m3u8", "GET")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetBaselineHlsVideoStream : VideoStreamRequest
{
}
/// <summary>
/// Class GetHlsVideoSegment
/// </summary>
@ -73,16 +62,11 @@ namespace MediaBrowser.Api.Playback.Hls
public object Get(GetDynamicHlsVideoSegment request)
{
if (string.Equals("baseline", request.PlaylistId, StringComparison.OrdinalIgnoreCase))
{
return GetDynamicSegment(request, false).Result;
}
return GetDynamicSegment(request, true).Result;
return GetDynamicSegment(request).Result;
}
private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request, bool isMain)
private async Task<object> GetDynamicSegment(GetDynamicHlsVideoSegment request)
{
if ((request.StartTimeTicks ?? 0) > 0)
{
@ -322,7 +306,9 @@ namespace MediaBrowser.Api.Playback.Hls
var queryString = queryStringIndex == -1 ? string.Empty : Request.RawUrl.Substring(queryStringIndex);
// Main stream
var playlistUrl = "main.m3u8" + queryString;
var playlistUrl = (state.RunTimeTicks ?? 0) > 0 ? "main.m3u8" : "live.m3u8";
playlistUrl += queryString;
AppendPlaylist(builder, playlistUrl, totalBitrate);
if (state.VideoRequest.VideoBitRate.HasValue)
@ -385,13 +371,6 @@ namespace MediaBrowser.Api.Playback.Hls
return result;
}
public object Get(GetBaselineHlsVideoStream request)
{
var result = GetPlaylistAsync(request, "baseline").Result;
return result;
}
private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
@ -506,14 +485,6 @@ namespace MediaBrowser.Api.Playback.Hls
/// <returns>System.String.</returns>
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
{
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);
@ -521,8 +492,7 @@ namespace MediaBrowser.Api.Playback.Hls
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
var args = string.Format("{0} {1} -i {2} -map_metadata -1 -threads {3} {4} {5} -flags -global_header {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
itsOffset,
var args = string.Format("{0} -i {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
inputModifier,
GetInputArgument(state),
threads,

View File

@ -32,6 +32,12 @@ namespace MediaBrowser.Api.Playback.Hls
public int TimeStampOffsetMs { get; set; }
}
[Route("/Videos/{Id}/live.m3u8", "GET")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetLiveHlsStream : VideoStreamRequest
{
}
/// <summary>
/// Class GetHlsVideoSegment
/// </summary>
@ -105,7 +111,12 @@ namespace MediaBrowser.Api.Playback.Hls
/// <returns>System.Object.</returns>
public object Get(GetHlsVideoStream request)
{
return ProcessRequest(request);
return ProcessRequest(request, false);
}
public object Get(GetLiveHlsStream request)
{
return ProcessRequest(request, true);
}
/// <summary>

View File

@ -192,14 +192,14 @@ namespace MediaBrowser.Api
{
result.Series = season.Series.Name;
result.EpisodeCount = season.GetRecursiveChildren(i => i is Episode).Count;
result.EpisodeCount = season.GetRecursiveChildren().Count(i => i is Episode);
}
var series = item as Series;
if (series != null)
{
result.EpisodeCount = series.GetRecursiveChildren(i => i is Episode).Count;
result.EpisodeCount = series.GetRecursiveChildren().Count(i => i is Episode);
}
var album = item as MusicAlbum;

View File

@ -78,8 +78,8 @@ namespace MediaBrowser.Api
var fields = request.GetItemFields().ToList();
var inputItems = user == null
? libraryManager.RootFolder.GetRecursiveChildren(i => i.Id != item.Id)
: user.RootFolder.GetRecursiveChildren(user, i => i.Id != item.Id);
? libraryManager.RootFolder.GetRecursiveChildren().Where(i => i.Id != item.Id)
: user.RootFolder.GetRecursiveChildren(user).Where(i => i.Id != item.Id);
var items = GetSimilaritems(item, inputItems.Where(includeInSearch), getSimilarityScore)
.ToList();

View File

@ -541,7 +541,8 @@ namespace MediaBrowser.Api.UserLibrary
if (series != null)
{
var dtos = series
.GetRecursiveChildren(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
.GetRecursiveChildren()
.Where(i => i is Episode && i.ParentIndexNumber.HasValue && i.ParentIndexNumber.Value == 0)
.OrderBy(i =>
{
if (i.PremiereDate.HasValue)

View File

@ -856,19 +856,6 @@ namespace MediaBrowser.Controller.Entities
/// <returns>IEnumerable{BaseItem}.</returns>
/// <exception cref="System.ArgumentNullException"></exception>
public IEnumerable<BaseItem> GetRecursiveChildren(User user, bool includeLinkedChildren = true)
{
return GetRecursiveChildren(user, null, includeLinkedChildren);
}
/// <summary>
/// Gets the recursive children.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="filter">The filter.</param>
/// <param name="includeLinkedChildren">if set to <c>true</c> [include linked children].</param>
/// <returns>IList{BaseItem}.</returns>
/// <exception cref="System.ArgumentNullException"></exception>
public IList<BaseItem> GetRecursiveChildren(User user, Func<BaseItem, bool> filter, bool includeLinkedChildren = true)
{
if (user == null)
{
@ -877,7 +864,7 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem>();
var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, true, filter);
var hasLinkedChildren = AddChildrenToList(user, includeLinkedChildren, list, true, null);
return hasLinkedChildren ? list.DistinctBy(i => i.Id).ToList() : list;
}
@ -887,20 +874,10 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <returns>IList{BaseItem}.</returns>
public IList<BaseItem> GetRecursiveChildren()
{
return GetRecursiveChildren(i => true);
}
/// <summary>
/// Gets the recursive children.
/// </summary>
/// <param name="filter">The filter.</param>
/// <returns>IEnumerable{BaseItem}.</returns>
public IList<BaseItem> GetRecursiveChildren(Func<BaseItem, bool> filter)
{
var list = new List<BaseItem>();
AddChildrenToList(list, true, filter);
AddChildrenToList(list, true, null);
return list;
}

View File

@ -18,11 +18,17 @@ namespace MediaBrowser.Controller.Entities
switch (ViewType)
{
case CollectionType.Games:
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren)).OfType<GameSystem>();
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
.OfType<GameSystem>();
case CollectionType.BoxSets:
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren)).OfType<BoxSet>();
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
.OfType<BoxSet>();
case CollectionType.TvShows:
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren)).OfType<Series>();
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
.OfType<Series>();
case CollectionType.Trailers:
return mediaFolders.SelectMany(i => i.GetRecursiveChildren(user, includeLinkedChildren))
.OfType<Trailer>();
default:
return mediaFolders.SelectMany(i => i.GetChildren(user, includeLinkedChildren));
}

View File

@ -217,6 +217,7 @@ namespace MediaBrowser.Model.ApiClient
/// Gets the additional parts.
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <param name="userId">The user identifier.</param>
/// <returns>Task{BaseItemDto[]}.</returns>
Task<ItemsResult> GetAdditionalParts(string itemId, string userId);
@ -241,6 +242,12 @@ namespace MediaBrowser.Model.ApiClient
/// <returns>Task{SessionInfoDto[]}.</returns>
Task<SessionInfoDto[]> GetClientSessionsAsync(SessionQuery query);
/// <summary>
/// Gets the client session asynchronous.
/// </summary>
/// <returns>Task{SessionInfoDto}.</returns>
Task<SessionInfoDto> GetCurrentSessionAsync();
/// <summary>
/// Gets the item counts async.
/// </summary>

View File

@ -93,8 +93,6 @@ namespace MediaBrowser.Model.Configuration
BlockUnratedItems = new UnratedItem[] { };
ExcludeFoldersFromGrouping = new string[] { };
DisplayCollectionsView = true;
}
}
}

View File

@ -1,5 +1,4 @@
using System;
using System.ComponentModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.Serialization;
@ -17,6 +16,12 @@ namespace MediaBrowser.Model.Dto
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>The identifier.</value>
public string Id { get; set; }
/// <summary>
/// Gets or sets the primary image tag.
/// </summary>

View File

@ -328,7 +328,8 @@ namespace MediaBrowser.Server.Implementations.Dto
if (!string.IsNullOrEmpty(item.Album))
{
var parentAlbum = _libraryManager.RootFolder
.GetRecursiveChildren(i => i is MusicAlbum)
.GetRecursiveChildren()
.Where(i => i is MusicAlbum)
.FirstOrDefault(i => string.Equals(i.Name, item.Album, StringComparison.OrdinalIgnoreCase));
if (parentAlbum != null)
@ -539,6 +540,7 @@ namespace MediaBrowser.Server.Implementations.Dto
if (dictionary.TryGetValue(studio, out entity))
{
studioDto.Id = entity.Id.ToString("N");
studioDto.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary);
}
@ -1248,7 +1250,8 @@ namespace MediaBrowser.Server.Implementations.Dto
}
else
{
children = folder.GetRecursiveChildren(user, i => !i.IsFolder && i.LocationType != LocationType.Virtual);
children = folder.GetRecursiveChildren(user)
.Where(i => !i.IsFolder && i.LocationType != LocationType.Virtual);
}
// Loop through each recursive child

View File

@ -41,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.Library
{
var user = _userManager.GetUserById(new Guid(query.UserId));
inputItems = user.RootFolder.GetRecursiveChildren(user, null);
inputItems = user.RootFolder.GetRecursiveChildren(user, true);
}

View File

@ -15,6 +15,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Server.Implementations.Library
{
@ -84,7 +85,7 @@ namespace MediaBrowser.Server.Implementations.Library
if (user.Configuration.DisplayCollectionsView ||
recursiveChildren.OfType<BoxSet>().Any())
{
list.Add(await GetUserView(CollectionType.BoxSets, user, "zzz_" + CollectionType.BoxSets, cancellationToken).ConfigureAwait(false));
list.Add(await GetUserView(CollectionType.BoxSets, user, CollectionType.BoxSets, cancellationToken).ConfigureAwait(false));
}
if (query.IncludeExternalContent)
@ -114,7 +115,7 @@ namespace MediaBrowser.Server.Implementations.Library
}
}
return list.OrderBy(i => i.SortName);
return _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).Cast<Folder>();
}
private Task<UserView> GetUserView(string type, User user, string sortName, CancellationToken cancellationToken)