diff options
Diffstat (limited to 'server/lib/transcoding')
-rw-r--r-- | server/lib/transcoding/video-transcoding.ts | 228 |
1 files changed, 115 insertions, 113 deletions
diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/video-transcoding.ts index d2a556360..ee228c011 100644 --- a/server/lib/transcoding/video-transcoding.ts +++ b/server/lib/transcoding/video-transcoding.ts | |||
@@ -4,13 +4,13 @@ import { basename, extname as extnameUtil, join } from 'path' | |||
4 | import { toEven } from '@server/helpers/core-utils' | 4 | import { toEven } from '@server/helpers/core-utils' |
5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 5 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
6 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 6 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
7 | import { VideoResolution } from '../../../shared/models/videos' | 7 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' |
8 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 8 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
9 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils' | 9 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils' |
10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' | 10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' |
11 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
13 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' | 13 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' |
14 | import { VideoFileModel } from '../../models/video/video-file' | 14 | import { VideoFileModel } from '../../models/video/video-file' |
15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | 15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' | 16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' |
@@ -19,9 +19,9 @@ import { | |||
19 | generateHlsSha256SegmentsFilename, | 19 | generateHlsSha256SegmentsFilename, |
20 | generateHLSVideoFilename, | 20 | generateHLSVideoFilename, |
21 | generateWebTorrentVideoFilename, | 21 | generateWebTorrentVideoFilename, |
22 | getHlsResolutionPlaylistFilename, | 22 | getHlsResolutionPlaylistFilename |
23 | getVideoFilePath | 23 | } from '../paths' |
24 | } from '../video-paths' | 24 | import { VideoPathManager } from '../video-path-manager' |
25 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | 25 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' |
26 | 26 | ||
27 | /** | 27 | /** |
@@ -32,159 +32,162 @@ import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | |||
32 | */ | 32 | */ |
33 | 33 | ||
34 | // Optimize the original video file and replace it. The resolution is not changed. | 34 | // Optimize the original video file and replace it. The resolution is not changed. |
35 | async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { | 35 | function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { |
36 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 36 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
37 | const newExtname = '.mp4' | 37 | const newExtname = '.mp4' |
38 | 38 | ||
39 | const videoInputPath = getVideoFilePath(video, inputVideoFile) | 39 | return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async videoInputPath => { |
40 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 40 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
41 | 41 | ||
42 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) | 42 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) |
43 | ? 'quick-transcode' | 43 | ? 'quick-transcode' |
44 | : 'video' | 44 | : 'video' |
45 | 45 | ||
46 | const resolution = toEven(inputVideoFile.resolution) | 46 | const resolution = toEven(inputVideoFile.resolution) |
47 | 47 | ||
48 | const transcodeOptions: TranscodeOptions = { | 48 | const transcodeOptions: TranscodeOptions = { |
49 | type: transcodeType, | 49 | type: transcodeType, |
50 | 50 | ||
51 | inputPath: videoInputPath, | 51 | inputPath: videoInputPath, |
52 | outputPath: videoTranscodedPath, | 52 | outputPath: videoTranscodedPath, |
53 | 53 | ||
54 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 54 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
55 | profile: CONFIG.TRANSCODING.PROFILE, | 55 | profile: CONFIG.TRANSCODING.PROFILE, |
56 | 56 | ||
57 | resolution, | 57 | resolution, |
58 | 58 | ||
59 | job | 59 | job |
60 | } | 60 | } |
61 | 61 | ||
62 | // Could be very long! | 62 | // Could be very long! |
63 | await transcode(transcodeOptions) | 63 | await transcode(transcodeOptions) |
64 | 64 | ||
65 | try { | 65 | try { |
66 | await remove(videoInputPath) | 66 | await remove(videoInputPath) |
67 | 67 | ||
68 | // Important to do this before getVideoFilename() to take in account the new filename | 68 | // Important to do this before getVideoFilename() to take in account the new filename |
69 | inputVideoFile.extname = newExtname | 69 | inputVideoFile.extname = newExtname |
70 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) | 70 | inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) |
71 | inputVideoFile.storage = VideoStorage.FILE_SYSTEM | ||
71 | 72 | ||
72 | const videoOutputPath = getVideoFilePath(video, inputVideoFile) | 73 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) |
73 | 74 | ||
74 | await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 75 | const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
75 | 76 | ||
76 | return transcodeType | 77 | return { transcodeType, videoFile } |
77 | } catch (err) { | 78 | } catch (err) { |
78 | // Auto destruction... | 79 | // Auto destruction... |
79 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) | 80 | video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) |
80 | 81 | ||
81 | throw err | 82 | throw err |
82 | } | 83 | } |
84 | }) | ||
83 | } | 85 | } |
84 | 86 | ||
85 | // Transcode the original video file to a lower resolution. | 87 | // Transcode the original video file to a lower resolution |
86 | async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) { | 88 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed |
89 | function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) { | ||
87 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 90 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
88 | const extname = '.mp4' | 91 | const extname = '.mp4' |
89 | 92 | ||
90 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | 93 | return VideoPathManager.Instance.makeAvailableVideoFile(video, video.getMaxQualityFile(), async videoInputPath => { |
91 | const videoInputPath = getVideoFilePath(video, video.getMaxQualityFile()) | 94 | const newVideoFile = new VideoFileModel({ |
95 | resolution, | ||
96 | extname, | ||
97 | filename: generateWebTorrentVideoFilename(resolution, extname), | ||
98 | size: 0, | ||
99 | videoId: video.id | ||
100 | }) | ||
92 | 101 | ||
93 | const newVideoFile = new VideoFileModel({ | 102 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) |
94 | resolution, | 103 | const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) |
95 | extname, | ||
96 | filename: generateWebTorrentVideoFilename(resolution, extname), | ||
97 | size: 0, | ||
98 | videoId: video.id | ||
99 | }) | ||
100 | 104 | ||
101 | const videoOutputPath = getVideoFilePath(video, newVideoFile) | 105 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO |
102 | const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) | 106 | ? { |
107 | type: 'only-audio' as 'only-audio', | ||
103 | 108 | ||
104 | const transcodeOptions = resolution === VideoResolution.H_NOVIDEO | 109 | inputPath: videoInputPath, |
105 | ? { | 110 | outputPath: videoTranscodedPath, |
106 | type: 'only-audio' as 'only-audio', | ||
107 | 111 | ||
108 | inputPath: videoInputPath, | 112 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
109 | outputPath: videoTranscodedPath, | 113 | profile: CONFIG.TRANSCODING.PROFILE, |
110 | 114 | ||
111 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 115 | resolution, |
112 | profile: CONFIG.TRANSCODING.PROFILE, | ||
113 | 116 | ||
114 | resolution, | 117 | job |
118 | } | ||
119 | : { | ||
120 | type: 'video' as 'video', | ||
121 | inputPath: videoInputPath, | ||
122 | outputPath: videoTranscodedPath, | ||
115 | 123 | ||
116 | job | 124 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
117 | } | 125 | profile: CONFIG.TRANSCODING.PROFILE, |
118 | : { | ||
119 | type: 'video' as 'video', | ||
120 | inputPath: videoInputPath, | ||
121 | outputPath: videoTranscodedPath, | ||
122 | 126 | ||
123 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 127 | resolution, |
124 | profile: CONFIG.TRANSCODING.PROFILE, | 128 | isPortraitMode: isPortrait, |
125 | 129 | ||
126 | resolution, | 130 | job |
127 | isPortraitMode: isPortrait, | 131 | } |
128 | 132 | ||
129 | job | 133 | await transcode(transcodeOptions) |
130 | } | ||
131 | |||
132 | await transcode(transcodeOptions) | ||
133 | 134 | ||
134 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | 135 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) |
136 | }) | ||
135 | } | 137 | } |
136 | 138 | ||
137 | // Merge an image with an audio file to create a video | 139 | // Merge an image with an audio file to create a video |
138 | async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { | 140 | function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { |
139 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR | 141 | const transcodeDirectory = CONFIG.STORAGE.TMP_DIR |
140 | const newExtname = '.mp4' | 142 | const newExtname = '.mp4' |
141 | 143 | ||
142 | const inputVideoFile = video.getMinQualityFile() | 144 | const inputVideoFile = video.getMinQualityFile() |
143 | 145 | ||
144 | const audioInputPath = getVideoFilePath(video, inputVideoFile) | 146 | return VideoPathManager.Instance.makeAvailableVideoFile(video, inputVideoFile, async audioInputPath => { |
145 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 147 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
146 | 148 | ||
147 | // If the user updates the video preview during transcoding | 149 | // If the user updates the video preview during transcoding |
148 | const previewPath = video.getPreview().getPath() | 150 | const previewPath = video.getPreview().getPath() |
149 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) | 151 | const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) |
150 | await copyFile(previewPath, tmpPreviewPath) | 152 | await copyFile(previewPath, tmpPreviewPath) |
151 | 153 | ||
152 | const transcodeOptions = { | 154 | const transcodeOptions = { |
153 | type: 'merge-audio' as 'merge-audio', | 155 | type: 'merge-audio' as 'merge-audio', |
154 | 156 | ||
155 | inputPath: tmpPreviewPath, | 157 | inputPath: tmpPreviewPath, |
156 | outputPath: videoTranscodedPath, | 158 | outputPath: videoTranscodedPath, |
157 | 159 | ||
158 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 160 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
159 | profile: CONFIG.TRANSCODING.PROFILE, | 161 | profile: CONFIG.TRANSCODING.PROFILE, |
160 | 162 | ||
161 | audioPath: audioInputPath, | 163 | audioPath: audioInputPath, |
162 | resolution, | 164 | resolution, |
163 | 165 | ||
164 | job | 166 | job |
165 | } | 167 | } |
166 | 168 | ||
167 | try { | 169 | try { |
168 | await transcode(transcodeOptions) | 170 | await transcode(transcodeOptions) |
169 | 171 | ||
170 | await remove(audioInputPath) | 172 | await remove(audioInputPath) |
171 | await remove(tmpPreviewPath) | 173 | await remove(tmpPreviewPath) |
172 | } catch (err) { | 174 | } catch (err) { |
173 | await remove(tmpPreviewPath) | 175 | await remove(tmpPreviewPath) |
174 | throw err | 176 | throw err |
175 | } | 177 | } |
176 | 178 | ||
177 | // Important to do this before getVideoFilename() to take in account the new file extension | 179 | // Important to do this before getVideoFilename() to take in account the new file extension |
178 | inputVideoFile.extname = newExtname | 180 | inputVideoFile.extname = newExtname |
179 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) | 181 | inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) |
180 | 182 | ||
181 | const videoOutputPath = getVideoFilePath(video, inputVideoFile) | 183 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) |
182 | // ffmpeg generated a new video file, so update the video duration | 184 | // ffmpeg generated a new video file, so update the video duration |
183 | // See https://trac.ffmpeg.org/ticket/5456 | 185 | // See https://trac.ffmpeg.org/ticket/5456 |
184 | video.duration = await getDurationFromVideoFile(videoTranscodedPath) | 186 | video.duration = await getDurationFromVideoFile(videoTranscodedPath) |
185 | await video.save() | 187 | await video.save() |
186 | 188 | ||
187 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 189 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
190 | }) | ||
188 | } | 191 | } |
189 | 192 | ||
190 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist | 193 | // Concat TS segments from a live video to a fragmented mp4 HLS playlist |
@@ -258,7 +261,7 @@ async function onWebTorrentVideoFileTranscoding ( | |||
258 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) | 261 | await VideoFileModel.customUpsert(videoFile, 'video', undefined) |
259 | video.VideoFiles = await video.$get('VideoFiles') | 262 | video.VideoFiles = await video.$get('VideoFiles') |
260 | 263 | ||
261 | return video | 264 | return { video, videoFile } |
262 | } | 265 | } |
263 | 266 | ||
264 | async function generateHlsPlaylistCommon (options: { | 267 | async function generateHlsPlaylistCommon (options: { |
@@ -335,14 +338,13 @@ async function generateHlsPlaylistCommon (options: { | |||
335 | videoStreamingPlaylistId: playlist.id | 338 | videoStreamingPlaylistId: playlist.id |
336 | }) | 339 | }) |
337 | 340 | ||
338 | const videoFilePath = getVideoFilePath(playlist, newVideoFile) | 341 | const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) |
339 | 342 | ||
340 | // Move files from tmp transcoded directory to the appropriate place | 343 | // Move files from tmp transcoded directory to the appropriate place |
341 | const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) | 344 | await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) |
342 | await ensureDir(baseHlsDirectory) | ||
343 | 345 | ||
344 | // Move playlist file | 346 | // Move playlist file |
345 | const resolutionPlaylistPath = join(baseHlsDirectory, resolutionPlaylistFilename) | 347 | const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) |
346 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) | 348 | await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) |
347 | // Move video file | 349 | // Move video file |
348 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) | 350 | await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) |
@@ -355,7 +357,7 @@ async function generateHlsPlaylistCommon (options: { | |||
355 | 357 | ||
356 | await createTorrentAndSetInfoHash(playlist, newVideoFile) | 358 | await createTorrentAndSetInfoHash(playlist, newVideoFile) |
357 | 359 | ||
358 | await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) | 360 | const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) |
359 | 361 | ||
360 | const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo | 362 | const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo |
361 | playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles') | 363 | playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles') |
@@ -368,5 +370,5 @@ async function generateHlsPlaylistCommon (options: { | |||
368 | await updateMasterHLSPlaylist(video, playlistWithFiles) | 370 | await updateMasterHLSPlaylist(video, playlistWithFiles) |
369 | await updateSha256VODSegments(video, playlistWithFiles) | 371 | await updateSha256VODSegments(video, playlistWithFiles) |
370 | 372 | ||
371 | return resolutionPlaylistPath | 373 | return { resolutionPlaylistPath, videoFile: savedVideoFile } |
372 | } | 374 | } |