Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
99700de
Add factory api functions for streaming.
michelinewu Dec 1, 2025
cc4632b
Add shutdown to streaming service.
michelinewu Dec 1, 2025
54c2de2
Enhanced broadcasting with multistream.
michelinewu Oct 28, 2025
c444f66
WIP
michelinewu Dec 1, 2025
97099fb
Enhanced broadcasting UI changes and restream setup changes.
michelinewu Dec 1, 2025
4afef5f
Use factory api migration create streaming to create additional strea…
michelinewu Dec 3, 2025
1a631c5
WIP: Enhanced broadcasting with vertical display.
michelinewu Dec 3, 2025
7f96369
WIP: Twitch dual stream multistream.
michelinewu Dec 3, 2025
fdf933f
Multistream enhanced broadcasting in dual output mode.
michelinewu Dec 3, 2025
cd9b53d
Merge branch 'staging' into mw_eb_ms_4
michelinewu Dec 4, 2025
a581f0d
WIP
michelinewu Dec 4, 2025
08b01cf
Dual Output Enhanced Broadcasting single streams displays.
michelinewu Dec 5, 2025
12dd10b
Dual Output Enhanced Broadcast multistream and Dual Output Twitch Dua…
michelinewu Dec 5, 2025
7b44974
Code cleanup.
michelinewu Dec 5, 2025
438fde2
Stictnulls fixes.
michelinewu Dec 5, 2025
599e459
Merge branch 'master' into mw_eb_ms_4
michelinewu Jan 9, 2026
d7ba247
Always show enhanced broadcasting checkbox.
michelinewu Jan 9, 2026
6a43d00
Merge branch 'master' into mw_eb_ms_4
michelinewu Jan 27, 2026
44f7c58
Merge branch 'master' into mw_eb_ms_4
michelinewu Jan 27, 2026
37c125c
WIP: Factory API migration, everything except streaming service.
michelinewu Jan 27, 2026
5fcef18
WIP: Factory API migration, highlighter.
michelinewu Jan 27, 2026
d5489d1
WIP Factory API Migration: Migrate recording and replay buffer with t…
michelinewu Jan 28, 2026
8b6ab2d
WIP: use factory api for enhanced broadcasting.
michelinewu Feb 6, 2026
be088ad
Merge branch 'master' into mw_eb_ms_4
michelinewu Feb 6, 2026
6716203
WIP
michelinewu Feb 6, 2026
5e30816
WIP: Factory API Enhanced broadcasting Simple Mode single output and …
michelinewu Feb 9, 2026
23befdb
WIP: Factory API Enhanced Broadcasting multistream single output.
michelinewu Feb 9, 2026
f76b59c
WIP: Factory API Enhanced broadcasting dual output single streams and…
michelinewu Feb 10, 2026
49342e2
Factory API enhanced broadcasting multistream dual output.
michelinewu Feb 11, 2026
2370360
Fixes for Twitch dual stream.
michelinewu Feb 11, 2026
b3923ae
Code cleanup.
michelinewu Feb 11, 2026
a71e802
Fixes for strictnulls.
michelinewu Feb 11, 2026
12e2e9a
Merge branch 'master' into mw_eb_ms_4
michelinewu Feb 11, 2026
ed2958f
Fix toggling enhanced broadcasting from form.
michelinewu Feb 13, 2026
43aad28
Fixes for and expand Selective Recording test.
michelinewu Feb 13, 2026
ba87f1d
Merge branch 'mw_eb_ms_4' of https://github.com/streamlabs/desktop in…
michelinewu Feb 13, 2026
31caae2
Correct and expand settings applied to streaming and recording instan…
michelinewu Feb 24, 2026
3acfcc5
New OSN version.
michelinewu Feb 24, 2026
3c98b03
Update osn version.
michelinewu Feb 24, 2026
1ee7d90
Update repositories.json
summeroff Mar 10, 2026
a9f23aa
Merge branch 'master' into mw_eb_ms_4
michelinewu Mar 11, 2026
519df9d
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 11, 2026
a5a2a03
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 13, 2026
a3c1cb1
Merge branch 'mw_eb_ms_4' of https://github.com/streamlabs/desktop in…
michelinewu Mar 13, 2026
50cb32b
Fixes for tests.
michelinewu Mar 13, 2026
45f8fca
More test fixes.
michelinewu Mar 13, 2026
fe09c08
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 17, 2026
8e2c56d
Merge branch 'mw_eb_ms_4' of https://github.com/streamlabs/desktop in…
michelinewu Mar 17, 2026
bad792c
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 18, 2026
0d76042
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 18, 2026
6f7bcb1
Fix video encoder mapping.
michelinewu Mar 18, 2026
822503d
Refactor status updating UI.
michelinewu Mar 19, 2026
e55043e
Todo for recording test.
michelinewu Mar 19, 2026
87fc21b
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 19, 2026
08d243d
Remove duplicate function implementation and add commits from 8dd2f1f…
michelinewu Mar 19, 2026
a9688fc
Merge branch 'staging' into mw_eb_ms_4
michelinewu Mar 19, 2026
1688ced
Fixes from code review
michelinewu Mar 20, 2026
09d81b0
Fix ff
michelinewu Mar 20, 2026
46ea96d
Fix recording test.
michelinewu Mar 20, 2026
3588701
Add unmount to studio footer and start streaming button.
michelinewu Mar 20, 2026
6499bd4
Fix recording signals.
michelinewu Mar 20, 2026
7277886
Remove log.
michelinewu Mar 20, 2026
4362c4a
Fix create streaming create service.
michelinewu Mar 20, 2026
d8cd334
Fix streaming refactor error after code review.
michelinewu Mar 20, 2026
4726ffe
UI optimization, test fixes, and add signal handling test.
michelinewu Mar 23, 2026
b30a3fe
Revert to a9688fc.
michelinewu Mar 23, 2026
054d4a0
Apply smaller changes from code review.
michelinewu Mar 23, 2026
7d8d004
Streaming service changes from code review.
michelinewu Mar 23, 2026
9b676d4
Memoize studio footer.
michelinewu Mar 23, 2026
007f17e
Test fixes.
michelinewu Mar 23, 2026
65a8d86
Fix streaming accidentally starting on create.
michelinewu Mar 23, 2026
f6b8f10
Add getters for frames to video settings service.
michelinewu Mar 23, 2026
4a71e95
Remove comments and logs, restore rtmp common service handling and ad…
michelinewu Mar 23, 2026
9041849
Fix to for destroying contexts on shutdown.
michelinewu Mar 23, 2026
2a12dcb
Dismiss alert in Twitch test.
michelinewu Mar 23, 2026
5a35059
Fixes for dual output and protected mode tests.
michelinewu Mar 24, 2026
9b80a5d
Remove returned promise when creating recording and unify signal hand…
michelinewu Mar 24, 2026
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 @@ -17,6 +17,11 @@
border-radius: 10px;
}

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

.deleted-preview {
border-radius: 10px;
background: black;
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
18 changes: 5 additions & 13 deletions app/components-react/highlighter/Export/ExportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import React, { useState, useEffect, useMemo, useRef } from 'react';
import {
EExportStep,
TFPS,
TResolution,
TPreset,
} from 'services/highlighter/models/rendering.models';
import React, { useState, useEffect, useMemo } from 'react';
import { TFPS, TResolution, TPreset } from 'services/highlighter/models/rendering.models';
import { Services } from 'components-react/service-provider';
import { FileInput, TextInput, ListInput } from 'components-react/shared/inputs';
import { FileInput } from 'components-react/shared/inputs';
import Form from 'components-react/shared/inputs/Form';
import path from 'path';
import { Button, Progress, Alert, Dropdown } from 'antd';
import YoutubeUpload from './YoutubeUpload';
import { RadioInput } from 'components-react/shared/inputs/RadioInput';
import { confirmAsync } from 'components-react/modals';
import { $t } from 'services/i18n';
import StorageUpload from './StorageUpload';
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 +377,7 @@ function ExportFlow({
style={
currentFormat === EOrientation.HORIZONTAL
? { objectPosition: 'left' }
: { objectPosition: `-${(SCRUB_WIDTH * 1.32) / 3 + 4}px` }
: { objectPosition: `-${(SCRUB_WIDTH_VERTICAL * 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
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
2 changes: 1 addition & 1 deletion app/components-react/root/LiveDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class LiveDockController {
}

get streamingStatus() {
return this.streamingService.state.streamingStatus;
return this.streamingService.views.streamingStatus;
}

get isStreaming() {
Expand Down
Loading
Loading