Source access enforcement (the catalog): 10-source catalog + per-scene access matrix enforced by SceneAccessPolicy; Add-Source menu is one ItemsSource rebuilt per scene (Screen = Live-only, BG/Image/Text every scene), webcam row always present + greys out; Chatbox/Alerts/Socials are inert SourceType rows ready for their features; Text queued as a ship step (countdown + credits modes); Monetization notes socialBar free = YouTube + 1, unlimited with the paid key (79 tests passing)
This commit is contained in:
+11
-9
@@ -4,7 +4,6 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
xmlns:vm="clr-namespace:ytLive.ViewModels"
|
||||||
xmlns:models="clr-namespace:ytLive.Models"
|
|
||||||
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
xmlns:Helpers="clr-namespace:ytLive.Helpers"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="{Binding WindowTitle}" Height="720" Width="1280"
|
Title="{Binding WindowTitle}" Height="720" Width="1280"
|
||||||
@@ -257,14 +256,17 @@
|
|||||||
<Button ToolTip="Add Source" Style="{StaticResource IconButton}" Margin="4,-4,0,0"
|
<Button ToolTip="Add Source" Style="{StaticResource IconButton}" Margin="4,-4,0,0"
|
||||||
Click="AddSourceButton_Click">
|
Click="AddSourceButton_Click">
|
||||||
<Button.ContextMenu>
|
<Button.ContextMenu>
|
||||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
<ContextMenu ItemsSource="{Binding AddSourceMenuItems}"
|
||||||
<MenuItem Header="Webcam" Command="{Binding AddWebcamCommand}"
|
DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||||
IsEnabled="{Binding CanAddWebcamToActiveScene}"
|
<ContextMenu.ItemContainerStyle>
|
||||||
ToolTip="One webcam at a time — it's already in your stream"/>
|
<Style TargetType="MenuItem">
|
||||||
<MenuItem Header="Screen" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.DisplayCapture}"/>
|
<Setter Property="Header" Value="{Binding Header}"/>
|
||||||
<MenuItem Header="Background" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.Background}"/>
|
<Setter Property="Command" Value="{Binding Command}"/>
|
||||||
<MenuItem Header="Image" Command="{Binding AddImageCommand}"/>
|
<Setter Property="CommandParameter" Value="{Binding Parameter}"/>
|
||||||
<MenuItem Header="Text" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.TextOverlay}"/>
|
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
|
||||||
|
<Setter Property="ToolTip" Value="{Binding ToolTip}"/>
|
||||||
|
</Style>
|
||||||
|
</ContextMenu.ItemContainerStyle>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</Button.ContextMenu>
|
</Button.ContextMenu>
|
||||||
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
|
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
|
||||||
|
|||||||
+10
-1
@@ -3,13 +3,22 @@ using ytLive.Helpers;
|
|||||||
|
|
||||||
namespace ytLive.Models;
|
namespace ytLive.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source catalog (see TASKS.md). Chatbox / Alerts / Socials are inert
|
||||||
|
/// values until their features ship — the access-policy rows are live from
|
||||||
|
/// today, the menu items arrive with the features. Countdown and Credits are
|
||||||
|
/// NOT types: they're Text modes, gated by the same policy when Text ships.
|
||||||
|
/// </summary>
|
||||||
public enum SourceType
|
public enum SourceType
|
||||||
{
|
{
|
||||||
DisplayCapture,
|
DisplayCapture,
|
||||||
WindowCapture,
|
WindowCapture,
|
||||||
Background,
|
Background,
|
||||||
Image,
|
Image,
|
||||||
TextOverlay
|
TextOverlay,
|
||||||
|
Chatbox,
|
||||||
|
Alerts,
|
||||||
|
Socials
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ClipShape
|
public enum ClipShape
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using ytLive.Models;
|
||||||
|
|
||||||
|
namespace ytLive.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The per-scene source access matrix (see the source catalog in TASKS.md).
|
||||||
|
/// One source's availability in a scene is a product decision, enforced by
|
||||||
|
/// scene name on every place a source can be added. Countdown and Credits are
|
||||||
|
/// Text *modes*, not source types: their scene restrictions gate the mode when
|
||||||
|
/// the Text ship step lands. Chatbox / Alerts / Socials are inert values until
|
||||||
|
/// their features ship — the rows are live from today.
|
||||||
|
/// </summary>
|
||||||
|
public static class SceneAccessPolicy
|
||||||
|
{
|
||||||
|
public static bool Allows(string? sceneName, SourceType type)
|
||||||
|
{
|
||||||
|
if (!SceneCatalog.IsCanonical(sceneName)) return false;
|
||||||
|
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
SourceType.DisplayCapture or SourceType.WindowCapture =>
|
||||||
|
SceneCatalog.Is(sceneName, SceneCatalog.Live),
|
||||||
|
SourceType.Background or SourceType.Image or SourceType.TextOverlay or
|
||||||
|
SourceType.Alerts or SourceType.Socials => true,
|
||||||
|
SourceType.Chatbox =>
|
||||||
|
SceneCatalog.Is(sceneName, SceneCatalog.Live)
|
||||||
|
|| SceneCatalog.Is(sceneName, SceneCatalog.Brb)
|
||||||
|
|| SceneCatalog.IsChat(sceneName),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,29 +109,55 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
|||||||
12. ✅ The connected YouTube account's avatar/name shows in the top bar next to Start Stream (`SyncConnectedAccount`); the scenes list is content-height now (no dead space before SOURCES)
|
12. ✅ The connected YouTube account's avatar/name shows in the top bar next to Start Stream (`SyncConnectedAccount`); the scenes list is content-height now (no dead space before SOURCES)
|
||||||
13. ☐ **Window capture** — absorbed into the Screen picker (no separate source type); dedicated window-as-source work is pending
|
13. ☐ **Window capture** — absorbed into the Screen picker (no separate source type); dedicated window-as-source work is pending
|
||||||
14. ☐ **Scene compositing** — the D3DImage/MediaElement preview compositor (this task's requirement 5; the output compositor ships as TASK 4 ship step 1)
|
14. ☐ **Scene compositing** — the D3DImage/MediaElement preview compositor (this task's requirement 5; the output compositor ships as TASK 4 ship step 1)
|
||||||
15. ☐ **Text source** — live text ("Starting soon", "Back in 5", handle, callout)
|
15. ☐ **Text source** — live text ("Starting soon", "Back in 5", handle, callout); a queued ship step carries its **countdown** + **credits** (scroll) modes (see below)
|
||||||
16. ☐ **Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video
|
16. ☐ **Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video
|
||||||
17. ❌ **Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build
|
17. ❌ **Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build
|
||||||
18. ☐ **Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; **the one paid feature** (see Monetization in `ai.md`)
|
18. ☐ **Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; unlocked by the one-time paid key (see Monetization in `ai.md`)
|
||||||
|
19. ✅ **The source catalog + access enforcement** — the 10-source catalog and per-scene matrix (the section below) live as `SceneAccessPolicy.Allows(sceneName, sourceType)`; the Add-Source menu is one `ItemsSource` (`AddSourceMenuItems`) rebuilt per scene from the policy (Screen is Live-only; BG/Image/Text every scene; the webcam row is always present and greys out once the active scene has one); Chatbox/Alerts/Socials are inert `SourceType` rows ready for their features — 79 tests passing
|
||||||
|
|
||||||
### The Minimal Source Set (design decision — do not expand casually)
|
### The Source Catalog & scene access (design decision — do not expand casually)
|
||||||
|
|
||||||
ytLlive is YouTube-only and 90% of users are casual. OBS's long source list is off-putting; we ship
|
ytLlive is YouTube-only and 90% of users are casual. OBS's long source list is off-putting; we ship
|
||||||
the hot few and nothing esoteric. If a user needs more, they've graduated to OBS.
|
the hot few and nothing esoteric. If a user needs more, they've graduated to OBS.
|
||||||
|
|
||||||
1. **Webcam** — the face cam. Non-negotiable.
|
1. **Screen capture** — the main event (game, slides, browser) as the Live scene's permanent backdrop
|
||||||
2. **Screen** — the main event (game, slides, browser). One source; a picker chooses a monitor *or* a
|
(`IsBackdrop`, schema v5/v6); the picker chooses a monitor *or* a window. **Live only** (policy).
|
||||||
window. (Window capture is absorbed here — no separate source type.)
|
2. **BG (Background)** — a full-canvas backdrop image. Fills the whole scene automatically, zero
|
||||||
3. **Background** — a full-canvas backdrop image. Fills the whole scene automatically, zero fiddling.
|
fiddling. Kept separate from Image on purpose: same pixels, but this one needs no positioning.
|
||||||
Kept separate from Image on purpose: same pixels, but this one needs no positioning.
|
3. **Image** — a floating graphic/logo overlay (watermark, badge, corner branding). Free-positioned.
|
||||||
4. **Image** — a floating graphic/logo overlay (watermark, badge, corner branding). Free-positioned.
|
4. **Text** — live text ("Starting soon", "Back in 5", handle, callout). Casual streamers live on this.
|
||||||
5. **Text** — live text ("Starting soon", "Back in 5", handle, callout). Casual streamers live on this.
|
Ships with two **modes**: **Countdown** (Starting/BRB) and **Credits** (Ending) — see the queued
|
||||||
|
Text ship step below. No new source types (KISS).
|
||||||
|
5. **Webcam** — the face cam. Non-negotiable. A singleton resource (`WebcamSceneConfig`, not a `Source`
|
||||||
|
type) usable in any scene.
|
||||||
6. **Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video. YT-native.
|
6. **Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video. YT-native.
|
||||||
7. **Alerts** — Super Chat / membership / subscribe pop-ins. The dopamine source. **The one big lift**
|
**Live / BRB / Chat scenes.**
|
||||||
(Super Chat event streaming + on-stream rendering/animation); build after the six. **Also the one
|
7. **Alerts** — Super Chat / membership / subscribe pop-ins. The dopamine source. **All scenes** —
|
||||||
paid feature** — see Monetization in `ai.md`.
|
alerts fire regardless of which scene is showing. Build after the six.
|
||||||
|
8. **socialBar** — the creator's social links as one bar (branded anchor icon + in-app drop-down).
|
||||||
|
**All scenes.** Free tier = the creator's YouTube channel + **one** more platform of their choice;
|
||||||
|
unlimited platforms come with the one-time paid key (see Monetization in `ai.md`).
|
||||||
|
|
||||||
Deliberately NOT supported: game capture, browser source, media playlist, VLC, color-key voodoo, MIDI.
|
**Per-scene access matrix** — enforced by `SceneAccessPolicy.Allows(sceneName, sourceType)`:
|
||||||
|
|
||||||
|
| Source | Starting | Live | BRB | Chat | Ending |
|
||||||
|
|--------|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
| Screen capture (backdrop) | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
|
| BG / Image / Text | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Webcam | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Countdown (Text mode) | ✅ | — | ✅ | — | — |
|
||||||
|
| Chat box | — | ✅ | ✅ | ✅ | — |
|
||||||
|
| Alerts | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Credits (Text mode) | — | — | — | — | ✅ |
|
||||||
|
| socialBar | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
Countdown and Credits are **Text modes** (no new source types): the scene restrictions gate the
|
||||||
|
*mode*, applied when the Text ship step lands. Chatbox / Alerts / socialBar are inert `SourceType`
|
||||||
|
values until their features ship — the policy rows are live from today, the menu items arrive with
|
||||||
|
the features.
|
||||||
|
|
||||||
|
Deliberately NOT supported: game capture (absorbed into Screen), browser source, media playlist, VLC,
|
||||||
|
color-key voodoo, MIDI.
|
||||||
|
|
||||||
### Source memory model (design decision)
|
### Source memory model (design decision)
|
||||||
|
|
||||||
@@ -187,6 +213,20 @@ Preview shows the transition too (WYSIWYG). No wipes/slides/LUTs beyond the four
|
|||||||
2. **Scenes list:** drag rows to reorder scenes
|
2. **Scenes list:** drag rows to reorder scenes
|
||||||
3. **Sources list:** drag rows to reorder sources (this *is* the z-order) — implemented
|
3. **Sources list:** drag rows to reorder sources (this *is* the z-order) — implemented
|
||||||
|
|
||||||
|
#### Queued ship step — Text source (countdown + credits modes)
|
||||||
|
|
||||||
|
**Goal:** make Text real — content + font/size/color/alignment rendered in the preview AND the output
|
||||||
|
compositor — then its two modes on top. Status item 15 above.
|
||||||
|
|
||||||
|
1. ☐ **Text core** — `SourceType.TextOverlay` gains content + styling; the XAML preview renders it and
|
||||||
|
the output compositor rasterizes it (preview via `FormattedText`, output via a `frameFor` frame,
|
||||||
|
the `StaticPixelCache` pattern)
|
||||||
|
2. ☐ **Countdown mode** — a Text bound to a countdown timer ("Live in 2:30"); the mode is gated to
|
||||||
|
Starting/BRB per the access matrix
|
||||||
|
3. ☐ **Credits mode** — a Text that scrolls up on the Ending scene
|
||||||
|
|
||||||
|
Ships only when queued (the Good Dog Rule: one integration test per PR, no multi-test branches).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TASK 4 — RTMP Ingest to YouTube
|
## TASK 4 — RTMP Ingest to YouTube
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
public ObservableCollection<Scene> Scenes { get; } = new();
|
public ObservableCollection<Scene> Scenes { get; } = new();
|
||||||
public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
|
public ObservableCollection<ChatMessage> ChatMessages { get; } = new();
|
||||||
public ObservableCollection<DisplayInfo> Displays { get; } = new();
|
public ObservableCollection<DisplayInfo> Displays { get; } = new();
|
||||||
|
public ObservableCollection<SourceMenuItem> AddSourceMenuItems { get; } = new();
|
||||||
public string[] Visibilities { get; } = { "Public", "Unlisted", "Private" };
|
public string[] Visibilities { get; } = { "Public", "Unlisted", "Private" };
|
||||||
|
|
||||||
public Scene? ActiveScene
|
public Scene? ActiveScene
|
||||||
@@ -123,6 +124,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(CanShowWebcamInActiveScene));
|
OnPropertyChanged(nameof(CanShowWebcamInActiveScene));
|
||||||
UpdateActiveBackground();
|
UpdateActiveBackground();
|
||||||
UpdateBackdropImage();
|
UpdateBackdropImage();
|
||||||
|
RebuildAddSourceMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1189,6 +1191,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
foreach (SceneElement element in e.OldItems)
|
foreach (SceneElement element in e.OldItems)
|
||||||
element.PropertyChanged -= OnElementPropertyChanged;
|
element.PropertyChanged -= OnElementPropertyChanged;
|
||||||
OnPropertyChanged(nameof(CanAddWebcamToActiveScene));
|
OnPropertyChanged(nameof(CanAddWebcamToActiveScene));
|
||||||
|
RebuildAddSourceMenu();
|
||||||
ScheduleSave();
|
ScheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1245,6 +1248,34 @@ public class MainViewModel : ViewModelBase
|
|||||||
ActiveScene = Scenes.FirstOrDefault();
|
ActiveScene = Scenes.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Add-Source menu lists only what the active scene allows per the access
|
||||||
|
// matrix (SceneAccessPolicy). The webcam row is always present (webcams are
|
||||||
|
// allowed in every scene) but greys out once the active scene has one.
|
||||||
|
private void RebuildAddSourceMenu()
|
||||||
|
{
|
||||||
|
AddSourceMenuItems.Clear();
|
||||||
|
var sceneName = ActiveScene?.Name;
|
||||||
|
if (sceneName == null) return;
|
||||||
|
|
||||||
|
AddSourceMenuItems.Add(new SourceMenuItem(
|
||||||
|
"Webcam", AddWebcamCommand,
|
||||||
|
isEnabled: CanAddWebcamToActiveScene,
|
||||||
|
toolTip: "One webcam at a time — it's already in your stream"));
|
||||||
|
|
||||||
|
var menu = new[]
|
||||||
|
{
|
||||||
|
(Header: "Screen", Type: SourceType.DisplayCapture, Command: AddSourceCommand),
|
||||||
|
(Header: "Background", Type: SourceType.Background, Command: AddSourceCommand),
|
||||||
|
(Header: "Image", Type: SourceType.Image, Command: AddImageCommand),
|
||||||
|
(Header: "Text", Type: SourceType.TextOverlay, Command: AddSourceCommand),
|
||||||
|
};
|
||||||
|
foreach (var (header, type, command) in menu)
|
||||||
|
{
|
||||||
|
if (SceneAccessPolicy.Allows(sceneName, type))
|
||||||
|
AddSourceMenuItems.Add(new SourceMenuItem(header, command, type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AddSource(object? parameter)
|
private void AddSource(object? parameter)
|
||||||
{
|
{
|
||||||
var scene = ActiveScene;
|
var scene = ActiveScene;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace ytLive.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One Add-Source menu entry: a label, its action, and an optional command
|
||||||
|
/// parameter. The collection is rebuilt per scene from
|
||||||
|
/// <see cref="Services.SceneAccessPolicy"/> so only the sources the active
|
||||||
|
/// scene allows appear in the menu.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SourceMenuItem
|
||||||
|
{
|
||||||
|
public SourceMenuItem(string header, ICommand command, object? parameter = null,
|
||||||
|
bool isEnabled = true, string? toolTip = null)
|
||||||
|
{
|
||||||
|
Header = header;
|
||||||
|
Command = command;
|
||||||
|
Parameter = parameter;
|
||||||
|
IsEnabled = isEnabled;
|
||||||
|
ToolTip = toolTip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Header { get; }
|
||||||
|
public ICommand Command { get; }
|
||||||
|
public object? Parameter { get; }
|
||||||
|
public bool IsEnabled { get; }
|
||||||
|
public string? ToolTip { get; }
|
||||||
|
}
|
||||||
@@ -379,7 +379,9 @@ delivery; we never own a server or a key shop):
|
|||||||
~5s after go-live, only while live or recording. An always-on watermark can be cropped or covered;
|
~5s after go-live, only while live or recording. An always-on watermark can be cropped or covered;
|
||||||
an intermittent full-frame flash can't be cropped and is impractical to edit around on a live feed.
|
an intermittent full-frame flash can't be cropped and is impractical to edit around on a live feed.
|
||||||
- **Paid (one-time):** branding flash removed (flips `BrandFlashEnabled` off) + **Alerts**
|
- **Paid (one-time):** branding flash removed (flips `BrandFlashEnabled` off) + **Alerts**
|
||||||
(Super Chat / membership / subscribe pop-ins).
|
(Super Chat / membership / subscribe pop-ins) + **unlimited socialBar platforms** (the free tier
|
||||||
|
is the creator's YouTube channel + one more; unlimited is the unlock — the teaser, per the source
|
||||||
|
catalog in `TASKS.md`).
|
||||||
|
|
||||||
Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time
|
Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time
|
||||||
cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
|
cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Xunit;
|
||||||
|
using ytLive.Models;
|
||||||
|
using ytLive.Services;
|
||||||
|
|
||||||
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The per-scene source access matrix from the source catalog (TASKS.md).
|
||||||
|
/// One integration test: each canonical scene allows exactly its matrix set,
|
||||||
|
/// nothing more; non-canonical scenes allow nothing.
|
||||||
|
/// </summary>
|
||||||
|
public class SceneAccessPolicyTests
|
||||||
|
{
|
||||||
|
private static readonly SourceType[] All =
|
||||||
|
{
|
||||||
|
SourceType.DisplayCapture, SourceType.WindowCapture, SourceType.Background,
|
||||||
|
SourceType.Image, SourceType.TextOverlay, SourceType.Chatbox,
|
||||||
|
SourceType.Alerts, SourceType.Socials,
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SceneAccessPolicy_Enforces_The_Full_Access_Matrix()
|
||||||
|
{
|
||||||
|
var allowed = new[]
|
||||||
|
{
|
||||||
|
(Scene: "Starting", Types: new[]
|
||||||
|
{
|
||||||
|
SourceType.Background, SourceType.Image, SourceType.TextOverlay,
|
||||||
|
SourceType.Alerts, SourceType.Socials,
|
||||||
|
}),
|
||||||
|
(Scene: "Live", Types: All),
|
||||||
|
(Scene: "BRB", Types: new[]
|
||||||
|
{
|
||||||
|
SourceType.Background, SourceType.Image, SourceType.TextOverlay,
|
||||||
|
SourceType.Chatbox, SourceType.Alerts, SourceType.Socials,
|
||||||
|
}),
|
||||||
|
(Scene: "Chat", Types: new[]
|
||||||
|
{
|
||||||
|
SourceType.Background, SourceType.Image, SourceType.TextOverlay,
|
||||||
|
SourceType.Chatbox, SourceType.Alerts, SourceType.Socials,
|
||||||
|
}),
|
||||||
|
(Scene: "Ending", Types: new[]
|
||||||
|
{
|
||||||
|
SourceType.Background, SourceType.Image, SourceType.TextOverlay,
|
||||||
|
SourceType.Alerts, SourceType.Socials,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (scene, types) in allowed)
|
||||||
|
{
|
||||||
|
foreach (var type in types)
|
||||||
|
Assert.True(
|
||||||
|
SceneAccessPolicy.Allows(scene, type),
|
||||||
|
$"{type} should be allowed in {scene}");
|
||||||
|
|
||||||
|
foreach (var type in All.Where(t => !types.Contains(t)))
|
||||||
|
Assert.False(
|
||||||
|
SceneAccessPolicy.Allows(scene, type),
|
||||||
|
$"{type} should NOT be allowed in {scene}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.False(SceneAccessPolicy.Allows(null, SourceType.Image));
|
||||||
|
Assert.False(SceneAccessPolicy.Allows("My Scene", SourceType.Image));
|
||||||
|
Assert.False(SceneAccessPolicy.Allows(" ", SourceType.TextOverlay));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user