vid support
This commit is contained in:
2
back/.gitignore
vendored
2
back/.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
downloads
|
||||
downloads_vids
|
||||
frames.txt
|
||||
*.mp3
|
||||
*.mp4
|
||||
|
||||
|
||||
@@ -1,27 +1,78 @@
|
||||
import { execa } from "execa";
|
||||
import ffmpegPath from "ffmpeg-static";
|
||||
import { globSync, mkdirSync, readdirSync, rmSync, statSync, writeFile, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { LOCAL_DIR, LOCAL_DIR_VID, LOCAL_DIR_VID_EXTRACTED } from "./nextcloud";
|
||||
|
||||
export async function encodeFrames({
|
||||
inputPattern,
|
||||
output,
|
||||
fps = 30,
|
||||
}: {
|
||||
inputPattern: string;
|
||||
output: string;
|
||||
fps?: number;
|
||||
}) {
|
||||
export async function createVid(
|
||||
fps: number,
|
||||
) {
|
||||
const entries = readdirSync(LOCAL_DIR_VID);
|
||||
const videos = (
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fullPath = path.join(LOCAL_DIR_VID, entry);
|
||||
const info = statSync(fullPath);
|
||||
|
||||
if (!info.isFile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!entry.toLowerCase().endsWith(".mp4")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
})
|
||||
)
|
||||
).filter(Boolean) as string[];
|
||||
|
||||
for (const file of globSync(`${LOCAL_DIR_VID_EXTRACTED}/*`)) {
|
||||
rmSync(file, { recursive: true, force: true });
|
||||
}
|
||||
await Promise.all(
|
||||
videos.map((video) =>
|
||||
extractFrames(video, LOCAL_DIR_VID_EXTRACTED, 1)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
encodeFrames([
|
||||
`${LOCAL_DIR}/*.jpg`,
|
||||
`${LOCAL_DIR}/*.png`,
|
||||
`${LOCAL_DIR_VID_EXTRACTED}/*.jpg`,
|
||||
], path.resolve("./out.mp4"), fps)
|
||||
}
|
||||
|
||||
export async function encodeFrames(
|
||||
inputPatterns: string[],
|
||||
output: string,
|
||||
fps: number = 30,
|
||||
) {
|
||||
console.log("kleje filmik");
|
||||
const files = inputPatterns
|
||||
.flatMap(pattern => globSync(pattern))
|
||||
.sort();
|
||||
|
||||
const concatFile = files
|
||||
.map(file => `file '${file}'\nduration ${1/fps}`)
|
||||
.join("\n");
|
||||
|
||||
|
||||
writeFileSync("./frames.txt", concatFile);
|
||||
|
||||
await execa(ffmpegPath!, [
|
||||
"-y",
|
||||
"-pattern_type", "glob",
|
||||
"-framerate", fps.toString(),
|
||||
"-i", inputPattern.toString(),
|
||||
// "-pattern_type", "glob",
|
||||
// "-framerate", fps.toString(),
|
||||
"-f", "concat",
|
||||
"-safe", "0",
|
||||
"-i", "./frames.txt",
|
||||
|
||||
"-i", path.resolve("./music.mp3"),
|
||||
|
||||
"-vf",
|
||||
"transpose=2,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2",
|
||||
`transpose=2,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2`,
|
||||
// "-autorotate",
|
||||
|
||||
"-c:v", "libx264",
|
||||
@@ -37,3 +88,28 @@ export async function encodeFrames({
|
||||
]);
|
||||
console.log("filmik sklejony");
|
||||
}
|
||||
|
||||
export async function extractFrames(
|
||||
input: string,
|
||||
outputDir: string,
|
||||
targetFps: number,
|
||||
): Promise<void> {
|
||||
if (!ffmpegPath) {
|
||||
throw new Error("ffmpeg-static binary not found.");
|
||||
}
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const fileName = path.basename(path.resolve(input));
|
||||
const outputPattern = path.join(outputDir, `${fileName}_frame_%06d.jpg`);
|
||||
|
||||
await execa(ffmpegPath!, [
|
||||
"-y", "-i",
|
||||
input,
|
||||
"-vf",
|
||||
`fps=${targetFps}`,
|
||||
"-q:v",
|
||||
"2", // JPEG quality (2 = very high)
|
||||
outputPattern,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { createClient } from "webdav";
|
||||
import { createClient, type FileStat } from "webdav";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
export const REMOTE_DIR = "/";
|
||||
export const LOCAL_DIR = path.resolve("./downloads");
|
||||
export const LOCAL_DIR_VID = path.resolve("./downloads_vids");
|
||||
export const LOCAL_DIR_VID_EXTRACTED = path.join(path.resolve("./downloads_vids"), "extracted");
|
||||
|
||||
export async function downloadMissing(remoteDir: string = REMOTE_DIR) {
|
||||
await fs.mkdir(LOCAL_DIR, { recursive: true })
|
||||
await Promise.all([
|
||||
fs.mkdir(LOCAL_DIR, { recursive: true }),
|
||||
fs.mkdir(LOCAL_DIR_VID, { recursive: true }),
|
||||
fs.mkdir(LOCAL_DIR_VID_EXTRACTED, { recursive: true }),
|
||||
]);
|
||||
|
||||
const client = createClient(
|
||||
"https://nextcloud.papryk.com/public.php/dav/files/ckkL3Pg4X4Ye6pz",
|
||||
{
|
||||
@@ -14,16 +21,48 @@ export async function downloadMissing(remoteDir: string = REMOTE_DIR) {
|
||||
password: "",
|
||||
}
|
||||
);
|
||||
|
||||
const contents = await client.getDirectoryContents(remoteDir);
|
||||
|
||||
const exisitngFiles = await fs.readdir(LOCAL_DIR);
|
||||
const [
|
||||
existingImgFiles,
|
||||
existingVidFiles,
|
||||
] = await Promise.all([
|
||||
fs.readdir(LOCAL_DIR),
|
||||
fs.readdir(LOCAL_DIR_VID),
|
||||
]);
|
||||
|
||||
const missing = contents.filter(f => !exisitngFiles.includes(f.basename));
|
||||
const existingImgs = new Set(existingImgFiles);
|
||||
const existingVids = new Set(existingVidFiles);
|
||||
|
||||
console.log(missing);
|
||||
const missingImages = contents.filter(
|
||||
({ basename, mime }) =>
|
||||
mime?.startsWith("image/") &&
|
||||
!existingImgs.has(basename)
|
||||
);
|
||||
|
||||
const missingVideos = contents.filter(
|
||||
({ basename, mime }) =>
|
||||
mime?.startsWith("video/") &&
|
||||
!existingVids.has(basename)
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
downloadToDir(missingImages, LOCAL_DIR),
|
||||
downloadToDir(missingVideos, LOCAL_DIR_VID),
|
||||
]);
|
||||
}
|
||||
|
||||
async function downloadToDir(files: FileStat[], dir: string) {
|
||||
const client = createClient(
|
||||
"https://nextcloud.papryk.com/public.php/dav/files/ckkL3Pg4X4Ye6pz",
|
||||
{
|
||||
username: "ckkL3Pg4X4Ye6pz",
|
||||
password: "",
|
||||
}
|
||||
);
|
||||
let i = 0;
|
||||
for (const item of missing) {
|
||||
for (const item of files) {
|
||||
const result = await client.getFileContents(item.filename, {
|
||||
format: "binary",
|
||||
});
|
||||
@@ -51,12 +90,10 @@ export async function downloadMissing(remoteDir: string = REMOTE_DIR) {
|
||||
buffer = Buffer.from(result as any);
|
||||
}
|
||||
|
||||
const localPath = path.resolve(path.join(LOCAL_DIR, item.filename))
|
||||
const localPath = path.resolve(path.join(dir, item.filename))
|
||||
|
||||
await fs.writeFile(localPath, buffer);
|
||||
i++;
|
||||
console.log(`downloaded ${i}/${missing.length} ${item.basename}`)
|
||||
console.log(`downloaded ${i}/${files.length} ${item.basename}`)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Router } from "express";
|
||||
import { downloadMissing, LOCAL_DIR } from "../lib/nextcloud";
|
||||
import path from "node:path";
|
||||
import { encodeFrames } from "../lib/crateClip";
|
||||
import { createVid, encodeFrames } from "../lib/crateClip";
|
||||
import fs from "node:fs"
|
||||
|
||||
const router = Router();
|
||||
@@ -25,13 +25,11 @@ router.get("/video", async (req, res) => {
|
||||
|
||||
router.post("/createclip", async (req, res) => {
|
||||
try {
|
||||
await encodeFrames({
|
||||
inputPattern: `${LOCAL_DIR}/*.jpg`,
|
||||
output: path.resolve("./out.mp4"),
|
||||
fps: 8,
|
||||
});
|
||||
console.log("create clip request")
|
||||
await createVid(8)
|
||||
res.status(200).end();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(501).end();
|
||||
}
|
||||
})
|
||||
|
||||
1
back/src/types/express.d.ts
vendored
1
back/src/types/express.d.ts
vendored
@@ -10,3 +10,4 @@ declare module "express-serve-static-core" {
|
||||
} | string | jwt.JwtPayload;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user