From e5531c5c252522a6d96c9cb6607e96ad65c21eae Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 14 Dec 2018 22:46:23 -0500 Subject: [PATCH 1/5] Lower the ffmpeg stop timeout duration As this was, ffmpeg would continue to transcode video for timerDuration seconds after stopping playback (i.e. returning to the menu). However, this 60s timeout was a little obnoxious. Tested a 100ms timeout but this ended up causing playback to constantly terminate. 2s seems like the sweet spot where playback works normally, but terminating quickly on stop. --- MediaBrowser.Api/ApiEntryPoint.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 5aa803b9b6..ef74aecb2a 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -421,12 +421,15 @@ namespace MediaBrowser.Api return; } + /* var timerDuration = 10000; if (job.Type != TranscodingJobType.Progressive) { timerDuration = 60000; } + */ + var timerDuration = 2000; job.PingTimeout = timerDuration; job.LastPingDate = DateTime.UtcNow; From fda3f0ad9ba93ab44f5754bb9dd3e6b81ccb85af Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 14 Dec 2018 23:04:04 -0500 Subject: [PATCH 2/5] Up the timerDuration back to 10s, 2s is too low --- MediaBrowser.Api/ApiEntryPoint.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index ef74aecb2a..b3f1923e5b 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -421,15 +421,14 @@ namespace MediaBrowser.Api return; } - /* var timerDuration = 10000; + /* if (job.Type != TranscodingJobType.Progressive) { timerDuration = 60000; } */ - var timerDuration = 2000; job.PingTimeout = timerDuration; job.LastPingDate = DateTime.UtcNow; From c3ade725dc8f863be91a743f68a4a2fd2bf3f911 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 14 Dec 2018 23:37:32 -0500 Subject: [PATCH 3/5] Restore timeout to 60s as it breaks --- MediaBrowser.Api/ApiEntryPoint.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index b3f1923e5b..5aa803b9b6 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -423,12 +423,10 @@ namespace MediaBrowser.Api var timerDuration = 10000; - /* if (job.Type != TranscodingJobType.Progressive) { timerDuration = 60000; } - */ job.PingTimeout = timerDuration; job.LastPingDate = DateTime.UtcNow; From d137977a6da7ec258a9a8a8f016036d109babbea Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Sat, 15 Dec 2018 00:17:07 -0500 Subject: [PATCH 4/5] Partial revert of 443356fd34c7b3377c41c1029c2973e9d61e0539 --- .../UserLibrary/PlaystateService.cs | 451 ++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 MediaBrowser.Api/UserLibrary/PlaystateService.cs diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs new file mode 100644 index 0000000000..98b4a5d5d8 --- /dev/null +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -0,0 +1,451 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Session; +using System; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Api.UserLibrary +{ + /// + /// Class MarkPlayedItem + /// + [Route("/Users/{UserId}/PlayedItems/{Id}", "POST", Summary = "Marks an item as played")] + public class MarkPlayedItem : IReturn + { + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + [ApiMember(Name = "DatePlayed", Description = "The date the item was played (if any). Format = yyyyMMddHHmmss", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string DatePlayed { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + } + + /// + /// Class MarkUnplayedItem + /// + [Route("/Users/{UserId}/PlayedItems/{Id}", "DELETE", Summary = "Marks an item as unplayed")] + public class MarkUnplayedItem : IReturn + { + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string UserId { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + } + + [Route("/Sessions/Playing", "POST", Summary = "Reports playback has started within a session")] + public class ReportPlaybackStart : PlaybackStartInfo, IReturnVoid + { + } + + [Route("/Sessions/Playing/Progress", "POST", Summary = "Reports playback progress within a session")] + public class ReportPlaybackProgress : PlaybackProgressInfo, IReturnVoid + { + } + + [Route("/Sessions/Playing/Ping", "POST", Summary = "Pings a playback session")] + public class PingPlaybackSession : IReturnVoid + { + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + } + + [Route("/Sessions/Playing/Stopped", "POST", Summary = "Reports playback has stopped within a session")] + public class ReportPlaybackStopped : PlaybackStopInfo, IReturnVoid + { + } + + /// + /// Class OnPlaybackStart + /// + [Route("/Users/{UserId}/PlayingItems/{Id}", "POST", Summary = "Reports that a user has begun playing an item")] + public class OnPlaybackStart : IReturnVoid + { + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MediaSourceId { get; set; } + + /// + /// Gets or sets a value indicating whether this is likes. + /// + /// true if likes; otherwise, false. + [ApiMember(Name = "CanSeek", Description = "Indicates if the client can seek", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool CanSeek { get; set; } + + [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? AudioStreamIndex { get; set; } + + [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? SubtitleStreamIndex { get; set; } + + [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public PlayMethod PlayMethod { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + } + + /// + /// Class OnPlaybackProgress + /// + [Route("/Users/{UserId}/PlayingItems/{Id}/Progress", "POST", Summary = "Reports a user's playback progress")] + public class OnPlaybackProgress : IReturnVoid + { + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string UserId { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] + public string MediaSourceId { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + [ApiMember(Name = "PositionTicks", Description = "Optional. The current position, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public long? PositionTicks { get; set; } + + [ApiMember(Name = "IsPaused", Description = "Indicates if the player is paused.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool IsPaused { get; set; } + + [ApiMember(Name = "IsMuted", Description = "Indicates if the player is muted.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "POST")] + public bool IsMuted { get; set; } + + [ApiMember(Name = "AudioStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? AudioStreamIndex { get; set; } + + [ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? SubtitleStreamIndex { get; set; } + + [ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")] + public int? VolumeLevel { get; set; } + + [ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public PlayMethod PlayMethod { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + + [ApiMember(Name = "RepeatMode", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public RepeatMode RepeatMode { get; set; } + } + + /// + /// Class OnPlaybackStopped + /// + [Route("/Users/{UserId}/PlayingItems/{Id}", "DELETE", Summary = "Reports that a user has stopped playing an item")] + public class OnPlaybackStopped : IReturnVoid + { + /// + /// Gets or sets the user id. + /// + /// The user id. + [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string UserId { get; set; } + + /// + /// Gets or sets the id. + /// + /// The id. + [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")] + public string Id { get; set; } + + [ApiMember(Name = "MediaSourceId", Description = "The id of the MediaSource", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string MediaSourceId { get; set; } + + [ApiMember(Name = "NextMediaType", Description = "The next media type that will play", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] + public string NextMediaType { get; set; } + + /// + /// Gets or sets the position ticks. + /// + /// The position ticks. + [ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")] + public long? PositionTicks { get; set; } + + [ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string LiveStreamId { get; set; } + + [ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] + public string PlaySessionId { get; set; } + } + + [Authenticated] + public class PlaystateService : BaseApiService + { + private readonly IUserManager _userManager; + private readonly IUserDataManager _userDataRepository; + private readonly ILibraryManager _libraryManager; + private readonly ISessionManager _sessionManager; + private readonly ISessionContext _sessionContext; + private readonly IAuthorizationContext _authContext; + + public PlaystateService(IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, ISessionManager sessionManager, ISessionContext sessionContext, IAuthorizationContext authContext) + { + _userManager = userManager; + _userDataRepository = userDataRepository; + _libraryManager = libraryManager; + _sessionManager = sessionManager; + _sessionContext = sessionContext; + _authContext = authContext; + } + + /// + /// Posts the specified request. + /// + /// The request. + public async Task Post(MarkPlayedItem request) + { + var result = await MarkPlayed(request).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + + private async Task MarkPlayed(MarkPlayedItem request) + { + var user = _userManager.GetUserById(request.UserId); + + DateTime? datePlayed = null; + + if (!string.IsNullOrEmpty(request.DatePlayed)) + { + datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); + } + + var session = await GetSession(_sessionContext).ConfigureAwait(false); + + var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false); + + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + + await UpdatePlayedStatus(additionalUser, request.Id, true, datePlayed).ConfigureAwait(false); + } + + return dto; + } + + private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId) + { + if (method == PlayMethod.Transcode) + { + var job = string.IsNullOrWhiteSpace(playSessionId) ? null : ApiEntryPoint.Instance.GetTranscodingJob(playSessionId); + if (job == null) + { + return PlayMethod.DirectPlay; + } + } + + return method; + } + + /// + /// Posts the specified request. + /// + /// The request. + public void Post(OnPlaybackStart request) + { + Post(new ReportPlaybackStart + { + CanSeek = request.CanSeek, + ItemId = request.Id, + MediaSourceId = request.MediaSourceId, + AudioStreamIndex = request.AudioStreamIndex, + SubtitleStreamIndex = request.SubtitleStreamIndex, + PlayMethod = request.PlayMethod, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId + }); + } + + public void Post(ReportPlaybackStart request) + { + request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId); + + request.SessionId = GetSession(_sessionContext).Result.Id; + + var task = _sessionManager.OnPlaybackStart(request); + + Task.WaitAll(task); + } + + /// + /// Posts the specified request. + /// + /// The request. + public void Post(OnPlaybackProgress request) + { + Post(new ReportPlaybackProgress + { + ItemId = request.Id, + PositionTicks = request.PositionTicks, + IsMuted = request.IsMuted, + IsPaused = request.IsPaused, + MediaSourceId = request.MediaSourceId, + AudioStreamIndex = request.AudioStreamIndex, + SubtitleStreamIndex = request.SubtitleStreamIndex, + VolumeLevel = request.VolumeLevel, + PlayMethod = request.PlayMethod, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId, + RepeatMode = request.RepeatMode + }); + } + + public void Post(ReportPlaybackProgress request) + { + request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId); + + request.SessionId = GetSession(_sessionContext).Result.Id; + + var task = _sessionManager.OnPlaybackProgress(request); + + Task.WaitAll(task); + } + + public void Post(PingPlaybackSession request) + { + ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId, null); + } + + /// + /// Posts the specified request. + /// + /// The request. + public void Delete(OnPlaybackStopped request) + { + Post(new ReportPlaybackStopped + { + ItemId = request.Id, + PositionTicks = request.PositionTicks, + MediaSourceId = request.MediaSourceId, + PlaySessionId = request.PlaySessionId, + LiveStreamId = request.LiveStreamId, + NextMediaType = request.NextMediaType + }); + } + + public void Post(ReportPlaybackStopped request) + { + Logger.Debug("ReportPlaybackStopped PlaySessionId: {0}", request.PlaySessionId ?? string.Empty); + + if (!string.IsNullOrWhiteSpace(request.PlaySessionId)) + { + ApiEntryPoint.Instance.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true); + } + + request.SessionId = GetSession(_sessionContext).Result.Id; + + var task = _sessionManager.OnPlaybackStopped(request); + + Task.WaitAll(task); + } + + /// + /// Deletes the specified request. + /// + /// The request. + public object Delete(MarkUnplayedItem request) + { + var task = MarkUnplayed(request); + + return ToOptimizedResult(task.Result); + } + + private async Task MarkUnplayed(MarkUnplayedItem request) + { + var user = _userManager.GetUserById(request.UserId); + + var session = await GetSession(_sessionContext).ConfigureAwait(false); + + var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false); + + foreach (var additionalUserInfo in session.AdditionalUsers) + { + var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId); + + await UpdatePlayedStatus(additionalUser, request.Id, false, null).ConfigureAwait(false); + } + + return dto; + } + + /// + /// Updates the played status. + /// + /// The user. + /// The item id. + /// if set to true [was played]. + /// The date played. + /// Task. + private async Task UpdatePlayedStatus(User user, string itemId, bool wasPlayed, DateTime? datePlayed) + { + var item = _libraryManager.GetItemById(itemId); + + if (wasPlayed) + { + await item.MarkPlayed(user, datePlayed, true).ConfigureAwait(false); + } + else + { + await item.MarkUnplayed(user).ConfigureAwait(false); + } + + return _userDataRepository.GetUserDataDto(item, user); + } + } +} \ No newline at end of file From bac06eea6e824a65ec4afae832111cc430bed018 Mon Sep 17 00:00:00 2001 From: Andrew Rabert Date: Sat, 15 Dec 2018 00:17:53 -0500 Subject: [PATCH 5/5] Adapt for Jellyfin --- .../UserLibrary/PlaystateService.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/MediaBrowser.Api/UserLibrary/PlaystateService.cs b/MediaBrowser.Api/UserLibrary/PlaystateService.cs index 98b4a5d5d8..2c514a1097 100644 --- a/MediaBrowser.Api/UserLibrary/PlaystateService.cs +++ b/MediaBrowser.Api/UserLibrary/PlaystateService.cs @@ -265,7 +265,7 @@ namespace MediaBrowser.Api.UserLibrary datePlayed = DateTime.ParseExact(request.DatePlayed, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal); } - var session = await GetSession(_sessionContext).ConfigureAwait(false); + var session = GetSession(_sessionContext); var dto = await UpdatePlayedStatus(user, request.Id, true, datePlayed).ConfigureAwait(false); @@ -302,7 +302,7 @@ namespace MediaBrowser.Api.UserLibrary Post(new ReportPlaybackStart { CanSeek = request.CanSeek, - ItemId = request.Id, + ItemId = new Guid(request.Id), MediaSourceId = request.MediaSourceId, AudioStreamIndex = request.AudioStreamIndex, SubtitleStreamIndex = request.SubtitleStreamIndex, @@ -316,7 +316,7 @@ namespace MediaBrowser.Api.UserLibrary { request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId); - request.SessionId = GetSession(_sessionContext).Result.Id; + request.SessionId = GetSession(_sessionContext).Id; var task = _sessionManager.OnPlaybackStart(request); @@ -331,7 +331,7 @@ namespace MediaBrowser.Api.UserLibrary { Post(new ReportPlaybackProgress { - ItemId = request.Id, + ItemId = new Guid(request.Id), PositionTicks = request.PositionTicks, IsMuted = request.IsMuted, IsPaused = request.IsPaused, @@ -350,7 +350,7 @@ namespace MediaBrowser.Api.UserLibrary { request.PlayMethod = ValidatePlayMethod(request.PlayMethod, request.PlaySessionId); - request.SessionId = GetSession(_sessionContext).Result.Id; + request.SessionId = GetSession(_sessionContext).Id; var task = _sessionManager.OnPlaybackProgress(request); @@ -370,7 +370,7 @@ namespace MediaBrowser.Api.UserLibrary { Post(new ReportPlaybackStopped { - ItemId = request.Id, + ItemId = new Guid(request.Id), PositionTicks = request.PositionTicks, MediaSourceId = request.MediaSourceId, PlaySessionId = request.PlaySessionId, @@ -388,7 +388,7 @@ namespace MediaBrowser.Api.UserLibrary ApiEntryPoint.Instance.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true); } - request.SessionId = GetSession(_sessionContext).Result.Id; + request.SessionId = GetSession(_sessionContext).Id; var task = _sessionManager.OnPlaybackStopped(request); @@ -410,7 +410,7 @@ namespace MediaBrowser.Api.UserLibrary { var user = _userManager.GetUserById(request.UserId); - var session = await GetSession(_sessionContext).ConfigureAwait(false); + var session = GetSession(_sessionContext); var dto = await UpdatePlayedStatus(user, request.Id, false, null).ConfigureAwait(false); @@ -438,14 +438,14 @@ namespace MediaBrowser.Api.UserLibrary if (wasPlayed) { - await item.MarkPlayed(user, datePlayed, true).ConfigureAwait(false); + item.MarkPlayed(user, datePlayed, true); } else { - await item.MarkUnplayed(user).ConfigureAwait(false); + item.MarkUnplayed(user); } return _userDataRepository.GetUserDataDto(item, user); } } -} \ No newline at end of file +}