Browse Source

add optional progress bar to generated song videos (#1945)

* optionally include progress bar in generated song video

* update progress bar size/location

* move everything up 10% when song progress is enabled

* add watermark border to song progress bar

* always show accurate progress bar

* lower progress bar to 90% alpha

* update changelog
pull/1946/head
Jason Dove 2 years ago committed by GitHub
parent
commit
c8ec87b01f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Channels/ChannelViewModel.cs
  3. 3
      ErsatzTV.Application/Channels/Commands/CreateChannel.cs
  4. 3
      ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs
  5. 3
      ErsatzTV.Application/Channels/Commands/UpdateChannel.cs
  6. 1
      ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs
  7. 3
      ErsatzTV.Application/Channels/Mapper.cs
  8. 19
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  9. 1
      ErsatzTV.Core/Domain/Channel.cs
  10. 7
      ErsatzTV.Core/Domain/ChannelSongVideoMode.cs
  11. 4
      ErsatzTV.Core/Domain/ChannelWatermark.cs
  12. 20
      ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs
  13. 9
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  14. 7
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  15. 16
      ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs
  16. 12
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  17. 6
      ErsatzTV.FFmpeg/FFmpegState.cs
  18. 32
      ErsatzTV.FFmpeg/Filter/SongProgressFilter.cs
  19. 2
      ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs
  20. 11
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  21. 2
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs
  22. 2
      ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs
  23. 2
      ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs
  24. 5872
      ErsatzTV.Infrastructure.MySql/Migrations/20241120195710_Add_Channel_SongVideoMode.Designer.cs
  25. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20241120195710_Add_Channel_SongVideoMode.cs
  26. 5
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  27. 5711
      ErsatzTV.Infrastructure.Sqlite/Migrations/20241120195625_Add_Channel_SongVideoMode.Designer.cs
  28. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20241120195625_Add_Channel_SongVideoMode.cs
  29. 5
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  30. 1
      ErsatzTV/ErsatzTV.csproj
  31. 5
      ErsatzTV/Pages/ChannelEditor.razor
  32. BIN
      ErsatzTV/Resources/song_progress_overlay.png
  33. 1
      ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
  34. 8
      ErsatzTV/ViewModels/ChannelEditViewModel.cs

3
CHANGELOG.md

@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `advance` option to `epg_group` YAML playout instruction - Add `advance` option to `epg_group` YAML playout instruction
- When set to `false`, this option will lock the guide group without starting a new guide group - When set to `false`, this option will lock the guide group without starting a new guide group
- This can be helpful for "post roll" items that should be part of the previous item's guide group - This can be helpful for "post roll" items that should be part of the previous item's guide group
- Add `Song Video Mode` to channel settings
- `Default` - existing behavior
- `With Progress` - show animated progress bar at bottom of generated video
### Changed ### Changed
- **BREAKING CHANGE**: Change channel identifiers used in XMLTV to work around bad behavior in Plex - **BREAKING CHANGE**: Change channel identifiers used in XMLTV to work around bad behavior in Plex

3
ErsatzTV.Application/Channels/ChannelViewModel.cs

@ -21,7 +21,8 @@ public record ChannelViewModel(
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode, ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode, ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) string MusicVideoCreditsTemplate,
ChannelSongVideoMode SongVideoMode)
{ {
public string WebEncodedName => WebUtility.UrlEncode(Name); public string WebEncodedName => WebUtility.UrlEncode(Name);
} }

3
ErsatzTV.Application/Channels/Commands/CreateChannel.cs

@ -19,4 +19,5 @@ public record CreateChannel(
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode, ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode, ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, CreateChannelResult>>; string MusicVideoCreditsTemplate,
ChannelSongVideoMode SongVideoMode) : IRequest<Either<BaseError, CreateChannelResult>>;

3
ErsatzTV.Application/Channels/Commands/CreateChannelHandler.cs

@ -76,7 +76,8 @@ public class CreateChannelHandler(
PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode = request.PreferredSubtitleLanguageCode,
SubtitleMode = request.SubtitleMode, SubtitleMode = request.SubtitleMode,
MusicVideoCreditsMode = request.MusicVideoCreditsMode, MusicVideoCreditsMode = request.MusicVideoCreditsMode,
MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate MusicVideoCreditsTemplate = request.MusicVideoCreditsTemplate,
SongVideoMode = request.SongVideoMode
}; };
foreach (int id in watermarkId) foreach (int id in watermarkId)

3
ErsatzTV.Application/Channels/Commands/UpdateChannel.cs

@ -20,4 +20,5 @@ public record UpdateChannel(
string PreferredSubtitleLanguageCode, string PreferredSubtitleLanguageCode,
ChannelSubtitleMode SubtitleMode, ChannelSubtitleMode SubtitleMode,
ChannelMusicVideoCreditsMode MusicVideoCreditsMode, ChannelMusicVideoCreditsMode MusicVideoCreditsMode,
string MusicVideoCreditsTemplate) : IRequest<Either<BaseError, ChannelViewModel>>; string MusicVideoCreditsTemplate,
ChannelSongVideoMode SongVideoMode) : IRequest<Either<BaseError, ChannelViewModel>>;

1
ErsatzTV.Application/Channels/Commands/UpdateChannelHandler.cs

@ -41,6 +41,7 @@ public class UpdateChannelHandler(
c.SubtitleMode = update.SubtitleMode; c.SubtitleMode = update.SubtitleMode;
c.MusicVideoCreditsMode = update.MusicVideoCreditsMode; c.MusicVideoCreditsMode = update.MusicVideoCreditsMode;
c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate; c.MusicVideoCreditsTemplate = update.MusicVideoCreditsTemplate;
c.SongVideoMode = update.SongVideoMode;
c.Artwork ??= new List<Artwork>(); c.Artwork ??= new List<Artwork>();
if (!string.IsNullOrWhiteSpace(update.Logo)) if (!string.IsNullOrWhiteSpace(update.Logo))

3
ErsatzTV.Application/Channels/Mapper.cs

@ -24,7 +24,8 @@ internal static class Mapper
channel.PreferredSubtitleLanguageCode, channel.PreferredSubtitleLanguageCode,
channel.SubtitleMode, channel.SubtitleMode,
channel.MusicVideoCreditsMode, channel.MusicVideoCreditsMode,
channel.MusicVideoCreditsTemplate); channel.MusicVideoCreditsTemplate,
channel.SongVideoMode);
internal static ChannelResponseModel ProjectToResponseModel(Channel channel) => internal static ChannelResponseModel ProjectToResponseModel(Channel channel) =>
new( new(

19
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -16,6 +16,7 @@ using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Streaming; using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Scheduling;
using ErsatzTV.FFmpeg.State;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -256,6 +257,24 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
ffmpegPath, ffmpegPath,
ffprobePath, ffprobePath,
cancellationToken); cancellationToken);
// override watermark as song_progress_overlay.png
if (videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true })
{
disableWatermarks = false;
playoutItemWatermark = new ChannelWatermark
{
Mode = ChannelWatermarkMode.Permanent,
Size = WatermarkSize.Scaled,
WidthPercent = 100,
HorizontalMarginPercent = 0,
VerticalMarginPercent = 0,
Opacity = 100,
Location = WatermarkLocation.TopLeft,
ImageSource = ChannelWatermarkImageSource.Resource,
Image = "song_progress_overlay.png"
};
}
} }
if (playoutItemWithPath.PlayoutItem.MediaItem is Image) if (playoutItemWithPath.PlayoutItem.MediaItem is Image)

1
ErsatzTV.Core/Domain/Channel.cs

@ -29,6 +29,7 @@ public class Channel
public ChannelSubtitleMode SubtitleMode { get; set; } public ChannelSubtitleMode SubtitleMode { get; set; }
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; } public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
public string MusicVideoCreditsTemplate { get; set; } public string MusicVideoCreditsTemplate { get; set; }
public ChannelSongVideoMode SongVideoMode { get; set; }
public ChannelProgressMode ProgressMode { get; set; } public ChannelProgressMode ProgressMode { get; set; }
public string WebEncodedName => WebUtility.UrlEncode(Name); public string WebEncodedName => WebUtility.UrlEncode(Name);
} }

7
ErsatzTV.Core/Domain/ChannelSongVideoMode.cs

@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum ChannelSongVideoMode
{
Default = 0,
WithProgress = 1
}

4
ErsatzTV.Core/Domain/ChannelWatermark.cs

@ -30,5 +30,7 @@ public enum ChannelWatermarkMode
public enum ChannelWatermarkImageSource public enum ChannelWatermarkImageSource
{ {
Custom = 0, Custom = 0,
ChannelLogo = 1 ChannelLogo = 1,
Resource = 100
} }

20
ErsatzTV.Core/Domain/MediaItem/BackgroundImageMediaVersion.cs

@ -5,27 +5,27 @@ namespace ErsatzTV.Core.Domain;
public class BackgroundImageMediaVersion : MediaVersion public class BackgroundImageMediaVersion : MediaVersion
{ {
public static BackgroundImageMediaVersion ForPath(string path, IDisplaySize resolution) => public static BackgroundImageMediaVersion ForPath(string path, IDisplaySize resolution, bool isSongWithProgress = false) =>
new() new()
{ {
Chapters = new List<MediaChapter>(), Chapters = [],
// image has been pre-generated with correct size // image has been pre-generated with correct size
Height = resolution.Height, Height = resolution.Height,
Width = resolution.Width, Width = resolution.Width,
SampleAspectRatio = "1:1", SampleAspectRatio = "1:1",
Streams = new List<MediaStream> Streams =
{ [
new() new MediaStream
{ {
MediaStreamKind = MediaStreamKind.Video, MediaStreamKind = MediaStreamKind.Video,
Index = 0, Index = 0,
Codec = VideoFormat.GeneratedImage, Codec = VideoFormat.GeneratedImage,
PixelFormat = new PixelFormatUnknown().Name // the resulting pixel format is unknown PixelFormat = new PixelFormatUnknown().Name // the resulting pixel format is unknown
} }
}, ],
MediaFiles = new List<MediaFile> MediaFiles = [new MediaFile { Path = path }],
{ IsSongWithProgress = isSongWithProgress
new() { Path = path }
}
}; };
public bool IsSongWithProgress { get; private set; }
} }

9
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -396,7 +396,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsSegmentTemplate, hlsSegmentTemplate,
ptsOffset, ptsOffset,
playbackSettings.ThreadCount, playbackSettings.ThreadCount,
qsvExtraHardwareFrames); qsvExtraHardwareFrames,
videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true });
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState); _logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
@ -547,7 +548,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsSegmentTemplate, hlsSegmentTemplate,
ptsOffset, ptsOffset,
Option<int>.None, Option<int>.None,
qsvExtraHardwareFrames); qsvExtraHardwareFrames,
IsSongWithProgress: false);
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video); var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video);
@ -737,7 +739,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
hlsSegmentTemplate, hlsSegmentTemplate,
0, 0,
playbackSettings.ThreadCount, playbackSettings.ThreadCount,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames)); Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
IsSongWithProgress: false);
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState); _logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);

7
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -179,6 +179,13 @@ public class FFmpegProcessService
{ {
switch (watermark.ImageSource) switch (watermark.ImageSource)
{ {
// used for song progress overlay
case ChannelWatermarkImageSource.Resource:
return new WatermarkOptions(
await watermarkOverride.IfNoneAsync(watermark),
Path.Combine(FileSystemLayout.ResourcesCacheFolder, watermark.Image),
Option<int>.None,
false);
case ChannelWatermarkImageSource.Custom: case ChannelWatermarkImageSource.Custom:
string customPath = _imageCache.GetPathForImage( string customPath = _imageCache.GetPathForImage(
watermark.Image, watermark.Image,

16
ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs

@ -64,12 +64,17 @@ public class SongVideoGenerator : ISongVideoGenerator
var boxBlur = false; var boxBlur = false;
const int HORIZONTAL_MARGIN_PERCENT = 3; const int HORIZONTAL_MARGIN_PERCENT = 3;
const int VERTICAL_MARGIN_PERCENT = 5; var verticalMarginPercent = 5;
const int WATERMARK_WIDTH_PERCENT = 25; const int WATERMARK_WIDTH_PERCENT = 25;
WatermarkLocation watermarkLocation = NextRandom(2) == 0 WatermarkLocation watermarkLocation = NextRandom(2) == 0
? WatermarkLocation.BottomLeft ? WatermarkLocation.BottomLeft
: WatermarkLocation.BottomRight; : WatermarkLocation.BottomRight;
if (channel.SongVideoMode is ChannelSongVideoMode.WithProgress)
{
verticalMarginPercent += 10;
}
foreach (SongMetadata metadata in song.SongMetadata) foreach (SongMetadata metadata in song.SongMetadata)
{ {
var fontSize = (int)Math.Round(channel.FFmpegProfile.Resolution.Height / 20.0); var fontSize = (int)Math.Round(channel.FFmpegProfile.Resolution.Height / 20.0);
@ -138,7 +143,7 @@ public class SongVideoGenerator : ISongVideoGenerator
var leftMargin = (int)Math.Round(leftMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Width); var leftMargin = (int)Math.Round(leftMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Width);
var rightMargin = (int)Math.Round(rightMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Width); var rightMargin = (int)Math.Round(rightMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Width);
var verticalMargin = var verticalMargin =
(int)Math.Round(VERTICAL_MARGIN_PERCENT / 100.0 * channel.FFmpegProfile.Resolution.Height); (int)Math.Round(verticalMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Height);
subtitleFile = await new SubtitleBuilder(_tempFilePool) subtitleFile = await new SubtitleBuilder(_tempFilePool)
.WithResolution(channel.FFmpegProfile.Resolution) .WithResolution(channel.FFmpegProfile.Resolution)
@ -225,14 +230,17 @@ public class SongVideoGenerator : ISongVideoGenerator
watermarkPath, watermarkPath,
watermarkLocation, watermarkLocation,
HORIZONTAL_MARGIN_PERCENT, HORIZONTAL_MARGIN_PERCENT,
VERTICAL_MARGIN_PERCENT, verticalMarginPercent,
WATERMARK_WIDTH_PERCENT, WATERMARK_WIDTH_PERCENT,
cancellationToken); cancellationToken);
foreach (string si in maybeSongImage.RightToSeq()) foreach (string si in maybeSongImage.RightToSeq())
{ {
videoPath = si; videoPath = si;
videoVersion = BackgroundImageMediaVersion.ForPath(si, channel.FFmpegProfile.Resolution); videoVersion = BackgroundImageMediaVersion.ForPath(
si,
channel.FFmpegProfile.Resolution,
isSongWithProgress: channel.SongVideoMode is ChannelSongVideoMode.WithProgress);
} }
return Tuple(videoPath, videoVersion); return Tuple(videoPath, videoVersion);

12
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -92,7 +92,8 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
0, 0,
Option<int>.None, Option<int>.None,
Option<int>.None); Option<int>.None,
false);
var builder = new SoftwarePipelineBuilder( var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(), new DefaultFFmpegCapabilities(),
@ -185,7 +186,8 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
0, 0,
Option<int>.None, Option<int>.None,
Option<int>.None); Option<int>.None,
false);
var builder = new SoftwarePipelineBuilder( var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(), new DefaultFFmpegCapabilities(),
@ -334,7 +336,8 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
0, 0,
Option<int>.None, Option<int>.None,
Option<int>.None); Option<int>.None,
false);
var builder = new SoftwarePipelineBuilder( var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(), new DefaultFFmpegCapabilities(),
@ -421,7 +424,8 @@ public class PipelineBuilderBaseTests
Option<string>.None, Option<string>.None,
0, 0,
Option<int>.None, Option<int>.None,
Option<int>.None); Option<int>.None,
false);
var builder = new SoftwarePipelineBuilder( var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(), new DefaultFFmpegCapabilities(),

6
ErsatzTV.FFmpeg/FFmpegState.cs

@ -21,7 +21,8 @@ public record FFmpegState(
Option<string> HlsSegmentTemplate, Option<string> HlsSegmentTemplate,
long PtsOffset, long PtsOffset,
Option<int> ThreadCount, Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames) Option<int> MaybeQsvExtraHardwareFrames,
bool IsSongWithProgress)
{ {
public int QsvExtraHardwareFrames => MaybeQsvExtraHardwareFrames.IfNone(64); public int QsvExtraHardwareFrames => MaybeQsvExtraHardwareFrames.IfNone(64);
@ -45,5 +46,6 @@ public record FFmpegState(
Option<string>.None, Option<string>.None,
0, 0,
Option<int>.None, Option<int>.None,
Option<int>.None); Option<int>.None,
false);
} }

32
ErsatzTV.FFmpeg/Filter/SongProgressFilter.cs

@ -0,0 +1,32 @@
namespace ErsatzTV.FFmpeg.Filter;
public class SongProgressFilter(FrameSize frameSize, Option<TimeSpan> maybeStart, Option<TimeSpan> maybeDuration) : BaseFilter
{
public override string Filter
{
get
{
foreach (TimeSpan duration in maybeDuration)
{
TimeSpan start = maybeStart.IfNone(TimeSpan.Zero);
TimeSpan finish = start + duration;
double width = frameSize.Width * 0.9;
double height = frameSize.Height * 0.025;
double seconds = duration.TotalSeconds;
double alreadyPlayed = start.TotalSeconds / finish.TotalSeconds;
double scale = 1 - alreadyPlayed;
var generateWhiteBar = $"color=c=white:s={width}x{height}";
var scaleToFullWidth = $"scale=iw*{alreadyPlayed}+iw*(t/{seconds})*{scale}:ih:eval=frame";
var overlayBar = "overlay=W*0.05:H-h-H*0.05:shortest=1:enable='gt(t,0.1)'";
return $"loop=-1:1[si],{generateWhiteBar},format=rgba,colorchannelmixer=aa=0.9,{scaleToFullWidth}[sbar];[si][sbar]{overlayBar}";
}
return string.Empty;
}
}
public override FrameState NextState(FrameState currentState) => currentState;
}

2
ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs

@ -170,7 +170,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState); currentState = SetScale(videoInputFile, videoStream, context, ffmpegState, desiredState, currentState);
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); currentState = SetPad(videoInputFile, videoStream, desiredState, currentState);
currentState = SetCrop(videoInputFile, desiredState, currentState); currentState = SetCrop(videoInputFile, desiredState, currentState);
SetStillImageLoop(videoInputFile, videoStream, desiredState, pipelineSteps); SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps);
if (currentState.BitDepth == 8 && context.HasSubtitleOverlay || context.HasWatermark) if (currentState.BitDepth == 8 && context.HasSubtitleOverlay || context.HasWatermark)
{ {

11
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -733,12 +733,21 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
protected static void SetStillImageLoop( protected static void SetStillImageLoop(
VideoInputFile videoInputFile, VideoInputFile videoInputFile,
VideoStream videoStream, VideoStream videoStream,
FFmpegState ffmpegState,
FrameState desiredState, FrameState desiredState,
ICollection<IPipelineStep> pipelineSteps) ICollection<IPipelineStep> pipelineSteps)
{ {
if (videoStream.StillImage) if (videoStream.StillImage)
{ {
videoInputFile.FilterSteps.Add(new LoopFilter()); if (ffmpegState.IsSongWithProgress)
{
videoInputFile.FilterSteps.Add(new SongProgressFilter(videoStream.FrameSize, ffmpegState.Start, ffmpegState.Finish));
}
else
{
videoInputFile.FilterSteps.Add(new LoopFilter());
}
if (desiredState.Realtime) if (desiredState.Realtime)
{ {
videoInputFile.FilterSteps.Add(new RealtimeFilter()); videoInputFile.FilterSteps.Add(new RealtimeFilter());

2
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -175,7 +175,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); currentState = SetPad(videoInputFile, videoStream, desiredState, currentState);
// _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat); // _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat);
currentState = SetCrop(videoInputFile, desiredState, currentState); currentState = SetCrop(videoInputFile, desiredState, currentState);
SetStillImageLoop(videoInputFile, videoStream, desiredState, pipelineSteps); SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps);
// need to download for any sort of overlay // need to download for any sort of overlay
if (currentState.FrameDataLocation == FrameDataLocation.Hardware && if (currentState.FrameDataLocation == FrameDataLocation.Hardware &&

2
ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs

@ -107,7 +107,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
currentState = SetScale(videoInputFile, videoStream, desiredState, currentState); currentState = SetScale(videoInputFile, videoStream, desiredState, currentState);
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState); currentState = SetPad(videoInputFile, videoStream, desiredState, currentState);
currentState = SetCrop(videoInputFile, desiredState, currentState); currentState = SetCrop(videoInputFile, desiredState, currentState);
SetStillImageLoop(videoInputFile, videoStream, desiredState, pipelineSteps); SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps);
SetSubtitle( SetSubtitle(
videoInputFile, videoInputFile,
subtitleInputFile, subtitleInputFile,

2
ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs

@ -179,7 +179,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
currentState = SetCrop(videoInputFile, desiredState, currentState); currentState = SetCrop(videoInputFile, desiredState, currentState);
SetStillImageLoop(videoInputFile, videoStream, desiredState, pipelineSteps); SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps);
// need to upload for hardware overlay // need to upload for hardware overlay
bool forceSoftwareOverlay = context is { HasSubtitleOverlay: true, HasWatermark: true } bool forceSoftwareOverlay = context is { HasSubtitleOverlay: true, HasWatermark: true }

5872
ErsatzTV.Infrastructure.MySql/Migrations/20241120195710_Add_Channel_SongVideoMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20241120195710_Add_Channel_SongVideoMode.cs

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_Channel_SongVideoMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SongVideoMode",
table: "Channel",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SongVideoMode",
table: "Channel");
}
}
}

5
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -17,7 +17,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.7") .HasAnnotation("ProductVersion", "8.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 64); .HasAnnotation("Relational:MaxIdentifierLength", 64);
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
@ -280,6 +280,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("ProgressMode") b.Property<int>("ProgressMode")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("SongVideoMode")
.HasColumnType("int");
b.Property<int>("StreamingMode") b.Property<int>("StreamingMode")
.HasColumnType("int"); .HasColumnType("int");

5711
ErsatzTV.Infrastructure.Sqlite/Migrations/20241120195625_Add_Channel_SongVideoMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20241120195625_Add_Channel_SongVideoMode.cs

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Channel_SongVideoMode : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "SongVideoMode",
table: "Channel",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SongVideoMode",
table: "Channel");
}
}
}

5
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); modelBuilder.HasAnnotation("ProductVersion", "8.0.10");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{ {
@ -267,6 +267,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("ProgressMode") b.Property<int>("ProgressMode")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("SongVideoMode")
.HasColumnType("INTEGER");
b.Property<int>("StreamingMode") b.Property<int>("StreamingMode")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

1
ErsatzTV/ErsatzTV.csproj

@ -71,6 +71,7 @@
<EmbeddedResource Include="Resources\Scripts\_threePartEpisodes.js" /> <EmbeddedResource Include="Resources\Scripts\_threePartEpisodes.js" />
<EmbeddedResource Include="Resources\Scripts\_episode.js" /> <EmbeddedResource Include="Resources\Scripts\_episode.js" />
<EmbeddedResource Include="Resources\Scripts\_movie.js" /> <EmbeddedResource Include="Resources\Scripts\_movie.js" />
<EmbeddedResource Include="Resources\song_progress_overlay.png" />
<EmbeddedResource Include="Resources\song_background_1.png" /> <EmbeddedResource Include="Resources\song_background_1.png" />
<EmbeddedResource Include="Resources\song_background_2.png" /> <EmbeddedResource Include="Resources\song_background_2.png" />
<EmbeddedResource Include="Resources\song_background_3.png" /> <EmbeddedResource Include="Resources\song_background_3.png" />

5
ErsatzTV/Pages/ChannelEditor.razor

@ -92,6 +92,10 @@
<MudSelectItem T="string" Value="@template">@template</MudSelectItem> <MudSelectItem T="string" Value="@template">@template</MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudSelect Class="mt-3" Label="Song Video Mode" @bind-Value="_model.SongVideoMode" For="@(() => _model.SongVideoMode)">
<MudSelectItem Value="@(ChannelSongVideoMode.Default)">Default</MudSelectItem>
<MudSelectItem Value="@(ChannelSongVideoMode.WithProgress)">With Progress</MudSelectItem>
</MudSelect>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center"> <MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6"> <MudItem xs="6">
<InputFile id="fileInput" OnChange="UploadLogo" hidden/> <InputFile id="fileInput" OnChange="UploadLogo" hidden/>
@ -194,6 +198,7 @@
_model.SubtitleMode = channelViewModel.SubtitleMode; _model.SubtitleMode = channelViewModel.SubtitleMode;
_model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode; _model.MusicVideoCreditsMode = channelViewModel.MusicVideoCreditsMode;
_model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate; _model.MusicVideoCreditsTemplate = channelViewModel.MusicVideoCreditsTemplate;
_model.SongVideoMode = channelViewModel.SongVideoMode;
}, },
() => NavigationManager.NavigateTo("404")); () => NavigationManager.NavigateTo("404"));
} }

BIN
ErsatzTV/Resources/song_progress_overlay.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

1
ErsatzTV/Services/RunOnce/ResourceExtractorService.cs

@ -17,6 +17,7 @@ public class ResourceExtractorService : BackgroundService
Assembly assembly = typeof(ResourceExtractorService).GetTypeInfo().Assembly; Assembly assembly = typeof(ResourceExtractorService).GetTypeInfo().Assembly;
await ExtractResource(assembly, "background.png", stoppingToken); await ExtractResource(assembly, "background.png", stoppingToken);
await ExtractResource(assembly, "song_progress_overlay.png", stoppingToken);
await ExtractResource(assembly, "song_background_1.png", stoppingToken); await ExtractResource(assembly, "song_background_1.png", stoppingToken);
await ExtractResource(assembly, "song_background_2.png", stoppingToken); await ExtractResource(assembly, "song_background_2.png", stoppingToken);
await ExtractResource(assembly, "song_background_3.png", stoppingToken); await ExtractResource(assembly, "song_background_3.png", stoppingToken);

8
ErsatzTV/ViewModels/ChannelEditViewModel.cs

@ -22,7 +22,6 @@ public class ChannelEditViewModel
public string PreferredSubtitleLanguageCode { get; set; } public string PreferredSubtitleLanguageCode { get; set; }
public ChannelSubtitleMode SubtitleMode { get; set; } public ChannelSubtitleMode SubtitleMode { get; set; }
public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; } public ChannelMusicVideoCreditsMode MusicVideoCreditsMode { get; set; }
public string MusicVideoCreditsTemplate public string MusicVideoCreditsTemplate
{ {
get => MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles get => MusicVideoCreditsMode == ChannelMusicVideoCreditsMode.GenerateSubtitles
@ -30,6 +29,7 @@ public class ChannelEditViewModel
: null; : null;
set => _musicVideoCreditsTemplate = value; set => _musicVideoCreditsTemplate = value;
} }
public ChannelSongVideoMode SongVideoMode { get; set; }
public UpdateChannel ToUpdate() => public UpdateChannel ToUpdate() =>
new( new(
@ -49,7 +49,8 @@ public class ChannelEditViewModel
PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode,
SubtitleMode, SubtitleMode,
MusicVideoCreditsMode, MusicVideoCreditsMode,
MusicVideoCreditsTemplate); MusicVideoCreditsTemplate,
SongVideoMode);
public CreateChannel ToCreate() => public CreateChannel ToCreate() =>
new( new(
@ -68,5 +69,6 @@ public class ChannelEditViewModel
PreferredSubtitleLanguageCode, PreferredSubtitleLanguageCode,
SubtitleMode, SubtitleMode,
MusicVideoCreditsMode, MusicVideoCreditsMode,
MusicVideoCreditsTemplate); MusicVideoCreditsTemplate,
SongVideoMode);
} }

Loading…
Cancel
Save