Skip to content

Commit 5cf1aa1

Browse files
committed
Add local recording controls (#38)
1 parent df3d068 commit 5cf1aa1

6 files changed

Lines changed: 207 additions & 0 deletions

File tree

src/PrompterOne.Shared/Contracts/UiTestIds.GoLive.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public static class GoLive
2222
public const string LiveKitServer = "go-live-livekit-server";
2323
public const string LiveKitToggle = "go-live-livekit-toggle";
2424
public const string LiveKitToken = "go-live-livekit-token";
25+
public const string LocalRecordingAudioButton = "go-live-local-recording-audio-button";
26+
public const string LocalRecordingControls = "go-live-local-recording-controls";
27+
public const string LocalRecordingStatus = "go-live-local-recording-status";
28+
public const string LocalRecordingVideoButton = "go-live-local-recording-video-button";
2529
public const string ModeDirector = "go-live-mode-director";
2630
public const string ModeStudio = "go-live-mode-studio";
2731
public const string OpenHome = Back;

src/PrompterOne.Shared/GoLive/Components/GoLiveStudioSidebar.razor

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,35 @@
2626
@if (ActiveTab == GoLiveStudioTab.Stream)
2727
{
2828
<div class="gl-tab-content active">
29+
<div class="gl-info-section"
30+
data-test="@UiTestIds.GoLive.LocalRecordingControls">
31+
<span class="gl-info-label">Local recording</span>
32+
<div class="gl-local-rec-grid">
33+
<button type="button"
34+
class="gl-local-rec-btn @(IsRecordingActive ? "active" : null)"
35+
disabled="@(!CanControlLocalRecording)"
36+
data-state="@LocalRecordingState"
37+
data-test="@UiTestIds.GoLive.LocalRecordingVideoButton"
38+
@onclick="ToggleLocalRecording">
39+
<UiIcon Kind="UiIconKind.Video" Size="13" />
40+
<strong>Video</strong>
41+
<span>@LocalRecordingVideoStatus</span>
42+
</button>
43+
<button type="button"
44+
class="gl-local-rec-btn @(IsRecordingActive ? "active" : null)"
45+
disabled="@(!CanControlLocalRecording || !HasPrimaryMicrophone)"
46+
data-state="@(HasPrimaryMicrophone ? LocalRecordingState : UnavailableStateValue)"
47+
data-test="@UiTestIds.GoLive.LocalRecordingAudioButton"
48+
@onclick="ToggleLocalRecording">
49+
<UiIcon Kind="UiIconKind.Microphone" Size="13" />
50+
<strong>Audio</strong>
51+
<span>@LocalRecordingAudioStatus</span>
52+
</button>
53+
</div>
54+
<p class="gl-local-rec-copy"
55+
data-test="@UiTestIds.GoLive.LocalRecordingStatus">@LocalRecordingDetail</p>
56+
</div>
57+
2958
<div class="gl-info-section">
3059
<span class="gl-info-label">@Text(GoLiveText.Sidebar.DestinationsLabel)</span>
3160
<div class="gl-dest-list">
@@ -229,13 +258,19 @@
229258
private const string ActiveMeterStateValue = "active";
230259
private const string DisabledStateValue = "false";
231260
private const string EnabledStateValue = "true";
261+
private const string RecordingStateValue = "recording";
262+
private const string StoppedStateValue = "stopped";
263+
private const string UnavailableStateValue = "unavailable";
232264

233265
[Parameter] public GoLiveStudioTab ActiveTab { get; set; }
234266
[Parameter] public IReadOnlyList<GoLiveAudioChannelViewModel> AudioChannels { get; set; } = [];
267+
[Parameter] public bool CanControlLocalRecording { get; set; }
235268
[Parameter] public EventCallback CreateRoom { get; set; }
236269
[Parameter] public bool CueArmed { get; set; }
237270
[Parameter] public IReadOnlyList<GoLiveDestinationSummaryViewModel> Destinations { get; set; } = [];
238271
[Parameter] public bool IsRoomActive { get; set; }
272+
[Parameter] public bool IsRecordingActive { get; set; }
273+
[Parameter] public bool HasPrimaryMicrophone { get; set; }
239274
[Parameter] public bool MuteAllGuests { get; set; }
240275
[Parameter] public IReadOnlyList<GoLiveRoomParticipantViewModel> Participants { get; set; } = [];
241276
[Parameter] public string RoomCode { get; set; } = string.Empty;
@@ -244,6 +279,7 @@
244279
[Parameter] public EventCallback SendCue { get; set; }
245280
[Parameter] public IReadOnlyList<GoLiveMetricViewModel> StatusMetrics { get; set; } = [];
246281
[Parameter] public EventCallback<string> ToggleDestination { get; set; }
282+
[Parameter] public EventCallback ToggleLocalRecording { get; set; }
247283
[Parameter] public EventCallback ToggleMuteAllGuests { get; set; }
248284
[Parameter] public EventCallback ToggleTalkback { get; set; }
249285
[Parameter] public bool TalkbackEnabled { get; set; }
@@ -252,5 +288,31 @@
252288
private bool _muteAllGuests => MuteAllGuests;
253289
private bool _talkbackEnabled => TalkbackEnabled;
254290

291+
private string LocalRecordingState =>
292+
!CanControlLocalRecording
293+
? UnavailableStateValue
294+
: IsRecordingActive
295+
? RecordingStateValue
296+
: StoppedStateValue;
297+
298+
private string LocalRecordingVideoStatus =>
299+
!CanControlLocalRecording
300+
? "Unavailable"
301+
: IsRecordingActive
302+
? "Recording"
303+
: "Stopped";
304+
305+
private string LocalRecordingAudioStatus =>
306+
!HasPrimaryMicrophone
307+
? "Unavailable"
308+
: IsRecordingActive
309+
? "Recording"
310+
: "Stopped";
311+
312+
private string LocalRecordingDetail =>
313+
!CanControlLocalRecording
314+
? "Camera permission or a local source is required."
315+
: "Browser-local audio/video export. No stream or cloud upload.";
316+
255317
private string Text(string key) => Localizer[key];
256318
}

src/PrompterOne.Shared/GoLive/Components/GoLiveStudioSidebar.razor.css

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,59 @@
7272
flex-direction: column;
7373
}
7474

75+
.gl-local-rec-grid {
76+
display: grid;
77+
grid-template-columns: repeat(2, minmax(0, 1fr));
78+
gap: 8px;
79+
}
80+
81+
.gl-local-rec-btn {
82+
display: grid;
83+
min-height: 70px;
84+
min-width: 0;
85+
align-content: center;
86+
justify-items: center;
87+
gap: 4px;
88+
padding: 9px 8px;
89+
border: 1px solid rgba(255, 255, 255, .07);
90+
border-radius: 8px;
91+
background: rgba(255, 255, 255, .035);
92+
color: rgba(248, 241, 226, .7);
93+
cursor: pointer;
94+
}
95+
96+
.gl-local-rec-btn:hover:not(:disabled) {
97+
border-color: rgba(255, 141, 141, .28);
98+
background: rgba(255, 141, 141, .08);
99+
}
100+
101+
.gl-local-rec-btn.active {
102+
border-color: rgba(255, 107, 107, .45);
103+
background: rgba(255, 107, 107, .14);
104+
color: rgba(255, 245, 224, .94);
105+
}
106+
107+
.gl-local-rec-btn:disabled {
108+
cursor: not-allowed;
109+
opacity: .48;
110+
}
111+
112+
.gl-local-rec-btn strong {
113+
font-size: 12px;
114+
line-height: 1.2;
115+
}
116+
117+
.gl-local-rec-btn span,
118+
.gl-local-rec-copy {
119+
color: rgba(248, 241, 226, .48);
120+
font-size: 10px;
121+
line-height: 1.35;
122+
}
123+
124+
.gl-local-rec-copy {
125+
margin: 8px 0 0;
126+
}
127+
75128
.gl-destination-card {
76129
display: flex;
77130
flex-direction: column;

src/PrompterOne.Shared/GoLive/Pages/GoLivePage.razor

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,13 @@
174174
Title="@Text(GoLiveText.Chrome.LivePreviewTitle)" />
175175
<GoLiveStudioSidebar ActiveTab="@_activeStudioTab"
176176
AudioChannels="@AudioChannels"
177+
CanControlLocalRecording="@CanControlProgram"
177178
CreateRoom="CreateRoomAsync"
178179
CueArmed="@_cueArmed"
179180
Destinations="@DestinationSummary"
181+
HasPrimaryMicrophone="@HasPrimaryMicrophone"
180182
IsRoomActive="@IsRoomActive"
183+
IsRecordingActive="@GoLiveSession.State.IsRecordingActive"
181184
MuteAllGuests="@_muteAllGuests"
182185
Participants="@Participants"
183186
RoomCode="@RoomCode"
@@ -187,6 +190,7 @@
187190
StatusMetrics="@StatusMetrics"
188191
TalkbackEnabled="@_talkbackEnabled"
189192
ToggleDestination="ToggleDestinationSummaryAsync"
193+
ToggleLocalRecording="ToggleRecordingSessionAsync"
190194
ToggleMuteAllGuests="ToggleMuteAllGuestsAsync"
191195
ToggleTalkback="ToggleTalkbackAsync" />
192196
</aside>

tests/PrompterOne.Web.Tests/GoLive/GoLiveSessionInteractionTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,41 @@ public void GoLivePage_Load_HidesLocalOutputTogglesFromRuntimeSidebar()
7878
});
7979
}
8080

81+
[Test]
82+
public void GoLivePage_LocalRecordingControls_ShowBrowserLocalStatesAndToggleRecording()
83+
{
84+
SeedSceneState(CreateTwoCameraScene());
85+
SeedStudioSettings(StudioSettings.Default with
86+
{
87+
Streaming = StudioSettings.Default.Streaming with
88+
{
89+
Recording = new RecordingProfile(IsEnabled: true)
90+
}
91+
});
92+
93+
Services.GetRequiredService<NavigationManager>().NavigateTo(AppTestData.Routes.GoLiveDemo);
94+
var cut = Render<GoLivePage>();
95+
96+
cut.WaitForAssertion(() =>
97+
{
98+
Assert.NotNull(cut.FindByTestId(UiTestIds.GoLive.LocalRecordingControls));
99+
Assert.Contains("Browser-local", cut.FindByTestId(UiTestIds.GoLive.LocalRecordingStatus).TextContent, StringComparison.Ordinal);
100+
Assert.Equal("stopped", cut.FindByTestId(UiTestIds.GoLive.LocalRecordingVideoButton).GetAttribute("data-state"));
101+
Assert.Equal("stopped", cut.FindByTestId(UiTestIds.GoLive.LocalRecordingAudioButton).GetAttribute("data-state"));
102+
});
103+
104+
cut.FindByTestId(UiTestIds.GoLive.LocalRecordingVideoButton).Click();
105+
106+
cut.WaitForAssertion(() =>
107+
{
108+
Assert.Contains(
109+
_harness.JsRuntime.Invocations,
110+
invocation => string.Equals(invocation, StartLocalRecordingInteropMethod, StringComparison.Ordinal));
111+
Assert.Equal("recording", cut.FindByTestId(UiTestIds.GoLive.LocalRecordingVideoButton).GetAttribute("data-state"));
112+
Assert.Equal("recording", cut.FindByTestId(UiTestIds.GoLive.LocalRecordingAudioButton).GetAttribute("data-state"));
113+
});
114+
}
115+
81116
[Test]
82117
public void GoLivePage_StartStream_WithVdoNinjaArmed_CallsVdoNinjaOutputInterop()
83118
{

tests/PrompterOne.Web.UITests.Studio/GoLive/GoLiveShellSessionFlowTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,55 @@ await Assert.That(runtimeMetricText).Contains(recordingState.GetProperty("mimeTy
295295
}
296296
}
297297

298+
[Test]
299+
public async Task GoLivePage_LocalRecordingControls_ToggleBrowserLocalRecordingStates()
300+
{
301+
var page = await _fixture.NewPageAsync(additionalContext: true);
302+
303+
try
304+
{
305+
await GoLiveFlowTests.SeedGoLiveSceneForReuseAsync(page);
306+
await GoLiveTestSeedHelper.SeedBrowserLocalRecordingPreferencesAsync(page);
307+
await StudioRouteDriver.OpenGoLiveAsync(page);
308+
await Expect(page.GetByTestId(UiTestIds.GoLive.Page)).ToBeVisibleAsync();
309+
310+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingControls)).ToBeVisibleAsync();
311+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingStatus)).ToContainTextAsync("Browser-local");
312+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingVideoButton))
313+
.ToHaveAttributeAsync("data-state", "stopped");
314+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingAudioButton))
315+
.ToHaveAttributeAsync("data-state", "stopped");
316+
317+
await UiInteractionDriver.ClickAndContinueAsync(
318+
page.GetByTestId(UiTestIds.GoLive.LocalRecordingVideoButton),
319+
noWaitAfter: true);
320+
await page.WaitForFunctionAsync(
321+
BrowserTestConstants.GoLive.RecordingRuntimeActiveScript,
322+
BrowserTestConstants.GoLive.RuntimeSessionId,
323+
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
324+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingVideoButton))
325+
.ToHaveAttributeAsync("data-state", "recording");
326+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingAudioButton))
327+
.ToHaveAttributeAsync("data-state", "recording");
328+
329+
await UiInteractionDriver.ClickAndContinueAsync(
330+
page.GetByTestId(UiTestIds.GoLive.LocalRecordingAudioButton),
331+
noWaitAfter: true);
332+
await page.WaitForFunctionAsync(
333+
BrowserTestConstants.GoLive.RecordingRuntimeInactiveScript,
334+
BrowserTestConstants.GoLive.RuntimeSessionId,
335+
new() { Timeout = BrowserTestConstants.Timing.ExtendedVisibleTimeoutMs });
336+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingVideoButton))
337+
.ToHaveAttributeAsync("data-state", "stopped");
338+
await Expect(page.GetByTestId(UiTestIds.GoLive.LocalRecordingAudioButton))
339+
.ToHaveAttributeAsync("data-state", "stopped");
340+
}
341+
finally
342+
{
343+
await page.Context.CloseAsync();
344+
}
345+
}
346+
298347
[Test]
299348
public async Task GoLivePage_StartRecording_FilePickerSave_ProducesDecodableProgramVideoAndAudio()
300349
{

0 commit comments

Comments
 (0)