2 Commits

Author SHA1 Message Date
priyanshu_bharti 5457244d74 Add in-memory thumbnail caching 2026-04-14 22:48:18 +05:30
priyanshu_bharti f3a32b0923 Add Docker workflow and docs (#1)
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-07 19:50:24 +00:00
8 changed files with 1192 additions and 35 deletions
+6
View File
@@ -0,0 +1,6 @@
.git
.gitea
node_modules
npm-debug.log
README.md
LICENSE
+50
View File
@@ -0,0 +1,50 @@
name: Docker
on:
push:
branches: [master]
env:
REGISTRY: ghcr.io
IMAGE_NAME: coracle-social/vthumbs
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+3
View File
@@ -0,0 +1,3 @@
node_modules/
npm-debug.log*
.env
+19
View File
@@ -0,0 +1,19 @@
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3100
RUN apk add --no-cache ffmpeg
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY index.js ./
USER node
EXPOSE 3100
CMD ["npm", "start"]
+39 -10
View File
@@ -1,33 +1,62 @@
# Video Thumbnail Service # Video Thumbnail Service
A lightweight microservice that generates video thumbnails by extracting a frame using ffmpeg. A lightweight microservice that generates JPEG thumbnails from video URLs using ffmpeg.
## Requirements ## Requirements
- Node.js 18+ - Node.js 18+
- ffmpeg installed on the system - ffmpeg installed on the system
## Setup ## Environment Configuration
Set environment variables in your shell, process manager, or deployment platform.
| Variable | Default | Description |
| --- | --- | --- |
| `PORT` | `3100` | HTTP port used by the service |
### Local Run
```bash ```bash
npm install npm install
npm start PORT=3100 npm start
``` ```
The service runs on port 3100 by default. Set the `PORT` environment variable to change it. ## Docker
Build and run the service with Docker:
```bash
docker build -t vthumbs:local .
docker run --rm -p 3100:3100 -e PORT=3100 vthumbs:local
```
## Usage ## Usage
``` ```http
GET /thumbnail?url=https://example.com/video.mp4 GET /thumbnail?url=https://example.com/video.mp4
``` ```
Returns a JPEG image of the first frame (at 1 second). Returns a JPEG image extracted at 1 second.
## Configuration ## Health Check
Set `VITE_THUMBNAIL_URL` in the Flotilla `.env` file to the deployed service URL:
```http
GET /health
``` ```
VITE_THUMBNAIL_URL=https://your-thumbnail-service.example.com
Returns JSON:
```json
{"status":"ok"}
```
## Client Configuration
Configure your client app with the base URL where this service is deployed.
Example:
```env
THUMBNAIL_SERVICE_URL=https://your-thumbnail-service.example.com
``` ```
+56 -22
View File
@@ -1,44 +1,78 @@
import express from "express" import express from "express";
import ffmpeg from "fluent-ffmpeg" import ffmpeg from "fluent-ffmpeg";
import {PassThrough} from "stream" import { PassThrough } from "stream";
import NodeCache from "node-cache";
import ffmpegInstaller from "@ffmpeg-installer/ffmpeg";
const app = express() ffmpeg.setFfmpegPath(ffmpegInstaller.path);
const PORT = process.env.PORT || 3100
app.get("/thumbnail", async (req, res) => { const app = express();
const videoUrl = req.query.url const PORT = process.env.PORT || 3100;
const thumbnailCache = new NodeCache({
stdTTL: 3600,
checkperiod: 600,
useClones: false
});
app.get("/thumbnail", (req, res) => {
const videoUrl = req.query.url?.trim();
if (!videoUrl) { if (!videoUrl) {
return res.status(400).json({error: "Missing 'url' query parameter"}) return res.status(400).json({ error: "Missing 'url' query parameter" });
}
const cachedImage = thumbnailCache.get(videoUrl);
if (cachedImage) {
res.setHeader("Content-Type", "image/jpeg");
res.setHeader("Cache-Control", "public, max-age=86400");
return res.send(cachedImage);
} }
try { try {
const passthrough = new PassThrough() const passthrough = new PassThrough();
let imageBuffer = Buffer.alloc(0);
passthrough.on("data", (chunk) => {
imageBuffer = Buffer.concat([imageBuffer, chunk]);
});
passthrough.on("end", () => {
if (imageBuffer.length > 0) {
thumbnailCache.set(videoUrl, imageBuffer);
}
});
ffmpeg(videoUrl) ffmpeg(videoUrl)
.seekInput(1) .seekInput(1)
.frames(1) .frames(1)
.format("image2") .format("image2")
.outputOptions("-vcodec", "mjpeg") .outputOptions("-vcodec", "mjpeg")
.on("error", err => { .on("error", (err) => {
console.error("ffmpeg error:", err.message);
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({error: "Failed to generate thumbnail"}) res.status(500).json({ error: "Failed to generate thumbnail" });
} }
}) })
.pipe(passthrough, {end: true}) .pipe(passthrough, { end: true });
res.setHeader("Content-Type", "image/jpeg") res.setHeader("Content-Type", "image/jpeg");
res.setHeader("Cache-Control", "public, max-age=86400") res.setHeader("Cache-Control", "public, max-age=86400");
passthrough.pipe(res)
} catch { passthrough.pipe(res);
res.status(500).json({error: "Failed to process video"})
} catch (err) {
console.error("Error:", err);
if (!res.headersSent) {
res.status(500).json({ error: "Failed to process video" });
}
} }
}) });
app.get("/health", (_req, res) => { app.get("/health", (_req, res) => {
res.json({status: "ok"}) res.json({ status: "ok" });
}) });
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Video thumbnail service running on port ${PORT}`) console.log(`Video thumbnail service running on port ${PORT}`);
}) });
+1014
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -1,14 +1,16 @@
{ {
"name": "flotilla-video-thumbnails", "name": "vthumbs",
"version": "1.0.0", "version": "1.0.0",
"description": "Microservice to generate video thumbnails for Flotilla", "description": "Microservice to generate video thumbnails from video URLs",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node index.js" "start": "node index.js"
}, },
"dependencies": { "dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"express": "^4.21.0", "express": "^4.21.0",
"fluent-ffmpeg": "^2.1.3" "fluent-ffmpeg": "^2.1.3",
"node-cache": "^5.1.2"
} }
} }