]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/transcoding/web-transcoding.ts
Fix transcoding error
[github/Chocobozzz/PeerTube.git] / server / lib / transcoding / web-transcoding.ts
1 import { Job } from 'bullmq'
2 import { copyFile, move, remove, stat } from 'fs-extra'
3 import { basename, join } from 'path'
4 import { computeOutputFPS } from '@server/helpers/ffmpeg'
5 import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
6 import { VideoModel } from '@server/models/video/video'
7 import { MVideoFile, MVideoFullLight } from '@server/types/models'
8 import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, TranscodeVODOptionsType } from '@shared/ffmpeg'
9 import { VideoResolution, VideoStorage } from '@shared/models'
10 import { CONFIG } from '../../initializers/config'
11 import { VideoFileModel } from '../../models/video/video-file'
12 import { generateWebTorrentVideoFilename } from '../paths'
13 import { buildFileMetadata } from '../video-file'
14 import { VideoPathManager } from '../video-path-manager'
15 import { buildFFmpegVOD } from './shared'
16 import { buildOriginalFileResolution } from './transcoding-resolutions'
17
18 // Optimize the original video file and replace it. The resolution is not changed.
19 export async function optimizeOriginalVideofile (options: {
20 video: MVideoFullLight
21 inputVideoFile: MVideoFile
22 quickTranscode: boolean
23 job: Job
24 }) {
25 const { video, inputVideoFile, quickTranscode, job } = options
26
27 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
28 const newExtname = '.mp4'
29
30 // Will be released by our transcodeVOD function once ffmpeg is ran
31 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
32
33 try {
34 await video.reload()
35 await inputVideoFile.reload()
36
37 const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
38
39 const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => {
40 const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
41
42 const transcodeType: TranscodeVODOptionsType = quickTranscode
43 ? 'quick-transcode'
44 : 'video'
45
46 const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
47 const fps = computeOutputFPS({ inputFPS: inputVideoFile.fps, resolution })
48
49 // Could be very long!
50 await buildFFmpegVOD(job).transcode({
51 type: transcodeType,
52
53 inputPath: videoInputPath,
54 outputPath: videoOutputPath,
55
56 inputFileMutexReleaser,
57
58 resolution,
59 fps
60 })
61
62 // Important to do this before getVideoFilename() to take in account the new filename
63 inputVideoFile.resolution = resolution
64 inputVideoFile.extname = newExtname
65 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
66 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
67
68 const { videoFile } = await onWebTorrentVideoFileTranscoding({
69 video,
70 videoFile: inputVideoFile,
71 videoOutputPath
72 })
73
74 await remove(videoInputPath)
75
76 return { transcodeType, videoFile }
77 })
78
79 return result
80 } finally {
81 inputFileMutexReleaser()
82 }
83 }
84
85 // Transcode the original video file to a lower resolution compatible with WebTorrent
86 export async function transcodeNewWebTorrentResolution (options: {
87 video: MVideoFullLight
88 resolution: VideoResolution
89 fps: number
90 job: Job
91 }) {
92 const { video: videoArg, resolution, fps, job } = options
93
94 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
95 const newExtname = '.mp4'
96
97 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
98
99 try {
100 const video = await VideoModel.loadFull(videoArg.uuid)
101 const file = video.getMaxQualityFile().withVideoOrPlaylist(video)
102
103 const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => {
104 const newVideoFile = new VideoFileModel({
105 resolution,
106 extname: newExtname,
107 filename: generateWebTorrentVideoFilename(resolution, newExtname),
108 size: 0,
109 videoId: video.id
110 })
111
112 const videoOutputPath = join(transcodeDirectory, newVideoFile.filename)
113
114 const transcodeOptions = {
115 type: 'video' as 'video',
116
117 inputPath: videoInputPath,
118 outputPath: videoOutputPath,
119
120 inputFileMutexReleaser,
121
122 resolution,
123 fps
124 }
125
126 await buildFFmpegVOD(job).transcode(transcodeOptions)
127
128 return onWebTorrentVideoFileTranscoding({ video, videoFile: newVideoFile, videoOutputPath })
129 })
130
131 return result
132 } finally {
133 inputFileMutexReleaser()
134 }
135 }
136
137 // Merge an image with an audio file to create a video
138 export async function mergeAudioVideofile (options: {
139 video: MVideoFullLight
140 resolution: VideoResolution
141 fps: number
142 job: Job
143 }) {
144 const { video: videoArg, resolution, fps, job } = options
145
146 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
147 const newExtname = '.mp4'
148
149 const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid)
150
151 try {
152 const video = await VideoModel.loadFull(videoArg.uuid)
153 const inputVideoFile = video.getMinQualityFile()
154
155 const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video)
156
157 const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => {
158 const videoOutputPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname)
159
160 // If the user updates the video preview during transcoding
161 const previewPath = video.getPreview().getPath()
162 const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath))
163 await copyFile(previewPath, tmpPreviewPath)
164
165 const transcodeOptions = {
166 type: 'merge-audio' as 'merge-audio',
167
168 inputPath: tmpPreviewPath,
169 outputPath: videoOutputPath,
170
171 inputFileMutexReleaser,
172
173 audioPath: audioInputPath,
174 resolution,
175 fps
176 }
177
178 try {
179 await buildFFmpegVOD(job).transcode(transcodeOptions)
180
181 await remove(audioInputPath)
182 await remove(tmpPreviewPath)
183 } catch (err) {
184 await remove(tmpPreviewPath)
185 throw err
186 }
187
188 // Important to do this before getVideoFilename() to take in account the new file extension
189 inputVideoFile.extname = newExtname
190 inputVideoFile.resolution = resolution
191 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
192
193 // ffmpeg generated a new video file, so update the video duration
194 // See https://trac.ffmpeg.org/ticket/5456
195 video.duration = await getVideoStreamDuration(videoOutputPath)
196 await video.save()
197
198 return onWebTorrentVideoFileTranscoding({
199 video,
200 videoFile: inputVideoFile,
201 videoOutputPath
202 })
203 })
204
205 return result
206 } finally {
207 inputFileMutexReleaser()
208 }
209 }
210
211 export async function onWebTorrentVideoFileTranscoding (options: {
212 video: MVideoFullLight
213 videoFile: MVideoFile
214 videoOutputPath: string
215 }) {
216 const { video, videoFile, videoOutputPath } = options
217
218 const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid)
219
220 try {
221 await video.reload()
222
223 const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, videoFile)
224
225 const stats = await stat(videoOutputPath)
226
227 const probe = await ffprobePromise(videoOutputPath)
228 const fps = await getVideoStreamFPS(videoOutputPath, probe)
229 const metadata = await buildFileMetadata(videoOutputPath, probe)
230
231 await move(videoOutputPath, outputPath, { overwrite: true })
232
233 videoFile.size = stats.size
234 videoFile.fps = fps
235 videoFile.metadata = metadata
236
237 await createTorrentAndSetInfoHash(video, videoFile)
238
239 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
240 if (oldFile) await video.removeWebTorrentFile(oldFile)
241
242 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
243 video.VideoFiles = await video.$get('VideoFiles')
244
245 return { video, videoFile }
246 } finally {
247 mutexReleaser()
248 }
249 }