Restore playback countdown display
This commit is contained in:
@@ -46,6 +46,61 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Padding="14,12"
|
||||
Background="#251A0B"
|
||||
BorderBrush="{StaticResource ControlRoomSignalAmberBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="18"
|
||||
Visibility="{x:Bind ViewModel.PlaybackCountdownVisibility, Mode=OneWay}">
|
||||
<Grid ColumnSpacing="14">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Width="54"
|
||||
Height="54"
|
||||
Background="#33FFB81C"
|
||||
BorderBrush="{StaticResource ControlRoomSignalAmberBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="27">
|
||||
<SymbolIcon
|
||||
Foreground="{StaticResource ControlRoomSignalAmberBrush}"
|
||||
Symbol="Clock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="6">
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
FontFamily="Consolas"
|
||||
FontSize="30"
|
||||
Foreground="{StaticResource ControlRoomTextPrimaryBrush}"
|
||||
Text="{x:Bind ViewModel.PlaybackCountdownText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource ConsoleBodyTextStyle}"
|
||||
Text="{x:Bind ViewModel.PlaybackCountdownDetail, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
<ProgressBar
|
||||
Height="8"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
Value="{x:Bind ViewModel.PlaybackCountdownProgress, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1.25*" />
|
||||
|
||||
@@ -29,6 +29,8 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
private ImageSource? _internalNextPreviewSource;
|
||||
private string _internalNextPreviewStatusLabel = string.Empty;
|
||||
private string _internalNextPreviewDisplayName = string.Empty;
|
||||
private double _playbackRemainingSeconds;
|
||||
private double _playbackTotalSeconds;
|
||||
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
@@ -105,6 +107,7 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
OnPropertyChanged(nameof(StateBadgeBackgroundBrush));
|
||||
OnPropertyChanged(nameof(CardOpacity));
|
||||
OnPropertyChanged(nameof(CanDelete));
|
||||
OnPropertyChanged(nameof(PlaybackCountdownVisibility));
|
||||
OnIssueStateChanged();
|
||||
}
|
||||
}
|
||||
@@ -307,6 +310,27 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
? "실데이터 프리뷰 준비 중"
|
||||
: _renderedPreviewStatusLabel;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasPlaybackCountdown => State == ScheduleQueueItemState.OnAir && _playbackTotalSeconds > 0;
|
||||
|
||||
[JsonIgnore]
|
||||
public Visibility PlaybackCountdownVisibility => HasPlaybackCountdown ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
[JsonIgnore]
|
||||
public string PlaybackCountdownText => HasPlaybackCountdown
|
||||
? $"다음 컷까지 {Math.Max(0, (int)Math.Ceiling(_playbackRemainingSeconds))}초"
|
||||
: "대기 중";
|
||||
|
||||
[JsonIgnore]
|
||||
public string PlaybackCountdownDetail => HasPlaybackCountdown
|
||||
? $"{FormatName} / {DisplayRegionLabel}"
|
||||
: string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public double PlaybackCountdownProgress => HasPlaybackCountdown
|
||||
? Math.Clamp((_playbackRemainingSeconds / _playbackTotalSeconds) * 100d, 0d, 100d)
|
||||
: 0d;
|
||||
|
||||
[JsonIgnore]
|
||||
public ImageSource? InternalNextPreviewSource => _internalNextPreviewSource;
|
||||
|
||||
@@ -403,6 +427,33 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
OnInternalNextPreviewChanged();
|
||||
}
|
||||
|
||||
public void UpdatePlaybackCountdown(TimeSpan remaining, TimeSpan total)
|
||||
{
|
||||
var normalizedTotal = Math.Max(0, total.TotalSeconds);
|
||||
var normalizedRemaining = Math.Clamp(remaining.TotalSeconds, 0, normalizedTotal);
|
||||
if (Math.Abs(_playbackRemainingSeconds - normalizedRemaining) < 0.001d &&
|
||||
Math.Abs(_playbackTotalSeconds - normalizedTotal) < 0.001d)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_playbackRemainingSeconds = normalizedRemaining;
|
||||
_playbackTotalSeconds = normalizedTotal;
|
||||
OnPlaybackCountdownChanged();
|
||||
}
|
||||
|
||||
public void ClearPlaybackCountdown()
|
||||
{
|
||||
if (_playbackRemainingSeconds <= 0 && _playbackTotalSeconds <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_playbackRemainingSeconds = 0;
|
||||
_playbackTotalSeconds = 0;
|
||||
OnPlaybackCountdownChanged();
|
||||
}
|
||||
|
||||
public void UpdateThumbnailLayout(ThumbnailDisplayMetrics metrics)
|
||||
{
|
||||
ThumbnailWidth = metrics.Width;
|
||||
@@ -453,6 +504,15 @@ public sealed class ChannelScheduleItem : ObservableObject
|
||||
OnPropertyChanged(nameof(InternalNextPreviewDisplayName));
|
||||
}
|
||||
|
||||
private void OnPlaybackCountdownChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(HasPlaybackCountdown));
|
||||
OnPropertyChanged(nameof(PlaybackCountdownVisibility));
|
||||
OnPropertyChanged(nameof(PlaybackCountdownText));
|
||||
OnPropertyChanged(nameof(PlaybackCountdownDetail));
|
||||
OnPropertyChanged(nameof(PlaybackCountdownProgress));
|
||||
}
|
||||
|
||||
public static ChannelScheduleItem FromTemplate(FormatTemplateDefinition template, ScheduleRegionOption? regionOption = null)
|
||||
{
|
||||
var selectedRegion = regionOption ?? new ScheduleRegionOption
|
||||
|
||||
@@ -130,6 +130,7 @@ public sealed class ChannelScheduleEngine
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearRenderedPreview();
|
||||
item.ClearInternalNextPreview();
|
||||
item.ClearPlaybackCountdown();
|
||||
}
|
||||
|
||||
_lastPlaybackItemId = null;
|
||||
@@ -203,6 +204,7 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
item.State = ScheduleQueueItemState.Queued;
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearPlaybackCountdown();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
@@ -221,6 +223,7 @@ public sealed class ChannelScheduleEngine
|
||||
_directPlaybackItem.CurrentRegionLabel = string.Empty;
|
||||
_directPlaybackItem.ClearRenderedPreview();
|
||||
_directPlaybackItem.ClearInternalNextPreview();
|
||||
_directPlaybackItem.ClearPlaybackCountdown();
|
||||
_directPlaybackItem = null;
|
||||
}
|
||||
|
||||
@@ -244,6 +247,7 @@ public sealed class ChannelScheduleEngine
|
||||
{
|
||||
item.State = ScheduleQueueItemState.Queued;
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearPlaybackCountdown();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -264,6 +268,7 @@ public sealed class ChannelScheduleEngine
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearRenderedPreview();
|
||||
item.ClearInternalNextPreview();
|
||||
item.ClearPlaybackCountdown();
|
||||
}
|
||||
|
||||
RefreshQueueMarkers();
|
||||
@@ -281,6 +286,7 @@ public sealed class ChannelScheduleEngine
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearRenderedPreview();
|
||||
item.ClearInternalNextPreview();
|
||||
item.ClearPlaybackCountdown();
|
||||
RefreshQueueMarkers();
|
||||
QueueChanged?.Invoke(this, EventArgs.Empty);
|
||||
return true;
|
||||
@@ -312,6 +318,7 @@ public sealed class ChannelScheduleEngine
|
||||
activeItem.LastError = string.Empty;
|
||||
activeItem.CurrentRegionLabel = string.Empty;
|
||||
activeItem.ClearInternalNextPreview();
|
||||
activeItem.ClearPlaybackCountdown();
|
||||
}
|
||||
|
||||
RefreshQueueMarkers();
|
||||
@@ -449,6 +456,7 @@ public sealed class ChannelScheduleEngine
|
||||
sendingItem.LastError = ex.Message;
|
||||
sendingItem.CurrentRegionLabel = string.Empty;
|
||||
sendingItem.ClearInternalNextPreview();
|
||||
sendingItem.ClearPlaybackCountdown();
|
||||
ClearSkipCurrentItem(sendingItem);
|
||||
}
|
||||
|
||||
@@ -628,6 +636,7 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
queueItem.ClearInternalNextPreview();
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
_lastPlaybackItemId = queueItem.Id;
|
||||
if (playedAny)
|
||||
{
|
||||
@@ -727,6 +736,7 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
queueItem.ClearInternalNextPreview();
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
_lastPlaybackItemId = queueItem.Id;
|
||||
if (playedAny)
|
||||
{
|
||||
@@ -758,6 +768,7 @@ public sealed class ChannelScheduleEngine
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
queueItem.State = ScheduleQueueItemState.Sending;
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
RefreshQueueMarkers();
|
||||
QueueChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
@@ -780,8 +791,11 @@ public sealed class ChannelScheduleEngine
|
||||
await _adapter.TakeAsync(Channel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var onAirAt = DateTimeOffset.Now;
|
||||
var durationSeconds = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(cut.DurationSeconds, template);
|
||||
var playbackDuration = TimeSpan.FromSeconds(durationSeconds);
|
||||
queueItem.State = ScheduleQueueItemState.OnAir;
|
||||
queueItem.LastPlayedAt = onAirAt;
|
||||
queueItem.UpdatePlaybackCountdown(playbackDuration, playbackDuration);
|
||||
RefreshQueueMarkers();
|
||||
QueueChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
@@ -792,8 +806,6 @@ public sealed class ChannelScheduleEngine
|
||||
signal.TrySetResult(true);
|
||||
}
|
||||
|
||||
var durationSeconds = ScheduleTemplatePolicy.NormalizeCutDurationSeconds(cut.DurationSeconds, template);
|
||||
var playbackDuration = TimeSpan.FromSeconds(durationSeconds);
|
||||
await CaptureCurrentPreviewAsync(queueItem, template, cancellationToken).ConfigureAwait(false);
|
||||
QueueChanged?.Invoke(this, EventArgs.Empty);
|
||||
if (!ShouldSkipCurrentItem(queueItem))
|
||||
@@ -829,17 +841,51 @@ public sealed class ChannelScheduleEngine
|
||||
|
||||
if (ShouldSkipCurrentItem(queueItem))
|
||||
{
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var remainingDuration = playbackDuration - (DateTimeOffset.Now - onAirAt);
|
||||
if (remainingDuration <= TimeSpan.Zero)
|
||||
{
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
var delayTask = Task.Delay(remainingDuration, cancellationToken);
|
||||
await Task.WhenAny(delayTask, signal.Task).ConfigureAwait(false);
|
||||
await WaitForPlaybackCountdownAsync(queueItem, onAirAt, playbackDuration, signal, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task WaitForPlaybackCountdownAsync(
|
||||
ChannelScheduleItem queueItem,
|
||||
DateTimeOffset onAirAt,
|
||||
TimeSpan playbackDuration,
|
||||
TaskCompletionSource<bool> advanceSignal,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var remaining = playbackDuration - (DateTimeOffset.Now - onAirAt);
|
||||
if (remaining <= TimeSpan.Zero)
|
||||
{
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
queueItem.UpdatePlaybackCountdown(remaining, playbackDuration);
|
||||
var updateInterval = remaining > TimeSpan.FromSeconds(1)
|
||||
? TimeSpan.FromSeconds(1)
|
||||
: remaining;
|
||||
var delayTask = Task.Delay(updateInterval, cancellationToken);
|
||||
var completedTask = await Task.WhenAny(delayTask, advanceSignal.Task).ConfigureAwait(false);
|
||||
if (completedTask == advanceSignal.Task)
|
||||
{
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
}
|
||||
|
||||
private async Task CaptureCurrentPreviewAsync(
|
||||
@@ -1215,6 +1261,7 @@ public sealed class ChannelScheduleEngine
|
||||
preparedFrame.Item.CurrentRegionLabel = string.Empty;
|
||||
preparedFrame.Item.ClearRenderedPreview();
|
||||
preparedFrame.Item.ClearInternalNextPreview();
|
||||
preparedFrame.Item.ClearPlaybackCountdown();
|
||||
}
|
||||
|
||||
private void ClearSkipCurrentItem(ChannelScheduleItem queueItem)
|
||||
@@ -1732,6 +1779,7 @@ public sealed class ChannelScheduleEngine
|
||||
item.CurrentRegionLabel = string.Empty;
|
||||
item.ClearRenderedPreview();
|
||||
item.ClearInternalNextPreview();
|
||||
item.ClearPlaybackCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1742,6 +1790,7 @@ public sealed class ChannelScheduleEngine
|
||||
queueItem.CurrentRegionLabel = string.Empty;
|
||||
queueItem.ClearRenderedPreview();
|
||||
queueItem.ClearInternalNextPreview();
|
||||
queueItem.ClearPlaybackCountdown();
|
||||
}
|
||||
|
||||
private static string NormalizeDataUnavailableReason(string reason)
|
||||
|
||||
@@ -372,6 +372,15 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
?? QueueNextPlaybackItem?.PreviewStatusLabel
|
||||
?? "다음 컷 없음";
|
||||
|
||||
public Visibility PlaybackCountdownVisibility =>
|
||||
CurrentPlaybackItem?.PlaybackCountdownVisibility ?? Visibility.Collapsed;
|
||||
|
||||
public string PlaybackCountdownText => CurrentPlaybackItem?.PlaybackCountdownText ?? "대기 중";
|
||||
|
||||
public string PlaybackCountdownDetail => CurrentPlaybackItem?.PlaybackCountdownDetail ?? string.Empty;
|
||||
|
||||
public double PlaybackCountdownProgress => CurrentPlaybackItem?.PlaybackCountdownProgress ?? 0d;
|
||||
|
||||
public double CurrentPreviewWidth => ResolvePlaybackPreviewMetrics(CurrentPlaybackItem).Width;
|
||||
|
||||
public double CurrentPreviewHeight => ResolvePlaybackPreviewMetrics(CurrentPlaybackItem).Height;
|
||||
@@ -892,6 +901,10 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
nameof(NextPreviewSource),
|
||||
nameof(CurrentPreviewStatusLabel),
|
||||
nameof(NextPreviewStatusLabel),
|
||||
nameof(PlaybackCountdownVisibility),
|
||||
nameof(PlaybackCountdownText),
|
||||
nameof(PlaybackCountdownDetail),
|
||||
nameof(PlaybackCountdownProgress),
|
||||
nameof(CurrentPreviewWidth),
|
||||
nameof(CurrentPreviewHeight),
|
||||
nameof(NextPreviewWidth),
|
||||
@@ -1155,6 +1168,10 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
or nameof(ChannelScheduleItem.HasInternalNextPreview)
|
||||
or nameof(ChannelScheduleItem.LastError)
|
||||
or nameof(ChannelScheduleItem.LastIssueAt)
|
||||
or nameof(ChannelScheduleItem.PlaybackCountdownVisibility)
|
||||
or nameof(ChannelScheduleItem.PlaybackCountdownText)
|
||||
or nameof(ChannelScheduleItem.PlaybackCountdownDetail)
|
||||
or nameof(ChannelScheduleItem.PlaybackCountdownProgress)
|
||||
or nameof(ChannelScheduleItem.ThumbnailSource))
|
||||
{
|
||||
if (e.PropertyName is nameof(ChannelScheduleItem.State)
|
||||
@@ -1182,6 +1199,10 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
nameof(QueuedItemCount),
|
||||
nameof(QueueFootnote),
|
||||
nameof(QueueSummary),
|
||||
nameof(PlaybackCountdownVisibility),
|
||||
nameof(PlaybackCountdownText),
|
||||
nameof(PlaybackCountdownDetail),
|
||||
nameof(PlaybackCountdownProgress),
|
||||
nameof(ScheduleDataIssueVisibility),
|
||||
nameof(ScheduleDataIssueBackgroundBrush),
|
||||
nameof(ScheduleDataIssueBorderBrush),
|
||||
@@ -1198,6 +1219,10 @@ public sealed class ChannelScheduleViewModel : ObservableObject
|
||||
nameof(NextPreviewSource),
|
||||
nameof(CurrentPreviewStatusLabel),
|
||||
nameof(NextPreviewStatusLabel),
|
||||
nameof(PlaybackCountdownVisibility),
|
||||
nameof(PlaybackCountdownText),
|
||||
nameof(PlaybackCountdownDetail),
|
||||
nameof(PlaybackCountdownProgress),
|
||||
nameof(CurrentPreviewWidth),
|
||||
nameof(CurrentPreviewHeight),
|
||||
nameof(NextPreviewWidth),
|
||||
|
||||
Reference in New Issue
Block a user