Browse Source

feat: troubleshoot music video credits (#3017)

pull/3018/head
Jason Dove 5 days ago committed by GitHub
parent
commit
0840e0fb91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 95
      ErsatzTV.Application/Streaming/Queries/GetMusicVideoCreditsByPlayoutItemIdHandler.cs
  3. 6
      ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResultsHandler.cs
  4. 1
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs
  5. 57
      ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs
  6. 1
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlayback.cs
  7. 16
      ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs
  8. 28
      ErsatzTV.Core/FFmpeg/MusicVideoCreditsSubtitle.cs
  9. 19
      ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs
  10. 4
      ErsatzTV/Controllers/Api/TroubleshootController.cs
  11. 29
      ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

1
CHANGELOG.md

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Periodically delete unused artwork from cache folder on disk
- Probe Dolby Vision and HDR10 metadata in local library content
- All HDR content in local libraries will be re-scanned once after updating to refresh this metadata
- Add music video credits drop down to playback troubleshooter
### Changed
- Next engine:

95
ErsatzTV.Application/Streaming/Queries/GetMusicVideoCreditsByPlayoutItemIdHandler.cs

@ -1,6 +1,9 @@ @@ -1,6 +1,9 @@
using System.IO.Abstractions;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -10,6 +13,8 @@ namespace ErsatzTV.Application.Streaming; @@ -10,6 +13,8 @@ namespace ErsatzTV.Application.Streaming;
public class GetMusicVideoCreditsByPlayoutItemIdHandler(
IDbContextFactory<TvContext> dbContextFactory,
IMusicVideoCreditsGenerator musicVideoCreditsGenerator,
ITroubleshootingPlayoutItemStore troubleshootingPlayoutItemStore,
IFileSystem fileSystem,
ILogger<GetMusicVideoCreditsByPlayoutItemIdHandler> logger)
: IRequestHandler<GetMusicVideoCreditsByPlayoutItemId, Option<string>>
{
@ -17,6 +22,30 @@ public class GetMusicVideoCreditsByPlayoutItemIdHandler( @@ -17,6 +22,30 @@ public class GetMusicVideoCreditsByPlayoutItemIdHandler(
GetMusicVideoCreditsByPlayoutItemId request,
CancellationToken cancellationToken)
{
if (request.PlayoutItemId == MusicVideoCreditsSubtitle.TroubleshootingPlayoutItemId)
{
foreach (TroubleshootingPlayoutItem item in troubleshootingPlayoutItemStore.Current())
{
if (item.PlayoutItem.MediaItem is not MusicVideo musicVideo)
{
return None;
}
Option<string> maybePath = await Generate(musicVideo, item.Channel, request.SeekToMs);
foreach (string path in maybePath)
{
fileSystem.File.Copy(
path,
Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "music-video-credits.ass"),
overwrite: true);
}
return maybePath;
}
return None;
}
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<PlayoutItem> maybePlayoutItem = await dbContext.PlayoutItems
@ -49,47 +78,47 @@ public class GetMusicVideoCreditsByPlayoutItemIdHandler( @@ -49,47 +78,47 @@ public class GetMusicVideoCreditsByPlayoutItemIdHandler(
.SingleOrDefaultAsync(pi => pi.Id == request.PlayoutItemId, cancellationToken)
.Map(Optional);
var subtitles = new List<Subtitle>();
foreach (PlayoutItem playoutItem in maybePlayoutItem)
{
if (playoutItem.MediaItem is not MusicVideo musicVideo)
if (playoutItem.MediaItem is MusicVideo musicVideo)
{
break;
return await Generate(musicVideo, playoutItem.Playout.Channel, request.SeekToMs);
}
}
switch (playoutItem.Playout.Channel.MusicVideoCreditsMode)
{
case ChannelMusicVideoCreditsMode.GenerateSubtitles:
string templateName = playoutItem.Playout.Channel.MusicVideoCreditsTemplate;
if (!string.IsNullOrWhiteSpace(templateName))
{
var fileWithExtension = $"{templateName}.sbntxt";
subtitles.AddRange(
await musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate(
musicVideo,
playoutItem.Playout.Channel.FFmpegProfile,
request.SeekToMs.Map(TimeSpan.FromMilliseconds),
Path.Combine(FileSystemLayout.MusicVideoCreditsTemplatesFolder, fileWithExtension)));
}
else
{
logger.LogWarning(
"Music video credits template {Template} does not exist; falling back to built-in template",
templateName);
return None;
}
subtitles.AddRange(
await musicVideoCreditsGenerator.GenerateCreditsSubtitle(
musicVideo,
playoutItem.Playout.Channel.FFmpegProfile));
}
private async Task<Option<string>> Generate(MusicVideo musicVideo, Channel channel, Option<long> seekToMs)
{
if (channel.MusicVideoCreditsMode is not ChannelMusicVideoCreditsMode.GenerateSubtitles)
{
return None;
}
break;
case ChannelMusicVideoCreditsMode.None:
default:
break;
}
Option<Subtitle> maybeSubtitle;
string templateName = channel.MusicVideoCreditsTemplate;
if (!string.IsNullOrWhiteSpace(templateName))
{
var fileWithExtension = $"{templateName}.sbntxt";
maybeSubtitle = await musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate(
musicVideo,
channel.FFmpegProfile,
seekToMs.Map(TimeSpan.FromMilliseconds),
Path.Combine(FileSystemLayout.MusicVideoCreditsTemplatesFolder, fileWithExtension));
}
else
{
logger.LogWarning(
"Music video credits template {Template} does not exist; falling back to built-in template",
templateName);
maybeSubtitle = await musicVideoCreditsGenerator.GenerateCreditsSubtitle(
musicVideo,
channel.FFmpegProfile);
}
return subtitles.HeadOrNone().Map(s => s.Path);
return maybeSubtitle.Map(s => s.Path);
}
}

6
ErsatzTV.Application/Troubleshooting/Commands/ArchiveTroubleshootingResultsHandler.cs

@ -57,6 +57,12 @@ public class ArchiveTroubleshootingResultsHandler(ILocalFileSystem localFileSyst @@ -57,6 +57,12 @@ public class ArchiveTroubleshootingResultsHandler(ILocalFileSystem localFileSyst
continue;
}
if (fileName.Contains("music-video-credits", StringComparison.OrdinalIgnoreCase))
{
zipArchive.CreateEntryFromFile(file, fileName);
continue;
}
if (fileName.Contains("ffreport", StringComparison.OrdinalIgnoreCase))
{
hasReport = true;

1
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlayback.cs

@ -15,6 +15,7 @@ public record PrepareTroubleshootingPlayback( @@ -15,6 +15,7 @@ public record PrepareTroubleshootingPlayback(
List<int> WatermarkIds,
List<int> GraphicsElementIds,
int? SubtitleId,
string MusicVideoCreditsTemplate,
Option<int> SeekSeconds,
Option<DateTimeOffset> Start)
: IRequest<Either<BaseError, PlayoutItemResult>>;

57
ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs

@ -39,6 +39,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -39,6 +39,7 @@ public class PrepareTroubleshootingPlaybackHandler(
IFileSystem fileSystem,
ILocalFileSystem localFileSystem,
ISongVideoGenerator songVideoGenerator,
IMusicVideoCreditsGenerator musicVideoCreditsGenerator,
IWatermarkSelector watermarkSelector,
IEntityLocker entityLocker,
IChannelConfigConverter channelConfigConverter,
@ -210,6 +211,12 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -210,6 +211,12 @@ public class PrepareTroubleshootingPlaybackHandler(
channel.StreamSelector = request.StreamSelector;
}
if (mediaItem is MusicVideo && !string.IsNullOrWhiteSpace(request.MusicVideoCreditsTemplate))
{
channel.MusicVideoCreditsMode = ChannelMusicVideoCreditsMode.GenerateSubtitles;
channel.MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate;
}
MediaVersion version = mediaItem.GetHeadVersion();
var duration = TimeSpan.FromSeconds(Math.Min(version.Duration.TotalSeconds, 30));
@ -371,7 +378,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -371,7 +378,7 @@ public class PrepareTroubleshootingPlaybackHandler(
[],
TimeSpan.Zero,
playoutItem,
await GetSubtitles(mediaItem, request),
await GetNextSubtitles(mediaItem, channel, request, inPoint),
shouldLogMessages: true,
cancellationToken);
@ -496,7 +503,7 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -496,7 +503,7 @@ public class PrepareTroubleshootingPlaybackHandler(
new MediaItemAudioVersion(mediaItem, version),
videoPath,
mediaPath,
_ => GetSubtitles(mediaItem, request),
settings => GetLegacySubtitles(mediaItem, channel, request, settings),
string.Empty,
string.Empty,
string.Empty,
@ -526,6 +533,52 @@ public class PrepareTroubleshootingPlaybackHandler( @@ -526,6 +533,52 @@ public class PrepareTroubleshootingPlaybackHandler(
return playoutItemResult;
}
private async Task<List<Subtitle>> GetLegacySubtitles(
MediaItem mediaItem,
Channel channel,
PrepareTroubleshootingPlayback request,
FFmpegPlaybackSettings settings)
{
if (mediaItem is not MusicVideo musicVideo ||
channel.MusicVideoCreditsMode is not ChannelMusicVideoCreditsMode.GenerateSubtitles)
{
return await GetSubtitles(mediaItem, request);
}
Option<Subtitle> maybeSubtitle = await musicVideoCreditsGenerator.GenerateCreditsSubtitleFromTemplate(
musicVideo,
channel.FFmpegProfile,
settings.StreamSeek,
Path.Combine(
FileSystemLayout.MusicVideoCreditsTemplatesFolder,
$"{channel.MusicVideoCreditsTemplate}.sbntxt"));
foreach (Subtitle subtitle in maybeSubtitle)
{
_fileSystem.File.Copy(
subtitle.Path,
_fileSystem.Path.Combine(FileSystemLayout.TranscodeTroubleshootingFolder, "music-video-credits.ass"),
overwrite: true);
}
return maybeSubtitle.ToSeq().ToList();
}
private static async Task<List<Subtitle>> GetNextSubtitles(
MediaItem mediaItem,
Channel channel,
PrepareTroubleshootingPlayback request,
TimeSpan inPoint)
{
if (mediaItem is MusicVideo && channel.MusicVideoCreditsMode is ChannelMusicVideoCreditsMode.GenerateSubtitles)
{
// next fetches the credits over http; the endpoint resolves the troubleshooting playout item from the store
return [MusicVideoCreditsSubtitle.ForPlayoutItem(MusicVideoCreditsSubtitle.TroubleshootingPlayoutItemId, inPoint)];
}
return await GetSubtitles(mediaItem, request);
}
private static async Task<List<Subtitle>> GetSubtitles(MediaItem mediaItem, PrepareTroubleshootingPlayback request)
{
List<Subtitle> allSubtitles = mediaItem switch

1
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlayback.cs

@ -8,6 +8,7 @@ public record StartTroubleshootingPlayback( @@ -8,6 +8,7 @@ public record StartTroubleshootingPlayback(
Guid SessionId,
StreamingEngine StreamingEngine,
string StreamSelector,
string MusicVideoCreditsTemplate,
PlayoutItemResult PlayoutItemResult,
Option<MediaItemInfo> MediaItemInfo,
TroubleshootingInfo TroubleshootingInfo) : IRequest, IFFmpegWorkerRequest;

16
ErsatzTV.Application/Troubleshooting/Commands/StartTroubleshootingPlaybackHandler.cs

@ -94,6 +94,22 @@ public class StartTroubleshootingPlaybackHandler( @@ -94,6 +94,22 @@ public class StartTroubleshootingPlaybackHandler(
}
}
// write music video credits template
if (!string.IsNullOrWhiteSpace(request.MusicVideoCreditsTemplate))
{
string fullPath = Path.Combine(
FileSystemLayout.MusicVideoCreditsTemplatesFolder,
$"{request.MusicVideoCreditsTemplate}.sbntxt");
if (File.Exists(fullPath))
{
File.Copy(
fullPath,
Path.Combine(
FileSystemLayout.TranscodeTroubleshootingFolder,
"music-video-credits-template.sbntxt"));
}
}
HardwareAccelerationKind hwAccel = request.TroubleshootingInfo.FFmpegProfiles.Head().HardwareAcceleration;
if (hwAccel is HardwareAccelerationKind.Qsv)
{

28
ErsatzTV.Core/FFmpeg/MusicVideoCreditsSubtitle.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.FFmpeg;
public static class MusicVideoCreditsSubtitle
{
// troubleshooting playout items never reach the database, so the credits endpoint
// resolves this id from the troubleshooting playout item store instead
public const int TroubleshootingPlayoutItemId = 0;
public static Subtitle ForPlayoutItem(int playoutItemId, TimeSpan inPoint)
{
string seekToMs = inPoint > TimeSpan.Zero
? $"?seekToMs={(long)inPoint.TotalMilliseconds}"
: string.Empty;
return new Subtitle
{
Codec = "ass",
Default = true,
Forced = true,
IsExtracted = false,
SubtitleKind = SubtitleKind.Generated,
Path = $"http://localhost:{Settings.StreamingPort}/internal/ffmpeg/music-video-credits/{playoutItemId}{seekToMs}",
SDH = false
};
}
}

19
ErsatzTV.Infrastructure/Scheduling/PlayoutItemConverter.cs

@ -678,24 +678,7 @@ public class PlayoutItemConverter( @@ -678,24 +678,7 @@ public class PlayoutItemConverter(
return [];
}
string seekToMs = playoutItemInPoint > TimeSpan.Zero
? $"?seekToMs={(long)playoutItemInPoint.TotalMilliseconds}"
: string.Empty;
return
[
new Subtitle
{
Codec = "ass",
Default = true,
Forced = true,
IsExtracted = false,
SubtitleKind = SubtitleKind.Generated,
Path =
$"http://localhost:{Settings.StreamingPort}/internal/ffmpeg/music-video-credits/{playoutItemId}{seekToMs}",
SDH = false
}
];
return [MusicVideoCreditsSubtitle.ForPlayoutItem(playoutItemId, playoutItemInPoint)];
}
private static void SetInOutPoints(PlayoutItem playoutItem, Core.Next.Source source)

4
ErsatzTV/Controllers/Api/TroubleshootController.cs

@ -47,6 +47,8 @@ public class TroubleshootController( @@ -47,6 +47,8 @@ public class TroubleshootController(
[FromQuery]
int? subtitleId,
[FromQuery]
string musicVideoCreditsTemplate,
[FromQuery]
int seekSeconds,
[FromQuery]
DateTimeOffset? start,
@ -71,6 +73,7 @@ public class TroubleshootController( @@ -71,6 +73,7 @@ public class TroubleshootController(
watermark,
graphicsElement,
subtitleId,
musicVideoCreditsTemplate,
ss,
Optional(start)),
cancellationToken);
@ -104,6 +107,7 @@ public class TroubleshootController( @@ -104,6 +107,7 @@ public class TroubleshootController(
sessionId,
streamingEngine,
streamSelector,
musicVideoCreditsTemplate,
playoutItemResult,
maybeMediaInfo.ToOption(),
troubleshootingInfo),

29
ErsatzTV/Pages/Troubleshooting/PlaybackTroubleshooting.razor

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
@using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Application.Graphics
@using ErsatzTV.Application.MediaItems
@using ErsatzTV.Application.Templates
@using ErsatzTV.Application.Troubleshooting
@using ErsatzTV.Application.Troubleshooting.Queries
@using ErsatzTV.Application.Watermarks
@ -115,6 +116,21 @@ @@ -115,6 +116,21 @@
}
</MudSelect>
</MudStack>
@if (_isMusicVideo)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Music Video Credits</MudText>
</div>
<MudSelect @bind-Value="_musicVideoCreditsTemplate" For="@(() => _musicVideoCreditsTemplate)" Clearable="true">
<MudSelectItem T="string" Value="@((string)null)">(none)</MudSelectItem>
@foreach (string template in _musicVideoCreditsTemplates)
{
<MudSelectItem T="string" Value="@template">@template</MudSelectItem>
}
</MudSelect>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Watermarks</MudText>
@ -210,6 +226,7 @@ @@ -210,6 +226,7 @@
private readonly List<WatermarkViewModel> _watermarks = [];
private readonly List<SubtitleViewModel> _subtitleStreams = [];
private readonly List<GraphicsElementViewModel> _graphicsElements = [];
private readonly List<string> _musicVideoCreditsTemplates = [];
private string _title;
private MediaItemInfo _info;
private readonly StreamingMode _streamingMode = StreamingMode.HttpLiveStreamingSegmenter;
@ -221,6 +238,8 @@ @@ -221,6 +238,8 @@
private IReadOnlyCollection<string> _graphicsElementNames = new System.Collections.Generic.HashSet<string>();
private bool _startFromBeginning;
private int? _subtitleId;
private bool _isMusicVideo;
private string _musicVideoCreditsTemplate;
private int _seekSeconds;
private bool _hasPlayed;
private double? _lastSpeed;
@ -273,6 +292,9 @@ @@ -273,6 +292,9 @@
_graphicsElements.Clear();
_graphicsElements.AddRange(await Mediator.Send(new GetAllGraphicsElements(), token));
_musicVideoCreditsTemplates.Clear();
_musicVideoCreditsTemplates.AddRange(await Mediator.Send(new GetMusicVideoCreditTemplates(), token));
if (MediaItemId is not null)
{
_channelMode = false;
@ -353,6 +375,11 @@ @@ -353,6 +375,11 @@
}
}
if (_isMusicVideo && !string.IsNullOrWhiteSpace(_musicVideoCreditsTemplate))
{
queryString.Add(new KeyValuePair<string, string>("musicVideoCreditsTemplate", _musicVideoCreditsTemplate));
}
if (!string.IsNullOrWhiteSpace(_streamSelector))
{
queryString.Add(new KeyValuePair<string, string>("streamSelector", _streamSelector));
@ -383,6 +410,8 @@ @@ -383,6 +410,8 @@
IEnumerable<char> kindString = info.Kind.SelectMany((c, i) => i != 0 && char.IsUpper(c) && !char.IsUpper(info.Kind[i - 1]) ? new[] { ' ', c } : new[] { c });
_info = info with { Kind = new string(kindString.ToArray()) };
_title = info.Title;
_isMusicVideo = string.Equals(info.Kind, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase);
_musicVideoCreditsTemplate = null;
OnStartFromBeginningChanged(string.Equals(info.Kind, "RemoteStream", StringComparison.OrdinalIgnoreCase));

Loading…
Cancel
Save