Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
3 changes: 3 additions & 0 deletions app/app-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export { SideNavService } from 'services/side-nav';
export { VideoSettingsService } from 'services/settings-v2/video';
export { SettingsManagerService } from 'services/settings-manager';
export { MarkersService } from 'services/markers';
export { ScreenshotService } from 'services/screenshot';
export { RealmService } from 'services/realm';
export { StreamAvatarService } from 'services/stream-avatar/stream-avatar-service';
export { StreamAvatarApiService } from 'services/stream-avatar/stream-avatar-api-service';
Expand Down Expand Up @@ -195,6 +196,7 @@ import { VideoSettingsService } from 'services/settings-v2/video';
import { SettingsManagerService } from 'services/settings-manager';
import { DualOutputService } from 'services/dual-output';
import { MarkersService } from 'services/markers';
import { ScreenshotService } from 'services/screenshot';
import { SharedStorageService } from 'services/integrations/shared-storage';
import { RealmService } from 'services/realm';
import { InstagramService } from 'services/platforms/instagram';
Expand Down Expand Up @@ -292,6 +294,7 @@ export const AppServices = {
SettingsManagerService,
DualOutputService,
MarkersService,
ScreenshotService,
SharedStorageService,
RealmService,
RemoteControlService,
Expand Down
5 changes: 5 additions & 0 deletions app/i18n/en-US/hotkeys.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"Start Streaming": "Start Streaming",
"Stop Streaming": "Stop Streaming",
"Screenshot Output": "Screenshot Output",
"Screenshot saved as %{filename}": "Screenshot saved as %{filename}",
"Could not save screenshot": "Could not save screenshot",
"Screenshots need a newer version of the Streamlabs backend.": "Screenshots need a newer version of the Streamlabs backend.",
"Set a recording folder in Settings > Output before taking a screenshot.": "Set a recording folder in Settings > Output before taking a screenshot.",
"Start Recording": "Start Recording",
"Stop Recording": "Stop Recording",
"Switch to scene": "Switch to scene",
Expand Down
10 changes: 10 additions & 0 deletions app/services/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SourcesService, TSourceType } from 'services/sources';
import { TransitionsService } from 'services/transitions';
import { KeyListenerService } from 'services/key-listener';
import { MarkersService } from 'services/markers';
import { ScreenshotService } from 'services/screenshot';
import { Inject } from 'services/core/injector';
import { StatefulService, mutation, ServiceHelper } from 'services';
import defer from 'lodash/defer';
Expand Down Expand Up @@ -54,6 +55,10 @@ function getMarkersService(): MarkersService {
return MarkersService.instance;
}

function getScreenshotService(): ScreenshotService {
return ScreenshotService.instance;
}

const isAudio = (sourceId: string) => {
const source = getSourcesService().views.getSource(sourceId);

Expand Down Expand Up @@ -177,6 +182,11 @@ const GENERAL_ACTIONS: HotkeyGroup = {
description: () => $t('Split Recording File'),
down: () => getStreamingService().splitFile(),
},
SCREENSHOT_OUTPUT: {
name: 'SCREENSHOT_OUTPUT',
description: () => $t('Screenshot Output'),
down: () => getScreenshotService().takeScreenshot(),
},
TOGGLE_OVERLAY: {
name: 'TOGGLE_OVERLAY',
description: () => $t('Toggle in-game overlay'),
Expand Down
90 changes: 90 additions & 0 deletions app/services/screenshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import fs from 'fs';
import path from 'path';
import * as remote from '@electron/remote';
import { Inject, Service } from 'services/core';
import * as obs from '../../obs-api';
import { $t } from './i18n';
import { SettingsService } from './settings';
import { OutputSettingsService } from './settings/output';
import { TDisplayType, VideoSettingsService } from './settings-v2/video';
import { ENotificationType, NotificationsService } from './notifications';
import { JsonrpcService } from './api/jsonrpc';
import { UsageStatisticsService } from './usage-statistics';

/** OBS's default "Filename Formatting", used when the setting is empty. */
const DEFAULT_FILENAME_FORMAT = '%CCYY-%MM-%DD %hh-%mm-%ss';

/**
* Saves a still of the program output, the way OBS Studio's "Screenshot Output"
* hotkey does: a PNG at canvas resolution, written to the recording folder as
* `Screenshot <Filename Formatting>.png`. The capture itself happens in the OBS
* backend (`OBS_content_takeScreenshot`); this service supplies the settings and
* reports the result.
*/
export class ScreenshotService extends Service {
@Inject() private settingsService: SettingsService;
@Inject() private outputSettingsService: OutputSettingsService;
@Inject() private videoSettingsService: VideoSettingsService;
@Inject() private notificationsService: NotificationsService;
@Inject() private jsonrpcService: JsonrpcService;
@Inject() private usageStatisticsService: UsageStatisticsService;

async takeScreenshot(display: TDisplayType = 'horizontal') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; deferred until the osn release with OBS_content_takeScreenshot is pinned. Before that, a test can only reach the needs-newer-backend path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (typeof obs.NodeObs.OBS_content_takeScreenshot !== 'function') {
this.warn($t('Screenshots need a newer version of the Streamlabs backend.'));
return;
}

const context =
this.videoSettingsService.contexts[display] ?? this.videoSettingsService.contexts.horizontal;
Comment thread
summeroff marked this conversation as resolved.
Outdated
if (!context) {
this.warn($t('Could not save screenshot'));
return;
}

const directory = this.outputSettingsService.getRecordingSettings(display).path;
if (!directory || !fs.existsSync(directory)) {
this.warn($t('Set a recording folder in Settings > Output before taking a screenshot.'));
return;
}

const output = this.settingsService.state.Output.formData;
const advanced = this.settingsService.state.Advanced.formData;
const mode = this.settingsService.findSettingValue(output, 'Untitled', 'Mode');
const noSpaceKey = mode === 'Advanced' ? 'RecFileNameWithoutSpace' : 'FileNameWithoutSpace';
const noSpace = !!this.settingsService.findSettingValue(output, 'Recording', noSpaceKey);
const format: string =
this.settingsService.findSettingValue(advanced, 'Recording', 'FilenameFormatting') ||
DEFAULT_FILENAME_FORMAT;

try {
const result = await obs.NodeObs.OBS_content_takeScreenshot(
context,
directory,
format,
noSpace,
);
this.notificationsService.push({
type: ENotificationType.SUCCESS,
message: $t('Screenshot saved as %{filename}', { filename: path.basename(result.path) }),
action: this.jsonrpcService.createRequest(
Service.getResourceId(this),
'showScreenshot',
result.path,
),
});
this.usageStatisticsService.recordFeatureUsage('ScreenshotOutput');
} catch (e: unknown) {
console.error('Failed to take screenshot', e);
this.warn($t('Could not save screenshot'));
}
}

showScreenshot(filePath: string) {
remote.shell.showItemInFolder(filePath);
}

private warn(message: string) {
this.notificationsService.push({ type: ENotificationType.WARNING, message });
}
}
1 change: 1 addition & 0 deletions test/regular/settings/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ test('Populates essential hotkeys for them to be bound', async t => {
'Disable Studio Mode',
'Transition (Studio Mode)',
'Save Replay',
'Screenshot Output',
'Mute',
'Unmute',
'Push to Mute',
Expand Down
Loading