feat: proxy video response

This commit is contained in:
Nevo David 2025-09-14 14:42:12 +07:00
parent 4bfef6bade
commit 43c3af4e35
5 changed files with 117 additions and 49 deletions

View File

@ -1,4 +1,14 @@
import { Body, Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
import {
Body,
Controller,
Get,
Param,
Post,
Query,
Req,
Res,
StreamableFile,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AgenciesService } from '@gitroom/nestjs-libraries/database/prisma/agencies/agencies.service';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
@ -11,6 +21,10 @@ import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
import { Nowpayments } from '@gitroom/nestjs-libraries/crypto/nowpayments';
import { Readable, pipeline } from 'stream';
import { promisify } from 'util';
const pump = promisify(pipeline);
@ApiTags('Public')
@Controller('/public')
@ -136,4 +150,46 @@ export class PublicController {
console.log('cryptoPost', body, path);
return this._nowpayments.processPayment(path, body);
}
@Get('/stream')
async streamFile(
@Query('url') url: string,
@Res() res: Response,
@Req() req: Request
) {
if (!url.endsWith('mp4')) {
return res.status(400).send('Invalid video URL');
}
const ac = new AbortController();
const onClose = () => ac.abort();
req.on('aborted', onClose);
res.on('close', onClose);
const r = await fetch(url, { signal: ac.signal });
if (!r.ok && r.status !== 206) {
res.status(r.status);
throw new Error(`Upstream error: ${r.statusText}`);
}
const type = r.headers.get('content-type') ?? 'application/octet-stream';
res.setHeader('Content-Type', type);
const contentRange = r.headers.get('content-range');
if (contentRange) res.setHeader('Content-Range', contentRange);
const len = r.headers.get('content-length');
if (len) res.setHeader('Content-Length', len);
const acceptRanges = r.headers.get('accept-ranges') ?? 'bytes';
res.setHeader('Accept-Ranges', acceptRanges);
if (r.status === 206) res.status(206); // Partial Content for range responses
try {
await pump(Readable.fromWeb(r.body as any), res);
} catch (err) {
}
}
}

View File

@ -50,7 +50,7 @@ export class MainMcp {
@McpTool({
toolName: 'POSTIZ_SCHEDULE_POST',
zod: {
type: eenum(['draft', 'scheduled']),
type: eenum(['draft', 'schedule']),
configId: string(),
generatePictures: boolean(),
date: string().describe('UTC TIME'),

View File

@ -5,6 +5,7 @@ import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store';
import { useVariables } from '@gitroom/react/helpers/variable.context';
const postUrlEmitter = new EventEmitter();
export const MediaSettingsLayout = () => {
@ -97,7 +98,8 @@ export const CreateThumbnail: FC<{
altText?: string;
onAltTextChange?: (altText: string) => void;
}> = (props) => {
const { onSelect, media, altText, onAltTextChange } = props;
const { onSelect, media } = props;
const { backendUrl } = useVariables();
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [currentTime, setCurrentTime] = useState(0);
@ -211,7 +213,7 @@ export const CreateThumbnail: FC<{
<div className="relative bg-black rounded-lg overflow-hidden">
<video
ref={videoRef}
src={media.path}
src={backendUrl + '/public/stream?url=' + encodeURIComponent(media.path)}
className="w-full h-[200px] object-contain"
onLoadedMetadata={handleLoadedMetadata}
onTimeUpdate={handleTimeUpdate}

View File

@ -80,31 +80,36 @@ class CloudflareStorage implements IUploadProvider {
}
async uploadFile(file: Express.Multer.File): Promise<any> {
const id = makeId(10);
const extension = mime.extension(file.mimetype) || '';
try {
const id = makeId(10);
const extension = mime.extension(file.mimetype) || '';
// Create the PutObjectCommand to upload the file to Cloudflare R2
const command = new PutObjectCommand({
Bucket: this._bucketName,
ACL: 'public-read',
Key: `${id}.${extension}`,
Body: file.buffer,
});
// Create the PutObjectCommand to upload the file to Cloudflare R2
const command = new PutObjectCommand({
Bucket: this._bucketName,
ACL: 'public-read',
Key: `${id}.${extension}`,
Body: file.buffer,
});
await this._client.send(command);
await this._client.send(command);
return {
filename: `${id}.${extension}`,
mimetype: file.mimetype,
size: file.size,
buffer: file.buffer,
originalname: `${id}.${extension}`,
fieldname: 'file',
path: `${this._uploadUrl}/${id}.${extension}`,
destination: `${this._uploadUrl}/${id}.${extension}`,
encoding: '7bit',
stream: file.buffer as any,
};
return {
filename: `${id}.${extension}`,
mimetype: file.mimetype,
size: file.size,
buffer: file.buffer,
originalname: `${id}.${extension}`,
fieldname: 'file',
path: `${this._uploadUrl}/${id}.${extension}`,
destination: `${this._uploadUrl}/${id}.${extension}`,
encoding: '7bit',
stream: file.buffer as any,
};
} catch (err) {
console.error('Error uploading file to Cloudflare R2:', err);
throw err;
}
}
// Implement the removeFile method from IUploadProvider

View File

@ -38,34 +38,39 @@ export class LocalStorage implements IUploadProvider {
}
async uploadFile(file: Express.Multer.File): Promise<any> {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
try {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const innerPath = `/${year}/${month}/${day}`;
const dir = `${this.uploadDirectory}${innerPath}`;
mkdirSync(dir, { recursive: true });
const innerPath = `/${year}/${month}/${day}`;
const dir = `${this.uploadDirectory}${innerPath}`;
mkdirSync(dir, { recursive: true });
const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');
const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
const publicPath = `${innerPath}/${randomName}${extname(
file.originalname
)}`;
const filePath = `${dir}/${randomName}${extname(file.originalname)}`;
const publicPath = `${innerPath}/${randomName}${extname(
file.originalname
)}`;
// Logic to save the file to the filesystem goes here
writeFileSync(filePath, file.buffer);
// Logic to save the file to the filesystem goes here
writeFileSync(filePath, file.buffer);
return {
filename: `${randomName}${extname(file.originalname)}`,
path: process.env.FRONTEND_URL + '/uploads' + publicPath,
mimetype: file.mimetype,
originalname: file.originalname,
};
return {
filename: `${randomName}${extname(file.originalname)}`,
path: process.env.FRONTEND_URL + '/uploads' + publicPath,
mimetype: file.mimetype,
originalname: file.originalname,
};
} catch (err) {
console.error('Error uploading file to Local Storage:', err);
throw err;
}
}
async removeFile(filePath: string): Promise<void> {