Browse Source

optionally place watermark within source content (#986)

pull/987/head
Jason Dove 3 years ago committed by GitHub
parent
commit
6f892bea6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs
  3. 3
      ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs
  4. 3
      ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs
  5. 3
      ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs
  6. 3
      ErsatzTV.Application/Watermarks/Mapper.cs
  7. 2
      ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksHandler.cs
  8. 3
      ErsatzTV.Application/Watermarks/WatermarkViewModel.cs
  9. 1
      ErsatzTV.Core/Domain/ChannelWatermark.cs
  10. 3
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  11. 6
      ErsatzTV.FFmpeg.Tests/Filter/WatermarkOpacityFilterTests.cs
  12. 45
      ErsatzTV.FFmpeg/Filter/OverlayWatermarkFilter.cs
  13. 3
      ErsatzTV.FFmpeg/State/WatermarkState.cs
  14. 4312
      ErsatzTV.Infrastructure/Migrations/20221009130454_Add_WatermarkPlaceWithinSourceContent.Designer.cs
  15. 26
      ErsatzTV.Infrastructure/Migrations/20221009130454_Add_WatermarkPlaceWithinSourceContent.cs
  16. 3
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  17. 43
      ErsatzTV/Pages/WatermarkEditor.razor
  18. 10
      ErsatzTV/ViewModels/WatermarkEditViewModel.cs

4
CHANGELOG.md

@ -27,6 +27,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -27,6 +27,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `duration`: the timespan duration of the music video, which can be used to calculate timing of additional subtitles
- `stream_seek`: the timespan that ffmpeg will seek into the media item before beginning playback
### Changed
- No longer place watermarks within content by default (e.g. within 4:3 content padded to a 16:9 resolution)
- This can be re-enabled if desired using the `Place Within Source Content` checkbox in watermark settings
## [0.6.8-beta] - 2022-10-05
### Fixed
- Fix typo introduced in `0.6.7-beta` that stopped QSV HEVC encoder from working

3
ErsatzTV.Application/Watermarks/Commands/CreateWatermark.cs

@ -16,6 +16,7 @@ public record CreateWatermark( @@ -16,6 +16,7 @@ public record CreateWatermark(
int VerticalMargin,
int FrequencyMinutes,
int DurationSeconds,
int Opacity) : IRequest<Either<BaseError, CreateWatermarkResult>>;
int Opacity,
bool PlaceWithinSourceContent) : IRequest<Either<BaseError, CreateWatermarkResult>>;
public record CreateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId);

3
ErsatzTV.Application/Watermarks/Commands/CreateWatermarkHandler.cs

@ -46,7 +46,8 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba @@ -46,7 +46,8 @@ public class CreateWatermarkHandler : IRequestHandler<CreateWatermark, Either<Ba
VerticalMarginPercent = request.VerticalMargin,
FrequencyMinutes = request.FrequencyMinutes,
DurationSeconds = request.DurationSeconds,
Opacity = request.Opacity
Opacity = request.Opacity,
PlaceWithinSourceContent = request.PlaceWithinSourceContent
});
private static Validation<BaseError, string> ValidateName(CreateWatermark request) =>

3
ErsatzTV.Application/Watermarks/Commands/UpdateWatermark.cs

@ -17,6 +17,7 @@ public record UpdateWatermark( @@ -17,6 +17,7 @@ public record UpdateWatermark(
int VerticalMargin,
int FrequencyMinutes,
int DurationSeconds,
int Opacity) : IRequest<Either<BaseError, UpdateWatermarkResult>>;
int Opacity,
bool PlaceWithinSourceContent) : IRequest<Either<BaseError, UpdateWatermarkResult>>;
public record UpdateWatermarkResult(int WatermarkId) : EntityIdResult(WatermarkId);

3
ErsatzTV.Application/Watermarks/Commands/UpdateWatermarkHandler.cs

@ -19,7 +19,7 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba @@ -19,7 +19,7 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, ChannelWatermark> validation = await Validate(dbContext, request);
return await LanguageExtensions.Apply(validation, p => ApplyUpdateRequest(dbContext, p, request));
return await validation.Apply(p => ApplyUpdateRequest(dbContext, p, request));
}
private static async Task<UpdateWatermarkResult> ApplyUpdateRequest(
@ -39,6 +39,7 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba @@ -39,6 +39,7 @@ public class UpdateWatermarkHandler : IRequestHandler<UpdateWatermark, Either<Ba
p.FrequencyMinutes = update.FrequencyMinutes;
p.DurationSeconds = update.DurationSeconds;
p.Opacity = update.Opacity;
p.PlaceWithinSourceContent = update.PlaceWithinSourceContent;
await dbContext.SaveChangesAsync();
return new UpdateWatermarkResult(p.Id);
}

3
ErsatzTV.Application/Watermarks/Mapper.cs

@ -18,5 +18,6 @@ internal static class Mapper @@ -18,5 +18,6 @@ internal static class Mapper
watermark.VerticalMarginPercent,
watermark.FrequencyMinutes,
watermark.DurationSeconds,
watermark.Opacity);
watermark.Opacity,
watermark.PlaceWithinSourceContent);
}

2
ErsatzTV.Application/Watermarks/Queries/GetAllWatermarksHandler.cs

@ -15,7 +15,7 @@ public class GetAllWatermarksHandler : IRequestHandler<GetAllWatermarks, List<Wa @@ -15,7 +15,7 @@ public class GetAllWatermarksHandler : IRequestHandler<GetAllWatermarks, List<Wa
GetAllWatermarks request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.ChannelWatermarks
.ToListAsync(cancellationToken)
.Map(list => list.Map(ProjectToViewModel).ToList());

3
ErsatzTV.Application/Watermarks/WatermarkViewModel.cs

@ -16,5 +16,6 @@ public record WatermarkViewModel( @@ -16,5 +16,6 @@ public record WatermarkViewModel(
int VerticalMargin,
int FrequencyMinutes,
int DurationSeconds,
int Opacity
int Opacity,
bool PlaceWithinSourceContent
);

1
ErsatzTV.Core/Domain/ChannelWatermark.cs

@ -17,6 +17,7 @@ public class ChannelWatermark @@ -17,6 +17,7 @@ public class ChannelWatermark
public int FrequencyMinutes { get; set; }
public int DurationSeconds { get; set; }
public int Opacity { get; set; }
public bool PlaceWithinSourceContent { get; set; }
}
public enum ChannelWatermarkMode

3
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -546,7 +546,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -546,7 +546,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
watermark.WidthPercent,
watermark.HorizontalMarginPercent,
watermark.VerticalMarginPercent,
watermark.Opacity));
watermark.Opacity,
watermark.PlaceWithinSourceContent));
return watermarkInputFile;
}

6
ErsatzTV.FFmpeg.Tests/Filter/WatermarkOpacityFilterTests.cs

@ -23,7 +23,8 @@ public class WatermarkOpacityFilterTests @@ -23,7 +23,8 @@ public class WatermarkOpacityFilterTests
50,
50,
50,
75));
75,
false));
filter.Filter.Should().Be("colorchannelmixer=aa=0.75");
}
@ -40,7 +41,8 @@ public class WatermarkOpacityFilterTests @@ -40,7 +41,8 @@ public class WatermarkOpacityFilterTests
50,
50,
50,
75));
75,
false));
filter.Filter.Should().Be("colorchannelmixer=aa=0.75");
}

45
ErsatzTV.FFmpeg/Filter/OverlayWatermarkFilter.cs

@ -28,20 +28,9 @@ public class OverlayWatermarkFilter : BaseFilter @@ -28,20 +28,9 @@ public class OverlayWatermarkFilter : BaseFilter
{
get
{
int horizontalPadding = _resolution.Width - _squarePixelFrameSize.Width;
int verticalPadding = _resolution.Height - _squarePixelFrameSize.Height;
_logger.LogDebug(
$"Resolution: {_resolution.Width}x{_resolution.Height}");
_logger.LogDebug(
$"Square Pix: {_squarePixelFrameSize.Width}x{_squarePixelFrameSize.Height}");
double horizontalMargin = Math.Round(
_watermarkState.HorizontalMarginPercent / 100.0 * _squarePixelFrameSize.Width
+ horizontalPadding / 2.0);
double verticalMargin = Math.Round(
_watermarkState.VerticalMarginPercent / 100.0 * _squarePixelFrameSize.Height
+ verticalPadding / 2.0);
(double horizontalMargin, double verticalMargin) = _watermarkState.PlaceWithinSourceContent
? SourceContentMargins()
: NormalMargins();
return _watermarkState.Location switch
{
@ -58,4 +47,32 @@ public class OverlayWatermarkFilter : BaseFilter @@ -58,4 +47,32 @@ public class OverlayWatermarkFilter : BaseFilter
}
public override FrameState NextState(FrameState currentState) => currentState;
private WatermarkMargins NormalMargins()
{
double horizontalMargin = Math.Round(_watermarkState.HorizontalMarginPercent / 100.0 * _resolution.Width);
double verticalMargin = Math.Round(_watermarkState.VerticalMarginPercent / 100.0 * _resolution.Height);
return new WatermarkMargins(horizontalMargin, verticalMargin);
}
private WatermarkMargins SourceContentMargins()
{
int horizontalPadding = _resolution.Width - _squarePixelFrameSize.Width;
int verticalPadding = _resolution.Height - _squarePixelFrameSize.Height;
_logger.LogDebug("Resolution: {Width}x{Height}", _resolution.Width, _resolution.Height);
_logger.LogDebug("Square Pix: {Width}x{Height}", _squarePixelFrameSize.Width, _squarePixelFrameSize.Height);
double horizontalMargin = Math.Round(
_watermarkState.HorizontalMarginPercent / 100.0 * _squarePixelFrameSize.Width
+ horizontalPadding / 2.0);
double verticalMargin = Math.Round(
_watermarkState.VerticalMarginPercent / 100.0 * _squarePixelFrameSize.Height
+ verticalPadding / 2.0);
return new WatermarkMargins(horizontalMargin, verticalMargin);
}
private record WatermarkMargins(double HorizontalMargin, double VerticalMargin);
}

3
ErsatzTV.FFmpeg/State/WatermarkState.cs

@ -7,7 +7,8 @@ public record WatermarkState( @@ -7,7 +7,8 @@ public record WatermarkState(
int WidthPercent,
int HorizontalMarginPercent,
int VerticalMarginPercent,
int Opacity);
int Opacity,
bool PlaceWithinSourceContent);
public record WatermarkFadePoint(TimeSpan Time, TimeSpan EnableStart, TimeSpan EnableFinish);

4312
ErsatzTV.Infrastructure/Migrations/20221009130454_Add_WatermarkPlaceWithinSourceContent.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure/Migrations/20221009130454_Add_WatermarkPlaceWithinSourceContent.cs

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_WatermarkPlaceWithinSourceContent : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "PlaceWithinSourceContent",
table: "ChannelWatermark",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PlaceWithinSourceContent",
table: "ChannelWatermark");
}
}
}

3
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -313,6 +313,9 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -313,6 +313,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("Opacity")
.HasColumnType("INTEGER");
b.Property<bool>("PlaceWithinSourceContent")
.HasColumnType("INTEGER");
b.Property<int>("Size")
.HasColumnType("INTEGER");

43
ErsatzTV/Pages/WatermarkEditor.razor

@ -4,10 +4,10 @@ @@ -4,10 +4,10 @@
@using ErsatzTV.Application.Watermarks
@using ErsatzTV.Application.Images
@implements IDisposable
@inject NavigationManager _navigationManager
@inject ILogger<WatermarkEditor> _logger
@inject ISnackbar _snackbar
@inject IMediator _mediator
@inject NavigationManager NavigationManager
@inject ILogger<WatermarkEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<div style="max-width: 400px;">
@ -65,6 +65,10 @@ @@ -65,6 +65,10 @@
<MudSelectItem Value="@(WatermarkLocation.TopRight)">Top Right</MudSelectItem>
<MudSelectItem Value="@(WatermarkLocation.RightMiddle)">Right Middle</MudSelectItem>
</MudSelect>
<MudCheckBox Class="mt-3" Label="Place Within Source Content"
@bind-Checked="_model.PlaceWithinSourceContent"
For="@(() => _model.PlaceWithinSourceContent)"
Disabled="@(_model.Mode == ChannelWatermarkMode.None)" />
<MudGrid Class="mt-3" Style="align-items: start" Justify="Justify.Center">
<MudItem xs="6">
<MudSelect Label="Size" @bind-Value="_model.Size"
@ -165,10 +169,10 @@ @@ -165,10 +169,10 @@
{
if (IsEdit)
{
Option<WatermarkViewModel> watermark = await _mediator.Send(new GetWatermarkById(Id), _cts.Token);
Option<WatermarkViewModel> watermark = await Mediator.Send(new GetWatermarkById(Id), _cts.Token);
watermark.Match(
watermarkViewModel => _model = new WatermarkEditViewModel(watermarkViewModel),
() => _navigationManager.NavigateTo("404"));
() => NavigationManager.NavigateTo("404"));
}
else
{
@ -184,7 +188,8 @@ @@ -184,7 +188,8 @@
VerticalMargin = 5,
FrequencyMinutes = 15,
DurationSeconds = 15,
Opacity = 100
Opacity = 100,
PlaceWithinSourceContent = false
};
}
@ -200,16 +205,16 @@ @@ -200,16 +205,16 @@
if (_editContext.Validate())
{
Seq<BaseError> errorMessage = IsEdit ?
(await _mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await _mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
(await Mediator.Send(_model.ToUpdate(), _cts.Token)).LeftToSeq() :
(await Mediator.Send(_model.ToCreate(), _cts.Token)).LeftToSeq();
errorMessage.HeadOrNone().Match(
error =>
{
_snackbar.Add("Unexpected error saving watermark");
_logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
Snackbar.Add("Unexpected error saving watermark");
Logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
},
() => _navigationManager.NavigateTo("/watermarks"));
() => NavigationManager.NavigateTo("/watermarks"));
}
}
@ -217,7 +222,7 @@ @@ -217,7 +222,7 @@
{
try
{
Either<BaseError, string> maybeCacheFileName = await _mediator.Send(
Either<BaseError, string> maybeCacheFileName = await Mediator.Send(
new SaveArtworkToDisk(e.File.OpenReadStream(10 * 1024 * 1024), ArtworkKind.Watermark),
_cts.Token);
maybeCacheFileName.Match(
@ -230,19 +235,19 @@ @@ -230,19 +235,19 @@
},
error =>
{
_snackbar.Add($"Unexpected error saving watermark: {error.Value}", Severity.Error);
_logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
Snackbar.Add($"Unexpected error saving watermark: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving watermark: {Error}", error.Value);
});
}
catch (IOException)
{
_snackbar.Add("Watermark exceeds maximum allowed file size of 10 MB", Severity.Error);
_logger.LogError("Watermark exceeds maximum allowed file size of 10 MB");
Snackbar.Add("Watermark exceeds maximum allowed file size of 10 MB", Severity.Error);
Logger.LogError("Watermark exceeds maximum allowed file size of 10 MB");
}
catch (Exception ex)
{
_snackbar.Add($"Unexpected error saving watermark: {ex.Message}", Severity.Error);
_logger.LogError("Unexpected error saving watermark: {Error}", ex.Message);
Snackbar.Add($"Unexpected error saving watermark: {ex.Message}", Severity.Error);
Logger.LogError("Unexpected error saving watermark: {Error}", ex.Message);
}
}

10
ErsatzTV/ViewModels/WatermarkEditViewModel.cs

@ -25,6 +25,7 @@ public class WatermarkEditViewModel @@ -25,6 +25,7 @@ public class WatermarkEditViewModel
FrequencyMinutes = vm.FrequencyMinutes;
DurationSeconds = vm.DurationSeconds;
Opacity = vm.Opacity;
PlaceWithinSourceContent = vm.PlaceWithinSourceContent;
}
public int Id { get; set; }
@ -40,6 +41,7 @@ public class WatermarkEditViewModel @@ -40,6 +41,7 @@ public class WatermarkEditViewModel
public int FrequencyMinutes { get; set; }
public int DurationSeconds { get; set; }
public int Opacity { get; set; }
public bool PlaceWithinSourceContent { get; set; }
public CreateWatermark ToCreate() =>
new(
@ -54,8 +56,8 @@ public class WatermarkEditViewModel @@ -54,8 +56,8 @@ public class WatermarkEditViewModel
VerticalMargin,
FrequencyMinutes,
DurationSeconds,
Opacity
);
Opacity,
PlaceWithinSourceContent);
public UpdateWatermark ToUpdate() =>
new(
@ -71,6 +73,6 @@ public class WatermarkEditViewModel @@ -71,6 +73,6 @@ public class WatermarkEditViewModel
VerticalMargin,
FrequencyMinutes,
DurationSeconds,
Opacity
);
Opacity,
PlaceWithinSourceContent);
}

Loading…
Cancel
Save