1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
import * as ffmpeg from 'fluent-ffmpeg'
import { join } from 'path'
import { VideoResolution } from '../../shared/models/videos'
import { CONFIG, VIDEO_TRANSCODING_FPS } from '../initializers'
import { unlinkPromise } from './core-utils'
import { processImage } from './image-utils'
import { logger } from './logger'
async function getVideoFileResolution (path: string) {
const videoStream = await getVideoFileStream(path)
return {
videoFileResolution: Math.min(videoStream.height, videoStream.width),
isPortraitMode: videoStream.height > videoStream.width
}
}
async function getVideoFileFPS (path: string) {
const videoStream = await getVideoFileStream(path)
for (const key of [ 'r_frame_rate' , 'avg_frame_rate' ]) {
const valuesText: string = videoStream[key]
if (!valuesText) continue
const [ frames, seconds ] = valuesText.split('/')
if (!frames || !seconds) continue
const result = parseInt(frames, 10) / parseInt(seconds, 10)
if (result > 0) return result
}
return 0
}
function getDurationFromVideoFile (path: string) {
return new Promise<number>((res, rej) => {
ffmpeg.ffprobe(path, (err, metadata) => {
if (err) return rej(err)
return res(Math.floor(metadata.format.duration))
})
})
}
async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) {
const pendingImageName = 'pending-' + imageName
const options = {
filename: pendingImageName,
count: 1,
folder
}
const pendingImagePath = join(folder, pendingImageName)
try {
await new Promise<string>((res, rej) => {
ffmpeg(fromPath)
.on('error', rej)
.on('end', () => res(imageName))
.thumbnail(options)
})
const destination = join(folder, imageName)
await processImage({ path: pendingImagePath }, destination, size)
} catch (err) {
logger.error('Cannot generate image from video %s.', fromPath, err)
try {
await unlinkPromise(pendingImagePath)
} catch (err) {
logger.debug('Cannot remove pending image path after generation error.', err)
}
}
}
type TranscodeOptions = {
inputPath: string
outputPath: string
resolution?: VideoResolution
isPortraitMode?: boolean
}
function transcode (options: TranscodeOptions) {
return new Promise<void>(async (res, rej) => {
const fps = await getVideoFileFPS(options.inputPath)
let command = ffmpeg(options.inputPath)
.output(options.outputPath)
.videoCodec('libx264')
.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
.outputOption('-movflags faststart')
// .outputOption('-crf 18')
// Our player has some FPS limits
if (fps > VIDEO_TRANSCODING_FPS.MAX) command = command.withFPS(VIDEO_TRANSCODING_FPS.MAX)
else if (fps < VIDEO_TRANSCODING_FPS.MIN) command = command.withFPS(VIDEO_TRANSCODING_FPS.MIN)
if (options.resolution !== undefined) {
// '?x720' or '720x?' for example
const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
command = command.size(size)
}
command.on('error', rej)
.on('end', res)
.run()
})
}
// ---------------------------------------------------------------------------
export {
getVideoFileResolution,
getDurationFromVideoFile,
generateImageFromVideoFile,
transcode,
getVideoFileFPS
}
// ---------------------------------------------------------------------------
function getVideoFileStream (path: string) {
return new Promise<any>((res, rej) => {
ffmpeg.ffprobe(path, (err, metadata) => {
if (err) return rej(err)
const videoStream = metadata.streams.find(s => s.codec_type === 'video')
if (!videoStream) throw new Error('Cannot find video stream of ' + path)
return res(videoStream)
})
})
}
|