feat: proxy video response
This commit is contained in:
parent
4bfef6bade
commit
43c3af4e35
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue