diff --git a/index.d.ts b/index.d.ts index ba988e7..40dd551 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,17 +1,24 @@ export * from './src/interfaces/server/app.ts'; +export * from './src/interfaces/server/cache.ts'; export * from './src/interfaces/server/context.ts'; + export * from './src/interfaces/migration/context.js'; -export * from './src/interfaces/server/cache.ts'; + export * from './src/interfaces/security/WebAuthn.js'; export * from './src/interfaces/security/Challenge.ts'; export * from './src/interfaces/security/decorators.ts'; + export * from './src/interfaces/types/Blob.ts'; export * from './src/interfaces/types/File.ts'; export * from './src/interfaces/types/Validators.ts'; + export * from './src/interfaces/postgres/model.ts'; export * from './src/interfaces/postgres/decorators.ts'; + export * from './src/interfaces/utils/io.ts'; +export * from './src/interfaces/media/video.ts'; + export * from './src/interfaces/exceptions/BadRequest.ts'; export * from './src/interfaces/exceptions/Conflict.ts'; export * from './src/interfaces/exceptions/ContentTooLarge.ts'; diff --git a/src/compiler/entities/api/handler.js b/src/compiler/entities/api/handler.js index 4474ed0..a6fea5d 100644 --- a/src/compiler/entities/api/handler.js +++ b/src/compiler/entities/api/handler.js @@ -118,7 +118,7 @@ export function makeRouteMethod(name, node) { } if (isVoidLikeType(returnType)) { - (ast = factoryStatement(ast)), internals.respondNoContent(res); + ast = internals.respondNoContent(res, ast); } else if (File.isAssignable(returnType)) { ast = internals.respondFile(res, ast); } else if (BinaryData.isAssignable(returnType) || isStringType(returnType)) { diff --git a/src/interfaces/media/video.ts b/src/interfaces/media/video.ts new file mode 100644 index 0000000..5050895 --- /dev/null +++ b/src/interfaces/media/video.ts @@ -0,0 +1,19 @@ +interface VideoInfo { + size: number; + width: number; + height: number; + duration: number; +} + +interface ConvertVideoOptions { + type?: 'AV1'; + poster?: string; + maxWidth: number; + maxHeight: number; +} + +export declare function convertVideo( + source: string, + target: string, + options: ConvertVideoOptions +): Promise; diff --git a/src/interfaces/security/decorators.ts b/src/interfaces/security/decorators.ts index cd6c52a..7f67e34 100644 --- a/src/interfaces/security/decorators.ts +++ b/src/interfaces/security/decorators.ts @@ -2,7 +2,4 @@ import type { ServerContext } from '../server/context.ts'; export declare function Permission( rule: (context: ServerContext, payload?: any) => Promise -): ( - target: (payload?: any) => any, - context: ClassMethodDecoratorContext -) => void; +): (target: unknown, context: ClassMethodDecoratorContext) => void; diff --git a/src/interfaces/server/cache.ts b/src/interfaces/server/cache.ts index 05abe7a..0631c81 100644 --- a/src/interfaces/server/cache.ts +++ b/src/interfaces/server/cache.ts @@ -6,7 +6,4 @@ interface CacheOptions { export declare function Cache( options: boolean | number | CacheOptions -): ( - target: (payload?: any) => any, - context: ClassMethodDecoratorContext -) => void; +): (target: unknown, context: ClassMethodDecoratorContext) => void; diff --git a/src/runtime/media/video.js b/src/runtime/media/video.js new file mode 100644 index 0000000..1e57626 --- /dev/null +++ b/src/runtime/media/video.js @@ -0,0 +1,78 @@ +import { IO, env } from '#utils/io.js'; +import { UnProcessable } from '../exceptions/UnProcessable.js'; + +const PATH_FFMPEG = env.PATH_FFMPEG || 'ffmpeg'; +const PATH_FFPROBE = env.PATH_FFPROBE || 'ffprobe'; + +export async function getVideoInfo(path) { + const { streams } = JSON.parse( + await IO.exec( + `${PATH_FFPROBE} -v error -print_format json -show_streams -select_streams v "${path}"` + ) + ); + + if (streams?.length) { + return streams[0]; + } else { + throw new UnProcessable('Not found video streams'); + } +} + +export async function convertVideo(source, target, options) { + let { width, height, duration: ds } = await getVideoInfo(source); + let duration = Math.round(Number(ds)); + + let cmd = PATH_FFMPEG + ' -i "' + source + '" -v error'; + + if (width > options.maxWidth || height > options.maxHeight) { + if (width > height) { + height = Math.round(options.maxHeight / (width / height)); + width = options.maxWidth; + + cmd += ' -vf "scale=' + width + ':-1"'; + } else { + width = Math.round(options.maxWidth / (height / width)); + height = options.maxHeight; + + cmd += ' -vf "scale=-1:' + height + '"'; + } + } + + if (options.type === 'AV1') { + cmd += ' -c:v libsvtav1'; + cmd += ' -preset 10'; + cmd += ' -crf 36'; + cmd += ' -pix_fmt yuv420p10le'; + cmd += ' -svtav1-params fast-decode=1'; + } else { + cmd += ' -c:v copy'; + } + + cmd += ' -c:a copy'; + cmd += ' -y "' + target + '"'; + + await IO.exec(cmd); + + if (options.poster) { + const time = duration / 10; + + await IO.exec( + `${PATH_FFMPEG} -y -v error -ss ${time} -i "${target}" -frames:v 1 "${options.poster}"` + ); + } + + return { width, height, duration, size: IO.getFileSize(target) }; +} + +// console.log( +// await convertVideo( +// '/Users/oleksiiskydan/Desktop/Labs/Files/2.mp4', +// '/Users/oleksiiskydan/Desktop/Labs/Files/aa.mkv', +// { +// type: 'AV1', +// maxWidth: 1280, +// maxHeight: 1280, +// poster: '/Users/oleksiiskydan/Desktop/Labs/Files/aa.avif', +// } +// ) +// ); diff --git a/src/runtime/utils/io.js b/src/runtime/utils/io.js index eed6c4c..e440912 100644 --- a/src/runtime/utils/io.js +++ b/src/runtime/utils/io.js @@ -2,11 +2,14 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { Buffer } from 'node:buffer'; import { Writable } from 'node:stream'; -import { createWriteStream } from 'node:fs'; +import { exec } from 'node:child_process'; +import { statSync, createWriteStream } from 'node:fs'; import { TransformStream } from 'node:stream/web'; import { open as openFile, rename as moveFile } from 'node:fs/promises'; import { randomUUID, createHash, webcrypto as crypto } from 'node:crypto'; +export { env } from 'node:process'; + export const getRandomString = length => Buffer.from(crypto.getRandomValues(new Uint8Array(length))) .toString('base64url') @@ -22,6 +25,7 @@ export const IO = { ), getTempFileName: () => join(tmpdir(), randomUUID()), + getFileSize: path => statSync(path).size, createFileWriteStream: (path, options) => Writable.toWeb(createWriteStream(path, options)), @@ -38,4 +42,15 @@ export const IO = { return hash; }, + + exec(command, options) { + return { + then(resolve, reject) { + exec(command, options, (error, stdout) => { + if (error) reject(error); + else resolve(stdout); + }); + }, + }; + }, };