Skip to content

Commit

Permalink
fixes #485
Browse files Browse the repository at this point in the history
  • Loading branch information
aza547 committed Jun 14, 2024
1 parent fd1dc0b commit 5705c39
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
### Added
- [Issue 485](https://github.com/aza547/wow-recorder/issues/485) - Added upload rate limit setting.

### Fixed

## [5.5.0] - 2024-06-12
Expand Down
15 changes: 12 additions & 3 deletions src/main/VideoProcessQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ export default class VideoProcessQueue {
): Promise<void> {
let lastProgress = 0;

// Decide if we need to use a rate limit or not. Setting to -1 is unlimited.
const rateLimit = this.cfg.get<boolean>('cloudUploadRateLimit')
? this.cfg.get<number>('cloudUploadRateLimitMbps')
: -1;

const progressCallback = (progress: number) => {
if (progress === lastProgress) {
return;
Expand All @@ -262,13 +267,17 @@ export default class VideoProcessQueue {

try {
assert(this.cloudClient);
const thumbNailPath = getThumbnailFileNameForVideo(item.path);

// Upload the video first, this can take a bit of time, and don't want
// to confuse the frontend by having metadata without video.
await this.cloudClient.putFile(item.path, progressCallback);
await this.cloudClient.putFile(item.path, rateLimit, progressCallback);
progressCallback(100);
await this.cloudClient.putFile(thumbNailPath);

// Upload the thumbnail.
const thumbNailPath = getThumbnailFileNameForVideo(item.path);
await this.cloudClient.putFile(thumbNailPath, rateLimit);

// Now add the metadata.
const metadata = await getMetadataForVideo(item.path);

const cloudMetadata: CloudMetadata = {
Expand Down
13 changes: 13 additions & 0 deletions src/main/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export type ConfigurationSchema = {
dungeonOverrun: number;
cloudStorage: boolean;
cloudUpload: boolean;
cloudUploadRateLimit: boolean;
cloudUploadRateLimitMbps: number;
cloudAccountName: string;
cloudAccountPassword: string;
cloudGuildName: string;
Expand Down Expand Up @@ -402,6 +404,17 @@ export const configSchema = {
type: 'boolean',
default: false,
},
cloudUploadRateLimit: {
description:
'If upload to the cloud should be rate limited. Useful if you are finding uploading is causing you to lag.',
type: 'boolean',
default: false,
},
cloudUploadRateLimitMbps: {
description: 'The upload rate limit in MB/s ',
type: 'integer',
default: 100,
},
cloudAccountName: {
description: 'Your Warcraft Recorder account username.',
type: 'string',
Expand Down
76 changes: 76 additions & 0 deletions src/renderer/CloudSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ const CloudSettings = (props: IProps) => {
cloudAccountPassword: config.cloudAccountPassword,
cloudGuildName: config.cloudGuildName,
cloudUpload: config.cloudUpload,
cloudUploadRateLimit: config.cloudUploadRateLimit,
cloudUploadRateLimitMbps: config.cloudUploadRateLimitMbps,
cloudUpload2v2: config.cloudUpload2v2,
cloudUpload3v3: config.cloudUpload3v3,
cloudUpload5v5: config.cloudUpload5v5,
Expand All @@ -99,6 +101,8 @@ const CloudSettings = (props: IProps) => {
config.cloudAccountPassword,
config.cloudGuildName,
config.cloudUpload,
config.cloudUploadRateLimit,
config.cloudUploadRateLimitMbps,
config.cloudUpload2v2,
config.cloudUpload3v3,
config.cloudUpload5v5,
Expand Down Expand Up @@ -257,6 +261,10 @@ const CloudSettings = (props: IProps) => {
};

const setMinKeystoneLevel = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.value) {
return;
}

setConfig((prevState) => {
return {
...prevState,
Expand Down Expand Up @@ -350,6 +358,35 @@ const CloudSettings = (props: IProps) => {
);
};

const setCloudUploadRateLimit = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setConfig((prevState) => {
return {
...prevState,
cloudUploadRateLimit: event.target.checked,
};
});
};

const getCloudUploadRateLimitSwitch = () => {
if (isComponentDisabled() || !config.cloudUpload) {
return <></>;
}

return (
<Box>
<FormControlLabel
control={getSwitch('cloudUploadRateLimit', setCloudUploadRateLimit)}
label="Upload Rate Limit"
labelPlacement="top"
style={formControlLabelStyle}
disabled={isComponentDisabled()}
/>
</Box>
);
};

const setCloudAccountName = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
Expand Down Expand Up @@ -560,13 +597,52 @@ const CloudSettings = (props: IProps) => {
);
};

const setUploadRateLimit = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.value) {
return;
}

setConfig((prevState) => {
return {
...prevState,
cloudUploadRateLimitMbps: parseInt(event.target.value, 10),
};
});
};

const getRateLimitField = () => {
if (!config.cloudUploadRateLimit) {
return <></>;
}

const helperText =
config.cloudUploadRateLimitMbps < 1 ? 'Must be 1 or greater' : '';

return (
<TextField
value={config.cloudUploadRateLimitMbps}
onChange={setUploadRateLimit}
label="Upload Rate Limit (MB/s)"
variant="outlined"
type="number"
error={config.cloudUploadRateLimitMbps < 1}
helperText={helperText}
InputLabelProps={{ shrink: true, style: { color: 'white' } }}
sx={{ ...style, maxWidth: '250px' }}
inputProps={{ min: 0, style: { color: 'white' } }}
/>
);
};

return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{getDisabledText()}

<Box sx={{ display: 'flex', flexDirection: 'row', mb: 1 }}>
{getCloudSwitch()}
{getCloudUploadSwitch()}
{getCloudUploadRateLimitSwitch()}
{getRateLimitField()}
</Box>

<Box sx={{ display: 'flex', flexDirection: 'row' }}>
Expand Down
1 change: 1 addition & 0 deletions src/renderer/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const getCloudSettingsInfoIcon = () => {
/* eslint-disable prettier/prettier */
['Cloud Playback', configSchema.cloudStorage.description].join('\n'),
['Cloud Upload', configSchema.cloudUpload.description].join('\n'),
['Upload Rate Limit', configSchema.cloudUploadRateLimit.description].join('\n'),
['Account Name', configSchema.cloudAccountName.description].join('\n'),
['Account Password', configSchema.cloudAccountPassword.description].join('\n'),
['Guild Name', configSchema.cloudGuildName.description].join('\n'),
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export const getSettings = (): ConfigurationSchema => {
dungeonOverrun: getConfigValue<number>('dungeonOverrun'),
cloudStorage: getConfigValue<boolean>('cloudStorage'),
cloudUpload: getConfigValue<boolean>('cloudUpload'),
cloudUploadRateLimit: getConfigValue<boolean>('cloudUploadRateLimit'),
cloudUploadRateLimitMbps: getConfigValue<number>('cloudUploadRateLimitMbps'),
cloudAccountName: getConfigValue<string>('cloudAccountName'),
cloudAccountPassword: getConfigValue<string>('cloudAccountPassword'),
cloudGuildName: getConfigValue<string>('cloudGuildName'),
Expand Down
30 changes: 27 additions & 3 deletions src/storage/CloudClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,20 +380,33 @@ export default class CloudClient extends EventEmitter {

/**
* Write a file into R2.
*
* @param file file path to upload
* @param rate rate of upload in MB/s, or -1 to signify no limit
*/
public async putFile(
file: string,
rate = -1,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
progressCallback = (_progress: number) => {}
) {
const key = path.basename(file);
console.info('[CloudClient] Uploading', file, 'to', key);

console.info(
'[CloudClient] Uploading',
file,
'to',
key,
'with rate limit',
rate
);

const stats = await fs.promises.stat(file);

if (stats.size < this.multiPartSizeBytes) {
await this.doSinglePartUpload(file, progressCallback);
await this.doSinglePartUpload(file, rate, progressCallback);
} else {
await this.doMultiPartUpload(file, progressCallback);
await this.doMultiPartUpload(file, rate, progressCallback);
}

await this.updateLastMod();
Expand Down Expand Up @@ -620,6 +633,7 @@ export default class CloudClient extends EventEmitter {
*/
private async doSinglePartUpload(
file: string,
rate: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
progressCallback = (_progress: number) => {}
) {
Expand All @@ -636,6 +650,10 @@ export default class CloudClient extends EventEmitter {
// into memory which is just a disaster. This makes me want to pick
// a different HTTP library. https://github.com/axios/axios/issues/1045.
maxRedirects: 0,

// Apply the rate limit here if it's in-play.
// Convert units from MB/s to bytes per sec.
maxRate: rate > 0 ? rate * 1024 ** 2 : undefined,
};

const signedUrl = await this.signPutUrl(key, stats.size);
Expand Down Expand Up @@ -690,6 +708,7 @@ export default class CloudClient extends EventEmitter {
*/
private async doMultiPartUpload(
file: string,
rate: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
progressCallback = (_progress: number) => {}
) {
Expand Down Expand Up @@ -737,10 +756,15 @@ export default class CloudClient extends EventEmitter {
const actual = Math.round(previous + normalized);
progressCallback(actual);
},

// Without this, we buffer the whole file (which can be several GB)
// into memory which is just a disaster. This makes me want to pick
// a different HTTP library. https://github.com/axios/axios/issues/1045.
maxRedirects: 0,

// Apply the rate limit here if it's in-play.
// Convert units from MB/s to bytes per sec.
maxRate: rate > 0 ? rate * 1024 ** 2 : undefined,
};

// Retry each part upload a few times on failure to allow for
Expand Down

0 comments on commit 5705c39

Please sign in to comment.