aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg-utils.ts
blob: 8f9a03c263d0569dbf85651d7ea501cda39d04f0 (plain) (blame)
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
135
136
137
138
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', (err, stdout, stderr) => {
        logger.error('Error in transcoding job.', { stdout, stderr })
        return rej(err)
      })
      .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)
    })
  })
}