Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
0213c74
Add restream checks in go live flow.
michelinewu Jun 26, 2025
eb04216
WIP: migrate - recording does not stop.
michelinewu Jun 27, 2025
425a7cd
Track recording displays with output.
michelinewu Jun 27, 2025
ccf315e
WIP fix recording.
michelinewu Jun 27, 2025
ce2c767
WIP: fix recording.
michelinewu Jun 27, 2025
f90723c
WIP
michelinewu Jul 1, 2025
29ed317
Merge branch 'master' into mw_factory_api
michelinewu Jul 7, 2025
56839aa
Fixes for start recording and replay buffer with stream.
michelinewu Jul 7, 2025
437c3d5
WIP: Dual output recording switcher.
michelinewu Jul 7, 2025
2a668db
Fix vertical replay buffer.
michelinewu Jul 8, 2025
bb16236
Remove logs.
michelinewu Jul 8, 2025
99ee4cf
Restore dual output toggle.
michelinewu Jul 8, 2025
4e942f6
Refactor show/hide toggles.
michelinewu Jul 8, 2025
3243a73
Merge branch 'master' into mw_factory_api
michelinewu Jul 14, 2025
7e8f19f
Remove logs.
michelinewu Jul 14, 2025
c599d5f
WIP: load
michelinewu Jul 14, 2025
5db7b36
Fix incorrect recording display error handling.
michelinewu Jul 16, 2025
8b3de42
Fix replay buffer error handling.
michelinewu Jul 16, 2025
f014c24
Fix twitch VOD track and replay buffer animation.
michelinewu Jul 16, 2025
f70b73d
Add default value for recording display toggle.
michelinewu Jul 16, 2025
49bc289
Fix twitch VOD track and replay buffer animation.
michelinewu Jul 16, 2025
0ce47c1
WIP: Recording tooltip UI.
michelinewu Jul 17, 2025
1c45e0d
Dual output recording and replay buffer without streaming always uses…
michelinewu Jul 17, 2025
8cce52c
Correctly update dual output recording state.
michelinewu Jul 18, 2025
2b5dc51
WIP: vods.
michelinewu Jul 18, 2025
8169900
Merge branch 'mw_factory_api' of https://github.com/streamlabs/deskto…
michelinewu Jul 18, 2025
1d9497b
WIP: Recording tooltip UI.
michelinewu Jul 18, 2025
56306ff
Recording only fixes and add display to recording history.
michelinewu Jul 18, 2025
e87926d
Fix styling for radio icons.
michelinewu Jul 22, 2025
b148f4f
WIP: add modal and fix recording.
michelinewu Jul 22, 2025
cb9a2a2
Fixes for vertical recording.
michelinewu Jul 23, 2025
862c03c
Replay buffer fixes.
michelinewu Jul 23, 2025
4fed920
UI polishes.
michelinewu Jul 24, 2025
acb05fe
Merge branch 'master' into mw_factory_api
michelinewu Aug 5, 2025
45d632c
Polishes and logging.
michelinewu Aug 5, 2025
85bff98
Fixes for strictnulls test.
michelinewu Aug 5, 2025
289c379
Fix for simple factory instances not destroying.
michelinewu Aug 7, 2025
1f5f3b1
Fix recording switcher visibility.
michelinewu Aug 7, 2025
ebb4a6d
WIP
michelinewu Aug 8, 2025
00423c1
WIP: fix recording button
michelinewu Aug 19, 2025
1576024
Fix recording switcher state.
michelinewu Aug 20, 2025
b6304dc
Remove resize observers and restore size watchers.
michelinewu Aug 20, 2025
77399b0
Remove logs and vertical recording fix.
michelinewu Aug 20, 2025
583b311
Merge branch 'master' into mw_factory_api
michelinewu Aug 20, 2025
6642ec0
Fix for recording status.
michelinewu Aug 20, 2025
ff1cd20
Replay buffer fixes.
michelinewu Aug 21, 2025
15cbe43
Fixes from automerge overwrites.
michelinewu Aug 21, 2025
c3e6beb
Remove comment.
michelinewu Aug 21, 2025
b09f524
Add loading spinner and fix recording time reset.
michelinewu Aug 22, 2025
3538327
WIP: investigate avatar and highlighter replay buffer enabled by defa…
michelinewu Aug 25, 2025
2b5f4c8
Vertical display highlighter support.
michelinewu Aug 26, 2025
be7fde9
Disable vertical recording when using highlighter.
michelinewu Aug 26, 2025
567e9ad
Fix check for replay buffer instance when stopping replay buffer.
michelinewu Aug 26, 2025
d41fe27
Highlighter and vertical recording handling.
michelinewu Sep 4, 2025
2492c4b
Styling fixes.
michelinewu Sep 4, 2025
33985b2
Fix failed start stream error handling.
michelinewu Sep 4, 2025
523b3ab
Toggle highlighter when toggling off vertical recording.
michelinewu Sep 15, 2025
a973b6a
Merge branch 'master' into mw_factory_api
michelinewu Sep 15, 2025
ddcc3dc
Vertical replay buffer and highlighter handling.
michelinewu Sep 15, 2025
f799b50
Fixes for tests.
michelinewu Sep 15, 2025
1a2f6f3
Fixes for tests.
michelinewu Sep 15, 2025
1b671b5
Optimize studio footer.
michelinewu Sep 17, 2025
e31d64d
Optimize start streaming button.
michelinewu Sep 17, 2025
97d497f
Update recording test
michelinewu Sep 18, 2025
7d57c21
Update osn version.
michelinewu Sep 23, 2025
9ba9c0b
Update toggle streaming dependencies in button.
michelinewu Sep 23, 2025
eb684ec
Merge branch 'master' into mw_factory_api with new osn.
michelinewu Sep 23, 2025
e199db3
Fixes for replay buffer and selective recording tests.
michelinewu Sep 26, 2025
b571b6b
Merge branch 'master' into mw_factory_api
michelinewu Sep 26, 2025
def9527
Fix for dual output tests.
michelinewu Sep 26, 2025
0c1ee78
Merge branch 'master' into mw_factory_api
michelinewu Oct 3, 2025
70b17fe
Fix side nav state update.
michelinewu Oct 3, 2025
e583f93
Add test for streaming with recording and replay buffer.
michelinewu Oct 3, 2025
3b5eecc
Merge branch master into mw_factory_api. Replay buffer not working.
michelinewu Oct 27, 2025
ff899a0
Fix for replay buffer history and UI fixes.
michelinewu Oct 27, 2025
b4e05d8
Merge branch 'master' into mw_factory_api
michelinewu Oct 27, 2025
d131c78
Merge branch 'master' into mw_factory_api
michelinewu Oct 28, 2025
3df5abf
Show vertical replays in highlighter clips.
michelinewu Oct 28, 2025
35298bb
WIP: Highlighter
michelinewu Oct 29, 2025
f02e8fe
Vertical highlighter clips preview correctly.
michelinewu Nov 3, 2025
2095a7e
Merge branch 'master' into mw_factory_api
michelinewu Nov 3, 2025
be44bcc
Fix audio encoder naming when creating instances.
michelinewu Nov 3, 2025
e33e807
Update osn version.
michelinewu Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/components-react/highlighter/ClipPreview.m.less
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
.preview-image {
object-fit: none;
border-radius: 10px;

&.vertical-preview {
margin: 0px 110px;
border-radius: unset !important;
}
}

.deleted-preview {
Expand Down
22 changes: 15 additions & 7 deletions app/components-react/highlighter/ClipPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { TClip } from 'services/highlighter/models/highlighter.models';
import { SCRUB_HEIGHT, SCRUB_WIDTH, SCRUB_FRAMES } from 'services/highlighter/constants';
import {
SCRUB_HEIGHT,
SCRUB_WIDTH,
SCRUB_WIDTH_VERTICAL,
SCRUB_FRAMES,
} from 'services/highlighter/constants';
import React, { useState } from 'react';
import { Services } from 'components-react/service-provider';
import { BoolButtonInput } from 'components-react/shared/inputs/BoolButtonInput';
Expand All @@ -10,7 +15,7 @@ import { isAiClip } from './utils';
import { useVuex } from 'components-react/hooks';
import ClipPreviewInfo from './ClipPreviewInfo';
import { EGame } from 'services/highlighter/models/ai-highlighter.models';
import * as remote from '@electron/remote';
import cx from 'classnames';

export default function ClipPreview(props: {
clipId: string;
Expand All @@ -33,8 +38,10 @@ export default function ClipPreview(props: {
return <>deleted</>;
}

const width = v.clip.display === 'vertical' ? SCRUB_WIDTH_VERTICAL : SCRUB_WIDTH;

function mouseMove(e: React.MouseEvent) {
const frameIdx = Math.floor((e.nativeEvent.offsetX / SCRUB_WIDTH) * SCRUB_FRAMES);
const frameIdx = Math.floor((e.nativeEvent.offsetX / width) * SCRUB_FRAMES);
if (scrubFrame !== frameIdx) {
setScrubFrame(frameIdx);
}
Expand All @@ -50,12 +57,13 @@ export default function ClipPreview(props: {
{!v.clip.deleted && (
<img
src={clipThumbnail}
className={styles.previewImage}
className={cx(styles.previewImage, {
[styles.verticalPreview]: v.clip.display === 'vertical',
})}
style={{
width: `${SCRUB_WIDTH}px`,
width: `${width}px`,
height: `${SCRUB_HEIGHT}px`,

objectPosition: `-${scrubFrame * SCRUB_WIDTH}px`,
objectPosition: `-${scrubFrame * width}px`,
}}
onMouseMove={mouseMove}
onClick={props.emitShowTrim}
Expand Down
16 changes: 11 additions & 5 deletions app/components-react/highlighter/ClipTrimmer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import React, { useEffect, useRef, useState, RefObject } from 'react';
import { TClip } from 'services/highlighter/models/highlighter.models';
import { SCRUB_FRAMES, SCRUB_HEIGHT, SCRUB_WIDTH } from 'services/highlighter/constants';
import {
SCRUB_FRAMES,
SCRUB_HEIGHT,
SCRUB_WIDTH,
SCRUB_WIDTH_VERTICAL,
} from 'services/highlighter/constants';
import { Services } from 'components-react/service-provider';
import times from 'lodash/times';
import styles from './ClipTrimmer.m.less';
Expand Down Expand Up @@ -44,6 +49,7 @@ export default function ClipTrimmer(props: { clip: TClip; streamId: string | und
const [isPlaying, setIsPlaying] = useStateRef(false);

const endTime = props.clip.duration! - localEndTrim;
const width = props.clip?.display === 'vertical' ? SCRUB_WIDTH_VERTICAL : SCRUB_WIDTH;

function playAt(t: number) {
if (!videoRef.current) return;
Expand Down Expand Up @@ -165,7 +171,7 @@ export default function ClipTrimmer(props: { clip: TClip; streamId: string | und
}

const scrubHeight = 100;
const scrubWidth = scrubHeight * (SCRUB_WIDTH / SCRUB_HEIGHT);
const scrubWidth = scrubHeight * (width / SCRUB_HEIGHT);

// TODO: React to window size change
useEffect(() => {
Expand All @@ -187,7 +193,7 @@ export default function ClipTrimmer(props: { clip: TClip; streamId: string | und
<video
ref={videoRef}
src={props.clip.path.replace('#', '%23')}
style={{ borderRadius: 5 }}
style={{ borderRadius: 5, height: '50vh' }}
width="100%"
onEnded={() => {
setIsPlaying(false);
Expand Down Expand Up @@ -218,13 +224,13 @@ export default function ClipTrimmer(props: { clip: TClip; streamId: string | und
<img
key={frame}
src={props.clip.scrubSprite?.replace('#', '%23')}
width={SCRUB_WIDTH}
width={width}
height={SCRUB_HEIGHT}
style={{
height: scrubHeight,
width: scrubWidth,
objectFit: 'cover',
objectPosition: `-${frame * SCRUB_WIDTH * (scrubHeight / SCRUB_HEIGHT)}px`,
objectPosition: `-${frame * width * (scrubHeight / SCRUB_HEIGHT)}px`,
pointerEvents: 'none',
maxWidth: '100%',
}}
Expand Down
4 changes: 2 additions & 2 deletions app/components-react/highlighter/Export/ExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useVuex } from 'components-react/hooks';
import { initStore, useController } from '../../hooks/zustand';
import { EOrientation, TOrientation } from 'services/highlighter/models/ai-highlighter.models';
import { fileExists } from 'services/highlighter/file-utils';
import { SCRUB_HEIGHT, SCRUB_WIDTH, SCRUB_FRAMES } from 'services/highlighter/constants';
import { SCRUB_WIDTH, SCRUB_WIDTH_VERTICAL } from 'services/highlighter/constants';
import styles from './ExportModal.m.less';
import { getCombinedClipsDuration } from '../utils';
import { formatSecondsToHMS } from '../ClipPreview';
Expand Down Expand Up @@ -385,7 +385,7 @@ function ExportFlow({
style={
currentFormat === EOrientation.HORIZONTAL
? { objectPosition: 'left' }
: { objectPosition: `-${(SCRUB_WIDTH * 1.32) / 3 + 4}px` }
: { objectPosition: `-${(SCRUB_WIDTH * 1.32) / 3 + 4}px` } // @@@
}
/>
</div>
Expand Down
5 changes: 3 additions & 2 deletions app/components-react/highlighter/MiniClipPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Services } from 'components-react/service-provider';
import { BoolButtonInput, CheckboxInput } from 'components-react/shared/inputs';
import { SwitchInput } from 'components-react/shared/inputs/SwitchInput';
import React from 'react';
import { SCRUB_HEIGHT, SCRUB_WIDTH } from 'services/highlighter/constants';
import { SCRUB_HEIGHT, SCRUB_WIDTH, SCRUB_WIDTH_VERTICAL } from 'services/highlighter/constants';
import { TClip } from 'services/highlighter/models/highlighter.models';
import styles from './MiniClipPreview.m.less';

Expand All @@ -24,6 +24,7 @@ export default function MiniClipPreview({
}) {
const { HighlighterService } = Services;
const clip = useVuex(() => HighlighterService.views.clipsDictionary[clipId] as TClip);
const width = clip.display === 'vertical' ? SCRUB_WIDTH_VERTICAL : SCRUB_WIDTH;

return (
<div
Expand All @@ -49,7 +50,7 @@ export default function MiniClipPreview({
className={styles.thumbnailSpecs}
style={{
opacity: !clip.enabled ? '0.3' : '1',
width: `${SCRUB_WIDTH / 6}px`,
width: `${width / 6}px`,
height: `${SCRUB_HEIGHT / 6}px`,
}}
></img>
Expand Down
6 changes: 4 additions & 2 deletions app/components-react/highlighter/RemoveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useState } from 'react';
import { TClip } from 'services/highlighter/models/highlighter.models';
import { $t } from 'services/i18n';
import styles from './RemoveModal.m.less';
import { SCRUB_HEIGHT, SCRUB_WIDTH } from 'services/highlighter/constants';
import { SCRUB_HEIGHT, SCRUB_WIDTH, SCRUB_WIDTH_VERTICAL } from 'services/highlighter/constants';

export default function RemoveModal(p: {
removeType: 'clip' | 'stream';
Expand All @@ -17,6 +17,8 @@ export default function RemoveModal(p: {
const [deleteAllSelected, setDeleteAllSelected] = useState<boolean>(false);
const [clipsToDelete, setClipsToDelete] = useState<TClip[]>([p.clip]);

const width = p.clip.display === 'vertical' ? SCRUB_WIDTH_VERTICAL : SCRUB_WIDTH;

function getClipsToDelete(): TClip[] {
return HighlighterService.getClips(HighlighterService.views.clips, p.streamId).filter(
clip => clip.path !== p.clip.path && clip.enabled,
Expand All @@ -38,7 +40,7 @@ export default function RemoveModal(p: {
<div
className={styles.thumbnail}
style={{
width: `${SCRUB_WIDTH / 2}px`,
width: `${width / 2}px`,
height: `${SCRUB_HEIGHT / 2}px`,
rotate: `${clipsToDelete.length !== 1 ? (index - 1) * 6 : 0}deg`,
scale: '1.2',
Expand Down
58 changes: 48 additions & 10 deletions app/components-react/highlighter/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import Scrollable from 'components-react/shared/Scrollable';
import styles from './SettingsView.m.less';
import { $t } from 'services/i18n';
import { EHighlighterView, IViewState } from 'services/highlighter/models/highlighter.models';
import { EAvailableFeatures } from 'services/incremental-rollout';
import SupportedGames from './supportedGames/SupportedGames';
import { promptAction } from 'components-react/modals';

export default function SettingsView({
emitSetView,
Expand All @@ -22,13 +22,7 @@ export default function SettingsView({
emitSetView: (data: IViewState) => void;
close: () => void;
}) {
const {
HotkeysService,
SettingsService,
StreamingService,
HighlighterService,
IncrementalRolloutService,
} = Services;
const { HotkeysService, SettingsService, StreamingService, HighlighterService } = Services;
const aiHighlighterFeatureEnabled = HighlighterService.aiHighlighterFeatureEnabled;
const [hotkey, setHotkey] = useState<IHotkey | null>(null);
const hotkeyRef = useRef<IHotkey | null>(null);
Expand All @@ -38,8 +32,14 @@ export default function SettingsView({
isStreaming: StreamingService.isStreaming,
useAiHighlighter: HighlighterService.views.useAiHighlighter,
highlighterVersion: HighlighterService.views.highlighterVersion,
isVerticalRecording: StreamingService.views.isVerticalRecording,
isVerticalReplayBuffer: StreamingService.views.isVerticalReplayBuffer,
outputDisplay: StreamingService.views.outputDisplay,
}));

const disableAIHighlighter =
(v.isVerticalRecording || v.isVerticalReplayBuffer) && v.outputDisplay === 'vertical';

const correctlyConfigured =
v.settingsValues.Output.RecRB &&
v.settingsValues.General.ReplayBufferWhileStreaming &&
Expand Down Expand Up @@ -112,6 +112,44 @@ export default function SettingsView({
HighlighterService.actions.toggleAiHighlighter();
}

function handleToggleHighlighter() {
if (disableAIHighlighter) {
const title = v.isVerticalRecording
? $t('Vertical Recording Active')
: $t('Vertical Replay Buffer Active');

const message = v.isVerticalRecording
? $t(
'Vertical recording is in-progress. Would you like to stop the recording to enable AI Highlighter?',
)
: $t(
'Vertical replay buffer is active. Would you like to stop the replay buffer to enable AI Highlighter?',
);

const btnText = v.isVerticalRecording ? $t('Stop Recording') : $t('Stop Replay Buffer');

promptAction({
title,
message,
btnText,
fn: () => {
if (v.isVerticalRecording) {
StreamingService.actions.toggleRecording();
} else {
StreamingService.actions.stopReplayBuffer();
}
toggleUseAiHighlighter();
},
cancelBtnPosition: 'left',
cancelBtnText: $t('Cancel'),
});

return;
}

toggleUseAiHighlighter();
}

return (
<div className={styles.settingsViewRoot}>
<div style={{ display: 'flex', padding: 20 }}>
Expand Down Expand Up @@ -175,8 +213,8 @@ export default function SettingsView({
<SwitchInput
style={{ margin: 0, marginLeft: '-10px' }}
size="default"
value={v.useAiHighlighter}
onChange={toggleUseAiHighlighter}
value={disableAIHighlighter ? false : v.useAiHighlighter}
onChange={handleToggleHighlighter}
/>
) : (
<Button
Expand Down
5 changes: 3 additions & 2 deletions app/components-react/modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,13 @@ export function promptAsync(
export function promptAction(p: {
title: string;
message: string;
btnText: string;
btns?: { text: string; fn?: () => void }[];
btnText?: string;
btnType?: 'default' | 'primary';
cancelBtnPosition?: 'left' | 'right' | 'none';
cancelBtnText?: string;
fn?(): void | ((props: any) => unknown | void);
cancelFn?: void | ((props?: any) => unknown | void);
cancelFn?(): void | ((props?: any) => unknown | void);
icon?: React.ReactNode;
secondaryActionText?: string;
secondaryActionFn?: () => unknown;
Expand Down
2 changes: 1 addition & 1 deletion app/components-react/obs/ObsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { LegacyRef, RefObject, forwardRef, useMemo, useRef, useState } from 'react';
import React, { LegacyRef, RefObject, forwardRef, useRef, useState } from 'react';
import { ISettingsSubCategory } from '../../services/settings';
import {
IObsInput,
Expand Down
6 changes: 6 additions & 0 deletions app/components-react/pages/RecordingHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,12 @@ export function RecordingHistory(p: { className?: string }) {
<Scrollable style={{ height: '100%' }}>
{recordings.map(recording => (
<div className={styles.recording} key={recording.timestamp}>
{recording?.display && recording.display === 'vertical' && (
<i className="icon-phone-case" style={{ paddingRight: '10px' }} />
)}
{recording?.display && recording.display === 'horizontal' && (
<i className="icon-desktop" style={{ paddingRight: '10px' }} />
)}
<span style={{ marginRight: '8px' }}>{formattedTimestamp(recording.timestamp)}</span>
<Tooltip title={$t('Show in folder')}>
<span
Expand Down
1 change: 0 additions & 1 deletion app/components-react/root/Banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import * as remote from '@electron/remote';
import { useVuex } from 'components-react/hooks';
import { Services } from 'components-react/service-provider';
import { TAppPage } from 'services/navigation';
import { $t } from 'services/i18n';
Expand Down
17 changes: 11 additions & 6 deletions app/components-react/root/StartStreamingButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,18 @@ export default function StartStreamingButton(p: { disabled?: boolean }) {
}
}, []);

const getIsRedButton =
streamingStatus !== EStreamingState.Offline && streamShiftStatus !== 'pending';
const getIsRedButton = useMemo(
() => streamingStatus !== EStreamingState.Offline && streamShiftStatus !== 'pending',
[streamingStatus],
);

const isDisabled =
p.disabled ||
(streamingStatus === EStreamingState.Starting && delaySecondsRemaining === 0) ||
(streamingStatus === EStreamingState.Ending && delaySecondsRemaining === 0);
const isDisabled = useMemo(() => {
return (
p.disabled ||
(streamingStatus === EStreamingState.Starting && delaySecondsRemaining === 0) ||
(streamingStatus === EStreamingState.Ending && delaySecondsRemaining === 0)
);
}, [p.disabled, streamingStatus, delaySecondsRemaining]);

const fetchStreamShiftStatus = useCallback(async () => {
try {
Expand Down
Loading
Loading