Skip to content

Commit 1184143

Browse files
committed
feat(game): resize canvas resolution
1 parent 83f9c6b commit 1184143

File tree

7 files changed

+114
-28
lines changed

7 files changed

+114
-28
lines changed

.yarn.installed

Whitespace-only changes.

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,36 @@ exports.screencapture:remoteUpload(args[1], "https://api.fivemanage.com/api/imag
111111
end, "blob")
112112
```
113113

114+
## High-Resolution Display Optimization
115+
116+
For users with 4K, ultrawide, or other high-resolution displays, you can customize the maximum screenshot resolution to balance quality and payload size:
117+
118+
```lua
119+
-- Allow higher resolution for better quality (may increase upload time)
120+
exports.screencapture:remoteUpload(args[1], "https://api.fivemanage.com/api/image", {
121+
encoding = "webp",
122+
maxWidth = 2560,
123+
maxHeight = 1440,
124+
headers = {
125+
["Authorization"] = ""
126+
}
127+
}, function(data)
128+
print(data.url)
129+
end, "blob")
130+
131+
-- Use lower resolution for faster uploads
132+
exports.screencapture:remoteUpload(args[1], "https://api.fivemanage.com/api/image", {
133+
encoding = "webp",
134+
maxWidth = 1280,
135+
maxHeight = 720,
136+
headers = {
137+
["Authorization"] = ""
138+
}
139+
}, function(data)
140+
print(data.url)
141+
end, "blob")
142+
```
143+
114144
## Screenshot Basic compatibility
115145

116146
### This is NOT recommended to use, as you risk exposing tokens to clients.

game/nui/src/capture.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ type CaptureRequest = {
1212
serverEndpoint: string;
1313
formField: string;
1414
dataType: 'blob' | 'base64';
15+
maxWidth?: number;
16+
maxHeight?: number;
1517
};
1618

1719
export class Capture {
1820
#gameView: any;
1921
#canvas: HTMLCanvasElement | null = null;
2022

23+
// Maximum resolution to prevent oversized payloads
24+
private readonly MAX_WIDTH = 1920;
25+
private readonly MAX_HEIGHT = 1080;
26+
2127
start() {
2228
window.addEventListener('message', async (event) => {
2329
const data = event.data as CaptureRequest;
@@ -34,12 +40,43 @@ export class Capture {
3440
});
3541
}
3642

43+
// fuck me
44+
// i guess this is the downside of using webgl??
45+
private calculateDimensions(request: CaptureRequest): { width: number; height: number } {
46+
const originalWidth = window.innerWidth;
47+
const originalHeight = window.innerHeight;
48+
49+
const maxWidth = request.maxWidth || this.MAX_WIDTH;
50+
const maxHeight = request.maxHeight || this.MAX_HEIGHT;
51+
52+
if (originalWidth <= maxWidth && originalHeight <= maxHeight) {
53+
return { width: originalWidth, height: originalHeight };
54+
}
55+
56+
const scaleX = maxWidth / originalWidth;
57+
const scaleY = maxHeight / originalHeight;
58+
const scale = Math.min(scaleX, scaleY);
59+
60+
return {
61+
width: Math.floor(originalWidth * scale),
62+
height: Math.floor(originalHeight * scale)
63+
};
64+
}
65+
3766
async captureScreen(request: CaptureRequest) {
3867
this.#canvas = document.createElement('canvas');
39-
this.#canvas.width = window.innerWidth;
40-
this.#canvas.height = window.innerHeight;
68+
69+
70+
const { width, height } = this.calculateDimensions(request);
71+
this.#canvas.width = width;
72+
this.#canvas.height = height;
73+
74+
console.log(`Capturing at ${width}x${height} (original: ${window.innerWidth}x${window.innerHeight})`);
4175

4276
this.#gameView = createGameView(this.#canvas);
77+
78+
79+
this.#gameView.resize(width, height);
4380

4481
const enc = request.encoding ?? 'png';
4582
let imageData: string | Blob;
@@ -51,9 +88,7 @@ export class Capture {
5188
}
5289

5390
if (!imageData) return console.error('No image available');
54-
console.log('Image data:', imageData);
55-
console.log("image size:", imageData.size);
56-
91+
5792
await this.httpUploadImage(request, imageData);
5893
this.#canvas.remove();
5994
}
@@ -102,16 +137,30 @@ export class Capture {
102137

103138
createBlob(canvas: HTMLCanvasElement, enc: Encoding): Promise<Blob> {
104139
return new Promise((resolve, reject) => {
140+
// Calculate adaptive quality based on canvas size
141+
const pixelCount = canvas.width * canvas.height;
142+
let quality = 0.7; // default
143+
144+
// wtf am I doing
145+
if (pixelCount > 2073600) { //1920x1080
146+
quality = 0.5;
147+
} else if (pixelCount > 1440000) { //1200x1200
148+
quality = 0.6;
149+
}
150+
151+
console.log(`Using quality ${quality} for ${canvas.width}x${canvas.height} (${pixelCount} pixels)`);
152+
105153
canvas.toBlob(
106154
(blob) => {
107155
if (blob) {
156+
console.log(`Generated blob: ${(blob.size / 1024 / 1024).toFixed(2)}MB`);
108157
resolve(blob);
109158
} else {
110159
reject('No blob available');
111160
}
112161
},
113162
`image/${enc}`,
114-
0.7,
163+
quality,
115164
);
116165
});
117166
}

game/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
},
1212
"dependencies": {
1313
"@citizenfx/http-wrapper": "^0.2.2",
14+
"@koa/router": "^13.1.0",
1415
"form-data": "^4.0.1",
16+
"formidable": "^3.5.4",
1517
"koa": "^2.15.4",
1618
"koa-body": "^6.0.1",
1719
"koa-router": "^13.0.1",
18-
"@koa/router": "^13.1.0",
1920
"multer": "1.4.5-lts.1",
2021
"nanoid": "^5.0.8",
2122
"node-fetch": "^3.3.2"

game/server/koa-router.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
import Koa from 'koa';
22
import Router from '@koa/router';
3-
import { readFile } from 'fs/promises';
43

54
// @ts-ignore - no types
65
import { setHttpCallback } from '@citizenfx/http-wrapper';
6+
import { multer } from './multer'
77

88
import FormData from 'form-data';
99
import fetch from 'node-fetch';
1010
import { Blob } from 'node:buffer';
1111
import { CaptureOptions, DataType } from './types';
1212
import { UploadStore } from './upload-store';
13-
import koaBody from 'koa-body';
13+
14+
const upload = multer({
15+
storage: multer.memoryStorage()
16+
});
1417

1518
export async function createServer(uploadStore: UploadStore) {
1619
const app = new Koa();
1720
const router = new Router();
1821

19-
20-
21-
router.post('/image', async (ctx) => {
22+
router.post('/image', upload.single('file') as any, async (ctx) => {
2223
const token = ctx.request.headers['x-screencapture-token'] as string;
2324
if (!token) {
2425
ctx.status = 401;
@@ -29,24 +30,15 @@ export async function createServer(uploadStore: UploadStore) {
2930
const { callback, dataType, isRemote, remoteConfig, url, playerSource, correlationId } =
3031
uploadStore.getUpload(token);
3132

32-
if (!ctx.request.files || !ctx.request.files['file']) {
33-
ctx.status = 400;
34-
ctx.body = { status: 'error', message: 'No file provided' };
35-
}
36-
37-
// i am so very sorry for this, but FivM and formidable fucking hate each other
38-
const file = ctx.request.files?.['file'] as any;
39-
if (!file) {
33+
if (!ctx.files) {
4034
ctx.status = 400;
4135
ctx.body = { status: 'error', message: 'No file provided' };
42-
return;
4336
}
4437

45-
const filePath = file.filepath || file.path
46-
const fileBuffer = await readFile(filePath)
38+
const file = ctx.file;
4739

4840
try {
49-
const buf = await buffer(dataType, fileBuffer);
41+
const buf = await buffer(dataType, file.buffer);
5042

5143
if (isRemote) {
5244
const response = await uploadFile(url, remoteConfig, buf, dataType);
@@ -74,10 +66,7 @@ export async function createServer(uploadStore: UploadStore) {
7466
}
7567
});
7668

77-
app.use(koaBody({
78-
patchKoa: true,
79-
multipart: true,
80-
}))
69+
app
8170
.use(router.routes())
8271
.use(router.allowedMethods());
8372

game/server/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface CaptureOptions {
3232
formField?: string;
3333
filename?: string;
3434
encoding?: string;
35+
maxWidth?: number;
36+
maxHeight?: number;
3537
}
3638

3739
export type CallbackFn = (data: unknown, _playerSource?: number, correlationId?: string) => void;

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)