forked from coracle/vthumbs
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5457244d74 | |||
| f3a32b0923 |
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
@@ -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 }}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
.env
|
||||||
+19
@@ -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"]
|
||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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}`);
|
||||||
})
|
});
|
||||||
Generated
+1014
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user