handle video tagging
This commit is contained in:
@@ -87,22 +87,13 @@ async function getVideoDuration(src: string): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
/** Generate a thumbnail from a video using ffmpeg. */
|
||||
async function generateVideoThumbnail(src: string, dest: string): Promise<void> {
|
||||
/** Extract a single frame from a video at the given offset (seconds) and write to dest. */
|
||||
async function generateVideoFrameAtOffset(src: string, dest: string, offsetSeconds: number): Promise<void> {
|
||||
const tmp = dest + '.tmp'
|
||||
|
||||
// Seek to 10% of the video duration for a representative frame
|
||||
let offset = 0
|
||||
try {
|
||||
const duration = await getVideoDuration(src)
|
||||
offset = Math.max(0, duration * 0.1)
|
||||
} catch {
|
||||
// If ffprobe fails, fall back to seeking to 0
|
||||
}
|
||||
|
||||
const args = [
|
||||
'-y', // overwrite output
|
||||
'-ss', String(offset), // seek before input (fast)
|
||||
'-ss', String(offsetSeconds), // seek before input (fast)
|
||||
'-i', src,
|
||||
'-frames:v', '1',
|
||||
'-q:v', '5',
|
||||
@@ -115,6 +106,58 @@ async function generateVideoThumbnail(src: string, dest: string): Promise<void>
|
||||
fs.renameSync(tmp, dest)
|
||||
}
|
||||
|
||||
/** Generate a thumbnail from a video using ffmpeg (seeks to 10% of duration). */
|
||||
async function generateVideoThumbnail(src: string, dest: string): Promise<void> {
|
||||
let offset = 0
|
||||
try {
|
||||
const duration = await getVideoDuration(src)
|
||||
offset = Math.max(0, duration * 0.1)
|
||||
} catch {
|
||||
// If ffprobe fails, fall back to seeking to 0
|
||||
}
|
||||
await generateVideoFrameAtOffset(src, dest, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract frames from a video at each given percentage of its duration.
|
||||
* Returns the absolute paths to the cached frame JPEGs, in the same order as `percentages`.
|
||||
* Uses a per-frame cache key so each frame is cached independently.
|
||||
*/
|
||||
export async function getVideoFramePaths(
|
||||
absoluteFilePath: string,
|
||||
libraryId: string,
|
||||
percentages: number[]
|
||||
): Promise<string[]> {
|
||||
ensureCacheDir()
|
||||
|
||||
let duration = 0
|
||||
try {
|
||||
duration = await getVideoDuration(absoluteFilePath)
|
||||
} catch {
|
||||
// Fall back to 0; all frames will seek to position 0
|
||||
}
|
||||
|
||||
const framePaths: string[] = []
|
||||
|
||||
for (const pct of percentages) {
|
||||
const offset = Math.max(0, duration * pct)
|
||||
const key = crypto
|
||||
.createHash('sha1')
|
||||
.update(libraryId + ':' + absoluteFilePath + ':' + pct)
|
||||
.digest('hex')
|
||||
const cacheFile = path.join(CACHE_DIR, key + '.jpg')
|
||||
|
||||
const cached = getCachedPath(cacheFile, absoluteFilePath)
|
||||
if (!cached) {
|
||||
await generateVideoFrameAtOffset(absoluteFilePath, cacheFile, offset)
|
||||
}
|
||||
|
||||
framePaths.push(cacheFile)
|
||||
}
|
||||
|
||||
return framePaths
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to a cached thumbnail JPEG for the given file.
|
||||
* Generates it on first call (or when the source has been modified).
|
||||
|
||||
Reference in New Issue
Block a user